@fenglimg/fabric-cli 2.0.0-rc.1 → 2.0.0-rc.10
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.md +6 -6
- package/dist/chunk-6ICJICVU.js +10 -0
- package/dist/chunk-AW3G7ZH5.js +576 -0
- package/dist/chunk-HQLEHH4O.js +321 -0
- package/dist/{chunk-UHNP7T7W.js → chunk-MT3R57VG.js} +346 -86
- package/dist/{chunk-5LOYBXWD.js → chunk-OBQU6NHO.js} +2 -52
- package/dist/chunk-WPTA74BY.js +184 -0
- package/dist/chunk-WWNXR34K.js +49 -0
- package/dist/doctor-RILCO5OG.js +282 -0
- package/dist/hooks-NX32PPEN.js +13 -0
- package/dist/index.js +8 -5
- package/dist/{init-DRHUYHYA.js → init-SAVH4SKE.js} +188 -491
- package/dist/plan-context-hint-QMUPAXIB.js +98 -0
- package/dist/{scan-HU2EGITF.js → scan-ELSNCSKS.js} +4 -2
- package/dist/{serve-3LXXSBFR.js → serve-NGLXHDYC.js} +8 -4
- package/dist/uninstall-DBAR2JBS.js +1082 -0
- package/package.json +3 -3
- package/templates/bootstrap/CLAUDE.md +1 -1
- package/templates/bootstrap/codex-AGENTS-header.md +1 -1
- package/templates/bootstrap/cursor-fabric-bootstrap.mdc +1 -1
- package/templates/hooks/configs/README.md +73 -0
- package/templates/hooks/configs/claude-code.json +37 -0
- package/templates/hooks/configs/codex-hooks.json +20 -0
- package/templates/hooks/configs/cursor-hooks.json +20 -0
- package/templates/hooks/fabric-hint.cjs +1337 -0
- package/templates/hooks/knowledge-hint-broad.cjs +612 -0
- package/templates/hooks/knowledge-hint-narrow.cjs +826 -0
- package/templates/hooks/lib/session-digest-writer.cjs +172 -0
- package/templates/skills/fabric-archive/SKILL.md +486 -0
- package/templates/skills/fabric-import/SKILL.md +560 -0
- package/templates/skills/fabric-review/SKILL.md +382 -0
- package/dist/doctor-DUHWLAYD.js +0 -98
package/README.md
CHANGED
|
@@ -1,16 +1,16 @@
|
|
|
1
|
-
# @fenglimg/fabric-cli
|
|
2
|
-
|
|
1
|
+
# @fenglimg/fabric-cli
|
|
2
|
+
|
|
3
3
|
`fabric` 是 Fabric 的主命令,`fab` 是永久别名,两者等价。
|
|
4
|
-
|
|
4
|
+
|
|
5
5
|
## 快速开始
|
|
6
6
|
|
|
7
7
|
1. 在 monorepo 根目录运行 `pnpm install`。
|
|
8
8
|
2. 用 `pnpm --filter @fenglimg/fabric-cli build` 构建 CLI。
|
|
9
9
|
3. 在目标项目运行 `fabric init`,完成一站式初始化。
|
|
10
|
-
4. 启动 `fabric serve`,再去客户端里验证 `fab_plan_context` 和 `
|
|
10
|
+
4. 启动 `fabric serve`,再去客户端里验证 `fab_plan_context` 和 `fab_get_knowledge_sections`。
|
|
11
11
|
|
|
12
12
|
`fabric init` 会自动准备 bootstrap、MCP 配置和 git hooks。公共命令面只保留 `init`、`scan`、`doctor`、`serve`。
|
|
13
|
-
|
|
13
|
+
|
|
14
14
|
## 常用命令
|
|
15
15
|
|
|
16
16
|
- `fabric init`
|
|
@@ -21,4 +21,4 @@
|
|
|
21
21
|
- `fabric doctor --fix`
|
|
22
22
|
- `fabric serve`
|
|
23
23
|
|
|
24
|
-
`fabric doctor --fix` 只修复确定性的派生状态,例如 `.fabric/agents.meta.json`、`.fabric/
|
|
24
|
+
`fabric doctor --fix` 只修复确定性的派生状态,例如 `.fabric/agents.meta.json`、`.fabric/.cache/knowledge-test.index.json`、缺失的 `.fabric/events.jsonl` 和 stale hashes;语义冲突、缺失 rule section、未完成的初始化确认仍需要人工处理。
|
|
@@ -0,0 +1,576 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
// src/install/skills-and-hooks.ts
|
|
4
|
+
import { chmodSync, existsSync as existsSync2, readFileSync } from "fs";
|
|
5
|
+
import { mkdir as mkdir2, readFile as readFile2 } from "fs/promises";
|
|
6
|
+
import { dirname as dirname2, join as join2, parse, resolve as resolve2 } from "path";
|
|
7
|
+
import { fileURLToPath } from "url";
|
|
8
|
+
import { atomicWriteJson as atomicWriteJson2, atomicWriteText } from "@fenglimg/fabric-shared/node/atomic-write";
|
|
9
|
+
|
|
10
|
+
// src/config/json.ts
|
|
11
|
+
import { existsSync } from "fs";
|
|
12
|
+
import { mkdir, readFile } from "fs/promises";
|
|
13
|
+
import { dirname, join, resolve } from "path";
|
|
14
|
+
import { homedir } from "os";
|
|
15
|
+
import { atomicWriteJson } from "@fenglimg/fabric-shared/node/atomic-write";
|
|
16
|
+
|
|
17
|
+
// src/config/writer.ts
|
|
18
|
+
function createServerEntry(serverPath) {
|
|
19
|
+
return {
|
|
20
|
+
command: process.execPath,
|
|
21
|
+
args: [serverPath]
|
|
22
|
+
};
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
// src/config/json.ts
|
|
26
|
+
function deepMerge(target, source, options = {}) {
|
|
27
|
+
return deepMergeAtPath(target, source, "", options);
|
|
28
|
+
}
|
|
29
|
+
function deepMergeAtPath(target, source, path, options) {
|
|
30
|
+
if (options.arrayAppendPaths && options.arrayAppendPaths.includes(path) && Array.isArray(target) && Array.isArray(source)) {
|
|
31
|
+
return appendArrayWithDedupe(target, source);
|
|
32
|
+
}
|
|
33
|
+
if (target === null || typeof target !== "object" || Array.isArray(target) || source === null || typeof source !== "object" || Array.isArray(source)) {
|
|
34
|
+
return source;
|
|
35
|
+
}
|
|
36
|
+
const out = { ...target };
|
|
37
|
+
for (const key of Object.keys(source)) {
|
|
38
|
+
const childPath = path === "" ? key : `${path}.${key}`;
|
|
39
|
+
out[key] = deepMergeAtPath(
|
|
40
|
+
target[key],
|
|
41
|
+
source[key],
|
|
42
|
+
childPath,
|
|
43
|
+
options
|
|
44
|
+
);
|
|
45
|
+
}
|
|
46
|
+
return out;
|
|
47
|
+
}
|
|
48
|
+
function appendArrayWithDedupe(target, source) {
|
|
49
|
+
const out = [...target];
|
|
50
|
+
for (const candidate of source) {
|
|
51
|
+
if (out.some((existing) => isSameHookEntry(existing, candidate))) {
|
|
52
|
+
continue;
|
|
53
|
+
}
|
|
54
|
+
out.push(candidate);
|
|
55
|
+
}
|
|
56
|
+
return out;
|
|
57
|
+
}
|
|
58
|
+
function isSameHookEntry(a, b) {
|
|
59
|
+
const cmdA = extractHookCommand(a);
|
|
60
|
+
const cmdB = extractHookCommand(b);
|
|
61
|
+
if (cmdA !== null && cmdB !== null) {
|
|
62
|
+
return cmdA === cmdB;
|
|
63
|
+
}
|
|
64
|
+
return deepEqual(a, b);
|
|
65
|
+
}
|
|
66
|
+
function extractHookCommand(item) {
|
|
67
|
+
if (item === null || typeof item !== "object") {
|
|
68
|
+
return null;
|
|
69
|
+
}
|
|
70
|
+
const obj = item;
|
|
71
|
+
if (typeof obj.command === "string") {
|
|
72
|
+
return obj.command;
|
|
73
|
+
}
|
|
74
|
+
if (Array.isArray(obj.hooks)) {
|
|
75
|
+
for (const inner of obj.hooks) {
|
|
76
|
+
if (inner !== null && typeof inner === "object") {
|
|
77
|
+
const innerObj = inner;
|
|
78
|
+
if (typeof innerObj.command === "string") {
|
|
79
|
+
return innerObj.command;
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
return null;
|
|
85
|
+
}
|
|
86
|
+
function deepEqual(a, b) {
|
|
87
|
+
if (a === b) {
|
|
88
|
+
return true;
|
|
89
|
+
}
|
|
90
|
+
if (a === null || b === null || typeof a !== "object" || typeof b !== "object") {
|
|
91
|
+
return false;
|
|
92
|
+
}
|
|
93
|
+
if (Array.isArray(a) !== Array.isArray(b)) {
|
|
94
|
+
return false;
|
|
95
|
+
}
|
|
96
|
+
if (Array.isArray(a) && Array.isArray(b)) {
|
|
97
|
+
if (a.length !== b.length) {
|
|
98
|
+
return false;
|
|
99
|
+
}
|
|
100
|
+
return a.every((value, index) => deepEqual(value, b[index]));
|
|
101
|
+
}
|
|
102
|
+
const aObj = a;
|
|
103
|
+
const bObj = b;
|
|
104
|
+
const aKeys = Object.keys(aObj);
|
|
105
|
+
const bKeys = Object.keys(bObj);
|
|
106
|
+
if (aKeys.length !== bKeys.length) {
|
|
107
|
+
return false;
|
|
108
|
+
}
|
|
109
|
+
return aKeys.every((key) => deepEqual(aObj[key], bObj[key]));
|
|
110
|
+
}
|
|
111
|
+
function expandHome(filePath) {
|
|
112
|
+
if (filePath === "~") {
|
|
113
|
+
return homedir();
|
|
114
|
+
}
|
|
115
|
+
if (filePath.startsWith("~/")) {
|
|
116
|
+
return join(homedir(), filePath.slice(2));
|
|
117
|
+
}
|
|
118
|
+
return filePath;
|
|
119
|
+
}
|
|
120
|
+
function normalizeConfigPath(filePath) {
|
|
121
|
+
return resolve(expandHome(filePath));
|
|
122
|
+
}
|
|
123
|
+
async function readJsonConfig(configPath) {
|
|
124
|
+
try {
|
|
125
|
+
const raw = await readFile(configPath, "utf8");
|
|
126
|
+
if (raw.trim().length === 0) {
|
|
127
|
+
return {};
|
|
128
|
+
}
|
|
129
|
+
const parsed = JSON.parse(raw);
|
|
130
|
+
if (parsed === null || typeof parsed !== "object" || Array.isArray(parsed)) {
|
|
131
|
+
throw new Error(`Expected JSON object in ${configPath}`);
|
|
132
|
+
}
|
|
133
|
+
return parsed;
|
|
134
|
+
} catch (error) {
|
|
135
|
+
if (error instanceof Error && "code" in error && error.code === "ENOENT") {
|
|
136
|
+
return {};
|
|
137
|
+
}
|
|
138
|
+
throw error;
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
async function writeJsonClientConfig(configPath, serverEntry) {
|
|
142
|
+
const existing = await readJsonConfig(configPath);
|
|
143
|
+
const merged = deepMerge(existing, { mcpServers: { fabric: serverEntry } });
|
|
144
|
+
await mkdir(dirname(configPath), { recursive: true });
|
|
145
|
+
await atomicWriteJson(configPath, merged, { indent: 2 });
|
|
146
|
+
}
|
|
147
|
+
async function removeJsonClientConfigEntry(configPath, serverName) {
|
|
148
|
+
if (!existsSync(configPath)) {
|
|
149
|
+
return { status: "skipped", path: configPath, message: "no-config-file" };
|
|
150
|
+
}
|
|
151
|
+
let existing;
|
|
152
|
+
try {
|
|
153
|
+
existing = await readJsonConfig(configPath);
|
|
154
|
+
} catch (error) {
|
|
155
|
+
return {
|
|
156
|
+
status: "error",
|
|
157
|
+
path: configPath,
|
|
158
|
+
message: error instanceof Error ? error.message : String(error)
|
|
159
|
+
};
|
|
160
|
+
}
|
|
161
|
+
const mcpServers = existing.mcpServers;
|
|
162
|
+
if (mcpServers === void 0 || mcpServers === null || typeof mcpServers !== "object" || Array.isArray(mcpServers)) {
|
|
163
|
+
return { status: "skipped", path: configPath, message: "no-mcp-servers-object" };
|
|
164
|
+
}
|
|
165
|
+
const servers = mcpServers;
|
|
166
|
+
if (!Object.prototype.hasOwnProperty.call(servers, serverName)) {
|
|
167
|
+
return { status: "skipped", path: configPath, message: "not-present" };
|
|
168
|
+
}
|
|
169
|
+
const nextServers = { ...servers };
|
|
170
|
+
delete nextServers[serverName];
|
|
171
|
+
const next = { ...existing, mcpServers: nextServers };
|
|
172
|
+
try {
|
|
173
|
+
await mkdir(dirname(configPath), { recursive: true });
|
|
174
|
+
await atomicWriteJson(configPath, next, { indent: 2 });
|
|
175
|
+
return { status: "removed", path: configPath };
|
|
176
|
+
} catch (error) {
|
|
177
|
+
return {
|
|
178
|
+
status: "error",
|
|
179
|
+
path: configPath,
|
|
180
|
+
message: error instanceof Error ? error.message : String(error)
|
|
181
|
+
};
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
var JsonClientConfigWriter = class {
|
|
185
|
+
configuredPath;
|
|
186
|
+
constructor(configuredPath) {
|
|
187
|
+
this.configuredPath = configuredPath;
|
|
188
|
+
}
|
|
189
|
+
async detect(workspaceRoot, overridePath) {
|
|
190
|
+
const explicitPath = overridePath ?? this.configuredPath;
|
|
191
|
+
if (explicitPath !== void 0) {
|
|
192
|
+
return normalizeConfigPath(explicitPath);
|
|
193
|
+
}
|
|
194
|
+
const configPath = this.defaultPath(workspaceRoot);
|
|
195
|
+
return configPath === null ? null : normalizeConfigPath(configPath);
|
|
196
|
+
}
|
|
197
|
+
async write(serverPath, workspaceRoot, overridePath) {
|
|
198
|
+
const configPath = await this.detect(workspaceRoot, overridePath);
|
|
199
|
+
if (configPath === null) {
|
|
200
|
+
return;
|
|
201
|
+
}
|
|
202
|
+
await writeJsonClientConfig(configPath, createServerEntry(serverPath));
|
|
203
|
+
}
|
|
204
|
+
async remove(serverName, workspaceRoot, overridePath) {
|
|
205
|
+
const configPath = await this.detect(workspaceRoot, overridePath);
|
|
206
|
+
if (configPath === null) {
|
|
207
|
+
return { status: "skipped", message: "no-config-path" };
|
|
208
|
+
}
|
|
209
|
+
return removeJsonClientConfigEntry(configPath, serverName);
|
|
210
|
+
}
|
|
211
|
+
};
|
|
212
|
+
var ClaudeCodeCLIWriter = class extends JsonClientConfigWriter {
|
|
213
|
+
clientKind = "ClaudeCodeCLI";
|
|
214
|
+
scope;
|
|
215
|
+
constructor(configuredPath, scope = "project") {
|
|
216
|
+
super(configuredPath);
|
|
217
|
+
this.scope = scope;
|
|
218
|
+
}
|
|
219
|
+
// Writes to project-level .mcp.json (per Claude Code MCP spec) by default,
|
|
220
|
+
// or ~/.claude.json for user scope.
|
|
221
|
+
// Detection still checks ~/.claude to confirm Claude Code is installed.
|
|
222
|
+
defaultPath(workspaceRoot) {
|
|
223
|
+
const globalClaudeDir = join(homedir(), ".claude");
|
|
224
|
+
const projectClaudeDir = join(workspaceRoot, ".claude");
|
|
225
|
+
if (!existsSync(globalClaudeDir) && !existsSync(projectClaudeDir)) {
|
|
226
|
+
return null;
|
|
227
|
+
}
|
|
228
|
+
return this.scope === "user" ? join(homedir(), ".claude.json") : join(workspaceRoot, ".mcp.json");
|
|
229
|
+
}
|
|
230
|
+
};
|
|
231
|
+
var CursorWriter = class extends JsonClientConfigWriter {
|
|
232
|
+
clientKind = "Cursor";
|
|
233
|
+
constructor(configuredPath) {
|
|
234
|
+
super(configuredPath);
|
|
235
|
+
}
|
|
236
|
+
defaultPath(workspaceRoot) {
|
|
237
|
+
const cursorDir = join(workspaceRoot, ".cursor");
|
|
238
|
+
return existsSync(cursorDir) ? join(cursorDir, "mcp.json") : null;
|
|
239
|
+
}
|
|
240
|
+
};
|
|
241
|
+
|
|
242
|
+
// src/install/skills-and-hooks.ts
|
|
243
|
+
var SKILL_TEMPLATE_REL = "skills/fabric-archive/SKILL.md";
|
|
244
|
+
var SKILL_REVIEW_TEMPLATE_REL = "skills/fabric-review/SKILL.md";
|
|
245
|
+
var SKILL_IMPORT_TEMPLATE_REL = "skills/fabric-import/SKILL.md";
|
|
246
|
+
var HOOK_SCRIPT_TEMPLATE_REL = "hooks/fabric-hint.cjs";
|
|
247
|
+
var HOOK_BROAD_SCRIPT_TEMPLATE_REL = "hooks/knowledge-hint-broad.cjs";
|
|
248
|
+
var HOOK_NARROW_SCRIPT_TEMPLATE_REL = "hooks/knowledge-hint-narrow.cjs";
|
|
249
|
+
var CLAUDE_HOOK_CONFIG_TEMPLATE_REL = "hooks/configs/claude-code.json";
|
|
250
|
+
var CODEX_HOOK_CONFIG_TEMPLATE_REL = "hooks/configs/codex-hooks.json";
|
|
251
|
+
var CURSOR_HOOK_CONFIG_TEMPLATE_REL = "hooks/configs/cursor-hooks.json";
|
|
252
|
+
var SKILL_DESTINATIONS = {
|
|
253
|
+
fabricArchive: [
|
|
254
|
+
".claude/skills/fabric-archive/SKILL.md",
|
|
255
|
+
".codex/skills/fabric-archive/SKILL.md"
|
|
256
|
+
],
|
|
257
|
+
fabricReview: [
|
|
258
|
+
".claude/skills/fabric-review/SKILL.md",
|
|
259
|
+
".codex/skills/fabric-review/SKILL.md"
|
|
260
|
+
],
|
|
261
|
+
fabricImport: [
|
|
262
|
+
".claude/skills/fabric-import/SKILL.md",
|
|
263
|
+
".codex/skills/fabric-import/SKILL.md"
|
|
264
|
+
]
|
|
265
|
+
};
|
|
266
|
+
var HOOK_SCRIPT_DESTINATIONS = {
|
|
267
|
+
fabricHint: [
|
|
268
|
+
".claude/hooks/fabric-hint.cjs",
|
|
269
|
+
".codex/hooks/fabric-hint.cjs",
|
|
270
|
+
".cursor/hooks/fabric-hint.cjs"
|
|
271
|
+
],
|
|
272
|
+
knowledgeHintBroad: [
|
|
273
|
+
".claude/hooks/knowledge-hint-broad.cjs",
|
|
274
|
+
".codex/hooks/knowledge-hint-broad.cjs",
|
|
275
|
+
".cursor/hooks/knowledge-hint-broad.cjs"
|
|
276
|
+
],
|
|
277
|
+
knowledgeHintNarrow: [
|
|
278
|
+
".claude/hooks/knowledge-hint-narrow.cjs",
|
|
279
|
+
".codex/hooks/knowledge-hint-narrow.cjs",
|
|
280
|
+
".cursor/hooks/knowledge-hint-narrow.cjs"
|
|
281
|
+
]
|
|
282
|
+
};
|
|
283
|
+
var HOOK_CONFIG_TARGETS = {
|
|
284
|
+
claudeCode: ".claude/settings.json",
|
|
285
|
+
codex: ".codex/hooks.json",
|
|
286
|
+
cursor: ".cursor/hooks.json"
|
|
287
|
+
};
|
|
288
|
+
var HOOK_CONFIG_ARRAY_PATHS = {
|
|
289
|
+
claudeCode: ["hooks.Stop", "hooks.SessionStart", "hooks.PreToolUse"],
|
|
290
|
+
codex: ["events.Stop", "events.SessionStart", "events.PreToolUse"],
|
|
291
|
+
cursor: ["events.Stop", "events.SessionStart", "events.PreToolUse"]
|
|
292
|
+
};
|
|
293
|
+
var FABRIC_HOOK_COMMAND_PATHS = {
|
|
294
|
+
claudeCode: {
|
|
295
|
+
fabricHint: ".claude/hooks/fabric-hint.cjs",
|
|
296
|
+
knowledgeHintBroad: ".claude/hooks/knowledge-hint-broad.cjs",
|
|
297
|
+
knowledgeHintNarrow: ".claude/hooks/knowledge-hint-narrow.cjs"
|
|
298
|
+
},
|
|
299
|
+
codex: {
|
|
300
|
+
fabricHint: ".codex/hooks/fabric-hint.cjs",
|
|
301
|
+
knowledgeHintBroad: ".codex/hooks/knowledge-hint-broad.cjs",
|
|
302
|
+
knowledgeHintNarrow: ".codex/hooks/knowledge-hint-narrow.cjs"
|
|
303
|
+
},
|
|
304
|
+
cursor: {
|
|
305
|
+
fabricHint: ".cursor/hooks/fabric-hint.cjs",
|
|
306
|
+
knowledgeHintBroad: ".cursor/hooks/knowledge-hint-broad.cjs",
|
|
307
|
+
knowledgeHintNarrow: ".cursor/hooks/knowledge-hint-narrow.cjs"
|
|
308
|
+
}
|
|
309
|
+
};
|
|
310
|
+
var POINTER_LINE = "> Use the fabric-archive Skill when archiving knowledge entries (see .claude/skills/fabric-archive/SKILL.md).";
|
|
311
|
+
var REVIEW_POINTER_LINE = "> Use the fabric-review Skill to review pending knowledge entries (see .claude/skills/fabric-review/SKILL.md).";
|
|
312
|
+
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).";
|
|
313
|
+
var POINTER_TARGETS = ["CLAUDE.md", "AGENTS.md", join2(".cursor", "rules")];
|
|
314
|
+
async function installFabricArchiveSkill(projectRoot, _options = {}) {
|
|
315
|
+
const source = await readTemplate(SKILL_TEMPLATE_REL);
|
|
316
|
+
const targets = SKILL_DESTINATIONS.fabricArchive.map((rel) => join2(projectRoot, rel));
|
|
317
|
+
const results = [];
|
|
318
|
+
for (const target of targets) {
|
|
319
|
+
results.push(await copyTextIdempotent("skill", source, target));
|
|
320
|
+
}
|
|
321
|
+
return results;
|
|
322
|
+
}
|
|
323
|
+
async function installFabricReviewSkill(projectRoot, _options = {}) {
|
|
324
|
+
const source = await readTemplate(SKILL_REVIEW_TEMPLATE_REL);
|
|
325
|
+
const targets = SKILL_DESTINATIONS.fabricReview.map((rel) => join2(projectRoot, rel));
|
|
326
|
+
const results = [];
|
|
327
|
+
for (const target of targets) {
|
|
328
|
+
results.push(await copyTextIdempotent("skill-review", source, target));
|
|
329
|
+
}
|
|
330
|
+
return results;
|
|
331
|
+
}
|
|
332
|
+
async function installFabricImportSkill(projectRoot, _options = {}) {
|
|
333
|
+
const source = await readTemplate(SKILL_IMPORT_TEMPLATE_REL);
|
|
334
|
+
const targets = SKILL_DESTINATIONS.fabricImport.map((rel) => join2(projectRoot, rel));
|
|
335
|
+
const results = [];
|
|
336
|
+
for (const target of targets) {
|
|
337
|
+
results.push(await copyTextIdempotent("skill-import", source, target));
|
|
338
|
+
}
|
|
339
|
+
return results;
|
|
340
|
+
}
|
|
341
|
+
async function installArchiveHintHook(projectRoot, _options = {}) {
|
|
342
|
+
const source = await readTemplate(HOOK_SCRIPT_TEMPLATE_REL);
|
|
343
|
+
const targets = HOOK_SCRIPT_DESTINATIONS.fabricHint.map((rel) => join2(projectRoot, rel));
|
|
344
|
+
const results = [];
|
|
345
|
+
for (const target of targets) {
|
|
346
|
+
const result = await copyTextIdempotent("hook-script", source, target);
|
|
347
|
+
if (result.status === "written" && process.platform !== "win32") {
|
|
348
|
+
try {
|
|
349
|
+
chmodSync(target, 493);
|
|
350
|
+
} catch {
|
|
351
|
+
}
|
|
352
|
+
}
|
|
353
|
+
results.push(result);
|
|
354
|
+
}
|
|
355
|
+
return results;
|
|
356
|
+
}
|
|
357
|
+
async function installKnowledgeHintBroadHook(projectRoot, _options = {}) {
|
|
358
|
+
const source = await readTemplate(HOOK_BROAD_SCRIPT_TEMPLATE_REL);
|
|
359
|
+
const targets = HOOK_SCRIPT_DESTINATIONS.knowledgeHintBroad.map((rel) => join2(projectRoot, rel));
|
|
360
|
+
const results = [];
|
|
361
|
+
for (const target of targets) {
|
|
362
|
+
const result = await copyTextIdempotent("hook-broad-script", source, target);
|
|
363
|
+
if (result.status === "written" && process.platform !== "win32") {
|
|
364
|
+
try {
|
|
365
|
+
chmodSync(target, 493);
|
|
366
|
+
} catch {
|
|
367
|
+
}
|
|
368
|
+
}
|
|
369
|
+
results.push(result);
|
|
370
|
+
}
|
|
371
|
+
return results;
|
|
372
|
+
}
|
|
373
|
+
async function installKnowledgeHintNarrowHook(projectRoot, _options = {}) {
|
|
374
|
+
const source = await readTemplate(HOOK_NARROW_SCRIPT_TEMPLATE_REL);
|
|
375
|
+
const targets = HOOK_SCRIPT_DESTINATIONS.knowledgeHintNarrow.map((rel) => join2(projectRoot, rel));
|
|
376
|
+
const results = [];
|
|
377
|
+
for (const target of targets) {
|
|
378
|
+
const result = await copyTextIdempotent("hook-narrow-script", source, target);
|
|
379
|
+
if (result.status === "written" && process.platform !== "win32") {
|
|
380
|
+
try {
|
|
381
|
+
chmodSync(target, 493);
|
|
382
|
+
} catch {
|
|
383
|
+
}
|
|
384
|
+
}
|
|
385
|
+
results.push(result);
|
|
386
|
+
}
|
|
387
|
+
return results;
|
|
388
|
+
}
|
|
389
|
+
async function mergeClaudeCodeHookConfig(projectRoot, _options = {}) {
|
|
390
|
+
const fragment = await readJsonTemplate(CLAUDE_HOOK_CONFIG_TEMPLATE_REL);
|
|
391
|
+
const targetPath = join2(projectRoot, HOOK_CONFIG_TARGETS.claudeCode);
|
|
392
|
+
return mergeJsonIdempotent(
|
|
393
|
+
"claude-hook-config",
|
|
394
|
+
targetPath,
|
|
395
|
+
fragment,
|
|
396
|
+
[...HOOK_CONFIG_ARRAY_PATHS.claudeCode]
|
|
397
|
+
);
|
|
398
|
+
}
|
|
399
|
+
async function mergeCodexHookConfig(projectRoot, _options = {}) {
|
|
400
|
+
const fragment = await readJsonTemplate(CODEX_HOOK_CONFIG_TEMPLATE_REL);
|
|
401
|
+
const targetPath = join2(projectRoot, HOOK_CONFIG_TARGETS.codex);
|
|
402
|
+
return mergeJsonIdempotent(
|
|
403
|
+
"codex-hook-config",
|
|
404
|
+
targetPath,
|
|
405
|
+
fragment,
|
|
406
|
+
[...HOOK_CONFIG_ARRAY_PATHS.codex]
|
|
407
|
+
);
|
|
408
|
+
}
|
|
409
|
+
async function mergeCursorHookConfig(projectRoot, _options = {}) {
|
|
410
|
+
const fragment = await readJsonTemplate(CURSOR_HOOK_CONFIG_TEMPLATE_REL);
|
|
411
|
+
const targetPath = join2(projectRoot, HOOK_CONFIG_TARGETS.cursor);
|
|
412
|
+
return mergeJsonIdempotent(
|
|
413
|
+
"cursor-hook-config",
|
|
414
|
+
targetPath,
|
|
415
|
+
fragment,
|
|
416
|
+
[...HOOK_CONFIG_ARRAY_PATHS.cursor]
|
|
417
|
+
);
|
|
418
|
+
}
|
|
419
|
+
async function addArchiveSkillPointer(projectRoot, _options = {}) {
|
|
420
|
+
const results = [];
|
|
421
|
+
for (const rel of POINTER_TARGETS) {
|
|
422
|
+
const target = join2(projectRoot, rel);
|
|
423
|
+
if (!existsSync2(target)) {
|
|
424
|
+
results.push({ step: "pointer", path: target, status: "skipped", message: "absent" });
|
|
425
|
+
continue;
|
|
426
|
+
}
|
|
427
|
+
let existing;
|
|
428
|
+
try {
|
|
429
|
+
existing = await readFile2(target, "utf8");
|
|
430
|
+
} catch (error) {
|
|
431
|
+
results.push({
|
|
432
|
+
step: "pointer",
|
|
433
|
+
path: target,
|
|
434
|
+
status: "error",
|
|
435
|
+
message: error instanceof Error ? error.message : String(error)
|
|
436
|
+
});
|
|
437
|
+
continue;
|
|
438
|
+
}
|
|
439
|
+
let next = existing;
|
|
440
|
+
let wrote = false;
|
|
441
|
+
if (next.includes(POINTER_LINE)) {
|
|
442
|
+
results.push({ step: "pointer", path: target, status: "skipped", message: "already-present" });
|
|
443
|
+
} else {
|
|
444
|
+
const trailingNewline = next.length === 0 || next.endsWith("\n") ? "" : "\n";
|
|
445
|
+
next = `${next}${trailingNewline}
|
|
446
|
+
${POINTER_LINE}
|
|
447
|
+
`;
|
|
448
|
+
wrote = true;
|
|
449
|
+
results.push({ step: "pointer", path: target, status: "written" });
|
|
450
|
+
}
|
|
451
|
+
if (next.includes(REVIEW_POINTER_LINE)) {
|
|
452
|
+
results.push({ step: "pointer-review", path: target, status: "skipped", message: "already-present" });
|
|
453
|
+
} else {
|
|
454
|
+
const trailingNewline = next.length === 0 || next.endsWith("\n") ? "" : "\n";
|
|
455
|
+
next = `${next}${trailingNewline}
|
|
456
|
+
${REVIEW_POINTER_LINE}
|
|
457
|
+
`;
|
|
458
|
+
wrote = true;
|
|
459
|
+
results.push({ step: "pointer-review", path: target, status: "written" });
|
|
460
|
+
}
|
|
461
|
+
if (next.includes(IMPORT_POINTER_LINE)) {
|
|
462
|
+
results.push({ step: "pointer-import", path: target, status: "skipped", message: "already-present" });
|
|
463
|
+
} else {
|
|
464
|
+
const trailingNewline = next.length === 0 || next.endsWith("\n") ? "" : "\n";
|
|
465
|
+
next = `${next}${trailingNewline}
|
|
466
|
+
${IMPORT_POINTER_LINE}
|
|
467
|
+
`;
|
|
468
|
+
wrote = true;
|
|
469
|
+
results.push({ step: "pointer-import", path: target, status: "written" });
|
|
470
|
+
}
|
|
471
|
+
if (wrote) {
|
|
472
|
+
await atomicWriteText(target, next);
|
|
473
|
+
}
|
|
474
|
+
}
|
|
475
|
+
return results;
|
|
476
|
+
}
|
|
477
|
+
async function copyTextIdempotent(step, source, target) {
|
|
478
|
+
if (existsSync2(target)) {
|
|
479
|
+
try {
|
|
480
|
+
const existing = readFileSync(target, "utf8");
|
|
481
|
+
if (existing === source) {
|
|
482
|
+
return { step, path: target, status: "skipped", message: "up-to-date" };
|
|
483
|
+
}
|
|
484
|
+
} catch {
|
|
485
|
+
}
|
|
486
|
+
}
|
|
487
|
+
await mkdir2(dirname2(target), { recursive: true });
|
|
488
|
+
await atomicWriteText(target, source);
|
|
489
|
+
return { step, path: target, status: "written" };
|
|
490
|
+
}
|
|
491
|
+
async function mergeJsonIdempotent(step, target, fragment, arrayAppendPaths) {
|
|
492
|
+
const existing = await readJsonObjectOrEmpty(target);
|
|
493
|
+
const merged = deepMerge(existing, fragment, { arrayAppendPaths });
|
|
494
|
+
if (jsonEqual(existing, merged)) {
|
|
495
|
+
return { step, path: target, status: "skipped", message: "up-to-date" };
|
|
496
|
+
}
|
|
497
|
+
await mkdir2(dirname2(target), { recursive: true });
|
|
498
|
+
await atomicWriteJson2(target, merged, { indent: 2 });
|
|
499
|
+
return { step, path: target, status: "written" };
|
|
500
|
+
}
|
|
501
|
+
async function readJsonObjectOrEmpty(path) {
|
|
502
|
+
try {
|
|
503
|
+
const raw = await readFile2(path, "utf8");
|
|
504
|
+
if (raw.trim().length === 0) {
|
|
505
|
+
return {};
|
|
506
|
+
}
|
|
507
|
+
const parsed = JSON.parse(raw);
|
|
508
|
+
if (parsed === null || typeof parsed !== "object" || Array.isArray(parsed)) {
|
|
509
|
+
return {};
|
|
510
|
+
}
|
|
511
|
+
return parsed;
|
|
512
|
+
} catch (error) {
|
|
513
|
+
if (error instanceof Error && "code" in error && error.code === "ENOENT") {
|
|
514
|
+
return {};
|
|
515
|
+
}
|
|
516
|
+
throw error;
|
|
517
|
+
}
|
|
518
|
+
}
|
|
519
|
+
function jsonEqual(a, b) {
|
|
520
|
+
return JSON.stringify(a) === JSON.stringify(b);
|
|
521
|
+
}
|
|
522
|
+
async function readTemplate(relativePath) {
|
|
523
|
+
const path = findTemplatePath(relativePath);
|
|
524
|
+
return readFile2(path, "utf8");
|
|
525
|
+
}
|
|
526
|
+
async function readJsonTemplate(relativePath) {
|
|
527
|
+
const raw = await readTemplate(relativePath);
|
|
528
|
+
const parsed = JSON.parse(raw);
|
|
529
|
+
if (parsed === null || typeof parsed !== "object" || Array.isArray(parsed)) {
|
|
530
|
+
throw new Error(`Template at ${relativePath} is not a JSON object`);
|
|
531
|
+
}
|
|
532
|
+
return parsed;
|
|
533
|
+
}
|
|
534
|
+
function findTemplatePath(relativePath) {
|
|
535
|
+
const startDir = dirname2(fileURLToPath(import.meta.url));
|
|
536
|
+
let current = resolve2(startDir);
|
|
537
|
+
while (true) {
|
|
538
|
+
const candidate = join2(current, "templates", relativePath);
|
|
539
|
+
if (existsSync2(candidate)) {
|
|
540
|
+
return candidate;
|
|
541
|
+
}
|
|
542
|
+
const parent = dirname2(current);
|
|
543
|
+
if (parent === current || parse(current).root === current) {
|
|
544
|
+
throw new Error(`Template not found: templates/${relativePath} (searched up from ${startDir})`);
|
|
545
|
+
}
|
|
546
|
+
current = parent;
|
|
547
|
+
}
|
|
548
|
+
}
|
|
549
|
+
|
|
550
|
+
export {
|
|
551
|
+
createServerEntry,
|
|
552
|
+
normalizeConfigPath,
|
|
553
|
+
writeJsonClientConfig,
|
|
554
|
+
removeJsonClientConfigEntry,
|
|
555
|
+
ClaudeCodeCLIWriter,
|
|
556
|
+
CursorWriter,
|
|
557
|
+
SKILL_DESTINATIONS,
|
|
558
|
+
HOOK_SCRIPT_DESTINATIONS,
|
|
559
|
+
HOOK_CONFIG_TARGETS,
|
|
560
|
+
HOOK_CONFIG_ARRAY_PATHS,
|
|
561
|
+
FABRIC_HOOK_COMMAND_PATHS,
|
|
562
|
+
POINTER_LINE,
|
|
563
|
+
REVIEW_POINTER_LINE,
|
|
564
|
+
IMPORT_POINTER_LINE,
|
|
565
|
+
POINTER_TARGETS,
|
|
566
|
+
installFabricArchiveSkill,
|
|
567
|
+
installFabricReviewSkill,
|
|
568
|
+
installFabricImportSkill,
|
|
569
|
+
installArchiveHintHook,
|
|
570
|
+
installKnowledgeHintBroadHook,
|
|
571
|
+
installKnowledgeHintNarrowHook,
|
|
572
|
+
mergeClaudeCodeHookConfig,
|
|
573
|
+
mergeCodexHookConfig,
|
|
574
|
+
mergeCursorHookConfig,
|
|
575
|
+
addArchiveSkillPointer
|
|
576
|
+
};
|