@fenglimg/fabric-cli 2.0.0-rc.13 → 2.0.0-rc.21
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 +4 -2
- package/dist/{chunk-X7QPY5KH.js → chunk-4HC5ZK7H.js} +296 -301
- package/dist/{chunk-FDRLV5PL.js → chunk-FNO7CQDG.js} +5 -213
- package/dist/{chunk-WWNXR34K.js → chunk-G2CIOLD4.js} +16 -1
- package/dist/chunk-KZ2YITOS.js +225 -0
- package/dist/{chunk-OHWQNSLH.js → chunk-MF3OTILQ.js} +267 -44
- package/dist/{chunk-OBQU6NHO.js → chunk-ZSESMG6L.js} +0 -6
- package/dist/config-AYP5F72E.js +13 -0
- package/dist/doctor-L6TIXXIX.js +425 -0
- package/dist/index.js +11 -9
- package/dist/{install-SLS5W27W.js → install-DNZXGFHJ.js} +344 -359
- package/dist/{plan-context-hint-QMUPAXIB.js → plan-context-hint-CFDGXHCA.js} +10 -5
- package/dist/{serve-NGLXHDYC.js → serve-6PPQX7AW.js} +16 -11
- package/dist/{uninstall-JHUSFENL.js → uninstall-L2HEEOU3.js} +200 -215
- package/package.json +3 -3
- package/templates/hooks/configs/README.md +9 -5
- package/templates/hooks/configs/cursor-hooks.json +7 -10
- package/templates/hooks/fabric-hint.cjs +350 -21
- package/templates/hooks/knowledge-hint-broad.cjs +39 -14
- package/templates/hooks/knowledge-hint-narrow.cjs +31 -7
- package/templates/hooks/lib/banner-i18n.cjs +252 -0
- package/dist/chunk-Q72D24BG.js +0 -186
- package/dist/doctor-RILCO5OG.js +0 -282
- package/dist/hooks-HIWYI3VG.js +0 -13
- package/dist/scan-VHKZPT2W.js +0 -24
- package/templates/agents-md/AGENTS.md.template +0 -59
- package/templates/bootstrap/CLAUDE.md +0 -8
- package/templates/bootstrap/codex-AGENTS-header.md +0 -6
- package/templates/bootstrap/cursor-fabric-bootstrap.mdc +0 -10
|
@@ -615,9 +615,17 @@ function formatEntryLine(entry) {
|
|
|
615
615
|
|
|
616
616
|
/**
|
|
617
617
|
* Render the narrow-match block to an array of stderr lines. Returns []
|
|
618
|
-
* when there is nothing to render (empty
|
|
618
|
+
* when there is nothing to render (empty entries set). Callers stay silent
|
|
619
619
|
* on empty output.
|
|
620
620
|
*
|
|
621
|
+
* Protocol gate (rc.18): only `payload.version === 2` payloads are
|
|
622
|
+
* rendered. Anything else returns []. When the payload exists but carries
|
|
623
|
+
* a mismatched (non-undefined) version, a one-line stderr breadcrumb is
|
|
624
|
+
* emitted as a debug aid — see `_protocol-v2-decisions.md` (Decision 2,
|
|
625
|
+
* "silent-skip + one-line stderr breadcrumb"). The wire field is
|
|
626
|
+
* `payload.entries` (renamed from `payload.narrow` in protocol v2,
|
|
627
|
+
* Decision 1).
|
|
628
|
+
*
|
|
621
629
|
* Output shape:
|
|
622
630
|
* [fabric] N narrow-scoped knowledge entries match your edit targets:
|
|
623
631
|
* [<id>] (<type>/<maturity>) <summary>
|
|
@@ -625,13 +633,28 @@ function formatEntryLine(entry) {
|
|
|
625
633
|
* (如需重读 broad 决策,调 fab_plan_context 或 fabric plan-context-hint --all)
|
|
626
634
|
*/
|
|
627
635
|
function renderSummary(payload) {
|
|
628
|
-
|
|
629
|
-
|
|
636
|
+
if (!payload || payload.version !== 2) {
|
|
637
|
+
if (payload && payload.version !== undefined) {
|
|
638
|
+
// breadcrumb only if payload exists but version mismatches (avoid
|
|
639
|
+
// spam on null). Best-effort write — silent-on-failure honors the
|
|
640
|
+
// hook's "never block edits" contract.
|
|
641
|
+
try {
|
|
642
|
+
process.stderr.write(
|
|
643
|
+
`[fabric] hint payload version=${payload.version} unsupported (expected 2), skipping\n`,
|
|
644
|
+
);
|
|
645
|
+
} catch {
|
|
646
|
+
// ignore — stderr unavailable, silent-skip still applies
|
|
647
|
+
}
|
|
648
|
+
}
|
|
649
|
+
return [];
|
|
650
|
+
}
|
|
651
|
+
const entries = Array.isArray(payload.entries) ? payload.entries : [];
|
|
652
|
+
if (entries.length === 0) return [];
|
|
630
653
|
|
|
631
654
|
const lines = [
|
|
632
|
-
`[fabric] ${
|
|
655
|
+
`[fabric] ${entries.length} narrow-scoped knowledge entries match your edit targets:`,
|
|
633
656
|
];
|
|
634
|
-
for (const entry of
|
|
657
|
+
for (const entry of entries) {
|
|
635
658
|
lines.push(formatEntryLine(entry));
|
|
636
659
|
}
|
|
637
660
|
lines.push(" (如需重读 broad 决策,调 fab_plan_context 或 fabric plan-context-hint --all)");
|
|
@@ -699,7 +722,8 @@ function main(env, stdio) {
|
|
|
699
722
|
: invokePlanContextHint(cwd, paths);
|
|
700
723
|
if (cliPayload === null || cliPayload === undefined) return;
|
|
701
724
|
|
|
702
|
-
|
|
725
|
+
// Protocol v2 (rc.18 TASK-005): wire field is `entries`, no v1 shim.
|
|
726
|
+
const narrow = Array.isArray(cliPayload.entries) ? cliPayload.entries : [];
|
|
703
727
|
if (narrow.length === 0) {
|
|
704
728
|
// rc.6 TASK-023 (E6): silence-counter — matched-narrow == 0. The CLI
|
|
705
729
|
// had a chance to match against the extracted paths but came back
|
|
@@ -764,7 +788,7 @@ function main(env, stdio) {
|
|
|
764
788
|
});
|
|
765
789
|
}
|
|
766
790
|
|
|
767
|
-
const lines = renderSummary({ ...cliPayload,
|
|
791
|
+
const lines = renderSummary({ ...cliPayload, entries: gateDecision.narrow });
|
|
768
792
|
if (lines.length === 0) return;
|
|
769
793
|
for (const line of lines) {
|
|
770
794
|
err.write(`${line}\n`);
|
|
@@ -0,0 +1,252 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* v2.0.0-rc.16 TASK-001 (F2-prep): shared banner-i18n library for hook scripts.
|
|
3
|
+
*
|
|
4
|
+
* Provides:
|
|
5
|
+
* - readFabricLanguage(projectRoot) → 'zh-CN' | 'en' | 'zh-CN-hybrid' | 'match-existing'
|
|
6
|
+
* Synchronously reads `fabric_language` from `.fabric/fabric-config.json`.
|
|
7
|
+
* Mirrors the never-throw contract of the existing config readers in
|
|
8
|
+
* fabric-hint.cjs (readReviewHintPendingCount, readMaintenanceHintDays, etc.):
|
|
9
|
+
* missing file / parse error / missing field → returns 'zh-CN' (the
|
|
10
|
+
* documented BACKWARD-COMPAT default — preserves rc.15 hard-coded zh-CN
|
|
11
|
+
* hook output when fabric_language was never a configurable key).
|
|
12
|
+
*
|
|
13
|
+
* 'match-existing' is ONLY returned when explicitly set in config; per UX
|
|
14
|
+
* i18n Policy class 1, renderBanner then folds 'match-existing' (and any
|
|
15
|
+
* unknown variant) down to 'en'.
|
|
16
|
+
*
|
|
17
|
+
* RC.16 TASK-002 NOTE: this default was originally 'match-existing' in
|
|
18
|
+
* TASK-001 but was tightened to 'zh-CN' here so that the existing rc.7+
|
|
19
|
+
* fabric-hint test fixtures (which never set fabric_language and expect
|
|
20
|
+
* zh-CN substrings byte-identically) continue passing without modification.
|
|
21
|
+
* Pre-user clean-slate policy: no shim, but back-compat for in-tree tests
|
|
22
|
+
* is the right line — they encode the rc.15 user-visible contract.
|
|
23
|
+
*
|
|
24
|
+
* - renderBanner(key, variant, params) → string
|
|
25
|
+
* Renders one of the 11 banner fragments for the requested variant.
|
|
26
|
+
* Variant fallback: STRINGS[key][variant] ?? STRINGS[key]['en'].
|
|
27
|
+
* Unknown / 'match-existing' / missing → 'en' table.
|
|
28
|
+
*
|
|
29
|
+
* - STRINGS — exported for test introspection only (read-only by convention).
|
|
30
|
+
*
|
|
31
|
+
* Banner keys (11 total):
|
|
32
|
+
* Signal A (archive): archiveLine1, archiveActivity, archiveCta
|
|
33
|
+
* Signal B (review): reviewLine1, reviewCta
|
|
34
|
+
* Signal C (import): importLine1, importCta
|
|
35
|
+
* Signal D (maintenance): maintenanceLine1Never, maintenanceLine1Aged, maintenanceLine2
|
|
36
|
+
* Broad hook: broadImportBanner
|
|
37
|
+
*
|
|
38
|
+
* Protected tokens — NEVER translated, kept verbatim across all 4 variants:
|
|
39
|
+
* - Slash commands: /fabric-archive, /fabric-review, /fabric-import
|
|
40
|
+
* - CLI commands: `fabric doctor --lint`
|
|
41
|
+
* - Numeric / template substrings the existing tests assert on:
|
|
42
|
+
* "${hoursElapsed.toFixed(1)}h" (e.g. "25.0h"), "阈值 ${N}h",
|
|
43
|
+
* "${count} 条", "${nodeCount}/${threshold}", "${days} 天"
|
|
44
|
+
* - 📋 Fabric: emoji prefix
|
|
45
|
+
*
|
|
46
|
+
* zh-CN-hybrid policy: Chinese narrative prose with English protected tokens
|
|
47
|
+
* preserved verbatim. In practice this matches zh-CN exactly because the
|
|
48
|
+
* banners already inline slash commands + CLI commands without translation;
|
|
49
|
+
* we keep the variant entries explicit anyway for forward-compat (future copy
|
|
50
|
+
* may diverge, e.g. mixing English connector words).
|
|
51
|
+
*
|
|
52
|
+
* match-existing policy: per UX i18n Policy class 1, falls back to 'en' at
|
|
53
|
+
* render time. The fallback decision is centralized in renderBanner; only an
|
|
54
|
+
* EXPLICIT `fabric_language: "match-existing"` in config triggers the en
|
|
55
|
+
* fallback. Unset / missing config defaults to 'zh-CN' (rc.15 back-compat).
|
|
56
|
+
*
|
|
57
|
+
* Pattern reference:
|
|
58
|
+
* - Never-throw fs read: fabric-hint.cjs `_readConfigNumber`,
|
|
59
|
+
* `readReviewHintPendingCount` (lines 720-743)
|
|
60
|
+
* - hooks/lib/*.cjs precedent: session-digest-writer.cjs
|
|
61
|
+
*/
|
|
62
|
+
"use strict";
|
|
63
|
+
|
|
64
|
+
const { existsSync, readFileSync } = require("node:fs");
|
|
65
|
+
const { join } = require("node:path");
|
|
66
|
+
|
|
67
|
+
const FABRIC_DIR = ".fabric";
|
|
68
|
+
const CONFIG_FILE = "fabric-config.json";
|
|
69
|
+
|
|
70
|
+
const VALID_LANGUAGES = ["zh-CN", "en", "zh-CN-hybrid", "match-existing"];
|
|
71
|
+
// rc.16 TASK-002: backward-compat default. rc.15 and earlier hardcoded
|
|
72
|
+
// zh-CN in the hook scripts; preserving zh-CN as the unset-default keeps
|
|
73
|
+
// the rc.7+ fabric-hint test fixtures (which assert Chinese substrings
|
|
74
|
+
// without ever setting fabric_language) green and matches the user-visible
|
|
75
|
+
// contract real workspaces have observed since rc.7.
|
|
76
|
+
const DEFAULT_LANGUAGE = "zh-CN";
|
|
77
|
+
const RENDER_FALLBACK_VARIANT = "en";
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Read `fabric_language` from <projectRoot>/.fabric/fabric-config.json.
|
|
81
|
+
*
|
|
82
|
+
* Returns one of the four valid language codes. Missing file, malformed JSON,
|
|
83
|
+
* missing/unknown field value → DEFAULT_LANGUAGE ('zh-CN' — see comment on
|
|
84
|
+
* the constant for the back-compat rationale). NEVER throws — config-read
|
|
85
|
+
* failure must not block any hook.
|
|
86
|
+
*/
|
|
87
|
+
function readFabricLanguage(projectRoot) {
|
|
88
|
+
if (typeof projectRoot !== "string" || projectRoot.length === 0) {
|
|
89
|
+
return DEFAULT_LANGUAGE;
|
|
90
|
+
}
|
|
91
|
+
const configPath = join(projectRoot, FABRIC_DIR, CONFIG_FILE);
|
|
92
|
+
if (!existsSync(configPath)) return DEFAULT_LANGUAGE;
|
|
93
|
+
try {
|
|
94
|
+
const parsed = JSON.parse(readFileSync(configPath, "utf8"));
|
|
95
|
+
const v = parsed && parsed.fabric_language;
|
|
96
|
+
if (typeof v === "string" && VALID_LANGUAGES.indexOf(v) !== -1) {
|
|
97
|
+
return v;
|
|
98
|
+
}
|
|
99
|
+
} catch {
|
|
100
|
+
// fall through to default
|
|
101
|
+
}
|
|
102
|
+
return DEFAULT_LANGUAGE;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
// ---------------------------------------------------------------------------
|
|
106
|
+
// String table
|
|
107
|
+
// ---------------------------------------------------------------------------
|
|
108
|
+
//
|
|
109
|
+
// Each key maps variant -> (params) => string. Templates intentionally use
|
|
110
|
+
// the same parameter names across all four variants so call sites pass one
|
|
111
|
+
// shape. Substring contracts (see file header) are preserved verbatim across
|
|
112
|
+
// translations; only narrative connector words shift.
|
|
113
|
+
// ---------------------------------------------------------------------------
|
|
114
|
+
|
|
115
|
+
const STRINGS = {
|
|
116
|
+
// ---- Signal A: archive ----------------------------------------------------
|
|
117
|
+
// Source (zh-CN): fabric-hint.cjs:614 `📋 Fabric: 距上次归档 ${parts}。`
|
|
118
|
+
// params: { parts } where parts is pre-joined `已过 25.0h(阈值 24h)` etc.
|
|
119
|
+
archiveLine1: {
|
|
120
|
+
"zh-CN": (p) => `📋 Fabric: 距上次归档 ${p.parts}。`,
|
|
121
|
+
en: (p) => `📋 Fabric: ${p.parts} since last archive.`,
|
|
122
|
+
"zh-CN-hybrid": (p) => `📋 Fabric: 距上次归档 ${p.parts}。`,
|
|
123
|
+
},
|
|
124
|
+
|
|
125
|
+
// Source (zh-CN): fabric-hint.cjs:619 ` 最近活动集中在: ${activity}。`
|
|
126
|
+
// params: { activity }
|
|
127
|
+
archiveActivity: {
|
|
128
|
+
"zh-CN": (p) => ` 最近活动集中在: ${p.activity}。`,
|
|
129
|
+
en: (p) => ` Recent activity centered on: ${p.activity}.`,
|
|
130
|
+
"zh-CN-hybrid": (p) => ` 最近活动集中在: ${p.activity}。`,
|
|
131
|
+
},
|
|
132
|
+
|
|
133
|
+
// Source (zh-CN): fabric-hint.cjs:621 ` 是否调 /fabric-archive 检查值得归档的决策/踩坑/复用?`
|
|
134
|
+
// params: {} — protected token /fabric-archive verbatim across all variants.
|
|
135
|
+
archiveCta: {
|
|
136
|
+
"zh-CN": () => " 是否调 /fabric-archive 检查值得归档的决策/踩坑/复用?",
|
|
137
|
+
en: () => " Run /fabric-archive to review decisions/pitfalls/reusables worth archiving?",
|
|
138
|
+
"zh-CN-hybrid": () => " 是否调 /fabric-archive 检查值得归档的决策/踩坑/复用?",
|
|
139
|
+
},
|
|
140
|
+
|
|
141
|
+
// ---- Signal B: review -----------------------------------------------------
|
|
142
|
+
// Source (zh-CN): fabric-hint.cjs:651 `📋 Fabric: 已积累 ${stats.count} 条待审核知识${ageSuffix}。`
|
|
143
|
+
// params: { count, ageSuffix } — ageSuffix is " / 最早一条 N.N 天前" or "" (zh-CN only)
|
|
144
|
+
// For en variant we shape the suffix inline to keep substring "${count}" addressable.
|
|
145
|
+
reviewLine1: {
|
|
146
|
+
"zh-CN": (p) => `📋 Fabric: 已积累 ${p.count} 条待审核知识${p.ageSuffix || ""}。`,
|
|
147
|
+
en: (p) => {
|
|
148
|
+
const suffix =
|
|
149
|
+
p.ageSuffix && p.ageSuffix.length > 0
|
|
150
|
+
? p.ageSuffix
|
|
151
|
+
.replace(" / 最早一条 ", " / oldest is ")
|
|
152
|
+
.replace(" 天前", "d old")
|
|
153
|
+
: "";
|
|
154
|
+
return `📋 Fabric: ${p.count} pending knowledge entries accumulated${suffix}.`;
|
|
155
|
+
},
|
|
156
|
+
"zh-CN-hybrid": (p) => `📋 Fabric: 已积累 ${p.count} 条待审核知识${p.ageSuffix || ""}。`,
|
|
157
|
+
},
|
|
158
|
+
|
|
159
|
+
// Source (zh-CN): fabric-hint.cjs:652 ` 是否调 /fabric-review 审核 pending/ 条目?`
|
|
160
|
+
// params: {} — protected token /fabric-review verbatim across all variants.
|
|
161
|
+
reviewCta: {
|
|
162
|
+
"zh-CN": () => " 是否调 /fabric-review 审核 pending/ 条目?",
|
|
163
|
+
en: () => " Run /fabric-review to triage pending/ entries?",
|
|
164
|
+
"zh-CN-hybrid": () => " 是否调 /fabric-review 审核 pending/ 条目?",
|
|
165
|
+
},
|
|
166
|
+
|
|
167
|
+
// ---- Signal C: import (underseed) ----------------------------------------
|
|
168
|
+
// Source (zh-CN): fabric-hint.cjs:697 `📋 Fabric: 知识库节点数 ${nodeCount}/${threshold},距 init_scan_completed ${hoursSinceInit.toFixed(1)}h。`
|
|
169
|
+
// params: { nodeCount, threshold, hoursSinceInit } — caller supplies hoursSinceInit
|
|
170
|
+
// already toFixed(1)'d (i.e. as string "24.5") to keep all rendering pure.
|
|
171
|
+
importLine1: {
|
|
172
|
+
"zh-CN": (p) =>
|
|
173
|
+
`📋 Fabric: 知识库节点数 ${p.nodeCount}/${p.threshold},距 init_scan_completed ${p.hoursSinceInit}h。`,
|
|
174
|
+
en: (p) =>
|
|
175
|
+
`📋 Fabric: knowledge node count ${p.nodeCount}/${p.threshold}, ${p.hoursSinceInit}h since init_scan_completed.`,
|
|
176
|
+
"zh-CN-hybrid": (p) =>
|
|
177
|
+
`📋 Fabric: 知识库节点数 ${p.nodeCount}/${p.threshold},距 init_scan_completed ${p.hoursSinceInit}h。`,
|
|
178
|
+
},
|
|
179
|
+
|
|
180
|
+
// Source (zh-CN): fabric-hint.cjs:698 ` 是否调 /fabric-import 从 git 历史与现有文档回灌知识?`
|
|
181
|
+
// params: {} — protected token /fabric-import verbatim across all variants.
|
|
182
|
+
importCta: {
|
|
183
|
+
"zh-CN": () => " 是否调 /fabric-import 从 git 历史与现有文档回灌知识?",
|
|
184
|
+
en: () => " Run /fabric-import to backfill knowledge from git history and existing docs?",
|
|
185
|
+
"zh-CN-hybrid": () => " 是否调 /fabric-import 从 git 历史与现有文档回灌知识?",
|
|
186
|
+
},
|
|
187
|
+
|
|
188
|
+
// ---- Signal D: maintenance -----------------------------------------------
|
|
189
|
+
// Source (zh-CN): fabric-hint.cjs:931 `📋 Fabric: 从未运行 lint 检查。`
|
|
190
|
+
// params: {} — substring "从未运行 lint 检查" is test-asserted (zh-CN test).
|
|
191
|
+
maintenanceLine1Never: {
|
|
192
|
+
"zh-CN": () => "📋 Fabric: 从未运行 lint 检查。",
|
|
193
|
+
en: () => "📋 Fabric: lint check has never been run.",
|
|
194
|
+
"zh-CN-hybrid": () => "📋 Fabric: 从未运行 lint 检查。",
|
|
195
|
+
},
|
|
196
|
+
|
|
197
|
+
// Source (zh-CN): fabric-hint.cjs:932 `📋 Fabric: 已 ${days} 天未跑 lint 检查(实际 ${ageDays.toFixed(1)}d)。`
|
|
198
|
+
// params: { days, ageDays } — ageDays caller-supplied as already-toFixed(1) string.
|
|
199
|
+
// Substring "已 N 天未跑 lint" is test-asserted (zh-CN test).
|
|
200
|
+
maintenanceLine1Aged: {
|
|
201
|
+
"zh-CN": (p) => `📋 Fabric: 已 ${p.days} 天未跑 lint 检查(实际 ${p.ageDays}d)。`,
|
|
202
|
+
en: (p) => `📋 Fabric: ${p.days} days since the last lint check (actual ${p.ageDays}d).`,
|
|
203
|
+
"zh-CN-hybrid": (p) => `📋 Fabric: 已 ${p.days} 天未跑 lint 检查(实际 ${p.ageDays}d)。`,
|
|
204
|
+
},
|
|
205
|
+
|
|
206
|
+
// Source (zh-CN): fabric-hint.cjs:929 ` 是否调 \`fabric doctor --lint\` 看看知识库健康度?`
|
|
207
|
+
// params: {} — protected token `fabric doctor --lint` (with backticks) verbatim.
|
|
208
|
+
maintenanceLine2: {
|
|
209
|
+
"zh-CN": () => " 是否调 `fabric doctor --lint` 看看知识库健康度?",
|
|
210
|
+
en: () => " Run `fabric doctor --lint` to check knowledge-base health?",
|
|
211
|
+
"zh-CN-hybrid": () => " 是否调 `fabric doctor --lint` 看看知识库健康度?",
|
|
212
|
+
},
|
|
213
|
+
|
|
214
|
+
// ---- Broad hook: import recommendation ------------------------------------
|
|
215
|
+
// Source (zh-CN): knowledge-hint-broad.cjs:262
|
|
216
|
+
// " 📋 Fabric: 知识库稀疏,是否调 /fabric-import 从 git 历史与现有文档回灌知识?"
|
|
217
|
+
// Note: leading two spaces are intentional (existing banner indent).
|
|
218
|
+
// params: {} — protected token /fabric-import verbatim.
|
|
219
|
+
broadImportBanner: {
|
|
220
|
+
"zh-CN": () => " 📋 Fabric: 知识库稀疏,是否调 /fabric-import 从 git 历史与现有文档回灌知识?",
|
|
221
|
+
en: () =>
|
|
222
|
+
" 📋 Fabric: knowledge base is sparse — run /fabric-import to backfill from git history and existing docs?",
|
|
223
|
+
"zh-CN-hybrid": () =>
|
|
224
|
+
" 📋 Fabric: 知识库稀疏,是否调 /fabric-import 从 git 历史与现有文档回灌知识?",
|
|
225
|
+
},
|
|
226
|
+
};
|
|
227
|
+
|
|
228
|
+
/**
|
|
229
|
+
* Render a banner fragment for the requested variant.
|
|
230
|
+
*
|
|
231
|
+
* Variant resolution:
|
|
232
|
+
* 1. If STRINGS[key][variant] exists → use it.
|
|
233
|
+
* 2. Else fall back to STRINGS[key][RENDER_FALLBACK_VARIANT] ('en').
|
|
234
|
+
* 3. If key itself is unknown → returns "" (defensive; never throws).
|
|
235
|
+
*
|
|
236
|
+
* 'match-existing' is intentionally NOT in the STRINGS table so it folds
|
|
237
|
+
* down to the 'en' fallback per UX i18n Policy class 1.
|
|
238
|
+
*/
|
|
239
|
+
function renderBanner(key, variant, params) {
|
|
240
|
+
const entry = STRINGS[key];
|
|
241
|
+
if (!entry) return "";
|
|
242
|
+
const tmpl = entry[variant] || entry[RENDER_FALLBACK_VARIANT];
|
|
243
|
+
if (typeof tmpl !== "function") return "";
|
|
244
|
+
try {
|
|
245
|
+
return tmpl(params || {});
|
|
246
|
+
} catch {
|
|
247
|
+
// Defensive: a missing param shouldn't crash the hook.
|
|
248
|
+
return "";
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
module.exports = { readFabricLanguage, renderBanner, STRINGS };
|
package/dist/chunk-Q72D24BG.js
DELETED
|
@@ -1,186 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
import {
|
|
3
|
-
addFabricKnowledgeBaseSection,
|
|
4
|
-
installArchiveHintHook,
|
|
5
|
-
installFabricArchiveSkill,
|
|
6
|
-
installFabricImportSkill,
|
|
7
|
-
installFabricReviewSkill,
|
|
8
|
-
installKnowledgeHintBroadHook,
|
|
9
|
-
installKnowledgeHintNarrowHook,
|
|
10
|
-
mergeClaudeCodeHookConfig,
|
|
11
|
-
mergeCodexHookConfig,
|
|
12
|
-
mergeCursorHookConfig,
|
|
13
|
-
readFabricLanguagePreference
|
|
14
|
-
} from "./chunk-X7QPY5KH.js";
|
|
15
|
-
import {
|
|
16
|
-
t
|
|
17
|
-
} from "./chunk-6ICJICVU.js";
|
|
18
|
-
|
|
19
|
-
// src/commands/hooks.ts
|
|
20
|
-
import { existsSync, statSync } from "fs";
|
|
21
|
-
import { isAbsolute, join, resolve } from "path";
|
|
22
|
-
import { defineCommand } from "citty";
|
|
23
|
-
var hooksCommand = defineCommand({
|
|
24
|
-
meta: {
|
|
25
|
-
name: "hooks",
|
|
26
|
-
description: t("cli.hooks.description")
|
|
27
|
-
},
|
|
28
|
-
subCommands: {
|
|
29
|
-
install: defineCommand({
|
|
30
|
-
meta: {
|
|
31
|
-
name: "install",
|
|
32
|
-
description: t("cli.hooks.install.description")
|
|
33
|
-
},
|
|
34
|
-
args: {
|
|
35
|
-
target: {
|
|
36
|
-
type: "string",
|
|
37
|
-
description: t("cli.hooks.install.args.target.description"),
|
|
38
|
-
default: process.cwd()
|
|
39
|
-
}
|
|
40
|
-
},
|
|
41
|
-
async run({ args }) {
|
|
42
|
-
const result = await installHooks(args.target);
|
|
43
|
-
for (const path of result.installed) {
|
|
44
|
-
console.log(`installed ${path}`);
|
|
45
|
-
}
|
|
46
|
-
for (const path of result.skipped) {
|
|
47
|
-
console.log(`skipped ${path}`);
|
|
48
|
-
}
|
|
49
|
-
for (const message of result.errors) {
|
|
50
|
-
console.error(`error ${message}`);
|
|
51
|
-
}
|
|
52
|
-
}
|
|
53
|
-
})
|
|
54
|
-
}
|
|
55
|
-
});
|
|
56
|
-
var hooks_default = hooksCommand;
|
|
57
|
-
async function installHooks(target, _options = {}) {
|
|
58
|
-
const normalizedTarget = normalizeTarget(target);
|
|
59
|
-
assertExistingDirectory(normalizedTarget);
|
|
60
|
-
const results = [];
|
|
61
|
-
results.push(...await runStep(() => installFabricArchiveSkill(normalizedTarget)));
|
|
62
|
-
results.push(...await runStep(() => installFabricReviewSkill(normalizedTarget)));
|
|
63
|
-
results.push(...await runStep(() => installFabricImportSkill(normalizedTarget)));
|
|
64
|
-
results.push(...await runStep(() => installArchiveHintHook(normalizedTarget)));
|
|
65
|
-
results.push(...await runStep(() => installKnowledgeHintBroadHook(normalizedTarget)));
|
|
66
|
-
results.push(...await runStep(() => installKnowledgeHintNarrowHook(normalizedTarget)));
|
|
67
|
-
results.push(await runSingleStep("claude-hook-config", () => mergeClaudeCodeHookConfig(normalizedTarget)));
|
|
68
|
-
results.push(await runSingleStep("codex-hook-config", () => mergeCodexHookConfig(normalizedTarget)));
|
|
69
|
-
results.push(await runSingleStep("cursor-hook-config", () => mergeCursorHookConfig(normalizedTarget)));
|
|
70
|
-
const fabricLanguage = readFabricLanguagePreference(normalizedTarget);
|
|
71
|
-
results.push(...await runStep(() => addFabricKnowledgeBaseSection(normalizedTarget, fabricLanguage)));
|
|
72
|
-
results.push(...validateHookPaths(normalizedTarget));
|
|
73
|
-
return summarizeResults(results);
|
|
74
|
-
}
|
|
75
|
-
function validateHookPaths(projectRoot) {
|
|
76
|
-
const scripts = [
|
|
77
|
-
{ stepSuffix: "", hookFile: "fabric-hint.cjs" },
|
|
78
|
-
{ stepSuffix: "-broad", hookFile: "knowledge-hint-broad.cjs" },
|
|
79
|
-
{ stepSuffix: "-narrow", hookFile: "knowledge-hint-narrow.cjs" }
|
|
80
|
-
];
|
|
81
|
-
const clients = [
|
|
82
|
-
{
|
|
83
|
-
client: "claude",
|
|
84
|
-
configRel: join(".claude", "settings.json"),
|
|
85
|
-
hookDir: join(".claude", "hooks")
|
|
86
|
-
},
|
|
87
|
-
{
|
|
88
|
-
client: "codex",
|
|
89
|
-
configRel: join(".codex", "hooks.json"),
|
|
90
|
-
hookDir: join(".codex", "hooks")
|
|
91
|
-
},
|
|
92
|
-
{
|
|
93
|
-
client: "cursor",
|
|
94
|
-
configRel: join(".cursor", "hooks.json"),
|
|
95
|
-
hookDir: join(".cursor", "hooks")
|
|
96
|
-
}
|
|
97
|
-
];
|
|
98
|
-
const results = [];
|
|
99
|
-
for (const { client, configRel, hookDir } of clients) {
|
|
100
|
-
const configPath = resolve(projectRoot, configRel);
|
|
101
|
-
if (!existsSync(configPath)) {
|
|
102
|
-
results.push({
|
|
103
|
-
step: `hook-validate-${client}`,
|
|
104
|
-
path: configPath,
|
|
105
|
-
status: "skipped",
|
|
106
|
-
message: "missing-config"
|
|
107
|
-
});
|
|
108
|
-
continue;
|
|
109
|
-
}
|
|
110
|
-
for (const { stepSuffix, hookFile } of scripts) {
|
|
111
|
-
const expectedHookPath = resolve(projectRoot, hookDir, hookFile);
|
|
112
|
-
const expectedHookRel = join(hookDir, hookFile);
|
|
113
|
-
const step = `hook-validate-${client}${stepSuffix}`;
|
|
114
|
-
if (!existsSync(expectedHookPath)) {
|
|
115
|
-
results.push({
|
|
116
|
-
step,
|
|
117
|
-
path: expectedHookPath,
|
|
118
|
-
status: "error",
|
|
119
|
-
message: `hook script missing: ${expectedHookRel}`
|
|
120
|
-
});
|
|
121
|
-
continue;
|
|
122
|
-
}
|
|
123
|
-
results.push({ step, path: expectedHookPath, status: "skipped", message: "ok" });
|
|
124
|
-
}
|
|
125
|
-
}
|
|
126
|
-
return results;
|
|
127
|
-
}
|
|
128
|
-
async function runStep(fn) {
|
|
129
|
-
try {
|
|
130
|
-
return await fn();
|
|
131
|
-
} catch (error) {
|
|
132
|
-
return [
|
|
133
|
-
{
|
|
134
|
-
step: "hook-install",
|
|
135
|
-
path: "",
|
|
136
|
-
status: "error",
|
|
137
|
-
message: error instanceof Error ? error.message : String(error)
|
|
138
|
-
}
|
|
139
|
-
];
|
|
140
|
-
}
|
|
141
|
-
}
|
|
142
|
-
async function runSingleStep(step, fn) {
|
|
143
|
-
try {
|
|
144
|
-
return await fn();
|
|
145
|
-
} catch (error) {
|
|
146
|
-
return {
|
|
147
|
-
step,
|
|
148
|
-
path: "",
|
|
149
|
-
status: "error",
|
|
150
|
-
message: error instanceof Error ? error.message : String(error)
|
|
151
|
-
};
|
|
152
|
-
}
|
|
153
|
-
}
|
|
154
|
-
function summarizeResults(results) {
|
|
155
|
-
const installed = [];
|
|
156
|
-
const skipped = [];
|
|
157
|
-
const errors = [];
|
|
158
|
-
for (const r of results) {
|
|
159
|
-
switch (r.status) {
|
|
160
|
-
case "written":
|
|
161
|
-
installed.push(r.path);
|
|
162
|
-
break;
|
|
163
|
-
case "skipped":
|
|
164
|
-
skipped.push(r.path);
|
|
165
|
-
break;
|
|
166
|
-
case "error":
|
|
167
|
-
errors.push(`${r.step} ${r.path}: ${r.message ?? "unknown error"}`);
|
|
168
|
-
break;
|
|
169
|
-
}
|
|
170
|
-
}
|
|
171
|
-
return { installed, skipped, errors };
|
|
172
|
-
}
|
|
173
|
-
function normalizeTarget(targetInput) {
|
|
174
|
-
return isAbsolute(targetInput) ? targetInput : resolve(process.cwd(), targetInput);
|
|
175
|
-
}
|
|
176
|
-
function assertExistingDirectory(target) {
|
|
177
|
-
if (!existsSync(target) || !statSync(target).isDirectory()) {
|
|
178
|
-
throw new Error(t("cli.shared.target-invalid", { target }));
|
|
179
|
-
}
|
|
180
|
-
}
|
|
181
|
-
|
|
182
|
-
export {
|
|
183
|
-
hooksCommand,
|
|
184
|
-
hooks_default,
|
|
185
|
-
installHooks
|
|
186
|
-
};
|