Skip to content

Commit b619de6

Browse files
authored
Merge pull request #96 from TrueNine/dev
chore: prepare 2026.10324.10015 release
2 parents fea6ca7 + aad9ea1 commit b619de6

30 files changed

+266
-258
lines changed

.githooks/sync-versions.ts

Lines changed: 156 additions & 175 deletions
Original file line numberDiff line numberDiff line change
@@ -1,221 +1,202 @@
11
#!/usr/bin/env tsx
22
/**
33
* Version Sync Script
4-
* Auto-sync all sub-package versions before commit
4+
* Auto-sync all publishable package versions before commit.
55
*/
6-
import { readFileSync, writeFileSync, readdirSync, existsSync, statSync } from 'node:fs'
7-
import { execSync } from 'node:child_process'
8-
import { resolve, join } from 'node:path'
6+
import {readdirSync, readFileSync, writeFileSync} from 'node:fs'
7+
import {join, relative, resolve} from 'node:path'
98
import process from 'node:process'
109

11-
interface PackageEntry {
12-
readonly path: string
13-
readonly name: string
10+
interface VersionedJson {
11+
version?: string
12+
[key: string]: unknown
1413
}
1514

16-
interface PackageJson {
17-
version?: string
15+
const ROOT_DIR = resolve('.')
16+
const ROOT_PACKAGE_PATH = resolve(ROOT_DIR, 'package.json')
17+
const ROOT_CARGO_PATH = resolve(ROOT_DIR, 'Cargo.toml')
18+
const IGNORED_DIRECTORIES = new Set([
19+
'.git',
20+
'.next',
21+
'.turbo',
22+
'coverage',
23+
'dist',
24+
'node_modules',
25+
'target',
26+
])
27+
28+
function readJsonFile(filePath: string): VersionedJson {
29+
return JSON.parse(readFileSync(filePath, 'utf-8').replace(/^\uFEFF/, '')) as VersionedJson
1830
}
1931

20-
function getCatalogVersion(pkgName: string): string | null {
21-
try {
22-
const yamlContent = readFileSync(resolve('pnpm-workspace.yaml'), 'utf-8')
23-
const match = yamlContent.match(new RegExp(`${pkgName.replace('@', '\\@')}:\\s*\\^?([^\\s]+)`))
24-
return match ? match[1] : null
25-
} catch {
26-
return null
27-
}
32+
function writeJsonFile(filePath: string, value: VersionedJson): void {
33+
writeFileSync(filePath, JSON.stringify(value, null, 2) + '\n', 'utf-8')
2834
}
2935

30-
const eslintConfigVersion = getCatalogVersion('@truenine/eslint10-config')
31-
const rootPackagePath = resolve('package.json')
32-
const requestedVersion = process.argv[2]?.trim()
33-
const rootPkg: PackageJson = JSON.parse(readFileSync(rootPackagePath, 'utf-8'))
36+
function discoverFilesByName(baseDir: string, fileName: string): string[] {
37+
const found: string[] = []
38+
const entries = readdirSync(baseDir, {withFileTypes: true})
3439

35-
if (requestedVersion && rootPkg.version !== requestedVersion) {
36-
rootPkg.version = requestedVersion
37-
writeFileSync(rootPackagePath, JSON.stringify(rootPkg, null, 2) + '\n', 'utf-8')
38-
}
40+
for (const entry of entries) {
41+
const entryPath = join(baseDir, entry.name)
3942

40-
const rootVersion = rootPkg.version
43+
if (entry.isDirectory()) {
44+
if (entry.name.startsWith('.')) {
45+
continue
46+
}
4147

42-
if (!rootVersion) {
43-
console.error('❌ Root package.json missing version field')
44-
process.exit(1)
45-
}
48+
if (IGNORED_DIRECTORIES.has(entry.name)) {
49+
continue
50+
}
4651

47-
console.log(`🔄 Syncing version: ${rootVersion}`)
48-
if (eslintConfigVersion) {
49-
console.log(`🔄 Catalog @truenine/eslint10-config: ^${eslintConfigVersion}`)
50-
}
52+
found.push(...discoverFilesByName(entryPath, fileName))
53+
continue
54+
}
5155

52-
const topLevelWorkspacePackages: readonly PackageEntry[] = [
53-
{ path: 'cli/package.json', name: 'cli' },
54-
{ path: 'mcp/package.json', name: 'mcp' },
55-
{ path: 'gui/package.json', name: 'gui' },
56-
{ path: 'doc/package.json', name: 'doc' },
57-
]
58-
59-
// Discover all libraries and their npm sub-packages
60-
function discoverLibraryPackages(): PackageEntry[] {
61-
const entries: PackageEntry[] = []
62-
const librariesDir = resolve('libraries')
63-
if (!existsSync(librariesDir)) return entries
64-
for (const lib of readdirSync(librariesDir)) {
65-
const libDir = join(librariesDir, lib)
66-
if (!statSync(libDir).isDirectory()) continue
67-
const libPkg = join(libDir, 'package.json')
68-
if (existsSync(libPkg)) {
69-
entries.push({ path: `libraries/${lib}/package.json`, name: `lib:${lib}` })
56+
if (entry.isFile() && entry.name === fileName) {
57+
found.push(entryPath)
7058
}
7159
}
72-
return entries
60+
61+
return found
7362
}
7463

75-
// Discover npm platform sub-packages under a given directory (e.g. cli/npm/)
76-
function discoverNpmSubPackages(baseDir: string, prefix: string): PackageEntry[] {
77-
const entries: PackageEntry[] = []
78-
const npmDir = resolve(baseDir, 'npm')
79-
if (!existsSync(npmDir) || !statSync(npmDir).isDirectory()) return entries
80-
for (const platform of readdirSync(npmDir)) {
81-
const platformDir = join(npmDir, platform)
82-
if (!statSync(platformDir).isDirectory()) continue
83-
const platformPkg = join(platformDir, 'package.json')
84-
if (existsSync(platformPkg)) {
85-
entries.push({ path: `${baseDir}/npm/${platform}/package.json`, name: `${prefix}/${platform}` })
64+
function updateVersionLineInSection(
65+
content: string,
66+
sectionName: string,
67+
targetVersion: string,
68+
): string {
69+
const lines = content.split(/\r?\n/)
70+
let inTargetSection = false
71+
72+
for (let index = 0; index < lines.length; index += 1) {
73+
const line = lines[index]
74+
const trimmed = line.trim()
75+
76+
if (/^\[.*\]$/.test(trimmed)) {
77+
inTargetSection = trimmed === `[${sectionName}]`
78+
continue
79+
}
80+
81+
if (!inTargetSection) {
82+
continue
83+
}
84+
85+
if (/^version\.workspace\s*=/.test(trimmed)) {
86+
return content
8687
}
87-
}
88-
return entries
89-
}
9088

91-
// Discover all packages under packages/
92-
function discoverPackagesDir(): PackageEntry[] {
93-
const entries: PackageEntry[] = []
94-
const packagesDir = resolve('packages')
95-
if (!existsSync(packagesDir)) return entries
96-
for (const pkg of readdirSync(packagesDir)) {
97-
const pkgDir = join(packagesDir, pkg)
98-
if (!statSync(pkgDir).isDirectory()) continue
99-
const pkgFile = join(pkgDir, 'package.json')
100-
if (existsSync(pkgFile)) {
101-
entries.push({ path: `packages/${pkg}/package.json`, name: `pkg:${pkg}` })
89+
const match = line.match(/^(\s*version\s*=\s*")([^"]+)(".*)$/)
90+
if (match == null) {
91+
continue
10292
}
93+
94+
if (match[2] === targetVersion) {
95+
return content
96+
}
97+
98+
lines[index] = `${match[1]}${targetVersion}${match[3]}`
99+
return lines.join('\n')
103100
}
104-
return entries
101+
102+
return content
105103
}
106104

107-
const libraryPackages = discoverLibraryPackages()
108-
const packagesPackages = discoverPackagesDir()
109-
const cliNpmPackages = discoverNpmSubPackages('cli', 'cli-napi')
105+
function syncJsonVersion(
106+
filePath: string,
107+
rootVersion: string,
108+
changedPaths: Set<string>,
109+
): void {
110+
try {
111+
const json = readJsonFile(filePath)
112+
if (json.version === rootVersion) {
113+
return
114+
}
110115

111-
let changed = false
116+
console.log(` ✓ ${relative(ROOT_DIR, filePath)}: version ${String(json.version ?? '(none)')} -> ${rootVersion}`)
117+
json.version = rootVersion
118+
writeJsonFile(filePath, json)
119+
changedPaths.add(filePath)
120+
} catch {
121+
console.log(`⚠️ ${relative(ROOT_DIR, filePath)} not found or invalid, skipping`)
122+
}
123+
}
112124

113-
for (const pkg of [...topLevelWorkspacePackages, ...libraryPackages, ...packagesPackages, ...cliNpmPackages]) {
114-
const fullPath = resolve(pkg.path)
125+
function syncCargoVersion(
126+
filePath: string,
127+
sectionName: string,
128+
rootVersion: string,
129+
changedPaths: Set<string>,
130+
): void {
115131
try {
116-
const content = readFileSync(fullPath, 'utf-8').replace(/^\uFEFF/, '')
117-
const pkgJson: PackageJson = JSON.parse(content)
118-
119-
if (pkgJson.version !== rootVersion) {
120-
console.log(` ✓ ${pkg.name}: version ${pkgJson.version}${rootVersion}`)
121-
pkgJson.version = rootVersion
122-
writeFileSync(fullPath, JSON.stringify(pkgJson, null, 2) + '\n', 'utf-8')
123-
changed = true
132+
const originalContent = readFileSync(filePath, 'utf-8')
133+
const updatedContent = updateVersionLineInSection(originalContent, sectionName, rootVersion)
134+
135+
if (updatedContent === originalContent) {
136+
return
124137
}
138+
139+
writeFileSync(filePath, updatedContent, 'utf-8')
140+
console.log(` ✓ ${relative(ROOT_DIR, filePath)}: version -> ${rootVersion}`)
141+
changedPaths.add(filePath)
125142
} catch {
126-
console.log(`⚠️ ${pkg.path} not found or invalid, skipping`)
143+
console.log(`⚠️ ${relative(ROOT_DIR, filePath)} not found or invalid, skipping`)
127144
}
128145
}
129146

130-
// Sync root workspace Cargo.toml version
131-
const workspaceCargoTomlPath = resolve('Cargo.toml')
132-
try {
133-
const cargoContent = readFileSync(workspaceCargoTomlPath, 'utf-8')
134-
const cargoUpdated = cargoContent.replace(
135-
/(\[workspace\.package\][\s\S]*?^version = ")([^"]+)(")/m,
136-
`$1${rootVersion}$3`,
137-
)
138-
if (cargoContent !== cargoUpdated) {
139-
writeFileSync(workspaceCargoTomlPath, cargoUpdated, 'utf-8')
140-
console.log(` ✓ workspace Cargo.toml: version → ${rootVersion}`)
141-
changed = true
142-
}
143-
} catch {
144-
console.log('⚠️ Cargo.toml not found, skipping')
147+
const requestedVersion = process.argv[2]?.trim()
148+
const rootPkg = readJsonFile(ROOT_PACKAGE_PATH)
149+
const changedPaths = new Set<string>()
150+
151+
if (requestedVersion && rootPkg.version !== requestedVersion) {
152+
rootPkg.version = requestedVersion
153+
writeJsonFile(ROOT_PACKAGE_PATH, rootPkg)
154+
changedPaths.add(ROOT_PACKAGE_PATH)
145155
}
146156

147-
// Sync GUI Cargo.toml version
148-
const cargoTomlPath = resolve('gui/src-tauri/Cargo.toml')
149-
try {
150-
const cargoContent = readFileSync(cargoTomlPath, 'utf-8')
151-
const cargoUpdated = cargoContent.replace(/^version = ".*"/m, `version = "${rootVersion}"`)
152-
if (cargoContent !== cargoUpdated) {
153-
writeFileSync(cargoTomlPath, cargoUpdated, 'utf-8')
154-
console.log(` ✓ Cargo.toml: version → ${rootVersion}`)
155-
changed = true
156-
}
157-
} catch {
158-
console.log('⚠️ gui/src-tauri/Cargo.toml not found, skipping')
157+
const rootVersion = rootPkg.version
158+
159+
if (rootVersion == null || rootVersion === '') {
160+
console.error('Root package.json missing version field')
161+
process.exit(1)
159162
}
160163

161-
// Sync tauri.conf.json version
162-
const tauriConfPath = resolve('gui/src-tauri/tauri.conf.json')
163-
try {
164-
const tauriConfContent = readFileSync(tauriConfPath, 'utf-8')
165-
const tauriConf = JSON.parse(tauriConfContent)
166-
if (tauriConf.version !== rootVersion) {
167-
console.log(` ✓ tauri.conf.json: version ${tauriConf.version ?? '(none)'}${rootVersion}`)
168-
tauriConf.version = rootVersion
169-
writeFileSync(tauriConfPath, JSON.stringify(tauriConf, null, 2) + '\n', 'utf-8')
170-
changed = true
171-
}
172-
} catch {
173-
console.log('⚠️ gui/src-tauri/tauri.conf.json not found, skipping')
164+
console.log(`🔄 Syncing version: ${rootVersion}`)
165+
166+
const packageJsonPaths = discoverFilesByName(ROOT_DIR, 'package.json')
167+
.filter(filePath => resolve(filePath) !== ROOT_PACKAGE_PATH)
168+
.sort()
169+
170+
for (const filePath of packageJsonPaths) {
171+
syncJsonVersion(filePath, rootVersion, changedPaths)
174172
}
175173

176-
// Sync version field in tnmsc.example.json files
177-
const exampleConfigPaths = [
178-
'libraries/init-bundle/public/public/tnmsc.example.json',
179-
]
174+
syncCargoVersion(ROOT_CARGO_PATH, 'workspace.package', rootVersion, changedPaths)
180175

181-
for (const examplePath of exampleConfigPaths) {
182-
const fullPath = resolve(examplePath)
183-
try {
184-
const content = readFileSync(fullPath, 'utf-8')
185-
const exampleJson = JSON.parse(content) as Record<string, unknown>
186-
if (exampleJson['version'] !== rootVersion) {
187-
console.log(` ✓ ${examplePath}: version ${String(exampleJson['version'] ?? '(none)')}${rootVersion}`)
188-
exampleJson['version'] = rootVersion
189-
writeFileSync(fullPath, JSON.stringify(exampleJson, null, 2) + '\n', 'utf-8')
190-
changed = true
191-
}
192-
} catch {
193-
console.log(`⚠️ ${examplePath} not found or invalid, skipping`)
194-
}
176+
const cargoTomlPaths = discoverFilesByName(ROOT_DIR, 'Cargo.toml')
177+
.filter(filePath => resolve(filePath) !== ROOT_CARGO_PATH)
178+
.sort()
179+
180+
for (const filePath of cargoTomlPaths) {
181+
syncCargoVersion(filePath, 'package', rootVersion, changedPaths)
195182
}
196183

197-
if (changed) {
198-
console.log('\n📦 Versions synced, auto-staging changes...')
199-
try {
200-
const filesToStage = [
201-
'package.json',
202-
'Cargo.toml',
203-
'gui/src-tauri/Cargo.toml',
204-
'gui/src-tauri/tauri.conf.json',
205-
'libraries/init-bundle/public/public/tnmsc.example.json',
206-
...topLevelWorkspacePackages.map(p => p.path),
207-
...libraryPackages.map(p => p.path),
208-
...packagesPackages.map(p => p.path),
209-
...cliNpmPackages.map(p => p.path),
210-
].filter(path => existsSync(resolve(path)))
211-
execSync(
212-
`git add ${filesToStage.join(' ')}`,
213-
{ stdio: 'inherit' }
214-
)
215-
console.log('✅ Staged modified files')
216-
} catch {
217-
console.log('⚠️ git add failed, please execute manually')
218-
}
219-
} else {
184+
for (const filePath of discoverFilesByName(ROOT_DIR, 'tauri.conf.json').sort()) {
185+
syncJsonVersion(filePath, rootVersion, changedPaths)
186+
}
187+
188+
if (changedPaths.size === 0) {
220189
console.log('\n✅ All versions consistent, no update needed')
190+
process.exit(0)
191+
}
192+
193+
const changedRelativePaths = [...changedPaths]
194+
.map(filePath => relative(ROOT_DIR, filePath))
195+
.sort()
196+
197+
console.error('\n❌ Versions were out of sync. Updated files:')
198+
for (const relativePath of changedRelativePaths) {
199+
console.error(` - ${relativePath}`)
221200
}
201+
console.error('\nReview these changes and rerun the commit.')
202+
process.exit(1)

0 commit comments

Comments
 (0)