@fenglimg/fabric-cli 1.1.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
+ };
@@ -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
  };
@@ -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
  };
@@ -0,0 +1,155 @@
1
+ #!/usr/bin/env node
2
+ import {
3
+ t
4
+ } from "./chunk-6ICJICVU.js";
5
+
6
+ // src/commands/hooks.ts
7
+ import { chmodSync, existsSync, mkdirSync, readFileSync, statSync, writeFileSync } from "fs";
8
+ import { dirname, isAbsolute, join, parse, resolve } from "path";
9
+ import { fileURLToPath } from "url";
10
+ import { defineCommand } from "citty";
11
+ var hooksCommand = defineCommand({
12
+ meta: {
13
+ name: "hooks",
14
+ description: t("cli.hooks.description")
15
+ },
16
+ subCommands: {
17
+ install: defineCommand({
18
+ meta: {
19
+ name: "install",
20
+ description: t("cli.hooks.install.description")
21
+ },
22
+ args: {
23
+ target: {
24
+ type: "string",
25
+ description: t("cli.hooks.install.args.target.description"),
26
+ default: process.cwd()
27
+ }
28
+ },
29
+ async run({ args }) {
30
+ const result = await installHooks(args.target);
31
+ if (result.hookAction === "skipped") {
32
+ writeStderr(t("cli.hooks.install.hook-skipped", { path: result.hookPath }));
33
+ } else if (result.hookAction === "appended") {
34
+ writeStderr(t("cli.hooks.install.hook-appended", { path: result.hookPath }));
35
+ } else {
36
+ writeStderr(t("cli.hooks.install.hook-created", { path: result.hookPath }));
37
+ }
38
+ if (result.prepareAction === "left") {
39
+ writeStderr(t("cli.hooks.install.prepare-left", { path: result.packageJsonPath }));
40
+ } else {
41
+ writeStderr(t("cli.hooks.install.prepare-added", { path: result.packageJsonPath }));
42
+ }
43
+ }
44
+ })
45
+ }
46
+ });
47
+ var hooks_default = hooksCommand;
48
+ async function installHooks(target, options = {}) {
49
+ const normalizedTarget = normalizeTarget(target);
50
+ assertExistingDirectory(normalizedTarget);
51
+ const huskyDir = join(normalizedTarget, ".husky");
52
+ const hookPath = join(huskyDir, "pre-commit");
53
+ const packageJsonPath = join(normalizedTarget, "package.json");
54
+ if (!existsSync(packageJsonPath)) {
55
+ throw new Error(t("cli.hooks.errors.package-json-required", { path: packageJsonPath }));
56
+ }
57
+ mkdirSync(huskyDir, { recursive: true });
58
+ const templateContent = readFileSync(findTemplatePath("templates/husky/pre-commit"), "utf8");
59
+ const hookAction = installHookFile(hookPath, templateContent, options.force);
60
+ chmodSync(hookPath, 493);
61
+ const packageJson = JSON.parse(readFileSync(packageJsonPath, "utf8"));
62
+ const scripts = packageJson.scripts && typeof packageJson.scripts === "object" && !Array.isArray(packageJson.scripts) ? packageJson.scripts : {};
63
+ const hadPrepare = typeof scripts.prepare === "string" && scripts.prepare.trim().length > 0;
64
+ let prepareAction = "left";
65
+ if (!hadPrepare) {
66
+ scripts.prepare = "husky install";
67
+ packageJson.scripts = scripts;
68
+ writeFileSync(packageJsonPath, `${JSON.stringify(packageJson, null, 2)}
69
+ `, "utf8");
70
+ prepareAction = "added";
71
+ }
72
+ const installed = [];
73
+ const skipped = [];
74
+ if (hookAction === "skipped") {
75
+ skipped.push(hookPath);
76
+ } else {
77
+ installed.push(hookPath);
78
+ }
79
+ if (prepareAction === "left") {
80
+ skipped.push(packageJsonPath);
81
+ } else {
82
+ installed.push(packageJsonPath);
83
+ }
84
+ return {
85
+ installed,
86
+ skipped,
87
+ hookPath,
88
+ packageJsonPath,
89
+ hookAction,
90
+ prepareAction
91
+ };
92
+ }
93
+ function normalizeTarget(targetInput) {
94
+ return isAbsolute(targetInput) ? targetInput : resolve(process.cwd(), targetInput);
95
+ }
96
+ function assertExistingDirectory(target) {
97
+ if (!existsSync(target) || !statSync(target).isDirectory()) {
98
+ throw new Error(t("cli.shared.target-invalid", { target }));
99
+ }
100
+ }
101
+ function installHookFile(hookPath, templateContent, force) {
102
+ if (existsSync(hookPath)) {
103
+ if (force) {
104
+ writeFileSync(hookPath, templateContent, "utf8");
105
+ return "overwritten";
106
+ }
107
+ const existing = readFileSync(hookPath, "utf8");
108
+ if (existing.includes("FAB_BIN=")) {
109
+ return "skipped";
110
+ }
111
+ const fabricBlock = templateContent.replace(/^#!\/bin\/sh\n/, "");
112
+ const separator = existing.endsWith("\n") ? "\n" : "\n\n";
113
+ writeFileSync(hookPath, `${existing}${separator}# --- Fabric ---
114
+ ${fabricBlock}`, "utf8");
115
+ return "appended";
116
+ }
117
+ writeFileSync(hookPath, templateContent, "utf8");
118
+ return "created";
119
+ }
120
+ function findTemplatePath(relativePath) {
121
+ const currentModuleDir = dirname(fileURLToPath(import.meta.url));
122
+ const candidates = [
123
+ ...templateCandidatesFrom(process.cwd(), relativePath),
124
+ ...templateCandidatesFrom(currentModuleDir, relativePath)
125
+ ];
126
+ for (const candidate of candidates) {
127
+ if (existsSync(candidate)) {
128
+ return candidate;
129
+ }
130
+ }
131
+ throw new Error(t("cli.shared.template-not-found", { path: relativePath }));
132
+ }
133
+ function templateCandidatesFrom(start, relativePath) {
134
+ const candidates = [];
135
+ let current = resolve(start);
136
+ while (true) {
137
+ candidates.push(join(current, ...relativePath.split("/")));
138
+ const parent = dirname(current);
139
+ if (parent === current || parse(current).root === current) {
140
+ break;
141
+ }
142
+ current = parent;
143
+ }
144
+ return candidates.reverse();
145
+ }
146
+ function writeStderr(message) {
147
+ process.stderr.write(`${message}
148
+ `);
149
+ }
150
+
151
+ export {
152
+ hooksCommand,
153
+ hooks_default,
154
+ installHooks
155
+ };
@@ -0,0 +1,15 @@
1
+ #!/usr/bin/env node
2
+ import {
3
+ configCmd,
4
+ config_default,
5
+ installMcpClients,
6
+ parseClientFilter
7
+ } from "./chunk-MDI7523D.js";
8
+ import "./chunk-VMYPJPKV.js";
9
+ import "./chunk-6ICJICVU.js";
10
+ export {
11
+ configCmd,
12
+ config_default as default,
13
+ installMcpClients,
14
+ parseClientFilter
15
+ };
@@ -0,0 +1,12 @@
1
+ #!/usr/bin/env node
2
+ import {
3
+ hooksCommand,
4
+ hooks_default,
5
+ installHooks
6
+ } from "./chunk-YDZJRLHL.js";
7
+ import "./chunk-6ICJICVU.js";
8
+ export {
9
+ hooks_default as default,
10
+ hooksCommand,
11
+ installHooks
12
+ };
package/dist/index.js CHANGED
@@ -1,7 +1,4 @@
1
1
  #!/usr/bin/env node
2
- import {
3
- t
4
- } from "./chunk-6ICJICVU.js";
5
2
 
6
3
  // src/index.ts
7
4
  import { realpathSync } from "fs";
@@ -11,25 +8,43 @@ import { defineCommand, runMain } from "citty";
11
8
 
12
9
  // src/commands/index.ts
13
10
  var allCommands = {
14
- bootstrap: () => import("./bootstrap-PMIA4W6G.js").then((module) => module.default),
15
- doctor: () => import("./doctor-QTSG2RWF.js").then((module) => module.default),
16
- init: () => import("./init-R73E5YTG.js").then((module) => module.default),
17
- scan: () => import("./scan-JBGFRB7P.js").then((module) => module.default),
18
- serve: () => import("./serve-4J2CQY25.js").then((module) => module.default),
11
+ init: () => import("./init-3FPLOABB.js").then((module) => module.default),
12
+ scan: () => import("./scan-WKDSKEBB.js").then((module) => module.default),
13
+ serve: () => import("./serve-MMN4GYLM.js").then((module) => module.default),
14
+ doctor: () => import("./doctor-5KJGOV2P.js").then((module) => module.default),
19
15
  "sync-meta": () => import("./sync-meta-THZSEM7Y.js").then((module) => module.default),
20
16
  "human-lint": () => import("./human-lint-YSFOZHZ7.js").then((module) => module.default),
21
17
  "ledger-append": () => import("./ledger-append-XZ5SX4O5.js").then((module) => module.default),
22
- hooks: () => import("./hooks-5S5IRVQE.js").then((module) => module.default),
23
- config: () => import("./config-PXEEXWLM.js").then((module) => module.configCmd),
24
- "pre-commit": () => import("./pre-commit-BLSUMT3P.js").then((module) => module.default)
18
+ "pre-commit": () => import("./pre-commit-CJ7EDKJK.js").then((module) => module.default),
19
+ bootstrap: () => import("./bootstrap-IUL4SAAK.js").then((module) => ({
20
+ ...module.default,
21
+ meta: {
22
+ ...module.default.meta,
23
+ hidden: true
24
+ }
25
+ })),
26
+ config: () => import("./config-3JBB77TX.js").then((module) => ({
27
+ ...module.configCmd,
28
+ meta: {
29
+ ...module.configCmd.meta,
30
+ hidden: true
31
+ }
32
+ })),
33
+ hooks: () => import("./hooks-ZSWVH2JD.js").then((module) => ({
34
+ ...module.default,
35
+ meta: {
36
+ ...module.default.meta,
37
+ hidden: true
38
+ }
39
+ }))
25
40
  };
26
41
 
27
42
  // src/index.ts
28
43
  var main = defineCommand({
29
44
  meta: {
30
- name: "fab",
31
- version: "1.1.0",
32
- description: t("cli.main.description")
45
+ name: "fabric",
46
+ version: "1.2.0",
47
+ description: 'Initialize and manage Fabric projects. Use "fabric init" for one-shot setup.'
33
48
  },
34
49
  subCommands: allCommands
35
50
  });
@@ -1,22 +1,33 @@
1
1
  #!/usr/bin/env node
2
+ import {
3
+ installBootstrap
4
+ } from "./chunk-RUQCZA2Q.js";
5
+ import {
6
+ installMcpClients
7
+ } from "./chunk-MDI7523D.js";
8
+ import "./chunk-VMYPJPKV.js";
9
+ import {
10
+ installHooks
11
+ } from "./chunk-YDZJRLHL.js";
2
12
  import {
3
13
  createScanReport,
4
14
  detectFramework
5
- } from "./chunk-JWUO6TIS.js";
15
+ } from "./chunk-N4DCTOXW.js";
16
+ import {
17
+ paint
18
+ } from "./chunk-WWNXR34K.js";
6
19
  import {
7
20
  createDebugLogger,
8
21
  resolveDevMode
9
22
  } from "./chunk-AEOYCVBG.js";
