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

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 (32) hide show
  1. package/README.md +6 -6
  2. package/dist/{chunk-UHNP7T7W.js → chunk-5MQ52F42.js} +347 -86
  3. package/dist/chunk-6ICJICVU.js +10 -0
  4. package/dist/chunk-AW3G7ZH5.js +576 -0
  5. package/dist/chunk-HQLEHH4O.js +321 -0
  6. package/dist/{chunk-5LOYBXWD.js → chunk-OBQU6NHO.js} +2 -52
  7. package/dist/chunk-WPTA74BY.js +184 -0
  8. package/dist/chunk-WWNXR34K.js +49 -0
  9. package/dist/doctor-RILCO5OG.js +282 -0
  10. package/dist/hooks-NX32PPEN.js +13 -0
  11. package/dist/index.js +8 -5
  12. package/dist/{init-DRHUYHYA.js → init-C56PWHID.js} +225 -491
  13. package/dist/plan-context-hint-QMUPAXIB.js +98 -0
  14. package/dist/{scan-HU2EGITF.js → scan-66EKMNAY.js} +6 -2
  15. package/dist/{serve-3LXXSBFR.js → serve-NGLXHDYC.js} +8 -4
  16. package/dist/uninstall-DBAR2JBS.js +1082 -0
  17. package/package.json +3 -3
  18. package/templates/bootstrap/CLAUDE.md +1 -1
  19. package/templates/bootstrap/codex-AGENTS-header.md +1 -1
  20. package/templates/bootstrap/cursor-fabric-bootstrap.mdc +1 -1
  21. package/templates/hooks/configs/README.md +73 -0
  22. package/templates/hooks/configs/claude-code.json +37 -0
  23. package/templates/hooks/configs/codex-hooks.json +20 -0
  24. package/templates/hooks/configs/cursor-hooks.json +20 -0
  25. package/templates/hooks/fabric-hint.cjs +1337 -0
  26. package/templates/hooks/knowledge-hint-broad.cjs +612 -0
  27. package/templates/hooks/knowledge-hint-narrow.cjs +826 -0
  28. package/templates/hooks/lib/session-digest-writer.cjs +172 -0
  29. package/templates/skills/fabric-archive/SKILL.md +640 -0
  30. package/templates/skills/fabric-import/SKILL.md +850 -0
  31. package/templates/skills/fabric-review/SKILL.md +717 -0
  32. package/dist/doctor-DUHWLAYD.js +0 -98
