@fenglimg/fabric-cli 1.0.0 → 1.1.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.
@@ -1,7 +1,4 @@
1
1
  #!/usr/bin/env node
2
- import {
3
- resolveIgnores
4
- } from "./chunk-P4KVFB2T.js";
5
2
  import {
6
3
  t
7
4
  } from "./chunk-6ICJICVU.js";
@@ -10,6 +7,11 @@ import {
10
7
  import { createHash } from "crypto";
11
8
  import { existsSync, mkdirSync, readdirSync, readFileSync, statSync, writeFileSync } from "fs";
12
9
  import { isAbsolute, join, relative, resolve, sep } from "path";
10
+ import {
11
+ agentsMetaSchema,
12
+ deriveAgentsMetaLayer,
13
+ deriveAgentsMetaTopologyType
14
+ } from "@fenglimg/fabric-shared";
13
15
  import { defineCommand } from "citty";
14
16
  var syncMetaCommand = defineCommand({
15
17
  meta: {
@@ -55,11 +57,15 @@ function computeAgentsMeta(target) {
55
57
  const metaPath = join(target, ".fabric", "agents.meta.json");
56
58
  const existingMeta = readExistingMeta(metaPath);
57
59
  const existingByFile = indexExistingNodesByFile(existingMeta);
58
- const agentsFiles = findAgentsFiles(target);
60
+ const agentsFiles = findFabricAgentsFiles(target);
59
61
  const nodes = {};
62
+ const bootstrapNode = createBootstrapNode(target, existingByFile.get("AGENTS.md")?.node);
63
+ if (bootstrapNode !== void 0) {
64
+ nodes.L0 = bootstrapNode;
65
+ }
60
66
  for (const file of agentsFiles) {
61
67
  const existing = existingByFile.get(file);
62
- const id = existing?.id ?? deriveNodeId(file);
68
+ const id = deriveNodeId(file);
63
69
  const hash = sha256(readFileSync(join(target, file), "utf8"));
64
70
  const defaults = createDefaultNodeMeta(file);
65
71
  nodes[id] = {
@@ -88,15 +94,18 @@ function readExistingMeta(metaPath) {
88
94
  return void 0;
89
95
  }
90
96
  try {
91
- return JSON.parse(readFileSync(metaPath, "utf8"));
97
+ return agentsMetaSchema.parse(JSON.parse(readFileSync(metaPath, "utf8")));
92
98
  } catch {
93
99
  return void 0;
94
100
  }
95
101
  }
96
- function findAgentsFiles(target) {
97
- const ignorePatterns = resolveIgnores();
102
+ function findFabricAgentsFiles(target) {
103
+ const agentsRoot = join(target, ".fabric", "agents");
104
+ if (!existsSync(agentsRoot) || !statSync(agentsRoot).isDirectory()) {
105
+ return [];
106
+ }
98
107
  const files = [];
99
- const stack = [target];
108
+ const stack = [agentsRoot];
100
109
  while (stack.length > 0) {
101
110
  const current = stack.pop();
102
111
  if (current === void 0) {
@@ -105,31 +114,20 @@ function findAgentsFiles(target) {
105
114
  for (const entry of readdirSync(current, { withFileTypes: true })) {
106
115
  const absolutePath = join(current, entry.name);
107
116
  const relativePath = toPosixPath(relative(target, absolutePath));
108
- if (shouldIgnore(relativePath, entry.isDirectory(), ignorePatterns)) {
109
- continue;
110
- }
111
117
  if (entry.isDirectory()) {
112
118
  stack.push(absolutePath);
113
- } else if (entry.isFile() && entry.name === "AGENTS.md") {
119
+ } else if (entry.isFile() && entry.name.endsWith(".md")) {
114
120
  files.push(relativePath);
115
121
  }
116
122
  }
117
123
  }
118
124
  return files.sort();
119
125
  }
120
- function shouldIgnore(relativePath, isDirectory, ignorePatterns) {
121
- return ignorePatterns.some((pattern) => matchesIgnorePattern(relativePath, isDirectory, pattern));
126
+ function deriveLayer(relativePath) {
127
+ return deriveAgentsMetaLayer(relativePath);
122
128
  }
123
- function matchesIgnorePattern(relativePath, isDirectory, pattern) {
124
- const normalizedPattern = toPosixPath(pattern);
125
- if (normalizedPattern === "**/*.meta") {
126
- return relativePath.endsWith(".meta");
127
- }
128
- if (normalizedPattern.endsWith("/**")) {
129
- const directoryPrefix = normalizedPattern.slice(0, -3);
130
- return relativePath === directoryPrefix || relativePath.startsWith(`${directoryPrefix}/`) || isDirectory && `${relativePath}/` === directoryPrefix;
131
- }
132
- return relativePath === normalizedPattern;
129
+ function deriveTopologyType(relativePath) {
130
+ return deriveAgentsMetaTopologyType(relativePath);
133
131
  }
134
132
  function indexExistingNodesByFile(existingMeta) {
135
133
  const byFile = /* @__PURE__ */ new Map();
@@ -142,18 +140,57 @@ function deriveNodeId(file) {
142
140
  if (file === "AGENTS.md") {
143
141
  return "L0";
144
142
  }
145
- return file.replace(/\/AGENTS\.md$/, "");
143
+ const layer = deriveLayer(file);
144
+ const relativeStem = getMirrorRelativeStem(file);
145
+ return `${layer}/${relativeStem}`;
146
146
  }
147
147
  function createDefaultNodeMeta(file) {
148
- const scope = file === "AGENTS.md" ? "**" : `${file.replace(/\/AGENTS\.md$/, "")}/**`;
148
+ const layer = deriveLayer(file);
149
+ const topologyType = deriveTopologyType(file);
149
150
  return {
150
151
  file,
151
- scope_glob: scope,
152
- deps: file === "AGENTS.md" ? [] : ["L0"],
153
- priority: file === "AGENTS.md" ? "high" : "medium",
152
+ scope_glob: deriveScopeGlob(file),
153
+ deps: layer === "L0" ? [] : ["L0"],
154
+ priority: layer === "L0" ? "high" : "medium",
155
+ layer,
156
+ topology_type: topologyType,
154
157
  hash: ""
155
158
  };
156
159
  }
160
+ function createBootstrapNode(target, existing) {
161
+ const bootstrapPath = join(target, "AGENTS.md");
162
+ if (!existsSync(bootstrapPath)) {
163
+ return void 0;
164
+ }
165
+ const hash = sha256(readFileSync(bootstrapPath, "utf8"));
166
+ return {
167
+ ...createDefaultNodeMeta("AGENTS.md"),
168
+ ...existing,
169
+ file: "AGENTS.md",
170
+ hash
171
+ };
172
+ }
173
+ function deriveScopeGlob(file) {
174
+ if (file === "AGENTS.md") {
175
+ return "**";
176
+ }
177
+ const stem = getMirrorRelativeStem(file);
178
+ const segments = stem.split("/").filter(Boolean);
179
+ if (segments.length === 0 || stem === "root") {
180
+ return "**";
181
+ }
182
+ if (segments[0] === "_cross") {
183
+ return "**";
184
+ }
185
+ if (segments.at(-1) === "rules") {
186
+ segments.pop();
187
+ }
188
+ const scopePath = segments.join("/");
189
+ return scopePath === "" ? "**" : `${scopePath}/**`;
190
+ }
191
+ function getMirrorRelativeStem(file) {
192
+ return file.replace(/^\.fabric\/agents\//, "").replace(/\.md$/, "");
193
+ }
157
194
  function sortNodes(nodes) {
158
195
  return Object.fromEntries(Object.entries(nodes).sort(([left], [right]) => left.localeCompare(right)));
159
196
  }
@@ -187,5 +224,7 @@ function sha256(content) {
187
224
  export {
188
225
  syncMetaCommand,
189
226
  sync_meta_default,
190
- computeAgentsMeta
227
+ computeAgentsMeta,
228
+ deriveLayer,
229
+ deriveTopologyType
191
230
  };
@@ -4,9 +4,6 @@ import {
4
4
  readFabricConfig,
5
5
  resolveDevMode
6
6
  } from "./chunk-AEOYCVBG.js";
7
- import {
8
- resolveIgnores
9
- } from "./chunk-P4KVFB2T.js";
10
7
  import {
11
8
  displayWidth,
12
9
  padEnd,
@@ -23,7 +20,24 @@ import { isAbsolute, join, relative, resolve, sep } from "path";
23
20
  import { defineCommand } from "citty";
24
21
 
25
22
  // src/scanner/detector.ts
26
- import { detectFramework } from "@fenglimg/fabric-shared";
23
+ import { detectFramework } from "@fenglimg/fabric-shared/node";
24
+
25
+ // src/scanner/ignores.ts
26
+ var DEFAULT_IGNORES = [
27
+ "**/*.meta",
28
+ "library/**",
29
+ "temp/**",
30
+ "build/**",
31
+ "settings/**",
32
+ "profiles/**",
33
+ "node_modules/**",
34
+ "dist/**",
35
+ ".git/**",
36
+ ".fabric/**"
37
+ ];
38
+ function resolveIgnores(fabricConfig) {
39
+ return [...DEFAULT_IGNORES, ...fabricConfig?.scanIgnores ?? []];
40
+ }
27
41
 
28
42
  // src/commands/scan.ts
29
43
  function createScanReport(targetInput = process.cwd(), fabricConfig) {
@@ -0,0 +1,125 @@
1
+ #!/usr/bin/env node
2
+ import {
3
+ resolveDevMode
4
+ } from "./chunk-AEOYCVBG.js";
5
+ import {
6
+ padEnd,
7
+ paint,
8
+ symbol
9
+ } from "./chunk-WWNXR34K.js";
10
+ import {
11
+ t
12
+ } from "./chunk-6ICJICVU.js";
13
+
14
+ // src/commands/doctor.ts
15
+ import { defineCommand } from "citty";
16
+ import { runDoctorAuditReport, runDoctorReport } from "@fenglimg/fabric-server";
17
+ var DEFAULT_AUDIT_WINDOW_MINUTES = 5;
18
+ var doctorCommand = defineCommand({
19
+ meta: {
20
+ name: "doctor",
21
+ description: t("cli.doctor.description")
22
+ },
23
+ args: {
24
+ target: {
25
+ type: "string",
26
+ description: t("cli.doctor.args.target.description")
27
+ },
28
+ audit: {
29
+ type: "boolean",
30
+ description: t("cli.doctor.args.audit.description"),
31
+ default: false
32
+ },
33
+ "window-minutes": {
34
+ type: "string",
35
+ description: t("cli.doctor.args.window-minutes.description"),
36
+ default: String(DEFAULT_AUDIT_WINDOW_MINUTES)
37
+ }
38
+ },
39
+ async run({ args }) {
40
+ const workspaceRoot = process.cwd();
41
+ const resolution = resolveDevMode(args.target, workspaceRoot);
42
+ const report = await runDoctorReport(resolution.target);
43
+ writeStdout(`${renderStatus(report.status)} ${paint.ai("fab doctor")} ${paint.human(resolution.target)}`);
44
+ for (const check of report.checks) {
45
+ writeStdout(`${renderStatus(check.status)} ${check.name}: ${check.message}`);
46
+ }
47
+ if (!args.audit) {
48
+ return;
49
+ }
50
+ const auditReport = await runDoctorAuditReport(resolution.target, {
51
+ force: true,
52
+ windowMs: parseWindowMinutes(args["window-minutes"])
53
+ });
54
+ if (auditReport.mode === "off") {
55
+ writeStderr(t("cli.doctor.audit.preview-only"));
56
+ }
57
+ if (auditReport.checkedPathCount === 0) {
58
+ writeStderr(t("cli.doctor.audit.none"));
59
+ return;
60
+ }
61
+ if (auditReport.violationCount === 0) {
62
+ writeStderr(
63
+ `${symbol.ok} ${t("cli.doctor.audit.clean", {
64
+ count: String(auditReport.checkedPathCount),
65
+ window: formatDuration(auditReport.windowMs)
66
+ })}`
67
+ );
68
+ return;
69
+ }
70
+ const writer = auditReport.mode === "strict" ? console.error : console.warn;
71
+ writer(
72
+ t("cli.doctor.audit.violations", {
73
+ count: String(auditReport.violationCount),
74
+ window: formatDuration(auditReport.windowMs)
75
+ })
76
+ );
77
+ writeStderr(
78
+ `${padEnd(t("cli.doctor.audit.table.path"), 32)} ${padEnd(t("cli.doctor.audit.table.edit"), 22)} ${padEnd(t("cli.doctor.audit.table.rules"), 22)} ${t("cli.doctor.audit.table.intent")}`
79
+ );
80
+ for (const violation of auditReport.violations) {
81
+ writeStderr(
82
+ `${padEnd(violation.path, 32)} ${padEnd(new Date(violation.editTs).toISOString(), 22)} ${padEnd(formatRulesTs(violation.lastGetRulesTs), 22)} ${violation.intent}`
83
+ );
84
+ }
85
+ if (auditReport.mode === "strict") {
86
+ process.exitCode = 1;
87
+ }
88
+ }
89
+ });
90
+ var doctor_default = doctorCommand;
91
+ function renderStatus(status) {
92
+ if (status === "ok") {
93
+ return symbol.ok;
94
+ }
95
+ if (status === "warn") {
96
+ return symbol.warn;
97
+ }
98
+ return symbol.error;
99
+ }
100
+ function parseWindowMinutes(value) {
101
+ const minutes = Number.parseInt(value ?? String(DEFAULT_AUDIT_WINDOW_MINUTES), 10);
102
+ if (!Number.isInteger(minutes) || minutes < 1) {
103
+ throw new Error(t("cli.doctor.errors.invalid-window", { value: value ?? "<unset>" }));
104
+ }
105
+ return minutes * 60 * 1e3;
106
+ }
107
+ function formatDuration(durationMs) {
108
+ const minutes = Math.max(Math.floor(durationMs / (60 * 1e3)), 1);
109
+ return `${minutes}m`;
110
+ }
111
+ function formatRulesTs(value) {
112
+ return value === null ? t("cli.shared.none") : new Date(value).toISOString();
113
+ }
114
+ function writeStdout(message) {
115
+ process.stdout.write(`${message}
116
+ `);
117
+ }
118
+ function writeStderr(message) {
119
+ process.stderr.write(`${message}
120
+ `);
121
+ }
122
+ export {
123
+ doctor_default as default,
124
+ doctorCommand
125
+ };
package/dist/index.js CHANGED
@@ -12,22 +12,23 @@ import { defineCommand, runMain } from "citty";
12
12
  // src/commands/index.ts
13
13
  var allCommands = {
14
14
  bootstrap: () => import("./bootstrap-PMIA4W6G.js").then((module) => module.default),
15
- init: () => import("./init-G6Q3OOMC.js").then((module) => module.default),
16
- scan: () => import("./scan-6CURGC3D.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),
17
18
  serve: () => import("./serve-4J2CQY25.js").then((module) => module.default),
18
- "sync-meta": () => import("./sync-meta-L6M4AEUT.js").then((module) => module.default),
19
+ "sync-meta": () => import("./sync-meta-THZSEM7Y.js").then((module) => module.default),
19
20
  "human-lint": () => import("./human-lint-YSFOZHZ7.js").then((module) => module.default),
20
21
  "ledger-append": () => import("./ledger-append-XZ5SX4O5.js").then((module) => module.default),
21
22
  hooks: () => import("./hooks-5S5IRVQE.js").then((module) => module.default),
22
23
  config: () => import("./config-PXEEXWLM.js").then((module) => module.configCmd),
23
- "pre-commit": () => import("./pre-commit-IEIXHKOD.js").then((module) => module.default)
24
+ "pre-commit": () => import("./pre-commit-BLSUMT3P.js").then((module) => module.default)
24
25
  };
25
26
 
26
27
  // src/index.ts
27
28
  var main = defineCommand({
28
29
  meta: {
29
30
  name: "fab",
30
- version: "1.0.0",
31
+ version: "1.1.0",
31
32
  description: t("cli.main.description")
32
33
  },
33
34
  subCommands: allCommands