@fenglimg/fabric-cli 0.1.3 → 1.0.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.
Files changed (29) hide show
  1. package/dist/{bootstrap-BBFPEJQP.js → bootstrap-PMIA4W6G.js} +16 -12
  2. package/dist/{chunk-ZW4M4WZB.js → chunk-5BSTO745.js} +9 -6
  3. package/dist/chunk-6ICJICVU.js +10 -0
  4. package/dist/chunk-AEOYCVBG.js +0 -0
  5. package/dist/{chunk-SC4A43WR.js → chunk-DKQ3HOTK.js} +59 -89
  6. package/dist/{chunk-PJ4J4377.js → chunk-F2BXHPM5.js} +11 -7
  7. package/dist/{chunk-YHIKLBTB.js → chunk-L43IGJ6X.js} +17 -7
  8. package/dist/chunk-P4KVFB2T.js +0 -0
  9. package/dist/{chunk-3FZPYATY.js → chunk-VMYPJPKV.js} +11 -4
  10. package/dist/chunk-WWNXR34K.js +49 -0
  11. package/dist/{config-TNY6BCQ2.js → config-PXEEXWLM.js} +14 -11
  12. package/dist/{hooks-QWVCECWF.js → hooks-5S5IRVQE.js} +36 -11
  13. package/dist/human-lint-YSFOZHZ7.js +13 -0
  14. package/dist/index.js +15 -11
  15. package/dist/init-G6Q3OOMC.js +601 -0
  16. package/dist/{ledger-append-KYPMIAM4.js → ledger-append-XZ5SX4O5.js} +2 -1
  17. package/dist/{pre-commit-ICTZBF6F.js → pre-commit-IEIXHKOD.js} +13 -7
  18. package/dist/{scan-COKVYPOH.js → scan-6CURGC3D.js} +3 -1
  19. package/dist/serve-4J2CQY25.js +112 -0
  20. package/dist/{sync-meta-YIB7IBHK.js → sync-meta-L6M4AEUT.js} +2 -1
  21. package/package.json +12 -8
  22. package/templates/agents-md/AGENTS.md.template +17 -11
  23. package/templates/agents-md/variants/cocos.md +37 -0
  24. package/templates/agents-md/variants/next.md +37 -0
  25. package/templates/agents-md/variants/vite.md +37 -0
  26. package/templates/claude-hooks/agents-md-init-reminder.cjs +18 -0
  27. package/templates/claude-skills/agents-md-init/SKILL.md +86 -0
  28. package/dist/human-lint-SV4D5LQ7.js +0 -9
  29. package/dist/init-VR43EQRO.js +0 -149
@@ -1,10 +1,13 @@
1
1
  #!/usr/bin/env node
2
2
  import {
3
3
  resolveClients
4
- } from "./chunk-3FZPYATY.js";
4
+ } from "./chunk-VMYPJPKV.js";
5
5
  import {
6
6
  readFabricConfig
7
7
  } from "./chunk-AEOYCVBG.js";
8
+ import {
9
+ t
10
+ } from "./chunk-6ICJICVU.js";
8
11
 
9
12
  // src/commands/bootstrap.ts
10
13
  import { existsSync, mkdirSync, readFileSync, writeFileSync } from "fs";
