tha-cli 0.0.0-stage → 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/ARCHITECTURE.md +193 -0
- package/LICENSE +21 -0
- package/README.md +408 -2
- package/package.json +42 -4
- package/tha/cli.mjs +116 -0
- package/tha/commands/adopt.mjs +119 -0
- package/tha/commands/check.mjs +226 -0
- package/tha/commands/diff.mjs +70 -0
- package/tha/commands/init.mjs +262 -0
- package/tha/commands/remove.mjs +328 -0
- package/tha/commands/sync.mjs +826 -0
- package/tha/config/field.mjs +178 -0
- package/tha/config/load.mjs +188 -0
- package/tha/config/manifest.mjs +75 -0
- package/tha/config/paths.mjs +124 -0
- package/tha/config/project.mjs +52 -0
- package/tha/config/rules.mjs +85 -0
- package/tha/config/schema.mjs +306 -0
- package/tha/config/template.mjs +78 -0
- package/tha/generate/domain/api.mjs +67 -0
- package/tha/generate/domain/context.mjs +76 -0
- package/tha/generate/domain/detail.mjs +35 -0
- package/tha/generate/domain/entry.mjs +91 -0
- package/tha/generate/domain/executors.mjs +93 -0
- package/tha/generate/domain/form.mjs +34 -0
- package/tha/generate/domain/index.mjs +72 -0
- package/tha/generate/domain/permissions.mjs +35 -0
- package/tha/generate/domain/store.mjs +68 -0
- package/tha/generate/domain/view.mjs +29 -0
- package/tha/generate/global.mjs +111 -0
- package/tha/generate/header.mjs +29 -0
- package/tha/generate/names.mjs +45 -0
- package/tha/generate/plan.mjs +41 -0
- package/tha/kit/ThaCrudPage.vue +344 -0
- package/tha/kit/ThaDetail.vue +146 -0
- package/tha/kit/ThaForm.vue +194 -0
- package/tha/kit/request.ts +30 -0
- package/tha/kit/transport.ts +11 -0
- package/tha/utils/args.mjs +59 -0
- package/tha/utils/compare.mjs +38 -0
- package/tha/utils/files.mjs +175 -0
- package/tha/utils/format.mjs +23 -0
- package/tha/utils/output.mjs +79 -0
- package/tha/utils/style.mjs +147 -0
- package/tha/utils/ts.mjs +73 -0
- package/tha/verify/ast.mjs +143 -0
- package/tha/verify/boundary.mjs +219 -0
- package/tha/verify/contract.mjs +186 -0
- package/tha/verify/openapi.mjs +345 -0
- package/tha/verify/rules.mjs +91 -0
- package/tha/verify/scan.mjs +59 -0
- package/tha.mjs +22 -0
package/tha/cli.mjs
ADDED
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
import fs from 'node:fs'
|
|
2
|
+
import path from 'node:path'
|
|
3
|
+
import { runInit } from './commands/init.mjs'
|
|
4
|
+
import { runCheck } from './commands/check.mjs'
|
|
5
|
+
import { runDiff } from './commands/diff.mjs'
|
|
6
|
+
import { runSync } from './commands/sync.mjs'
|
|
7
|
+
import { runRemove } from './commands/remove.mjs'
|
|
8
|
+
import { runAdopt } from './commands/adopt.mjs'
|
|
9
|
+
import { configureStyle, ui } from './utils/style.mjs'
|
|
10
|
+
|
|
11
|
+
const COMMANDS = {
|
|
12
|
+
init: runInit,
|
|
13
|
+
check: runCheck,
|
|
14
|
+
diff: runDiff,
|
|
15
|
+
sync: runSync,
|
|
16
|
+
remove: runRemove,
|
|
17
|
+
adopt: runAdopt
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export async function run(argv) {
|
|
21
|
+
const { root, args, styleOptions } = extractRoot(argv)
|
|
22
|
+
configureStyle(styleOptions)
|
|
23
|
+
|
|
24
|
+
if (args.length === 0 || args[0] === '--help' || args[0] === '-h' || args[0] === 'help') {
|
|
25
|
+
printHelp()
|
|
26
|
+
return
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const [command, ...rest] = args
|
|
30
|
+
const handler = COMMANDS[command]
|
|
31
|
+
|
|
32
|
+
if (!handler) {
|
|
33
|
+
throw new Error(`未知命令「${command}」。可用:${Object.keys(COMMANDS).join(' / ')}`)
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
if (!fs.existsSync(root)) {
|
|
37
|
+
throw new Error(`--root 目录不存在:${root}`)
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
await handler({ root, args: rest })
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function extractRoot(argv) {
|
|
44
|
+
let root = process.cwd()
|
|
45
|
+
const args = []
|
|
46
|
+
const styleOptions = {}
|
|
47
|
+
|
|
48
|
+
for (let i = 0; i < argv.length; i += 1) {
|
|
49
|
+
const arg = argv[i]
|
|
50
|
+
|
|
51
|
+
if (arg === '--root') {
|
|
52
|
+
const value = argv[i + 1]
|
|
53
|
+
if (!value || value.startsWith('--')) throw new Error('--root 需要一个目录')
|
|
54
|
+
root = value
|
|
55
|
+
i += 1
|
|
56
|
+
continue
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
if (arg.startsWith('--root=')) {
|
|
60
|
+
root = arg.slice('--root='.length)
|
|
61
|
+
continue
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
if (arg === '--no-color') {
|
|
65
|
+
styleOptions.noColor = true
|
|
66
|
+
delete styleOptions.color
|
|
67
|
+
continue
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
if (arg === '--color') {
|
|
71
|
+
styleOptions.color = true
|
|
72
|
+
delete styleOptions.noColor
|
|
73
|
+
continue
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
args.push(arg)
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
return { root: path.resolve(root), args, styleOptions }
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function printHelp() {
|
|
83
|
+
console.log('')
|
|
84
|
+
console.log(` ${ui.heading('标准增删改查生成器', 'page generator')}`)
|
|
85
|
+
console.log(` ${ui.rule(40)}`)
|
|
86
|
+
console.log('')
|
|
87
|
+
console.log(` ${ui.label('用法', 'Usage')}`)
|
|
88
|
+
console.log(` ${ui.command('tha init')} <模块> [--title 标题] [--path /页面地址] [--api /接口地址]`)
|
|
89
|
+
console.log(` ${ui.command('tha check')} [--json] [--strict]`)
|
|
90
|
+
console.log(` ${ui.command('tha diff')} [--json]`)
|
|
91
|
+
console.log(` ${ui.command('tha sync')} [<模块> | --all] [--prune] [--force] [--dry-run] [--json]`)
|
|
92
|
+
console.log(` ${ui.command('tha remove')} <模块> [--purge] [--dry-run] [--json]`)
|
|
93
|
+
console.log(` ${ui.command('tha adopt')} [<文件...>] [--release] ${ui.muted('# 生成的文件改成自己维护')}`)
|
|
94
|
+
console.log('')
|
|
95
|
+
console.log(` ${ui.label('例子', 'Examples')}`)
|
|
96
|
+
console.log(` ${ui.muted('tha init doc --title 文档 --path /doc --api /docs')}`)
|
|
97
|
+
console.log(` ${ui.muted('tha check')}`)
|
|
98
|
+
console.log(` ${ui.muted('tha diff')}`)
|
|
99
|
+
console.log(` ${ui.muted('tha sync doc # 只同步一个模块')}`)
|
|
100
|
+
console.log(` ${ui.muted('tha sync --all # 同步全部模块')}`)
|
|
101
|
+
console.log(` ${ui.muted('tha sync --all --prune # 顺带清理已删模块的残留')}`)
|
|
102
|
+
console.log(` ${ui.muted('tha remove doc # 删除模块(手写文件保留)')}`)
|
|
103
|
+
console.log(` ${ui.muted('tha adopt src/api/doc.ts # 这个文件改成自己维护')}`)
|
|
104
|
+
console.log(` ${ui.muted('tha adopt --release src/api/doc.ts')}`)
|
|
105
|
+
console.log('')
|
|
106
|
+
console.log(` ${ui.label('说明', 'Notes')}`)
|
|
107
|
+
console.log(` ${ui.bullet('reads config, never executes it (只读 config.ts,不会执行它)')}`)
|
|
108
|
+
console.log(` ${ui.bullet('5 files, scaffold hub once (生成 5 个文件;新模块首次补模块入口,之后不覆盖)')}`)
|
|
109
|
+
console.log(` ${ui.bullet('checks manual modules too (也检查手写模块:没 config 的按目录形状识别)')}`)
|
|
110
|
+
console.log(` ${ui.bullet('list only (不带模块只列模块,不写文件)')}`)
|
|
111
|
+
console.log(` ${ui.bullet('invalid config writes nothing (配置有误时不写任何文件)')}`)
|
|
112
|
+
console.log(` ${ui.bullet('set project root (指定项目目录)')}`)
|
|
113
|
+
console.log(` ${ui.bullet('disable color; THA_THEME=plain for plain text (关闭颜色;THA_THEME=plain 纯文本)')}`)
|
|
114
|
+
console.log(` ${ui.bullet('force color when piped (被管道接管时也强制上色)')}`)
|
|
115
|
+
console.log('')
|
|
116
|
+
}
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
// ============ 生成位交接:adopt / release ============
|
|
2
|
+
// adopt 只对「生成计划里存在的文件」生效 —— 它表达的是「这个生成位改由人维护」,
|
|
3
|
+
// 不是「随便标一个文件」。交接后 CLI 不写、不比对漂移,但护栏照查。
|
|
4
|
+
|
|
5
|
+
import path from 'node:path'
|
|
6
|
+
import { loadProject } from '../config/project.mjs'
|
|
7
|
+
import { adoptFiles, loadManifest, releaseFiles } from '../config/manifest.mjs'
|
|
8
|
+
import { buildPlan } from '../generate/plan.mjs'
|
|
9
|
+
import { parseArgs } from '../utils/args.mjs'
|
|
10
|
+
import { printErrors, printHeader } from '../utils/output.mjs'
|
|
11
|
+
import { toPosix } from '../utils/files.mjs'
|
|
12
|
+
import { glyph, ui } from '../utils/style.mjs'
|
|
13
|
+
|
|
14
|
+
export function runAdopt({ root, args }) {
|
|
15
|
+
// adopt 不写业务文件(只改 manifest),本来就不需要确认;--yes 接受但忽略,
|
|
16
|
+
// 免得和 sync / remove / init 的参数习惯不一致时报「未知参数」。
|
|
17
|
+
const { flags, positional } = parseArgs(args, { boolean: ['json', 'help', 'release', 'yes'] })
|
|
18
|
+
|
|
19
|
+
if (flags.help) {
|
|
20
|
+
printAdoptHelp()
|
|
21
|
+
return
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
const project = loadProject(root)
|
|
25
|
+
|
|
26
|
+
if (project.errors.length > 0) {
|
|
27
|
+
if (flags.json) {
|
|
28
|
+
console.log(JSON.stringify({ ok: false, errors: project.errors }, null, 2))
|
|
29
|
+
} else {
|
|
30
|
+
printErrors(project.errors)
|
|
31
|
+
}
|
|
32
|
+
process.exitCode = 1
|
|
33
|
+
return
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
const manifest = loadManifest(root)
|
|
37
|
+
|
|
38
|
+
// 不带参数:列出已接手的生成位
|
|
39
|
+
if (positional.length === 0) {
|
|
40
|
+
if (flags.json) {
|
|
41
|
+
console.log(JSON.stringify({ ok: true, manual: manifest.manual }, null, 2))
|
|
42
|
+
return
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
printHeader('改为自己维护', 'Adopt')
|
|
46
|
+
if (manifest.manual.length === 0) {
|
|
47
|
+
console.log(` ${ui.muted(glyph.dot)} ${ui.label('还没有改成自己维护的文件', 'No hand-maintained files')}`)
|
|
48
|
+
} else {
|
|
49
|
+
for (const entry of manifest.manual) {
|
|
50
|
+
console.log(` ${ui.path(entry)}`)
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
console.log('')
|
|
54
|
+
return
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
const wanted = positional.map((entry) => toPosix(path.relative(root, path.resolve(root, entry))))
|
|
58
|
+
|
|
59
|
+
if (flags.release) {
|
|
60
|
+
const removed = releaseFiles(root, wanted)
|
|
61
|
+
if (flags.json) {
|
|
62
|
+
console.log(JSON.stringify({ ok: true, released: removed }, null, 2))
|
|
63
|
+
return
|
|
64
|
+
}
|
|
65
|
+
printHeader('交回生成管理', 'Release')
|
|
66
|
+
for (const entry of removed) {
|
|
67
|
+
console.log(` ${ui.success(glyph.ok)} ${ui.path(entry)}`)
|
|
68
|
+
}
|
|
69
|
+
if (removed.length === 0) {
|
|
70
|
+
console.log(` ${ui.muted(glyph.dot)} ${ui.label('这些文件本来就不在交接清单里', 'Not in the manifest')}`)
|
|
71
|
+
}
|
|
72
|
+
console.log('')
|
|
73
|
+
return
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
const planPaths = new Set(buildPlan(project.domains, root, project.dirs).map((file) => file.path))
|
|
77
|
+
const unknown = wanted.filter((entry) => !planPaths.has(entry))
|
|
78
|
+
|
|
79
|
+
if (unknown.length > 0) {
|
|
80
|
+
throw new Error(
|
|
81
|
+
`这些文件不是 CLI 生成的,没法接手:${unknown.join(', ')}\n只接受构建期会生成的文件(用 tha diff 查看)`
|
|
82
|
+
)
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
const added = adoptFiles(root, wanted)
|
|
86
|
+
|
|
87
|
+
if (flags.json) {
|
|
88
|
+
console.log(JSON.stringify({ ok: true, adopted: added, kept: wanted.filter((e) => !added.includes(e)) }, null, 2))
|
|
89
|
+
return
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
printHeader('改成自己维护', 'Adopt')
|
|
93
|
+
for (const entry of wanted) {
|
|
94
|
+
const isNew = added.includes(entry)
|
|
95
|
+
const mark = isNew ? ui.success(glyph.ok) : ui.muted(glyph.dot)
|
|
96
|
+
console.log(` ${mark} ${ui.path(entry)} ${isNew ? '' : ui.muted('(already in list)(已在清单里)')}`)
|
|
97
|
+
}
|
|
98
|
+
console.log('')
|
|
99
|
+
console.log(` ${ui.muted('CLI 不再写这些文件;tha check 的分层规则照样会查它们')}`)
|
|
100
|
+
console.log(` ${ui.muted('Release (改回生成管理): tha adopt <文件> --release')}`)
|
|
101
|
+
console.log('')
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
function printAdoptHelp() {
|
|
105
|
+
console.log('')
|
|
106
|
+
console.log(` ${ui.heading('改为自己维护', 'adopt · take over generated files')}`)
|
|
107
|
+
console.log(` ${ui.rule(50)}`)
|
|
108
|
+
console.log('')
|
|
109
|
+
console.log(` ${ui.label('用法', 'Usage')}`)
|
|
110
|
+
console.log(` ${ui.command('tha adopt')} ${ui.muted('# 列出改成自己维护的文件')}`)
|
|
111
|
+
console.log(` ${ui.command('tha adopt')} <文件...> ${ui.muted('# 改成自己维护(CLI 不再生成)')}`)
|
|
112
|
+
console.log(` ${ui.command('tha adopt')} <文件...> --release ${ui.muted('# 交回给 CLI 管理')}`)
|
|
113
|
+
console.log('')
|
|
114
|
+
console.log(` ${ui.label('说明', 'Notes')}`)
|
|
115
|
+
console.log(` ${ui.bullet('只接受 CLI 会生成的文件(用 tha diff 查看)')}`)
|
|
116
|
+
console.log(` ${ui.bullet('改成自己维护后,不再算「被改过或有冲突」,但分层规则照样查')}`)
|
|
117
|
+
console.log(` ${ui.bullet('清单写在 .tha/manifest.json,建议提交到版本库')}`)
|
|
118
|
+
console.log('')
|
|
119
|
+
}
|
|
@@ -0,0 +1,226 @@
|
|
|
1
|
+
import { loadProject } from '../config/project.mjs'
|
|
2
|
+
import { manualPaths } from '../config/manifest.mjs'
|
|
3
|
+
import { buildPlan } from '../generate/plan.mjs'
|
|
4
|
+
import { comparePlan } from '../utils/compare.mjs'
|
|
5
|
+
import { parseArgs } from '../utils/args.mjs'
|
|
6
|
+
import { printErrors, printHeader } from '../utils/output.mjs'
|
|
7
|
+
import { checkBoundaries } from '../verify/boundary.mjs'
|
|
8
|
+
import { checkContracts } from '../verify/contract.mjs'
|
|
9
|
+
import { checkOpenApiContracts } from '../verify/openapi.mjs'
|
|
10
|
+
import { describeConfigured } from '../verify/scan.mjs'
|
|
11
|
+
import { glyph, ui } from '../utils/style.mjs'
|
|
12
|
+
|
|
13
|
+
export function runCheck({ root, args }) {
|
|
14
|
+
const { flags } = parseArgs(args, { boolean: ['json', 'help', 'strict'] })
|
|
15
|
+
|
|
16
|
+
if (flags.help) {
|
|
17
|
+
printCheckHelp()
|
|
18
|
+
return
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
const project = loadProject(root)
|
|
22
|
+
|
|
23
|
+
if (project.errors.length > 0) {
|
|
24
|
+
if (flags.json) {
|
|
25
|
+
console.log(JSON.stringify({ ok: false, errors: project.errors }, null, 2))
|
|
26
|
+
} else {
|
|
27
|
+
printErrors(project.errors)
|
|
28
|
+
}
|
|
29
|
+
process.exitCode = 1
|
|
30
|
+
return
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
// 两种模块一起检查:config 驱动的过完整契约,手写的只过中枢契约
|
|
34
|
+
const modules = [...describeConfigured(project.domains, root, project.dirs), ...project.manual]
|
|
35
|
+
// 纯手写项目(0 个 config 模块 + 有手写模块):CLI 不托管任何文件,不比对漂移
|
|
36
|
+
const pureManual = project.domains.length === 0 && project.manual.length > 0
|
|
37
|
+
const entries = pureManual
|
|
38
|
+
? []
|
|
39
|
+
: comparePlan(buildPlan(project.domains, root, project.dirs), root, manualPaths(root))
|
|
40
|
+
const adopted = entries.filter((entry) => entry.status === 'manual')
|
|
41
|
+
const missing = entries.filter((entry) => entry.status === 'add')
|
|
42
|
+
const drift = entries.filter((entry) => entry.status === 'update')
|
|
43
|
+
const edited = entries.filter((entry) => entry.status === 'edited')
|
|
44
|
+
const existing = entries.filter((entry) => entry.status === 'conflict')
|
|
45
|
+
const blocked = edited.length + existing.length
|
|
46
|
+
const contracts = [
|
|
47
|
+
...checkContracts(modules, root, project.dirs),
|
|
48
|
+
...checkOpenApiContracts(modules, root, project.dirs)
|
|
49
|
+
]
|
|
50
|
+
const contractErrors = contracts.filter((item) => (item.level ?? 'error') === 'error')
|
|
51
|
+
const contractWarns = contracts.filter((item) => item.level === 'warn')
|
|
52
|
+
const guards = checkBoundaries(modules, root, project.dirs)
|
|
53
|
+
const guardErrors = guards.filter((item) => item.level === 'error')
|
|
54
|
+
const guardWarns = guards.filter((item) => item.level === 'warn')
|
|
55
|
+
|
|
56
|
+
// 契约违约、漂移、手改、护栏 error,都让 CI 红;warn 只在 --strict 时红
|
|
57
|
+
const failed =
|
|
58
|
+
drift.length > 0 ||
|
|
59
|
+
blocked > 0 ||
|
|
60
|
+
contractErrors.length > 0 ||
|
|
61
|
+
guardErrors.length > 0 ||
|
|
62
|
+
(flags.strict && (contractWarns.length > 0 || guardWarns.length > 0))
|
|
63
|
+
|
|
64
|
+
if (failed) process.exitCode = 1
|
|
65
|
+
|
|
66
|
+
if (flags.json) {
|
|
67
|
+
console.log(
|
|
68
|
+
JSON.stringify(
|
|
69
|
+
{
|
|
70
|
+
ok: !failed,
|
|
71
|
+
domains: project.domains.map((domain) => domain.name),
|
|
72
|
+
manual: project.manual.map((module) => module.name),
|
|
73
|
+
contracts,
|
|
74
|
+
guards,
|
|
75
|
+
adopted: adopted.map((entry) => entry.path),
|
|
76
|
+
missing: missing.map((entry) => entry.path),
|
|
77
|
+
drift: drift.map((entry) => entry.path),
|
|
78
|
+
edited: edited.map((entry) => entry.path),
|
|
79
|
+
conflicts: existing.map((entry) => entry.path)
|
|
80
|
+
},
|
|
81
|
+
null,
|
|
82
|
+
2
|
|
83
|
+
)
|
|
84
|
+
)
|
|
85
|
+
return
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
printHeader('配置检查', 'Check')
|
|
89
|
+
console.log(
|
|
90
|
+
` ${ui.success(glyph.ok)} ${ui.label(
|
|
91
|
+
`配置没问题:${project.domains.length} 个模块,${project.manual.length} 个手写模块`,
|
|
92
|
+
`Config OK: ${project.domains.length} managed, ${project.manual.length} manual`
|
|
93
|
+
)}`
|
|
94
|
+
)
|
|
95
|
+
|
|
96
|
+
if (contracts.length > 0) {
|
|
97
|
+
console.log('')
|
|
98
|
+
if (contractErrors.length > 0) {
|
|
99
|
+
console.log(
|
|
100
|
+
` ${ui.danger(glyph.err)} ${ui.label(
|
|
101
|
+
`有 ${contractErrors.length} 处对不上,${contractWarns.length} 处要确认`,
|
|
102
|
+
`${contractErrors.length} mismatch, ${contractWarns.length} warning`
|
|
103
|
+
)}`
|
|
104
|
+
)
|
|
105
|
+
} else {
|
|
106
|
+
console.log(
|
|
107
|
+
` ${ui.warning(glyph.warn)} ${ui.label(
|
|
108
|
+
`有 ${contractWarns.length} 处要确认`,
|
|
109
|
+
`${contractWarns.length} warning`
|
|
110
|
+
)}`
|
|
111
|
+
)
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
for (const problem of contracts) {
|
|
115
|
+
const badge = (problem.level ?? 'error') === 'error' ? ui.danger(glyph.warn) : ui.warning(glyph.warn)
|
|
116
|
+
console.log(` ${badge} ${ui.path(problem.file)}`)
|
|
117
|
+
console.log(` ${ui.muted(problem.message)}`)
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
if (guards.length > 0) {
|
|
122
|
+
console.log('')
|
|
123
|
+
console.log(
|
|
124
|
+
` ${ui.danger(glyph.err)} ${ui.label(
|
|
125
|
+
`分层规则被破坏:${guardErrors.length} 处错误、${guardWarns.length} 处提醒`,
|
|
126
|
+
`Layering rules broken: ${guardErrors.length} errors, ${guardWarns.length} warnings`
|
|
127
|
+
)}`
|
|
128
|
+
)
|
|
129
|
+
for (const item of guards) {
|
|
130
|
+
const badge = item.level === 'error' ? ui.danger(item.rule) : ui.warning(item.rule)
|
|
131
|
+
console.log(` ${ui.path(`${item.file}:${item.line}`)} ${badge}`)
|
|
132
|
+
console.log(` 问题:${item.message}`)
|
|
133
|
+
if (item.note) console.log(` ${ui.muted(item.note)}`)
|
|
134
|
+
console.log(` 应该:${item.fix}`)
|
|
135
|
+
console.log(` ${ui.muted(`依据:${item.basis}`)}`)
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
if (blocked > 0) {
|
|
140
|
+
console.log('')
|
|
141
|
+
console.log(
|
|
142
|
+
` ${ui.warning(glyph.warn)} ${ui.label(
|
|
143
|
+
'tha sync 会被这些已有文件挡住',
|
|
144
|
+
'tha sync is blocked by these files'
|
|
145
|
+
)}`
|
|
146
|
+
)
|
|
147
|
+
|
|
148
|
+
if (existing.length > 0) {
|
|
149
|
+
console.log(
|
|
150
|
+
` ${ui.muted(`hand-written, kept as-is (${existing.length} 个手写文件,不是 CLI 生成,默认不覆盖)`)}`
|
|
151
|
+
)
|
|
152
|
+
for (const entry of existing) {
|
|
153
|
+
console.log(` ${ui.danger(glyph.warn)} ${ui.path(entry.path)}`)
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
if (edited.length > 0) {
|
|
157
|
+
console.log(
|
|
158
|
+
` ${ui.muted(`edited by hand, kept as-is (${edited.length} 个已有文件被手动改过,默认不覆盖)`)}`
|
|
159
|
+
)
|
|
160
|
+
for (const entry of edited) {
|
|
161
|
+
console.log(` ${ui.danger(glyph.warn)} ${ui.path(entry.path)}`)
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
if (missing.length > 0) {
|
|
165
|
+
console.log(
|
|
166
|
+
` ${ui.muted(`${missing.length} files not generated yet (还有 ${missing.length} 个文件没生成)`)}`
|
|
167
|
+
)
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
console.log(` ${ui.bullet(`${ui.label('看是哪几个文件', 'Details')}: ${ui.command('tha diff')}`)}`)
|
|
171
|
+
console.log(` ${ui.bullet(`${ui.label('强制重新生成', 'Overwrite')}: ${ui.command('tha sync --all --force')}`)}`)
|
|
172
|
+
} else if (missing.length > 0) {
|
|
173
|
+
console.log(
|
|
174
|
+
` ${ui.muted(glyph.dot)} ${ui.label(
|
|
175
|
+
`还有 ${missing.length} 个文件没生成`,
|
|
176
|
+
`${missing.length} files not generated yet`
|
|
177
|
+
)} ${ui.muted('→ tha sync')}`
|
|
178
|
+
)
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
if (drift.length > 0) {
|
|
182
|
+
console.log('')
|
|
183
|
+
console.log(` ${ui.warning(glyph.warn)} ${ui.label('这些文件需要更新', 'These files need updating')}`)
|
|
184
|
+
for (const entry of drift) {
|
|
185
|
+
console.log(` ${ui.warning(glyph.warn)} ${ui.path(entry.path)}`)
|
|
186
|
+
}
|
|
187
|
+
console.log(` ${ui.muted('Run (运行): tha sync --all')}`)
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
if (adopted.length > 0) {
|
|
191
|
+
console.log(
|
|
192
|
+
` ${ui.muted(glyph.dot)} ${ui.label(
|
|
193
|
+
`${adopted.length} 个文件改成你自己维护了(不再自动生成,分层规则还会查)`,
|
|
194
|
+
`${adopted.length} files now hand-maintained`
|
|
195
|
+
)}`
|
|
196
|
+
)
|
|
197
|
+
for (const entry of adopted) {
|
|
198
|
+
console.log(` ${ui.muted(entry.path)}`)
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
if (!failed) {
|
|
203
|
+
console.log(` ${ui.success(glyph.ok)} ${ui.label('文件都是最新的,分层规则没被破坏', 'All files up to date, structure intact')}`)
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
console.log('')
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
function printCheckHelp() {
|
|
210
|
+
console.log('')
|
|
211
|
+
console.log(` ${ui.heading('配置检查', 'check · validate config & guardrails')}`)
|
|
212
|
+
console.log(` ${ui.rule(50)}`)
|
|
213
|
+
console.log('')
|
|
214
|
+
console.log(` ${ui.label('用法', 'Usage')}`)
|
|
215
|
+
console.log(` ${ui.command('tha check')} [--json] [--strict]`)
|
|
216
|
+
console.log('')
|
|
217
|
+
console.log(` ${ui.label('说明', 'Notes')}`)
|
|
218
|
+
console.log(` ${ui.bullet('检查必填/选填、不认识的配置项、重复地址、可选值、默认值')}`)
|
|
219
|
+
console.log(` ${ui.bullet('配置和生成物对不对得上:模块入口在不在、动作挂没挂、页面文件齐不齐')}`)
|
|
220
|
+
console.log(` ${ui.bullet('分层规则(谁不许 import 谁):入口不许绕开动作、动作之间不许互引、页面不许直接调接口、状态文件不许发请求')}`)
|
|
221
|
+
console.log(` ${ui.bullet('手写模块也检查(没有 config 的模块按目录形状识别,不参与生成)')}`)
|
|
222
|
+
console.log(` ${ui.bullet('文件内容和配置不一致、文件不是 CLI 写的,都算不通过(退出码 1)')}`)
|
|
223
|
+
console.log(` ${ui.bullet('warnings fail the run (--strict 警告也算失败)')}`)
|
|
224
|
+
console.log(` ${ui.bullet('disable color (关闭颜色)')}`)
|
|
225
|
+
console.log('')
|
|
226
|
+
}
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import { loadProject } from '../config/project.mjs'
|
|
2
|
+
import { manualPaths } from '../config/manifest.mjs'
|
|
3
|
+
import { buildPlan } from '../generate/plan.mjs'
|
|
4
|
+
import { comparePlan } from '../utils/compare.mjs'
|
|
5
|
+
import { parseArgs } from '../utils/args.mjs'
|
|
6
|
+
import { printDiff, printErrors } from '../utils/output.mjs'
|
|
7
|
+
import { glyph, ui } from '../utils/style.mjs'
|
|
8
|
+
|
|
9
|
+
export function runDiff({ root, args }) {
|
|
10
|
+
const { flags } = parseArgs(args, { boolean: ['json', 'help'] })
|
|
11
|
+
|
|
12
|
+
if (flags.help) {
|
|
13
|
+
printDiffHelp()
|
|
14
|
+
return
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
const project = loadProject(root)
|
|
18
|
+
|
|
19
|
+
if (project.errors.length > 0) {
|
|
20
|
+
if (flags.json) {
|
|
21
|
+
console.log(JSON.stringify({ ok: false, errors: project.errors }, null, 2))
|
|
22
|
+
} else {
|
|
23
|
+
printErrors(project.errors)
|
|
24
|
+
}
|
|
25
|
+
process.exitCode = 1
|
|
26
|
+
return
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const entries = comparePlan(buildPlan(project.domains, root, project.dirs), root, manualPaths(root))
|
|
30
|
+
|
|
31
|
+
if (flags.json) {
|
|
32
|
+
console.log(
|
|
33
|
+
JSON.stringify(
|
|
34
|
+
{
|
|
35
|
+
ok: true,
|
|
36
|
+
files: entries.map((entry) => ({ path: entry.path, status: entry.status }))
|
|
37
|
+
},
|
|
38
|
+
null,
|
|
39
|
+
2
|
|
40
|
+
)
|
|
41
|
+
)
|
|
42
|
+
return
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
printDiff(entries)
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function printDiffHelp() {
|
|
49
|
+
console.log('')
|
|
50
|
+
console.log(` ${ui.heading('文件预览', 'diff · preview files')}`)
|
|
51
|
+
console.log(` ${ui.rule(46)}`)
|
|
52
|
+
console.log('')
|
|
53
|
+
console.log(` ${ui.label('用法', 'Usage')}`)
|
|
54
|
+
console.log(` ${ui.command('tha diff')} [--json]`)
|
|
55
|
+
console.log('')
|
|
56
|
+
console.log(` ${ui.label('状态', 'Status')}`)
|
|
57
|
+
console.log(` ${ui.success(glyph.add)} ${ui.label('将新增', 'Will add')}`)
|
|
58
|
+
console.log(
|
|
59
|
+
` ${ui.warning(glyph.update)} ${ui.label('将更新', 'Will update')} ${ui.muted('exists and will be updated (文件已存在,内容会更新)')}`
|
|
60
|
+
)
|
|
61
|
+
console.log(
|
|
62
|
+
` ${ui.danger(glyph.warn)} ${ui.label('不会覆盖', 'Will not overwrite')} ${ui.muted('not generated by CLI (文件已存在,但不是 CLI 生成的)')}`
|
|
63
|
+
)
|
|
64
|
+
console.log(` ${ui.muted(glyph.same)} ${ui.label('无变化', 'No change')}`)
|
|
65
|
+
console.log('')
|
|
66
|
+
console.log(` ${ui.label('说明', 'Notes')}`)
|
|
67
|
+
console.log(` ${ui.bullet('preview only (只显示结果,不写文件)')}`)
|
|
68
|
+
console.log(` ${ui.bullet('disable color (关闭颜色)')}`)
|
|
69
|
+
console.log('')
|
|
70
|
+
}
|