@fenglimg/fabric-cli 1.0.0 → 1.2.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.
package/README.md ADDED
@@ -0,0 +1,24 @@
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
+ `fabric init` auto-runs `bootstrap install`, `config install`, and `hooks install`. Use them standalone only for targeted re-runs.
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`
@@ -0,0 +1,14 @@
1
+ #!/usr/bin/env node
2
+ import {
3
+ bootstrapCommand,
4
+ bootstrap_default,
5
+ installBootstrap
6
+ } from "./chunk-RUQCZA2Q.js";
7
+ import "./chunk-VMYPJPKV.js";
8
+ import "./chunk-AEOYCVBG.js";
9
+ import "./chunk-6ICJICVU.js";
10
+ export {
11
+ bootstrapCommand,
12
+ bootstrap_default as default,
13
+ installBootstrap
14
+ };
@@ -1,7 +1,4 @@
1
1
  #!/usr/bin/env node
2
- import {
3
- resolveIgnores
4
- } from "./chunk-P4KVFB2T.js";
5
2
  import {
6
3
  t
7
4
  } from "./chunk-6ICJICVU.js";
@@ -10,6 +7,11 @@ import {
10
7
  import { createHash } from "crypto";
11
8
  import { existsSync, mkdirSync, readdirSync, readFileSync, statSync, writeFileSync } from "fs";
12
9
  import { isAbsolute, join, relative, resolve, sep } from "path";
10
+ import {
11
+ agentsMetaSchema,
12
+ deriveAgentsMetaLayer,
13
+ deriveAgentsMetaTopologyType
14
+ } from "@fenglimg/fabric-shared";
13
15
  import { defineCommand } from "citty";
14
16
  var syncMetaCommand = defineCommand({
15
17
  meta: {
@@ -55,11 +57,15 @@ function computeAgentsMeta(target) {
55
57
  const metaPath = join(target, ".fabric", "agents.meta.json");
56
58
  const existingMeta = readExistingMeta(metaPath);
57
59
  const existingByFile = indexExistingNodesByFile(existingMeta);
58
- const agentsFiles = findAgentsFiles(target);
60
+ const agentsFiles = findFabricAgentsFiles(target);
59
61
  const nodes = {};
62
+ const bootstrapNode = createBootstrapNode(target, existingByFile.get("AGENTS.md")?.node);
63
+ if (bootstrapNode !== void 0) {
64
+ nodes.L0 = bootstrapNode;
65
+ }
60
66
  for (const file of agentsFiles) {
61
67
  const existing = existingByFile.get(file);
62
- const id = existing?.id ?? deriveNodeId(file);
68
+ const id = deriveNodeId(file);
63
69
  const hash = sha256(readFileSync(join(target, file), "utf8"));
64
70
  const defaults = createDefaultNodeMeta(file);
65
71
  nodes[id] = {
@@ -88,15 +94,18 @@ function readExistingMeta(metaPath) {
88
94
  return void 0;
89
95
  }
90
96
  try {
91
- return JSON.parse(readFileSync(metaPath, "utf8"));
97
+ return agentsMetaSchema.parse(JSON.parse(readFileSync(metaPath, "utf8")));
92
98
  } catch {
93
99
  return void 0;
94
100
  }
95
101
  }
96
- function findAgentsFiles(target) {
97
- const ignorePatterns = resolveIgnores();
102
+ function findFabricAgentsFiles(target) {
103
+ const agentsRoot = join(target, ".fabric", "agents");
104
+ if (!existsSync(agentsRoot) || !statSync(agentsRoot).isDirectory()) {
105
+ return [];
106
+ }
98
107
  const files = [];
99
- const stack = [target];
108
+ const stack = [agentsRoot];
100
109
  while (stack.length > 0) {
101
110
  const current = stack.pop();
102
111
  if (current === void 0) {
@@ -105,31 +114,20 @@ function findAgentsFiles(target) {
105
114
  for (const entry of readdirSync(current, { withFileTypes: true })) {
106
115
  const absolutePath = join(current, entry.name);
107
116
  const relativePath = toPosixPath(relative(target, absolutePath));
108
- if (shouldIgnore(relativePath, entry.isDirectory(), ignorePatterns)) {
109
- continue;
110
- }
111
117
  if (entry.isDirectory()) {
112
118
  stack.push(absolutePath);
113
- } else if (entry.isFile() && entry.name === "AGENTS.md") {
119
+ } else if (entry.isFile() && entry.name.endsWith(".md")) {
114
120
  files.push(relativePath);
115
121
  }
116
122
  }
117
123
  }
118
124
  return files.sort();
119
125
  }
120
- function shouldIgnore(relativePath, isDirectory, ignorePatterns) {
121
- return ignorePatterns.some((pattern) => matchesIgnorePattern(relativePath, isDirectory, pattern));
126
+ function deriveLayer(relativePath) {
127
+ return deriveAgentsMetaLayer(relativePath);
122
128
  }
123
- function matchesIgnorePattern(relativePath, isDirectory, pattern) {
124
- const normalizedPattern = toPosixPath(pattern);
125
- if (normalizedPattern === "**/*.meta") {
126
- return relativePath.endsWith(".meta");
127
- }
128
- if (normalizedPattern.endsWith("/**")) {
129
- const directoryPrefix = normalizedPattern.slice(0, -3);
130
- return relativePath === directoryPrefix || relativePath.startsWith(`${directoryPrefix}/`) || isDirectory && `${relativePath}/` === directoryPrefix;
131
- }
132
- return relativePath === normalizedPattern;
129
+ function deriveTopologyType(relativePath) {
130
+ return deriveAgentsMetaTopologyType(relativePath);
133
131
  }
134
132
  function indexExistingNodesByFile(existingMeta) {
135
133
  const byFile = /* @__PURE__ */ new Map();
@@ -142,18 +140,57 @@ function deriveNodeId(file) {
142
140
  if (file === "AGENTS.md") {
143
141
  return "L0";
144
142
  }
145
- return file.replace(/\/AGENTS\.md$/, "");
143
+ const layer = deriveLayer(file);
144
+ const relativeStem = getMirrorRelativeStem(file);
145
+ return `${layer}/${relativeStem}`;
146
146
  }
147
147
  function createDefaultNodeMeta(file) {
148
- const scope = file === "AGENTS.md" ? "**" : `${file.replace(/\/AGENTS\.md$/, "")}/**`;
148
+ const layer = deriveLayer(file);
149
+ const topologyType = deriveTopologyType(file);
149
150
  return {
150
151
  file,
151
- scope_glob: scope,
152
- deps: file === "AGENTS.md" ? [] : ["L0"],
153
- priority: file === "AGENTS.md" ? "high" : "medium",
152
+ scope_glob: deriveScopeGlob(file),
153
+ deps: layer === "L0" ? [] : ["L0"],
154
+ priority: layer === "L0" ? "high" : "medium",
155
+ layer,
156
+ topology_type: topologyType,
154
157
  hash: ""
155
158
  };
156
159
  }
160
+ function createBootstrapNode(target, existing) {
161
+ const bootstrapPath = join(target, "AGENTS.md");
162
+ if (!existsSync(bootstrapPath)) {
163
+ return void 0;
164
+ }
165
+ const hash = sha256(readFileSync(bootstrapPath, "utf8"));
166
+ return {
167
+ ...createDefaultNodeMeta("AGENTS.md"),
168
+ ...existing,
169
+ file: "AGENTS.md",
170
+ hash
171
+ };
172
+ }
173
+ function deriveScopeGlob(file) {
174
+ if (file === "AGENTS.md") {
175
+ return "**";
176
+ }
177
+ const stem = getMirrorRelativeStem(file);
178
+ const segments = stem.split("/").filter(Boolean);
179
+ if (segments.length === 0 || stem === "root") {
180
+ return "**";
181
+ }
182
+ if (segments[0] === "_cross") {
183
+ return "**";
184
+ }
185
+ if (segments.at(-1) === "rules") {
186
+ segments.pop();
187
+ }
188
+ const scopePath = segments.join("/");
189
+ return scopePath === "" ? "**" : `${scopePath}/**`;
190
+ }
191
+ function getMirrorRelativeStem(file) {
192
+ return file.replace(/^\.fabric\/agents\//, "").replace(/\.md$/, "");
193
+ }
157
194
  function sortNodes(nodes) {
158
195
  return Object.fromEntries(Object.entries(nodes).sort(([left], [right]) => left.localeCompare(right)));
159
196
  }
@@ -187,5 +224,7 @@ function sha256(content) {
187
224
  export {
188
225
  syncMetaCommand,
189
226
  sync_meta_default,
190
- computeAgentsMeta
227
+ computeAgentsMeta,
228
+ deriveLayer,
229
+ deriveTopologyType
191
230
  };
@@ -13,6 +13,7 @@ import { resolve } from "path";
13
13
  import { fileURLToPath } from "url";
14
14
  import { defineCommand } from "citty";
15
15
  var CLIENT_ALIASES = {
16
+ claude: "ClaudeCodeCLI",
16
17
  claudecodecli: "ClaudeCodeCLI",
17
18
  "claude-code-cli": "ClaudeCodeCLI",
18
19
  claudecli: "ClaudeCodeCLI",
@@ -57,7 +58,8 @@ async function loadFabricConfig(workspaceRoot) {
57
58
  }
58
59
  return parsed;
59
60
  }
60
- function resolveServerPath() {
61
+ function resolveServerPath(override) {
62
+ if (override) return override;
61
63
  if (process.env.FAB_SERVER_PATH) return resolve(process.env.FAB_SERVER_PATH);
62
64
  return fileURLToPath(import.meta.resolve("@fenglimg/fabric-server"));
63
65
  }
@@ -88,36 +90,66 @@ var configCmd = defineCommand({
88
90
  }
89
91
  },
90
92
  async run({ args }) {
91
- const workspaceRoot = process.cwd();
92
- const fabricConfig = await loadFabricConfig(workspaceRoot);
93
93
  const selectedClients = parseClientFilter(args.clients);
94
- const serverPath = resolveServerPath();
95
- const writers = resolveClients(workspaceRoot, fabricConfig).filter(
96
- (writer) => selectedClients === null ? true : selectedClients.has(writer.clientKind)
97
- );
98
- if (writers.length === 0) {
94
+ const result = await installMcpClients(process.cwd(), {
95
+ clients: selectedClients === null ? void 0 : Array.from(selectedClients),
96
+ dryRun: args["dry-run"]
97
+ });
98
+ if (result.details.length === 0) {
99
99
  writeStderr(t("cli.config.install.no-configs"));
100
100
  return;
101
101
  }
102
- for (const writer of writers) {
103
- const configPath = await writer.detect(workspaceRoot);
104
- if (configPath === null) {
105
- writeStderr(t("cli.config.install.no-config-path", { client: writer.clientKind }));
102
+ for (const detail of result.details) {
103
+ if (detail.action === "skipped") {
104
+ writeStderr(t("cli.config.install.no-config-path", { client: detail.client }));
106
105
  continue;
107
106
  }
108
- if (args["dry-run"]) {
109
- writeStderr(t("cli.config.install.dry-run", { client: writer.clientKind, path: configPath }));
107
+ if (detail.action === "dry-run" && detail.path !== null) {
108
+ writeStderr(t("cli.config.install.dry-run", { client: detail.client, path: detail.path }));
110
109
  continue;
111
110
  }
112
- await writer.write(serverPath, workspaceRoot);
113
- writeStderr(t("cli.config.install.wrote", { client: writer.clientKind, path: configPath }));
111
+ if (detail.path !== null) {
112
+ writeStderr(t("cli.config.install.wrote", { client: detail.client, path: detail.path }));
113
+ }
114
114
  }
115
115
  }
116
116
  })
117
117
  }
118
118
  });
119
119
  var config_default = configCmd;
120
+ async function installMcpClients(target, options = {}) {
121
+ const workspaceRoot = resolve(target);
122
+ const fabricConfig = await loadFabricConfig(workspaceRoot);
123
+ const selectedClients = options.clients === void 0 ? null : new Set(options.clients);
124
+ const serverPath = resolveServerPath(options.localServerPath);
125
+ const writers = resolveClients(workspaceRoot, fabricConfig).filter(
126
+ (writer) => selectedClients === null ? true : selectedClients.has(writer.clientKind)
127
+ );
128
+ const installed = [];
129
+ const skipped = [];
130
+ const details = [];
131
+ for (const writer of writers) {
132
+ const configPath = await writer.detect(workspaceRoot);
133
+ if (configPath === null) {
134
+ skipped.push(writer.clientKind);
135
+ details.push({ client: writer.clientKind, path: null, action: "skipped" });
136
+ continue;
137
+ }
138
+ if (options.dryRun) {
139
+ skipped.push(writer.clientKind);
140
+ details.push({ client: writer.clientKind, path: configPath, action: "dry-run" });
141
+ continue;
142
+ }
143
+ await writer.write(serverPath, workspaceRoot);
144
+ installed.push(writer.clientKind);
145
+ details.push({ client: writer.clientKind, path: configPath, action: "wrote" });
146
+ }
147
+ return { installed, skipped, details };
148
+ }
149
+
120
150
  export {
151
+ parseClientFilter,
121
152
  configCmd,
122
- config_default as default
153
+ config_default,
154
+ installMcpClients
123
155
  };
@@ -1,18 +1,15 @@
1
1
  #!/usr/bin/env node
2
- import {
3
- createDebugLogger,
4
- readFabricConfig,
5
- resolveDevMode
6
- } from "./chunk-AEOYCVBG.js";
7
- import {
8
- resolveIgnores
9
- } from "./chunk-P4KVFB2T.js";
10
2
  import {
11
3
  displayWidth,
12
4
  padEnd,
13
5
  paint,
14
6
  symbol
15
7
  } from "./chunk-WWNXR34K.js";
8
+ import {
9
+ createDebugLogger,
10
+ readFabricConfig,
11
+ resolveDevMode
12
+ } from "./chunk-AEOYCVBG.js";
16
13
  import {
17
14
  t
18
15
  } from "./chunk-6ICJICVU.js";
@@ -23,7 +20,24 @@ import { isAbsolute, join, relative, resolve, sep } from "path";
23
20
  import { defineCommand } from "citty";
24
21
 
25
22
  // src/scanner/detector.ts
26
- import { detectFramework } from "@fenglimg/fabric-shared";
23
+ import { detectFramework } from "@fenglimg/fabric-shared/node";
24
+
25
+ // src/scanner/ignores.ts
26
+ var DEFAULT_IGNORES = [
27
+ "**/*.meta",
28
+ "library/**",
29
+ "temp/**",
30
+ "build/**",
31
+ "settings/**",
32
+ "profiles/**",
33
+ "node_modules/**",
34
+ "dist/**",
35
+ ".git/**",
36
+ ".fabric/**"
37
+ ];
38
+ function resolveIgnores(fabricConfig) {
39
+ return [...DEFAULT_IGNORES, ...fabricConfig?.scanIgnores ?? []];
40
+ }
27
41
 
