@fabricorg/cli 0.3.6 → 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
@@ -57,7 +57,7 @@ import { Command as Command21 } from "commander";
57
57
  // package.json
58
58
  var package_default = {
59
59
  name: "@fabricorg/cli",
60
- version: "0.3.6",
60
+ version: "0.3.7",
61
61
  description: "CLI for the Fabric AI platform",
62
62
  keywords: [
63
63
  "fabric",
@@ -2048,15 +2048,61 @@ function computeContextPlan(input) {
2048
2048
  }
2049
2049
  }
2050
2050
  push.sort((a, b) => byPath2(a.sourcePath, b.sourcePath));
2051
- unchangedLocal.sort(byPath2);
2052
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);
2053
2056
  forgotten.sort(byPath2);
2054
2057
  skipped.sort((a, b) => byPath2(a.path, b.path));
2055
- return { push, unchangedLocal, removed, forgotten, skipped };
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
+ };
2056
2066
  });
2057
2067
  }
2068
+ function pairMoves(lock, removed, push) {
2069
+ const added = push.filter(
2070
+ (entry) => (lock == null ? void 0 : lock.files[entry.sourcePath]) === void 0
2071
+ );
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;
2096
+ }
2058
2097
 
2059
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
+ ]);
2060
2106
  var RUN_LEVEL_STATUSES = /* @__PURE__ */ new Set([401, 403, 404, 429]);
2061
2107
  var RUN_LEVEL_CODES = /* @__PURE__ */ new Set(["NETWORK_ERROR", "TIMEOUT"]);
2062
2108
  function stopsTheRun(error) {
@@ -2094,158 +2140,439 @@ function readPlannedContent(root, entry) {
2094
2140
  return verdict.content;
2095
2141
  });
2096
2142
  }
