@fenglimg/fabric-cli 0.1.0

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.
@@ -0,0 +1,293 @@
1
+ // src/config/resolver.ts
2
+ import { existsSync as existsSync4 } from "fs";
3
+ import { join as join4 } from "path";
4
+ import { homedir as homedir4 } from "os";
5
+
6
+ // src/config/claude-code.ts
7
+ import { existsSync as existsSync2 } from "fs";
8
+ import { join as join2, resolve as resolve2 } from "path";
9
+ import { homedir as homedir2, platform } from "os";
10
+
11
+ // src/config/json.ts
12
+ import { existsSync } from "fs";
13
+ import { mkdir, readFile, writeFile } from "fs/promises";
14
+ import { dirname, join, resolve } from "path";
15
+ import { homedir } from "os";
16
+
17
+ // src/config/writer.ts
18
+ function createServerEntry(serverPath) {
19
+ return {
20
+ command: process.execPath,
21
+ args: [serverPath]
22
+ };
23
+ }
24
+
25
+ // src/config/json.ts
26
+ function expandHome(filePath) {
27
+ if (filePath === "~") {
28
+ return homedir();
29
+ }
30
+ if (filePath.startsWith("~/")) {
31
+ return join(homedir(), filePath.slice(2));
32
+ }
33
+ return filePath;
34
+ }
35
+ function normalizeConfigPath(filePath) {
36
+ return resolve(expandHome(filePath));
37
+ }
38
+ async function readJsonConfig(configPath) {
39
+ try {
40
+ const raw = await readFile(configPath, "utf8");
41
+ if (raw.trim().length === 0) {
42
+ return {};
43
+ }
44
+ const parsed = JSON.parse(raw);
45
+ if (parsed === null || typeof parsed !== "object" || Array.isArray(parsed)) {
46
+ throw new Error(`Expected JSON object in ${configPath}`);
47
+ }
48
+ return parsed;
49
+ } catch (error) {
50
+ if (error instanceof Error && "code" in error && error.code === "ENOENT") {
51
+ return {};
52
+ }
53
+ throw error;
54
+ }
55
+ }
56
+ async function writeJsonClientConfig(configPath, serverEntry) {
57
+ const config = await readJsonConfig(configPath);
58
+ const existingServers = config.mcpServers;
59
+ config.mcpServers = existingServers !== null && typeof existingServers === "object" && !Array.isArray(existingServers) ? { ...existingServers, fabric: serverEntry } : { fabric: serverEntry };
60
+ await mkdir(dirname(configPath), { recursive: true });
61
+ await writeFile(configPath, `${JSON.stringify(config, null, 2)}
62
+ `, "utf8");
63
+ }
64
+ var JsonClientConfigWriter = class {
65
+ configuredPath;
66
+ constructor(configuredPath) {
67
+ this.configuredPath = configuredPath;
68
+ }
69
+ async detect(workspaceRoot, overridePath) {
70
+ const explicitPath = overridePath ?? this.configuredPath;
71
+ if (explicitPath !== void 0) {
72
+ return normalizeConfigPath(explicitPath);
73
+ }
74
+ const configPath = this.defaultPath(workspaceRoot);
75
+ return configPath === null ? null : normalizeConfigPath(configPath);
76
+ }
77
+ async write(serverPath, workspaceRoot, overridePath) {
78
+ const configPath = await this.detect(workspaceRoot, overridePath);
79
+ if (configPath === null) {
80
+ return;
81
+ }
82
+ await writeJsonClientConfig(configPath, createServerEntry(serverPath));
83
+ }
84
+ };
85
+ var ClaudeCodeCLIWriter = class extends JsonClientConfigWriter {
86
+ clientKind = "ClaudeCodeCLI";
87
+ constructor(configuredPath) {
88
+ super(configuredPath);
89
+ }
90
+ defaultPath() {
91
+ const claudeDir = join(homedir(), ".claude");
92
+ return existsSync(claudeDir) ? join(claudeDir, "settings.json") : null;
93
+ }
94
+ };
95
+ var CursorWriter = class extends JsonClientConfigWriter {
96
+ clientKind = "Cursor";
97
+ constructor(configuredPath) {
98
+ super(configuredPath);
99
+ }
100
+ defaultPath(workspaceRoot) {
101
+ const cursorDir = join(workspaceRoot, ".cursor");
102
+ return existsSync(cursorDir) ? join(cursorDir, "mcp.json") : null;
103
+ }
104
+ };
105
+ var WindsurfWriter = class extends JsonClientConfigWriter {
106
+ clientKind = "Windsurf";
107
+ constructor(configuredPath) {
108
+ super(configuredPath);
109
+ }
110
+ defaultPath(workspaceRoot) {
111
+ const windsurfDir = join(workspaceRoot, ".windsurf");
112
+ return existsSync(windsurfDir) ? join(windsurfDir, "mcp.json") : null;
113
+ }
114
+ };
115
+ var RooCodeWriter = class extends JsonClientConfigWriter {
116
+ clientKind = "RooCode";
117
+ constructor(configuredPath) {
118
+ super(configuredPath);
119
+ }
120
+ defaultPath(workspaceRoot) {
121
+ const rooDir = join(workspaceRoot, ".roo");
122
+ return existsSync(rooDir) ? join(rooDir, "mcp.json") : null;
123
+ }
124
+ };
125
+ var GeminiCLIWriter = class extends JsonClientConfigWriter {
126
+ clientKind = "GeminiCLI";
127
+ constructor(configuredPath) {
128
+ super(configuredPath);
129
+ }
130
+ defaultPath(workspaceRoot) {
131
+ const geminiDir = join(homedir(), ".gemini");
132
+ return existsSync(geminiDir) || existsSync(join(workspaceRoot, "GEMINI.md")) ? join(geminiDir, "settings.json") : null;
133
+ }
134
+ };
135
+
136
+ // src/config/claude-code.ts
137
+ function getClaudeDesktopConfigPath() {
138
+ const os = platform();
139
+ if (os === "darwin") {
140
+ return join2(homedir2(), "Library", "Application Support", "Claude", "claude_desktop_config.json");
141
+ }
142
+ if (os === "win32") {
143
+ return join2(process.env.APPDATA ?? join2(homedir2(), "AppData", "Roaming"), "Claude", "claude_desktop_config.json");
144
+ }
145
+ return join2(homedir2(), ".config", "Claude", "claude_desktop_config.json");
146
+ }
147
+ var ClaudeCodeDesktopWriter = class {
148
+ clientKind = "ClaudeCodeDesktop";
149
+ configuredPath;
150
+ constructor(configuredPath) {
151
+ this.configuredPath = configuredPath;
152
+ }
153
+ async detect(_workspaceRoot, overridePath) {
154
+ const configPath = normalizeConfigPath(overridePath ?? this.configuredPath ?? getClaudeDesktopConfigPath());
155
+ return existsSync2(configPath) || overridePath !== void 0 || this.configuredPath !== void 0 ? configPath : null;
156
+ }
157
+ async write(serverPath, workspaceRoot, overridePath) {
158
+ const configPath = await this.detect(workspaceRoot, overridePath);
159
+ if (configPath === null) {
160
+ return;
161
+ }
162
+ await writeJsonClientConfig(configPath, {
163
+ command: process.execPath,
164
+ args: [serverPath]
165
+ });
166
+ }
167
+ };
168
+
169
+ // src/config/toml.ts
170
+ import { existsSync as existsSync3 } from "fs";
171
+ import { mkdir as mkdir2, readFile as readFile2, writeFile as writeFile2 } from "fs/promises";
172
+ import { dirname as dirname2, join as join3, resolve as resolve3 } from "path";
173
+ import { homedir as homedir3 } from "os";
174
+ import * as TOML from "@iarna/toml";
175
+ function expandHome2(filePath) {
176
+ if (filePath === "~") {
177
+ return homedir3();
178
+ }
179
+ if (filePath.startsWith("~/")) {
180
+ return join3(homedir3(), filePath.slice(2));
181
+ }
182
+ return filePath;
183
+ }
184
+ function asObject(value) {
185
+ return value !== null && typeof value === "object" && !Array.isArray(value) ? value : {};
186
+ }
187
+ async function readTomlConfig(configPath) {
188
+ try {
189
+ const raw = await readFile2(configPath, "utf8");
190
+ if (raw.trim().length === 0) {
191
+ return {};
192
+ }
193
+ return TOML.parse(raw);
194
+ } catch (error) {
195
+ if (error instanceof Error && "code" in error && error.code === "ENOENT") {
196
+ return {};
197
+ }
198
+ throw error;
199
+ }
200
+ }
201
+ function mergeCodexServer(config, serverEntry) {
202
+ const mcp = asObject(config.mcp);
203
+ const servers = asObject(mcp.servers);
204
+ servers.fabric = serverEntry;
205
+ mcp.servers = servers;
206
+ config.mcp = mcp;
207
+ return config;
208
+ }
209
+ var CodexTOMLConfigWriter = class {
210
+ clientKind = "CodexCLI";
211
+ configuredPath;
212
+ constructor(configuredPath) {
213
+ this.configuredPath = configuredPath;
214
+ }
215
+ async detect(_workspaceRoot, overridePath) {
216
+ const explicitPath = overridePath ?? this.configuredPath;
217
+ if (explicitPath !== void 0) {
218
+ return resolve3(expandHome2(explicitPath));
219
+ }
220
+ const codexDir = join3(homedir3(), ".codex");
221
+ return existsSync3(codexDir) ? resolve3(join3(codexDir, "config.toml")) : null;
222
+ }
223
+ async write(serverPath, workspaceRoot, overridePath) {
224
+ const configPath = await this.detect(workspaceRoot, overridePath);
225
+ if (configPath === null) {
226
+ return;
227
+ }
228
+ const config = mergeCodexServer(await readTomlConfig(configPath), createServerEntry(serverPath));
229
+ await mkdir2(dirname2(configPath), { recursive: true });
230
+ await writeFile2(configPath, TOML.stringify(config), "utf8");
231
+ }
232
+ };
233
+
234
+ // src/config/resolver.ts
235
+ function hasExplicitPath(clientPaths, key) {
236
+ return typeof clientPaths?.[key] === "string" && clientPaths[key].trim().length > 0;
237
+ }
238
+ function addIfDetected(writers, detected, createWriter, configuredPath) {
239
+ if (configuredPath !== void 0 || detected) {
240
+ writers.push(createWriter(configuredPath));
241
+ }
242
+ }
243
+ function resolveClients(workspaceRoot, fabricConfig = {}) {
244
+ const clientPaths = fabricConfig.clientPaths;
245
+ const writers = [];
246
+ addIfDetected(
247
+ writers,
248
+ existsSync4(join4(homedir4(), ".claude")),
249
+ (configuredPath) => new ClaudeCodeCLIWriter(configuredPath),
250
+ hasExplicitPath(clientPaths, "claudeCodeCLI") ? clientPaths.claudeCodeCLI : void 0
251
+ );
252
+ addIfDetected(
253
+ writers,
254
+ existsSync4(getClaudeDesktopConfigPath()),
255
+ (configuredPath) => new ClaudeCodeDesktopWriter(configuredPath),
256
+ hasExplicitPath(clientPaths, "claudeCodeDesktop") ? clientPaths.claudeCodeDesktop : void 0
257
+ );
258
+ addIfDetected(
259
+ writers,
260
+ existsSync4(join4(workspaceRoot, ".cursor")),
261
+ (configuredPath) => new CursorWriter(configuredPath),
262
+ hasExplicitPath(clientPaths, "cursor") ? clientPaths.cursor : void 0
263
+ );
264
+ addIfDetected(
265
+ writers,
266
+ existsSync4(join4(workspaceRoot, ".windsurf")),
267
+ (configuredPath) => new WindsurfWriter(configuredPath),
268
+ hasExplicitPath(clientPaths, "windsurf") ? clientPaths.windsurf : void 0
269
+ );
270
+ addIfDetected(
271
+ writers,
272
+ existsSync4(join4(workspaceRoot, ".roo")),
273
+ (configuredPath) => new RooCodeWriter(configuredPath),
274
+ hasExplicitPath(clientPaths, "rooCode") ? clientPaths.rooCode : void 0
275
+ );
276
+ addIfDetected(
277
+ writers,
278
+ existsSync4(join4(homedir4(), ".gemini")) || existsSync4(join4(workspaceRoot, "GEMINI.md")),
279
+ (configuredPath) => new GeminiCLIWriter(configuredPath),
280
+ hasExplicitPath(clientPaths, "geminiCLI") ? clientPaths.geminiCLI : void 0
281
+ );
282
+ addIfDetected(
283
+ writers,
284
+ existsSync4(join4(homedir4(), ".codex")),
285
+ (configuredPath) => new CodexTOMLConfigWriter(configuredPath),
286
+ hasExplicitPath(clientPaths, "codexCLI") ? clientPaths.codexCLI : void 0
287
+ );
288
+ return writers;
289
+ }
290
+
291
+ export {
292
+ resolveClients
293
+ };
@@ -0,0 +1,187 @@
1
+ import {
2
+ resolveIgnores
3
+ } from "./chunk-ZTNSMODH.js";
4
+
5
+ // src/commands/sync-meta.ts
6
+ import { createHash } from "crypto";
7
+ import { existsSync, mkdirSync, readdirSync, readFileSync, statSync, writeFileSync } from "fs";
8
+ import { isAbsolute, join, relative, resolve, sep } from "path";
9
+ import { defineCommand } from "citty";
10
+ var syncMetaCommand = defineCommand({
11
+ meta: {
12
+ name: "sync-meta",
13
+ description: "Sync Fabric metadata from AGENTS.md files."
14
+ },
15
+ args: {
16
+ target: {
17
+ type: "string",
18
+ description: "Target project path. Defaults to the current working directory.",
19
+ default: process.cwd()
20
+ },
21
+ "check-only": {
22
+ type: "boolean",
23
+ description: "Exit with code 1 if .fabric/agents.meta.json is stale.",
24
+ default: false
25
+ }
26
+ },
27
+ async run({ args }) {
28
+ const target = normalizeTarget(args.target);
29
+ const metaPath = join(target, ".fabric", "agents.meta.json");
30
+ const computedMeta = computeAgentsMeta(target);
31
+ const existingMeta = readExistingMeta(metaPath);
32
+ if (args["check-only"]) {
33
+ if (!existingMeta || stableStringify(existingMeta) !== stableStringify(computedMeta)) {
34
+ writeStderr("Fabric metadata drift detected. Run fab sync-meta to update.");
35
+ process.exitCode = 1;
36
+ }
37
+ return;
38
+ }
39
+ if (existingMeta && stableStringify(existingMeta) === stableStringify(computedMeta)) {
40
+ return;
41
+ }
42
+ mkdirSync(join(target, ".fabric"), { recursive: true });
43
+ writeFileSync(metaPath, `${JSON.stringify(computedMeta, null, 2)}
44
+ `, "utf8");
45
+ writeStderr(`Updated ${metaPath}`);
46
+ }
47
+ });
48
+ var sync_meta_default = syncMetaCommand;
49
+ function computeAgentsMeta(target) {
50
+ assertExistingDirectory(target);
51
+ const metaPath = join(target, ".fabric", "agents.meta.json");
52
+ const existingMeta = readExistingMeta(metaPath);
53
+ const existingByFile = indexExistingNodesByFile(existingMeta);
54
+ const agentsFiles = findAgentsFiles(target);
55
+ const nodes = {};
56
+ for (const file of agentsFiles) {
57
+ const existing = existingByFile.get(file);
58
+ const id = existing?.id ?? deriveNodeId(file);
59
+ const hash = sha256(readFileSync(join(target, file), "utf8"));
60
+ const defaults = createDefaultNodeMeta(file);
61
+ nodes[id] = {
62
+ ...defaults,
63
+ ...existing?.node,
64
+ file,
65
+ hash
66
+ };
67
+ }
68
+ return {
69
+ ...existingMeta ?? {},
70
+ revision: computeRevision(nodes),
71
+ nodes: sortNodes(nodes)
72
+ };
73
+ }
74
+ function normalizeTarget(targetInput) {
75
+ return isAbsolute(targetInput) ? targetInput : resolve(process.cwd(), targetInput);
76
+ }
77
+ function assertExistingDirectory(target) {
78
+ if (!existsSync(target) || !statSync(target).isDirectory()) {
79
+ throw new Error(`Target must be an existing directory: ${target}`);
80
+ }
81
+ }
82
+ function readExistingMeta(metaPath) {
83
+ if (!existsSync(metaPath)) {
84
+ return void 0;
85
+ }
86
+ try {
87
+ return JSON.parse(readFileSync(metaPath, "utf8"));
88
+ } catch {
89
+ return void 0;
90
+ }
91
+ }
92
+ function findAgentsFiles(target) {
93
+ const ignorePatterns = resolveIgnores();
94
+ const files = [];
95
+ const stack = [target];
96
+ while (stack.length > 0) {
97
+ const current = stack.pop();
98
+ if (current === void 0) {
99
+ continue;
100
+ }
101
+ for (const entry of readdirSync(current, { withFileTypes: true })) {
102
+ const absolutePath = join(current, entry.name);
103
+ const relativePath = toPosixPath(relative(target, absolutePath));
104
+ if (shouldIgnore(relativePath, entry.isDirectory(), ignorePatterns)) {
105
+ continue;
106
+ }
107
+ if (entry.isDirectory()) {
108
+ stack.push(absolutePath);
109
+ } else if (entry.isFile() && entry.name === "AGENTS.md") {
110
+ files.push(relativePath);
111
+ }
112
+ }
113
+ }
114
+ return files.sort();
115
+ }
116
+ function shouldIgnore(relativePath, isDirectory, ignorePatterns) {
117
+ return ignorePatterns.some((pattern) => matchesIgnorePattern(relativePath, isDirectory, pattern));
118
+ }
119
+ function matchesIgnorePattern(relativePath, isDirectory, pattern) {
120
+ const normalizedPattern = toPosixPath(pattern);
121
+ if (normalizedPattern === "**/*.meta") {
122
+ return relativePath.endsWith(".meta");
123
+ }
124
+ if (normalizedPattern.endsWith("/**")) {
125
+ const directoryPrefix = normalizedPattern.slice(0, -3);
126
+ return relativePath === directoryPrefix || relativePath.startsWith(`${directoryPrefix}/`) || isDirectory && `${relativePath}/` === directoryPrefix;
127
+ }
128
+ return relativePath === normalizedPattern;
129
+ }
130
+ function indexExistingNodesByFile(existingMeta) {
131
+ const byFile = /* @__PURE__ */ new Map();
132
+ for (const [id, node] of Object.entries(existingMeta?.nodes ?? {})) {
133
+ byFile.set(toPosixPath(node.file), { id, node });
134
+ }
135
+ return byFile;
136
+ }
137
+ function deriveNodeId(file) {
138
+ if (file === "AGENTS.md") {
139
+ return "L0";
140
+ }
141
+ return file.replace(/\/AGENTS\.md$/, "");
142
+ }
143
+ function createDefaultNodeMeta(file) {
144
+ const scope = file === "AGENTS.md" ? "**" : `${file.replace(/\/AGENTS\.md$/, "")}/**`;
145
+ return {
146
+ file,
147
+ scope_glob: scope,
148
+ deps: file === "AGENTS.md" ? [] : ["L0"],
149
+ priority: file === "AGENTS.md" ? "high" : "medium",
150
+ hash: ""
151
+ };
152
+ }
153
+ function sortNodes(nodes) {
154
+ return Object.fromEntries(Object.entries(nodes).sort(([left], [right]) => left.localeCompare(right)));
155
+ }
156
+ function computeRevision(nodes) {
157
+ const hashes = Object.values(sortNodes(nodes)).map((node) => node.hash).join("");
158
+ return sha256(hashes);
159
+ }
160
+ function writeStderr(message) {
161
+ process.stderr.write(`${message}
162
+ `);
163
+ }
164
+ function stableStringify(value) {
165
+ return JSON.stringify(value, Object.keys(flattenKeys(value)).sort());
166
+ }
167
+ function flattenKeys(value, keys = {}) {
168
+ if (value && typeof value === "object") {
169
+ for (const [key, child] of Object.entries(value)) {
170
+ keys[key] = true;
171
+ flattenKeys(child, keys);
172
+ }
173
+ }
174
+ return keys;
175
+ }
176
+ function toPosixPath(path) {
177
+ return path.split(sep).join("/");
178
+ }
179
+ function sha256(content) {
180
+ return `sha256:${createHash("sha256").update(content).digest("hex")}`;
181
+ }
182
+
183
+ export {
184
+ syncMetaCommand,
185
+ sync_meta_default,
186
+ computeAgentsMeta
187
+ };