@@ -0,0 +1,321 @@
1
+ #!/usr/bin/env node
2
+ import {
3
+ ClaudeCodeCLIWriter,
4
+ CursorWriter,
5
+ createServerEntry,
6
+ normalizeConfigPath,
7
+ removeJsonClientConfigEntry,
8
+ writeJsonClientConfig
9
+ } from "./chunk-AW3G7ZH5.js";
10
+
11
+ // src/config/resolver.ts
12
+ import { existsSync as existsSync3 } from "fs";
13
+ import { join as join3 } from "path";
14
+ import { homedir as homedir3 } from "os";
15
+
16
+ // src/config/claude-code.ts
17
+ import { existsSync } from "fs";
18
+ import { join, resolve } from "path";
19
+ import { homedir, platform } from "os";
20
+ function getClaudeDesktopConfigPath() {
21
+ const os = platform();
22
+ if (os === "darwin") {
23
+ return join(homedir(), "Library", "Application Support", "Claude", "claude_desktop_config.json");
24
+ }
25
+ if (os === "win32") {
26
+ return join(process.env.APPDATA ?? join(homedir(), "AppData", "Roaming"), "Claude", "claude_desktop_config.json");
27
+ }
28
+ return join(homedir(), ".config", "Claude", "claude_desktop_config.json");
29
+ }
30
+ var ClaudeCodeDesktopWriter = class {
31
+ clientKind = "ClaudeCodeDesktop";
32
+ configuredPath;
33
+ constructor(configuredPath) {
34
+ this.configuredPath = configuredPath;
35
+ }
36
+ async detect(_workspaceRoot, overridePath) {
37
+ const configPath = normalizeConfigPath(overridePath ?? this.configuredPath ?? getClaudeDesktopConfigPath());
38
+ return existsSync(configPath) || overridePath !== void 0 || this.configuredPath !== void 0 ? configPath : null;
39
+ }
40
+ async write(serverPath, workspaceRoot, overridePath) {
41
+ const configPath = await this.detect(workspaceRoot, overridePath);
42
+ if (configPath === null) {
43
+ return;
44
+ }
45
+ await writeJsonClientConfig(configPath, {
46
+ command: process.execPath,
47
+ args: [serverPath]
48
+ });
49
+ }
50
+ async remove(serverName, workspaceRoot, overridePath) {
51
+ const configPath = await this.detect(workspaceRoot, overridePath);
52
+ if (configPath === null) {
53
+ return { status: "skipped", message: "no-config-path" };
54
+ }
55
+ return removeJsonClientConfigEntry(configPath, serverName);
56
+ }
57
+ };
58
+
59
+ // src/config/toml.ts
60
+ import { existsSync as existsSync2 } from "fs";
61
+ import { mkdir, readFile } from "fs/promises";
62
+ import { dirname, join as join2, resolve as resolve2 } from "path";
63
+ import { homedir as homedir2 } from "os";
64
+ import { atomicWriteText } from "@fenglimg/fabric-shared/node/atomic-write";
65
+ function expandHome(filePath) {
66
+ if (filePath === "~") {
67
+ return homedir2();
68
+ }
69
+ if (filePath.startsWith("~/")) {
70
+ return join2(homedir2(), filePath.slice(2));
71
+ }
72
+ return filePath;
73
+ }
74
+ function escapeTomlString(value) {
75
+ return JSON.stringify(value);
76
+ }
77
+ function serializeTomlStringArray(values) {
78
+ return `[${values.map((value) => escapeTomlString(value)).join(", ")}]`;
79
+ }
80
+ function serializeTomlInlineTable(values) {
81
+ const entries = Object.entries(values).sort(([left], [right]) => left.localeCompare(right)).map(([key, value]) => `${key} = ${escapeTomlString(value)}`);
82
+ return `{ ${entries.join(", ")} }`;
83
+ }
84
+ function serializeCodexServerBlock(serverName, serverEntry) {
85
+ const lines = [
86
+ `[mcp_servers.${serverName}]`,
87
+ `command = ${escapeTomlString(serverEntry.command)}`,
88
+ `args = ${serializeTomlStringArray(serverEntry.args)}`
89
+ ];
90
+ if (serverEntry.env !== void 0 && Object.keys(serverEntry.env).length > 0) {
91
+ lines.push(`env = ${serializeTomlInlineTable(serverEntry.env)}`);
92
+ }
93
+ return `${lines.join("\n")}
94
+ `;
95
+ }
96
+ function trimTrailingBlankLines(value) {
97
+ return value.replace(/\s+$/u, "");
98
+ }
99
+ function removeCodexServerBlock(rawConfig, serverName) {
100
+ const normalized = rawConfig.replace(/\r\n/g, "\n");
101
+ const escaped = serverName.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
102
+ const legacyPattern = new RegExp(
103
+ String.raw`\n?\[mcp\.servers\.${escaped}\]\n[\s\S]*?(?=\n\[[^\n]+\]\n|$)`,
104
+ "g"
105
+ );
106
+ const currentPattern = new RegExp(
107
+ String.raw`\n?\[mcp_servers\.${escaped}\]\n[\s\S]*?(?=\n\[[^\n]+\]\n|$)`,
108
+ "g"
109
+ );
110
+ const withoutLegacy = normalized.replace(legacyPattern, "");
111
+ const withoutCurrent = withoutLegacy.replace(currentPattern, "");
112
+ const changed = withoutCurrent !== normalized;
113
+ const text = changed ? `${trimTrailingBlankLines(withoutCurrent)}
114
+ ` : rawConfig;
115
+ return { text, changed };
116
+ }
117
+ function upsertCodexServerBlock(rawConfig, serverName, serverEntry) {
118
+ const block = serializeCodexServerBlock(serverName, serverEntry);
119
+ const normalized = rawConfig.replace(/\r\n/g, "\n");
120
+ const legacyPattern = new RegExp(String.raw`\n?\[mcp\.servers\.${serverName.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}\]\n[\s\S]*?(?=\n\[[^\n]+\]\n|$)`, "g");
121
+ const currentPattern = new RegExp(
122
+ String.raw`\n?\[mcp_servers\.${serverName.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}\]\n[\s\S]*?(?=\n\[[^\n]+\]\n|$)`,
123
+ "g"
124
+ );
125
+ const withoutLegacy = normalized.replace(legacyPattern, "");
126
+ const withoutExisting = withoutLegacy.replace(currentPattern, "");
127
+ const trimmed = trimTrailingBlankLines(withoutExisting);
128
+ if (trimmed.length === 0) {
129
+ return block;
130
+ }
131
+ return `${trimmed}
132
+
133
+ ${block}`;
134
+ }
135
+ async function readTomlConfigText(configPath) {
136
+ try {
137
+ return await readFile(configPath, "utf8");
138
+ } catch (error) {
139
+ if (error instanceof Error && "code" in error && error.code === "ENOENT") {
140
+ return "";
141
+ }
142
+ throw error;
143
+ }
144
+ }
145
+ var CodexTOMLConfigWriter = class {
146
+ clientKind = "CodexCLI";
147
+ configuredPath;
148
+ constructor(configuredPath) {
149
+ this.configuredPath = configuredPath;
150
+ }
151
+ async detect(_workspaceRoot, overridePath) {
152
+ const explicitPath = overridePath ?? this.configuredPath;
153
+ if (explicitPath !== void 0) {
154
+ return resolve2(expandHome(explicitPath));
155
+ }
156
+ const codexDir = join2(homedir2(), ".codex");
157
+ return existsSync2(codexDir) ? resolve2(join2(codexDir, "config.toml")) : null;
158
+ }
159
+ async write(serverPath, workspaceRoot, overridePath) {
160
+ const configPath = await this.detect(workspaceRoot, overridePath);
161
+ if (configPath === null) {
162
+ return;
163
+ }
164
+ const rawConfig = await readTomlConfigText(configPath);
165
+ const nextConfig = upsertCodexServerBlock(rawConfig, "fabric", createServerEntry(serverPath));
166
+ await mkdir(dirname(configPath), { recursive: true });
167
+ await atomicWriteText(configPath, nextConfig);
168
+ }
169
+ async remove(serverName, workspaceRoot, overridePath) {
170
+ const configPath = await this.detect(workspaceRoot, overridePath);
171
+ if (configPath === null) {
172
+ return { status: "skipped", message: "no-config-path" };
173
+ }
174
+ if (!existsSync2(configPath)) {
175
+ return { status: "skipped", path: configPath, message: "no-config-file" };
176
+ }
177
+ let rawConfig;
178
+ try {
179
+ rawConfig = await readTomlConfigText(configPath);
180
+ } catch (error) {
181
+ return {
182
+ status: "error",
183
+ path: configPath,
184
+ message: error instanceof Error ? error.message : String(error)
185
+ };
186
+ }
187
+ const { text, changed } = removeCodexServerBlock(rawConfig, serverName);
188
+ if (!changed) {
189
+ return { status: "skipped", path: configPath, message: "not-present" };
190
+ }
191
+ try {
192
+ await mkdir(dirname(configPath), { recursive: true });
193
+ await atomicWriteText(configPath, text);
194
+ return { status: "removed", path: configPath };
195
+ } catch (error) {
196
+ return {
197
+ status: "error",
198
+ path: configPath,
199
+ message: error instanceof Error ? error.message : String(error)
200
+ };
201
+ }
202
+ }
203
+ };
204
+
205
+ // src/config/resolver.ts
206
+ import { clientPathsSchema, fabricConfigSchema } from "@fenglimg/fabric-shared";
207
+ function hasExplicitPath(clientPaths, key) {
208
+ return typeof clientPaths?.[key] === "string" && clientPaths[key].trim().length > 0;
209
+ }
210
+ function addIfDetected(writers, detected, createWriter, configuredPath) {
211
+ if (configuredPath !== void 0 || detected) {
212
+ writers.push(createWriter(configuredPath));
213
+ }
214
+ }
215
+ function resolveClients(workspaceRoot, fabricConfig = {}, opts = {}) {
216
+ const clientPaths = fabricConfig.clientPaths;
217
+ const writers = [];
218
+ const claudeMcpScope = opts.claudeMcpScope ?? "project";
219
+ addIfDetected(
220
+ writers,
221
+ existsSync3(join3(homedir3(), ".claude")) || existsSync3(join3(workspaceRoot, ".claude")),
222
+ (configuredPath) => new ClaudeCodeCLIWriter(configuredPath, claudeMcpScope),
223
+ hasExplicitPath(clientPaths, "claudeCodeCLI") ? clientPaths.claudeCodeCLI : void 0
224
+ );
225
+ addIfDetected(
226
+ writers,
227
+ existsSync3(getClaudeDesktopConfigPath()),
228
+ (configuredPath) => new ClaudeCodeDesktopWriter(configuredPath),
229
+ hasExplicitPath(clientPaths, "claudeCodeDesktop") ? clientPaths.claudeCodeDesktop : void 0
230
+ );
231
+ addIfDetected(
232
+ writers,
233
+ existsSync3(join3(workspaceRoot, ".cursor")),
234
+ (configuredPath) => new CursorWriter(configuredPath),
235
+ hasExplicitPath(clientPaths, "cursor") ? clientPaths.cursor : void 0
236
+ );
237
+ addIfDetected(
238
+ writers,
239
+ existsSync3(join3(homedir3(), ".codex")),
240
+ (configuredPath) => new CodexTOMLConfigWriter(configuredPath),
241
+ hasExplicitPath(clientPaths, "codexCLI") ? clientPaths.codexCLI : void 0
242
+ );
243
+ return writers;
244
+ }
245
+ function detectClientSupports(workspaceRoot, fabricConfig = {}) {
246
+ const clientPaths = fabricConfig.clientPaths;
247
+ const claudeDetected = existsSync3(join3(homedir3(), ".claude")) || existsSync3(join3(workspaceRoot, ".claude"));
248
+ const claudeDesktopDetected = existsSync3(getClaudeDesktopConfigPath());
249
+ const cursorDetected = existsSync3(join3(workspaceRoot, ".cursor"));
250
+ const codexDetected = existsSync3(join3(homedir3(), ".codex"));
251
+ return [
252
+ {
253
+ clientKind: "ClaudeCodeCLI",
254
+ label: "Claude Code CLI",
255
+ detected: claudeDetected || hasExplicitPath(clientPaths, "claudeCodeCLI"),
256
+ bootstrapTargetPath: ".fabric/bootstrap/README.md",
257
+ configPath: "project .claude/settings.json",
258
+ capabilities: {
259
+ bootstrap: true,
260
+ mcp: true,
261
+ hook: true,
262
+ skill: true
263
+ },
264
+ installedCapabilities: {
265
+ hook: true,
266
+ skill: true
267
+ }
268
+ },
269
+ {
270
+ clientKind: "ClaudeCodeDesktop",
271
+ label: "Claude Code Desktop",
272
+ detected: claudeDesktopDetected || hasExplicitPath(clientPaths, "claudeCodeDesktop"),
273
+ bootstrapTargetPath: ".fabric/bootstrap/README.md",
274
+ configPath: "desktop Claude config",
275
+ capabilities: {
276
+ bootstrap: true,
277
+ mcp: true,
278
+ hook: false,
279
+ skill: false
280
+ }
281
+ },
282
+ {
283
+ clientKind: "Cursor",
284
+ label: "Cursor",
285
+ detected: cursorDetected || hasExplicitPath(clientPaths, "cursor"),
286
+ bootstrapTargetPath: ".fabric/bootstrap/README.md",
287
+ configPath: ".cursor/mcp.json",
288
+ capabilities: {
289
+ bootstrap: true,
290
+ mcp: true,
291
+ hook: false,
292
+ skill: false
293
+ }
294
+ },
295
+ {
296
+ clientKind: "CodexCLI",
297
+ label: "Codex CLI",
298
+ detected: codexDetected || hasExplicitPath(clientPaths, "codexCLI"),
299
+ bootstrapTargetPath: ".fabric/bootstrap/README.md",
300
+ configPath: "~/.codex/config.toml",
301
+ capabilities: {
302
+ bootstrap: true,
303
+ mcp: true,
304
+ hook: true,
305
+ skill: true
306
+ },
307
+ installedCapabilities: {
308
+ hook: existsSync3(join3(workspaceRoot, ".codex", "hooks.json")),
309
+ // v2/rc.2: v1 client-side init skill removed; skill-installation probes
310
+ // will return once rc.2/3/4 introduce the v2 skills (fabric-archive,
311
+ // fabric-review, fabric-import). Until then there is nothing to probe.
312
+ skill: false
313
+ }
314
+ }
315
+ ];
316
+ }
317
+
318
+ export {
319
+ resolveClients,
320
+ detectClientSupports
321
+ };
@@ -1,46 +1,5 @@
1
1
  #!/usr/bin/env node
