@fenglimg/fabric-cli 2.0.0-rc.13 → 2.0.0-rc.21

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/README.md +4 -2
  2. package/dist/{chunk-X7QPY5KH.js → chunk-4HC5ZK7H.js} +296 -301
  3. package/dist/{chunk-FDRLV5PL.js → chunk-FNO7CQDG.js} +5 -213
  4. package/dist/{chunk-WWNXR34K.js → chunk-G2CIOLD4.js} +16 -1
  5. package/dist/chunk-KZ2YITOS.js +225 -0
  6. package/dist/{chunk-OHWQNSLH.js → chunk-MF3OTILQ.js} +267 -44
  7. package/dist/{chunk-OBQU6NHO.js → chunk-ZSESMG6L.js} +0 -6
  8. package/dist/config-AYP5F72E.js +13 -0
  9. package/dist/doctor-L6TIXXIX.js +425 -0
  10. package/dist/index.js +11 -9
  11. package/dist/{install-SLS5W27W.js → install-DNZXGFHJ.js} +344 -359
  12. package/dist/{plan-context-hint-QMUPAXIB.js → plan-context-hint-CFDGXHCA.js} +10 -5
  13. package/dist/{serve-NGLXHDYC.js → serve-6PPQX7AW.js} +16 -11
  14. package/dist/{uninstall-JHUSFENL.js → uninstall-L2HEEOU3.js} +200 -215
  15. package/package.json +3 -3
  16. package/templates/hooks/configs/README.md +9 -5
  17. package/templates/hooks/configs/cursor-hooks.json +7 -10
  18. package/templates/hooks/fabric-hint.cjs +350 -21
  19. package/templates/hooks/knowledge-hint-broad.cjs +39 -14
  20. package/templates/hooks/knowledge-hint-narrow.cjs +31 -7
  21. package/templates/hooks/lib/banner-i18n.cjs +252 -0
  22. package/dist/chunk-Q72D24BG.js +0 -186
  23. package/dist/doctor-RILCO5OG.js +0 -282
  24. package/dist/hooks-HIWYI3VG.js +0 -13
  25. package/dist/scan-VHKZPT2W.js +0 -24
  26. package/templates/agents-md/AGENTS.md.template +0 -59
  27. package/templates/bootstrap/CLAUDE.md +0 -8
  28. package/templates/bootstrap/codex-AGENTS-header.md +0 -6
  29. package/templates/bootstrap/cursor-fabric-bootstrap.mdc +0 -10
@@ -1,23 +1,16 @@
1
1
  #!/usr/bin/env node
2
- import {
3
- paint,
4
- symbol
5
- } from "./chunk-WWNXR34K.js";
6
- import {
7
- createDebugLogger,
8
- readFabricConfig,
9
- resolveDevMode
10
- } from "./chunk-OBQU6NHO.js";
11
2
  import {
12
3
  t
13
4
  } from "./chunk-6ICJICVU.js";
5
+ import {
6
+ readFabricConfig
7
+ } from "./chunk-ZSESMG6L.js";
14
8
 
15
9
  // src/commands/scan.ts
16
10
  import { createHash } from "crypto";
17
11
  import { existsSync, readdirSync, readFileSync, statSync } from "fs";
18
12
  import { mkdir, readFile } from "fs/promises";
