Files
CherryHQ-cherry-studio/v2-refactor-temp/tools/data-classify/scripts/generate-boot-config.js
fullex c514dcc049 refactor(shared): move packages/shared to src/shared
packages/shared was never a real pnpm workspace package (no package.json); it was referenced only through the @shared TypeScript path alias. Relocate it under src/ via git mv (143 files, detected as pure renames).

Repoint the @shared alias and include globs to src/shared across electron.vite.config.ts, tsconfig.{json,node,web}.json and vitest.config.ts; update scripts/check-custom-exts.ts, scripts/update-languages.ts, the eslint.config.mjs generated-file globs, the data-classify generator output targets, .github/CODEOWNERS path rules, and CLAUDE.md/docs/source-comment references.

The @shared alias name is unchanged, so all 1403 @shared/* import sites resolve without modification. Verified with typecheck:node, typecheck:web and the full test suite (700 files, 9739 tests passing).
2026-05-28 21:02:49 -07:00

303 lines
11 KiB
JavaScript

#!/usr/bin/env node
const fs = require('fs')
const path = require('path')
/**
* Manually maintained boot config items that aren't sourced from
* classification.json. These get merged into the generator's normal
* extraction pipeline so they flow through the same sort/emit code as
* classification-derived items — the output schema is a single, flat,
* fully auto-generated file.
*
* When classification.json learns to model new source kinds (e.g. a
* 'configfile' category), these entries should move into classification.json
* and this constant can shrink.
*/
const MANUAL_BOOT_CONFIG_ITEMS = [
{
source: 'configfile',
sourceCategory: 'legacy-home',
originalKey: 'appDataPath',
targetKey: 'app.user_data_path',
type: 'Record<string, string>',
defaultValue: 'VALUE: {}',
jsdoc: [
'Custom user data directory, keyed by executable path.',
'',
'Conceptually a single setting ("where user data lives"); stored as a',
'Record so the same machine can host multiple installations (stable / dev /',
"portable) with independent user data locations — matching the v1 behavior",
"of ~/.cherrystudio/config/config.json's `appDataPath` array.",
'',
"Key: executable path (matches Electron's `app.getPath('exe')`).",
'Value: absolute path to the chosen userData directory.',
'',
'Migrated from v1 ~/.cherrystudio/config/config.json on first v1→v2 run',
"via the 'configfile' source in BootConfigMigrator."
]
},
{
source: 'preboot',
sourceCategory: 'transient',
originalKey: 'userDataRelocation',
targetKey: 'temp.user_data_relocation',
type:
"\n | { status: 'pending'; from: string; to: string }" +
"\n | { status: 'failed'; from: string; to: string; error: string; failedAt: string }" +
'\n | null',
defaultValue: null,
jsdoc: [
'In-flight relocation of the Electron userData directory tree',
"(the directory returned by `app.getPath('userData')`).",
'',
'Lives under the `temp.*` top-level namespace — reserved for ephemeral',
'runtime state: single in-flight operations meant to be cleared once',
'consumed. **Never** backed up or synced: restoring a stale temp.* entry',
'on a different machine or at a different time can cause silent data',
'corruption (e.g. re-executing a relocation that already happened).',
'',
'Lifecycle:',
' - null: no relocation in progress (default).',
" - { status: 'pending', from, to }: an IPC handler wrote this request",
' and the next preboot should execute the copy.',
" - { status: 'failed', from, to, error, failedAt }: a previous preboot",
' attempted the copy and it failed. The record stays in BootConfig',
' until a renderer recovery flow lets the user retry, abandon, or',
' investigate. The app continues running on the previous userData',
' location until then.',
'',
'Note: "userData" here means the Electron OS directory',
"(app.getPath('userData')), not the colloquial sense of user content.",
'The copy includes everything under that directory — user files,',
'Chromium runtime state, logs, etc.',
'',
'Consumer: src/main/core/preboot/userDataLocation.ts'
]
}
]
class BootConfigGenerator {
constructor() {
this.dataDir = path.resolve(__dirname, '../data')
this.targetFile = path.resolve(__dirname, '../../../../src/shared/data/bootConfig/bootConfigSchemas.ts')
this.classificationFile = path.join(this.dataDir, 'classification.json')
}
generate() {
console.log('Generating bootConfigSchemas.ts...')
const classification = this.loadClassification()
const bootConfigData = this.extractBootConfigData(classification)
const content = this.generateTypeScriptCode(bootConfigData)
this.writeFile(content)
console.log('bootConfigSchemas.ts generated!')
this.printSummary(bootConfigData)
}
loadClassification() {
if (!fs.existsSync(this.classificationFile)) {
throw new Error(`Classification file not found: ${this.classificationFile}`)
}
const content = fs.readFileSync(this.classificationFile, 'utf8')
return JSON.parse(content)
}
extractBootConfigData(classification) {
const allItems = []
const sources = ['electronStore', 'redux', 'localStorage', 'dexieSettings']
const extractItems = (items, source, category, parentKey = '') => {
if (!Array.isArray(items)) return
items.forEach((item) => {
if (item.children && Array.isArray(item.children)) {
extractItems(item.children, source, category, `${parentKey}${item.originalKey}.`)
return
}
if (item.category === 'bootConfig' && item.status === 'classified' && item.targetKey) {
allItems.push({
...item,
source,
sourceCategory: category,
originalKey: parentKey + item.originalKey,
fullPath: `${source}/${category}/${parentKey}${item.originalKey}`,
})
}
})
}
sources.forEach((source) => {
if (classification.classifications[source]) {
Object.keys(classification.classifications[source]).forEach((category) => {
const items = classification.classifications[source][category]
extractItems(items, source, category)
})
}
})
console.log(`Extracted ${allItems.length} bootConfig items`)
// Deduplicate by targetKey: redux(4) > dexieSettings(3) > localStorage(2) > electronStore(1)
const targetKeyGroups = {}
allItems.forEach((item) => {
if (!targetKeyGroups[item.targetKey]) {
targetKeyGroups[item.targetKey] = []
}
targetKeyGroups[item.targetKey].push(item)
})
const sourcePriority = { redux: 4, dexieSettings: 3, localStorage: 2, electronStore: 1 }
const deduplicatedData = []
Object.keys(targetKeyGroups).forEach((targetKey) => {
const items = targetKeyGroups[targetKey]
if (items.length > 1) {
console.log(`Duplicate targetKey: ${targetKey}, ${items.length} items`)
items.forEach((item) => console.log(` - ${item.fullPath}`))
items.sort((a, b) => sourcePriority[b.source] - sourcePriority[a.source])
const selected = items[0]
console.log(` Selected: ${selected.fullPath}`)
deduplicatedData.push(selected)
} else {
deduplicatedData.push(items[0])
}
})
// Append manually maintained items (config-file source for v1 legacy
// home config). These bypass classification.json deliberately — see the
// MANUAL_BOOT_CONFIG_ITEMS comment at the top of this file.
for (const manual of MANUAL_BOOT_CONFIG_ITEMS) {
deduplicatedData.push({ ...manual, fullPath: `${manual.source}/${manual.sourceCategory}/${manual.originalKey}` })
}
console.log(`After deduplication + manual items: ${deduplicatedData.length} bootConfig items`)
return deduplicatedData
}
mapType(itemType, defaultValue) {
if (itemType && itemType !== 'unknown') {
if (itemType === 'boolean') return 'boolean'
if (itemType === 'string') return 'string'
if (itemType === 'number') return 'number'
return itemType
}
if (defaultValue !== null && defaultValue !== undefined) {
const valueType = typeof defaultValue
if (valueType === 'boolean' || valueType === 'string' || valueType === 'number') {
return valueType
}
}
return 'unknown'
}
formatDefaultValue(value) {
if (value === null || value === undefined) {
return 'null'
}
if (typeof value === 'string') {
if (value.startsWith('VALUE: ')) {
return value.substring(7)
}
return `'${value.replace(/'/g, "\\'")}'`
}
if (typeof value === 'boolean' || typeof value === 'number') {
return String(value)
}
if (Array.isArray(value)) {
return `[${value.map((item) => this.formatDefaultValue(item)).join(', ')}]`
}
if (typeof value === 'object') {
const entries = Object.entries(value).map(([k, v]) => `${k}: ${this.formatDefaultValue(v)}`)
return `{ ${entries.join(', ')} }`
}
return JSON.stringify(value)
}
generateTypeScriptCode(bootConfigData) {
const sortedData = [...bootConfigData].sort((a, b) => a.targetKey.localeCompare(b.targetKey))
const header = `/**
* Auto-generated boot config schema
* Generated at: ${new Date().toISOString()}
*
* This file is automatically generated from classification.json (plus a
* small MANUAL_BOOT_CONFIG_ITEMS list in generate-boot-config.js for keys
* that don't fit classification.json's model yet, e.g. config-file sources).
*
* To update this file, either modify classification.json or the manual list
* in the generator, then run:
* node v2-refactor-temp/tools/data-classify/scripts/generate-boot-config.js
*
* === AUTO-GENERATED CONTENT START ===
*/`
let interfaceCode = 'export interface BootConfigSchema {\n'
sortedData.forEach((item, index) => {
const tsType = this.mapType(item.type, item.defaultValue)
// Optional JSDoc block (currently only emitted for manual items).
if (Array.isArray(item.jsdoc) && item.jsdoc.length > 0) {
if (index > 0) interfaceCode += '\n'
interfaceCode += ' /**\n'
for (const line of item.jsdoc) {
interfaceCode += line.length > 0 ? ` * ${line}\n` : ' *\n'
}
interfaceCode += ' */\n'
}
interfaceCode += ` // ${item.source}/${item.sourceCategory}/${item.originalKey}\n`
interfaceCode += ` '${item.targetKey}': ${tsType}\n`
})
interfaceCode += '}'
let defaultsCode = 'export const DefaultBootConfig: BootConfigSchema = {\n'
sortedData.forEach((item, index) => {
const defaultVal = this.formatDefaultValue(item.defaultValue)
const isLast = index === sortedData.length - 1
defaultsCode += ` '${item.targetKey}': ${defaultVal}${isLast ? '' : ','}\n`
})
defaultsCode += '}'
const footer = '// === AUTO-GENERATED CONTENT END ==='
return [header, '', interfaceCode, '', defaultsCode, '', footer, ''].join('\n')
}
writeFile(content) {
const targetDir = path.dirname(this.targetFile)
if (!fs.existsSync(targetDir)) {
fs.mkdirSync(targetDir, { recursive: true })
}
fs.writeFileSync(this.targetFile, content, 'utf8')
console.log(`Written to: ${this.targetFile}`)
}
printSummary(bootConfigData) {
console.log(`\nSummary:`)
console.log(`- Total items: ${bootConfigData.length}`)
console.log(`- electronStore: ${bootConfigData.filter((p) => p.source === 'electronStore').length}`)
console.log(`- redux: ${bootConfigData.filter((p) => p.source === 'redux').length}`)
console.log(`- localStorage: ${bootConfigData.filter((p) => p.source === 'localStorage').length}`)
console.log(`- dexieSettings: ${bootConfigData.filter((p) => p.source === 'dexieSettings').length}`)
console.log(`- Output: ${this.targetFile}`)
}
}
if (require.main === module) {
try {
const generator = new BootConfigGenerator()
generator.generate()
} catch (error) {
console.error('Generation failed:', error.message)
process.exit(1)
}
}
module.exports = BootConfigGenerator