@fenglimg/fabric-cli 2.2.0-rc.1 → 2.2.0-rc.3

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 (35) hide show
  1. package/dist/chunk-5LQIHYFC.js +64 -0
  2. package/dist/chunk-5ZUMLCD5.js +248 -0
  3. package/dist/chunk-EOT63RDH.js +36 -0
  4. package/dist/{chunk-AOE6AYI7.js → chunk-F6ITRM7T.js} +2 -2
  5. package/dist/{chunk-WU6GAPKH.js → chunk-H3FE6VIK.js} +3 -5
  6. package/dist/chunk-XCBVSGCS.js +25 -0
  7. package/dist/{chunk-2R55HNVD.js → chunk-XHHCRDIR.js} +71 -6
  8. package/dist/{config-XYRBZJDU.js → config-VJMXCLXW.js} +1 -1
  9. package/dist/{doctor-YONYXDX6.js → doctor-J4O3X54I.js} +118 -7
  10. package/dist/index.js +13 -12
  11. package/dist/{install-74ANPCCP.js → install-BULNDUIM.js} +159 -80
  12. package/dist/{plan-context-hint-FC6P3WFE.js → plan-context-hint-CHVZGOZ5.js} +21 -8
  13. package/dist/{scope-explain-CDIZESP5.js → scope-explain-BWRWBCCP.js} +14 -4
  14. package/dist/{status-GLQWLWH6.js → status-PANEGKU2.js} +17 -6
  15. package/dist/store-66NK2FTQ.js +443 -0
  16. package/dist/{sync-UJ4BBCZJ.js → sync-EA5HZMXM.js} +165 -21
  17. package/dist/{uninstall-C3QXKOO6.js → uninstall-F75MPKQC.js} +27 -1
  18. package/dist/{whoami-2MLO4Y37.js → whoami-66YKY5DZ.js} +16 -5
  19. package/package.json +3 -3
  20. package/templates/hooks/cite-policy-evict.cjs +412 -160
  21. package/templates/hooks/configs/claude-code.json +17 -2
  22. package/templates/hooks/configs/codex-hooks.json +14 -2
  23. package/templates/hooks/configs/cursor-hooks.json +14 -2
  24. package/templates/hooks/fabric-hint.cjs +151 -15
  25. package/templates/hooks/knowledge-hint-broad.cjs +12 -1
  26. package/templates/hooks/knowledge-hint-narrow.cjs +54 -1
  27. package/templates/hooks/post-tooluse-mutation.cjs +285 -0
  28. package/templates/hooks/session-end-marker.cjs +140 -0
  29. package/templates/skills/fabric-archive/SKILL.md +7 -1
  30. package/dist/chunk-4R2CYEA4.js +0 -116
  31. package/dist/chunk-L4Q55UC4.js +0 -52
  32. package/dist/chunk-LFIKMVY7.js +0 -27
  33. package/dist/chunk-RYAFBNES.js +0 -33
  34. package/dist/chunk-T5RPGCCM.js +0 -40
  35. package/dist/store-XB3ADT65.js +0 -144
@@ -1,23 +1,22 @@
1
1
  #!/usr/bin/env node
2
2
  import {
3
3
  regenerateBindingsSnapshot
4
- } from "./chunk-WU6GAPKH.js";
5
- import "./chunk-L4Q55UC4.js";
4
+ } from "./chunk-H3FE6VIK.js";
5
+ import "./chunk-EOT63RDH.js";
6
6
  import {
7
7
  getProjectTranslator
8
8
  } from "./chunk-2CY4BMTH.js";
9
- import "./chunk-LFIKMVY7.js";
10
9
  import {
11
10
  loadGlobalConfig,
12
11
  resolveGlobalRoot
13
- } from "./chunk-RYAFBNES.js";
12
+ } from "./chunk-XCBVSGCS.js";
14
13
 
15
14
  // src/commands/sync.ts
16
15
  import { defineCommand } from "citty";
17
16
 
18
17
  // src/sync/run-sync.ts
19
18
  import { execFileSync } from "child_process";
20
- import { existsSync, mkdirSync, readFileSync, rmSync, writeFileSync } from "fs";
19
+ import { existsSync, mkdirSync, readFileSync, renameSync, rmSync, writeFileSync } from "fs";
21
20
  import { join } from "path";