2
2
 
3
- // src/colors.ts
4
- import pc from "picocolors";
5
- import stringWidth from "string-width";
6
- function isColorEnabled() {
7
- return !process.env.NO_COLOR && Boolean(process.stdout.isTTY) && Boolean(process.stderr.isTTY);
8
- }
9
- function colorize(painter) {
10
- return (value) => isColorEnabled() ? painter(value) : value;
11
- }
12
- var paint = {
13
- success: colorize(pc.green),
14
- warn: colorize(pc.yellow),
15
- error: colorize(pc.red),
16
- drift: colorize(pc.magenta),
17
- ai: colorize(pc.blue),
18
- human: colorize(pc.cyan),
19
- muted: colorize(pc.dim)
20
- };
21
- var symbol = {
22
- get ok() {
23
- return isColorEnabled() ? paint.success("\u2713") : "[ok]";
24
- },
25
- get warn() {
26
- return isColorEnabled() ? paint.warn("!") : "[warn]";
27
- },
28
- get error() {
29
- return isColorEnabled() ? paint.error("x") : "[error]";
30
- }
31
- };
32
- function displayWidth(value) {
33
- return stringWidth(value);
34
- }
35
- function padEnd(value, width, char = " ") {
36
- const fill = char.length > 0 ? char : " ";
37
- let result = value;
38
- while (displayWidth(result) < width) {
39
- result += fill;
40
- }
41
- return result;
42
- }
43
-
44
3
  // src/dev-mode.ts
