chore: consolidate heicode workspace into single repository
Import cc-haha and new-api into a single repository layout for unified delivery. Made-with: Cursor
This commit is contained in:
@@ -0,0 +1,256 @@
|
||||
import { readdirSync, readFileSync, statSync } from 'node:fs'
|
||||
import { extname, join, relative, sep } from 'node:path'
|
||||
|
||||
type Bucket = {
|
||||
files: number
|
||||
lines: number
|
||||
nonBlankLines: number
|
||||
}
|
||||
|
||||
type FileStat = Bucket & {
|
||||
path: string
|
||||
extension: string
|
||||
}
|
||||
|
||||
const root = process.cwd()
|
||||
|
||||
const targetRoots = ['adapters', 'desktop', 'runtime', 'src/server']
|
||||
|
||||
const codeExtensions = new Set([
|
||||
'.css',
|
||||
'.cjs',
|
||||
'.html',
|
||||
'.js',
|
||||
'.jsx',
|
||||
'.mjs',
|
||||
'.nsh',
|
||||
'.ps1',
|
||||
'.py',
|
||||
'.rs',
|
||||
'.sh',
|
||||
'.ts',
|
||||
'.tsx',
|
||||
])
|
||||
|
||||
const excludedDirectoryNames = new Set([
|
||||
'.cache',
|
||||
'.git',
|
||||
'.next',
|
||||
'.nuxt',
|
||||
'.omx',
|
||||
'.parcel-cache',
|
||||
'.svelte-kit',
|
||||
'.tauri',
|
||||
'.turbo',
|
||||
'.vite',
|
||||
'.vite-temp',
|
||||
'__pycache__',
|
||||
'build',
|
||||
'build-artifacts',
|
||||
'coverage',
|
||||
'dist',
|
||||
'node_modules',
|
||||
'out',
|
||||
'target',
|
||||
])
|
||||
|
||||
const excludedRelativePaths = new Set([
|
||||
'desktop/src-tauri/binaries',
|
||||
'desktop/src-tauri/icons',
|
||||
])
|
||||
|
||||
const files: FileStat[] = []
|
||||
|
||||
function emptyBucket(): Bucket {
|
||||
return {
|
||||
files: 0,
|
||||
lines: 0,
|
||||
nonBlankLines: 0,
|
||||
}
|
||||
}
|
||||
|
||||
function addToBucket(bucket: Bucket, file: FileStat) {
|
||||
bucket.files += file.files
|
||||
bucket.lines += file.lines
|
||||
bucket.nonBlankLines += file.nonBlankLines
|
||||
}
|
||||
|
||||
function shouldSkipDirectory(path: string) {
|
||||
const name = path.split(sep).at(-1)
|
||||
const normalized = relative(root, path).split(sep).join('/')
|
||||
|
||||
return (
|
||||
Boolean(name && excludedDirectoryNames.has(name)) ||
|
||||
excludedRelativePaths.has(normalized)
|
||||
)
|
||||
}
|
||||
|
||||
function countFile(path: string): FileStat | null {
|
||||
const extension = extname(path)
|
||||
|
||||
if (!codeExtensions.has(extension)) {
|
||||
return null
|
||||
}
|
||||
|
||||
const content = readFileSync(path, 'utf8')
|
||||
const newlineCount = content.match(/\r\n|\r|\n/g)?.length ?? 0
|
||||
const lines =
|
||||
content.length === 0 || content.endsWith('\n') || content.endsWith('\r')
|
||||
? newlineCount
|
||||
: newlineCount + 1
|
||||
const nonBlankLines = content
|
||||
.split(/\r\n|\r|\n/)
|
||||
.filter((line) => line.trim().length > 0).length
|
||||
|
||||
return {
|
||||
files: 1,
|
||||
lines,
|
||||
nonBlankLines,
|
||||
path: relative(root, path).split(sep).join('/'),
|
||||
extension,
|
||||
}
|
||||
}
|
||||
|
||||
function walk(path: string) {
|
||||
const stat = statSync(path)
|
||||
|
||||
if (stat.isDirectory()) {
|
||||
if (shouldSkipDirectory(path)) {
|
||||
return
|
||||
}
|
||||
|
||||
for (const entry of readdirSync(path)) {
|
||||
walk(join(path, entry))
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
if (!stat.isFile()) {
|
||||
return
|
||||
}
|
||||
|
||||
const result = countFile(path)
|
||||
if (result) {
|
||||
files.push(result)
|
||||
}
|
||||
}
|
||||
|
||||
function formatNumber(value: number) {
|
||||
return value.toLocaleString('en-US')
|
||||
}
|
||||
|
||||
function printTable(
|
||||
title: string,
|
||||
rows: Array<{ name: string } & Bucket>,
|
||||
nameHeader = 'Group',
|
||||
) {
|
||||
console.log(`\n${title}`)
|
||||
console.log(`${nameHeader.padEnd(28)} ${'Files'.padStart(7)} ${'Lines'.padStart(9)} ${'Nonblank'.padStart(9)}`)
|
||||
console.log(`${'-'.repeat(28)} ${'-'.repeat(7)} ${'-'.repeat(9)} ${'-'.repeat(9)}`)
|
||||
|
||||
for (const row of rows) {
|
||||
console.log(
|
||||
`${row.name.padEnd(28)} ${formatNumber(row.files).padStart(7)} ${formatNumber(row.lines).padStart(9)} ${formatNumber(row.nonBlankLines).padStart(9)}`,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
function groupBy<T extends string>(getKey: (file: FileStat) => T) {
|
||||
const result = new Map<T, Bucket>()
|
||||
|
||||
for (const file of files) {
|
||||
const key = getKey(file)
|
||||
const bucket = result.get(key) ?? emptyBucket()
|
||||
addToBucket(bucket, file)
|
||||
result.set(key, bucket)
|
||||
}
|
||||
|
||||
return [...result.entries()]
|
||||
.map(([name, bucket]) => ({ name, ...bucket }))
|
||||
.sort((left, right) => right.lines - left.lines)
|
||||
}
|
||||
|
||||
function areaForPath(path: string) {
|
||||
if (path.startsWith('src/server/')) {
|
||||
return 'server'
|
||||
}
|
||||
|
||||
if (path.startsWith('desktop/src/')) {
|
||||
return 'desktop frontend'
|
||||
}
|
||||
|
||||
if (path.startsWith('desktop/src-tauri/')) {
|
||||
return 'desktop tauri'
|
||||
}
|
||||
|
||||
if (path.startsWith('desktop/')) {
|
||||
return 'desktop support'
|
||||
}
|
||||
|
||||
if (path.startsWith('adapters/')) {
|
||||
return 'adapters'
|
||||
}
|
||||
|
||||
if (path.startsWith('runtime/')) {
|
||||
return 'runtime'
|
||||
}
|
||||
|
||||
return 'other'
|
||||
}
|
||||
|
||||
function purposeForPath(path: string) {
|
||||
const fileName = path.split('/').at(-1) ?? ''
|
||||
|
||||
if (
|
||||
path.includes('/__tests__/') ||
|
||||
path.includes('/fixtures/') ||
|
||||
fileName.startsWith('test_') ||
|
||||
/\.test\.[cm]?[jt]sx?$/.test(path) ||
|
||||
/\.spec\.[cm]?[jt]sx?$/.test(path)
|
||||
) {
|
||||
return 'tests and fixtures'
|
||||
}
|
||||
|
||||
return 'product source'
|
||||
}
|
||||
|
||||
for (const targetRoot of targetRoots) {
|
||||
walk(join(root, targetRoot))
|
||||
}
|
||||
|
||||
const total = emptyBucket()
|
||||
for (const file of files) {
|
||||
addToBucket(total, file)
|
||||
}
|
||||
|
||||
const sortedByPath = [...files].sort((left, right) =>
|
||||
left.path.localeCompare(right.path),
|
||||
)
|
||||
|
||||
console.log('Desktop app source line count')
|
||||
console.log('')
|
||||
console.log(`Targets: ${targetRoots.join(', ')}`)
|
||||
console.log(`Included extensions: ${[...codeExtensions].sort().join(', ')}`)
|
||||
console.log(
|
||||
`Excluded directories: ${[...excludedDirectoryNames].sort().join(', ')}`,
|
||||
)
|
||||
console.log(`Excluded paths: ${[...excludedRelativePaths].sort().join(', ')}`)
|
||||
|
||||
printTable('By area', groupBy((file) => areaForPath(file.path)))
|
||||
printTable('By purpose', groupBy((file) => purposeForPath(file.path)))
|
||||
printTable('By top-level target', groupBy((file) => file.path.split('/')[0]))
|
||||
printTable('By extension', groupBy((file) => file.extension), 'Extension')
|
||||
|
||||
console.log('\nTotal')
|
||||
console.log(`Files: ${formatNumber(total.files)}`)
|
||||
console.log(`Lines: ${formatNumber(total.lines)}`)
|
||||
console.log(`Nonblank lines: ${formatNumber(total.nonBlankLines)}`)
|
||||
|
||||
if (process.argv.includes('--files')) {
|
||||
printTable(
|
||||
'By file',
|
||||
sortedByPath.map((file) => ({ name: file.path, ...file })),
|
||||
'File',
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,144 @@
|
||||
#!/usr/bin/env bun
|
||||
/**
|
||||
* Release script for Claude Code Haha Desktop
|
||||
*
|
||||
* Usage:
|
||||
* bun run scripts/release.ts patch # 0.1.0 → 0.1.1
|
||||
* bun run scripts/release.ts minor # 0.1.0 → 0.2.0
|
||||
* bun run scripts/release.ts major # 0.1.0 → 1.0.0
|
||||
* bun run scripts/release.ts 2.0.0 # explicit version
|
||||
* bun run scripts/release.ts patch --dry # preview without changes
|
||||
*/
|
||||
|
||||
import { existsSync, readFileSync, writeFileSync } from 'node:fs'
|
||||
import path from 'node:path'
|
||||
|
||||
const root = path.resolve(import.meta.dir, '..')
|
||||
|
||||
const VERSION_FILES = [
|
||||
{
|
||||
path: path.join(root, 'desktop/package.json'),
|
||||
update(content: string, version: string) {
|
||||
return content.replace(/"version":\s*"[^"]*"/, `"version": "${version}"`)
|
||||
},
|
||||
},
|
||||
{
|
||||
path: path.join(root, 'desktop/src-tauri/tauri.conf.json'),
|
||||
update(content: string, version: string) {
|
||||
return content.replace(/"version":\s*"[^"]*"/, `"version": "${version}"`)
|
||||
},
|
||||
},
|
||||
{
|
||||
path: path.join(root, 'desktop/src-tauri/Cargo.toml'),
|
||||
update(content: string, version: string) {
|
||||
return content.replace(/^version\s*=\s*"[^"]*"/m, `version = "${version}"`)
|
||||
},
|
||||
},
|
||||
]
|
||||
|
||||
function getCurrentVersion(): string {
|
||||
const tauriConf = JSON.parse(
|
||||
readFileSync(path.join(root, 'desktop/src-tauri/tauri.conf.json'), 'utf-8'),
|
||||
)
|
||||
return tauriConf.version
|
||||
}
|
||||
|
||||
function getReleaseNotesPath(version: string): string {
|
||||
return path.join(root, 'release-notes', `v${version}.md`)
|
||||
}
|
||||
|
||||
function bumpVersion(current: string, bump: string): string {
|
||||
if (/^\d+\.\d+\.\d+$/.test(bump)) {
|
||||
return bump
|
||||
}
|
||||
|
||||
const [major, minor, patch] = current.split('.').map(Number)
|
||||
|
||||
switch (bump) {
|
||||
case 'patch':
|
||||
return `${major}.${minor}.${patch + 1}`
|
||||
case 'minor':
|
||||
return `${major}.${minor + 1}.0`
|
||||
case 'major':
|
||||
return `${major + 1}.0.0`
|
||||
default:
|
||||
console.error(`Invalid bump type: ${bump}`)
|
||||
console.error('Usage: bun run scripts/release.ts <patch|minor|major|x.y.z> [--dry]')
|
||||
process.exit(1)
|
||||
}
|
||||
}
|
||||
|
||||
async function run(cmd: string[], cwd = root) {
|
||||
const proc = Bun.spawn(cmd, { cwd, stdout: 'pipe', stderr: 'pipe' })
|
||||
const stdout = await new Response(proc.stdout).text()
|
||||
const stderr = await new Response(proc.stderr).text()
|
||||
const code = await proc.exited
|
||||
if (code !== 0) {
|
||||
throw new Error(`Command failed: ${cmd.join(' ')}\n${stderr || stdout}`)
|
||||
}
|
||||
return stdout.trim()
|
||||
}
|
||||
|
||||
// ── Main ──────────────────────────────────────────────
|
||||
|
||||
const args = process.argv.slice(2)
|
||||
const dryRun = args.includes('--dry')
|
||||
const bumpArg = args.find((a) => a !== '--dry')
|
||||
|
||||
if (!bumpArg) {
|
||||
console.error('Usage: bun run scripts/release.ts <patch|minor|major|x.y.z> [--dry]')
|
||||
process.exit(1)
|
||||
}
|
||||
|
||||
const current = getCurrentVersion()
|
||||
const next = bumpVersion(current, bumpArg)
|
||||
const releaseNotesPath = getReleaseNotesPath(next)
|
||||
|
||||
console.log(`\n Version: ${current} → ${next}`)
|
||||
console.log(` Tag: v${next}`)
|
||||
console.log(` Notes: ${path.relative(root, releaseNotesPath)}`)
|
||||
console.log(` Dry run: ${dryRun}\n`)
|
||||
|
||||
if (dryRun) {
|
||||
console.log('Files that would be updated:')
|
||||
for (const file of VERSION_FILES) {
|
||||
console.log(` - ${path.relative(root, file.path)}`)
|
||||
}
|
||||
console.log(` - ${path.relative(root, releaseNotesPath)} ${existsSync(releaseNotesPath) ? '(present)' : '(missing)'}`)
|
||||
process.exit(0)
|
||||
}
|
||||
|
||||
if (!existsSync(releaseNotesPath)) {
|
||||
console.error(`Missing release notes file: ${path.relative(root, releaseNotesPath)}`)
|
||||
console.error(`Create it before releasing so GitHub Release can use it automatically.`)
|
||||
process.exit(1)
|
||||
}
|
||||
|
||||
// Update version in all files
|
||||
for (const file of VERSION_FILES) {
|
||||
const content = readFileSync(file.path, 'utf-8')
|
||||
const updated = file.update(content, next)
|
||||
writeFileSync(file.path, updated)
|
||||
console.log(` Updated: ${path.relative(root, file.path)}`)
|
||||
}
|
||||
|
||||
// Regenerate Cargo.lock
|
||||
console.log('\n Updating Cargo.lock...')
|
||||
await run(['cargo', 'generate-lockfile'], path.join(root, 'desktop/src-tauri'))
|
||||
|
||||
// Git commit + tag
|
||||
console.log(' Creating git commit...')
|
||||
await run([
|
||||
'git',
|
||||
'add',
|
||||
'desktop/package.json',
|
||||
'desktop/src-tauri/tauri.conf.json',
|
||||
'desktop/src-tauri/Cargo.toml',
|
||||
'desktop/src-tauri/Cargo.lock',
|
||||
path.relative(root, releaseNotesPath),
|
||||
])
|
||||
await run(['git', 'commit', '-m', `release: v${next}`])
|
||||
await run(['git', 'tag', '-a', `v${next}`, '-m', `Release v${next}`])
|
||||
|
||||
console.log(`\n Done! Created commit and tag v${next}`)
|
||||
console.log(`\n To trigger the build:\n git push origin main --tags\n`)
|
||||
Reference in New Issue
Block a user