10
- import {
11
- paint
12
- } from "./chunk-WWNXR34K.js";
13
23
  import {
14
24
  t
15
25
  } from "./chunk-6ICJICVU.js";
16
26
 
17
27
  // src/commands/init.ts
18
28
  import { createHash } from "crypto";
19
- import { chmodSync, copyFileSync, existsSync as existsSync2, mkdirSync, readFileSync as readFileSync2, renameSync, statSync as statSync2, writeFileSync } from "fs";
29
+ import * as childProcess from "child_process";
30
+ import { chmodSync, copyFileSync, existsSync as existsSync2, mkdirSync, readFileSync as readFileSync2, renameSync, rmSync, statSync as statSync2, writeFileSync } from "fs";
20
31
  import { dirname, isAbsolute as isAbsolute2, join as join2, parse, resolve as resolve2 } from "path";
21
32
  import { fileURLToPath } from "url";
22
33
  import { defineCommand } from "citty";
@@ -841,7 +852,7 @@ function readProjectName(target) {
841
852
  return basename(target);
842
853
  }
843
854
  function getCliVersion() {
844
- return true ? "1.1.0" : "unknown";
855
+ return true ? "1.2.0" : "unknown";
845
856
  }
846
857
  function sortRecord(record) {
847
858
  return Object.fromEntries(Object.entries(record).sort(([left], [right]) => left.localeCompare(right)));
@@ -859,6 +870,8 @@ var AGENTS_TEMPLATE_BY_FRAMEWORK = {
859
870
  var CLAUDE_INIT_SKILL_TEMPLATE = "templates/claude-skills/agents-md-init/SKILL.md";
860
871
  var CLAUDE_INIT_REMINDER_HOOK_TEMPLATE = "templates/claude-hooks/agents-md-init-reminder.cjs";
861
872
  var CLAUDE_INIT_REMINDER_COMMAND = ".claude/hooks/agents-md-init-reminder.cjs";
873
+ var LOCAL_FABRIC_SERVER_PATH = join2("node_modules", "@fenglimg", "fabric-server", "dist", "index.js");
874
+ var FABRIC_SERVER_PACKAGE = "@fenglimg/fabric-server";
862
875
  var initCommand = defineCommand({
863
876
  meta: {
864
877
  name: "init",
@@ -873,44 +886,148 @@ var initCommand = defineCommand({
873
886
  type: "boolean",
874
887
  description: t("cli.init.args.debug.description"),
875
888
  default: false
889
+ },
890
+ force: {
891
+ type: "boolean",
892
+ description: t("cli.init.args.force.description"),
893
+ default: false
894
+ },
895
+ bootstrap: {
896
+ type: "boolean",
897
+ default: true,
898
+ negativeDescription: t("cli.init.args.no-bootstrap.description")
899
+ },
900
+ mcp: {
901
+ type: "boolean",
902
+ default: true,
903
+ negativeDescription: t("cli.init.args.no-mcp.description")
904
+ },
905
+ hooks: {
906
+ type: "boolean",
907
+ default: true,
908
+ negativeDescription: t("cli.init.args.no-hooks.description")
909
+ },
910
+ "mcp-install": {
911
+ type: "string",
912
+ default: "global",
913
+ description: t("cli.init.mcp.install.prompt")
876
914
  }
877
915
  },
878
916
  async run({ args }) {
879
917
  const logger = createDebugLogger(args.debug);
880
918
  const resolution = resolveDevMode(args.target, process.cwd());
881
919
  const target = normalizeTarget2(resolution.target);
920
+ const mcpInstallMode = resolveMcpInstallMode(args["mcp-install"]);
921
+ const options = {
922
+ force: args.force,
923
+ skipBootstrap: args.bootstrap === false ? true : args.skipBootstrap,
924
+ skipMcp: args.mcp === false ? true : args.skipMcp,
925
+ skipHooks: args.hooks === false ? true : args.skipHooks
926
+ };
882
927
  logger(`init target source: ${resolution.source}`);
883
928
  for (const step of resolution.chain) {
884
929
  logger(step);
885
930
  }
886
- const created = initFabric(target);
887
- console.log(t("cli.init.created-path", { label: createdLabel(), path: created.agentsPath }));
888
- console.log(t("cli.init.created-path", { label: createdLabel(), path: created.metaPath }));
889
- console.log(t("cli.init.created-path", { label: createdLabel(), path: created.humanLockPath }));
890
- console.log(t("cli.init.created-path", { label: createdLabel(), path: created.forensicPath }));
931
+ if (options.force) {
932
+ writeStderr(t("cli.init.force.warning", { path: target }));
933
+ }
934
+ const created = initFabric(target, options);
935
+ console.log(formatInitPathAction(created.agentsPath, created.agentsAction));
936
+ console.log(formatInitPathAction(created.metaPath, created.metaAction));
937
+ console.log(formatInitPathAction(created.humanLockPath, created.humanLockAction));
938
+ console.log(formatInitPathAction(created.forensicPath, created.forensicAction));
891
939
  writeStderr(
892
- created.claudeSkillAction === "created" ? t("cli.init.created-path", { label: createdLabel(), path: created.claudeSkillPath }) : t("cli.init.skipped-existing-path", { label: skippedLabel(), path: created.claudeSkillPath })
940
+ formatOptionalInitPathAction(created.claudeSkillPath, created.claudeSkillAction)
893
941
  );
894
942
  writeStderr(
895
- created.claudeHookAction === "created" ? t("cli.init.created-path", { label: createdLabel(), path: created.claudeHookPath }) : t("cli.init.skipped-existing-path", { label: skippedLabel(), path: created.claudeHookPath })
943
+ formatOptionalInitPathAction(created.claudeHookPath, created.claudeHookAction)
896
944
  );
897
945
  writeStderr(formatClaudeSettingsAction(created.claudeSettingsPath, created.claudeSettingsAction));
898
- console.log(
899
- t("cli.init.next-step", {
900
- label: nextLabel(),
901
- message: paint.muted(t("cli.init.next-step.message"))
902
- })
903
- );
946
+ const stageResults = [];
947
+ if (options.skipBootstrap) {
948
+ stageResults.push({ name: "bootstrap", disposition: "skipped" });
949
+ } else {
950
+ console.log(formatInitStageHeader(t("cli.init.stages.bootstrap")));
951
+ try {
952
+ const result = await installBootstrap(target, { force: options.force });
953
+ if (result.details.length === 0) {
954
+ console.log(formatInitStageResult("bootstrap", "skipped", 0, 0, t("cli.bootstrap.install.no-targets")));
955
+ stageResults.push({ name: "bootstrap", disposition: "skipped" });
956
+ } else {
957
+ console.log(
958
+ formatInitStageResult("bootstrap", "completed", result.installed.length, result.skipped.length)
959
+ );
960
+ stageResults.push({ name: "bootstrap", disposition: "ran" });
961
+ }
962
+ } catch (error) {
963
+ writeStderr(formatInitStageFailure("bootstrap", error));
964
+ stageResults.push({ name: "bootstrap", disposition: "failed" });
965
+ }
966
+ }
967
+ if (options.skipMcp) {
968
+ stageResults.push({ name: "mcp", disposition: "skipped" });
969
+ } else {
970
+ console.log(formatInitStageHeader(t("cli.init.stages.mcp")));
971
+ try {
972
+ let localServerPath;
973
+ if (mcpInstallMode === "local") {
974
+ const manager = detectPackageManager(target);
975
+ writeStderr(t("cli.init.mcp.install.local"));
976
+ writeStderr(t("cli.init.mcp.local.installing", { manager }));
977
+ installLocalFabricServer(target, manager);
978
+ writeStderr(t("cli.init.mcp.local.installed"));
979
+ localServerPath = LOCAL_FABRIC_SERVER_PATH;
980
+ } else {
981
+ writeStderr(t("cli.init.mcp.install.global"));
982
+ }
983
+ const result = await installMcpClients(target, {
984
+ force: options.force,
985
+ localServerPath
986
+ });
987
+ if (result.details.length === 0) {
988
+ console.log(formatInitStageResult("mcp", "skipped", 0, 0, t("cli.config.install.no-configs")));
989
+ stageResults.push({ name: "mcp", disposition: "skipped" });
990
+ } else {
991
+ console.log(formatInitStageResult("mcp", "completed", result.installed.length, result.skipped.length));
992
+ stageResults.push({ name: "mcp", disposition: "ran" });
993
+ }
994
+ } catch (error) {
995
+ writeStderr(formatInitStageFailure("mcp", error));
996
+ stageResults.push({ name: "mcp", disposition: "failed" });
997
+ }
998
+ }
999
+ if (options.skipHooks) {
1000
+ stageResults.push({ name: "hooks", disposition: "skipped" });
1001
+ } else {
1002
+ console.log(formatInitStageHeader(t("cli.init.stages.hooks")));
1003
+ try {
1004
+ const result = await installHooks(target, { force: options.force });
1005
+ console.log(formatInitStageResult("hooks", "completed", result.installed.length, result.skipped.length));
1006
+ stageResults.push({ name: "hooks", disposition: "ran" });
1007
+ } catch (error) {
1008
+ writeStderr(formatInitStageFailure("hooks", error));
1009
+ stageResults.push({ name: "hooks", disposition: "failed" });
1010
+ }
1011
+ }
1012
+ if (shouldPrintHooksNextStep(options, stageResults)) {
1013
+ console.log(
1014
+ t("cli.init.next-step", {
1015
+ label: nextLabel(),
1016
+ message: paint.muted(t("cli.init.next-step.message"))
1017
+ })
1018
+ );
1019
+ }
904
1020
  console.log(
905
1021
  t("cli.init.reason-message", {
906
1022
  label: reasonLabel(),
907
1023
  message: paint.muted(t("cli.init.reason-message.body"))
908
1024
  })
909
1025
  );
1026
+ printInitStageSummary(stageResults);
910
1027
  }
911
1028
  });
912
1029
  var init_default = initCommand;
913
- function initFabric(target) {
1030
+ function initFabric(target, options) {
914
1031
  assertExistingDirectory2(target);
915
1032
  const agentsPath = join2(target, "AGENTS.md");
916
1033
  const fabricDir = join2(target, ".fabric");
@@ -918,15 +1035,10 @@ function initFabric(target) {
918
1035
  const claudeSkillPath = join2(target, ".claude", "skills", "agents-md-init", "SKILL.md");
919
1036
  const claudeHookPath = join2(target, ".claude", "hooks", "agents-md-init-reminder.cjs");
920
1037
  const claudeSettingsPath = join2(target, ".claude", "settings.json");
921
- if (existsSync2(forensicPath)) {
922
- throw new Error(`ABORT: ${forensicPath} already exists. fab init is non-destructive.`);
923
- }
924
- if (existsSync2(agentsPath)) {
925
- throw new Error(`ABORT: ${agentsPath} already exists. fab init is non-destructive.`);
926
- }
927
- if (existsSync2(fabricDir)) {
928
- throw new Error(`ABORT: ${fabricDir} already exists. fab init is non-destructive.`);
929
- }
1038
+ const forensicGuardAction = prepareFreshPath(forensicPath, options);
1039
+ const agentsAction = prepareFreshPath(agentsPath, options);
1040
+ const fabricDirAction = prepareFreshPath(fabricDir, options);
1041
+ const forensicAction = forensicGuardAction === "overwritten" || fabricDirAction === "overwritten" ? "overwritten" : "created";
930
1042
  const scanReport = createScanReport(target);
931
1043
  const forensicReport = buildForensicReport(target);
932
1044
  const template = readFileSync2(findAgentsTemplatePath(scanReport.framework.kind), "utf8");
@@ -938,24 +1050,29 @@ function initFabric(target) {
938
1050
  const metaPath = join2(fabricDir, "agents.meta.json");
939
1051
  const humanLockPath = join2(fabricDir, "human-lock.json");
940
1052
  mkdirSync(fabricDir, { recursive: false });
941
- writeNewFile(agentsPath, agentsContent);
1053
+ writeNewFile(agentsPath, agentsContent, options);
942
1054
  writeNewFile(metaPath, `${JSON.stringify(meta, null, 2)}
943
- `);
1055
+ `, options);
944
1056
  writeNewFile(humanLockPath, humanLockTemplate.endsWith("\n") ? humanLockTemplate : `${humanLockTemplate}
945
- `);
1057
+ `, options);
946
1058
  writeNewFile(forensicPath, `${JSON.stringify(forensicReport, null, 2)}
947
- `);
948
- const claudeSkillAction = copyTemplateIfMissing(findTemplatePath(CLAUDE_INIT_SKILL_TEMPLATE), claudeSkillPath);
1059
+ `, options);
1060
+ const claudeSkillAction = copyTemplateIfMissing(findTemplatePath(CLAUDE_INIT_SKILL_TEMPLATE), claudeSkillPath, options);
949
1061
  const claudeHookAction = copyExecutableTemplateIfMissing(
950
1062
  findTemplatePath(CLAUDE_INIT_REMINDER_HOOK_TEMPLATE),
951
- claudeHookPath
1063
+ claudeHookPath,
1064
+ options
952
1065
  );
953
- const claudeSettingsAction = mergeClaudeStopHook(claudeSettingsPath);
1066
+ const claudeSettingsAction = mergeClaudeStopHook(claudeSettingsPath, options);
954
1067
  return {
955
1068
  agentsPath,
1069
+ agentsAction,
956
1070
  metaPath,
1071
+ metaAction: fabricDirAction,
957
1072
  humanLockPath,
1073
+ humanLockAction: fabricDirAction,
958
1074
  forensicPath,
1075
+ forensicAction,
959
1076
  claudeSkillPath,
960
1077
  claudeSkillAction,
961
1078
  claudeHookPath,
@@ -976,6 +1093,34 @@ function assertExistingDirectory2(target) {
976
1093
  throw new Error(`Target must be an existing directory: ${target}`);
977
1094
  }
978
1095
  }
1096
+ function detectPackageManager(cwd) {
1097
+ const workspaceRoot = resolve2(cwd);
1098
+ if (existsSync2(join2(workspaceRoot, "pnpm-lock.yaml"))) {
1099
+ return "pnpm";
1100
+ }
1101
+ if (existsSync2(join2(workspaceRoot, "yarn.lock"))) {
1102
+ return "yarn";
1103
+ }
1104
+ if (existsSync2(join2(workspaceRoot, "package-lock.json"))) {
1105
+ return "npm";
1106
+ }
1107
+ return "npm";
1108
+ }
1109
+ function resolveMcpInstallMode(rawMode) {
1110
+ if (rawMode === void 0 || rawMode === "global" || rawMode === "local") {
1111
+ return rawMode ?? "global";
1112
+ }
1113
+ writeStderr(t("cli.init.mcp.install.invalid", { value: rawMode }));
1114
+ return "global";
1115
+ }
1116
+ function installLocalFabricServer(target, manager) {
1117
+ const installArgs = manager === "npm" ? ["install", "-D", FABRIC_SERVER_PACKAGE] : ["add", "-D", FABRIC_SERVER_PACKAGE];
1118
+ childProcess.execFileSync(manager, installArgs, {
1119
+ cwd: target,
1120
+ stdio: "inherit",
1121
+ shell: process.platform === "win32"
1122
+ });
1123
+ }
979
1124
  function createInitialMeta(agentsHash) {
980
1125
  return {
981
1126
  revision: sha256(agentsHash),
@@ -1030,28 +1175,41 @@ function templateCandidatesFrom(start, relativePath) {
1030
1175
  }
1031
1176
  return candidates.reverse();
1032
1177
  }
1033
- function writeNewFile(path, content) {
1034
- if (existsSync2(path)) {
1035
- throw new Error(`ABORT: ${path} already exists. fab init is non-destructive.`);
1178
+ function prepareFreshPath(path, options) {
1179
+ if (!existsSync2(path)) {
1180
+ return "created";
1181
+ }
1182
+ if (!options?.force) {
1183
+ throw new Error(t("cli.init.errors.abort-existing", { path }));
1184
+ }
1185
+ rmSync(path, { recursive: true, force: true });
1186
+ return "overwritten";
1187
+ }
1188
+ function writeNewFile(path, content, options) {
1189
+ const existed = existsSync2(path);
1190
+ if (existed && !options?.force) {
1191
+ throw new Error(t("cli.init.errors.abort-existing", { path }));
1036
1192
  }
1037
1193
  writeFileSync(path, content, "utf8");
1194
+ return existed ? "overwritten" : "created";
1038
1195
  }
1039
- function copyTemplateIfMissing(templatePath, targetPath) {
1196
+ function copyTemplateIfMissing(templatePath, targetPath, options) {
1040
1197
  mkdirSync(dirname(targetPath), { recursive: true });
1041
- if (existsSync2(targetPath)) {
1198
+ const existed = existsSync2(targetPath);
1199
+ if (existed && !options?.force) {
1042
1200
  return "skipped";
1043
1201
  }
1044
1202
  copyFileSync(templatePath, targetPath);
1045
- return "created";
1203
+ return existed ? "overwritten" : "created";
1046
1204
  }
1047
- function copyExecutableTemplateIfMissing(templatePath, targetPath) {
1048
- const action = copyTemplateIfMissing(templatePath, targetPath);
1049
- if (action === "created") {
1205
+ function copyExecutableTemplateIfMissing(templatePath, targetPath, options) {
1206
+ const action = copyTemplateIfMissing(templatePath, targetPath, options);
1207
+ if (action !== "skipped") {
1050
1208
  chmodSync(targetPath, 493);
1051
1209
  }
1052
1210
  return action;
1053
1211
  }
1054
- function mergeClaudeStopHook(settingsPath) {
1212
+ function mergeClaudeStopHook(settingsPath, options) {
1055
1213
  mkdirSync(dirname(settingsPath), { recursive: true });
1056
1214
  let settings;
1057
1215
  let action = "updated";
@@ -1083,10 +1241,12 @@ function mergeClaudeStopHook(settingsPath) {
1083
1241
  return "skipped-invalid";
1084
1242
  }
1085
1243
  const stopHooks = Array.isArray(stopHooksValue) ? stopHooksValue : [];
1086
- if (hasClaudeInitReminderHook(stopHooks)) {
1244
+ const hasExistingFabricHook = hasClaudeInitReminderHook(stopHooks);
1245
+ if (hasExistingFabricHook && !options?.force) {
1087
1246
  return "skipped";
1088
1247
  }
1089
- stopHooks.push({
1248
+ const nextStopHooks = hasExistingFabricHook && options?.force ? removeClaudeInitReminderHook(stopHooks) : [...stopHooks];
1249
+ nextStopHooks.push({
1090
1250
  matcher: "*",
1091
1251
  hooks: [
1092
1252
  {
@@ -1097,20 +1257,24 @@ function mergeClaudeStopHook(settingsPath) {
1097
1257
  });
1098
1258
  settings.hooks = {
1099
1259
  ...hooks,
1100
- Stop: stopHooks
1260
+ Stop: nextStopHooks
1101
1261
  };
1102
1262
  writeJsonAtomically(settingsPath, settings);
1103
- return action;
1263
+ return hasExistingFabricHook && options?.force ? "overwritten" : action;
1104
1264
  }
1105
1265
  function hasClaudeInitReminderHook(stopHooks) {
1106
- return stopHooks.some((entry) => {
1107
- if (!isRecord(entry) || !Array.isArray(entry.hooks)) {
1108
- return false;
1109
- }
1110
- return entry.hooks.some(
1111
- (hook) => isRecord(hook) && hook.type === "command" && typeof hook.command === "string" && hook.command.includes("agents-md-init-reminder.cjs")
1112
- );
1113
- });
1266
+ return stopHooks.some((entry) => isClaudeInitReminderStopEntry(entry));
1267
+ }
1268
+ function removeClaudeInitReminderHook(stopHooks) {
1269
+ return stopHooks.filter((entry) => !isClaudeInitReminderStopEntry(entry));
1270
+ }
1271
+ function isClaudeInitReminderStopEntry(entry) {
1272
+ if (!isRecord(entry) || !Array.isArray(entry.hooks)) {
1273
+ return false;
1274
+ }
1275
+ return entry.hooks.some(
1276
+ (hook) => isRecord(hook) && hook.type === "command" && typeof hook.command === "string" && hook.command.includes("agents-md-init-reminder.cjs")
1277
+ );
1114
1278
  }
1115
1279
  function writeJsonAtomically(path, value) {
1116
1280
  const tempPath = `${path}.${process.pid}.tmp`;
@@ -1127,6 +1291,8 @@ function formatClaudeSettingsAction(settingsPath, action) {
1127
1291
  return t("cli.init.claude-settings.created", { label: createdLabel(), path: settingsPath });
1128
1292
  case "updated":
1129
1293
  return t("cli.init.claude-settings.updated", { label: updatedLabel(), path: settingsPath });
1294
+ case "overwritten":
1295
+ return t("cli.init.claude-settings.updated", { label: overwrittenLabel(), path: settingsPath });
1130
1296
  case "skipped":
1131
1297
  return t("cli.init.claude-settings.skipped", { label: skippedLabel(), path: settingsPath });
1132
1298
  case "skipped-invalid":
@@ -1135,6 +1301,46 @@ function formatClaudeSettingsAction(settingsPath, action) {
1135
1301
  return t("cli.init.claude-settings.updated", { label: updatedLabel(), path: settingsPath });
1136
1302
  }
1137
1303
  }
1304
+ function formatInitStageHeader(message) {
1305
+ return `${nextLabel()} ${paint.muted(message)}`;
1306
+ }
1307
+ function formatInitStageResult(stage, status, installedCount, skippedCount, note) {
1308
+ const label = status === "completed" ? completedStageLabel() : skippedStageLabel();
1309
+ const counts = `installed=${installedCount} skipped=${skippedCount}`;
1310
+ const suffix = note ? ` ${paint.muted(`(${note})`)}` : "";
1311
+ return `${label} ${stage}: ${counts}${suffix}`;
1312
+ }
1313
+ function formatInitStageFailure(stage, error) {
1314
+ const message = error instanceof Error ? error.message : String(error);
1315
+ return `${failedStageLabel()} ${stage}: ${message}`;
1316
+ }
1317
+ function printInitStageSummary(stageResults) {
1318
+ console.log(formatInitStageSummaryLine("ran", collectInitStageNames(stageResults, "ran")));
1319
+ console.log(formatInitStageSummaryLine("skipped", collectInitStageNames(stageResults, "skipped")));
1320
+ console.log(formatInitStageSummaryLine("failed", collectInitStageNames(stageResults, "failed")));
1321
+ }
1322
+ function formatInitStageSummaryLine(disposition, stages) {
1323
+ const label = disposition === "ran" ? paint.success(t("cli.init.stages.summary.ran")) : disposition === "skipped" ? paint.muted(t("cli.init.stages.summary.skipped")) : paint.error(t("cli.init.stages.summary.failed"));
1324
+ return `${label}: ${stages.length > 0 ? stages.join(", ") : t("cli.shared.none")}`;
1325
+ }
1326
+ function collectInitStageNames(stageResults, disposition) {
1327
+ return stageResults.filter((stage) => stage.disposition === disposition).map((stage) => stage.name);
1328
+ }
1329
+ function shouldPrintHooksNextStep(options, stageResults) {
1330
+ return Boolean(options.skipHooks) || stageResults.some((stage) => stage.name === "hooks" && stage.disposition === "failed");
1331
+ }
1332
+ function formatInitPathAction(path, action) {
1333
+ return t("cli.init.created-path", { label: labelForInitWriteAction(action), path });
1334
+ }
1335
+ function formatOptionalInitPathAction(path, action) {
1336
+ if (action === "skipped") {
1337
+ return t("cli.init.skipped-existing-path", { label: skippedLabel(), path });
1338
+ }
1339
+ return formatInitPathAction(path, action);
1340
+ }
1341
+ function labelForInitWriteAction(action) {
1342
+ return action === "overwritten" ? overwrittenLabel() : createdLabel();
1343
+ }
1138
1344
  function createdLabel() {
1139
1345
  return paint.success(t("cli.shared.created"));
1140
1346
  }
@@ -1150,6 +1356,18 @@ function reasonLabel() {
1150
1356
  function updatedLabel() {
1151
1357
  return paint.success(t("cli.shared.updated"));
1152
1358
  }
1359
+ function overwrittenLabel() {
1360
+ return paint.warn(t("cli.init.force.overwritten"));
1361
+ }
1362
+ function completedStageLabel() {
1363
+ return paint.success(t("cli.init.stages.completed"));
1364
+ }
1365
+ function skippedStageLabel() {
1366
+ return paint.muted(t("cli.init.stages.skipped"));
1367
+ }
1368
+ function failedStageLabel() {
1369
+ return paint.error(t("cli.init.stages.failed"));
1370
+ }
1153
1371
  function writeStderr(message) {
1154
1372
  process.stderr.write(`${message}
1155
1373
  `);
@@ -1159,6 +1377,7 @@ function sha256(content) {
1159
1377
  }
1160
1378
  export {
1161
1379
  init_default as default,
1380
+ detectPackageManager,
1162
1381
  initCommand,
1163
1382
  initFabric
1164
1383
  };
@@ -3,9 +3,9 @@ import {
3
3
  createScanReport,
4
4
  scanCommand,
5
5
  scan_default
6
- } from "./chunk-JWUO6TIS.js";
7
- import "./chunk-AEOYCVBG.js";
6
+ } from "./chunk-N4DCTOXW.js";
8
7
  import "./chunk-WWNXR34K.js";
8
+ import "./chunk-AEOYCVBG.js";
9
9
  import "./chunk-6ICJICVU.js";
10
10
  export {
11
11
  createScanReport,
package/package.json CHANGED
@@ -1,9 +1,10 @@
1
1
  {
2
2
  "name": "@fenglimg/fabric-cli",
3
- "version": "1.1.0",
3
+ "version": "1.2.0",
4
4
  "type": "module",
5
5
  "bin": {
6
- "fab": "dist/index.js"
6
+ "fab": "dist/index.js",
7
+ "fabric": "dist/index.js"
7
8
  },
8
9
  "main": "./dist/index.js",
9
10
  "types": "./dist/index.d.ts",
@@ -16,8 +17,8 @@
16
17
  "citty": "^0.2.2",
17
18
  "picocolors": "^1.1.1",
18
19
  "string-width": "^7.2.0",
19
- "@fenglimg/fabric-shared": "1.1.0",
20
- "@fenglimg/fabric-server": "1.1.0"
20
+ "@fenglimg/fabric-shared": "1.2.0",
21
+ "@fenglimg/fabric-server": "1.2.0"
21
22
  },
22
23
  "devDependencies": {
23
24
  "@types/iarna__toml": "^2.0.5",
@@ -1,124 +0,0 @@
1
- #!/usr/bin/env node
2
- import {
3
- t
4
- } from "./chunk-6ICJICVU.js";
5
-
6
- // src/commands/hooks.ts
7
- import { chmodSync, existsSync, mkdirSync, readFileSync, statSync, writeFileSync } from "fs";
8
- import { dirname, isAbsolute, join, parse, resolve } from "path";
9
- import { fileURLToPath } from "url";
10
- import { defineCommand } from "citty";
11
- var hooksCommand = defineCommand({
12
- meta: {
13
- name: "hooks",
14
- description: t("cli.hooks.description")
15
- },
16
- subCommands: {
17
- install: defineCommand({
18
- meta: {
19
- name: "install",
20
- description: t("cli.hooks.install.description")
21
- },
22
- args: {
23
- target: {
24
- type: "string",
25
- description: t("cli.hooks.install.args.target.description"),
26
- default: process.cwd()
27
- }
28
- },
29
- async run({ args }) {
30
- const target = normalizeTarget(args.target);
31
- assertExistingDirectory(target);
32
- const huskyDir = join(target, ".husky");
33
- const hookPath = join(huskyDir, "pre-commit");
34
- const packageJsonPath = join(target, "package.json");
35
- if (!existsSync(packageJsonPath)) {
36
- throw new Error(t("cli.hooks.errors.package-json-required", { path: packageJsonPath }));
37
- }
38
- mkdirSync(huskyDir, { recursive: true });
39
- const templateContent = readFileSync(findTemplatePath("templates/husky/pre-commit"), "utf8");
40
- let hookAction;
41
- if (existsSync(hookPath)) {
42
- const existing = readFileSync(hookPath, "utf8");
43
- if (existing.includes("FAB_BIN=")) {
44
- hookAction = "skipped";
45
- } else {
46
- const fabricBlock = templateContent.replace(/^#!\/bin\/sh\n/, "");
47
- const separator = existing.endsWith("\n") ? "\n" : "\n\n";
48
- writeFileSync(hookPath, `${existing}${separator}# --- Fabric ---
49
- ${fabricBlock}`, "utf8");
50
- hookAction = "appended";
51
- }
52
- } else {
53
- writeFileSync(hookPath, templateContent, "utf8");
54
- hookAction = "created";
55
- }
56
- chmodSync(hookPath, 493);
57
- const packageJson = JSON.parse(readFileSync(packageJsonPath, "utf8"));
58
- const scripts = packageJson.scripts && typeof packageJson.scripts === "object" && !Array.isArray(packageJson.scripts) ? packageJson.scripts : {};
59
- const hadPrepare = typeof scripts.prepare === "string" && scripts.prepare.trim().length > 0;
60
- if (!hadPrepare) {
61
- scripts.prepare = "husky install";
62
- packageJson.scripts = scripts;
63
- writeFileSync(packageJsonPath, `${JSON.stringify(packageJson, null, 2)}
64
- `, "utf8");
65
- }
66
- if (hookAction === "skipped") {
67
- writeStderr(t("cli.hooks.install.hook-skipped", { path: hookPath }));
68
- } else if (hookAction === "appended") {
69
- writeStderr(t("cli.hooks.install.hook-appended", { path: hookPath }));
70
- } else {
71
- writeStderr(t("cli.hooks.install.hook-created", { path: hookPath }));
72
- }
73
- if (hadPrepare) {
74
- writeStderr(t("cli.hooks.install.prepare-left", { path: packageJsonPath }));
75
- } else {
76
- writeStderr(t("cli.hooks.install.prepare-added", { path: packageJsonPath }));
77
- }
78
- }
79
- })
80
- }
81
- });
82
- var hooks_default = hooksCommand;
83
- function normalizeTarget(targetInput) {
84
- return isAbsolute(targetInput) ? targetInput : resolve(process.cwd(), targetInput);
85
- }
86
- function assertExistingDirectory(target) {
87
- if (!existsSync(target) || !statSync(target).isDirectory()) {
88
- throw new Error(t("cli.shared.target-invalid", { target }));
89
- }
90
- }
91
- function findTemplatePath(relativePath) {
92
- const currentModuleDir = dirname(fileURLToPath(import.meta.url));
93
- const candidates = [
94
- ...templateCandidatesFrom(process.cwd(), relativePath),
95
- ...templateCandidatesFrom(currentModuleDir, relativePath)
96
- ];
97
- for (const candidate of candidates) {
98
- if (existsSync(candidate)) {
99
- return candidate;
100
- }
101
- }
102
- throw new Error(t("cli.shared.template-not-found", { path: relativePath }));
103
- }
104
- function templateCandidatesFrom(start, relativePath) {
105
- const candidates = [];
106
- let current = resolve(start);
107
- while (true) {
108
- candidates.push(join(current, ...relativePath.split("/")));
109
- const parent = dirname(current);
110
- if (parent === current || parse(current).root === current) {
111
- break;
112
- }
113
- current = parent;
114
- }
115
- return candidates.reverse();
116
- }
117
- function writeStderr(message) {
118
- process.stderr.write(`${message}
119
- `);
120
- }
121
- export {
122
- hooks_default as default,
123
- hooksCommand
124
- };
@@ -1,15 +1,15 @@
1
1
  #!/usr/bin/env node
2
- import {
3
- createDebugLogger,
4
- readFabricConfig,
5
- resolveDevMode
6
- } from "./chunk-AEOYCVBG.js";
7
2
  import {
8
3
  displayWidth,
9
4
  padEnd,
10
5
  paint,
11
6
  symbol
12
7
  } 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";
@@ -1,12 +1,12 @@
1
1
  #!/usr/bin/env node
2
- import {
3
- resolveDevMode
4
- } from "./chunk-AEOYCVBG.js";
5
2
  import {
6
3
  padEnd,
7
4
  paint,
8
5
  symbol
9
6
  } from "./chunk-WWNXR34K.js";
7
+ import {
8
+ resolveDevMode
9
+ } from "./chunk-AEOYCVBG.js";
10
10
  import {
11
11
  t
12
12
  } from "./chunk-6ICJICVU.js";
@@ -1,17 +1,17 @@
1
1
  #!/usr/bin/env node
2
- import {
3
- ledger_append_default
4
- } from "./chunk-F2BXHPM5.js";
5
- import {
6
- resolveDevModeTarget
7
- } from "./chunk-AEOYCVBG.js";
8
2
  import {
9
3
  sync_meta_default
10
4
  } from "./chunk-6UUPKSDE.js";
11
5
  import {
12
6
  human_lint_default
13
7
  } from "./chunk-L43IGJ6X.js";
8
+ import {
9
+ ledger_append_default
10
+ } from "./chunk-F2BXHPM5.js";
14
11
  import "./chunk-WWNXR34K.js";
12
+ import {
13
+ resolveDevModeTarget
14
+ } from "./chunk-AEOYCVBG.js";
15
15
  import {
16
16
  t
17
17
  } from "./chunk-6ICJICVU.js";
@@ -1,12 +1,12 @@
1
1
  #!/usr/bin/env node
2
- import {
3
- createDebugLogger,
4
- resolveDevMode
5
- } from "./chunk-AEOYCVBG.js";
6
2
  import {
7
3
  paint,
8
4
  symbol
9
5
  } from "./chunk-WWNXR34K.js";
6
+ import {
7
+ createDebugLogger,
8
+ resolveDevMode
9
+ } from "./chunk-AEOYCVBG.js";
10
10
  import {
11
11
  t
12
12
  } from "./chunk-6ICJICVU.js";