@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 CHANGED
@@ -52,7 +52,7 @@ var __async = (__this, __arguments, generator) => {
52
52
  };
53
53
 
54
54
  // src/bin/fabric.ts
55
- import { Command as Command19 } from "commander";
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({ apiKey, baseUrl: getBaseUrl() });
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
- function refusePersonal(source) {
318
- return printError(
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
- refusePersonal("--personal");
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
- if (stored.type === "personal") {
335
- refusePersonal(
336
- process.env.FABRIC_PERSONAL === "1" ? "FABRIC_PERSONAL=1" : "Your stored default context"
337
- );
338
- }
339
- if (stored.type === "org") {
340
- return { type: "org", slug: stored.slug };
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
- printError(
345
- "Context is required. Use --org <slug>, or set a default with:\n fabric ctx use org <slug>",
346
- 2
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 void 0;
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/mcp/index.ts
1281
+ // src/commands/instructions/index.ts
1282
+ import path4 from "path";
1273
1283
  import { Command as Command11 } from "commander";
1274
- function buildMcpCommand() {
1275
- const mcp = new Command11("mcp").description(
1276
- "Manage MCP servers and configurations"
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
- mcp.command("servers").description("List accessible MCP servers (system + custom)").option("--org <slug>", "Organization context").option("--personal", "Retired \u2014 context is organization-only").option(
1279
- "--format <format>",
1280
- "Output format: table|json|yaml|csv",
1281
- "table"
1282
- ).option("-q, --quiet", "Print server keys only").action(
1283
- (opts) => __async(null, null, function* () {
1284
- const ctx = resolveContext(opts.personal, opts.org, false);
1285
- const client = getClient();
1286
- let list;
1287
- try {
1288
- list = yield client.mcp.listServers({
1289
- org: (ctx == null ? void 0 : ctx.type) === "org" ? ctx.slug : void 0
1290
- });
1291
- } catch (err) {
1292
- printError(err.message, 1);
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
- if (opts.quiet) {
1295
- for (const s of list) {
1296
- process.stdout.write(`${s.key}
1297
- `);
1298
- }
1299
- return;
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
- printOutput(list, {
1302
- format: opts.format,
1303
- columns: [
1304
- "key",
1305
- "name",
1306
- "transport",
1307
- "category",
1308
- "isSystemProvided"
1309
- ]
1310
- });
1311
- })
1312
- );
1313
- const configs = new Command11("configs").description(
1314
- "Manage MCP configurations"
1315
- );
1316
- configs.command("list", { isDefault: true }).description("List your MCP configurations").option("--org <slug>", "Organization context").option("--personal", "Retired \u2014 context is organization-only").option(
1317
- "--format <format>",
1318
- "Output format: table|json|yaml|csv",
1319
- "table"
1320
- ).option("-q, --quiet", "Print IDs only").action(
1321
- (opts) => __async(null, null, function* () {
1322
- const ctx = resolveContext(opts.personal, opts.org, false);
1323
- const client = getClient();
1324
- let list;
1325
- try {
1326
- list = yield client.mcp.listConfigs({
1327
- org: (ctx == null ? void 0 : ctx.type) === "org" ? ctx.slug : void 0
1328
- });
1329
- } catch (err) {
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 (opts.quiet) {
1333
- for (const c of list) {
1334
- process.stdout.write(`${c.id}
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
- printOutput(list, {
1340
- format: opts.format,
1341
- columns: [
1342
- "id",
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
- const fmt = opts.format;
1366
- if (fmt === "json" || fmt === "yaml") {
1367
- printOutput(config2, { format: fmt });
1368
- return;
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
- printRecord({
1371
- ID: config2.id,
1372
- Server: (_a2 = config2.serverName) != null ? _a2 : config2.mcpServerId,
1373
- "Server Key": (_b = config2.serverKey) != null ? _b : "(none)",
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
- try {
1397
- yield client.mcp.deleteConfig(id, {
1398
- org: (ctx == null ? void 0 : ctx.type) === "org" ? ctx.slug : void 0
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
- process.stdout.write(`MCP config ${id} deleted.
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
- // src/commands/orgs/index.ts
1418
- import { Command as Command12 } from "commander";
1419
- function buildOrgsCommand() {
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
- if (opts.quiet) {
1434
- for (const org of list) {
1435
- process.stdout.write(`${org.slug}
1436
- `);
1437
- }
1438
- return;
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
- printOutput(list, {
1441
- format: opts.format,
1442
- columns: ["name", "slug", "role", "joinedAt"]
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 Command13 } from "commander";
3239
+ import { Command as Command14 } from "commander";
1478
3240
  function buildProjectsCommand() {
1479
- const projects = new Command13("projects").description("Manage projects");
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 Command14 } from "commander";
3376
+ import { Command as Command15 } from "commander";
1615
3377
  function buildPromptsCommand() {
1616
- const prompts = new Command14("prompts").description(
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 Command15 } from "commander";
3531
+ import { Command as Command16 } from "commander";
1770
3532
  function buildReportsCommand() {
1771
- const reports = new Command15("reports").description(
3533
+ const reports = new Command16("reports").description(
1772
3534
  "Manage report templates and executions"
1773
3535
  );
1774
- const templates = new Command15("templates").description(
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 Command15("executions").description(
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 Command16 } from "commander";
3654
+ import { Command as Command17 } from "commander";
1893
3655
  function buildSkillsCommand() {
1894
- const skills = new Command16("skills").description(
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 Command17 } from "commander";
3723
+ import { Command as Command18 } from "commander";
1962
3724
  function buildWorkflowsCommand() {
1963
- const workflows = new Command17("workflows").description("Manage workflows");
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 Command18 } from "commander";
3853
+ import { Command as Command19 } from "commander";
2092
3854
  function buildWorkspacesCommand() {
2093
- const workspaces = new Command18("workspaces").description(
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 Command19();
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 Command19("auth").description(
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 Command19("config").description("Show CLI configuration");
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
  `);