@fabricorg/cli 0.3.0 → 0.3.2

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/dist/fabric.js CHANGED
@@ -54,6 +54,59 @@ var __async = (__this, __arguments, generator) => {
54
54
  // src/bin/fabric.ts
55
55
  import { Command as Command20 } from "commander";
56
56
 
57
+ // package.json
58
+ var package_default = {
59
+ name: "@fabricorg/cli",
60
+ version: "0.3.2",
61
+ description: "CLI for the Fabric AI platform",
62
+ keywords: [
63
+ "fabric",
64
+ "ai",
65
+ "cli",
66
+ "developer-tools"
67
+ ],
68
+ homepage: "https://fabric.pro",
69
+ license: "MIT",
70
+ repository: {
71
+ type: "git",
72
+ url: "git+https://github.com/Fabric-Pro/fabric-oss.git",
73
+ directory: "packages/cli"
74
+ },
75
+ type: "module",
76
+ bin: {
77
+ fabric: "./dist/fabric.js"
78
+ },
79
+ files: [
80
+ "dist"
81
+ ],
82
+ publishConfig: {
83
+ access: "public"
84
+ },
85
+ scripts: {
86
+ build: "tsup",
87
+ dev: "tsup --watch",
88
+ "type-check": "tsc --noEmit",
89
+ test: "vitest run",
90
+ "test:watch": "vitest",
91
+ fabric: "node --import tsx/esm src/bin/fabric.ts"
92
+ },
93
+ dependencies: {
94
+ "@fabricorg/sdk": "workspace:*",
95
+ "@fabricorg/sdk-mcp": "workspace:*",
96
+ "cli-table3": "^0.6.5",
97
+ commander: "^13.1.0",
98
+ conf: "^13.1.0",
99
+ fflate: "^0.8.3"
100
+ },
101
+ devDependencies: {
102
+ "@repo/tsconfig": "workspace:*",
103
+ tsup: "^8.0.0",
104
+ tsx: "^4.21.0",
105
+ typescript: "5.9.3",
106
+ vitest: "^4.1.11"
107
+ }
108
+ };
109
+
57
110
  // src/commands/agents/index.ts
58
111
  import { Command } from "commander";
59
112
 
@@ -112,21 +165,25 @@ function getOutputFormat() {
112
165
  }
113
166
  return getConfig().defaultFormat;
114
167
  }