19
- import { dirname, isAbsolute, join, relative, resolve, sep } from "path";
20
- import { defineCommand } from "citty";
13
+ import { dirname, isAbsolute, join, resolve } from "path";
21
14
  import {
22
15
  KnowledgeIdAllocator,
23
16
  appendEventLedgerEvent,
@@ -27,51 +20,6 @@ import {
27
20
  formatKnowledgeId
28
21
  } from "@fenglimg/fabric-shared";
29
22
  import { atomicWriteJson, atomicWriteText } from "@fenglimg/fabric-shared/node/atomic-write";
30
-
31
- // src/scanner/detector.ts
32
- import { detectFramework } from "@fenglimg/fabric-shared/node";
33
-
34
- // src/scanner/ignores.ts
35
- var DEFAULT_IGNORES = [
36
- "**/*.meta",
37
- "library/**",
38
- "temp/**",
39
- "build/**",
40
- "settings/**",
41
- "profiles/**",
42
- "node_modules/**",
43
- "dist/**",
44
- ".git/**",
45
- ".fabric/**"
46
- ];
47
- function resolveIgnores(fabricConfig) {
48
- return [...DEFAULT_IGNORES, ...fabricConfig?.scanIgnores ?? []];
49
- }
50
-
51
- // src/commands/scan.ts
52
- async function createScanReport(targetInput = process.cwd(), fabricConfig) {
53
- const target = normalizeTarget(targetInput);
54
- const framework = detectFramework(target);
55
- const readmeQuality = getReadmeQuality(target);
56
- const hasContributing = existsSync(join(target, "CONTRIBUTING.md"));
57
- const hasExistingFabric = existsSync(join(target, ".fabric", "bootstrap", "README.md")) || existsSync(join(target, ".fabric"));
58
- const walkResult = walkFiles(target, resolveIgnores(fabricConfig));
59
- return {
60
- target,
61
- framework,
62
- readmeQuality,
63
- hasContributing,
64
- fileCount: walkResult.fileCount,
65
- ignoredCount: walkResult.ignoredCount,
66
- hasExistingFabric,
67
- recommendations: buildRecommendations({
68
- framework,
69
- readmeQuality,
70
- hasContributing,
71
- hasExistingFabric
72
- })
73
- };
74
- }
75
23
  var KNOWLEDGE_DIR = ".fabric/knowledge";
76
24
  var SCAN_STATE_FILE = ".scan-state.json";
77
25
  var FORENSIC_FILE = ".fabric/forensic.json";
@@ -142,50 +90,6 @@ async function runInitScan(targetInput, options = {}) {
142
90
  duration_ms: durationMs
143
91
  };
144
92
  }
