@educa-corp/sdd-framework 0.5.0 → 0.6.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/bin/build.js +113 -19
- package/bin/gate-trace.js +464 -0
- package/bin/index.js +418 -146
- package/bin/lint-trace.js +602 -0
- package/bin/self-check.js +376 -1
- package/bin/trace-schema.json +252 -2
- package/commands/debug.md +123 -511
- package/commands/debug.tmpl +3 -0
- package/commands/define-product.md +86 -510
- package/commands/dev-gen-test.md +86 -510
- package/commands/dev-run-test.md +86 -510
- package/commands/dev-smoke-test.md +86 -510
- package/commands/extend-prd.md +89 -510
- package/commands/extend-prd.tmpl +3 -0
- package/commands/fix-bug.md +118 -509
- package/commands/generate-architecture.md +94 -515
- package/commands/generate-architecture.tmpl +3 -0
- package/commands/generate-bdd.md +85 -509
- package/commands/generate-code.md +86 -510
- package/commands/generate-design-spec.md +86 -510
- package/commands/generate-prd.md +89 -510
- package/commands/generate-prd.tmpl +3 -0
- package/commands/generate-spec-manifest.md +86 -510
- package/commands/generate-tech-docs.md +86 -510
- package/commands/learn.md +172 -496
- package/commands/learn.tmpl +70 -3
- package/commands/map-testids.md +86 -510
- package/commands/propose-scenario.md +86 -510
- package/commands/qc-analyze.md +86 -510
- package/commands/qc-design-test.md +86 -510
- package/commands/qc-plan.md +86 -510
- package/commands/qc-report.md +86 -510
- package/commands/qc-review.md +86 -510
- package/commands/qc-run-test.md +86 -510
- package/commands/refine-prd.md +99 -520
- package/commands/refine-prd.tmpl +3 -0
- package/commands/report-bug.md +86 -510
- package/commands/review-code.md +123 -511
- package/commands/review-code.tmpl +3 -0
- package/commands/review-context.md +93 -514
- package/commands/review-context.tmpl +3 -0
- package/commands/review-tech-docs.md +90 -511
- package/commands/review-tech-docs.tmpl +3 -0
- package/commands/setup-ai-first.md +166 -138
- package/commands/setup-ai-first.tmpl +72 -0
- package/commands/sync.md +50 -106
- package/commands/sync.tmpl +48 -3
- package/commands/update-framework.md +16 -103
- package/commands/update-framework.tmpl +14 -0
- package/commands/validate-traces.md +153 -511
- package/commands/validate-traces.tmpl +67 -1
- package/core/FRAMEWORK_VERSION +1 -1
- package/core/README.md +20 -0
- package/core/commands/debug.md +123 -511
- package/core/commands/define-product.md +86 -510
- package/core/commands/dev-gen-test.md +86 -510
- package/core/commands/dev-run-test.md +86 -510
- package/core/commands/dev-smoke-test.md +86 -510
- package/core/commands/extend-prd.md +89 -510
- package/core/commands/fix-bug.md +118 -509
- package/core/commands/generate-architecture.md +94 -515
- package/core/commands/generate-bdd.md +85 -509
- package/core/commands/generate-code.md +86 -510
- package/core/commands/generate-design-spec.md +86 -510
- package/core/commands/generate-prd.md +89 -510
- package/core/commands/generate-spec-manifest.md +86 -510
- package/core/commands/generate-tech-docs.md +86 -510
- package/core/commands/learn.md +172 -496
- package/core/commands/map-testids.md +86 -510
- package/core/commands/propose-scenario.md +86 -510
- package/core/commands/qc-analyze.md +86 -510
- package/core/commands/qc-design-test.md +86 -510
- package/core/commands/qc-plan.md +86 -510
- package/core/commands/qc-report.md +86 -510
- package/core/commands/qc-review.md +86 -510
- package/core/commands/qc-run-test.md +86 -510
- package/core/commands/refine-prd.md +99 -520
- package/core/commands/report-bug.md +86 -510
- package/core/commands/review-code.md +123 -511
- package/core/commands/review-context.md +93 -514
- package/core/commands/review-tech-docs.md +90 -511
- package/core/commands/setup-ai-first.md +166 -138
- package/core/commands/sync.md +50 -106
- package/core/commands/update-framework.md +16 -103
- package/core/commands/validate-traces.md +153 -511
- package/core/hooks/data-guard.js +174 -83
- package/core/hooks/settings.json +2 -1
- package/core/rules/workflow.md +30 -4
- package/core/steps/capture-lesson.md +34 -1
- package/core/steps/context-loader.md +24 -3
- package/core/steps/gate.md +92 -35
- package/core/steps/report-footer.md +23 -0
- package/core/templates/README.md +24 -1
- package/core/templates/ci/trace-gate.yml +146 -0
- package/core/templates/hooks/pre-push +61 -0
- package/docs/02-concepts/architecture.md +25 -6
- package/docs/02-concepts/traceability.md +57 -0
- package/docs/03-guides/architect.md +63 -0
- package/docs/04-reference/commands.md +1 -1
- package/docs/04-reference/model-selection.md +32 -19
- package/docs/explain/21-validate-traces.md +2 -1
- package/docs/explain/27-learn.md +5 -3
- package/hooks/data-guard.js +174 -83
- package/hooks/settings.json +2 -1
- package/package.json +5 -2
- package/rules/workflow.md +30 -4
- package/steps/capture-lesson.md +34 -1
- package/steps/context-loader.md +24 -3
- package/steps/gate.md +92 -35
- package/steps/report-footer.md +23 -0
- package/templates/README.md +24 -1
- package/templates/ci/trace-gate.yml +146 -0
- package/templates/hooks/pre-push +61 -0
- package/scripts/init.sh +0 -49
- package/scripts/upgrade.sh +0 -94
package/bin/index.js
CHANGED
|
@@ -2,7 +2,6 @@
|
|
|
2
2
|
|
|
3
3
|
const fs = require('fs');
|
|
4
4
|
const path = require('path');
|
|
5
|
-
const os = require('os');
|
|
6
5
|
|
|
7
6
|
const ROOT = path.join(__dirname, '..');
|
|
8
7
|
|
|
@@ -19,6 +18,8 @@ const args = process.argv.slice(2);
|
|
|
19
18
|
const isMigrateSpecs = args.includes('--migrate-specs');
|
|
20
19
|
const isMigrateBddPf = args.includes('--migrate-bdd-platform');
|
|
21
20
|
const isRenamePrd = args.includes('--rename-prd-files');
|
|
21
|
+
const isLintTrace = args.includes('--lint-trace');
|
|
22
|
+
const isGateTrace = args.includes('--gate-trace');
|
|
22
23
|
const isInit = args.includes('--init');
|
|
23
24
|
const isProject = args.includes('--project');
|
|
24
25
|
const installHooks = args.includes('--hooks');
|
|
@@ -80,12 +81,13 @@ if (showHelp) {
|
|
|
80
81
|
console.log('');
|
|
81
82
|
console.log('Install modes:');
|
|
82
83
|
console.log(' --init Install framework to .agent/ + create shortcuts in .claude/commands/');
|
|
83
|
-
console.log(' Recommended for new projects.
|
|
84
|
+
console.log(' Recommended for new projects. Later: /update-framework in Claude Code.');
|
|
84
85
|
console.log(' --init --umbrella Umbrella repo setup: installs framework at umbrella level only.');
|
|
85
86
|
console.log(' Generates project-context.yaml with services routing.');
|
|
86
87
|
console.log(' Does NOT install into service submodule directories.');
|
|
87
|
-
console.log('
|
|
88
|
-
console.log(' (
|
|
88
|
+
console.log('');
|
|
89
|
+
console.log(' (--project và chế độ không-cờ đã được GỠ ở v0.5.1 — chúng cài lệnh mà không cài');
|
|
90
|
+
console.log(' .agent/, nên lệnh chạy thiếu guardrail và pipeline QC không hoạt động. Dùng --init.)');
|
|
89
91
|
console.log('');
|
|
90
92
|
console.log('Maintenance:');
|
|
91
93
|
console.log(' --migrate-specs Migrate an existing project from the legacy artifact-type-first');
|
|
@@ -99,6 +101,23 @@ if (showHelp) {
|
|
|
99
101
|
console.log(' else step vocabulary; inserts @trace.platform when missing. Reports');
|
|
100
102
|
console.log(' filename COLLISION and LOST SPEC instead of guessing.');
|
|
101
103
|
console.log(' DRY-RUN by default; add --apply to execute. Uses git mv when possible.');
|
|
104
|
+
console.log(' --lint-trace Kiểm SỔ TRACE THẬT (.trace/**) đối chiếu bin/trace-schema.json:');
|
|
105
|
+
console.log(' header 24 cột · số ô mỗi row · giá trị enum · sc_id trùng · tên file');
|
|
106
|
+
console.log(' {UC-ID}-{platform}.tsv · ô ngày · marker conflict git · trace-history.jsonl.');
|
|
107
|
+
console.log(' Read-only, KHÔNG sửa gì. Exit 1 nếu có lỗi → cắm được vào CI/pre-push.');
|
|
108
|
+
console.log(' Options: --trace DIR[,DIR] (mặc định .trace) · --specs DIR (mặc định specs)');
|
|
109
|
+
console.log(' --warn-only (luôn exit 0) · --json');
|
|
110
|
+
console.log(' Vì sao: self-check canh CONTRACT (file lệnh); cái này canh DỮ LIỆU.');
|
|
111
|
+
console.log(' Sổ trace do LLM ghi tay, 24 cột — một tab lệch là không ai thấy (G38).');
|
|
112
|
+
console.log('');
|
|
113
|
+
console.log(' --gate-trace CỔNG CHẶN PR — exit 1 khi trace có cờ 🔴 (ORPHANED · TRACE_ORPHAN ·');
|
|
114
|
+
console.log(' SEAM_UNWIRED · STUB_UNRESOLVED). Đây là lớp lỗi mà build xanh +');
|
|
115
|
+
console.log(' test từng-UC xanh KHÔNG phát hiện được. Read-only.');
|
|
116
|
+
console.log(' Ba tầng: sổ đúng hình dạng (gọi --lint-trace) → report còn TƯƠI');
|
|
117
|
+
console.log(' (đối chiếu với sổ TSV) → không có cờ 🔴.');
|
|
118
|
+
console.log(' Options: --trace DIR[,DIR] · --report PATH · --skip-lint');
|
|
119
|
+
console.log(' --no-reconcile · --warn-only · --json');
|
|
120
|
+
console.log('');
|
|
102
121
|
console.log(' --rename-prd-files Rename feature-package PRD files from the old fixed name prd.md to the');
|
|
103
122
|
console.log(' {TICKET-ID}-{prd-slug}.md convention (e.g. SEG01-segment-scoring-service.md).');
|
|
104
123
|
console.log(' For projects already on the feature-package layout. DRY-RUN by default;');
|
|
@@ -125,7 +144,8 @@ if (showHelp) {
|
|
|
125
144
|
console.log(' npx @educa-corp/sdd-framework --init --umbrella \\');
|
|
126
145
|
console.log(' --spec-source free-trial-specs \\');
|
|
127
146
|
console.log(' --services mass-product-web:nextjs # umbrella with one FE service');
|
|
128
|
-
console.log(' npx @educa-corp/sdd-framework --
|
|
147
|
+
console.log(' npx @educa-corp/sdd-framework --lint-trace # kiểm sổ trace (CI / pre-push)');
|
|
148
|
+
console.log(' npx @educa-corp/sdd-framework --lint-trace --warn-only # xem nợ tồn, chưa chặn');
|
|
129
149
|
process.exit(0);
|
|
130
150
|
}
|
|
131
151
|
|
|
@@ -149,15 +169,52 @@ if (isRenamePrd) {
|
|
|
149
169
|
return; // rename-prd-files.js calls process.exit itself
|
|
150
170
|
}
|
|
151
171
|
|
|
152
|
-
// ──
|
|
172
|
+
// ── --lint-trace: kiểm sổ trace thật của người dùng (G38) ──────────────────
|
|
173
|
+
// Read-only, đi trước bước build vì nó làm việc trên .trace/ của project, không phải
|
|
174
|
+
// package. Cùng khuôn với các flag maintenance ở trên.
|
|
175
|
+
if (isLintTrace) {
|
|
176
|
+
require(path.join(__dirname, 'lint-trace.js'));
|
|
177
|
+
return; // lint-trace.js calls process.exit itself
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
// ── --gate-trace: cổng chặn PR — exit 1 khi có cờ 🔴 (G39) ─────────────────
|
|
181
|
+
// Cũng read-only. Tự gọi lint-trace ở tầng G1, nên một lệnh là đủ cho CI.
|
|
182
|
+
if (isGateTrace) {
|
|
183
|
+
require(path.join(__dirname, 'gate-trace.js'));
|
|
184
|
+
return; // gate-trace.js calls process.exit itself
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
// ── Step 1: Build .tmpl → .md — CHỈ khi cần ───────────────────────────────
|
|
188
|
+
//
|
|
189
|
+
// Tarball publish đã chứa `core/` dựng sẵn (npm pack: 138 entry). Build lại ở máy
|
|
190
|
+
// người dùng không đem lại gì và mang rủi ro thật (GAPS-v3 G43): nó ghi vào THƯ MỤC
|
|
191
|
+
// PACKAGE — tức npx cache, hoặc node_modules của bản cài global. Thư mục đó có thể
|
|
192
|
+
// read-only (CI doanh nghiệp, `sudo npm i -g` rồi chạy bằng user thường), và hai
|
|
193
|
+
// `--init` chạy đồng thời ở hai project dùng chung cache sẽ đua nhau trên cùng core/.
|
|
194
|
+
//
|
|
195
|
+
// Nên: build CHỈ khi core/ vắng mặt hoặc lệch version — tức chỉ trong dev checkout.
|
|
153
196
|
const buildScript = path.join(__dirname, 'build.js');
|
|
154
|
-
|
|
197
|
+
const coreVersionFile = path.join(ROOT, 'core', 'FRAMEWORK_VERSION');
|
|
198
|
+
|
|
199
|
+
let corePrebuilt = false;
|
|
200
|
+
try {
|
|
201
|
+
corePrebuilt = fs.readFileSync(coreVersionFile, 'utf8').trim() === VERSION;
|
|
202
|
+
} catch { /* không có file = chưa build */ }
|
|
203
|
+
|
|
204
|
+
if (!corePrebuilt && fs.existsSync(buildScript)) {
|
|
155
205
|
try {
|
|
156
206
|
require(buildScript);
|
|
157
207
|
} catch (err) {
|
|
158
208
|
console.error('Build step failed:', err.message);
|
|
159
209
|
process.exit(1);
|
|
160
210
|
}
|
|
211
|
+
} else if (corePrebuilt && fs.existsSync(path.join(ROOT, 'commands', 'generate-code.tmpl'))) {
|
|
212
|
+
// Chỉ nói trong DEV CHECKOUT (có .tmpl nguồn). Consumer không cần biết bước này tồn tại.
|
|
213
|
+
// Cần thiết vì trước G43 thì `--init` luôn build, nên "sửa .tmpl rồi --init" từng có tác
|
|
214
|
+
// dụng. Giờ không — và một thay đổi lặng lẽ như thế sẽ tốn của ai đó nửa tiếng.
|
|
215
|
+
console.log('');
|
|
216
|
+
console.log(`ℹ️ core/ đã build sẵn (v${VERSION}) — bỏ qua bước build.`);
|
|
217
|
+
console.log(' Vừa sửa commands/*.tmpl · steps/ · templates/ ? Chạy `npm run build` trước.');
|
|
161
218
|
}
|
|
162
219
|
|
|
163
220
|
// ── --init mode: install to .agent/ + create .claude/commands/ shortcuts ─────
|
|
@@ -205,27 +262,10 @@ if (isInit) {
|
|
|
205
262
|
console.log('');
|
|
206
263
|
console.log('Creating .claude/commands/ shortcuts ...');
|
|
207
264
|
console.log('');
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
const
|
|
211
|
-
|
|
212
|
-
const failedShortcuts = [];
|
|
213
|
-
|
|
214
|
-
for (const cmdFile of commandFiles) {
|
|
215
|
-
const cmdName = cmdFile.replace('.md', '');
|
|
216
|
-
const shortcut =
|
|
217
|
-
`# /${cmdName}\n` +
|
|
218
|
-
`\n` +
|
|
219
|
-
`Read the full command definition from \`.agent/commands/${cmdFile}\`` +
|
|
220
|
-
` and execute it with arguments: $ARGUMENTS\n`;
|
|
221
|
-
try {
|
|
222
|
-
fs.writeFileSync(path.join(claudeCommandsDir, cmdFile), shortcut, 'utf8');
|
|
223
|
-
console.log(` ✅ /${cmdName}`);
|
|
224
|
-
} catch {
|
|
225
|
-
console.log(` ❌ /${cmdName} (failed to write shortcut)`);
|
|
226
|
-
failedShortcuts.push(cmdName);
|
|
227
|
-
}
|
|
228
|
-
}
|
|
265
|
+
const sc = writeShortcuts(agentDir, claudeCommandsDir, { verbose: true });
|
|
266
|
+
const commandFiles = sc.commandFiles;
|
|
267
|
+
const failedShortcuts = sc.failed;
|
|
268
|
+
reportShortcuts(sc, '.claude/commands/');
|
|
229
269
|
|
|
230
270
|
// 3. Install module (optional --module flag)
|
|
231
271
|
if (moduleName) {
|
|
@@ -383,17 +423,11 @@ if (isInit) {
|
|
|
383
423
|
}
|
|
384
424
|
}
|
|
385
425
|
|
|
386
|
-
// Create .claude/commands/ shortcuts
|
|
387
|
-
|
|
388
|
-
const
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
const shortcut =
|
|
392
|
-
`# /${cmdName}\n\nRead the full command definition from \`.agent/commands/${cmdFile}\`` +
|
|
393
|
-
` and execute it with arguments: $ARGUMENTS\n`;
|
|
394
|
-
fs.writeFileSync(path.join(svcClaudeCmds, cmdFile), shortcut, 'utf8');
|
|
395
|
-
}
|
|
396
|
-
console.log(` ✅ ${svc.name}/.claude/commands/ (${svcCmdFiles.length} shortcuts)`);
|
|
426
|
+
// Create .claude/commands/ shortcuts (cùng hàm với đường single-service — G44:
|
|
427
|
+
// trước đó hai chỗ này copy-paste nhau và chỉ một chỗ được sửa khi có thay đổi)
|
|
428
|
+
const svcSc = writeShortcuts(svcAgentDir, svcClaudeCmds, { verbose: false });
|
|
429
|
+
console.log(` ✅ ${svc.name}/.claude/commands/ (${svcSc.commandFiles.length} shortcuts)`);
|
|
430
|
+
reportShortcuts(svcSc, `${svc.name}/.claude/commands/`);
|
|
397
431
|
|
|
398
432
|
// Generate project-context.yaml (only if not already present)
|
|
399
433
|
const ctxPath = path.join(svcAgentDir, 'project-context.yaml');
|
|
@@ -510,99 +544,59 @@ if (isInit) {
|
|
|
510
544
|
console.log(' 2. Commit .agent/ to git so your whole team has the framework');
|
|
511
545
|
}
|
|
512
546
|
console.log('');
|
|
513
|
-
|
|
547
|
+
// G47 mục 1: dòng này từng in `bash scripts/upgrade.sh` sau MỌI lần cài — nhưng
|
|
548
|
+
// `scripts/` không bao giờ được cài vào project (coreMappings không có nó), nên
|
|
549
|
+
// consumer nhận "No such file or directory" ngay câu chỉ dẫn ĐẦU TIÊN họ đọc.
|
|
550
|
+
console.log('To upgrade later: /update-framework (trong Claude Code)');
|
|
514
551
|
console.log(' (or: npx @educa-corp/sdd-framework@latest --init)');
|
|
515
552
|
console.log('');
|
|
516
553
|
process.exit(0);
|
|
517
554
|
}
|
|
518
555
|
|
|
519
|
-
// ──
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
556
|
+
// ── Không phải --init ⇒ không cài gì, chỉ hướng dẫn (GAPS-v3 G50) ─────────
|
|
557
|
+
//
|
|
558
|
+
// Trước đây chỗ này là hai chế độ 'legacy': `--project` và nhánh KHÔNG-CỜ (global).
|
|
559
|
+
// Cả hai chỉ copy commands/*.md vào .claude/commands/ và KHÔNG cài .agent/ — trong khi
|
|
560
|
+
// chính các file lệnh đó tham chiếu .agent/ ở 612 chỗ, và bước ĐẦU TIÊN của mọi lệnh là
|
|
561
|
+
// 'Đọc .agent/project-context.yaml'. Kết quả không phải hỏng hẳn mà THOÁI HOÁ ÂM THẦM:
|
|
562
|
+
// • 6 lệnh QC chết (nạp skill lúc chạy từ .agent/skills/qc/)
|
|
563
|
+
// • mọi lệnh còn lại VẪN CHẠY nhưng không nạp được .agent/rules/ — tức mất data-guard
|
|
564
|
+
// và mất luật CHECKPOINT/scope. Không ai được báo.
|
|
565
|
+
// • /setup-ai-first Step 6b bảo copy từ .agent/templates/ — không có ở đó
|
|
566
|
+
// Và installer thì in '✅ Installed 32/32 commands'. Đúng lớp lỗi cả loạt GAP này chống:
|
|
567
|
+
// tín hiệu xanh trên thứ đã hết đúng.
|
|
568
|
+
//
|
|
569
|
+
// --init phủ MỌI ca thật (single-service · monorepo · umbrella). Legacy không giải quyết
|
|
570
|
+
// vấn đề nào mà --init không giải quyết — nó chỉ là di sản.
|
|
526
571
|
console.log('');
|
|
527
572
|
console.log('╔══════════════════════════════════════════╗');
|
|
528
573
|
console.log('║ SDD Framework — CLI Installer ║');
|
|
529
574
|
console.log(`║ v${VERSION} ║`);
|
|
530
575
|
console.log('╚══════════════════════════════════════════╝');
|
|
531
576
|
console.log('');
|
|
532
|
-
|
|
533
|
-
console.log(`
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
console.log('');
|
|
537
|
-
|
|
538
|
-
// ── Step 2: Install commands ──────────────────────────────────────────────
|
|
539
|
-
fs.mkdirSync(targetDir, { recursive: true });
|
|
540
|
-
|
|
541
|
-
const commandsDir = path.join(ROOT, 'commands');
|
|
542
|
-
const commands = fs.readdirSync(commandsDir).filter(f => f.endsWith('.md'));
|
|
543
|
-
const failed = [];
|
|
544
|
-
|
|
545
|
-
for (const cmd of commands) {
|
|
546
|
-
try {
|
|
547
|
-
fs.copyFileSync(path.join(commandsDir, cmd), path.join(targetDir, cmd));
|
|
548
|
-
console.log(` ✅ ${cmd.replace('.md', '')}`);
|
|
549
|
-
} catch {
|
|
550
|
-
console.log(` ❌ ${cmd.replace('.md', '')} (failed)`);
|
|
551
|
-
failed.push(cmd);
|
|
552
|
-
}
|
|
577
|
+
if (isProject) {
|
|
578
|
+
console.log('ℹ️ `--project` đã được gỡ (v0.5.1).');
|
|
579
|
+
} else {
|
|
580
|
+
console.log('ℹ️ Chạy không kèm cờ nào thì không có gì để cài.');
|
|
553
581
|
}
|
|
554
|
-
|
|
555
582
|
console.log('');
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
}
|
|
561
|
-
|
|
562
|
-
console.log(`✅ Installed ${commands.length}/${commands.length} commands to:`);
|
|
563
|
-
console.log(` ${targetDir}`);
|
|
564
|
-
|
|
565
|
-
// ── Step 3: Install module (optional) ─────────────────────────────────────
|
|
566
|
-
if (moduleName) {
|
|
567
|
-
console.log('');
|
|
568
|
-
if (!AVAILABLE_MODULES.includes(moduleName)) {
|
|
569
|
-
console.log(`⚠️ Unknown module: "${moduleName}"`);
|
|
570
|
-
console.log(` Available: ${AVAILABLE_MODULES.join(', ')}`);
|
|
571
|
-
process.exit(1);
|
|
572
|
-
}
|
|
573
|
-
|
|
574
|
-
const srcModuleDir = path.join(ROOT, 'modules', moduleName);
|
|
575
|
-
const destModuleDir = path.join(process.cwd(), '.agent', 'modules', moduleName);
|
|
576
|
-
|
|
577
|
-
fs.mkdirSync(destModuleDir, { recursive: true });
|
|
578
|
-
copyDirRecursive(srcModuleDir, destModuleDir);
|
|
579
|
-
|
|
580
|
-
console.log(`✅ Module "${moduleName}" installed to:`);
|
|
581
|
-
console.log(` ${destModuleDir}`);
|
|
582
|
-
}
|
|
583
|
-
|
|
584
|
-
// ── Step 4: Install data-guard hook (optional, --hooks flag) ──────────────
|
|
585
|
-
if (installHooks) {
|
|
586
|
-
console.log('');
|
|
587
|
-
installDataGuardHook();
|
|
588
|
-
}
|
|
589
|
-
|
|
590
|
-
// ── Summary ───────────────────────────────────────────────────────────────
|
|
583
|
+
console.log(' Chế độ cũ chỉ copy file lệnh vào .claude/commands/ mà KHÔNG cài .agent/.');
|
|
584
|
+
console.log(' Nhưng các lệnh tham chiếu .agent/ ở 612 chỗ — bước đầu tiên của mọi lệnh là');
|
|
585
|
+
console.log(' đọc .agent/project-context.yaml. Nên bản cài đó chạy MÀ KHÔNG có data-guard,');
|
|
586
|
+
console.log(' không có luật CHECKPOINT/scope, và cả pipeline QC không hoạt động — im lặng.');
|
|
591
587
|
console.log('');
|
|
592
|
-
console.log('
|
|
593
|
-
console.log('
|
|
594
|
-
console.log('
|
|
595
|
-
console.log('
|
|
596
|
-
if (!moduleName) {
|
|
597
|
-
console.log('');
|
|
598
|
-
console.log(`Tip: re-run with --module <name> to install a stack profile.`);
|
|
599
|
-
console.log(` Available: ${AVAILABLE_MODULES.join(', ')}`);
|
|
600
|
-
}
|
|
601
|
-
if (!installHooks) {
|
|
602
|
-
console.log('');
|
|
603
|
-
console.log('Tip: re-run with --hooks to install data-guard hook (sensitive file protection).');
|
|
604
|
-
}
|
|
588
|
+
console.log(' Dùng:');
|
|
589
|
+
console.log(' npx @educa-corp/sdd-framework --init # single-service');
|
|
590
|
+
console.log(' npx @educa-corp/sdd-framework --init --module java-spring --hooks');
|
|
591
|
+
console.log(' npx @educa-corp/sdd-framework --init --services be:java-spring,web:react');
|
|
605
592
|
console.log('');
|
|
593
|
+
console.log(' Đã lỡ cài kiểu cũ? Cứ chạy --init: nó phát hiện file lệnh cũ trong');
|
|
594
|
+
console.log(' .claude/commands/, backup rồi báo ra — không đè im lặng.');
|
|
595
|
+
console.log('');
|
|
596
|
+
console.log(' Xem đủ tuỳ chọn: npx @educa-corp/sdd-framework --help');
|
|
597
|
+
console.log('');
|
|
598
|
+
process.exit(0);
|
|
599
|
+
|
|
606
600
|
|
|
607
601
|
// ── Helpers ───────────────────────────────────────────────────────────────
|
|
608
602
|
|
|
@@ -642,10 +636,15 @@ function readManifest(agentDir) {
|
|
|
642
636
|
}
|
|
643
637
|
|
|
644
638
|
/**
|
|
645
|
-
* Copy core/ → .agent/, backing up any file the project edited since the last install
|
|
646
|
-
*
|
|
647
|
-
*
|
|
648
|
-
*
|
|
639
|
+
* Copy core/ → .agent/, backing up any file the project edited since the last install,
|
|
640
|
+
* AND removing files the framework no longer ships (GAPS-v3 G44).
|
|
641
|
+
*
|
|
642
|
+
* @returns {{ edited: string[], removed: string[], keptEdited: string[],
|
|
643
|
+
* backupDir: string|null, firstRun: boolean }}
|
|
644
|
+
* edited — file bị ghi đè VÀ có sửa đổi cục bộ (đã backup)
|
|
645
|
+
* removed — file framework đã bỏ, còn nguyên bản ⇒ đã xoá
|
|
646
|
+
* keptEdited — file framework đã bỏ NHƯNG người dùng đã sửa ⇒ GIỮ LẠI + backup
|
|
647
|
+
* firstRun — chưa có manifest nên lần này không phát hiện được sửa đổi
|
|
649
648
|
*/
|
|
650
649
|
function installCore(coreDir, agentDir, version) {
|
|
651
650
|
const manifest = readManifest(agentDir);
|
|
@@ -655,6 +654,12 @@ function installCore(coreDir, agentDir, version) {
|
|
|
655
654
|
const edited = [];
|
|
656
655
|
const written = {};
|
|
657
656
|
|
|
657
|
+
const backup = (abs, rel) => {
|
|
658
|
+
const bak = path.join(backupDir, rel);
|
|
659
|
+
fs.mkdirSync(path.dirname(bak), { recursive: true });
|
|
660
|
+
fs.copyFileSync(abs, bak);
|
|
661
|
+
};
|
|
662
|
+
|
|
658
663
|
const walk = (src, dst) => {
|
|
659
664
|
fs.mkdirSync(dst, { recursive: true });
|
|
660
665
|
for (const entry of fs.readdirSync(src, { withFileTypes: true })) {
|
|
@@ -666,9 +671,7 @@ function installCore(coreDir, agentDir, version) {
|
|
|
666
671
|
// Locally edited == exists, we have a record of what we last wrote, and the
|
|
667
672
|
// file no longer matches that record. Framework-only changes never match here.
|
|
668
673
|
if (manifest && fs.existsSync(d) && manifest.files[rel] && sha1(d) !== manifest.files[rel]) {
|
|
669
|
-
|
|
670
|
-
fs.mkdirSync(path.dirname(bak), { recursive: true });
|
|
671
|
-
fs.copyFileSync(d, bak);
|
|
674
|
+
backup(d, rel);
|
|
672
675
|
edited.push(rel);
|
|
673
676
|
}
|
|
674
677
|
fs.copyFileSync(s, d);
|
|
@@ -677,33 +680,253 @@ function installCore(coreDir, agentDir, version) {
|
|
|
677
680
|
};
|
|
678
681
|
walk(coreDir, agentDir);
|
|
679
682
|
|
|
683
|
+
// ── Prune: gỡ file framework KHÔNG CÒN ship (G44) ──────────────────────────
|
|
684
|
+
//
|
|
685
|
+
// Trước G44, installCore chỉ copy — không có nhánh xoá nào. Nên một lệnh bị bỏ ở
|
|
686
|
+
// version mới nằm lại trong .agent/commands/ và .claude/commands/ của mọi project
|
|
687
|
+
// VĨNH VIỄN: vẫn hiện trong menu `/`, vẫn chạy được, vẫn mang logic của version cũ
|
|
688
|
+
// — kể cả khi framework đã bỏ nó VÌ NÓ SAI. Và `/update-framework` Step 5 thì hứa
|
|
689
|
+
// báo cáo "Removed", một trạng thái installer không thể tạo ra.
|
|
690
|
+
//
|
|
691
|
+
// Đây là hành vi XOÁ FILE TRONG PROJECT NGƯỜI DÙNG, nên chỉ xoá khi CẢ BA đúng:
|
|
692
|
+
// 1. có trong manifest cũ ⇒ do framework đặt vào, không phải người dùng tạo
|
|
693
|
+
// 2. không có trong core/ mới ⇒ framework đã bỏ
|
|
694
|
+
// 3. hash khớp manifest ⇒ nguyên bản, xoá không mất gì của ai
|
|
695
|
+
// Đúng (1)+(2) mà sai (3) ⇒ người dùng đã sửa ⇒ GIỮ LẠI + backup + báo. Thà để lại
|
|
696
|
+
// một file lạc còn hơn xoá thứ ai đó đã bỏ công viết.
|
|
697
|
+
const removed = [];
|
|
698
|
+
const keptEdited = [];
|
|
699
|
+
if (manifest) {
|
|
700
|
+
for (const rel of Object.keys(manifest.files)) {
|
|
701
|
+
if (written[rel]) continue; // vẫn còn ship
|
|
702
|
+
const abs = path.join(agentDir, rel);
|
|
703
|
+
if (!fs.existsSync(abs)) continue; // đã biến mất từ trước
|
|
704
|
+
if (sha1(abs) === manifest.files[rel]) {
|
|
705
|
+
fs.unlinkSync(abs);
|
|
706
|
+
removed.push(rel);
|
|
707
|
+
} else {
|
|
708
|
+
backup(abs, rel);
|
|
709
|
+
keptEdited.push(rel);
|
|
710
|
+
}
|
|
711
|
+
}
|
|
712
|
+
// Dọn thư mục rỗng còn lại sau khi prune (vd cả một module bị bỏ). Chỉ xoá dir
|
|
713
|
+
// RỖNG — không recursive, nên không thể chạm vào file nào của người dùng.
|
|
714
|
+
for (const rel of removed) {
|
|
715
|
+
let dir = path.dirname(path.join(agentDir, rel));
|
|
716
|
+
while (dir !== agentDir && dir.startsWith(agentDir)) {
|
|
717
|
+
try {
|
|
718
|
+
if (fs.readdirSync(dir).length) break;
|
|
719
|
+
fs.rmdirSync(dir);
|
|
720
|
+
} catch { break; }
|
|
721
|
+
dir = path.dirname(dir);
|
|
722
|
+
}
|
|
723
|
+
}
|
|
724
|
+
}
|
|
725
|
+
|
|
726
|
+
// GIỮ phần `shortcuts` của manifest cũ. installCore chạy TRƯỚC writeShortcuts, nên
|
|
727
|
+
// ghi một object mới chỉ có `files` sẽ xoá sạch lịch sử shortcut — và writeShortcuts
|
|
728
|
+
// sau đó thấy `prev = {}`, mất cả prune LẪN phát hiện trùng tên. Bug này đã xảy ra
|
|
729
|
+
// thật khi làm G44: .agent/commands/debug.md được gỡ nhưng shortcut của nó thì không.
|
|
680
730
|
fs.writeFileSync(
|
|
681
731
|
path.join(agentDir, MANIFEST_NAME),
|
|
682
|
-
JSON.stringify({
|
|
732
|
+
JSON.stringify({
|
|
733
|
+
version,
|
|
734
|
+
generated_at: new Date().toISOString(),
|
|
735
|
+
files: written,
|
|
736
|
+
shortcuts: (manifest && manifest.shortcuts) || {},
|
|
737
|
+
}, null, 2),
|
|
683
738
|
'utf8'
|
|
684
739
|
);
|
|
685
740
|
|
|
686
|
-
return {
|
|
741
|
+
return {
|
|
742
|
+
edited, removed, keptEdited,
|
|
743
|
+
backupDir: (edited.length || keptEdited.length) ? backupDir : null,
|
|
744
|
+
firstRun,
|
|
745
|
+
};
|
|
687
746
|
}
|
|
688
747
|
|
|
689
748
|
function reportOverwritten(result, agentLabel) {
|
|
690
749
|
if (result.firstRun) {
|
|
691
750
|
console.log('');
|
|
692
751
|
console.log(` ℹ️ ${agentLabel}${MANIFEST_NAME} created. Từ lần nâng cấp SAU, mọi file bạn`);
|
|
693
|
-
console.log(` sửa trong ${agentLabel} mà bị ghi đè sẽ được backup + liệt k
|
|
752
|
+
console.log(` sửa trong ${agentLabel} mà bị ghi đè sẽ được backup + liệt kê,`);
|
|
753
|
+
console.log(` và file framework đã bỏ sẽ được gỡ khỏi ${agentLabel}.`);
|
|
694
754
|
return;
|
|
695
755
|
}
|
|
696
|
-
if (!result.edited.length) return;
|
|
697
756
|
|
|
698
|
-
const
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
757
|
+
const bak = result.backupDir
|
|
758
|
+
? path.relative(process.cwd(), result.backupDir).replace(/\\/g, '/')
|
|
759
|
+
: null;
|
|
760
|
+
const list = (arr, n = 12) => {
|
|
761
|
+
for (const f of arr.slice(0, n)) console.log(` ${agentLabel}${f}`);
|
|
762
|
+
if (arr.length > n) console.log(` … và ${arr.length - n} file nữa`);
|
|
763
|
+
};
|
|
764
|
+
|
|
765
|
+
if (result.edited.length) {
|
|
766
|
+
console.log('');
|
|
767
|
+
console.log(` ⚠️ ${result.edited.length} file trong ${agentLabel} đã bị bạn sửa và VỪA BỊ GHI ĐÈ:`);
|
|
768
|
+
list(result.edited);
|
|
769
|
+
console.log(` Bản cũ đã lưu ở: ${bak}/`);
|
|
770
|
+
console.log(` ${agentLabel} là bản mirror SINH RA — sửa trực tiếp ở đó không bền.`);
|
|
771
|
+
console.log(` Muốn giữ thay đổi: chuyển nó vào repo framework rồi phát hành,`);
|
|
772
|
+
console.log(` hoặc áp lại thủ công sau khi xem diff. Xoá thư mục backup khi xong.`);
|
|
773
|
+
}
|
|
774
|
+
|
|
775
|
+
// G44 — nói ra thứ đã bị GỠ. Trong một diff nâng cấp lớn, file biến mất là thứ dễ
|
|
776
|
+
// trôi nhất; và nếu không báo thì người dùng chỉ phát hiện khi gõ một slash command
|
|
777
|
+
// giờ đã không còn.
|
|
778
|
+
if (result.removed.length) {
|
|
779
|
+
console.log('');
|
|
780
|
+
console.log(` 🗑️ ${result.removed.length} file framework đã bị BỎ ở version này — đã gỡ khỏi ${agentLabel}:`);
|
|
781
|
+
list(result.removed, 8);
|
|
782
|
+
console.log(` Chúng còn nguyên bản (không ai sửa) nên gỡ đi không mất gì.`);
|
|
783
|
+
console.log(` Lệnh nào biến mất khỏi menu / là vì vậy.`);
|
|
784
|
+
}
|
|
785
|
+
|
|
786
|
+
if (result.keptEdited.length) {
|
|
787
|
+
console.log('');
|
|
788
|
+
console.log(` ⚠️ ${result.keptEdited.length} file framework đã bị bỏ NHƯNG bạn đã sửa — GIỮ LẠI:`);
|
|
789
|
+
list(result.keptEdited, 8);
|
|
790
|
+
console.log(` Đã backup ở: ${bak}/`);
|
|
791
|
+
console.log(` Framework không dùng chúng nữa. Xoá tay khi bạn chắc không cần,`);
|
|
792
|
+
console.log(` hoặc chuyển thay đổi đó vào repo framework nếu nó còn giá trị.`);
|
|
793
|
+
}
|
|
794
|
+
}
|
|
795
|
+
|
|
796
|
+
// ── Shortcut .claude/commands/ — có manifest, có prune, có cảnh báo trùng tên ──
|
|
797
|
+
//
|
|
798
|
+
// Vì sao cần (GAPS-v3 G44): bản cũ ghi 32 file vào .claude/commands/ VÔ ĐIỀU KIỆN,
|
|
799
|
+
// không backup, không cảnh báo, và không bao giờ gỡ shortcut của lệnh đã bị bỏ.
|
|
800
|
+
// Hai hệ quả:
|
|
801
|
+
// • Tên lệnh rất chung — /debug, /sync, /learn, /code-review — và trùng cả với
|
|
802
|
+
// skill/plugin sẵn có của Claude Code. Project đã có /sync riêng thì MẤT nó,
|
|
803
|
+
// im lặng, ngay lần cài đầu. Bảo vệ G24 chỉ áp cho .agent/, không tới đây.
|
|
804
|
+
// • Lệnh bị bỏ ở version mới vẫn hiện trong menu `/` mãi mãi, và khi ai đó gõ nó
|
|
805
|
+
// thì file .agent/commands/ đích đã không còn.
|
|
806
|
+
//
|
|
807
|
+
// Shortcut được ghi vào manifest ở khoá `shortcuts` (path tương đối gốc project),
|
|
808
|
+
// tách khỏi `files` (tương đối .agent/). Manifest cũ chỉ có `files` → vẫn đọc được.
|
|
809
|
+
function shortcutBody(cmdFile) {
|
|
810
|
+
const cmdName = cmdFile.replace(/\.md$/, '');
|
|
811
|
+
return `# /${cmdName}\n\nRead the full command definition from \`.agent/commands/${cmdFile}\``
|
|
812
|
+
+ ` and execute it with arguments: $ARGUMENTS\n`;
|
|
813
|
+
}
|
|
814
|
+
|
|
815
|
+
function writeShortcuts(agentDir, claudeCommandsDir, { verbose }) {
|
|
816
|
+
const manifest = readManifest(agentDir) || {};
|
|
817
|
+
const prev = manifest.shortcuts || {};
|
|
818
|
+
const stamp = new Date().toISOString().slice(0, 10).replace(/-/g, '');
|
|
819
|
+
|
|
820
|
+
fs.mkdirSync(claudeCommandsDir, { recursive: true });
|
|
821
|
+
const commandFiles = fs.readdirSync(path.join(agentDir, 'commands')).filter(f => f.endsWith('.md'));
|
|
822
|
+
|
|
823
|
+
const written = {};
|
|
824
|
+
const failed = [];
|
|
825
|
+
const collided = [];
|
|
826
|
+
const removed = [];
|
|
827
|
+
const keptEdited = [];
|
|
828
|
+
const legacyFound = []; // file lệnh ĐẦY ĐỦ từ bản cài legacy — KHÁC lệnh của project
|
|
829
|
+
const sha1s = (str) => require('crypto').createHash('sha1').update(str).digest('hex');
|
|
830
|
+
|
|
831
|
+
for (const cmdFile of commandFiles) {
|
|
832
|
+
const abs = path.join(claudeCommandsDir, cmdFile);
|
|
833
|
+
const body = shortcutBody(cmdFile);
|
|
834
|
+
const key = cmdFile;
|
|
835
|
+
|
|
836
|
+
// Đã tồn tại, KHÔNG do lần cài trước của framework tạo, và nội dung khác ⇒ backup
|
|
837
|
+
// rồi báo — đừng lặng lẽ đè. Nhưng PHÂN BIỆT HAI CA, vì thông điệp phải khác nhau:
|
|
838
|
+
//
|
|
839
|
+
// (a) BẢN CÀI FRAMEWORK CŨ (legacy). Legacy mode copy file lệnh ĐẦY ĐỦ (60–110 KB)
|
|
840
|
+
// vào .claude/commands/ với đúng 32 tên đó. G50 gỡ legacy nên mọi project đã cài
|
|
841
|
+
// kiểu cũ SẼ đi qua đường này. Gọi nó là "slash command của bạn" là SAI: người
|
|
842
|
+
// dùng sẽ tưởng vừa mất 32 lệnh tự viết, đi mở backup tìm, rồi phát hiện đó chỉ
|
|
843
|
+
// là bản framework cũ.
|
|
844
|
+
// (b) slash command THẬT của project — tên lệnh framework rất chung (/debug /sync
|
|
845
|
+
// /learn) nên trùng là chuyện có thật.
|
|
846
|
+
//
|
|
847
|
+
// Nhận diện: shortcut ~150 B · bản legacy 60–110 KB · lệnh project thường < 5 KB.
|
|
848
|
+
// Chênh ~500 lần, không cần đoán. Ngưỡng 20 KB nằm giữa 5 và 60 nên rất an toàn;
|
|
849
|
+
// `## Gate` là dấu hiệu thứ hai (mọi file lệnh built đều có, shortcut thì không).
|
|
850
|
+
if (fs.existsSync(abs) && !prev[key]) {
|
|
851
|
+
const cur = fs.readFileSync(abs, 'utf8');
|
|
852
|
+
if (cur !== body) {
|
|
853
|
+
const bak = path.join(agentDir, `.overwritten-${stamp}-shortcuts`, cmdFile);
|
|
854
|
+
try {
|
|
855
|
+
fs.mkdirSync(path.dirname(bak), { recursive: true });
|
|
856
|
+
fs.copyFileSync(abs, bak);
|
|
857
|
+
(cur.length > 20 * 1024 || cur.includes('## Gate') ? legacyFound : collided).push(cmdFile);
|
|
858
|
+
} catch { /* backup fail thì vẫn báo ở dưới */ }
|
|
859
|
+
}
|
|
860
|
+
}
|
|
861
|
+
|
|
862
|
+
try {
|
|
863
|
+
fs.writeFileSync(abs, body, 'utf8');
|
|
864
|
+
written[key] = sha1s(body);
|
|
865
|
+
if (verbose) console.log(` ✅ /${cmdFile.replace(/\.md$/, '')}`);
|
|
866
|
+
} catch {
|
|
867
|
+
if (verbose) console.log(` ❌ /${cmdFile.replace(/\.md$/, '')} (failed to write shortcut)`);
|
|
868
|
+
failed.push(cmdFile.replace(/\.md$/, ''));
|
|
869
|
+
}
|
|
870
|
+
}
|
|
871
|
+
|
|
872
|
+
// Prune — cùng ba điều kiện như installCore: có trong manifest cũ, không còn ship,
|
|
873
|
+
// và còn nguyên bản. Người dùng sửa shortcut thì giữ lại.
|
|
874
|
+
for (const key of Object.keys(prev)) {
|
|
875
|
+
if (written[key]) continue;
|
|
876
|
+
const abs = path.join(claudeCommandsDir, key);
|
|
877
|
+
if (!fs.existsSync(abs)) continue;
|
|
878
|
+
if (sha1s(fs.readFileSync(abs, 'utf8')) === prev[key]) {
|
|
879
|
+
try { fs.unlinkSync(abs); removed.push(key); } catch { /* bỏ qua */ }
|
|
880
|
+
} else {
|
|
881
|
+
keptEdited.push(key);
|
|
882
|
+
}
|
|
883
|
+
}
|
|
884
|
+
|
|
885
|
+
// Ghi lại manifest (installCore đã ghi `files` trước đó — nối thêm `shortcuts`)
|
|
886
|
+
try {
|
|
887
|
+
const mPath = path.join(agentDir, MANIFEST_NAME);
|
|
888
|
+
const m = readManifest(agentDir) || { files: {} };
|
|
889
|
+
m.shortcuts = written;
|
|
890
|
+
fs.writeFileSync(mPath, JSON.stringify(m, null, 2), 'utf8');
|
|
891
|
+
} catch { /* manifest không ghi được thì lần sau coi như firstRun — không chặn cài */ }
|
|
892
|
+
|
|
893
|
+
return { commandFiles, failed, collided, legacyFound, removed, keptEdited };
|
|
894
|
+
}
|
|
895
|
+
|
|
896
|
+
function reportShortcuts(sc, label) {
|
|
897
|
+
// Bản cài legacy cũ — KHÔNG phải slash command của người dùng. Nói đúng tên nó, vì
|
|
898
|
+
// G50 gỡ legacy nên mọi project đã cài kiểu cũ sẽ đi qua đây, và gọi sai tên làm họ
|
|
899
|
+
// tưởng vừa mất 32 lệnh tự viết rồi đi mở backup tìm một thứ không tồn tại.
|
|
900
|
+
if ((sc.legacyFound || []).length) {
|
|
901
|
+
console.log('');
|
|
902
|
+
console.log(` 🔄 ${sc.legacyFound.length} file trong ${label} là BẢN CÀI FRAMEWORK CŨ (legacy)`);
|
|
903
|
+
console.log(` — đã thay bằng shortcut. Đây KHÔNG phải slash command bạn tự viết.`);
|
|
904
|
+
console.log(` Bản cũ vẫn được backup ở .agent/.overwritten-*-shortcuts/, nhưng bạn gần như`);
|
|
905
|
+
console.log(` chắc chắn không cần nó — xoá thư mục đó khi thấy \`/\` vẫn hoạt động bình thường.`);
|
|
906
|
+
console.log(` (Chế độ cài legacy đã được gỡ ở v0.5.1: nó cài lệnh mà không cài .agent/,`);
|
|
907
|
+
console.log(` nên lệnh chạy thiếu guardrail và pipeline QC không hoạt động.)`);
|
|
908
|
+
}
|
|
909
|
+
if (sc.collided.length) {
|
|
910
|
+
console.log('');
|
|
911
|
+
console.log(` ⚠️ ${sc.collided.length} slash command CÓ SẴN của project vừa bị shortcut framework ghi đè:`);
|
|
912
|
+
for (const f of sc.collided.slice(0, 10)) console.log(` ${label}${f}`);
|
|
913
|
+
if (sc.collided.length > 10) console.log(` … và ${sc.collided.length - 10} file nữa`);
|
|
914
|
+
console.log(` Bản cũ đã lưu trong .agent/.overwritten-*-shortcuts/`);
|
|
915
|
+
console.log(` Tên lệnh của framework rất chung (/debug /sync /learn) nên dễ trùng.`);
|
|
916
|
+
console.log(` Muốn giữ lệnh của bạn: đổi tên nó rồi khôi phục từ backup.`);
|
|
917
|
+
}
|
|
918
|
+
if (sc.removed.length) {
|
|
919
|
+
console.log('');
|
|
920
|
+
console.log(` 🗑️ ${sc.removed.length} shortcut của lệnh đã bị bỏ — đã gỡ khỏi ${label}`);
|
|
921
|
+
for (const f of sc.removed.slice(0, 8)) console.log(` /${f.replace(/\.md$/, '')}`);
|
|
922
|
+
if (sc.removed.length > 8) console.log(` … và ${sc.removed.length - 8} nữa`);
|
|
923
|
+
}
|
|
924
|
+
if (sc.keptEdited.length) {
|
|
925
|
+
console.log('');
|
|
926
|
+
console.log(` ⚠️ ${sc.keptEdited.length} shortcut của lệnh đã bị bỏ NHƯNG bạn đã sửa — giữ lại:`);
|
|
927
|
+
for (const f of sc.keptEdited.slice(0, 8)) console.log(` ${label}${f}`);
|
|
928
|
+
console.log(` Lệnh đích trong .agent/commands/ đã không còn — shortcut này sẽ lỗi khi gọi.`);
|
|
929
|
+
}
|
|
707
930
|
}
|
|
708
931
|
|
|
709
932
|
function installDataGuardHook() {
|
|
@@ -719,34 +942,83 @@ function installDataGuardHook() {
|
|
|
719
942
|
console.log(` ✅ Hook copied to: .claude/hooks/data-guard.js`);
|
|
720
943
|
|
|
721
944
|
// 2. Update .claude/settings.json
|
|
945
|
+
//
|
|
946
|
+
// settings.json HỎNG thì DỪNG, đừng ghi đè. Bản cũ `catch {}` rồi đi tiếp với
|
|
947
|
+
// `settings = {}`, và bước writeFileSync bên dưới ghi đè TOÀN BỘ file bằng đúng
|
|
948
|
+
// một entry hook — tức một dấu phẩy thừa trong settings.json làm mất sạch cấu hình
|
|
949
|
+
// Claude Code của project, im lặng.
|
|
722
950
|
let settings = {};
|
|
723
951
|
if (fs.existsSync(settingsPath)) {
|
|
724
|
-
|
|
952
|
+
const raw = fs.readFileSync(settingsPath, 'utf8');
|
|
953
|
+
try {
|
|
954
|
+
settings = JSON.parse(raw);
|
|
955
|
+
} catch (err) {
|
|
956
|
+
console.log('');
|
|
957
|
+
console.log(` ❌ .claude/settings.json không phải JSON hợp lệ: ${err.message}`);
|
|
958
|
+
console.log(' KHÔNG đăng ký hook — ghi vào đây bây giờ sẽ xoá sạch cấu hình hiện có.');
|
|
959
|
+
console.log(' Sửa file rồi chạy lại với --hooks.');
|
|
960
|
+
console.log('');
|
|
961
|
+
console.log(' ℹ️ Script hook đã được copy; chỉ còn thiếu bước đăng ký. Mẫu:');
|
|
962
|
+
console.log(' .agent/hooks/settings.json');
|
|
963
|
+
return;
|
|
964
|
+
}
|
|
965
|
+
if (settings === null || typeof settings !== 'object' || Array.isArray(settings)) {
|
|
966
|
+
console.log('');
|
|
967
|
+
console.log(' ❌ .claude/settings.json không phải một JSON object — KHÔNG đăng ký hook.');
|
|
968
|
+
console.log('');
|
|
969
|
+
return;
|
|
970
|
+
}
|
|
725
971
|
}
|
|
726
972
|
|
|
727
973
|
// Merge: add PreToolUse hook if not already present
|
|
728
974
|
if (!settings.hooks) settings.hooks = {};
|
|
729
975
|
if (!settings.hooks.PreToolUse) settings.hooks.PreToolUse = [];
|
|
730
976
|
|
|
977
|
+
// `Grep` BẮT BUỘC có mặt: Grep với output_mode:"content" ĐỌC ĐƯỢC nội dung file,
|
|
978
|
+
// nên thiếu nó thì việc chặn Read chỉ là cửa trước, cửa sau mở toang (GAPS-v3 G42a).
|
|
731
979
|
const hookEntry = {
|
|
732
|
-
matcher: 'Read|Write|Edit|Bash',
|
|
980
|
+
matcher: 'Read|Write|Edit|Grep|NotebookEdit|Bash',
|
|
733
981
|
hooks: [{ type: 'command', command: 'node .claude/hooks/data-guard.js' }],
|
|
734
982
|
};
|
|
735
983
|
|
|
736
|
-
|
|
984
|
+
// Tìm entry đã có (nếu có) — KHÔNG chỉ để bỏ qua, mà để NÂNG CẤP matcher.
|
|
985
|
+
//
|
|
986
|
+
// Bản cũ chỉ hỏi "có đăng ký chưa?" rồi bỏ qua nếu có. Nghĩa là mọi project đã cài
|
|
987
|
+
// hook TRƯỚC bản sửa G42 sẽ giữ matcher cũ `Read|Write|Edit|Bash` — thiếu `Grep` —
|
|
988
|
+
// VĨNH VIỄN, và không lần `--hooks` nào sau đó chữa được. Lỗ bảo mật sống sót qua
|
|
989
|
+
// đúng cái lệnh đáng ra phải bịt nó.
|
|
990
|
+
const existing = settings.hooks.PreToolUse.find(
|
|
737
991
|
h => h.hooks && h.hooks.some(hh => hh.command && hh.command.includes('data-guard.js'))
|
|
738
992
|
);
|
|
739
993
|
|
|
740
|
-
|
|
994
|
+
const REQUIRED_TOOLS = hookEntry.matcher.split('|');
|
|
995
|
+
|
|
996
|
+
if (!existing) {
|
|
741
997
|
settings.hooks.PreToolUse.push(hookEntry);
|
|
742
998
|
fs.writeFileSync(settingsPath, JSON.stringify(settings, null, 2) + '\n', 'utf8');
|
|
743
|
-
console.log(
|
|
999
|
+
console.log(' ✅ Hook registered in: .claude/settings.json');
|
|
744
1000
|
} else {
|
|
745
|
-
|
|
1001
|
+
const have = String(existing.matcher || '').split('|').map(s => s.trim());
|
|
1002
|
+
const missing = REQUIRED_TOOLS.filter(t => !have.includes(t));
|
|
1003
|
+
if (missing.length) {
|
|
1004
|
+
existing.matcher = hookEntry.matcher;
|
|
1005
|
+
fs.writeFileSync(settingsPath, JSON.stringify(settings, null, 2) + '\n', 'utf8');
|
|
1006
|
+
console.log(` ✅ Hook matcher nâng cấp: thêm ${missing.join(', ')}`);
|
|
1007
|
+
if (missing.includes('Grep')) {
|
|
1008
|
+
console.log(' (thiếu Grep = lỗ đọc file: Grep với output_mode:"content" trả về nội dung)');
|
|
1009
|
+
}
|
|
1010
|
+
} else {
|
|
1011
|
+
console.log(' ℹ️ Hook already registered in .claude/settings.json');
|
|
1012
|
+
}
|
|
746
1013
|
}
|
|
747
1014
|
|
|
748
1015
|
console.log('');
|
|
749
|
-
console.log(' 🔒 Data guard
|
|
750
|
-
console.log(' .env
|
|
751
|
-
console.log('
|
|
1016
|
+
console.log(' 🔒 Data guard đang canh các tool: Read · Write · Edit · Grep · NotebookEdit · Bash');
|
|
1017
|
+
console.log(' Chặn: .env* · *.key/.pem/.p12/.jks · application-prod.* · .ssh/ .aws/ secrets/');
|
|
1018
|
+
console.log(' file cấu hình/dữ liệu có tên chứa secret|credential|password|token|api-key');
|
|
1019
|
+
console.log('');
|
|
1020
|
+
console.log(' KHÔNG chặn (có chủ ý — đây là công việc của framework):');
|
|
1021
|
+
console.log(' • specs/ .trace/ docs/ .agent/ feedback/ — vùng làm việc, không chứa secret thật');
|
|
1022
|
+
console.log(' • file mã nguồn (.java/.ts/.dart/.kt…) — PasswordResetController.java là code');
|
|
1023
|
+
console.log(' Guard không canh được đường đưa dữ liệu ra ngoài (WebFetch…) — nó là file guard.');
|
|
752
1024
|
}
|