add-coder 0.1.17 → 0.2.1

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.
Files changed (100) hide show
  1. package/README.en.md +108 -10
  2. package/README.md +254 -17
  3. package/dist/index.js +72 -98
  4. package/package.json +2 -2
  5. package/templates/adapters/claude/hooks/doc-format-guard.sh +164 -9
  6. package/templates/adapters/claude/hooks/notification.sh +29 -0
  7. package/templates/adapters/claude/hooks/permission-denied.sh +42 -0
  8. package/templates/adapters/claude/hooks/post-tool-use.sh +25 -8
  9. package/templates/adapters/claude/hooks/pre-compact.sh +31 -8
  10. package/templates/adapters/claude/hooks/pre-tool-use.sh +63 -13
  11. package/templates/adapters/claude/hooks/prompt-submit.sh +70 -8
  12. package/templates/adapters/claude/hooks/session-end.sh +21 -0
  13. package/templates/adapters/claude/hooks/session-start.sh +26 -13
  14. package/templates/adapters/claude/hooks/stop-check.sh +40 -5
  15. package/templates/adapters/claude/hooks/stop-failure.sh +44 -0
  16. package/templates/adapters/claude/hooks/subagent-guard.sh +29 -8
  17. package/templates/adapters/claude/hooks/subagent-stop.sh +20 -0
  18. package/templates/adapters/codex/hooks/notification.sh +38 -0
  19. package/templates/adapters/codex/hooks/post-tool-use.sh +35 -0
  20. package/templates/adapters/codex/hooks/pre-tool-use.sh +78 -0
  21. package/templates/adapters/codex/hooks/prompt-submit.sh +78 -0
  22. package/templates/adapters/codex/hooks/session-start.sh +36 -0
  23. package/templates/adapters/codex/hooks/stop-check.sh +45 -0
  24. package/templates/adapters/codex/hooks.json +1 -0
  25. package/templates/adapters/codex/settings.json +1 -0
  26. package/templates/adapters/qoder/hooks/doc-format-guard.sh +9 -1
  27. package/templates/adapters/qoder/hooks/lib/vocabulary.sh +1 -1
  28. package/templates/adapters/qoder/hooks/notification.sh +8 -5
  29. package/templates/adapters/qoder/hooks/post-tool-use.sh +21 -11
  30. package/templates/adapters/qoder/hooks/pre-compact.sh +31 -6
  31. package/templates/adapters/qoder/hooks/pre-tool-use.sh +66 -64
  32. package/templates/adapters/qoder/hooks/prompt-submit.sh +12 -2
  33. package/templates/adapters/qoder/hooks/session-end.sh +24 -0
  34. package/templates/adapters/qoder/hooks/session-start.sh +22 -7
  35. package/templates/adapters/qoder/hooks/stop-check.sh +18 -41
  36. package/templates/adapters/qoder/hooks/subagent-guard.sh +22 -6
  37. package/templates/adapters/qoder/hooks/subagent-stop.sh +26 -0
  38. package/templates/adapters/qoder/settings.json +13 -13
  39. package/templates/adapters/trae/hooks/notification.sh +38 -0
  40. package/templates/adapters/trae/hooks/post-tool-failure.sh +10 -0
  41. package/templates/adapters/trae/hooks/post-tool-use.sh +35 -0
  42. package/templates/adapters/trae/hooks/pre-compact.sh +37 -0
  43. package/templates/adapters/trae/hooks/pre-tool-use.sh +78 -0
  44. package/templates/adapters/trae/hooks/prompt-submit.sh +78 -0
  45. package/templates/adapters/trae/hooks/session-end.sh +21 -0
  46. package/templates/adapters/trae/hooks/session-start.sh +36 -0
  47. package/templates/adapters/trae/hooks/stop-check.sh +45 -0
  48. package/templates/adapters/trae/hooks/subagent-guard.sh +36 -0
  49. package/templates/adapters/trae/hooks/subagent-stop.sh +20 -0
  50. package/templates/adapters/trae/hooks.json +1 -0
  51. package/templates/adapters/trae/settings.json +1 -0
  52. package/templates/adapters/vscode/.github/hooks/error-occurred.json +1 -0
  53. package/templates/adapters/vscode/.github/hooks/post-tool-use.json +1 -0
  54. package/templates/adapters/vscode/.github/hooks/pre-compact.json +1 -0
  55. package/templates/adapters/vscode/.github/hooks/pre-tool-use.json +14 -0
  56. package/templates/adapters/vscode/.github/hooks/session-end.json +1 -0
  57. package/templates/adapters/vscode/.github/hooks/session-start.json +14 -0
  58. package/templates/adapters/vscode/.github/hooks/stop-check.json +1 -0
  59. package/templates/adapters/vscode/.github/hooks/subagent-start.json +1 -0
  60. package/templates/adapters/vscode/.github/hooks/subagent-stop.json +1 -0
  61. package/templates/adapters/vscode/.github/hooks/user-prompt-submit.json +14 -0
  62. package/templates/adapters/vscode/hooks/notification.sh +37 -0
  63. package/templates/adapters/vscode/hooks/post-tool-failure.sh +28 -0
  64. package/templates/adapters/vscode/hooks/post-tool-use.sh +35 -0
  65. package/templates/adapters/vscode/hooks/pre-compact.sh +37 -0
  66. package/templates/adapters/vscode/hooks/pre-tool-use.sh +78 -0
  67. package/templates/adapters/vscode/hooks/prompt-submit.sh +78 -0
  68. package/templates/adapters/vscode/hooks/session-end.sh +13 -0
  69. package/templates/adapters/vscode/hooks/session-start.sh +35 -0
  70. package/templates/adapters/vscode/hooks/stop-check.sh +53 -0
  71. package/templates/adapters/vscode/hooks/subagent-guard.sh +37 -0
  72. package/templates/adapters/vscode/hooks/subagent-stop.sh +12 -0
  73. package/templates/adapters/vscode/launch.json +1 -1
  74. package/templates/adapters/vscode/settings.json +2 -3
  75. package/templates/adapters/vscode/tasks.json +3 -3
  76. package/templates/core/docs/ADD-governance-claude-code.md +121 -0
  77. package/templates/core/docs/ADD-governance-codex.md +75 -0
  78. package/templates/core/docs/ADD-governance-qoder-cn.md +99 -0
  79. package/templates/core/docs/ADD-governance-trae.md +63 -0
  80. package/templates/core/docs/ADD-governance-vscode-copilot.md +132 -0
  81. package/templates/core/hooks/doc-format-guard.sh +164 -9
  82. package/templates/core/hooks/lib/common.sh +261 -0
  83. package/templates/core/hooks/lib/preload-templates.sh +187 -0
  84. package/templates/core/hooks/lib/session-end.sh +76 -0
  85. package/templates/core/hooks/lib/subagent-stop.sh +90 -0
  86. package/templates/core/hooks/notification.sh +29 -0
  87. package/templates/core/hooks/post-tool-use.sh +25 -8
  88. package/templates/core/hooks/pre-compact.sh +31 -8
  89. package/templates/core/hooks/pre-tool-use.sh +63 -13
  90. package/templates/core/hooks/prompt-submit.sh +70 -8
  91. package/templates/core/hooks/session-end.sh +38 -0
  92. package/templates/core/hooks/session-start.sh +26 -13
  93. package/templates/core/hooks/stop-check.sh +40 -5
  94. package/templates/core/hooks/subagent-stop.sh +47 -0
  95. package/templates/core/rules/project_rules.md +1 -1
  96. package/templates/core/scripts/mcp-server.ts +38 -120
  97. package/templates/core/skills/add-paradigm/SKILL.md +24 -0
  98. package/templates/core/hooks/lib/context-inject.sh +0 -96
  99. package/templates/core/hooks/lib/state-detect.sh +0 -104
  100. package/templates/core/hooks/lib/vocabulary.sh +0 -49