115
- function saveApiKey(apiKey, profile = "default") {
168
+ function saveApiKey(apiKey, { baseUrl } = {}) {
116
169
  var _a2;
117
170
  const store = getStore();
171
+ const profile = store.get("activeProfile");
118
172
  const profiles = store.get("profiles");
173
+ const current = (_a2 = profiles[profile]) != null ? _a2 : {};
119
174
  store.set("profiles", __spreadProps(__spreadValues({}, profiles), {
120
- [profile]: __spreadProps(__spreadValues({}, (_a2 = profiles[profile]) != null ? _a2 : {}), { apiKey })
175
+ [profile]: __spreadValues(__spreadProps(__spreadValues({}, current), {
176
+ apiKey
177
+ }), baseUrl === void 0 ? {} : { baseUrl })
121
178
  }));
122
- store.set("activeProfile", profile);
123
179
  }
124
- function clearApiKey(profile = "default") {
180
+ function clearApiKey(profile) {
125
181
  var _a2;
126
182
  const store = getStore();
183
+ const active = profile != null ? profile : store.get("activeProfile");
127
184
  const profiles = store.get("profiles");
128
- const _b = (_a2 = profiles[profile]) != null ? _a2 : {}, { apiKey: _ } = _b, rest = __objRest(_b, ["apiKey"]);
129
- store.set("profiles", __spreadProps(__spreadValues({}, profiles), { [profile]: rest }));
185
+ const _b = (_a2 = profiles[active]) != null ? _a2 : {}, { apiKey: _ } = _b, rest = __objRest(_b, ["apiKey"]);
186
+ store.set("profiles", __spreadProps(__spreadValues({}, profiles), { [active]: rest }));
130
187
  }
131
188
  function saveDefaultContext(ctx, profile) {
132
189
  var _a2;
@@ -562,8 +619,16 @@ function buildKeysCommand() {
562
619
 
563
620
  // src/commands/auth/login.ts
564
621
  import * as readline from "readline";
565
- import { FabricClient as FabricClient2 } from "@fabricorg/sdk";
622
+ import { FabricAuthError as FabricAuthError2, FabricClient as FabricClient2, FabricError } from "@fabricorg/sdk";
566
623
  import { Command as Command3 } from "commander";
624
+ var DEFAULT_BASE_URL = "https://fabric.pro";
625
+ function displayBaseUrl(baseUrl) {
626
+ try {
627
+ return new URL(baseUrl).origin;
628
+ } catch (e) {
629
+ return "the configured Fabric deployment";
630
+ }
631
+ }
567
632
  function promptApiKey() {
568
633
  return __async(this, null, function* () {
569
634
  const rl = readline.createInterface({
@@ -601,6 +666,9 @@ function buildLoginCommand() {
601
666
  );
602
667
  }
603
668
  const baseUrl = (_b = opts.baseUrl) != null ? _b : getBaseUrl();
669
+ const effectiveBaseUrl = displayBaseUrl(
670
+ baseUrl != null ? baseUrl : DEFAULT_BASE_URL
671
+ );
604
672
  const client = new FabricClient2({ apiKey, baseUrl });
605
673
  let name;
606
674
  let email;
@@ -608,13 +676,25 @@ function buildLoginCommand() {
608
676
  const me = yield client.auth.whoami();
609
677
  name = (_c = me.user.name) != null ? _c : me.user.email;
610
678
  email = me.user.email;
611
- } catch (e) {
679
+ } catch (error) {
680
+ if (error instanceof FabricAuthError2 || error instanceof FabricError && error.status === 401) {
681
+ printError(
682
+ "Authentication failed. Check that the key is valid and not expired.",
683
+ 3
684
+ );
685
+ }
686
+ if (error instanceof FabricError && error.status > 0) {
687
+ printError(
688
+ `Authentication request to ${effectiveBaseUrl} failed with HTTP ${error.status}.`,
689
+ 3
690
+ );
691
+ }
612
692
  printError(
613
- "Authentication failed. Check that the key is valid and not expired.",
693
+ `Could not connect to ${effectiveBaseUrl}. Check that the deployment URL is correct and reachable.`,
614
694
  3
615
695
  );
616
696
  }
617
- saveApiKey(apiKey);
697
+ saveApiKey(apiKey, __spreadValues({}, opts.baseUrl === void 0 ? {} : { baseUrl: opts.baseUrl }));
618
698
  printSuccess(`Authenticated as ${name} (${email})`);
619
699
  }));
620
700
  }
@@ -1289,6 +1369,7 @@ import { chmod } from "fs/promises";
1289
1369
  // src/lib/instructions/paths.ts
1290
1370
  var DRIVE_LETTER = /^[A-Za-z]:/;
1291
1371
  var RESERVED_DEVICE_NAME = /^(?:CON|PRN|AUX|NUL|COM[1-9]|LPT[1-9])(?:\.|$)/i;
1372
+ var FORBIDDEN_NAME_CHARACTER = /[<>:"|?*]/;
1292
1373
  var RESERVED_ROOTS = [".git", ".fabric"];
1293
1374
  var RESERVED_EXACT_PATHS = [".claude/settings.local.json"];
1294
1375
  function hasControlCharacter(input) {
@@ -1338,10 +1419,10 @@ function checkRelativePath(input) {
1338
1419
  detail: input
1339
1420
  };
1340
1421
  }
1341
- if (segment.includes(":")) {
1422
+ if (FORBIDDEN_NAME_CHARACTER.test(segment)) {
1342
1423
  return {
1343
1424
  ok: false,
1344
- reason: "alternate_data_stream",
1425
+ reason: "forbidden_character",
1345
1426
  detail: input
1346
1427
  };
1347
1428
  }
@@ -1392,8 +1473,8 @@ function describeRejection(rejection) {
1392
1473
  return `path segment ending in a dot or space refused: ${rejection.detail}`;
1393
1474
  case "reserved_device_name":
1394
1475
  return `reserved device name refused: ${rejection.detail}`;
1395
- case "alternate_data_stream":
1396
- return `colon in path refused: ${rejection.detail}`;
1476
+ case "forbidden_character":
1477
+ return `path contains a character Windows will not put in a filename (< > : " | ? *): ${rejection.detail}`;
1397
1478
  case "reserved_path":
1398
1479
  return `this tool never writes or deletes ${RESERVED_ROOTS.join(", ")} or ${RESERVED_EXACT_PATHS.join(", ")}: ${rejection.detail}`;
1399
1480
  case "not_a_regular_file":
@@ -2498,11 +2579,158 @@ function nextLock(input) {
2498
2579
  };
2499
2580
  }
2500
2581
 
2582
+ // src/lib/instructions/push.ts
2583
+ import { createHash as createHash5 } from "crypto";
2584
+ var MAX_PUSH_CHANGES = 50;
2585
+ function assertPushablePath(candidate, source) {
2586
+ const check = checkRelativePath(candidate);
2587
+ if (!check.ok) {
2588
+ throw new Error(
2589
+ `Refusing to push: ${describeRejection(check)}. ${source}`
2590
+ );
2591
+ }
2592
+ if (isReservedPath(candidate)) {
2593
+ throw new Error(
2594
+ `Refusing to push: ${describeRejection({
2595
+ ok: false,
2596
+ reason: "reserved_path",
2597
+ detail: candidate
2598
+ })}. ${source}`
2599
+ );
2600
+ }
2601
+ }
2602
+ function encodeContent(bytes) {
2603
+ const text = new TextDecoder("utf-8", { fatal: false }).decode(bytes);
2604
+ const roundTrip = new TextEncoder().encode(text);
2605
+ if (roundTrip.length === bytes.length && roundTrip.every((byte, index) => byte === bytes[index])) {
2606
+ return { content: text, encoding: "utf8" };
2607
+ }
2608
+ return {
2609
+ content: Buffer.from(bytes).toString("base64"),
2610
+ encoding: "base64"
2611
+ };
2612
+ }
2613
+ function refuseStaleLedger(detail) {
2614
+ throw new Error(
2615
+ `Refusing to push: the sync lock does not match the published version \u2014 ${detail}. Run \`fabric instructions sync\` and try again. Nothing was read and nothing was sent.`
2616
+ );
2617
+ }
2618
+ function assertLockMatchesManifest(lock, manifest) {
2619
+ const published = new Map(
2620
+ manifest.map((entry) => [entry.path, entry.sha256])
2621
+ );
2622
+ for (const [path5, entry] of Object.entries(lock.files)) {
2623
+ const expected = published.get(path5);
2624
+ if (expected === void 0) {
2625
+ refuseStaleLedger(
2626
+ `it lists ${path5}, which version ${lock.snapshotVersion} does not contain`
2627
+ );
2628
+ }
2629
+ if (expected !== entry.sha256) {
2630
+ refuseStaleLedger(
2631
+ `its record of ${path5} is not the published file`
2632
+ );
2633
+ }
2634
+ }
2635
+ for (const entry of manifest) {
2636
+ if (!Object.hasOwn(lock.files, entry.path)) {
2637
+ refuseStaleLedger(
2638
+ `version ${lock.snapshotVersion} contains ${entry.path}, which the lock does not list`
2639
+ );
2640
+ }
2641
+ }
2642
+ }
2643
+ function computePushPlan(input) {
2644
+ return __async(this, null, function* () {
2645
+ var _a2, _b;
2646
+ const { root, lock } = input;
2647
+ const lockedPaths = Object.keys(lock.files);
2648
+ for (const lockedPath of lockedPaths) {
2649
+ assertPushablePath(
2650
+ lockedPath,
2651
+ "The lock names a path this tool will not touch."
2652
+ );
2653
+ }
2654
+ assertLockMatchesManifest(lock, input.manifest);
2655
+ const lockCollision = findCollision(lockedPaths);
2656
+ if (lockCollision !== null) {
2657
+ throw new Error(
2658
+ `Refusing to push: ${describeRejection({
2659
+ ok: false,
2660
+ reason: "collision",
2661
+ detail: `${lockCollision.first} and ${lockCollision.second}`
2662
+ })}. The lock names two paths this tool cannot tell apart.`
2663
+ );
2664
+ }
2665
+ const added = [];
2666
+ const locked = new Set(lockedPaths);
2667
+ for (const candidate of (_a2 = input.added) != null ? _a2 : []) {
2668
+ assertPushablePath(candidate, "That path cannot be pushed.");
2669
+ if (locked.has(candidate)) {
2670
+ continue;
2671
+ }
2672
+ if (added.includes(candidate)) {
2673
+ continue;
2674
+ }
2675
+ added.push(candidate);
2676
+ }
2677
+ const allCollision = findCollision([...lockedPaths, ...added]);
2678
+ if (allCollision !== null) {
2679
+ throw new Error(
2680
+ `Refusing to push: ${describeRejection({
2681
+ ok: false,
2682
+ reason: "collision",
2683
+ detail: `${allCollision.first} and ${allCollision.second}`
2684
+ })}. Two of these paths are one file on a case-insensitive filesystem.`
2685
+ );
2686
+ }
2687
+ const entries = [];
2688
+ const changes = [];
2689
+ const unchanged = [];
2690
+ for (const lockedPath of lockedPaths.sort()) {
2691
+ const read = yield readFileSafely(root, lockedPath);
2692
+ if (read === null) {
2693
+ entries.push({ path: lockedPath, action: "delete" });
2694
+ changes.push({ op: "delete", path: lockedPath });
2695
+ continue;
2696
+ }
2697
+ const actual = createHash5("sha256").update(read.bytes).digest("hex");
2698
+ if (actual === ((_b = lock.files[lockedPath]) == null ? void 0 : _b.sha256)) {
2699
+ unchanged.push(lockedPath);
2700
+ continue;
2701
+ }
2702
+ const encoded = encodeContent(read.bytes);
2703
+ entries.push({
2704
+ path: lockedPath,
2705
+ action: "put",
2706
+ size: read.bytes.length
2707
+ });
2708
+ changes.push(__spreadValues({ op: "put", path: lockedPath }, encoded));
2709
+ }
2710
+ for (const newPath of added.sort()) {
2711
+ const read = yield readFileSafely(root, newPath);
2712
+ if (read === null) {
2713
+ throw new Error(
2714
+ `Refusing to push: there is no file at ${newPath}.`
2715
+ );
2716
+ }
2717
+ const encoded = encodeContent(read.bytes);
2718
+ entries.push({ path: newPath, action: "put", size: read.bytes.length });
2719
+ changes.push(__spreadValues({ op: "put", path: newPath }, encoded));
2720
+ }
2721
+ return { entries, changes, unchanged };
2722
+ });
2723
+ }
2724
+
2501
2725
  // src/commands/instructions/index.ts
2502
2726
  var HOOK_DEADLINE_MS = 1e4;
2503
2727
  var CHECK_TIMEOUT_MS = 5e3;
2504
2728
  var SYNC_TIMEOUT_MS = 15e3;
2505
2729
  var BUNDLE_TIMEOUT_MS = 6e4;
2730
+ var PUSH_TIMEOUT_MS = 6e4;
2731
+ function collectAdded(value, previous) {
2732
+ return [...previous, value];
2733
+ }
2506
2734
  var CliFailure = class extends Error {
2507
2735
  constructor(message, exitCode) {
2508
2736
  super(message);
@@ -2537,6 +2765,22 @@ function buildInstructionsCommand() {
2537
2765
  yield run(opts, "sync", () => runSync(opts, formatFor(this)));
2538
2766
  });
2539
2767
  });
2768
+ instructions.command("push").description(
2769
+ "Suggest this checkout's edits to the project's coding instructions"
2770
+ ).requiredOption("--project <id>", "Project ID").option("--dest <dir>", "Destination directory (default: cwd)").option("--org <slug>", "Organization context").option(
2771
+ "--add <path>",
2772
+ "Also send a file the last sync did not write (repeatable)",
2773
+ collectAdded,
2774
+ []
2775
+ ).option("--dry-run", "Print the change set and send nothing").option("--format <format>", "Output format: text|json").action(function(opts) {
2776
+ return __async(this, null, function* () {
2777
+ yield run(
2778
+ __spreadProps(__spreadValues({}, opts), { hook: false }),
2779
+ "push",
2780
+ () => runPush(opts, formatFor(this))
2781
+ );
2782
+ });
2783
+ });
2540
2784
  instructions.command("init").description(
2541
2785
  "Write a SessionStart hook for a coding tool and take the first copy"
2542
2786
  ).requiredOption("--project <id>", "Project ID").requiredOption(
@@ -2650,6 +2894,10 @@ function instructionsClient(opts, timeoutMs) {
2650
2894
  }
2651
2895
  return getClient(
2652
2896
  opts.hook ? {
2897
+ // Hook mode has ONE absolute deadline covering every
2898
+ // call it makes, so it cannot spend it on retries: a
2899
+ // session that will not start is worse than instructions
2900
+ // one version stale.
2653
2901
  timeoutMs: Math.min(timeoutMs, HOOK_DEADLINE_MS),
2654
2902
  retry: { maxRetries: 0 }
2655
2903
  } : { timeoutMs }
@@ -2824,6 +3072,7 @@ function syncOnce(opts) {
2824
3072
  const lock = yield readLockForProject(root, opts.project);
2825
3073
  const client = instructionsClient(opts, SYNC_TIMEOUT_MS);
2826
3074
  let published = yield fetchPublished(client, opts, lock == null ? void 0 : lock.digest);
3075
+ assertInitSyncMayAct(opts, published);
2827
3076
  if (!published.published || !published.snapshot) {
2828
3077
  throw new CliFailure(
2829
3078
  "This project has no published coding instructions yet.",
@@ -2840,6 +3089,7 @@ function syncOnce(opts) {
2840
3089
  }
2841
3090
  outcome.drifted = drifted;
2842
3091
  published = yield fetchPublished(client, opts);
3092
+ assertInitSyncMayAct(opts, published);
2843
3093
  if (!published.published || !published.snapshot) {
2844
3094
  throw new CliFailure(
2845
3095
  "This project has no published coding instructions yet.",
@@ -2914,6 +3164,14 @@ function syncOnce(opts) {
2914
3164
  return outcome;
2915
3165
  });
2916
3166
  }
3167
+ function assertInitSyncMayAct(opts, published) {
3168
+ if (opts.rejectRepository && published.sourceOfTruth === "REPOSITORY") {
3169
+ throw new CliFailure(
3170
+ "This project's coding instructions come from its repository, so they arrive with `git pull`. A session hook would fight it; nothing was written.",
3171
+ 7
3172
+ );
3173
+ }
3174
+ }
2917
3175
  function fillPlanPaths(outcome, plan) {
2918
3176
  outcome.added = plan.entries.filter((entry) => entry.action === "added").map((entry) => entry.path);
2919
3177
  outcome.updated = plan.entries.filter((entry) => entry.action === "updated").map((entry) => entry.path);
@@ -2954,6 +3212,153 @@ function reportSync(outcome, opts) {
2954
3212
  line(`Everything in ${outcome.destination} already matched.`);
2955
3213
  }
2956
3214
  }
3215
+ function runPush(opts, format) {
3216
+ return __async(this, null, function* () {
3217
+ var _a2;
3218
+ const destination = destinationOf(opts);
3219
+ const root = yield resolveExistingRoot(destination);
3220
+ const lock = yield readLockForProject(root, opts.project);
3221
+ if (lock === null) {
3222
+ throw new CliFailure(
3223
+ `No coding-instructions lock in ${destination}. Run \`fabric instructions sync --project ${opts.project}\` first: a push is a diff against the version that was last applied here, and without that ledger there is nothing to diff against.`,
3224
+ 7
3225
+ );
3226
+ }
3227
+ const client = instructionsClient(
3228
+ __spreadProps(__spreadValues({}, opts), { hook: false }),
3229
+ PUSH_TIMEOUT_MS
3230
+ );
3231
+ const published = yield fetchPublished(client, opts);
3232
+ if (!published.published || !published.snapshot) {
3233
+ throw new CliFailure(
3234
+ "this project has no published coding instructions to change yet; the first version is uploaded from the Coding Instructions tab",
3235
+ 4
3236
+ );
3237
+ }
3238
+ if (published.snapshot.id !== lock.snapshotId) {
3239
+ throw new CliFailure(
3240
+ "published instructions moved past your last sync; run `fabric instructions sync` then push again",
3241
+ 7
3242
+ );
3243
+ }
3244
+ const plan = yield computePushPlan({
3245
+ root,
3246
+ lock,
3247
+ manifest: (_a2 = published.manifest) != null ? _a2 : [],
3248
+ added: opts.add
3249
+ });
3250
+ if (plan.changes.length === 0) {
3251
+ throw new CliFailure(
3252
+ `Nothing to push: every file the last sync wrote still matches version ${lock.snapshotVersion}. Add a new file with --add <path> if you meant to suggest one.`,
3253
+ 7
3254
+ );
3255
+ }
3256
+ if (plan.changes.length > MAX_PUSH_CHANGES) {
3257
+ throw new CliFailure(
3258
+ `Too many changes to push (${plan.changes.length} > ${MAX_PUSH_CHANGES}). A change set this large is a replacement rather than an edit \u2014 upload the folder from the project's Coding Instructions tab, which is also the only path that re-reads the project's exclusion rules.`,
3259
+ 7
3260
+ );
3261
+ }
3262
+ const outcome = {
3263
+ projectId: opts.project,
3264
+ destination: root,
3265
+ dryRun: Boolean(opts.dryRun),
3266
+ baseSnapshotId: lock.snapshotId,
3267
+ baseVersion: lock.snapshotVersion,
3268
+ put: plan.entries.filter((entry) => entry.action === "put").map((entry) => entry.path),
3269
+ deleted: plan.entries.filter((entry) => entry.action === "delete").map((entry) => entry.path),
3270
+ unchanged: plan.unchanged.length,
3271
+ snapshotId: null,
3272
+ version: null,
3273
+ proposalStatus: null,
3274
+ status: null
3275
+ };
3276
+ if (!opts.dryRun) {
3277
+ try {
3278
+ const submitted = yield client.instructions.submitChange(
3279
+ opts.project,
3280
+ lock.snapshotId,
3281
+ plan.changes,
3282
+ { org: orgSlugFor(opts) }
3283
+ );
3284
+ outcome.snapshotId = submitted.snapshotId;
3285
+ outcome.version = submitted.version;
3286
+ outcome.proposalStatus = submitted.proposalStatus;
3287
+ outcome.status = submitted.status;
3288
+ } catch (error) {
3289
+ throw asPushFailure(error);
3290
+ }
3291
+ }
3292
+ if (format === "json") {
3293
+ printOutput(outcome, { format: "json" });
3294
+ return;
3295
+ }
3296
+ reportPush(outcome);
3297
+ });
3298
+ }
3299
+ function asPushFailure(error) {
3300
+ const code = error.code;
3301
+ const message = error instanceof Error ? error.message : String(error);
3302
+ switch (code) {
3303
+ case "PULL_FIRST":
3304
+ return new CliFailure(
3305
+ "published instructions moved past your last sync; run `fabric instructions sync` then push again",
3306
+ 7
3307
+ );
3308
+ case "REPOSITORY_SOURCE_OF_TRUTH":
3309
+ return new CliFailure(
3310
+ "this project's coding instructions come from its repository, so they are changed there and mirrored into Fabric \u2014 commit and push to the repository instead; nothing was sent",
3311
+ 7
3312
+ );
3313
+ case "NOTHING_PUBLISHED":
3314
+ return new CliFailure(
3315
+ "this project has no published coding instructions to change yet; the first version is uploaded from the Coding Instructions tab",
3316
+ 4
3317
+ );
3318
+ case "PROPOSAL_PROPOSER_LIMIT":
3319
+ case "PROPOSAL_PROJECT_LIMIT":
3320
+ return new CliFailure(`${message} Nothing was sent.`, 7);
3321
+ default:
3322
+ return asCliFailure(error);
3323
+ }
3324
+ }
3325
+ function pushVerdict(outcome) {
3326
+ var _a2;
3327
+ if (outcome.proposalStatus === "PENDING" && outcome.status === "RECEIVING") {
3328
+ return `An earlier attempt is still sending this change; if version ${outcome.version} stays unfinished, cancel it in the project's Coding Instructions tab, or leave it and it is closed out automatically after six hours.`;
3329
+ }
3330
+ if (outcome.proposalStatus === "PENDING" && outcome.status === "FAILED") {
3331
+ return `This proposal did not pass its checks; retry version ${outcome.version} from the project's Coding Instructions tab.`;
3332
+ }
3333
+ if (outcome.proposalStatus === "PENDING") {
3334
+ if (outcome.status === "REJECTED") {
3335
+ return `Version ${outcome.version} was rejected by its checks \u2014 cancel it in the project's Coding Instructions tab before proposing this change again.`;
3336
+ }
3337
+ return `Proposed as version ${outcome.version}. It is pending review \u2014 nothing is published until somebody who can edit this project's coding instructions approves it in the Coding Instructions tab.`;
3338
+ }
3339
+ if (outcome.proposalStatus === "APPROVED") {
3340
+ return `Version ${outcome.version} has already been approved and published \u2014 run \`fabric instructions sync\` to bring this checkout up to it.`;
3341
+ }
3342
+ return `Version ${outcome.version} is no longer open for review (${(_a2 = outcome.proposalStatus) != null ? _a2 : outcome.status}): an earlier attempt at this same change was closed out, so push again.`;
3343
+ }
3344
+ function reportPush(outcome) {
3345
+ const prefix = outcome.dryRun ? "Would send" : "Sent";
3346
+ line(
3347
+ `${prefix} ${outcome.put.length} changed file(s) and ${outcome.deleted.length} deletion(s) against version ${outcome.baseVersion} (${outcome.unchanged} unchanged).`
3348
+ );
3349
+ listPaths("changed", outcome.put);
3350
+ listPaths("deleted", outcome.deleted);
3351
+ if (outcome.dryRun) {
3352
+ line(
3353
+ "Nothing was sent. Without --dry-run this would open a proposal for review."
3354
+ );
3355
+ return;
3356
+ }
3357
+ line(pushVerdict(outcome));
3358
+ line(
3359
+ "Your lock was not changed: it still names the published version, which is what `fabric instructions sync` compares against."
3360
+ );
3361
+ }
2957
3362
  function runInit(opts, format) {
2958
3363
  return __async(this, null, function* () {
2959
3364
  if (opts.tool !== "claude-code") {
@@ -2977,6 +3382,13 @@ function runInit(opts, format) {
2977
3382
  7
2978
3383
  );
2979
3384
  }
3385
+ const outcome = published.published ? (
3386
+ // A second manifest call, deliberately: the one above answered "may
3387
+ // a hook be installed for this project at all", and this one is the
3388
+ // sync's own. Do it before the hook write: a failed first copy must
3389
+ // not leave a new or updated hook behind.
3390
+ yield syncOnce(__spreadProps(__spreadValues({}, opts), { hook: false, rejectRepository: true }))
3391
+ ) : null;
2980
3392
  const command = buildHookCommand(
2981
3393
  opts.project,
2982
3394
  Boolean(opts.apply),
@@ -2987,13 +3399,6 @@ function runInit(opts, format) {
2987
3399
  projectId: opts.project,
2988
3400
  command
2989
3401
  });
2990
- const outcome = published.published ? (
2991
- // A second manifest call, deliberately: the one above answered "may
2992
- // a hook be installed for this project at all", and this one is the
2993
- // sync's own — it carries the lock's digest and is the call the
2994
- // hook will make from now on.
2995
- yield syncOnce(__spreadProps(__spreadValues({}, opts), { hook: false }))
2996
- ) : null;
2997
3402
  if (format === "json") {
2998
3403
  printOutput(
2999
3404
  {
@@ -3930,7 +4335,7 @@ function buildWorkspacesCommand() {
3930
4335
  // src/bin/fabric.ts
3931
4336
  var program = new Command20();
3932
4337
  var _a;
3933
- program.name("fabric").description("Fabric AI platform CLI").version("0.1.0").option(
4338
+ program.name("fabric").description("Fabric AI platform CLI").version(package_default.version).option(
3934
4339
  "--format <format>",
3935
4340
  "Output format: table|json|yaml|csv",
3936
4341
  (_a = process.env.FABRIC_FORMAT) != null ? _a : "table"