@fenglimg/fabric-cli 2.0.0-rc.13 → 2.0.0-rc.15

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.
@@ -1,31 +1,257 @@
1
1
  #!/usr/bin/env node
2
- import {
3
- ClaudeCodeCLIWriter,
4
- CursorWriter,
5
- createServerEntry,
6
- normalizeConfigPath,
7
- removeJsonClientConfigEntry,
8
- writeJsonClientConfig
9
- } from "./chunk-X7QPY5KH.js";
10
2
 
11
3
  // src/config/resolver.ts
12
- import { existsSync as existsSync3 } from "fs";
13
- import { join as join3 } from "path";
14
- import { homedir as homedir3 } from "os";
4
+ import { existsSync as existsSync4 } from "fs";
5
+ import { join as join4 } from "path";
6
+ import { homedir as homedir4 } from "os";
15
7
 
16
8
  // src/config/claude-code.ts
9
+ import { existsSync as existsSync2 } from "fs";
10
+ import { join as join2, resolve as resolve2 } from "path";
11
+ import { homedir as homedir2, platform } from "os";
12
+
13
+ // src/config/json.ts
17
14
  import { existsSync } from "fs";
18
- import { join, resolve } from "path";
19
- import { homedir, platform } from "os";
15
+ import { mkdir, readFile } from "fs/promises";
16
+ import { dirname, join, resolve } from "path";
17
+ import { homedir } from "os";
18
+ import { atomicWriteJson } from "@fenglimg/fabric-shared/node/atomic-write";
19
+
20
+ // src/config/writer.ts
21
+ function createServerEntry(serverPath) {
22
+ return {
23
+ command: process.execPath,
24
+ args: [serverPath]
25
+ };
26
+ }
27
+
28
+ // src/config/json.ts
29
+ function deepMerge(target, source, options = {}) {
30
+ return deepMergeAtPath(target, source, "", options);
31
+ }
32
+ function deepMergeAtPath(target, source, path, options) {
33
+ if (options.arrayAppendPaths && options.arrayAppendPaths.includes(path) && Array.isArray(target) && Array.isArray(source)) {
34
+ return appendArrayWithDedupe(target, source);
35
+ }
36
+ if (target === null || typeof target !== "object" || Array.isArray(target) || source === null || typeof source !== "object" || Array.isArray(source)) {
37
+ return source;
38
+ }
39
+ const out = { ...target };
40
+ for (const key of Object.keys(source)) {
41
+ const childPath = path === "" ? key : `${path}.${key}`;
42
+ out[key] = deepMergeAtPath(
43
+ target[key],
44
+ source[key],
45
+ childPath,
46
+ options
47
+ );
48
+ }
49
+ return out;
50
+ }
51
+ function appendArrayWithDedupe(target, source) {
52
+ const out = [...target];
53
+ for (const candidate of source) {
54
+ if (out.some((existing) => isSameHookEntry(existing, candidate))) {
55
+ continue;
56
+ }
57
+ out.push(candidate);
58
+ }
59
+ return out;
60
+ }
61
+ function isSameHookEntry(a, b) {
62
+ const cmdA = extractHookCommand(a);
63
+ const cmdB = extractHookCommand(b);
64
+ if (cmdA !== null && cmdB !== null) {
65
+ return cmdA === cmdB;
66
+ }
67
+ return deepEqual(a, b);
68
+ }
69
+ function extractHookCommand(item) {
70
+ if (item === null || typeof item !== "object") {
71
+ return null;
72
+ }
73
+ const obj = item;
74
+ if (typeof obj.command === "string") {
75
+ return obj.command;
76
+ }
77
+ if (Array.isArray(obj.hooks)) {
78
+ for (const inner of obj.hooks) {
79
+ if (inner !== null && typeof inner === "object") {
80
+ const innerObj = inner;
81
+ if (typeof innerObj.command === "string") {
82
+ return innerObj.command;
83
+ }
84
+ }
85
+ }
86
+ }
87
+ return null;
88
+ }
89
+ function deepEqual(a, b) {
90
+ if (a === b) {
91
+ return true;
92
+ }
93
+ if (a === null || b === null || typeof a !== "object" || typeof b !== "object") {
94
+ return false;
95
+ }
96
+ if (Array.isArray(a) !== Array.isArray(b)) {
97
+ return false;
98
+ }
99
+ if (Array.isArray(a) && Array.isArray(b)) {
100
+ if (a.length !== b.length) {
101
+ return false;
102
+ }
103
+ return a.every((value, index) => deepEqual(value, b[index]));
104
+ }
105
+ const aObj = a;
106
+ const bObj = b;
107
+ const aKeys = Object.keys(aObj);
108
+ const bKeys = Object.keys(bObj);
109
+ if (aKeys.length !== bKeys.length) {
110
+ return false;
111
+ }
112
+ return aKeys.every((key) => deepEqual(aObj[key], bObj[key]));
113
+ }
114
+ function expandHome(filePath) {
115
+ if (filePath === "~") {
116
+ return homedir();
117
+ }
118
+ if (filePath.startsWith("~/")) {
119
+ return join(homedir(), filePath.slice(2));
120
+ }
121
+ return filePath;
122
+ }
123
+ function normalizeConfigPath(filePath) {
124
+ return resolve(expandHome(filePath));
125
+ }
126
+ async function readJsonConfig(configPath) {
127
+ try {
128
+ const raw = await readFile(configPath, "utf8");
129
+ if (raw.trim().length === 0) {
130
+ return {};
131
+ }
132
+ const parsed = JSON.parse(raw);
133
+ if (parsed === null || typeof parsed !== "object" || Array.isArray(parsed)) {
134
+ throw new Error(`Expected JSON object in ${configPath}`);
135
+ }
136
+ return parsed;
137
+ } catch (error) {
138
+ if (error instanceof Error && "code" in error && error.code === "ENOENT") {
139
+ return {};
140
+ }
141
+ throw error;
142
+ }
143
+ }
144
+ async function writeJsonClientConfig(configPath, serverEntry) {
145
+ const existing = await readJsonConfig(configPath);
146
+ const merged = deepMerge(existing, { mcpServers: { fabric: serverEntry } });
147
+ await mkdir(dirname(configPath), { recursive: true });
148
+ await atomicWriteJson(configPath, merged, { indent: 2 });
149
+ }
150
+ async function removeJsonClientConfigEntry(configPath, serverName) {
151
+ if (!existsSync(configPath)) {
152
+ return { status: "skipped", path: configPath, message: "no-config-file" };
153
+ }
154
+ let existing;
155
+ try {
156
+ existing = await readJsonConfig(configPath);
157
+ } catch (error) {
158
+ return {
159
+ status: "error",
160
+ path: configPath,
161
+ message: error instanceof Error ? error.message : String(error)
162
+ };
163
+ }
164
+ const mcpServers = existing.mcpServers;
165
+ if (mcpServers === void 0 || mcpServers === null || typeof mcpServers !== "object" || Array.isArray(mcpServers)) {
166
+ return { status: "skipped", path: configPath, message: "no-mcp-servers-object" };
167
+ }
168
+ const servers = mcpServers;
169
+ if (!Object.prototype.hasOwnProperty.call(servers, serverName)) {
170
+ return { status: "skipped", path: configPath, message: "not-present" };
171
+ }
172
+ const nextServers = { ...servers };
173
+ delete nextServers[serverName];
174
+ const next = { ...existing, mcpServers: nextServers };
175
+ try {
176
+ await mkdir(dirname(configPath), { recursive: true });
177
+ await atomicWriteJson(configPath, next, { indent: 2 });
178
+ return { status: "removed", path: configPath };
179
+ } catch (error) {
180
+ return {
181
+ status: "error",
182
+ path: configPath,
183
+ message: error instanceof Error ? error.message : String(error)
184
+ };
185
+ }
186
+ }
187
+ var JsonClientConfigWriter = class {
188
+ configuredPath;
189
+ constructor(configuredPath) {
190
+ this.configuredPath = configuredPath;
191
+ }
192
+ async detect(workspaceRoot, overridePath) {
193
+ const explicitPath = overridePath ?? this.configuredPath;
194
+ if (explicitPath !== void 0) {
195
+ return normalizeConfigPath(explicitPath);
196
+ }
197
+ const configPath = this.defaultPath(workspaceRoot);
198
+ return configPath === null ? null : normalizeConfigPath(configPath);
199
+ }
200
+ async write(serverPath, workspaceRoot, overridePath) {
201
+ const configPath = await this.detect(workspaceRoot, overridePath);
202
+ if (configPath === null) {
203
+ return;
204
+ }
205
+ await writeJsonClientConfig(configPath, createServerEntry(serverPath));
206
+ }
207
+ async remove(serverName, workspaceRoot, overridePath) {
208
+ const configPath = await this.detect(workspaceRoot, overridePath);
209
+ if (configPath === null) {
210
+ return { status: "skipped", message: "no-config-path" };
211
+ }
212
+ return removeJsonClientConfigEntry(configPath, serverName);
213
+ }
214
+ };
215
+ var ClaudeCodeCLIWriter = class extends JsonClientConfigWriter {
216
+ clientKind = "ClaudeCodeCLI";
217
+ scope;
218
+ constructor(configuredPath, scope = "project") {
219
+ super(configuredPath);
220
+ this.scope = scope;
221
+ }
222
+ // Writes to project-level .mcp.json (per Claude Code MCP spec) by default,
223
+ // or ~/.claude.json for user scope.
224
+ // Detection still checks ~/.claude to confirm Claude Code is installed.
225
+ defaultPath(workspaceRoot) {
226
+ const globalClaudeDir = join(homedir(), ".claude");
227
+ const projectClaudeDir = join(workspaceRoot, ".claude");
228
+ if (!existsSync(globalClaudeDir) && !existsSync(projectClaudeDir)) {
229
+ return null;
230
+ }
231
+ return this.scope === "user" ? join(homedir(), ".claude.json") : join(workspaceRoot, ".mcp.json");
232
+ }
233
+ };
234
+ var CursorWriter = class extends JsonClientConfigWriter {
235
+ clientKind = "Cursor";
236
+ constructor(configuredPath) {
237
+ super(configuredPath);
238
+ }
239
+ defaultPath(workspaceRoot) {
240
+ const cursorDir = join(workspaceRoot, ".cursor");
241
+ return existsSync(cursorDir) ? join(cursorDir, "mcp.json") : null;
242
+ }
243
+ };
244
+
245
+ // src/config/claude-code.ts
20
246
  function getClaudeDesktopConfigPath() {
21
247
  const os = platform();
22
248
  if (os === "darwin") {
23
- return join(homedir(), "Library", "Application Support", "Claude", "claude_desktop_config.json");
249
+ return join2(homedir2(), "Library", "Application Support", "Claude", "claude_desktop_config.json");
24
250
  }
25
251
  if (os === "win32") {
26
- return join(process.env.APPDATA ?? join(homedir(), "AppData", "Roaming"), "Claude", "claude_desktop_config.json");
252
+ return join2(process.env.APPDATA ?? join2(homedir2(), "AppData", "Roaming"), "Claude", "claude_desktop_config.json");
27
253
  }
28
- return join(homedir(), ".config", "Claude", "claude_desktop_config.json");
254
+ return join2(homedir2(), ".config", "Claude", "claude_desktop_config.json");
29
255
  }
