@fabricorg/cli 0.2.1 → 0.3.1
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 +2057 -204
- package/dist/fabric.js.map +1 -1
- package/dist/tsconfig.tsbuildinfo +1 -1
- package/package.json +9 -5
package/dist/fabric.js
CHANGED
|
@@ -52,7 +52,60 @@ var __async = (__this, __arguments, generator) => {
|
|
|
52
52
|
};
|
|
53
53
|
|
|
54
54
|
// src/bin/fabric.ts
|
|
55
|
-
import { Command as
|
|
55
|
+
import { Command as Command20 } from "commander";
|
|
56
|
+
|
|
57
|
+
// package.json
|
|
58
|
+
var package_default = {
|
|
59
|
+
name: "@fabricorg/cli",
|
|
60
|
+
version: "0.3.1",
|
|
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
|
+
};
|
|
56
109
|
|
|
57
110
|
// src/commands/agents/index.ts
|
|
58
111
|
import { Command } from "commander";
|
|
@@ -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;
|
|
@@ -295,7 +352,7 @@ ${valStr}` : `${pad}${k}: ${valStr}`;
|
|
|
295
352
|
}
|
|
296
353
|
|
|
297
354
|
// src/lib/client.ts
|
|
298
|
-
function getClient() {
|
|
355
|
+
function getClient(overrides = {}) {
|
|
299
356
|
const apiKey = getApiKey();
|
|
300
357
|
if (!apiKey) {
|
|
301
358
|
printError(
|
|
@@ -304,7 +361,10 @@ function getClient() {
|
|
|
304
361
|
);
|
|
305
362
|
}
|
|
306
363
|
try {
|
|
307
|
-
return new FabricClient({
|
|
364
|
+
return new FabricClient(__spreadValues(__spreadValues({
|
|
365
|
+
apiKey,
|
|
366
|
+
baseUrl: getBaseUrl()
|
|
367
|
+
}, overrides.timeoutMs !== void 0 ? { timeoutMs: overrides.timeoutMs } : {}), overrides.retry !== void 0 ? { retry: overrides.retry } : {}));
|
|
308
368
|
} catch (err) {
|
|
309
369
|
if (err instanceof FabricAuthError) {
|
|
310
370
|
printError(err.message, 3);
|
|
@@ -314,39 +374,45 @@ function getClient() {
|
|
|
314
374
|
}
|
|
315
375
|
|
|
316
376
|
// src/lib/context.ts
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
`${source} selects personal context, which no longer exists \u2014 every command runs inside an organization.
|
|
320
|
-
Name one with --org <slug>, or set a default with:
|
|
321
|
-
fabric ctx use org <slug>`,
|
|
322
|
-
2
|
|
323
|
-
);
|
|
324
|
-
}
|
|
325
|
-
function resolveContext(personal, org, required = true) {
|
|
377
|
+
var PERSONAL_RETIRED_HINT = "selects personal context, which no longer exists \u2014 every command runs inside an organization.\nName one with --org <slug>, or set a default with:\n fabric ctx use org <slug>";
|
|
378
|
+
function tryResolveContext(personal, org, required = true) {
|
|
326
379
|
if (personal) {
|
|
327
|
-
|
|
380
|
+
return {
|
|
381
|
+
ok: false,
|
|
382
|
+
message: `--personal ${PERSONAL_RETIRED_HINT}`,
|
|
383
|
+
exitCode: 2
|
|
384
|
+
};
|
|
328
385
|
}
|
|
329
386
|
if (org) {
|
|
330
|
-
return { type: "org", slug: org };
|
|
387
|
+
return { ok: true, context: { type: "org", slug: org } };
|
|
331
388
|
}
|
|
332
389
|
const stored = getDefaultContext();
|
|
333
|
-
if (stored) {
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
390
|
+
if ((stored == null ? void 0 : stored.type) === "personal") {
|
|
391
|
+
const source = process.env.FABRIC_PERSONAL === "1" ? "FABRIC_PERSONAL=1" : "Your stored default context";
|
|
392
|
+
return {
|
|
393
|
+
ok: false,
|
|
394
|
+
message: `${source} ${PERSONAL_RETIRED_HINT}`,
|
|
395
|
+
exitCode: 2
|
|
396
|
+
};
|
|
397
|
+
}
|
|
398
|
+
if ((stored == null ? void 0 : stored.type) === "org") {
|
|
399
|
+
return { ok: true, context: { type: "org", slug: stored.slug } };
|
|
342
400
|
}
|
|
343
401
|
if (required) {
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
402
|
+
return {
|
|
403
|
+
ok: false,
|
|
404
|
+
message: "Context is required. Use --org <slug>, or set a default with:\n fabric ctx use org <slug>",
|
|
405
|
+
exitCode: 2
|
|
406
|
+
};
|
|
407
|
+
}
|
|
408
|
+
return { ok: true, context: void 0 };
|
|
409
|
+
}
|
|
410
|
+
function resolveContext(personal, org, required = true) {
|
|
411
|
+
const resolution = tryResolveContext(personal, org, required);
|
|
412
|
+
if (!resolution.ok) {
|
|
413
|
+
printError(resolution.message, resolution.exitCode);
|
|
348
414
|
}
|
|
349
|
-
return
|
|
415
|
+
return resolution.context;
|
|
350
416
|
}
|
|
351
417
|
|
|
352
418
|
// src/commands/agents/index.ts
|
|
@@ -553,8 +619,16 @@ function buildKeysCommand() {
|
|
|
553
619
|
|
|
554
620
|
// src/commands/auth/login.ts
|
|
555
621
|
import * as readline from "readline";
|
|
556
|
-
import { FabricClient as FabricClient2 } from "@fabricorg/sdk";
|
|
622
|
+
import { FabricAuthError as FabricAuthError2, FabricClient as FabricClient2, FabricError } from "@fabricorg/sdk";
|
|
557
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
|
+
}
|
|
558
632
|
function promptApiKey() {
|
|
559
633
|
return __async(this, null, function* () {
|
|
560
634
|
const rl = readline.createInterface({
|
|
@@ -592,6 +666,9 @@ function buildLoginCommand() {
|
|
|
592
666
|
);
|
|
593
667
|
}
|
|
594
668
|
const baseUrl = (_b = opts.baseUrl) != null ? _b : getBaseUrl();
|
|
669
|
+
const effectiveBaseUrl = displayBaseUrl(
|
|
670
|
+
baseUrl != null ? baseUrl : DEFAULT_BASE_URL
|
|
671
|
+
);
|
|
595
672
|
const client = new FabricClient2({ apiKey, baseUrl });
|
|
596
673
|
let name;
|
|
597
674
|
let email;
|
|
@@ -599,13 +676,25 @@ function buildLoginCommand() {
|
|
|
599
676
|
const me = yield client.auth.whoami();
|
|
600
677
|
name = (_c = me.user.name) != null ? _c : me.user.email;
|
|
601
678
|
email = me.user.email;
|
|
602
|
-
} 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
|
+
}
|
|
603
692
|
printError(
|
|
604
|
-
|
|
693
|
+
`Could not connect to ${effectiveBaseUrl}. Check that the deployment URL is correct and reachable.`,
|
|
605
694
|
3
|
|
606
695
|
);
|
|
607
696
|
}
|
|
608
|
-
saveApiKey(apiKey);
|
|
697
|
+
saveApiKey(apiKey, __spreadValues({}, opts.baseUrl === void 0 ? {} : { baseUrl: opts.baseUrl }));
|
|
609
698
|
printSuccess(`Authenticated as ${name} (${email})`);
|
|
610
699
|
}));
|
|
611
700
|
}
|
|
@@ -1269,168 +1358,1931 @@ function buildFramesCommand() {
|
|
|
1269
1358
|
return frames;
|
|
1270
1359
|
}
|
|
1271
1360
|
|
|
1272
|
-
// src/commands/
|
|
1361
|
+
// src/commands/instructions/index.ts
|
|
1362
|
+
import path4 from "path";
|
|
1273
1363
|
import { Command as Command11 } from "commander";
|
|
1274
|
-
|
|
1275
|
-
|
|
1276
|
-
|
|
1364
|
+
|
|
1365
|
+
// src/lib/instructions/apply.ts
|
|
1366
|
+
import { createHash as createHash2 } from "crypto";
|
|
1367
|
+
import { chmod } from "fs/promises";
|
|
1368
|
+
|
|
1369
|
+
// src/lib/instructions/paths.ts
|
|
1370
|
+
var DRIVE_LETTER = /^[A-Za-z]:/;
|
|
1371
|
+
var RESERVED_DEVICE_NAME = /^(?:CON|PRN|AUX|NUL|COM[1-9]|LPT[1-9])(?:\.|$)/i;
|
|
1372
|
+
var RESERVED_ROOTS = [".git", ".fabric"];
|
|
1373
|
+
var RESERVED_EXACT_PATHS = [".claude/settings.local.json"];
|
|
1374
|
+
function hasControlCharacter(input) {
|
|
1375
|
+
for (let index = 0; index < input.length; index++) {
|
|
1376
|
+
const code = input.charCodeAt(index);
|
|
1377
|
+
if (code <= 31 || code === 127) {
|
|
1378
|
+
return true;
|
|
1379
|
+
}
|
|
1380
|
+
}
|
|
1381
|
+
return false;
|
|
1382
|
+
}
|
|
1383
|
+
function checkRelativePath(input) {
|
|
1384
|
+
if (input.length === 0) {
|
|
1385
|
+
return { ok: false, reason: "empty", detail: input };
|
|
1386
|
+
}
|
|
1387
|
+
if (hasControlCharacter(input)) {
|
|
1388
|
+
return { ok: false, reason: "control_char", detail: input };
|
|
1389
|
+
}
|
|
1390
|
+
if (input.includes("\\")) {
|
|
1391
|
+
return { ok: false, reason: "backslash", detail: input };
|
|
1392
|
+
}
|
|
1393
|
+
if (input.startsWith("/") || DRIVE_LETTER.test(input)) {
|
|
1394
|
+
return { ok: false, reason: "absolute", detail: input };
|
|
1395
|
+
}
|
|
1396
|
+
if (input.endsWith("/")) {
|
|
1397
|
+
return { ok: false, reason: "trailing_separator", detail: input };
|
|
1398
|
+
}
|
|
1399
|
+
const segments = input.split("/");
|
|
1400
|
+
if (segments.some((s) => s.length === 0)) {
|
|
1401
|
+
return { ok: false, reason: "empty", detail: input };
|
|
1402
|
+
}
|
|
1403
|
+
if (segments.some((s) => s === "." || s === "..")) {
|
|
1404
|
+
return { ok: false, reason: "traversal", detail: input };
|
|
1405
|
+
}
|
|
1406
|
+
for (const segment of segments) {
|
|
1407
|
+
if (/[. ]$/.test(segment)) {
|
|
1408
|
+
return {
|
|
1409
|
+
ok: false,
|
|
1410
|
+
reason: "trailing_dot_or_space",
|
|
1411
|
+
detail: input
|
|
1412
|
+
};
|
|
1413
|
+
}
|
|
1414
|
+
if (RESERVED_DEVICE_NAME.test(segment)) {
|
|
1415
|
+
return {
|
|
1416
|
+
ok: false,
|
|
1417
|
+
reason: "reserved_device_name",
|
|
1418
|
+
detail: input
|
|
1419
|
+
};
|
|
1420
|
+
}
|
|
1421
|
+
if (segment.includes(":")) {
|
|
1422
|
+
return {
|
|
1423
|
+
ok: false,
|
|
1424
|
+
reason: "alternate_data_stream",
|
|
1425
|
+
detail: input
|
|
1426
|
+
};
|
|
1427
|
+
}
|
|
1428
|
+
}
|
|
1429
|
+
return { ok: true, path: input };
|
|
1430
|
+
}
|
|
1431
|
+
function isReservedPath(input) {
|
|
1432
|
+
var _a2;
|
|
1433
|
+
const first = (_a2 = input.split("/")[0]) == null ? void 0 : _a2.toLowerCase();
|
|
1434
|
+
if (RESERVED_ROOTS.some((root) => first === root)) {
|
|
1435
|
+
return true;
|
|
1436
|
+
}
|
|
1437
|
+
const key = collisionKey(input);
|
|
1438
|
+
return RESERVED_EXACT_PATHS.some(
|
|
1439
|
+
(reserved) => collisionKey(reserved) === key
|
|
1277
1440
|
);
|
|
1278
|
-
|
|
1279
|
-
|
|
1280
|
-
|
|
1281
|
-
|
|
1282
|
-
|
|
1283
|
-
|
|
1284
|
-
|
|
1285
|
-
|
|
1286
|
-
|
|
1287
|
-
|
|
1288
|
-
|
|
1289
|
-
|
|
1290
|
-
|
|
1291
|
-
|
|
1292
|
-
|
|
1441
|
+
}
|
|
1442
|
+
function collisionKey(input) {
|
|
1443
|
+
return input.normalize("NFC").toLowerCase();
|
|
1444
|
+
}
|
|
1445
|
+
function findCollision(paths) {
|
|
1446
|
+
const seen = /* @__PURE__ */ new Map();
|
|
1447
|
+
for (const candidate of paths) {
|
|
1448
|
+
const key = collisionKey(candidate);
|
|
1449
|
+
const previous = seen.get(key);
|
|
1450
|
+
if (previous !== void 0) {
|
|
1451
|
+
return { first: previous, second: candidate };
|
|
1452
|
+
}
|
|
1453
|
+
seen.set(key, candidate);
|
|
1454
|
+
}
|
|
1455
|
+
return null;
|
|
1456
|
+
}
|
|
1457
|
+
function describeRejection(rejection) {
|
|
1458
|
+
switch (rejection.reason) {
|
|
1459
|
+
case "empty":
|
|
1460
|
+
return `empty or malformed path (${JSON.stringify(rejection.detail)})`;
|
|
1461
|
+
case "absolute":
|
|
1462
|
+
return `absolute path refused: ${rejection.detail}`;
|
|
1463
|
+
case "traversal":
|
|
1464
|
+
return `path traversal refused: ${rejection.detail}`;
|
|
1465
|
+
case "backslash":
|
|
1466
|
+
return `backslash in path refused: ${rejection.detail}`;
|
|
1467
|
+
case "control_char":
|
|
1468
|
+
return `control character in path refused: ${JSON.stringify(rejection.detail)}`;
|
|
1469
|
+
case "trailing_separator":
|
|
1470
|
+
return `path names a directory, not a file: ${rejection.detail}`;
|
|
1471
|
+
case "trailing_dot_or_space":
|
|
1472
|
+
return `path segment ending in a dot or space refused: ${rejection.detail}`;
|
|
1473
|
+
case "reserved_device_name":
|
|
1474
|
+
return `reserved device name refused: ${rejection.detail}`;
|
|
1475
|
+
case "alternate_data_stream":
|
|
1476
|
+
return `colon in path refused: ${rejection.detail}`;
|
|
1477
|
+
case "reserved_path":
|
|
1478
|
+
return `this tool never writes or deletes ${RESERVED_ROOTS.join(", ")} or ${RESERVED_EXACT_PATHS.join(", ")}: ${rejection.detail}`;
|
|
1479
|
+
case "not_a_regular_file":
|
|
1480
|
+
return `path is not a regular file: ${rejection.detail}`;
|
|
1481
|
+
case "escapes_destination":
|
|
1482
|
+
return `path resolves outside the destination: ${rejection.detail}`;
|
|
1483
|
+
case "symlink":
|
|
1484
|
+
return `refusing to follow a symlink: ${rejection.detail}`;
|
|
1485
|
+
case "not_a_directory":
|
|
1486
|
+
return `a path component is not a directory: ${rejection.detail}`;
|
|
1487
|
+
case "collision":
|
|
1488
|
+
return `two paths name the same file on a case- or Unicode-insensitive filesystem: ${rejection.detail}`;
|
|
1489
|
+
}
|
|
1490
|
+
}
|
|
1491
|
+
|
|
1492
|
+
// src/lib/instructions/safe-write.ts
|
|
1493
|
+
import { createHash, randomBytes } from "crypto";
|
|
1494
|
+
import {
|
|
1495
|
+
lstat,
|
|
1496
|
+
mkdir,
|
|
1497
|
+
open,
|
|
1498
|
+
readFile,
|
|
1499
|
+
realpath,
|
|
1500
|
+
rename,
|
|
1501
|
+
unlink
|
|
1502
|
+
} from "fs/promises";
|
|
1503
|
+
import path from "path";
|
|
1504
|
+
var ALLOWED_MODES = /* @__PURE__ */ new Set([420, 493]);
|
|
1505
|
+
function isAllowedMode(mode) {
|
|
1506
|
+
if (mode === null || mode === void 0) {
|
|
1507
|
+
return true;
|
|
1508
|
+
}
|
|
1509
|
+
if (!Number.isInteger(mode) || mode < 0) {
|
|
1510
|
+
return false;
|
|
1511
|
+
}
|
|
1512
|
+
const type = mode & 61440;
|
|
1513
|
+
if (type !== 0 && type !== 32768) {
|
|
1514
|
+
return false;
|
|
1515
|
+
}
|
|
1516
|
+
return ALLOWED_MODES.has(mode & 4095);
|
|
1517
|
+
}
|
|
1518
|
+
function permissionBits(mode) {
|
|
1519
|
+
if (mode === null || mode === void 0) {
|
|
1520
|
+
return null;
|
|
1521
|
+
}
|
|
1522
|
+
return mode & 4095;
|
|
1523
|
+
}
|
|
1524
|
+
function resolveDestinationRoot(destination) {
|
|
1525
|
+
return __async(this, null, function* () {
|
|
1526
|
+
yield mkdir(destination, { recursive: true });
|
|
1527
|
+
return realpath(destination);
|
|
1528
|
+
});
|
|
1529
|
+
}
|
|
1530
|
+
function assertSafeTarget(root, relativePath) {
|
|
1531
|
+
return __async(this, null, function* () {
|
|
1532
|
+
const target = path.resolve(root, relativePath);
|
|
1533
|
+
if (target === root || !target.startsWith(root + path.sep)) {
|
|
1534
|
+
return {
|
|
1535
|
+
ok: false,
|
|
1536
|
+
reason: "escapes_destination",
|
|
1537
|
+
detail: relativePath
|
|
1538
|
+
};
|
|
1539
|
+
}
|
|
1540
|
+
const segments = relativePath.split("/");
|
|
1541
|
+
let current = root;
|
|
1542
|
+
let deepestExisting = root;
|
|
1543
|
+
for (const [index, segment] of segments.entries()) {
|
|
1544
|
+
current = path.join(current, segment);
|
|
1545
|
+
const stats = yield lstatOrNull(current);
|
|
1546
|
+
if (stats === null) {
|
|
1547
|
+
break;
|
|
1548
|
+
}
|
|
1549
|
+
if (stats.isSymbolicLink()) {
|
|
1550
|
+
return { ok: false, reason: "symlink", detail: current };
|
|
1551
|
+
}
|
|
1552
|
+
if (index < segments.length - 1 && !stats.isDirectory()) {
|
|
1553
|
+
return { ok: false, reason: "not_a_directory", detail: current };
|
|
1554
|
+
}
|
|
1555
|
+
deepestExisting = current;
|
|
1556
|
+
}
|
|
1557
|
+
if (deepestExisting !== root) {
|
|
1558
|
+
const canonical = yield realpath(deepestExisting).catch(() => null);
|
|
1559
|
+
if (canonical !== null && canonical !== root && !canonical.startsWith(root + path.sep)) {
|
|
1560
|
+
return {
|
|
1561
|
+
ok: false,
|
|
1562
|
+
reason: "escapes_destination",
|
|
1563
|
+
detail: relativePath
|
|
1564
|
+
};
|
|
1293
1565
|
}
|
|
1294
|
-
|
|
1295
|
-
|
|
1296
|
-
|
|
1297
|
-
|
|
1298
|
-
|
|
1299
|
-
|
|
1566
|
+
}
|
|
1567
|
+
return { ok: true, path: target };
|
|
1568
|
+
});
|
|
1569
|
+
}
|
|
1570
|
+
function assertWritableTarget(root, relativePath) {
|
|
1571
|
+
return __async(this, null, function* () {
|
|
1572
|
+
const check = yield assertSafeTarget(root, relativePath);
|
|
1573
|
+
if (!check.ok) {
|
|
1574
|
+
return check;
|
|
1575
|
+
}
|
|
1576
|
+
const stats = yield lstatOrNull(check.path);
|
|
1577
|
+
if (stats === null) {
|
|
1578
|
+
return check;
|
|
1579
|
+
}
|
|
1580
|
+
if (stats.isSymbolicLink()) {
|
|
1581
|
+
return { ok: false, reason: "symlink", detail: check.path };
|
|
1582
|
+
}
|
|
1583
|
+
if (!stats.isFile()) {
|
|
1584
|
+
return { ok: false, reason: "not_a_regular_file", detail: check.path };
|
|
1585
|
+
}
|
|
1586
|
+
return check;
|
|
1587
|
+
});
|
|
1588
|
+
}
|
|
1589
|
+
function readFileSafely(root, relativePath) {
|
|
1590
|
+
return __async(this, null, function* () {
|
|
1591
|
+
const check = yield assertWritableTarget(root, relativePath);
|
|
1592
|
+
if (!check.ok) {
|
|
1593
|
+
throw new Error(`Refusing to sync: ${describeRejection(check)}.`);
|
|
1594
|
+
}
|
|
1595
|
+
const stats = yield lstatOrNull(check.path);
|
|
1596
|
+
if (stats === null) {
|
|
1597
|
+
return null;
|
|
1598
|
+
}
|
|
1599
|
+
try {
|
|
1600
|
+
return { bytes: yield readFile(check.path), mode: stats.mode };
|
|
1601
|
+
} catch (error) {
|
|
1602
|
+
if (error.code === "ENOENT") {
|
|
1603
|
+
return null;
|
|
1300
1604
|
}
|
|
1301
|
-
|
|
1302
|
-
|
|
1303
|
-
|
|
1304
|
-
|
|
1305
|
-
|
|
1306
|
-
|
|
1307
|
-
|
|
1308
|
-
|
|
1309
|
-
|
|
1310
|
-
|
|
1311
|
-
|
|
1312
|
-
|
|
1313
|
-
|
|
1314
|
-
|
|
1315
|
-
|
|
1316
|
-
|
|
1317
|
-
|
|
1318
|
-
|
|
1319
|
-
|
|
1320
|
-
|
|
1321
|
-
|
|
1322
|
-
|
|
1323
|
-
|
|
1324
|
-
|
|
1325
|
-
|
|
1326
|
-
|
|
1327
|
-
|
|
1328
|
-
});
|
|
1329
|
-
|
|
1330
|
-
printError(err.message, 1);
|
|
1605
|
+
throw error;
|
|
1606
|
+
}
|
|
1607
|
+
});
|
|
1608
|
+
}
|
|
1609
|
+
function resolveExistingRoot(destination) {
|
|
1610
|
+
return __async(this, null, function* () {
|
|
1611
|
+
return realpath(destination).catch(() => path.resolve(destination));
|
|
1612
|
+
});
|
|
1613
|
+
}
|
|
1614
|
+
function makeParents(root, relativePath) {
|
|
1615
|
+
return __async(this, null, function* () {
|
|
1616
|
+
const segments = relativePath.split("/").slice(0, -1);
|
|
1617
|
+
let current = root;
|
|
1618
|
+
for (const segment of segments) {
|
|
1619
|
+
current = path.join(current, segment);
|
|
1620
|
+
const stats = yield lstatOrNull(current);
|
|
1621
|
+
if (stats === null) {
|
|
1622
|
+
yield mkdir(current).catch((error) => __async(null, null, function* () {
|
|
1623
|
+
if (error.code !== "EEXIST") {
|
|
1624
|
+
throw error;
|
|
1625
|
+
}
|
|
1626
|
+
const raced = yield lstatOrNull(current);
|
|
1627
|
+
if (raced == null ? void 0 : raced.isSymbolicLink()) {
|
|
1628
|
+
throw new Error(
|
|
1629
|
+
`Refusing to sync: ${describeRejection({ ok: false, reason: "symlink", detail: current })}.`
|
|
1630
|
+
);
|
|
1631
|
+
}
|
|
1632
|
+
}));
|
|
1633
|
+
continue;
|
|
1331
1634
|
}
|
|
1332
|
-
if (
|
|
1333
|
-
|
|
1334
|
-
|
|
1335
|
-
|
|
1336
|
-
}
|
|
1337
|
-
return;
|
|
1635
|
+
if (stats.isSymbolicLink()) {
|
|
1636
|
+
throw new Error(
|
|
1637
|
+
`Refusing to sync: ${describeRejection({ ok: false, reason: "symlink", detail: current })}.`
|
|
1638
|
+
);
|
|
1338
1639
|
}
|
|
1339
|
-
|
|
1340
|
-
|
|
1341
|
-
|
|
1342
|
-
|
|
1343
|
-
"serverName",
|
|
1344
|
-
"displayName",
|
|
1345
|
-
"authType",
|
|
1346
|
-
"enabled",
|
|
1347
|
-
"status"
|
|
1348
|
-
]
|
|
1349
|
-
});
|
|
1350
|
-
})
|
|
1351
|
-
);
|
|
1352
|
-
configs.command("show <id>").description("Show a specific MCP configuration").option("--org <slug>", "Organization context").option("--personal", "Retired \u2014 context is organization-only").option("--format <format>", "Output format: table|json|yaml", "table").action(
|
|
1353
|
-
(id, opts) => __async(null, null, function* () {
|
|
1354
|
-
var _a2, _b, _c, _d, _e, _f, _g;
|
|
1355
|
-
const ctx = resolveContext(opts.personal, opts.org, false);
|
|
1356
|
-
const client = getClient();
|
|
1357
|
-
let config2;
|
|
1358
|
-
try {
|
|
1359
|
-
config2 = yield client.mcp.getConfig(id, {
|
|
1360
|
-
org: (ctx == null ? void 0 : ctx.type) === "org" ? ctx.slug : void 0
|
|
1361
|
-
});
|
|
1362
|
-
} catch (err) {
|
|
1363
|
-
printError(err.message, 1);
|
|
1640
|
+
if (!stats.isDirectory()) {
|
|
1641
|
+
throw new Error(
|
|
1642
|
+
`Refusing to sync: ${describeRejection({ ok: false, reason: "not_a_directory", detail: current })}.`
|
|
1643
|
+
);
|
|
1364
1644
|
}
|
|
1365
|
-
|
|
1366
|
-
|
|
1367
|
-
|
|
1368
|
-
|
|
1645
|
+
}
|
|
1646
|
+
});
|
|
1647
|
+
}
|
|
1648
|
+
function writeFileSafely(input) {
|
|
1649
|
+
return __async(this, null, function* () {
|
|
1650
|
+
var _a2;
|
|
1651
|
+
const check = yield assertSafeTarget(input.root, input.relativePath);
|
|
1652
|
+
if (!check.ok) {
|
|
1653
|
+
throw new Error(`Refusing to sync: ${describeRejection(check)}.`);
|
|
1654
|
+
}
|
|
1655
|
+
if (!isAllowedMode(input.mode)) {
|
|
1656
|
+
throw new Error(
|
|
1657
|
+
`Refusing to sync: ${input.relativePath} asks for file mode ${((_a2 = input.mode) != null ? _a2 : 0).toString(8)}, and only 644 and 755 are accepted.`
|
|
1658
|
+
);
|
|
1659
|
+
}
|
|
1660
|
+
yield makeParents(input.root, input.relativePath);
|
|
1661
|
+
const target = check.path;
|
|
1662
|
+
const directory = path.dirname(target);
|
|
1663
|
+
const directoryBefore = yield lstatOrNull(directory);
|
|
1664
|
+
const temporary = path.join(
|
|
1665
|
+
directory,
|
|
1666
|
+
`.${path.basename(target)}.${randomBytes(8).toString("hex")}.tmp`
|
|
1667
|
+
);
|
|
1668
|
+
let handle = null;
|
|
1669
|
+
try {
|
|
1670
|
+
handle = yield open(temporary, "wx", 384);
|
|
1671
|
+
yield handle.writeFile(input.bytes);
|
|
1672
|
+
const bits = permissionBits(input.mode);
|
|
1673
|
+
if (bits !== null && process.platform !== "win32") {
|
|
1674
|
+
yield handle.chmod(bits);
|
|
1675
|
+
}
|
|
1676
|
+
yield handle.close();
|
|
1677
|
+
handle = null;
|
|
1678
|
+
const parentStats = yield lstatOrNull(directory);
|
|
1679
|
+
if (parentStats === null || parentStats.isSymbolicLink()) {
|
|
1680
|
+
throw new Error(
|
|
1681
|
+
`Refusing to sync: ${describeRejection({ ok: false, reason: "symlink", detail: directory })}.`
|
|
1682
|
+
);
|
|
1369
1683
|
}
|
|
1370
|
-
|
|
1371
|
-
|
|
1372
|
-
|
|
1373
|
-
|
|
1374
|
-
"Display Name": (_c = config2.displayName) != null ? _c : "(none)",
|
|
1375
|
-
"Base URL": (_d = config2.baseUrl) != null ? _d : "(none)",
|
|
1376
|
-
Transport: (_e = config2.transport) != null ? _e : "(default)",
|
|
1377
|
-
"Auth Type": config2.authType,
|
|
1378
|
-
Enabled: config2.enabled ? "yes" : "no",
|
|
1379
|
-
Status: config2.status,
|
|
1380
|
-
"Last Health Check": (_g = (_f = config2.lastHealthCheckAt) == null ? void 0 : _f.slice(0, 19)) != null ? _g : "(never)",
|
|
1381
|
-
Created: config2.createdAt.slice(0, 10)
|
|
1382
|
-
});
|
|
1383
|
-
})
|
|
1384
|
-
);
|
|
1385
|
-
configs.command("delete <id>").description("Delete an MCP configuration").option("--org <slug>", "Organization context").option("--personal", "Retired \u2014 context is organization-only").option("-y, --yes", "Skip confirmation prompt").action(
|
|
1386
|
-
(id, opts) => __async(null, null, function* () {
|
|
1387
|
-
const ctx = resolveContext(opts.personal, opts.org, false);
|
|
1388
|
-
const client = getClient();
|
|
1389
|
-
if (!opts.yes) {
|
|
1390
|
-
process.stderr.write(
|
|
1391
|
-
`Delete MCP config ${id}? This cannot be undone. Pass --yes to confirm.
|
|
1392
|
-
`
|
|
1684
|
+
const targetStats = yield lstatOrNull(target);
|
|
1685
|
+
if (targetStats == null ? void 0 : targetStats.isSymbolicLink()) {
|
|
1686
|
+
throw new Error(
|
|
1687
|
+
`Refusing to sync: ${describeRejection({ ok: false, reason: "symlink", detail: target })}.`
|
|
1393
1688
|
);
|
|
1394
|
-
process.exit(2);
|
|
1395
1689
|
}
|
|
1396
|
-
|
|
1397
|
-
|
|
1398
|
-
|
|
1690
|
+
yield rename(temporary, target);
|
|
1691
|
+
} catch (error) {
|
|
1692
|
+
yield handle == null ? void 0 : handle.close().catch(() => {
|
|
1693
|
+
});
|
|
1694
|
+
if (yield isSameDirectory(directory, directoryBefore)) {
|
|
1695
|
+
yield unlink(temporary).catch(() => {
|
|
1399
1696
|
});
|
|
1400
|
-
} catch (err) {
|
|
1401
|
-
printError(err.message, 1);
|
|
1402
1697
|
}
|
|
1403
|
-
|
|
1404
|
-
|
|
1405
|
-
|
|
1406
|
-
);
|
|
1407
|
-
mcp.addCommand(configs);
|
|
1408
|
-
mcp.command("serve").description(
|
|
1409
|
-
"Run a stdio MCP server exposing this workspace's whole integration catalog. Point Claude Desktop, Cursor, or any MCP client at it."
|
|
1410
|
-
).action(() => __async(null, null, function* () {
|
|
1411
|
-
const { runStdio } = yield import("@fabricorg/sdk-mcp/stdio");
|
|
1412
|
-
yield runStdio();
|
|
1413
|
-
}));
|
|
1414
|
-
return mcp;
|
|
1698
|
+
throw error;
|
|
1699
|
+
}
|
|
1700
|
+
});
|
|
1415
1701
|
}
|
|
1416
|
-
|
|
1417
|
-
|
|
1418
|
-
|
|
1419
|
-
|
|
1420
|
-
const orgs = new Command12("orgs").description("Manage organizations");
|
|
1421
|
-
orgs.command("list").description("List organizations you belong to").option(
|
|
1422
|
-
"--format <format>",
|
|
1423
|
-
"Output format: table|json|yaml|csv",
|
|
1424
|
-
"table"
|
|
1425
|
-
).option("-q, --quiet", "Print slugs only").action((opts) => __async(null, null, function* () {
|
|
1426
|
-
const client = getClient();
|
|
1427
|
-
let list;
|
|
1428
|
-
try {
|
|
1429
|
-
list = yield client.orgs.list();
|
|
1430
|
-
} catch (err) {
|
|
1431
|
-
printError(err.message, 1);
|
|
1702
|
+
function isSameDirectory(directory, before) {
|
|
1703
|
+
return __async(this, null, function* () {
|
|
1704
|
+
if (before === null) {
|
|
1705
|
+
return false;
|
|
1432
1706
|
}
|
|
1433
|
-
|
|
1707
|
+
const now = yield lstatOrNull(directory);
|
|
1708
|
+
return now !== null && now.dev === before.dev && now.ino === before.ino;
|
|
1709
|
+
});
|
|
1710
|
+
}
|
|
1711
|
+
function deleteFileSafely(root, relativePath, expectedSha256) {
|
|
1712
|
+
return __async(this, null, function* () {
|
|
1713
|
+
const check = yield assertSafeTarget(root, relativePath);
|
|
1714
|
+
if (!check.ok) {
|
|
1715
|
+
throw new Error(`Refusing to sync: ${describeRejection(check)}.`);
|
|
1716
|
+
}
|
|
1717
|
+
const stats = yield lstatOrNull(check.path);
|
|
1718
|
+
if (stats === null) {
|
|
1719
|
+
return "missing";
|
|
1720
|
+
}
|
|
1721
|
+
if (stats.isSymbolicLink()) {
|
|
1722
|
+
throw new Error(
|
|
1723
|
+
`Refusing to sync: ${describeRejection({ ok: false, reason: "symlink", detail: check.path })}.`
|
|
1724
|
+
);
|
|
1725
|
+
}
|
|
1726
|
+
if (!stats.isFile()) {
|
|
1727
|
+
throw new Error(
|
|
1728
|
+
`Refusing to sync: ${describeRejection({ ok: false, reason: "not_a_regular_file", detail: check.path })}.`
|
|
1729
|
+
);
|
|
1730
|
+
}
|
|
1731
|
+
let actual;
|
|
1732
|
+
try {
|
|
1733
|
+
actual = createHash("sha256").update(yield readFile(check.path)).digest("hex");
|
|
1734
|
+
} catch (error) {
|
|
1735
|
+
if (error.code === "ENOENT") {
|
|
1736
|
+
return "missing";
|
|
1737
|
+
}
|
|
1738
|
+
throw error;
|
|
1739
|
+
}
|
|
1740
|
+
if (actual !== expectedSha256) {
|
|
1741
|
+
return "modified";
|
|
1742
|
+
}
|
|
1743
|
+
try {
|
|
1744
|
+
yield unlink(check.path);
|
|
1745
|
+
return "deleted";
|
|
1746
|
+
} catch (error) {
|
|
1747
|
+
if (error.code === "ENOENT") {
|
|
1748
|
+
return "missing";
|
|
1749
|
+
}
|
|
1750
|
+
throw error;
|
|
1751
|
+
}
|
|
1752
|
+
});
|
|
1753
|
+
}
|
|
1754
|
+
function lstatOrNull(target) {
|
|
1755
|
+
return __async(this, null, function* () {
|
|
1756
|
+
try {
|
|
1757
|
+
return yield lstat(target);
|
|
1758
|
+
} catch (error) {
|
|
1759
|
+
if (error.code === "ENOENT") {
|
|
1760
|
+
return null;
|
|
1761
|
+
}
|
|
1762
|
+
throw error;
|
|
1763
|
+
}
|
|
1764
|
+
});
|
|
1765
|
+
}
|
|
1766
|
+
function resolvesInside(root, candidate) {
|
|
1767
|
+
return __async(this, null, function* () {
|
|
1768
|
+
const canonicalRoot = yield realpath(root).catch(() => path.resolve(root));
|
|
1769
|
+
const canonicalCandidate = yield realpathDeepest(candidate);
|
|
1770
|
+
return canonicalCandidate === canonicalRoot || canonicalCandidate.startsWith(canonicalRoot + path.sep);
|
|
1771
|
+
});
|
|
1772
|
+
}
|
|
1773
|
+
function realpathDeepest(candidate) {
|
|
1774
|
+
return __async(this, null, function* () {
|
|
1775
|
+
const absolute = path.resolve(candidate);
|
|
1776
|
+
const tail = [];
|
|
1777
|
+
let current = absolute;
|
|
1778
|
+
for (; ; ) {
|
|
1779
|
+
const resolved = yield realpath(current).catch(() => null);
|
|
1780
|
+
if (resolved !== null) {
|
|
1781
|
+
return tail.length === 0 ? resolved : path.join(resolved, ...tail);
|
|
1782
|
+
}
|
|
1783
|
+
const parent = path.dirname(current);
|
|
1784
|
+
if (parent === current) {
|
|
1785
|
+
return absolute;
|
|
1786
|
+
}
|
|
1787
|
+
tail.unshift(path.basename(current));
|
|
1788
|
+
current = parent;
|
|
1789
|
+
}
|
|
1790
|
+
});
|
|
1791
|
+
}
|
|
1792
|
+
|
|
1793
|
+
// src/lib/instructions/apply.ts
|
|
1794
|
+
function applyPlan(input) {
|
|
1795
|
+
return __async(this, null, function* () {
|
|
1796
|
+
var _a2, _b;
|
|
1797
|
+
const { root, plan, contents } = input;
|
|
1798
|
+
for (const entry of [
|
|
1799
|
+
...plan.writes,
|
|
1800
|
+
...plan.verified,
|
|
1801
|
+
...plan.deletes,
|
|
1802
|
+
...plan.keptRenamed
|
|
1803
|
+
]) {
|
|
1804
|
+
const check = yield assertWritableTarget(root, entry.path);
|
|
1805
|
+
if (!check.ok) {
|
|
1806
|
+
throw new Error(
|
|
1807
|
+
`Refusing to sync: ${describeRejection(check)}. Nothing was written.`
|
|
1808
|
+
);
|
|
1809
|
+
}
|
|
1810
|
+
}
|
|
1811
|
+
for (const entry of plan.writes) {
|
|
1812
|
+
if (!isAllowedMode(entry.mode)) {
|
|
1813
|
+
throw new Error(
|
|
1814
|
+
`Refusing to sync: ${entry.path} asks for file mode ${((_a2 = entry.mode) != null ? _a2 : 0).toString(8)}, and only 644 and 755 are accepted. Nothing was written.`
|
|
1815
|
+
);
|
|
1816
|
+
}
|
|
1817
|
+
const bytes = contents.get(entry.path);
|
|
1818
|
+
if (!bytes) {
|
|
1819
|
+
throw new Error(
|
|
1820
|
+
`The downloaded bundle is missing ${entry.path}. Nothing was written.`
|
|
1821
|
+
);
|
|
1822
|
+
}
|
|
1823
|
+
const actual = createHash2("sha256").update(bytes).digest("hex");
|
|
1824
|
+
if (actual !== entry.sha256) {
|
|
1825
|
+
throw new Error(
|
|
1826
|
+
`Checksum mismatch for ${entry.path}: the published snapshot expects ${entry.sha256} and the downloaded bundle contains ${actual}. Nothing was written.`
|
|
1827
|
+
);
|
|
1828
|
+
}
|
|
1829
|
+
}
|
|
1830
|
+
const written = [];
|
|
1831
|
+
const deleted = [];
|
|
1832
|
+
const remoded = [];
|
|
1833
|
+
const keptModified = [];
|
|
1834
|
+
for (const entry of plan.writes) {
|
|
1835
|
+
const bytes = contents.get(entry.path);
|
|
1836
|
+
yield writeFileSafely({
|
|
1837
|
+
root,
|
|
1838
|
+
relativePath: entry.path,
|
|
1839
|
+
bytes,
|
|
1840
|
+
mode: (_b = entry.mode) != null ? _b : null
|
|
1841
|
+
});
|
|
1842
|
+
written.push(entry.path);
|
|
1843
|
+
}
|
|
1844
|
+
for (const entry of plan.verified) {
|
|
1845
|
+
if (yield applyMode(root, entry)) {
|
|
1846
|
+
remoded.push(entry.path);
|
|
1847
|
+
}
|
|
1848
|
+
}
|
|
1849
|
+
for (const entry of plan.deletes) {
|
|
1850
|
+
if (entry.sha256 === void 0) {
|
|
1851
|
+
throw new Error(
|
|
1852
|
+
`Refusing to delete ${entry.path}: the plan carries no hash for it.`
|
|
1853
|
+
);
|
|
1854
|
+
}
|
|
1855
|
+
const outcome = yield deleteFileSafely(root, entry.path, entry.sha256);
|
|
1856
|
+
if (outcome === "deleted") {
|
|
1857
|
+
deleted.push(entry.path);
|
|
1858
|
+
} else if (outcome === "modified") {
|
|
1859
|
+
keptModified.push(entry.path);
|
|
1860
|
+
}
|
|
1861
|
+
}
|
|
1862
|
+
return { written, deleted, remoded, keptModified };
|
|
1863
|
+
});
|
|
1864
|
+
}
|
|
1865
|
+
function applyMode(root, entry) {
|
|
1866
|
+
return __async(this, null, function* () {
|
|
1867
|
+
var _a2;
|
|
1868
|
+
const bits = permissionBits(entry.mode);
|
|
1869
|
+
if (bits === null || process.platform === "win32") {
|
|
1870
|
+
return false;
|
|
1871
|
+
}
|
|
1872
|
+
if (!isAllowedMode(entry.mode)) {
|
|
1873
|
+
throw new Error(
|
|
1874
|
+
`Refusing to sync: ${entry.path} asks for file mode ${((_a2 = entry.mode) != null ? _a2 : 0).toString(8)}, and only 644 and 755 are accepted.`
|
|
1875
|
+
);
|
|
1876
|
+
}
|
|
1877
|
+
const check = yield assertWritableTarget(root, entry.path);
|
|
1878
|
+
if (!check.ok) {
|
|
1879
|
+
throw new Error(`Refusing to sync: ${describeRejection(check)}.`);
|
|
1880
|
+
}
|
|
1881
|
+
try {
|
|
1882
|
+
yield chmod(check.path, bits);
|
|
1883
|
+
return true;
|
|
1884
|
+
} catch (error) {
|
|
1885
|
+
if (error.code === "ENOENT") {
|
|
1886
|
+
return false;
|
|
1887
|
+
}
|
|
1888
|
+
throw error;
|
|
1889
|
+
}
|
|
1890
|
+
});
|
|
1891
|
+
}
|
|
1892
|
+
|
|
1893
|
+
// src/lib/instructions/bundle.ts
|
|
1894
|
+
import { unzipSync } from "fflate";
|
|
1895
|
+
function fetchBundle(url, options) {
|
|
1896
|
+
return __async(this, null, function* () {
|
|
1897
|
+
var _a2, _b, _c, _d;
|
|
1898
|
+
const fetchImpl = (_a2 = options.fetchImpl) != null ? _a2 : fetch;
|
|
1899
|
+
const controller = new AbortController();
|
|
1900
|
+
const timer = setTimeout(() => controller.abort(), options.timeoutMs);
|
|
1901
|
+
const onExternalAbort = () => controller.abort();
|
|
1902
|
+
(_b = options.signal) == null ? void 0 : _b.addEventListener("abort", onExternalAbort, { once: true });
|
|
1903
|
+
if ((_c = options.signal) == null ? void 0 : _c.aborted) {
|
|
1904
|
+
controller.abort();
|
|
1905
|
+
}
|
|
1906
|
+
try {
|
|
1907
|
+
const response = yield fetchImpl(url, { signal: controller.signal });
|
|
1908
|
+
if (!response.ok) {
|
|
1909
|
+
throw new Error(
|
|
1910
|
+
`Downloading the coding-instructions bundle failed with HTTP ${response.status}.`
|
|
1911
|
+
);
|
|
1912
|
+
}
|
|
1913
|
+
const declared = Number(response.headers.get("content-length"));
|
|
1914
|
+
if (Number.isFinite(declared) && declared > options.maxBytes) {
|
|
1915
|
+
throw new Error(
|
|
1916
|
+
`The coding-instructions bundle declares ${declared} bytes and this snapshot's manifest allows at most ${options.maxBytes}. Nothing was written.`
|
|
1917
|
+
);
|
|
1918
|
+
}
|
|
1919
|
+
return yield readBounded(response, options.maxBytes);
|
|
1920
|
+
} catch (error) {
|
|
1921
|
+
if (error instanceof Error && error.name === "AbortError") {
|
|
1922
|
+
throw new Error(
|
|
1923
|
+
`Downloading the coding-instructions bundle timed out after ${options.timeoutMs}ms.`
|
|
1924
|
+
);
|
|
1925
|
+
}
|
|
1926
|
+
throw error;
|
|
1927
|
+
} finally {
|
|
1928
|
+
clearTimeout(timer);
|
|
1929
|
+
(_d = options.signal) == null ? void 0 : _d.removeEventListener("abort", onExternalAbort);
|
|
1930
|
+
}
|
|
1931
|
+
});
|
|
1932
|
+
}
|
|
1933
|
+
function readBounded(response, maxBytes) {
|
|
1934
|
+
return __async(this, null, function* () {
|
|
1935
|
+
const body = response.body;
|
|
1936
|
+
if (!body) {
|
|
1937
|
+
const bytes = new Uint8Array(yield response.arrayBuffer());
|
|
1938
|
+
if (bytes.length > maxBytes) {
|
|
1939
|
+
throw new Error(
|
|
1940
|
+
`The coding-instructions bundle is larger than the ${maxBytes} bytes this snapshot's manifest allows. Nothing was written.`
|
|
1941
|
+
);
|
|
1942
|
+
}
|
|
1943
|
+
return bytes;
|
|
1944
|
+
}
|
|
1945
|
+
const reader = body.getReader();
|
|
1946
|
+
const chunks = [];
|
|
1947
|
+
let total = 0;
|
|
1948
|
+
try {
|
|
1949
|
+
for (; ; ) {
|
|
1950
|
+
const { done, value } = yield reader.read();
|
|
1951
|
+
if (done) {
|
|
1952
|
+
break;
|
|
1953
|
+
}
|
|
1954
|
+
if (!value) {
|
|
1955
|
+
continue;
|
|
1956
|
+
}
|
|
1957
|
+
total += value.length;
|
|
1958
|
+
if (total > maxBytes) {
|
|
1959
|
+
throw new Error(
|
|
1960
|
+
`The coding-instructions bundle is larger than the ${maxBytes} bytes this snapshot's manifest allows. Nothing was written.`
|
|
1961
|
+
);
|
|
1962
|
+
}
|
|
1963
|
+
chunks.push(value);
|
|
1964
|
+
}
|
|
1965
|
+
} finally {
|
|
1966
|
+
yield reader.cancel().catch(() => {
|
|
1967
|
+
});
|
|
1968
|
+
}
|
|
1969
|
+
const archive = new Uint8Array(total);
|
|
1970
|
+
let offset = 0;
|
|
1971
|
+
for (const chunk of chunks) {
|
|
1972
|
+
archive.set(chunk, offset);
|
|
1973
|
+
offset += chunk.length;
|
|
1974
|
+
}
|
|
1975
|
+
return archive;
|
|
1976
|
+
});
|
|
1977
|
+
}
|
|
1978
|
+
function extractBundle(archive, wanted) {
|
|
1979
|
+
const sizes = /* @__PURE__ */ new Map();
|
|
1980
|
+
for (const entry of wanted) {
|
|
1981
|
+
sizes.set(entry.path, entry.size);
|
|
1982
|
+
}
|
|
1983
|
+
if (sizes.size === 0) {
|
|
1984
|
+
return /* @__PURE__ */ new Map();
|
|
1985
|
+
}
|
|
1986
|
+
const seen = /* @__PURE__ */ new Set();
|
|
1987
|
+
const unzipped = unzipSync(archive, {
|
|
1988
|
+
filter: (file) => {
|
|
1989
|
+
const expected = sizes.get(file.name);
|
|
1990
|
+
if (expected === void 0) {
|
|
1991
|
+
return false;
|
|
1992
|
+
}
|
|
1993
|
+
if (seen.has(file.name)) {
|
|
1994
|
+
throw new Error(
|
|
1995
|
+
`The downloaded bundle contains ${file.name} more than once. Nothing was written.`
|
|
1996
|
+
);
|
|
1997
|
+
}
|
|
1998
|
+
seen.add(file.name);
|
|
1999
|
+
if (file.originalSize !== void 0 && file.originalSize !== expected) {
|
|
2000
|
+
throw new Error(
|
|
2001
|
+
`The downloaded bundle declares ${file.originalSize} bytes for ${file.name} and the manifest says ${expected}. Nothing was written.`
|
|
2002
|
+
);
|
|
2003
|
+
}
|
|
2004
|
+
return true;
|
|
2005
|
+
}
|
|
2006
|
+
});
|
|
2007
|
+
const contents = /* @__PURE__ */ new Map();
|
|
2008
|
+
for (const [name, bytes] of Object.entries(unzipped)) {
|
|
2009
|
+
const expected = sizes.get(name);
|
|
2010
|
+
if (expected === void 0) {
|
|
2011
|
+
continue;
|
|
2012
|
+
}
|
|
2013
|
+
if (bytes.length !== expected) {
|
|
2014
|
+
throw new Error(
|
|
2015
|
+
`The downloaded bundle holds ${bytes.length} bytes for ${name} and the manifest says ${expected}. Nothing was written.`
|
|
2016
|
+
);
|
|
2017
|
+
}
|
|
2018
|
+
contents.set(name, bytes);
|
|
2019
|
+
}
|
|
2020
|
+
return contents;
|
|
2021
|
+
}
|
|
2022
|
+
|
|
2023
|
+
// src/lib/instructions/hook.ts
|
|
2024
|
+
import { readFile as readFile2 } from "fs/promises";
|
|
2025
|
+
import path2 from "path";
|
|
2026
|
+
var CLAUDE_SETTINGS_RELATIVE_PATH = path2.join(
|
|
2027
|
+
".claude",
|
|
2028
|
+
"settings.local.json"
|
|
2029
|
+
);
|
|
2030
|
+
var CLAUDE_SETTINGS_POSIX_PATH = ".claude/settings.local.json";
|
|
2031
|
+
var HOOK_TIMEOUT_SECONDS = 15;
|
|
2032
|
+
var HOOK_SUBCOMMANDS = /* @__PURE__ */ new Set(["check", "sync"]);
|
|
2033
|
+
function buildHookCommand(projectId, apply, org) {
|
|
2034
|
+
const verb = apply ? "sync" : "check";
|
|
2035
|
+
const context = org ? ` --org ${org}` : "";
|
|
2036
|
+
return `fabric instructions ${verb} --project ${projectId}${context} --hook`;
|
|
2037
|
+
}
|
|
2038
|
+
function assertKeyStaysOutside(destination, configPath) {
|
|
2039
|
+
return __async(this, null, function* () {
|
|
2040
|
+
if (yield resolvesInside(destination, configPath)) {
|
|
2041
|
+
throw new Error(
|
|
2042
|
+
`The CLI stores its API key at ${configPath}, which resolves inside ${destination}. Writing a session hook here would leave a live credential in the repository. Move the CLI config outside the project (set XDG_CONFIG_HOME) or export FABRIC_API_KEY instead, then run this again.`
|
|
2043
|
+
);
|
|
2044
|
+
}
|
|
2045
|
+
});
|
|
2046
|
+
}
|
|
2047
|
+
function mergeSessionStartHook(input) {
|
|
2048
|
+
return __async(this, null, function* () {
|
|
2049
|
+
const settingsPath = path2.join(input.root, CLAUDE_SETTINGS_RELATIVE_PATH);
|
|
2050
|
+
let existingRaw = null;
|
|
2051
|
+
try {
|
|
2052
|
+
existingRaw = yield readFile2(settingsPath, "utf8");
|
|
2053
|
+
} catch (error) {
|
|
2054
|
+
if (error.code !== "ENOENT") {
|
|
2055
|
+
throw error;
|
|
2056
|
+
}
|
|
2057
|
+
}
|
|
2058
|
+
let settings = {};
|
|
2059
|
+
if (existingRaw !== null && existingRaw.trim().length > 0) {
|
|
2060
|
+
let parsed;
|
|
2061
|
+
try {
|
|
2062
|
+
parsed = JSON.parse(existingRaw);
|
|
2063
|
+
} catch (e) {
|
|
2064
|
+
throw new Error(
|
|
2065
|
+
`${settingsPath} is not valid JSON. Fix or remove it, then run this again \u2014 it was left untouched.`
|
|
2066
|
+
);
|
|
2067
|
+
}
|
|
2068
|
+
if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) {
|
|
2069
|
+
throw new Error(
|
|
2070
|
+
`${settingsPath} does not contain a JSON object. It was left untouched.`
|
|
2071
|
+
);
|
|
2072
|
+
}
|
|
2073
|
+
settings = parsed;
|
|
2074
|
+
}
|
|
2075
|
+
if (settings.hooks !== void 0 && !isRecord(settings.hooks)) {
|
|
2076
|
+
throw refuseShape(settingsPath, '"hooks" is not an object');
|
|
2077
|
+
}
|
|
2078
|
+
const hooks = isRecord(settings.hooks) ? __spreadValues({}, settings.hooks) : {};
|
|
2079
|
+
if (hooks.SessionStart !== void 0 && !Array.isArray(hooks.SessionStart)) {
|
|
2080
|
+
throw refuseShape(settingsPath, '"hooks.SessionStart" is not an array');
|
|
2081
|
+
}
|
|
2082
|
+
const rawGroups = Array.isArray(hooks.SessionStart) ? hooks.SessionStart : [];
|
|
2083
|
+
for (const [index, group] of rawGroups.entries()) {
|
|
2084
|
+
if (!isRecord(group)) {
|
|
2085
|
+
throw refuseShape(
|
|
2086
|
+
settingsPath,
|
|
2087
|
+
`"hooks.SessionStart[${index}]" is not an object`
|
|
2088
|
+
);
|
|
2089
|
+
}
|
|
2090
|
+
if (group.hooks !== void 0 && !Array.isArray(group.hooks)) {
|
|
2091
|
+
throw refuseShape(
|
|
2092
|
+
settingsPath,
|
|
2093
|
+
`"hooks.SessionStart[${index}].hooks" is not an array`
|
|
2094
|
+
);
|
|
2095
|
+
}
|
|
2096
|
+
}
|
|
2097
|
+
const sessionStart = rawGroups.map(
|
|
2098
|
+
(group) => __spreadProps(__spreadValues({}, group), {
|
|
2099
|
+
hooks: Array.isArray(group == null ? void 0 : group.hooks) ? [...group.hooks] : []
|
|
2100
|
+
})
|
|
2101
|
+
);
|
|
2102
|
+
const replacement = {
|
|
2103
|
+
type: "command",
|
|
2104
|
+
command: input.command,
|
|
2105
|
+
timeout: HOOK_TIMEOUT_SECONDS
|
|
2106
|
+
};
|
|
2107
|
+
let replacedCount = 0;
|
|
2108
|
+
const pruned = [];
|
|
2109
|
+
for (const group of sessionStart) {
|
|
2110
|
+
const kept = group.hooks.filter(
|
|
2111
|
+
(hook) => !isFabricHookFor(hook, input.projectId)
|
|
2112
|
+
);
|
|
2113
|
+
const removed = group.hooks.length - kept.length;
|
|
2114
|
+
replacedCount += removed;
|
|
2115
|
+
if (kept.length === 0 && removed > 0) {
|
|
2116
|
+
continue;
|
|
2117
|
+
}
|
|
2118
|
+
pruned.push(__spreadProps(__spreadValues({}, group), { hooks: kept }));
|
|
2119
|
+
}
|
|
2120
|
+
pruned.push({ hooks: [replacement] });
|
|
2121
|
+
const next = __spreadProps(__spreadValues({}, settings), {
|
|
2122
|
+
hooks: __spreadProps(__spreadValues({}, hooks), { SessionStart: pruned })
|
|
2123
|
+
});
|
|
2124
|
+
yield writeFileSafely({
|
|
2125
|
+
root: input.root,
|
|
2126
|
+
relativePath: CLAUDE_SETTINGS_POSIX_PATH,
|
|
2127
|
+
bytes: new TextEncoder().encode(
|
|
2128
|
+
`${JSON.stringify(next, null, 2).replace(/\r\n/g, "\n")}
|
|
2129
|
+
`
|
|
2130
|
+
)
|
|
2131
|
+
});
|
|
2132
|
+
return {
|
|
2133
|
+
settingsPath,
|
|
2134
|
+
command: input.command,
|
|
2135
|
+
createdFile: existingRaw === null,
|
|
2136
|
+
replacedCount
|
|
2137
|
+
};
|
|
2138
|
+
});
|
|
2139
|
+
}
|
|
2140
|
+
function isFabricHookFor(hook, projectId) {
|
|
2141
|
+
if (!isRecord(hook) || typeof hook.command !== "string") {
|
|
2142
|
+
return false;
|
|
2143
|
+
}
|
|
2144
|
+
const tokens = hook.command.trim().split(/\s+/);
|
|
2145
|
+
if (tokens[0] !== "fabric" || tokens[1] !== "instructions") {
|
|
2146
|
+
return false;
|
|
2147
|
+
}
|
|
2148
|
+
if (tokens[2] === void 0 || !HOOK_SUBCOMMANDS.has(tokens[2])) {
|
|
2149
|
+
return false;
|
|
2150
|
+
}
|
|
2151
|
+
return readProjectToken(tokens) === projectId;
|
|
2152
|
+
}
|
|
2153
|
+
function readProjectToken(tokens) {
|
|
2154
|
+
var _a2;
|
|
2155
|
+
let found = null;
|
|
2156
|
+
let occurrences = 0;
|
|
2157
|
+
for (let index = 3; index < tokens.length; index++) {
|
|
2158
|
+
const token = tokens[index];
|
|
2159
|
+
if (token === "--project") {
|
|
2160
|
+
occurrences++;
|
|
2161
|
+
found = (_a2 = tokens[index + 1]) != null ? _a2 : null;
|
|
2162
|
+
continue;
|
|
2163
|
+
}
|
|
2164
|
+
if (token.startsWith("--project=")) {
|
|
2165
|
+
occurrences++;
|
|
2166
|
+
found = token.slice("--project=".length);
|
|
2167
|
+
}
|
|
2168
|
+
}
|
|
2169
|
+
return occurrences === 1 ? found : null;
|
|
2170
|
+
}
|
|
2171
|
+
function refuseShape(settingsPath, what) {
|
|
2172
|
+
return new Error(
|
|
2173
|
+
`${settingsPath} has a shape this tool does not recognise: ${what}. Fix or remove it, then run this again \u2014 it was left untouched.`
|
|
2174
|
+
);
|
|
2175
|
+
}
|
|
2176
|
+
function isRecord(value) {
|
|
2177
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
2178
|
+
}
|
|
2179
|
+
|
|
2180
|
+
// src/lib/instructions/lock.ts
|
|
2181
|
+
import { readFile as readFile3 } from "fs/promises";
|
|
2182
|
+
import path3 from "path";
|
|
2183
|
+
var LOCK_DIRECTORY = ".fabric";
|
|
2184
|
+
var LOCK_FILENAME = "instructions.lock";
|
|
2185
|
+
var LOCK_VERSION = 1;
|
|
2186
|
+
var LOCK_RELATIVE_PATH = `${LOCK_DIRECTORY}/${LOCK_FILENAME}`;
|
|
2187
|
+
var SHA256_HEX = /^[0-9a-f]{64}$/;
|
|
2188
|
+
function lockPath(destination) {
|
|
2189
|
+
return path3.join(destination, LOCK_DIRECTORY, LOCK_FILENAME);
|
|
2190
|
+
}
|
|
2191
|
+
function readLock(destination) {
|
|
2192
|
+
return __async(this, null, function* () {
|
|
2193
|
+
const file = lockPath(destination);
|
|
2194
|
+
let raw;
|
|
2195
|
+
try {
|
|
2196
|
+
raw = yield readFile3(file, "utf8");
|
|
2197
|
+
} catch (error) {
|
|
2198
|
+
if (error.code === "ENOENT") {
|
|
2199
|
+
return null;
|
|
2200
|
+
}
|
|
2201
|
+
throw error;
|
|
2202
|
+
}
|
|
2203
|
+
let parsed;
|
|
2204
|
+
try {
|
|
2205
|
+
parsed = JSON.parse(raw);
|
|
2206
|
+
} catch (e) {
|
|
2207
|
+
throw new Error(
|
|
2208
|
+
`${file} is unreadable: it is not valid JSON. Delete it to start from a clean sync.`
|
|
2209
|
+
);
|
|
2210
|
+
}
|
|
2211
|
+
const problem = lockProblem(parsed);
|
|
2212
|
+
if (problem !== null) {
|
|
2213
|
+
throw new Error(
|
|
2214
|
+
`${file} is unreadable: ${problem}. Delete it to start from a clean sync.`
|
|
2215
|
+
);
|
|
2216
|
+
}
|
|
2217
|
+
return parsed;
|
|
2218
|
+
});
|
|
2219
|
+
}
|
|
2220
|
+
function serializeLock(lock) {
|
|
2221
|
+
return `${JSON.stringify(lock, null, 2).replace(/\r\n/g, "\n")}
|
|
2222
|
+
`;
|
|
2223
|
+
}
|
|
2224
|
+
function writeLock(root, lock) {
|
|
2225
|
+
return __async(this, null, function* () {
|
|
2226
|
+
yield writeFileSafely({
|
|
2227
|
+
root,
|
|
2228
|
+
relativePath: LOCK_RELATIVE_PATH,
|
|
2229
|
+
bytes: new TextEncoder().encode(serializeLock(lock))
|
|
2230
|
+
});
|
|
2231
|
+
});
|
|
2232
|
+
}
|
|
2233
|
+
function lockProblem(value) {
|
|
2234
|
+
if (typeof value !== "object" || value === null || Array.isArray(value)) {
|
|
2235
|
+
return "it is not a JSON object";
|
|
2236
|
+
}
|
|
2237
|
+
const candidate = value;
|
|
2238
|
+
if (candidate.version !== LOCK_VERSION) {
|
|
2239
|
+
return `its version is ${JSON.stringify(candidate.version)} and this build writes version ${LOCK_VERSION}`;
|
|
2240
|
+
}
|
|
2241
|
+
for (const field of [
|
|
2242
|
+
"projectId",
|
|
2243
|
+
"snapshotId",
|
|
2244
|
+
"digest",
|
|
2245
|
+
"syncedAt"
|
|
2246
|
+
]) {
|
|
2247
|
+
const actual = candidate[field];
|
|
2248
|
+
if (typeof actual !== "string" || actual.length === 0) {
|
|
2249
|
+
return `its "${field}" is not a non-empty string`;
|
|
2250
|
+
}
|
|
2251
|
+
}
|
|
2252
|
+
if (typeof candidate.snapshotVersion !== "number" || !Number.isInteger(candidate.snapshotVersion) || candidate.snapshotVersion < 0) {
|
|
2253
|
+
return 'its "snapshotVersion" is not a non-negative integer';
|
|
2254
|
+
}
|
|
2255
|
+
if (typeof candidate.files !== "object" || candidate.files === null || Array.isArray(candidate.files)) {
|
|
2256
|
+
return 'its "files" is not an object';
|
|
2257
|
+
}
|
|
2258
|
+
for (const [filePath, entry] of Object.entries(
|
|
2259
|
+
candidate.files
|
|
2260
|
+
)) {
|
|
2261
|
+
if (typeof entry !== "object" || entry === null || Array.isArray(entry)) {
|
|
2262
|
+
return `its entry for ${JSON.stringify(filePath)} is not an object`;
|
|
2263
|
+
}
|
|
2264
|
+
const record = entry;
|
|
2265
|
+
if (typeof record.sha256 !== "string" || !SHA256_HEX.test(record.sha256)) {
|
|
2266
|
+
return `its entry for ${JSON.stringify(filePath)} has no 64-character hex sha256`;
|
|
2267
|
+
}
|
|
2268
|
+
if (record.mode !== null && (typeof record.mode !== "number" || !Number.isInteger(record.mode) || record.mode < 0)) {
|
|
2269
|
+
return `its entry for ${JSON.stringify(filePath)} has a mode that is neither null nor a non-negative integer`;
|
|
2270
|
+
}
|
|
2271
|
+
}
|
|
2272
|
+
return null;
|
|
2273
|
+
}
|
|
2274
|
+
|
|
2275
|
+
// src/lib/instructions/manifest.ts
|
|
2276
|
+
import { createHash as createHash3 } from "crypto";
|
|
2277
|
+
var SHA256_HEX2 = /^[0-9a-f]{64}$/;
|
|
2278
|
+
var MANIFEST_LIMITS = {
|
|
2279
|
+
maxFiles: 5e3,
|
|
2280
|
+
maxTotalBytes: 52428800,
|
|
2281
|
+
maxFileBytes: 5242880,
|
|
2282
|
+
maxPathBytes: 1024,
|
|
2283
|
+
maxDepth: 64
|
|
2284
|
+
};
|
|
2285
|
+
var ZIP_ENTRY_OVERHEAD_BYTES = 512;
|
|
2286
|
+
function maxArchiveBytes(entries) {
|
|
2287
|
+
let total = 1048576;
|
|
2288
|
+
for (const entry of entries) {
|
|
2289
|
+
total += entry.size + ZIP_ENTRY_OVERHEAD_BYTES + 2 * Buffer.byteLength(entry.path, "utf8");
|
|
2290
|
+
}
|
|
2291
|
+
return total;
|
|
2292
|
+
}
|
|
2293
|
+
function computeSnapshotDigest(entries) {
|
|
2294
|
+
const lines = [...entries].sort((a, b) => a.path < b.path ? -1 : a.path > b.path ? 1 : 0).map((entry) => `${entry.path}\0${entry.sha256}
|
|
2295
|
+
`).join("");
|
|
2296
|
+
return createHash3("sha256").update(Buffer.from(lines, "utf8")).digest("hex");
|
|
2297
|
+
}
|
|
2298
|
+
function assertValidManifest(input) {
|
|
2299
|
+
const { manifest, snapshot } = input;
|
|
2300
|
+
if (!Array.isArray(manifest)) {
|
|
2301
|
+
throw new Error(
|
|
2302
|
+
`The server described version ${snapshot.version} as published but sent no file manifest. Nothing was changed.`
|
|
2303
|
+
);
|
|
2304
|
+
}
|
|
2305
|
+
if (manifest.length > MANIFEST_LIMITS.maxFiles) {
|
|
2306
|
+
throw new Error(
|
|
2307
|
+
`The manifest for version ${snapshot.version} lists ${manifest.length} files and a published snapshot holds at most ${MANIFEST_LIMITS.maxFiles}. Nothing was changed.`
|
|
2308
|
+
);
|
|
2309
|
+
}
|
|
2310
|
+
const entries = [];
|
|
2311
|
+
for (const raw of manifest) {
|
|
2312
|
+
entries.push(validateEntry(raw));
|
|
2313
|
+
}
|
|
2314
|
+
const totalBytes = entries.reduce((total, entry) => total + entry.size, 0);
|
|
2315
|
+
if (totalBytes > MANIFEST_LIMITS.maxTotalBytes) {
|
|
2316
|
+
throw new Error(
|
|
2317
|
+
`The manifest for version ${snapshot.version} describes ${totalBytes} bytes and a published snapshot holds at most ${MANIFEST_LIMITS.maxTotalBytes}. Nothing was changed.`
|
|
2318
|
+
);
|
|
2319
|
+
}
|
|
2320
|
+
const collision = findCollision(entries.map((entry) => entry.path));
|
|
2321
|
+
if (collision !== null) {
|
|
2322
|
+
throw new Error(
|
|
2323
|
+
`Refusing to sync: ${describeRejection({
|
|
2324
|
+
ok: false,
|
|
2325
|
+
reason: "collision",
|
|
2326
|
+
detail: `${collision.first} and ${collision.second}`
|
|
2327
|
+
})}.`
|
|
2328
|
+
);
|
|
2329
|
+
}
|
|
2330
|
+
if (entries.length !== snapshot.fileCount) {
|
|
2331
|
+
throw new Error(
|
|
2332
|
+
`The manifest for version ${snapshot.version} lists ${entries.length} files and the snapshot says ${snapshot.fileCount}. Nothing was changed.`
|
|
2333
|
+
);
|
|
2334
|
+
}
|
|
2335
|
+
const recomputed = computeSnapshotDigest(entries);
|
|
2336
|
+
if (recomputed !== snapshot.digest) {
|
|
2337
|
+
throw new Error(
|
|
2338
|
+
`The manifest for version ${snapshot.version} does not match its own digest (${snapshot.digest}); it hashes to ${recomputed}. Nothing was changed.`
|
|
2339
|
+
);
|
|
2340
|
+
}
|
|
2341
|
+
return entries;
|
|
2342
|
+
}
|
|
2343
|
+
function validateEntry(raw) {
|
|
2344
|
+
var _a2, _b;
|
|
2345
|
+
if (typeof raw !== "object" || raw === null || Array.isArray(raw)) {
|
|
2346
|
+
throw new Error(
|
|
2347
|
+
"The server sent a manifest entry that is not an object. Nothing was changed."
|
|
2348
|
+
);
|
|
2349
|
+
}
|
|
2350
|
+
const entry = raw;
|
|
2351
|
+
if (typeof entry.path !== "string") {
|
|
2352
|
+
throw new Error(
|
|
2353
|
+
"The server sent a manifest entry with no path. Nothing was changed."
|
|
2354
|
+
);
|
|
2355
|
+
}
|
|
2356
|
+
const check = checkRelativePath(entry.path);
|
|
2357
|
+
if (!check.ok) {
|
|
2358
|
+
throw new Error(
|
|
2359
|
+
`Refusing to sync: ${describeRejection(check)}. The published snapshot names a path this tool will not write.`
|
|
2360
|
+
);
|
|
2361
|
+
}
|
|
2362
|
+
const pathBytes = Buffer.byteLength(entry.path, "utf8");
|
|
2363
|
+
if (pathBytes > MANIFEST_LIMITS.maxPathBytes) {
|
|
2364
|
+
throw new Error(
|
|
2365
|
+
`The manifest entry for ${entry.path.slice(0, 80)}\u2026 has a ${pathBytes}-byte path and at most ${MANIFEST_LIMITS.maxPathBytes} is accepted. Nothing was changed.`
|
|
2366
|
+
);
|
|
2367
|
+
}
|
|
2368
|
+
if (entry.path.split("/").length > MANIFEST_LIMITS.maxDepth) {
|
|
2369
|
+
throw new Error(
|
|
2370
|
+
`The manifest entry for ${entry.path} is nested deeper than ${MANIFEST_LIMITS.maxDepth} directories. Nothing was changed.`
|
|
2371
|
+
);
|
|
2372
|
+
}
|
|
2373
|
+
if (isReservedPath(entry.path)) {
|
|
2374
|
+
throw new Error(
|
|
2375
|
+
`Refusing to sync: ${describeRejection({
|
|
2376
|
+
ok: false,
|
|
2377
|
+
reason: "reserved_path",
|
|
2378
|
+
detail: entry.path
|
|
2379
|
+
})}.`
|
|
2380
|
+
);
|
|
2381
|
+
}
|
|
2382
|
+
if (typeof entry.sha256 !== "string" || !SHA256_HEX2.test(entry.sha256)) {
|
|
2383
|
+
throw new Error(
|
|
2384
|
+
`The manifest entry for ${entry.path} has no 64-character hex sha256. Nothing was changed.`
|
|
2385
|
+
);
|
|
2386
|
+
}
|
|
2387
|
+
if (typeof entry.size !== "number" || !Number.isInteger(entry.size) || entry.size < 0) {
|
|
2388
|
+
throw new Error(
|
|
2389
|
+
`The manifest entry for ${entry.path} has no non-negative integer size. Nothing was changed.`
|
|
2390
|
+
);
|
|
2391
|
+
}
|
|
2392
|
+
if (entry.size > MANIFEST_LIMITS.maxFileBytes) {
|
|
2393
|
+
throw new Error(
|
|
2394
|
+
`The manifest entry for ${entry.path} declares ${entry.size} bytes and a published file holds at most ${MANIFEST_LIMITS.maxFileBytes}. Nothing was changed.`
|
|
2395
|
+
);
|
|
2396
|
+
}
|
|
2397
|
+
if (entry.mode !== null && (typeof entry.mode !== "number" || !Number.isInteger(entry.mode))) {
|
|
2398
|
+
throw new Error(
|
|
2399
|
+
`The manifest entry for ${entry.path} has a mode that is neither null nor an integer. Nothing was changed.`
|
|
2400
|
+
);
|
|
2401
|
+
}
|
|
2402
|
+
if (!isAllowedMode(entry.mode)) {
|
|
2403
|
+
throw new Error(
|
|
2404
|
+
`Refusing to sync: ${entry.path} asks for file mode ${((_a2 = entry.mode) != null ? _a2 : 0).toString(8)}, and only 644 and 755 are accepted.`
|
|
2405
|
+
);
|
|
2406
|
+
}
|
|
2407
|
+
return {
|
|
2408
|
+
path: entry.path,
|
|
2409
|
+
sha256: entry.sha256,
|
|
2410
|
+
size: entry.size,
|
|
2411
|
+
mode: entry.mode,
|
|
2412
|
+
kind: (_b = entry.kind) != null ? _b : "OTHER"
|
|
2413
|
+
};
|
|
2414
|
+
}
|
|
2415
|
+
|
|
2416
|
+
// src/lib/instructions/plan.ts
|
|
2417
|
+
import { createHash as createHash4 } from "crypto";
|
|
2418
|
+
function hashLocalFile(root, relativePath) {
|
|
2419
|
+
return __async(this, null, function* () {
|
|
2420
|
+
const read = yield readFileSafely(root, relativePath);
|
|
2421
|
+
return read === null ? null : createHash4("sha256").update(read.bytes).digest("hex");
|
|
2422
|
+
});
|
|
2423
|
+
}
|
|
2424
|
+
function lockPaths(lock) {
|
|
2425
|
+
return lock === null ? [] : Object.keys(lock.files);
|
|
2426
|
+
}
|
|
2427
|
+
function assertSafeLockPaths(paths) {
|
|
2428
|
+
for (const lockedPath of paths) {
|
|
2429
|
+
const check = checkRelativePath(lockedPath);
|
|
2430
|
+
if (!check.ok) {
|
|
2431
|
+
throw new Error(
|
|
2432
|
+
`Refusing to sync: ${describeRejection(check)}. The lock names a path this tool will not touch.`
|
|
2433
|
+
);
|
|
2434
|
+
}
|
|
2435
|
+
if (isReservedPath(lockedPath)) {
|
|
2436
|
+
throw new Error(
|
|
2437
|
+
`Refusing to sync: ${describeRejection({
|
|
2438
|
+
ok: false,
|
|
2439
|
+
reason: "reserved_path",
|
|
2440
|
+
detail: lockedPath
|
|
2441
|
+
})}. The lock names a path this tool will not touch.`
|
|
2442
|
+
);
|
|
2443
|
+
}
|
|
2444
|
+
}
|
|
2445
|
+
const collision = findCollision(paths);
|
|
2446
|
+
if (collision !== null) {
|
|
2447
|
+
throw new Error(
|
|
2448
|
+
`Refusing to sync: ${describeRejection({
|
|
2449
|
+
ok: false,
|
|
2450
|
+
reason: "collision",
|
|
2451
|
+
detail: `${collision.first} and ${collision.second}`
|
|
2452
|
+
})}. The lock names two paths this tool cannot tell apart.`
|
|
2453
|
+
);
|
|
2454
|
+
}
|
|
2455
|
+
}
|
|
2456
|
+
function verifyLedger(input) {
|
|
2457
|
+
return __async(this, null, function* () {
|
|
2458
|
+
assertSafeLockPaths(lockPaths(input.lock));
|
|
2459
|
+
const drifted = [];
|
|
2460
|
+
for (const [lockedPath, locked] of Object.entries(input.lock.files)) {
|
|
2461
|
+
let read;
|
|
2462
|
+
try {
|
|
2463
|
+
read = yield readFileSafely(input.root, lockedPath);
|
|
2464
|
+
} catch (error) {
|
|
2465
|
+
drifted.push(
|
|
2466
|
+
`${lockedPath} (${error instanceof Error ? error.message.replace(/^Refusing to sync: /, "").replace(/\.$/, "") : String(error)})`
|
|
2467
|
+
);
|
|
2468
|
+
continue;
|
|
2469
|
+
}
|
|
2470
|
+
if (read === null) {
|
|
2471
|
+
drifted.push(`${lockedPath} (missing)`);
|
|
2472
|
+
continue;
|
|
2473
|
+
}
|
|
2474
|
+
const actual = createHash4("sha256").update(read.bytes).digest("hex");
|
|
2475
|
+
if (actual !== locked.sha256) {
|
|
2476
|
+
drifted.push(`${lockedPath} (edited)`);
|
|
2477
|
+
continue;
|
|
2478
|
+
}
|
|
2479
|
+
if (process.platform !== "win32" && locked.mode !== null && (read.mode & 4095) !== (locked.mode & 4095)) {
|
|
2480
|
+
drifted.push(
|
|
2481
|
+
`${lockedPath} (mode ${(read.mode & 4095).toString(8)}, published as ${(locked.mode & 4095).toString(8)})`
|
|
2482
|
+
);
|
|
2483
|
+
}
|
|
2484
|
+
}
|
|
2485
|
+
return drifted;
|
|
2486
|
+
});
|
|
2487
|
+
}
|
|
2488
|
+
function computeSyncPlan(input) {
|
|
2489
|
+
return __async(this, null, function* () {
|
|
2490
|
+
var _a2, _b;
|
|
2491
|
+
const { destination, manifest, lock } = input;
|
|
2492
|
+
assertSafeLockPaths(lockPaths(lock));
|
|
2493
|
+
const lockedFiles = new Map(Object.entries((_a2 = lock == null ? void 0 : lock.files) != null ? _a2 : {}));
|
|
2494
|
+
const manifestPaths = new Set(manifest.map((entry) => entry.path));
|
|
2495
|
+
const manifestKeys = new Map(
|
|
2496
|
+
manifest.map(
|
|
2497
|
+
(entry) => [collisionKey(entry.path), entry.path]
|
|
2498
|
+
)
|
|
2499
|
+
);
|
|
2500
|
+
const entries = [];
|
|
2501
|
+
for (const entry of manifest) {
|
|
2502
|
+
const localHash = yield hashLocalFile(destination, entry.path);
|
|
2503
|
+
if (localHash === entry.sha256) {
|
|
2504
|
+
entries.push({
|
|
2505
|
+
path: entry.path,
|
|
2506
|
+
action: "verified",
|
|
2507
|
+
sha256: entry.sha256,
|
|
2508
|
+
mode: entry.mode
|
|
2509
|
+
});
|
|
2510
|
+
continue;
|
|
2511
|
+
}
|
|
2512
|
+
const lockedHash = (_b = lockedFiles.get(entry.path)) == null ? void 0 : _b.sha256;
|
|
2513
|
+
const action = localHash === null ? "added" : localHash === lockedHash ? "updated" : "replaced";
|
|
2514
|
+
entries.push({
|
|
2515
|
+
path: entry.path,
|
|
2516
|
+
action,
|
|
2517
|
+
sha256: entry.sha256,
|
|
2518
|
+
mode: entry.mode
|
|
2519
|
+
});
|
|
2520
|
+
}
|
|
2521
|
+
for (const [lockedPath, locked] of lockedFiles) {
|
|
2522
|
+
if (manifestPaths.has(lockedPath)) {
|
|
2523
|
+
continue;
|
|
2524
|
+
}
|
|
2525
|
+
const renamedTo = manifestKeys.get(collisionKey(lockedPath));
|
|
2526
|
+
if (renamedTo !== void 0) {
|
|
2527
|
+
entries.push({
|
|
2528
|
+
path: lockedPath,
|
|
2529
|
+
action: "kept-renamed",
|
|
2530
|
+
sha256: locked.sha256
|
|
2531
|
+
});
|
|
2532
|
+
continue;
|
|
2533
|
+
}
|
|
2534
|
+
const localHash = yield hashLocalFile(destination, lockedPath);
|
|
2535
|
+
if (localHash === null) {
|
|
2536
|
+
continue;
|
|
2537
|
+
}
|
|
2538
|
+
entries.push({
|
|
2539
|
+
path: lockedPath,
|
|
2540
|
+
action: localHash === locked.sha256 ? "deleted" : "kept-modified",
|
|
2541
|
+
// Carried so the unlink can prove it is still removing the file
|
|
2542
|
+
// this hash described, rather than whatever is at the path by the
|
|
2543
|
+
// time the writes are done.
|
|
2544
|
+
sha256: locked.sha256
|
|
2545
|
+
});
|
|
2546
|
+
}
|
|
2547
|
+
entries.sort((a, b) => a.path.localeCompare(b.path));
|
|
2548
|
+
return {
|
|
2549
|
+
entries,
|
|
2550
|
+
writes: entries.filter(
|
|
2551
|
+
(entry) => entry.action === "added" || entry.action === "updated" || entry.action === "replaced"
|
|
2552
|
+
),
|
|
2553
|
+
deletes: entries.filter((entry) => entry.action === "deleted"),
|
|
2554
|
+
verified: entries.filter((entry) => entry.action === "verified"),
|
|
2555
|
+
keptModified: entries.filter(
|
|
2556
|
+
(entry) => entry.action === "kept-modified"
|
|
2557
|
+
),
|
|
2558
|
+
keptRenamed: entries.filter((entry) => entry.action === "kept-renamed")
|
|
2559
|
+
};
|
|
2560
|
+
});
|
|
2561
|
+
}
|
|
2562
|
+
function nextLock(input) {
|
|
2563
|
+
var _a2;
|
|
2564
|
+
const files = Object.fromEntries(
|
|
2565
|
+
input.manifest.map((entry) => [
|
|
2566
|
+
entry.path,
|
|
2567
|
+
{ sha256: entry.sha256, mode: entry.mode }
|
|
2568
|
+
])
|
|
2569
|
+
);
|
|
2570
|
+
return {
|
|
2571
|
+
version: LOCK_VERSION,
|
|
2572
|
+
projectId: input.projectId,
|
|
2573
|
+
snapshotId: input.snapshot.id,
|
|
2574
|
+
snapshotVersion: input.snapshot.version,
|
|
2575
|
+
digest: input.snapshot.digest,
|
|
2576
|
+
syncedAt: ((_a2 = input.now) != null ? _a2 : /* @__PURE__ */ new Date()).toISOString(),
|
|
2577
|
+
files
|
|
2578
|
+
};
|
|
2579
|
+
}
|
|
2580
|
+
|
|
2581
|
+
// src/commands/instructions/index.ts
|
|
2582
|
+
var HOOK_DEADLINE_MS = 1e4;
|
|
2583
|
+
var CHECK_TIMEOUT_MS = 5e3;
|
|
2584
|
+
var SYNC_TIMEOUT_MS = 15e3;
|
|
2585
|
+
var BUNDLE_TIMEOUT_MS = 6e4;
|
|
2586
|
+
var CliFailure = class extends Error {
|
|
2587
|
+
constructor(message, exitCode) {
|
|
2588
|
+
super(message);
|
|
2589
|
+
this.exitCode = exitCode;
|
|
2590
|
+
this.name = "CliFailure";
|
|
2591
|
+
}
|
|
2592
|
+
};
|
|
2593
|
+
function buildInstructionsCommand() {
|
|
2594
|
+
const instructions = new Command11("instructions").description(
|
|
2595
|
+
"Keep a checkout current with a project's published coding instructions"
|
|
2596
|
+
);
|
|
2597
|
+
instructions.command("check").description(
|
|
2598
|
+
"Report whether the local copy is behind the published one"
|
|
2599
|
+
).requiredOption("--project <id>", "Project ID").option("--dest <dir>", "Destination directory (default: cwd)").option("--org <slug>", "Organization context").option(
|
|
2600
|
+
"--verify",
|
|
2601
|
+
"Also hash the local files against the lock and report any that drifted"
|
|
2602
|
+
).option(
|
|
2603
|
+
"--hook",
|
|
2604
|
+
"Session-hook mode: plain text, never fails, never blocks a session"
|
|
2605
|
+
).option("--format <format>", "Output format: text|json").action(function(opts) {
|
|
2606
|
+
return __async(this, null, function* () {
|
|
2607
|
+
yield run(opts, "check", () => runCheck(opts, formatFor(this)));
|
|
2608
|
+
});
|
|
2609
|
+
});
|
|
2610
|
+
instructions.command("sync").description(
|
|
2611
|
+
"Write the published coding instructions into the checkout"
|
|
2612
|
+
).requiredOption("--project <id>", "Project ID").option("--dest <dir>", "Destination directory (default: cwd)").option("--org <slug>", "Organization context").option("--dry-run", "Print the plan and write nothing").option(
|
|
2613
|
+
"--hook",
|
|
2614
|
+
"Session-hook mode: plain text, never fails, never blocks a session"
|
|
2615
|
+
).option("--format <format>", "Output format: text|json").action(function(opts) {
|
|
2616
|
+
return __async(this, null, function* () {
|
|
2617
|
+
yield run(opts, "sync", () => runSync(opts, formatFor(this)));
|
|
2618
|
+
});
|
|
2619
|
+
});
|
|
2620
|
+
instructions.command("init").description(
|
|
2621
|
+
"Write a SessionStart hook for a coding tool and take the first copy"
|
|
2622
|
+
).requiredOption("--project <id>", "Project ID").requiredOption(
|
|
2623
|
+
"--tool <tool>",
|
|
2624
|
+
"Coding tool to configure: claude-code"
|
|
2625
|
+
).option("--dest <dir>", "Destination directory (default: cwd)").option("--org <slug>", "Organization context").option(
|
|
2626
|
+
"--apply",
|
|
2627
|
+
"Let the hook apply changes instead of only reporting them"
|
|
2628
|
+
).option("--format <format>", "Output format: text|json").action(function(opts) {
|
|
2629
|
+
return __async(this, null, function* () {
|
|
2630
|
+
yield run(
|
|
2631
|
+
__spreadProps(__spreadValues({}, opts), { hook: false }),
|
|
2632
|
+
"init",
|
|
2633
|
+
() => runInit(opts, formatFor(this))
|
|
2634
|
+
);
|
|
2635
|
+
});
|
|
2636
|
+
});
|
|
2637
|
+
return instructions;
|
|
2638
|
+
}
|
|
2639
|
+
function formatFor(command) {
|
|
2640
|
+
const resolved = command.optsWithGlobals();
|
|
2641
|
+
return resolved.format === "json" ? "json" : "text";
|
|
2642
|
+
}
|
|
2643
|
+
function run(opts, verb, body) {
|
|
2644
|
+
return __async(this, null, function* () {
|
|
2645
|
+
try {
|
|
2646
|
+
if (opts.hook) {
|
|
2647
|
+
yield withDeadline(HOOK_DEADLINE_MS, body);
|
|
2648
|
+
} else {
|
|
2649
|
+
yield body();
|
|
2650
|
+
}
|
|
2651
|
+
} catch (error) {
|
|
2652
|
+
const message = describeError(error);
|
|
2653
|
+
if (opts.hook) {
|
|
2654
|
+
process.stderr.write(
|
|
2655
|
+
`fabric: coding instructions ${verb} skipped: ${message}
|
|
2656
|
+
`
|
|
2657
|
+
);
|
|
2658
|
+
process.exit(0);
|
|
2659
|
+
}
|
|
2660
|
+
process.stderr.write(`\u2717 ${message}
|
|
2661
|
+
`);
|
|
2662
|
+
process.exit(error instanceof CliFailure ? error.exitCode : 1);
|
|
2663
|
+
}
|
|
2664
|
+
});
|
|
2665
|
+
}
|
|
2666
|
+
function withDeadline(totalMs, body) {
|
|
2667
|
+
return __async(this, null, function* () {
|
|
2668
|
+
const controller = new AbortController();
|
|
2669
|
+
let timer;
|
|
2670
|
+
const deadline = new Promise((_resolve, reject) => {
|
|
2671
|
+
timer = setTimeout(() => {
|
|
2672
|
+
controller.abort();
|
|
2673
|
+
reject(
|
|
2674
|
+
new Error(
|
|
2675
|
+
`gave up after ${totalMs}ms so the session is not held up`
|
|
2676
|
+
)
|
|
2677
|
+
);
|
|
2678
|
+
}, totalMs);
|
|
2679
|
+
});
|
|
2680
|
+
activeDeadline = controller.signal;
|
|
2681
|
+
try {
|
|
2682
|
+
yield Promise.race([body(), deadline]);
|
|
2683
|
+
} finally {
|
|
2684
|
+
activeDeadline = void 0;
|
|
2685
|
+
if (timer) {
|
|
2686
|
+
clearTimeout(timer);
|
|
2687
|
+
}
|
|
2688
|
+
}
|
|
2689
|
+
});
|
|
2690
|
+
}
|
|
2691
|
+
var activeDeadline;
|
|
2692
|
+
function describeError(error) {
|
|
2693
|
+
if (error instanceof Error) {
|
|
2694
|
+
return error.message.replace(/\s*\n\s*/g, " ");
|
|
2695
|
+
}
|
|
2696
|
+
return String(error);
|
|
2697
|
+
}
|
|
2698
|
+
function asCliFailure(error) {
|
|
2699
|
+
if (error instanceof CliFailure) {
|
|
2700
|
+
return error;
|
|
2701
|
+
}
|
|
2702
|
+
const status = error.status;
|
|
2703
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
2704
|
+
switch (status) {
|
|
2705
|
+
case 401:
|
|
2706
|
+
return new CliFailure(message, 3);
|
|
2707
|
+
case 403:
|
|
2708
|
+
return new CliFailure(message, 5);
|
|
2709
|
+
case 404:
|
|
2710
|
+
return new CliFailure(message, 4);
|
|
2711
|
+
case 429:
|
|
2712
|
+
return new CliFailure(message, 6);
|
|
2713
|
+
case 400:
|
|
2714
|
+
case 422:
|
|
2715
|
+
return new CliFailure(message, 7);
|
|
2716
|
+
default:
|
|
2717
|
+
return new CliFailure(message, 1);
|
|
2718
|
+
}
|
|
2719
|
+
}
|
|
2720
|
+
function destinationOf(opts) {
|
|
2721
|
+
var _a2;
|
|
2722
|
+
return path4.resolve((_a2 = opts.dest) != null ? _a2 : process.cwd());
|
|
2723
|
+
}
|
|
2724
|
+
function instructionsClient(opts, timeoutMs) {
|
|
2725
|
+
if (!getApiKey()) {
|
|
2726
|
+
throw new CliFailure(
|
|
2727
|
+
"Not authenticated. Run: fabric auth login --key <api-key>",
|
|
2728
|
+
3
|
|
2729
|
+
);
|
|
2730
|
+
}
|
|
2731
|
+
return getClient(
|
|
2732
|
+
opts.hook ? {
|
|
2733
|
+
timeoutMs: Math.min(timeoutMs, HOOK_DEADLINE_MS),
|
|
2734
|
+
retry: { maxRetries: 0 }
|
|
2735
|
+
} : { timeoutMs }
|
|
2736
|
+
).withoutContext();
|
|
2737
|
+
}
|
|
2738
|
+
function orgSlugFor(opts) {
|
|
2739
|
+
return opts.org;
|
|
2740
|
+
}
|
|
2741
|
+
function fetchPublished(client, opts, sinceDigest) {
|
|
2742
|
+
return __async(this, null, function* () {
|
|
2743
|
+
const org = orgSlugFor(opts);
|
|
2744
|
+
let published;
|
|
2745
|
+
try {
|
|
2746
|
+
published = yield client.instructions.getPublished(opts.project, {
|
|
2747
|
+
org,
|
|
2748
|
+
sinceDigest
|
|
2749
|
+
});
|
|
2750
|
+
} catch (error) {
|
|
2751
|
+
throw asCliFailure(error);
|
|
2752
|
+
}
|
|
2753
|
+
assertHookMayAct(opts, published);
|
|
2754
|
+
return published;
|
|
2755
|
+
});
|
|
2756
|
+
}
|
|
2757
|
+
function assertHookMayAct(opts, published) {
|
|
2758
|
+
if (opts.hook && published.sourceOfTruth === "REPOSITORY") {
|
|
2759
|
+
throw new CliFailure(
|
|
2760
|
+
"this project's instructions now come from a git repository; run `fabric instructions init` again or remove the hook",
|
|
2761
|
+
4
|
|
2762
|
+
);
|
|
2763
|
+
}
|
|
2764
|
+
}
|
|
2765
|
+
function readLockForProject(destination, projectId) {
|
|
2766
|
+
return __async(this, null, function* () {
|
|
2767
|
+
const lock = yield readLock(destination);
|
|
2768
|
+
if (lock !== null && lock.projectId !== projectId) {
|
|
2769
|
+
throw new CliFailure(
|
|
2770
|
+
`${lockPath(destination)} belongs to project ${lock.projectId}, not ${projectId}. Use a different --dest, or remove that lock if this directory should now follow ${projectId}.`,
|
|
2771
|
+
7
|
|
2772
|
+
);
|
|
2773
|
+
}
|
|
2774
|
+
return lock;
|
|
2775
|
+
});
|
|
2776
|
+
}
|
|
2777
|
+
function line(text) {
|
|
2778
|
+
process.stdout.write(`${text}
|
|
2779
|
+
`);
|
|
2780
|
+
}
|
|
2781
|
+
function listPaths(label, paths) {
|
|
2782
|
+
if (paths.length === 0) {
|
|
2783
|
+
return;
|
|
2784
|
+
}
|
|
2785
|
+
line(` ${label}:`);
|
|
2786
|
+
for (const p of paths) {
|
|
2787
|
+
line(` ${p}`);
|
|
2788
|
+
}
|
|
2789
|
+
}
|
|
2790
|
+
function runCheck(opts, format) {
|
|
2791
|
+
return __async(this, null, function* () {
|
|
2792
|
+
var _a2;
|
|
2793
|
+
const destination = destinationOf(opts);
|
|
2794
|
+
const root = yield resolveExistingRoot(destination);
|
|
2795
|
+
const lock = yield readLockForProject(destination, opts.project);
|
|
2796
|
+
const client = instructionsClient(opts, CHECK_TIMEOUT_MS);
|
|
2797
|
+
const published = yield fetchPublished(client, opts, lock == null ? void 0 : lock.digest);
|
|
2798
|
+
const drifted = opts.verify && lock !== null ? yield verifyLedger({ root, lock }) : [];
|
|
2799
|
+
if (format === "json") {
|
|
2800
|
+
printOutput(
|
|
2801
|
+
__spreadValues({
|
|
2802
|
+
projectId: opts.project,
|
|
2803
|
+
destination,
|
|
2804
|
+
synced: lock !== null,
|
|
2805
|
+
localDigest: (_a2 = lock == null ? void 0 : lock.digest) != null ? _a2 : null,
|
|
2806
|
+
verified: Boolean(opts.verify),
|
|
2807
|
+
drifted
|
|
2808
|
+
}, published),
|
|
2809
|
+
{ format: "json" }
|
|
2810
|
+
);
|
|
2811
|
+
return;
|
|
2812
|
+
}
|
|
2813
|
+
reportPublishedState(opts, destination, lock, published);
|
|
2814
|
+
if (opts.verify) {
|
|
2815
|
+
reportDrift(opts, destination, drifted);
|
|
2816
|
+
}
|
|
2817
|
+
});
|
|
2818
|
+
}
|
|
2819
|
+
function reportPublishedState(opts, destination, lock, published) {
|
|
2820
|
+
var _a2, _b;
|
|
2821
|
+
if (!published.published) {
|
|
2822
|
+
line("This project has no published coding instructions yet.");
|
|
2823
|
+
return;
|
|
2824
|
+
}
|
|
2825
|
+
const version = (_a2 = published.snapshot) == null ? void 0 : _a2.version;
|
|
2826
|
+
if (!lock) {
|
|
2827
|
+
line(
|
|
2828
|
+
`Coding instructions have not been synced into ${destination} yet (published version ${version}, ${(_b = published.snapshot) == null ? void 0 : _b.fileCount} files).`
|
|
2829
|
+
);
|
|
2830
|
+
line(
|
|
2831
|
+
`Run \`fabric instructions sync --project ${opts.project}\` to take a copy.`
|
|
2832
|
+
);
|
|
2833
|
+
return;
|
|
2834
|
+
}
|
|
2835
|
+
if (published.unchanged) {
|
|
2836
|
+
line(`Published coding instructions unchanged (version ${version}).`);
|
|
2837
|
+
return;
|
|
2838
|
+
}
|
|
2839
|
+
if (published.changes === null || published.changes === void 0) {
|
|
2840
|
+
line(
|
|
2841
|
+
`Coding instructions changed, and the version in ${destination} is not one the server still recognises, so what changed cannot be listed. A full sync is needed (published version ${version}).`
|
|
2842
|
+
);
|
|
2843
|
+
line(
|
|
2844
|
+
`Run \`fabric instructions sync --project ${opts.project}\` to apply.`
|
|
2845
|
+
);
|
|
2846
|
+
return;
|
|
2847
|
+
}
|
|
2848
|
+
const { added, changed, removed } = published.changes;
|
|
2849
|
+
line(
|
|
2850
|
+
`Coding instructions updated: ${added.length} added, ${changed.length} changed, ${removed.length} removed (version ${version}).`
|
|
2851
|
+
);
|
|
2852
|
+
listPaths("added", added);
|
|
2853
|
+
listPaths("changed", changed);
|
|
2854
|
+
listPaths("removed", removed);
|
|
2855
|
+
line(
|
|
2856
|
+
`Run \`fabric instructions sync --project ${opts.project}\` to apply.`
|
|
2857
|
+
);
|
|
2858
|
+
}
|
|
2859
|
+
function reportDrift(opts, destination, drifted) {
|
|
2860
|
+
if (drifted.length === 0) {
|
|
2861
|
+
line(`Every file in ${destination} matches the lock.`);
|
|
2862
|
+
return;
|
|
2863
|
+
}
|
|
2864
|
+
line(`${drifted.length} local file(s) no longer match the lock:`);
|
|
2865
|
+
listPaths("drifted", drifted);
|
|
2866
|
+
line(
|
|
2867
|
+
`Run \`fabric instructions sync --project ${opts.project}\` to put them back.`
|
|
2868
|
+
);
|
|
2869
|
+
}
|
|
2870
|
+
function runSync(opts, format) {
|
|
2871
|
+
return __async(this, null, function* () {
|
|
2872
|
+
const result = yield syncOnce(opts);
|
|
2873
|
+
if (format === "json") {
|
|
2874
|
+
printOutput(result, { format: "json" });
|
|
2875
|
+
return;
|
|
2876
|
+
}
|
|
2877
|
+
reportSync(result, opts);
|
|
2878
|
+
});
|
|
2879
|
+
}
|
|
2880
|
+
function emptyOutcome(opts, destination) {
|
|
2881
|
+
return {
|
|
2882
|
+
projectId: opts.project,
|
|
2883
|
+
destination,
|
|
2884
|
+
published: true,
|
|
2885
|
+
unchanged: false,
|
|
2886
|
+
dryRun: Boolean(opts.dryRun),
|
|
2887
|
+
version: null,
|
|
2888
|
+
digest: null,
|
|
2889
|
+
added: [],
|
|
2890
|
+
updated: [],
|
|
2891
|
+
replaced: [],
|
|
2892
|
+
removed: [],
|
|
2893
|
+
keptModified: [],
|
|
2894
|
+
keptRenamed: [],
|
|
2895
|
+
drifted: [],
|
|
2896
|
+
verified: 0,
|
|
2897
|
+
lockPath: lockPath(destination)
|
|
2898
|
+
};
|
|
2899
|
+
}
|
|
2900
|
+
function syncOnce(opts) {
|
|
2901
|
+
return __async(this, null, function* () {
|
|
2902
|
+
const root = yield resolveDestinationRoot(destinationOf(opts));
|
|
2903
|
+
const outcome = emptyOutcome(opts, root);
|
|
2904
|
+
const lock = yield readLockForProject(root, opts.project);
|
|
2905
|
+
const client = instructionsClient(opts, SYNC_TIMEOUT_MS);
|
|
2906
|
+
let published = yield fetchPublished(client, opts, lock == null ? void 0 : lock.digest);
|
|
2907
|
+
assertInitSyncMayAct(opts, published);
|
|
2908
|
+
if (!published.published || !published.snapshot) {
|
|
2909
|
+
throw new CliFailure(
|
|
2910
|
+
"This project has no published coding instructions yet.",
|
|
2911
|
+
4
|
|
2912
|
+
);
|
|
2913
|
+
}
|
|
2914
|
+
outcome.version = published.snapshot.version;
|
|
2915
|
+
outcome.digest = published.snapshot.digest;
|
|
2916
|
+
if (published.unchanged) {
|
|
2917
|
+
const drifted = lock === null ? [] : yield verifyLedger({ root, lock });
|
|
2918
|
+
if (drifted.length === 0) {
|
|
2919
|
+
outcome.unchanged = true;
|
|
2920
|
+
return outcome;
|
|
2921
|
+
}
|
|
2922
|
+
outcome.drifted = drifted;
|
|
2923
|
+
published = yield fetchPublished(client, opts);
|
|
2924
|
+
assertInitSyncMayAct(opts, published);
|
|
2925
|
+
if (!published.published || !published.snapshot) {
|
|
2926
|
+
throw new CliFailure(
|
|
2927
|
+
"This project has no published coding instructions yet.",
|
|
2928
|
+
4
|
|
2929
|
+
);
|
|
2930
|
+
}
|
|
2931
|
+
outcome.version = published.snapshot.version;
|
|
2932
|
+
outcome.digest = published.snapshot.digest;
|
|
2933
|
+
}
|
|
2934
|
+
const manifest = assertValidManifest({
|
|
2935
|
+
manifest: published.manifest,
|
|
2936
|
+
snapshot: published.snapshot
|
|
2937
|
+
});
|
|
2938
|
+
const plan = yield computeSyncPlan({ destination: root, manifest, lock });
|
|
2939
|
+
fillPlanPaths(outcome, plan);
|
|
2940
|
+
if (opts.dryRun) {
|
|
2941
|
+
return outcome;
|
|
2942
|
+
}
|
|
2943
|
+
let contents = /* @__PURE__ */ new Map();
|
|
2944
|
+
if (plan.writes.length > 0) {
|
|
2945
|
+
const org = orgSlugFor(opts);
|
|
2946
|
+
let download;
|
|
2947
|
+
try {
|
|
2948
|
+
download = yield client.instructions.createDownloadUrl(
|
|
2949
|
+
opts.project,
|
|
2950
|
+
{ org }
|
|
2951
|
+
);
|
|
2952
|
+
} catch (error) {
|
|
2953
|
+
throw asCliFailure(error);
|
|
2954
|
+
}
|
|
2955
|
+
const sizes = new Map(
|
|
2956
|
+
manifest.map((entry) => [entry.path, entry.size])
|
|
2957
|
+
);
|
|
2958
|
+
const archive = yield fetchBundle(download.url, {
|
|
2959
|
+
timeoutMs: opts.hook ? HOOK_DEADLINE_MS : BUNDLE_TIMEOUT_MS,
|
|
2960
|
+
// Bounded by what this manifest says it holds, not by what the
|
|
2961
|
+
// response claims. The manifest has already been checked against
|
|
2962
|
+
// the published snapshot limits, so this is a number the client
|
|
2963
|
+
// decided.
|
|
2964
|
+
maxBytes: maxArchiveBytes(manifest),
|
|
2965
|
+
// In hook mode the same clock that bounds the command bounds the
|
|
2966
|
+
// download, rather than a second independent budget after it.
|
|
2967
|
+
signal: activeDeadline
|
|
2968
|
+
});
|
|
2969
|
+
contents = extractBundle(
|
|
2970
|
+
archive,
|
|
2971
|
+
plan.writes.map((entry) => {
|
|
2972
|
+
var _a2;
|
|
2973
|
+
return {
|
|
2974
|
+
path: entry.path,
|
|
2975
|
+
size: (_a2 = sizes.get(entry.path)) != null ? _a2 : -1
|
|
2976
|
+
};
|
|
2977
|
+
})
|
|
2978
|
+
);
|
|
2979
|
+
}
|
|
2980
|
+
const applied = yield applyPlan({ root, plan, contents });
|
|
2981
|
+
outcome.removed = applied.deleted;
|
|
2982
|
+
outcome.keptModified = [
|
|
2983
|
+
...outcome.keptModified,
|
|
2984
|
+
...applied.keptModified
|
|
2985
|
+
].sort();
|
|
2986
|
+
const lockToWrite = nextLock({
|
|
2987
|
+
projectId: opts.project,
|
|
2988
|
+
snapshot: {
|
|
2989
|
+
id: published.snapshot.id,
|
|
2990
|
+
version: published.snapshot.version,
|
|
2991
|
+
digest: published.snapshot.digest
|
|
2992
|
+
},
|
|
2993
|
+
manifest
|
|
2994
|
+
});
|
|
2995
|
+
yield writeLock(root, lockToWrite);
|
|
2996
|
+
return outcome;
|
|
2997
|
+
});
|
|
2998
|
+
}
|
|
2999
|
+
function assertInitSyncMayAct(opts, published) {
|
|
3000
|
+
if (opts.rejectRepository && published.sourceOfTruth === "REPOSITORY") {
|
|
3001
|
+
throw new CliFailure(
|
|
3002
|
+
"This project's coding instructions come from its repository, so they arrive with `git pull`. A session hook would fight it; nothing was written.",
|
|
3003
|
+
7
|
|
3004
|
+
);
|
|
3005
|
+
}
|
|
3006
|
+
}
|
|
3007
|
+
function fillPlanPaths(outcome, plan) {
|
|
3008
|
+
outcome.added = plan.entries.filter((entry) => entry.action === "added").map((entry) => entry.path);
|
|
3009
|
+
outcome.updated = plan.entries.filter((entry) => entry.action === "updated").map((entry) => entry.path);
|
|
3010
|
+
outcome.replaced = plan.entries.filter((entry) => entry.action === "replaced").map((entry) => entry.path);
|
|
3011
|
+
outcome.removed = plan.deletes.map((entry) => entry.path);
|
|
3012
|
+
outcome.keptModified = plan.keptModified.map((entry) => entry.path);
|
|
3013
|
+
outcome.keptRenamed = plan.keptRenamed.map((entry) => entry.path);
|
|
3014
|
+
outcome.verified = plan.verified.length;
|
|
3015
|
+
}
|
|
3016
|
+
function reportSync(outcome, opts) {
|
|
3017
|
+
if (outcome.unchanged) {
|
|
3018
|
+
line(
|
|
3019
|
+
`Coding instructions are up to date (version ${outcome.version}).`
|
|
3020
|
+
);
|
|
3021
|
+
return;
|
|
3022
|
+
}
|
|
3023
|
+
const total = outcome.added.length + outcome.updated.length + outcome.replaced.length + outcome.removed.length;
|
|
3024
|
+
if (outcome.drifted.length > 0) {
|
|
3025
|
+
line(
|
|
3026
|
+
`Coding instructions version ${outcome.version} is still the published one, but ${outcome.drifted.length} local file(s) no longer match it:`
|
|
3027
|
+
);
|
|
3028
|
+
listPaths("drifted", outcome.drifted);
|
|
3029
|
+
}
|
|
3030
|
+
const prefix = opts.dryRun ? "Would apply" : "Applied";
|
|
3031
|
+
line(
|
|
3032
|
+
`${prefix} coding instructions version ${outcome.version}: ${outcome.added.length} added, ${outcome.updated.length} updated, ${outcome.replaced.length} replaced, ${outcome.removed.length} removed (${outcome.verified} already current).`
|
|
3033
|
+
);
|
|
3034
|
+
listPaths("added", outcome.added);
|
|
3035
|
+
listPaths("updated", outcome.updated);
|
|
3036
|
+
listPaths("replaced local edits", outcome.replaced);
|
|
3037
|
+
listPaths("removed", outcome.removed);
|
|
3038
|
+
listPaths("kept, modified locally", outcome.keptModified);
|
|
3039
|
+
listPaths("kept, renamed in the published snapshot", outcome.keptRenamed);
|
|
3040
|
+
if (!opts.dryRun) {
|
|
3041
|
+
line(`Lock: ${outcome.lockPath}`);
|
|
3042
|
+
}
|
|
3043
|
+
if (total === 0 && outcome.keptModified.length === 0 && outcome.keptRenamed.length === 0) {
|
|
3044
|
+
line(`Everything in ${outcome.destination} already matched.`);
|
|
3045
|
+
}
|
|
3046
|
+
}
|
|
3047
|
+
function runInit(opts, format) {
|
|
3048
|
+
return __async(this, null, function* () {
|
|
3049
|
+
if (opts.tool !== "claude-code") {
|
|
3050
|
+
throw new CliFailure(
|
|
3051
|
+
`Unsupported tool "${opts.tool}". Only --tool claude-code is supported today; other tools follow.`,
|
|
3052
|
+
2
|
|
3053
|
+
);
|
|
3054
|
+
}
|
|
3055
|
+
const root = yield resolveDestinationRoot(destinationOf(opts));
|
|
3056
|
+
yield readLockForProject(root, opts.project);
|
|
3057
|
+
try {
|
|
3058
|
+
yield assertKeyStaysOutside(root, getConfigPath());
|
|
3059
|
+
} catch (error) {
|
|
3060
|
+
throw new CliFailure(describeError(error), 7);
|
|
3061
|
+
}
|
|
3062
|
+
const client = instructionsClient(opts, SYNC_TIMEOUT_MS);
|
|
3063
|
+
const published = yield fetchPublished(client, opts);
|
|
3064
|
+
if (published.sourceOfTruth === "REPOSITORY") {
|
|
3065
|
+
throw new CliFailure(
|
|
3066
|
+
"This project's coding instructions come from its repository, so they arrive with `git pull`. A session hook would fight it; nothing was written.",
|
|
3067
|
+
7
|
|
3068
|
+
);
|
|
3069
|
+
}
|
|
3070
|
+
const outcome = published.published ? (
|
|
3071
|
+
// A second manifest call, deliberately: the one above answered "may
|
|
3072
|
+
// a hook be installed for this project at all", and this one is the
|
|
3073
|
+
// sync's own. Do it before the hook write: a failed first copy must
|
|
3074
|
+
// not leave a new or updated hook behind.
|
|
3075
|
+
yield syncOnce(__spreadProps(__spreadValues({}, opts), { hook: false, rejectRepository: true }))
|
|
3076
|
+
) : null;
|
|
3077
|
+
const command = buildHookCommand(
|
|
3078
|
+
opts.project,
|
|
3079
|
+
Boolean(opts.apply),
|
|
3080
|
+
opts.org
|
|
3081
|
+
);
|
|
3082
|
+
const merged = yield mergeSessionStartHook({
|
|
3083
|
+
root,
|
|
3084
|
+
projectId: opts.project,
|
|
3085
|
+
command
|
|
3086
|
+
});
|
|
3087
|
+
if (format === "json") {
|
|
3088
|
+
printOutput(
|
|
3089
|
+
{
|
|
3090
|
+
projectId: opts.project,
|
|
3091
|
+
destination: root,
|
|
3092
|
+
settingsPath: merged.settingsPath,
|
|
3093
|
+
hookCommand: merged.command,
|
|
3094
|
+
createdSettingsFile: merged.createdFile,
|
|
3095
|
+
replacedHooks: merged.replacedCount,
|
|
3096
|
+
sync: outcome
|
|
3097
|
+
},
|
|
3098
|
+
{ format: "json" }
|
|
3099
|
+
);
|
|
3100
|
+
return;
|
|
3101
|
+
}
|
|
3102
|
+
line(
|
|
3103
|
+
merged.replacedCount > 0 ? `Updated the SessionStart hook in ${merged.settingsPath}.` : `Added a SessionStart hook to ${merged.settingsPath}.`
|
|
3104
|
+
);
|
|
3105
|
+
line(` ${command}`);
|
|
3106
|
+
line(
|
|
3107
|
+
opts.apply ? " It applies changes at session start." : " It only reports changes; add --apply to init to have it apply them."
|
|
3108
|
+
);
|
|
3109
|
+
if (outcome === null) {
|
|
3110
|
+
line(
|
|
3111
|
+
"This project has nothing published yet, so there is nothing to copy \u2014 the hook will pick it up once there is."
|
|
3112
|
+
);
|
|
3113
|
+
} else {
|
|
3114
|
+
line("");
|
|
3115
|
+
reportSync(outcome, opts);
|
|
3116
|
+
}
|
|
3117
|
+
line("");
|
|
3118
|
+
line(
|
|
3119
|
+
`${CLAUDE_SETTINGS_RELATIVE_PATH} and ${LOCK_DIRECTORY}/ are local to this machine. If this repository does not ignore them already, add them to your own ignore rules \u2014 this command does not edit .gitignore.`
|
|
3120
|
+
);
|
|
3121
|
+
});
|
|
3122
|
+
}
|
|
3123
|
+
|
|
3124
|
+
// src/commands/mcp/index.ts
|
|
3125
|
+
import { Command as Command12 } from "commander";
|
|
3126
|
+
function buildMcpCommand() {
|
|
3127
|
+
const mcp = new Command12("mcp").description(
|
|
3128
|
+
"Manage MCP servers and configurations"
|
|
3129
|
+
);
|
|
3130
|
+
mcp.command("servers").description("List accessible MCP servers (system + custom)").option("--org <slug>", "Organization context").option("--personal", "Retired \u2014 context is organization-only").option(
|
|
3131
|
+
"--format <format>",
|
|
3132
|
+
"Output format: table|json|yaml|csv",
|
|
3133
|
+
"table"
|
|
3134
|
+
).option("-q, --quiet", "Print server keys only").action(
|
|
3135
|
+
(opts) => __async(null, null, function* () {
|
|
3136
|
+
const ctx = resolveContext(opts.personal, opts.org, false);
|
|
3137
|
+
const client = getClient();
|
|
3138
|
+
let list;
|
|
3139
|
+
try {
|
|
3140
|
+
list = yield client.mcp.listServers({
|
|
3141
|
+
org: (ctx == null ? void 0 : ctx.type) === "org" ? ctx.slug : void 0
|
|
3142
|
+
});
|
|
3143
|
+
} catch (err) {
|
|
3144
|
+
printError(err.message, 1);
|
|
3145
|
+
}
|
|
3146
|
+
if (opts.quiet) {
|
|
3147
|
+
for (const s of list) {
|
|
3148
|
+
process.stdout.write(`${s.key}
|
|
3149
|
+
`);
|
|
3150
|
+
}
|
|
3151
|
+
return;
|
|
3152
|
+
}
|
|
3153
|
+
printOutput(list, {
|
|
3154
|
+
format: opts.format,
|
|
3155
|
+
columns: [
|
|
3156
|
+
"key",
|
|
3157
|
+
"name",
|
|
3158
|
+
"transport",
|
|
3159
|
+
"category",
|
|
3160
|
+
"isSystemProvided"
|
|
3161
|
+
]
|
|
3162
|
+
});
|
|
3163
|
+
})
|
|
3164
|
+
);
|
|
3165
|
+
const configs = new Command12("configs").description(
|
|
3166
|
+
"Manage MCP configurations"
|
|
3167
|
+
);
|
|
3168
|
+
configs.command("list", { isDefault: true }).description("List your MCP configurations").option("--org <slug>", "Organization context").option("--personal", "Retired \u2014 context is organization-only").option(
|
|
3169
|
+
"--format <format>",
|
|
3170
|
+
"Output format: table|json|yaml|csv",
|
|
3171
|
+
"table"
|
|
3172
|
+
).option("-q, --quiet", "Print IDs only").action(
|
|
3173
|
+
(opts) => __async(null, null, function* () {
|
|
3174
|
+
const ctx = resolveContext(opts.personal, opts.org, false);
|
|
3175
|
+
const client = getClient();
|
|
3176
|
+
let list;
|
|
3177
|
+
try {
|
|
3178
|
+
list = yield client.mcp.listConfigs({
|
|
3179
|
+
org: (ctx == null ? void 0 : ctx.type) === "org" ? ctx.slug : void 0
|
|
3180
|
+
});
|
|
3181
|
+
} catch (err) {
|
|
3182
|
+
printError(err.message, 1);
|
|
3183
|
+
}
|
|
3184
|
+
if (opts.quiet) {
|
|
3185
|
+
for (const c of list) {
|
|
3186
|
+
process.stdout.write(`${c.id}
|
|
3187
|
+
`);
|
|
3188
|
+
}
|
|
3189
|
+
return;
|
|
3190
|
+
}
|
|
3191
|
+
printOutput(list, {
|
|
3192
|
+
format: opts.format,
|
|
3193
|
+
columns: [
|
|
3194
|
+
"id",
|
|
3195
|
+
"serverName",
|
|
3196
|
+
"displayName",
|
|
3197
|
+
"authType",
|
|
3198
|
+
"enabled",
|
|
3199
|
+
"status"
|
|
3200
|
+
]
|
|
3201
|
+
});
|
|
3202
|
+
})
|
|
3203
|
+
);
|
|
3204
|
+
configs.command("show <id>").description("Show a specific MCP configuration").option("--org <slug>", "Organization context").option("--personal", "Retired \u2014 context is organization-only").option("--format <format>", "Output format: table|json|yaml", "table").action(
|
|
3205
|
+
(id, opts) => __async(null, null, function* () {
|
|
3206
|
+
var _a2, _b, _c, _d, _e, _f, _g;
|
|
3207
|
+
const ctx = resolveContext(opts.personal, opts.org, false);
|
|
3208
|
+
const client = getClient();
|
|
3209
|
+
let config2;
|
|
3210
|
+
try {
|
|
3211
|
+
config2 = yield client.mcp.getConfig(id, {
|
|
3212
|
+
org: (ctx == null ? void 0 : ctx.type) === "org" ? ctx.slug : void 0
|
|
3213
|
+
});
|
|
3214
|
+
} catch (err) {
|
|
3215
|
+
printError(err.message, 1);
|
|
3216
|
+
}
|
|
3217
|
+
const fmt = opts.format;
|
|
3218
|
+
if (fmt === "json" || fmt === "yaml") {
|
|
3219
|
+
printOutput(config2, { format: fmt });
|
|
3220
|
+
return;
|
|
3221
|
+
}
|
|
3222
|
+
printRecord({
|
|
3223
|
+
ID: config2.id,
|
|
3224
|
+
Server: (_a2 = config2.serverName) != null ? _a2 : config2.mcpServerId,
|
|
3225
|
+
"Server Key": (_b = config2.serverKey) != null ? _b : "(none)",
|
|
3226
|
+
"Display Name": (_c = config2.displayName) != null ? _c : "(none)",
|
|
3227
|
+
"Base URL": (_d = config2.baseUrl) != null ? _d : "(none)",
|
|
3228
|
+
Transport: (_e = config2.transport) != null ? _e : "(default)",
|
|
3229
|
+
"Auth Type": config2.authType,
|
|
3230
|
+
Enabled: config2.enabled ? "yes" : "no",
|
|
3231
|
+
Status: config2.status,
|
|
3232
|
+
"Last Health Check": (_g = (_f = config2.lastHealthCheckAt) == null ? void 0 : _f.slice(0, 19)) != null ? _g : "(never)",
|
|
3233
|
+
Created: config2.createdAt.slice(0, 10)
|
|
3234
|
+
});
|
|
3235
|
+
})
|
|
3236
|
+
);
|
|
3237
|
+
configs.command("delete <id>").description("Delete an MCP configuration").option("--org <slug>", "Organization context").option("--personal", "Retired \u2014 context is organization-only").option("-y, --yes", "Skip confirmation prompt").action(
|
|
3238
|
+
(id, opts) => __async(null, null, function* () {
|
|
3239
|
+
const ctx = resolveContext(opts.personal, opts.org, false);
|
|
3240
|
+
const client = getClient();
|
|
3241
|
+
if (!opts.yes) {
|
|
3242
|
+
process.stderr.write(
|
|
3243
|
+
`Delete MCP config ${id}? This cannot be undone. Pass --yes to confirm.
|
|
3244
|
+
`
|
|
3245
|
+
);
|
|
3246
|
+
process.exit(2);
|
|
3247
|
+
}
|
|
3248
|
+
try {
|
|
3249
|
+
yield client.mcp.deleteConfig(id, {
|
|
3250
|
+
org: (ctx == null ? void 0 : ctx.type) === "org" ? ctx.slug : void 0
|
|
3251
|
+
});
|
|
3252
|
+
} catch (err) {
|
|
3253
|
+
printError(err.message, 1);
|
|
3254
|
+
}
|
|
3255
|
+
process.stdout.write(`MCP config ${id} deleted.
|
|
3256
|
+
`);
|
|
3257
|
+
})
|
|
3258
|
+
);
|
|
3259
|
+
mcp.addCommand(configs);
|
|
3260
|
+
mcp.command("serve").description(
|
|
3261
|
+
"Run a stdio MCP server exposing this workspace's whole integration catalog. Point Claude Desktop, Cursor, or any MCP client at it."
|
|
3262
|
+
).action(() => __async(null, null, function* () {
|
|
3263
|
+
const { runStdio } = yield import("@fabricorg/sdk-mcp/stdio");
|
|
3264
|
+
yield runStdio();
|
|
3265
|
+
}));
|
|
3266
|
+
return mcp;
|
|
3267
|
+
}
|
|
3268
|
+
|
|
3269
|
+
// src/commands/orgs/index.ts
|
|
3270
|
+
import { Command as Command13 } from "commander";
|
|
3271
|
+
function buildOrgsCommand() {
|
|
3272
|
+
const orgs = new Command13("orgs").description("Manage organizations");
|
|
3273
|
+
orgs.command("list").description("List organizations you belong to").option(
|
|
3274
|
+
"--format <format>",
|
|
3275
|
+
"Output format: table|json|yaml|csv",
|
|
3276
|
+
"table"
|
|
3277
|
+
).option("-q, --quiet", "Print slugs only").action((opts) => __async(null, null, function* () {
|
|
3278
|
+
const client = getClient();
|
|
3279
|
+
let list;
|
|
3280
|
+
try {
|
|
3281
|
+
list = yield client.orgs.list();
|
|
3282
|
+
} catch (err) {
|
|
3283
|
+
printError(err.message, 1);
|
|
3284
|
+
}
|
|
3285
|
+
if (opts.quiet) {
|
|
1434
3286
|
for (const org of list) {
|
|
1435
3287
|
process.stdout.write(`${org.slug}
|
|
1436
3288
|
`);
|
|
@@ -1474,9 +3326,9 @@ function buildOrgsCommand() {
|
|
|
1474
3326
|
}
|
|
1475
3327
|
|
|
1476
3328
|
// src/commands/projects/index.ts
|
|
1477
|
-
import { Command as
|
|
3329
|
+
import { Command as Command14 } from "commander";
|
|
1478
3330
|
function buildProjectsCommand() {
|
|
1479
|
-
const projects = new
|
|
3331
|
+
const projects = new Command14("projects").description("Manage projects");
|
|
1480
3332
|
projects.command("list").description("List projects").option("--org <slug>", "Organization context").option("--personal", "Retired \u2014 context is organization-only").option(
|
|
1481
3333
|
"--status <status>",
|
|
1482
3334
|
"Filter by status: ACTIVE|DRAFT|COMPLETED|ARCHIVED"
|
|
@@ -1611,9 +3463,9 @@ function buildProjectsCommand() {
|
|
|
1611
3463
|
}
|
|
1612
3464
|
|
|
1613
3465
|
// src/commands/prompts/index.ts
|
|
1614
|
-
import { Command as
|
|
3466
|
+
import { Command as Command15 } from "commander";
|
|
1615
3467
|
function buildPromptsCommand() {
|
|
1616
|
-
const prompts = new
|
|
3468
|
+
const prompts = new Command15("prompts").description(
|
|
1617
3469
|
"Manage prompt templates"
|
|
1618
3470
|
);
|
|
1619
3471
|
prompts.command("list").description("List prompt templates").option("--org <slug>", "Organization context").option("--personal", "Retired \u2014 context is organization-only").option("--search <query>", "Search by name").option("--category <cat>", "Filter by category").option("--limit <n>", "Max results", "50").option(
|
|
@@ -1766,12 +3618,12 @@ ${content}
|
|
|
1766
3618
|
}
|
|
1767
3619
|
|
|
1768
3620
|
// src/commands/reports/index.ts
|
|
1769
|
-
import { Command as
|
|
3621
|
+
import { Command as Command16 } from "commander";
|
|
1770
3622
|
function buildReportsCommand() {
|
|
1771
|
-
const reports = new
|
|
3623
|
+
const reports = new Command16("reports").description(
|
|
1772
3624
|
"Manage report templates and executions"
|
|
1773
3625
|
);
|
|
1774
|
-
const templates = new
|
|
3626
|
+
const templates = new Command16("templates").description(
|
|
1775
3627
|
"Manage report templates"
|
|
1776
3628
|
);
|
|
1777
3629
|
templates.command("list", { isDefault: true }).description("List report templates").option("--org <slug>", "Organization context").option("--personal", "Retired \u2014 context is organization-only").option("--search <query>", "Search by name").option("--category <cat>", "Filter by category").option("--limit <n>", "Max results (default 20)", "20").option(
|
|
@@ -1845,7 +3697,7 @@ function buildReportsCommand() {
|
|
|
1845
3697
|
})
|
|
1846
3698
|
);
|
|
1847
3699
|
reports.addCommand(templates);
|
|
1848
|
-
const executions = new
|
|
3700
|
+
const executions = new Command16("executions").description(
|
|
1849
3701
|
"View report executions"
|
|
1850
3702
|
);
|
|
1851
3703
|
executions.command("list", { isDefault: true }).description("List report executions").option("--org <slug>", "Organization context").option("--personal", "Retired \u2014 context is organization-only").option("--limit <n>", "Max results (default 20)", "20").option(
|
|
@@ -1889,9 +3741,9 @@ function buildReportsCommand() {
|
|
|
1889
3741
|
}
|
|
1890
3742
|
|
|
1891
3743
|
// src/commands/skills/index.ts
|
|
1892
|
-
import { Command as
|
|
3744
|
+
import { Command as Command17 } from "commander";
|
|
1893
3745
|
function buildSkillsCommand() {
|
|
1894
|
-
const skills = new
|
|
3746
|
+
const skills = new Command17("skills").description(
|
|
1895
3747
|
"Browse skill definitions"
|
|
1896
3748
|
);
|
|
1897
3749
|
skills.command("list").description("List skills").option("--org <slug>", "Organization context").option("--personal", "Retired \u2014 context is organization-only").option("--search <query>", "Search by name").option("--category <cat>", "Filter by category").option("--limit <n>", "Max results", "50").option(
|
|
@@ -1958,9 +3810,9 @@ function buildSkillsCommand() {
|
|
|
1958
3810
|
}
|
|
1959
3811
|
|
|
1960
3812
|
// src/commands/workflows/index.ts
|
|
1961
|
-
import { Command as
|
|
3813
|
+
import { Command as Command18 } from "commander";
|
|
1962
3814
|
function buildWorkflowsCommand() {
|
|
1963
|
-
const workflows = new
|
|
3815
|
+
const workflows = new Command18("workflows").description("Manage workflows");
|
|
1964
3816
|
workflows.command("list").description("List workflows").option("--org <slug>", "Organization context").option("--personal", "Retired \u2014 context is organization-only").option("--status <status>", "Filter: DRAFT|ACTIVE|PAUSED|ARCHIVED").option("--search <query>", "Search by name").option("--limit <n>", "Max results", "20").option(
|
|
1965
3817
|
"--format <format>",
|
|
1966
3818
|
"Output format: table|json|yaml|csv",
|
|
@@ -2088,9 +3940,9 @@ function buildWorkflowsCommand() {
|
|
|
2088
3940
|
}
|
|
2089
3941
|
|
|
2090
3942
|
// src/commands/workspaces/index.ts
|
|
2091
|
-
import { Command as
|
|
3943
|
+
import { Command as Command19 } from "commander";
|
|
2092
3944
|
function buildWorkspacesCommand() {
|
|
2093
|
-
const workspaces = new
|
|
3945
|
+
const workspaces = new Command19("workspaces").description(
|
|
2094
3946
|
"Manage workspaces"
|
|
2095
3947
|
);
|
|
2096
3948
|
workspaces.command("list").description("List workspaces").option("--org <slug>", "Organization context").option("--personal", "Retired \u2014 context is organization-only").option("--search <query>", "Search by name").option("--status <status>", "Filter: ACTIVE|ARCHIVED").option("--limit <n>", "Max results (default 20)", "20").option(
|
|
@@ -2166,14 +4018,14 @@ function buildWorkspacesCommand() {
|
|
|
2166
4018
|
}
|
|
2167
4019
|
|
|
2168
4020
|
// src/bin/fabric.ts
|
|
2169
|
-
var program = new
|
|
4021
|
+
var program = new Command20();
|
|
2170
4022
|
var _a;
|
|
2171
|
-
program.name("fabric").description("Fabric AI platform CLI").version(
|
|
4023
|
+
program.name("fabric").description("Fabric AI platform CLI").version(package_default.version).option(
|
|
2172
4024
|
"--format <format>",
|
|
2173
4025
|
"Output format: table|json|yaml|csv",
|
|
2174
4026
|
(_a = process.env.FABRIC_FORMAT) != null ? _a : "table"
|
|
2175
4027
|
);
|
|
2176
|
-
var auth = new
|
|
4028
|
+
var auth = new Command20("auth").description(
|
|
2177
4029
|
"Authenticate and manage API keys"
|
|
2178
4030
|
);
|
|
2179
4031
|
auth.addCommand(buildLoginCommand());
|
|
@@ -2189,13 +4041,14 @@ program.addCommand(buildWorkflowsCommand());
|
|
|
2189
4041
|
program.addCommand(buildPromptsCommand());
|
|
2190
4042
|
program.addCommand(buildAgentsCommand());
|
|
2191
4043
|
program.addCommand(buildSkillsCommand());
|
|
4044
|
+
program.addCommand(buildInstructionsCommand());
|
|
2192
4045
|
program.addCommand(buildMcpCommand());
|
|
2193
4046
|
program.addCommand(buildFramesCommand());
|
|
2194
4047
|
program.addCommand(buildWorkspacesCommand());
|
|
2195
4048
|
program.addCommand(buildChatsCommand());
|
|
2196
4049
|
program.addCommand(buildReportsCommand());
|
|
2197
4050
|
program.addCommand(buildCompletionCommand());
|
|
2198
|
-
var config = new
|
|
4051
|
+
var config = new Command20("config").description("Show CLI configuration");
|
|
2199
4052
|
config.command("path").description("Print the path to the config file").action(() => {
|
|
2200
4053
|
process.stdout.write(`${getConfigPath()}
|
|
2201
4054
|
`);
|