@@ -50,18 +53,18 @@ var CLIENT_TARGET_MAP = {
50
53
  var bootstrapCommand = defineCommand({
51
54
  meta: {
52
55
  name: "bootstrap",
53
- description: "Install Fabric bootstrap prompt templates for supported AI clients."
56
+ description: t("cli.bootstrap.description")
54
57
  },
55
58
  subCommands: {
56
59
  install: defineCommand({
57
60
  meta: {
58
61
  name: "install",
59
- description: "Copy Fabric bootstrap templates into client-native locations."
62
+ description: t("cli.bootstrap.install.description")
60
63
  },
61
64
  args: {
62
65
  clients: {
63
66
  type: "string",
64
- description: "Optional comma-separated client filter, e.g. claude,cursor,codex."
67
+ description: t("cli.bootstrap.install.args.clients.description")
65
68
  }
66
69
  },
67
70
  async run({ args }) {
@@ -72,7 +75,8 @@ var bootstrapCommand = defineCommand({
72
75
  const clients = selectedClients ?? detectedClients;
73
76
  if (clients.size === 0) {
74
77
  process.stderr.write(
75
- "No bootstrap targets detected. Pass --clients claude,cursor,windsurf,roo,gemini,codex to install explicitly.\n"
78
+ `${t("cli.bootstrap.install.no-targets")}
79
+ `
76
80
  );
77
81
  return;
78
82
  }
@@ -93,7 +97,7 @@ function parseClientFilter(value) {
93
97
  const alias = rawClient.trim().toLowerCase();
94
98
  const client = CLIENT_ALIASES[alias];
95
99
  if (client === void 0) {
96
- throw new Error(`Unknown client "${rawClient}". Use a comma-separated list such as claude,cursor,codex.`);
100
+ throw new Error(t("cli.bootstrap.errors.unknown-client", { client: rawClient }));
97
101
  }
98
102
  clients.add(client);
99
103
  }
@@ -138,26 +142,26 @@ function installBootstrap(client, workspaceRoot) {
138
142
  return;
139
143
  }
140
144
  writeFileSync(targetPath, ensureTrailingNewline(template), "utf8");
141
- process.stderr.write(`Installed ${targetPath}
145
+ process.stderr.write(`${t("cli.bootstrap.install.installed", { path: targetPath })}
142
146
  `);
143
147
  }
144
148
  function writeCodexBootstrap(targetPath, template) {
145
149
  const nextContent = ensureTrailingNewline(template);
146
150
  if (!existsSync(targetPath)) {
147
151
  writeFileSync(targetPath, nextContent, "utf8");
148
- process.stderr.write(`Installed ${targetPath}
152
+ process.stderr.write(`${t("cli.bootstrap.install.installed", { path: targetPath })}
149
153
  `);
150
154
  return;
151
155
  }
152
156
  const existing = readFileSync(targetPath, "utf8");
153
157
  if (existing.includes("# Fabric Bootstrap")) {
154
- process.stderr.write(`Skipped ${targetPath}: Fabric Bootstrap header already present.
158
+ process.stderr.write(`${t("cli.bootstrap.install.skipped-header", { path: targetPath })}
155
159
  `);
156
160
  return;
157
161
  }
158
162
  const separator = existing.startsWith("\n") || existing.length === 0 ? "" : "\n";
159
163
  writeFileSync(targetPath, `${nextContent}${separator}${existing}`, "utf8");
160
- process.stderr.write(`Prepended ${targetPath}
164
+ process.stderr.write(`${t("cli.bootstrap.install.prepended", { path: targetPath })}
161
165
  `);
162
166
  }
163
167
  function ensureTrailingNewline(content) {
@@ -175,7 +179,7 @@ function findTemplatePath(relativePath) {
175
179
  return candidate;
176
180
  }
177
181
  }
178
- throw new Error(`Template not found: ${relativePath}`);
182
+ throw new Error(t("cli.shared.template-not-found", { path: relativePath }));
179
183
  }
180
184
  function templateCandidatesFrom(start, relativePath) {
181
185
  const candidates = [];
@@ -188,7 +192,7 @@ function templateCandidatesFrom(start, relativePath) {
188
192
  }
189
193
  current = parent;
190
194
  }
191
- return candidates;
195
+ return candidates.reverse();
192
196
  }
193
197
  export {
194
198
  bootstrapCommand,
@@ -2,6 +2,9 @@
2
2
  import {
3
3
  resolveIgnores
4
4
  } from "./chunk-P4KVFB2T.js";
5
+ import {
6
+ t
7
+ } from "./chunk-6ICJICVU.js";
5
8
 
6
9
  // src/commands/sync-meta.ts
7
10
  import { createHash } from "crypto";
@@ -11,17 +14,17 @@ import { defineCommand } from "citty";
11
14
  var syncMetaCommand = defineCommand({
12
15
  meta: {
13
16
  name: "sync-meta",
14
- description: "Sync Fabric metadata from AGENTS.md files."
17
+ description: t("cli.sync-meta.description")
15
18
  },
16
19
  args: {
17
20
  target: {
18
21
  type: "string",
19
- description: "Target project path. Defaults to the current working directory.",
22
+ description: t("cli.sync-meta.args.target.description"),
20
23
  default: process.cwd()
21
24
  },
22
25
  "check-only": {
23
26
  type: "boolean",
24
- description: "Exit with code 1 if .fabric/agents.meta.json is stale.",
27
+ description: t("cli.sync-meta.args.check-only.description"),
25
28
  default: false
26
29
  }
27
30
  },
@@ -32,7 +35,7 @@ var syncMetaCommand = defineCommand({
32
35
  const existingMeta = readExistingMeta(metaPath);
33
36
  if (args["check-only"]) {
34
37
  if (!existingMeta || stableStringify(existingMeta) !== stableStringify(computedMeta)) {
35
- writeStderr("Fabric metadata drift detected. Run fab sync-meta to update.");
38
+ writeStderr(t("cli.sync-meta.drift-detected"));
36
39
  process.exitCode = 1;
37
40
  }
38
41
  return;
@@ -43,7 +46,7 @@ var syncMetaCommand = defineCommand({
43
46
  mkdirSync(join(target, ".fabric"), { recursive: true });
44
47
  writeFileSync(metaPath, `${JSON.stringify(computedMeta, null, 2)}
45
48
  `, "utf8");
46
- writeStderr(`Updated ${metaPath}`);
49
+ writeStderr(t("cli.sync-meta.updated", { label: t("cli.shared.updated"), path: metaPath }));
47
50
  }
48
51
  });
49
52
  var sync_meta_default = syncMetaCommand;
@@ -77,7 +80,7 @@ function normalizeTarget(targetInput) {
77
80
  }
78
81
  function assertExistingDirectory(target) {
79
82
  if (!existsSync(target) || !statSync(target).isDirectory()) {
80
- throw new Error(`Target must be an existing directory: ${target}`);
83
+ throw new Error(t("cli.shared.target-invalid", { target }));
81
84
  }
82
85
  }
83
86
  function readExistingMeta(metaPath) {
@@ -0,0 +1,10 @@
1
+ #!/usr/bin/env node
2
+
3
+ // src/i18n.ts
4
+ import { createTranslator, detectNodeLocale } from "@fenglimg/fabric-shared";
5
+ var locale = detectNodeLocale();
6
+ var t = createTranslator(locale);
7
+
8
+ export {
9
+ t
10
+ };
File without changes
@@ -7,80 +7,31 @@ import {
7
7
  import {
8
8
  resolveIgnores
9
9
  } from "./chunk-P4KVFB2T.js";
10
+ import {
11
+ displayWidth,
12
+ padEnd,
13
+ paint,
14
+ symbol
15
+ } from "./chunk-WWNXR34K.js";
16
+ import {
17
+ t
18
+ } from "./chunk-6ICJICVU.js";
10
19
 
11
20
  // src/commands/scan.ts
12
- import { existsSync as existsSync2, readdirSync, readFileSync as readFileSync2, statSync } from "fs";
13
- import { isAbsolute, join as join2, relative, resolve, sep } from "path";
21
+ import { existsSync, readdirSync, readFileSync, statSync } from "fs";
22
+ import { isAbsolute, join, relative, resolve, sep } from "path";
14
23
  import { defineCommand } from "citty";
15
24
 
16
25
  // src/scanner/detector.ts
17
- import { existsSync, readFileSync } from "fs";
18
- import { join } from "path";
19
- function detectFramework(root) {
20
- const evidence = [];
21
- if (existsSync(join(root, "project.config.json"))) {
22
- return {
23
- kind: "cocos-creator",
24
- evidence: ["project.config.json"]
25
- };
26
- }
27
- const packageJsonPath = join(root, "package.json");
28
- if (existsSync(packageJsonPath)) {
29
- const packageJson = readPackageJson(packageJsonPath);
30
- const deps = collectDependencyNames(packageJson);
31
- for (const [dependencyName, kind] of [
32
- ["next", "next"],
33
- ["vite", "vite"],
34
- ["react", "react"],
35
- ["vue", "vue"]
36
- ]) {
37
- if (deps.has(dependencyName)) {
38
- evidence.push(`package.json dependency: ${dependencyName}`);
39
- return { kind, evidence };
40
- }
41
- }
42
- evidence.push("package.json");
43
- }
44
- if (existsSync(join(root, "Cargo.toml"))) {
45
- return {
46
- kind: "rust",
47
- evidence: ["Cargo.toml"]
48
- };
49
- }
50
- if (existsSync(join(root, "pyproject.toml"))) {
51
- return {
52
- kind: "python",
53
- evidence: ["pyproject.toml"]
54
- };
55
- }
56
- return {
57
- kind: "unknown",
58
- evidence
59
- };
60
- }
61
- function readPackageJson(packageJsonPath) {
62
- try {
63
- return JSON.parse(readFileSync(packageJsonPath, "utf8"));
64
- } catch {
65
- return {};
66
- }
67
- }
68
- function collectDependencyNames(packageJson) {
69
- return /* @__PURE__ */ new Set([
70
- ...Object.keys(packageJson.dependencies ?? {}),
71
- ...Object.keys(packageJson.devDependencies ?? {}),
72
- ...Object.keys(packageJson.peerDependencies ?? {}),
73
- ...Object.keys(packageJson.optionalDependencies ?? {})
74
- ]);
75
- }
26
+ import { detectFramework } from "@fenglimg/fabric-shared";
76
27
 
77
28
  // src/commands/scan.ts
78
29
  function createScanReport(targetInput = process.cwd(), fabricConfig) {
79
30
  const target = normalizeTarget(targetInput);
80
31
  const framework = detectFramework(target);
81
32
  const readmeQuality = getReadmeQuality(target);
82
- const hasContributing = existsSync2(join2(target, "CONTRIBUTING.md"));
83
- const hasExistingFabric = existsSync2(join2(target, "AGENTS.md")) || existsSync2(join2(target, ".fabric"));
33
+ const hasContributing = existsSync(join(target, "CONTRIBUTING.md"));
34
+ const hasExistingFabric = existsSync(join(target, "AGENTS.md")) || existsSync(join(target, ".fabric"));
84
35
  const walkResult = walkFiles(target, resolveIgnores(fabricConfig));
85
36
  return {
86
37
  target,
@@ -101,21 +52,21 @@ function createScanReport(targetInput = process.cwd(), fabricConfig) {
101
52
  var scanCommand = defineCommand({
102
53
  meta: {
103
54
  name: "scan",
104
- description: "Scan a project for Fabric bootstrap candidates."
55
+ description: t("cli.scan.description")
105
56
  },
106
57
  args: {
107
58
  target: {
108
59
  type: "string",
109
- description: "Absolute target path to scan. Defaults to CLI target, EXTERNAL_FIXTURE_PATH, fabric.config.json, or cwd."
60
+ description: t("cli.scan.args.target.description")
110
61
  },
111
62
  debug: {
112
63
  type: "boolean",
113
- description: "Print detector evidence in pretty output.",
64
+ description: t("cli.scan.args.debug.description"),
114
65
  default: false
115
66
  },
116
67
  json: {
117
68
  type: "boolean",
118
- description: "Print the diagnostic report as JSON.",
69
+ description: t("cli.scan.args.json.description"),
119
70
  default: false
120
71
  }
121
72
  },
@@ -141,16 +92,16 @@ function normalizeTarget(targetInput) {
141
92
  return isAbsolute(targetInput) ? targetInput : resolve(process.cwd(), targetInput);
142
93
  }
143
94
  function getReadmeQuality(target) {
144
- const readmePath = join2(target, "README.md");
145
- if (!existsSync2(readmePath)) {
95
+ const readmePath = join(target, "README.md");
96
+ if (!existsSync(readmePath)) {
146
97
  return "stub";
147
98
  }
148
- const wordCount = readFileSync2(readmePath, "utf8").trim().split(/\s+/).filter(Boolean).length;
99
+ const wordCount = readFileSync(readmePath, "utf8").trim().split(/\s+/).filter(Boolean).length;
149
100
  return wordCount >= 200 ? "ok" : "stub";
150
101
  }
151
102
  function walkFiles(root, ignorePatterns) {
152
- if (!existsSync2(root) || !statSync(root).isDirectory()) {
153
- throw new Error(`Target must be an existing directory: ${root}`);
103
+ if (!existsSync(root) || !statSync(root).isDirectory()) {
104
+ throw new Error(t("cli.shared.target-invalid", { target: root }));
154
105
  }
155
106
  let fileCount = 0;
156
107
  let ignoredCount = 0;
@@ -161,7 +112,7 @@ function walkFiles(root, ignorePatterns) {
161
112
  continue;
162
113
  }
163
114
  for (const entry of readdirSync(current, { withFileTypes: true })) {
164
- const absolutePath = join2(current, entry.name);
115
+ const absolutePath = join(current, entry.name);
165
116
  const relativePath = toPosixPath(relative(root, absolutePath));
166
117
  if (shouldIgnore(relativePath, entry.isDirectory(), ignorePatterns)) {
167
118
  ignoredCount += 1;
@@ -196,40 +147,59 @@ function toPosixPath(path) {
196
147
  function buildRecommendations(input) {
197
148
  const recommendations = [];
198
149
  if (!input.hasExistingFabric) {
199
- recommendations.push("L0: Run fab init to scaffold AGENTS.md with TODO markers.");
150
+ recommendations.push(t("cli.scan.recommendation.init"));
200
151
  }
201
152
  if (input.readmeQuality === "stub") {
202
- recommendations.push("L0: Expand README.md before promoting project facts into AGENTS.md references.");
153
+ recommendations.push(t("cli.scan.recommendation.readme"));
203
154
  }
204
155
  if (!input.hasContributing) {
205
- recommendations.push("L0: Add CONTRIBUTING.md or leave an AGENTS.md TODO reference for contribution flow.");
156
+ recommendations.push(t("cli.scan.recommendation.contributing"));
206
157
  }
207
158
  if (input.framework.kind === "unknown") {
208
- recommendations.push("L1: Add tech-stack TODOs manually because no framework marker was detected.");
159
+ recommendations.push(t("cli.scan.recommendation.unknown-framework"));
209
160
  } else {
210
- recommendations.push(`L1: Review ${input.framework.kind} directories for future scoped AGENTS.md files.`);
161
+ recommendations.push(t("cli.scan.recommendation.framework-dirs", { framework: input.framework.kind }));
211
162
  }
212
163
  return recommendations;
213
164
  }
214
165
  function printPrettyReport(report, debug) {
215
- console.log("Fabric scan report");
216
- console.log(`Target: ${report.target}`);
217
- console.log(`Framework: ${report.framework.kind}`);
166
+ console.log(paint.ai(t("cli.scan.report.title")));
167
+ const rows = [
168
+ [t("cli.scan.report.target"), paint.human(report.target)],
169
+ [t("cli.scan.report.framework"), paint.ai(report.framework.kind)],
170
+ [
171
+ t("cli.scan.report.readme-quality"),
172
+ report.readmeQuality === "ok" ? paint.success(t("cli.scan.readme-quality.ok")) : paint.warn(t("cli.scan.readme-quality.stub"))
173
+ ],
174
+ [
175
+ t("cli.scan.report.contributing"),
176
+ report.hasContributing ? paint.success(t("cli.shared.present")) : paint.warn(t("cli.shared.absent"))
177
+ ],
178
+ [t("cli.scan.report.files-counted"), String(report.fileCount)],
179
+ [t("cli.scan.report.ignored-entries"), report.ignoredCount > 0 ? paint.muted(String(report.ignoredCount)) : "0"],
180
+ [
181
+ t("cli.scan.report.existing-fabric"),
182
+ report.hasExistingFabric ? paint.warn(t("cli.shared.yes")) : paint.success(t("cli.shared.no"))
183
+ ]
184
+ ];
218
185
  if (debug) {
219
- console.log(`Evidence: ${report.framework.evidence.length > 0 ? report.framework.evidence.join(", ") : "none"}`);
186
+ rows.splice(2, 0, [
187
+ t("cli.scan.report.evidence"),
188
+ report.framework.evidence.length > 0 ? paint.muted(report.framework.evidence.join(", ")) : paint.muted(t("cli.shared.none"))
189
+ ]);
190
+ }
191
+ const labelWidth = Math.max(...rows.map(([key]) => displayWidth(key)));
192
+ for (const [key, value] of rows) {
193
+ console.log(`${paint.muted(padEnd(key, labelWidth))} ${value}`);
220
194
  }
221
- console.log(`README quality: ${report.readmeQuality}`);
222
- console.log(`CONTRIBUTING.md: ${report.hasContributing ? "present" : "missing"}`);
223
- console.log(`Files counted: ${report.fileCount}`);
224
- console.log(`Ignored entries: ${report.ignoredCount}`);
225
- console.log(`Existing Fabric files: ${report.hasExistingFabric ? "yes" : "no"}`);
226
- console.log("Recommendations:");
195
+ console.log(paint.muted(t("cli.scan.report.recommendations")));
227
196
  for (const recommendation of report.recommendations) {
228
- console.log(`- ${recommendation}`);
197
+ console.log(`${symbol.warn} ${paint.drift(recommendation)}`);
229
198
  }
230
199
  }
231
200
 
232
201
  export {
202
+ detectFramework,
233
203
  createScanReport,
234
204
  scanCommand,
235
205
  scan_default
@@ -1,4 +1,7 @@
1
1
  #!/usr/bin/env node
2
+ import {
3
+ t
4
+ } from "./chunk-6ICJICVU.js";
2
5
 
3
6
  // src/commands/ledger-append.ts
4
7
  import { execSync } from "child_process";
@@ -10,17 +13,17 @@ var INITIAL_PARENT_SHA = "root";
10
13
  var ledgerAppendCommand = defineCommand({
11
14
  meta: {
12
15
  name: "ledger-append",
13
- description: "Append a Fabric intent ledger entry."
16
+ description: t("cli.ledger-append.description")
14
17
  },
15
18
  args: {
16
19
  target: {
17
20
  type: "string",
18
- description: "Target project path. Defaults to the current working directory.",
21
+ description: t("cli.ledger-append.args.target.description"),
19
22
  default: process.cwd()
20
23
  },
21
24
  staged: {
22
25
  type: "boolean",
23
- description: "Derive the entry from staged changes for pre-commit.",
26
+ description: t("cli.ledger-append.args.staged.description"),
24
27
  default: false
25
28
  }
26
29
  },
@@ -28,7 +31,7 @@ var ledgerAppendCommand = defineCommand({
28
31
  const target = normalizeTarget(args.target);
29
32
  assertExistingDirectory(target);
30
33
  if (!args.staged) {
31
- writeStderr("requires --staged in pre-commit context");
34
+ writeStderr(t("cli.ledger-append.requires-staged"));
32
35
  process.exitCode = 1;
33
36
  return;
34
37
  }
@@ -40,6 +43,7 @@ var ledgerAppendCommand = defineCommand({
40
43
  const diffStat = readDiffStat(target).trim();
41
44
  const entry = {
42
45
  ts: Date.now(),
46
+ source: "human",
43
47
  parent_sha: readParentSha(target),
44
48
  intent,
45
49
  affected_paths: stagedFiles,
@@ -59,7 +63,7 @@ function normalizeTarget(targetInput) {
59
63
  }
60
64
  function assertExistingDirectory(target) {
61
65
  if (!existsSync(target) || !statSync(target).isDirectory()) {
62
- throw new Error(`Target must be an existing directory: ${target}`);
66
+ throw new Error(t("cli.shared.target-invalid", { target }));
63
67
  }
64
68
  }
65
69
  function getStagedFiles(target) {
@@ -83,8 +87,8 @@ function deriveIntent(stagedFiles) {
83
87
  }
84
88
  const uniqueNames = Array.from(new Set(stagedFiles.map((file) => basename(file))));
85
89
  const head = uniqueNames.slice(0, 2).join(", ");
86
- const suffix = uniqueNames.length > 2 ? ` +${uniqueNames.length - 2} more` : "";
87
- return `auto: ${head}${suffix}`;
90
+ const suffix = uniqueNames.length > 2 ? t("cli.ledger-append.intent.auto-more", { count: String(uniqueNames.length - 2) }) : "";
91
+ return t("cli.ledger-append.intent.auto", { head, suffix });
88
92
  }
89
93
  function hasMatchingTailEntry(target, entry) {
90
94
  const ledgerPath = join(target, LEDGER_FILE);
@@ -1,4 +1,10 @@
1
1
  #!/usr/bin/env node
2
+ import {
3
+ padEnd
4
+ } from "./chunk-WWNXR34K.js";
5
+ import {
6
+ t
7
+ } from "./chunk-6ICJICVU.js";
2
8
 
3
9
  // src/commands/human-lint.ts
4
10
  import { createHash } from "crypto";
@@ -6,15 +12,16 @@ import { existsSync } from "fs";
6
12
  import { readFile } from "fs/promises";
7
13
  import { isAbsolute, join, resolve } from "path";
8
14
  import { defineCommand } from "citty";
15
+ import { humanLockEntrySchema } from "@fenglimg/fabric-shared";
9
16
  var humanLintCommand = defineCommand({
10
17
  meta: {
11
18
  name: "human-lint",
12
- description: "Validate locked human sections."
19
+ description: t("cli.human-lint.description")
13
20
  },
14
21
  args: {
15
22
  target: {
16
23
  type: "string",
17
- description: "Target project path. Defaults to the current working directory.",
24
+ description: t("cli.human-lint.args.target.description"),
18
25
  default: process.cwd()
19
26
  }
20
27
  },
@@ -60,11 +67,13 @@ var humanLintCommand = defineCommand({
60
67
  if (violations.length === 0) {
61
68
  return;
62
69
  }
63
- writeStderr("Human-locked content drift detected. Revert the edit or update approved hashes before committing.");
64
- writeStderr("Location Expected Got");
70
+ writeStderr(t("cli.human-lint.drift-detected"));
71
+ writeStderr(
72
+ `${padEnd(t("cli.human-lint.table.location"), 32)} ${padEnd(t("cli.human-lint.table.expected"), 18)} ${t("cli.human-lint.table.got")}`
73
+ );
65
74
  for (const violation of violations) {
66
75
  writeStderr(
67
- `${violation.location.padEnd(32)} ${violation.expected.padEnd(18)} ${violation.actual}`
76
+ `${padEnd(violation.location, 32)} ${padEnd(violation.expected, 18)} ${violation.actual}`
68
77
  );
69
78
  }
70
79
  process.exitCode = 1;
@@ -81,7 +90,7 @@ function hashLockedContent(content, entry) {
81
90
  }
82
91
  function shortenHash(value) {
83
92
  if (value === "missing") {
84
- return value;
93
+ return t("cli.shared.missing");
85
94
  }
86
95
  return value.slice(0, 15);
87
96
  }
@@ -92,5 +101,6 @@ function writeStderr(message) {
92
101
 
93
102
  export {
94
103
  humanLintCommand,
95
- human_lint_default
104
+ human_lint_default,
105
+ humanLockEntrySchema
96
106
  };
File without changes
@@ -89,9 +89,15 @@ var ClaudeCodeCLIWriter = class extends JsonClientConfigWriter {
89
89
  constructor(configuredPath) {
90
90
  super(configuredPath);
91
91
  }
92
- defaultPath() {
93
- const claudeDir = join(homedir(), ".claude");
94
- return existsSync(claudeDir) ? join(claudeDir, "settings.json") : null;
92
+ // Writes to project-level .claude/settings.json so MCP is scoped to the project.
93
+ // Detection in resolver still checks ~/ to confirm Claude Code is installed.
94
+ defaultPath(workspaceRoot) {
95
+ const globalClaudeDir = join(homedir(), ".claude");
96
+ const projectClaudeDir = join(workspaceRoot, ".claude");
97
+ if (!existsSync(globalClaudeDir) && !existsSync(projectClaudeDir)) {
98
+ return null;
99
+ }
100
+ return join(projectClaudeDir, "settings.json");
95
101
  }
96
102
  };
97
103
  var CursorWriter = class extends JsonClientConfigWriter {
@@ -234,6 +240,7 @@ var CodexTOMLConfigWriter = class {
234
240
  };
235
241
 
236
242
  // src/config/resolver.ts
243
+ import { clientPathsSchema, fabricConfigSchema } from "@fenglimg/fabric-shared";
237
244
  function hasExplicitPath(clientPaths, key) {
238
245
  return typeof clientPaths?.[key] === "string" && clientPaths[key].trim().length > 0;
239
246
  }
@@ -247,7 +254,7 @@ function resolveClients(workspaceRoot, fabricConfig = {}) {
247
254
  const writers = [];
248
255
  addIfDetected(
249
256
  writers,
250
- existsSync4(join4(homedir4(), ".claude")),
257
+ existsSync4(join4(homedir4(), ".claude")) || existsSync4(join4(workspaceRoot, ".claude")),
251
258
  (configuredPath) => new ClaudeCodeCLIWriter(configuredPath),
252
259
  hasExplicitPath(clientPaths, "claudeCodeCLI") ? clientPaths.claudeCodeCLI : void 0
253
260
  );
@@ -0,0 +1,49 @@
1
+ #!/usr/bin/env node
2
+
3
+ // src/colors.ts
4
+ import pc from "picocolors";
5
+ import stringWidth from "string-width";
6
+ function isColorEnabled() {
7
+ return !process.env.NO_COLOR && Boolean(process.stdout.isTTY) && Boolean(process.stderr.isTTY);
8
+ }
9
+ function colorize(painter) {
10
+ return (value) => isColorEnabled() ? painter(value) : value;
11
+ }
12
+ var paint = {
13
+ success: colorize(pc.green),
14
+ warn: colorize(pc.yellow),
15
+ error: colorize(pc.red),
16
+ drift: colorize(pc.magenta),
17
+ ai: colorize(pc.blue),
18
+ human: colorize(pc.cyan),
19
+ muted: colorize(pc.dim)
20
+ };
21
+ var symbol = {
22
+ get ok() {
23
+ return isColorEnabled() ? paint.success("\u2713") : "[ok]";
24
+ },
25
+ get warn() {
26
+ return isColorEnabled() ? paint.warn("!") : "[warn]";
27
+ },
28
+ get error() {
29
+ return isColorEnabled() ? paint.error("x") : "[error]";
30
+ }
31
+ };
32
+ function displayWidth(value) {
33
+ return stringWidth(value);
34
+ }
35
+ function padEnd(value, width, char = " ") {
36
+ const fill = char.length > 0 ? char : " ";
37
+ let result = value;
38
+ while (displayWidth(result) < width) {
39
+ result += fill;
40
+ }
41
+ return result;
42
+ }
43
+
44
+ export {
45
+ paint,
46
+ symbol,
47
+ displayWidth,
48
+ padEnd
49
+ };