@fenglimg/fabric-cli 0.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.
@@ -0,0 +1,195 @@
1
+ import {
2
+ resolveClients
3
+ } from "./chunk-LHPBYOF5.js";
4
+ import {
5
+ readFabricConfig
6
+ } from "./chunk-JLIIQ75E.js";
7
+
8
+ // src/commands/bootstrap.ts
9
+ import { existsSync, mkdirSync, readFileSync, writeFileSync } from "fs";
10
+ import { dirname, join, parse, resolve } from "path";
11
+ import { fileURLToPath } from "url";
12
+ import { defineCommand } from "citty";
13
+ var CLIENT_ALIASES = {
14
+ claude: "claude",
15
+ "claude-code": "claude",
16
+ claudecode: "claude",
17
+ claudecli: "claude",
18
+ claudecodecli: "claude",
19
+ claudedesktop: "claude",
20
+ claudecodedesktop: "claude",
21
+ cursor: "cursor",
22
+ windsurf: "windsurf",
23
+ roo: "roo",
24
+ "roo-code": "roo",
25
+ roocode: "roo",
26
+ gemini: "gemini",
27
+ "gemini-cli": "gemini",
28
+ geminicli: "gemini",
29
+ codex: "codex",
30
+ "codex-cli": "codex",
31
+ codexcli: "codex"
32
+ };
33
+ var CLIENT_TEMPLATE_MAP = {
34
+ claude: "templates/bootstrap/CLAUDE.md",
35
+ cursor: "templates/bootstrap/cursor-fabric-bootstrap.mdc",
36
+ windsurf: "templates/bootstrap/windsurf-fabric.md",
37
+ roo: "templates/bootstrap/roo-fabric.md",
38
+ gemini: "templates/bootstrap/GEMINI.md",
39
+ codex: "templates/bootstrap/codex-AGENTS-header.md"
40
+ };
41
+ var CLIENT_TARGET_MAP = {
42
+ claude: "CLAUDE.md",
43
+ cursor: ".cursor/rules/fabric-bootstrap.mdc",
44
+ windsurf: ".windsurf/rules/fabric.md",
45
+ roo: ".roo/rules/fabric.md",
46
+ gemini: "GEMINI.md",
47
+ codex: "AGENTS.md"
48
+ };
49
+ var bootstrapCommand = defineCommand({
50
+ meta: {
51
+ name: "bootstrap",
52
+ description: "Install Fabric bootstrap prompt templates for supported AI clients."
53
+ },
54
+ subCommands: {
55
+ install: defineCommand({
56
+ meta: {
57
+ name: "install",
58
+ description: "Copy Fabric bootstrap templates into client-native locations."
59
+ },
60
+ args: {
61
+ clients: {
62
+ type: "string",
63
+ description: "Optional comma-separated client filter, e.g. claude,cursor,codex."
64
+ }
65
+ },
66
+ async run({ args }) {
67
+ const workspaceRoot = process.cwd();
68
+ const selectedClients = parseClientFilter(args.clients);
69
+ const fabricConfig = readFabricConfig(workspaceRoot);
70
+ const detectedClients = detectBootstrapClients(workspaceRoot, fabricConfig);
71
+ const clients = selectedClients ?? detectedClients;
72
+ if (clients.size === 0) {
73
+ process.stderr.write(
74
+ "No bootstrap targets detected. Pass --clients claude,cursor,windsurf,roo,gemini,codex to install explicitly.\n"
75
+ );
76
+ return;
77
+ }
78
+ for (const client of clients) {
79
+ installBootstrap(client, workspaceRoot);
80
+ }
81
+ }
82
+ })
83
+ }
84
+ });
85
+ var bootstrap_default = bootstrapCommand;
86
+ function parseClientFilter(value) {
87
+ if (value === void 0 || value.trim().length === 0) {
88
+ return null;
89
+ }
90
+ const clients = /* @__PURE__ */ new Set();
91
+ for (const rawClient of value.split(",")) {
92
+ const alias = rawClient.trim().toLowerCase();
93
+ const client = CLIENT_ALIASES[alias];
94
+ if (client === void 0) {
95
+ throw new Error(`Unknown client "${rawClient}". Use a comma-separated list such as claude,cursor,codex.`);
96
+ }
97
+ clients.add(client);
98
+ }
99
+ return clients;
100
+ }
101
+ function detectBootstrapClients(workspaceRoot, fabricConfig) {
102
+ const clients = /* @__PURE__ */ new Set();
103
+ for (const writer of resolveClients(workspaceRoot, fabricConfig)) {
104
+ const bootstrapClient = mapClientKind(writer.clientKind);
105
+ if (bootstrapClient !== null) {
106
+ clients.add(bootstrapClient);
107
+ }
108
+ }
109
+ return clients;
110
+ }
111
+ function mapClientKind(clientKind) {
112
+ switch (clientKind) {
113
+ case "ClaudeCodeCLI":
114
+ case "ClaudeCodeDesktop":
115
+ return "claude";
116
+ case "Cursor":
117
+ return "cursor";
118
+ case "Windsurf":
119
+ return "windsurf";
120
+ case "RooCode":
121
+ return "roo";
122
+ case "GeminiCLI":
123
+ return "gemini";
124
+ case "CodexCLI":
125
+ return "codex";
126
+ default:
127
+ return null;
128
+ }
129
+ }
130
+ function installBootstrap(client, workspaceRoot) {
131
+ const targetPath = resolve(workspaceRoot, CLIENT_TARGET_MAP[client]);
132
+ const templatePath = findTemplatePath(CLIENT_TEMPLATE_MAP[client]);
133
+ const template = readFileSync(templatePath, "utf8");
134
+ mkdirSync(dirname(targetPath), { recursive: true });
135
+ if (client === "codex") {
136
+ writeCodexBootstrap(targetPath, template);
137
+ return;
138
+ }
139
+ writeFileSync(targetPath, ensureTrailingNewline(template), "utf8");
140
+ process.stderr.write(`Installed ${targetPath}
141
+ `);
142
+ }
143
+ function writeCodexBootstrap(targetPath, template) {
144
+ const nextContent = ensureTrailingNewline(template);
145
+ if (!existsSync(targetPath)) {
146
+ writeFileSync(targetPath, nextContent, "utf8");
147
+ process.stderr.write(`Installed ${targetPath}
148
+ `);
149
+ return;
150
+ }
151
+ const existing = readFileSync(targetPath, "utf8");
152
+ if (existing.includes("# Fabric Bootstrap")) {
153
+ process.stderr.write(`Skipped ${targetPath}: Fabric Bootstrap header already present.
154
+ `);
155
+ return;
156
+ }
157
+ const separator = existing.startsWith("\n") || existing.length === 0 ? "" : "\n";
158
+ writeFileSync(targetPath, `${nextContent}${separator}${existing}`, "utf8");
159
+ process.stderr.write(`Prepended ${targetPath}
160
+ `);
161
+ }
162
+ function ensureTrailingNewline(content) {
163
+ return content.endsWith("\n") ? content : `${content}
164
+ `;
165
+ }
166
+ function findTemplatePath(relativePath) {
167
+ const currentModuleDir = dirname(fileURLToPath(import.meta.url));
168
+ const candidates = [
169
+ ...templateCandidatesFrom(process.cwd(), relativePath),
170
+ ...templateCandidatesFrom(currentModuleDir, relativePath)
171
+ ];
172
+ for (const candidate of candidates) {
173
+ if (existsSync(candidate)) {
174
+ return candidate;
175
+ }
176
+ }
177
+ throw new Error(`Template not found: ${relativePath}`);
178
+ }
179
+ function templateCandidatesFrom(start, relativePath) {
180
+ const candidates = [];
181
+ let current = resolve(start);
182
+ while (true) {
183
+ candidates.push(join(current, ...relativePath.split("/")));
184
+ const parent = dirname(current);
185
+ if (parent === current || parse(current).root === current) {
186
+ break;
187
+ }
188
+ current = parent;
189
+ }
190
+ return candidates;
191
+ }
192
+ export {
193
+ bootstrapCommand,
194
+ bootstrap_default as default
195
+ };
@@ -0,0 +1,124 @@
1
+ // src/commands/ledger-append.ts
2
+ import { execSync } from "child_process";
3
+ import { appendFileSync, existsSync, readFileSync, statSync } from "fs";
4
+ import { basename, isAbsolute, join, resolve } from "path";
5
+ import { defineCommand } from "citty";
6
+ var LEDGER_FILE = ".intent-ledger.jsonl";
7
+ var INITIAL_PARENT_SHA = "root";
8
+ var ledgerAppendCommand = defineCommand({
9
+ meta: {
10
+ name: "ledger-append",
11
+ description: "Append a Fabric intent ledger entry."
12
+ },
13
+ args: {
14
+ target: {
15
+ type: "string",
16
+ description: "Target project path. Defaults to the current working directory.",
17
+ default: process.cwd()
18
+ },
19
+ staged: {
20
+ type: "boolean",
21
+ description: "Derive the entry from staged changes for pre-commit.",
22
+ default: false
23
+ }
24
+ },
25
+ async run({ args }) {
26
+ const target = normalizeTarget(args.target);
27
+ assertExistingDirectory(target);
28
+ if (!args.staged) {
29
+ writeStderr("requires --staged in pre-commit context");
30
+ process.exitCode = 1;
31
+ return;
32
+ }
33
+ const stagedFiles = getStagedFiles(target).filter((file) => file !== LEDGER_FILE);
34
+ if (stagedFiles.length === 0) {
35
+ return;
36
+ }
37
+ const intent = deriveIntent(stagedFiles);
38
+ const diffStat = readDiffStat(target).trim();
39
+ const entry = {
40
+ ts: Date.now(),
41
+ parent_sha: readParentSha(target),
42
+ intent,
43
+ affected_paths: stagedFiles,
44
+ diff_stat: diffStat
45
+ };
46
+ if (hasMatchingTailEntry(target, entry)) {
47
+ return;
48
+ }
49
+ appendFileSync(join(target, LEDGER_FILE), `${JSON.stringify(entry)}
50
+ `, "utf8");
51
+ execGit(target, `git add ${LEDGER_FILE}`);
52
+ }
53
+ });
54
+ var ledger_append_default = ledgerAppendCommand;
55
+ function normalizeTarget(targetInput) {
56
+ return isAbsolute(targetInput) ? targetInput : resolve(process.cwd(), targetInput);
57
+ }
58
+ function assertExistingDirectory(target) {
59
+ if (!existsSync(target) || !statSync(target).isDirectory()) {
60
+ throw new Error(`Target must be an existing directory: ${target}`);
61
+ }
62
+ }
63
+ function getStagedFiles(target) {
64
+ const output = execGit(target, "git diff --cached --name-only --no-renames");
65
+ return output.split(/\r?\n/).map((line) => line.trim()).filter(Boolean);
66
+ }
67
+ function readDiffStat(target) {
68
+ return execGit(target, "git diff --cached --stat");
69
+ }
70
+ function readParentSha(target) {
71
+ try {
72
+ return execGit(target, "git rev-parse --short HEAD").trim();
73
+ } catch {
74
+ return INITIAL_PARENT_SHA;
75
+ }
76
+ }
77
+ function deriveIntent(stagedFiles) {
78
+ const explicitIntent = process.env.FABRIC_INTENT?.trim();
79
+ if (explicitIntent) {
80
+ return explicitIntent;
81
+ }
82
+ const uniqueNames = Array.from(new Set(stagedFiles.map((file) => basename(file))));
83
+ const head = uniqueNames.slice(0, 2).join(", ");
84
+ const suffix = uniqueNames.length > 2 ? ` +${uniqueNames.length - 2} more` : "";
85
+ return `auto: ${head}${suffix}`;
86
+ }
87
+ function hasMatchingTailEntry(target, entry) {
88
+ const ledgerPath = join(target, LEDGER_FILE);
89
+ if (!existsSync(ledgerPath)) {
90
+ return false;
91
+ }
92
+ const tail = readFileSync(ledgerPath, "utf8").trim().split(/\r?\n/).filter(Boolean).slice(-1)[0];
93
+ if (!tail) {
94
+ return false;
95
+ }
96
+ try {
97
+ const parsed = JSON.parse(tail);
98
+ return parsed.parent_sha === entry.parent_sha && parsed.intent === entry.intent && Array.isArray(parsed.affected_paths) && parsed.affected_paths.length === entry.affected_paths.length && parsed.affected_paths.every((value, index) => value === entry.affected_paths[index]) && normalizeDiffStat(parsed.diff_stat) === normalizeDiffStat(entry.diff_stat);
99
+ } catch {
100
+ return false;
101
+ }
102
+ }
103
+ function normalizeDiffStat(diffStat) {
104
+ if (typeof diffStat !== "string") {
105
+ return "";
106
+ }
107
+ return diffStat.split(/\r?\n/).map((line) => line.trim()).map((line) => line.replace(/\s+\|\s+/g, " | ")).map((line) => line.replace(/\s+/g, " ")).filter((line) => line.length > 0).filter((line) => !line.includes(LEDGER_FILE)).filter((line) => !/\d+ files? changed(?:, \d+ insertions?\(\+\))?(?:, \d+ deletions?\(-\))?$/.test(line.trim())).join("\n");
108
+ }
109
+ function execGit(target, command) {
110
+ return execSync(command, {
111
+ cwd: target,
112
+ encoding: "utf8",
113
+ stdio: ["ignore", "pipe", "pipe"]
114
+ });
115
+ }
116
+ function writeStderr(message) {
117
+ process.stderr.write(`${message}
118
+ `);
119
+ }
120
+
121
+ export {
122
+ ledgerAppendCommand,
123
+ ledger_append_default
124
+ };
@@ -0,0 +1,65 @@
1
+ // src/dev-mode.ts
2
+ import { existsSync, readFileSync } from "fs";
3
+ import { isAbsolute, join, resolve } from "path";
4
+ function readFabricConfig(workspaceRoot = process.cwd()) {
5
+ const configPath = join(workspaceRoot, "fabric.config.json");
6
+ if (!existsSync(configPath)) {
7
+ return {};
8
+ }
9
+ const parsed = JSON.parse(readFileSync(configPath, "utf8"));
10
+ if (parsed === null || typeof parsed !== "object" || Array.isArray(parsed)) {
11
+ throw new Error(`Expected object in ${configPath}`);
12
+ }
13
+ return parsed;
14
+ }
15
+ function resolveDevMode(cliTarget, workspaceRoot = process.cwd()) {
16
+ const envTarget = normalizeTarget(process.env.EXTERNAL_FIXTURE_PATH, workspaceRoot);
17
+ const fabricConfig = readFabricConfig(workspaceRoot);
18
+ const configTarget = normalizeTarget(fabricConfig.externalFixturePath, workspaceRoot);
19
+ const directTarget = normalizeTarget(cliTarget, workspaceRoot);
20
+ const chain = [
21
+ formatResolutionStep("cliTarget", directTarget),
22
+ formatResolutionStep("EXTERNAL_FIXTURE_PATH", envTarget),
23
+ formatResolutionStep("fabric.config.json.externalFixturePath", configTarget),
24
+ formatResolutionStep("process.cwd()", workspaceRoot)
25
+ ];
26
+ if (directTarget !== void 0) {
27
+ return { target: directTarget, source: "cli", chain };
28
+ }
29
+ if (envTarget !== void 0) {
30
+ return { target: envTarget, source: "env", chain };
31
+ }
32
+ if (configTarget !== void 0) {
33
+ return { target: configTarget, source: "config", chain };
34
+ }
35
+ return { target: workspaceRoot, source: "cwd", chain };
36
+ }
37
+ function resolveDevModeTarget(cliTarget) {
38
+ return resolveDevMode(cliTarget).target;
39
+ }
40
+ function createDebugLogger(debug) {
41
+ const enabled = debug === true || process.env.FABRIC_DEBUG === "1";
42
+ return (message) => {
43
+ if (!enabled) {
44
+ return;
45
+ }
46
+ process.stderr.write(`[fabric:debug] ${message}
47
+ `);
48
+ };
49
+ }
50
+ function normalizeTarget(value, workspaceRoot = process.cwd()) {
51
+ if (typeof value !== "string" || value.trim().length === 0) {
52
+ return void 0;
53
+ }
54
+ return isAbsolute(value) ? value : resolve(workspaceRoot, value);
55
+ }
56
+ function formatResolutionStep(source, value) {
57
+ return `${source}: ${value ?? "<unset>"}`;
58
+ }
59
+
60
+ export {
61
+ readFabricConfig,
62
+ resolveDevMode,
63
+ resolveDevModeTarget,
64
+ createDebugLogger
65
+ };
@@ -0,0 +1,94 @@
1
+ // src/commands/human-lint.ts
2
+ import { createHash } from "crypto";
3
+ import { existsSync } from "fs";
4
+ import { readFile } from "fs/promises";
5
+ import { isAbsolute, join, resolve } from "path";
6
+ import { defineCommand } from "citty";
7
+ var humanLintCommand = defineCommand({
8
+ meta: {
9
+ name: "human-lint",
10
+ description: "Validate locked human sections."
11
+ },
12
+ args: {
13
+ target: {
14
+ type: "string",
15
+ description: "Target project path. Defaults to the current working directory.",
16
+ default: process.cwd()
17
+ }
18
+ },
19
+ async run({ args }) {
20
+ const target = normalizeTarget(args.target);
21
+ const humanLockPath = join(target, ".fabric", "human-lock.json");
22
+ if (!existsSync(humanLockPath)) {
23
+ return;
24
+ }
25
+ const parsed = JSON.parse(await readFile(humanLockPath, "utf8"));
26
+ const locked = Array.isArray(parsed.locked) ? parsed.locked : [];
27
+ if (locked.length === 0) {
28
+ return;
29
+ }
30
+ const snapshots = await Promise.all(
31
+ Array.from(new Set(locked.map((entry) => entry.file))).map(async (file) => {
32
+ try {
33
+ return {
34
+ file,
35
+ content: await readFile(join(target, file), "utf8")
36
+ };
37
+ } catch {
38
+ return {
39
+ file,
40
+ content: null
41
+ };
42
+ }
43
+ })
44
+ );
45
+ const snapshotByFile = new Map(snapshots.map((snapshot) => [snapshot.file, snapshot]));
46
+ const violations = [];
47
+ for (const entry of locked) {
48
+ const snapshot = snapshotByFile.get(entry.file);
49
+ const actual = snapshot?.content === null || snapshot === void 0 ? "missing" : hashLockedContent(snapshot.content, entry);
50
+ if (actual !== entry.hash) {
51
+ violations.push({
52
+ location: `${entry.file}:${entry.start_line}-${entry.end_line}`,
53
+ expected: shortenHash(entry.hash),
54
+ actual: shortenHash(actual)
55
+ });
56
+ }
57
+ }
58
+ if (violations.length === 0) {
59
+ return;
60
+ }
61
+ writeStderr("Human-locked content drift detected. Revert the edit or update approved hashes before committing.");
62
+ writeStderr("Location Expected Got");
63
+ for (const violation of violations) {
64
+ writeStderr(
65
+ `${violation.location.padEnd(32)} ${violation.expected.padEnd(18)} ${violation.actual}`
66
+ );
67
+ }
68
+ process.exitCode = 1;
69
+ }
70
+ });
71
+ var human_lint_default = humanLintCommand;
72
+ function normalizeTarget(targetInput) {
73
+ return isAbsolute(targetInput) ? targetInput : resolve(process.cwd(), targetInput);
74
+ }
75
+ function hashLockedContent(content, entry) {
76
+ const lines = content.split(/\r?\n/);
77
+ const slice = lines.slice(Math.max(entry.start_line - 1, 0), Math.max(entry.end_line, 0)).join("\n");
78
+ return `sha256:${createHash("sha256").update(slice).digest("hex")}`;
79
+ }
80
+ function shortenHash(value) {
81
+ if (value === "missing") {
82
+ return value;
83
+ }
84
+ return value.slice(0, 15);
85
+ }
86
+ function writeStderr(message) {
87
+ process.stderr.write(`${message}
88
+ `);
89
+ }
90
+
91
+ export {
92
+ humanLintCommand,
93
+ human_lint_default
94
+ };