monday-cli 0.6.0 → 0.7.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (54) hide show
  1. package/CHANGELOG.md +438 -0
  2. package/README.md +165 -52
  3. package/dist/api/board-metadata.d.ts +7 -4
  4. package/dist/api/board-metadata.d.ts.map +1 -1
  5. package/dist/api/board-metadata.js +21 -6
  6. package/dist/api/board-metadata.js.map +1 -1
  7. package/dist/api/column-types.d.ts +61 -28
  8. package/dist/api/column-types.d.ts.map +1 -1
  9. package/dist/api/column-types.js +32 -13
  10. package/dist/api/column-types.js.map +1 -1
  11. package/dist/api/column-values.d.ts +22 -17
  12. package/dist/api/column-values.d.ts.map +1 -1
  13. package/dist/api/column-values.js +50 -34
  14. package/dist/api/column-values.js.map +1 -1
  15. package/dist/api/file-column-set.d.ts +164 -58
  16. package/dist/api/file-column-set.d.ts.map +1 -1
  17. package/dist/api/file-column-set.js +168 -110
  18. package/dist/api/file-column-set.js.map +1 -1
  19. package/dist/api/raw-write.d.ts +29 -18
  20. package/dist/api/raw-write.d.ts.map +1 -1
  21. package/dist/api/raw-write.js +48 -26
  22. package/dist/api/raw-write.js.map +1 -1
  23. package/dist/commands/board/column-create.d.ts +11 -6
  24. package/dist/commands/board/column-create.d.ts.map +1 -1
  25. package/dist/commands/board/column-create.js +23 -12
  26. package/dist/commands/board/column-create.js.map +1 -1
  27. package/dist/commands/board/describe.d.ts +5 -3
  28. package/dist/commands/board/describe.d.ts.map +1 -1
  29. package/dist/commands/board/describe.js +12 -4
  30. package/dist/commands/board/describe.js.map +1 -1
  31. package/dist/commands/emit.d.ts.map +1 -1
  32. package/dist/commands/emit.js +19 -16
  33. package/dist/commands/emit.js.map +1 -1
  34. package/dist/commands/item/create.d.ts +24 -8
  35. package/dist/commands/item/create.d.ts.map +1 -1
  36. package/dist/commands/item/create.js +494 -35
  37. package/dist/commands/item/create.js.map +1 -1
  38. package/dist/commands/item/update.d.ts +175 -6
  39. package/dist/commands/item/update.d.ts.map +1 -1
  40. package/dist/commands/item/update.js +697 -29
  41. package/dist/commands/item/update.js.map +1 -1
  42. package/dist/utils/output/select.d.ts +22 -0
  43. package/dist/utils/output/select.d.ts.map +1 -1
  44. package/dist/utils/output/select.js +30 -0
  45. package/dist/utils/output/select.js.map +1 -1
  46. package/dist/utils/output/table.d.ts +9 -0
  47. package/dist/utils/output/table.d.ts.map +1 -1
  48. package/dist/utils/output/table.js +13 -3
  49. package/dist/utils/output/table.js.map +1 -1
  50. package/package.json +1 -1
  51. package/dist/commands/update/body-source.d.ts +0 -38
  52. package/dist/commands/update/body-source.d.ts.map +0 -1
  53. package/dist/commands/update/body-source.js +0 -80
  54. package/dist/commands/update/body-source.js.map +0 -1
@@ -55,6 +55,8 @@ import { executeItemMutation } from '../../api/item-mutation-execute.js';
55
55
  import { executeFileColumnSet, fileColumnSetOutputSchema, preCheckM38FileDispatch, } from '../../api/file-column-set.js';
56
56
  import { precheckLocalFile } from '../../utils/file-source.js';
57
57
  import { invalidateBoard } from '../../api/cache.js';
58
+ import { dispatchSequential, } from '../../api/partial-success-mutation.js';
59
+ import { dispatchParallel } from '../../api/parallel-dispatch.js';
58
60
  import { parseSetRawExpression, } from '../../api/raw-write.js';
59
61
  import { splitSetExpression } from '../../api/set-expression.js';
60
62
  import { buildResolutionContexts } from '../../api/resolution-context.js';
@@ -75,11 +77,15 @@ import { MIN_CONCURRENCY, MAX_CONCURRENCY, } from '../../api/parallel-dispatch.j
75
77
  /**
76
78
  * Output envelope union — projected-item for the JSON translator
77
79
  * path (text / status / dropdown / date / people / etc.) +
78
- * file-dispatch envelope for the v0.6-M38 friendly `--set
79
- * <file-col>=<path>` path (single-item shape only; bulk file
80
- * dispatch rejects per D5). Agents discriminate on the `operation`
81
- * field: present (`'add_file_to_column'`) file dispatch shape;
82
- * absent projected-item shape.
80
+ * file-dispatch envelope for the friendly `--set <file-col>=<path>`
81
+ * path. The file-dispatch shape ships across v0.6-M38 (single-item
82
+ * `operation: 'add_file_to_column'`) and v0.7-M42 (bulk
83
+ * `operation: 'item_update_bulk_file_set'`; the per-item fan-out's
84
+ * `data.results[i].asset` slots wrap M31's `Asset` projection). The
85
+ * union below admits only the single-item shape because the bulk
86
+ * variant is emitted via its own `bulkFileSetDataSchema` at the
87
+ * `runItemUpdateBulkFileDispatch` helper; agents discriminate on
88
+ * `operation` (present + literal value identifies the variant).
83
89
  */
84
90
  export const itemUpdateOutputSchema = z.union([
85
91
  projectedItemSchema,
@@ -281,6 +287,7 @@ export const itemUpdateCommand = {
281
287
  apiVersion,
282
288
  ctx,
283
289
  programOpts: program.opts(),
290
+ multipart,
284
291
  });
285
292
  return;
286
293
  }