45
4
  import { existsSync, readFileSync } from "fs";
46
5
  import { isAbsolute, join, resolve } from "path";
@@ -97,17 +56,8 @@ function formatResolutionStep(source, value) {
97
56
  return `${source}: ${value ?? "<unset>"}`;
98
57
  }
99
58
 
100
- // src/i18n.ts
101
- import { createTranslator, detectNodeLocale } from "@fenglimg/fabric-shared";
102
- var locale = detectNodeLocale();
103
- var t = createTranslator(locale);
104
-
105
59
  export {
106
- paint,
107
- symbol,
108
- displayWidth,
109
- padEnd,
60
+ readFabricConfig,
110
61
  resolveDevMode,
111
- createDebugLogger,
112
- t
62
+ createDebugLogger
113
63
  };
@@ -0,0 +1,184 @@
1
+ #!/usr/bin/env node
2
+ import {
3
+ addArchiveSkillPointer,
4
+ installArchiveHintHook,
5
+ installFabricArchiveSkill,
6
+ installFabricImportSkill,
7
+ installFabricReviewSkill,
8
+ installKnowledgeHintBroadHook,
9
+ installKnowledgeHintNarrowHook,
10
+ mergeClaudeCodeHookConfig,
11
+ mergeCodexHookConfig,
12
+ mergeCursorHookConfig
13
+ } from "./chunk-AW3G7ZH5.js";
14
+ import {
15
+ t
16
+ } from "./chunk-6ICJICVU.js";
17
+
18
+ // src/commands/hooks.ts
19
+ import { existsSync, statSync } from "fs";
20
+ import { isAbsolute, join, resolve } from "path";
21
+ import { defineCommand } from "citty";
22
+ var hooksCommand = defineCommand({
23
+ meta: {
24
+ name: "hooks",
25
+ description: t("cli.hooks.description")
26
+ },
27
+ subCommands: {
28
+ install: defineCommand({
29
+ meta: {
30
+ name: "install",
31
+ description: t("cli.hooks.install.description")
32
+ },
33
+ args: {
34
+ target: {
35
+ type: "string",
36
+ description: t("cli.hooks.install.args.target.description"),
37
+ default: process.cwd()
38
+ }
39
+ },
40
+ async run({ args }) {
41
+ const result = await installHooks(args.target);
42
+ for (const path of result.installed) {
43
+ console.log(`installed ${path}`);
44
+ }
45
+ for (const path of result.skipped) {
46
+ console.log(`skipped ${path}`);
47
+ }
48
+ for (const message of result.errors) {
49
+ console.error(`error ${message}`);
50
+ }
51
+ }
52
+ })
53
+ }
54
+ });
55
+ var hooks_default = hooksCommand;
56
+ async function installHooks(target, _options = {}) {
57
+ const normalizedTarget = normalizeTarget(target);
58
+ assertExistingDirectory(normalizedTarget);
59
+ const results = [];
60
+ results.push(...await runStep(() => installFabricArchiveSkill(normalizedTarget)));
61
+ results.push(...await runStep(() => installFabricReviewSkill(normalizedTarget)));
62
+ results.push(...await runStep(() => installFabricImportSkill(normalizedTarget)));
63
+ results.push(...await runStep(() => installArchiveHintHook(normalizedTarget)));
64
+ results.push(...await runStep(() => installKnowledgeHintBroadHook(normalizedTarget)));
65
+ results.push(...await runStep(() => installKnowledgeHintNarrowHook(normalizedTarget)));
66
+ results.push(await runSingleStep("claude-hook-config", () => mergeClaudeCodeHookConfig(normalizedTarget)));
67
+ results.push(await runSingleStep("codex-hook-config", () => mergeCodexHookConfig(normalizedTarget)));
68
+ results.push(await runSingleStep("cursor-hook-config", () => mergeCursorHookConfig(normalizedTarget)));
69
+ results.push(...await runStep(() => addArchiveSkillPointer(normalizedTarget)));
70
+ results.push(...validateHookPaths(normalizedTarget));
71
+ return summarizeResults(results);
72
+ }
73
+ function validateHookPaths(projectRoot) {
74
+ const scripts = [
75
+ { stepSuffix: "", hookFile: "fabric-hint.cjs" },
76
+ { stepSuffix: "-broad", hookFile: "knowledge-hint-broad.cjs" },
77
+ { stepSuffix: "-narrow", hookFile: "knowledge-hint-narrow.cjs" }
78
+ ];
79
+ const clients = [
80
+ {
81
+ client: "claude",
82
+ configRel: join(".claude", "settings.json"),
83
+ hookDir: join(".claude", "hooks")
84
+ },
85
+ {
86
+ client: "codex",
87
+ configRel: join(".codex", "hooks.json"),
88
+ hookDir: join(".codex", "hooks")
89
+ },
90
+ {
91
+ client: "cursor",
92
+ configRel: join(".cursor", "hooks.json"),
93
+ hookDir: join(".cursor", "hooks")
94
+ }
95
+ ];
96
+ const results = [];
97
+ for (const { client, configRel, hookDir } of clients) {
98
+ const configPath = resolve(projectRoot, configRel);
99
+ if (!existsSync(configPath)) {
100
+ results.push({
101
+ step: `hook-validate-${client}`,
102
+ path: configPath,
103
+ status: "skipped",
104
+ message: "missing-config"
105
+ });
106
+ continue;
107
+ }
108
+ for (const { stepSuffix, hookFile } of scripts) {
109
+ const expectedHookPath = resolve(projectRoot, hookDir, hookFile);
110
+ const expectedHookRel = join(hookDir, hookFile);
111
+ const step = `hook-validate-${client}${stepSuffix}`;
112
+ if (!existsSync(expectedHookPath)) {
113
+ results.push({
114
+ step,
115
+ path: expectedHookPath,
116
+ status: "error",
117
+ message: `hook script missing: ${expectedHookRel}`
118
+ });
119
+ continue;
120
+ }
121
+ results.push({ step, path: expectedHookPath, status: "skipped", message: "ok" });
122
+ }
123
+ }
124
+ return results;
125
+ }
126
+ async function runStep(fn) {
127
+ try {
128
+ return await fn();
129
+ } catch (error) {
130
+ return [
131
+ {
132
+ step: "hook-install",
133
+ path: "",
134
+ status: "error",
135
+ message: error instanceof Error ? error.message : String(error)
136
+ }
137
+ ];
138
+ }
139
+ }
140
+ async function runSingleStep(step, fn) {
141
+ try {
142
+ return await fn();
143
+ } catch (error) {
144
+ return {
145
+ step,
146
+ path: "",
147
+ status: "error",
148
+ message: error instanceof Error ? error.message : String(error)
149
+ };
150
+ }
151
+ }
152
+ function summarizeResults(results) {
153
+ const installed = [];
154
+ const skipped = [];
155
+ const errors = [];
156
+ for (const r of results) {
157
+ switch (r.status) {
158
+ case "written":
159
+ installed.push(r.path);
160
+ break;
161
+ case "skipped":
162
+ skipped.push(r.path);
163
+ break;
164
+ case "error":
165
+ errors.push(`${r.step} ${r.path}: ${r.message ?? "unknown error"}`);
166
+ break;
167
+ }
168
+ }
169
+ return { installed, skipped, errors };
170
+ }
171
+ function normalizeTarget(targetInput) {
172
+ return isAbsolute(targetInput) ? targetInput : resolve(process.cwd(), targetInput);
173
+ }
174
+ function assertExistingDirectory(target) {
175
+ if (!existsSync(target) || !statSync(target).isDirectory()) {
176
+ throw new Error(t("cli.shared.target-invalid", { target }));
177
+ }
178
+ }
179
+
180
+ export {
181
+ hooksCommand,
182
+ hooks_default,
183
+ installHooks
184
+ };
@@ -0,0 +1,49 @@
1
+ #!/usr/bin/env node
2
+
3
+ // src/colors.ts
4
+ import pc from "picocolors";
5
+ import stringWidth from "string-width";
6
+ function isColorEnabled() {
7
+ return !process.env.NO_COLOR && Boolean(process.stdout.isTTY) && Boolean(process.stderr.isTTY);
8
+ }
9
+ function colorize(painter) {
10
+ return (value) => isColorEnabled() ? painter(value) : value;
11
+ }
12
+ var paint = {
13
+ success: colorize(pc.green),
14
+ warn: colorize(pc.yellow),
15
+ error: colorize(pc.red),
16
+ drift: colorize(pc.magenta),
17
+ ai: colorize(pc.blue),
18
+ human: colorize(pc.cyan),
19
+ muted: colorize(pc.dim)
20
+ };
21
+ var symbol = {
22
+ get ok() {
23
+ return isColorEnabled() ? paint.success("\u2713") : "[ok]";
24
+ },
25
+ get warn() {
26
+ return isColorEnabled() ? paint.warn("!") : "[warn]";
27
+ },
28
+ get error() {
29
+ return isColorEnabled() ? paint.error("x") : "[error]";
30
+ }
31
+ };
32
+ function displayWidth(value) {
33
+ return stringWidth(value);
34
+ }
35
+ function padEnd(value, width, char = " ") {
36
+ const fill = char.length > 0 ? char : " ";
37
+ let result = value;
38
+ while (displayWidth(result) < width) {
39
+ result += fill;
40
+ }
41
+ return result;
42
+ }
43
+
44
+ export {
45
+ paint,
46
+ symbol,
47
+ displayWidth,
48
+ padEnd
49
+ };