@fenglimg/fabric-cli 2.0.0-rc.13 → 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
@@ -9,16 +9,18 @@
9
9
  3. 在目标项目运行 `fabric install`,完成一站式安装。
10
10
  4. 启动 `fabric serve`,再去客户端里验证 `fab_plan_context` 和 `fab_get_knowledge_sections`。
11
11
 
12
- `fabric install` 会自动准备 bootstrap、MCP 配置和 git hooks。公共命令面只保留 `install`、`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
16
  - `fabric install`
17
- - `fabric scan`
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";
@@ -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
  };