add-coder 0.1.15 → 0.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/README.en.md +13 -3
- package/README.md +9 -8
- package/dist/index.js +73 -99
- package/package.json +5 -1
- package/templates/adapters/claude/hooks/doc-format-guard.sh +164 -9
- package/templates/adapters/claude/hooks/notification.sh +29 -0
- package/templates/adapters/claude/hooks/permission-denied.sh +42 -0
- package/templates/adapters/claude/hooks/post-tool-use.sh +25 -8
- package/templates/adapters/claude/hooks/pre-compact.sh +31 -8
- package/templates/adapters/claude/hooks/pre-tool-use.sh +63 -13
- package/templates/adapters/claude/hooks/prompt-submit.sh +70 -8
- package/templates/adapters/claude/hooks/session-end.sh +21 -0
- package/templates/adapters/claude/hooks/session-start.sh +26 -13
- package/templates/adapters/claude/hooks/stop-check.sh +40 -5
- package/templates/adapters/claude/hooks/stop-failure.sh +44 -0
- package/templates/adapters/claude/hooks/subagent-guard.sh +29 -8
- package/templates/adapters/claude/hooks/subagent-stop.sh +20 -0
- package/templates/adapters/codex/hooks/notification.sh +38 -0
- package/templates/adapters/codex/hooks/post-tool-use.sh +35 -0
- package/templates/adapters/codex/hooks/pre-tool-use.sh +78 -0
- package/templates/adapters/codex/hooks/prompt-submit.sh +78 -0
- package/templates/adapters/codex/hooks/session-start.sh +36 -0
- package/templates/adapters/codex/hooks/stop-check.sh +45 -0
- package/templates/adapters/codex/hooks.json +1 -0
- package/templates/adapters/codex/settings.json +1 -0
- package/templates/adapters/qoder/hooks/doc-format-guard.sh +9 -1
- package/templates/adapters/qoder/hooks/lib/vocabulary.sh +1 -1
- package/templates/adapters/qoder/hooks/notification.sh +8 -5
- package/templates/adapters/qoder/hooks/post-tool-use.sh +21 -11
- package/templates/adapters/qoder/hooks/pre-compact.sh +31 -6
- package/templates/adapters/qoder/hooks/pre-tool-use.sh +66 -64
- package/templates/adapters/qoder/hooks/prompt-submit.sh +12 -2
- package/templates/adapters/qoder/hooks/session-end.sh +24 -0
- package/templates/adapters/qoder/hooks/session-start.sh +22 -7
- package/templates/adapters/qoder/hooks/stop-check.sh +18 -41
- package/templates/adapters/qoder/hooks/subagent-guard.sh +22 -6
- package/templates/adapters/qoder/hooks/subagent-stop.sh +26 -0
- package/templates/adapters/qoder/settings.json +13 -13
- package/templates/adapters/trae/hooks/notification.sh +38 -0
- package/templates/adapters/trae/hooks/post-tool-failure.sh +10 -0
- package/templates/adapters/trae/hooks/post-tool-use.sh +35 -0
- package/templates/adapters/trae/hooks/pre-compact.sh +37 -0
- package/templates/adapters/trae/hooks/pre-tool-use.sh +78 -0
- package/templates/adapters/trae/hooks/prompt-submit.sh +78 -0
- package/templates/adapters/trae/hooks/session-end.sh +21 -0
- package/templates/adapters/trae/hooks/session-start.sh +36 -0
- package/templates/adapters/trae/hooks/stop-check.sh +45 -0
- package/templates/adapters/trae/hooks/subagent-guard.sh +36 -0
- package/templates/adapters/trae/hooks/subagent-stop.sh +20 -0
- package/templates/adapters/trae/hooks.json +1 -0
- package/templates/adapters/trae/settings.json +1 -0
- package/templates/adapters/vscode/.github/hooks/error-occurred.json +1 -0
- package/templates/adapters/vscode/.github/hooks/post-tool-use.json +1 -0
- package/templates/adapters/vscode/.github/hooks/pre-compact.json +1 -0
- package/templates/adapters/vscode/.github/hooks/pre-tool-use.json +14 -0
- package/templates/adapters/vscode/.github/hooks/session-end.json +1 -0
- package/templates/adapters/vscode/.github/hooks/session-start.json +14 -0
- package/templates/adapters/vscode/.github/hooks/stop-check.json +1 -0
- package/templates/adapters/vscode/.github/hooks/subagent-start.json +1 -0
- package/templates/adapters/vscode/.github/hooks/subagent-stop.json +1 -0
- package/templates/adapters/vscode/.github/hooks/user-prompt-submit.json +14 -0
- package/templates/adapters/vscode/hooks/notification.sh +37 -0
- package/templates/adapters/vscode/hooks/post-tool-failure.sh +28 -0
- package/templates/adapters/vscode/hooks/post-tool-use.sh +35 -0
- package/templates/adapters/vscode/hooks/pre-compact.sh +37 -0
- package/templates/adapters/vscode/hooks/pre-tool-use.sh +78 -0
- package/templates/adapters/vscode/hooks/prompt-submit.sh +78 -0
- package/templates/adapters/vscode/hooks/session-end.sh +13 -0
- package/templates/adapters/vscode/hooks/session-start.sh +35 -0
- package/templates/adapters/vscode/hooks/stop-check.sh +53 -0
- package/templates/adapters/vscode/hooks/subagent-guard.sh +37 -0
- package/templates/adapters/vscode/hooks/subagent-stop.sh +12 -0
- package/templates/adapters/vscode/launch.json +1 -1
- package/templates/adapters/vscode/settings.json +2 -3
- package/templates/adapters/vscode/tasks.json +3 -3
- package/templates/core/docs/ADD-governance-claude-code.md +121 -0
- package/templates/core/docs/ADD-governance-codex.md +75 -0
- package/templates/core/docs/ADD-governance-qoder-cn.md +99 -0
- package/templates/core/docs/ADD-governance-trae.md +63 -0
- package/templates/core/docs/ADD-governance-vscode-copilot.md +132 -0
- package/templates/core/hooks/doc-format-guard.sh +164 -9
- package/templates/core/hooks/lib/common.sh +261 -0
- package/templates/core/hooks/lib/preload-templates.sh +187 -0
- package/templates/core/hooks/lib/session-end.sh +76 -0
- package/templates/core/hooks/lib/subagent-stop.sh +90 -0
- package/templates/core/hooks/notification.sh +29 -0
- package/templates/core/hooks/post-tool-use.sh +25 -8
- package/templates/core/hooks/pre-compact.sh +31 -8
- package/templates/core/hooks/pre-tool-use.sh +63 -13
- package/templates/core/hooks/prompt-submit.sh +70 -8
- package/templates/core/hooks/session-end.sh +38 -0
- package/templates/core/hooks/session-start.sh +26 -13
- package/templates/core/hooks/stop-check.sh +40 -5
- package/templates/core/hooks/subagent-stop.sh +47 -0
- package/templates/core/rules/project_rules.md +1 -1
- package/templates/core/scripts/db-ensure.sh +6 -5
- package/templates/core/scripts/mcp-server.ts +38 -120
- package/templates/core/skills/add-paradigm/SKILL.md +24 -0
- package/templates/core/templates/standard-plan-template.md +39 -13
- package/templates/core/hooks/lib/context-inject.sh +0 -96
- package/templates/core/hooks/lib/state-detect.sh +0 -104
- package/templates/core/hooks/lib/vocabulary.sh +0 -49
package/README.en.md
CHANGED
|
@@ -67,7 +67,15 @@ Hooks are not "notification push" — they are the **IDE runtime interception la
|
|
|
67
67
|
| PreCompact | Forces retention of critical document paths during cross-session context compression |
|
|
68
68
|
| PromptSubmit | Injects ADD vocabulary triggers, ensuring zero-latency LLM response to commands like "acceptance" and "gateway" |
|
|
69
69
|
|
|
70
|
-
Each IDE
|
|
70
|
+
Each IDE(Claude Code / Qoder CN / VS Code Copilot / Trae / Codex)has its own hook implementation, but the **governance logic is unified** — the architecture is consistent, only the adapter layer differs.
|
|
71
|
+
|
|
72
|
+
| IDE | Governance Doc | Events Covered | Hook Config |
|
|
73
|
+
|---|---|---|---|
|
|
74
|
+
| Claude Code | [ADD-governance-claude-code.md](./ADD-governance-claude-code.md) | 14/17 | `.claude/hooks/*.sh` |
|
|
75
|
+
| Qoder CN | [ADD-governance-qoder-cn.md](./ADD-governance-qoder-cn.md) | 10/17 | `.qoder/hooks/*.sh` |
|
|
76
|
+
| VS Code Copilot | [ADD-governance-vscode-copilot.md](./ADD-governance-vscode-copilot.md) | 10/17 | `.github/hooks/*.json` → `.vscode/hooks/*.sh` |
|
|
77
|
+
| Trae | [ADD-governance-trae.md](./ADD-governance-trae.md) | 6/17 | `hooks.json` → `.trae/hooks/*.sh` |
|
|
78
|
+
| Codex | [ADD-governance-codex.md](./ADD-governance-codex.md) | 0 native / 14 (via Claude import) | `.codex/hooks.json` |
|
|
71
79
|
|
|
72
80
|
---
|
|
73
81
|
|
|
@@ -116,7 +124,7 @@ npx add-coder init
|
|
|
116
124
|
|
|
117
125
|
| Option | Description |
|
|
118
126
|
|--------|-------------|
|
|
119
|
-
| `--adapter <type>` | Target IDE: claude / qoder / vscode / auto (default) |
|
|
127
|
+
| `--adapter <type>` | Target IDE: claude / qoder / vscode / trae / codex / auto (default) |
|
|
120
128
|
| `--config <path>` | Specify config file |
|
|
121
129
|
| `--yes` | Skip interactions, create new files only |
|
|
122
130
|
| `--force` | Overwrite existing files |
|
|
@@ -127,9 +135,11 @@ npx add-coder init
|
|
|
127
135
|
| Directory | Content |
|
|
128
136
|
|-----------|---------|
|
|
129
137
|
| `.add/` | ADD shared core (skills, agents, docs, scripts, rules, etc.) |
|
|
130
|
-
| `.claude/` | Claude Code adapter (hooks, settings.json) |
|
|
138
|
+
| `.claude/` | Claude Code adapter (hooks, settings.json, mcp.json) |
|
|
131
139
|
| `.qoder/` | Qoder adapter (hooks, settings.json, mcp.json) |
|
|
132
140
|
| `.vscode/` | VS Code adapter (settings.json, tasks.json) |
|
|
141
|
+
| `.trae/` | Trae adapter (hooks.json, settings.json) |
|
|
142
|
+
| `.codex/` | Codex adapter (hooks.json, settings.json) |
|
|
133
143
|
|
|
134
144
|
## MCP Audit Toolchain
|
|
135
145
|
|
package/README.md
CHANGED
|
@@ -58,16 +58,17 @@ AI 对话的致命缺陷:上次讨论的架构决策、已修复的 Bug、达
|
|
|
58
58
|
|
|
59
59
|
### ⑤ 多 IDE 的 Hook 即治理层
|
|
60
60
|
|
|
61
|
-
hook 不是「通知推送」,而是 **IDE
|
|
61
|
+
hook 不是「通知推送」,而是 **ADD 范式在 IDE agent 生命周期中的 17 个确定性治理卡位**。每个 IDE(Claude Code / Qoder CN / VS Code Copilot / Trae / Codex)有各自的 hook 机制,但治理逻辑统一——架构一致,适配层不同。
|
|
62
62
|
|
|
63
|
-
|
|
|
64
|
-
|
|
65
|
-
|
|
|
66
|
-
|
|
|
67
|
-
|
|
|
68
|
-
|
|
|
63
|
+
| IDE | 治理文档 | 覆盖事件 | Hook 配置 |
|
|
64
|
+
|---|---|---|---|
|
|
65
|
+
| Claude Code | [ADD-governance-claude-code.md](./ADD-governance-claude-code.md) | 14/17 | `.claude/hooks/*.sh` |
|
|
66
|
+
| Qoder CN | [ADD-governance-qoder-cn.md](./ADD-governance-qoder-cn.md) | 10/17 | `.qoder/hooks/*.sh` |
|
|
67
|
+
| VS Code Copilot | [ADD-governance-vscode-copilot.md](./ADD-governance-vscode-copilot.md) | 10/17 | `.github/hooks/*.json` → `.vscode/hooks/*.sh` |
|
|
68
|
+
| Trae | [ADD-governance-trae.md](./ADD-governance-trae.md) | 6/17 | `hooks.json` → `.trae/hooks/*.sh` |
|
|
69
|
+
| Codex | [ADD-governance-codex.md](./ADD-governance-codex.md) | 0 (原生) / 14 (导入 Claude) | `.codex/hooks.json` |
|
|
69
70
|
|
|
70
|
-
|
|
71
|
+
> 实施 Plan: [add-coder-hook-full-alignment-plan-v1](./.qoder/plans/2026-07/17/add-coder-hook-full-alignment-plan-v1.md) | 触发源: [GitHub Issue #6](https://github.com/xiaomingming92/add-coder/issues/6)
|
|
71
72
|
|
|
72
73
|
---
|
|
73
74
|
|
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
|
|
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
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
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 =
|
|
255
|
+
const content = readFileSync2(full, "utf-8");
|
|
261
256
|
const rendered = render(content, config);
|
|
262
|
-
|
|
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(
|
|
269
|
+
walk(adapterDir, "");
|
|
270
|
+
walk(coreHooksLib, "", join4(magicPath, "hooks", "lib"));
|
|
268
271
|
if (dryRun) {
|
|
269
|
-
console.log(`[dry-run]
|
|
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
|
-
|
|
276
|
-
|
|
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
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
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
|
|
338
|
-
import { fileURLToPath as
|
|
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
|
|
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 =
|
|
388
|
+
const envContent = readFileSync3(envPath, "utf-8");
|
|
423
389
|
const dbUrl = envContent.match(/DATABASE_URL=.*/);
|
|
424
390
|
if (dbUrl) {
|
|
425
|
-
const existing = existsSync5(devEnvPath) ?
|
|
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" +
|
|
480
|
-
console.log("=== \u6A21\u677F ===\n" +
|
|
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
|
|
528
|
-
var
|
|
529
|
-
var ADD_PRISMA_TEMPLATE = resolve3(
|
|
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
|
|
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(
|
|
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";
|
|
@@ -736,7 +704,7 @@ async function initCommand(options) {
|
|
|
736
704
|
}
|
|
737
705
|
if (db.engine === "postgresql" && db.container && db.container !== "manual") {
|
|
738
706
|
if (!db.reuseExisting) {
|
|
739
|
-
const composeName = db.container === "podman" ? "podman-compose.yml" : "docker-compose.yml";
|
|
707
|
+
const composeName = db.container === "podman" ? "podman-compose.add.yml" : "docker-compose.add.yml";
|
|
740
708
|
const composePath = resolve4(projectRoot, composeName);
|
|
741
709
|
if (!options.dryRun && (!existsSync6(composePath) || options.force)) {
|
|
742
710
|
writeFileSync3(composePath, composeContent(config.projectName || "add-project"), "utf-8");
|
|
@@ -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 =
|
|
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
|
-
|
|
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
|
|
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(
|
|
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
|
-
|
|
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,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "add-coder",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.2.0",
|
|
4
4
|
"private": false,
|
|
5
5
|
"description": "[codein2027](https://github.com/xiaomingming92/codein2027) 快速构建 ADD 编程范式的完整脚手架——AI 代码治理的落地方案。以「审计即基础设施」为核心,彻底打破编程过程黑盒与跨轮失忆,让编程范式进化为可审计、可追溯、可收敛的新时代。npx 即用,人人可体验。",
|
|
6
|
+
"repository": {
|
|
7
|
+
"type": "git",
|
|
8
|
+
"url": "https://github.com/xiaomingming92/add-coder.git"
|
|
9
|
+
},
|
|
6
10
|
"type": "module",
|
|
7
11
|
"bin": {
|
|
8
12
|
"add-coder": "bin/add-coder.js"
|
|
@@ -1,17 +1,172 @@
|
|
|
1
1
|
#!/bin/bash
|
|
2
|
-
# doc-format-guard.sh —
|
|
2
|
+
# doc-format-guard.sh — schema.json 驱动的 ADD 文档格式守卫
|
|
3
3
|
set -euo pipefail
|
|
4
4
|
|
|
5
|
-
|
|
6
|
-
SHARED_LIB="$HOOK_DIR/../../shared/hooks-lib/common.sh"
|
|
7
|
-
[ -f "$SHARED_LIB" ] && source "$SHARED_LIB"
|
|
5
|
+
input=$(cat)
|
|
8
6
|
|
|
9
|
-
|
|
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
|
-
|
|
14
|
-
|
|
15
|
-
echo "[ADD DocFormatGuard] 检测到 ADD 文档变更,请确保格式符合模板规范。"
|
|
20
|
+
if ! echo "$file_path" | grep -qE '\.qoder/(plans|specs)/'; then
|
|
21
|
+
exit 0
|
|
16
22
|
fi
|
|
17
|
-
|
|
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
|