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