@fenglimg/fabric-cli 2.0.0 → 2.1.0-rc.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +6 -5
- package/dist/chunk-BATF4PEJ.js +361 -0
- package/dist/{chunk-OBQU6NHO.js → chunk-COI5VDFU.js} +0 -18
- package/dist/chunk-F46ORPOA.js +903 -0
- package/dist/chunk-HFQVXY6P.js +86 -0
- package/dist/chunk-L4Q55UC4.js +52 -0
- package/dist/chunk-LFIKMVY7.js +27 -0
- package/dist/chunk-MF3OTILQ.js +544 -0
- package/dist/chunk-PWLW3B57.js +18 -0
- package/dist/chunk-RYAFBNES.js +33 -0
- package/dist/chunk-T5RPGCCM.js +40 -0
- package/dist/chunk-WU6GAPKH.js +36 -0
- package/dist/config-XJIPZNUP.js +13 -0
- package/dist/doctor-QVNPHLJK.js +920 -0
- package/dist/index.js +23 -8
- package/dist/{init-BIRSIOXO.js → install-2HDO5FTQ.js} +807 -705
- package/dist/metrics-ACEQFPDU.js +122 -0
- package/dist/onboard-coverage-MFCAEBDO.js +220 -0
- package/dist/{plan-context-hint-QMUPAXIB.js → plan-context-hint-FC6P3WFE.js} +34 -28
- package/dist/scope-explain-2F2R5URO.js +33 -0
- package/dist/status-GLQWLWH6.js +23 -0
- package/dist/store-XTSE5TY6.js +105 -0
- package/dist/sync-BJCWDPNC.js +245 -0
- package/dist/uninstall-TAXSUSKH.js +1073 -0
- package/dist/whoami-B6AEMSEV.js +31 -0
- package/package.json +30 -5
- package/templates/hooks/cite-policy-evict.cjs +231 -0
- package/templates/hooks/configs/README.md +29 -6
- package/templates/hooks/configs/claude-code.json +14 -3
- package/templates/hooks/configs/codex-hooks.json +6 -3
- package/templates/hooks/configs/cursor-hooks.json +8 -10
- package/templates/hooks/fabric-hint.cjs +873 -105
- package/templates/hooks/knowledge-hint-broad.cjs +549 -135
- package/templates/hooks/knowledge-hint-narrow.cjs +830 -26
- package/templates/hooks/lib/banner-i18n.cjs +309 -0
- package/templates/hooks/lib/bindings-snapshot-reader.cjs +81 -0
- package/templates/hooks/lib/cite-contract-reminder.cjs +179 -0
- package/templates/hooks/lib/cite-line-parser.cjs +180 -0
- package/templates/hooks/lib/client-adapter.cjs +106 -0
- package/templates/hooks/lib/config-cache.cjs +107 -0
- package/templates/hooks/lib/state-store.cjs +84 -0
- package/templates/hooks/lib/summary-fallback.cjs +210 -0
- package/templates/skills/fabric-archive/SKILL.md +97 -419
- package/templates/skills/fabric-archive/ref/dry-run-scope.md +16 -0
- package/templates/skills/fabric-archive/ref/e5-cron-recap.md +58 -0
- package/templates/skills/fabric-archive/ref/i18n-policy.md +86 -0
- package/templates/skills/fabric-archive/ref/phase-0-range-resolution.md +156 -0
- package/templates/skills/fabric-archive/ref/phase-1-5-onboard.md +218 -0
- package/templates/skills/fabric-archive/ref/phase-1-cross-session.md +62 -0
- package/templates/skills/fabric-archive/ref/phase-2-5-viability.md +68 -0
- package/templates/skills/fabric-archive/ref/phase-3-5-scope.md +108 -0
- package/templates/skills/fabric-archive/ref/phase-3-classify.md +63 -0
- package/templates/skills/fabric-archive/ref/phase-4-5-emit.md +78 -0
- package/templates/skills/fabric-archive/ref/phase-4-mcp-persist.md +89 -0
- package/templates/skills/fabric-archive/ref/rc-history.md +38 -0
- package/templates/skills/fabric-archive/ref/worked-examples.md +78 -0
- package/templates/skills/fabric-import/SKILL.md +77 -514
- package/templates/skills/fabric-import/ref/checkpoint-state.md +85 -0
- package/templates/skills/fabric-import/ref/i18n-policy.md +79 -0
- package/templates/skills/fabric-import/ref/output-contract.md +61 -0
- package/templates/skills/fabric-import/ref/phase-2-mining.md +213 -0
- package/templates/skills/fabric-import/ref/phase-3-dedup.md +75 -0
- package/templates/skills/fabric-import/ref/state-recovery.md +57 -0
- package/templates/skills/fabric-import/ref/worked-examples.md +127 -0
- package/templates/skills/fabric-review/SKILL.md +90 -284
- package/templates/skills/fabric-review/ref/askuserquestion-policy.md +66 -0
- package/templates/skills/fabric-review/ref/i18n-policy.md +111 -0
- package/templates/skills/fabric-review/ref/modify-flow.md +103 -0
- package/templates/skills/fabric-review/ref/output-contract.md +58 -0
- package/templates/skills/fabric-review/ref/per-mode-flows.md +155 -0
- package/templates/skills/fabric-review/ref/semantic-check.md +26 -0
- package/templates/skills/fabric-review/ref/worked-examples.md +95 -0
- package/templates/skills/fabric-sync/SKILL.md +46 -0
- package/templates/skills/lib/shared-policy.md +69 -0
- package/dist/chunk-6ICJICVU.js +0 -10
- package/dist/chunk-74SZWYPH.js +0 -658
- package/dist/chunk-EYIDD2YS.js +0 -1000
- package/dist/doctor-T7JWODKG.js +0 -282
- package/dist/hooks-Y74Y5LQS.js +0 -12
- package/dist/scan-LMK3UCWL.js +0 -22
- package/dist/serve-H554BHLG.js +0 -124
- package/templates/agents-md/AGENTS.md.template +0 -59
- package/templates/bootstrap/CLAUDE.md +0 -8
- package/templates/bootstrap/codex-AGENTS-header.md +0 -6
- package/templates/bootstrap/cursor-fabric-bootstrap.mdc +0 -10
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import {
|
|
3
|
+
loadProjectConfig,
|
|
4
|
+
saveProjectConfig
|
|
5
|
+
} from "./chunk-LFIKMVY7.js";
|
|
6
|
+
import {
|
|
7
|
+
loadGlobalConfig,
|
|
8
|
+
resolveGlobalRoot,
|
|
9
|
+
saveGlobalConfig
|
|
10
|
+
} from "./chunk-RYAFBNES.js";
|
|
11
|
+
|
|
12
|
+
// src/store/store-ops.ts
|
|
13
|
+
import {
|
|
14
|
+
addMountedStore,
|
|
15
|
+
bindRequiredStore,
|
|
16
|
+
detachMountedStore,
|
|
17
|
+
explainStore
|
|
18
|
+
} from "@fenglimg/fabric-shared";
|
|
19
|
+
var NO_GLOBAL_CONFIG = "no global Fabric config found \u2014 run `fabric install --global <url>` first";
|
|
20
|
+
function requireConfig(globalRoot) {
|
|
21
|
+
const config = loadGlobalConfig(globalRoot);
|
|
22
|
+
if (config === null) {
|
|
23
|
+
throw new Error(NO_GLOBAL_CONFIG);
|
|
24
|
+
}
|
|
25
|
+
return config;
|
|
26
|
+
}
|
|
27
|
+
function storeList(globalRoot = resolveGlobalRoot()) {
|
|
28
|
+
return requireConfig(globalRoot).stores;
|
|
29
|
+
}
|
|
30
|
+
function storeAdd(store, globalRoot = resolveGlobalRoot()) {
|
|
31
|
+
const next = addMountedStore(requireConfig(globalRoot), store);
|
|
32
|
+
saveGlobalConfig(next, globalRoot);
|
|
33
|
+
return next;
|
|
34
|
+
}
|
|
35
|
+
function storeRemove(alias, globalRoot = resolveGlobalRoot()) {
|
|
36
|
+
const result = detachMountedStore(requireConfig(globalRoot), alias);
|
|
37
|
+
saveGlobalConfig(result.config, globalRoot);
|
|
38
|
+
return result;
|
|
39
|
+
}
|
|
40
|
+
function storeExplain(alias, globalRoot = resolveGlobalRoot()) {
|
|
41
|
+
return explainStore(requireConfig(globalRoot), alias);
|
|
42
|
+
}
|
|
43
|
+
var NO_PROJECT_CONFIG = "no project Fabric config \u2014 run `fabric install` in this repo first";
|
|
44
|
+
function requireProjectConfig(projectRoot) {
|
|
45
|
+
const config = loadProjectConfig(projectRoot);
|
|
46
|
+
if (config === null) {
|
|
47
|
+
throw new Error(NO_PROJECT_CONFIG);
|
|
48
|
+
}
|
|
49
|
+
return config;
|
|
50
|
+
}
|
|
51
|
+
function storeBind(projectRoot, entry) {
|
|
52
|
+
const config = requireProjectConfig(projectRoot);
|
|
53
|
+
const next = {
|
|
54
|
+
...config,
|
|
55
|
+
required_stores: bindRequiredStore(config.required_stores ?? [], entry)
|
|
56
|
+
};
|
|
57
|
+
saveProjectConfig(next, projectRoot);
|
|
58
|
+
return next;
|
|
59
|
+
}
|
|
60
|
+
function storeSwitchWrite(projectRoot, alias) {
|
|
61
|
+
const config = requireProjectConfig(projectRoot);
|
|
62
|
+
const next = { ...config, active_write_store: alias };
|
|
63
|
+
saveProjectConfig(next, projectRoot);
|
|
64
|
+
return next;
|
|
65
|
+
}
|
|
66
|
+
function missingRequiredStores(projectRoot, globalRoot = resolveGlobalRoot()) {
|
|
67
|
+
const project = loadProjectConfig(projectRoot);
|
|
68
|
+
if (project === null || project.required_stores === void 0) {
|
|
69
|
+
return [];
|
|
70
|
+
}
|
|
71
|
+
const global = loadGlobalConfig(globalRoot);
|
|
72
|
+
const mounted = new Set(
|
|
73
|
+
(global?.stores ?? []).flatMap((s) => [s.alias, s.store_uuid])
|
|
74
|
+
);
|
|
75
|
+
return project.required_stores.filter((r) => !mounted.has(r.id));
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
export {
|
|
79
|
+
storeList,
|
|
80
|
+
storeAdd,
|
|
81
|
+
storeRemove,
|
|
82
|
+
storeExplain,
|
|
83
|
+
storeBind,
|
|
84
|
+
storeSwitchWrite,
|
|
85
|
+
missingRequiredStores
|
|
86
|
+
};
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import {
|
|
3
|
+
loadProjectConfig
|
|
4
|
+
} from "./chunk-LFIKMVY7.js";
|
|
5
|
+
import {
|
|
6
|
+
loadGlobalConfig,
|
|
7
|
+
resolveGlobalRoot
|
|
8
|
+
} from "./chunk-RYAFBNES.js";
|
|
9
|
+
|
|
10
|
+
// src/store/scope-explain.ts
|
|
11
|
+
import {
|
|
12
|
+
createStoreResolver
|
|
13
|
+
} from "@fenglimg/fabric-shared";
|
|
14
|
+
function buildResolveInput(projectRoot, globalRoot = resolveGlobalRoot()) {
|
|
15
|
+
const global = loadGlobalConfig(globalRoot);
|
|
16
|
+
if (global === null) {
|
|
17
|
+
return null;
|
|
18
|
+
}
|
|
19
|
+
const project = loadProjectConfig(projectRoot);
|
|
20
|
+
return {
|
|
21
|
+
uid: global.uid,
|
|
22
|
+
mountedStores: global.stores.map((s) => ({
|
|
23
|
+
store_uuid: s.store_uuid,
|
|
24
|
+
alias: s.alias,
|
|
25
|
+
...s.remote === void 0 ? {} : { remote: s.remote },
|
|
26
|
+
writable: s.writable ?? true,
|
|
27
|
+
personal: s.personal ?? false
|
|
28
|
+
})),
|
|
29
|
+
requiredStores: (project?.required_stores ?? []).map((r) => ({
|
|
30
|
+
id: r.id,
|
|
31
|
+
...r.suggested_remote === void 0 ? {} : { suggested_remote: r.suggested_remote }
|
|
32
|
+
})),
|
|
33
|
+
...project?.active_write_store === void 0 ? {} : { activeWriteAlias: project.active_write_store }
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
function scopeExplain(projectRoot, scope, globalRoot = resolveGlobalRoot()) {
|
|
37
|
+
const input = buildResolveInput(projectRoot, globalRoot);
|
|
38
|
+
if (input === null) {
|
|
39
|
+
return null;
|
|
40
|
+
}
|
|
41
|
+
const resolver = createStoreResolver();
|
|
42
|
+
return {
|
|
43
|
+
scope,
|
|
44
|
+
readSet: resolver.resolveReadSet(input),
|
|
45
|
+
writeTarget: resolver.resolveWriteTarget(input, scope).target
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export {
|
|
50
|
+
buildResolveInput,
|
|
51
|
+
scopeExplain
|
|
52
|
+
};
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
// src/store/project-config-io.ts
|
|
4
|
+
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "fs";
|
|
5
|
+
import { join } from "path";
|
|
6
|
+
import { fabricConfigSchema } from "@fenglimg/fabric-shared";
|
|
7
|
+
function projectConfigPath(projectRoot) {
|
|
8
|
+
return join(projectRoot, ".fabric", "fabric-config.json");
|
|
9
|
+
}
|
|
10
|
+
function loadProjectConfig(projectRoot) {
|
|
11
|
+
const path = projectConfigPath(projectRoot);
|
|
12
|
+
if (!existsSync(path)) {
|
|
13
|
+
return null;
|
|
14
|
+
}
|
|
15
|
+
return fabricConfigSchema.parse(JSON.parse(readFileSync(path, "utf8")));
|
|
16
|
+
}
|
|
17
|
+
function saveProjectConfig(config, projectRoot) {
|
|
18
|
+
const validated = fabricConfigSchema.parse(config);
|
|
19
|
+
mkdirSync(join(projectRoot, ".fabric"), { recursive: true });
|
|
20
|
+
writeFileSync(projectConfigPath(projectRoot), `${JSON.stringify(validated, null, 2)}
|
|
21
|
+
`, "utf8");
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export {
|
|
25
|
+
loadProjectConfig,
|
|
26
|
+
saveProjectConfig
|
|
27
|
+
};
|
|
@@ -0,0 +1,544 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
// src/config/resolver.ts
|
|
4
|
+
import { existsSync as existsSync4 } from "fs";
|
|
5
|
+
import { join as join4 } from "path";
|
|
6
|
+
import { homedir as homedir4 } from "os";
|
|
7
|
+
|
|
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
|
|
14
|
+
import { existsSync } from "fs";
|
|
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
|
|
246
|
+
function getClaudeDesktopConfigPath() {
|
|
247
|
+
const os = platform();
|
|
248
|
+
if (os === "darwin") {
|
|
249
|
+
return join2(homedir2(), "Library", "Application Support", "Claude", "claude_desktop_config.json");
|
|
250
|
+
}
|
|
251
|
+
if (os === "win32") {
|
|
252
|
+
return join2(process.env.APPDATA ?? join2(homedir2(), "AppData", "Roaming"), "Claude", "claude_desktop_config.json");
|
|
253
|
+
}
|
|
254
|
+
return join2(homedir2(), ".config", "Claude", "claude_desktop_config.json");
|
|
255
|
+
}
|
|
256
|
+
var ClaudeCodeDesktopWriter = class {
|
|
257
|
+
clientKind = "ClaudeCodeDesktop";
|
|
258
|
+
configuredPath;
|
|
259
|
+
constructor(configuredPath) {
|
|
260
|
+
this.configuredPath = configuredPath;
|
|
261
|
+
}
|
|
262
|
+
async detect(_workspaceRoot, overridePath) {
|
|
263
|
+
const configPath = normalizeConfigPath(overridePath ?? this.configuredPath ?? getClaudeDesktopConfigPath());
|
|
264
|
+
return existsSync2(configPath) || overridePath !== void 0 || this.configuredPath !== void 0 ? configPath : null;
|
|
265
|
+
}
|
|
266
|
+
async write(serverPath, workspaceRoot, overridePath) {
|
|
267
|
+
const configPath = await this.detect(workspaceRoot, overridePath);
|
|
268
|
+
if (configPath === null) {
|
|
269
|
+
return;
|
|
270
|
+
}
|
|
271
|
+
await writeJsonClientConfig(configPath, {
|
|
272
|
+
command: process.execPath,
|
|
273
|
+
args: [serverPath]
|
|
274
|
+
});
|
|
275
|
+
}
|
|
276
|
+
async remove(serverName, workspaceRoot, overridePath) {
|
|
277
|
+
const configPath = await this.detect(workspaceRoot, overridePath);
|
|
278
|
+
if (configPath === null) {
|
|
279
|
+
return { status: "skipped", message: "no-config-path" };
|
|
280
|
+
}
|
|
281
|
+
return removeJsonClientConfigEntry(configPath, serverName);
|
|
282
|
+
}
|
|
283
|
+
};
|
|
284
|
+
|
|
285
|
+
// src/config/toml.ts
|
|
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";
|
|
290
|
+
import { atomicWriteText } from "@fenglimg/fabric-shared/node/atomic-write";
|
|
291
|
+
function expandHome2(filePath) {
|
|
292
|
+
if (filePath === "~") {
|
|
293
|
+
return homedir3();
|
|
294
|
+
}
|
|
295
|
+
if (filePath.startsWith("~/")) {
|
|
296
|
+
return join3(homedir3(), filePath.slice(2));
|
|
297
|
+
}
|
|
298
|
+
return filePath;
|
|
299
|
+
}
|
|
300
|
+
function escapeTomlString(value) {
|
|
301
|
+
return JSON.stringify(value);
|
|
302
|
+
}
|
|
303
|
+
function serializeTomlStringArray(values) {
|
|
304
|
+
return `[${values.map((value) => escapeTomlString(value)).join(", ")}]`;
|
|
305
|
+
}
|
|
306
|
+
function serializeTomlInlineTable(values) {
|
|
307
|
+
const entries = Object.entries(values).sort(([left], [right]) => left.localeCompare(right)).map(([key, value]) => `${key} = ${escapeTomlString(value)}`);
|
|
308
|
+
return `{ ${entries.join(", ")} }`;
|
|
309
|
+
}
|
|
310
|
+
function serializeCodexServerBlock(serverName, serverEntry) {
|
|
311
|
+
const lines = [
|
|
312
|
+
`[mcp_servers.${serverName}]`,
|
|
313
|
+
`command = ${escapeTomlString(serverEntry.command)}`,
|
|
314
|
+
`args = ${serializeTomlStringArray(serverEntry.args)}`
|
|
315
|
+
];
|
|
316
|
+
if (serverEntry.env !== void 0 && Object.keys(serverEntry.env).length > 0) {
|
|
317
|
+
lines.push(`env = ${serializeTomlInlineTable(serverEntry.env)}`);
|
|
318
|
+
}
|
|
319
|
+
return `${lines.join("\n")}
|
|
320
|
+
`;
|
|
321
|
+
}
|
|
322
|
+
function trimTrailingBlankLines(value) {
|
|
323
|
+
return value.replace(/\s+$/u, "");
|
|
324
|
+
}
|
|
325
|
+
function removeCodexServerBlock(rawConfig, serverName) {
|
|
326
|
+
const normalized = rawConfig.replace(/\r\n/g, "\n");
|
|
327
|
+
const escaped = serverName.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
328
|
+
const legacyPattern = new RegExp(
|
|
329
|
+
String.raw`\n?\[mcp\.servers\.${escaped}\]\n[\s\S]*?(?=\n\[[^\n]+\]\n|$)`,
|
|
330
|
+
"g"
|
|
331
|
+
);
|
|
332
|
+
const currentPattern = new RegExp(
|
|
333
|
+
String.raw`\n?\[mcp_servers\.${escaped}\]\n[\s\S]*?(?=\n\[[^\n]+\]\n|$)`,
|
|
334
|
+
"g"
|
|
335
|
+
);
|
|
336
|
+
const withoutLegacy = normalized.replace(legacyPattern, "");
|
|
337
|
+
const withoutCurrent = withoutLegacy.replace(currentPattern, "");
|
|
338
|
+
const changed = withoutCurrent !== normalized;
|
|
339
|
+
const text = changed ? `${trimTrailingBlankLines(withoutCurrent)}
|
|
340
|
+
` : rawConfig;
|
|
341
|
+
return { text, changed };
|
|
342
|
+
}
|
|
343
|
+
function upsertCodexServerBlock(rawConfig, serverName, serverEntry) {
|
|
344
|
+
const block = serializeCodexServerBlock(serverName, serverEntry);
|
|
345
|
+
const normalized = rawConfig.replace(/\r\n/g, "\n");
|
|
346
|
+
const legacyPattern = new RegExp(String.raw`\n?\[mcp\.servers\.${serverName.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}\]\n[\s\S]*?(?=\n\[[^\n]+\]\n|$)`, "g");
|
|
347
|
+
const currentPattern = new RegExp(
|
|
348
|
+
String.raw`\n?\[mcp_servers\.${serverName.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}\]\n[\s\S]*?(?=\n\[[^\n]+\]\n|$)`,
|
|
349
|
+
"g"
|
|
350
|
+
);
|
|
351
|
+
const withoutLegacy = normalized.replace(legacyPattern, "");
|
|
352
|
+
const withoutExisting = withoutLegacy.replace(currentPattern, "");
|
|
353
|
+
const trimmed = trimTrailingBlankLines(withoutExisting);
|
|
354
|
+
if (trimmed.length === 0) {
|
|
355
|
+
return block;
|
|
356
|
+
}
|
|
357
|
+
return `${trimmed}
|
|
358
|
+
|
|
359
|
+
${block}`;
|
|
360
|
+
}
|
|
361
|
+
async function readTomlConfigText(configPath) {
|
|
362
|
+
try {
|
|
363
|
+
return await readFile2(configPath, "utf8");
|
|
364
|
+
} catch (error) {
|
|
365
|
+
if (error instanceof Error && "code" in error && error.code === "ENOENT") {
|
|
366
|
+
return "";
|
|
367
|
+
}
|
|
368
|
+
throw error;
|
|
369
|
+
}
|
|
370
|
+
}
|
|
371
|
+
var CodexTOMLConfigWriter = class {
|
|
372
|
+
clientKind = "CodexCLI";
|
|
373
|
+
configuredPath;
|
|
374
|
+
constructor(configuredPath) {
|
|
375
|
+
this.configuredPath = configuredPath;
|
|
376
|
+
}
|
|
377
|
+
async detect(_workspaceRoot, overridePath) {
|
|
378
|
+
const explicitPath = overridePath ?? this.configuredPath;
|
|
379
|
+
if (explicitPath !== void 0) {
|
|
380
|
+
return resolve3(expandHome2(explicitPath));
|
|
381
|
+
}
|
|
382
|
+
const codexDir = join3(homedir3(), ".codex");
|
|
383
|
+
return existsSync3(codexDir) ? resolve3(join3(codexDir, "config.toml")) : null;
|
|
384
|
+
}
|
|
385
|
+
async write(serverPath, workspaceRoot, overridePath) {
|
|
386
|
+
const configPath = await this.detect(workspaceRoot, overridePath);
|
|
387
|
+
if (configPath === null) {
|
|
388
|
+
return;
|
|
389
|
+
}
|
|
390
|
+
const rawConfig = await readTomlConfigText(configPath);
|
|
391
|
+
const nextConfig = upsertCodexServerBlock(rawConfig, "fabric", createServerEntry(serverPath));
|
|
392
|
+
await mkdir2(dirname2(configPath), { recursive: true });
|
|
393
|
+
await atomicWriteText(configPath, nextConfig);
|
|
394
|
+
}
|
|
395
|
+
async remove(serverName, workspaceRoot, overridePath) {
|
|
396
|
+
const configPath = await this.detect(workspaceRoot, overridePath);
|
|
397
|
+
if (configPath === null) {
|
|
398
|
+
return { status: "skipped", message: "no-config-path" };
|
|
399
|
+
}
|
|
400
|
+
if (!existsSync3(configPath)) {
|
|
401
|
+
return { status: "skipped", path: configPath, message: "no-config-file" };
|
|
402
|
+
}
|
|
403
|
+
let rawConfig;
|
|
404
|
+
try {
|
|
405
|
+
rawConfig = await readTomlConfigText(configPath);
|
|
406
|
+
} catch (error) {
|
|
407
|
+
return {
|
|
408
|
+
status: "error",
|
|
409
|
+
path: configPath,
|
|
410
|
+
message: error instanceof Error ? error.message : String(error)
|
|
411
|
+
};
|
|
412
|
+
}
|
|
413
|
+
const { text, changed } = removeCodexServerBlock(rawConfig, serverName);
|
|
414
|
+
if (!changed) {
|
|
415
|
+
return { status: "skipped", path: configPath, message: "not-present" };
|
|
416
|
+
}
|
|
417
|
+
try {
|
|
418
|
+
await mkdir2(dirname2(configPath), { recursive: true });
|
|
419
|
+
await atomicWriteText(configPath, text);
|
|
420
|
+
return { status: "removed", path: configPath };
|
|
421
|
+
} catch (error) {
|
|
422
|
+
return {
|
|
423
|
+
status: "error",
|
|
424
|
+
path: configPath,
|
|
425
|
+
message: error instanceof Error ? error.message : String(error)
|
|
426
|
+
};
|
|
427
|
+
}
|
|
428
|
+
}
|
|
429
|
+
};
|
|
430
|
+
|
|
431
|
+
// src/config/resolver.ts
|
|
432
|
+
import { clientPathsSchema, fabricConfigSchema } from "@fenglimg/fabric-shared";
|
|
433
|
+
function hasExplicitPath(clientPaths, key) {
|
|
434
|
+
return typeof clientPaths?.[key] === "string" && clientPaths[key].trim().length > 0;
|
|
435
|
+
}
|
|
436
|
+
function addIfDetected(writers, detected, createWriter, configuredPath) {
|
|
437
|
+
if (configuredPath !== void 0 || detected) {
|
|
438
|
+
writers.push(createWriter(configuredPath));
|
|
439
|
+
}
|
|
440
|
+
}
|
|
441
|
+
function resolveClients(workspaceRoot, fabricConfig = {}, opts = {}) {
|
|
442
|
+
const clientPaths = fabricConfig.clientPaths;
|
|
443
|
+
const writers = [];
|
|
444
|
+
const claudeMcpScope = opts.claudeMcpScope ?? "project";
|
|
445
|
+
addIfDetected(
|
|
446
|
+
writers,
|
|
447
|
+
existsSync4(join4(homedir4(), ".claude")) || existsSync4(join4(workspaceRoot, ".claude")),
|
|
448
|
+
(configuredPath) => new ClaudeCodeCLIWriter(configuredPath, claudeMcpScope),
|
|
449
|
+
hasExplicitPath(clientPaths, "claudeCodeCLI") ? clientPaths.claudeCodeCLI : void 0
|
|
450
|
+
);
|
|
451
|
+
addIfDetected(
|
|
452
|
+
writers,
|
|
453
|
+
existsSync4(getClaudeDesktopConfigPath()),
|
|
454
|
+
(configuredPath) => new ClaudeCodeDesktopWriter(configuredPath),
|
|
455
|
+
hasExplicitPath(clientPaths, "claudeCodeDesktop") ? clientPaths.claudeCodeDesktop : void 0
|
|
456
|
+
);
|
|
457
|
+
addIfDetected(
|
|
458
|
+
writers,
|
|
459
|
+
existsSync4(join4(workspaceRoot, ".cursor")),
|
|
460
|
+
(configuredPath) => new CursorWriter(configuredPath),
|
|
461
|
+
hasExplicitPath(clientPaths, "cursor") ? clientPaths.cursor : void 0
|
|
462
|
+
);
|
|
463
|
+
addIfDetected(
|
|
464
|
+
writers,
|
|
465
|
+
existsSync4(join4(homedir4(), ".codex")),
|
|
466
|
+
(configuredPath) => new CodexTOMLConfigWriter(configuredPath),
|
|
467
|
+
hasExplicitPath(clientPaths, "codexCLI") ? clientPaths.codexCLI : void 0
|
|
468
|
+
);
|
|
469
|
+
return writers;
|
|
470
|
+
}
|
|
471
|
+
function detectClientSupports(workspaceRoot, fabricConfig = {}) {
|
|
472
|
+
const clientPaths = fabricConfig.clientPaths;
|
|
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"));
|
|
477
|
+
return [
|
|
478
|
+
{
|
|
479
|
+
clientKind: "ClaudeCodeCLI",
|
|
480
|
+
label: "Claude Code CLI",
|
|
481
|
+
detected: claudeDetected || hasExplicitPath(clientPaths, "claudeCodeCLI"),
|
|
482
|
+
configPath: "project .claude/settings.json",
|
|
483
|
+
capabilities: {
|
|
484
|
+
bootstrap: true,
|
|
485
|
+
mcp: true,
|
|
486
|
+
hook: true,
|
|
487
|
+
skill: true
|
|
488
|
+
},
|
|
489
|
+
installedCapabilities: {
|
|
490
|
+
hook: true,
|
|
491
|
+
skill: true
|
|
492
|
+
}
|
|
493
|
+
},
|
|
494
|
+
{
|
|
495
|
+
clientKind: "ClaudeCodeDesktop",
|
|
496
|
+
label: "Claude Code Desktop",
|
|
497
|
+
detected: claudeDesktopDetected || hasExplicitPath(clientPaths, "claudeCodeDesktop"),
|
|
498
|
+
configPath: "desktop Claude config",
|
|
499
|
+
capabilities: {
|
|
500
|
+
bootstrap: true,
|
|
501
|
+
mcp: true,
|
|
502
|
+
hook: false,
|
|
503
|
+
skill: false
|
|
504
|
+
}
|
|
505
|
+
},
|
|
506
|
+
{
|
|
507
|
+
clientKind: "Cursor",
|
|
508
|
+
label: "Cursor",
|
|
509
|
+
detected: cursorDetected || hasExplicitPath(clientPaths, "cursor"),
|
|
510
|
+
configPath: ".cursor/mcp.json",
|
|
511
|
+
capabilities: {
|
|
512
|
+
bootstrap: true,
|
|
513
|
+
mcp: true,
|
|
514
|
+
hook: false,
|
|
515
|
+
skill: false
|
|
516
|
+
}
|
|
517
|
+
},
|
|
518
|
+
{
|
|
519
|
+
clientKind: "CodexCLI",
|
|
520
|
+
label: "Codex CLI",
|
|
521
|
+
detected: codexDetected || hasExplicitPath(clientPaths, "codexCLI"),
|
|
522
|
+
configPath: "~/.codex/config.toml",
|
|
523
|
+
capabilities: {
|
|
524
|
+
bootstrap: true,
|
|
525
|
+
mcp: true,
|
|
526
|
+
hook: true,
|
|
527
|
+
skill: true
|
|
528
|
+
},
|
|
529
|
+
installedCapabilities: {
|
|
530
|
+
hook: existsSync4(join4(workspaceRoot, ".codex", "hooks.json")),
|
|
531
|
+
// v2/rc.2: v1 client-side init skill removed; skill-installation probes
|
|
532
|
+
// will return once rc.2/3/4 introduce the v2 skills (fabric-archive,
|
|
533
|
+
// fabric-review, fabric-import). Until then there is nothing to probe.
|
|
534
|
+
skill: false
|
|
535
|
+
}
|
|
536
|
+
}
|
|
537
|
+
];
|
|
538
|
+
}
|
|
539
|
+
|
|
540
|
+
export {
|
|
541
|
+
deepMerge,
|
|
542
|
+
resolveClients,
|
|
543
|
+
detectClientSupports
|
|
544
|
+
};
|