@fenglimg/fabric-cli 2.0.0 → 2.1.0-rc.2
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/LICENSE +21 -0
- package/README.md +6 -5
- package/dist/chunk-BATF4PEJ.js +361 -0
- package/dist/{chunk-OBQU6NHO.js → chunk-COI5VDFU.js} +0 -18
- package/dist/chunk-F46ORPOA.js +903 -0
- package/dist/chunk-HFQVXY6P.js +86 -0
- package/dist/chunk-L4Q55UC4.js +52 -0
- package/dist/chunk-LFIKMVY7.js +27 -0
- package/dist/chunk-MF3OTILQ.js +544 -0
- package/dist/chunk-PWLW3B57.js +18 -0
- package/dist/chunk-RYAFBNES.js +33 -0
- package/dist/chunk-T5RPGCCM.js +40 -0
- package/dist/chunk-WU6GAPKH.js +36 -0
- package/dist/config-XJIPZNUP.js +13 -0
- package/dist/doctor-QVNPHLJK.js +920 -0
- package/dist/index.js +23 -8
- package/dist/{init-BIRSIOXO.js → install-2HDO5FTQ.js} +807 -705
- package/dist/metrics-ACEQFPDU.js +122 -0
- package/dist/onboard-coverage-MFCAEBDO.js +220 -0
- package/dist/{plan-context-hint-QMUPAXIB.js → plan-context-hint-FC6P3WFE.js} +34 -28
- package/dist/scope-explain-2F2R5URO.js +33 -0
- package/dist/status-GLQWLWH6.js +23 -0
- package/dist/store-XTSE5TY6.js +105 -0
- package/dist/sync-BJCWDPNC.js +245 -0
- package/dist/uninstall-TAXSUSKH.js +1073 -0
- package/dist/whoami-B6AEMSEV.js +31 -0
- package/package.json +30 -5
- package/templates/hooks/cite-policy-evict.cjs +231 -0
- package/templates/hooks/configs/README.md +29 -6
- package/templates/hooks/configs/claude-code.json +14 -3
- package/templates/hooks/configs/codex-hooks.json +6 -3
- package/templates/hooks/configs/cursor-hooks.json +8 -10
- package/templates/hooks/fabric-hint.cjs +873 -105
- package/templates/hooks/knowledge-hint-broad.cjs +549 -135
- package/templates/hooks/knowledge-hint-narrow.cjs +830 -26
- package/templates/hooks/lib/banner-i18n.cjs +309 -0
- package/templates/hooks/lib/bindings-snapshot-reader.cjs +81 -0
- package/templates/hooks/lib/cite-contract-reminder.cjs +179 -0
- package/templates/hooks/lib/cite-line-parser.cjs +180 -0
- package/templates/hooks/lib/client-adapter.cjs +106 -0
- package/templates/hooks/lib/config-cache.cjs +107 -0
- package/templates/hooks/lib/state-store.cjs +84 -0
- package/templates/hooks/lib/summary-fallback.cjs +210 -0
- package/templates/skills/fabric-archive/SKILL.md +97 -419
- package/templates/skills/fabric-archive/ref/dry-run-scope.md +16 -0
- package/templates/skills/fabric-archive/ref/e5-cron-recap.md +58 -0
- package/templates/skills/fabric-archive/ref/i18n-policy.md +86 -0
- package/templates/skills/fabric-archive/ref/phase-0-range-resolution.md +156 -0
- package/templates/skills/fabric-archive/ref/phase-1-5-onboard.md +218 -0
- package/templates/skills/fabric-archive/ref/phase-1-cross-session.md +62 -0
- package/templates/skills/fabric-archive/ref/phase-2-5-viability.md +68 -0
- package/templates/skills/fabric-archive/ref/phase-3-5-scope.md +108 -0
- package/templates/skills/fabric-archive/ref/phase-3-classify.md +63 -0
- package/templates/skills/fabric-archive/ref/phase-4-5-emit.md +78 -0
- package/templates/skills/fabric-archive/ref/phase-4-mcp-persist.md +89 -0
- package/templates/skills/fabric-archive/ref/rc-history.md +38 -0
- package/templates/skills/fabric-archive/ref/worked-examples.md +78 -0
- package/templates/skills/fabric-import/SKILL.md +77 -514
- package/templates/skills/fabric-import/ref/checkpoint-state.md +85 -0
- package/templates/skills/fabric-import/ref/i18n-policy.md +79 -0
- package/templates/skills/fabric-import/ref/output-contract.md +61 -0
- package/templates/skills/fabric-import/ref/phase-2-mining.md +213 -0
- package/templates/skills/fabric-import/ref/phase-3-dedup.md +75 -0
- package/templates/skills/fabric-import/ref/state-recovery.md +57 -0
- package/templates/skills/fabric-import/ref/worked-examples.md +127 -0
- package/templates/skills/fabric-review/SKILL.md +90 -284
- package/templates/skills/fabric-review/ref/askuserquestion-policy.md +66 -0
- package/templates/skills/fabric-review/ref/i18n-policy.md +111 -0
- package/templates/skills/fabric-review/ref/modify-flow.md +103 -0
- package/templates/skills/fabric-review/ref/output-contract.md +58 -0
- package/templates/skills/fabric-review/ref/per-mode-flows.md +155 -0
- package/templates/skills/fabric-review/ref/semantic-check.md +26 -0
- package/templates/skills/fabric-review/ref/worked-examples.md +95 -0
- package/templates/skills/fabric-sync/SKILL.md +46 -0
- package/templates/skills/lib/shared-policy.md +69 -0
- package/dist/chunk-6ICJICVU.js +0 -10
- package/dist/chunk-74SZWYPH.js +0 -658
- package/dist/chunk-EYIDD2YS.js +0 -1000
- package/dist/doctor-T7JWODKG.js +0 -282
- package/dist/hooks-Y74Y5LQS.js +0 -12
- package/dist/scan-LMK3UCWL.js +0 -22
- package/dist/serve-H554BHLG.js +0 -124
- package/templates/agents-md/AGENTS.md.template +0 -59
- package/templates/bootstrap/CLAUDE.md +0 -8
- package/templates/bootstrap/codex-AGENTS-header.md +0 -6
- package/templates/bootstrap/cursor-fabric-bootstrap.mdc +0 -10
package/dist/chunk-74SZWYPH.js
DELETED
|
@@ -1,658 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
import {
|
|
3
|
-
t
|
|
4
|
-
} from "./chunk-6ICJICVU.js";
|
|
5
|
-
|
|
6
|
-
// src/commands/hooks.ts
|
|
7
|
-
import { existsSync as existsSync3, statSync } from "fs";
|
|
8
|
-
import { isAbsolute, join as join3, resolve as resolve3 } from "path";
|
|
9
|
-
import { defineCommand } from "citty";
|
|
10
|
-
|
|
11
|
-
// src/install/skills-and-hooks.ts
|
|
12
|
-
import { chmodSync, existsSync as existsSync2, readFileSync } from "fs";
|
|
13
|
-
import { mkdir as mkdir2, readFile as readFile2 } from "fs/promises";
|
|
14
|
-
import { dirname as dirname2, join as join2, parse, resolve as resolve2 } from "path";
|
|
15
|
-
import { fileURLToPath } from "url";
|
|
16
|
-
import { atomicWriteJson as atomicWriteJson2, atomicWriteText } from "@fenglimg/fabric-shared/node/atomic-write";
|
|
17
|
-
|
|
18
|
-
// src/config/json.ts
|
|
19
|
-
import { existsSync } from "fs";
|
|
20
|
-
import { mkdir, readFile } from "fs/promises";
|
|
21
|
-
import { dirname, join, resolve } from "path";
|
|
22
|
-
import { homedir } from "os";
|
|
23
|
-
import { atomicWriteJson } from "@fenglimg/fabric-shared/node/atomic-write";
|
|
24
|
-
|
|
25
|
-
// src/config/writer.ts
|
|
26
|
-
function createServerEntry(serverPath) {
|
|
27
|
-
return {
|
|
28
|
-
command: process.execPath,
|
|
29
|
-
args: [serverPath]
|
|
30
|
-
};
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
// src/config/json.ts
|
|
34
|
-
function deepMerge(target, source, options = {}) {
|
|
35
|
-
return deepMergeAtPath(target, source, "", options);
|
|
36
|
-
}
|
|
37
|
-
function deepMergeAtPath(target, source, path, options) {
|
|
38
|
-
if (options.arrayAppendPaths && options.arrayAppendPaths.includes(path) && Array.isArray(target) && Array.isArray(source)) {
|
|
39
|
-
return appendArrayWithDedupe(target, source);
|
|
40
|
-
}
|
|
41
|
-
if (target === null || typeof target !== "object" || Array.isArray(target) || source === null || typeof source !== "object" || Array.isArray(source)) {
|
|
42
|
-
return source;
|
|
43
|
-
}
|
|
44
|
-
const out = { ...target };
|
|
45
|
-
for (const key of Object.keys(source)) {
|
|
46
|
-
const childPath = path === "" ? key : `${path}.${key}`;
|
|
47
|
-
out[key] = deepMergeAtPath(
|
|
48
|
-
target[key],
|
|
49
|
-
source[key],
|
|
50
|
-
childPath,
|
|
51
|
-
options
|
|
52
|
-
);
|
|
53
|
-
}
|
|
54
|
-
return out;
|
|
55
|
-
}
|
|
56
|
-
function appendArrayWithDedupe(target, source) {
|
|
57
|
-
const out = [...target];
|
|
58
|
-
for (const candidate of source) {
|
|
59
|
-
if (out.some((existing) => isSameHookEntry(existing, candidate))) {
|
|
60
|
-
continue;
|
|
61
|
-
}
|
|
62
|
-
out.push(candidate);
|
|
63
|
-
}
|
|
64
|
-
return out;
|
|
65
|
-
}
|
|
66
|
-
function isSameHookEntry(a, b) {
|
|
67
|
-
const cmdA = extractHookCommand(a);
|
|
68
|
-
const cmdB = extractHookCommand(b);
|
|
69
|
-
if (cmdA !== null && cmdB !== null) {
|
|
70
|
-
return cmdA === cmdB;
|
|
71
|
-
}
|
|
72
|
-
return deepEqual(a, b);
|
|
73
|
-
}
|
|
74
|
-
function extractHookCommand(item) {
|
|
75
|
-
if (item === null || typeof item !== "object") {
|
|
76
|
-
return null;
|
|
77
|
-
}
|
|
78
|
-
const obj = item;
|
|
79
|
-
if (typeof obj.command === "string") {
|
|
80
|
-
return obj.command;
|
|
81
|
-
}
|
|
82
|
-
if (Array.isArray(obj.hooks)) {
|
|
83
|
-
for (const inner of obj.hooks) {
|
|
84
|
-
if (inner !== null && typeof inner === "object") {
|
|
85
|
-
const innerObj = inner;
|
|
86
|
-
if (typeof innerObj.command === "string") {
|
|
87
|
-
return innerObj.command;
|
|
88
|
-
}
|
|
89
|
-
}
|
|
90
|
-
}
|
|
91
|
-
}
|
|
92
|
-
return null;
|
|
93
|
-
}
|
|
94
|
-
function deepEqual(a, b) {
|
|
95
|
-
if (a === b) {
|
|
96
|
-
return true;
|
|
97
|
-
}
|
|
98
|
-
if (a === null || b === null || typeof a !== "object" || typeof b !== "object") {
|
|
99
|
-
return false;
|
|
100
|
-
}
|
|
101
|
-
if (Array.isArray(a) !== Array.isArray(b)) {
|
|
102
|
-
return false;
|
|
103
|
-
}
|
|
104
|
-
if (Array.isArray(a) && Array.isArray(b)) {
|
|
105
|
-
if (a.length !== b.length) {
|
|
106
|
-
return false;
|
|
107
|
-
}
|
|
108
|
-
return a.every((value, index) => deepEqual(value, b[index]));
|
|
109
|
-
}
|
|
110
|
-
const aObj = a;
|
|
111
|
-
const bObj = b;
|
|
112
|
-
const aKeys = Object.keys(aObj);
|
|
113
|
-
const bKeys = Object.keys(bObj);
|
|
114
|
-
if (aKeys.length !== bKeys.length) {
|
|
115
|
-
return false;
|
|
116
|
-
}
|
|
117
|
-
return aKeys.every((key) => deepEqual(aObj[key], bObj[key]));
|
|
118
|
-
}
|
|
119
|
-
function expandHome(filePath) {
|
|
120
|
-
if (filePath === "~") {
|
|
121
|
-
return homedir();
|
|
122
|
-
}
|
|
123
|
-
if (filePath.startsWith("~/")) {
|
|
124
|
-
return join(homedir(), filePath.slice(2));
|
|
125
|
-
}
|
|
126
|
-
return filePath;
|
|
127
|
-
}
|
|
128
|
-
function normalizeConfigPath(filePath) {
|
|
129
|
-
return resolve(expandHome(filePath));
|
|
130
|
-
}
|
|
131
|
-
async function readJsonConfig(configPath) {
|
|
132
|
-
try {
|
|
133
|
-
const raw = await readFile(configPath, "utf8");
|
|
134
|
-
if (raw.trim().length === 0) {
|
|
135
|
-
return {};
|
|
136
|
-
}
|
|
137
|
-
const parsed = JSON.parse(raw);
|
|
138
|
-
if (parsed === null || typeof parsed !== "object" || Array.isArray(parsed)) {
|
|
139
|
-
throw new Error(`Expected JSON object in ${configPath}`);
|
|
140
|
-
}
|
|
141
|
-
return parsed;
|
|
142
|
-
} catch (error) {
|
|
143
|
-
if (error instanceof Error && "code" in error && error.code === "ENOENT") {
|
|
144
|
-
return {};
|
|
145
|
-
}
|
|
146
|
-
throw error;
|
|
147
|
-
}
|
|
148
|
-
}
|
|
149
|
-
async function writeJsonClientConfig(configPath, serverEntry) {
|
|
150
|
-
const existing = await readJsonConfig(configPath);
|
|
151
|
-
const merged = deepMerge(existing, { mcpServers: { fabric: serverEntry } });
|
|
152
|
-
await mkdir(dirname(configPath), { recursive: true });
|
|
153
|
-
await atomicWriteJson(configPath, merged, { indent: 2 });
|
|
154
|
-
}
|
|
155
|
-
var JsonClientConfigWriter = class {
|
|
156
|
-
configuredPath;
|
|
157
|
-
constructor(configuredPath) {
|
|
158
|
-
this.configuredPath = configuredPath;
|
|
159
|
-
}
|
|
160
|
-
async detect(workspaceRoot, overridePath) {
|
|
161
|
-
const explicitPath = overridePath ?? this.configuredPath;
|
|
162
|
-
if (explicitPath !== void 0) {
|
|
163
|
-
return normalizeConfigPath(explicitPath);
|
|
164
|
-
}
|
|
165
|
-
const configPath = this.defaultPath(workspaceRoot);
|
|
166
|
-
return configPath === null ? null : normalizeConfigPath(configPath);
|
|
167
|
-
}
|
|
168
|
-
async write(serverPath, workspaceRoot, overridePath) {
|
|
169
|
-
const configPath = await this.detect(workspaceRoot, overridePath);
|
|
170
|
-
if (configPath === null) {
|
|
171
|
-
return;
|
|
172
|
-
}
|
|
173
|
-
await writeJsonClientConfig(configPath, createServerEntry(serverPath));
|
|
174
|
-
}
|
|
175
|
-
};
|
|
176
|
-
var ClaudeCodeCLIWriter = class extends JsonClientConfigWriter {
|
|
177
|
-
clientKind = "ClaudeCodeCLI";
|
|
178
|
-
scope;
|
|
179
|
-
constructor(configuredPath, scope = "project") {
|
|
180
|
-
super(configuredPath);
|
|
181
|
-
this.scope = scope;
|
|
182
|
-
}
|
|
183
|
-
// Writes to project-level .mcp.json (per Claude Code MCP spec) by default,
|
|
184
|
-
// or ~/.claude.json for user scope.
|
|
185
|
-
// Detection still checks ~/.claude to confirm Claude Code is installed.
|
|
186
|
-
defaultPath(workspaceRoot) {
|
|
187
|
-
const globalClaudeDir = join(homedir(), ".claude");
|
|
188
|
-
const projectClaudeDir = join(workspaceRoot, ".claude");
|
|
189
|
-
if (!existsSync(globalClaudeDir) && !existsSync(projectClaudeDir)) {
|
|
190
|
-
return null;
|
|
191
|
-
}
|
|
192
|
-
return this.scope === "user" ? join(homedir(), ".claude.json") : join(workspaceRoot, ".mcp.json");
|
|
193
|
-
}
|
|
194
|
-
};
|
|
195
|
-
var CursorWriter = class extends JsonClientConfigWriter {
|
|
196
|
-
clientKind = "Cursor";
|
|
197
|
-
constructor(configuredPath) {
|
|
198
|
-
super(configuredPath);
|
|
199
|
-
}
|
|
200
|
-
defaultPath(workspaceRoot) {
|
|
201
|
-
const cursorDir = join(workspaceRoot, ".cursor");
|
|
202
|
-
return existsSync(cursorDir) ? join(cursorDir, "mcp.json") : null;
|
|
203
|
-
}
|
|
204
|
-
};
|
|
205
|
-
|
|
206
|
-
// src/install/skills-and-hooks.ts
|
|
207
|
-
var SKILL_TEMPLATE_REL = "skills/fabric-archive/SKILL.md";
|
|
208
|
-
var SKILL_REVIEW_TEMPLATE_REL = "skills/fabric-review/SKILL.md";
|
|
209
|
-
var SKILL_IMPORT_TEMPLATE_REL = "skills/fabric-import/SKILL.md";
|
|
210
|
-
var HOOK_SCRIPT_TEMPLATE_REL = "hooks/fabric-hint.cjs";
|
|
211
|
-
var HOOK_BROAD_SCRIPT_TEMPLATE_REL = "hooks/knowledge-hint-broad.cjs";
|
|
212
|
-
var HOOK_NARROW_SCRIPT_TEMPLATE_REL = "hooks/knowledge-hint-narrow.cjs";
|
|
213
|
-
var CLAUDE_HOOK_CONFIG_TEMPLATE_REL = "hooks/configs/claude-code.json";
|
|
214
|
-
var CODEX_HOOK_CONFIG_TEMPLATE_REL = "hooks/configs/codex-hooks.json";
|
|
215
|
-
var CURSOR_HOOK_CONFIG_TEMPLATE_REL = "hooks/configs/cursor-hooks.json";
|
|
216
|
-
var SKILL_DEST_REL = join2("skills", "fabric-archive", "SKILL.md");
|
|
217
|
-
var SKILL_REVIEW_DEST_REL = join2("skills", "fabric-review", "SKILL.md");
|
|
218
|
-
var SKILL_IMPORT_DEST_REL = join2("skills", "fabric-import", "SKILL.md");
|
|
219
|
-
var HOOK_SCRIPT_DEST_REL = join2("hooks", "fabric-hint.cjs");
|
|
220
|
-
var HOOK_BROAD_SCRIPT_DEST_REL = join2("hooks", "knowledge-hint-broad.cjs");
|
|
221
|
-
var HOOK_NARROW_SCRIPT_DEST_REL = join2("hooks", "knowledge-hint-narrow.cjs");
|
|
222
|
-
var POINTER_LINE = "> Use the fabric-archive Skill when archiving knowledge entries (see .claude/skills/fabric-archive/SKILL.md).";
|
|
223
|
-
var REVIEW_POINTER_LINE = "> Use the fabric-review Skill to review pending knowledge entries (see .claude/skills/fabric-review/SKILL.md).";
|
|
224
|
-
var IMPORT_POINTER_LINE = "> Use the fabric-import Skill for cold-start enrichment from git history and docs (see .claude/skills/fabric-import/SKILL.md).";
|
|
225
|
-
var POINTER_TARGETS = ["CLAUDE.md", "AGENTS.md", join2(".cursor", "rules")];
|
|
226
|
-
async function installFabricArchiveSkill(projectRoot, _options = {}) {
|
|
227
|
-
const source = await readTemplate(SKILL_TEMPLATE_REL);
|
|
228
|
-
const targets = [
|
|
229
|
-
join2(projectRoot, ".claude", SKILL_DEST_REL),
|
|
230
|
-
join2(projectRoot, ".codex", SKILL_DEST_REL)
|
|
231
|
-
];
|
|
232
|
-
const results = [];
|
|
233
|
-
for (const target of targets) {
|
|
234
|
-
results.push(await copyTextIdempotent("skill", source, target));
|
|
235
|
-
}
|
|
236
|
-
return results;
|
|
237
|
-
}
|
|
238
|
-
async function installFabricReviewSkill(projectRoot, _options = {}) {
|
|
239
|
-
const source = await readTemplate(SKILL_REVIEW_TEMPLATE_REL);
|
|
240
|
-
const targets = [
|
|
241
|
-
join2(projectRoot, ".claude", SKILL_REVIEW_DEST_REL),
|
|
242
|
-
join2(projectRoot, ".codex", SKILL_REVIEW_DEST_REL)
|
|
243
|
-
];
|
|
244
|
-
const results = [];
|
|
245
|
-
for (const target of targets) {
|
|
246
|
-
results.push(await copyTextIdempotent("skill-review", source, target));
|
|
247
|
-
}
|
|
248
|
-
return results;
|
|
249
|
-
}
|
|
250
|
-
async function installFabricImportSkill(projectRoot, _options = {}) {
|
|
251
|
-
const source = await readTemplate(SKILL_IMPORT_TEMPLATE_REL);
|
|
252
|
-
const targets = [
|
|
253
|
-
join2(projectRoot, ".claude", SKILL_IMPORT_DEST_REL),
|
|
254
|
-
join2(projectRoot, ".codex", SKILL_IMPORT_DEST_REL)
|
|
255
|
-
];
|
|
256
|
-
const results = [];
|
|
257
|
-
for (const target of targets) {
|
|
258
|
-
results.push(await copyTextIdempotent("skill-import", source, target));
|
|
259
|
-
}
|
|
260
|
-
return results;
|
|
261
|
-
}
|
|
262
|
-
async function installArchiveHintHook(projectRoot, _options = {}) {
|
|
263
|
-
const source = await readTemplate(HOOK_SCRIPT_TEMPLATE_REL);
|
|
264
|
-
const targets = [
|
|
265
|
-
join2(projectRoot, ".claude", HOOK_SCRIPT_DEST_REL),
|
|
266
|
-
join2(projectRoot, ".codex", HOOK_SCRIPT_DEST_REL),
|
|
267
|
-
join2(projectRoot, ".cursor", HOOK_SCRIPT_DEST_REL)
|
|
268
|
-
];
|
|
269
|
-
const results = [];
|
|
270
|
-
for (const target of targets) {
|
|
271
|
-
const result = await copyTextIdempotent("hook-script", source, target);
|
|
272
|
-
if (result.status === "written" && process.platform !== "win32") {
|
|
273
|
-
try {
|
|
274
|
-
chmodSync(target, 493);
|
|
275
|
-
} catch {
|
|
276
|
-
}
|
|
277
|
-
}
|
|
278
|
-
results.push(result);
|
|
279
|
-
}
|
|
280
|
-
return results;
|
|
281
|
-
}
|
|
282
|
-
async function installKnowledgeHintBroadHook(projectRoot, _options = {}) {
|
|
283
|
-
const source = await readTemplate(HOOK_BROAD_SCRIPT_TEMPLATE_REL);
|
|
284
|
-
const targets = [
|
|
285
|
-
join2(projectRoot, ".claude", HOOK_BROAD_SCRIPT_DEST_REL),
|
|
286
|
-
join2(projectRoot, ".codex", HOOK_BROAD_SCRIPT_DEST_REL),
|
|
287
|
-
join2(projectRoot, ".cursor", HOOK_BROAD_SCRIPT_DEST_REL)
|
|
288
|
-
];
|
|
289
|
-
const results = [];
|
|
290
|
-
for (const target of targets) {
|
|
291
|
-
const result = await copyTextIdempotent("hook-broad-script", source, target);
|
|
292
|
-
if (result.status === "written" && process.platform !== "win32") {
|
|
293
|
-
try {
|
|
294
|
-
chmodSync(target, 493);
|
|
295
|
-
} catch {
|
|
296
|
-
}
|
|
297
|
-
}
|
|
298
|
-
results.push(result);
|
|
299
|
-
}
|
|
300
|
-
return results;
|
|
301
|
-
}
|
|
302
|
-
async function installKnowledgeHintNarrowHook(projectRoot, _options = {}) {
|
|
303
|
-
const source = await readTemplate(HOOK_NARROW_SCRIPT_TEMPLATE_REL);
|
|
304
|
-
const targets = [
|
|
305
|
-
join2(projectRoot, ".claude", HOOK_NARROW_SCRIPT_DEST_REL),
|
|
306
|
-
join2(projectRoot, ".codex", HOOK_NARROW_SCRIPT_DEST_REL),
|
|
307
|
-
join2(projectRoot, ".cursor", HOOK_NARROW_SCRIPT_DEST_REL)
|
|
308
|
-
];
|
|
309
|
-
const results = [];
|
|
310
|
-
for (const target of targets) {
|
|
311
|
-
const result = await copyTextIdempotent("hook-narrow-script", source, target);
|
|
312
|
-
if (result.status === "written" && process.platform !== "win32") {
|
|
313
|
-
try {
|
|
314
|
-
chmodSync(target, 493);
|
|
315
|
-
} catch {
|
|
316
|
-
}
|
|
317
|
-
}
|
|
318
|
-
results.push(result);
|
|
319
|
-
}
|
|
320
|
-
return results;
|
|
321
|
-
}
|
|
322
|
-
async function mergeClaudeCodeHookConfig(projectRoot, _options = {}) {
|
|
323
|
-
const fragment = await readJsonTemplate(CLAUDE_HOOK_CONFIG_TEMPLATE_REL);
|
|
324
|
-
const targetPath = join2(projectRoot, ".claude", "settings.json");
|
|
325
|
-
return mergeJsonIdempotent("claude-hook-config", targetPath, fragment, [
|
|
326
|
-
"hooks.Stop",
|
|
327
|
-
"hooks.SessionStart",
|
|
328
|
-
"hooks.PreToolUse"
|
|
329
|
-
]);
|
|
330
|
-
}
|
|
331
|
-
async function mergeCodexHookConfig(projectRoot, _options = {}) {
|
|
332
|
-
const fragment = await readJsonTemplate(CODEX_HOOK_CONFIG_TEMPLATE_REL);
|
|
333
|
-
const targetPath = join2(projectRoot, ".codex", "hooks.json");
|
|
334
|
-
return mergeJsonIdempotent("codex-hook-config", targetPath, fragment, [
|
|
335
|
-
"events.Stop",
|
|
336
|
-
"events.SessionStart",
|
|
337
|
-
"events.PreToolUse"
|
|
338
|
-
]);
|
|
339
|
-
}
|
|
340
|
-
async function mergeCursorHookConfig(projectRoot, _options = {}) {
|
|
341
|
-
const fragment = await readJsonTemplate(CURSOR_HOOK_CONFIG_TEMPLATE_REL);
|
|
342
|
-
const targetPath = join2(projectRoot, ".cursor", "hooks.json");
|
|
343
|
-
return mergeJsonIdempotent("cursor-hook-config", targetPath, fragment, [
|
|
344
|
-
"events.Stop",
|
|
345
|
-
"events.SessionStart",
|
|
346
|
-
"events.PreToolUse"
|
|
347
|
-
]);
|
|
348
|
-
}
|
|
349
|
-
async function addArchiveSkillPointer(projectRoot, _options = {}) {
|
|
350
|
-
const results = [];
|
|
351
|
-
for (const rel of POINTER_TARGETS) {
|
|
352
|
-
const target = join2(projectRoot, rel);
|
|
353
|
-
if (!existsSync2(target)) {
|
|
354
|
-
results.push({ step: "pointer", path: target, status: "skipped", message: "absent" });
|
|
355
|
-
continue;
|
|
356
|
-
}
|
|
357
|
-
let existing;
|
|
358
|
-
try {
|
|
359
|
-
existing = await readFile2(target, "utf8");
|
|
360
|
-
} catch (error) {
|
|
361
|
-
results.push({
|
|
362
|
-
step: "pointer",
|
|
363
|
-
path: target,
|
|
364
|
-
status: "error",
|
|
365
|
-
message: error instanceof Error ? error.message : String(error)
|
|
366
|
-
});
|
|
367
|
-
continue;
|
|
368
|
-
}
|
|
369
|
-
let next = existing;
|
|
370
|
-
let wrote = false;
|
|
371
|
-
if (next.includes(POINTER_LINE)) {
|
|
372
|
-
results.push({ step: "pointer", path: target, status: "skipped", message: "already-present" });
|
|
373
|
-
} else {
|
|
374
|
-
const trailingNewline = next.length === 0 || next.endsWith("\n") ? "" : "\n";
|
|
375
|
-
next = `${next}${trailingNewline}
|
|
376
|
-
${POINTER_LINE}
|
|
377
|
-
`;
|
|
378
|
-
wrote = true;
|
|
379
|
-
results.push({ step: "pointer", path: target, status: "written" });
|
|
380
|
-
}
|
|
381
|
-
if (next.includes(REVIEW_POINTER_LINE)) {
|
|
382
|
-
results.push({ step: "pointer-review", path: target, status: "skipped", message: "already-present" });
|
|
383
|
-
} else {
|
|
384
|
-
const trailingNewline = next.length === 0 || next.endsWith("\n") ? "" : "\n";
|
|
385
|
-
next = `${next}${trailingNewline}
|
|
386
|
-
${REVIEW_POINTER_LINE}
|
|
387
|
-
`;
|
|
388
|
-
wrote = true;
|
|
389
|
-
results.push({ step: "pointer-review", path: target, status: "written" });
|
|
390
|
-
}
|
|
391
|
-
if (next.includes(IMPORT_POINTER_LINE)) {
|
|
392
|
-
results.push({ step: "pointer-import", path: target, status: "skipped", message: "already-present" });
|
|
393
|
-
} else {
|
|
394
|
-
const trailingNewline = next.length === 0 || next.endsWith("\n") ? "" : "\n";
|
|
395
|
-
next = `${next}${trailingNewline}
|
|
396
|
-
${IMPORT_POINTER_LINE}
|
|
397
|
-
`;
|
|
398
|
-
wrote = true;
|
|
399
|
-
results.push({ step: "pointer-import", path: target, status: "written" });
|
|
400
|
-
}
|
|
401
|
-
if (wrote) {
|
|
402
|
-
await atomicWriteText(target, next);
|
|
403
|
-
}
|
|
404
|
-
}
|
|
405
|
-
return results;
|
|
406
|
-
}
|
|
407
|
-
async function copyTextIdempotent(step, source, target) {
|
|
408
|
-
if (existsSync2(target)) {
|
|
409
|
-
try {
|
|
410
|
-
const existing = readFileSync(target, "utf8");
|
|
411
|
-
if (existing === source) {
|
|
412
|
-
return { step, path: target, status: "skipped", message: "up-to-date" };
|
|
413
|
-
}
|
|
414
|
-
} catch {
|
|
415
|
-
}
|
|
416
|
-
}
|
|
417
|
-
await mkdir2(dirname2(target), { recursive: true });
|
|
418
|
-
await atomicWriteText(target, source);
|
|
419
|
-
return { step, path: target, status: "written" };
|
|
420
|
-
}
|
|
421
|
-
async function mergeJsonIdempotent(step, target, fragment, arrayAppendPaths) {
|
|
422
|
-
const existing = await readJsonObjectOrEmpty(target);
|
|
423
|
-
const merged = deepMerge(existing, fragment, { arrayAppendPaths });
|
|
424
|
-
if (jsonEqual(existing, merged)) {
|
|
425
|
-
return { step, path: target, status: "skipped", message: "up-to-date" };
|
|
426
|
-
}
|
|
427
|
-
await mkdir2(dirname2(target), { recursive: true });
|
|
428
|
-
await atomicWriteJson2(target, merged, { indent: 2 });
|
|
429
|
-
return { step, path: target, status: "written" };
|
|
430
|
-
}
|
|
431
|
-
async function readJsonObjectOrEmpty(path) {
|
|
432
|
-
try {
|
|
433
|
-
const raw = await readFile2(path, "utf8");
|
|
434
|
-
if (raw.trim().length === 0) {
|
|
435
|
-
return {};
|
|
436
|
-
}
|
|
437
|
-
const parsed = JSON.parse(raw);
|
|
438
|
-
if (parsed === null || typeof parsed !== "object" || Array.isArray(parsed)) {
|
|
439
|
-
return {};
|
|
440
|
-
}
|
|
441
|
-
return parsed;
|
|
442
|
-
} catch (error) {
|
|
443
|
-
if (error instanceof Error && "code" in error && error.code === "ENOENT") {
|
|
444
|
-
return {};
|
|
445
|
-
}
|
|
446
|
-
throw error;
|
|
447
|
-
}
|
|
448
|
-
}
|
|
449
|
-
function jsonEqual(a, b) {
|
|
450
|
-
return JSON.stringify(a) === JSON.stringify(b);
|
|
451
|
-
}
|
|
452
|
-
async function readTemplate(relativePath) {
|
|
453
|
-
const path = findTemplatePath(relativePath);
|
|
454
|
-
return readFile2(path, "utf8");
|
|
455
|
-
}
|
|
456
|
-
async function readJsonTemplate(relativePath) {
|
|
457
|
-
const raw = await readTemplate(relativePath);
|
|
458
|
-
const parsed = JSON.parse(raw);
|
|
459
|
-
if (parsed === null || typeof parsed !== "object" || Array.isArray(parsed)) {
|
|
460
|
-
throw new Error(`Template at ${relativePath} is not a JSON object`);
|
|
461
|
-
}
|
|
462
|
-
return parsed;
|
|
463
|
-
}
|
|
464
|
-
function findTemplatePath(relativePath) {
|
|
465
|
-
const startDir = dirname2(fileURLToPath(import.meta.url));
|
|
466
|
-
let current = resolve2(startDir);
|
|
467
|
-
while (true) {
|
|
468
|
-
const candidate = join2(current, "templates", relativePath);
|
|
469
|
-
if (existsSync2(candidate)) {
|
|
470
|
-
return candidate;
|
|
471
|
-
}
|
|
472
|
-
const parent = dirname2(current);
|
|
473
|
-
if (parent === current || parse(current).root === current) {
|
|
474
|
-
throw new Error(`Template not found: templates/${relativePath} (searched up from ${startDir})`);
|
|
475
|
-
}
|
|
476
|
-
current = parent;
|
|
477
|
-
}
|
|
478
|
-
}
|
|
479
|
-
|
|
480
|
-
// src/commands/hooks.ts
|
|
481
|
-
var hooksCommand = defineCommand({
|
|
482
|
-
meta: {
|
|
483
|
-
name: "hooks",
|
|
484
|
-
description: t("cli.hooks.description")
|
|
485
|
-
},
|
|
486
|
-
subCommands: {
|
|
487
|
-
install: defineCommand({
|
|
488
|
-
meta: {
|
|
489
|
-
name: "install",
|
|
490
|
-
description: t("cli.hooks.install.description")
|
|
491
|
-
},
|
|
492
|
-
args: {
|
|
493
|
-
target: {
|
|
494
|
-
type: "string",
|
|
495
|
-
description: t("cli.hooks.install.args.target.description"),
|
|
496
|
-
default: process.cwd()
|
|
497
|
-
}
|
|
498
|
-
},
|
|
499
|
-
async run({ args }) {
|
|
500
|
-
const result = await installHooks(args.target);
|
|
501
|
-
for (const path of result.installed) {
|
|
502
|
-
console.log(`installed ${path}`);
|
|
503
|
-
}
|
|
504
|
-
for (const path of result.skipped) {
|
|
505
|
-
console.log(`skipped ${path}`);
|
|
506
|
-
}
|
|
507
|
-
for (const message of result.errors) {
|
|
508
|
-
console.error(`error ${message}`);
|
|
509
|
-
}
|
|
510
|
-
}
|
|
511
|
-
})
|
|
512
|
-
}
|
|
513
|
-
});
|
|
514
|
-
var hooks_default = hooksCommand;
|
|
515
|
-
async function installHooks(target, _options = {}) {
|
|
516
|
-
const normalizedTarget = normalizeTarget(target);
|
|
517
|
-
assertExistingDirectory(normalizedTarget);
|
|
518
|
-
const results = [];
|
|
519
|
-
results.push(...await runStep(() => installFabricArchiveSkill(normalizedTarget)));
|
|
520
|
-
results.push(...await runStep(() => installFabricReviewSkill(normalizedTarget)));
|
|
521
|
-
results.push(...await runStep(() => installFabricImportSkill(normalizedTarget)));
|
|
522
|
-
results.push(...await runStep(() => installArchiveHintHook(normalizedTarget)));
|
|
523
|
-
results.push(...await runStep(() => installKnowledgeHintBroadHook(normalizedTarget)));
|
|
524
|
-
results.push(...await runStep(() => installKnowledgeHintNarrowHook(normalizedTarget)));
|
|
525
|
-
results.push(await runSingleStep("claude-hook-config", () => mergeClaudeCodeHookConfig(normalizedTarget)));
|
|
526
|
-
results.push(await runSingleStep("codex-hook-config", () => mergeCodexHookConfig(normalizedTarget)));
|
|
527
|
-
results.push(await runSingleStep("cursor-hook-config", () => mergeCursorHookConfig(normalizedTarget)));
|
|
528
|
-
results.push(...await runStep(() => addArchiveSkillPointer(normalizedTarget)));
|
|
529
|
-
results.push(...validateHookPaths(normalizedTarget));
|
|
530
|
-
return summarizeResults(results);
|
|
531
|
-
}
|
|
532
|
-
function validateHookPaths(projectRoot) {
|
|
533
|
-
const scripts = [
|
|
534
|
-
{ stepSuffix: "", hookFile: "fabric-hint.cjs" },
|
|
535
|
-
{ stepSuffix: "-broad", hookFile: "knowledge-hint-broad.cjs" },
|
|
536
|
-
{ stepSuffix: "-narrow", hookFile: "knowledge-hint-narrow.cjs" }
|
|
537
|
-
];
|
|
538
|
-
const clients = [
|
|
539
|
-
{
|
|
540
|
-
client: "claude",
|
|
541
|
-
configRel: join3(".claude", "settings.json"),
|
|
542
|
-
hookDir: join3(".claude", "hooks")
|
|
543
|
-
},
|
|
544
|
-
{
|
|
545
|
-
client: "codex",
|
|
546
|
-
configRel: join3(".codex", "hooks.json"),
|
|
547
|
-
hookDir: join3(".codex", "hooks")
|
|
548
|
-
},
|
|
549
|
-
{
|
|
550
|
-
client: "cursor",
|
|
551
|
-
configRel: join3(".cursor", "hooks.json"),
|
|
552
|
-
hookDir: join3(".cursor", "hooks")
|
|
553
|
-
}
|
|
554
|
-
];
|
|
555
|
-
const results = [];
|
|
556
|
-
for (const { client, configRel, hookDir } of clients) {
|
|
557
|
-
const configPath = resolve3(projectRoot, configRel);
|
|
558
|
-
if (!existsSync3(configPath)) {
|
|
559
|
-
results.push({
|
|
560
|
-
step: `hook-validate-${client}`,
|
|
561
|
-
path: configPath,
|
|
562
|
-
status: "skipped",
|
|
563
|
-
message: "missing-config"
|
|
564
|
-
});
|
|
565
|
-
continue;
|
|
566
|
-
}
|
|
567
|
-
for (const { stepSuffix, hookFile } of scripts) {
|
|
568
|
-
const expectedHookPath = resolve3(projectRoot, hookDir, hookFile);
|
|
569
|
-
const expectedHookRel = join3(hookDir, hookFile);
|
|
570
|
-
const step = `hook-validate-${client}${stepSuffix}`;
|
|
571
|
-
if (!existsSync3(expectedHookPath)) {
|
|
572
|
-
results.push({
|
|
573
|
-
step,
|
|
574
|
-
path: expectedHookPath,
|
|
575
|
-
status: "error",
|
|
576
|
-
message: `hook script missing: ${expectedHookRel}`
|
|
577
|
-
});
|
|
578
|
-
continue;
|
|
579
|
-
}
|
|
580
|
-
results.push({ step, path: expectedHookPath, status: "skipped", message: "ok" });
|
|
581
|
-
}
|
|
582
|
-
}
|
|
583
|
-
return results;
|
|
584
|
-
}
|
|
585
|
-
async function runStep(fn) {
|
|
586
|
-
try {
|
|
587
|
-
return await fn();
|
|
588
|
-
} catch (error) {
|
|
589
|
-
return [
|
|
590
|
-
{
|
|
591
|
-
step: "hook-install",
|
|
592
|
-
path: "",
|
|
593
|
-
status: "error",
|
|
594
|
-
message: error instanceof Error ? error.message : String(error)
|
|
595
|
-
}
|
|
596
|
-
];
|
|
597
|
-
}
|
|
598
|
-
}
|
|
599
|
-
async function runSingleStep(step, fn) {
|
|
600
|
-
try {
|
|
601
|
-
return await fn();
|
|
602
|
-
} catch (error) {
|
|
603
|
-
return {
|
|
604
|
-
step,
|
|
605
|
-
path: "",
|
|
606
|
-
status: "error",
|
|
607
|
-
message: error instanceof Error ? error.message : String(error)
|
|
608
|
-
};
|
|
609
|
-
}
|
|
610
|
-
}
|
|
611
|
-
function summarizeResults(results) {
|
|
612
|
-
const installed = [];
|
|
613
|
-
const skipped = [];
|
|
614
|
-
const errors = [];
|
|
615
|
-
for (const r of results) {
|
|
616
|
-
switch (r.status) {
|
|
617
|
-
case "written":
|
|
618
|
-
installed.push(r.path);
|
|
619
|
-
break;
|
|
620
|
-
case "skipped":
|
|
621
|
-
skipped.push(r.path);
|
|
622
|
-
break;
|
|
623
|
-
case "error":
|
|
624
|
-
errors.push(`${r.step} ${r.path}: ${r.message ?? "unknown error"}`);
|
|
625
|
-
break;
|
|
626
|
-
}
|
|
627
|
-
}
|
|
628
|
-
return { installed, skipped, errors };
|
|
629
|
-
}
|
|
630
|
-
function normalizeTarget(targetInput) {
|
|
631
|
-
return isAbsolute(targetInput) ? targetInput : resolve3(process.cwd(), targetInput);
|
|
632
|
-
}
|
|
633
|
-
function assertExistingDirectory(target) {
|
|
634
|
-
if (!existsSync3(target) || !statSync(target).isDirectory()) {
|
|
635
|
-
throw new Error(t("cli.shared.target-invalid", { target }));
|
|
636
|
-
}
|
|
637
|
-
}
|
|
638
|
-
|
|
639
|
-
export {
|
|
640
|
-
createServerEntry,
|
|
641
|
-
normalizeConfigPath,
|
|
642
|
-
writeJsonClientConfig,
|
|
643
|
-
ClaudeCodeCLIWriter,
|
|
644
|
-
CursorWriter,
|
|
645
|
-
installFabricArchiveSkill,
|
|
646
|
-
installFabricReviewSkill,
|
|
647
|
-
installFabricImportSkill,
|
|
648
|
-
installArchiveHintHook,
|
|
649
|
-
installKnowledgeHintBroadHook,
|
|
650
|
-
installKnowledgeHintNarrowHook,
|
|
651
|
-
mergeClaudeCodeHookConfig,
|
|
652
|
-
mergeCodexHookConfig,
|
|
653
|
-
mergeCursorHookConfig,
|
|
654
|
-
addArchiveSkillPointer,
|
|
655
|
-
hooksCommand,
|
|
656
|
-
hooks_default,
|
|
657
|
-
installHooks
|
|
658
|
-
};
|