22
21
  import { GLOBAL_STATE_DIR, storeRelativePath } from "@fenglimg/fabric-shared";
23
22
  import { GenericIOError } from "@fenglimg/fabric-shared/errors";
@@ -33,6 +32,7 @@ function syncTransition(state, event) {
33
32
  case "conflict":
34
33
  if (event === "user_continue") return "synced";
35
34
  if (event === "user_abort") return "aborted";
35
+ if (event === "network_unavailable") return "offline";
36
36
  break;
37
37
  case "offline":
38
38
  if (event === "retry" || event === "rebase_clean") return "synced";
@@ -88,13 +88,31 @@ function loadSession(globalRoot) {
88
88
  if (!existsSync(path)) {
89
89
  return null;
90
90
  }
91
- return JSON.parse(readFileSync(path, "utf8"));
91
+ const raw = readFileSync(path, "utf8");
92
+ try {
93
+ return JSON.parse(raw);
94
+ } catch (error) {
95
+ const corruptedPath = `${path}.corrupted.${Date.now()}`;
96
+ try {
97
+ writeFileSync(corruptedPath, raw, "utf8");
98
+ } catch {
99
+ }
100
+ throw new GenericIOError(
101
+ `sync-session.json is corrupt (forensic copy: ${corruptedPath}). Parse error: ${error instanceof Error ? error.message : String(error)}`,
102
+ {
103
+ actionHint: `Delete ${path} to start a fresh sync (any in-progress rebase must be resolved manually with git first).`,
104
+ details: { path, corruptedPath }
105
+ }
106
+ );
107
+ }
92
108
  }
93
109
  function saveSession(globalRoot, session) {
94
110
  const path = syncSessionPath(globalRoot);
95
111
  mkdirSync(join(path, ".."), { recursive: true });
96
- writeFileSync(path, `${JSON.stringify(session, null, 2)}
112
+ const tmpPath = `${path}.${process.pid}.tmp`;
113
+ writeFileSync(tmpPath, `${JSON.stringify(session, null, 2)}
97
114
  `, "utf8");
115
+ renameSync(tmpPath, path);
98
116
  }
99
117
  function clearSession(globalRoot) {
100
118
  rmSync(syncSessionPath(globalRoot), { force: true });
@@ -127,28 +145,105 @@ function gitErrText(error, key) {
127
145
  const value = error[key];
128
146
  return typeof value === "string" || Buffer.isBuffer(value) ? String(value) : "";
129
147
  }
148
+ function defaultPush(storeDir) {
149
+ try {
150
+ execFileSync("git", ["push"], {
151
+ cwd: storeDir,
152
+ stdio: ["ignore", "pipe", "pipe"]
153
+ });
154
+ return "clean";
155
+ } catch (error) {
156
+ const detail = `${gitErrText(error, "stdout")}${gitErrText(error, "stderr")}`;
157
+ if (/could not resolve host|could not read from remote|unable to access|connection|network is unreachable|timed out/i.test(
158
+ detail
159
+ )) {
160
+ return "offline";
161
+ }
162
+ const gitMessage = detail.trim().length > 0 ? detail.trim() : "unknown git error";
163
+ throw new GenericIOError(`git push failed in ${storeDir}: ${gitMessage}`, {
164
+ actionHint: "resolve the git issue above (e.g. authentication, no upstream branch, or a rejected non-fast-forward push), then re-run `fabric sync`",
165
+ details: error
166
+ });
167
+ }
168
+ }
169
+ function defaultCommitDirty(storeDir) {
170
+ try {
171
+ execFileSync("git", ["rev-parse", "--is-inside-work-tree"], {
172
+ cwd: storeDir,
173
+ stdio: ["ignore", "ignore", "ignore"]
174
+ });
175
+ } catch {
176
+ return;
177
+ }
178
+ try {
179
+ execFileSync("git", ["add", "-A"], { cwd: storeDir, stdio: ["ignore", "ignore", "pipe"] });
180
+ try {
181
+ execFileSync("git", ["diff", "--cached", "--quiet"], {
182
+ cwd: storeDir,
183
+ stdio: ["ignore", "ignore", "ignore"]
184
+ });
185
+ return;
186
+ } catch {
187
+ execFileSync("git", ["commit", "-m", "fabric: sync local knowledge changes"], {
188
+ cwd: storeDir,
189
+ stdio: ["ignore", "ignore", "pipe"]
190
+ });
191
+ }
192
+ } catch {
193
+ }
194
+ }
195
+ function runRebaseStep(storeDir, step) {
196
+ try {
197
+ execFileSync("git", ["rebase", `--${step}`], {
198
+ cwd: storeDir,
199
+ stdio: ["ignore", "pipe", "pipe"]
200
+ });
201
+ } catch (error) {
202
+ const detail = `${gitErrText(error, "stdout")}${gitErrText(error, "stderr")}`.trim();
203
+ const gitMessage = detail.length > 0 ? detail : "unknown git error";
204
+ throw new GenericIOError(`git rebase --${step} failed in ${storeDir}: ${gitMessage}`, {
205
+ actionHint: step === "continue" ? "resolve the remaining conflicts (git status) and stage them, then re-run `fabric sync --continue`; or run `fabric sync --abort` to discard the rebase" : "inspect the store with `git status`; if no rebase is in progress the session may already be resolved \u2014 delete sync-session.json to reset",
206
+ details: error
207
+ });
208
+ }
209
+ }
130
210
  function defaultRebaseContinue(storeDir) {
131
- execFileSync("git", ["rebase", "--continue"], { cwd: storeDir, stdio: "ignore" });
211
+ runRebaseStep(storeDir, "continue");
132
212
  }
133
213
  function defaultRebaseAbort(storeDir) {
134
- execFileSync("git", ["rebase", "--abort"], { cwd: storeDir, stdio: "ignore" });
214
+ runRebaseStep(storeDir, "abort");
135
215
  }
136
216
  var OUTCOME_EVENT = {
137
217
  clean: "rebase_clean",
138
218
  conflict: "rebase_conflict",
139
219
  offline: "network_unavailable"
140
220
  };
141
- function walkPending(session, storeDirOf, pull) {
221
+ function walkPending(session, storeDirOf, pull, push, commit, pushableAliases) {
142
222
  let next = session;
143
223
  for (const store of session.stores) {
144
224
  if (store.state !== "pending") {
145
225
  continue;
146
226
  }
147
- const outcome = pull(storeDirOf(store));
148
- next = applySyncEvent(next, store.alias, OUTCOME_EVENT[outcome]);
149
- if (outcome === "conflict") {
150
- break;
227
+ const dir = storeDirOf(store);
228
+ commit(dir);
229
+ const pullOutcome = pull(dir);
230
+ if (pullOutcome !== "clean") {
231
+ next = applySyncEvent(next, store.alias, OUTCOME_EVENT[pullOutcome]);
232
+ if (pullOutcome === "conflict") {
233
+ break;
234
+ }
235
+ continue;
151
236
  }
237
+ if (!pushableAliases.has(store.alias)) {
238
+ next = applySyncEvent(next, store.alias, "rebase_clean");
239
+ continue;
240
+ }
241
+ const pushOutcome = push(dir);
242
+ next = applySyncEvent(
243
+ next,
244
+ store.alias,
245
+ pushOutcome === "clean" ? "rebase_clean" : "network_unavailable"
246
+ );
152
247
  }
153
248
  return next;
154
249
  }
@@ -179,37 +274,86 @@ function runStartSync(options) {
179
274
  syncable.map((store) => ({ alias: store.alias, store_uuid: store.store_uuid }))
180
275
  );
181
276
  const storeDirOf = (status) => join(globalRoot, storeRelativePath(status.store_uuid));
182
- const walked = walkPending(session, storeDirOf, options.pull ?? defaultPull);
277
+ const pushableAliases = pushableAliasesOf(config);
278
+ const walked = walkPending(
279
+ session,
280
+ storeDirOf,
281
+ options.pull ?? defaultPull,
282
+ options.push ?? defaultPush,
283
+ options.commit ?? defaultCommitDirty,
284
+ pushableAliases
285
+ );
183
286
  return finalize(walked, options, globalRoot);
184
287
  }
288
+ function pushableAliasesOf(config) {
289
+ return new Set(
290
+ config.stores.filter((store) => store.remote !== void 0 && (store.writable ?? true)).map((store) => store.alias)
291
+ );
292
+ }
185
293
  function runContinueSync(options) {
186
294
  const globalRoot = options.globalRoot ?? resolveGlobalRoot();
187
295
  const session = loadSession(globalRoot);
188
296
  if (session === null) {
189
- throw new Error(NO_SESSION);
297
+ throw new GenericIOError(NO_SESSION, {
298
+ actionHint: "Run `fabric sync` to start a sync before `--continue`/`--abort`."
299
+ });
190
300
  }
191
301
  const conflicted = session.stores.find((store) => store.state === "conflict");
192
302
  if (conflicted === void 0) {
193
- throw new Error(NO_CONFLICT);
303
+ throw new GenericIOError(NO_CONFLICT, {
304
+ actionHint: "The sync is not paused on a conflict; there is nothing to resume."
305
+ });
194
306
  }
195
307
  const storeDirOf = (status) => join(globalRoot, storeRelativePath(status.store_uuid));
196
308
  (options.rebaseContinue ?? defaultRebaseContinue)(storeDirOf(conflicted));
197
- const resumed = walkPending(continueSync(session), storeDirOf, options.pull ?? defaultPull);
309
+ const pushableAliases = pushableAliasesOf(loadGlobalConfig(globalRoot) ?? { stores: [] });
310
+ const push = options.push ?? defaultPush;
311
+ let advanced;
312
+ if (pushableAliases.has(conflicted.alias)) {
313
+ const pushOutcome = push(storeDirOf(conflicted));
314
+ advanced = applySyncEvent(
315
+ session,
316
+ conflicted.alias,
317
+ pushOutcome === "clean" ? "user_continue" : "network_unavailable"
318
+ );
319
+ } else {
320
+ advanced = continueSync(session);
321
+ }
322
+ const resumed = walkPending(
323
+ advanced,
324
+ storeDirOf,
325
+ options.pull ?? defaultPull,
326
+ push,
327
+ options.commit ?? defaultCommitDirty,
328
+ pushableAliases
329
+ );
198
330
  return finalize(resumed, options, globalRoot);
199
331
  }
200
332
  function runAbortSync(options) {
201
333
  const globalRoot = options.globalRoot ?? resolveGlobalRoot();
202
334
  const session = loadSession(globalRoot);
203
335
  if (session === null) {
204
- throw new Error(NO_SESSION);
336
+ throw new GenericIOError(NO_SESSION, {
337
+ actionHint: "Run `fabric sync` to start a sync before `--continue`/`--abort`."
338
+ });
205
339
  }
206
340
  const conflicted = session.stores.find((store) => store.state === "conflict");
207
341
  if (conflicted === void 0) {
208
- throw new Error(NO_CONFLICT);
342
+ throw new GenericIOError(NO_CONFLICT, {
343
+ actionHint: "The sync is not paused on a conflict; there is nothing to resume."
344
+ });
209
345
  }
210
346
  const storeDirOf = (status) => join(globalRoot, storeRelativePath(status.store_uuid));
211
347
  (options.rebaseAbort ?? defaultRebaseAbort)(storeDirOf(conflicted));
212
- const resumed = walkPending(abortSync(session), storeDirOf, options.pull ?? defaultPull);
348
+ const pushableAliases = pushableAliasesOf(loadGlobalConfig(globalRoot) ?? { stores: [] });
349
+ const resumed = walkPending(
350
+ abortSync(session),
351
+ storeDirOf,
352
+ options.pull ?? defaultPull,
353
+ options.push ?? defaultPush,
354
+ options.commit ?? defaultCommitDirty,
355
+ pushableAliases
356
+ );
213
357
  return finalize(resumed, options, globalRoot);
214
358
  }
215
359
 
@@ -7,7 +7,7 @@ import {
7
7
  HOOK_SCRIPT_DESTINATIONS,
8
8
  SKILL_DESTINATIONS,
9
9
  fabricAgentsSnapshotPath
10
- } from "./chunk-2R55HNVD.js";
10
+ } from "./chunk-XHHCRDIR.js";
11
11
  import {
12
12
  paint
13
13
  } from "./chunk-BO4XIZWZ.js";
@@ -88,6 +88,20 @@ async function removeCitePolicyEvictHook(projectRoot) {
88
88
  projectRoot
89
89
  );
90
90
  }
91
+ async function removeSessionEndMarkerHook(projectRoot) {
92
+ return removeHookScripts(
93
+ "hook-session-end-script",
94
+ HOOK_SCRIPT_DESTINATIONS.sessionEndMarker,
95
+ projectRoot
96
+ );
97
+ }
98
+ async function removePostTooluseMutationHook(projectRoot) {
99
+ return removeHookScripts(
100
+ "hook-post-tooluse-script",
101
+ HOOK_SCRIPT_DESTINATIONS.postTooluseMutation,
102
+ projectRoot
103
+ );
104
+ }
91
105
  async function removeHookScripts(step, rels, projectRoot) {
92
106
  const results = [];
93
107
  for (const rel of rels) {
@@ -323,6 +337,18 @@ async function uninstallBootstrapStage(projectRoot, _opts = {}) {
323
337
  projectRoot,
324
338
  () => removeCitePolicyEvictHook(projectRoot)
325
339
  );
340
+ await runAndCollect(
341
+ results,
342
+ "hook-session-end-script",
343
+ projectRoot,
344
+ () => removeSessionEndMarkerHook(projectRoot)
345
+ );
346
+ await runAndCollect(
347
+ results,
348
+ "hook-post-tooluse-script",
349
+ projectRoot,
350
+ () => removePostTooluseMutationHook(projectRoot)
351
+ );
326
352
  await runAndCollect(
327
353
  results,
328
354
  "skill-connect",
@@ -3,18 +3,29 @@ import {
3
3
  getProjectTranslator
4
4
  } from "./chunk-2CY4BMTH.js";
5
5
  import {
6
+ warnUnknownFlags,
6
7
  whoami
7
- } from "./chunk-T5RPGCCM.js";
8
- import "./chunk-LFIKMVY7.js";
9
- import "./chunk-RYAFBNES.js";
8
+ } from "./chunk-5LQIHYFC.js";
9
+ import "./chunk-5ZUMLCD5.js";
10
+ import "./chunk-XCBVSGCS.js";
10
11
 
11
12
  // src/commands/whoami.ts
12
13
  import { defineCommand } from "citty";
13
14
  var whoami_default = defineCommand({
14
15
  meta: { name: "whoami", description: "Show this machine's Fabric uid and mounted stores" },
15
- run() {
16
- const t = getProjectTranslator();
16
+ args: {
17
+ // F27: `--json` machine-readable output (was silently ignored — the command
18
+ // declared no args, so citty swallowed the flag and still printed text).
19
+ json: { type: "boolean", description: "Emit machine-readable JSON instead of text" }
20
+ },
21
+ run({ args }) {
22
+ warnUnknownFlags(["json"]);
17
23
  const info = whoami();
24
+ if (args.json === true) {
25
+ console.log(JSON.stringify(info, null, 2));
26
+ return;
27
+ }
28
+ const t = getProjectTranslator();
18
29
  if (info === null) {
19
30
  console.log(t("cli.cmd.no-global-config"));
20
31
  return;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@fenglimg/fabric-cli",
3
- "version": "2.2.0-rc.1",
3
+ "version": "2.2.0-rc.3",
4
4
  "description": "Fabric CLI — installs the MCP server + skills + hooks for Claude Code, Cursor, and Codex CLI; runs doctor / knowledge maintenance.",
5
5
  "license": "MIT",
6
6
  "author": "wangzhichao <fenglimg90@gmail.com>",
@@ -45,8 +45,8 @@
45
45
  "tree-sitter-javascript": "^0.25.0",
46
46
  "tree-sitter-typescript": "^0.23.2",
47
47
  "web-tree-sitter": "^0.26.8",
48
- "@fenglimg/fabric-server": "2.2.0-rc.1",
49
- "@fenglimg/fabric-shared": "2.2.0-rc.1"
48
+ "@fenglimg/fabric-server": "2.2.0-rc.3",
49
+ "@fenglimg/fabric-shared": "2.2.0-rc.3"
50
50
  },
51
51
  "devDependencies": {
52
52
  "@types/node": "^22.15.0",