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:
gongzhiyong
2026-04-29 21:08:15 +08:00
commit 1efcf73992
4715 changed files with 1082862 additions and 0 deletions
+256
View File
@@ -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',
)
}
+144
View 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`)