@fabricorg/cli 0.3.5 → 0.3.7

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,12 +52,12 @@ var __async = (__this, __arguments, generator) => {
52
52
  };
53
53
 
54
54
  // src/bin/fabric.ts
55
- import { Command as Command20 } from "commander";
55
+ import { Command as Command21 } from "commander";
56
56
 
57
57
  // package.json
58
58
  var package_default = {
59
59
  name: "@fabricorg/cli",
60
- version: "0.3.5",
60
+ version: "0.3.7",
61
61
  description: "CLI for the Fabric AI platform",
62
62
  keywords: [
63
63
  "fabric",
@@ -96,7 +96,8 @@ var package_default = {
96
96
  "cli-table3": "^0.6.5",
97
97
  commander: "^13.1.0",
98
98
  conf: "^13.1.0",
99
- fflate: "^0.8.3"
99
+ fflate: "^0.8.3",
100
+ ignore: "7.0.9"
100
101
  },
101
102
  devDependencies: {
102
103
  "@repo/tsconfig": "workspace:*",
@@ -1051,806 +1052,2427 @@ function buildCompletionCommand() {
1051
1052
  return completion;
1052
1053
  }
1053
1054
 
1054
- // src/commands/ctx/index.ts
1055
+ // src/commands/context/index.ts
1056
+ import { lstat as lstat3 } from "fs/promises";
1057
+ import path5 from "path";
1055
1058
  import { Command as Command8 } from "commander";
1056
- function buildCtxCommand() {
1057
- const ctx = new Command8("ctx").description(
1058
- "Manage the organization commands run in"
1059
- );
1060
- ctx.command("current").description("Show the active default context").action(() => {
1061
- const stored = getDefaultContext();
1062
- if (!stored) {
1063
- printWarning(
1064
- "No default context set. Use --org <slug> on each command, or run:\n fabric ctx use org <slug>"
1065
- );
1066
- return;
1067
- }
1068
- if (stored.type === "personal") {
1069
- printWarning(
1070
- "Context: personal \u2014 retired, and no command will accept it.\nReplace it with:\n fabric ctx use org <slug>"
1071
- );
1072
- return;
1059
+
1060
+ // src/lib/command-boundary.ts
1061
+ function outputFormatFor(command) {
1062
+ const resolved = command.optsWithGlobals();
1063
+ return resolved.format === "json" ? "json" : "text";
1064
+ }
1065
+ var CliFailure = class extends Error {
1066
+ constructor(message, exitCode) {
1067
+ super(message);
1068
+ this.exitCode = exitCode;
1069
+ this.name = "CliFailure";
1070
+ }
1071
+ };
1072
+ function withDeadline(totalMs, body) {
1073
+ return __async(this, null, function* () {
1074
+ const controller = new AbortController();
1075
+ let timer;
1076
+ const deadline = new Promise((_resolve, reject) => {
1077
+ timer = setTimeout(() => {
1078
+ controller.abort();
1079
+ reject(
1080
+ new Error(
1081
+ `gave up after ${totalMs}ms so the session is not held up`
1082
+ )
1083
+ );
1084
+ }, totalMs);
1085
+ });
1086
+ try {
1087
+ yield Promise.race([body(controller.signal), deadline]);
1088
+ } finally {
1089
+ if (timer) {
1090
+ clearTimeout(timer);
1091
+ }
1073
1092
  }
1074
- process.stdout.write(`Context: org:${stored.slug}
1075
- `);
1076
- });
1077
- const use = new Command8("use").description(
1078
- "Set the default organization for commands"
1079
- );
1080
- use.command("personal").description("Retired \u2014 context is organization-only").action(() => {
1081
- printError(
1082
- "Personal context no longer exists \u2014 every command runs inside an organization.\nSet one with:\n fabric ctx use org <slug>",
1083
- 2
1084
- );
1085
- });
1086
- use.command("org <slug>").description("Use an org context by default").action((slug) => {
1087
- saveDefaultContext({ type: "org", slug });
1088
- printSuccess(`Default context set to: org:${slug}`);
1089
1093
  });
1090
- ctx.addCommand(use);
1091
- return ctx;
1094
+ }
1095
+ function describeError(error) {
1096
+ if (error instanceof Error) {
1097
+ return error.message.replace(/\s*\n\s*/g, " ");
1098
+ }
1099
+ return String(error);
1100
+ }
1101
+ function asCliFailure(error) {
1102
+ if (error instanceof CliFailure) {
1103
+ return error;
1104
+ }
1105
+ const status = error.status;
1106
+ const message = error instanceof Error ? error.message : String(error);
1107
+ switch (status) {
1108
+ case 401:
1109
+ return new CliFailure(message, 3);
1110
+ case 403:
1111
+ return new CliFailure(message, 5);
1112
+ case 404:
1113
+ return new CliFailure(message, 4);
1114
+ case 429:
1115
+ return new CliFailure(message, 6);
1116
+ case 400:
1117
+ case 422:
1118
+ return new CliFailure(message, 7);
1119
+ default:
1120
+ return new CliFailure(message, 1);
1121
+ }
1092
1122
  }
1093
1123
 
1094
- // src/commands/features/index.ts
1095
- import { Command as Command9 } from "commander";
1096
- function buildFeaturesCommand() {
1097
- const features = new Command9("features").description(
1098
- "Manage project features"
1099
- );
1100
- features.command("list").description("List features in a project").requiredOption("-p, --project <id>", "Project ID").option("--org <slug>", "Organization context").option("--personal", "Retired \u2014 context is organization-only").option(
1101
- "--priority <priority>",
1102
- "Filter: P0_CRITICAL|P1_HIGH|P2_MEDIUM|P3_LOW"
1103
- ).option("--search <query>", "Search by title").option("--limit <n>", "Max results (default 50)", "50").option(
1104
- "--format <format>",
1105
- "Output format: table|json|yaml|csv",
1106
- "table"
1107
- ).option("-q, --quiet", "Print IDs only").action(
1108
- (opts) => __async(null, null, function* () {
1109
- const ctx = resolveContext(opts.personal, opts.org, false);
1110
- const client = getClient();
1111
- let list;
1112
- try {
1113
- list = yield client.projects.listFeatures(opts.project, {
1114
- org: (ctx == null ? void 0 : ctx.type) === "org" ? ctx.slug : void 0,
1115
- priority: opts.priority,
1116
- search: opts.search,
1117
- limit: Number(opts.limit)
1118
- });
1119
- } catch (err) {
1120
- printError(err.message, 1);
1121
- }
1122
- if (opts.quiet) {
1123
- for (const f of list) {
1124
- process.stdout.write(`${f.id}
1125
- `);
1126
- }
1127
- return;
1128
- }
1129
- printOutput(list, {
1130
- format: opts.format,
1131
- columns: ["identifier", "title", "priority", "createdAt"]
1132
- });
1133
- })
1124
+ // src/lib/context-sync/lock.ts
1125
+ import path2 from "path";
1126
+
1127
+ // src/lib/instructions/safe-write.ts
1128
+ import { createHash, randomBytes } from "crypto";
1129
+ import { constants } from "fs";
1130
+ import {
1131
+ lstat,
1132
+ mkdir,
1133
+ open,
1134
+ readFile,
1135
+ realpath,
1136
+ rename,
1137
+ unlink
1138
+ } from "fs/promises";
1139
+ import path from "path";
1140
+
1141
+ // src/lib/instructions/paths.ts
1142
+ var DRIVE_LETTER = /^[A-Za-z]:/;
1143
+ var RESERVED_DEVICE_NAME = /^(?:CON|PRN|AUX|NUL|COM[1-9]|LPT[1-9])(?:\.|$)/i;
1144
+ var FORBIDDEN_NAME_CHARACTER = /[<>:"|?*]/;
1145
+ var RESERVED_ROOTS = [".git", ".fabric"];
1146
+ var RESERVED_EXACT_PATHS = [
1147
+ ".claude/settings.local.json",
1148
+ ".codex/hooks.json"
1149
+ ];
1150
+ function hasControlCharacter(input) {
1151
+ for (let index = 0; index < input.length; index++) {
1152
+ const code = input.charCodeAt(index);
1153
+ if (code <= 31 || code === 127) {
1154
+ return true;
1155
+ }
1156
+ }
1157
+ return false;
1158
+ }
1159
+ function checkRelativePath(input) {
1160
+ if (input.length === 0) {
1161
+ return { ok: false, reason: "empty", detail: input };
1162
+ }
1163
+ if (hasControlCharacter(input)) {
1164
+ return { ok: false, reason: "control_char", detail: input };
1165
+ }
1166
+ if (input.includes("\\")) {
1167
+ return { ok: false, reason: "backslash", detail: input };
1168
+ }
1169
+ if (input.startsWith("/") || DRIVE_LETTER.test(input)) {
1170
+ return { ok: false, reason: "absolute", detail: input };
1171
+ }
1172
+ if (input.endsWith("/")) {
1173
+ return { ok: false, reason: "trailing_separator", detail: input };
1174
+ }
1175
+ const segments = input.split("/");
1176
+ if (segments.some((s) => s.length === 0)) {
1177
+ return { ok: false, reason: "empty", detail: input };
1178
+ }
1179
+ if (segments.some((s) => s === "." || s === "..")) {
1180
+ return { ok: false, reason: "traversal", detail: input };
1181
+ }
1182
+ for (const segment of segments) {
1183
+ if (/[. ]$/.test(segment)) {
1184
+ return {
1185
+ ok: false,
1186
+ reason: "trailing_dot_or_space",
1187
+ detail: input
1188
+ };
1189
+ }
1190
+ if (RESERVED_DEVICE_NAME.test(segment)) {
1191
+ return {
1192
+ ok: false,
1193
+ reason: "reserved_device_name",
1194
+ detail: input
1195
+ };
1196
+ }
1197
+ if (FORBIDDEN_NAME_CHARACTER.test(segment)) {
1198
+ return {
1199
+ ok: false,
1200
+ reason: "forbidden_character",
1201
+ detail: input
1202
+ };
1203
+ }
1204
+ }
1205
+ return { ok: true, path: input };
1206
+ }
1207
+ function isReservedPath(input) {
1208
+ var _a2;
1209
+ const first = (_a2 = input.split("/")[0]) == null ? void 0 : _a2.toLowerCase();
1210
+ if (RESERVED_ROOTS.some((root) => first === root)) {
1211
+ return true;
1212
+ }
1213
+ const key = collisionKey(input);
1214
+ return RESERVED_EXACT_PATHS.some(
1215
+ (reserved) => collisionKey(reserved) === key
1134
1216
  );
1135
- features.command("show <id>").description("Show feature details").requiredOption("-p, --project <projectId>", "Project ID").option("--org <slug>", "Organization context").option("--personal", "Retired \u2014 context is organization-only").option("--format <format>", "Output format: table|json|yaml", "table").action(
1136
- (id, opts) => __async(null, null, function* () {
1137
- var _a2, _b, _c;
1138
- const ctx = resolveContext(opts.personal, opts.org, false);
1139
- const client = getClient();
1140
- let feature;
1141
- try {
1142
- feature = yield client.projects.getFeature(
1143
- opts.project,
1144
- id,
1145
- {
1146
- org: (ctx == null ? void 0 : ctx.type) === "org" ? ctx.slug : void 0
1147
- }
1148
- );
1149
- } catch (err) {
1150
- printError(err.message, 1);
1151
- }
1152
- const fmt = opts.format;
1153
- if (fmt === "json" || fmt === "yaml") {
1154
- printOutput(feature, { format: fmt });
1155
- return;
1156
- }
1157
- printRecord({
1158
- ID: feature.id,
1159
- Identifier: feature.identifier,
1160
- Title: feature.title,
1161
- Priority: feature.priority,
1162
- Description: (_a2 = feature.description) != null ? _a2 : "(none)",
1163
- "Acceptance Criteria": (_b = feature.acceptanceCriteria) != null ? _b : "(none)",
1164
- Project: feature.projectId,
1165
- Assignee: (_c = feature.assigneeId) != null ? _c : "(none)",
1166
- Created: feature.createdAt.slice(0, 10)
1167
- });
1168
- })
1217
+ }
1218
+ function collisionKey(input) {
1219
+ return input.normalize("NFC").toLowerCase();
1220
+ }
1221
+ function findCollision(paths) {
1222
+ const seen = /* @__PURE__ */ new Map();
1223
+ for (const candidate of paths) {
1224
+ const key = collisionKey(candidate);
1225
+ const previous = seen.get(key);
1226
+ if (previous !== void 0) {
1227
+ return { first: previous, second: candidate };
1228
+ }
1229
+ seen.set(key, candidate);
1230
+ }
1231
+ return null;
1232
+ }
1233
+ function describeRejection(rejection) {
1234
+ switch (rejection.reason) {
1235
+ case "empty":
1236
+ return `empty or malformed path (${JSON.stringify(rejection.detail)})`;
1237
+ case "absolute":
1238
+ return `absolute path refused: ${rejection.detail}`;
1239
+ case "traversal":
1240
+ return `path traversal refused: ${rejection.detail}`;
1241
+ case "backslash":
1242
+ return `backslash in path refused: ${rejection.detail}`;
1243
+ case "control_char":
1244
+ return `control character in path refused: ${JSON.stringify(rejection.detail)}`;
1245
+ case "trailing_separator":
1246
+ return `path names a directory, not a file: ${rejection.detail}`;
1247
+ case "trailing_dot_or_space":
1248
+ return `path segment ending in a dot or space refused: ${rejection.detail}`;
1249
+ case "reserved_device_name":
1250
+ return `reserved device name refused: ${rejection.detail}`;
1251
+ case "forbidden_character":
1252
+ return `path contains a character Windows will not put in a filename (< > : " | ? *): ${rejection.detail}`;
1253
+ case "reserved_path":
1254
+ return `this tool never writes or deletes ${RESERVED_ROOTS.join(", ")} or ${RESERVED_EXACT_PATHS.join(", ")}: ${rejection.detail}`;
1255
+ case "not_a_regular_file":
1256
+ return `path is not a regular file: ${rejection.detail}`;
1257
+ case "escapes_destination":
1258
+ return `path resolves outside the destination: ${rejection.detail}`;
1259
+ case "symlink":
1260
+ return `refusing to follow a symlink: ${rejection.detail}`;
1261
+ case "not_a_directory":
1262
+ return `a path component is not a directory: ${rejection.detail}`;
1263
+ case "collision":
1264
+ return `two paths name the same file on a case- or Unicode-insensitive filesystem: ${rejection.detail}`;
1265
+ }
1266
+ }
1267
+
1268
+ // src/lib/instructions/safe-write.ts
1269
+ var ALLOWED_MODES = /* @__PURE__ */ new Set([420, 493]);
1270
+ function isAllowedMode(mode) {
1271
+ if (mode === null || mode === void 0) {
1272
+ return true;
1273
+ }
1274
+ if (!Number.isInteger(mode) || mode < 0) {
1275
+ return false;
1276
+ }
1277
+ const type = mode & 61440;
1278
+ if (type !== 0 && type !== 32768) {
1279
+ return false;
1280
+ }
1281
+ return ALLOWED_MODES.has(mode & 4095);
1282
+ }
1283
+ function permissionBits(mode) {
1284
+ if (mode === null || mode === void 0) {
1285
+ return null;
1286
+ }
1287
+ return mode & 4095;
1288
+ }
1289
+ function resolveDestinationRoot(destination) {
1290
+ return __async(this, null, function* () {
1291
+ yield mkdir(destination, { recursive: true });
1292
+ return realpath(destination);
1293
+ });
1294
+ }
1295
+ function assertSafeTarget(root, relativePath) {
1296
+ return __async(this, null, function* () {
1297
+ const target = path.resolve(root, relativePath);
1298
+ if (target === root || !target.startsWith(root + path.sep)) {
1299
+ return {
1300
+ ok: false,
1301
+ reason: "escapes_destination",
1302
+ detail: relativePath
1303
+ };
1304
+ }
1305
+ const segments = relativePath.split("/");
1306
+ let current = root;
1307
+ let deepestExisting = root;
1308
+ for (const [index, segment] of segments.entries()) {
1309
+ current = path.join(current, segment);
1310
+ const stats = yield lstatOrNull(current);
1311
+ if (stats === null) {
1312
+ break;
1313
+ }
1314
+ if (stats.isSymbolicLink()) {
1315
+ return { ok: false, reason: "symlink", detail: current };
1316
+ }
1317
+ if (index < segments.length - 1 && !stats.isDirectory()) {
1318
+ return { ok: false, reason: "not_a_directory", detail: current };
1319
+ }
1320
+ deepestExisting = current;
1321
+ }
1322
+ if (deepestExisting !== root) {
1323
+ const canonical = yield realpath(deepestExisting).catch(() => null);
1324
+ if (canonical !== null && canonical !== root && !canonical.startsWith(root + path.sep)) {
1325
+ return {
1326
+ ok: false,
1327
+ reason: "escapes_destination",
1328
+ detail: relativePath
1329
+ };
1330
+ }
1331
+ }
1332
+ return { ok: true, path: target };
1333
+ });
1334
+ }
1335
+ function assertWritableTarget(root, relativePath) {
1336
+ return __async(this, null, function* () {
1337
+ const check = yield assertSafeTarget(root, relativePath);
1338
+ if (!check.ok) {
1339
+ return check;
1340
+ }
1341
+ const stats = yield lstatOrNull(check.path);
1342
+ if (stats === null) {
1343
+ return check;
1344
+ }
1345
+ if (stats.isSymbolicLink()) {
1346
+ return { ok: false, reason: "symlink", detail: check.path };
1347
+ }
1348
+ if (!stats.isFile()) {
1349
+ return { ok: false, reason: "not_a_regular_file", detail: check.path };
1350
+ }
1351
+ return check;
1352
+ });
1353
+ }
1354
+ function readFileSafely(_0, _1) {
1355
+ return __async(this, arguments, function* (root, relativePath, options = {}) {
1356
+ const check = yield assertWritableTarget(root, relativePath);
1357
+ if (!check.ok) {
1358
+ throw new Error(`Refusing to sync: ${describeRejection(check)}.`);
1359
+ }
1360
+ let handle;
1361
+ try {
1362
+ handle = yield open(
1363
+ check.path,
1364
+ constants.O_RDONLY | constants.O_NOFOLLOW | constants.O_NONBLOCK
1365
+ );
1366
+ } catch (error) {
1367
+ if (error.code === "ENOENT") {
1368
+ return null;
1369
+ }
1370
+ throw error;
1371
+ }
1372
+ try {
1373
+ const opened = yield handle.stat();
1374
+ if (!opened.isFile()) {
1375
+ throw new Error(
1376
+ `Refusing to sync: ${relativePath} is not a regular file.`
1377
+ );
1378
+ }
1379
+ const afterOpen = yield assertWritableTarget(root, relativePath);
1380
+ if (!afterOpen.ok) {
1381
+ throw new Error(
1382
+ `Refusing to sync: ${describeRejection(afterOpen)}.`
1383
+ );
1384
+ }
1385
+ const current = yield lstatOrNull(afterOpen.path);
1386
+ if (current === null || !current.isFile() || current.dev !== opened.dev || current.ino !== opened.ino) {
1387
+ throw new Error(
1388
+ `Refusing to sync: ${relativePath} changed while it was being opened.`
1389
+ );
1390
+ }
1391
+ if (options.maxBytes === void 0) {
1392
+ return { bytes: yield handle.readFile(), mode: opened.mode };
1393
+ }
1394
+ if (!Number.isSafeInteger(options.maxBytes) || options.maxBytes < 0) {
1395
+ throw new Error(
1396
+ "A safe file-read limit must be a non-negative integer."
1397
+ );
1398
+ }
1399
+ if (opened.size > options.maxBytes) {
1400
+ throw new Error(
1401
+ `${relativePath} is too large to read safely (maximum ${options.maxBytes} bytes).`
1402
+ );
1403
+ }
1404
+ const buffer = Buffer.allocUnsafe(options.maxBytes + 1);
1405
+ let total = 0;
1406
+ while (total <= options.maxBytes) {
1407
+ const { bytesRead } = yield handle.read(
1408
+ buffer,
1409
+ total,
1410
+ options.maxBytes + 1 - total,
1411
+ total
1412
+ );
1413
+ if (bytesRead === 0) {
1414
+ break;
1415
+ }
1416
+ total += bytesRead;
1417
+ }
1418
+ if (total > options.maxBytes) {
1419
+ throw new Error(
1420
+ `${relativePath} is too large to read safely (maximum ${options.maxBytes} bytes).`
1421
+ );
1422
+ }
1423
+ return { bytes: buffer.subarray(0, total), mode: opened.mode };
1424
+ } finally {
1425
+ yield handle.close();
1426
+ }
1427
+ });
1428
+ }
1429
+ function resolveExistingRoot(destination) {
1430
+ return __async(this, null, function* () {
1431
+ return realpath(destination).catch(() => path.resolve(destination));
1432
+ });
1433
+ }
1434
+ function makeParents(root, relativePath) {
1435
+ return __async(this, null, function* () {
1436
+ const segments = relativePath.split("/").slice(0, -1);
1437
+ let current = root;
1438
+ for (const segment of segments) {
1439
+ current = path.join(current, segment);
1440
+ const stats = yield lstatOrNull(current);
1441
+ if (stats === null) {
1442
+ yield mkdir(current).catch((error) => __async(null, null, function* () {
1443
+ if (error.code !== "EEXIST") {
1444
+ throw error;
1445
+ }
1446
+ const raced = yield lstatOrNull(current);
1447
+ if (raced == null ? void 0 : raced.isSymbolicLink()) {
1448
+ throw new Error(
1449
+ `Refusing to sync: ${describeRejection({ ok: false, reason: "symlink", detail: current })}.`
1450
+ );
1451
+ }
1452
+ }));
1453
+ continue;
1454
+ }
1455
+ if (stats.isSymbolicLink()) {
1456
+ throw new Error(
1457
+ `Refusing to sync: ${describeRejection({ ok: false, reason: "symlink", detail: current })}.`
1458
+ );
1459
+ }
1460
+ if (!stats.isDirectory()) {
1461
+ throw new Error(
1462
+ `Refusing to sync: ${describeRejection({ ok: false, reason: "not_a_directory", detail: current })}.`
1463
+ );
1464
+ }
1465
+ }
1466
+ });
1467
+ }
1468
+ function writeFileSafely(input) {
1469
+ return __async(this, null, function* () {
1470
+ var _a2;
1471
+ const check = yield assertSafeTarget(input.root, input.relativePath);
1472
+ if (!check.ok) {
1473
+ throw new Error(`Refusing to sync: ${describeRejection(check)}.`);
1474
+ }
1475
+ if (!isAllowedMode(input.mode)) {
1476
+ throw new Error(
1477
+ `Refusing to sync: ${input.relativePath} asks for file mode ${((_a2 = input.mode) != null ? _a2 : 0).toString(8)}, and only 644 and 755 are accepted.`
1478
+ );
1479
+ }
1480
+ yield makeParents(input.root, input.relativePath);
1481
+ const target = check.path;
1482
+ const directory = path.dirname(target);
1483
+ const directoryBefore = yield lstatOrNull(directory);
1484
+ const temporary = path.join(
1485
+ directory,
1486
+ `.${path.basename(target)}.${randomBytes(8).toString("hex")}.tmp`
1487
+ );
1488
+ let handle = null;
1489
+ try {
1490
+ handle = yield open(temporary, "wx", 384);
1491
+ yield handle.writeFile(input.bytes);
1492
+ const bits = permissionBits(input.mode);
1493
+ if (bits !== null && process.platform !== "win32") {
1494
+ yield handle.chmod(bits);
1495
+ }
1496
+ yield handle.close();
1497
+ handle = null;
1498
+ const parentStats = yield lstatOrNull(directory);
1499
+ if (parentStats === null || parentStats.isSymbolicLink()) {
1500
+ throw new Error(
1501
+ `Refusing to sync: ${describeRejection({ ok: false, reason: "symlink", detail: directory })}.`
1502
+ );
1503
+ }
1504
+ const targetStats = yield lstatOrNull(target);
1505
+ if (targetStats == null ? void 0 : targetStats.isSymbolicLink()) {
1506
+ throw new Error(
1507
+ `Refusing to sync: ${describeRejection({ ok: false, reason: "symlink", detail: target })}.`
1508
+ );
1509
+ }
1510
+ yield rename(temporary, target);
1511
+ } catch (error) {
1512
+ yield handle == null ? void 0 : handle.close().catch(() => {
1513
+ });
1514
+ if (yield isSameDirectory(directory, directoryBefore)) {
1515
+ yield unlink(temporary).catch(() => {
1516
+ });
1517
+ }
1518
+ throw error;
1519
+ }
1520
+ });
1521
+ }
1522
+ function isSameDirectory(directory, before) {
1523
+ return __async(this, null, function* () {
1524
+ if (before === null) {
1525
+ return false;
1526
+ }
1527
+ const now = yield lstatOrNull(directory);
1528
+ return now !== null && now.dev === before.dev && now.ino === before.ino;
1529
+ });
1530
+ }
1531
+ function deleteFileSafely(root, relativePath, expectedSha256) {
1532
+ return __async(this, null, function* () {
1533
+ const check = yield assertSafeTarget(root, relativePath);
1534
+ if (!check.ok) {
1535
+ throw new Error(`Refusing to sync: ${describeRejection(check)}.`);
1536
+ }
1537
+ const stats = yield lstatOrNull(check.path);
1538
+ if (stats === null) {
1539
+ return "missing";
1540
+ }
1541
+ if (stats.isSymbolicLink()) {
1542
+ throw new Error(
1543
+ `Refusing to sync: ${describeRejection({ ok: false, reason: "symlink", detail: check.path })}.`
1544
+ );
1545
+ }
1546
+ if (!stats.isFile()) {
1547
+ throw new Error(
1548
+ `Refusing to sync: ${describeRejection({ ok: false, reason: "not_a_regular_file", detail: check.path })}.`
1549
+ );
1550
+ }
1551
+ let actual;
1552
+ try {
1553
+ actual = createHash("sha256").update(yield readFile(check.path)).digest("hex");
1554
+ } catch (error) {
1555
+ if (error.code === "ENOENT") {
1556
+ return "missing";
1557
+ }
1558
+ throw error;
1559
+ }
1560
+ if (actual !== expectedSha256) {
1561
+ return "modified";
1562
+ }
1563
+ try {
1564
+ yield unlink(check.path);
1565
+ return "deleted";
1566
+ } catch (error) {
1567
+ if (error.code === "ENOENT") {
1568
+ return "missing";
1569
+ }
1570
+ throw error;
1571
+ }
1572
+ });
1573
+ }
1574
+ function lstatOrNull(target) {
1575
+ return __async(this, null, function* () {
1576
+ try {
1577
+ return yield lstat(target);
1578
+ } catch (error) {
1579
+ if (error.code === "ENOENT") {
1580
+ return null;
1581
+ }
1582
+ throw error;
1583
+ }
1584
+ });
1585
+ }
1586
+ function resolvesInside(root, candidate) {
1587
+ return __async(this, null, function* () {
1588
+ const canonicalRoot = yield realpath(root).catch(() => path.resolve(root));
1589
+ const canonicalCandidate = yield realpathDeepest(candidate);
1590
+ return canonicalCandidate === canonicalRoot || canonicalCandidate.startsWith(canonicalRoot + path.sep);
1591
+ });
1592
+ }
1593
+ function realpathDeepest(candidate) {
1594
+ return __async(this, null, function* () {
1595
+ const absolute = path.resolve(candidate);
1596
+ const tail = [];
1597
+ let current = absolute;
1598
+ for (; ; ) {
1599
+ const resolved = yield realpath(current).catch(() => null);
1600
+ if (resolved !== null) {
1601
+ return tail.length === 0 ? resolved : path.join(resolved, ...tail);
1602
+ }
1603
+ const parent = path.dirname(current);
1604
+ if (parent === current) {
1605
+ return absolute;
1606
+ }
1607
+ tail.unshift(path.basename(current));
1608
+ current = parent;
1609
+ }
1610
+ });
1611
+ }
1612
+
1613
+ // src/lib/context-sync/source-path.ts
1614
+ var MAX_CONTEXT_SOURCE_PATH_LENGTH = 512;
1615
+ var CONTROL_OR_FORMAT_CHARACTER = /[\u0000-\u001f\u007f-\u009f\p{Cf}]/u;
1616
+ var DRIVE_LETTER_PREFIX = /^[A-Za-z]:/;
1617
+ function hasControlOrFormatCharacter(value) {
1618
+ return CONTROL_OR_FORMAT_CHARACTER.test(value);
1619
+ }
1620
+ function normalizeContextSourcePath(input) {
1621
+ const slashed = input.normalize("NFC").replace(/\\/g, "/");
1622
+ if (hasControlOrFormatCharacter(slashed)) {
1623
+ return { ok: false, reason: "control-character" };
1624
+ }
1625
+ if (slashed.startsWith("/") || DRIVE_LETTER_PREFIX.test(slashed)) {
1626
+ return { ok: false, reason: "absolute" };
1627
+ }
1628
+ let path9 = slashed.replace(/\/{2,}/g, "/");
1629
+ while (path9.startsWith("./")) {
1630
+ path9 = path9.slice(2);
1631
+ }
1632
+ if (path9.length === 0) {
1633
+ return { ok: false, reason: "empty" };
1634
+ }
1635
+ if (path9.endsWith("/")) {
1636
+ return { ok: false, reason: "trailing-slash" };
1637
+ }
1638
+ if (path9.split("/").some((segment) => segment === "." || segment === "..")) {
1639
+ return { ok: false, reason: "dot-segment" };
1640
+ }
1641
+ if (path9.length > MAX_CONTEXT_SOURCE_PATH_LENGTH) {
1642
+ return { ok: false, reason: "too-long" };
1643
+ }
1644
+ return { ok: true, sourcePath: path9 };
1645
+ }
1646
+
1647
+ // src/lib/context-sync/lock.ts
1648
+ var CONTEXT_LOCK_DIRECTORY = ".fabric";
1649
+ var CONTEXT_LOCK_FILENAME = "context.lock";
1650
+ var CONTEXT_LOCK_VERSION = 1;
1651
+ var CONTEXT_LOCK_RELATIVE_PATH = `${CONTEXT_LOCK_DIRECTORY}/${CONTEXT_LOCK_FILENAME}`;
1652
+ var MAX_CONTEXT_LOCK_BYTES = 32 * 1024 * 1024;
1653
+ var SHA256_HEX = /^[0-9a-f]{64}$/;
1654
+ function contextLockPath(root) {
1655
+ return path2.join(root, CONTEXT_LOCK_DIRECTORY, CONTEXT_LOCK_FILENAME);
1656
+ }
1657
+ function readContextLock(root) {
1658
+ return __async(this, null, function* () {
1659
+ const file = contextLockPath(root);
1660
+ const read = yield readFileSafely(root, CONTEXT_LOCK_RELATIVE_PATH, {
1661
+ maxBytes: MAX_CONTEXT_LOCK_BYTES
1662
+ });
1663
+ if (read === null) {
1664
+ return null;
1665
+ }
1666
+ let parsed;
1667
+ try {
1668
+ parsed = JSON.parse(new TextDecoder().decode(read.bytes));
1669
+ } catch (e) {
1670
+ throw new Error(
1671
+ `${file} is unreadable: it is not valid JSON. Delete it to push every file again with no expected version.`
1672
+ );
1673
+ }
1674
+ const problem = lockProblem(parsed);
1675
+ if (problem !== null) {
1676
+ throw new Error(
1677
+ `${file} is unreadable: ${problem}. Delete it to push every file again with no expected version.`
1678
+ );
1679
+ }
1680
+ return parsed;
1681
+ });
1682
+ }
1683
+ function serializeLock(lock) {
1684
+ return `${JSON.stringify(lock, null, 2).replace(/\r\n/g, "\n")}
1685
+ `;
1686
+ }
1687
+ function writeContextLock(root, lock) {
1688
+ return __async(this, null, function* () {
1689
+ yield writeFileSafely({
1690
+ root,
1691
+ relativePath: CONTEXT_LOCK_RELATIVE_PATH,
1692
+ bytes: new TextEncoder().encode(serializeLock(lock))
1693
+ });
1694
+ });
1695
+ }
1696
+ function lockProblem(value) {
1697
+ if (typeof value !== "object" || value === null || Array.isArray(value)) {
1698
+ return "it is not a JSON object";
1699
+ }
1700
+ const candidate = value;
1701
+ if (candidate.version !== CONTEXT_LOCK_VERSION) {
1702
+ return `its version is ${JSON.stringify(candidate.version)} and this build writes version ${CONTEXT_LOCK_VERSION}`;
1703
+ }
1704
+ for (const field of ["projectId", "pushedAt"]) {
1705
+ const actual = candidate[field];
1706
+ if (typeof actual !== "string" || actual.length === 0) {
1707
+ return `its "${field}" is not a non-empty string`;
1708
+ }
1709
+ }
1710
+ if (typeof candidate.files !== "object" || candidate.files === null || Array.isArray(candidate.files)) {
1711
+ return 'its "files" is not an object';
1712
+ }
1713
+ for (const [filePath, entry] of Object.entries(
1714
+ candidate.files
1715
+ )) {
1716
+ const normalized = normalizeContextSourcePath(filePath);
1717
+ if (!normalized.ok || normalized.sourcePath !== filePath) {
1718
+ return `it names ${JSON.stringify(filePath)}, which is not a path the server stores`;
1719
+ }
1720
+ if (typeof entry !== "object" || entry === null || Array.isArray(entry)) {
1721
+ return `its entry for ${JSON.stringify(filePath)} is not an object`;
1722
+ }
1723
+ const record = entry;
1724
+ if (typeof record.sha256 !== "string" || !SHA256_HEX.test(record.sha256)) {
1725
+ return `its entry for ${JSON.stringify(filePath)} has no 64-character hex sha256`;
1726
+ }
1727
+ if (record.state === "duplicate") {
1728
+ if (record.contextId !== void 0) {
1729
+ return `its duplicate entry for ${JSON.stringify(filePath)} names a contextId, which a duplicate never has`;
1730
+ }
1731
+ continue;
1732
+ }
1733
+ if (record.state !== void 0) {
1734
+ return `its entry for ${JSON.stringify(filePath)} has an unknown state ${JSON.stringify(record.state)}`;
1735
+ }
1736
+ if (typeof record.contextId !== "string" || record.contextId.length === 0) {
1737
+ return `its entry for ${JSON.stringify(filePath)} has no contextId`;
1738
+ }
1739
+ }
1740
+ return null;
1741
+ }
1742
+
1743
+ // src/lib/context-sync/classify.ts
1744
+ import { createHash as createHash2 } from "crypto";
1745
+ import path3 from "path";
1746
+ var MAX_CONTEXT_FILE_BYTES = 2 * 1024 * 1024;
1747
+ var CONTEXT_TEXT_EXTENSIONS = /* @__PURE__ */ new Set([
1748
+ ".md",
1749
+ ".markdown",
1750
+ ".txt",
1751
+ ".json",
1752
+ ".yaml",
1753
+ ".yml"
1754
+ ]);
1755
+ var HAS_NON_WHITESPACE = /\S/;
1756
+ function hasTextExtension(relativePath) {
1757
+ return CONTEXT_TEXT_EXTENSIONS.has(
1758
+ path3.posix.extname(relativePath).toLowerCase()
1169
1759
  );
1170
- features.command("create <title>").description("Create a feature in a project").requiredOption("-p, --project <projectId>", "Project ID").option("--org <slug>", "Organization context").option("--personal", "Retired \u2014 context is organization-only").option("-d, --description <text>", "Feature description").option(
1171
- "--priority <priority>",
1172
- "Priority: P0_CRITICAL|P1_HIGH|P2_MEDIUM|P3_LOW (default: P2_MEDIUM)"
1173
- ).option("--format <format>", "Output format: table|json|yaml", "table").action(
1174
- (title, opts) => __async(null, null, function* () {
1175
- const ctx = resolveContext(opts.personal, opts.org, false);
1176
- const client = getClient();
1177
- let feature;
1178
- try {
1179
- feature = yield client.projects.createFeature(
1180
- opts.project,
1181
- {
1182
- title,
1183
- description: opts.description,
1184
- priority: opts.priority,
1185
- org: (ctx == null ? void 0 : ctx.type) === "org" ? ctx.slug : void 0
1760
+ }
1761
+ function classifyContextBytes(bytes) {
1762
+ if (bytes.byteLength > MAX_CONTEXT_FILE_BYTES) {
1763
+ return { ok: false, reason: "too-large" };
1764
+ }
1765
+ let content;
1766
+ try {
1767
+ content = new TextDecoder("utf-8", {
1768
+ fatal: true,
1769
+ ignoreBOM: true
1770
+ }).decode(bytes);
1771
+ } catch (e) {
1772
+ return { ok: false, reason: "binary" };
1773
+ }
1774
+ if (content.includes("\0")) {
1775
+ return { ok: false, reason: "binary" };
1776
+ }
1777
+ if (!HAS_NON_WHITESPACE.test(content)) {
1778
+ return { ok: false, reason: "empty" };
1779
+ }
1780
+ return { ok: true, content };
1781
+ }
1782
+ function hashContextContent(content) {
1783
+ return createHash2("sha256").update(content, "utf8").digest("hex");
1784
+ }
1785
+
1786
+ // src/lib/context-sync/ignore.ts
1787
+ import ignore from "ignore";
1788
+ var CONTEXT_IGNORE_FILENAME = ".contextignore";
1789
+ var DEFAULT_CONTEXT_IGNORE_PATTERNS = [
1790
+ ".git/",
1791
+ ".fabric/",
1792
+ ".claude/",
1793
+ ".cursor/",
1794
+ ".codex/",
1795
+ "node_modules/",
1796
+ "CLAUDE.md",
1797
+ "AGENTS.md",
1798
+ "GEMINI.md",
1799
+ "skills/",
1800
+ "agents/",
1801
+ "hooks/",
1802
+ "rules/",
1803
+ "scripts/",
1804
+ CONTEXT_IGNORE_FILENAME
1805
+ ];
1806
+ function buildContextIgnoreRules(input = {}) {
1807
+ const defaults = ignore().add([...DEFAULT_CONTEXT_IGNORE_PATTERNS]);
1808
+ const user = ignore();
1809
+ if (input.contextIgnore) {
1810
+ user.add(input.contextIgnore);
1811
+ }
1812
+ if (input.excludes && input.excludes.length > 0) {
1813
+ user.add([...input.excludes]);
1814
+ }
1815
+ const ignores = (candidate) => defaults.ignores(candidate) || user.ignores(candidate);
1816
+ return {
1817
+ ignoresFile: (relativePath) => ignores(relativePath),
1818
+ // The trailing slash is what makes a directory-only pattern
1819
+ // (`drafts/`) match: gitignore semantics, as the package implements
1820
+ // them.
1821
+ ignoresDirectory: (relativePath) => ignores(`${relativePath}/`)
1822
+ };
1823
+ }
1824
+
1825
+ // src/lib/context-sync/walk.ts
1826
+ import { lstat as lstat2, readdir } from "fs/promises";
1827
+ import path4 from "path";
1828
+ function byPath(a, b) {
1829
+ return a < b ? -1 : a > b ? 1 : 0;
1830
+ }
1831
+ function walkContextDirectory(root, rules) {
1832
+ return __async(this, null, function* () {
1833
+ const walk = {
1834
+ files: [],
1835
+ symlinks: [],
1836
+ special: [],
1837
+ unmatchable: [],
1838
+ ignored: []
1839
+ };
1840
+ const pending = [""];
1841
+ while (pending.length > 0) {
1842
+ const relativeDirectory = pending.pop();
1843
+ const names = yield readdir(path4.join(root, relativeDirectory));
1844
+ for (const name of names) {
1845
+ const relative = relativeDirectory ? `${relativeDirectory}/${name}` : name;
1846
+ let stats;
1847
+ try {
1848
+ stats = yield lstat2(path4.join(root, relative));
1849
+ } catch (error) {
1850
+ if (error.code === "ENOENT") {
1851
+ continue;
1186
1852
  }
1187
- );
1188
- } catch (err) {
1189
- printError(err.message, 1);
1853
+ throw error;
1854
+ }
1855
+ let ignored;
1856
+ try {
1857
+ ignored = stats.isDirectory() ? rules.ignoresDirectory(relative) : stats.isSymbolicLink() ? rules.ignoresFile(relative) || rules.ignoresDirectory(relative) : rules.ignoresFile(relative);
1858
+ } catch (e) {
1859
+ walk.unmatchable.push(relative);
1860
+ continue;
1861
+ }
1862
+ if (ignored) {
1863
+ walk.ignored.push({
1864
+ path: relative,
1865
+ // A link was matched as a directory too, so what would sit
1866
+ // beneath it counts as ignored with it.
1867
+ directory: stats.isDirectory() || stats.isSymbolicLink()
1868
+ });
1869
+ continue;
1870
+ }
1871
+ if (stats.isSymbolicLink()) {
1872
+ walk.symlinks.push(relative);
1873
+ } else if (stats.isDirectory()) {
1874
+ pending.push(relative);
1875
+ } else if (stats.isFile()) {
1876
+ walk.files.push({ path: relative, size: stats.size });
1877
+ } else {
1878
+ walk.special.push(relative);
1879
+ }
1880
+ }
1881
+ }
1882
+ walk.files.sort((a, b) => byPath(a.path, b.path));
1883
+ walk.symlinks.sort(byPath);
1884
+ walk.special.sort(byPath);
1885
+ walk.unmatchable.sort(byPath);
1886
+ walk.ignored.sort((a, b) => byPath(a.path, b.path));
1887
+ return walk;
1888
+ });
1889
+ }
1890
+
1891
+ // src/lib/context-sync/plan.ts
1892
+ function byPath2(a, b) {
1893
+ return a < b ? -1 : a > b ? 1 : 0;
1894
+ }
1895
+ function readContextIgnore(root) {
1896
+ return __async(this, null, function* () {
1897
+ const read = yield readFileSafely(root, CONTEXT_IGNORE_FILENAME, {
1898
+ maxBytes: 1024 * 1024
1899
+ });
1900
+ return read === null ? null : new TextDecoder().decode(read.bytes);
1901
+ });
1902
+ }
1903
+ function ignoredByWalk(ignored) {
1904
+ const exact = new Set(ignored.map((entry) => entry.path.normalize("NFC")));
1905
+ const directories = ignored.filter((entry) => entry.directory).map((entry) => `${entry.path.normalize("NFC")}/`);
1906
+ return (sourcePath) => exact.has(sourcePath) || directories.some((prefix) => sourcePath.startsWith(prefix));
1907
+ }
1908
+ function sharedPathProblem(entry, group2) {
1909
+ const others = group2.filter((other) => other.diskPath !== entry.diskPath);
1910
+ if (others.length === 0) {
1911
+ return null;
1912
+ }
1913
+ const names = others.map((other) => other.diskPath).join(", ");
1914
+ return others.every((other) => other.sourcePath === entry.sourcePath) ? `names the same source as ${names}` : `case-only collision with ${names}`;
1915
+ }
1916
+ function computeContextPlan(input) {
1917
+ return __async(this, null, function* () {
1918
+ var _a2, _b, _c;
1919
+ const { root, lock } = input;
1920
+ const rules = buildContextIgnoreRules({
1921
+ contextIgnore: yield readContextIgnore(root),
1922
+ excludes: input.excludes
1923
+ });
1924
+ const walk = yield walkContextDirectory(root, rules);
1925
+ const skipped = [
1926
+ ...walk.symlinks.map((p) => ({ path: p, reason: "symlink" })),
1927
+ ...walk.special.map((p) => ({
1928
+ path: p,
1929
+ reason: "unsupported-type"
1930
+ })),
1931
+ ...walk.unmatchable.map((p) => ({
1932
+ path: p,
1933
+ reason: "invalid-path",
1934
+ detail: "cannot be matched against the ignore rules"
1935
+ }))
1936
+ ];
1937
+ const readable = [];
1938
+ for (const file of walk.files) {
1939
+ if (file.path.includes("\\")) {
1940
+ skipped.push({
1941
+ path: file.path,
1942
+ reason: "invalid-path",
1943
+ detail: "backslash"
1944
+ });
1945
+ continue;
1190
1946
  }
1191
- const fmt = opts.format;
1192
- if (fmt === "json" || fmt === "yaml") {
1193
- printOutput(feature, { format: fmt });
1194
- return;
1947
+ const normalized = normalizeContextSourcePath(file.path);
1948
+ if (!normalized.ok) {
1949
+ skipped.push({
1950
+ path: file.path,
1951
+ reason: "invalid-path",
1952
+ detail: normalized.reason
1953
+ });
1954
+ continue;
1195
1955
  }
1196
- printSuccess(`Feature ${feature.identifier} created`);
1197
- printRecord({
1198
- ID: feature.id,
1199
- Identifier: feature.identifier,
1200
- Title: feature.title,
1201
- Priority: feature.priority,
1202
- Project: feature.projectId,
1203
- Created: feature.createdAt.slice(0, 10)
1956
+ if (!hasTextExtension(file.path)) {
1957
+ skipped.push({ path: file.path, reason: "unsupported-type" });
1958
+ continue;
1959
+ }
1960
+ if (file.size > MAX_CONTEXT_FILE_BYTES) {
1961
+ skipped.push({ path: file.path, reason: "too-large" });
1962
+ continue;
1963
+ }
1964
+ readable.push({
1965
+ diskPath: file.path,
1966
+ sourcePath: normalized.sourcePath
1204
1967
  });
1205
- })
1206
- );
1207
- features.command("tasks").description("List tasks (sub-items) for a feature").requiredOption("-p, --project <projectId>", "Project ID").requiredOption("-f, --feature <featureId>", "Feature ID").option("--org <slug>", "Organization context").option("--personal", "Retired \u2014 context is organization-only").option(
1208
- "--format <format>",
1209
- "Output format: table|json|yaml|csv",
1210
- "table"
1211
- ).option("-q, --quiet", "Print IDs only").action(
1212
- (opts) => __async(null, null, function* () {
1213
- const ctx = resolveContext(opts.personal, opts.org, false);
1214
- const client = getClient();
1215
- let tasks;
1216
- try {
1217
- tasks = yield client.projects.listTasks(
1218
- opts.project,
1219
- opts.feature,
1220
- {
1221
- org: (ctx == null ? void 0 : ctx.type) === "org" ? ctx.slug : void 0
1222
- }
1223
- );
1224
- } catch (err) {
1225
- printError(err.message, 1);
1968
+ }
1969
+ const byFoldedPath = /* @__PURE__ */ new Map();
1970
+ for (const entry of readable) {
1971
+ const key = entry.sourcePath.toLowerCase();
1972
+ byFoldedPath.set(key, [...(_a2 = byFoldedPath.get(key)) != null ? _a2 : [], entry]);
1973
+ }
1974
+ const push = [];
1975
+ const unchangedLocal = [];
1976
+ const present = /* @__PURE__ */ new Set();
1977
+ for (const entry of readable) {
1978
+ const problem = sharedPathProblem(
1979
+ entry,
1980
+ (_b = byFoldedPath.get(entry.sourcePath.toLowerCase())) != null ? _b : []
1981
+ );
1982
+ if (problem !== null) {
1983
+ skipped.push({
1984
+ path: entry.diskPath,
1985
+ reason: "invalid-path",
1986
+ detail: problem
1987
+ });
1988
+ continue;
1226
1989
  }
1227
- if (opts.quiet) {
1228
- for (const t of tasks) {
1229
- process.stdout.write(`${t.id}
1230
- `);
1990
+ const read = yield readFileSafely(root, entry.diskPath, {
1991
+ maxBytes: MAX_CONTEXT_FILE_BYTES
1992
+ }).catch((error) => {
1993
+ if (error instanceof Error && /too large/.test(error.message)) {
1994
+ return "too-large";
1231
1995
  }
1232
- return;
1233
- }
1234
- printOutput(tasks, {
1235
- format: opts.format,
1236
- columns: [
1237
- "identifier",
1238
- "title",
1239
- "status",
1240
- "estimatedHours"
1241
- ]
1996
+ throw error;
1242
1997
  });
1243
- })
1244
- );
1245
- features.command("update <id>").description("Update a feature").requiredOption("-p, --project <projectId>", "Project ID").option("--org <slug>", "Organization context").option("--personal", "Retired \u2014 context is organization-only").option("--title <title>", "New title").option("-d, --description <text>", "New description").option(
1246
- "--priority <priority>",
1247
- "Priority: P0_CRITICAL|P1_HIGH|P2_MEDIUM|P3_LOW"
1248
- ).option("--format <format>", "Output format: table|json|yaml", "table").action(
1249
- (id, opts) => __async(null, null, function* () {
1250
- const ctx = resolveContext(opts.personal, opts.org, false);
1251
- const client = getClient();
1252
- let feature;
1253
- try {
1254
- feature = yield client.projects.updateFeature(
1255
- opts.project,
1256
- id,
1257
- {
1258
- title: opts.title,
1259
- description: opts.description,
1260
- priority: opts.priority,
1261
- org: (ctx == null ? void 0 : ctx.type) === "org" ? ctx.slug : void 0
1262
- }
1998
+ if (read === null) {
1999
+ continue;
2000
+ }
2001
+ if (read === "too-large") {
2002
+ skipped.push({ path: entry.diskPath, reason: "too-large" });
2003
+ continue;
2004
+ }
2005
+ const verdict = classifyContextBytes(read.bytes);
2006
+ if (!verdict.ok) {
2007
+ skipped.push({ path: entry.diskPath, reason: verdict.reason });
2008
+ continue;
2009
+ }
2010
+ const sha256 = hashContextContent(verdict.content);
2011
+ const locked = lock == null ? void 0 : lock.files[entry.sourcePath];
2012
+ const plannedFile = {
2013
+ sourcePath: entry.sourcePath,
2014
+ diskPath: entry.diskPath,
2015
+ sha256,
2016
+ bytes: read.bytes.byteLength
2017
+ };
2018
+ if ((locked == null ? void 0 : locked.sha256) === sha256) {
2019
+ unchangedLocal.push(entry.sourcePath);
2020
+ } else {
2021
+ push.push(
2022
+ // A duplicate never had a row of its own, so there is no
2023
+ // version to name: it is sent like a new path.
2024
+ locked && locked.state !== "duplicate" ? __spreadProps(__spreadValues({}, plannedFile), { expectedContentHash: locked.sha256 }) : plannedFile
1263
2025
  );
1264
- } catch (err) {
1265
- printError(err.message, 1);
1266
2026
  }
1267
- const fmt = opts.format;
1268
- if (fmt === "json" || fmt === "yaml") {
1269
- printOutput(feature, { format: fmt });
1270
- return;
2027
+ present.add(entry.sourcePath);
2028
+ }
2029
+ for (const skip of skipped) {
2030
+ const check = normalizeContextSourcePath(skip.path);
2031
+ if (check.ok) {
2032
+ present.add(check.sourcePath);
1271
2033
  }
1272
- printRecord({
1273
- ID: feature.id,
1274
- Identifier: feature.identifier,
1275
- Title: feature.title,
1276
- Priority: feature.priority,
1277
- Updated: feature.updatedAt.slice(0, 10)
1278
- });
1279
- })
2034
+ }
2035
+ const isIgnored = ignoredByWalk(walk.ignored);
2036
+ const removed = [];
2037
+ const forgotten = [];
2038
+ for (const [sourcePath, entry] of Object.entries((_c = lock == null ? void 0 : lock.files) != null ? _c : {})) {
2039
+ if (present.has(sourcePath)) {
2040
+ continue;
2041
+ }
2042
+ if (entry.state === "duplicate") {
2043
+ forgotten.push(sourcePath);
2044
+ } else if (isIgnored(sourcePath)) {
2045
+ skipped.push({ path: sourcePath, reason: "ignored" });
2046
+ } else {
2047
+ removed.push(sourcePath);
2048
+ }
2049
+ }
2050
+ push.sort((a, b) => byPath2(a.sourcePath, b.sourcePath));
2051
+ removed.sort(byPath2);
2052
+ const moves = pairMoves(lock, removed, push);
2053
+ const moved = new Set(moves.map((move) => move.from));
2054
+ const arrived = new Set(moves.map((move) => move.to));
2055
+ unchangedLocal.sort(byPath2);
2056
+ forgotten.sort(byPath2);
2057
+ skipped.sort((a, b) => byPath2(a.path, b.path));
2058
+ return {
2059
+ push: push.filter((entry) => !arrived.has(entry.sourcePath)),
2060
+ moves,
2061
+ unchangedLocal,
2062
+ removed: removed.filter((sourcePath) => !moved.has(sourcePath)),
2063
+ forgotten,
2064
+ skipped
2065
+ };
2066
+ });
2067
+ }
2068
+ function pairMoves(lock, removed, push) {
2069
+ const added = push.filter(
2070
+ (entry) => (lock == null ? void 0 : lock.files[entry.sourcePath]) === void 0
1280
2071
  );
1281
- return features;
2072
+ const taken = /* @__PURE__ */ new Set();
2073
+ const moves = [];
2074
+ for (const from of removed) {
2075
+ const entry = lock == null ? void 0 : lock.files[from];
2076
+ if (!entry || entry.state === "duplicate") {
2077
+ continue;
2078
+ }
2079
+ const match = added.find(
2080
+ (candidate) => !taken.has(candidate.sourcePath) && candidate.sha256 === entry.sha256
2081
+ );
2082
+ if (!match) {
2083
+ continue;
2084
+ }
2085
+ taken.add(match.sourcePath);
2086
+ moves.push({
2087
+ from,
2088
+ to: match.sourcePath,
2089
+ diskPath: match.diskPath,
2090
+ sha256: match.sha256,
2091
+ bytes: match.bytes,
2092
+ contextId: entry.contextId
2093
+ });
2094
+ }
2095
+ return moves;
1282
2096
  }
1283
2097
 
1284
- // src/commands/frames/index.ts
1285
- import { Command as Command10 } from "commander";
1286
- function buildFramesCommand() {
1287
- const frames = new Command10("frames").description(
1288
- "Manage AI-generated frames and slideshows"
1289
- );
1290
- frames.command("list").description("List frames and slideshows").option("--org <slug>", "Organization context").option("--personal", "Retired \u2014 context is organization-only").option("--limit <n>", "Max results (default 50)", "50").option(
1291
- "--format <format>",
1292
- "Output format: table|json|yaml|csv",
1293
- "table"
1294
- ).option("-q, --quiet", "Print IDs only").action(
1295
- (opts) => __async(null, null, function* () {
1296
- const ctx = resolveContext(opts.personal, opts.org, false);
1297
- const client = getClient();
1298
- let list;
2098
+ // src/lib/context-sync/push.ts
2099
+ var DELETE_STATUSES = /* @__PURE__ */ new Set([
2100
+ "deleted",
2101
+ "already-gone",
2102
+ "delete-in-progress",
2103
+ "delete-conflict",
2104
+ "delete-failed"
2105
+ ]);
2106
+ var RUN_LEVEL_STATUSES = /* @__PURE__ */ new Set([401, 403, 404, 429]);
2107
+ var RUN_LEVEL_CODES = /* @__PURE__ */ new Set(["NETWORK_ERROR", "TIMEOUT"]);
2108
+ function stopsTheRun(error) {
2109
+ const { status, code } = error;
2110
+ return typeof status === "number" && RUN_LEVEL_STATUSES.has(status) || typeof code === "string" && RUN_LEVEL_CODES.has(code);
2111
+ }
2112
+ function conflictOf(error) {
2113
+ if (typeof error !== "object" || error === null) {
2114
+ return null;
2115
+ }
2116
+ const conflict = error.conflict;
2117
+ if (typeof conflict === "object" && conflict !== null && conflict.status === "conflict" && "current" in conflict && // An object, or `null` for a named version deleted on the server.
2118
+ typeof conflict.current === "object") {
2119
+ return conflict;
2120
+ }
2121
+ return null;
2122
+ }
2123
+ function readPlannedContent(root, entry) {
2124
+ return __async(this, null, function* () {
2125
+ const read = yield readFileSafely(root, entry.diskPath, {
2126
+ maxBytes: MAX_CONTEXT_FILE_BYTES
2127
+ }).catch((error) => {
2128
+ if (error instanceof Error && /too large/.test(error.message)) {
2129
+ return null;
2130
+ }
2131
+ throw error;
2132
+ });
2133
+ if (read === null) {
2134
+ return null;
2135
+ }
2136
+ const verdict = classifyContextBytes(read.bytes);
2137
+ if (!verdict.ok || hashContextContent(verdict.content) !== entry.sha256) {
2138
+ return null;
2139
+ }
2140
+ return verdict.content;
2141
+ });
2142
+ }
2143
+ function fromResult(sourcePath, result, movedFrom) {
2144
+ if (result.status === "moved") {
2145
+ return {
2146
+ sourcePath,
2147
+ status: "moved",
2148
+ movedFrom: movedFrom != null ? movedFrom : result.movedFromSourcePath,
2149
+ contextId: result.contextId,
2150
+ contentHash: result.contentHash
2151
+ };
2152
+ }
2153
+ const move = movedFrom === void 0 ? {} : __spreadValues({
2154
+ movedFrom
2155
+ }, result.moveNotApplied ? { moveNotApplied: result.moveNotApplied.reason } : {});
2156
+ if (result.status === "duplicate") {
2157
+ return __spreadValues({
2158
+ sourcePath,
2159
+ status: "duplicate",
2160
+ contentHash: result.contentHash,
2161
+ duplicateOfContextId: result.duplicateOfContextId,
2162
+ duplicateOfSourcePath: result.duplicateOfSourcePath
2163
+ }, move);
2164
+ }
2165
+ return __spreadValues({
2166
+ sourcePath,
2167
+ status: result.status,
2168
+ contextId: result.contextId,
2169
+ contentHash: result.contentHash
2170
+ }, move);
2171
+ }
2172
+ function moveKeptOldPath(result) {
2173
+ if ((result.status === "created" || result.status === "updated" || result.status === "unchanged" || result.status === "duplicate") && result.movedFrom !== void 0 && result.moveNotApplied !== "source-missing") {
2174
+ return result.movedFrom;
2175
+ }
2176
+ return null;
2177
+ }
2178
+ function pushContextPlan(input) {
2179
+ return __async(this, null, function* () {
2180
+ var _a2;
2181
+ const results = [];
2182
+ const pruneTargets = [];
2183
+ const { force } = input;
2184
+ const send = (sourcePath, content, expectedContentHash, movedFromSourcePath) => input.client.contexts.upsertSyncedFile(
2185
+ input.projectId,
2186
+ __spreadValues(__spreadValues({
2187
+ sourcePath,
2188
+ content
2189
+ }, expectedContentHash !== void 0 ? { expectedContentHash } : {}), movedFromSourcePath !== void 0 ? { movedFromSourcePath } : {}),
2190
+ { org: input.org }
2191
+ );
2192
+ const pushEntry = (entry) => __async(null, null, function* () {
2193
+ var _a3;
2194
+ const content = yield readPlannedContent(input.root, entry);
2195
+ if (content === null) {
2196
+ return {
2197
+ sourcePath: entry.sourcePath,
2198
+ status: "changed-during-run"
2199
+ };
2200
+ }
1299
2201
  try {
1300
- list = yield client.frames.list({
1301
- org: (ctx == null ? void 0 : ctx.type) === "org" ? ctx.slug : void 0,
1302
- limit: Number(opts.limit)
1303
- });
1304
- } catch (err) {
1305
- printError(err.message, 1);
2202
+ return fromResult(
2203
+ entry.sourcePath,
2204
+ yield send(
2205
+ entry.sourcePath,
2206
+ content,
2207
+ entry.expectedContentHash
2208
+ )
2209
+ );
2210
+ } catch (error) {
2211
+ const conflict = conflictOf(error);
2212
+ if (conflict === null) {
2213
+ throw error;
2214
+ }
2215
+ const { current } = conflict;
2216
+ if (!force || current !== null && current.contentHash === null) {
2217
+ return {
2218
+ sourcePath: entry.sourcePath,
2219
+ status: "conflict",
2220
+ contentHash: conflict.contentHash,
2221
+ wasLocked: entry.expectedContentHash !== void 0,
2222
+ afterForce: false,
2223
+ current
2224
+ };
2225
+ }
2226
+ const forcedContent = yield readPlannedContent(input.root, entry);
2227
+ if (forcedContent === null) {
2228
+ return {
2229
+ sourcePath: entry.sourcePath,
2230
+ status: "changed-during-run"
2231
+ };
2232
+ }
2233
+ try {
2234
+ const forced = fromResult(
2235
+ entry.sourcePath,
2236
+ yield send(
2237
+ entry.sourcePath,
2238
+ forcedContent,
2239
+ current === null ? void 0 : (_a3 = current.contentHash) != null ? _a3 : void 0
2240
+ )
2241
+ );
2242
+ return forced.status === "updated" && current !== null ? __spreadProps(__spreadValues({}, forced), { overwrote: current }) : forced;
2243
+ } catch (retryError) {
2244
+ const again = conflictOf(retryError);
2245
+ if (again === null) {
2246
+ throw retryError;
2247
+ }
2248
+ return {
2249
+ sourcePath: entry.sourcePath,
2250
+ status: "conflict",
2251
+ contentHash: again.contentHash,
2252
+ wasLocked: entry.expectedContentHash !== void 0,
2253
+ afterForce: true,
2254
+ current: again.current
2255
+ };
2256
+ }
1306
2257
  }
1307
- if (opts.quiet) {
1308
- for (const f of list) {
1309
- process.stdout.write(`${f.id}
1310
- `);
2258
+ });
2259
+ const pushMove = (move) => __async(null, null, function* () {
2260
+ var _a3, _b, _c, _d;
2261
+ const entry = {
2262
+ sourcePath: move.to,
2263
+ diskPath: move.diskPath,
2264
+ sha256: move.sha256,
2265
+ bytes: move.bytes
2266
+ };
2267
+ const moveFields = { movedFrom: move.from };
2268
+ const content = yield readPlannedContent(input.root, entry);
2269
+ if (content === null) {
2270
+ return __spreadValues({
2271
+ sourcePath: move.to,
2272
+ status: "changed-during-run"
2273
+ }, moveFields);
2274
+ }
2275
+ const asConflict = (conflict, afterForce) => {
2276
+ var _a4;
2277
+ return __spreadValues(__spreadValues({
2278
+ sourcePath: move.to,
2279
+ status: "conflict",
2280
+ contentHash: conflict.contentHash,
2281
+ // About the old path (which the lock names) or the new one.
2282
+ wasLocked: ((_a4 = conflict.moveNotApplied) == null ? void 0 : _a4.reason) === "source-changed",
2283
+ afterForce,
2284
+ current: conflict.current
2285
+ }, moveFields), conflict.moveNotApplied ? { moveNotApplied: conflict.moveNotApplied.reason } : {});
2286
+ };
2287
+ try {
2288
+ return fromResult(
2289
+ move.to,
2290
+ yield send(move.to, content, move.sha256, move.from),
2291
+ move.from
2292
+ );
2293
+ } catch (error) {
2294
+ const conflict = conflictOf(error);
2295
+ if (conflict === null) {
2296
+ throw error;
2297
+ }
2298
+ const { current } = conflict;
2299
+ if (current === null && conflict.moveNotApplied === void 0) {
2300
+ return {
2301
+ sourcePath: move.to,
2302
+ status: "move-unsupported",
2303
+ movedFrom: move.from
2304
+ };
2305
+ }
2306
+ if (!force || current !== null && current.contentHash === null) {
2307
+ return asConflict(conflict, false);
2308
+ }
2309
+ const forcedContent = yield readPlannedContent(input.root, entry);
2310
+ if (forcedContent === null) {
2311
+ return __spreadValues({
2312
+ sourcePath: move.to,
2313
+ status: "changed-during-run"
2314
+ }, moveFields);
2315
+ }
2316
+ const aboutOldPath = ((_a3 = conflict.moveNotApplied) == null ? void 0 : _a3.reason) === "source-changed";
2317
+ try {
2318
+ if (aboutOldPath && current === null) {
2319
+ const created = fromResult(
2320
+ move.to,
2321
+ yield send(move.to, forcedContent)
2322
+ );
2323
+ return created.status === "moved" ? created : __spreadProps(__spreadValues(__spreadValues({}, created), moveFields), {
2324
+ moveNotApplied: "source-missing"
2325
+ });
2326
+ }
2327
+ if (aboutOldPath) {
2328
+ return fromResult(
2329
+ move.to,
2330
+ yield send(
2331
+ move.to,
2332
+ forcedContent,
2333
+ (_b = current == null ? void 0 : current.contentHash) != null ? _b : void 0,
2334
+ move.from
2335
+ ),
2336
+ move.from
2337
+ );
2338
+ }
2339
+ const forced = fromResult(
2340
+ move.to,
2341
+ yield send(
2342
+ move.to,
2343
+ forcedContent,
2344
+ (_c = current == null ? void 0 : current.contentHash) != null ? _c : void 0
2345
+ )
2346
+ );
2347
+ if (forced.status === "moved") {
2348
+ return forced;
2349
+ }
2350
+ return __spreadValues(__spreadValues(__spreadValues({}, forced.status === "updated" && current !== null ? __spreadProps(__spreadValues({}, forced), { overwrote: current }) : forced), moveFields), conflict.moveNotApplied ? { moveNotApplied: conflict.moveNotApplied.reason } : {});
2351
+ } catch (retryError) {
2352
+ const again = conflictOf(retryError);
2353
+ if (again === null) {
2354
+ throw retryError;
2355
+ }
2356
+ if (again.moveNotApplied !== void 0) {
2357
+ return asConflict(again, true);
2358
+ }
2359
+ const reason = aboutOldPath ? current === null ? "source-missing" : void 0 : (_d = conflict.moveNotApplied) == null ? void 0 : _d.reason;
2360
+ return asConflict(
2361
+ reason === void 0 ? again : __spreadProps(__spreadValues({}, again), {
2362
+ moveNotApplied: {
2363
+ movedFromSourcePath: move.from,
2364
+ reason
2365
+ }
2366
+ }),
2367
+ true
2368
+ );
1311
2369
  }
1312
- return;
1313
2370
  }
1314
- printOutput(list, {
1315
- format: opts.format,
1316
- columns: [
1317
- "id",
1318
- "title",
1319
- "fileType",
1320
- "status",
1321
- "isPublic",
1322
- "updatedAt"
1323
- ]
1324
- });
1325
- })
1326
- );
1327
- frames.command("show <id>").description("Show frame details").option("--org <slug>", "Organization context").option("--personal", "Retired \u2014 context is organization-only").option("--format <format>", "Output format: table|json|yaml", "table").action(
1328
- (id, opts) => __async(null, null, function* () {
1329
- var _a2, _b, _c;
1330
- const ctx = resolveContext(opts.personal, opts.org, false);
1331
- const client = getClient();
1332
- let frame;
2371
+ });
2372
+ const deleteEntry = (sourcePath, sha256) => __async(null, null, function* () {
2373
+ const remove = (expectedContentHash) => input.client.contexts.deleteSyncedFile(
2374
+ input.projectId,
2375
+ { sourcePath, expectedContentHash },
2376
+ { org: input.org }
2377
+ );
2378
+ const fromAnswer = (answer, overwrote) => {
2379
+ switch (answer.status) {
2380
+ case "deleted":
2381
+ return __spreadValues({
2382
+ sourcePath,
2383
+ status: "deleted",
2384
+ contextId: answer.contextId,
2385
+ contentHash: answer.contentHash
2386
+ }, overwrote ? { overwrote } : {});
2387
+ case "absent":
2388
+ return { sourcePath, status: "already-gone" };
2389
+ case "in-progress":
2390
+ return { sourcePath, status: "delete-in-progress" };
2391
+ }
2392
+ };
1333
2393
  try {
1334
- frame = yield client.frames.get(id, {
1335
- org: (ctx == null ? void 0 : ctx.type) === "org" ? ctx.slug : void 0
1336
- });
1337
- } catch (err) {
1338
- printError(err.message, 1);
2394
+ return fromAnswer(yield remove(sha256));
2395
+ } catch (error) {
2396
+ const conflict = conflictOf(error);
2397
+ if (conflict === null || conflict.current === null) {
2398
+ throw error;
2399
+ }
2400
+ const { current } = conflict;
2401
+ if (!force || current.contentHash === null) {
2402
+ return {
2403
+ sourcePath,
2404
+ status: "delete-conflict",
2405
+ afterForce: false,
2406
+ current
2407
+ };
2408
+ }
2409
+ try {
2410
+ return fromAnswer(yield remove(current.contentHash), current);
2411
+ } catch (retryError) {
2412
+ const again = conflictOf(retryError);
2413
+ if (again === null || again.current === null) {
2414
+ throw retryError;
2415
+ }
2416
+ return {
2417
+ sourcePath,
2418
+ status: "delete-conflict",
2419
+ afterForce: true,
2420
+ current: again.current
2421
+ };
2422
+ }
2423
+ }
2424
+ });
2425
+ const step = (run3, failed) => __async(null, null, function* () {
2426
+ try {
2427
+ results.push(yield run3());
2428
+ return null;
2429
+ } catch (error) {
2430
+ const failure = asCliFailure(error);
2431
+ if (stopsTheRun(error)) {
2432
+ return failure;
2433
+ }
2434
+ results.push(failed(error, failure.exitCode));
2435
+ return null;
1339
2436
  }
1340
- const fmt = opts.format;
1341
- if (fmt === "json" || fmt === "yaml") {
1342
- printOutput(frame, { format: fmt });
1343
- return;
2437
+ });
2438
+ const pruneTargetsSoFar = () => {
2439
+ const keptByMoves = results.map(moveKeptOldPath).filter((path9) => path9 !== null);
2440
+ return [.../* @__PURE__ */ new Set([...input.plan.removed, ...keptByMoves])].filter((path9) => {
2441
+ var _a3;
2442
+ const locked = (_a3 = input.lock) == null ? void 0 : _a3.files[path9];
2443
+ return locked !== void 0 && locked.state !== "duplicate";
2444
+ }).sort((a, b) => a < b ? -1 : a > b ? 1 : 0);
2445
+ };
2446
+ const stopped = (stoppedBy) => {
2447
+ if (input.prune && pruneTargets.length === 0) {
2448
+ pruneTargets.push(...pruneTargetsSoFar());
1344
2449
  }
1345
- printRecord({
1346
- ID: frame.id,
1347
- Title: frame.title,
1348
- Kind: (_a2 = frame.kind) != null ? _a2 : "(none)",
1349
- "File Type": frame.fileType,
1350
- Status: frame.status,
1351
- Public: frame.isPublic ? "yes" : "no",
1352
- "Share Token": (_b = frame.shareToken) != null ? _b : "(none)",
1353
- Description: (_c = frame.description) != null ? _c : "(none)",
1354
- Updated: frame.updatedAt.slice(0, 10)
1355
- });
1356
- })
1357
- );
1358
- return frames;
2450
+ return { results, stoppedBy, pruneTargets };
2451
+ };
2452
+ for (const move of input.plan.moves) {
2453
+ const stoppedBy = yield step(
2454
+ () => pushMove(move),
2455
+ (error, exitCode) => ({
2456
+ sourcePath: move.to,
2457
+ status: "failed",
2458
+ error: describeError(error),
2459
+ exitCode,
2460
+ movedFrom: move.from
2461
+ })
2462
+ );
2463
+ if (stoppedBy) {
2464
+ return stopped(stoppedBy);
2465
+ }
2466
+ }
2467
+ for (const entry of input.plan.push) {
2468
+ const stoppedBy = yield step(
2469
+ () => pushEntry(entry),
2470
+ (error, exitCode) => ({
2471
+ sourcePath: entry.sourcePath,
2472
+ status: "failed",
2473
+ error: describeError(error),
2474
+ exitCode
2475
+ })
2476
+ );
2477
+ if (stoppedBy) {
2478
+ return stopped(stoppedBy);
2479
+ }
2480
+ }
2481
+ if (input.prune) {
2482
+ pruneTargets.push(...pruneTargetsSoFar());
2483
+ for (const sourcePath of pruneTargets) {
2484
+ const locked = (_a2 = input.lock) == null ? void 0 : _a2.files[sourcePath];
2485
+ if (!locked || locked.state === "duplicate") {
2486
+ continue;
2487
+ }
2488
+ const stoppedBy = yield step(
2489
+ () => deleteEntry(sourcePath, locked.sha256),
2490
+ (error, exitCode) => ({
2491
+ sourcePath,
2492
+ status: "delete-failed",
2493
+ error: describeError(error),
2494
+ exitCode
2495
+ })
2496
+ );
2497
+ if (stoppedBy) {
2498
+ return stopped(stoppedBy);
2499
+ }
2500
+ }
2501
+ }
2502
+ return { results, stoppedBy: null, pruneTargets };
2503
+ });
1359
2504
  }
1360
-
1361
- // src/commands/instructions/index.ts
1362
- import path4 from "path";
1363
- import { Command as Command11 } from "commander";
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 FORBIDDEN_NAME_CHARACTER = /[<>:"|?*]/;
1373
- var RESERVED_ROOTS = [".git", ".fabric"];
1374
- var RESERVED_EXACT_PATHS = [
1375
- ".claude/settings.local.json",
1376
- ".codex/hooks.json"
1377
- ];
1378
- function hasControlCharacter(input) {
1379
- for (let index = 0; index < input.length; index++) {
1380
- const code = input.charCodeAt(index);
1381
- if (code <= 31 || code === 127) {
1382
- return true;
2505
+ function nextContextLock(input) {
2506
+ var _a2, _b, _c;
2507
+ const files = __spreadValues({}, (_b = (_a2 = input.previous) == null ? void 0 : _a2.files) != null ? _b : {});
2508
+ let changed = false;
2509
+ const drop = (sourcePath) => {
2510
+ if (sourcePath in files) {
2511
+ delete files[sourcePath];
2512
+ changed = true;
2513
+ }
2514
+ };
2515
+ const confirm = (sourcePath, sha256, contextId) => {
2516
+ const before = files[sourcePath];
2517
+ if ((before == null ? void 0 : before.state) === void 0 && (before == null ? void 0 : before.sha256) === sha256 && before.contextId === contextId) {
2518
+ return;
1383
2519
  }
2520
+ files[sourcePath] = { sha256, contextId };
2521
+ changed = true;
2522
+ };
2523
+ for (const sourcePath of (_c = input.forgotten) != null ? _c : []) {
2524
+ drop(sourcePath);
1384
2525
  }
1385
- return false;
1386
- }
1387
- function checkRelativePath(input) {
1388
- if (input.length === 0) {
1389
- return { ok: false, reason: "empty", detail: input };
2526
+ for (const result of input.results) {
2527
+ switch (result.status) {
2528
+ case "deleted":
2529
+ case "already-gone":
2530
+ drop(result.sourcePath);
2531
+ break;
2532
+ case "delete-in-progress":
2533
+ break;
2534
+ case "moved":
2535
+ drop(result.movedFrom);
2536
+ confirm(
2537
+ result.sourcePath,
2538
+ result.contentHash,
2539
+ result.contextId
2540
+ );
2541
+ break;
2542
+ case "duplicate": {
2543
+ const before = files[result.sourcePath];
2544
+ if ((before == null ? void 0 : before.state) !== "duplicate" || before.sha256 !== result.contentHash) {
2545
+ files[result.sourcePath] = {
2546
+ sha256: result.contentHash,
2547
+ state: "duplicate"
2548
+ };
2549
+ changed = true;
2550
+ }
2551
+ if (result.moveNotApplied === "source-missing" && result.movedFrom) {
2552
+ drop(result.movedFrom);
2553
+ }
2554
+ break;
2555
+ }
2556
+ case "created":
2557
+ case "updated":
2558
+ case "unchanged":
2559
+ confirm(
2560
+ result.sourcePath,
2561
+ result.contentHash,
2562
+ result.contextId
2563
+ );
2564
+ if (result.moveNotApplied === "source-missing" && result.movedFrom) {
2565
+ drop(result.movedFrom);
2566
+ }
2567
+ break;
2568
+ case "conflict":
2569
+ if (result.moveNotApplied === "source-missing" && result.movedFrom) {
2570
+ drop(result.movedFrom);
2571
+ }
2572
+ break;
2573
+ default:
2574
+ break;
2575
+ }
1390
2576
  }
1391
- if (hasControlCharacter(input)) {
1392
- return { ok: false, reason: "control_char", detail: input };
2577
+ if (!changed) {
2578
+ return null;
1393
2579
  }
1394
- if (input.includes("\\")) {
1395
- return { ok: false, reason: "backslash", detail: input };
2580
+ return {
2581
+ version: CONTEXT_LOCK_VERSION,
2582
+ projectId: input.projectId,
2583
+ pushedAt: input.now.toISOString(),
2584
+ files
2585
+ };
2586
+ }
2587
+
2588
+ // src/lib/context-sync/report.ts
2589
+ function countOutcomes(plan, results) {
2590
+ const count = (status) => results.filter((r) => r.status === status).length;
2591
+ return {
2592
+ created: count("created"),
2593
+ updated: count("updated"),
2594
+ unchanged: count("unchanged") + plan.unchangedLocal.length,
2595
+ duplicate: count("duplicate"),
2596
+ moved: count("moved"),
2597
+ moveUnsupported: count("move-unsupported"),
2598
+ conflict: count("conflict"),
2599
+ changedDuringRun: count("changed-during-run"),
2600
+ failed: count("failed"),
2601
+ removed: plan.removed.length,
2602
+ skipped: plan.skipped.length,
2603
+ deleted: count("deleted"),
2604
+ alreadyGone: count("already-gone"),
2605
+ deleteInProgress: count("delete-in-progress"),
2606
+ deleteConflict: count("delete-conflict"),
2607
+ deleteFailed: count("delete-failed")
2608
+ };
2609
+ }
2610
+ function who(current) {
2611
+ var _a2, _b;
2612
+ return (_b = (_a2 = current.contentUpdatedBy) == null ? void 0 : _a2.name) != null ? _b : "someone";
2613
+ }
2614
+ function when(current) {
2615
+ var _a2;
2616
+ return (_a2 = current.contentUpdatedAt) != null ? _a2 : "an unknown time";
2617
+ }
2618
+ function moveSourceConflictLine(result, from) {
2619
+ const { current } = result;
2620
+ if (current === null) {
2621
+ return `${from}: deleted on the server since your last push; not moved to ${result.sourcePath}`;
1396
2622
  }
1397
- if (input.startsWith("/") || DRIVE_LETTER.test(input)) {
1398
- return { ok: false, reason: "absolute", detail: input };
2623
+ let text = result.afterForce ? `changed on the server again by ${who(current)} at ${when(current)} while --force was moving it` : `changed on the server by ${who(current)} at ${when(current)} since your last push`;
2624
+ if (current.contentHash === null) {
2625
+ text += "; that version has no content hash, so --force cannot move it";
1399
2626
  }
1400
- if (input.endsWith("/")) {
1401
- return { ok: false, reason: "trailing_separator", detail: input };
2627
+ return `${from}: ${text}; not moved to ${result.sourcePath}`;
2628
+ }
2629
+ function conflictLine(result) {
2630
+ if (result.movedFrom !== void 0) {
2631
+ if (result.moveNotApplied === "source-changed") {
2632
+ return moveSourceConflictLine(result, result.movedFrom);
2633
+ }
2634
+ if (result.moveNotApplied === "source-missing") {
2635
+ return `${pushConflictLine(result)}; ${result.movedFrom} gone on the server`;
2636
+ }
2637
+ return `${pushConflictLine(result)}; ${result.movedFrom} not moved`;
1402
2638
  }
1403
- const segments = input.split("/");
1404
- if (segments.some((s) => s.length === 0)) {
1405
- return { ok: false, reason: "empty", detail: input };
2639
+ return pushConflictLine(result);
2640
+ }
2641
+ function pushConflictLine(result) {
2642
+ const { current } = result;
2643
+ if (current === null) {
2644
+ return `${result.sourcePath}: deleted on the server since your last push`;
1406
2645
  }
1407
- if (segments.some((s) => s === "." || s === "..")) {
1408
- return { ok: false, reason: "traversal", detail: input };
2646
+ let text;
2647
+ if (result.afterForce) {
2648
+ text = `changed on the server again by ${who(current)} at ${when(current)} while --force was replacing it`;
2649
+ } else if (result.wasLocked) {
2650
+ text = `changed on the server by ${who(current)} at ${when(current)} since your last push`;
2651
+ } else {
2652
+ text = `already on the server with different content, last changed by ${who(current)} at ${when(current)}`;
1409
2653
  }
1410
- for (const segment of segments) {
1411
- if (/[. ]$/.test(segment)) {
1412
- return {
1413
- ok: false,
1414
- reason: "trailing_dot_or_space",
1415
- detail: input
1416
- };
1417
- }
1418
- if (RESERVED_DEVICE_NAME.test(segment)) {
1419
- return {
1420
- ok: false,
1421
- reason: "reserved_device_name",
1422
- detail: input
1423
- };
1424
- }
1425
- if (FORBIDDEN_NAME_CHARACTER.test(segment)) {
1426
- return {
1427
- ok: false,
1428
- reason: "forbidden_character",
1429
- detail: input
1430
- };
1431
- }
2654
+ if (current.contentHash === null) {
2655
+ text += "; that version has no content hash, so --force cannot replace it";
1432
2656
  }
1433
- return { ok: true, path: input };
2657
+ return `${result.sourcePath}: ${text}`;
1434
2658
  }
1435
- function isReservedPath(input) {
1436
- var _a2;
1437
- const first = (_a2 = input.split("/")[0]) == null ? void 0 : _a2.toLowerCase();
1438
- if (RESERVED_ROOTS.some((root) => first === root)) {
1439
- return true;
2659
+ function deleteConflictLine(result) {
2660
+ const { current } = result;
2661
+ let text = result.afterForce ? `changed on the server again by ${who(current)} at ${when(current)} while --force was deleting it` : `changed on the server by ${who(current)} at ${when(current)} since your last push`;
2662
+ if (current.contentHash === null) {
2663
+ text += "; that version has no content hash, so --force cannot delete it";
1440
2664
  }
1441
- const key = collisionKey(input);
1442
- return RESERVED_EXACT_PATHS.some(
1443
- (reserved) => collisionKey(reserved) === key
1444
- );
2665
+ return `${result.sourcePath}: ${text}; not deleted`;
1445
2666
  }
1446
- function collisionKey(input) {
1447
- return input.normalize("NFC").toLowerCase();
2667
+ function storedLine(result) {
2668
+ if (result.overwrote) {
2669
+ return `${result.sourcePath}: replaced the version ${who(result.overwrote)} changed at ${when(result.overwrote)} (--force)`;
2670
+ }
2671
+ return result.sourcePath;
1448
2672
  }
1449
- function findCollision(paths) {
1450
- const seen = /* @__PURE__ */ new Map();
1451
- for (const candidate of paths) {
1452
- const key = collisionKey(candidate);
1453
- const previous = seen.get(key);
1454
- if (previous !== void 0) {
1455
- return { first: previous, second: candidate };
1456
- }
1457
- seen.set(key, candidate);
2673
+ function isMoveFallback(result) {
2674
+ return (result.status === "created" || result.status === "updated" || result.status === "unchanged" || result.status === "duplicate") && result.movedFrom !== void 0;
2675
+ }
2676
+ function moveFallbackLine(result, prune) {
2677
+ var _a2, _b;
2678
+ const from = (_a2 = result.movedFrom) != null ? _a2 : "";
2679
+ const to = result.sourcePath;
2680
+ let pushed;
2681
+ if (result.status === "duplicate") {
2682
+ pushed = `${to} pushed as a duplicate of ${(_b = result.duplicateOfSourcePath) != null ? _b : "another source in this project"}`;
2683
+ } else if (result.overwrote) {
2684
+ pushed = `${to} replaced the version ${who(result.overwrote)} changed at ${when(result.overwrote)} (--force)`;
2685
+ } else {
2686
+ pushed = `${to} pushed as ${result.status}`;
2687
+ }
2688
+ const kept = prune ? "" : `; ${from} kept`;
2689
+ switch (result.moveNotApplied) {
2690
+ case "source-missing":
2691
+ return `${from}: gone on the server; ${pushed}`;
2692
+ case "target-exists":
2693
+ return `${from}: not moved, ${to} is already on the server; ${pushed}${kept}`;
2694
+ case "content-differs":
2695
+ return `${from}: not moved, the server's version of it differs from ${to}; ${pushed}${kept}`;
2696
+ default:
2697
+ return `${from}: not moved; ${pushed}${kept}`;
1458
2698
  }
1459
- return null;
1460
2699
  }
1461
- function describeRejection(rejection) {
1462
- switch (rejection.reason) {
1463
- case "empty":
1464
- return `empty or malformed path (${JSON.stringify(rejection.detail)})`;
1465
- case "absolute":
1466
- return `absolute path refused: ${rejection.detail}`;
1467
- case "traversal":
1468
- return `path traversal refused: ${rejection.detail}`;
1469
- case "backslash":
1470
- return `backslash in path refused: ${rejection.detail}`;
1471
- case "control_char":
1472
- return `control character in path refused: ${JSON.stringify(rejection.detail)}`;
1473
- case "trailing_separator":
1474
- return `path names a directory, not a file: ${rejection.detail}`;
1475
- case "trailing_dot_or_space":
1476
- return `path segment ending in a dot or space refused: ${rejection.detail}`;
1477
- case "reserved_device_name":
1478
- return `reserved device name refused: ${rejection.detail}`;
1479
- case "forbidden_character":
1480
- return `path contains a character Windows will not put in a filename (< > : " | ? *): ${rejection.detail}`;
1481
- case "reserved_path":
1482
- return `this tool never writes or deletes ${RESERVED_ROOTS.join(", ")} or ${RESERVED_EXACT_PATHS.join(", ")}: ${rejection.detail}`;
1483
- case "not_a_regular_file":
1484
- return `path is not a regular file: ${rejection.detail}`;
1485
- case "escapes_destination":
1486
- return `path resolves outside the destination: ${rejection.detail}`;
1487
- case "symlink":
1488
- return `refusing to follow a symlink: ${rejection.detail}`;
1489
- case "not_a_directory":
1490
- return `a path component is not a directory: ${rejection.detail}`;
1491
- case "collision":
1492
- return `two paths name the same file on a case- or Unicode-insensitive filesystem: ${rejection.detail}`;
2700
+ function skippedLine(skip) {
2701
+ if (skip.reason === "ignored") {
2702
+ return `${skip.path}: now excluded by the ignore rules; server entry kept`;
1493
2703
  }
2704
+ return skip.detail ? `${skip.path}: ${skip.reason} (${skip.detail})` : `${skip.path}: ${skip.reason}`;
1494
2705
  }
1495
-
1496
- // src/lib/instructions/safe-write.ts
1497
- import { createHash, randomBytes } from "crypto";
1498
- import { constants } from "fs";
1499
- import {
1500
- lstat,
1501
- mkdir,
1502
- open,
1503
- readFile,
1504
- realpath,
1505
- rename,
1506
- unlink
1507
- } from "fs/promises";
1508
- import path from "path";
1509
- var ALLOWED_MODES = /* @__PURE__ */ new Set([420, 493]);
1510
- function isAllowedMode(mode) {
1511
- if (mode === null || mode === void 0) {
1512
- return true;
2706
+ function group(lines, label, items) {
2707
+ if (items.length === 0) {
2708
+ return;
1513
2709
  }
1514
- if (!Number.isInteger(mode) || mode < 0) {
1515
- return false;
2710
+ lines.push(`${label} (${items.length})`);
2711
+ for (const item of items) {
2712
+ lines.push(` ${item}`);
1516
2713
  }
1517
- const type = mode & 61440;
1518
- if (type !== 0 && type !== 32768) {
1519
- return false;
2714
+ }
2715
+ function formatPushReport(input) {
2716
+ var _a2;
2717
+ const { plan, results } = input;
2718
+ const prune = Boolean(input.prune);
2719
+ const of = (status) => results.filter(
2720
+ (r) => r.status === status && !isMoveFallback(r)
2721
+ );
2722
+ const lines = [
2723
+ `Context push from ${input.directory} to project ${input.projectId}:`
2724
+ ];
2725
+ group(lines, "created", of("created").map(storedLine));
2726
+ group(lines, "updated", of("updated").map(storedLine));
2727
+ group(
2728
+ lines,
2729
+ "moved",
2730
+ of("moved").map((r) => `${r.movedFrom} -> ${r.sourcePath}`)
2731
+ );
2732
+ group(lines, "not moved", [
2733
+ ...results.filter(isMoveFallback).map((r) => moveFallbackLine(r, prune)),
2734
+ ...of("move-unsupported").map(
2735
+ (r) => `${r.movedFrom} -> ${r.sourcePath}: the server does not support moves yet; ${r.movedFrom} kept, ${r.sourcePath} not pushed`
2736
+ )
2737
+ ]);
2738
+ group(
2739
+ lines,
2740
+ "duplicate",
2741
+ of("duplicate").map(
2742
+ (r) => {
2743
+ var _a3;
2744
+ return `${r.sourcePath}: duplicate of ${(_a3 = r.duplicateOfSourcePath) != null ? _a3 : "another source in this project"}`;
2745
+ }
2746
+ )
2747
+ );
2748
+ group(lines, "conflict", [
2749
+ ...of("conflict").map(conflictLine),
2750
+ ...of("delete-conflict").map(deleteConflictLine)
2751
+ ]);
2752
+ group(
2753
+ lines,
2754
+ "changed during the run",
2755
+ of("changed-during-run").map(
2756
+ (r) => `${r.sourcePath}: changed-during-run; not sent, the next push sends the new content`
2757
+ )
2758
+ );
2759
+ group(lines, "failed", [
2760
+ ...of("failed").map((r) => `${r.sourcePath}: ${r.error}`),
2761
+ ...of("delete-failed").map(
2762
+ (r) => `${r.sourcePath}: ${r.error}; not deleted`
2763
+ )
2764
+ ]);
2765
+ group(
2766
+ lines,
2767
+ "deleted",
2768
+ of("deleted").map(
2769
+ (r) => r.overwrote ? `${r.sourcePath}: deleted the version ${who(r.overwrote)} changed at ${when(r.overwrote)} (--force)` : r.sourcePath
2770
+ )
2771
+ );
2772
+ group(
2773
+ lines,
2774
+ "already gone",
2775
+ of("already-gone").map(
2776
+ (r) => `${r.sourcePath}: already gone on the server`
2777
+ )
2778
+ );
2779
+ group(
2780
+ lines,
2781
+ "deletion still running",
2782
+ of("delete-in-progress").map(
2783
+ (r) => `${r.sourcePath}: deletion still running on the server; run again to confirm`
2784
+ )
2785
+ );
2786
+ const pruned = new Set(
2787
+ results.filter((r) => DELETE_STATUSES.has(r.status)).map((r) => r.sourcePath)
2788
+ );
2789
+ const kept = [
2790
+ ...plan.removed,
2791
+ ...((_a2 = input.pruneTargets) != null ? _a2 : []).filter((p) => !plan.removed.includes(p))
2792
+ ];
2793
+ group(
2794
+ lines,
2795
+ "removed",
2796
+ kept.filter((p) => !pruned.has(p)).map((p) => `${p}: removed locally; server entry kept`)
2797
+ );
2798
+ group(lines, "skipped", plan.skipped.map(skippedLine));
2799
+ const unchanged = countOutcomes(plan, results).unchanged;
2800
+ if (unchanged > 0) {
2801
+ lines.push(`unchanged (${unchanged})`);
1520
2802
  }
1521
- return ALLOWED_MODES.has(mode & 4095);
2803
+ return lines;
2804
+ }
2805
+ function formatDryRunReport(input) {
2806
+ const { plan } = input;
2807
+ const lines = [
2808
+ `Dry run of a context push from ${input.directory} to project ${input.projectId}: nothing was sent and the lock was not written.`
2809
+ ];
2810
+ group(
2811
+ lines,
2812
+ "moved",
2813
+ plan.moves.map((move) => `${move.from} -> ${move.to}`)
2814
+ );
2815
+ group(
2816
+ lines,
2817
+ "new",
2818
+ plan.push.filter((entry) => entry.expectedContentHash === void 0).map((entry) => entry.sourcePath)
2819
+ );
2820
+ group(
2821
+ lines,
2822
+ "changed",
2823
+ plan.push.filter((entry) => entry.expectedContentHash !== void 0).map((entry) => entry.sourcePath)
2824
+ );
2825
+ if (input.prune) {
2826
+ group(lines, "would delete", plan.removed);
2827
+ } else {
2828
+ group(
2829
+ lines,
2830
+ "removed",
2831
+ plan.removed.map(
2832
+ (p) => `${p}: removed locally; server entry would be kept`
2833
+ )
2834
+ );
2835
+ }
2836
+ group(lines, "skipped", plan.skipped.map(skippedLine));
2837
+ if (plan.unchangedLocal.length > 0) {
2838
+ lines.push(`unchanged (${plan.unchangedLocal.length})`);
2839
+ }
2840
+ return lines;
2841
+ }
2842
+ function contextPushJson(input) {
2843
+ return {
2844
+ projectId: input.projectId,
2845
+ directory: input.directory,
2846
+ dryRun: input.dryRun,
2847
+ force: input.force,
2848
+ prune: input.prune,
2849
+ lock: input.lockPath,
2850
+ lockWritten: input.lockWritten,
2851
+ plan: {
2852
+ push: input.plan.push.map((entry) => {
2853
+ var _a2;
2854
+ return {
2855
+ sourcePath: entry.sourcePath,
2856
+ sha256: entry.sha256,
2857
+ bytes: entry.bytes,
2858
+ expectedContentHash: (_a2 = entry.expectedContentHash) != null ? _a2 : null
2859
+ };
2860
+ }),
2861
+ moves: input.plan.moves.map((move) => ({
2862
+ from: move.from,
2863
+ to: move.to,
2864
+ sha256: move.sha256,
2865
+ contextId: move.contextId
2866
+ })),
2867
+ unchangedLocal: input.plan.unchangedLocal,
2868
+ removed: input.plan.removed,
2869
+ forgotten: input.plan.forgotten,
2870
+ skipped: input.plan.skipped
2871
+ },
2872
+ results: input.results,
2873
+ counts: countOutcomes(input.plan, input.results)
2874
+ };
2875
+ }
2876
+
2877
+ // src/commands/context/index.ts
2878
+ var HOOK_DEADLINE_MS = 1e4;
2879
+ var PUSH_TIMEOUT_MS = 6e4;
2880
+ function collectPattern(value, previous) {
2881
+ return [...previous, value];
2882
+ }
2883
+ function buildContextCommand() {
2884
+ const context = new Command8("context").description(
2885
+ "Sync local knowledge files into a project's Context"
2886
+ );
2887
+ context.command("push").description(
2888
+ "Push every text file under <dir> into the project's Context by its relative path"
2889
+ ).argument(
2890
+ "<dir>",
2891
+ "Folder to push (required; never the working directory by default)"
2892
+ ).requiredOption("--project <id>", "Project ID").option("--org <slug>", "Organization context").option(
2893
+ "--force",
2894
+ "On a conflict, replace the server's version once with this folder's"
2895
+ ).option(
2896
+ "--prune",
2897
+ "Delete the server entries of files removed locally, only in the version last pushed"
2898
+ ).option("--dry-run", "Print the plan; send nothing and write no lock").option(
2899
+ "--exclude <pattern>",
2900
+ "Also leave out paths matching this gitignore pattern (repeatable)",
2901
+ collectPattern,
2902
+ []
2903
+ ).option(
2904
+ "--hook",
2905
+ "Hook mode: never fails, never blocks, one line on stderr on failure"
2906
+ ).option("--format <format>", "Output format: text|json").action(function(dir, opts) {
2907
+ return __async(this, null, function* () {
2908
+ yield run(opts, () => runPush(dir, opts, outputFormatFor(this)));
2909
+ });
2910
+ });
2911
+ return context;
1522
2912
  }
1523
- function permissionBits(mode) {
1524
- if (mode === null || mode === void 0) {
1525
- return null;
2913
+ function run(opts, body) {
2914
+ return __async(this, null, function* () {
2915
+ try {
2916
+ if (opts.hook) {
2917
+ yield withDeadline(HOOK_DEADLINE_MS, () => body());
2918
+ } else {
2919
+ yield body();
2920
+ }
2921
+ } catch (error) {
2922
+ const message = describeError(error);
2923
+ if (opts.hook) {
2924
+ process.stderr.write(`fabric: context push failed: ${message}
2925
+ `);
2926
+ process.exit(0);
2927
+ }
2928
+ process.stderr.write(`\u2717 ${message}
2929
+ `);
2930
+ process.exit(error instanceof CliFailure ? error.exitCode : 1);
2931
+ }
2932
+ });
2933
+ }
2934
+ function contextClient(opts) {
2935
+ if (!getApiKey()) {
2936
+ throw new CliFailure(
2937
+ "Not authenticated. Run: fabric auth login --key <api-key>",
2938
+ 3
2939
+ );
1526
2940
  }
1527
- return mode & 4095;
2941
+ return getClient(
2942
+ opts.hook ? { timeoutMs: HOOK_DEADLINE_MS, retry: { maxRetries: 0 } } : { timeoutMs: PUSH_TIMEOUT_MS }
2943
+ ).withoutContext();
1528
2944
  }
1529
- function resolveDestinationRoot(destination) {
2945
+ function resolveFolder(dir) {
1530
2946
  return __async(this, null, function* () {
1531
- yield mkdir(destination, { recursive: true });
1532
- return realpath(destination);
2947
+ const directory = path5.resolve(dir);
2948
+ const root = yield resolveExistingRoot(directory);
2949
+ const stats = yield lstat3(root).catch(() => null);
2950
+ if (stats === null || !stats.isDirectory()) {
2951
+ throw new CliFailure(`${directory} is not a directory.`, 2);
2952
+ }
2953
+ return root;
1533
2954
  });
1534
2955
  }
1535
- function assertSafeTarget(root, relativePath) {
2956
+ function readLockForProject(root, projectId) {
1536
2957
  return __async(this, null, function* () {
1537
- const target = path.resolve(root, relativePath);
1538
- if (target === root || !target.startsWith(root + path.sep)) {
1539
- return {
1540
- ok: false,
1541
- reason: "escapes_destination",
1542
- detail: relativePath
1543
- };
1544
- }
1545
- const segments = relativePath.split("/");
1546
- let current = root;
1547
- let deepestExisting = root;
1548
- for (const [index, segment] of segments.entries()) {
1549
- current = path.join(current, segment);
1550
- const stats = yield lstatOrNull(current);
1551
- if (stats === null) {
1552
- break;
1553
- }
1554
- if (stats.isSymbolicLink()) {
1555
- return { ok: false, reason: "symlink", detail: current };
1556
- }
1557
- if (index < segments.length - 1 && !stats.isDirectory()) {
1558
- return { ok: false, reason: "not_a_directory", detail: current };
1559
- }
1560
- deepestExisting = current;
2958
+ let lock;
2959
+ try {
2960
+ lock = yield readContextLock(root);
2961
+ } catch (error) {
2962
+ throw new CliFailure(describeError(error), 7);
1561
2963
  }
1562
- if (deepestExisting !== root) {
1563
- const canonical = yield realpath(deepestExisting).catch(() => null);
1564
- if (canonical !== null && canonical !== root && !canonical.startsWith(root + path.sep)) {
1565
- return {
1566
- ok: false,
1567
- reason: "escapes_destination",
1568
- detail: relativePath
1569
- };
1570
- }
2964
+ if (lock !== null && lock.projectId !== projectId) {
2965
+ throw new CliFailure(
2966
+ `${contextLockPath(root)} belongs to project ${lock.projectId}, not ${projectId}. Push this folder to ${lock.projectId}, or remove that lock if this folder should now feed ${projectId}.`,
2967
+ 7
2968
+ );
1571
2969
  }
1572
- return { ok: true, path: target };
2970
+ return lock;
1573
2971
  });
1574
2972
  }
1575
- function assertWritableTarget(root, relativePath) {
2973
+ function print(lines) {
2974
+ process.stdout.write(`${lines.join("\n")}
2975
+ `);
2976
+ }
2977
+ function runPush(dir, opts, format) {
1576
2978
  return __async(this, null, function* () {
1577
- const check = yield assertSafeTarget(root, relativePath);
1578
- if (!check.ok) {
1579
- return check;
2979
+ const root = yield resolveFolder(dir);
2980
+ const client = opts.dryRun ? null : contextClient(opts);
2981
+ const lock = yield readLockForProject(root, opts.project);
2982
+ const plan = yield computeContextPlan({
2983
+ root,
2984
+ lock,
2985
+ excludes: opts.exclude
2986
+ });
2987
+ if (client === null) {
2988
+ if (format === "json") {
2989
+ printOutput(
2990
+ jsonOutcome({
2991
+ root,
2992
+ opts,
2993
+ plan,
2994
+ results: [],
2995
+ lockWritten: false
2996
+ }),
2997
+ { format: "json" }
2998
+ );
2999
+ return;
3000
+ }
3001
+ print(
3002
+ formatDryRunReport({
3003
+ projectId: opts.project,
3004
+ directory: root,
3005
+ plan,
3006
+ prune: Boolean(opts.prune)
3007
+ })
3008
+ );
3009
+ return;
1580
3010
  }
1581
- const stats = yield lstatOrNull(check.path);
1582
- if (stats === null) {
1583
- return check;
3011
+ const pushed = yield pushContextPlan({
3012
+ client,
3013
+ projectId: opts.project,
3014
+ org: opts.org,
3015
+ root,
3016
+ plan,
3017
+ force: Boolean(opts.force),
3018
+ prune: Boolean(opts.prune),
3019
+ lock
3020
+ });
3021
+ const next = nextContextLock({
3022
+ previous: lock,
3023
+ projectId: opts.project,
3024
+ results: pushed.results,
3025
+ forgotten: plan.forgotten,
3026
+ now: /* @__PURE__ */ new Date()
3027
+ });
3028
+ if (next !== null) {
3029
+ yield writeContextLock(root, next);
1584
3030
  }
1585
- if (stats.isSymbolicLink()) {
1586
- return { ok: false, reason: "symlink", detail: check.path };
3031
+ if (format === "json") {
3032
+ printOutput(
3033
+ jsonOutcome({
3034
+ root,
3035
+ opts,
3036
+ plan,
3037
+ results: pushed.results,
3038
+ lockWritten: next !== null
3039
+ }),
3040
+ { format: "json" }
3041
+ );
3042
+ } else {
3043
+ print(
3044
+ formatPushReport({
3045
+ projectId: opts.project,
3046
+ directory: root,
3047
+ plan,
3048
+ results: pushed.results,
3049
+ prune: Boolean(opts.prune),
3050
+ pruneTargets: pushed.pruneTargets
3051
+ })
3052
+ );
1587
3053
  }
1588
- if (!stats.isFile()) {
1589
- return { ok: false, reason: "not_a_regular_file", detail: check.path };
3054
+ const prune = Boolean(opts.prune);
3055
+ if (pushed.stoppedBy !== null) {
3056
+ throw prune ? new CliFailure(
3057
+ `${pushed.stoppedBy.message} (${pruneSummary(pushed.results, pushed.pruneTargets)})`,
3058
+ pushed.stoppedBy.exitCode
3059
+ ) : pushed.stoppedBy;
1590
3060
  }
1591
- return check;
3061
+ throwIfIncomplete(pushed.results, prune, pushed.pruneTargets);
1592
3062
  });
1593
3063
  }
1594
- function readFileSafely(_0, _1) {
1595
- return __async(this, arguments, function* (root, relativePath, options = {}) {
1596
- const check = yield assertWritableTarget(root, relativePath);
1597
- if (!check.ok) {
1598
- throw new Error(`Refusing to sync: ${describeRejection(check)}.`);
3064
+ function jsonOutcome(input) {
3065
+ return contextPushJson({
3066
+ projectId: input.opts.project,
3067
+ directory: input.root,
3068
+ lockPath: contextLockPath(input.root),
3069
+ lockWritten: input.lockWritten,
3070
+ dryRun: Boolean(input.opts.dryRun),
3071
+ force: Boolean(input.opts.force),
3072
+ prune: Boolean(input.opts.prune),
3073
+ plan: input.plan,
3074
+ results: input.results
3075
+ });
3076
+ }
3077
+ function pruneSummary(results, targets) {
3078
+ const count = (status) => results.filter((r) => r.status === status).length;
3079
+ const settled = new Set(
3080
+ results.filter(
3081
+ (r) => r.status === "deleted" || r.status === "already-gone"
3082
+ ).map((r) => r.sourcePath)
3083
+ );
3084
+ const notDeleted = targets.filter((path9) => !settled.has(path9)).length;
3085
+ let text = `--prune: ${count("deleted")} deleted, ${count("already-gone")} already gone, ${notDeleted} not deleted`;
3086
+ const first = results.find((r) => r.status === "delete-failed");
3087
+ if (first && first.status === "delete-failed") {
3088
+ text += `, first ${first.sourcePath}: ${first.error}`;
3089
+ }
3090
+ return text;
3091
+ }
3092
+ function throwIfIncomplete(results, prune, pruneTargets) {
3093
+ var _a2;
3094
+ const pushes = results.filter((r) => !DELETE_STATUSES.has(r.status));
3095
+ const failed = pushes.filter((r) => r.status === "failed");
3096
+ const conflicts = pushes.filter((r) => r.status === "conflict");
3097
+ const unsupported = pushes.filter((r) => r.status === "move-unsupported");
3098
+ const deleteFailed = results.filter((r) => r.status === "delete-failed");
3099
+ const deleteConflicts = results.filter(
3100
+ (r) => r.status === "delete-conflict"
3101
+ );
3102
+ const stillDeleting = results.filter(
3103
+ (r) => r.status === "delete-in-progress"
3104
+ );
3105
+ if (failed.length === 0 && conflicts.length === 0 && unsupported.length === 0 && deleteFailed.length === 0 && deleteConflicts.length === 0 && stillDeleting.length === 0) {
3106
+ return;
3107
+ }
3108
+ const sentences = [];
3109
+ const total = conflicts.length + failed.length + unsupported.length;
3110
+ if (total > 0) {
3111
+ const parts = [];
3112
+ if (conflicts.length > 0) {
3113
+ parts.push(
3114
+ `${conflicts.length} conflict${conflicts.length === 1 ? "" : "s"}`
3115
+ );
1599
3116
  }
1600
- let handle;
1601
- try {
1602
- handle = yield open(
1603
- check.path,
1604
- constants.O_RDONLY | constants.O_NOFOLLOW | constants.O_NONBLOCK
3117
+ if (unsupported.length > 0) {
3118
+ parts.push(
3119
+ `${unsupported.length} move${unsupported.length === 1 ? "" : "s"} the server does not support yet`
1605
3120
  );
1606
- } catch (error) {
1607
- if (error.code === "ENOENT") {
1608
- return null;
1609
- }
1610
- throw error;
1611
3121
  }
1612
- try {
1613
- const opened = yield handle.stat();
1614
- if (!opened.isFile()) {
1615
- throw new Error(
1616
- `Refusing to sync: ${relativePath} is not a regular file.`
1617
- );
1618
- }
1619
- const afterOpen = yield assertWritableTarget(root, relativePath);
1620
- if (!afterOpen.ok) {
1621
- throw new Error(
1622
- `Refusing to sync: ${describeRejection(afterOpen)}.`
1623
- );
1624
- }
1625
- const current = yield lstatOrNull(afterOpen.path);
1626
- if (current === null || !current.isFile() || current.dev !== opened.dev || current.ino !== opened.ino) {
1627
- throw new Error(
1628
- `Refusing to sync: ${relativePath} changed while it was being opened.`
1629
- );
1630
- }
1631
- if (options.maxBytes === void 0) {
1632
- return { bytes: yield handle.readFile(), mode: opened.mode };
1633
- }
1634
- if (!Number.isSafeInteger(options.maxBytes) || options.maxBytes < 0) {
1635
- throw new Error(
1636
- "A safe file-read limit must be a non-negative integer."
1637
- );
1638
- }
1639
- if (opened.size > options.maxBytes) {
1640
- throw new Error(
1641
- `${relativePath} is too large to read safely (maximum ${options.maxBytes} bytes).`
1642
- );
3122
+ const first = failed[0];
3123
+ if (first && first.status === "failed") {
3124
+ parts.push(
3125
+ `${failed.length} failed, first ${first.sourcePath}: ${first.error}`
3126
+ );
3127
+ }
3128
+ sentences.push(
3129
+ `${total} of ${pushes.length} file${pushes.length === 1 ? "" : "s"} sent ${total === 1 ? "was" : "were"} not stored (${parts.join(", ")}).`
3130
+ );
3131
+ if (conflicts.length > 0) {
3132
+ sentences.push(
3133
+ "A conflict means the server holds a version this folder did not name, or deleted the one it named: compare the two in the project's Context tab, and use --force to replace the version the conflict reported with this folder's (or to send a deleted file again with no version, which recreates it, or answers duplicate if that content is already stored under another path)."
3134
+ );
3135
+ }
3136
+ }
3137
+ if (unsupported.length > 0) {
3138
+ sentences.push(
3139
+ "This server does not support moves yet: a moved file's old path stays on the server and in the lock, its new path is not pushed, and the move is sent again on the next push."
3140
+ );
3141
+ }
3142
+ if (prune) {
3143
+ sentences.push(`${pruneSummary(results, pruneTargets)}.`);
3144
+ if (deleteConflicts.length > 0) {
3145
+ sentences.push(
3146
+ "A file changed on the server since your last push is not deleted: compare it in the project's Context tab, and use --force to delete the version the conflict reported."
3147
+ );
3148
+ }
3149
+ if (stillDeleting.length > 0) {
3150
+ sentences.push(
3151
+ "A deletion still running on the server keeps its lock entry: run again to confirm it."
3152
+ );
3153
+ }
3154
+ }
3155
+ const firstFailure = (_a2 = failed[0]) != null ? _a2 : deleteFailed[0];
3156
+ throw new CliFailure(
3157
+ sentences.join(" "),
3158
+ firstFailure && (firstFailure.status === "failed" || firstFailure.status === "delete-failed") ? firstFailure.exitCode : 1
3159
+ );
3160
+ }
3161
+
3162
+ // src/commands/ctx/index.ts
3163
+ import { Command as Command9 } from "commander";
3164
+ function buildCtxCommand() {
3165
+ const ctx = new Command9("ctx").description(
3166
+ "Manage the organization commands run in"
3167
+ );
3168
+ ctx.command("current").description("Show the active default context").action(() => {
3169
+ const stored = getDefaultContext();
3170
+ if (!stored) {
3171
+ printWarning(
3172
+ "No default context set. Use --org <slug> on each command, or run:\n fabric ctx use org <slug>"
3173
+ );
3174
+ return;
3175
+ }
3176
+ if (stored.type === "personal") {
3177
+ printWarning(
3178
+ "Context: personal \u2014 retired, and no command will accept it.\nReplace it with:\n fabric ctx use org <slug>"
3179
+ );
3180
+ return;
3181
+ }
3182
+ process.stdout.write(`Context: org:${stored.slug}
3183
+ `);
3184
+ });
3185
+ const use = new Command9("use").description(
3186
+ "Set the default organization for commands"
3187
+ );
3188
+ use.command("personal").description("Retired \u2014 context is organization-only").action(() => {
3189
+ printError(
3190
+ "Personal context no longer exists \u2014 every command runs inside an organization.\nSet one with:\n fabric ctx use org <slug>",
3191
+ 2
3192
+ );
3193
+ });
3194
+ use.command("org <slug>").description("Use an org context by default").action((slug) => {
3195
+ saveDefaultContext({ type: "org", slug });
3196
+ printSuccess(`Default context set to: org:${slug}`);
3197
+ });
3198
+ ctx.addCommand(use);
3199
+ return ctx;
3200
+ }
3201
+
3202
+ // src/commands/features/index.ts
3203
+ import { Command as Command10 } from "commander";
3204
+ function buildFeaturesCommand() {
3205
+ const features = new Command10("features").description(
3206
+ "Manage project features"
3207
+ );
3208
+ features.command("list").description("List features in a project").requiredOption("-p, --project <id>", "Project ID").option("--org <slug>", "Organization context").option("--personal", "Retired \u2014 context is organization-only").option(
3209
+ "--priority <priority>",
3210
+ "Filter: P0_CRITICAL|P1_HIGH|P2_MEDIUM|P3_LOW"
3211
+ ).option("--search <query>", "Search by title").option("--limit <n>", "Max results (default 50)", "50").option(
3212
+ "--format <format>",
3213
+ "Output format: table|json|yaml|csv",
3214
+ "table"
3215
+ ).option("-q, --quiet", "Print IDs only").action(
3216
+ (opts) => __async(null, null, function* () {
3217
+ const ctx = resolveContext(opts.personal, opts.org, false);
3218
+ const client = getClient();
3219
+ let list;
3220
+ try {
3221
+ list = yield client.projects.listFeatures(opts.project, {
3222
+ org: (ctx == null ? void 0 : ctx.type) === "org" ? ctx.slug : void 0,
3223
+ priority: opts.priority,
3224
+ search: opts.search,
3225
+ limit: Number(opts.limit)
3226
+ });
3227
+ } catch (err) {
3228
+ printError(err.message, 1);
1643
3229
  }
1644
- const buffer = Buffer.allocUnsafe(options.maxBytes + 1);
1645
- let total = 0;
1646
- while (total <= options.maxBytes) {
1647
- const { bytesRead } = yield handle.read(
1648
- buffer,
1649
- total,
1650
- options.maxBytes + 1 - total,
1651
- total
1652
- );
1653
- if (bytesRead === 0) {
1654
- break;
3230
+ if (opts.quiet) {
3231
+ for (const f of list) {
3232
+ process.stdout.write(`${f.id}
3233
+ `);
1655
3234
  }
1656
- total += bytesRead;
1657
- }
1658
- if (total > options.maxBytes) {
1659
- throw new Error(
1660
- `${relativePath} is too large to read safely (maximum ${options.maxBytes} bytes).`
1661
- );
3235
+ return;
1662
3236
  }
1663
- return { bytes: buffer.subarray(0, total), mode: opened.mode };
1664
- } finally {
1665
- yield handle.close();
1666
- }
1667
- });
1668
- }
1669
- function resolveExistingRoot(destination) {
1670
- return __async(this, null, function* () {
1671
- return realpath(destination).catch(() => path.resolve(destination));
1672
- });
1673
- }
1674
- function makeParents(root, relativePath) {
1675
- return __async(this, null, function* () {
1676
- const segments = relativePath.split("/").slice(0, -1);
1677
- let current = root;
1678
- for (const segment of segments) {
1679
- current = path.join(current, segment);
1680
- const stats = yield lstatOrNull(current);
1681
- if (stats === null) {
1682
- yield mkdir(current).catch((error) => __async(null, null, function* () {
1683
- if (error.code !== "EEXIST") {
1684
- throw error;
1685
- }
1686
- const raced = yield lstatOrNull(current);
1687
- if (raced == null ? void 0 : raced.isSymbolicLink()) {
1688
- throw new Error(
1689
- `Refusing to sync: ${describeRejection({ ok: false, reason: "symlink", detail: current })}.`
1690
- );
3237
+ printOutput(list, {
3238
+ format: opts.format,
3239
+ columns: ["identifier", "title", "priority", "createdAt"]
3240
+ });
3241
+ })
3242
+ );
3243
+ features.command("show <id>").description("Show feature details").requiredOption("-p, --project <projectId>", "Project ID").option("--org <slug>", "Organization context").option("--personal", "Retired \u2014 context is organization-only").option("--format <format>", "Output format: table|json|yaml", "table").action(
3244
+ (id, opts) => __async(null, null, function* () {
3245
+ var _a2, _b, _c;
3246
+ const ctx = resolveContext(opts.personal, opts.org, false);
3247
+ const client = getClient();
3248
+ let feature;
3249
+ try {
3250
+ feature = yield client.projects.getFeature(
3251
+ opts.project,
3252
+ id,
3253
+ {
3254
+ org: (ctx == null ? void 0 : ctx.type) === "org" ? ctx.slug : void 0
1691
3255
  }
1692
- }));
1693
- continue;
1694
- }
1695
- if (stats.isSymbolicLink()) {
1696
- throw new Error(
1697
- `Refusing to sync: ${describeRejection({ ok: false, reason: "symlink", detail: current })}.`
1698
3256
  );
3257
+ } catch (err) {
3258
+ printError(err.message, 1);
1699
3259
  }
1700
- if (!stats.isDirectory()) {
1701
- throw new Error(
1702
- `Refusing to sync: ${describeRejection({ ok: false, reason: "not_a_directory", detail: current })}.`
3260
+ const fmt = opts.format;
3261
+ if (fmt === "json" || fmt === "yaml") {
3262
+ printOutput(feature, { format: fmt });
3263
+ return;
3264
+ }
3265
+ printRecord({
3266
+ ID: feature.id,
3267
+ Identifier: feature.identifier,
3268
+ Title: feature.title,
3269
+ Priority: feature.priority,
3270
+ Description: (_a2 = feature.description) != null ? _a2 : "(none)",
3271
+ "Acceptance Criteria": (_b = feature.acceptanceCriteria) != null ? _b : "(none)",
3272
+ Project: feature.projectId,
3273
+ Assignee: (_c = feature.assigneeId) != null ? _c : "(none)",
3274
+ Created: feature.createdAt.slice(0, 10)
3275
+ });
3276
+ })
3277
+ );
3278
+ features.command("create <title>").description("Create a feature in a project").requiredOption("-p, --project <projectId>", "Project ID").option("--org <slug>", "Organization context").option("--personal", "Retired \u2014 context is organization-only").option("-d, --description <text>", "Feature description").option(
3279
+ "--priority <priority>",
3280
+ "Priority: P0_CRITICAL|P1_HIGH|P2_MEDIUM|P3_LOW (default: P2_MEDIUM)"
3281
+ ).option("--format <format>", "Output format: table|json|yaml", "table").action(
3282
+ (title, opts) => __async(null, null, function* () {
3283
+ const ctx = resolveContext(opts.personal, opts.org, false);
3284
+ const client = getClient();
3285
+ let feature;
3286
+ try {
3287
+ feature = yield client.projects.createFeature(
3288
+ opts.project,
3289
+ {
3290
+ title,
3291
+ description: opts.description,
3292
+ priority: opts.priority,
3293
+ org: (ctx == null ? void 0 : ctx.type) === "org" ? ctx.slug : void 0
3294
+ }
1703
3295
  );
3296
+ } catch (err) {
3297
+ printError(err.message, 1);
1704
3298
  }
1705
- }
1706
- });
1707
- }
1708
- function writeFileSafely(input) {
1709
- return __async(this, null, function* () {
1710
- var _a2;
1711
- const check = yield assertSafeTarget(input.root, input.relativePath);
1712
- if (!check.ok) {
1713
- throw new Error(`Refusing to sync: ${describeRejection(check)}.`);
1714
- }
1715
- if (!isAllowedMode(input.mode)) {
1716
- throw new Error(
1717
- `Refusing to sync: ${input.relativePath} asks for file mode ${((_a2 = input.mode) != null ? _a2 : 0).toString(8)}, and only 644 and 755 are accepted.`
1718
- );
1719
- }
1720
- yield makeParents(input.root, input.relativePath);
1721
- const target = check.path;
1722
- const directory = path.dirname(target);
1723
- const directoryBefore = yield lstatOrNull(directory);
1724
- const temporary = path.join(
1725
- directory,
1726
- `.${path.basename(target)}.${randomBytes(8).toString("hex")}.tmp`
1727
- );
1728
- let handle = null;
1729
- try {
1730
- handle = yield open(temporary, "wx", 384);
1731
- yield handle.writeFile(input.bytes);
1732
- const bits = permissionBits(input.mode);
1733
- if (bits !== null && process.platform !== "win32") {
1734
- yield handle.chmod(bits);
3299
+ const fmt = opts.format;
3300
+ if (fmt === "json" || fmt === "yaml") {
3301
+ printOutput(feature, { format: fmt });
3302
+ return;
1735
3303
  }
1736
- yield handle.close();
1737
- handle = null;
1738
- const parentStats = yield lstatOrNull(directory);
1739
- if (parentStats === null || parentStats.isSymbolicLink()) {
1740
- throw new Error(
1741
- `Refusing to sync: ${describeRejection({ ok: false, reason: "symlink", detail: directory })}.`
3304
+ printSuccess(`Feature ${feature.identifier} created`);
3305
+ printRecord({
3306
+ ID: feature.id,
3307
+ Identifier: feature.identifier,
3308
+ Title: feature.title,
3309
+ Priority: feature.priority,
3310
+ Project: feature.projectId,
3311
+ Created: feature.createdAt.slice(0, 10)
3312
+ });
3313
+ })
3314
+ );
3315
+ features.command("tasks").description("List tasks (sub-items) for a feature").requiredOption("-p, --project <projectId>", "Project ID").requiredOption("-f, --feature <featureId>", "Feature ID").option("--org <slug>", "Organization context").option("--personal", "Retired \u2014 context is organization-only").option(
3316
+ "--format <format>",
3317
+ "Output format: table|json|yaml|csv",
3318
+ "table"
3319
+ ).option("-q, --quiet", "Print IDs only").action(
3320
+ (opts) => __async(null, null, function* () {
3321
+ const ctx = resolveContext(opts.personal, opts.org, false);
3322
+ const client = getClient();
3323
+ let tasks;
3324
+ try {
3325
+ tasks = yield client.projects.listTasks(
3326
+ opts.project,
3327
+ opts.feature,
3328
+ {
3329
+ org: (ctx == null ? void 0 : ctx.type) === "org" ? ctx.slug : void 0
3330
+ }
1742
3331
  );
3332
+ } catch (err) {
3333
+ printError(err.message, 1);
1743
3334
  }
1744
- const targetStats = yield lstatOrNull(target);
1745
- if (targetStats == null ? void 0 : targetStats.isSymbolicLink()) {
1746
- throw new Error(
1747
- `Refusing to sync: ${describeRejection({ ok: false, reason: "symlink", detail: target })}.`
1748
- );
3335
+ if (opts.quiet) {
3336
+ for (const t of tasks) {
3337
+ process.stdout.write(`${t.id}
3338
+ `);
3339
+ }
3340
+ return;
1749
3341
  }
1750
- yield rename(temporary, target);
1751
- } catch (error) {
1752
- yield handle == null ? void 0 : handle.close().catch(() => {
3342
+ printOutput(tasks, {
3343
+ format: opts.format,
3344
+ columns: [
3345
+ "identifier",
3346
+ "title",
3347
+ "status",
3348
+ "estimatedHours"
3349
+ ]
1753
3350
  });
1754
- if (yield isSameDirectory(directory, directoryBefore)) {
1755
- yield unlink(temporary).catch(() => {
1756
- });
1757
- }
1758
- throw error;
1759
- }
1760
- });
1761
- }
1762
- function isSameDirectory(directory, before) {
1763
- return __async(this, null, function* () {
1764
- if (before === null) {
1765
- return false;
1766
- }
1767
- const now = yield lstatOrNull(directory);
1768
- return now !== null && now.dev === before.dev && now.ino === before.ino;
1769
- });
1770
- }
1771
- function deleteFileSafely(root, relativePath, expectedSha256) {
1772
- return __async(this, null, function* () {
1773
- const check = yield assertSafeTarget(root, relativePath);
1774
- if (!check.ok) {
1775
- throw new Error(`Refusing to sync: ${describeRejection(check)}.`);
1776
- }
1777
- const stats = yield lstatOrNull(check.path);
1778
- if (stats === null) {
1779
- return "missing";
1780
- }
1781
- if (stats.isSymbolicLink()) {
1782
- throw new Error(
1783
- `Refusing to sync: ${describeRejection({ ok: false, reason: "symlink", detail: check.path })}.`
1784
- );
1785
- }
1786
- if (!stats.isFile()) {
1787
- throw new Error(
1788
- `Refusing to sync: ${describeRejection({ ok: false, reason: "not_a_regular_file", detail: check.path })}.`
1789
- );
1790
- }
1791
- let actual;
1792
- try {
1793
- actual = createHash("sha256").update(yield readFile(check.path)).digest("hex");
1794
- } catch (error) {
1795
- if (error.code === "ENOENT") {
1796
- return "missing";
3351
+ })
3352
+ );
3353
+ features.command("update <id>").description("Update a feature").requiredOption("-p, --project <projectId>", "Project ID").option("--org <slug>", "Organization context").option("--personal", "Retired \u2014 context is organization-only").option("--title <title>", "New title").option("-d, --description <text>", "New description").option(
3354
+ "--priority <priority>",
3355
+ "Priority: P0_CRITICAL|P1_HIGH|P2_MEDIUM|P3_LOW"
3356
+ ).option("--format <format>", "Output format: table|json|yaml", "table").action(
3357
+ (id, opts) => __async(null, null, function* () {
3358
+ const ctx = resolveContext(opts.personal, opts.org, false);
3359
+ const client = getClient();
3360
+ let feature;
3361
+ try {
3362
+ feature = yield client.projects.updateFeature(
3363
+ opts.project,
3364
+ id,
3365
+ {
3366
+ title: opts.title,
3367
+ description: opts.description,
3368
+ priority: opts.priority,
3369
+ org: (ctx == null ? void 0 : ctx.type) === "org" ? ctx.slug : void 0
3370
+ }
3371
+ );
3372
+ } catch (err) {
3373
+ printError(err.message, 1);
1797
3374
  }
1798
- throw error;
1799
- }
1800
- if (actual !== expectedSha256) {
1801
- return "modified";
1802
- }
1803
- try {
1804
- yield unlink(check.path);
1805
- return "deleted";
1806
- } catch (error) {
1807
- if (error.code === "ENOENT") {
1808
- return "missing";
3375
+ const fmt = opts.format;
3376
+ if (fmt === "json" || fmt === "yaml") {
3377
+ printOutput(feature, { format: fmt });
3378
+ return;
1809
3379
  }
1810
- throw error;
1811
- }
1812
- });
3380
+ printRecord({
3381
+ ID: feature.id,
3382
+ Identifier: feature.identifier,
3383
+ Title: feature.title,
3384
+ Priority: feature.priority,
3385
+ Updated: feature.updatedAt.slice(0, 10)
3386
+ });
3387
+ })
3388
+ );
3389
+ return features;
1813
3390
  }
1814
- function lstatOrNull(target) {
1815
- return __async(this, null, function* () {
1816
- try {
1817
- return yield lstat(target);
1818
- } catch (error) {
1819
- if (error.code === "ENOENT") {
1820
- return null;
3391
+
3392
+ // src/commands/frames/index.ts
3393
+ import { Command as Command11 } from "commander";
3394
+ function buildFramesCommand() {
3395
+ const frames = new Command11("frames").description(
3396
+ "Manage AI-generated frames and slideshows"
3397
+ );
3398
+ frames.command("list").description("List frames and slideshows").option("--org <slug>", "Organization context").option("--personal", "Retired \u2014 context is organization-only").option("--limit <n>", "Max results (default 50)", "50").option(
3399
+ "--format <format>",
3400
+ "Output format: table|json|yaml|csv",
3401
+ "table"
3402
+ ).option("-q, --quiet", "Print IDs only").action(
3403
+ (opts) => __async(null, null, function* () {
3404
+ const ctx = resolveContext(opts.personal, opts.org, false);
3405
+ const client = getClient();
3406
+ let list;
3407
+ try {
3408
+ list = yield client.frames.list({
3409
+ org: (ctx == null ? void 0 : ctx.type) === "org" ? ctx.slug : void 0,
3410
+ limit: Number(opts.limit)
3411
+ });
3412
+ } catch (err) {
3413
+ printError(err.message, 1);
1821
3414
  }
1822
- throw error;
1823
- }
1824
- });
1825
- }
1826
- function resolvesInside(root, candidate) {
1827
- return __async(this, null, function* () {
1828
- const canonicalRoot = yield realpath(root).catch(() => path.resolve(root));
1829
- const canonicalCandidate = yield realpathDeepest(candidate);
1830
- return canonicalCandidate === canonicalRoot || canonicalCandidate.startsWith(canonicalRoot + path.sep);
1831
- });
1832
- }
1833
- function realpathDeepest(candidate) {
1834
- return __async(this, null, function* () {
1835
- const absolute = path.resolve(candidate);
1836
- const tail = [];
1837
- let current = absolute;
1838
- for (; ; ) {
1839
- const resolved = yield realpath(current).catch(() => null);
1840
- if (resolved !== null) {
1841
- return tail.length === 0 ? resolved : path.join(resolved, ...tail);
3415
+ if (opts.quiet) {
3416
+ for (const f of list) {
3417
+ process.stdout.write(`${f.id}
3418
+ `);
3419
+ }
3420
+ return;
1842
3421
  }
1843
- const parent = path.dirname(current);
1844
- if (parent === current) {
1845
- return absolute;
3422
+ printOutput(list, {
3423
+ format: opts.format,
3424
+ columns: [
3425
+ "id",
3426
+ "title",
3427
+ "fileType",
3428
+ "status",
3429
+ "isPublic",
3430
+ "updatedAt"
3431
+ ]
3432
+ });
3433
+ })
3434
+ );
3435
+ frames.command("show <id>").description("Show frame details").option("--org <slug>", "Organization context").option("--personal", "Retired \u2014 context is organization-only").option("--format <format>", "Output format: table|json|yaml", "table").action(
3436
+ (id, opts) => __async(null, null, function* () {
3437
+ var _a2, _b, _c;
3438
+ const ctx = resolveContext(opts.personal, opts.org, false);
3439
+ const client = getClient();
3440
+ let frame;
3441
+ try {
3442
+ frame = yield client.frames.get(id, {
3443
+ org: (ctx == null ? void 0 : ctx.type) === "org" ? ctx.slug : void 0
3444
+ });
3445
+ } catch (err) {
3446
+ printError(err.message, 1);
1846
3447
  }
1847
- tail.unshift(path.basename(current));
1848
- current = parent;
1849
- }
1850
- });
3448
+ const fmt = opts.format;
3449
+ if (fmt === "json" || fmt === "yaml") {
3450
+ printOutput(frame, { format: fmt });
3451
+ return;
3452
+ }
3453
+ printRecord({
3454
+ ID: frame.id,
3455
+ Title: frame.title,
3456
+ Kind: (_a2 = frame.kind) != null ? _a2 : "(none)",
3457
+ "File Type": frame.fileType,
3458
+ Status: frame.status,
3459
+ Public: frame.isPublic ? "yes" : "no",
3460
+ "Share Token": (_b = frame.shareToken) != null ? _b : "(none)",
3461
+ Description: (_c = frame.description) != null ? _c : "(none)",
3462
+ Updated: frame.updatedAt.slice(0, 10)
3463
+ });
3464
+ })
3465
+ );
3466
+ return frames;
1851
3467
  }
1852
3468
 
3469
+ // src/commands/instructions/index.ts
3470
+ import path8 from "path";
3471
+ import { Command as Command12 } from "commander";
3472
+
1853
3473
  // src/lib/instructions/apply.ts
3474
+ import { createHash as createHash3 } from "crypto";
3475
+ import { chmod } from "fs/promises";
1854
3476
  function applyPlan(input) {
1855
3477
  return __async(this, null, function* () {
1856
3478
  var _a2, _b;
@@ -1880,7 +3502,7 @@ function applyPlan(input) {
1880
3502
  `The downloaded bundle is missing ${entry.path}. Nothing was written.`
1881
3503
  );
1882
3504
  }
1883
- const actual = createHash2("sha256").update(bytes).digest("hex");
3505
+ const actual = createHash3("sha256").update(bytes).digest("hex");
1884
3506
  if (actual !== entry.sha256) {
1885
3507
  throw new Error(
1886
3508
  `Checksum mismatch for ${entry.path}: the published snapshot expects ${entry.sha256} and the downloaded bundle contains ${actual}. Nothing was written.`
@@ -2081,12 +3703,12 @@ function extractBundle(archive, wanted) {
2081
3703
  }
2082
3704
 
2083
3705
  // src/lib/instructions/hook.ts
2084
- import path2 from "path";
2085
- var CLAUDE_SETTINGS_RELATIVE_PATH = path2.join(
3706
+ import path6 from "path";
3707
+ var CLAUDE_SETTINGS_RELATIVE_PATH = path6.join(
2086
3708
  ".claude",
2087
3709
  "settings.local.json"
2088
3710
  );
2089
- var CODEX_HOOKS_RELATIVE_PATH = path2.join(".codex", "hooks.json");
3711
+ var CODEX_HOOKS_RELATIVE_PATH = path6.join(".codex", "hooks.json");
2090
3712
  var CLAUDE_SETTINGS_POSIX_PATH = ".claude/settings.local.json";
2091
3713
  var CODEX_HOOKS_POSIX_PATH = ".codex/hooks.json";
2092
3714
  var HOOK_TIMEOUT_SECONDS = 15;
@@ -2110,7 +3732,7 @@ function mergeSessionStartHook(input) {
2110
3732
  return __async(this, null, function* () {
2111
3733
  var _a2;
2112
3734
  const hookTarget = hookTargetFor((_a2 = input.tool) != null ? _a2 : "claude-code");
2113
- const settingsPath = path2.join(input.root, hookTarget.relativePath);
3735
+ const settingsPath = path6.join(input.root, hookTarget.relativePath);
2114
3736
  let existingRaw;
2115
3737
  try {
2116
3738
  const existing = yield readFileSafely(
@@ -2150,14 +3772,14 @@ function mergeSessionStartHook(input) {
2150
3772
  throw refuseShape(settingsPath, '"hooks.SessionStart" is not an array');
2151
3773
  }
2152
3774
  const rawGroups = Array.isArray(hooks.SessionStart) ? hooks.SessionStart : [];
2153
- for (const [index, group] of rawGroups.entries()) {
2154
- if (!isRecord(group)) {
3775
+ for (const [index, group2] of rawGroups.entries()) {
3776
+ if (!isRecord(group2)) {
2155
3777
  throw refuseShape(
2156
3778
  settingsPath,
2157
3779
  `"hooks.SessionStart[${index}]" is not an object`
2158
3780
  );
2159
3781
  }
2160
- if (group.hooks !== void 0 && !Array.isArray(group.hooks)) {
3782
+ if (group2.hooks !== void 0 && !Array.isArray(group2.hooks)) {
2161
3783
  throw refuseShape(
2162
3784
  settingsPath,
2163
3785
  `"hooks.SessionStart[${index}].hooks" is not an array`
@@ -2165,8 +3787,8 @@ function mergeSessionStartHook(input) {
2165
3787
  }
2166
3788
  }
2167
3789
  const sessionStart = rawGroups.map(
2168
- (group) => __spreadProps(__spreadValues({}, group), {
2169
- hooks: Array.isArray(group == null ? void 0 : group.hooks) ? [...group.hooks] : []
3790
+ (group2) => __spreadProps(__spreadValues({}, group2), {
3791
+ hooks: Array.isArray(group2 == null ? void 0 : group2.hooks) ? [...group2.hooks] : []
2170
3792
  })
2171
3793
  );
2172
3794
  const replacement = {
@@ -2176,16 +3798,16 @@ function mergeSessionStartHook(input) {
2176
3798
  };
2177
3799
  let replacedCount = 0;
2178
3800
  const pruned = [];
2179
- for (const group of sessionStart) {
2180
- const kept = group.hooks.filter(
3801
+ for (const group2 of sessionStart) {
3802
+ const kept = group2.hooks.filter(
2181
3803
  (hook) => !isFabricHookFor(hook, input.projectId)
2182
3804
  );
2183
- const removed = group.hooks.length - kept.length;
3805
+ const removed = group2.hooks.length - kept.length;
2184
3806
  replacedCount += removed;
2185
3807
  if (kept.length === 0 && removed > 0) {
2186
3808
  continue;
2187
3809
  }
2188
- pruned.push(__spreadProps(__spreadValues({}, group), { hooks: kept }));
3810
+ pruned.push(__spreadProps(__spreadValues({}, group2), { hooks: kept }));
2189
3811
  }
2190
3812
  pruned.push({ hooks: [replacement] });
2191
3813
  const next = __spreadProps(__spreadValues({}, settings), {
@@ -2258,14 +3880,14 @@ function isRecord(value) {
2258
3880
 
2259
3881
  // src/lib/instructions/lock.ts
2260
3882
  import { readFile as readFile2 } from "fs/promises";
2261
- import path3 from "path";
3883
+ import path7 from "path";
2262
3884
  var LOCK_DIRECTORY = ".fabric";
2263
3885
  var LOCK_FILENAME = "instructions.lock";
2264
3886
  var LOCK_VERSION = 1;
2265
3887
  var LOCK_RELATIVE_PATH = `${LOCK_DIRECTORY}/${LOCK_FILENAME}`;
2266
- var SHA256_HEX = /^[0-9a-f]{64}$/;
3888
+ var SHA256_HEX2 = /^[0-9a-f]{64}$/;
2267
3889
  function lockPath(destination) {
2268
- return path3.join(destination, LOCK_DIRECTORY, LOCK_FILENAME);
3890
+ return path7.join(destination, LOCK_DIRECTORY, LOCK_FILENAME);
2269
3891
  }
2270
3892
  function readLock(destination) {
2271
3893
  return __async(this, null, function* () {
@@ -2287,7 +3909,7 @@ function readLock(destination) {
2287
3909
  `${file} is unreadable: it is not valid JSON. Delete it to start from a clean sync.`
2288
3910
  );
2289
3911
  }
2290
- const problem = lockProblem(parsed);
3912
+ const problem = lockProblem2(parsed);
2291
3913
  if (problem !== null) {
2292
3914
  throw new Error(
2293
3915
  `${file} is unreadable: ${problem}. Delete it to start from a clean sync.`
@@ -2296,7 +3918,7 @@ function readLock(destination) {
2296
3918
  return parsed;
2297
3919
  });
2298
3920
  }
2299
- function serializeLock(lock) {
3921
+ function serializeLock2(lock) {
2300
3922
  return `${JSON.stringify(lock, null, 2).replace(/\r\n/g, "\n")}
2301
3923
  `;
2302
3924
  }
@@ -2305,11 +3927,11 @@ function writeLock(root, lock) {
2305
3927
  yield writeFileSafely({
2306
3928
  root,
2307
3929
  relativePath: LOCK_RELATIVE_PATH,
2308
- bytes: new TextEncoder().encode(serializeLock(lock))
3930
+ bytes: new TextEncoder().encode(serializeLock2(lock))
2309
3931
  });
2310
3932
  });
2311
3933
  }
2312
- function lockProblem(value) {
3934
+ function lockProblem2(value) {
2313
3935
  if (typeof value !== "object" || value === null || Array.isArray(value)) {
2314
3936
  return "it is not a JSON object";
2315
3937
  }
@@ -2341,7 +3963,7 @@ function lockProblem(value) {
2341
3963
  return `its entry for ${JSON.stringify(filePath)} is not an object`;
2342
3964
  }
2343
3965
  const record = entry;
2344
- if (typeof record.sha256 !== "string" || !SHA256_HEX.test(record.sha256)) {
3966
+ if (typeof record.sha256 !== "string" || !SHA256_HEX2.test(record.sha256)) {
2345
3967
  return `its entry for ${JSON.stringify(filePath)} has no 64-character hex sha256`;
2346
3968
  }
2347
3969
  if (record.mode !== null && (typeof record.mode !== "number" || !Number.isInteger(record.mode) || record.mode < 0)) {
@@ -2352,8 +3974,8 @@ function lockProblem(value) {
2352
3974
  }
2353
3975
 
2354
3976
  // src/lib/instructions/manifest.ts
2355
- import { createHash as createHash3 } from "crypto";
2356
- var SHA256_HEX2 = /^[0-9a-f]{64}$/;
3977
+ import { createHash as createHash4 } from "crypto";
3978
+ var SHA256_HEX3 = /^[0-9a-f]{64}$/;
2357
3979
  var MANIFEST_LIMITS = {
2358
3980
  maxFiles: 5e3,
2359
3981
  maxTotalBytes: 52428800,
@@ -2372,7 +3994,7 @@ function maxArchiveBytes(entries) {
2372
3994
  function computeSnapshotDigest(entries) {
2373
3995
  const lines = [...entries].sort((a, b) => a.path < b.path ? -1 : a.path > b.path ? 1 : 0).map((entry) => `${entry.path}\0${entry.sha256}
2374
3996
  `).join("");
2375
- return createHash3("sha256").update(Buffer.from(lines, "utf8")).digest("hex");
3997
+ return createHash4("sha256").update(Buffer.from(lines, "utf8")).digest("hex");
2376
3998
  }
2377
3999
  function assertValidManifest(input) {
2378
4000
  const { manifest, snapshot } = input;
@@ -2458,7 +4080,7 @@ function validateEntry(raw) {
2458
4080
  })}.`
2459
4081
  );
2460
4082
  }
2461
- if (typeof entry.sha256 !== "string" || !SHA256_HEX2.test(entry.sha256)) {
4083
+ if (typeof entry.sha256 !== "string" || !SHA256_HEX3.test(entry.sha256)) {
2462
4084
  throw new Error(
2463
4085
  `The manifest entry for ${entry.path} has no 64-character hex sha256. Nothing was changed.`
2464
4086
  );
@@ -2493,11 +4115,11 @@ function validateEntry(raw) {
2493
4115
  }
2494
4116
 
2495
4117
  // src/lib/instructions/plan.ts
2496
- import { createHash as createHash4 } from "crypto";
4118
+ import { createHash as createHash5 } from "crypto";
2497
4119
  function hashLocalFile(root, relativePath) {
2498
4120
  return __async(this, null, function* () {
2499
4121
  const read = yield readFileSafely(root, relativePath);
2500
- return read === null ? null : createHash4("sha256").update(read.bytes).digest("hex");
4122
+ return read === null ? null : createHash5("sha256").update(read.bytes).digest("hex");
2501
4123
  });
2502
4124
  }
2503
4125
  function lockPaths(lock) {
@@ -2550,7 +4172,7 @@ function verifyLedger(input) {
2550
4172
  drifted.push(`${lockedPath} (missing)`);
2551
4173
  continue;
2552
4174
  }
2553
- const actual = createHash4("sha256").update(read.bytes).digest("hex");
4175
+ const actual = createHash5("sha256").update(read.bytes).digest("hex");
2554
4176
  if (actual !== locked.sha256) {
2555
4177
  drifted.push(`${lockedPath} (edited)`);
2556
4178
  continue;
@@ -2658,7 +4280,7 @@ function nextLock(input) {
2658
4280
  }
2659
4281
 
2660
4282
  // src/lib/instructions/push.ts
2661
- import { createHash as createHash5 } from "crypto";
4283
+ import { createHash as createHash6 } from "crypto";
2662
4284
  var MAX_PUSH_CHANGES = 50;
2663
4285
  function assertPushablePath(candidate, source) {
2664
4286
  const check = checkRelativePath(candidate);
@@ -2697,16 +4319,16 @@ function assertLockMatchesManifest(lock, manifest) {
2697
4319
  const published = new Map(
2698
4320
  manifest.map((entry) => [entry.path, entry.sha256])
2699
4321
  );
2700
- for (const [path5, entry] of Object.entries(lock.files)) {
2701
- const expected = published.get(path5);
4322
+ for (const [path9, entry] of Object.entries(lock.files)) {
4323
+ const expected = published.get(path9);
2702
4324
  if (expected === void 0) {
2703
4325
  refuseStaleLedger(
2704
- `it lists ${path5}, which version ${lock.snapshotVersion} does not contain`
4326
+ `it lists ${path9}, which version ${lock.snapshotVersion} does not contain`
2705
4327
  );
2706
4328
  }
2707
4329
  if (expected !== entry.sha256) {
2708
4330
  refuseStaleLedger(
2709
- `its record of ${path5} is not the published file`
4331
+ `its record of ${path9} is not the published file`
2710
4332
  );
2711
4333
  }
2712
4334
  }
@@ -2772,7 +4394,7 @@ function computePushPlan(input) {
2772
4394
  changes.push({ op: "delete", path: lockedPath });
2773
4395
  continue;
2774
4396
  }
2775
- const actual = createHash5("sha256").update(read.bytes).digest("hex");
4397
+ const actual = createHash6("sha256").update(read.bytes).digest("hex");
2776
4398
  if (actual === ((_b = lock.files[lockedPath]) == null ? void 0 : _b.sha256)) {
2777
4399
  unchanged.push(lockedPath);
2778
4400
  continue;
@@ -2801,23 +4423,16 @@ function computePushPlan(input) {
2801
4423
  }
2802
4424
 
2803
4425
  // src/commands/instructions/index.ts
2804
- var HOOK_DEADLINE_MS = 1e4;
4426
+ var HOOK_DEADLINE_MS2 = 1e4;
2805
4427
  var CHECK_TIMEOUT_MS = 5e3;
2806
4428
  var SYNC_TIMEOUT_MS = 15e3;
2807
4429
  var BUNDLE_TIMEOUT_MS = 6e4;
2808
- var PUSH_TIMEOUT_MS = 6e4;
4430
+ var PUSH_TIMEOUT_MS2 = 6e4;
2809
4431
  function collectAdded(value, previous) {
2810
4432
  return [...previous, value];
2811
4433
  }
2812
- var CliFailure = class extends Error {
2813
- constructor(message, exitCode) {
2814
- super(message);
2815
- this.exitCode = exitCode;
2816
- this.name = "CliFailure";
2817
- }
2818
- };
2819
4434
  function buildInstructionsCommand() {
2820
- const instructions = new Command11("instructions").description(
4435
+ const instructions = new Command12("instructions").description(
2821
4436
  "Keep a checkout current with a project's published coding instructions"
2822
4437
  );
2823
4438
  instructions.command("check").description(
@@ -2830,7 +4445,11 @@ function buildInstructionsCommand() {
2830
4445
  "Session-hook mode: plain text, never fails, never blocks a session"
2831
4446
  ).option("--format <format>", "Output format: text|json").action(function(opts) {
2832
4447
  return __async(this, null, function* () {
2833
- yield run(opts, "check", () => runCheck(opts, formatFor(this)));
4448
+ yield run2(
4449
+ opts,
4450
+ "check",
4451
+ () => runCheck(opts, outputFormatFor(this))
4452
+ );
2834
4453
  });
2835
4454
  });
2836
4455
  instructions.command("sync").description(
@@ -2840,7 +4459,7 @@ function buildInstructionsCommand() {
2840
4459
  "Session-hook mode: plain text, never fails, never blocks a session"
2841
4460
  ).option("--format <format>", "Output format: text|json").action(function(opts) {
2842
4461
  return __async(this, null, function* () {
2843
- yield run(opts, "sync", () => runSync(opts, formatFor(this)));
4462
+ yield run2(opts, "sync", () => runSync(opts, outputFormatFor(this)));
2844
4463
  });
2845
4464
  });
2846
4465
  instructions.command("push").description(
@@ -2855,10 +4474,10 @@ function buildInstructionsCommand() {
2855
4474
  "Publish the change as a new version instead of proposing it (needs a key with instructions:publish)"
2856
4475
  ).option("--dry-run", "Print the change set and send nothing").option("--format <format>", "Output format: text|json").action(function(opts) {
2857
4476
  return __async(this, null, function* () {
2858
- yield run(
4477
+ yield run2(
2859
4478
  __spreadProps(__spreadValues({}, opts), { hook: false }),
2860
4479
  "push",
2861
- () => runPush(opts, formatFor(this))
4480
+ () => runPush2(opts, outputFormatFor(this))
2862
4481
  );
2863
4482
  });
2864
4483
  });
@@ -2872,24 +4491,25 @@ function buildInstructionsCommand() {
2872
4491
  "Let the hook apply changes instead of only reporting them"
2873
4492
  ).option("--format <format>", "Output format: text|json").action(function(opts) {
2874
4493
  return __async(this, null, function* () {
2875
- yield run(
4494
+ yield run2(
2876
4495
  __spreadProps(__spreadValues({}, opts), { hook: false }),
2877
4496
  "init",
2878
- () => runInit(opts, formatFor(this))
4497
+ () => runInit(opts, outputFormatFor(this))
2879
4498
  );
2880
4499
  });
2881
4500
  });
2882
4501
  return instructions;
2883
4502
  }
2884
- function formatFor(command) {
2885
- const resolved = command.optsWithGlobals();
2886
- return resolved.format === "json" ? "json" : "text";
2887
- }
2888
- function run(opts, verb, body) {
4503
+ function run2(opts, verb, body) {
2889
4504
  return __async(this, null, function* () {
2890
4505
  try {
2891
4506
  if (opts.hook) {
2892
- yield withDeadline(HOOK_DEADLINE_MS, body);
4507
+ yield withDeadline(HOOK_DEADLINE_MS2, (signal) => {
4508
+ activeDeadline = signal;
4509
+ return body();
4510
+ }).finally(() => {
4511
+ activeDeadline = void 0;
4512
+ });
2893
4513
  } else {
2894
4514
  yield body();
2895
4515
  }
@@ -2908,63 +4528,10 @@ function run(opts, verb, body) {
2908
4528
  }
2909
4529
  });
2910
4530
  }
2911
- function withDeadline(totalMs, body) {
2912
- return __async(this, null, function* () {
2913
- const controller = new AbortController();
2914
- let timer;
2915
- const deadline = new Promise((_resolve, reject) => {
2916
- timer = setTimeout(() => {
2917
- controller.abort();
2918
- reject(
2919
- new Error(
2920
- `gave up after ${totalMs}ms so the session is not held up`
2921
- )
2922
- );
2923
- }, totalMs);
2924
- });
2925
- activeDeadline = controller.signal;
2926
- try {
2927
- yield Promise.race([body(), deadline]);
2928
- } finally {
2929
- activeDeadline = void 0;
2930
- if (timer) {
2931
- clearTimeout(timer);
2932
- }
2933
- }
2934
- });
2935
- }
2936
4531
  var activeDeadline;
2937
- function describeError(error) {
2938
- if (error instanceof Error) {
2939
- return error.message.replace(/\s*\n\s*/g, " ");
2940
- }
2941
- return String(error);
2942
- }
2943
- function asCliFailure(error) {
2944
- if (error instanceof CliFailure) {
2945
- return error;
2946
- }
2947
- const status = error.status;
2948
- const message = error instanceof Error ? error.message : String(error);
2949
- switch (status) {
2950
- case 401:
2951
- return new CliFailure(message, 3);
2952
- case 403:
2953
- return new CliFailure(message, 5);
2954
- case 404:
2955
- return new CliFailure(message, 4);
2956
- case 429:
2957
- return new CliFailure(message, 6);
2958
- case 400:
2959
- case 422:
2960
- return new CliFailure(message, 7);
2961
- default:
2962
- return new CliFailure(message, 1);
2963
- }
2964
- }
2965
4532
  function destinationOf(opts) {
2966
4533
  var _a2;
2967
- return path4.resolve((_a2 = opts.dest) != null ? _a2 : process.cwd());
4534
+ return path8.resolve((_a2 = opts.dest) != null ? _a2 : process.cwd());
2968
4535
  }
2969
4536
  function instructionsClient(opts, timeoutMs, { neverRetry = false } = {}) {
2970
4537
  if (!getApiKey()) {
@@ -2979,7 +4546,7 @@ function instructionsClient(opts, timeoutMs, { neverRetry = false } = {}) {
2979
4546
  // call it makes, so it cannot spend it on retries: a
2980
4547
  // session that will not start is worse than instructions
2981
4548
  // one version stale.
2982
- timeoutMs: Math.min(timeoutMs, HOOK_DEADLINE_MS),
4549
+ timeoutMs: Math.min(timeoutMs, HOOK_DEADLINE_MS2),
2983
4550
  retry: { maxRetries: 0 }
2984
4551
  } : neverRetry ? { timeoutMs, retry: { maxRetries: 0 } } : { timeoutMs }
2985
4552
  ).withoutContext();
@@ -2987,7 +4554,7 @@ function instructionsClient(opts, timeoutMs, { neverRetry = false } = {}) {
2987
4554
  function downloadUrlClient(opts) {
2988
4555
  return instructionsClient(
2989
4556
  opts,
2990
- opts.hook ? HOOK_DEADLINE_MS : BUNDLE_TIMEOUT_MS,
4557
+ opts.hook ? HOOK_DEADLINE_MS2 : BUNDLE_TIMEOUT_MS,
2991
4558
  { neverRetry: true }
2992
4559
  );
2993
4560
  }
@@ -3018,7 +4585,7 @@ function assertHookMayAct(opts, published) {
3018
4585
  );
3019
4586
  }
3020
4587
  }
3021
- function readLockForProject(destination, projectId) {
4588
+ function readLockForProject2(destination, projectId) {
3022
4589
  return __async(this, null, function* () {
3023
4590
  const lock = yield readLock(destination);
3024
4591
  if (lock !== null && lock.projectId !== projectId) {
@@ -3048,7 +4615,7 @@ function runCheck(opts, format) {
3048
4615
  var _a2;
3049
4616
  const destination = destinationOf(opts);
3050
4617
  const root = yield resolveExistingRoot(destination);
3051
- const lock = yield readLockForProject(destination, opts.project);
4618
+ const lock = yield readLockForProject2(destination, opts.project);
3052
4619
  const client = instructionsClient(opts, CHECK_TIMEOUT_MS);
3053
4620
  const published = yield fetchPublished(client, opts, lock == null ? void 0 : lock.digest);
3054
4621
  const drifted = opts.verify && lock !== null ? yield verifyLedger({ root, lock }) : [];
@@ -3157,7 +4724,7 @@ function syncOnce(opts) {
3157
4724
  return __async(this, null, function* () {
3158
4725
  const root = yield resolveDestinationRoot(destinationOf(opts));
3159
4726
  const outcome = emptyOutcome(opts, root);
3160
- const lock = yield readLockForProject(root, opts.project);
4727
+ const lock = yield readLockForProject2(root, opts.project);
3161
4728
  const client = instructionsClient(opts, SYNC_TIMEOUT_MS);
3162
4729
  let published = yield fetchPublished(client, opts, lock == null ? void 0 : lock.digest);
3163
4730
  assertInitSyncMayAct(opts, published);
@@ -3211,7 +4778,7 @@ function syncOnce(opts) {
3211
4778
  manifest.map((entry) => [entry.path, entry.size])
3212
4779
  );
3213
4780
  const archive = yield fetchBundle(download.url, {
3214
- timeoutMs: opts.hook ? HOOK_DEADLINE_MS : BUNDLE_TIMEOUT_MS,
4781
+ timeoutMs: opts.hook ? HOOK_DEADLINE_MS2 : BUNDLE_TIMEOUT_MS,
3215
4782
  // Bounded by what this manifest says it holds, not by what the
3216
4783
  // response claims. The manifest has already been checked against
3217
4784
  // the published snapshot limits, so this is a number the client
@@ -3299,12 +4866,12 @@ function reportSync(outcome, opts) {
3299
4866
  line(`Everything in ${outcome.destination} already matched.`);
3300
4867
  }
3301
4868
  }
3302
- function runPush(opts, format) {
4869
+ function runPush2(opts, format) {
3303
4870
  return __async(this, null, function* () {
3304
4871
  var _a2;
3305
4872
  const destination = destinationOf(opts);
3306
4873
  const root = yield resolveExistingRoot(destination);
3307
- const lock = yield readLockForProject(root, opts.project);
4874
+ const lock = yield readLockForProject2(root, opts.project);
3308
4875
  if (lock === null) {
3309
4876
  throw new CliFailure(
3310
4877
  `No coding-instructions lock in ${destination}. Run \`fabric instructions sync --project ${opts.project}\` first: a push is a diff against the version that was last applied here, and without that ledger there is nothing to diff against.`,
@@ -3313,7 +4880,7 @@ function runPush(opts, format) {
3313
4880
  }
3314
4881
  const client = instructionsClient(
3315
4882
  __spreadProps(__spreadValues({}, opts), { hook: false }),
3316
- PUSH_TIMEOUT_MS
4883
+ PUSH_TIMEOUT_MS2
3317
4884
  );
3318
4885
  const published = yield fetchPublished(client, opts);
3319
4886
  if (!published.published || !published.snapshot) {
@@ -3474,7 +5041,7 @@ function runInit(opts, format) {
3474
5041
  }
3475
5042
  const tool = opts.tool;
3476
5043
  const root = yield resolveDestinationRoot(destinationOf(opts));
3477
- yield readLockForProject(root, opts.project);
5044
+ yield readLockForProject2(root, opts.project);
3478
5045
  try {
3479
5046
  yield assertKeyStaysOutside(root, getConfigPath());
3480
5047
  } catch (error) {
@@ -3555,9 +5122,9 @@ function hookPathFor(tool) {
3555
5122
  }
3556
5123
 
3557
5124
  // src/commands/mcp/index.ts
3558
- import { Command as Command12 } from "commander";
5125
+ import { Command as Command13 } from "commander";
3559
5126
  function buildMcpCommand() {
3560
- const mcp = new Command12("mcp").description(
5127
+ const mcp = new Command13("mcp").description(
3561
5128
  "Manage MCP servers and configurations"
3562
5129
  );
3563
5130
  mcp.command("servers").description("List accessible MCP servers (system + custom)").option("--org <slug>", "Organization context").option("--personal", "Retired \u2014 context is organization-only").option(
@@ -3595,7 +5162,7 @@ function buildMcpCommand() {
3595
5162
  });
3596
5163
  })
3597
5164
  );
3598
- const configs = new Command12("configs").description(
5165
+ const configs = new Command13("configs").description(
3599
5166
  "Manage MCP configurations"
3600
5167
  );
3601
5168
  configs.command("list", { isDefault: true }).description("List your MCP configurations").option("--org <slug>", "Organization context").option("--personal", "Retired \u2014 context is organization-only").option(
@@ -3700,9 +5267,9 @@ function buildMcpCommand() {
3700
5267
  }
3701
5268
 
3702
5269
  // src/commands/orgs/index.ts
3703
- import { Command as Command13 } from "commander";
5270
+ import { Command as Command14 } from "commander";
3704
5271
  function buildOrgsCommand() {
3705
- const orgs = new Command13("orgs").description("Manage organizations");
5272
+ const orgs = new Command14("orgs").description("Manage organizations");
3706
5273
  orgs.command("list").description("List organizations you belong to").option(
3707
5274
  "--format <format>",
3708
5275
  "Output format: table|json|yaml|csv",
@@ -3759,9 +5326,9 @@ function buildOrgsCommand() {
3759
5326
  }
3760
5327
 
3761
5328
  // src/commands/projects/index.ts
3762
- import { Command as Command14 } from "commander";
5329
+ import { Command as Command15 } from "commander";
3763
5330
  function buildProjectsCommand() {
3764
- const projects = new Command14("projects").description("Manage projects");
5331
+ const projects = new Command15("projects").description("Manage projects");
3765
5332
  projects.command("list").description("List projects").option("--org <slug>", "Organization context").option("--personal", "Retired \u2014 context is organization-only").option(
3766
5333
  "--status <status>",
3767
5334
  "Filter by status: ACTIVE|DRAFT|COMPLETED|ARCHIVED"
@@ -3896,9 +5463,9 @@ function buildProjectsCommand() {
3896
5463
  }
3897
5464
 
3898
5465
  // src/commands/prompts/index.ts
3899
- import { Command as Command15 } from "commander";
5466
+ import { Command as Command16 } from "commander";
3900
5467
  function buildPromptsCommand() {
3901
- const prompts = new Command15("prompts").description(
5468
+ const prompts = new Command16("prompts").description(
3902
5469
  "Manage prompt templates"
3903
5470
  );
3904
5471
  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(
@@ -4051,12 +5618,12 @@ ${content}
4051
5618
  }
4052
5619
 
4053
5620
  // src/commands/reports/index.ts
4054
- import { Command as Command16 } from "commander";
5621
+ import { Command as Command17 } from "commander";
4055
5622
  function buildReportsCommand() {
4056
- const reports = new Command16("reports").description(
5623
+ const reports = new Command17("reports").description(
4057
5624
  "Manage report templates and executions"
4058
5625
  );
4059
- const templates = new Command16("templates").description(
5626
+ const templates = new Command17("templates").description(
4060
5627
  "Manage report templates"
4061
5628
  );
4062
5629
  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(
@@ -4130,7 +5697,7 @@ function buildReportsCommand() {
4130
5697
  })
4131
5698
  );
4132
5699
  reports.addCommand(templates);
4133
- const executions = new Command16("executions").description(
5700
+ const executions = new Command17("executions").description(
4134
5701
  "View report executions"
4135
5702
  );
4136
5703
  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(
@@ -4174,9 +5741,9 @@ function buildReportsCommand() {
4174
5741
  }
4175
5742
 
4176
5743
  // src/commands/skills/index.ts
4177
- import { Command as Command17 } from "commander";
5744
+ import { Command as Command18 } from "commander";
4178
5745
  function buildSkillsCommand() {
4179
- const skills = new Command17("skills").description(
5746
+ const skills = new Command18("skills").description(
4180
5747
  "Browse skill definitions"
4181
5748
  );
4182
5749
  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(
@@ -4243,9 +5810,9 @@ function buildSkillsCommand() {
4243
5810
  }
4244
5811
 
4245
5812
  // src/commands/workflows/index.ts
4246
- import { Command as Command18 } from "commander";
5813
+ import { Command as Command19 } from "commander";
4247
5814
  function buildWorkflowsCommand() {
4248
- const workflows = new Command18("workflows").description("Manage workflows");
5815
+ const workflows = new Command19("workflows").description("Manage workflows");
4249
5816
  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(
4250
5817
  "--format <format>",
4251
5818
  "Output format: table|json|yaml|csv",
@@ -4373,9 +5940,9 @@ function buildWorkflowsCommand() {
4373
5940
  }
4374
5941
 
4375
5942
  // src/commands/workspaces/index.ts
4376
- import { Command as Command19 } from "commander";
5943
+ import { Command as Command20 } from "commander";
4377
5944
  function buildWorkspacesCommand() {
4378
- const workspaces = new Command19("workspaces").description(
5945
+ const workspaces = new Command20("workspaces").description(
4379
5946
  "Manage workspaces"
4380
5947
  );
4381
5948
  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(
@@ -4451,14 +6018,14 @@ function buildWorkspacesCommand() {
4451
6018
  }
4452
6019
 
4453
6020
  // src/bin/fabric.ts
4454
- var program = new Command20();
6021
+ var program = new Command21();
4455
6022
  var _a;
4456
6023
  program.name("fabric").description("Fabric AI platform CLI").version(package_default.version).option(
4457
6024
  "--format <format>",
4458
6025
  "Output format: table|json|yaml|csv",
4459
6026
  (_a = process.env.FABRIC_FORMAT) != null ? _a : "table"
4460
6027
  );
4461
- var auth = new Command20("auth").description(
6028
+ var auth = new Command21("auth").description(
4462
6029
  "Authenticate and manage API keys"
4463
6030
  );
4464
6031
  auth.addCommand(buildLoginCommand());
@@ -4475,13 +6042,14 @@ program.addCommand(buildPromptsCommand());
4475
6042
  program.addCommand(buildAgentsCommand());
4476
6043
  program.addCommand(buildSkillsCommand());
4477
6044
  program.addCommand(buildInstructionsCommand());
6045
+ program.addCommand(buildContextCommand());
4478
6046
  program.addCommand(buildMcpCommand());
4479
6047
  program.addCommand(buildFramesCommand());
4480
6048
  program.addCommand(buildWorkspacesCommand());
4481
6049
  program.addCommand(buildChatsCommand());
4482
6050
  program.addCommand(buildReportsCommand());
4483
6051
  program.addCommand(buildCompletionCommand());
4484
- var config = new Command20("config").description("Show CLI configuration");
6052
+ var config = new Command21("config").description("Show CLI configuration");
4485
6053
  config.command("path").description("Print the path to the config file").action(() => {
4486
6054
  process.stdout.write(`${getConfigPath()}
4487
6055
  `);