package/dist/index.js CHANGED
@@ -1,7 +1,7 @@
1
1
  #!/usr/bin/env node
2
2
 
3
3
  // src/cli/index.ts
4
- import { readFileSync as readFileSync8 } from "fs";
4
+ import { readFileSync as readFileSync5 } from "fs";
5
5
  import { Command } from "commander";
6
6
 
7
7
  // src/caijuehub/strategies/detect.strategy.ts
@@ -51,7 +51,7 @@ import { join as join2 } from "path";
51
51
 
52
52
  // src/config/schema.ts
53
53
  import { z } from "zod";
54
- var AdapterEnum = z.enum(["claude", "qoder", "vscode"]);
54
+ var AdapterEnum = z.enum(["claude", "qoder", "vscode", "trae", "codex"]);
55
55
  var AddCoderConfigSchema = z.object({
56
56
  projectName: z.string().min(1, "\u9879\u76EE\u540D\u4E0D\u80FD\u4E3A\u7A7A"),
57
57
  projectRoot: z.string().default(""),
@@ -230,6 +230,9 @@ function renderCore(config, dryRun) {
230
230
  const rendered = render(content, config);
231
231
  const targetRel = join4(CORE_TARGET, relative(CORE_DIR, full));
232
232
  files.set(targetRel, rendered);
233
+ if (relative(CORE_DIR, full).startsWith("docs/")) {
234
+ files.set(name, rendered);
235
+ }
233
236
  }
234
237
  }
235
238
  }
@@ -239,107 +242,70 @@ function renderCore(config, dryRun) {
239
242
  }
240
243
  return files;
241
244
  }
