@fenglimg/fabric-cli 2.0.0 → 2.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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-D25XJ4BC.js +880 -0
- package/dist/chunk-MF3OTILQ.js +544 -0
- package/dist/chunk-PWLW3B57.js +18 -0
- package/dist/config-XJIPZNUP.js +13 -0
- package/dist/doctor-EJDSEJSS.js +810 -0
- package/dist/index.js +15 -8
- package/dist/{init-BIRSIOXO.js → install-EKWMFLUU.js} +622 -711
- 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/uninstall-MH7ZIB6M.js +1064 -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 +833 -105
- package/templates/hooks/knowledge-hint-broad.cjs +509 -135
- package/templates/hooks/knowledge-hint-narrow.cjs +791 -26
- package/templates/hooks/lib/banner-i18n.cjs +309 -0
- package/templates/hooks/lib/cite-contract-reminder.cjs +173 -0
- package/templates/hooks/lib/cite-line-parser.cjs +158 -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 +93 -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 +75 -516
- 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 +86 -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/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
|
@@ -0,0 +1,880 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import {
|
|
3
|
+
deepMerge
|
|
4
|
+
} from "./chunk-MF3OTILQ.js";
|
|
5
|
+
|
|
6
|
+
// src/install/write-bootstrap-snapshot.ts
|
|
7
|
+
import { existsSync, readFileSync } from "fs";
|
|
8
|
+
import { mkdir } from "fs/promises";
|
|
9
|
+
import { dirname, join } from "path";
|
|
10
|
+
import { atomicWriteText } from "@fenglimg/fabric-shared/node/atomic-write";
|
|
11
|
+
import { BOOTSTRAP_CANONICAL } from "@fenglimg/fabric-shared/templates/bootstrap-canonical";
|
|
12
|
+
var FABRIC_AGENTS_RELPATH = join(".fabric", "AGENTS.md");
|
|
13
|
+
var PROJECT_RULES_RELPATH = join(".fabric", "project-rules.md");
|
|
14
|
+
function fabricAgentsSnapshotPath(targetRoot) {
|
|
15
|
+
return join(targetRoot, FABRIC_AGENTS_RELPATH);
|
|
16
|
+
}
|
|
17
|
+
function projectRulesPath(targetRoot) {
|
|
18
|
+
return join(targetRoot, PROJECT_RULES_RELPATH);
|
|
19
|
+
}
|
|
20
|
+
function readProjectRulesIfPresent(targetRoot) {
|
|
21
|
+
const path = projectRulesPath(targetRoot);
|
|
22
|
+
if (!existsSync(path)) return null;
|
|
23
|
+
return readFileSync(path, "utf8");
|
|
24
|
+
}
|
|
25
|
+
async function writeFabricAgentsSnapshot(targetRoot) {
|
|
26
|
+
const step = "bootstrap-snapshot";
|
|
27
|
+
const target = fabricAgentsSnapshotPath(targetRoot);
|
|
28
|
+
if (existsSync(target)) {
|
|
29
|
+
try {
|
|
30
|
+
const existing = readFileSync(target, "utf8");
|
|
31
|
+
if (existing === BOOTSTRAP_CANONICAL) {
|
|
32
|
+
return { step, path: target, status: "skipped", message: "up-to-date" };
|
|
33
|
+
}
|
|
34
|
+
} catch {
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
await mkdir(dirname(target), { recursive: true });
|
|
38
|
+
await atomicWriteText(target, BOOTSTRAP_CANONICAL);
|
|
39
|
+
return { step, path: target, status: "written" };
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// src/install/skills-and-hooks.ts
|
|
43
|
+
import { chmodSync, existsSync as existsSync2, readdirSync, readFileSync as readFileSync2, statSync } from "fs";
|
|
44
|
+
import { mkdir as mkdir2, readFile, rm } from "fs/promises";
|
|
45
|
+
import { dirname as dirname2, join as join2, parse, resolve } from "path";
|
|
46
|
+
import { fileURLToPath } from "url";
|
|
47
|
+
import { atomicWriteJson, atomicWriteText as atomicWriteText2 } from "@fenglimg/fabric-shared/node/atomic-write";
|
|
48
|
+
import {
|
|
49
|
+
BOOTSTRAP_MARKER_BEGIN,
|
|
50
|
+
BOOTSTRAP_MARKER_END,
|
|
51
|
+
BOOTSTRAP_REGEX,
|
|
52
|
+
LEGACY_KB_REGEX
|
|
53
|
+
} from "@fenglimg/fabric-shared/templates/bootstrap-canonical";
|
|
54
|
+
var SKILL_TEMPLATE_REL = "skills/fabric-archive/SKILL.md";
|
|
55
|
+
var SKILL_REVIEW_TEMPLATE_REL = "skills/fabric-review/SKILL.md";
|
|
56
|
+
var SKILL_IMPORT_TEMPLATE_REL = "skills/fabric-import/SKILL.md";
|
|
57
|
+
var HOOK_SCRIPT_TEMPLATE_REL = "hooks/fabric-hint.cjs";
|
|
58
|
+
var HOOK_BROAD_SCRIPT_TEMPLATE_REL = "hooks/knowledge-hint-broad.cjs";
|
|
59
|
+
var HOOK_NARROW_SCRIPT_TEMPLATE_REL = "hooks/knowledge-hint-narrow.cjs";
|
|
60
|
+
var HOOK_CITE_EVICT_SCRIPT_TEMPLATE_REL = "hooks/cite-policy-evict.cjs";
|
|
61
|
+
var HOOK_LIB_TEMPLATE_DIR_REL = "hooks/lib";
|
|
62
|
+
var CLAUDE_HOOK_CONFIG_TEMPLATE_REL = "hooks/configs/claude-code.json";
|
|
63
|
+
var CODEX_HOOK_CONFIG_TEMPLATE_REL = "hooks/configs/codex-hooks.json";
|
|
64
|
+
var CURSOR_HOOK_CONFIG_TEMPLATE_REL = "hooks/configs/cursor-hooks.json";
|
|
65
|
+
var SKILL_DESTINATIONS = {
|
|
66
|
+
fabricArchive: [
|
|
67
|
+
".claude/skills/fabric-archive/SKILL.md",
|
|
68
|
+
".codex/skills/fabric-archive/SKILL.md"
|
|
69
|
+
],
|
|
70
|
+
fabricReview: [
|
|
71
|
+
".claude/skills/fabric-review/SKILL.md",
|
|
72
|
+
".codex/skills/fabric-review/SKILL.md"
|
|
73
|
+
],
|
|
74
|
+
fabricImport: [
|
|
75
|
+
".claude/skills/fabric-import/SKILL.md",
|
|
76
|
+
".codex/skills/fabric-import/SKILL.md"
|
|
77
|
+
]
|
|
78
|
+
};
|
|
79
|
+
var DEPRECATED_SKILL_DIRS = [
|
|
80
|
+
".claude/skills/fabric-init",
|
|
81
|
+
".codex/skills/fabric-init"
|
|
82
|
+
];
|
|
83
|
+
var HOOK_SCRIPT_DESTINATIONS = {
|
|
84
|
+
fabricHint: [
|
|
85
|
+
".claude/hooks/fabric-hint.cjs",
|
|
86
|
+
".codex/hooks/fabric-hint.cjs",
|
|
87
|
+
".cursor/hooks/fabric-hint.cjs"
|
|
88
|
+
],
|
|
89
|
+
knowledgeHintBroad: [
|
|
90
|
+
".claude/hooks/knowledge-hint-broad.cjs",
|
|
91
|
+
".codex/hooks/knowledge-hint-broad.cjs",
|
|
92
|
+
".cursor/hooks/knowledge-hint-broad.cjs"
|
|
93
|
+
],
|
|
94
|
+
knowledgeHintNarrow: [
|
|
95
|
+
".claude/hooks/knowledge-hint-narrow.cjs",
|
|
96
|
+
".codex/hooks/knowledge-hint-narrow.cjs",
|
|
97
|
+
".cursor/hooks/knowledge-hint-narrow.cjs"
|
|
98
|
+
],
|
|
99
|
+
// v2.0.0-rc.34 TASK-06: Claude Code — UserPromptSubmit cite-policy long-
|
|
100
|
+
// session evict sidecar.
|
|
101
|
+
// v2.0.0-rc.37 NEW-21: extended to Codex / Cursor SessionStart slots.
|
|
102
|
+
// Those clients don't have an equivalent per-prompt event, so cite-policy-
|
|
103
|
+
// evict.cjs runs in "SessionStart mode" (one-shot stderr emit per session
|
|
104
|
+
// boot, no turn-counter). Cadence is lower than Claude Code's per-prompt
|
|
105
|
+
// window but strictly better than 0 (rc.32 baseline measured Codex/Cursor
|
|
106
|
+
// at 3.1% cite coverage when no cite-reminder surface existed).
|
|
107
|
+
citePolicyEvict: [
|
|
108
|
+
".claude/hooks/cite-policy-evict.cjs",
|
|
109
|
+
".codex/hooks/cite-policy-evict.cjs",
|
|
110
|
+
".cursor/hooks/cite-policy-evict.cjs"
|
|
111
|
+
]
|
|
112
|
+
};
|
|
113
|
+
var HOOK_LIB_DESTINATIONS = [
|
|
114
|
+
".claude/hooks/lib",
|
|
115
|
+
".codex/hooks/lib",
|
|
116
|
+
".cursor/hooks/lib"
|
|
117
|
+
];
|
|
118
|
+
var HOOK_CONFIG_TARGETS = {
|
|
119
|
+
claudeCode: ".claude/settings.json",
|
|
120
|
+
codex: ".codex/hooks.json",
|
|
121
|
+
cursor: ".cursor/hooks.json"
|
|
122
|
+
};
|
|
123
|
+
var HOOK_CONFIG_ARRAY_PATHS = {
|
|
124
|
+
claudeCode: ["hooks.Stop", "hooks.SessionStart", "hooks.PreToolUse"],
|
|
125
|
+
codex: ["events.Stop", "events.SessionStart", "events.PreToolUse"],
|
|
126
|
+
cursor: ["hooks.stop", "hooks.sessionStart", "hooks.preToolUse"]
|
|
127
|
+
};
|
|
128
|
+
var FABRIC_HOOK_COMMAND_PATHS = {
|
|
129
|
+
claudeCode: {
|
|
130
|
+
fabricHint: "${CLAUDE_PROJECT_DIR}/.claude/hooks/fabric-hint.cjs",
|
|
131
|
+
knowledgeHintBroad: "${CLAUDE_PROJECT_DIR}/.claude/hooks/knowledge-hint-broad.cjs",
|
|
132
|
+
knowledgeHintNarrow: "${CLAUDE_PROJECT_DIR}/.claude/hooks/knowledge-hint-narrow.cjs"
|
|
133
|
+
},
|
|
134
|
+
codex: {
|
|
135
|
+
fabricHint: '"$(git rev-parse --show-toplevel)/.codex/hooks/fabric-hint.cjs"',
|
|
136
|
+
knowledgeHintBroad: '"$(git rev-parse --show-toplevel)/.codex/hooks/knowledge-hint-broad.cjs"',
|
|
137
|
+
knowledgeHintNarrow: '"$(git rev-parse --show-toplevel)/.codex/hooks/knowledge-hint-narrow.cjs"'
|
|
138
|
+
},
|
|
139
|
+
cursor: {
|
|
140
|
+
fabricHint: ".cursor/hooks/fabric-hint.cjs",
|
|
141
|
+
knowledgeHintBroad: ".cursor/hooks/knowledge-hint-broad.cjs",
|
|
142
|
+
knowledgeHintNarrow: ".cursor/hooks/knowledge-hint-narrow.cjs"
|
|
143
|
+
}
|
|
144
|
+
};
|
|
145
|
+
function readFabricLanguagePreference(projectRoot) {
|
|
146
|
+
const configPath = join2(projectRoot, ".fabric", "fabric-config.json");
|
|
147
|
+
if (!existsSync2(configPath)) {
|
|
148
|
+
return "match-existing";
|
|
149
|
+
}
|
|
150
|
+
try {
|
|
151
|
+
const raw = readFileSync2(configPath, "utf8");
|
|
152
|
+
const parsed = JSON.parse(raw);
|
|
153
|
+
if (parsed === null || typeof parsed !== "object" || Array.isArray(parsed)) {
|
|
154
|
+
return "match-existing";
|
|
155
|
+
}
|
|
156
|
+
const value = parsed["fabric_language"];
|
|
157
|
+
return typeof value === "string" && value.length > 0 ? value : "match-existing";
|
|
158
|
+
} catch {
|
|
159
|
+
return "match-existing";
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
var SKILL_TOKEN_ERROR_TOKENS = 1e4;
|
|
163
|
+
var STALE_INSTALL_RATIO = 1.5;
|
|
164
|
+
function estimateSkillTokens(text) {
|
|
165
|
+
return Math.ceil(text.length / 3);
|
|
166
|
+
}
|
|
167
|
+
function validateSkillCanonicalSize(source, slug) {
|
|
168
|
+
const tokens = estimateSkillTokens(source);
|
|
169
|
+
if (tokens > SKILL_TOKEN_ERROR_TOKENS) {
|
|
170
|
+
throw new Error(
|
|
171
|
+
`Skill '${slug}' canonical SKILL.md estimates ${tokens} tok (>${SKILL_TOKEN_ERROR_TOKENS} ERROR threshold). Install aborted \u2014 this is a Fabric release bug, not a user-recoverable state. Re-split SKILL.md via progressive disclosure (see fabric-archive/phases/* as canonical example) and rebuild.`
|
|
172
|
+
);
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
function inspectStaleInstall(target, source) {
|
|
176
|
+
if (!existsSync2(target)) return null;
|
|
177
|
+
let existing;
|
|
178
|
+
try {
|
|
179
|
+
existing = readFileSync2(target, "utf8");
|
|
180
|
+
} catch {
|
|
181
|
+
return null;
|
|
182
|
+
}
|
|
183
|
+
const existingTok = estimateSkillTokens(existing);
|
|
184
|
+
const sourceTok = estimateSkillTokens(source);
|
|
185
|
+
if (existingTok > sourceTok * STALE_INSTALL_RATIO) {
|
|
186
|
+
return `stale-replaced (${existingTok} tok \u2192 ${sourceTok} tok canonical)`;
|
|
187
|
+
}
|
|
188
|
+
return null;
|
|
189
|
+
}
|
|
190
|
+
async function installFabricArchiveSkill(projectRoot, _options = {}) {
|
|
191
|
+
const source = await readTemplate(SKILL_TEMPLATE_REL);
|
|
192
|
+
validateSkillCanonicalSize(source, "fabric-archive");
|
|
193
|
+
const targets = SKILL_DESTINATIONS.fabricArchive.map((rel) => join2(projectRoot, rel));
|
|
194
|
+
const results = [];
|
|
195
|
+
for (const target of targets) {
|
|
196
|
+
const staleMsg = inspectStaleInstall(target, source);
|
|
197
|
+
const result = await copyTextIdempotent("skill", source, target);
|
|
198
|
+
if (staleMsg && result.status === "written") {
|
|
199
|
+
result.message = result.message ? `${staleMsg}; ${result.message}` : staleMsg;
|
|
200
|
+
}
|
|
201
|
+
results.push(result);
|
|
202
|
+
}
|
|
203
|
+
results.push(...await installSkillRefFiles(projectRoot, "fabric-archive"));
|
|
204
|
+
return results;
|
|
205
|
+
}
|
|
206
|
+
async function installFabricReviewSkill(projectRoot, _options = {}) {
|
|
207
|
+
const source = await readTemplate(SKILL_REVIEW_TEMPLATE_REL);
|
|
208
|
+
validateSkillCanonicalSize(source, "fabric-review");
|
|
209
|
+
const targets = SKILL_DESTINATIONS.fabricReview.map((rel) => join2(projectRoot, rel));
|
|
210
|
+
const results = [];
|
|
211
|
+
for (const target of targets) {
|
|
212
|
+
const staleMsg = inspectStaleInstall(target, source);
|
|
213
|
+
const result = await copyTextIdempotent("skill-review", source, target);
|
|
214
|
+
if (staleMsg && result.status === "written") {
|
|
215
|
+
result.message = result.message ? `${staleMsg}; ${result.message}` : staleMsg;
|
|
216
|
+
}
|
|
217
|
+
results.push(result);
|
|
218
|
+
}
|
|
219
|
+
results.push(...await installSkillRefFiles(projectRoot, "fabric-review"));
|
|
220
|
+
return results;
|
|
221
|
+
}
|
|
222
|
+
async function installFabricImportSkill(projectRoot, _options = {}) {
|
|
223
|
+
const source = await readTemplate(SKILL_IMPORT_TEMPLATE_REL);
|
|
224
|
+
validateSkillCanonicalSize(source, "fabric-import");
|
|
225
|
+
const targets = SKILL_DESTINATIONS.fabricImport.map((rel) => join2(projectRoot, rel));
|
|
226
|
+
const results = [];
|
|
227
|
+
for (const target of targets) {
|
|
228
|
+
const staleMsg = inspectStaleInstall(target, source);
|
|
229
|
+
const result = await copyTextIdempotent("skill-import", source, target);
|
|
230
|
+
if (staleMsg && result.status === "written") {
|
|
231
|
+
result.message = result.message ? `${staleMsg}; ${result.message}` : staleMsg;
|
|
232
|
+
}
|
|
233
|
+
results.push(result);
|
|
234
|
+
}
|
|
235
|
+
results.push(...await installSkillRefFiles(projectRoot, "fabric-import"));
|
|
236
|
+
return results;
|
|
237
|
+
}
|
|
238
|
+
async function cleanupDeprecatedSkills(projectRoot) {
|
|
239
|
+
const results = [];
|
|
240
|
+
for (const rel of DEPRECATED_SKILL_DIRS) {
|
|
241
|
+
const target = join2(projectRoot, rel);
|
|
242
|
+
if (!existsSync2(target)) {
|
|
243
|
+
results.push({ step: "skill-deprecated-cleanup", path: target, status: "skipped", message: "absent" });
|
|
244
|
+
continue;
|
|
245
|
+
}
|
|
246
|
+
try {
|
|
247
|
+
await rm(target, { recursive: true, force: true });
|
|
248
|
+
results.push({
|
|
249
|
+
step: "skill-deprecated-cleanup",
|
|
250
|
+
path: target,
|
|
251
|
+
status: "written",
|
|
252
|
+
message: "removed-deprecated"
|
|
253
|
+
});
|
|
254
|
+
} catch (error) {
|
|
255
|
+
results.push({
|
|
256
|
+
step: "skill-deprecated-cleanup",
|
|
257
|
+
path: target,
|
|
258
|
+
status: "error",
|
|
259
|
+
message: error instanceof Error ? error.message : String(error)
|
|
260
|
+
});
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
return results;
|
|
264
|
+
}
|
|
265
|
+
async function installSkillRefFiles(projectRoot, skillSlug) {
|
|
266
|
+
let refTemplateDir;
|
|
267
|
+
try {
|
|
268
|
+
refTemplateDir = findTemplatePath(`skills/${skillSlug}/ref`);
|
|
269
|
+
} catch {
|
|
270
|
+
return [
|
|
271
|
+
{
|
|
272
|
+
step: "skill-ref",
|
|
273
|
+
path: `skills/${skillSlug}/ref`,
|
|
274
|
+
status: "skipped",
|
|
275
|
+
message: `no-ref-dir: ${skillSlug}`
|
|
276
|
+
}
|
|
277
|
+
];
|
|
278
|
+
}
|
|
279
|
+
let refFiles;
|
|
280
|
+
try {
|
|
281
|
+
refFiles = readdirSync(refTemplateDir).filter((name) => name.endsWith(".md"));
|
|
282
|
+
} catch {
|
|
283
|
+
return [
|
|
284
|
+
{
|
|
285
|
+
step: "skill-ref",
|
|
286
|
+
path: refTemplateDir,
|
|
287
|
+
status: "skipped",
|
|
288
|
+
message: `no-ref-files: ${skillSlug}`
|
|
289
|
+
}
|
|
290
|
+
];
|
|
291
|
+
}
|
|
292
|
+
if (refFiles.length === 0) {
|
|
293
|
+
return [
|
|
294
|
+
{
|
|
295
|
+
step: "skill-ref",
|
|
296
|
+
path: refTemplateDir,
|
|
297
|
+
status: "skipped",
|
|
298
|
+
message: `no-ref-files: ${skillSlug}`
|
|
299
|
+
}
|
|
300
|
+
];
|
|
301
|
+
}
|
|
302
|
+
const clientPrefixes = [".claude", ".codex"];
|
|
303
|
+
const results = [];
|
|
304
|
+
for (const refFile of refFiles) {
|
|
305
|
+
const sourcePath = join2(refTemplateDir, refFile);
|
|
306
|
+
let source;
|
|
307
|
+
try {
|
|
308
|
+
source = readFileSync2(sourcePath, "utf8");
|
|
309
|
+
} catch (error) {
|
|
310
|
+
results.push({
|
|
311
|
+
step: "skill-ref",
|
|
312
|
+
path: sourcePath,
|
|
313
|
+
status: "error",
|
|
314
|
+
message: error instanceof Error ? error.message : String(error)
|
|
315
|
+
});
|
|
316
|
+
continue;
|
|
317
|
+
}
|
|
318
|
+
for (const prefix of clientPrefixes) {
|
|
319
|
+
const target = join2(projectRoot, prefix, "skills", skillSlug, "ref", refFile);
|
|
320
|
+
results.push(await copyTextIdempotent("skill-ref", source, target));
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
return results;
|
|
324
|
+
}
|
|
325
|
+
async function installSharedSkillLib(projectRoot, _options = {}) {
|
|
326
|
+
let libTemplateDir;
|
|
327
|
+
try {
|
|
328
|
+
libTemplateDir = findTemplatePath("skills/lib");
|
|
329
|
+
} catch {
|
|
330
|
+
return [{ step: "skill-lib", path: "skills/lib", status: "skipped", message: "no-lib-dir" }];
|
|
331
|
+
}
|
|
332
|
+
let libFiles;
|
|
333
|
+
try {
|
|
334
|
+
libFiles = readdirSync(libTemplateDir).filter((name) => name.endsWith(".md"));
|
|
335
|
+
} catch {
|
|
336
|
+
return [{ step: "skill-lib", path: libTemplateDir, status: "skipped", message: "no-lib-files" }];
|
|
337
|
+
}
|
|
338
|
+
const clientPrefixes = [".claude", ".codex"];
|
|
339
|
+
const results = [];
|
|
340
|
+
for (const libFile of libFiles) {
|
|
341
|
+
let source;
|
|
342
|
+
try {
|
|
343
|
+
source = readFileSync2(join2(libTemplateDir, libFile), "utf8");
|
|
344
|
+
} catch (error) {
|
|
345
|
+
results.push({
|
|
346
|
+
step: "skill-lib",
|
|
347
|
+
path: join2(libTemplateDir, libFile),
|
|
348
|
+
status: "error",
|
|
349
|
+
message: error instanceof Error ? error.message : String(error)
|
|
350
|
+
});
|
|
351
|
+
continue;
|
|
352
|
+
}
|
|
353
|
+
for (const prefix of clientPrefixes) {
|
|
354
|
+
const target = join2(projectRoot, prefix, "skills", "lib", libFile);
|
|
355
|
+
results.push(await copyTextIdempotent("skill-lib", source, target));
|
|
356
|
+
}
|
|
357
|
+
}
|
|
358
|
+
return results;
|
|
359
|
+
}
|
|
360
|
+
async function installArchiveHintHook(projectRoot, _options = {}) {
|
|
361
|
+
const source = await readTemplate(HOOK_SCRIPT_TEMPLATE_REL);
|
|
362
|
+
const targets = HOOK_SCRIPT_DESTINATIONS.fabricHint.map((rel) => join2(projectRoot, rel));
|
|
363
|
+
const results = [];
|
|
364
|
+
for (const target of targets) {
|
|
365
|
+
const result = await copyTextIdempotent("hook-script", source, target);
|
|
366
|
+
if (result.status === "written" && process.platform !== "win32") {
|
|
367
|
+
try {
|
|
368
|
+
chmodSync(target, 493);
|
|
369
|
+
} catch {
|
|
370
|
+
}
|
|
371
|
+
}
|
|
372
|
+
results.push(result);
|
|
373
|
+
}
|
|
374
|
+
return results;
|
|
375
|
+
}
|
|
376
|
+
async function installKnowledgeHintBroadHook(projectRoot, _options = {}) {
|
|
377
|
+
const source = await readTemplate(HOOK_BROAD_SCRIPT_TEMPLATE_REL);
|
|
378
|
+
const targets = HOOK_SCRIPT_DESTINATIONS.knowledgeHintBroad.map((rel) => join2(projectRoot, rel));
|
|
379
|
+
const results = [];
|
|
380
|
+
for (const target of targets) {
|
|
381
|
+
const result = await copyTextIdempotent("hook-broad-script", source, target);
|
|
382
|
+
if (result.status === "written" && process.platform !== "win32") {
|
|
383
|
+
try {
|
|
384
|
+
chmodSync(target, 493);
|
|
385
|
+
} catch {
|
|
386
|
+
}
|
|
387
|
+
}
|
|
388
|
+
results.push(result);
|
|
389
|
+
}
|
|
390
|
+
return results;
|
|
391
|
+
}
|
|
392
|
+
async function installKnowledgeHintNarrowHook(projectRoot, _options = {}) {
|
|
393
|
+
const source = await readTemplate(HOOK_NARROW_SCRIPT_TEMPLATE_REL);
|
|
394
|
+
const targets = HOOK_SCRIPT_DESTINATIONS.knowledgeHintNarrow.map((rel) => join2(projectRoot, rel));
|
|
395
|
+
const results = [];
|
|
396
|
+
for (const target of targets) {
|
|
397
|
+
const result = await copyTextIdempotent("hook-narrow-script", source, target);
|
|
398
|
+
if (result.status === "written" && process.platform !== "win32") {
|
|
399
|
+
try {
|
|
400
|
+
chmodSync(target, 493);
|
|
401
|
+
} catch {
|
|
402
|
+
}
|
|
403
|
+
}
|
|
404
|
+
results.push(result);
|
|
405
|
+
}
|
|
406
|
+
return results;
|
|
407
|
+
}
|
|
408
|
+
async function installCitePolicyEvictHook(projectRoot, _options = {}) {
|
|
409
|
+
const source = await readTemplate(HOOK_CITE_EVICT_SCRIPT_TEMPLATE_REL);
|
|
410
|
+
const targets = HOOK_SCRIPT_DESTINATIONS.citePolicyEvict.map((rel) => join2(projectRoot, rel));
|
|
411
|
+
const results = [];
|
|
412
|
+
for (const target of targets) {
|
|
413
|
+
const result = await copyTextIdempotent("hook-cite-evict-script", source, target);
|
|
414
|
+
if (result.status === "written" && process.platform !== "win32") {
|
|
415
|
+
try {
|
|
416
|
+
chmodSync(target, 493);
|
|
417
|
+
} catch {
|
|
418
|
+
}
|
|
419
|
+
}
|
|
420
|
+
results.push(result);
|
|
421
|
+
}
|
|
422
|
+
return results;
|
|
423
|
+
}
|
|
424
|
+
async function installHookLibs(projectRoot, _options = {}) {
|
|
425
|
+
const libTemplateDir = findTemplatePath(HOOK_LIB_TEMPLATE_DIR_REL);
|
|
426
|
+
let libFiles;
|
|
427
|
+
try {
|
|
428
|
+
libFiles = readdirSync(libTemplateDir).filter((name) => name.endsWith(".cjs"));
|
|
429
|
+
} catch (error) {
|
|
430
|
+
return [
|
|
431
|
+
{
|
|
432
|
+
step: "hook-lib",
|
|
433
|
+
path: libTemplateDir,
|
|
434
|
+
status: "error",
|
|
435
|
+
message: error instanceof Error ? error.message : String(error)
|
|
436
|
+
}
|
|
437
|
+
];
|
|
438
|
+
}
|
|
439
|
+
if (libFiles.length === 0) {
|
|
440
|
+
return [
|
|
441
|
+
{
|
|
442
|
+
step: "hook-lib",
|
|
443
|
+
path: libTemplateDir,
|
|
444
|
+
status: "skipped",
|
|
445
|
+
message: "no-libs-to-ship"
|
|
446
|
+
}
|
|
447
|
+
];
|
|
448
|
+
}
|
|
449
|
+
const results = [];
|
|
450
|
+
for (const libFile of libFiles) {
|
|
451
|
+
const sourcePath = join2(libTemplateDir, libFile);
|
|
452
|
+
let source;
|
|
453
|
+
try {
|
|
454
|
+
source = readFileSync2(sourcePath, "utf8");
|
|
455
|
+
} catch (error) {
|
|
456
|
+
results.push({
|
|
457
|
+
step: "hook-lib",
|
|
458
|
+
path: sourcePath,
|
|
459
|
+
status: "error",
|
|
460
|
+
message: error instanceof Error ? error.message : String(error)
|
|
461
|
+
});
|
|
462
|
+
continue;
|
|
463
|
+
}
|
|
464
|
+
for (const destDirRel of HOOK_LIB_DESTINATIONS) {
|
|
465
|
+
const target = join2(projectRoot, destDirRel, libFile);
|
|
466
|
+
results.push(await copyTextIdempotent("hook-lib", source, target));
|
|
467
|
+
}
|
|
468
|
+
}
|
|
469
|
+
return results;
|
|
470
|
+
}
|
|
471
|
+
async function mergeClaudeCodeHookConfig(projectRoot, _options = {}) {
|
|
472
|
+
const fragment = await readJsonTemplate(CLAUDE_HOOK_CONFIG_TEMPLATE_REL);
|
|
473
|
+
const targetPath = join2(projectRoot, HOOK_CONFIG_TARGETS.claudeCode);
|
|
474
|
+
return mergeJsonIdempotent(
|
|
475
|
+
"claude-hook-config",
|
|
476
|
+
targetPath,
|
|
477
|
+
fragment,
|
|
478
|
+
[...HOOK_CONFIG_ARRAY_PATHS.claudeCode]
|
|
479
|
+
);
|
|
480
|
+
}
|
|
481
|
+
async function mergeCodexHookConfig(projectRoot, _options = {}) {
|
|
482
|
+
const fragment = await readJsonTemplate(CODEX_HOOK_CONFIG_TEMPLATE_REL);
|
|
483
|
+
const targetPath = join2(projectRoot, HOOK_CONFIG_TARGETS.codex);
|
|
484
|
+
return mergeJsonIdempotent(
|
|
485
|
+
"codex-hook-config",
|
|
486
|
+
targetPath,
|
|
487
|
+
fragment,
|
|
488
|
+
[...HOOK_CONFIG_ARRAY_PATHS.codex]
|
|
489
|
+
);
|
|
490
|
+
}
|
|
491
|
+
async function mergeCursorHookConfig(projectRoot, _options = {}) {
|
|
492
|
+
const fragment = await readJsonTemplate(CURSOR_HOOK_CONFIG_TEMPLATE_REL);
|
|
493
|
+
const targetPath = join2(projectRoot, HOOK_CONFIG_TARGETS.cursor);
|
|
494
|
+
return mergeJsonIdempotent(
|
|
495
|
+
"cursor-hook-config",
|
|
496
|
+
targetPath,
|
|
497
|
+
fragment,
|
|
498
|
+
[...HOOK_CONFIG_ARRAY_PATHS.cursor]
|
|
499
|
+
);
|
|
500
|
+
}
|
|
501
|
+
function buildManagedBlockBody(targetRoot) {
|
|
502
|
+
const snapshotPath = fabricAgentsSnapshotPath(targetRoot);
|
|
503
|
+
const snapshot = readFileSync2(snapshotPath, "utf8");
|
|
504
|
+
const projectRules = readProjectRulesIfPresent(targetRoot);
|
|
505
|
+
if (projectRules === null) {
|
|
506
|
+
return snapshot;
|
|
507
|
+
}
|
|
508
|
+
return `${snapshot}
|
|
509
|
+
---
|
|
510
|
+
${projectRules}`;
|
|
511
|
+
}
|
|
512
|
+
function wrapInBootstrapMarkers(body) {
|
|
513
|
+
return `${BOOTSTRAP_MARKER_BEGIN}
|
|
514
|
+
${body}
|
|
515
|
+
${BOOTSTRAP_MARKER_END}`;
|
|
516
|
+
}
|
|
517
|
+
function stripLegacyKnowledgeBaseSection(existing) {
|
|
518
|
+
const match = existing.match(LEGACY_KB_REGEX);
|
|
519
|
+
if (match === null) return existing;
|
|
520
|
+
const before = existing.slice(0, match.index ?? 0);
|
|
521
|
+
const after = existing.slice((match.index ?? 0) + match[0].length);
|
|
522
|
+
return `${before}${after.replace(/^\r?\n/, "")}`;
|
|
523
|
+
}
|
|
524
|
+
var CLAUDE_BOOTSTRAP_HEADER = "# Project Knowledge";
|
|
525
|
+
var CLAUDE_AGENTS_IMPORT_LINE = "@.fabric/AGENTS.md";
|
|
526
|
+
var CLAUDE_PROJECT_RULES_IMPORT_LINE = "@.fabric/project-rules.md";
|
|
527
|
+
async function writeClaudeBootstrapThinShell(targetRoot, _options = {}) {
|
|
528
|
+
const step = "bootstrap-claude";
|
|
529
|
+
const target = join2(targetRoot, "CLAUDE.md");
|
|
530
|
+
const projectRulesPresent = existsSync2(projectRulesPath(targetRoot));
|
|
531
|
+
let existing = "";
|
|
532
|
+
let preExisted = false;
|
|
533
|
+
if (existsSync2(target)) {
|
|
534
|
+
preExisted = true;
|
|
535
|
+
try {
|
|
536
|
+
existing = await readFile(target, "utf8");
|
|
537
|
+
} catch (error) {
|
|
538
|
+
return {
|
|
539
|
+
step,
|
|
540
|
+
path: target,
|
|
541
|
+
status: "error",
|
|
542
|
+
message: error instanceof Error ? error.message : String(error)
|
|
543
|
+
};
|
|
544
|
+
}
|
|
545
|
+
}
|
|
546
|
+
let next = stripLegacyKnowledgeBaseSection(existing);
|
|
547
|
+
if (!projectRulesPresent) {
|
|
548
|
+
next = removeImportLine(next, CLAUDE_PROJECT_RULES_IMPORT_LINE);
|
|
549
|
+
}
|
|
550
|
+
if (!preExisted && next.length === 0) {
|
|
551
|
+
next = `${CLAUDE_BOOTSTRAP_HEADER}
|
|
552
|
+
`;
|
|
553
|
+
}
|
|
554
|
+
next = ensureImportLine(next, CLAUDE_AGENTS_IMPORT_LINE);
|
|
555
|
+
if (projectRulesPresent) {
|
|
556
|
+
next = ensureImportLine(next, CLAUDE_PROJECT_RULES_IMPORT_LINE);
|
|
557
|
+
}
|
|
558
|
+
if (next === existing) {
|
|
559
|
+
return { step, path: target, status: "skipped", message: "up-to-date" };
|
|
560
|
+
}
|
|
561
|
+
try {
|
|
562
|
+
await mkdir2(dirname2(target), { recursive: true });
|
|
563
|
+
await atomicWriteText2(target, next);
|
|
564
|
+
return { step, path: target, status: "written" };
|
|
565
|
+
} catch (error) {
|
|
566
|
+
return {
|
|
567
|
+
step,
|
|
568
|
+
path: target,
|
|
569
|
+
status: "error",
|
|
570
|
+
message: error instanceof Error ? error.message : String(error)
|
|
571
|
+
};
|
|
572
|
+
}
|
|
573
|
+
}
|
|
574
|
+
function ensureImportLine(content, line) {
|
|
575
|
+
if (hasExactLine(content, line)) return content;
|
|
576
|
+
if (content.length === 0) return `${line}
|
|
577
|
+
`;
|
|
578
|
+
const endsWithBlank = content.endsWith("\n\n");
|
|
579
|
+
const endsWithNewline = content.endsWith("\n");
|
|
580
|
+
if (endsWithBlank) {
|
|
581
|
+
return `${content}${line}
|
|
582
|
+
`;
|
|
583
|
+
}
|
|
584
|
+
if (endsWithNewline) {
|
|
585
|
+
return `${content}
|
|
586
|
+
${line}
|
|
587
|
+
`;
|
|
588
|
+
}
|
|
589
|
+
return `${content}
|
|
590
|
+
|
|
591
|
+
${line}
|
|
592
|
+
`;
|
|
593
|
+
}
|
|
594
|
+
function removeImportLine(content, line) {
|
|
595
|
+
const lines = content.split(/\r?\n/);
|
|
596
|
+
const filtered = lines.filter((l) => l.replace(/\s+$/, "") !== line);
|
|
597
|
+
return filtered.join("\n");
|
|
598
|
+
}
|
|
599
|
+
function hasExactLine(content, line) {
|
|
600
|
+
const lines = content.split(/\r?\n/);
|
|
601
|
+
return lines.some((l) => l.replace(/\s+$/, "") === line);
|
|
602
|
+
}
|
|
603
|
+
async function writeCodexBootstrapManagedBlock(targetRoot, _options = {}) {
|
|
604
|
+
const step = "bootstrap-codex";
|
|
605
|
+
const target = join2(targetRoot, "AGENTS.md");
|
|
606
|
+
let existing = "";
|
|
607
|
+
if (existsSync2(target)) {
|
|
608
|
+
try {
|
|
609
|
+
existing = await readFile(target, "utf8");
|
|
610
|
+
} catch (error) {
|
|
611
|
+
return {
|
|
612
|
+
step,
|
|
613
|
+
path: target,
|
|
614
|
+
status: "error",
|
|
615
|
+
message: error instanceof Error ? error.message : String(error)
|
|
616
|
+
};
|
|
617
|
+
}
|
|
618
|
+
}
|
|
619
|
+
const body = buildManagedBlockBody(targetRoot);
|
|
620
|
+
const managedBlock = wrapInBootstrapMarkers(body);
|
|
621
|
+
const stripped = stripLegacyKnowledgeBaseSection(existing);
|
|
622
|
+
let next;
|
|
623
|
+
const match = stripped.match(BOOTSTRAP_REGEX);
|
|
624
|
+
if (match !== null) {
|
|
625
|
+
const before = stripped.slice(0, match.index ?? 0);
|
|
626
|
+
const after = stripped.slice((match.index ?? 0) + match[0].length);
|
|
627
|
+
const cleaned = `${before}${after.replace(/^\r?\n/, "")}`;
|
|
628
|
+
const trailingNewline = cleaned.length === 0 || cleaned.endsWith("\n") ? "" : "\n";
|
|
629
|
+
next = `${cleaned}${trailingNewline}${cleaned.length === 0 ? "" : "\n"}${managedBlock}
|
|
630
|
+
`;
|
|
631
|
+
} else {
|
|
632
|
+
if (stripped.length === 0) {
|
|
633
|
+
next = `${managedBlock}
|
|
634
|
+
`;
|
|
635
|
+
} else {
|
|
636
|
+
const trailingNewline = stripped.endsWith("\n") ? "" : "\n";
|
|
637
|
+
next = `${stripped}${trailingNewline}
|
|
638
|
+
${managedBlock}
|
|
639
|
+
`;
|
|
640
|
+
}
|
|
641
|
+
}
|
|
642
|
+
if (next === existing) {
|
|
643
|
+
return { step, path: target, status: "skipped", message: "up-to-date" };
|
|
644
|
+
}
|
|
645
|
+
try {
|
|
646
|
+
await mkdir2(dirname2(target), { recursive: true });
|
|
647
|
+
await atomicWriteText2(target, next);
|
|
648
|
+
return { step, path: target, status: "written" };
|
|
649
|
+
} catch (error) {
|
|
650
|
+
return {
|
|
651
|
+
step,
|
|
652
|
+
path: target,
|
|
653
|
+
status: "error",
|
|
654
|
+
message: error instanceof Error ? error.message : String(error)
|
|
655
|
+
};
|
|
656
|
+
}
|
|
657
|
+
}
|
|
658
|
+
var CURSOR_RULE_FRONT_MATTER = "---\nalwaysApply: true\ndescription: Fabric Protocol bootstrap rules\n---\n\n";
|
|
659
|
+
var CURSOR_LEGACY_FLAT_FILE_REL = join2(".cursor", "rules");
|
|
660
|
+
var CURSOR_BOOTSTRAP_MDC_REL = join2(".cursor", "rules", "fabric-bootstrap.mdc");
|
|
661
|
+
async function writeCursorBootstrapManagedBlock(targetRoot, _options = {}) {
|
|
662
|
+
const step = "bootstrap-cursor";
|
|
663
|
+
const target = join2(targetRoot, CURSOR_BOOTSTRAP_MDC_REL);
|
|
664
|
+
const legacyFlatFile = join2(targetRoot, CURSOR_LEGACY_FLAT_FILE_REL);
|
|
665
|
+
try {
|
|
666
|
+
if (existsSync2(legacyFlatFile)) {
|
|
667
|
+
const stat = statSync(legacyFlatFile);
|
|
668
|
+
if (stat.isFile()) {
|
|
669
|
+
await rm(legacyFlatFile, { force: true });
|
|
670
|
+
}
|
|
671
|
+
}
|
|
672
|
+
} catch {
|
|
673
|
+
}
|
|
674
|
+
const body = buildManagedBlockBody(targetRoot);
|
|
675
|
+
const managedBlock = wrapInBootstrapMarkers(body);
|
|
676
|
+
const expected = `${CURSOR_RULE_FRONT_MATTER}${managedBlock}
|
|
677
|
+
`;
|
|
678
|
+
let existing = "";
|
|
679
|
+
if (existsSync2(target)) {
|
|
680
|
+
try {
|
|
681
|
+
existing = await readFile(target, "utf8");
|
|
682
|
+
} catch (error) {
|
|
683
|
+
return {
|
|
684
|
+
step,
|
|
685
|
+
path: target,
|
|
686
|
+
status: "error",
|
|
687
|
+
message: error instanceof Error ? error.message : String(error)
|
|
688
|
+
};
|
|
689
|
+
}
|
|
690
|
+
}
|
|
691
|
+
if (existing === expected) {
|
|
692
|
+
return { step, path: target, status: "skipped", message: "up-to-date" };
|
|
693
|
+
}
|
|
694
|
+
try {
|
|
695
|
+
await mkdir2(dirname2(target), { recursive: true });
|
|
696
|
+
await atomicWriteText2(target, expected);
|
|
697
|
+
return { step, path: target, status: "written" };
|
|
698
|
+
} catch (error) {
|
|
699
|
+
return {
|
|
700
|
+
step,
|
|
701
|
+
path: target,
|
|
702
|
+
status: "error",
|
|
703
|
+
message: error instanceof Error ? error.message : String(error)
|
|
704
|
+
};
|
|
705
|
+
}
|
|
706
|
+
}
|
|
707
|
+
async function copyTextIdempotent(step, source, target) {
|
|
708
|
+
if (existsSync2(target)) {
|
|
709
|
+
try {
|
|
710
|
+
const existing = readFileSync2(target, "utf8");
|
|
711
|
+
if (existing === source) {
|
|
712
|
+
return { step, path: target, status: "skipped", message: "up-to-date" };
|
|
713
|
+
}
|
|
714
|
+
} catch {
|
|
715
|
+
}
|
|
716
|
+
}
|
|
717
|
+
await mkdir2(dirname2(target), { recursive: true });
|
|
718
|
+
await atomicWriteText2(target, source);
|
|
719
|
+
return { step, path: target, status: "written" };
|
|
720
|
+
}
|
|
721
|
+
var FABRIC_HOOK_SCRIPT_BASENAMES = /* @__PURE__ */ new Set([
|
|
722
|
+
"fabric-hint.cjs",
|
|
723
|
+
"knowledge-hint-broad.cjs",
|
|
724
|
+
"knowledge-hint-narrow.cjs",
|
|
725
|
+
// rc.5 TASK-010 rename — old hook scripts that pre-upgrade workspaces
|
|
726
|
+
// may still have registered. Sweeping them prevents the double-fire
|
|
727
|
+
// documented in audit §2.6.
|
|
728
|
+
"archive-hint.cjs"
|
|
729
|
+
]);
|
|
730
|
+
function commandBasename(command) {
|
|
731
|
+
const trimmed = command.trim().replace(/^"+|"+$/g, "");
|
|
732
|
+
const match = /([^/\\]+\.cjs)$/u.exec(trimmed);
|
|
733
|
+
return match === null ? null : match[1];
|
|
734
|
+
}
|
|
735
|
+
function stripStaleHookEntries(existing, arrayAppendPaths) {
|
|
736
|
+
const swept = JSON.parse(JSON.stringify(existing));
|
|
737
|
+
let removed = 0;
|
|
738
|
+
for (const dottedPath of arrayAppendPaths) {
|
|
739
|
+
const segments = dottedPath.split(".");
|
|
740
|
+
let cursor = swept;
|
|
741
|
+
for (let i = 0; i < segments.length - 1; i++) {
|
|
742
|
+
const seg = segments[i];
|
|
743
|
+
if (cursor === null || typeof cursor !== "object" || Array.isArray(cursor)) {
|
|
744
|
+
cursor = void 0;
|
|
745
|
+
break;
|
|
746
|
+
}
|
|
747
|
+
cursor = cursor[seg];
|
|
748
|
+
}
|
|
749
|
+
if (cursor === null || cursor === void 0 || typeof cursor !== "object" || Array.isArray(cursor)) {
|
|
750
|
+
continue;
|
|
751
|
+
}
|
|
752
|
+
const finalSeg = segments[segments.length - 1];
|
|
753
|
+
const arr = cursor[finalSeg];
|
|
754
|
+
if (!Array.isArray(arr)) continue;
|
|
755
|
+
const filtered = [];
|
|
756
|
+
for (const item of arr) {
|
|
757
|
+
if (item === null || typeof item !== "object") {
|
|
758
|
+
filtered.push(item);
|
|
759
|
+
continue;
|
|
760
|
+
}
|
|
761
|
+
const entry = item;
|
|
762
|
+
const hooks = entry.hooks;
|
|
763
|
+
let isFabricOwned = false;
|
|
764
|
+
if (Array.isArray(hooks)) {
|
|
765
|
+
for (const h of hooks) {
|
|
766
|
+
if (h !== null && typeof h === "object") {
|
|
767
|
+
const cmd = h.command;
|
|
768
|
+
if (typeof cmd === "string") {
|
|
769
|
+
const base = commandBasename(cmd);
|
|
770
|
+
if (base !== null && FABRIC_HOOK_SCRIPT_BASENAMES.has(base)) {
|
|
771
|
+
isFabricOwned = true;
|
|
772
|
+
break;
|
|
773
|
+
}
|
|
774
|
+
}
|
|
775
|
+
}
|
|
776
|
+
}
|
|
777
|
+
}
|
|
778
|
+
if (!isFabricOwned && typeof entry.command === "string") {
|
|
779
|
+
const base = commandBasename(entry.command);
|
|
780
|
+
if (base !== null && FABRIC_HOOK_SCRIPT_BASENAMES.has(base)) {
|
|
781
|
+
isFabricOwned = true;
|
|
782
|
+
}
|
|
783
|
+
}
|
|
784
|
+
if (isFabricOwned) {
|
|
785
|
+
removed += 1;
|
|
786
|
+
} else {
|
|
787
|
+
filtered.push(item);
|
|
788
|
+
}
|
|
789
|
+
}
|
|
790
|
+
cursor[finalSeg] = filtered;
|
|
791
|
+
}
|
|
792
|
+
return { swept, removed };
|
|
793
|
+
}
|
|
794
|
+
async function mergeJsonIdempotent(step, target, fragment, arrayAppendPaths) {
|
|
795
|
+
const existing = await readJsonObjectOrEmpty(target);
|
|
796
|
+
const { swept } = stripStaleHookEntries(existing, arrayAppendPaths);
|
|
797
|
+
const merged = deepMerge(swept, fragment, { arrayAppendPaths });
|
|
798
|
+
if (jsonEqual(existing, merged)) {
|
|
799
|
+
return { step, path: target, status: "skipped", message: "up-to-date" };
|
|
800
|
+
}
|
|
801
|
+
await mkdir2(dirname2(target), { recursive: true });
|
|
802
|
+
await atomicWriteJson(target, merged, { indent: 2 });
|
|
803
|
+
return { step, path: target, status: "written" };
|
|
804
|
+
}
|
|
805
|
+
async function readJsonObjectOrEmpty(path) {
|
|
806
|
+
try {
|
|
807
|
+
const raw = await readFile(path, "utf8");
|
|
808
|
+
if (raw.trim().length === 0) {
|
|
809
|
+
return {};
|
|
810
|
+
}
|
|
811
|
+
const parsed = JSON.parse(raw);
|
|
812
|
+
if (parsed === null || typeof parsed !== "object" || Array.isArray(parsed)) {
|
|
813
|
+
return {};
|
|
814
|
+
}
|
|
815
|
+
return parsed;
|
|
816
|
+
} catch (error) {
|
|
817
|
+
if (error instanceof Error && "code" in error && error.code === "ENOENT") {
|
|
818
|
+
return {};
|
|
819
|
+
}
|
|
820
|
+
throw error;
|
|
821
|
+
}
|
|
822
|
+
}
|
|
823
|
+
function jsonEqual(a, b) {
|
|
824
|
+
return JSON.stringify(a) === JSON.stringify(b);
|
|
825
|
+
}
|
|
826
|
+
async function readTemplate(relativePath) {
|
|
827
|
+
const path = findTemplatePath(relativePath);
|
|
828
|
+
return readFile(path, "utf8");
|
|
829
|
+
}
|
|
830
|
+
async function readJsonTemplate(relativePath) {
|
|
831
|
+
const raw = await readTemplate(relativePath);
|
|
832
|
+
const parsed = JSON.parse(raw);
|
|
833
|
+
if (parsed === null || typeof parsed !== "object" || Array.isArray(parsed)) {
|
|
834
|
+
throw new Error(`Template at ${relativePath} is not a JSON object`);
|
|
835
|
+
}
|
|
836
|
+
return parsed;
|
|
837
|
+
}
|
|
838
|
+
function findTemplatePath(relativePath) {
|
|
839
|
+
const startDir = dirname2(fileURLToPath(import.meta.url));
|
|
840
|
+
let current = resolve(startDir);
|
|
841
|
+
while (true) {
|
|
842
|
+
const candidate = join2(current, "templates", relativePath);
|
|
843
|
+
if (existsSync2(candidate)) {
|
|
844
|
+
return candidate;
|
|
845
|
+
}
|
|
846
|
+
const parent = dirname2(current);
|
|
847
|
+
if (parent === current || parse(current).root === current) {
|
|
848
|
+
throw new Error(`Template not found: templates/${relativePath} (searched up from ${startDir})`);
|
|
849
|
+
}
|
|
850
|
+
current = parent;
|
|
851
|
+
}
|
|
852
|
+
}
|
|
853
|
+
|
|
854
|
+
export {
|
|
855
|
+
fabricAgentsSnapshotPath,
|
|
856
|
+
writeFabricAgentsSnapshot,
|
|
857
|
+
SKILL_DESTINATIONS,
|
|
858
|
+
HOOK_SCRIPT_DESTINATIONS,
|
|
859
|
+
HOOK_LIB_DESTINATIONS,
|
|
860
|
+
HOOK_CONFIG_TARGETS,
|
|
861
|
+
HOOK_CONFIG_ARRAY_PATHS,
|
|
862
|
+
FABRIC_HOOK_COMMAND_PATHS,
|
|
863
|
+
readFabricLanguagePreference,
|
|
864
|
+
installFabricArchiveSkill,
|
|
865
|
+
installFabricReviewSkill,
|
|
866
|
+
installFabricImportSkill,
|
|
867
|
+
cleanupDeprecatedSkills,
|
|
868
|
+
installSharedSkillLib,
|
|
869
|
+
installArchiveHintHook,
|
|
870
|
+
installKnowledgeHintBroadHook,
|
|
871
|
+
installKnowledgeHintNarrowHook,
|
|
872
|
+
installCitePolicyEvictHook,
|
|
873
|
+
installHookLibs,
|
|
874
|
+
mergeClaudeCodeHookConfig,
|
|
875
|
+
mergeCodexHookConfig,
|
|
876
|
+
mergeCursorHookConfig,
|
|
877
|
+
writeClaudeBootstrapThinShell,
|
|
878
|
+
writeCodexBootstrapManagedBlock,
|
|
879
|
+
writeCursorBootstrapManagedBlock
|
|
880
|
+
};
|