@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 +427 -22
- package/dist/fabric.js.map +1 -1
- package/dist/tsconfig.tsbuildinfo +1 -1
- package/package.json +3 -3
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,
|
|
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({},
|
|
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
|
|
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[
|
|
129
|
-
store.set("profiles", __spreadProps(__spreadValues({}, profiles), { [
|
|
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 (
|
|
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
|
-
|
|
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 (
|
|
1422
|
+
if (FORBIDDEN_NAME_CHARACTER.test(segment)) {
|
|
1342
1423
|
return {
|
|
1343
1424
|
ok: false,
|
|
1344
|
-
reason: "
|
|
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 "
|
|
1396
|
-
return `
|
|
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(
|
|
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"
|