@fenglimg/fabric-cli 1.2.0 → 1.3.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (34) hide show
  1. package/README.md +24 -22
  2. package/dist/{bootstrap-IUL4SAAK.js → bootstrap-3PUKUYTY.js} +4 -2
  3. package/dist/{chunk-N4DCTOXW.js → chunk-AZRKMFRY.js} +6 -6
  4. package/dist/{chunk-F2BXHPM5.js → chunk-N7EZORJZ.js} +9 -1
  5. package/dist/{chunk-6UUPKSDE.js → chunk-Q4LOVXML.js} +13 -8
  6. package/dist/{chunk-RUQCZA2Q.js → chunk-TKTWHAKV.js} +92 -115
  7. package/dist/{chunk-VMYPJPKV.js → chunk-VOQKQ6W2.js} +153 -21
  8. package/dist/{chunk-MDI7523D.js → chunk-XFSQM3LJ.js} +5 -1
  9. package/dist/{config-3JBB77TX.js → config-GINBGANU.js} +3 -2
  10. package/dist/index.js +11 -22
  11. package/dist/{init-3FPLOABB.js → init-T3LGMGAO.js} +170 -54
  12. package/dist/{ledger-append-XZ5SX4O5.js → ledger-append-DULKJ6Q2.js} +1 -1
  13. package/dist/pre-commit-IK6SJOPT.js +97 -0
  14. package/dist/{scan-WKDSKEBB.js → scan-43R3IBLR.js} +2 -2
  15. package/dist/{sync-meta-THZSEM7Y.js → sync-meta-LKVSO6TS.js} +1 -1
  16. package/dist/update-AN3FYF2O.js +116 -0
  17. package/package.json +4 -3
  18. package/templates/agents-md/AGENTS.md.template +14 -13
  19. package/templates/agents-md/variants/cocos.md +20 -20
  20. package/templates/agents-md/variants/next.md +20 -20
  21. package/templates/agents-md/variants/vite.md +20 -20
  22. package/templates/bootstrap/CLAUDE.md +3 -5
  23. package/templates/bootstrap/GEMINI.md +3 -5
  24. package/templates/bootstrap/codex-AGENTS-header.md +3 -5
  25. package/templates/bootstrap/cursor-fabric-bootstrap.mdc +11 -11
  26. package/templates/bootstrap/roo-fabric.md +6 -6
  27. package/templates/bootstrap/windsurf-fabric.md +6 -6
  28. package/templates/claude-hooks/agents-md-init-reminder.cjs +18 -18
  29. package/templates/claude-skills/agents-md-init/SKILL.md +86 -86
  30. package/templates/fabric/human-lock.json +12 -12
  31. package/templates/husky/pre-commit +24 -24
  32. package/dist/pre-commit-CJ7EDKJK.js +0 -59
  33. package/dist/{doctor-5KJGOV2P.js → doctor-QTSG2RWF.js} +3 -3
  34. package/dist/{serve-MMN4GYLM.js → serve-4J2CQY25.js} +4 -4
package/README.md CHANGED
@@ -1,24 +1,26 @@
1
- # @fenglimg/fabric-cli
2
-
3
- `fabric` is the primary CLI binary for Fabric. `fab` is a permanent alias, so you can use either binary.
4
-
5
- ## Quick Start
6
-
7
- 1. Install dependencies from the monorepo root with `pnpm install`.
8
- 2. Build the CLI with `pnpm --filter @fenglimg/fabric-cli build`.
9
- 3. Run `fabric init` in the target project for the one-shot setup flow.
10
- 4. Start `fabric serve` and verify `fab_get_rules` in your client.
11
-
1
+ # @fenglimg/fabric-cli
2
+
3
+ `fabric` is the primary CLI binary for Fabric. `fab` is a permanent alias, so you can use either binary.
4
+
5
+ ## Quick Start
6
+
7
+ 1. Install dependencies from the monorepo root with `pnpm install`.
8
+ 2. Build the CLI with `pnpm --filter @fenglimg/fabric-cli build`.
9
+ 3. Run `fabric init` in the target project for the one-shot setup flow.
10
+ 4. Start `fabric serve` and verify `fab_get_rules` in your client.
11
+
12
12
  `fabric init` auto-runs `bootstrap install`, `config install`, and `hooks install`. Use them standalone only for targeted re-runs.