28
42
  // src/commands/scan.ts
29
43
  function createScanReport(targetInput = process.cwd(), fabricConfig) {
@@ -70,24 +70,53 @@ var bootstrapCommand = defineCommand({
70
70
  async run({ args }) {
71
71
  const workspaceRoot = process.cwd();
72
72
  const selectedClients = parseClientFilter(args.clients);
73
- const fabricConfig = readFabricConfig(workspaceRoot);
74
- const detectedClients = detectBootstrapClients(workspaceRoot, fabricConfig);
75
- const clients = selectedClients ?? detectedClients;
76
- if (clients.size === 0) {
73
+ const result = await installBootstrap(workspaceRoot, {
74
+ clients: selectedClients === null ? void 0 : Array.from(selectedClients, mapBootstrapClientToClientKind)
75
+ });
76
+ if (result.details.length === 0) {
77
77
  process.stderr.write(
78
78
  `${t("cli.bootstrap.install.no-targets")}
79
79
  `
80
80
  );
81
81
  return;
82
82
  }
83
- for (const client of clients) {
84
- installBootstrap(client, workspaceRoot);
83
+ for (const detail of result.details) {
84
+ if (detail.action === "skipped") {
85
+ process.stderr.write(`${t("cli.bootstrap.install.skipped-header", { path: detail.path })}
86
+ `);
87
+ continue;
88
+ }
89
+ if (detail.action === "prepended") {
90
+ process.stderr.write(`${t("cli.bootstrap.install.prepended", { path: detail.path })}
91
+ `);
92
+ continue;
93
+ }
94
+ process.stderr.write(`${t("cli.bootstrap.install.installed", { path: detail.path })}
95
+ `);
85
96
  }
86
97
  }
87
98
  })
88
99
  }
89
100
  });