145
- var scanCommand = defineCommand({
146
- meta: {
147
- name: "scan",
148
- description: t("cli.scan.description")
149
- },
150
- args: {
151
- target: {
152
- type: "string",
153
- description: t("cli.scan.args.target.description")
154
- },
155
- debug: {
156
- type: "boolean",
157
- description: t("cli.scan.args.debug.description"),
158
- default: false
159
- },
160
- json: {
161
- type: "boolean",
162
- description: t("cli.scan.args.json.description"),
163
- default: false
164
- }
165
- },
166
- async run({ args }) {
167
- const workspaceRoot = process.cwd();
168
- const logger = createDebugLogger(args.debug);
169
- const resolution = resolveDevMode(args.target, workspaceRoot);
170
- logger(`scan target source: ${resolution.source}`);
171
- for (const step of resolution.chain) {
172
- logger(step);
173
- }
174
- try {
175
- const result = await runInitScan(resolution.target, { source: "scan" });
176
- if (args.json) {
177
- console.log(JSON.stringify(result, null, 2));
178
- return;
179
- }
180
- printPrettyResult(result);
181
- } catch (error) {
182
- const message = error instanceof Error ? error.message : String(error);
183
- console.error(`${symbol.warn} ${paint.warn(message)}`);
184
- process.exitCode = 1;
185
- }
186
- }
187
- });
188
- var scan_default = scanCommand;
189
93
  var STRICT_BASELINE_TEMPLATES = {
190
94
  en: {
191
95
  "tech-stack": {
@@ -884,123 +788,11 @@ async function registerKnowledgeNodesInMeta(target, entries) {
884
788
  async function ensureParentDirectory(filePath) {
885
789
  await mkdir(dirname(filePath), { recursive: true });
886
790
  }
887
- function printPrettyResult(result) {
888
- const writtenCount = result.written_stable_ids.length;
889
- const skippedCount = result.skipped_stable_ids.length;
890
- if (writtenCount === 0) {
891
- console.log(`${symbol.ok} ${paint.success(t("cli.scan.summary.skipped", { count: String(skippedCount) }))}`);
892
- return;
893
- }
894
- console.log(`${symbol.ok} ${paint.success(t("cli.scan.summary.created", { count: String(writtenCount) }))}`);
895
- for (const id of result.written_stable_ids) {
896
- console.log(` - ${paint.ai(id)}`);
897
- }
898
- if (skippedCount > 0) {
899
- console.log(paint.muted(`(${skippedCount} unchanged, skipped)`));
900
- }
901
- }
902
791
  function normalizeTarget(targetInput) {
903
792
  return isAbsolute(targetInput) ? targetInput : resolve(process.cwd(), targetInput);
904
793
  }
905
- function getReadmeQuality(target) {
906
- const readmePath = join(target, "README.md");
907
- if (!existsSync(readmePath)) {
908
- return "stub";
909
- }
910
- const wordCount = readFileSync(readmePath, "utf8").trim().split(/\s+/).filter(Boolean).length;
911
- return wordCount >= 200 ? "ok" : "stub";
912
- }
913
- function walkFiles(root, ignorePatterns) {
914
- if (!existsSync(root) || !statSync(root).isDirectory()) {
915
- throw new Error(t("cli.shared.target-invalid", { target: root }));
916
- }
917
- let fileCount = 0;
918
- let ignoredCount = 0;
919
- const stack = [root];
920
- while (stack.length > 0) {
921
- const current = stack.pop();
922
- if (current === void 0) {
923
- continue;
924
- }
925
- for (const entry of readdirSync(current, { withFileTypes: true })) {
926
- const absolutePath = join(current, entry.name);
927
- const relativePath = toPosixPath(relative(root, absolutePath));
928
- if (shouldIgnore(relativePath, entry.isDirectory(), ignorePatterns)) {
929
- ignoredCount += 1;
930
- continue;
931
- }
932
- if (entry.isDirectory()) {
933
- stack.push(absolutePath);
934
- } else if (entry.isFile()) {
935
- fileCount += 1;
936
- }
937
- }
938
- }
939
- return { fileCount, ignoredCount };
940
- }
941
- function shouldIgnore(relativePath, isDirectory, ignorePatterns) {
942
- return ignorePatterns.some((pattern) => matchesIgnorePattern(relativePath, isDirectory, pattern));
943
- }
944
- function matchesIgnorePattern(relativePath, isDirectory, pattern) {
945
- const normalizedPattern = toPosixPath(pattern);
946
- if (normalizedPattern === "**/*.meta") {
947
- return relativePath.endsWith(".meta");
948
- }
949
- if (normalizedPattern.endsWith("/**")) {
950
- const directoryPrefix = normalizedPattern.slice(0, -3);
951
- return relativePath === directoryPrefix || relativePath.startsWith(`${directoryPrefix}/`) || isDirectory && `${relativePath}/` === directoryPrefix;
952
- }
953
- return relativePath === normalizedPattern;
954
- }
955
- function toPosixPath(path) {
956
- return path.split(sep).join("/");
957
- }
958
- function buildRecommendations(input) {
959
- const recommendations = [];
960
- if (!input.hasExistingFabric) {
961
- recommendations.push(t("cli.scan.recommendation.init"));
962
- }
963
- if (input.readmeQuality === "stub") {
964
- recommendations.push(t("cli.scan.recommendation.readme"));
965
- }
966
- if (!input.hasContributing) {
967
- recommendations.push(t("cli.scan.recommendation.contributing"));
968
- }
969
- if (input.framework.kind === "unknown") {
970
- recommendations.push(t("cli.scan.recommendation.unknown-framework"));
971
- } else {
972
- recommendations.push(t("cli.scan.recommendation.framework-dirs", { framework: input.framework.kind }));
973
- }
974
- return recommendations;
975
- }
976
- var __testing__ = {
977
- buildTechStackEntry,
978
- buildModuleStructureEntry,
979
- buildBuildConfigEntry,
980
- buildCodeStyleEntry,
981
- buildCIConfigEntry,
982
- buildReadmeFirstParaEntry,
983
- buildProjectBriefEntry,
984
- renderMarkdown,
985
- stripFrontmatter,
986
- isCIConfigPath,
987
- isBuildConfigPath,
988
- extractFirstParagraph,
989
- extractExplicitDescription,
990
- // TASK-008 / rc.12: bilingual template registry + language detection
991
- detectExistingLanguage,
992
- resolveFabricLanguage,
993
- BASELINE_TEMPLATES
994
- };
995
794
 
996
795
  export {
997
- detectFramework,
998
- formatKnowledgeId,
999
- createScanReport,
1000
796
  runInitScan,
1001
- scanCommand,
1002
- scan_default,
1003
- detectExistingLanguage,
1004
- deriveTagsFromForensic,
1005
- __testing__
797
+ detectExistingLanguage
1006
798
  };
@@ -41,9 +41,24 @@ function padEnd(value, width, char = " ") {
41
41
  return result;
42
42
  }
43
43
 
44
+ // src/lib/error-render.ts
45
+ function hasActionHint(err) {
46
+ if (err === null || typeof err !== "object") return false;
47
+ const candidate = err;
48
+ return typeof candidate.message === "string" && candidate.message.length > 0 && typeof candidate.actionHint === "string" && candidate.actionHint.length > 0;
49
+ }
50
+ function renderFabricError(err, stream = process.stderr) {
51
+ stream.write(`${err.message}
52
+ `);
53
+ stream.write(` -> ${err.actionHint}
54
+ `);
55
+ }
56
+
44
57
  export {
45
58
  paint,
46
59
  symbol,
47
60
  displayWidth,
48
- padEnd
61
+ padEnd,
62
+ hasActionHint,
63
+ renderFabricError
49
64
  };
@@ -0,0 +1,225 @@
1
+ #!/usr/bin/env node
2
+ import {
3
+ resolveClients
4
+ } from "./chunk-MF3OTILQ.js";
5
+ import {
6
+ t
7
+ } from "./chunk-6ICJICVU.js";
8
+
9
+ // src/commands/config.ts
10
+ import { existsSync, statSync } from "fs";
11
+ import { readFile } from "fs/promises";
12
+ import { join, resolve } from "path";
13
+ import { fileURLToPath } from "url";
14
+ import { cancel, intro, isCancel, log, outro, select, text } from "@clack/prompts";
15
+ import { getPanelFields } from "@fenglimg/fabric-shared";
16
+ import { atomicWriteJson } from "@fenglimg/fabric-shared/node/atomic-write";
17
+ import { defineCommand } from "citty";
18
+ async function loadFabricConfig(workspaceRoot) {
19
+ const configPath = resolve(workspaceRoot, "fabric.config.json");
20
+ if (!existsSync(configPath)) {
21
+ return {};
22
+ }
23
+ const parsed = JSON.parse(await readFile(configPath, "utf8"));
24
+ if (parsed === null || typeof parsed !== "object" || Array.isArray(parsed)) {
25
+ throw new Error(t("cli.config.errors.expected-object", { path: configPath }));
26
+ }
27
+ return parsed;
28
+ }
29
+ function resolveServerPath(override) {
30
+ if (override) return override;
31
+ if (process.env.FAB_SERVER_PATH) return resolve(process.env.FAB_SERVER_PATH);
32
+ return fileURLToPath(import.meta.resolve("@fenglimg/fabric-server"));
33
+ }
34
+ var PANEL_CONFIG_RELATIVE_PATH = [".fabric", "fabric-config.json"];
35
+ var EXIT_CHOICE = "__exit__";
36
+ var configCmd = defineCommand({
37
+ meta: {
38
+ name: "config",
39
+ description: t("cli.config.description")
40
+ },
41
+ args: {
42
+ target: {
43
+ type: "string",
44
+ description: t("cli.config.args.target.description"),
45
+ valueHint: "path"
46
+ }
47
+ },
48
+ async run({ args }) {
49
+ const workspaceRoot = resolve(args.target ?? process.cwd());
50
+ const configPath = join(workspaceRoot, ...PANEL_CONFIG_RELATIVE_PATH);
51
+ const fabricDir = join(workspaceRoot, ".fabric");
52
+ const fabricDirOk = existsSync(fabricDir) && statSync(fabricDir).isDirectory();
53
+ const configOk = fabricDirOk && existsSync(configPath);
54
+ if (!configOk) {
55
+ console.error(t("cli.config.errors.uninit-workspace.message"));
56
+ process.exitCode = 1;
57
+ return;
58
+ }
59
+ if (!isInteractiveConfig()) {
60
+ console.log(t("cli.config.intro"));
61
+ console.log(t("cli.config.non-tty-notice"));
62
+ return;
63
+ }
64
+ intro(t("cli.config.intro"));
65
+ let edited = false;
66
+ while (true) {
67
+ const current = await readPanelConfig(configPath);
68
+ const fields = getPanelFields();
69
+ const fieldChoice = await select({
70
+ message: t("cli.config.menu.field-select"),
71
+ options: [
72
+ ...fields.map((field2) => ({
73
+ value: field2.key,
74
+ label: formatFieldMenuLabel(field2, current)
75
+ })),
76
+ { value: EXIT_CHOICE, label: t("cli.config.menu.exit") }
77
+ ]
78
+ });
79
+ if (isCancel(fieldChoice)) {
80
+ cancel(t("cli.config.cancel"));
81
+ return;
82
+ }
83
+ if (fieldChoice === EXIT_CHOICE) {
84
+ outro(edited ? t("cli.config.outro") : t("cli.config.outro-no-changes"));
85
+ return;
86
+ }
87
+ const field = fields.find((f) => f.key === fieldChoice);
88
+ if (!field) {
89
+ log.warn(t("cli.config.errors.unknown-field"));
90
+ continue;
91
+ }
92
+ const newValue = await promptFieldValue(field, current);
93
+ if (newValue === CANCELLED) {
94
+ cancel(t("cli.config.cancel"));
95
+ return;
96
+ }
97
+ if (newValue === SKIPPED) {
98
+ continue;
99
+ }
100
+ try {
101
+ const refreshed = await readPanelConfig(configPath);
102
+ const merged = { ...refreshed, [field.key]: newValue };
103
+ await atomicWriteJson(configPath, merged);
104
+ edited = true;
105
+ log.success(
106
+ t("cli.config.write.success", {
107
+ key: field.key,
108
+ value: field.format_for_display(newValue)
109
+ })
110
+ );
111
+ } catch (err) {
112
+ const message = err instanceof Error ? err.message : String(err);
113
+ log.error(t("cli.config.write.failure", { message }));
114
+ }
115
+ }
116
+ }
117
+ });
118
+ var config_default = configCmd;
119
+ var CANCELLED = /* @__PURE__ */ Symbol("config-cancelled");
120
+ var SKIPPED = /* @__PURE__ */ Symbol("config-skipped");
121
+ async function promptFieldValue(field, current) {
122
+ const currentValue = current[field.key];
123
+ const currentDisplay = field.format_for_display(currentValue);
124
+ if (field.widget === "select") {
125
+ const enumValues = field.enum_values ?? [];
126
+ if (enumValues.length === 0) {
127
+ log.warn(t("cli.config.errors.no-enum-options"));
128
+ return SKIPPED;
129
+ }
130
+ const initialValue = enumValues.includes(String(currentValue)) ? String(currentValue) : enumValues.includes(String(field.default)) ? String(field.default) : enumValues[0];
131
+ const picked = await select({
132
+ message: t("cli.config.prompt.select", {
133
+ key: field.key,
134
+ current: currentDisplay
135
+ }),
136
+ options: enumValues.map((value) => ({ value, label: value })),
137
+ initialValue
138
+ });
139
+ if (isCancel(picked)) {
140
+ return CANCELLED;
141
+ }
142
+ const result = field.validate(String(picked));
143
+ if (!result.ok) {
144
+ log.error(result.error);
145
+ return SKIPPED;
146
+ }
147
+ return result.value;
148
+ }
149
+ const entered = await text({
150
+ message: t("cli.config.prompt.text", {
151
+ key: field.key,
152
+ current: currentDisplay
153
+ }),
154
+ placeholder: currentDisplay,
155
+ initialValue: currentDisplay,
156
+ validate(raw) {
157
+ const result = field.validate(raw ?? "");
158
+ return result.ok ? void 0 : result.error;
159
+ }
160
+ });
161
+ if (isCancel(entered)) {
162
+ return CANCELLED;
163
+ }
164
+ const finalResult = field.validate(String(entered));
165
+ if (!finalResult.ok) {
166
+ log.error(finalResult.error);
167
+ return SKIPPED;
168
+ }
169
+ return finalResult.value;
170
+ }
171
+ function formatFieldMenuLabel(field, current) {
172
+ const key = field.key;
173
+ const rawValue = current[key];
174
+ const display = field.format_for_display(rawValue);
175
+ const isDefault = rawValue === void 0 || rawValue === null;
176
+ const labelText = t(field.label_i18n_key);
177
+ const valueLabel = isDefault ? `${display} ${t("cli.config.value.default-marker")}` : display;
178
+ return `[${field.group}] ${key} (${labelText}) \u2014 ${t("cli.config.value.current", { value: valueLabel })}`;
179
+ }
180
+ async function readPanelConfig(configPath) {
181
+ const raw = await readFile(configPath, "utf8");
182
+ const parsed = JSON.parse(raw);
183
+ if (parsed === null || typeof parsed !== "object" || Array.isArray(parsed)) {
184
+ throw new Error(t("cli.config.errors.expected-object", { path: configPath }));
185
+ }
186
+ return parsed;
187
+ }
188
+ function isInteractiveConfig() {
189
+ return Boolean(process.stdin.isTTY) && Boolean(process.stdout.isTTY) && Boolean(process.stderr.isTTY);
190
+ }
191
+ async function installMcpClients(target, options = {}) {
192
+ const workspaceRoot = resolve(target);
193
+ const fabricConfig = await loadFabricConfig(workspaceRoot);
194
+ const selectedClients = options.clients === void 0 ? null : new Set(options.clients);
195
+ const serverPath = resolveServerPath(options.localServerPath);
196
+ const writers = resolveClients(workspaceRoot, fabricConfig, { claudeMcpScope: options.claudeMcpScope }).filter(
197
+ (writer) => selectedClients === null ? true : selectedClients.has(writer.clientKind)
198
+ );
199
+ const installed = [];
200
+ const skipped = [];
201
+ const details = [];
202
+ for (const writer of writers) {
203
+ const configPath = await writer.detect(workspaceRoot);
204
+ if (configPath === null) {
205
+ skipped.push(writer.clientKind);
206
+ details.push({ client: writer.clientKind, path: null, action: "skipped" });
207
+ continue;
208
+ }
209
+ if (options.dryRun) {
210
+ skipped.push(writer.clientKind);
211
+ details.push({ client: writer.clientKind, path: configPath, action: "dry-run" });
212
+ continue;
213
+ }
214
+ await writer.write(serverPath, workspaceRoot);
215
+ installed.push(writer.clientKind);
216
+ details.push({ client: writer.clientKind, path: configPath, action: "wrote" });
217
+ }
218
+ return { installed, skipped, details };
219
+ }
220
+
221
+ export {
222
+ configCmd,
223
+ config_default,
224
+ installMcpClients
225
+ };