2097
- function fromResult(sourcePath, result) {
2098
- if (result.status === "duplicate") {
2143
+ function fromResult(sourcePath, result, movedFrom) {
2144
+ if (result.status === "moved") {
2099
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({
2100
2158
  sourcePath,
2101
2159
  status: "duplicate",
2102
2160
  contentHash: result.contentHash,
2103
2161
  duplicateOfContextId: result.duplicateOfContextId,
2104
2162
  duplicateOfSourcePath: result.duplicateOfSourcePath
2105
- };
2163
+ }, move);
2106
2164
  }
2107
- return {
2165
+ return __spreadValues({
2108
2166
  sourcePath,
2109
2167
  status: result.status,
2110
2168
  contextId: result.contextId,
2111
2169
  contentHash: result.contentHash
2112
- };
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;
2113
2177
  }
2114
2178
  function pushContextPlan(input) {
2115
2179
  return __async(this, null, function* () {
2116
2180
  var _a2;
2117
2181
  const results = [];
2118
- const send = (entry, content, expectedContentHash) => input.client.contexts.upsertSyncedFile(
2182
+ const pruneTargets = [];
2183
+ const { force } = input;
2184
+ const send = (sourcePath, content, expectedContentHash, movedFromSourcePath) => input.client.contexts.upsertSyncedFile(
2119
2185
  input.projectId,
2120
- __spreadValues({
2121
- sourcePath: entry.sourcePath,
2186
+ __spreadValues(__spreadValues({
2187
+ sourcePath,
2122
2188
  content
2123
- }, expectedContentHash !== void 0 ? { expectedContentHash } : {}),
2189
+ }, expectedContentHash !== void 0 ? { expectedContentHash } : {}), movedFromSourcePath !== void 0 ? { movedFromSourcePath } : {}),
2124
2190
  { org: input.org }
2125
2191
  );
2126
- for (const entry of input.plan.push) {
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
+ }
2127
2201
  try {
2128
- const content = yield readPlannedContent(input.root, entry);
2129
- if (content === null) {
2130
- results.push({
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 {
2131
2229
  sourcePath: entry.sourcePath,
2132
2230
  status: "changed-during-run"
2133
- });
2134
- continue;
2231
+ };
2135
2232
  }
2136
2233
  try {
2137
- results.push(
2138
- fromResult(
2234
+ const forced = fromResult(
2235
+ entry.sourcePath,
2236
+ yield send(
2139
2237
  entry.sourcePath,
2140
- yield send(entry, content, entry.expectedContentHash)
2238
+ forcedContent,
2239
+ current === null ? void 0 : (_a3 = current.contentHash) != null ? _a3 : void 0
2141
2240
  )
2142
2241
  );
2143
- } catch (error) {
2144
- const conflict = conflictOf(error);
2145
- if (conflict === null) {
2146
- throw error;
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;
2147
2247
  }
2148
- const { current } = conflict;
2149
- if (!input.force || current !== null && current.contentHash === null) {
2150
- results.push({
2151
- sourcePath: entry.sourcePath,
2152
- status: "conflict",
2153
- contentHash: conflict.contentHash,
2154
- wasLocked: entry.expectedContentHash !== void 0,
2155
- afterForce: false,
2156
- current
2157
- });
2158
- continue;
2159
- }
2160
- const forcedContent = yield readPlannedContent(
2161
- input.root,
2162
- entry
2163
- );
2164
- if (forcedContent === null) {
2165
- results.push({
2166
- sourcePath: entry.sourcePath,
2167
- status: "changed-during-run"
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
+ }
2257
+ }
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"
2168
2325
  });
2169
- continue;
2170
2326
  }
2171
- try {
2172
- const forced = fromResult(
2173
- entry.sourcePath,
2327
+ if (aboutOldPath) {
2328
+ return fromResult(
2329
+ move.to,
2174
2330
  yield send(
2175
- entry,
2331
+ move.to,
2176
2332
  forcedContent,
2177
- current === null ? void 0 : (_a2 = current.contentHash) != null ? _a2 : void 0
2178
- )
2333
+ (_b = current == null ? void 0 : current.contentHash) != null ? _b : void 0,
2334
+ move.from
2335
+ ),
2336
+ move.from
2179
2337
  );
2180
- results.push(
2181
- forced.status === "updated" && current !== null ? __spreadProps(__spreadValues({}, forced), { overwrote: current }) : forced
2182
- );
2183
- } catch (retryError) {
2184
- const again = conflictOf(retryError);
2185
- if (again === null) {
2186
- throw retryError;
2187
- }
2188
- results.push({
2189
- sourcePath: entry.sourcePath,
2190
- status: "conflict",
2191
- contentHash: again.contentHash,
2192
- wasLocked: entry.expectedContentHash !== void 0,
2193
- afterForce: true,
2194
- current: again.current
2195
- });
2196
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
+ );
2197
2369
  }
2370
+ }
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
+ };
2393
+ try {
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;
2198
2429
  } catch (error) {
2199
2430
  const failure = asCliFailure(error);
2200
2431
  if (stopsTheRun(error)) {
2201
- return { results, stoppedBy: failure };
2432
+ return failure;
2202
2433
  }
2203
- results.push({
2434
+ results.push(failed(error, failure.exitCode));
2435
+ return null;
2436
+ }
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());
2449
+ }
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) => ({
2204
2471
  sourcePath: entry.sourcePath,
2205
2472
  status: "failed",
2206
2473
  error: describeError(error),
2207
- exitCode: failure.exitCode
2208
- });
2474
+ exitCode
2475
+ })
2476
+ );
2477
+ if (stoppedBy) {
2478
+ return stopped(stoppedBy);
2209
2479
  }
2210
2480
  }
2211
- return { results, stoppedBy: null };
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 };
2212
2503
  });
2213
2504
  }