90
101
  var bootstrap_default = bootstrapCommand;
102
+ async function installBootstrap(target, options = {}) {
103
+ const workspaceRoot = resolve(target);
104
+ const fabricConfig = readFabricConfig(workspaceRoot);
105
+ const targets = resolveBootstrapTargets(workspaceRoot, fabricConfig, options.clients);
106
+ const installed = [];
107
+ const skipped = [];
108
+ const details = [];
109
+ 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
+ }
117
+ }
118
+ return { installed, skipped, details };
119
+ }
91
120
  function parseClientFilter(value) {
92
121
  if (value === void 0 || value.trim().length === 0) {
93
122
  return null;
@@ -103,15 +132,19 @@ function parseClientFilter(value) {
103
132
  }
104
133
  return clients;
105
134
  }
106
- function detectBootstrapClients(workspaceRoot, fabricConfig) {
107
- const clients = /* @__PURE__ */ new Set();
108
- for (const writer of resolveClients(workspaceRoot, fabricConfig)) {
109
- const bootstrapClient = mapClientKind(writer.clientKind);
110
- if (bootstrapClient !== null) {
111
- clients.add(bootstrapClient);
135
+ function resolveBootstrapTargets(workspaceRoot, fabricConfig, selectedClients) {
136
+ const targets = [];
137
+ const seenClients = /* @__PURE__ */ new Set();
138
+ const clientKinds = selectedClients ?? resolveClients(workspaceRoot, fabricConfig).map((writer) => writer.clientKind);
139
+ for (const clientKind of clientKinds) {
140
+ const bootstrapClient = mapClientKind(clientKind);
141
+ if (bootstrapClient === null || seenClients.has(bootstrapClient)) {
142
+ continue;
112
143
  }
144
+ seenClients.add(bootstrapClient);
145
+ targets.push({ client: clientKind, bootstrapClient });
113
146
  }
114
- return clients;
147
+ return targets;
115
148
  }
116
149
  function mapClientKind(clientKind) {
117
150
  switch (clientKind) {
@@ -132,37 +165,79 @@ function mapClientKind(clientKind) {
132
165
  return null;
133
166
  }
134
167
  }
135
- function installBootstrap(client, workspaceRoot) {
136
- const targetPath = resolve(workspaceRoot, CLIENT_TARGET_MAP[client]);
137
- const templatePath = findTemplatePath(CLIENT_TEMPLATE_MAP[client]);
168
+ function mapBootstrapClientToClientKind(client) {
169
+ switch (client) {
170
+ case "claude":
171
+ return "ClaudeCodeCLI";
172
+ case "cursor":
173
+ return "Cursor";
174
+ case "windsurf":
175
+ return "Windsurf";
176
+ case "roo":
177
+ return "RooCode";
178
+ case "gemini":
179
+ return "GeminiCLI";
180
+ case "codex":
181
+ return "CodexCLI";
182
+ }
183
+ }
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]);
138
187
  const template = readFileSync(templatePath, "utf8");
139
188
  mkdirSync(dirname(targetPath), { recursive: true });
140
- if (client === "codex") {
141
- writeCodexBootstrap(targetPath, template);
142
- return;
189
+ if (target.bootstrapClient === "codex") {
190
+ return {
191
+ client: target.client,
192
+ path: targetPath,
193
+ action: writeCodexBootstrap(targetPath, template, options.force)
194
+ };
143
195
  }
196
+ const existed = existsSync(targetPath);
144
197
  writeFileSync(targetPath, ensureTrailingNewline(template), "utf8");
145
- process.stderr.write(`${t("cli.bootstrap.install.installed", { path: targetPath })}
146
- `);
198
+ return {
199
+ client: target.client,
200
+ path: targetPath,
201
+ action: existed ? "overwritten" : "installed"
202
+ };
147
203
  }
148
- function writeCodexBootstrap(targetPath, template) {
204
+ function writeCodexBootstrap(targetPath, template, force) {
149
205
  const nextContent = ensureTrailingNewline(template);
150
206
  if (!existsSync(targetPath)) {
151
207
  writeFileSync(targetPath, nextContent, "utf8");
152
- process.stderr.write(`${t("cli.bootstrap.install.installed", { path: targetPath })}
153
- `);
154
- return;
208
+ return "installed";
155
209
  }
156
210
  const existing = readFileSync(targetPath, "utf8");
157
211
  if (existing.includes("# Fabric Bootstrap")) {
158
- process.stderr.write(`${t("cli.bootstrap.install.skipped-header", { path: targetPath })}
159
- `);
160
- return;
212
+ if (!force) {
213
+ return "skipped";
214
+ }
215
+ const remainder = stripExistingCodexBootstrap(existing, nextContent);
216
+ writeFileSync(targetPath, joinBootstrapSections(nextContent, remainder), "utf8");
217
+ return "overwritten";
161
218
  }
162
- const separator = existing.startsWith("\n") || existing.length === 0 ? "" : "\n";
163
- writeFileSync(targetPath, `${nextContent}${separator}${existing}`, "utf8");
164
- process.stderr.write(`${t("cli.bootstrap.install.prepended", { path: targetPath })}
165
- `);
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}`;
166
241
  }
167
242
  function ensureTrailingNewline(content) {
168
243
  return content.endsWith("\n") ? content : `${content}
@@ -194,7 +269,9 @@ function templateCandidatesFrom(start, relativePath) {
194
269
  }
195
270
  return candidates.reverse();
196
271
  }
272
+
197
273
  export {
198
274
  bootstrapCommand,
199
- bootstrap_default as default
275
+ bootstrap_default,
276
+ installBootstrap
200
277
  };