13
13
 
14
- ## Common Commands
15
-
16
- - `fabric init`
17
- - `fabric serve`
18
- - `fabric doctor --audit`
19
-
20
- ## Advanced Commands
21
-
22
- - `fabric bootstrap install`
23
- - `fabric config install`
24
- - `fabric hooks install`
14
+ `fabric bootstrap install` refreshes the internal bootstrap guide at `.fabric/bootstrap/README.md`. It does not generate root `AGENTS.md`, `CLAUDE.md`, or `GEMINI.md`.
15
+
16
+ ## Common Commands
17
+
18
+ - `fabric init`
19
+ - `fabric serve`
20
+ - `fabric doctor --audit`
21
+
22
+ ## Advanced Commands
23
+
24
+ - `fabric bootstrap install`
25
+ - `fabric config install`
26
+ - `fabric hooks install`
@@ -3,9 +3,11 @@ import {
3
3
  bootstrapCommand,
4
4
  bootstrap_default,
5
5
  installBootstrap
6
- } from "./chunk-RUQCZA2Q.js";
7
- import "./chunk-VMYPJPKV.js";
6
+ } from "./chunk-TKTWHAKV.js";
7
+ import "./chunk-AZRKMFRY.js";
8
+ import "./chunk-VOQKQ6W2.js";
8
9
  import "./chunk-AEOYCVBG.js";
10
+ import "./chunk-WWNXR34K.js";
9
11
  import "./chunk-6ICJICVU.js";