2214
2505
  function nextContextLock(input) {
2215
2506
  var _a2, _b, _c;
2216
2507
  const files = __spreadValues({}, (_b = (_a2 = input.previous) == null ? void 0 : _a2.files) != null ? _b : {});
2217
2508
  let changed = false;
2218
- for (const sourcePath of (_c = input.forgotten) != null ? _c : []) {
2509
+ const drop = (sourcePath) => {
2219
2510
  if (sourcePath in files) {
2220
2511
  delete files[sourcePath];
2221
2512
  changed = true;
2222
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;
2519
+ }
2520
+ files[sourcePath] = { sha256, contextId };
2521
+ changed = true;
2522
+ };
2523
+ for (const sourcePath of (_c = input.forgotten) != null ? _c : []) {
2524
+ drop(sourcePath);
2223
2525
  }
2224
2526
  for (const result of input.results) {
2225
- if (result.status === "duplicate") {
2226
- const before2 = files[result.sourcePath];
2227
- if ((before2 == null ? void 0 : before2.state) === "duplicate" && before2.sha256 === result.contentHash) {
2228
- continue;
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;
2229
2555
  }
2230
- files[result.sourcePath] = {
2231
- sha256: result.contentHash,
2232
- state: "duplicate"
2233
- };
2234
- changed = true;
2235
- continue;
2236
- }
2237
- if (result.status !== "created" && result.status !== "updated" && result.status !== "unchanged") {
2238
- continue;
2239
- }
2240
- const before = files[result.sourcePath];
2241
- if ((before == null ? void 0 : before.state) === void 0 && (before == null ? void 0 : before.sha256) === result.contentHash && before.contextId === result.contextId) {
2242
- continue;
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;
2243
2575
  }
2244
- files[result.sourcePath] = {
2245
- sha256: result.contentHash,
2246
- contextId: result.contextId
2247
- };
2248
- changed = true;
2249
2576
  }
2250
2577
  if (!changed) {
2251
2578
  return null;
@@ -2266,11 +2593,18 @@ function countOutcomes(plan, results) {
2266
2593
  updated: count("updated"),
2267
2594
  unchanged: count("unchanged") + plan.unchangedLocal.length,
2268
2595
  duplicate: count("duplicate"),
2596
+ moved: count("moved"),
2597
+ moveUnsupported: count("move-unsupported"),
2269
2598
  conflict: count("conflict"),
2270
2599
  changedDuringRun: count("changed-during-run"),
2271
2600
  failed: count("failed"),
2272
2601
  removed: plan.removed.length,
2273
- skipped: plan.skipped.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")
2274
2608
  };
2275
2609
  }
2276
2610
  function who(current) {
@@ -2281,7 +2615,30 @@ function when(current) {
2281
2615
  var _a2;
2282
2616
  return (_a2 = current.contentUpdatedAt) != null ? _a2 : "an unknown time";
2283
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}`;
2622
+ }
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";
2626
+ }
2627
+ return `${from}: ${text}; not moved to ${result.sourcePath}`;
2628
+ }
2284
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`;
2638
+ }
2639
+ return pushConflictLine(result);
2640
+ }
2641
+ function pushConflictLine(result) {
2285
2642
  const { current } = result;
2286
2643
  if (current === null) {
2287
2644
  return `${result.sourcePath}: deleted on the server since your last push`;
@@ -2299,12 +2656,47 @@ function conflictLine(result) {
2299
2656
  }
2300
2657
  return `${result.sourcePath}: ${text}`;
2301
2658
  }
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";
2664
+ }
2665
+ return `${result.sourcePath}: ${text}; not deleted`;
2666
+ }
2302
2667
  function storedLine(result) {
2303
2668
  if (result.overwrote) {
2304
2669
  return `${result.sourcePath}: replaced the version ${who(result.overwrote)} changed at ${when(result.overwrote)} (--force)`;
2305
2670
  }
2306
2671
  return result.sourcePath;
2307
2672
  }
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}`;
2698
+ }
2699
+ }
2308
2700
  function skippedLine(skip) {
2309
2701
  if (skip.reason === "ignored") {
2310
2702
  return `${skip.path}: now excluded by the ignore rules; server entry kept`;
@@ -2321,24 +2713,42 @@ function group(lines, label, items) {
2321
2713
  }
2322
2714
  }
2323
2715
  function formatPushReport(input) {
2716
+ var _a2;
2324
2717
  const { plan, results } = input;
2325
- const of = (status) => results.filter((r) => r.status === status);
2718
+ const prune = Boolean(input.prune);
2719
+ const of = (status) => results.filter(
2720
+ (r) => r.status === status && !isMoveFallback(r)
2721
+ );
2326
2722
  const lines = [
2327
2723
  `Context push from ${input.directory} to project ${input.projectId}:`
2328
2724
  ];
2329
2725
  group(lines, "created", of("created").map(storedLine));
2330
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
+ ]);
2331
2738
  group(
2332
2739
  lines,
2333
2740
  "duplicate",
2334
2741
  of("duplicate").map(
2335
2742
  (r) => {
2336
- var _a2;
2337
- return `${r.sourcePath}: duplicate of ${(_a2 = r.duplicateOfSourcePath) != null ? _a2 : "another source in this project"}`;
2743
+ var _a3;
2744
+ return `${r.sourcePath}: duplicate of ${(_a3 = r.duplicateOfSourcePath) != null ? _a3 : "another source in this project"}`;
2338
2745
  }
2339
2746
  )
2340
2747
  );
2341
- group(lines, "conflict", of("conflict").map(conflictLine));
2748
+ group(lines, "conflict", [
2749
+ ...of("conflict").map(conflictLine),
2750
+ ...of("delete-conflict").map(deleteConflictLine)
2751
+ ]);
2342
2752
  group(
2343
2753
  lines,
2344
2754
  "changed during the run",
@@ -2346,15 +2756,44 @@ function formatPushReport(input) {
2346
2756
  (r) => `${r.sourcePath}: changed-during-run; not sent, the next push sends the new content`
2347
2757
  )
2348
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
+ ]);
2349
2765
  group(
2350
2766
  lines,
2351
- "failed",
2352
- of("failed").map((r) => `${r.sourcePath}: ${r.error}`)
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
+ )
2353
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
+ ];
2354
2793
  group(
2355
2794
  lines,
2356
2795
  "removed",
2357
- plan.removed.map((p) => `${p}: removed locally; server entry kept`)
2796
+ kept.filter((p) => !pruned.has(p)).map((p) => `${p}: removed locally; server entry kept`)
2358
2797
  );
2359
2798
  group(lines, "skipped", plan.skipped.map(skippedLine));
2360
2799
  const unchanged = countOutcomes(plan, results).unchanged;
@@ -2368,6 +2807,11 @@ function formatDryRunReport(input) {
2368
2807
  const lines = [
2369
2808
  `Dry run of a context push from ${input.directory} to project ${input.projectId}: nothing was sent and the lock was not written.`
2370
2809
  ];
2810
+ group(
2811
+ lines,
2812
+ "moved",
2813
+ plan.moves.map((move) => `${move.from} -> ${move.to}`)
2814
+ );
2371
2815
  group(
2372
2816
  lines,
2373
2817
  "new",
@@ -2378,13 +2822,17 @@ function formatDryRunReport(input) {
2378
2822
  "changed",
2379
2823
  plan.push.filter((entry) => entry.expectedContentHash !== void 0).map((entry) => entry.sourcePath)
2380
2824
  );
2381
- group(
2382
- lines,
2383
- "removed",
2384
- plan.removed.map(
2385
- (p) => `${p}: removed locally; server entry would be kept`
2386
- )
2387
- );
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
+ }
2388
2836
  group(lines, "skipped", plan.skipped.map(skippedLine));
2389
2837
  if (plan.unchangedLocal.length > 0) {
2390
2838
  lines.push(`unchanged (${plan.unchangedLocal.length})`);
@@ -2397,6 +2845,7 @@ function contextPushJson(input) {
2397
2845
  directory: input.directory,
2398
2846
  dryRun: input.dryRun,
2399
2847
  force: input.force,
2848
+ prune: input.prune,
2400
2849
  lock: input.lockPath,
2401
2850
  lockWritten: input.lockWritten,
2402
2851
  plan: {
@@ -2409,6 +2858,12 @@ function contextPushJson(input) {
2409
2858
  expectedContentHash: (_a2 = entry.expectedContentHash) != null ? _a2 : null
2410
2859
  };
2411
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
+ })),
2412
2867
  unchangedLocal: input.plan.unchangedLocal,
2413
2868
  removed: input.plan.removed,
2414
2869
  forgotten: input.plan.forgotten,
@@ -2437,6 +2892,9 @@ function buildContextCommand() {
2437
2892
  ).requiredOption("--project <id>", "Project ID").option("--org <slug>", "Organization context").option(
2438
2893
  "--force",
2439
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"
2440
2898
  ).option("--dry-run", "Print the plan; send nothing and write no lock").option(
2441
2899
  "--exclude <pattern>",
2442
2900
  "Also leave out paths matching this gitignore pattern (repeatable)",
@@ -2544,7 +3002,8 @@ function runPush(dir, opts, format) {
2544
3002
  formatDryRunReport({
2545
3003
  projectId: opts.project,
2546
3004
  directory: root,
2547
- plan
3005
+ plan,
3006
+ prune: Boolean(opts.prune)
2548
3007
  })
2549
3008
  );
2550
3009
  return;
@@ -2555,7 +3014,9 @@ function runPush(dir, opts, format) {
2555
3014
  org: opts.org,
2556
3015
  root,
2557
3016
  plan,
2558
- force: Boolean(opts.force)
3017
+ force: Boolean(opts.force),
3018
+ prune: Boolean(opts.prune),
3019
+ lock
2559
3020
  });
2560
3021
  const next = nextContextLock({
2561
3022
  previous: lock,
@@ -2584,14 +3045,20 @@ function runPush(dir, opts, format) {
2584
3045
  projectId: opts.project,
2585
3046
  directory: root,
2586
3047
  plan,
2587
- results: pushed.results
3048
+ results: pushed.results,
3049
+ prune: Boolean(opts.prune),
3050
+ pruneTargets: pushed.pruneTargets
2588
3051
  })
2589
3052
  );
2590
3053
  }
3054
+ const prune = Boolean(opts.prune);
2591
3055
  if (pushed.stoppedBy !== null) {
2592
- throw pushed.stoppedBy;
3056
+ throw prune ? new CliFailure(
3057
+ `${pushed.stoppedBy.message} (${pruneSummary(pushed.results, pushed.pruneTargets)})`,
3058
+ pushed.stoppedBy.exitCode
3059
+ ) : pushed.stoppedBy;
2593
3060
  }
2594
- throwIfIncomplete(pushed.results);
3061
+ throwIfIncomplete(pushed.results, prune, pushed.pruneTargets);
2595
3062
  });
2596
3063
  }
2597
3064
  function jsonOutcome(input) {
@@ -2602,36 +3069,93 @@ function jsonOutcome(input) {
2602
3069
  lockWritten: input.lockWritten,
2603
3070
  dryRun: Boolean(input.opts.dryRun),
2604
3071
  force: Boolean(input.opts.force),
3072
+ prune: Boolean(input.opts.prune),
2605
3073
  plan: input.plan,
2606
3074
  results: input.results
2607
3075
  });
2608
3076
  }
2609
- function throwIfIncomplete(results) {
2610
- const failed = results.filter((r) => r.status === "failed");
2611
- const conflicts = results.filter((r) => r.status === "conflict");
2612
- if (failed.length === 0 && conflicts.length === 0) {
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) {
2613
3106
  return;
2614
3107
  }
2615
- const parts = [];
2616
- if (conflicts.length > 0) {
2617
- parts.push(
2618
- `${conflicts.length} conflict${conflicts.length === 1 ? "" : "s"}`
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
+ );
3116
+ }
3117
+ if (unsupported.length > 0) {
3118
+ parts.push(
3119
+ `${unsupported.length} move${unsupported.length === 1 ? "" : "s"} the server does not support yet`
3120
+ );
3121
+ }
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(", ")}).`
2619
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
+ }
2620
3136
  }
2621
- const first = failed[0];
2622
- if (first && first.status === "failed") {
2623
- parts.push(
2624
- `${failed.length} failed, first ${first.sourcePath}: ${first.error}`
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."
2625
3140
  );
2626
3141
  }
2627
- const total = conflicts.length + failed.length;
2628
- let message = `${total} of ${results.length} file${results.length === 1 ? "" : "s"} sent ${total === 1 ? "was" : "were"} not stored (${parts.join(", ")}).`;
2629
- if (conflicts.length > 0) {
2630
- message += " 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).";
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
+ }
2631
3154
  }
3155
+ const firstFailure = (_a2 = failed[0]) != null ? _a2 : deleteFailed[0];
2632
3156
  throw new CliFailure(
2633
- message,
2634
- first && first.status === "failed" ? first.exitCode : 1
3157
+ sentences.join(" "),
3158
+ firstFailure && (firstFailure.status === "failed" || firstFailure.status === "delete-failed") ? firstFailure.exitCode : 1
2635
3159
  );
2636
3160
  }
2637
3161