30
256
  var ClaudeCodeDesktopWriter = class {
31
257
  clientKind = "ClaudeCodeDesktop";
@@ -35,7 +261,7 @@ var ClaudeCodeDesktopWriter = class {
35
261
  }
36
262
  async detect(_workspaceRoot, overridePath) {
37
263
  const configPath = normalizeConfigPath(overridePath ?? this.configuredPath ?? getClaudeDesktopConfigPath());
38
- return existsSync(configPath) || overridePath !== void 0 || this.configuredPath !== void 0 ? configPath : null;
264
+ return existsSync2(configPath) || overridePath !== void 0 || this.configuredPath !== void 0 ? configPath : null;
39
265
  }
40
266
  async write(serverPath, workspaceRoot, overridePath) {
41
267
  const configPath = await this.detect(workspaceRoot, overridePath);
@@ -57,17 +283,17 @@ var ClaudeCodeDesktopWriter = class {
57
283
  };
58
284
 
59
285
  // src/config/toml.ts
60
- import { existsSync as existsSync2 } from "fs";
61
- import { mkdir, readFile } from "fs/promises";
62
- import { dirname, join as join2, resolve as resolve2 } from "path";
63
- import { homedir as homedir2 } from "os";
286
+ import { existsSync as existsSync3 } from "fs";
287
+ import { mkdir as mkdir2, readFile as readFile2 } from "fs/promises";
288
+ import { dirname as dirname2, join as join3, resolve as resolve3 } from "path";
289
+ import { homedir as homedir3 } from "os";
64
290
  import { atomicWriteText } from "@fenglimg/fabric-shared/node/atomic-write";
65
- function expandHome(filePath) {
291
+ function expandHome2(filePath) {
66
292
  if (filePath === "~") {
67
- return homedir2();
293
+ return homedir3();
68
294
  }
69
295
  if (filePath.startsWith("~/")) {
70
- return join2(homedir2(), filePath.slice(2));
296
+ return join3(homedir3(), filePath.slice(2));
71
297
  }
72
298
  return filePath;
73
299
  }
@@ -134,7 +360,7 @@ ${block}`;
134
360
  }
135
361
  async function readTomlConfigText(configPath) {
136
362
  try {
137
- return await readFile(configPath, "utf8");
363
+ return await readFile2(configPath, "utf8");
138
364
  } catch (error) {
139
365
  if (error instanceof Error && "code" in error && error.code === "ENOENT") {
140
366
  return "";
@@ -151,10 +377,10 @@ var CodexTOMLConfigWriter = class {
151
377
  async detect(_workspaceRoot, overridePath) {
152
378
  const explicitPath = overridePath ?? this.configuredPath;
153
379
  if (explicitPath !== void 0) {
154
- return resolve2(expandHome(explicitPath));
380
+ return resolve3(expandHome2(explicitPath));
155
381
  }
156
- const codexDir = join2(homedir2(), ".codex");
157
- return existsSync2(codexDir) ? resolve2(join2(codexDir, "config.toml")) : null;
382
+ const codexDir = join3(homedir3(), ".codex");
383
+ return existsSync3(codexDir) ? resolve3(join3(codexDir, "config.toml")) : null;
158
384
  }
159
385
  async write(serverPath, workspaceRoot, overridePath) {
160
386
  const configPath = await this.detect(workspaceRoot, overridePath);
@@ -163,7 +389,7 @@ var CodexTOMLConfigWriter = class {
163
389
  }
164
390
  const rawConfig = await readTomlConfigText(configPath);
165
391
  const nextConfig = upsertCodexServerBlock(rawConfig, "fabric", createServerEntry(serverPath));
166
- await mkdir(dirname(configPath), { recursive: true });
392
+ await mkdir2(dirname2(configPath), { recursive: true });
167
393
  await atomicWriteText(configPath, nextConfig);
168
394
  }
169
395
  async remove(serverName, workspaceRoot, overridePath) {
@@ -171,7 +397,7 @@ var CodexTOMLConfigWriter = class {
171
397
  if (configPath === null) {
172
398
  return { status: "skipped", message: "no-config-path" };
173
399
  }
174
- if (!existsSync2(configPath)) {
400
+ if (!existsSync3(configPath)) {
175
401
  return { status: "skipped", path: configPath, message: "no-config-file" };
176
402
  }
177
403
  let rawConfig;
@@ -189,7 +415,7 @@ var CodexTOMLConfigWriter = class {
189
415
  return { status: "skipped", path: configPath, message: "not-present" };
190
416
  }
191
417
  try {
192
- await mkdir(dirname(configPath), { recursive: true });
418
+ await mkdir2(dirname2(configPath), { recursive: true });
193
419
  await atomicWriteText(configPath, text);
194
420
  return { status: "removed", path: configPath };
195
421
  } catch (error) {
@@ -218,25 +444,25 @@ function resolveClients(workspaceRoot, fabricConfig = {}, opts = {}) {
218
444
  const claudeMcpScope = opts.claudeMcpScope ?? "project";
219
445
  addIfDetected(
220
446
  writers,
221
- existsSync3(join3(homedir3(), ".claude")) || existsSync3(join3(workspaceRoot, ".claude")),
447
+ existsSync4(join4(homedir4(), ".claude")) || existsSync4(join4(workspaceRoot, ".claude")),
222
448
  (configuredPath) => new ClaudeCodeCLIWriter(configuredPath, claudeMcpScope),
223
449
  hasExplicitPath(clientPaths, "claudeCodeCLI") ? clientPaths.claudeCodeCLI : void 0
224
450
  );
225
451
  addIfDetected(
226
452
  writers,
227
- existsSync3(getClaudeDesktopConfigPath()),
453
+ existsSync4(getClaudeDesktopConfigPath()),
228
454
  (configuredPath) => new ClaudeCodeDesktopWriter(configuredPath),
229
455
  hasExplicitPath(clientPaths, "claudeCodeDesktop") ? clientPaths.claudeCodeDesktop : void 0
230
456
  );
231
457
  addIfDetected(
232
458
  writers,
233
- existsSync3(join3(workspaceRoot, ".cursor")),
459
+ existsSync4(join4(workspaceRoot, ".cursor")),
234
460
  (configuredPath) => new CursorWriter(configuredPath),
235
461
  hasExplicitPath(clientPaths, "cursor") ? clientPaths.cursor : void 0
236
462
  );
237
463
  addIfDetected(
238
464
  writers,
239
- existsSync3(join3(homedir3(), ".codex")),
465
+ existsSync4(join4(homedir4(), ".codex")),
240
466
  (configuredPath) => new CodexTOMLConfigWriter(configuredPath),
241
467
  hasExplicitPath(clientPaths, "codexCLI") ? clientPaths.codexCLI : void 0
242
468
  );
@@ -244,10 +470,10 @@ function resolveClients(workspaceRoot, fabricConfig = {}, opts = {}) {
244
470
  }
245
471
  function detectClientSupports(workspaceRoot, fabricConfig = {}) {
246
472
  const clientPaths = fabricConfig.clientPaths;
247
- const claudeDetected = existsSync3(join3(homedir3(), ".claude")) || existsSync3(join3(workspaceRoot, ".claude"));
248
- const claudeDesktopDetected = existsSync3(getClaudeDesktopConfigPath());
249
- const cursorDetected = existsSync3(join3(workspaceRoot, ".cursor"));
250
- const codexDetected = existsSync3(join3(homedir3(), ".codex"));
473
+ const claudeDetected = existsSync4(join4(homedir4(), ".claude")) || existsSync4(join4(workspaceRoot, ".claude"));
474
+ const claudeDesktopDetected = existsSync4(getClaudeDesktopConfigPath());
475
+ const cursorDetected = existsSync4(join4(workspaceRoot, ".cursor"));
476
+ const codexDetected = existsSync4(join4(homedir4(), ".codex"));
251
477
  return [
252
478
  {
253
479
  clientKind: "ClaudeCodeCLI",
@@ -305,7 +531,7 @@ function detectClientSupports(workspaceRoot, fabricConfig = {}) {
305
531
  skill: true
306
532
  },
307
533
  installedCapabilities: {
308
- hook: existsSync3(join3(workspaceRoot, ".codex", "hooks.json")),
534
+ hook: existsSync4(join4(workspaceRoot, ".codex", "hooks.json")),
309
535
  // v2/rc.2: v1 client-side init skill removed; skill-installation probes
310
536
  // will return once rc.2/3/4 introduce the v2 skills (fabric-archive,
311
537
  // fabric-review, fabric-import). Until then there is nothing to probe.
@@ -316,6 +542,7 @@ function detectClientSupports(workspaceRoot, fabricConfig = {}) {
316
542
  }
317
543
 
318
544
  export {
545
+ deepMerge,
319
546
  resolveClients,
320
547
  detectClientSupports
321
548
  };