@fenglimg/fabric-cli 2.0.0-rc.11 → 2.0.0-rc.15

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 CHANGED
@@ -6,19 +6,21 @@
6
6
 
7
7
  1. 在 monorepo 根目录运行 `pnpm install`。
8
8
  2. 用 `pnpm --filter @fenglimg/fabric-cli build` 构建 CLI。
9
- 3. 在目标项目运行 `fabric init`,完成一站式初始化。
9
+ 3. 在目标项目运行 `fabric install`,完成一站式安装。
10
10
  4. 启动 `fabric serve`,再去客户端里验证 `fab_plan_context` 和 `fab_get_knowledge_sections`。
11
11
 
12
- `fabric init` 会自动准备 bootstrap、MCP 配置和 git hooks。公共命令面只保留 `init`、`scan`、`doctor`、`serve`。
12
+ `fabric install` 会自动准备 bootstrap、MCP 配置和 git hooks。公共命令面只保留 `install`、`doctor`、`serve`、`uninstall`、`config`(rc.15 起 `fab scan` 已折叠到 `fab doctor --rescan`)。
13
13
 
14
14
  ## 常用命令
15
15
 
16
- - `fabric init`
17
- - `fabric scan`
16
+ - `fabric install`
18
17
  - `fabric doctor`
19
18
  - `fabric doctor --json`
20
19
  - `fabric doctor --strict`
21
20
  - `fabric doctor --fix`
21
+ - `fabric doctor --rescan`(替代旧的 `fabric scan`)
22
22
  - `fabric serve`
23
+ - `fabric uninstall`
24
+ - `fabric config`(rc.16 起将提供配置面板;当前为占位提示)
23
25
 
24
26
  `fabric doctor --fix` 只修复确定性的派生状态,例如 `.fabric/agents.meta.json`、`.fabric/.cache/knowledge-test.index.json`、缺失的 `.fabric/events.jsonl` 和 stale hashes;语义冲突、缺失 rule section、未完成的初始化确认仍需要人工处理。