10
12
  export {
11
13
  bootstrapCommand,
@@ -1,15 +1,15 @@
1
1
  #!/usr/bin/env node
2
+ import {
3
+ createDebugLogger,
4
+ readFabricConfig,
5
+ resolveDevMode
6
+ } from "./chunk-AEOYCVBG.js";
2
7
  import {
3
8
  displayWidth,
4
9
  padEnd,
5
10
  paint,
6
11
  symbol
7
12
  } from "./chunk-WWNXR34K.js";
8
- import {
9
- createDebugLogger,
10
- readFabricConfig,
11
- resolveDevMode
12
- } from "./chunk-AEOYCVBG.js";
13
13
  import {
14
14
  t
15
15
  } from "./chunk-6ICJICVU.js";
@@ -45,7 +45,7 @@ function createScanReport(targetInput = process.cwd(), fabricConfig) {
45
45
  const framework = detectFramework(target);
46
46
  const readmeQuality = getReadmeQuality(target);
47
47
  const hasContributing = existsSync(join(target, "CONTRIBUTING.md"));
48
- const hasExistingFabric = existsSync(join(target, "AGENTS.md")) || existsSync(join(target, ".fabric"));
48
+ const hasExistingFabric = existsSync(join(target, ".fabric", "bootstrap", "README.md")) || existsSync(join(target, ".fabric"));
49
49
  const walkResult = walkFiles(target, resolveIgnores(fabricConfig));
50
50
  return {
51
51
  target,
@@ -95,7 +95,7 @@ function hasMatchingTailEntry(target, entry) {
95
95
  if (!existsSync(ledgerPath)) {
96
96
  return false;
97
97
  }
98
- const tail = readFileSync(ledgerPath, "utf8").trim().split(/\r?\n/).filter(Boolean).slice(-1)[0];
98
+ const tail = readFileSync(ledgerPath, "utf8").trim().split(/\r?\n/).filter(Boolean).reverse().find((line) => isLedgerEntryLine(line));
99
99
  if (!tail) {
100
100
  return false;
101
101
  }
@@ -106,6 +106,14 @@ function hasMatchingTailEntry(target, entry) {
106
106
  return false;
107
107
  }
108
108
  }
109
+ function isLedgerEntryLine(line) {
110
+ try {
111
+ const parsed = JSON.parse(line);
112
+ return parsed.kind !== "mcp-event";
113
+ } catch {
114
+ return false;
115
+ }
116
+ }
109
117
  function normalizeDiffStat(diffStat) {
110
118
  if (typeof diffStat !== "string") {
111
119
  return "";
@@ -59,7 +59,10 @@ function computeAgentsMeta(target) {
59
59
  const existingByFile = indexExistingNodesByFile(existingMeta);
60
60
  const agentsFiles = findFabricAgentsFiles(target);
61
61
  const nodes = {};
62
- const bootstrapNode = createBootstrapNode(target, existingByFile.get("AGENTS.md")?.node);
62
+ const bootstrapNode = createBootstrapNode(
63
+ target,
64
+ existingByFile.get(".fabric/bootstrap/README.md")?.node ?? existingByFile.get("AGENTS.md")?.node
65
+ );
63
66
  if (bootstrapNode !== void 0) {
64
67
  nodes.L0 = bootstrapNode;
65
68
  }
@@ -137,7 +140,7 @@ function indexExistingNodesByFile(existingMeta) {
137
140
  return byFile;
138
141
  }
139
142
  function deriveNodeId(file) {
140
- if (file === "AGENTS.md") {
143
+ if (file === ".fabric/bootstrap/README.md") {
141
144
  return "L0";
142
145
  }
143
146
  const layer = deriveLayer(file);
@@ -158,20 +161,22 @@ function createDefaultNodeMeta(file) {
158
161
  };
159
162
  }
160
163
  function createBootstrapNode(target, existing) {
161
- const bootstrapPath = join(target, "AGENTS.md");
162
- if (!existsSync(bootstrapPath)) {
164
+ const bootstrapPath = join(target, ".fabric", "bootstrap", "README.md");
165
+ const legacyBootstrapPath = join(target, "AGENTS.md");
166
+ const sourcePath = existsSync(bootstrapPath) ? bootstrapPath : existsSync(legacyBootstrapPath) ? legacyBootstrapPath : void 0;
167
+ if (sourcePath === void 0) {
163
168
  return void 0;
164
169
  }
165
- const hash = sha256(readFileSync(bootstrapPath, "utf8"));
170
+ const hash = sha256(readFileSync(sourcePath, "utf8"));
166
171
  return {
167
- ...createDefaultNodeMeta("AGENTS.md"),
172
+ ...createDefaultNodeMeta(".fabric/bootstrap/README.md"),
168
173
  ...existing,
169
- file: "AGENTS.md",
174
+ file: ".fabric/bootstrap/README.md",
170
175
  hash
171
176
  };
172
177
  }
173
178
  function deriveScopeGlob(file) {
174
- if (file === "AGENTS.md") {
179
+ if (file === ".fabric/bootstrap/README.md") {
175
180
  return "**";
176
181
  }
177
182
  const stem = getMirrorRelativeStem(file);
@@ -1,7 +1,10 @@
1
1
  #!/usr/bin/env node
2
+ import {
3
+ createScanReport
4
+ } from "./chunk-AZRKMFRY.js";
2
5
  import {
3
6
  resolveClients
4
- } from "./chunk-VMYPJPKV.js";
7
+ } from "./chunk-VOQKQ6W2.js";
5
8
  import {
6
9
  readFabricConfig
7
10
  } from "./chunk-AEOYCVBG.js";
@@ -10,10 +13,87 @@ import {
10
13
  } from "./chunk-6ICJICVU.js";
11
14
 
12
15
  // src/commands/bootstrap.ts
16
+ import { resolve as resolve2 } from "path";
17
+ import { defineCommand } from "citty";
18
+
19
+ // src/bootstrap-guide.ts
13
20
  import { existsSync, mkdirSync, readFileSync, writeFileSync } from "fs";
14
- import { dirname, join, parse, resolve } from "path";
21
+ import { dirname, isAbsolute, join, parse, resolve } from "path";
15
22
  import { fileURLToPath } from "url";
16
- import { defineCommand } from "citty";
23
+ var AGENTS_TEMPLATE_BY_FRAMEWORK = {
24
+ "cocos-creator": "templates/agents-md/variants/cocos.md",
25
+ vite: "templates/agents-md/variants/vite.md",
26
+ next: "templates/agents-md/variants/next.md"
27
+ };
28
+ var FABRIC_GUIDE_PATH = ".fabric/bootstrap/README.md";
29
+ function buildFabricBootstrapGuide(target) {
30
+ const workspaceRoot = normalizeTarget(target);
31
+ const scanReport = createScanReport(workspaceRoot);
32
+ const template = readFileSync(findBootstrapTemplatePath(scanReport.framework.kind), "utf8");
33
+ const packageName = readPackageName(workspaceRoot) ?? parse(workspaceRoot).base;
34
+ return ensureTrailingNewline(
35
+ template.replaceAll("{ projectName }", packageName).replaceAll("{ frameworkKind }", scanReport.framework.kind)
36
+ );
37
+ }
38
+ function ensureFabricBootstrapGuide(workspaceRoot, force) {
39
+ const guidePath = resolve(workspaceRoot, FABRIC_GUIDE_PATH);
40
+ if (existsSync(guidePath) && !force) {
41
+ return;
42
+ }
43
+ mkdirSync(dirname(guidePath), { recursive: true });
44
+ writeFileSync(guidePath, buildFabricBootstrapGuide(workspaceRoot), "utf8");
45
+ }
46
+ function findBootstrapTemplatePath(frameworkKind) {
47
+ const relativePath = AGENTS_TEMPLATE_BY_FRAMEWORK[frameworkKind] ?? "templates/agents-md/AGENTS.md.template";
48
+ return findTemplatePath(relativePath);
49
+ }
50
+ function readPackageName(target) {
51
+ const packageJsonPath = join(target, "package.json");
52
+ if (!existsSync(packageJsonPath)) {
53
+ return void 0;
54
+ }
55
+ try {
56
+ const packageJson = JSON.parse(readFileSync(packageJsonPath, "utf8"));
57
+ return packageJson.name;
58
+ } catch {
59
+ return void 0;
60
+ }
61
+ }
62
+ function findTemplatePath(relativePath) {
63
+ const currentModuleDir = dirname(fileURLToPath(import.meta.url));
64
+ const candidates = [
65
+ ...templateCandidatesFrom(process.cwd(), relativePath),
66
+ ...templateCandidatesFrom(currentModuleDir, relativePath)
67
+ ];
68
+ for (const candidate of candidates) {
69
+ if (existsSync(candidate)) {
70
+ return candidate;
71
+ }
72
+ }
73
+ throw new Error(t("cli.shared.template-not-found", { path: relativePath }));
74
+ }
75
+ function templateCandidatesFrom(start, relativePath) {
76
+ const candidates = [];
77
+ let current = resolve(start);
78
+ while (true) {
79
+ candidates.push(join(current, ...relativePath.split("/")));
80
+ const parent = dirname(current);
81
+ if (parent === current || parse(current).root === current) {
82
+ break;
83
+ }
84
+ current = parent;
85
+ }
86
+ return candidates.reverse();
87
+ }
88
+ function ensureTrailingNewline(content) {
89
+ return content.endsWith("\n") ? content : `${content}
90
+ `;
91
+ }
92
+ function normalizeTarget(targetInput) {
93
+ return isAbsolute(targetInput) ? targetInput : resolve(process.cwd(), targetInput);
94
+ }
95
+
96
+ // src/commands/bootstrap.ts
17
97
  var CLIENT_ALIASES = {
18
98
  claude: "claude",
19
99
  "claude-code": "claude",
@@ -34,22 +114,6 @@ var CLIENT_ALIASES = {
34
114
  "codex-cli": "codex",
35
115
  codexcli: "codex"
36
116
  };
37
- var CLIENT_TEMPLATE_MAP = {
38
- claude: "templates/bootstrap/CLAUDE.md",
39
- cursor: "templates/bootstrap/cursor-fabric-bootstrap.mdc",
40
- windsurf: "templates/bootstrap/windsurf-fabric.md",
41
- roo: "templates/bootstrap/roo-fabric.md",
42
- gemini: "templates/bootstrap/GEMINI.md",
43
- codex: "templates/bootstrap/codex-AGENTS-header.md"
44
- };
45
- var CLIENT_TARGET_MAP = {
46
- claude: "CLAUDE.md",
47
- cursor: ".cursor/rules/fabric-bootstrap.mdc",
48
- windsurf: ".windsurf/rules/fabric.md",
49
- roo: ".roo/rules/fabric.md",
50
- gemini: "GEMINI.md",
51
- codex: "AGENTS.md"
52
- };
53
117
  var bootstrapCommand = defineCommand({
54
118
  meta: {
55
119
  name: "bootstrap",
@@ -100,20 +164,20 @@ var bootstrapCommand = defineCommand({
100
164
  });
101
165
  var bootstrap_default = bootstrapCommand;
102
166
  async function installBootstrap(target, options = {}) {
103
- const workspaceRoot = resolve(target);
167
+ const workspaceRoot = resolve2(target);
104
168
  const fabricConfig = readFabricConfig(workspaceRoot);
105
169
  const targets = resolveBootstrapTargets(workspaceRoot, fabricConfig, options.clients);
106
170
  const installed = [];
107
171
  const skipped = [];
108
172
  const details = [];
173
+ ensureFabricBootstrapGuide(workspaceRoot, options.force);
109
174
  for (const bootstrapTarget of targets) {
110
- const detail = installBootstrapTarget(bootstrapTarget, workspaceRoot, options);
111
- details.push(detail);
112
- if (detail.action === "skipped") {
113
- skipped.push(bootstrapTarget.client);
114
- } else {
115
- installed.push(bootstrapTarget.client);
116
- }
175
+ details.push({
176
+ client: bootstrapTarget.client,
177
+ path: resolve2(workspaceRoot, FABRIC_GUIDE_PATH),
178
+ action: "skipped"
179
+ });
180
+ skipped.push(bootstrapTarget.client);
117
181
  }
118
182
  return { installed, skipped, details };
119
183
  }
@@ -181,96 +245,9 @@ function mapBootstrapClientToClientKind(client) {
181
245
  return "CodexCLI";
182
246
  }
183
247
  }
184
- function installBootstrapTarget(target, workspaceRoot, options) {
185
- const targetPath = resolve(workspaceRoot, CLIENT_TARGET_MAP[target.bootstrapClient]);
186
- const templatePath = findTemplatePath(CLIENT_TEMPLATE_MAP[target.bootstrapClient]);
187
- const template = readFileSync(templatePath, "utf8");
188
- mkdirSync(dirname(targetPath), { recursive: true });
189
- if (target.bootstrapClient === "codex") {
190
- return {
191
- client: target.client,
192
- path: targetPath,
193
- action: writeCodexBootstrap(targetPath, template, options.force)
194
- };
195
- }
196
- const existed = existsSync(targetPath);
197
- writeFileSync(targetPath, ensureTrailingNewline(template), "utf8");
198
- return {
199
- client: target.client,
200
- path: targetPath,
201
- action: existed ? "overwritten" : "installed"
202
- };
203
- }
204
- function writeCodexBootstrap(targetPath, template, force) {
205
- const nextContent = ensureTrailingNewline(template);
206
- if (!existsSync(targetPath)) {
207
- writeFileSync(targetPath, nextContent, "utf8");
208
- return "installed";
209
- }
210
- const existing = readFileSync(targetPath, "utf8");
211
- if (existing.includes("# Fabric Bootstrap")) {
212
- if (!force) {
213
- return "skipped";
214
- }
215
- const remainder = stripExistingCodexBootstrap(existing, nextContent);
216
- writeFileSync(targetPath, joinBootstrapSections(nextContent, remainder), "utf8");
217
- return "overwritten";
218
- }
219
- writeFileSync(targetPath, joinBootstrapSections(nextContent, existing), "utf8");
220
- return force ? "overwritten" : "prepended";
221
- }
222
- function stripExistingCodexBootstrap(existing, template) {
223
- if (existing.startsWith(template)) {
224
- return existing.slice(template.length).replace(/^\n+/, "");
225
- }
226
- if (!existing.startsWith("# Fabric Bootstrap")) {
227
- return existing;
228
- }
229
- const nextTopLevelHeadingIndex = existing.indexOf("\n# ", "# Fabric Bootstrap".length);
230
- if (nextTopLevelHeadingIndex === -1) {
231
- return "";
232
- }
233
- return existing.slice(nextTopLevelHeadingIndex + 1).replace(/^\n+/, "");
234
- }
235
- function joinBootstrapSections(header, body) {
236
- if (body.trim().length === 0) {
237
- return header;
238
- }
239
- const separator = body.startsWith("\n") ? "" : "\n";
240
- return `${header}${separator}${body}`;
241
- }
242
- function ensureTrailingNewline(content) {
243
- return content.endsWith("\n") ? content : `${content}
244
- `;
245
- }
246
- function findTemplatePath(relativePath) {
247
- const currentModuleDir = dirname(fileURLToPath(import.meta.url));
248
- const candidates = [
249
- ...templateCandidatesFrom(process.cwd(), relativePath),
250
- ...templateCandidatesFrom(currentModuleDir, relativePath)
251
- ];
252
- for (const candidate of candidates) {
253
- if (existsSync(candidate)) {
254
- return candidate;
255
- }
256
- }
257
- throw new Error(t("cli.shared.template-not-found", { path: relativePath }));
258
- }
259
- function templateCandidatesFrom(start, relativePath) {
260
- const candidates = [];
261
- let current = resolve(start);
262
- while (true) {
263
- candidates.push(join(current, ...relativePath.split("/")));
264
- const parent = dirname(current);
265
- if (parent === current || parse(current).root === current) {
266
- break;
267
- }
268
- current = parent;
269
- }
270
- return candidates.reverse();
271
- }
272
248
 
273
249
  export {
250
+ buildFabricBootstrapGuide,
274
251
  bootstrapCommand,
275
252
  bootstrap_default,
276
253
  installBootstrap
@@ -179,7 +179,6 @@ import { existsSync as existsSync3 } from "fs";
179
179
  import { mkdir as mkdir2, readFile as readFile2, writeFile as writeFile2 } from "fs/promises";
180
180
  import { dirname as dirname2, join as join3, resolve as resolve3 } from "path";
181
181
  import { homedir as homedir3 } from "os";
182
- import * as TOML from "@iarna/toml";
183
182
  function expandHome2(filePath) {
184
183
  if (filePath === "~") {
185
184
  return homedir3();
@@ -189,31 +188,59 @@ function expandHome2(filePath) {
189
188
  }
190
189
  return filePath;
191
190
  }
192
- function asObject(value) {
193
- return value !== null && typeof value === "object" && !Array.isArray(value) ? value : {};
191
+ function escapeTomlString(value) {
192
+ return JSON.stringify(value);
194
193
  }
195
- async function readTomlConfig(configPath) {
194
+ function serializeTomlStringArray(values) {
195
+ return `[${values.map((value) => escapeTomlString(value)).join(", ")}]`;
196
+ }
197
+ function serializeTomlInlineTable(values) {
198
+ const entries = Object.entries(values).sort(([left], [right]) => left.localeCompare(right)).map(([key, value]) => `${key} = ${escapeTomlString(value)}`);
199
+ return `{ ${entries.join(", ")} }`;
200
+ }
201
+ function serializeCodexServerBlock(serverName, serverEntry) {
202
+ const lines = [
203
+ `[mcp_servers.${serverName}]`,
204
+ `command = ${escapeTomlString(serverEntry.command)}`,
205
+ `args = ${serializeTomlStringArray(serverEntry.args)}`
206
+ ];
207
+ if (serverEntry.env !== void 0 && Object.keys(serverEntry.env).length > 0) {
208
+ lines.push(`env = ${serializeTomlInlineTable(serverEntry.env)}`);
209
+ }
210
+ return `${lines.join("\n")}
211
+ `;
212
+ }
213
+ function trimTrailingBlankLines(value) {
214
+ return value.replace(/\s+$/u, "");
215
+ }
216
+ function upsertCodexServerBlock(rawConfig, serverName, serverEntry) {
217
+ const block = serializeCodexServerBlock(serverName, serverEntry);
218
+ const normalized = rawConfig.replace(/\r\n/g, "\n");
219
+ const legacyPattern = new RegExp(String.raw`\n?\[mcp\.servers\.${serverName.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}\]\n[\s\S]*?(?=\n\[[^\n]+\]\n|$)`, "g");
220
+ const currentPattern = new RegExp(
221
+ String.raw`\n?\[mcp_servers\.${serverName.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}\]\n[\s\S]*?(?=\n\[[^\n]+\]\n|$)`,
222
+ "g"
223
+ );
224
+ const withoutLegacy = normalized.replace(legacyPattern, "");
225
+ const withoutExisting = withoutLegacy.replace(currentPattern, "");
226
+ const trimmed = trimTrailingBlankLines(withoutExisting);
227
+ if (trimmed.length === 0) {
228
+ return block;
229
+ }
230
+ return `${trimmed}
231
+
232
+ ${block}`;
233
+ }
234
+ async function readTomlConfigText(configPath) {
196
235
  try {
197
- const raw = await readFile2(configPath, "utf8");
198
- if (raw.trim().length === 0) {
199
- return {};
200
- }
201
- return TOML.parse(raw);
236
+ return await readFile2(configPath, "utf8");
202
237
  } catch (error) {
203
238
  if (error instanceof Error && "code" in error && error.code === "ENOENT") {
204
- return {};
239
+ return "";
205
240
  }
206
241
  throw error;
207
242
  }
208
243
  }
209
- function mergeCodexServer(config, serverEntry) {
210
- const mcp = asObject(config.mcp);
211
- const servers = asObject(mcp.servers);
212
- servers.fabric = serverEntry;
213
- mcp.servers = servers;
214
- config.mcp = mcp;
215
- return config;
216
- }
217
244
  var CodexTOMLConfigWriter = class {
218
245
  clientKind = "CodexCLI";
219
246
  configuredPath;
@@ -233,9 +260,10 @@ var CodexTOMLConfigWriter = class {
233
260
  if (configPath === null) {
234
261
  return;
235
262
  }
236
- const config = mergeCodexServer(await readTomlConfig(configPath), createServerEntry(serverPath));
263
+ const rawConfig = await readTomlConfigText(configPath);
264
+ const nextConfig = upsertCodexServerBlock(rawConfig, "fabric", createServerEntry(serverPath));
237
265
  await mkdir2(dirname2(configPath), { recursive: true });
238
- await writeFile2(configPath, TOML.stringify(config), "utf8");
266
+ await writeFile2(configPath, nextConfig, "utf8");
239
267
  }
240
268
  };
241
269
 
@@ -296,7 +324,111 @@ function resolveClients(workspaceRoot, fabricConfig = {}) {
296
324
  );
297
325
  return writers;
298
326
  }
327
+ function detectClientSupports(workspaceRoot, fabricConfig = {}) {
328
+ const clientPaths = fabricConfig.clientPaths;
329
+ const claudeDetected = existsSync4(join4(homedir4(), ".claude")) || existsSync4(join4(workspaceRoot, ".claude"));
330
+ const claudeDesktopDetected = existsSync4(getClaudeDesktopConfigPath());
331
+ const cursorDetected = existsSync4(join4(workspaceRoot, ".cursor"));
332
+ const windsurfDetected = existsSync4(join4(workspaceRoot, ".windsurf"));
333
+ const rooDetected = existsSync4(join4(workspaceRoot, ".roo"));
334
+ const geminiDetected = existsSync4(join4(homedir4(), ".gemini")) || existsSync4(join4(workspaceRoot, "GEMINI.md"));
335
+ const codexDetected = existsSync4(join4(homedir4(), ".codex"));
336
+ return [
337
+ {
338
+ clientKind: "ClaudeCodeCLI",
339
+ label: "Claude Code CLI",
340
+ detected: claudeDetected || hasExplicitPath(clientPaths, "claudeCodeCLI"),
341
+ bootstrapTargetPath: ".fabric/bootstrap/README.md",
342
+ configPath: "project .claude/settings.json",
343
+ capabilities: {
344
+ bootstrap: true,
345
+ mcp: true,
346
+ hook: true,
347
+ skill: true
348
+ }
349
+ },
350
+ {
351
+ clientKind: "ClaudeCodeDesktop",
352
+ label: "Claude Code Desktop",
353
+ detected: claudeDesktopDetected || hasExplicitPath(clientPaths, "claudeCodeDesktop"),
354
+ bootstrapTargetPath: ".fabric/bootstrap/README.md",
355
+ configPath: "desktop Claude config",
356
+ capabilities: {
357
+ bootstrap: true,
358
+ mcp: true,
359
+ hook: false,
360
+ skill: false
361
+ }
362
+ },
363
+ {
364
+ clientKind: "Cursor",
365
+ label: "Cursor",
366
+ detected: cursorDetected || hasExplicitPath(clientPaths, "cursor"),
367
+ bootstrapTargetPath: ".fabric/bootstrap/README.md",
368
+ configPath: ".cursor/mcp.json",
369
+ capabilities: {
370
+ bootstrap: true,
371
+ mcp: true,
372
+ hook: false,
373
+ skill: false
374
+ }
375
+ },
376
+ {
377
+ clientKind: "Windsurf",
378
+ label: "Windsurf",
379
+ detected: windsurfDetected || hasExplicitPath(clientPaths, "windsurf"),
380
+ bootstrapTargetPath: ".fabric/bootstrap/README.md",
381
+ configPath: ".windsurf/mcp.json",
382
+ capabilities: {
383
+ bootstrap: true,
384
+ mcp: true,
385
+ hook: false,
386
+ skill: false
387
+ }
388
+ },
389
+ {
390
+ clientKind: "RooCode",
391
+ label: "Roo Code",
392
+ detected: rooDetected || hasExplicitPath(clientPaths, "rooCode"),
393
+ bootstrapTargetPath: ".fabric/bootstrap/README.md",
394
+ configPath: ".roo/mcp.json",
395
+ capabilities: {
396
+ bootstrap: true,
397
+ mcp: true,
398
+ hook: false,
399
+ skill: false
400
+ }
401
+ },
402
+ {
403
+ clientKind: "GeminiCLI",
404
+ label: "Gemini CLI",
405
+ detected: geminiDetected || hasExplicitPath(clientPaths, "geminiCLI"),
406
+ bootstrapTargetPath: ".fabric/bootstrap/README.md",
407
+ configPath: "~/.gemini/settings.json",
408
+ capabilities: {
409
+ bootstrap: true,
410
+ mcp: true,
411
+ hook: false,
412
+ skill: false
413
+ }
414
+ },
415
+ {
416
+ clientKind: "CodexCLI",
417
+ label: "Codex CLI",
418
+ detected: codexDetected || hasExplicitPath(clientPaths, "codexCLI"),
419
+ bootstrapTargetPath: ".fabric/bootstrap/README.md",
420
+ configPath: "~/.codex/config.toml",
421
+ capabilities: {
422
+ bootstrap: true,
423
+ mcp: true,
424
+ hook: false,
425
+ skill: false
426
+ }
427
+ }
428
+ ];
429
+ }
299
430
 
300
431
  export {
301
- resolveClients
432
+ resolveClients,
433
+ detectClientSupports
302
434
  };
@@ -1,7 +1,10 @@
1
1
  #!/usr/bin/env node
2
2
  import {
3
3
  resolveClients
4
- } from "./chunk-VMYPJPKV.js";
4
+ } from "./chunk-VOQKQ6W2.js";
5
+ import {
6
+ hooksCommand
7
+ } from "./chunk-YDZJRLHL.js";
5
8
  import {
6
9
  t
7
10
  } from "./chunk-6ICJICVU.js";
@@ -73,6 +76,7 @@ var configCmd = defineCommand({
73
76
  description: t("cli.config.description")
74
77
  },
75
78
  subCommands: {
79
+ hooks: hooksCommand,
76
80
  install: defineCommand({
77
81
  meta: {
78
82
  name: "install",
@@ -4,8 +4,9 @@ import {
4
4
  config_default,
5
5
  installMcpClients,
6
6
  parseClientFilter
7
- } from "./chunk-MDI7523D.js";
8
- import "./chunk-VMYPJPKV.js";
7
+ } from "./chunk-XFSQM3LJ.js";
8
+ import "./chunk-VOQKQ6W2.js";
9
+ import "./chunk-YDZJRLHL.js";
9
10
  import "./chunk-6ICJICVU.js";
10
11
  export {
11
12
  configCmd,