@@ -605,7 +612,7 @@ const bulkLiveDataSchema = z.object({
605
612
  * `--concurrency` flag is the future extension point.
606
613
  */
607
614
  const runBulk = async (inputs) => {
608
- const { parsed, client, globalFlags, apiVersion, ctx, programOpts } = inputs;
615
+ const { parsed, client, globalFlags, apiVersion, ctx, programOpts, multipart } = inputs;
609
616
  /* c8 ignore next 6 — defensive: validateInputShape guarantees
610
617
  parsed.board is non-undefined when shape is bulk; the type
611
618
  guard exists for TS. */
@@ -630,18 +637,37 @@ const runBulk = async (inputs) => {
630
637
  env: ctx.env,
631
638
  noCache: globalFlags.noCache,
632
639
  });
633
- // 1.5) v0.6-M38 D5 closure — bulk file-set REJECTS at the
634
- // column-resolution boundary, BEFORE the items_page walker
635
- // + confirmation gate. The pre-check resolves setEntries
636
- // against the now-warm metadata cache and runs
637
- // `enforceSingleFileColumnSet({callShape: 'item_update_bulk'})`
638
- // which throws `'file_set_on_bulk_unsupported'` whenever
639
- // any setEntry is a `file`-typed column. `--set-raw
640
- // <file-col>=<json>` stays at `translateRawColumnValue`'s
641
- // D3 permanent rejection (the pre-check only inspects
642
- // setEntries; the standard path's `translateRawColumnValue`
643
- // handles --set-raw rejection unchanged).
640
+ // 1.5) File-column dispatch pre-check at the column-resolution
641
+ // boundary, BEFORE the items_page walker + confirmation
642
+ // gate. The pre-check resolves setEntries against the now-
643
+ // warm metadata cache and runs
644
+ // `enforceSingleFileColumnSet({callShape: 'item_update_bulk'})`:
645
+ //
646
+ // - Multi-file `--set` throws `'multi_file_set_unsupported'`
647
+ // (universal rule; single-column-per-wire-call).
648
+ // - File `--set` + value `--set` / `--set-raw` / `--name`
649
+ // throws `'mixed_file_and_value_sets'` (universal rule;
650
+ // mixing forces non-atomic multi-leg dispatch).
651
+ // - Clean single file `--set` → returns
652
+ // `kind: 'file_bulk'` (v0.7-M42 D5 carve-out fold;
653
+ // action body branches into the per-item multipart
654
+ // fan-out helper `runItemUpdateBulkFileDispatch` —
655
+ // shipped at v0.7-M42 IMPL).
656
+ // - No file `--set` → returns `kind: 'json'` (standard
657
+ // JSON-translator path continues).
658
+ //
659
+ // At v0.6-M38 the `'item_update_bulk'` callShape rejected
660
+ // ALL bulk file `--set` paths with
661
+ // `'file_set_on_bulk_unsupported'`; v0.7-M42 pre-flight
662
+ // contract diff (`160330b`) carved that out per D5 fold,
663
+ // and IMPL (`22df2fa` + R1 fix-up `968b154`) shipped the
664
+ // runtime body. `--set-raw <file-col>=<json>` stays at
665
+ // `translateRawColumnValue`'s D3 permanent rejection (the
666
+ // pre-check only inspects setEntries; the standard path's
667
+ // `translateRawColumnValue` handles --set-raw rejection
668
+ // unchanged).
644
669
  let m38Warnings = [];
670
+ let m38FileBulk;
645
671
  if (setEntries.length > 0) {
646
672
  const m38 = await preCheckM38FileDispatch({
647
673
  client,
@@ -653,13 +679,20 @@ const runBulk = async (inputs) => {
653
679
  env: ctx.env,
654
680
  noCache: globalFlags.noCache,
655
681
  });
656
- // Round-2 P3-1 fix: capture pre-check warnings to thread
657
- // them into the final envelope. The bulk `'item_update_bulk'`
658
- // callShape throws on any file entry, so reaching this point
659
- // means `m38.kind === 'json'`. Pre-check resolution warnings
660
- // (column_token_collision / stale_cache_refreshed) survive
661
- // even though downstream cache hits suppress re-emission.
682
+ // Round-2 P3-1 fix carry-forward: capture pre-check warnings
683
+ // to thread them into the final envelope. Pre-check resolution
684
+ // warnings (column_token_collision / stale_cache_refreshed)
685
+ // survive even though downstream cache hits suppress
686
+ // re-emission.
662
687
  m38Warnings = m38.warnings;
688
+ if (m38.kind === 'file_bulk') {
689
+ // v0.7-M42 D5 carve-out fold. Hold the file_bulk slot for
690
+ // the items_page-walked dispatch leg below; the items_page
691
+ // walker still runs (collects target item IDs) + the
692
+ // confirmation gate still applies + the dispatch loop fans
693
+ // `executeFileColumnSet` across matched items.
694
+ m38FileBulk = m38;
695
+ }
663
696
  }
664
697
  const onColumnNotFound = meta.source === 'cache'
665
698
  ? async () => {
@@ -781,6 +814,40 @@ const runBulk = async (inputs) => {
781
814
  // fail-fast invariant. (See step 0 above — moving the parse there
782
815
  // means a malformed --set / --set-raw doesn't pay for the metadata
783
816
  // load + items_page walk first.)
817
+ // v0.7-M42 D5 carve-out fold — bulk file `--set` dispatch leg.
818
+ // When the pre-check returned `kind: 'file_bulk'`, branch into
819
+ // the per-item multipart fan-out helper here. The branch fires
820
+ // AFTER the items_page walker + confirmation gate (so an agent
821
+ // sees the matched-count via `confirmation_required` before
822
+ // the bulk file dispatch fans out) and BEFORE the JSON
823
+ // translator path's `resolveAndTranslate` call (which has
824
+ // nothing to translate when every `--set` is a file column).
825
+ //
826
+ // The helper runs single upfront `precheckLocalFile` + per-item
827
+ // `executeFileColumnSet` (fail-fast vs `--continue-on-error`
828
+ // per `parsed.continueOnError`; sequential vs parallel per
829
+ // `parsed.concurrency`) + post-dispatch `invalidateBoard` +
830
+ // envelope emit (`operation: 'item_update_bulk_file_set'`).
831
+ if (m38FileBulk !== undefined) {
832
+ await runItemUpdateBulkFileDispatch({
833
+ parsed,
834
+ client,
835
+ multipart,
836
+ ctx,
837
+ programOpts,
838
+ apiVersion,
839
+ boardId,
840
+ matchedItemIds,
841
+ m38: m38FileBulk,
842
+ metaSource: meta.source,
843
+ metaCacheAgeSeconds: meta.cacheAgeSeconds,
844
+ filterWarnings: filterResult.warnings,
845
+ retries: globalFlags.retry,
846
+ isDryRun: globalFlags.dryRun,
847
+ noCache: globalFlags.noCache,
848
+ });
849
+ return;
850
+ }
784
851
  const { dateResolution, peopleResolution, tagResolution, relationResolution } = buildResolutionContexts({ client, ctx, globalFlags });
785
852
  // 5) Dry-run path: per-item planChanges. Column resolution is
786
853
  // cached after the first call; per-item state read fires per
@@ -806,12 +873,12 @@ const runBulk = async (inputs) => {
806
873
  cacheAgeSeconds: meta.cacheAgeSeconds,
807
874
  });
808
875
  for (const itemId of matchedItemIds) {
809
- // v0.6-M38 D5 file-set rejection already fired at the pre-check
810
- // above (step 1.5) — bulk file-set never reaches this loop body
811
- // since `preCheckM38FileDispatch({callShape: 'item_update_bulk'})`
812
- // throws `'file_set_on_bulk_unsupported'` before items_page
813
- // walks. `--set-raw <file-col>=<json>` still rejects normally
814
- // via `translateRawColumnValue`'s D3 permanent rejection.
876
+ // v0.7-M42 IMPL: clean bulk file `--set` paths branched into
877
+ // `runItemUpdateBulkFileDispatch` above (the `m38FileBulk !==
878
+ // undefined` arm) and returned before reaching this loop, so
879
+ // this dry-run body only ever sees JSON-shaped paths.
880
+ // `--set-raw <file-col>=<json>` still rejects normally via
881
+ // `translateRawColumnValue`'s D3 permanent rejection.
815
882
  let result;
816
883
  try {
817
884
  result = await planChanges({
@@ -1211,4 +1278,605 @@ const runItemUpdateSingleFileDispatch = async (inputs) => {
1211
1278
  resolvedIds: { [inputs.m38.token]: inputs.m38.columnId },
1212
1279
  });
1213
1280
  };
1281
+ // ============================================================
1282
+ // v0.7-M42 bulk file `--set` carve-out fold (D5 closure from
1283
+ // v0.6-M38). Per-item multipart fan-out over the `--where`-resolved
1284
+ // item-id set, reusing v0.4-M31's `executeFileColumnSet` runtime
1285
+ // body verbatim under v0.4-M30's `dispatchParallel` /
1286
+ // `dispatchSequential` selector for `--concurrency` semantics.
1287
+ //
1288
+ // **Status: runtime body shipped at v0.7-M42 IMPL.** The pre-flight
1289
+ // contract diff (commit `160330b`) shipped the argv + pre-check +
1290
+ // items_page + confirmation-gate surface plus the per-item dispatch
1291
+ // stub that threw `internal_error` with `details.reason:
1292
+ // 'm42_preflight_stub'`; IMPL replaces the stub with the runtime
1293
+ // body below. The `'m42_preflight_stub'` literal is RESERVED across
1294
+ // the codebase (regression-guarded by an integration test that
1295
+ // asserts the literal does not appear in stdout/stderr); a
1296
+ // historical `'file_set_on_bulk_unsupported'` literal stays
1297
+ // reserved alongside it.
1298
+ //
1299
+ // **D-list closures (v0.7-plan §3 M42 entry):**
1300
+ //
1301
+ // - **D1 — `--concurrency` semantics for file dispatch.** Reuse
1302
+ // v0.4-M30's `dispatchParallel` over a shared
1303
+ // `MultipartTransport`. Each parallel worker constructs its
1304
+ // own `MultipartTransportRequest` per call; the transport
1305
+ // itself is connection-pool-shared per-token. Closes by
1306
+ // inheritance from M30 (concurrency probe pinned at
1307
+ // `scripts/probe/m30-concurrency.report.txt`) + M31 (multipart
1308
+ // wire pinned at `scripts/probe/m31-asset-upload.report.txt`).
1309
+ // No new probe required.
1310
+ //
1311
+ // - **D2 — Per-item asset slot in envelope.** Per-item
1312
+ // `data.results[i].asset: { id, name, ... }` echo on success
1313
+ // (mirrors M31's `itemUploadOutputSchema`'s `asset` slot);
1314
+ // per-item failure surfaces as
1315
+ // `data.results[i].error: { code, message }` per M25
1316
+ // partial-success. Aggregate `data.summary.{matched_count,
1317
+ // applied_count, failed_count, board_id, column_id, filename,
1318
+ // file_size_bytes}` extends M25's
1319
+ // `partialSuccessBulkUpdateDataSchema` with file-dispatch slots
1320
+ // so an agent reading the envelope sees which file was
1321
+ // dispatched + where.
1322
+ //
1323
+ // - **D3 — Per-item file pre-check timing.** Single upfront
1324
+ // `precheckLocalFile` call BEFORE the per-item dispatch loop —
1325
+ // the bulk shape has ONE file path (the value of the file
1326
+ // `--set`) shared across N matched items. A failed pre-check
1327
+ // surfaces upfront as `usage_error` with `details.reason:
1328
+ // 'file_not_readable'` / `'file_empty'` (mirrors M31 single-
1329
+ // item shape) — this is whole-call-abort regardless of
1330
+ // `--continue-on-error`, per cli-design §5.8's "pre-checks
1331
+ // MUST fire BEFORE any wire round-trip" atomicity discipline.
1332
+ // The `--continue-on-error` flag partitions ONLY the wire-
1333
+ // dispatch failures (per-item `add_file_to_column` rejections
1334
+ // from Monday), never the local file pre-check.
1335
+ //
1336
+ // - **D4 — ERROR_CODES delta.** Zero. Registry stays at 29.
1337
+ // Per-item dispatch failures route through the existing
1338
+ // `m25-shaped` per-record `error: { code, message }` shape
1339
+ // under `--continue-on-error`, or through the v0.1 fail-fast
1340
+ // decoration (`details.applied_to` + `applied_count` +
1341
+ // `failed_at_item` + `matched_count`) on the default path;
1342
+ // no new top-level code surfaces.
1343
+ //
1344
+ // **R-class watch-items.**
1345
+ //
1346
+ // - R-v0.6-NEW-1 (file pre-check + Blob-construction helper) —
1347
+ // `precheckLocalFile` consumer count goes 3 → 4 at IMPL (M31
1348
+ // item upload + M31 update upload + M38 item set / item update
1349
+ // single + M42 bulk item update). Already-shipped helper
1350
+ // scales cleanly to consumer 4; graduation candidate at the
1351
+ // 5th consumer (v0.7-M43 create-time fold likely tips it).
1352
+ // - R-v0.6-NEW-2 (`details.reason` discriminator pattern) — 4
1353
+ // instances at v0.6-M38; M42 IMPL adds zero new reasons (per
1354
+ // D4 zero-delta closure — the existing `'file_not_readable'`
1355
+ // / `'file_empty'` / `'multi_file_set_unsupported'` /
1356
+ // `'mixed_file_and_value_sets'` reasons cover every M42
1357
+ // rejection shape).
1358
+ // - R-NEW-76 (parseArgv-BEFORE-c8) — applied at pre-flight; the
1359
+ // c8-ignore boundary dropped at IMPL (runtime body fully
1360
+ // covered).
1361
+ // - R-NEW-72 (post-fix-up cross-doc grep) — apply at every
1362
+ // Codex IMPL fix-up round that flips a contract surface.
1363
+ //
1364
+ // **Future lift candidate.** The fail-fast error-decoration block
1365
+ // (`if (err.code === 'usage_error') { throw new UsageError(...) } else
1366
+ // { throw new ApiError(...) }`) is byte-equivalent across this
1367
+ // helper and the JSON-bulk action body (R-NEW-58 2-consumer
1368
+ // trigger). Lift candidate fires at the 3rd consumer (M43 create-
1369
+ // time fold may add one if its rollback / orphan-warn shape
1370
+ // re-uses the same decoration).
1371
+ // ============================================================
1372
+ /**
1373
+ * Per-item dispatch result for v0.7-M42 bulk file `--set` carve-out
1374
+ * fold. Mirrors the M25 `partialSuccessBulkUpdateResultSchema` shape
1375
+ * with the file-dispatch's `asset` slot replacing the JSON path's
1376
+ * `item` projection:
1377
+ *
1378
+ * - Success: `{ item_id, ok: true, asset: { id, name, ... } }`
1379
+ * - Failure: `{ item_id, ok: false, error: { code, message } }`
1380
+ *
1381
+ * Schema landed at the v0.7-M42 pre-flight contract diff
1382
+ * (`160330b`); runtime body shipped at v0.7-M42 IMPL (`22df2fa`)
1383
+ * + R1 fix-up (`968b154`).
1384
+ */
1385
+ export const bulkFileSetResultSchema = z.object({
1386
+ item_id: z.string().min(1),
1387
+ ok: z.boolean(),
1388
+ asset: z
1389
+ .object({
1390
+ id: z.string().min(1),
1391
+ name: z.string().min(1),
1392
+ })
1393
+ .loose()
1394
+ .optional(),
1395
+ error: z
1396
+ .object({
1397
+ code: z.string().min(1),
1398
+ message: z.string().min(1),
1399
+ })
1400
+ .optional(),
1401
+ });
1402
+ /**
1403
+ * Output `data` shape for the v0.7-M42 bulk file `--set` envelope.
1404
+ * Mirrors M25's `partialSuccessBulkUpdateDataSchema` structure —
1405
+ * `operation: 'item_update_bulk_file_set'` literal discriminator
1406
+ * + `summary.{matched_count, applied_count, failed_count,
1407
+ * board_id}` aggregate slots + per-item `results[]` array.
1408
+ *
1409
+ * Invariant: `matched_count === applied_count + failed_count` for
1410
+ * every emitted envelope (mirrors M25's invariant).
1411
+ */
1412
+ export const bulkFileSetDataSchema = z.object({
1413
+ operation: z.literal('item_update_bulk_file_set'),
1414
+ summary: z.object({
1415
+ matched_count: z.number().int().nonnegative(),
1416
+ applied_count: z.number().int().nonnegative(),
1417
+ failed_count: z.number().int().nonnegative(),
1418
+ board_id: z.string().min(1),
1419
+ column_id: z.string().min(1),
1420
+ filename: z.string().min(1),
1421
+ file_size_bytes: z.number().int().nonnegative(),
1422
+ }),
1423
+ results: z.array(bulkFileSetResultSchema),
1424
+ });
1425
+ /**
1426
+ * Bulk file `--set` per-item dispatch helper (v0.7-M42 D5 carve-out
1427
+ * fold).
1428
+ *
1429
+ * **Status: runtime body shipped at v0.7-M42 IMPL.** argv parse,
1430
+ * shape validation, board-metadata load, pre-check (which returned
1431
+ * `kind: 'file_bulk'` for callers reaching this helper), items_page
1432
+ * walk, and the confirmation gate run upstream — this helper takes
1433
+ * the resolved file-column dispatch slot + matched item-IDs and
1434
+ * fans the multipart wire across them under the partial-success vs
1435
+ * fail-fast contract.
1436
+ *
1437
+ * **Execution shape:**
1438
+ *
1439
+ * 1. Single upfront `precheckLocalFile(inputs.m38.rawValue)` —
1440
+ * one file path shared across all matched items. Failure
1441
+ * surfaces as `usage_error` (`file_not_readable` /
1442
+ * `file_empty`) whole-call-abort regardless of
1443
+ * `--continue-on-error` per D3 + cli-design §5.8 atomicity
1444
+ * discipline (pre-checks MUST fire BEFORE any wire round-trip).
1445
+ * 2. Dry-run branch — emits the D4-shaped envelope with one
1446
+ * `add_file_to_column` planned_change per matched item-ID
1447
+ * (no file bytes loaded; no multipart wire). Unlike M38
1448
+ * single-item which pins `source: 'none'` (its dry-run is
1449
+ * pure-local), bulk dry-run carries the upstream legs'
1450
+ * aggregated `source` (metadata load + items_page walk —
1451
+ * `mixed` when metadata was cache-served, `live` otherwise);
1452
+ * reaching this branch already paid for those wire legs.
1453
+ * 3. Live dispatch — two shapes per `parsed.continueOnError`:
1454
+ * - **Fail-fast (default)** — sequential loop over matched
1455
+ * items (no `--concurrency` per M30 D2 closure:
1456
+ * `--concurrency requires --continue-on-error`); first
1457
+ * per-item failure aborts whole-call with the v0.1-shaped
1458
+ * `details.applied_to` / `applied_count` / `failed_at_item`
1459
+ * / `matched_count` decoration so agents see how many
1460
+ * items applied before the failure.
1461
+ * - **`--continue-on-error`** — routes through
1462
+ * {@link dispatchSequential} (concurrency `undefined`/`1`)
1463
+ * or {@link dispatchParallel} (concurrency `> 1`) over a
1464
+ * shared {@link MultipartTransport}; per-item failures
1465
+ * land as `data.results[i].error: {code, message}` records,
1466
+ * successes carry `data.results[i].asset` via a side-map
1467
+ * fold keyed by `item_id`. `internal_error` re-throws
1468
+ * whole-call via the shared dispatchers' escape hatch
1469
+ * (M14 round-2 F1 precedent) so schema drift surfaces as
1470
+ * top-level `ok: false` rather than per-record.
1471
+ * 4. Cache invalidation — single `invalidateBoard(boardId, env)`
1472
+ * after the dispatch loop completes (mirrors M38's single-
1473
+ * leg invalidate timing; one board covers every matched
1474
+ * item's mutated `asset` slot).
1475
+ * 5. Envelope emit — `data: BulkFileSetData` with the
1476
+ * `operation: 'item_update_bulk_file_set'` literal
1477
+ * discriminator + per-item `results[]` + aggregate
1478
+ * `summary.{matched_count, applied_count, failed_count,
1479
+ * board_id, column_id, filename, file_size_bytes}`. Warnings
1480
+ * threaded as `dedupeWarnings([...filterWarnings,
1481
+ * ...m38.warnings])` (mirrors the JSON-bulk path); source
1482
+ * derives from `metaSource` (cache-served metadata + live
1483
+ * wire calls → `mixed`).
1484
+ */
1485
+ export const runItemUpdateBulkFileDispatch = async (inputs) => {
1486
+ // 1) Single upfront pre-check (D3 closure). Whole-call abort
1487
+ // regardless of `--continue-on-error` per cli-design §5.8 —
1488
+ // the local file pre-check is shared across N matched items,
1489
+ // so failure aborts the whole call before any multipart wire
1490
+ // leg fires. `precheckLocalFile` throws `usage_error` with
1491
+ // `details.reason: 'file_not_readable'` / `'file_empty'`
1492
+ // (M31 single-item discriminators reused per D4 zero-delta
1493
+ // closure).
1494
+ const precheck = await precheckLocalFile(inputs.m38.rawValue);
1495
+ // Combined warnings list threaded into every envelope emit below.
1496
+ // Mirrors the JSON-bulk `dedupeWarnings(filter ∪ m38)` pattern at
1497
+ // the action body; key is `code+message+token` so a pre-check
1498
+ // `stale_cache_refreshed` plus a filter-time `stale_cache_refreshed`
1499
+ // for the SAME token collapse to one entry.
1500
+ const combinedWarnings = dedupeWarnings([
1501
+ ...inputs.filterWarnings,
1502
+ ...inputs.m38.warnings,
1503
+ ]);
1504
+ // Source/cache-age aggregator. Seeded with the metadata leg, then
1505
+ // folds the M38 pre-check leg + a synthetic 'live' leg representing
1506
+ // the items_page walker (always live, fired upstream before this
1507
+ // helper). On dry-run the dispatch leg never fires; on live the
1508
+ // dispatch leg adds another 'live' record (idempotent under
1509
+ // `mergeSource` since 'live' + 'live' = 'live'). Codex IMPL R1
1510
+ // P2-1 fix — pre-fix the helper dropped `inputs.m38.source` +
1511
+ // `inputs.m38.cacheAgeSeconds`, so a cache-served file-column
1512
+ // resolution after a live metadata fetch surfaced `'live'`
1513
+ // instead of `'mixed'`. Mirrors the JSON-bulk path's
1514
+ // SourceAggregator pattern at runBulk's dry-run + live legs.
1515
+ const sourceAgg = new SourceAggregator({
1516
+ source: inputs.metaSource,
1517
+ cacheAgeSeconds: inputs.metaCacheAgeSeconds,
1518
+ });
1519
+ if (inputs.m38.source !== undefined) {
1520
+ sourceAgg.record(inputs.m38.source, inputs.m38.cacheAgeSeconds);
1521
+ }
1522
+ // items_page walker always fires live before reaching the helper
1523
+ // (the empty-match short-circuit is the only path that skips it,
1524
+ // and that path emits the envelope upstream — this helper never
1525
+ // sees matchedItemIds.length === 0). Record one synthetic 'live'
1526
+ // leg so the aggregate reflects the wire round-trip cost the
1527
+ // caller already paid.
1528
+ sourceAgg.record('live', null);
1529
+ // 2) Dry-run branch — D4-shaped envelope. One
1530
+ // `add_file_to_column` planned_change per matched item-ID;
1531
+ // no file bytes loaded, no multipart wire round-trip. Unlike
1532
+ // the M38 single-item dry-run (which pins `source: 'none'`
1533
+ // because its dry-run is pure-local), bulk dry-run carries
1534
+ // the aggregated upstream `source` — metadata load + items_page
1535
+ // walk + M38 pre-check already paid for wire legs to reach
1536
+ // here.
1537
+ if (inputs.isDryRun) {
1538
+ const plannedChanges = inputs.matchedItemIds.map((itemId) => ({
1539
+ operation: 'add_file_to_column',
1540
+ item_id: itemId,
1541
+ column_id: inputs.m38.columnId,
1542
+ file_path: inputs.m38.rawValue,
1543
+ filename: precheck.filename,
1544
+ file_size_bytes: precheck.fileSizeBytes,
1545
+ }));
1546
+ const dryRunAgg = sourceAgg.result();
1547
+ emitDryRun({
1548
+ ctx: inputs.ctx,
1549
+ programOpts: inputs.programOpts,
1550
+ plannedChanges,
1551
+ source: dryRunAgg.source,
1552
+ cacheAgeSeconds: dryRunAgg.cacheAgeSeconds,
1553
+ warnings: combinedWarnings,
1554
+ apiVersion: inputs.apiVersion,
1555
+ });
1556
+ return;
1557
+ }
1558
+ // 3) Live dispatch. Build the shared `FileColumnSetEntry` once —
1559
+ // every per-item leg uses the SAME local file (one path × N
1560
+ // items), so `buildBlobFromPath` (inside `executeFileColumnSet`)
1561
+ // re-reads the bytes per leg rather than sharing one Blob
1562
+ // instance. Re-reading per-leg is the simpler shape; a future
1563
+ // optimisation could memoise the bytes if profiling motivates
1564
+ // it (~bytes × N reads vs ~bytes × 1 read + held in memory).
1565
+ const entry = {
1566
+ columnId: inputs.m38.columnId,
1567
+ columnType: 'file',
1568
+ rawValue: inputs.m38.rawValue,
1569
+ filePath: precheck.filePath,
1570
+ filename: precheck.filename,
1571
+ fileSizeBytes: precheck.fileSizeBytes,
1572
+ };
1573
+ // Live dispatch is always 'live' — fold one more leg into the
1574
+ // aggregator. Idempotent if metadata + pre-check were also live
1575
+ // ('live' + 'live' = 'live'); promotes 'cache' to 'mixed' when
1576
+ // metadata/pre-check served from cache.
1577
+ sourceAgg.record('live', null);
1578
+ const liveAgg = sourceAgg.result();
1579
+ // resolved_ids slot — pre-check returned the resolved column ID
1580
+ // for the file token; echo it into the envelope's
1581
+ // `meta.resolved_ids` so agents can confirm token-to-ID resolution
1582
+ // (mirrors the M38 single-item envelope at `runItemUpdateSingleFileDispatch`).
1583
+ const resolvedIds = { [inputs.m38.token]: inputs.m38.columnId };
1584
+ // Resolution source for foldAndRemap — defaults to 'live' when
1585
+ // pre-check didn't record one (no resolveColumnWithRefresh leg
1586
+ // fired; the only path that's possible is the no-setEntries
1587
+ // shortcut, which doesn't reach this helper). The remap probe
1588
+ // refreshes board metadata + re-checks the file column's
1589
+ // `archived` flag when a Monday-side `validation_failed`
1590
+ // surfaces against cache-served file-column resolution. Codex
1591
+ // IMPL R1 P1-1 fix.
1592
+ const remapSource = inputs.m38.source ?? 'live';
1593
+ // Discriminator for the dispatch shape: fail-fast (default,
1594
+ // applied to the v0.1 fail-fast bulk path's `applied_to` decoration)
1595
+ // vs `--continue-on-error` (partial-success per-record envelope).
1596
+ // M30 D2 closure pins `--concurrency requires --continue-on-error`,
1597
+ // so fail-fast bulk file dispatch is always sequential N=1.
1598
+ const continueOnError = inputs.parsed.continueOnError === true;
1599
+ if (!continueOnError) {
1600
+ // Fail-fast bulk file dispatch. Sequential loop; first per-item
1601
+ // failure aborts whole-call. Track applied (item_id, asset)
1602
+ // pairs so the failure decoration can echo `applied_to` (matches
1603
+ // the JSON-bulk fail-fast pattern at runBulk's main loop).
1604
+ const appliedAssets = [];
1605
+ for (const itemId of inputs.matchedItemIds) {
1606
+ try {
1607
+ const result = await executeFileColumnSet({
1608
+ client: inputs.client,
1609
+ multipart: inputs.multipart,
1610
+ itemId,
1611
+ entry,
1612
+ signal: inputs.ctx.signal,
1613
+ retries: inputs.retries,
1614
+ });
1615
+ appliedAssets.push({ itemId, asset: result.asset });
1616
+ }
1617
+ catch (err) {
1618
+ if (err instanceof MondayCliError) {
1619
+ // Codex IMPL R1 P2-2 fix: if any prior item applied
1620
+ // successfully, the board's asset state already mutated
1621
+ // wire-side — invalidate the cache BEFORE re-throwing the
1622
+ // fail-fast error so a follow-up read doesn't serve stale
1623
+ // metadata. Mirrors the M38 single-item invalidate-on-
1624
+ // success pattern; the JSON-bulk fail-fast path has the
1625
+ // same gap (unchanged by this commit — separate lift
1626
+ // candidate per the future-lift-candidate note in the
1627
+ // module docstring).
1628
+ if (appliedAssets.length > 0) {
1629
+ await invalidateBoard(inputs.boardId, inputs.ctx.env);
1630
+ }
1631
+ // Codex IMPL R1 P1-1 fix: apply `foldAndRemap` BEFORE
1632
+ // building the decoration so per-item failures inherit
1633
+ // the SAME `validation_failed` → `column_archived`
1634
+ // stale-cache remap the JSON-bulk fail-fast path applies
1635
+ // (cli-design §6.5 stable-code rule). Without this, an
1636
+ // archived file column surfaces `validation_failed` on
1637
+ // file-bulk dispatch but `column_archived` on JSON-bulk
1638
+ // dispatch — agents keying on the stable code see
1639
+ // inconsistent outcomes for the same root cause.
1640
+ const remapped = await foldAndRemap({
1641
+ err,
1642
+ warnings: inputs.m38.warnings,
1643
+ client: inputs.client,
1644
+ boardId: inputs.boardId,
1645
+ columnIds: [inputs.m38.columnId],
1646
+ env: inputs.ctx.env,
1647
+ noCache: inputs.noCache,
1648
+ resolutionSource: remapSource,
1649
+ });
1650
+ // Same decoration shape as the JSON-bulk fail-fast path
1651
+ // (lines ~1334-1361 above). Preserves the existing error
1652
+ // class' fields and grafts `applied_count` / `applied_to`
1653
+ // / `failed_at_item` / `matched_count` onto `details`.
1654
+ const existing = remapped.details ?? {};
1655
+ const decoration = {
1656
+ ...existing,
1657
+ applied_count: appliedAssets.length,
1658
+ applied_to: appliedAssets.map((a) => a.itemId),
1659
+ failed_at_item: itemId,
1660
+ matched_count: inputs.matchedItemIds.length,
1661
+ };
1662
+ if (remapped.code === 'usage_error') {
1663
+ throw new UsageError(remapped.message, {
1664
+ ...(remapped.cause === undefined
1665
+ ? {}
1666
+ : { cause: remapped.cause }),
1667
+ details: decoration,
1668
+ });
1669
+ }
1670
+ throw new ApiError(remapped.code, remapped.message, {
1671
+ ...(remapped.cause === undefined
1672
+ ? {}
1673
+ : { cause: remapped.cause }),
1674
+ ...(remapped.httpStatus === undefined
1675
+ ? {}
1676
+ : { httpStatus: remapped.httpStatus }),
1677
+ ...(remapped.mondayCode === undefined
1678
+ ? {}
1679
+ : { mondayCode: remapped.mondayCode }),
1680
+ ...(remapped.requestId === undefined
1681
+ ? {}
1682
+ : { requestId: remapped.requestId }),
1683
+ retryable: remapped.retryable,
1684
+ ...(remapped.retryAfterSeconds === undefined
1685
+ ? {}
1686
+ : { retryAfterSeconds: remapped.retryAfterSeconds }),
1687
+ details: decoration,
1688
+ });
1689
+ }
1690
+ // Non-CliError programmer bug — re-throw to the runner's
1691
+ // catch-all (surfaces as `internal_error` whole-call;
1692
+ // mirrors the JSON-bulk fail-fast path). The partial-
1693
+ // success invalidate above doesn't fire for this branch
1694
+ // because non-CliError throws indicate broken contract,
1695
+ // not partial-mutation state worth preserving.
1696
+ throw err;
1697
+ }
1698
+ }
1699
+ // Every item applied — single board-cache invalidate before emit
1700
+ // (mirrors M38 single-leg invalidate timing).
1701
+ await invalidateBoard(inputs.boardId, inputs.ctx.env);
1702
+ const results = appliedAssets.map(({ itemId, asset }) => ({
1703
+ item_id: itemId,
1704
+ ok: true,
1705
+ asset,
1706
+ }));
1707
+ const data = {
1708
+ operation: 'item_update_bulk_file_set',
1709
+ summary: {
1710
+ matched_count: inputs.matchedItemIds.length,
1711
+ applied_count: appliedAssets.length,
1712
+ failed_count: 0,
1713
+ board_id: inputs.boardId,
1714
+ column_id: inputs.m38.columnId,
1715
+ filename: precheck.filename,
1716
+ file_size_bytes: precheck.fileSizeBytes,
1717
+ },
1718
+ results,
1719
+ };
1720
+ emitMutation({
1721
+ ctx: inputs.ctx,
1722
+ data,
1723
+ schema: bulkFileSetDataSchema,
1724
+ programOpts: inputs.programOpts,
1725
+ warnings: combinedWarnings,
1726
+ source: liveAgg.source,
1727
+ cacheAgeSeconds: liveAgg.cacheAgeSeconds,
1728
+ apiVersion: inputs.apiVersion,
1729
+ resolvedIds,
1730
+ });
1731
+ return;
1732
+ }
1733
+ // `--continue-on-error` path. Per-item failures land as per-record
1734
+ // `error: {code, message}` slots; the envelope is `ok: true`
1735
+ // regardless of how many items failed (universal partial-success
1736
+ // rule per cli-design §6.4). `internal_error` re-throws whole-call
1737
+ // via the shared dispatcher's escape hatch — schema drift in the
1738
+ // multipart response surfaces as top-level `ok: false`, not
1739
+ // papered over as a per-record slot.
1740
+ const assetById = new Map();
1741
+ const perTargetDispatch = async ({ targetId, }) => {
1742
+ try {
1743
+ const result = await executeFileColumnSet({
1744
+ client: inputs.client,
1745
+ multipart: inputs.multipart,
1746
+ itemId: targetId,
1747
+ entry,
1748
+ signal: inputs.ctx.signal,
1749
+ retries: inputs.retries,
1750
+ });
1751
+ assetById.set(targetId, result.asset);
1752
+ }
1753
+ catch (err) {
1754
+ if (err instanceof MondayCliError) {
1755
+ // Codex IMPL R1 P1-1 fix: apply `foldAndRemap` BEFORE
1756
+ // re-throwing into the shared dispatcher so per-record
1757
+ // `error.code` carries the SAME `column_archived` stable
1758
+ // code the JSON-bulk partial-success path emits. Mirrors
1759
+ // `runPartialSuccessBulkUpdate`'s perTargetDispatch
1760
+ // closure (src/api/partial-success-bulk.ts:431-475).
1761
+ // `foldAndRemap` NEVER converts a non-`internal_error`
1762
+ // into `internal_error`, so the dispatcher's
1763
+ // `internal_error` re-throw escape hatch (M14 round-2 F1)
1764
+ // stays intact: schema drift still surfaces as top-level
1765
+ // `ok: false` rather than papered over as a per-record
1766
+ // slot.
1767
+ const remapped = await foldAndRemap({
1768
+ err,
1769
+ warnings: inputs.m38.warnings,
1770
+ client: inputs.client,
1771
+ boardId: inputs.boardId,
1772
+ columnIds: [inputs.m38.columnId],
1773
+ env: inputs.ctx.env,
1774
+ noCache: inputs.noCache,
1775
+ resolutionSource: remapSource,
1776
+ });
1777
+ throw remapped;
1778
+ }
1779
+ // Non-CliError — programmer bug. Re-throw through
1780
+ // dispatchSequential / dispatchParallel's non-CliError
1781
+ // branch so the runner's catch-all surfaces as
1782
+ // internal_error (whole-call, not per-record).
1783
+ throw err;
1784
+ }
1785
+ };
1786
+ // Routing — `--concurrency > 1` routes through dispatchParallel
1787
+ // (bounded async-pool); absent / `=== 1` routes through
1788
+ // dispatchSequential. Both dispatchers thread the optional
1789
+ // `signal` so SIGINT-aware callers see consistent cooperative-
1790
+ // abort semantics (R-NEW-28 axis 6 — identical between routes).
1791
+ let dispatchResults;
1792
+ if (inputs.parsed.concurrency !== undefined &&
1793
+ inputs.parsed.concurrency > 1) {
1794
+ dispatchResults = await dispatchParallel(inputs.matchedItemIds, 'item_id', perTargetDispatch, inputs.parsed.concurrency, inputs.ctx.signal);
1795
+ }
1796
+ else {
1797
+ dispatchResults = await dispatchSequential(inputs.matchedItemIds, 'item_id', perTargetDispatch, inputs.ctx.signal);
1798
+ }
1799
+ // Single post-dispatch invalidate. Fires even when every per-item
1800
+ // dispatch failed (failed_count === matched_count) — wire calls
1801
+ // still fired against Monday, so the metadata cache's view of the
1802
+ // file column's content count may be stale even if the asset
1803
+ // didn't land. Cheap to always fire; expensive if missed.
1804
+ await invalidateBoard(inputs.boardId, inputs.ctx.env);
1805
+ // Fold dispatcher results + side-map assets into the
1806
+ // BulkFileSetResult[] shape. Mirrors `foldPartialSuccessBulkResult`
1807
+ // (`src/api/partial-success-bulk.ts`) but with the file-dispatch's
1808
+ // `asset` slot replacing the JSON path's `item` projection.
1809
+ const results = dispatchResults.map((row) => {
1810
+ const itemIdSlot = row.item_id;
1811
+ /* c8 ignore next 8 — dispatcher contract: every result row
1812
+ carries the id-field slot (populated by the dispatch helper);
1813
+ this guard catches a contract violation that would surface as
1814
+ a programmer bug, not a Monday-side failure. */
1815
+ if (typeof itemIdSlot !== 'string' || itemIdSlot.length === 0) {
1816
+ throw new ApiError('internal_error', 'bulk file dispatch result row is missing the `item_id` field — dispatcher contract violation.', { details: { record_keys: Object.keys(row) } });
1817
+ }
1818
+ if (row.ok) {
1819
+ const asset = assetById.get(itemIdSlot);
1820
+ /* c8 ignore next 8 — side-map invariant: every successful
1821
+ per-target dispatch records the asset; this guard catches a
1822
+ wrapper-layer miss (programmer bug). */
1823
+ if (asset === undefined) {
1824
+ throw new ApiError('internal_error', `bulk file dispatch result row for item_id ${itemIdSlot} reported ok: true but no Asset was captured — wrapper-layer side-map miss.`, { details: { item_id: itemIdSlot } });
1825
+ }
1826
+ return { item_id: itemIdSlot, ok: true, asset };
1827
+ }
1828
+ /* c8 ignore next 8 — dispatcher contract: every `ok: false` row
1829
+ carries the `error` slot (populated by the shared dispatcher's
1830
+ per-target error decoration). */
1831
+ if (row.error === undefined) {
1832
+ throw new ApiError('internal_error', `bulk file dispatch result row for item_id ${itemIdSlot} reported ok: false but no error payload was captured — dispatcher contract violation.`, { details: { item_id: itemIdSlot } });
1833
+ }
1834
+ return {
1835
+ item_id: itemIdSlot,
1836
+ ok: false,
1837
+ error: { code: row.error.code, message: row.error.message },
1838
+ };
1839
+ });
1840
+ const appliedCount = results.filter((r) => r.ok).length;
1841
+ const failedCount = results.filter((r) => !r.ok).length;
1842
+ /* c8 ignore next 11 — invariant: every matched item produces
1843
+ exactly one result row (success or failure) under both
1844
+ dispatchers; mismatch would indicate a programmer bug in the
1845
+ dispatcher or the fold. Mirrors `buildPartialSuccessBulkSummary`'s
1846
+ defensive check. */
1847
+ if (appliedCount + failedCount !== inputs.matchedItemIds.length) {
1848
+ throw new ApiError('internal_error', `bulk file dispatch summary invariant violated — matched_count (${String(inputs.matchedItemIds.length)}) !== applied_count (${String(appliedCount)}) + failed_count (${String(failedCount)}).`, {
1849
+ details: {
1850
+ matched_count: inputs.matchedItemIds.length,
1851
+ applied_count: appliedCount,
1852
+ failed_count: failedCount,
1853
+ board_id: inputs.boardId,
1854
+ },
1855
+ });
1856
+ }
1857
+ const data = {
1858
+ operation: 'item_update_bulk_file_set',
1859
+ summary: {
1860
+ matched_count: inputs.matchedItemIds.length,
1861
+ applied_count: appliedCount,
1862
+ failed_count: failedCount,
1863
+ board_id: inputs.boardId,
1864
+ column_id: inputs.m38.columnId,
1865
+ filename: precheck.filename,
1866
+ file_size_bytes: precheck.fileSizeBytes,
1867
+ },
1868
+ results,
1869
+ };
1870
+ emitMutation({
1871
+ ctx: inputs.ctx,
1872
+ data,
1873
+ schema: bulkFileSetDataSchema,
1874
+ programOpts: inputs.programOpts,
1875
+ warnings: combinedWarnings,
1876
+ source: liveAgg.source,
1877
+ cacheAgeSeconds: liveAgg.cacheAgeSeconds,
1878
+ apiVersion: inputs.apiVersion,
1879
+ resolvedIds,
1880
+ });
1881
+ };
1214
1882
  //# sourceMappingURL=update.js.map