@@ -0,0 +1,82 @@
1
+ #!/usr/bin/env node
2
+ import {
3
+ resolveClients
4
+ } from "./chunk-SKSYUHKK.js";
5
+ import {
6
+ t
7
+ } from "./chunk-6ICJICVU.js";
8
+
9
+ // src/commands/config.ts
10
+ import { existsSync } from "fs";
11
+ import { readFile } from "fs/promises";
12
+ import { resolve } from "path";
13
+ import { fileURLToPath } from "url";
14
+ import { defineCommand } from "citty";
15
+ async function loadFabricConfig(workspaceRoot) {
16
+ const configPath = resolve(workspaceRoot, "fabric.config.json");
17
+ if (!existsSync(configPath)) {
18
+ return {};
19
+ }
20
+ const parsed = JSON.parse(await readFile(configPath, "utf8"));
21
+ if (parsed === null || typeof parsed !== "object" || Array.isArray(parsed)) {
22
+ throw new Error(t("cli.config.errors.expected-object", { path: configPath }));
23
+ }
24
+ return parsed;
25
+ }
26
+ function resolveServerPath(override) {
27
+ if (override) return override;
28
+ if (process.env.FAB_SERVER_PATH) return resolve(process.env.FAB_SERVER_PATH);
29
+ return fileURLToPath(import.meta.resolve("@fenglimg/fabric-server"));
30
+ }
31
+ var configCmd = defineCommand({
32
+ meta: {
33
+ name: "config",
34
+ description: t("cli.config.description")
35
+ },
36
+ args: {
37
+ target: {
38
+ type: "string",
39
+ description: t("cli.config.args.target.description"),
40
+ valueHint: "path"
41
+ }
42
+ },
43
+ async run(_ctx) {
44
+ console.log(t("cli.config.placeholder"));
45
+ }
46
+ });
47
+ var config_default = configCmd;
48
+ async function installMcpClients(target, options = {}) {
49
+ const workspaceRoot = resolve(target);
50
+ const fabricConfig = await loadFabricConfig(workspaceRoot);
51
+ const selectedClients = options.clients === void 0 ? null : new Set(options.clients);
52
+ const serverPath = resolveServerPath(options.localServerPath);
53
+ const writers = resolveClients(workspaceRoot, fabricConfig, { claudeMcpScope: options.claudeMcpScope }).filter(
54
+ (writer) => selectedClients === null ? true : selectedClients.has(writer.clientKind)
55
+ );
56
+ const installed = [];
57
+ const skipped = [];
58
+ const details = [];
59
+ for (const writer of writers) {
60
+ const configPath = await writer.detect(workspaceRoot);
61
+ if (configPath === null) {
62
+ skipped.push(writer.clientKind);
63
+ details.push({ client: writer.clientKind, path: null, action: "skipped" });
64
+ continue;
65
+ }
66
+ if (options.dryRun) {
67
+ skipped.push(writer.clientKind);
68
+ details.push({ client: writer.clientKind, path: configPath, action: "dry-run" });
69
+ continue;
70
+ }
71
+ await writer.write(serverPath, workspaceRoot);
72
+ installed.push(writer.clientKind);
73
+ details.push({ client: writer.clientKind, path: configPath, action: "wrote" });
74
+ }
75
+ return { installed, skipped, details };
76
+ }
77
+
78
+ export {
79
+ configCmd,
80
+ config_default,
81
+ installMcpClients
82
+ };
@@ -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-OBQU6NHO.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";
@@ -88,8 +36,8 @@ async function runInitScan(targetInput, options = {}) {
88
36
  const nowIso = (options.now ?? /* @__PURE__ */ new Date()).toISOString();
89
37
  const tags = deriveTagsFromForensic(forensic);
90
38
  const fabricConfig = readFabricConfig(target);
91
- const knowledgeLanguage = fabricConfig.knowledge_language ?? "match-existing";
92
- const resolvedLanguage = resolveKnowledgeLanguage(knowledgeLanguage, target);
39
+ const fabricLanguage = fabricConfig.fabric_language ?? "match-existing";
40
+ const resolvedLanguage = resolveFabricLanguage(fabricLanguage, target);
93
41
  const candidates = [
94
42
  buildTechStackEntry(forensic, nowIso, tags, resolvedLanguage),
95
43
  buildModuleStructureEntry(forensic, nowIso, tags, resolvedLanguage),
@@ -142,51 +90,7 @@ 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
- var BASELINE_TEMPLATES = {
93
+ var STRICT_BASELINE_TEMPLATES = {
190
94
  en: {
191
95
  "tech-stack": {
192
96
  title: ({ frameworkSummary }) => `Tech stack: ${frameworkSummary}`,
@@ -336,11 +240,16 @@ var BASELINE_TEMPLATES = {
336
240
  }
337
241
  }
338
242
  };
243
+ var BASELINE_TEMPLATES = {
244
+ en: STRICT_BASELINE_TEMPLATES.en,
245
+ "zh-CN": STRICT_BASELINE_TEMPLATES["zh-CN"],
246
+ "zh-CN-hybrid": STRICT_BASELINE_TEMPLATES["zh-CN"]
247
+ };
339
248
  function resolveTemplateTitle(template, inputs) {
340
249
  return typeof template.title === "function" ? template.title(inputs) : template.title;
341
250
  }
342
- function resolveKnowledgeLanguage(configured, target) {
343
- if (configured === "en" || configured === "zh-CN") {
251
+ function resolveFabricLanguage(configured, target) {
252
+ if (configured === "en" || configured === "zh-CN" || configured === "zh-CN-hybrid") {
344
253
  return configured;
345
254
  }
346
255
  return detectExistingLanguage(target);
@@ -392,7 +301,7 @@ function detectExistingLanguage(target) {
392
301
  return "en";
393
302
  }
394
303
  const ratio = cjkCount / denominator;
395
- return ratio > ZH_CN_RATIO_THRESHOLD ? "zh-CN" : "en";
304
+ return ratio > ZH_CN_RATIO_THRESHOLD ? "zh-CN-hybrid" : "en";
396
305
  }
397
306
  function buildTechStackEntry(forensic, nowIso, tags, language = "en") {
398
307
  const framework = forensic.framework;
@@ -879,123 +788,11 @@ async function registerKnowledgeNodesInMeta(target, entries) {
879
788
  async function ensureParentDirectory(filePath) {
880
789
  await mkdir(dirname(filePath), { recursive: true });
881
790
  }
882
- function printPrettyResult(result) {
883
- const writtenCount = result.written_stable_ids.length;
884
- const skippedCount = result.skipped_stable_ids.length;
885
- if (writtenCount === 0) {
886
- console.log(`${symbol.ok} ${paint.success(t("cli.scan.summary.skipped", { count: String(skippedCount) }))}`);
887
- return;
888
- }
889
- console.log(`${symbol.ok} ${paint.success(t("cli.scan.summary.created", { count: String(writtenCount) }))}`);
890
- for (const id of result.written_stable_ids) {
891
- console.log(` - ${paint.ai(id)}`);
892
- }
893
- if (skippedCount > 0) {
894
- console.log(paint.muted(`(${skippedCount} unchanged, skipped)`));
895
- }
896
- }
897
791
  function normalizeTarget(targetInput) {
898
792
  return isAbsolute(targetInput) ? targetInput : resolve(process.cwd(), targetInput);
899
793
  }
900
- function getReadmeQuality(target) {
901
- const readmePath = join(target, "README.md");
902
- if (!existsSync(readmePath)) {
903
- return "stub";
904
- }
905
- const wordCount = readFileSync(readmePath, "utf8").trim().split(/\s+/).filter(Boolean).length;
906
- return wordCount >= 200 ? "ok" : "stub";
907
- }
908
- function walkFiles(root, ignorePatterns) {
909
- if (!existsSync(root) || !statSync(root).isDirectory()) {
910
- throw new Error(t("cli.shared.target-invalid", { target: root }));
911
- }
912
- let fileCount = 0;
913
- let ignoredCount = 0;
914
- const stack = [root];
915
- while (stack.length > 0) {
916
- const current = stack.pop();
917
- if (current === void 0) {
918
- continue;
919
- }
920
- for (const entry of readdirSync(current, { withFileTypes: true })) {
921
- const absolutePath = join(current, entry.name);
922
- const relativePath = toPosixPath(relative(root, absolutePath));
923
- if (shouldIgnore(relativePath, entry.isDirectory(), ignorePatterns)) {
924
- ignoredCount += 1;
925
- continue;
926
- }
927
- if (entry.isDirectory()) {
928
- stack.push(absolutePath);
929
- } else if (entry.isFile()) {
930
- fileCount += 1;
931
- }
932
- }
933
- }
934
- return { fileCount, ignoredCount };
935
- }
936
- function shouldIgnore(relativePath, isDirectory, ignorePatterns) {
937
- return ignorePatterns.some((pattern) => matchesIgnorePattern(relativePath, isDirectory, pattern));
938
- }
939
- function matchesIgnorePattern(relativePath, isDirectory, pattern) {
940
- const normalizedPattern = toPosixPath(pattern);
941
- if (normalizedPattern === "**/*.meta") {
942
- return relativePath.endsWith(".meta");
943
- }
944
- if (normalizedPattern.endsWith("/**")) {
945
- const directoryPrefix = normalizedPattern.slice(0, -3);
946
- return relativePath === directoryPrefix || relativePath.startsWith(`${directoryPrefix}/`) || isDirectory && `${relativePath}/` === directoryPrefix;
947
- }
948
- return relativePath === normalizedPattern;
949
- }
950
- function toPosixPath(path) {
951
- return path.split(sep).join("/");
952
- }
953
- function buildRecommendations(input) {
954
- const recommendations = [];
955
- if (!input.hasExistingFabric) {
956
- recommendations.push(t("cli.scan.recommendation.init"));
957
- }
958
- if (input.readmeQuality === "stub") {
959
- recommendations.push(t("cli.scan.recommendation.readme"));
960
- }
961
- if (!input.hasContributing) {
962
- recommendations.push(t("cli.scan.recommendation.contributing"));
963
- }
964
- if (input.framework.kind === "unknown") {
965
- recommendations.push(t("cli.scan.recommendation.unknown-framework"));
966
- } else {
967
- recommendations.push(t("cli.scan.recommendation.framework-dirs", { framework: input.framework.kind }));
968
- }
969
- return recommendations;
970
- }
971
- var __testing__ = {
972
- buildTechStackEntry,
973
- buildModuleStructureEntry,
974
- buildBuildConfigEntry,
975
- buildCodeStyleEntry,
976
- buildCIConfigEntry,
977
- buildReadmeFirstParaEntry,
978
- buildProjectBriefEntry,
979
- renderMarkdown,
980
- stripFrontmatter,
981
- isCIConfigPath,
982
- isBuildConfigPath,
983
- extractFirstParagraph,
984
- extractExplicitDescription,
985
- // TASK-008: bilingual template registry + language detection
986
- detectExistingLanguage,
987
- resolveKnowledgeLanguage,
988
- BASELINE_TEMPLATES
989
- };
990
794
 
991
795
  export {
992
- detectFramework,
993
- formatKnowledgeId,
994
- createScanReport,
995
796
  runInitScan,
996
- scanCommand,
997
- scan_default,
998
- detectExistingLanguage,
999
- deriveTagsFromForensic,
1000
- __testing__
797
+ detectExistingLanguage
1001
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
  };