@fabricorg/cli 0.2.1 → 0.3.1

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