repo-audit-tool 1.2.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/AGENTS.md +67 -0
- package/AUDIT.md +802 -0
- package/BEST-PRACTICES.md +268 -0
- package/CLAUDE.md +1 -0
- package/CONTRIBUTING.md +42 -0
- package/DEVELOPMENT.md +112 -0
- package/HANDOFF.md +124 -0
- package/LICENSE +21 -0
- package/PUBLISHING.md +66 -0
- package/README.en.md +71 -0
- package/README.md +71 -0
- package/REPO-AUDIT.md +107 -0
- package/REPO-CLASSIFICATION.md +137 -0
- package/SECURITY.md +19 -0
- package/docs/repo-audit/AGENT-GUIDE.md +474 -0
- package/docs/repo-audit/HUMAN-GUIDE.md +749 -0
- package/package.json +36 -0
- package/repo-audit.mjs +1507 -0
- package/rules/categories/archive.yaml +48 -0
- package/rules/categories/content.yaml +34 -0
- package/rules/categories/dsh-plugin.yaml +79 -0
- package/rules/categories/go-service.yaml +67 -0
- package/rules/categories/product-oss.yaml +75 -0
- package/rules/categories/python-app.yaml +60 -0
- package/rules/categories/sandbox.yaml +33 -0
- package/rules/domains/docs.yaml +105 -0
- package/rules/domains/git.yaml +52 -0
- package/rules/domains/quality.yaml +69 -0
- package/rules/domains/security.yaml +79 -0
- package/shim/repo-audit.cmd +7 -0
- package/shim/repo-audit.ps1 +4 -0
- package/shim/repo-audit.sh +5 -0
- package/templates/AGENT-GUIDE.md +331 -0
- package/templates/README.md +192 -0
- package/templates/UPDATE-MODEL.md +141 -0
- package/templates/UPDATE-PLAN.md +79 -0
- package/templates/categories/content/AGENTS-append.md +6 -0
- package/templates/categories/content/CATEGORY.md +26 -0
- package/templates/categories/content/README.en.md +12 -0
- package/templates/categories/content/README.md +14 -0
- package/templates/categories/content/gitignore-append.md +6 -0
- package/templates/categories/dsh-plugin/AGENTS-append.md +21 -0
- package/templates/categories/dsh-plugin/CATEGORY.md +9 -0
- package/templates/categories/dsh-plugin/CONTRIBUTING-append.md +22 -0
- package/templates/categories/dsh-plugin/DEVELOPMENT-append.md +71 -0
- package/templates/categories/dsh-plugin/PUBLISHING-append.md +53 -0
- package/templates/categories/dsh-plugin/README-append.md +28 -0
- package/templates/categories/dsh-plugin/README.en.md +44 -0
- package/templates/categories/dsh-plugin/gitignore-append.md +15 -0
- package/templates/categories/go-service/.githooks/pre-commit +13 -0
- package/templates/categories/go-service/.github/workflows/ci.yml +31 -0
- package/templates/categories/go-service/.github/workflows/release.yml +58 -0
- package/templates/categories/go-service/.goreleaser.yaml +86 -0
- package/templates/categories/go-service/AGENTS-append.md +7 -0
- package/templates/categories/go-service/CATEGORY.md +33 -0
- package/templates/categories/go-service/CONTRIBUTING-append.md +13 -0
- package/templates/categories/go-service/DEVELOPMENT-append.md +21 -0
- package/templates/categories/go-service/Makefile +21 -0
- package/templates/categories/go-service/PUBLISHING-append.md +48 -0
- package/templates/categories/go-service/PUBLISHING.md +59 -0
- package/templates/categories/go-service/README-append.md +26 -0
- package/templates/categories/go-service/README.en.md +43 -0
- package/templates/categories/go-service/gitignore-append.md +13 -0
- package/templates/categories/product-oss/.github/CODEOWNERS +6 -0
- package/templates/categories/product-oss/.github/ISSUE_TEMPLATE/bug_report.yml +52 -0
- package/templates/categories/product-oss/.github/ISSUE_TEMPLATE/config.yml +8 -0
- package/templates/categories/product-oss/.github/dependabot.yml +28 -0
- package/templates/categories/product-oss/.github/workflows/dependency-review.yml +18 -0
- package/templates/categories/product-oss/.github/workflows/release-please.yml +39 -0
- package/templates/categories/product-oss/.release-please-manifest.json +3 -0
- package/templates/categories/product-oss/CATEGORY.md +27 -0
- package/templates/categories/product-oss/CHANGELOG.md +14 -0
- package/templates/categories/product-oss/PULL_REQUEST_TEMPLATE.md +15 -0
- package/templates/categories/product-oss/SUPPORT.md +71 -0
- package/templates/categories/product-oss/release-please-config.json +10 -0
- package/templates/categories/python-app/.githooks/pre-commit +13 -0
- package/templates/categories/python-app/.github/workflows/ci.yml +33 -0
- package/templates/categories/python-app/.github/workflows/publish.yml +112 -0
- package/templates/categories/python-app/AGENTS-append.md +18 -0
- package/templates/categories/python-app/CATEGORY.md +34 -0
- package/templates/categories/python-app/CONTRIBUTING-append.md +13 -0
- package/templates/categories/python-app/DEVELOPMENT-append.md +21 -0
- package/templates/categories/python-app/PUBLISHING-append.md +52 -0
- package/templates/categories/python-app/PUBLISHING.md +61 -0
- package/templates/categories/python-app/README-append.md +27 -0
- package/templates/categories/python-app/README.en.md +44 -0
- package/templates/categories/python-app/gitignore-append.md +19 -0
- package/templates/categories/python-app/pyproject.toml +41 -0
- package/templates/categories/python-app/scripts/verify.py +39 -0
- package/templates/categories/sandbox/AGENTS-append.md +7 -0
- package/templates/categories/sandbox/CATEGORY.md +31 -0
- package/templates/categories/sandbox/README.en.md +24 -0
- package/templates/categories/sandbox/README.md +23 -0
- package/templates/check-single-source.mjs +107 -0
- package/templates/common/AGENTS-core.md +28 -0
- package/templates/common/CONTRIBUTING-core.md +21 -0
- package/templates/common/DEVELOPMENT-core.md +45 -0
- package/templates/common/PUBLISHING-core.md +31 -0
- package/templates/common/README-core.md +60 -0
- package/templates/common/SECURITY-core.md +19 -0
- package/templates/common/copilot-instructions.md +5 -0
- package/templates/common/gitignore-core.md +11 -0
- package/templates/repo-root/.githooks/pre-commit +20 -0
- package/templates/repo-root/.github/workflows/ci.yml +45 -0
- package/templates/repo-root/.github/workflows/publish.yml +77 -0
- package/templates/repo-root/CLAUDE.md +3 -0
- package/templates/repo-root/LICENSE +21 -0
- package/templates/repo-root/package.json +71 -0
- package/templates/repo-root/scripts/build.mjs +24 -0
- package/templates/repo-root/scripts/check-deploy.mjs +176 -0
- package/templates/repo-root/scripts/verify.mjs +52 -0
- package/templates/repo-root/src/config.ts +25 -0
- package/templates/repo-root/src/index.ts +40 -0
- package/templates/repo-root/tsconfig.build.json +22 -0
- package/templates/repo-root/tsconfig.json +23 -0
- package/templates/scaffold.mjs +793 -0
- package/templates/skill/SKILL.md +47 -0
|
@@ -0,0 +1,793 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* 个人仓库脚手架(五分类 + overlay)与模板更新器。
|
|
4
|
+
*
|
|
5
|
+
* 三种模式:
|
|
6
|
+
* 1) 生成(默认) node scaffold.mjs --type <t> --name <n> --org <o> --desc "..." [--target ...] [--force]
|
|
7
|
+
* 2) 更新(无锁采用) node scaffold.mjs --update <已有仓库路径> [--type ...] [--name/--org/--desc/--surface/--legal]
|
|
8
|
+
* [--dry-run] [--skip <path>](可多次)——生成到 <repo>/.scaffold-update/ 并对比分类落位
|
|
9
|
+
* 3) 回滚 node scaffold.mjs --rollback <已有仓库路径>——按 .scaffold/last-update.json 执行 git revert
|
|
10
|
+
* node scaffold.mjs --help
|
|
11
|
+
*
|
|
12
|
+
* 更新模式(UPDATE-MODEL.md v2.1):
|
|
13
|
+
* 前置:git 工作区必须 clean(revert 回滚语义前提)
|
|
14
|
+
* 状态机(base=.scaffold/lock/base/<file>,new=模板新输出,ours=仓库当前):
|
|
15
|
+
* ours 缺失→ADDED;ours==base→FAST-FORWARD(仓库未改,直接写入);
|
|
16
|
+
* new==base→UNCHANGED;ours≠base≠new→git merge-file --diff3 --histogram 三方合并
|
|
17
|
+
* → 干净=MERGED-CLEAN;冲突=QUARANTINED(.scaffold/conflicts/*.rej,不阻塞整体)
|
|
18
|
+
* 首次(无 lock)= adopt 模式:建快照 + 缺失直拷 + 已存在文件写 .scaffold-merge/(零覆盖)
|
|
19
|
+
* 记录/回滚:自动 git commit(回滚点);--rollback 按 last-update.json 执行 revert
|
|
20
|
+
*
|
|
21
|
+
* 不做的事(有意):不覆盖任何仓库既有内容(update 零覆盖);不做远端操作;不装依赖。
|
|
22
|
+
*/
|
|
23
|
+
|
|
24
|
+
import { cpSync, readdirSync, readFileSync, writeFileSync, chmodSync, existsSync, rmSync, mkdirSync, statSync } from 'node:fs';
|
|
25
|
+
import { spawnSync } from 'node:child_process';
|
|
26
|
+
import { createHash } from 'node:crypto';
|
|
27
|
+
import { join, resolve, dirname, relative } from 'node:path';
|
|
28
|
+
import { fileURLToPath } from 'node:url';
|
|
29
|
+
|
|
30
|
+
const TEMPLATES = resolve(dirname(fileURLToPath(import.meta.url)));
|
|
31
|
+
const TEMPLATE_ROOT = join(TEMPLATES, 'repo-root');
|
|
32
|
+
const CATEGORIES = join(TEMPLATES, 'categories');
|
|
33
|
+
const COMMON = join(TEMPLATES, 'common');
|
|
34
|
+
|
|
35
|
+
const CATEGORY_TYPES = ['dsh-plugin', 'python-app', 'go-service', 'sandbox', 'content'];
|
|
36
|
+
const OVERLAYS = ['product-oss'];
|
|
37
|
+
const UPDATE_IGNORE_LINES = ['.scaffold-update/', '.scaffold-merge/', '.scaffold/conflicts/'];
|
|
38
|
+
|
|
39
|
+
// ---------- 参数解析 ----------
|
|
40
|
+
// M-2/N-1 修复:--flag=value 归一为两元素;值位状态机保证旗标值(含 --type=xxx 形态)不被误拆
|
|
41
|
+
const KNOWN_FLAGS = new Set(['--type', '--name', '--org', '--desc', '--surface', '--legal', '--target', '--force', '--overlay', '--update', '--dry-run', '--skip', '--rollback', '--help', '-h']);
|
|
42
|
+
const VALUE_FLAGS = new Set(['--type', '--name', '--org', '--desc', '--surface', '--legal', '--target', '--overlay', '--update', '--skip']);
|
|
43
|
+
|
|
44
|
+
let argv = [];
|
|
45
|
+
let expectValue = false;
|
|
46
|
+
const orphans = [];
|
|
47
|
+
for (const a of process.argv.slice(2)) {
|
|
48
|
+
if (expectValue) { argv.push(a); expectValue = false; continue; }
|
|
49
|
+
const m = a.match(/^(--[a-z]+)=(.*)$/);
|
|
50
|
+
if (m && KNOWN_FLAGS.has(m[1])) { argv.push(m[1], m[2]); continue; }
|
|
51
|
+
if (a.startsWith('--') && KNOWN_FLAGS.has(a) && !['--help', '-h', '--force', '--dry-run', '--rollback'].includes(a)) {
|
|
52
|
+
argv.push(a); expectValue = true; continue;
|
|
53
|
+
}
|
|
54
|
+
if (a.startsWith('--') && !KNOWN_FLAGS.has(a.split('=')[0])) {
|
|
55
|
+
orphans.push(a); // 未知旗标:原样保留并告警
|
|
56
|
+
argv.push(a);
|
|
57
|
+
expectValue = false;
|
|
58
|
+
continue;
|
|
59
|
+
}
|
|
60
|
+
if (a.startsWith('--')) { argv.push(a); expectValue = true; continue; } // 未知旗标(可能带值)
|
|
61
|
+
if (expectValue === false && argv.length && KNOWN_FLAGS.has(argv[argv.length - 1]) === false) {
|
|
62
|
+
orphans.push(a); // 裸位置参数(无旗标引导)→ 孤儿告警
|
|
63
|
+
}
|
|
64
|
+
argv.push(a);
|
|
65
|
+
}
|
|
66
|
+
for (const o of orphans) {
|
|
67
|
+
console.error(`⚠ 孤儿参数 "${o}"(未匹配任何旗标,多为 shell 引号缺失导致值被拆分)`);
|
|
68
|
+
}
|
|
69
|
+
function getFlag(name) {
|
|
70
|
+
const i = argv.indexOf(name);
|
|
71
|
+
return i >= 0 ? argv[i + 1] : undefined;
|
|
72
|
+
}
|
|
73
|
+
function hasFlag(name) { return argv.includes(name); }
|
|
74
|
+
function getAllFlag(name) {
|
|
75
|
+
const out = [];
|
|
76
|
+
for (let i = 0; i < argv.length; i++) if (argv[i] === name) out.push(argv[i + 1]);
|
|
77
|
+
return out;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
function usage() {
|
|
81
|
+
console.log(`用法:
|
|
82
|
+
生成:node scaffold.mjs [--type <分类>] [--overlay product-oss] --name <name> --org <org> --desc "<一句话>"
|
|
83
|
+
[--surface "<宿主能力面>"] [--legal "<版权主体>"] [--target <目录>] [--force]
|
|
84
|
+
更新:node scaffold.mjs --update <已有仓库路径> [--type <分类>] [--name/--org/--desc 自动探测,可显式覆盖]
|
|
85
|
+
[--dry-run] [--skip <path>](可多次)
|
|
86
|
+
回滚:node scaffold.mjs --rollback <已有仓库路径>
|
|
87
|
+
帮助:node scaffold.mjs --help
|
|
88
|
+
|
|
89
|
+
分类 --type(默认 dsh-plugin):${CATEGORY_TYPES.join(' | ')}
|
|
90
|
+
B 类(product-oss)= 语言基类 + --overlay product-oss
|
|
91
|
+
状态层:.scaffold/lock/(base 快照 + manifest)入库——勿手改
|
|
92
|
+
|
|
93
|
+
更新模式零覆盖保证:只写 <repo>/.scaffold-update/、.scaffold-merge/、.scaffold/conflicts/
|
|
94
|
+
与(验证链全绿后的)一次 git commit;仓库既有文件一个字节都不动。`);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
if (hasFlag('--help') || hasFlag('-h')) { usage(); process.exit(0); }
|
|
98
|
+
|
|
99
|
+
// ---------- 通用工具 ----------
|
|
100
|
+
function sha256(buf) { return createHash('sha256').update(buf).digest('hex'); }
|
|
101
|
+
function hashTree(dir, base = dir, acc = new Map()) {
|
|
102
|
+
for (const e of readdirSync(dir, { withFileTypes: true })) {
|
|
103
|
+
const full = join(dir, e.name);
|
|
104
|
+
// 跳过 symlink(EISDIR 防护:.venv/bin/python 等指向目录的 symlink 会让 readFileSync 崩溃)
|
|
105
|
+
if (e.isSymbolicLink()) continue;
|
|
106
|
+
if (e.isDirectory()) hashTree(full, base, acc);
|
|
107
|
+
else acc.set(full.slice(base.length + 1), sha256(readFileSync(full)));
|
|
108
|
+
}
|
|
109
|
+
return acc;
|
|
110
|
+
}
|
|
111
|
+
function run(cmd, args, cwd, opts = {}) {
|
|
112
|
+
const r = spawnSync(cmd, args, { cwd, encoding: 'utf8', ...opts });
|
|
113
|
+
return { code: r.status ?? 1, stdout: r.stdout || '', stderr: r.stderr || '' };
|
|
114
|
+
}
|
|
115
|
+
function isClean(repoPath) {
|
|
116
|
+
return run('git', ['status', '--porcelain'], repoPath).stdout.trim() === '';
|
|
117
|
+
}
|
|
118
|
+
function ensureGitignore(repoPath, lines) {
|
|
119
|
+
const gi = join(repoPath, '.gitignore');
|
|
120
|
+
let text = existsSync(gi) ? readFileSync(gi, 'utf8') : '';
|
|
121
|
+
let added = false;
|
|
122
|
+
for (const l of lines) {
|
|
123
|
+
if (!text.split('\n').some((x) => x.trim() === l)) { text = text.trimEnd() + '\n' + l + '\n'; added = true; }
|
|
124
|
+
}
|
|
125
|
+
if (added) writeFileSync(gi, text);
|
|
126
|
+
return added;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
// ---------- 模板拼装(生成与更新共用的纯函数层) ----------
|
|
130
|
+
const NOTE_HTML_RE = /<!--\s*模板说明[\s\S]*?-->\n?/g;
|
|
131
|
+
const NOTE_BLOCK_RE = /\n?[ \t]*\/\*+\s*\n?[ \t]*\*\s*模板说明[\s\S]*?\*\/\n?/g;
|
|
132
|
+
const NOTE_LINE_RE = /\n?[ \t]*\/\/\s*模板说明[^\n]*/g;
|
|
133
|
+
const PLACEHOLDER_RE = /(?<!--)<[^<>\n]{1,80}>/g;
|
|
134
|
+
const KEEP_PLACEHOLDERS = new Set(['<user>', '<盘>']);
|
|
135
|
+
|
|
136
|
+
/** 在 stage 目录上执行:模板说明剥离 + 占位符替换;返回剩余占位符 Map */
|
|
137
|
+
function transformTree(dir, substitutions) {
|
|
138
|
+
const remaining = new Map();
|
|
139
|
+
function walk(d) {
|
|
140
|
+
for (const e of readdirSync(d, { withFileTypes: true })) {
|
|
141
|
+
const full = join(d, e.name);
|
|
142
|
+
if (e.isDirectory()) { walk(full); continue; }
|
|
143
|
+
let text = readFileSync(full, 'utf8');
|
|
144
|
+
const before = text;
|
|
145
|
+
text = text.replace(NOTE_HTML_RE, '').replace(NOTE_BLOCK_RE, '').replace(NOTE_LINE_RE, '');
|
|
146
|
+
for (const [from, to] of substitutions) text = text.split(from).join(to);
|
|
147
|
+
if (text !== before) writeFileSync(full, text);
|
|
148
|
+
const scan = text.replace(/<!--[\s\S]*?-->/g, '');
|
|
149
|
+
const rel = relative(dir, full);
|
|
150
|
+
for (const m of scan.matchAll(PLACEHOLDER_RE)) {
|
|
151
|
+
if (KEEP_PLACEHOLDERS.has(m[0])) continue;
|
|
152
|
+
if (!remaining.has(m[0])) remaining.set(m[0], new Set());
|
|
153
|
+
const relFromRoot = relative(dir, full);
|
|
154
|
+
remaining.get(m[0]).add(relFromRoot === '' ? '.' : relFromRoot);
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
walk(dir);
|
|
159
|
+
return remaining;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
/** 出库一个分类的完整产物到空目录 stageDir(不 git init;占位符不替换——由调用方统一替换) */
|
|
163
|
+
function generateInto(stageDir, type, { overlay, params }) {
|
|
164
|
+
cpSync(TEMPLATE_ROOT, stageDir, { recursive: true });
|
|
165
|
+
const catDir = join(CATEGORIES, type);
|
|
166
|
+
const cat = (p) => join(catDir, p);
|
|
167
|
+
const cp = (src, dst) => cpSync(src, join(stageDir, dst), { recursive: true });
|
|
168
|
+
const del = (...paths) => {
|
|
169
|
+
for (const p of paths) {
|
|
170
|
+
const full = join(stageDir, p);
|
|
171
|
+
if (existsSync(full)) rmSync(full, { recursive: true, force: true });
|
|
172
|
+
}
|
|
173
|
+
};
|
|
174
|
+
|
|
175
|
+
// —— 单源拼装:AGENTS/CONTRIBUTING/.gitignore/README/DEVELOPMENT/PUBLISHING/SECURITY ——
|
|
176
|
+
const substitutions = params.substitutions;
|
|
177
|
+
const assemble = (base, dst) => {
|
|
178
|
+
const corePath = join(COMMON, `${base}-core.md`);
|
|
179
|
+
let text = readFileSync(corePath, 'utf8');
|
|
180
|
+
if (base === 'gitignore') text = text.replace(/^# ===== 通用段[^\n]*=====\n/, '');
|
|
181
|
+
const appPath = join(catDir, `${base}-append.md`);
|
|
182
|
+
if (existsSync(appPath)) {
|
|
183
|
+
let appText = readFileSync(appPath, 'utf8')
|
|
184
|
+
.replace(/^<!--\s*=====?\s*append[\s\S]*?=====\s*-->\n?/, '')
|
|
185
|
+
.replace(/\n?<!--\s*=====?\s*append[\s\S]*?=====\s*-->\n?/g, '\n');
|
|
186
|
+
const anchor = '<分类 append 拼装:功能/安装/使用/配置段(Standard Readme 的 Install/Usage/Extra)>';
|
|
187
|
+
if (base === 'README' && text.includes(anchor)) {
|
|
188
|
+
// 徽章前移(Title 后)+ 其余节锚点插入
|
|
189
|
+
const badgeLines = []; const restLines = []; let collecting = true;
|
|
190
|
+
for (const line of appText.split('\n')) {
|
|
191
|
+
if (collecting) {
|
|
192
|
+
if (line.trim() === '') { restLines.push(line); continue; }
|
|
193
|
+
collecting = false;
|
|
194
|
+
if (/^\[!\[/.test(line.trim())) { badgeLines.push(line.trim()); continue; }
|
|
195
|
+
}
|
|
196
|
+
restLines.push(line);
|
|
197
|
+
}
|
|
198
|
+
const rest = restLines.join('\n').replace(/^\n+/, '').trim();
|
|
199
|
+
if (badgeLines.length) {
|
|
200
|
+
text = text.replace('<!-- 分类徽章(npm/PyPI 等)由 append 提供并插到本行之后 -->', badgeLines.join('\n'));
|
|
201
|
+
} else {
|
|
202
|
+
text = text.replace('<!-- 分类徽章(npm/PyPI 等)由 append 提供并插到本行之后 -->\n', '')
|
|
203
|
+
.replace('<!-- 分类徽章(npm/PyPI 等)由 append 提供并插到本行之后 -->', '');
|
|
204
|
+
}
|
|
205
|
+
text = text.replace(anchor, rest);
|
|
206
|
+
// —— 分类差异(工程件与删清单) ——
|
|
207
|
+
} else {
|
|
208
|
+
text = `${text.trimEnd()}\n\n${appText.trim()}\n`;
|
|
209
|
+
}
|
|
210
|
+
// 分类差异(工程件与删清单)
|
|
211
|
+
writeFileSync(join(stageDir, dst), text);
|
|
212
|
+
} else {
|
|
213
|
+
cp(corePath, dst);
|
|
214
|
+
}
|
|
215
|
+
};
|
|
216
|
+
assemble('AGENTS', 'AGENTS.md');
|
|
217
|
+
assemble('CONTRIBUTING', 'CONTRIBUTING.md');
|
|
218
|
+
// gitignore:core(去头行)+ append
|
|
219
|
+
{
|
|
220
|
+
let gi = readFileSync(join(COMMON, 'gitignore-core.md'), 'utf8').replace(/^# ===== 通用段[^\n]*=====\n/, '');
|
|
221
|
+
const app = join(catDir, 'gitignore-append.md');
|
|
222
|
+
if (existsSync(app)) gi += '\n' + readFileSync(app, 'utf8').replace(/^# ===== append[^\n]*\n/, '');
|
|
223
|
+
writeFileSync(join(stageDir, '.gitignore'), gi);
|
|
224
|
+
}
|
|
225
|
+
assemble('README', 'README.md');
|
|
226
|
+
assemble('DEVELOPMENT', 'DEVELOPMENT.md');
|
|
227
|
+
assemble('PUBLISHING', 'PUBLISHING.md');
|
|
228
|
+
cp(join(COMMON, 'SECURITY-core.md'), 'SECURITY.md');
|
|
229
|
+
if (existsSync(cat('README.en.md'))) cp(cat('README.en.md'), 'README.en.md');
|
|
230
|
+
if (type !== 'sandbox' && type !== 'content') cp(join(COMMON, 'copilot-instructions.md'), '.github/copilot-instructions.md');
|
|
231
|
+
|
|
232
|
+
// —— 分类差异 ——
|
|
233
|
+
if (type === 'python-app') {
|
|
234
|
+
del('package.json', 'tsconfig.json', 'tsconfig.build.json', 'src/index.ts', 'src/config.ts',
|
|
235
|
+
'scripts/build.mjs', 'scripts/check-deploy.mjs', 'scripts/verify.mjs', 'cordis.patch.yml');
|
|
236
|
+
cp(cat('pyproject.toml'), 'pyproject.toml');
|
|
237
|
+
cp(cat('scripts/verify.py'), 'scripts/verify.py');
|
|
238
|
+
cp(cat('.github/workflows/ci.yml'), '.github/workflows/ci.yml');
|
|
239
|
+
cp(cat('.github/workflows/publish.yml'), '.github/workflows/publish.yml');
|
|
240
|
+
cp(cat('.githooks/pre-commit'), '.githooks/pre-commit');
|
|
241
|
+
} else if (type === 'go-service') {
|
|
242
|
+
del('package.json', 'tsconfig.json', 'tsconfig.build.json', 'src/index.ts', 'src/config.ts',
|
|
243
|
+
'scripts/build.mjs', 'scripts/check-deploy.mjs', 'scripts/verify.mjs',
|
|
244
|
+
'.github/workflows/publish.yml', 'cordis.patch.yml');
|
|
245
|
+
for (const d of ['src', 'scripts']) {
|
|
246
|
+
const full = join(stageDir, d);
|
|
247
|
+
if (existsSync(full) && readdirSync(full).length === 0) rmSync(full, { recursive: true, force: true });
|
|
248
|
+
}
|
|
249
|
+
cp(cat('Makefile'), 'Makefile');
|
|
250
|
+
cp(cat('.goreleaser.yaml'), '.goreleaser.yaml');
|
|
251
|
+
cp(cat('.github/workflows/ci.yml'), '.github/workflows/ci.yml');
|
|
252
|
+
cp(cat('.github/workflows/release.yml'), '.github/workflows/release.yml');
|
|
253
|
+
cp(cat('.githooks/pre-commit'), '.githooks/pre-commit');
|
|
254
|
+
} else if (type === 'sandbox') {
|
|
255
|
+
del('LICENSE', 'PUBLISHING.md', 'DEVELOPMENT.md', 'SECURITY.md',
|
|
256
|
+
'.github', '.githooks', 'scripts', 'src', 'CLAUDE.md',
|
|
257
|
+
'package.json', 'tsconfig.json', 'tsconfig.build.json', 'CONTRIBUTING.md');
|
|
258
|
+
cp(cat('README.md'), 'README.md');
|
|
259
|
+
} else if (type === 'content') {
|
|
260
|
+
del('PUBLISHING.md', 'DEVELOPMENT.md', 'SECURITY.md',
|
|
261
|
+
'.github', '.githooks', 'scripts', 'src', 'package.json', 'CLAUDE.md',
|
|
262
|
+
'tsconfig.json', 'tsconfig.build.json', 'CONTRIBUTING.md');
|
|
263
|
+
cp(cat('README.md'), 'README.md');
|
|
264
|
+
}
|
|
265
|
+
// dsh-plugin:基类本体
|
|
266
|
+
|
|
267
|
+
// —— overlay ——
|
|
268
|
+
if (overlay) {
|
|
269
|
+
const oDir = join(CATEGORIES, overlay);
|
|
270
|
+
if (!existsSync(oDir)) throw new Error(`overlay 缺失:${oDir}`);
|
|
271
|
+
cpSync(oDir, stageDir, { recursive: true });
|
|
272
|
+
for (const doc of ['CATEGORY.md', 'SUPPORT.md']) {
|
|
273
|
+
const d = join(stageDir, doc);
|
|
274
|
+
if (existsSync(d)) rmSync(d);
|
|
275
|
+
}
|
|
276
|
+
const prtSrc = join(stageDir, 'PULL_REQUEST_TEMPLATE.md');
|
|
277
|
+
if (existsSync(prtSrc)) {
|
|
278
|
+
mkdirSync(join(stageDir, '.github'), { recursive: true });
|
|
279
|
+
rmSync(prtSrc);
|
|
280
|
+
cpSync(join(oDir, 'PULL_REQUEST_TEMPLATE.md'), join(stageDir, '.github', 'PULL_REQUEST_TEMPLATE.md'));
|
|
281
|
+
}
|
|
282
|
+
const oGitApp = join(oDir, 'gitignore-append.md');
|
|
283
|
+
if (existsSync(oGitApp)) {
|
|
284
|
+
const gi = join(stageDir, '.gitignore');
|
|
285
|
+
writeFileSync(gi, readFileSync(gi, 'utf8').trimEnd() + '\n\n' +
|
|
286
|
+
readFileSync(oGitApp, 'utf8').replace(/^# ===== append[^\n]*\n/, '').trim() + '\n');
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
// 可执行位
|
|
291
|
+
for (const rel of ['.githooks/pre-commit', 'scripts/check-deploy.mjs', 'scripts/verify.mjs', 'scripts/build.mjs', 'scripts/verify.py']) {
|
|
292
|
+
const dst = join(stageDir, rel);
|
|
293
|
+
if (existsSync(dst)) chmodSync(dst, 0o755);
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
// 占位符残留扫描:模板特定占位符(非用户可填)未被替换时警告
|
|
297
|
+
const TEMPLATE_PLACEHOLDERS = [
|
|
298
|
+
/<commit-SHA>/g,
|
|
299
|
+
/dsh-demo/g,
|
|
300
|
+
/demo-v\*/g,
|
|
301
|
+
/<一句话描述>/g,
|
|
302
|
+
/<一句话主旨>/g,
|
|
303
|
+
/<版本>/g,
|
|
304
|
+
]
|
|
305
|
+
const scanDir = (dir) => {
|
|
306
|
+
for (const e of readdirSync(dir, { withFileTypes: true })) {
|
|
307
|
+
if (e.isSymbolicLink()) continue
|
|
308
|
+
const full = join(dir, e.name)
|
|
309
|
+
if (e.isDirectory()) { scanDir(full); continue }
|
|
310
|
+
if (!/\.(md|yml|yaml|json|sh|cmd|ps1|toml)$/.test(e.name)) continue
|
|
311
|
+
let text
|
|
312
|
+
try { text = readFileSync(full, 'utf8') } catch { continue }
|
|
313
|
+
const clean = text.replace(/<!--[\s\S]*?-->/g, '')
|
|
314
|
+
for (const pat of TEMPLATE_PLACEHOLDERS) {
|
|
315
|
+
pat.lastIndex = 0
|
|
316
|
+
if (pat.test(clean)) {
|
|
317
|
+
const rel = relative(stageDir, full)
|
|
318
|
+
console.error(`⚠ 模板占位符残留:${rel} 含 ${pat.source} — 请手动替换或适配为 ${type} 分类`)
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
scanDir(stageDir)
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
/** 生成模式的 TODO 清单 */
|
|
327
|
+
function todoFor(type, { org, repo, name }) {
|
|
328
|
+
const TODO = {
|
|
329
|
+
'dsh-plugin': ` 1. package.json:按契约预检结果填 peer/devDependencies 的宿主包(宽 caret + 预发布下界;
|
|
330
|
+
peer 同范围复制进 devDeps + 运行时可达宿主包全量)。config 骨架的编译依赖
|
|
331
|
+
(@deepseek-ai/schemastery、@types/node)已预置;有 client half 时填 dsh.client.inject
|
|
332
|
+
并从 dsh-context-compass/scripts/build-client.mjs 复制适配(build.mjs 探测式,接入即生效)
|
|
333
|
+
2. src/:实现能力(入口骨架与 config 骨架已就位);用到的服务逐一对照 cordis_inspect_query 签名
|
|
334
|
+
3. 验证链:scripts/verify.mjs 探测式——接入 smoke.mjs / vitest.config / mount.mjs 后自动进链;
|
|
335
|
+
链全绿前禁止发布(npm test 即单源入口)
|
|
336
|
+
4. README(.en).md 的使用/配置段、SECURITY.md 支持面、DEVELOPMENT.md 校对命令前缀
|
|
337
|
+
5. npm install --legacy-peer-deps → 生成 package-lock.json【必须入库:CI 的 npm ci 与
|
|
338
|
+
cache-dependency-path 依赖它,首推前本地先生成】→ npm run build → dsh plugin --profile web
|
|
339
|
+
install(file: 接线)→ npm run check:deploy(部署纪律)
|
|
340
|
+
6. .githooks/commit-msg 为本机私有不入库(已 ignore)
|
|
341
|
+
7. GitHub 建库(scaffold 不做远端操作):
|
|
342
|
+
gh repo create ${org}/${repo} --public --source . --description "<一句话>" --push
|
|
343
|
+
然后 gh repo edit ${org}/${repo} --add-topic deepseek-harness --add-topic dsh-plugin
|
|
344
|
+
8. .github/workflows/{ci,publish}.yml 的 <commit-SHA>:从 dsh-context-compass 同名文件抄当前
|
|
345
|
+
锁定值(actions/checkout、actions/setup-node),或查 GitHub 官方 tag 对应 SHA
|
|
346
|
+
9. 首发前置见 PUBLISHING.md「首次发布前置」(Trusted Publisher + bootstrap)`,
|
|
347
|
+
'python-app': ` 1. pyproject.toml:填依赖占位;自建 src/${name}/__init__.py 包目录(pyproject packages 已指向它)
|
|
348
|
+
2. src/ 实现能力;README 安装段/徽章改 pip install
|
|
349
|
+
3. 验证链单源 scripts/verify.py(ruff → pytest 探测式);tests/ 就位后自动进链;
|
|
350
|
+
.githooks/pre-commit 已指向它(git config core.hooksPath .githooks 启用)
|
|
351
|
+
4. 发布链已就位:.github/workflows/publish.yml(tag v* → PyPI Trusted Publishing,attestations 自动);
|
|
352
|
+
发布前一次性配置见 PUBLISHING.md「一次性前置」(GitHub pypi 环境 + pending publisher 登记)
|
|
353
|
+
5. python -m pip install -e ".[dev]" → python scripts/verify.py 全绿
|
|
354
|
+
6. GitHub 建库:gh repo create ${org}/${repo} --public --source . --description "<一句话>" --push
|
|
355
|
+
(topics 建议:python);workflow <commit-SHA> 从参照库抄(checkout/setup-python@v7、
|
|
356
|
+
upload/download-artifact);pypa/gh-action-pypi-publish 用 @release/v1 可不 pin
|
|
357
|
+
7. 版本工具:小库推荐 bump-my-version(自动 commit+tag);不需要发布则删 publish.yml`,
|
|
358
|
+
'go-service': ` 1. go mod init github.com/${org}/${repo};自建 cmd/${name}/main.go 入口(声明 var version = "dev"——
|
|
359
|
+
GoReleaser 默认 ldflags 注入 main.version/main.commit/main.date)
|
|
360
|
+
2. Makefile verify(gofmt → vet → test → build)为验证链单源;.githooks/pre-commit 已指向它
|
|
361
|
+
(git config core.hooksPath .githooks 启用)
|
|
362
|
+
3. 发布链已就位:.goreleaser.yaml(v2 最小配置)+ .github/workflows/release.yml(v* tag 触发);
|
|
363
|
+
发布前本地演练 goreleaser check && goreleaser release --snapshot --clean(见 PUBLISHING.md)
|
|
364
|
+
4. go build ./... 全绿;go.sum 随 go mod tidy 生成并入库
|
|
365
|
+
5. GitHub 建库:gh repo create ${org}/${repo} --public --source . --description "<一句话>" --push
|
|
366
|
+
(topics 建议:go);workflow <commit-SHA> 从参照库抄(checkout/setup-go/goreleaser-action@v7)
|
|
367
|
+
6. 可选启用(.goreleaser.yaml + release.yml 成对解开):SBOM(syft)/ cosign keyless / GHCR 镜像
|
|
368
|
+
(dockers_v2);CHANGELOG.md 文件与自动版本号需要时再引入 release-please(分工见 PUBLISHING.md)`,
|
|
369
|
+
'sandbox': ` 1. README「目标/跑法」按实验实际填写;不需要的目录(agents/docs/src/tests)删掉
|
|
370
|
+
2. 开工:master 直提,无 CI 无发布、不打 tag
|
|
371
|
+
3. 收尾:README「结论」节写验证结果;转正走 scaffold.mjs 目标分类全量脚手架迁移`,
|
|
372
|
+
'content': ` 1. README 导航按实际文件调整;主文开写(正文与评审分离:意见进 review.md)
|
|
373
|
+
2. 双语时声明权威语言并同步镜像;无 CI 无发布
|
|
374
|
+
3. 完稿可选:GitHub Release 挂终稿;导出产物按 .gitignore 决定是否入库`,
|
|
375
|
+
};
|
|
376
|
+
return TODO[type];
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
function printScaffoldSummary({ type, repo, name, target, remaining }) {
|
|
380
|
+
console.log(`
|
|
381
|
+
──────────────────────────────────────────────
|
|
382
|
+
脚手架完成:${repo}(${type})→ ${target}
|
|
383
|
+
|
|
384
|
+
剩余占位符(待手工填写):`);
|
|
385
|
+
for (const [ph, files] of remaining)
|
|
386
|
+
console.log(` ${ph.padEnd(30)} ${[...files].join(', ')}`);
|
|
387
|
+
console.log(`
|
|
388
|
+
手工 TODO(分类 ${type} 专属):
|
|
389
|
+
${todoFor(type, { org, repo, name })}
|
|
390
|
+
──────────────────────────────────────────────`);
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
// ---------- 模式判定 ----------
|
|
394
|
+
const mode = hasFlag('--rollback') ? 'rollback' : (getFlag('--update') ? 'update' : 'generate');
|
|
395
|
+
|
|
396
|
+
// ---------- 参数与元数据探测 ----------
|
|
397
|
+
function usageFull() { usage(); }
|
|
398
|
+
|
|
399
|
+
const UPD_FLAG = getFlag('--update');
|
|
400
|
+
const ROLLBACK_FLAG = getFlag('--rollback');
|
|
401
|
+
|
|
402
|
+
// update/rollback 共用:仓库路径校验
|
|
403
|
+
function validateRepo(repoPath) {
|
|
404
|
+
if (!existsSync(repoPath)) { console.error(`✗ 仓库路径不存在:${repoPath}`); process.exit(1); }
|
|
405
|
+
if (!existsSync(join(repoPath, '.git'))) {
|
|
406
|
+
console.error(`✗ 目标不是 git 仓库(回滚依赖 git 历史):${repoPath}`);
|
|
407
|
+
process.exit(1);
|
|
408
|
+
}
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
// ---------- 模式:回滚 ----------
|
|
412
|
+
if (mode === 'rollback') {
|
|
413
|
+
validateRepo(resolve(ROLLBACK_FLAG));
|
|
414
|
+
const repoPath = resolve(ROLLBACK_FLAG);
|
|
415
|
+
const luPath = join(repoPath, '.scaffold', 'last-update.json');
|
|
416
|
+
if (!existsSync(luPath)) { console.error(`✗ 无更新记录(.scaffold/last-update.json 不存在)——无可回滚`); process.exit(1); }
|
|
417
|
+
const lu = JSON.parse(readFileSync(luPath, 'utf8'));
|
|
418
|
+
if (!isClean(repoPath)) { console.error('✗ 工作区不干净——先 commit/stash 再回滚'); process.exit(1); }
|
|
419
|
+
console.log(`回滚 update(reset 到更新前 HEAD: ${lu.preUpdateHEAD})...`);
|
|
420
|
+
const r = run('git', ['reset', '--hard', lu.preUpdateHEAD], repoPath);
|
|
421
|
+
if (r.code !== 0) { console.error(`✗ reset 失败:\n${r.stderr}`); process.exit(1); }
|
|
422
|
+
// revert 撤销了 last-update.json 的入库 → 工作区出现 D 状态 → 自动 commit 清理
|
|
423
|
+
run('git', ['add', '-A'], repoPath);
|
|
424
|
+
const cr = run('git', ['commit', '-m', 'chore(scaffold): clean up after rollback'], repoPath);
|
|
425
|
+
if (cr.code !== 0) { /* 如果没有 staged 变化就不 commit */ }
|
|
426
|
+
console.log('✓ 已回滚(git revert 生成撤销 commit,审计历史保留)');
|
|
427
|
+
process.exit(0);
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
// ---------- 模式:update ----------
|
|
431
|
+
if (mode === 'update') {
|
|
432
|
+
const repoPath = resolve(UPD_FLAG);
|
|
433
|
+
validateRepo(repoPath);
|
|
434
|
+
if (!isClean(repoPath)) {
|
|
435
|
+
console.error('✗ 工作区不干净(git status 非空)——先 commit/stash,保证回滚点唯一');
|
|
436
|
+
process.exit(1);
|
|
437
|
+
}
|
|
438
|
+
// 类型判定:lock.template 优先(防类型漂移),显式 --type 次之,最后 fallback dsh-plugin
|
|
439
|
+
const manifestPathPre = join(repoPath, '.scaffold', 'lock', 'manifest.json');
|
|
440
|
+
let lockType = null;
|
|
441
|
+
if (existsSync(manifestPathPre)) {
|
|
442
|
+
try {
|
|
443
|
+
const raw = readFileSync(manifestPathPre, 'utf8');
|
|
444
|
+
const prev = JSON.parse(raw.slice(raw.indexOf('{')));
|
|
445
|
+
lockType = prev.template ? prev.template.replace('scaffold/', '') : null;
|
|
446
|
+
} catch { /* lock 损坏由后续 compare 降级处理 */ }
|
|
447
|
+
}
|
|
448
|
+
const explicitType = getFlag('--type');
|
|
449
|
+
if (explicitType && lockType && explicitType !== lockType) {
|
|
450
|
+
console.error(`✗ --type ${explicitType} 与 lock.template ${lockType} 不一致——如确认要切换分类,先删除 .scaffold/lock/ 后重跑`);
|
|
451
|
+
process.exit(1);
|
|
452
|
+
}
|
|
453
|
+
const type = explicitType || lockType || 'dsh-plugin';
|
|
454
|
+
if (!CATEGORY_TYPES.includes(type)) { console.error(`✗ 未知分类 --type ${type}`); process.exit(1); }
|
|
455
|
+
const overlayArg = getFlag('--overlay');
|
|
456
|
+
if (overlayArg && !OVERLAYS.includes(overlayArg)) { console.error(`✗ 未知 overlay:${overlayArg}`); process.exit(1); }
|
|
457
|
+
const dryRun = hasFlag('--dry-run');
|
|
458
|
+
const skips = getAllFlag('--skip');
|
|
459
|
+
|
|
460
|
+
// —— 元数据自动探测(显式参数优先)——
|
|
461
|
+
function readJSON(p) { try { return JSON.parse(readFileSync(p, 'utf8')); } catch { return null; } }
|
|
462
|
+
let dName = getFlag('--name'), dDesc = getFlag('--desc'), dOrg = getFlag('--org');
|
|
463
|
+
const pj = readJSON(join(repoPath, 'package.json'));
|
|
464
|
+
try {
|
|
465
|
+
const { tomllib } = await import('node:util');
|
|
466
|
+
} catch {}
|
|
467
|
+
let pyName = null, pyDesc = null;
|
|
468
|
+
const pp = join(repoPath, 'pyproject.toml');
|
|
469
|
+
if (existsSync(pp)) {
|
|
470
|
+
try {
|
|
471
|
+
const t = readFileSync(pp, 'utf8');
|
|
472
|
+
pyName = (t.match(/^name\s*=\s*"([^"]+)"/m) || [])[1] || null;
|
|
473
|
+
pyDesc = (t.match(/^description\s*=\s*"([^"]+)"/m) || [])[1] || null;
|
|
474
|
+
} catch {}
|
|
475
|
+
}
|
|
476
|
+
if (!dName) dName = (pj && pj.name ? pj.name.replace(/^dsh-/, '') : null) || pyName || repoPath.split('/').pop();
|
|
477
|
+
if (!dDesc) dDesc = (pj && pj.description) || pyDesc || '';
|
|
478
|
+
if (!dOrg) {
|
|
479
|
+
const remote = run('git', ['remote', 'get-url', 'origin'], repoPath).stdout.trim();
|
|
480
|
+
const m = remote.match(/github\.com[/:]([^/]+)\//);
|
|
481
|
+
dOrg = m ? m[1] : 'NinjaSln-labs';
|
|
482
|
+
}
|
|
483
|
+
const dSurface = getFlag('--surface') || dDesc;
|
|
484
|
+
const dLegal = getFlag('--legal') || dOrg;
|
|
485
|
+
const repo = type === 'dsh-plugin' ? `dsh-${dName}` : dName;
|
|
486
|
+
console.log(`探测: name=${dName} repo=${repo} org=${dOrg} type=${type}`);
|
|
487
|
+
|
|
488
|
+
// —— 出库到 stage(.scaffold-update/,不 git init)——
|
|
489
|
+
const stage = join(repoPath, '.scaffold-update');
|
|
490
|
+
if (existsSync(stage)) rmSync(stage, { recursive: true, force: true });
|
|
491
|
+
mkdirSync(stage, { recursive: true });
|
|
492
|
+
generateInto(stage, type, {
|
|
493
|
+
overlay: overlayArg,
|
|
494
|
+
params: { substitutions: [
|
|
495
|
+
['<org 法律名>', dLegal], ['<name>', dName], ['<repo>', repo], ['<org>', dOrg],
|
|
496
|
+
['<一句话描述>', dDesc], ['<宿主能力面>', dSurface], ['<year>', String(new Date().getFullYear())],
|
|
497
|
+
] },
|
|
498
|
+
});
|
|
499
|
+
// 占位符替换(stage 全树)
|
|
500
|
+
transformTree(stage, [
|
|
501
|
+
['<org 法律名>', dLegal], ['<name>', dName], ['<repo>', repo], ['<org>', dOrg],
|
|
502
|
+
['<一句话描述>', dDesc], ['<宿主能力面>', dSurface], ['<year>', String(new Date().getFullYear())],
|
|
503
|
+
]);
|
|
504
|
+
|
|
505
|
+
// —— lock ——
|
|
506
|
+
const lockDir = join(repoPath, '.scaffold', 'lock');
|
|
507
|
+
const manifestPath = join(repoPath, '.scaffold', 'lock', 'manifest.json');
|
|
508
|
+
const lock = existsSync(manifestPath)
|
|
509
|
+
? (() => { try { return JSON.parse(readFileSync(manifestPath, 'utf8').replace(/^[^\n]*NEVER EDIT MANUALLY[^\n]*\n/, '')); } catch { console.error('△ lock 损坏——降级为 adopt 模式(重建 lock)'); return null; } })()
|
|
510
|
+
: { template: 'scaffold/' + type, adopt: true, files: {} };
|
|
511
|
+
const baseContent = (rel) => {
|
|
512
|
+
const p = join(lockDir, 'base', rel);
|
|
513
|
+
return existsSync(p) ? readFileSync(p, 'utf8') : null;
|
|
514
|
+
};
|
|
515
|
+
|
|
516
|
+
// —— 状态机对比 ——
|
|
517
|
+
const stageFiles = hashTree(stage);
|
|
518
|
+
const results = [];
|
|
519
|
+
const writes = []; // {rel, content} 待写入仓库
|
|
520
|
+
const conflicts = []; // {rel, merged}
|
|
521
|
+
for (const [rel, stageHash] of [...stageFiles.entries()].sort()) {
|
|
522
|
+
if (skips.some((s) => rel === s || rel.startsWith(s + '/'))) { results.push(['SKIP', rel, '用户接管']); continue; }
|
|
523
|
+
const repoFile = join(repoPath, rel);
|
|
524
|
+
const ours = existsSync(repoFile) ? sha256(readFileSync(repoFile)) : null;
|
|
525
|
+
const baseHash = lock.files[rel] || null;
|
|
526
|
+
const base = baseContent(rel);
|
|
527
|
+
const newContent = readFileSync(join(stage, rel), 'utf8');
|
|
528
|
+
const newHash = stageHash;
|
|
529
|
+
|
|
530
|
+
if (ours === null) { writes.push({ rel, content: newContent }); results.push(['ADDED', rel, '仓库缺失→直拷']); continue; }
|
|
531
|
+
if (ours === newHash) { results.push(['UNCHANGED', rel, '与模板一致']); continue; }
|
|
532
|
+
if (baseHash !== null && ours === baseHash) { writes.push({ rel, content: newContent }); results.push(['FAST-FORWARD', rel, '仓库未改→采纳模板']); continue; }
|
|
533
|
+
if (baseHash !== null && newHash === baseHash) { results.push(['UNCHANGED', rel, '模板未变,保持仓库版']); continue; }
|
|
534
|
+
// ours ≠ base ≠ new → 三方合并
|
|
535
|
+
if (base === null) { // adopt 后无 base 且内容不同:保守——写 merge 区
|
|
536
|
+
conflicts.push({ rel, reason: 'adopt 首见差异' });
|
|
537
|
+
if (!dryRun) {
|
|
538
|
+
mkdirSync(join(repoPath, '.scaffold-merge', dirname(rel) || '.'), { recursive: true });
|
|
539
|
+
writeFileSync(join(repoPath, '.scaffold-merge', rel), newContent);
|
|
540
|
+
}
|
|
541
|
+
results.push(['MERGE-REVIEW', rel, 'adopt 首见差异→.scaffold-merge/']);
|
|
542
|
+
continue;
|
|
543
|
+
}
|
|
544
|
+
const bTmp = join(repoPath, '.scaffold', 'base.tmp');
|
|
545
|
+
mkdirSync(dirname(bTmp), { recursive: true });
|
|
546
|
+
writeFileSync(bTmp, base);
|
|
547
|
+
const oTmp = join(repoPath, '.scaffold', 'ours.tmp');
|
|
548
|
+
writeFileSync(oTmp, readFileSync(repoFile));
|
|
549
|
+
// 语法:git merge-file [选项] <current> <base> <other>——-L/-p/--diff3 必须在文件名之前
|
|
550
|
+
// 退出码:0=干净;1..127=冲突数;负/127+=调用错误(降级 merge-review)
|
|
551
|
+
const mf = run('git', ['merge-file', '-p', '--zdiff3',
|
|
552
|
+
'-L', 'current', '-L', 'base', '-L', 'template',
|
|
553
|
+
oTmp, bTmp, join(stage, rel)], repoPath);
|
|
554
|
+
rmSync(bTmp, { force: true }); rmSync(oTmp, { force: true });
|
|
555
|
+
if (mf.code === 0) {
|
|
556
|
+
writes.push({ rel, content: mf.stdout });
|
|
557
|
+
results.push(['MERGED-CLEAN', rel, '三方合并干净']);
|
|
558
|
+
} else if (mf.code > 0 && mf.code <= 127) {
|
|
559
|
+
conflicts.push({ rel, reason: `${mf.code} 处冲突` });
|
|
560
|
+
mkdirSync(join(repoPath, '.scaffold', 'conflicts', dirname(rel) || '.'), { recursive: true });
|
|
561
|
+
writeFileSync(join(repoPath, '.scaffold', 'conflicts', rel + '.rej'), mf.stdout);
|
|
562
|
+
results.push(['QUARANTINED', rel, `${mf.code} 处冲突→.scaffold/conflicts/`]);
|
|
563
|
+
} else {
|
|
564
|
+
conflicts.push({ rel, reason: `merge-file 调用失败(exit ${mf.code})` });
|
|
565
|
+
mkdirSync(join(repoPath, '.scaffold-merge', dirname(rel) || '.'), { recursive: true });
|
|
566
|
+
writeFileSync(join(repoPath, '.scaffold-merge', rel), newContent);
|
|
567
|
+
results.push(['MERGE-REVIEW', rel, '三方合并调用失败→.scaffold-merge/(降级人工)']);
|
|
568
|
+
}
|
|
569
|
+
}
|
|
570
|
+
// 仓库有、模板没有的文件(非管辖):仅统计
|
|
571
|
+
const repoFiles = hashTree(repoPath, repoPath, new Map());
|
|
572
|
+
const managed = new Set(stageFiles.keys());
|
|
573
|
+
let unmanaged = 0;
|
|
574
|
+
for (const [rel] of repoFiles) if (!managed.has(rel) && !rel.startsWith('.scaffold/')) unmanaged++;
|
|
575
|
+
|
|
576
|
+
// —— dry-run:到此为止,只打印报告 ——
|
|
577
|
+
console.log(`
|
|
578
|
+
──────────────────────────────────────────────
|
|
579
|
+
更新对比(${type},模板 → ${repoPath}${dryRun ? ',DRY-RUN' : ''})
|
|
580
|
+
`);
|
|
581
|
+
for (const [st, rel, note] of results) console.log(` ${st.padEnd(14)} ${rel}${note ? ' — ' + note : ''}`);
|
|
582
|
+
console.log(` 未管辖文件: ${unmanaged} 个(跳过)`);
|
|
583
|
+
|
|
584
|
+
if (dryRun) {
|
|
585
|
+
// P3-8 修复:dry-run 清掉 stage 残留(.scaffold-update/ 是临时目录,不应留在仓库)
|
|
586
|
+
rmSync(stage, { recursive: true, force: true });
|
|
587
|
+
console.log('\nDRY-RUN:未写入任何文件(临时目录已清理)。');
|
|
588
|
+
process.exit(0);
|
|
589
|
+
}
|
|
590
|
+
|
|
591
|
+
// 无可写入项且无冲突 → 模板与仓库已同步,优雅退出(仍记录状态供 rollback)
|
|
592
|
+
if (!writes.length && !conflicts.length) {
|
|
593
|
+
rmSync(stage, { recursive: true, force: true });
|
|
594
|
+
const syncHEAD = run('git', ['rev-parse', 'HEAD'], repoPath).stdout.trim();
|
|
595
|
+
writeFileSync(join(repoPath, '.scaffold', 'last-update.json'), JSON.stringify({
|
|
596
|
+
preUpdateHEAD: syncHEAD, commitSha: syncHEAD, type, at: new Date().toISOString(),
|
|
597
|
+
files: Object.fromEntries(results.map(([st, rel]) => [rel, st])), noChanges: true,
|
|
598
|
+
}, null, 2) + '\n');
|
|
599
|
+
console.log('\n✓ 模板与仓库已同步(全部 UNCHANGED),无需更新。');
|
|
600
|
+
process.exit(0);
|
|
601
|
+
}
|
|
602
|
+
|
|
603
|
+
// —— 落位写入 ——
|
|
604
|
+
for (const w of writes) {
|
|
605
|
+
mkdirSync(dirname(join(repoPath, w.rel)) || repoPath, { recursive: true });
|
|
606
|
+
writeFileSync(join(repoPath, w.rel), w.content);
|
|
607
|
+
}
|
|
608
|
+
// 冲突标记防漏:已写文件不得含 markers
|
|
609
|
+
const markerHit = writes.filter((w) => /<{7}|>{7}/.test(w.content));
|
|
610
|
+
if (markerHit.length) {
|
|
611
|
+
console.error(`✗ 检测到未解决冲突标记:${markerHit.map((w) => w.rel).join(', ')}——已回退这些文件`);
|
|
612
|
+
for (const w of markerHit) {
|
|
613
|
+
const mine = baseContent(w.rel);
|
|
614
|
+
const rf = join(repoPath, w.rel);
|
|
615
|
+
if (mine !== null) writeFileSync(rf, mine); else rmSync(rf, { force: true });
|
|
616
|
+
}
|
|
617
|
+
process.exit(1);
|
|
618
|
+
}
|
|
619
|
+
// —— 更新 lock(base = 成功落位的内容;未落位的保持不变)——
|
|
620
|
+
const lockBase = join(lockDir, 'base');
|
|
621
|
+
mkdirSync(lockBase, { recursive: true });
|
|
622
|
+
for (const [rel, content] of writes.map((w) => [w.rel, w.content])) {
|
|
623
|
+
mkdirSync(dirname(join(lockBase, rel)) || lockBase, { recursive: true });
|
|
624
|
+
writeFileSync(join(lockBase, rel), content);
|
|
625
|
+
}
|
|
626
|
+
// —— 验证门禁(分类验证链;依赖未装则跳过并提示)——
|
|
627
|
+
const verifyCmds = {
|
|
628
|
+
'dsh-plugin': ['npm', ['run', 'test']], // P0-1 修复:test=verify.mjs(模板已定义),非 verify(不存在)
|
|
629
|
+
'python-app': ['python3', ['scripts/verify.py']],
|
|
630
|
+
'go-service': ['make', ['verify']],
|
|
631
|
+
};
|
|
632
|
+
if (verifyCmds[type]) {
|
|
633
|
+
const [c, a] = verifyCmds[type];
|
|
634
|
+
if (type === 'dsh-plugin' && !existsSync(join(repoPath, 'node_modules'))) {
|
|
635
|
+
console.log('△ node_modules 不存在——跳过验证门禁(先 npm install)');
|
|
636
|
+
} else {
|
|
637
|
+
const vr = run(c, a, repoPath);
|
|
638
|
+
if (vr.code === 2) {
|
|
639
|
+
// SKIP 语义(verify.py 工具未安装):不阻塞 adopt/commit,提示补环境
|
|
640
|
+
console.log('△ 验证链工具未安装——门禁 SKIP(commit 继续;补环境后下次 update 会真跑)');
|
|
641
|
+
} else if (vr.code !== 0) {
|
|
642
|
+
console.error('✗ 验证门禁 FAIL——本次更新已写入但未 commit;修正后重跑 update(会继续合并)');
|
|
643
|
+
process.exit(1);
|
|
644
|
+
} else {
|
|
645
|
+
console.log('✓ 验证门禁 PASS');
|
|
646
|
+
}
|
|
647
|
+
}
|
|
648
|
+
}
|
|
649
|
+
// —— lock/manifest 持久化(P0 修复:manifest 此前从未写盘)——
|
|
650
|
+
const prevManifest = existsSync(manifestPath)
|
|
651
|
+
? (() => { try { return JSON.parse(readFileSync(manifestPath, 'utf8').replace(/^[^\n]*NEVER EDIT MANUALLY[^\n]*\n/, '')); } catch { console.error('△ lock 损坏——降级为 adopt 模式(重建 lock)'); return { files: {} }; } })()
|
|
652
|
+
: { files: {} };
|
|
653
|
+
const newManifest = {
|
|
654
|
+
template: 'scaffold/' + type,
|
|
655
|
+
adopt: false,
|
|
656
|
+
at: new Date().toISOString(),
|
|
657
|
+
files: { ...prevManifest.files }, // 从旧 manifest 起步(防全 UNCHANGED 清空)
|
|
658
|
+
};
|
|
659
|
+
// QUARANTINED:base 追平到 ours(agent 裁决后下次 update 不再冲突)
|
|
660
|
+
for (const c of conflicts.filter((c) => c.reason.includes('处冲突'))) {
|
|
661
|
+
const rf = join(repoPath, c.rel);
|
|
662
|
+
if (existsSync(rf)) {
|
|
663
|
+
newManifest.files[c.rel] = sha256(readFileSync(rf));
|
|
664
|
+
const bp = join(lockDir, 'base', c.rel);
|
|
665
|
+
mkdirSync(dirname(bp) || lockDir, { recursive: true });
|
|
666
|
+
cpSync(rf, bp);
|
|
667
|
+
}
|
|
668
|
+
}
|
|
669
|
+
// 成功落位:base = new
|
|
670
|
+
for (const w of writes) {
|
|
671
|
+
newManifest.files[w.rel] = sha256(Buffer.from(w.content));
|
|
672
|
+
const bp = join(lockDir, 'base', w.rel);
|
|
673
|
+
mkdirSync(dirname(bp) || lockDir, { recursive: true });
|
|
674
|
+
writeFileSync(bp, w.content);
|
|
675
|
+
}
|
|
676
|
+
mkdirSync(lockDir, { recursive: true });
|
|
677
|
+
writeFileSync(manifestPath, '# NEVER EDIT MANUALLY——scaffold 状态层(手改会欺骗三方合并算法)\n' +
|
|
678
|
+
JSON.stringify(newManifest, null, 2) + '\n');
|
|
679
|
+
// —— adopt 时 MERGE-REVIEW 的文件:base = ours(用户尚未采纳)——
|
|
680
|
+
// P1-4 修复:不给 MERGE-REVIEW 建 base 会导致 FF/三方永远不可达
|
|
681
|
+
for (const [st, rel] of results) {
|
|
682
|
+
if (st !== 'MERGE-REVIEW') continue;
|
|
683
|
+
const rf = join(repoPath, rel);
|
|
684
|
+
if (existsSync(rf)) {
|
|
685
|
+
const bp = join(lockDir, 'base', rel);
|
|
686
|
+
mkdirSync(dirname(bp) || lockDir, { recursive: true });
|
|
687
|
+
cpSync(rf, bp);
|
|
688
|
+
}
|
|
689
|
+
}
|
|
690
|
+
// —— 单次 git commit(含全部改动 + 状态层文件,不用 amend——P0-2 修复 SHA dangling)——
|
|
691
|
+
const preHEAD = run('git', ['rev-parse', 'HEAD'], repoPath).stdout.trim();
|
|
692
|
+
run('git', ['add', '-A'], repoPath);
|
|
693
|
+
const cm = run('git', ['commit', '-m', `chore(scaffold): update template files (${type})`], repoPath);
|
|
694
|
+
if (cm.code !== 0) {
|
|
695
|
+
console.error(`✗ git commit 失败(exit ${cm.code})——\n${cm.stderr}`);
|
|
696
|
+
console.error(' → 修正后重跑 update;或手动 git add -A && git commit');
|
|
697
|
+
process.exit(1);
|
|
698
|
+
}
|
|
699
|
+
// —— gitignore 增补(确保 .scaffold-update/.scaffold-merge 在 ignore 里)——
|
|
700
|
+
ensureGitignore(repoPath, UPDATE_IGNORE_LINES);
|
|
701
|
+
// —— last-update.json 写盘(只记 preUpdateHEAD,不含 commitSha——避免 amend 循环)——
|
|
702
|
+
// rollback 用 git reset --hard <preUpdateHEAD>(比 revert 更可靠,不依赖 SHA 在 amend 后不变)
|
|
703
|
+
const luPath = join(repoPath, '.scaffold', 'last-update.json');
|
|
704
|
+
writeFileSync(luPath, JSON.stringify({
|
|
705
|
+
preUpdateHEAD: preHEAD, type, at: new Date().toISOString(),
|
|
706
|
+
files: Object.fromEntries(results.map(([st, rel]) => [rel, st])),
|
|
707
|
+
}, null, 2) + '\n');
|
|
708
|
+
run('git', ['add', '-A'], repoPath);
|
|
709
|
+
run('git', ['commit', '-m', `chore(scaffold): record update metadata (${type})`], repoPath);
|
|
710
|
+
process.exit(0);
|
|
711
|
+
}
|
|
712
|
+
|
|
713
|
+
// ---------- 模式:生成(默认) ----------
|
|
714
|
+
const name0 = getFlag('--name');
|
|
715
|
+
if (!name0) { console.error('✗ 缺少必填参数 --name'); usage(); process.exit(1); }
|
|
716
|
+
let name = name0;
|
|
717
|
+
if (/^dsh-/.test(name)) {
|
|
718
|
+
const stripped = name.replace(/^dsh-/, '');
|
|
719
|
+
console.error(`⚠ --name 带了 dsh- 前缀:已自动剥除(${name} → ${stripped});npm 包名/仓库名将派生为 dsh-${stripped}`);
|
|
720
|
+
name = stripped;
|
|
721
|
+
}
|
|
722
|
+
const pkgName = `dsh-${name}`;
|
|
723
|
+
const org = getFlag('--org') || 'NinjaSln-labs';
|
|
724
|
+
const desc = getFlag('--desc');
|
|
725
|
+
if (!desc) { console.error('✗ 缺少必填参数 --desc'); usage(); process.exit(1); }
|
|
726
|
+
const surface = getFlag('--surface') || desc;
|
|
727
|
+
const legal = getFlag('--legal') || org;
|
|
728
|
+
const target = resolve(getFlag('--target') || `../${pkgName}`);
|
|
729
|
+
|
|
730
|
+
// ---------- 目标目录防护 ----------
|
|
731
|
+
if (existsSync(target)) {
|
|
732
|
+
const existing = readdirSync(target);
|
|
733
|
+
if (existing.length) {
|
|
734
|
+
if (!hasFlag('--force')) {
|
|
735
|
+
console.error(`✗ 目标目录非空:${target}(防误并覆盖;确认空库复用请加 --force,含 .git 时强烈建议换目录)`);
|
|
736
|
+
process.exit(1);
|
|
737
|
+
}
|
|
738
|
+
if (existing.includes('.git')) {
|
|
739
|
+
console.error(`✗ 目标目录含 .git,--force 也会损坏仓库状态——请换目录`);
|
|
740
|
+
process.exit(1);
|
|
741
|
+
}
|
|
742
|
+
}
|
|
743
|
+
}
|
|
744
|
+
|
|
745
|
+
// ---------- 出库 ----------
|
|
746
|
+
const type = getFlag('--type') || 'dsh-plugin';
|
|
747
|
+
if (!CATEGORY_TYPES.includes(type)) {
|
|
748
|
+
console.error(`✗ 未知分类 --type ${type}(可选:${CATEGORY_TYPES.join(' | ')})`);
|
|
749
|
+
process.exit(1);
|
|
750
|
+
}
|
|
751
|
+
const overlayArg = getFlag('--overlay');
|
|
752
|
+
if (overlayArg && !OVERLAYS.includes(overlayArg)) {
|
|
753
|
+
console.error(`✗ 未知 overlay:${overlayArg}(可选:${OVERLAYS.join(' | ')})`);
|
|
754
|
+
process.exit(1);
|
|
755
|
+
}
|
|
756
|
+
mkdirSync(target, { recursive: true });
|
|
757
|
+
generateInto(target, type, { overlay: overlayArg, params: { substitutions: [] } });
|
|
758
|
+
|
|
759
|
+
// 可执行位
|
|
760
|
+
for (const rel of ['.githooks/pre-commit', 'scripts/check-deploy.mjs', 'scripts/verify.mjs', 'scripts/build.mjs', 'scripts/verify.py']) {
|
|
761
|
+
const dst = join(target, rel);
|
|
762
|
+
if (existsSync(dst)) chmodSync(dst, 0o755);
|
|
763
|
+
}
|
|
764
|
+
|
|
765
|
+
// ---------- 占位符替换 + 模板说明剥离 ----------
|
|
766
|
+
const repo = type === 'dsh-plugin' ? pkgName : name;
|
|
767
|
+
const substitutions = [
|
|
768
|
+
['<org 法律名>', legal],
|
|
769
|
+
['<name>', name],
|
|
770
|
+
['<repo>', repo],
|
|
771
|
+
['<org>', org],
|
|
772
|
+
['<一句话描述>', desc],
|
|
773
|
+
['<宿主能力面>', surface],
|
|
774
|
+
['<year>', String(new Date().getFullYear())],
|
|
775
|
+
];
|
|
776
|
+
const remaining = transformTree(target, substitutions);
|
|
777
|
+
console.log('✓ 占位符替换 + 模板说明注释删除');
|
|
778
|
+
|
|
779
|
+
// ---------- git init + hooks ----------
|
|
780
|
+
const git = (args) => spawnSync('git', args, { cwd: target, stdio: 'ignore' });
|
|
781
|
+
const usesHooks = type !== 'sandbox' && type !== 'content';
|
|
782
|
+
if (git(['init']).status !== 0) {
|
|
783
|
+
console.error('✗ git init 失败(git 不可用?)——请手动执行 git init');
|
|
784
|
+
} else if (usesHooks) {
|
|
785
|
+
git(['config', 'core.hooksPath', '.githooks']);
|
|
786
|
+
console.log('✓ git init + core.hooksPath .githooks(pre-commit 验证链前门已启用)');
|
|
787
|
+
} else {
|
|
788
|
+
console.log('✓ git init(沙盒/内容仓:无钩子、无 CI)');
|
|
789
|
+
}
|
|
790
|
+
|
|
791
|
+
// ---------- TODO 清单(分类感知) ----------
|
|
792
|
+
printScaffoldSummary({ type, repo, name, org, target, remaining });
|
|
793
|
+
process.exit(0);
|