242
-
243
- // src/adapters/claude/renderer.ts
244
- import { readFileSync as readFileSync3, readdirSync as readdirSync2, statSync as statSync2 } from "fs";
245
- import { join as join5, relative as relative2, dirname as dirname3 } from "path";
246
- import { fileURLToPath as fileURLToPath2 } from "url";
247
- var __filename2 = fileURLToPath2(import.meta.url);
248
- var __dirname2 = dirname3(__filename2);
249
- var TEMPLATES_ROOT2 = join5(__dirname2, "../templates");
250
- var ADAPTER_DIR = join5(TEMPLATES_ROOT2, "adapters", "claude");
251
- var TARGET_MAGIC_PATH = ".claude";
252
- function renderAdapter(config, targetDir, dryRun) {
245
+ function renderAdapterBase(config, magicPath, githubHooksToRoot, dryRun) {
253
246
  const files = /* @__PURE__ */ new Map();
254
- function walk(dir, base) {
255
- for (const name of readdirSync2(dir)) {
256
- const full = join5(dir, name);
257
- if (statSync2(full).isDirectory()) {
258
- walk(full, join5(base, name));
247
+ const adapterDir = join4(TEMPLATES_ROOT, "adapters", magicPath.replace(".", ""));
248
+ const coreHooksLib = join4(TEMPLATES_ROOT, "core", "hooks", "lib");
249
+ function walk(dir, base, targetPrefix) {
250
+ for (const name of readdirSync(dir)) {
251
+ const full = join4(dir, name);
252
+ if (statSync(full).isDirectory()) {
253
+ walk(full, join4(base, name), targetPrefix);
259
254
  } else {
260
- const content = readFileSync3(full, "utf-8");
255
+ const content = readFileSync2(full, "utf-8");
261
256
  const rendered = render(content, config);
262
- const targetRel = join5(TARGET_MAGIC_PATH, relative2(ADAPTER_DIR, full));
257
+ let targetRel;
258
+ if (targetPrefix) {
259
+ targetRel = join4(targetPrefix, relative(dir, full));
260
+ } else if (githubHooksToRoot && relative(adapterDir, full).startsWith(".github/")) {
261
+ targetRel = relative(adapterDir, full);
262
+ } else {
263
+ targetRel = join4(magicPath, relative(adapterDir, full));
264
+ }
263
265
  files.set(targetRel, rendered);
264
266
  }
265
267
  }
266
268
  }
267
- walk(ADAPTER_DIR, "");
269
+ walk(adapterDir, "");
270
+ walk(coreHooksLib, "", join4(magicPath, "hooks", "lib"));
268
271
  if (dryRun) {
269
- console.log(`[dry-run] Claude adapter: ${files.size} files`);
272
+ console.log(`[dry-run] ${magicPath} adapter: ${files.size} files`);
270
273
  }
271
274
  return files;
272
275
  }
273
276
 
277
+ // src/adapters/claude/renderer.ts
278
+ function renderAdapter(config, targetDir, dryRun, magicDir) {
279
+ return renderAdapterBase(config, magicDir, magicDir === ".vscode", dryRun);
280
+ }
281
+
274
282
  // src/adapters/qoder/renderer.ts
275
- import { readFileSync as readFileSync4, readdirSync as readdirSync3, statSync as statSync3 } from "fs";
276
- import { join as join6, relative as relative3, dirname as dirname4 } from "path";
277
- import { fileURLToPath as fileURLToPath3 } from "url";
278
- var __filename3 = fileURLToPath3(import.meta.url);
279
- var __dirname3 = dirname4(__filename3);
280
- var TEMPLATES_ROOT3 = join6(__dirname3, "../templates");
281
- var ADAPTER_DIR2 = join6(TEMPLATES_ROOT3, "adapters", "qoder");
282
- var TARGET_MAGIC_PATH2 = ".qoder";
283
- function renderAdapter2(config, targetDir, dryRun) {
284
- const files = /* @__PURE__ */ new Map();
285
- function walk(dir, base) {
286
- for (const name of readdirSync3(dir)) {
287
- const full = join6(dir, name);
288
- if (statSync3(full).isDirectory()) {
289
- walk(full, join6(base, name));
290
- } else {
291
- const content = readFileSync4(full, "utf-8");
292
- const rendered = render(content, config);
293
- const targetRel = join6(TARGET_MAGIC_PATH2, relative3(ADAPTER_DIR2, full));
294
- files.set(targetRel, rendered);
295
- }
296
- }
297
- }
298
- walk(ADAPTER_DIR2, "");
299
- if (dryRun) {
300
- console.log(`[dry-run] Qoder adapter: ${files.size} files`);
301
- }
302
- return files;
283
+ function renderAdapter2(config, targetDir, dryRun, magicDir) {
284
+ return renderAdapterBase(config, magicDir, magicDir === ".vscode", dryRun);
303
285
  }
304
286
 
305
287
  // src/adapters/vscode/renderer.ts
306
- import { readFileSync as readFileSync5, readdirSync as readdirSync4, statSync as statSync4 } from "fs";
307
- import { join as join7, relative as relative4, dirname as dirname5 } from "path";
308
- import { fileURLToPath as fileURLToPath4 } from "url";
309
- var __filename4 = fileURLToPath4(import.meta.url);
310
- var __dirname4 = dirname5(__filename4);
311
- var TEMPLATES_ROOT4 = join7(__dirname4, "../templates");
312
- var ADAPTER_DIR3 = join7(TEMPLATES_ROOT4, "adapters", "vscode");
313
- var TARGET_MAGIC_PATH3 = ".vscode";
314
- function renderAdapter3(config, targetDir, dryRun) {
315
- const files = /* @__PURE__ */ new Map();
316
- function walk(dir, base) {
317
- for (const name of readdirSync4(dir)) {
318
- const full = join7(dir, name);
319
- if (statSync4(full).isDirectory()) {
320
- walk(full, join7(base, name));
321
- } else {
322
- const content = readFileSync5(full, "utf-8");
323
- const rendered = render(content, config);
324
- const targetRel = join7(TARGET_MAGIC_PATH3, relative4(ADAPTER_DIR3, full));
325
- files.set(targetRel, rendered);
326
- }
327
- }
328
- }
329
- walk(ADAPTER_DIR3, "");
330
- if (dryRun) {
331
- console.log(`[dry-run] VS Code adapter: ${files.size} files`);
332
- }
333
- return files;
288
+ function renderAdapter3(config, targetDir, dryRun, magicDir) {
289
+ return renderAdapterBase(config, magicDir, magicDir === ".vscode", dryRun);
290
+ }
291
+
292
+ // src/adapters/trae/renderer.ts
293
+ function renderAdapter4(config, targetDir, dryRun, magicDir) {
294
+ return renderAdapterBase(config, magicDir, magicDir === ".vscode", dryRun);
295
+ }
296
+
297
+ // src/adapters/codex/renderer.ts
298
+ function renderAdapter5(config, targetDir, dryRun, magicDir) {
299
+ return renderAdapterBase(config, magicDir, magicDir === ".vscode", dryRun);
334
300
  }
335
301
 
336
302
  // src/cli/prisma-injector.ts
337
- import { resolve as resolve3, dirname as dirname6 } from "path";
338
- import { fileURLToPath as fileURLToPath5 } from "url";
303
+ import { resolve as resolve3, dirname as dirname3 } from "path";
304
+ import { fileURLToPath as fileURLToPath2 } from "url";
339
305
 
340
306
  // src/caijuehub/strategies/prisma.strategy.ts
341
307
  import { spawnSync } from "child_process";
342
- import { copyFileSync, existsSync as existsSync5, readFileSync as readFileSync6, unlinkSync, writeFileSync as writeFileSync2, mkdirSync as mkdirSync2 } from "fs";
308
+ import { copyFileSync, existsSync as existsSync5, readFileSync as readFileSync3, unlinkSync, writeFileSync as writeFileSync2, mkdirSync as mkdirSync2 } from "fs";
343
309
  import { resolve as resolve2 } from "path";
344
310
  var PRISMA_CONFIG = {
345
311
  onMissing: "ask",
@@ -419,10 +385,10 @@ function postInitSetup(projectRoot, schemaPath, addPrismaTemplate, destPath) {
419
385
  const envPath = resolve2(projectRoot, ".env");
420
386
  const devEnvPath = resolve2(projectRoot, ".env.development");
421
387
  if (existsSync5(envPath)) {
422
- const envContent = readFileSync6(envPath, "utf-8");
388
+ const envContent = readFileSync3(envPath, "utf-8");
423
389
  const dbUrl = envContent.match(/DATABASE_URL=.*/);
424
390
  if (dbUrl) {
425
- const existing = existsSync5(devEnvPath) ? readFileSync6(devEnvPath, "utf-8") : "";
391
+ const existing = existsSync5(devEnvPath) ? readFileSync3(devEnvPath, "utf-8") : "";
426
392
  if (!existing.includes("DATABASE_URL=")) {
427
393
  writeFileSync2(devEnvPath, `${existing}${existing ? "\n" : ""}${dbUrl[0]}
428
394
  `, "utf-8");
@@ -476,8 +442,8 @@ async function injectPrisma(projectRoot, addPrismaTemplate, options = {}) {
476
442
  console.log("\u8986\u76D6");
477
443
  } else if (choice === "d") {
478
444
  copyFileSync(destPath, destPath + ".bak");
479
- console.log("=== \u5F53\u524D\uFF08\u5DF2\u5907\u4EFD\uFF09===\n" + readFileSync6(destPath, "utf-8"));
480
- console.log("=== \u6A21\u677F ===\n" + readFileSync6(addPrismaTemplate, "utf-8"));
445
+ console.log("=== \u5F53\u524D\uFF08\u5DF2\u5907\u4EFD\uFF09===\n" + readFileSync3(destPath, "utf-8"));
446
+ console.log("=== \u6A21\u677F ===\n" + readFileSync3(addPrismaTemplate, "utf-8"));
481
447
  if (await ask("\u786E\u8BA4\u8986\u76D6\uFF1F[y/N] ") !== "y") {
482
448
  console.log("\u5DF2\u8DF3\u8FC7");
483
449
  return true;
@@ -524,24 +490,26 @@ async function injectPrisma(projectRoot, addPrismaTemplate, options = {}) {
524
490
  }
525
491
 
526
492
  // src/cli/prisma-injector.ts
527
- var __filename5 = fileURLToPath5(import.meta.url);
528
- var __dirname5 = dirname6(__filename5);
529
- var ADD_PRISMA_TEMPLATE = resolve3(__dirname5, "../templates/core/prisma/add.prisma");
493
+ var __filename2 = fileURLToPath2(import.meta.url);
494
+ var __dirname2 = dirname3(__filename2);
495
+ var ADD_PRISMA_TEMPLATE = resolve3(__dirname2, "../templates/core/prisma/add.prisma");
530
496
  async function injectPrisma2(projectRoot, options = {}) {
531
497
  return injectPrisma(projectRoot, ADD_PRISMA_TEMPLATE, options);
532
498
  }
533
499
 
534
500
  // src/cli/commands/init.ts
535
- import { readFileSync as readFileSync7, writeFileSync as writeFileSync3, existsSync as existsSync6, mkdirSync as mkdirSync3, copyFileSync as copyFileSync2, readdirSync as readdirSync5 } from "fs";
501
+ import { readFileSync as readFileSync4, writeFileSync as writeFileSync3, existsSync as existsSync6, mkdirSync as mkdirSync3, copyFileSync as copyFileSync2, readdirSync as readdirSync2 } from "fs";
536
502
  import { resolve as resolve4 } from "path";
537
503
  import { spawnSync as spawnSync2 } from "child_process";
538
504
  import { createConnection } from "net";
539
505
  var ADAPTER_RENDERERS = {
540
506
  claude: renderAdapter,
541
507
  qoder: renderAdapter2,
542
- vscode: renderAdapter3
508
+ vscode: renderAdapter3,
509
+ trae: renderAdapter4,
510
+ codex: renderAdapter5
543
511
  };
544
- var MAGIC_DIR_MAP = { claude: ".claude", qoder: ".qoder", vscode: ".vscode" };
512
+ var MAGIC_DIR_MAP = { claude: ".claude", qoder: ".qoder", vscode: ".vscode", trae: ".trae", codex: ".codex" };
545
513
  async function resolveAdapter(projectRoot, specified) {
546
514
  if (specified) {
547
515
  if (!MAGIC_DIR_MAP[specified]) throw new Error(`\u672A\u77E5 adapter: ${specified}`);
@@ -712,7 +680,7 @@ function injectDbExportScript(projectRoot, dryRun) {
712
680
  console.log("[dry-run] \u5C06\u6CE8\u5165 db:export");
713
681
  return;
714
682
  }
715
- const pkg = JSON.parse(readFileSync7(pkgPath, "utf-8"));
683
+ const pkg = JSON.parse(readFileSync4(pkgPath, "utf-8"));
716
684
  if (!pkg.scripts) pkg.scripts = {};
717
685
  if (!pkg.scripts["db:export"]) {
718
686
  pkg.scripts["db:export"] = "npx tsx scripts/export-db.ts";
@@ -745,7 +713,7 @@ async function initCommand(options) {
745
713
  }
746
714
  const devEnvPath = resolve4(projectRoot, ".env.development");
747
715
  if (!options.dryRun && existsSync6(devEnvPath)) {
748
- const existing = readFileSync7(devEnvPath, "utf-8");
716
+ const existing = readFileSync4(devEnvPath, "utf-8");
749
717
  if (!/^DATABASE_USER=/m.test(existing)) {
750
718
  writeFileSync3(devEnvPath, existing + `
751
719
  DATABASE_USER=${db.user || "admin"}
@@ -767,14 +735,20 @@ PROJECT_NAME=${config.projectName || "add-project"}
767
735
  if (!allFiles.has(targetPath)) allFiles.set(targetPath, content);
768
736
  }
769
737
  }
770
- for (const adapter of resolveAdapters2(target)) {
738
+ const resolved = resolveAdapters2(target);
739
+ for (const adapter of resolved) {
771
740
  const renderFn = ADAPTER_RENDERERS[adapter];
772
741
  if (renderFn) {
773
- const adapterFiles = renderFn(config, projectRoot, !!options.dryRun);
742
+ const adapterFiles = renderFn(config, projectRoot, !!options.dryRun, magicDir);
774
743
  for (const [p, c] of adapterFiles) allFiles.set(p, c);
775
744
  console.log(`${adapter} adapter: ${adapterFiles.size} \u6587\u4EF6`);
776
745
  }
777
746
  }
747
+ if (resolved.includes("vscode") || resolved.includes("trae") || resolved.includes("codex")) {
748
+ const claudeFiles = renderAdapter(config, projectRoot, !!options.dryRun, ".claude");
749
+ for (const [p, c] of claudeFiles) allFiles.set(p, c);
750
+ console.log(`claude adapter (via Agent Host): ${claudeFiles.size} \u6587\u4EF6`);
751
+ }
778
752
  const result = await writeFiles2(projectRoot, allFiles, { force: options.force, dryRun: options.dryRun });
779
753
  if (db.engine === "postgresql" && db.container && db.container !== "manual") {
780
754
  if (!options.dryRun) {
@@ -822,7 +796,7 @@ PROJECT_NAME=${config.projectName || "add-project"}
822
796
  const destDir = resolve4(docsBase, d);
823
797
  if (!existsSync6(destDir)) mkdirSync3(destDir, { recursive: true });
824
798
  if (!existsSync6(srcDir)) continue;
825
- for (const f of readdirSync5(srcDir)) {
799
+ for (const f of readdirSync2(srcDir)) {
826
800
  const src = resolve4(srcDir, f);
827
801
  const dest = resolve4(destDir, f);
828
802
  if (existsSync6(dest)) continue;
@@ -837,7 +811,7 @@ PROJECT_NAME=${config.projectName || "add-project"}
837
811
  \u5B8C\u6210: \u65B0\u5EFA ${result.created}, \u8DF3\u8FC7 ${result.skipped}, \u8986\u76D6 ${result.overwritten}`);
838
812
  if (!options.dryRun) {
839
813
  if (db.engine === "sqlite") console.log("\u6570\u636E\u5907\u4EFD: npm run db:export \u2192 data/exports/");
840
- const pkg = JSON.parse(readFileSync7(resolve4(import.meta.dirname, "../package.json"), "utf-8"));
814
+ const pkg = JSON.parse(readFileSync4(resolve4(import.meta.dirname, "../package.json"), "utf-8"));
841
815
  const peerNames = Object.keys(pkg.peerDependencies || {});
842
816
  if (peerNames.length > 0) {
843
817
  console.log(`
@@ -902,7 +876,7 @@ async function statusCommand() {
902
876
 
903
877
  // src/cli/index.ts
904
878
  var { version } = JSON.parse(
905
- readFileSync8(new URL("../package.json", import.meta.url), "utf-8")
879
+ readFileSync5(new URL("../package.json", import.meta.url), "utf-8")
906
880
  );
907
881
  var program = new Command();
908
882
  program.name("add-coder").description("\u521D\u59CB\u5316 ADD \u8303\u5F0F\u5DE5\u4F5C\u6D41\u6A21\u677F").version(version);
package/package.json CHANGED
@@ -1,8 +1,8 @@
1
1
  {
2
2
  "name": "add-coder",
3
- "version": "0.1.17",
3
+ "version": "0.2.1",
4
4
  "private": false,
5
- "description": "[codein2027](https://github.com/xiaomingming92/codein2027) 快速构建 ADD 编程范式的完整脚手架——AI 代码治理的落地方案。以「审计即基础设施」为核心,彻底打破编程过程黑盒与跨轮失忆,让编程范式进化为可审计、可追溯、可收敛的新时代。npx 即用,人人可体验。",
5
+ "description": "[codein2027](https://github.com/xiaomingming92/codein2027) - A complete scaffolding for building the ADD programming paradigm, the implementation layer for AI code governance. Core principle: Audit as Infrastructure. Breaks the black-box programming process and cross-session amnesia, evolving programming paradigms into an auditable, traceable, and convergent new era. npx-ready.\n\n[codein2027](https://github.com/xiaomingming92/codein2027) 快速构建 ADD 编程范式的完整脚手架——AI 代码治理的落地方案。以「审计即基础设施」为核心,彻底打破编程过程黑盒与跨轮失忆,让编程范式进化为可审计、可追溯、可收敛的新时代。npx 即用,人人可体验。",
6
6
  "repository": {
7
7
  "type": "git",
8
8
  "url": "https://github.com/xiaomingming92/add-coder.git"
@@ -1,17 +1,172 @@
1
1
  #!/bin/bash
2
- # doc-format-guard.sh — 文档格式守卫(Claude Code 适配)
2
+ # doc-format-guard.sh — schema.json 驱动的 ADD 文档格式守卫
3
3
  set -euo pipefail
4
4
 
5
- HOOK_DIR="$(cd "$(dirname "$0")" && pwd)"
6
- SHARED_LIB="$HOOK_DIR/../../shared/hooks-lib/common.sh"
7
- [ -f "$SHARED_LIB" ] && source "$SHARED_LIB"
5
+ input=$(cat)
8
6
 
9
- input=$(parse_input)
7
+ # DEBUG: dump stdin for investigation
8
+ mkdir -p .qoder/debug-dump
9
+ echo "=== $(date) ===" >> .qoder/debug-dump/stdin.log
10
+ echo "file_path: $(echo "$input" | jq -r '.tool_input.file_path // "EMPTY"')" >> .qoder/debug-dump/stdin.log
11
+ echo "has_file_content: $(echo "$input" | jq 'has("tool_input") and (.tool_input | has("file_content"))')" >> .qoder/debug-dump/stdin.log
12
+ echo "has_replacements: $(echo "$input" | jq 'has("tool_input") and (.tool_input | has("replacements"))')" >> .qoder/debug-dump/stdin.log
13
+ echo "top_keys: $(echo "$input" | jq -r 'keys | join(", ")')" >> .qoder/debug-dump/stdin.log
14
+ echo "tool_input_keys: $(echo "$input" | jq -r '.tool_input | keys | join(", ") // "NO_TOOL_INPUT"')" >> .qoder/debug-dump/stdin.log
15
+ echo "=== DONE ===" >> .qoder/debug-dump/stdin.log
10
16
  file_path=$(echo "$input" | jq -r '.tool_input.file_path // empty')
17
+ # L17: 非文件工具事件(空 stdin)→ 不拦截(由 matcher 层过滤)
11
18
  [ -z "$file_path" ] && exit 0
12
19
 
13
- # 检查 ADD 文档格式
14
- if echo "$file_path" | grep -qE '\.qoder/(specs|plans|reviews)/.*\.md$'; then
15
- echo "[ADD DocFormatGuard] 检测到 ADD 文档变更,请确保格式符合模板规范。"
20
+ if ! echo "$file_path" | grep -qE '\.qoder/(plans|specs)/'; then
21
+ exit 0
16
22
  fi
17
- exit 0
23
+
24
+ CONTENT=$(echo "$input" | jq -r '
25
+ if .tool_input.file_content then .tool_input.file_content
26
+ elif .tool_input.content then .tool_input.content
27
+ elif .tool_input.replacements then .tool_input.replacements[0].new_text
28
+ else "" end')
29
+ # L24: PostToolUse 不可阻断(仅反馈),PreToolUse exit 2 + ask 可拦截
30
+ # L24: 文件在 .qoder/(plans|specs)/ 下但 Write 工具未传 content → 无法校验,阻断
31
+ if [ -z "$CONTENT" ]; then
32
+ echo "⛔ 拒绝:Write 工具未传 file_content,无法校验手写文档格式" >&2
33
+ echo '{"hookSpecificOutput":{"hookEventName":"PreToolUse","permissionDecision":"ask","permissionDecisionReason":"Write 工具未传 file_content,无法校验手写文档。请用 SearchReplace 改写已有文件,或用 Write 工具重试。"}}'
34
+ exit 2
35
+ fi
36
+
37
+ PROJECT_DIR="${QODER_PROJECT_DIR:-${QODERCN_PROJECT_DIR:-$PWD}}"
38
+ TEMPLATES_DIR="$PROJECT_DIR/.qoder/templates"
39
+
40
+ # 通过 filename 匹配模板名
41
+ TEMPLATE_NAME=""
42
+ for tmpl in "$TEMPLATES_DIR"/*.md; do
43
+ base=$(basename "$tmpl")
44
+ if echo "$file_path" | grep -q "$base"; then
45
+ TEMPLATE_NAME="$base"
46
+ break
47
+ fi
48
+ done
49
+
50
+ # 退一步:根据文件内容特征猜测模板类型
51
+ if [ -z "$TEMPLATE_NAME" ]; then
52
+ if echo "$CONTENT" | grep -q "## 四、Handoff"; then
53
+ TEMPLATE_NAME="simple-standard-plan-template.md"
54
+ elif echo "$CONTENT" | grep -q "## PLAN 元信息"; then
55
+ TEMPLATE_NAME="standard-plan-template.md"
56
+ elif echo "$CONTENT" | grep -q "## Review 元信息"; then
57
+ if echo "$CONTENT" | grep -q "运行时验证"; then
58
+ TEMPLATE_NAME="review-runtime-template.md"
59
+ elif echo "$CONTENT" | grep -q "跨仓库格式契约"; then
60
+ TEMPLATE_NAME="review-implementation-template.md"
61
+ else
62
+ TEMPLATE_NAME="review-template.md"
63
+ fi
64
+ elif echo "$CONTENT" | grep -q "## Why"; then
65
+ TEMPLATE_NAME="spec-template.md"
66
+ elif echo "$CONTENT" | grep -q "## Preconditions"; then
67
+ TEMPLATE_NAME="tasks-template.md"
68
+ elif echo "$CONTENT" | grep -q "审计链(证据→devlog→checklist)"; then
69
+ TEMPLATE_NAME="checklist-template.md"
70
+ else
71
+ case "$file_path" in
72
+ *handoff*)
73
+ # ★ 按内容特征区分单/多轮 handoff,不依赖文件名
74
+ if echo "$CONTENT" | grep -qF "## 全局元信息"; then
75
+ TEMPLATE_NAME="handoff-multi-round-template.md"
76
+ elif echo "$CONTENT" | grep -qF "## 1. 交接前状态"; then
77
+ TEMPLATE_NAME="handoff-single-round-template.md"
78
+ else
79
+ echo "⛔ handoff 文件内容无法识别模板类型(缺 '## 全局元信息' 或 '## 1. 交接前状态'),拒绝写入" >&2
80
+ echo '{"hookSpecificOutput":{"hookEventName":"PreToolUse","permissionDecision":"ask","permissionDecisionReason":"handoff 内容不符合 single/multi 模板规范"}}'
81
+ exit 2
82
+ fi
83
+ ;;
84
+ *plan*) TEMPLATE_NAME="standard-plan-template.md" ;;
85
+ *add-route*heavy*) TEMPLATE_NAME="add-route-template-heavyweight.md" ;;
86
+ *add-route*) TEMPLATE_NAME="add-route-template.md" ;;
87
+ *tasks*) TEMPLATE_NAME="tasks-template.md" ;;
88
+ *spec*) TEMPLATE_NAME="spec-template.md" ;;
89
+ *checklist*) TEMPLATE_NAME="checklist-template.md" ;;
90
+ *report*runtime*) TEMPLATE_NAME="runtime-report-template.md" ;;
91
+ *report*) TEMPLATE_NAME="report-template.md" ;;
92
+ *fix-verif*) TEMPLATE_NAME="fix-verification-template.md" ;;
93
+ *)
94
+ # 增量修订识别:包含 ~~删除线~~ / → 新增标记 / [修订日期] 任意一个 → 视为增量更新,放行
95
+ if echo "$CONTENT" | grep -qE '~~.+~~|→|\[[0-9]{4}-[0-9]{2}-[0-9]{2}\s+修订'; then
96
+ echo "[doc-format-guard] 检测到增量修订格式,跳过完整章节校验" >&2
97
+ exit 0
98
+ fi
99
+ echo "⛔ 拒绝:无法识别文档类型 (file_path: $file_path),缺少模板匹配规则" >&2
100
+ echo '{"hookSpecificOutput":{"hookEventName":"PreToolUse","permissionDecision":"ask","permissionDecisionReason":"无法识别 ADD 文档类型,请联系管理员更新 doc-format-guard.sh"}}'
101
+ exit 2
102
+ ;;
103
+ esac
104
+ fi
105
+ fi
106
+
107
+ SCHEMA_FILE="$TEMPLATES_DIR/$(echo "${TEMPLATE_NAME%.md}" | sed 's/-template$//').schema.json"
108
+ # L84: schema 文件不存在 → 无校验规则,阻断(不允许无规则放行)
109
+ if [ ! -f "$SCHEMA_FILE" ]; then
110
+ echo "⛔ 阻断:模板 ${TEMPLATE_NAME} 缺少对应的 .schema.json 校验规则" >&2
111
+ echo '{"hookSpecificOutput":{"hookEventName":"PreToolUse","permissionDecision":"ask","permissionDecisionReason":"缺少 .schema.json 校验规则文件,禁止无规则放行"}}'
112
+ exit 2
113
+ fi
114
+
115
+ # SearchReplace 只传 patch(replacements[].new_text),不传全文件 → 跳过章节校验
116
+ IS_SEARCH_REPLACE=$(echo "$input" | jq -r '.tool_input.replacements | length > 0 // false')
117
+
118
+ ISSUES=""
119
+
120
+ # ── 章节校验 ──
121
+ # 使用项目目录下的临时文件,避免 /tmp 在沙箱中不可写导致静默跳过
122
+ TMPFILE="$PROJECT_DIR/.qoder/.doc-guard-issues.tmp"
123
+ : > "$TMPFILE"
124
+ trap "rm -f $TMPFILE" EXIT
125
+ # SearchReplace 只传 patch → 跳过章节/子章节校验(无法从 patch 推断完整文档结构)
126
+ if [ "$IS_SEARCH_REPLACE" != "true" ]; then
127
+ jq -r '.sections[] | select(.required == true) | .heading' "$SCHEMA_FILE" 2>/dev/null | while IFS= read -r heading; do
128
+ if ! echo "$CONTENT" | grep -qF "$heading"; then
129
+ echo " 缺章节: $heading"
130
+ fi
131
+ done > "$TMPFILE"
132
+
133
+ # 子章节
134
+ jq -r '.sections[].subsections[]?.heading' "$SCHEMA_FILE" 2>/dev/null | while IFS= read -r sub; do
135
+ if ! echo "$CONTENT" | grep -qF "$sub"; then
136
+ echo " 缺子章节: $sub"
137
+ fi
138
+ done >> "$TMPFILE"
139
+ fi
140
+
141
+ # ── 占位符校验 ──
142
+ jq -r '.placeholders[]?' "$SCHEMA_FILE" 2>/dev/null | while IFS= read -r ph; do
143
+ if echo "$CONTENT" | grep -qF "$ph"; then
144
+ echo " 未替换占位符: $ph"
145
+ fi
146
+ done >> "$TMPFILE"
147
+
148
+ # ── 禁止词校验 ──
149
+ jq -r '.forbidden_terms[]?' "$SCHEMA_FILE" 2>/dev/null | while IFS= read -r term; do
150
+ if echo "$CONTENT" | grep -qw "$term"; then
151
+ echo " 禁止词: $term"
152
+ fi
153
+ done >> "$TMPFILE"
154
+
155
+ ISSUES=$(cat "$TMPFILE" 2>/dev/null)
156
+
157
+ # ── 阻断或放行 ──
158
+ if [ -n "$ISSUES" ]; then
159
+ echo "⛔ $TEMPLATE_NAME 校验不通过:
160
+ $ISSUES" >&2
161
+ echo "{\"hookSpecificOutput\":{\"hookEventName\":\"PreToolUse\",\"permissionDecision\":\"ask\",\"permissionDecisionReason\":\"文档格式校验不通过,请对照模板修正\"}}"
162
+ exit 2
163
+ fi
164
+
165
+ # ── 自动更新 index.md ──
166
+ if echo "$file_path" | grep -q '\.qoder/plans/'; then
167
+ if [ -x "$PROJECT_DIR/scripts/gen-plan-index.sh" ]; then
168
+ "$PROJECT_DIR/scripts/gen-plan-index.sh" 2>/dev/null || true
169
+ fi
170
+ fi
171
+
172
+ exit 0
@@ -1,4 +1,33 @@
1
1
  #!/bin/bash
2
+ # Notification — Review 提醒 + Token 预警(Claude Code 适配)
3
+ # 治理卡位 #12: 开发提醒/Token 预警
4
+ set -euo pipefail
5
+
6
+ input=$(cat)
7
+ ntype=$(echo "$input" | jq -r '.notification_type // ""' 2>/dev/null || echo "")
8
+
9
+ if [ "$ntype" != "result" ]; then
10
+ exit 0
11
+ fi
12
+
13
+ HOOK_DIR="$(cd "$(dirname "$0")" && pwd)"
14
+ export CURRENT_MAGIC=$(basename "$(dirname "$HOOK_DIR")")
15
+ COMMON_LIB="$HOOK_DIR/lib/common.sh"
16
+ [ -f "$COMMON_LIB" ] && source "$COMMON_LIB"
17
+
18
+ export PROJECT_DIR="${CLAUDE_PROJECT_DIR:-$PWD}"
19
+
20
+ state=$(detect_active_add 2>/dev/null || true)
21
+ [ -z "$state" ] && exit 0
22
+
23
+ IFS='::' read -r plan _ _ _ _ <<< "$state"
24
+
25
+ reviews_dir="${PROJECT_DIR}/.qoder/reviews"
26
+ if ls "$reviews_dir"/*.md >/dev/null 2>&1; then
27
+ echo "[ADD Notification] Plan: ${plan} — 请检查 Review 文档: ${reviews_dir}"
28
+ fi
29
+ exit 0
30
+ #!/bin/bash
2
31
  # Notification — 通知事件处理(Claude Code 适配)
3
32
  set -euo pipefail
4
33
 
@@ -0,0 +1,42 @@
1
+ #!/bin/bash
2
+ ###
3
+ # @Author : xiaomingming wujixmm@gmail.com
4
+ # @Date : 2026-07-17 13:36:32
5
+ # @LastEditors : xiaomingming wujixmm@gmail.com
6
+ # @LastEditTime : 2026-07-17 13:36:33
7
+ # @FilePath : /add-coder/templates/adapters/claude/hooks/permission-denied.sh
8
+ # @Description :
9
+ ###
10
+ # permission-denied.sh — Claude Code PermissionDenied:拒绝记录 + 替代方案
11
+ # 治理卡位 #14: 拒绝原因记录 + 替代方案注入
12
+ # Claude Code 独有事件
13
+ set -euo pipefail
14
+
15
+ HOOK_DIR="$(cd "$(dirname "$0")" && pwd)"
16
+ COMMON_LIB="$HOOK_DIR/lib/common.sh"
17
+ [ -f "$COMMON_LIB" ] && source "$COMMON_LIB"
18
+
19
+ input=$(parse_input)
20
+ tool_name=$(json_get "$input" "tool_name")
21
+ [ -z "$tool_name" ] && tool_name=$(echo "$input" | grep -o '"tool_name"[[:space:]]*:[[:space:]]*"[^"]*"' 2>/dev/null | sed 's/.*: *"\([^"]*\)".*/\1/' || echo "unknown")
22
+
23
+ reason=$(json_get "$input" "reason")
24
+ [ -z "$reason" ] && reason=$(echo "$input" | grep -o '"reason"[[:space:]]*:[[:space:]]*"[^"]*"' 2>/dev/null | sed 's/.*: *"\([^"]*\)".*/\1/' || echo "权限被拒绝")
25
+
26
+ # 记录拒绝
27
+ echo "[ADD PermissionDenied] ${tool_name} 被拒绝: ${reason}" >&2
28
+
29
+ # 按工具类型提供替代建议
30
+ case "$tool_name" in
31
+ Bash)
32
+ echo "[ADD PermissionDenied] 建议: 使用安全的等价命令,或通过 permission-gate.sh 白名单放行" >&2
33
+ ;;
34
+ Write|Edit)
35
+ echo "[ADD PermissionDenied] 建议: 检查目标路径是否在项目范围内,敏感文件(.env等)不可写入" >&2
36
+ ;;
37
+ Read)
38
+ echo "[ADD PermissionDenied] 建议: 该文件可能受读保护,尝试读取项目公有文件替代" >&2
39
+ ;;
40
+ esac
41
+
42
+ exit 0
@@ -1,18 +1,35 @@
1
1
  #!/bin/bash
2
- # post-tool-use.sh — Claude Code PostToolUse 审计提醒
2
+ # post-tool-use.sh — Claude Code PostToolUse:格式化 + 文档守卫 + 审计提醒
3
+ # 治理卡位 #5: 格式化 + ADD文档守卫 + 审计落库 + 结果增强
3
4
  set -euo pipefail
4
5
 
5
6
  HOOK_DIR="$(cd "$(dirname "$0")" && pwd)"
6
- SHARED_LIB="$HOOK_DIR/../../shared/hooks-lib/common.sh"
7
- [ -f "$SHARED_LIB" ] && source "$SHARED_LIB"
7
+ COMMON_LIB="$HOOK_DIR/lib/common.sh"
8
+ [ -f "$COMMON_LIB" ] && source "$COMMON_LIB"
8
9
 
9
10
  input=$(parse_input)
10
- file_path=$(echo "$input" | jq -r '.tool_input.file_path // empty')
11
- [ -z "$file_path" ] && exit 0
11
+ tool_name=$(json_get "$input" "tool_name")
12
+ [ -z "$tool_name" ] && tool_name=$(echo "$input" | grep -o '"tool_name"[[:space:]]*:[[:space:]]*"[^"]*"' 2>/dev/null | sed 's/.*: *"\([^"]*\)".*/\1/' || echo "")
12
13
 
13
- if ! echo "$file_path" | grep -qE 'src/agents/|src/lib/agent-audit-logger\.ts|src/types/|prisma/schema\.prisma'; then
14
+ # ── Edit/Write matcher: 格式化 + ADD文档守卫 ──
15
+ if [ "$tool_name" = "Edit" ] || [ "$tool_name" = "Write" ]; then
16
+ file_path=$(echo "$input" | grep -o '"file_path"[[:space:]]*:[[:space:]]*"[^"]*"' 2>/dev/null | sed 's/.*: *"\([^"]*\)".*/\1/' || echo "")
17
+ [ -z "$file_path" ] && exit 0
18
+
19
+ # ADD 文档结构守卫:写入 plans/specs/reviews 后提示
20
+ if echo "$file_path" | grep -qE '\.(qoder|claude|add)/(plans|specs|reviews)/'; then
21
+ echo "[ADD PostToolUse] ADD 文档已写入: ${file_path}。请确保章节完整、双向链接齐全、增量修订格式正确。" >&2
22
+ fi
23
+
24
+ # 审计提醒(所有文件写入)
25
+ echo "[ADD PostToolUse] 文件已写入: ${file_path}。请执行 record_dev_operation 落库审计(ADD-7)。" >&2
26
+ exit 0
27
+ fi
28
+
29
+ # ── Bash matcher: 结果增强 ──
30
+ if [ "$tool_name" = "Bash" ]; then
31
+ echo "[ADD PostToolUse] 命令执行完成。如有 lint/tsc 错误请修复。" >&2
14
32
  exit 0
15
33
  fi
16
34
 
17
- echo "[ADD PostToolUse] 文件已写入: ${file_path}。如果尚未记录,请执行 record_dev_operation 落库审计。"
18
- exit 0
35
+ exit 0