monday-cli 0.7.1 → 0.8.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (122) hide show
  1. package/CHANGELOG.md +249 -49
  2. package/README.md +87 -45
  3. package/dist/api/assets.d.ts +3 -3
  4. package/dist/api/column-types.d.ts +14 -7
  5. package/dist/api/column-types.d.ts.map +1 -1
  6. package/dist/api/column-types.js +14 -7
  7. package/dist/api/column-types.js.map +1 -1
  8. package/dist/api/error-decoration.d.ts +124 -0
  9. package/dist/api/error-decoration.d.ts.map +1 -0
  10. package/dist/api/error-decoration.js +161 -0
  11. package/dist/api/error-decoration.js.map +1 -0
  12. package/dist/api/fetch-transport-helpers.d.ts +97 -0
  13. package/dist/api/fetch-transport-helpers.d.ts.map +1 -0
  14. package/dist/api/fetch-transport-helpers.js +175 -0
  15. package/dist/api/fetch-transport-helpers.js.map +1 -0
  16. package/dist/api/file-column-set.d.ts +388 -82
  17. package/dist/api/file-column-set.d.ts.map +1 -1
  18. package/dist/api/file-column-set.js +466 -88
  19. package/dist/api/file-column-set.js.map +1 -1
  20. package/dist/api/multipart-transport.d.ts +95 -60
  21. package/dist/api/multipart-transport.d.ts.map +1 -1
  22. package/dist/api/multipart-transport.js +102 -120
  23. package/dist/api/multipart-transport.js.map +1 -1
  24. package/dist/api/transport.d.ts.map +1 -1
  25. package/dist/api/transport.js +2 -99
  26. package/dist/api/transport.js.map +1 -1
  27. package/dist/cli/program.js +1 -1
  28. package/dist/cli/program.js.map +1 -1
  29. package/dist/commands/auth/login.js +1 -1
  30. package/dist/commands/auth/login.js.map +1 -1
  31. package/dist/commands/auth/logout.js +1 -1
  32. package/dist/commands/auth/logout.js.map +1 -1
  33. package/dist/commands/board/column-create.d.ts +20 -2
  34. package/dist/commands/board/column-create.d.ts.map +1 -1
  35. package/dist/commands/board/column-create.js +191 -20
  36. package/dist/commands/board/column-create.js.map +1 -1
  37. package/dist/commands/completion.js +1 -1
  38. package/dist/commands/completion.js.map +1 -1
  39. package/dist/commands/dev/configure.js +1 -1
  40. package/dist/commands/dev/configure.js.map +1 -1
  41. package/dist/commands/dev/discover.js +1 -1
  42. package/dist/commands/dev/discover.js.map +1 -1
  43. package/dist/commands/dev/doctor.js +1 -1
  44. package/dist/commands/dev/doctor.js.map +1 -1
  45. package/dist/commands/dev/epic/items.js +2 -2
  46. package/dist/commands/dev/epic/items.js.map +1 -1
  47. package/dist/commands/dev/epic/list.js +2 -2
  48. package/dist/commands/dev/epic/list.js.map +1 -1
  49. package/dist/commands/dev/release/list.js +2 -2
  50. package/dist/commands/dev/release/list.js.map +1 -1
  51. package/dist/commands/dev/sprint/current.js +2 -2
  52. package/dist/commands/dev/sprint/current.js.map +1 -1
  53. package/dist/commands/dev/sprint/items.js +2 -2
  54. package/dist/commands/dev/sprint/items.js.map +1 -1
  55. package/dist/commands/dev/sprint/list.js +2 -2
  56. package/dist/commands/dev/sprint/list.js.map +1 -1
  57. package/dist/commands/dev/task/block.js +2 -2
  58. package/dist/commands/dev/task/block.js.map +1 -1
  59. package/dist/commands/dev/task/done.js +2 -2
  60. package/dist/commands/dev/task/done.js.map +1 -1
  61. package/dist/commands/dev/task/list.js +2 -2
  62. package/dist/commands/dev/task/list.js.map +1 -1
  63. package/dist/commands/dev/task/start.js +2 -2
  64. package/dist/commands/dev/task/start.js.map +1 -1
  65. package/dist/commands/doc/get.js +1 -1
  66. package/dist/commands/doc/get.js.map +1 -1
  67. package/dist/commands/doc/list.js +1 -1
  68. package/dist/commands/doc/list.js.map +1 -1
  69. package/dist/commands/item/clear.d.ts.map +1 -1
  70. package/dist/commands/item/clear.js +15 -41
  71. package/dist/commands/item/clear.js.map +1 -1
  72. package/dist/commands/item/create.d.ts +93 -1
  73. package/dist/commands/item/create.d.ts.map +1 -1
  74. package/dist/commands/item/create.js +474 -53
  75. package/dist/commands/item/create.js.map +1 -1
  76. package/dist/commands/item/search.js +7 -7
  77. package/dist/commands/item/search.js.map +1 -1
  78. package/dist/commands/item/set.d.ts +1 -0
  79. package/dist/commands/item/set.d.ts.map +1 -1
  80. package/dist/commands/item/set.js +94 -1
  81. package/dist/commands/item/set.js.map +1 -1
  82. package/dist/commands/item/time-track/start.js +2 -2
  83. package/dist/commands/item/time-track/start.js.map +1 -1
  84. package/dist/commands/item/time-track/stop.js +2 -2
  85. package/dist/commands/item/time-track/stop.js.map +1 -1
  86. package/dist/commands/item/update.d.ts +128 -11
  87. package/dist/commands/item/update.d.ts.map +1 -1
  88. package/dist/commands/item/update.js +784 -82
  89. package/dist/commands/item/update.js.map +1 -1
  90. package/dist/commands/item/upload.js +5 -5
  91. package/dist/commands/item/upload.js.map +1 -1
  92. package/dist/commands/item/watch.js +2 -2
  93. package/dist/commands/item/watch.js.map +1 -1
  94. package/dist/commands/notification/send.js +1 -1
  95. package/dist/commands/notification/send.js.map +1 -1
  96. package/dist/commands/update/body-source.d.ts +38 -0
  97. package/dist/commands/update/body-source.d.ts.map +1 -0
  98. package/dist/commands/update/body-source.js +80 -0
  99. package/dist/commands/update/body-source.js.map +1 -0
  100. package/dist/commands/update/upload.js +3 -3
  101. package/dist/commands/update/upload.js.map +1 -1
  102. package/dist/commands/user/team-add-members.js +1 -1
  103. package/dist/commands/user/team-add-members.js.map +1 -1
  104. package/dist/commands/user/team-create.js +1 -1
  105. package/dist/commands/user/team-create.js.map +1 -1
  106. package/dist/commands/user/team-remove-members.js +1 -1
  107. package/dist/commands/user/team-remove-members.js.map +1 -1
  108. package/dist/commands/webhook/create.js +1 -1
  109. package/dist/commands/webhook/create.js.map +1 -1
  110. package/dist/commands/webhook/delete.js +1 -1
  111. package/dist/commands/webhook/delete.js.map +1 -1
  112. package/dist/commands/webhook/list.js +1 -1
  113. package/dist/commands/webhook/list.js.map +1 -1
  114. package/dist/utils/file-source.d.ts +109 -0
  115. package/dist/utils/file-source.d.ts.map +1 -1
  116. package/dist/utils/file-source.js +123 -0
  117. package/dist/utils/file-source.js.map +1 -1
  118. package/dist/utils/output/table.d.ts +7 -6
  119. package/dist/utils/output/table.d.ts.map +1 -1
  120. package/dist/utils/output/table.js +32 -5
  121. package/dist/utils/output/table.js.map +1 -1
  122. package/package.json +2 -1
@@ -36,12 +36,25 @@
36
36
  * 2. The atomicity contract differs — the 13 existing types
37
37
  * bundle atomically via `change_multiple_column_values` when
38
38
  * ≥2 columns target the same item. File-column writes are
39
- * single-column-per-call only on Monday's wire; mixing a
40
- * file-column `--set` with any value `--set` / `--set-raw` /
41
- * `--name` in the same call would force a multi-leg dispatch
42
- * that breaks the existing atomicity guarantee. M38 enforces
43
- * single-file-only via the mutex rules below (D2 closure) —
44
- * the existing atomicity contract stays intact.
39
+ * single-column-per-wire-call only on Monday's wire (each
40
+ * `add_file_to_column` round-trip targets one file column);
41
+ * mixing a file-column `--set` with any value `--set` /
42
+ * `--set-raw` / `--name` in the same call would force a
43
+ * multi-leg dispatch across the multipart + JSON wire
44
+ * surfaces that breaks the existing atomicity guarantee
45
+ * (M38 enforces the mixed-rule mutex below — D2 closure;
46
+ * universal on non-create callShapes, SUPPRESSED on
47
+ * `'item_create'` per v0.7-M43 D6 asymmetry). Multi-file
48
+ * `--set` CARVED OUT at v0.8-M46 (D2 fold): the CLI now
49
+ * routes multi-leg fan-out for the 3 reachable callShapes
50
+ * (`'item_update_single'` / `'item_update_bulk'` /
51
+ * `'item_create'`), preserving the single-column-per-wire-
52
+ * call invariant by firing N sequential
53
+ * `add_file_to_column` calls per item. The existing
54
+ * atomicity contract on the JSON `column_values` bundle
55
+ * stays intact; the multi-file dispatch's atomicity
56
+ * envelope (partial-failure shape per D2 closure) is
57
+ * separately decorated.
45
58
  * 3. The translator's input contract is "value-string-to-JSON-
46
59
  * payload" — file columns take a file path, not a value
47
60
  * string. Path validation + `fs.stat` + `fs.access(R_OK)` +
@@ -82,20 +95,36 @@
82
95
  * after metadata loads). When any resolved column has `type ===
83
96
  * 'file'`:
84
97
  *
85
- * - Exactly ONE file `--set` entry allowed per call (single-
86
- * file scope; multi-file dispatch defers to v0.7.x Monday's
87
- * `add_file_to_column` is single-column per call on the wire
88
- * regardless of how many items the dispatch fans out across,
89
- * so this rule is universal).
98
+ * - Multi-file `--set` per call — **CARVED OUT at v0.8-M46**
99
+ * (D2 fold). At v0.6-M38 + v0.7 this rejected universally
100
+ * with `'multi_file_set_unsupported'`; v0.8-M46 lifts the
101
+ * CLI mutex for the 3 reachable callShapes
102
+ * (`'item_update_single'` / `'item_update_bulk'` /
103
+ * `'item_create'`) and routes through new `'file_multi'` /
104
+ * `'file_bulk_multi'` / `'file_create_multi'` enforcement
105
+ * kinds for the action body's per-item multi-leg fan-out
106
+ * (sequential within an item × parallel across items for
107
+ * bulk). Monday wire stays single-column-per-wire-call
108
+ * (each `add_file_to_column` round-trip targets one file
109
+ * column); M46 fans N CLI legs per item rather than
110
+ * bundling them on the wire. The throw remains for the
111
+ * `'item_set'` callShape as a defensive type-system
112
+ * ceiling — `monday item set <iid> <col>=<value>` accepts
113
+ * exactly one positional and is argv-incapable of
114
+ * expressing 2+ file `--set` entries; the throw is
115
+ * unreachable from production argv but kept as defense-in-
116
+ * depth.
90
117
  * - NO other value `--set` / `--set-raw` / `--name` flags
91
118
  * allowed (mixing would force non-atomic multi-leg dispatch
92
- * across the multipart + JSON wire surfaces).
119
+ * across the multipart + JSON wire surfaces — universal on
120
+ * `'item_set'` / `'item_update_single'` / `'item_update_bulk'`,
121
+ * SUPPRESSED on `'item_create'` per v0.7-M43 D6 asymmetry).
93
122
  * - Bulk `item update --where ... --set <file-col>=<path>` —
94
123
  * **CARVED OUT at v0.7-M42** (D5 fold). At v0.6-M38 this was
95
124
  * REJECTED with `'file_set_on_bulk_unsupported'`; v0.7-M42's
96
125
  * pre-flight contract diff returns
97
126
  * `{ kind: 'file_bulk', columnId, rawValue }` from
98
- * {@link enforceSingleFileColumnSet} on the clean dispatch
127
+ * {@link routeFileColumnDispatch} on the clean dispatch
99
128
  * path so the action body can branch into the per-item
100
129
  * multipart fan-out. Multi-file / mixed mutex rules STILL
101
130
  * apply on bulk (those are universal).
@@ -104,7 +133,7 @@
104
133
  * `'file_set_on_create_unsupported'`; v0.7-M43's pre-flight
105
134
  * contract diff returns
106
135
  * `{ kind: 'file_create', columnId, rawValue }` from
107
- * {@link enforceSingleFileColumnSet} on the clean dispatch
136
+ * {@link routeFileColumnDispatch} on the clean dispatch
108
137
  * path so the action body can branch into the two-leg
109
138
  * dispatch (`create_item` then `add_file_to_column`).
110
139
  * Multi-file mutex rule STILL applies on create (universal).
@@ -122,9 +151,20 @@
122
151
  * - `'mixed_file_and_value_sets'` — file `--set` + any value
123
152
  * `--set` / `--set-raw` / `--name` in same call. Applies on
124
153
  * single-item AND bulk call shapes (universal mutex rule).
125
- * - `'multi_file_set_unsupported'` — 2+ file `--set` entries
126
- * in same call. Applies on single-item AND bulk call shapes
127
- * (universal mutex rule).
154
+ * - `'multi_file_set_unsupported'` — **NO LONGER SURFACES**
155
+ * from `'item_update_single'` / `'item_update_bulk'` /
156
+ * `'item_create'` callShapes at v0.8-M46 onwards (D2 carve-out
157
+ * fold; multi-file dispatch now routes through new `file_multi`
158
+ * / `file_bulk_multi` / `file_create_multi` enforcement kinds
159
+ * for the action body's per-item multi-leg fan-out). The
160
+ * discriminator literal stays RESERVED across the codebase
161
+ * (do not reuse for a different rejection reason; mirrors
162
+ * M42 / M43 reserved-literal discipline). The throw remains
163
+ * for the `'item_set'` callShape as a defensive type-system
164
+ * ceiling — the single-positional `monday item set <iid>
165
+ * <col>=<value>` shape is argv-incapable of expressing 2+
166
+ * file `--set` entries, so this rejection is argv-unreachable
167
+ * in practice; kept as defense-in-depth.
128
168
  * - `'file_set_on_bulk_unsupported'` — **NO LONGER SURFACES**
129
169
  * at v0.7-M42 onwards. Historical reference only; the
130
170
  * discriminator literal stays reserved across the codebase
@@ -146,13 +186,33 @@
146
186
  * rejection see the full set of working alternatives rather
147
187
  * than just the M38 single-item form.
148
188
  *
149
- * **D7 closure — `<path>='-'` stdin support OUT OF SCOPE.**
150
- * Mirrors M31 `monday item upload`'s rejection rationale — no
151
- * clean `--filename` companion shape pinned for `--set
152
- * <file-col>=-` syntax (stdin reads byte-anonymously; the
153
- * filename is the load-bearing handle for Monday's wire
154
- * `Asset.name` slot). Carry-forward candidate for v0.7.x once a
155
- * `--filename` companion shape is pinned.
189
+ * **D7 closure — `<path>='-'` stdin support CARVED OUT at
190
+ * v0.8-M47.** Deferred from v0.6-M38 (no `--filename` companion
191
+ * shape pinned then); v0.8-M47 pins the OPTIONAL `--filename
192
+ * <name>` companion (default `"blob"`; Monday accepts any non-empty
193
+ * `Asset.name` and `500`s only an empty one — probe-pinned) and
194
+ * accepts a bare `-` file `--set` value as a stdin source. Because
195
+ * stdin is a single non-replayable stream, the carve-out is scoped
196
+ * to single-file, single-target dispatch: exactly one
197
+ * `<file-col>=-` per call, as the SOLE file `--set` entry, on
198
+ * `item set` / single-item `item update` / `item create`.
199
+ * {@link routeFileColumnDispatch}'s stdin scope gate (mutex
200
+ * priority 0) enforces it — `'multiple_stdin_file_sets'` /
201
+ * `'stdin_file_set_not_sole_file'` / `'stdin_file_set_on_bulk_
202
+ * unsupported'` reject with `usage_error` (literals reserved). A
203
+ * clean stdin source routes through the same single-file
204
+ * `kind: 'file'` / `'file_create'` as a path source; the action
205
+ * body sources the Blob from `readStdinFileSource`
206
+ * (`src/utils/file-source.ts`) instead of {@link buildBlobFromPath},
207
+ * then dispatches it through M31's `addFileToColumn` fetcher directly
208
+ * (the path leg builds its Blob inside {@link executeFileColumnSet};
209
+ * the stdin Blob is already in hand). Live read + dispatch shipped at
210
+ * the v0.8-M47 IMPL alongside the size-less dry-run echo (D4 — a
211
+ * stream can't be `fs.stat`'d, so the echo omits `file_size_bytes`);
212
+ * an empty stdin payload rejects `usage_error` with
213
+ * `details.reason: 'stdin_file_empty'` before any wire activity. The
214
+ * verb-shaped `monday item upload` stays path-only (no stdin) —
215
+ * M47 carves out only the friendly `--set` path.
156
216
  *
157
217
  * **No new ERROR_CODE (D8 closure; registry stays at 29).** All
158
218
  * M38-specific rejections route through existing `usage_error`
@@ -169,8 +229,8 @@
169
229
  * canonical cross-link.
170
230
  */
171
231
  import { z } from 'zod';
172
- import { ApiError } from '../utils/errors.js';
173
- import { buildBlobFromPath } from '../utils/file-source.js';
232
+ import { ApiError, MondayCliError } from '../utils/errors.js';
233
+ import { buildBlobFromPath, isStdinFileSetSource } from '../utils/file-source.js';
174
234
  import { addFileToColumn, assetSchema } from './assets.js';
175
235
  import { resolveColumnWithRefresh } from './columns.js';
176
236
  import { foldResolverWarningsIntoError } from './resolver-error-fold.js';
@@ -199,6 +259,42 @@ export const fileColumnSetOutputSchema = z
199
259
  asset: assetSchema,
200
260
  })
201
261
  .strict();
262
+ /**
263
+ * Output envelope shape for the v0.8-M46 multi-file `--set` dispatch
264
+ * leg on single-item `monday item update <iid> --set f1=p1 --set
265
+ * f2=p2 ...`. Discriminate from M38's single-file envelope via the
266
+ * new `operation: 'add_files_to_columns'` literal (plural) — agents
267
+ * branch on `data.operation` to handle single vs multi shapes
268
+ * uniformly.
269
+ *
270
+ * Per-file slot mirrors M31's `Asset` shape verbatim wrapped under
271
+ * a `column_id` + `filename` + `file_size_bytes` per-entry context.
272
+ * The `applied_file_columns` slot echoes the file columns that
273
+ * landed on success (length N; always present on multi-file success
274
+ * envelopes).
275
+ *
276
+ * **Status: schema landed at v0.8-M46 pre-flight contract diff;
277
+ * runtime emit shipped at v0.8-M46 IMPL.**
278
+ * `runItemUpdateSingleFileMultiDispatch` (commands/item/update.ts)
279
+ * emits against this schema on the single-item multi-file path.
280
+ */
281
+ export const fileColumnSetMultiOutputSchema = z
282
+ .object({
283
+ operation: z.literal('add_files_to_columns'),
284
+ item_id: z.string().min(1),
285
+ assets: z
286
+ .array(z
287
+ .object({
288
+ column_id: z.string().min(1),
289
+ filename: z.string().min(1),
290
+ file_size_bytes: z.number().int().nonnegative(),
291
+ asset: assetSchema,
292
+ })
293
+ .strict())
294
+ .min(2),
295
+ applied_file_columns: z.array(z.string().min(1)).min(2),
296
+ })
297
+ .strict();
202
298
  /**
203
299
  * Reads the local file at `inputs.entry.filePath`, constructs a Blob
204
300
  * via {@link buildBlobFromPath} (with `Content-Type` sniffed from the
@@ -219,7 +315,7 @@ export const fileColumnSetOutputSchema = z
219
315
  * 1. Parsing argv + collecting `--set` / `--set-raw` / `--name`
220
316
  * entries.
221
317
  * 2. Resolving columns via `resolveColumnWithRefresh`.
222
- * 3. Calling {@link enforceSingleFileColumnSet} to detect the
318
+ * 3. Calling {@link routeFileColumnDispatch} to detect the
223
319
  * file-column dispatch leg + enforce the mutex rules.
224
320
  * 4. Running {@link precheckLocalFile} from
225
321
  * `src/utils/file-source.ts` on the agent-supplied path to
@@ -254,6 +350,79 @@ export const executeFileColumnSet = async (inputs) => {
254
350
  complexity: result.complexity,
255
351
  };
256
352
  };
353
+ /**
354
+ * Sequential multi-leg `add_file_to_column` fan-out shared by the
355
+ * three v0.8-M46 multi-file callShape helpers
356
+ * (`runItemUpdateSingleFileMultiDispatch` /
357
+ * `runItemUpdateBulkFileMultiDispatch` per-item worker /
358
+ * `runItemCreateFileMultiDispatch`). R-v0.8-NEW-1 lift — the inner
359
+ * loop + partial-failure accumulator is byte-identical across all 3
360
+ * sites; only the envelope decoration diverges (kept in the caller).
361
+ *
362
+ * **Sequential within an item (D1 closure).** Legs fire in
363
+ * `inputs.entries` order so `appliedColumns` echoes the file columns
364
+ * that landed before a failure in dispatch order. The bulk path
365
+ * achieves cross-item parallelism by invoking this helper inside each
366
+ * per-item `dispatchParallel` worker — parallel ACROSS items ×
367
+ * sequential WITHIN each item.
368
+ *
369
+ * **Failure handling.** A `MondayCliError` from any leg stops the
370
+ * loop and returns the accumulator with `failure` populated (the
371
+ * caller decorates + optionally remaps). A non-`MondayCliError`
372
+ * (programmer bug in the wire layer) re-throws to the runner's
373
+ * catch-all so it surfaces as a whole-call `internal_error` rather
374
+ * than masquerading as a recoverable partial failure — mirrors the
375
+ * non-CliError re-throw arms in M42's bulk + M43's create-time
376
+ * single-file dispatch helpers.
377
+ */
378
+ export const dispatchFileLegsSequentially = async (inputs) => {
379
+ const assets = [];
380
+ const appliedColumns = [];
381
+ let lastComplexity = null;
382
+ for (const entry of inputs.entries) {
383
+ try {
384
+ const result = await executeFileColumnSet({
385
+ client: inputs.client,
386
+ multipart: inputs.multipart,
387
+ itemId: inputs.itemId,
388
+ entry: {
389
+ columnId: entry.columnId,
390
+ columnType: 'file',
391
+ rawValue: entry.rawValue,
392
+ filePath: entry.filePath,
393
+ filename: entry.filename,
394
+ fileSizeBytes: entry.fileSizeBytes,
395
+ },
396
+ signal: inputs.signal,
397
+ retries: inputs.retries,
398
+ });
399
+ assets.push({
400
+ column_id: entry.columnId,
401
+ filename: entry.filename,
402
+ file_size_bytes: entry.fileSizeBytes,
403
+ asset: result.asset,
404
+ });
405
+ appliedColumns.push(entry.columnId);
406
+ lastComplexity = result.complexity;
407
+ }
408
+ catch (err) {
409
+ if (err instanceof MondayCliError) {
410
+ return {
411
+ appliedColumns,
412
+ assets,
413
+ lastComplexity,
414
+ failure: { failedColumn: entry.columnId, cause: err },
415
+ };
416
+ }
417
+ // Non-CliError programmer bug — re-throw to the runner's
418
+ // catch-all (whole-call `internal_error`). Routing it through
419
+ // a partial-failure envelope would falsely promise a recovery
420
+ // handle for a broken contract.
421
+ throw err;
422
+ }
423
+ }
424
+ return { appliedColumns, assets, lastComplexity };
425
+ };
257
426
  /**
258
427
  * Iterates `inputs.setEntries`, identifies entries with
259
428
  * `columnType === 'file'`, applies the mutex rules per D2 / D5 / D6
@@ -262,19 +431,39 @@ export const executeFileColumnSet = async (inputs) => {
262
431
  * throws `ApiError('usage_error', ...)` on a mutex violation.
263
432
  *
264
433
  * Mutex priority (ratified at M38 pre-flight; updated at v0.7-M42
265
- * + v0.7-M43 pre-flights to fold the D5 bulk + D6 create carve-outs):
434
+ * + v0.7-M43 pre-flights to fold the D5 bulk + D6 create carve-outs;
435
+ * updated at v0.8-M46 pre-flight to fold the D2 multi-file carve-out;
436
+ * updated at v0.8-M47 pre-flight to add the stdin scope gate):
266
437
  *
267
- * 1. **callShape gate — NONE remaining post-v0.7-M43** both
268
- * `'item_update_bulk'` (D5 fold at v0.7-M42) and `'item_create'`
269
- * (D6 fold at v0.7-M43) short-circuit-throws have been removed.
270
- * Every callShape falls through to the universal multi-file
271
- * gate + a callShape-aware mixed gate, then returns a per-
272
- * callShape `kind` on the clean path.
273
- * 2. **multi-file leg** 2+ file `--set` entries (any callShape)
274
- * surface `'multi_file_set_unsupported'`. Universal rule:
275
- * Monday's `add_file_to_column` is single-column per call on
276
- * the wire regardless of fan-out shape.
277
- * 3. **mixed leg** 1 file `--set` + any value `--set` /
438
+ * 0. **stdin scope gate (v0.8-M47 D7 fold).** When ≥1 file `--set`
439
+ * value is the bare `-` stdin sentinel, enforce stdin's single-
440
+ * file / single-target scope: `'multiple_stdin_file_sets'` (2+
441
+ * `=-`), `'stdin_file_set_not_sole_file'` (stdin + another file
442
+ * `--set`), `'stdin_file_set_on_bulk_unsupported'` (`=-` on the
443
+ * `'item_update_bulk'` callShape). All `usage_error`; literals
444
+ * reserved. A clean stdin source (exactly one `=-`, sole file,
445
+ * single-target) falls through to the clean single-file leg and
446
+ * returns `kind: 'file'` / `'file_create'` identical routing to
447
+ * a path source; the action body sources the Blob from stdin. The
448
+ * mixed + duplicate gates below still apply to a stdin entry.
449
+ * 1. **callShape gate — ONLY `'item_set'` defensive throw remains**
450
+ * post-v0.8-M46. The v0.6-M38 `'item_update_bulk'` (D5 fold at
451
+ * v0.7-M42) and `'item_create'` (D6 fold at v0.7-M43) short-
452
+ * circuit-throws have been removed; the v0.6-M38 universal
453
+ * multi-file throw FOLDED at v0.8-M46 (D2) for the 3 reachable
454
+ * callShapes. The `'item_set'` callShape's multi-file throw
455
+ * stays as defensive type-system ceiling — the single-
456
+ * positional `monday item set <iid> <col>=<value>` verb is
457
+ * argv-incapable of expressing 2+ file `--set` entries, so
458
+ * this branch is argv-unreachable in practice; kept as
459
+ * defense-in-depth.
460
+ * 2. **multi-file leg (`'item_set'` only post-fold)** — 2+ file
461
+ * `--set` entries on `'item_set'` callShape surface
462
+ * `'multi_file_set_unsupported'` (argv-unreachable defensive).
463
+ * The other 3 reachable callShapes (`'item_update_single'` /
464
+ * `'item_update_bulk'` / `'item_create'`) fall through to the
465
+ * mixed gate + clean leg for the multi-file dispatch routing.
466
+ * 3. **mixed leg** — 1+ file `--set` + any value `--set` /
278
467
  * `--set-raw` / `--name` surfaces `'mixed_file_and_value_sets'`
279
468
  * on `'item_set'` / `'item_update_single'` / `'item_update_bulk'`.
280
469
  * Universal rule on those callShapes: mixing forces non-atomic
@@ -283,29 +472,106 @@ export const executeFileColumnSet = async (inputs) => {
283
472
  * asymmetry — `create_item` natively bundles non-file
284
473
  * `column_values` atomically into leg-1, and `--name` is
285
474
  * required on create.
286
- * 4. **clean leg** — 1 file `--set`, no mutex violation:
287
- * - `'item_update_single'` / `'item_set'` return
288
- * `{ kind: 'file', columnId, rawValue }` for downstream
289
- * {@link precheckLocalFile} + {@link executeFileColumnSet}
290
- * (M38 path; unchanged).
291
- * - `'item_update_bulk'` → return `{ kind: 'file_bulk',
292
- * columnId, rawValue }` for the action body's per-item
293
- * multipart fan-out (v0.7-M42 D5 carve-out fold).
294
- * - `'item_create'` return `{ kind: 'file_create', columnId,
295
- * rawValue }` for the action body's two-leg `create_item`
296
- * then `add_file_to_column` helper (v0.7-M43 D6 carve-out
297
- * fold).
475
+ * 4. **clean leg** — 1 file `--set`, no mutex violation. Branches
476
+ * on N (single vs multi) then callShape:
477
+ * - N=1 (single-file path):
478
+ * - `'item_update_single'` / `'item_set'` → `{ kind: 'file',
479
+ * columnId, rawValue }` (M38 path; unchanged).
480
+ * - `'item_update_bulk'` → `{ kind: 'file_bulk', ... }`
481
+ * (v0.7-M42 D5 carve-out fold).
482
+ * - `'item_create'` `{ kind: 'file_create', ... }`
483
+ * (v0.7-M43 D6 carve-out fold).
484
+ * - N>1 (multi-file path v0.8-M46 D2 carve-out fold):
485
+ * - `'item_update_single'` `{ kind: 'file_multi', entries }`
486
+ * for the single-item multi-leg fan-out helper.
487
+ * - `'item_update_bulk'` → `{ kind: 'file_bulk_multi',
488
+ * entries }` for the bulk per-item multi-leg fan-out.
489
+ * - `'item_create'` → `{ kind: 'file_create_multi', entries }`
490
+ * for the create-time two-leg-group multi-file helper.
298
491
  *
299
492
  * The function is sync + pure. No I/O. Path validation lives at a
300
493
  * SEPARATE step (`precheckLocalFile` from `src/utils/file-source.ts`)
301
- * that the caller runs AFTER this function returns a `kind: 'file'`
494
+ * that the caller runs AFTER this function returns a `kind: 'file*'`
302
495
  * result.
303
496
  */
304
- export const enforceSingleFileColumnSet = (inputs) => {
497
+ export const routeFileColumnDispatch = (inputs) => {
305
498
  const fileSetEntries = inputs.setEntries.filter((e) => e.columnType === 'file');
306
499
  if (fileSetEntries.length === 0) {
307
500
  return { kind: 'json' };
308
501
  }
502
+ // v0.8-M47 stdin file `--set` scope gate (D7 fold). A file `--set`
503
+ // value of the bare `-` sentinel sources the file body from stdin.
504
+ // stdin is a single non-replayable stream, so the contract scopes it
505
+ // to single-file, single-target dispatch (D-list closures at
506
+ // v0.8-plan §3 M47): exactly one `<file-col>=-` per call, as the
507
+ // SOLE file entry, on a single-target callShape. The three
508
+ // violations reject with `usage_error` + a reserved `details.reason`
509
+ // discriminator (no new ERROR_CODE — registry stays 29). A clean
510
+ // stdin source falls through to the single-file clean leg below and
511
+ // returns `kind: 'file'` (single-item update) / `'file_create'`
512
+ // (create) just like a path source — the rawValue carries the `-`
513
+ // sentinel and the action body's dispatch helper sources from stdin
514
+ // (via `readStdinFileSource`) instead of `precheckLocalFile`. The
515
+ // mixed-rule + duplicate-column checks below still apply to a stdin
516
+ // entry (e.g. `--set f=- --set status=Done` rejects `mixed_file_and_
517
+ // value_sets`); stdin only changes the file SOURCE, not the mutex
518
+ // surface around it.
519
+ const stdinFileEntries = fileSetEntries.filter((e) => isStdinFileSetSource(e.rawValue));
520
+ if (stdinFileEntries.length > 0) {
521
+ const firstStdin = stdinFileEntries[0];
522
+ /* c8 ignore next 6 — defensive: length > 0 guarantees [0] is
523
+ defined; the guard exists for `noUncheckedIndexedAccess`
524
+ narrowing (mirrors the `fe === undefined` guards below). */
525
+ if (firstStdin === undefined) {
526
+ throw new ApiError('internal_error', 'routeFileColumnDispatch: stdin entry narrowing failed');
527
+ }
528
+ if (stdinFileEntries.length > 1) {
529
+ throw new ApiError('usage_error', `Only one \`--set <file-col>=-\` stdin source is allowed per call ` +
530
+ `(stdin is a single non-replayable stream — it can't be split ` +
531
+ `across multiple file columns). Pass at most one \`<file-col>=-\` ` +
532
+ `and use local file paths for the rest.`, {
533
+ details: {
534
+ reason: 'multiple_stdin_file_sets',
535
+ stdin_file_count: stdinFileEntries.length,
536
+ stdin_file_column_ids: stdinFileEntries.map((e) => e.columnId),
537
+ hint: 'at most one `<file-col>=-` per call; source the other file ' +
538
+ 'columns from local paths.',
539
+ },
540
+ });
541
+ }
542
+ if (fileSetEntries.length > 1) {
543
+ throw new ApiError('usage_error', `A \`--set <file-col>=-\` stdin source must be the only file ` +
544
+ `\`--set\` entry in the call (stdin is a single stream — it ` +
545
+ `can't be combined with other file uploads in one invocation). ` +
546
+ `Run the stdin upload in its own call, or source every file ` +
547
+ `column from a local path.`, {
548
+ details: {
549
+ reason: 'stdin_file_set_not_sole_file',
550
+ file_count: fileSetEntries.length,
551
+ stdin_file_column_id: firstStdin.columnId,
552
+ file_column_ids: fileSetEntries.map((e) => e.columnId),
553
+ hint: 'use `<file-col>=-` alone (e.g. `cat f | monday item set ' +
554
+ '<iid> <file-col>=-`), or source all file columns from paths.',
555
+ },
556
+ });
557
+ }
558
+ if (inputs.callShape === 'item_update_bulk') {
559
+ throw new ApiError('usage_error', `\`--set <file-col>=-\` (stdin) is not supported on the bulk ` +
560
+ `\`monday item update --where ...\` path: stdin is a single ` +
561
+ `non-replayable stream and can't be fanned out across the ` +
562
+ `matched item set. Pipe to a temp file and \`--set ` +
563
+ `<file-col>=<path>\` for bulk, or target one item with ` +
564
+ `\`monday item update <iid> --set <file-col>=-\`.`, {
565
+ details: {
566
+ reason: 'stdin_file_set_on_bulk_unsupported',
567
+ column_id: firstStdin.columnId,
568
+ call_shape: inputs.callShape,
569
+ hint: 'stdin file `--set` is single-target only; use a local path ' +
570
+ 'for bulk fan-out or target a single item id.',
571
+ },
572
+ });
573
+ }
574
+ }
309
575
  // callShape gate — NO callShape short-circuits at this layer
310
576
  // post v0.7-M43. The v0.6-M38 `'item_create'` short-circuit-throw
311
577
  // (`'file_set_on_create_unsupported'`) FOLDED at v0.7-M43 (D6):
@@ -317,29 +583,41 @@ export const enforceSingleFileColumnSet = (inputs) => {
317
583
  // runtime rejection without a fresh contract decision. (Parallels
318
584
  // the v0.7-M42 fold of `'file_set_on_bulk_unsupported'` for
319
585
  // `'item_update_bulk'`.)
320
- // Multi-file leg (D2 multi). 2+ file `--set` entries on any
321
- // callShape applies universally (single-item AND bulk) because
322
- // Monday's `add_file_to_column` is single-column per call on the
323
- // wire regardless of how many items the dispatch fans out across.
324
- // Multi-file dispatch carries forward as a v0.7.x candidate.
325
- if (fileSetEntries.length > 1) {
326
- throw new ApiError('usage_error', `Multi-file \`--set <file-col>=<path>\` is not supported ` +
327
- `(${String(fileSetEntries.length)} file \`--set\` entries ` +
328
- `detected; deferred to v0.7.x per cli-design §5.3 + v0.6-plan ` +
329
- `§3 M38 D2 closure — carry-forward from v0.6 unchanged at ` +
330
- `v0.7-M42). Monday's \`add_file_to_column\` mutation is ` +
331
- `single-column per call on the wire; multi-file dispatch + ` +
332
- `concurrent multipart over the shared transport carry design ` +
333
- `dimensions worth their own milestone. Pass exactly one ` +
334
- `\`--set <file-col>=<path>\` per call; for multiple uploads, ` +
335
- `run separate calls.`, {
586
+ // Multi-file leg (D2 multi). v0.8-M46 D2 carve-out fold: lifted
587
+ // on the 3 reachable callShapes (`'item_update_single'`,
588
+ // `'item_update_bulk'`, `'item_create'`); the action body routes
589
+ // the multi-file dispatch into the per-callShape multi-leg
590
+ // helper via the new `file_multi` / `file_bulk_multi` /
591
+ // `file_create_multi` enforcement kinds (see clean-leg block
592
+ // below).
593
+ //
594
+ // The `'item_set'` callShape's defensive throw stays in force
595
+ // the single-positional `monday item set <iid> <col>=<value>`
596
+ // verb is argv-incapable of expressing 2+ file `--set` entries,
597
+ // so this branch is argv-unreachable in practice; kept as
598
+ // defense-in-depth + type-system ceiling. The
599
+ // `'multi_file_set_unsupported'` discriminator literal stays
600
+ // RESERVED across the codebase post-fold (mirrors v0.7-M42 D5
601
+ // fold of `'file_set_on_bulk_unsupported'` + v0.7-M43 D6 fold of
602
+ // `'file_set_on_create_unsupported'` reserved-literal discipline).
603
+ if (fileSetEntries.length > 1 && inputs.callShape === 'item_set') {
604
+ throw new ApiError('usage_error', `Multi-file \`--set <file-col>=<path>\` is not supported on the ` +
605
+ `\`monday item set\` single-positional verb (argv-unreachable ` +
606
+ `defensive guard — \`item set <iid> <col>=<value>\` accepts ` +
607
+ `exactly one positional). For multi-file dispatch, use ` +
608
+ `\`monday item update <iid> --set <file-col>=<path> --set ` +
609
+ `<file-col2>=<path2> ...\` (v0.8-M46 single-item multi-file ` +
610
+ `path) or \`monday item create --board <bid> --set ` +
611
+ `<file-col>=<path> --set <file-col2>=<path2> ...\` ` +
612
+ `(v0.8-M46 create-time multi-file path).`, {
336
613
  details: {
337
614
  reason: 'multi_file_set_unsupported',
338
615
  file_count: fileSetEntries.length,
339
616
  file_column_ids: fileSetEntries.map((e) => e.columnId),
340
- deferred_to: 'v0.7.x',
341
- hint: 'pass exactly one `--set <file-col>=<path>` per call; ' +
342
- 'run separate calls for multiple file uploads.',
617
+ call_shape: inputs.callShape,
618
+ hint: 'use `monday item update <iid> --set ...` or `monday item ' +
619
+ 'create --set ...` for multi-file dispatch (v0.8-M46 ' +
620
+ 'carve-out fold).',
343
621
  },
344
622
  });
345
623
  }
@@ -371,7 +649,7 @@ export const enforceSingleFileColumnSet = (inputs) => {
371
649
  const fe = fileSetEntries[0];
372
650
  /* c8 ignore next 3 */
373
651
  if (fe === undefined) {
374
- throw new ApiError('internal_error', 'enforceSingleFileColumnSet: file entry narrowing failed (mixed)');
652
+ throw new ApiError('internal_error', 'routeFileColumnDispatch: file entry narrowing failed (mixed)');
375
653
  }
376
654
  throw new ApiError('usage_error', `Mixing a file \`--set <file-col>=<path>\` with value \`--set\` / ` +
377
655
  `\`--set-raw\` / \`--name\` in the same call is not supported at ` +
@@ -394,20 +672,88 @@ export const enforceSingleFileColumnSet = (inputs) => {
394
672
  },
395
673
  });
396
674
  }
397
- // Clean dispatch leg. Single file `--set`, no mutex violation.
398
- // Discriminators:
399
- // - `'item_update_single'` / `'item_set'` → `kind: 'file'`
400
- // (M38 single-item path; unchanged).
401
- // - `'item_update_bulk'` → `kind: 'file_bulk'` (v0.7-M42 D5
402
- // carve-out fold; action body's per-item multipart fan-out).
403
- // - `'item_create'` `kind: 'file_create'` (v0.7-M43 D6
404
- // carve-out fold; action body's two-leg `create_item` then
405
- // `add_file_to_column` dispatch with orphan-warn atomicity
406
- // envelope per D1 closure).
675
+ // Clean dispatch leg. ≥1 file `--set`, no mutex violation.
676
+ // Branches on N (single vs multi) then on callShape:
677
+ // N=1 (single-file path):
678
+ // - `'item_update_single'` / `'item_set'` → `kind: 'file'`
679
+ // (M38 single-item path; unchanged).
680
+ // - `'item_update_bulk'` `kind: 'file_bulk'` (v0.7-M42 D5
681
+ // carve-out fold; action body's per-item multipart fan-out).
682
+ // - `'item_create'` `kind: 'file_create'` (v0.7-M43 D6
683
+ // carve-out fold; action body's two-leg `create_item` then
684
+ // `add_file_to_column` dispatch with orphan-warn atomicity
685
+ // envelope per D1 closure).
686
+ // N>1 (multi-file path — v0.8-M46 D2 carve-out fold):
687
+ // - `'item_update_single'` → `kind: 'file_multi'` (single-item
688
+ // N-leg fan-out helper `runItemUpdateSingleFileMultiDispatch`).
689
+ // - `'item_update_bulk'` → `kind: 'file_bulk_multi'` (per-item
690
+ // N-leg fan-out helper `runItemUpdateBulkFileMultiDispatch`;
691
+ // cross-item parallel × within-item sequential).
692
+ // - `'item_create'` → `kind: 'file_create_multi'` (two-leg-
693
+ // group helper `runItemCreateFileMultiDispatch`; leg-1
694
+ // `create_item` then N sequential `add_file_to_column`
695
+ // legs). The `'item_set'` callShape is argv-unreachable
696
+ // (single-positional; cannot express 2+ file `--set`) and
697
+ // throws the defensive `'multi_file_set_unsupported'` above.
698
+ if (fileSetEntries.length > 1) {
699
+ // v0.8-M46 Codex R1 P2-1 fix: reject duplicate resolved file-
700
+ // column IDs across multi-file entries (mirrors JSON path's
701
+ // existing cross-token duplicate-resolved-ID contract at
702
+ // `src/api/resolution-pass.ts` + `docs/output-shapes.md`).
703
+ // Without this guard, `--set attachments=/p/a --set
704
+ // id:attachments=/p/b` (two distinct argv tokens that resolve
705
+ // to the same file column ID) would either silently dispatch
706
+ // two `add_file_to_column` legs against the same column (the
707
+ // second leg's file replaces the first wire-side — surprising
708
+ // behavior) or downstream `projectedEntries` token-by-find
709
+ // logic in `preCheckM38FileDispatch` would lose token identity
710
+ // (map both entries to the first token's slot). Rejecting at
711
+ // the enforcement layer is the simplest contract that aligns
712
+ // with the JSON path's existing behavior + sidesteps the
713
+ // token-identity edge case.
714
+ const seenColumnIds = new Set();
715
+ for (const entry of fileSetEntries) {
716
+ if (seenColumnIds.has(entry.columnId)) {
717
+ throw new ApiError('usage_error', `Multiple \`--set\` entries resolve to the same file column ` +
718
+ `\`${entry.columnId}\` (v0.8-M46 multi-file dispatch requires ` +
719
+ `each file column to appear at most once per call). Monday's ` +
720
+ `\`add_file_to_column\` is single-column per call on the wire; ` +
721
+ `two legs against the same column would either silently ` +
722
+ `replace the first file or surface non-deterministic ` +
723
+ `wire-side ordering. Drop one of the duplicate entries or ` +
724
+ `target distinct file columns.`, {
725
+ details: {
726
+ reason: 'duplicate_resolved_file_columns',
727
+ column_id: entry.columnId,
728
+ file_count: fileSetEntries.length,
729
+ file_column_ids: fileSetEntries.map((e) => e.columnId),
730
+ hint: 'each file column may appear at most once across `--set` ' +
731
+ 'entries; drop the duplicate or target distinct file ' +
732
+ 'columns.',
733
+ },
734
+ });
735
+ }
736
+ seenColumnIds.add(entry.columnId);
737
+ }
738
+ const entries = fileSetEntries.map((e) => ({
739
+ columnId: e.columnId,
740
+ rawValue: e.rawValue,
741
+ }));
742
+ if (inputs.callShape === 'item_update_bulk') {
743
+ return { kind: 'file_bulk_multi', entries };
744
+ }
745
+ if (inputs.callShape === 'item_create') {
746
+ return { kind: 'file_create_multi', entries };
747
+ }
748
+ // `'item_update_single'` (and `'item_set'` defensively — the
749
+ // multi-file gate above already threw on `'item_set'`, so this
750
+ // branch only fires for `'item_update_single'` in practice).
751
+ return { kind: 'file_multi', entries };
752
+ }
407
753
  const fe = fileSetEntries[0];
408
754
  /* c8 ignore next 3 */
409
755
  if (fe === undefined) {
410
- throw new ApiError('internal_error', 'enforceSingleFileColumnSet: file entry narrowing failed (clean)');
756
+ throw new ApiError('internal_error', 'routeFileColumnDispatch: file entry narrowing failed (clean)');
411
757
  }
412
758
  if (inputs.callShape === 'item_update_bulk') {
413
759
  return { kind: 'file_bulk', columnId: fe.columnId, rawValue: fe.rawValue };
@@ -504,7 +850,7 @@ export const preCheckM38FileDispatch = async (inputs) => {
504
850
  // matters for the mutex check; columnId / columnType slots are
505
851
  // unused on the mixed-set discriminator path.
506
852
  const setRawEntries = Array.from({ length: inputs.setRawCount }, () => ({ columnId: '', columnType: '' }));
507
- const enforcement = enforceSingleFileColumnSet({
853
+ const enforcement = routeFileColumnDispatch({
508
854
  callShape: inputs.callShape,
509
855
  setEntries: resolved.map((r) => ({
510
856
  columnId: r.columnId,
@@ -522,9 +868,41 @@ export const preCheckM38FileDispatch = async (inputs) => {
522
868
  cacheAgeSeconds: aggregateCacheAge,
523
869
  };
524
870
  }
525
- // enforcement.kind is `'file'` (single-item) OR `'file_bulk'`
526
- // (v0.7-M42 D5 carve-out fold). Find the matching resolved entry
527
- // for the file-column token (echo into resolved_ids downstream).
871
+ // v0.8-M46 multi-file branches (D2 closure). The enforcement
872
+ // result carries a list of resolved file-column entries (length
873
+ // 2, argv order). Map each entry back to its argv token by
874
+ // matching (columnId, rawValue) against the pre-check's
875
+ // `resolved` list — the token is the load-bearing handle for
876
+ // the dispatch envelope's `resolved_ids` slot.
877
+ if (enforcement.kind === 'file_multi' ||
878
+ enforcement.kind === 'file_bulk_multi' ||
879
+ enforcement.kind === 'file_create_multi') {
880
+ const projectedEntries = enforcement.entries.map((entry) => {
881
+ const fileResolved = resolved.find((r) => r.columnType === 'file' &&
882
+ r.columnId === entry.columnId &&
883
+ r.rawValue === entry.rawValue);
884
+ /* c8 ignore next 6 — defensive: enforcement returned entries
885
+ the pre-check passed in; every find must succeed. */
886
+ if (fileResolved === undefined) {
887
+ throw new ApiError('internal_error', 'preCheckM38FileDispatch: multi-file entry not found in resolved set after enforcement');
888
+ }
889
+ return {
890
+ columnId: entry.columnId,
891
+ rawValue: entry.rawValue,
892
+ token: fileResolved.token,
893
+ };
894
+ });
895
+ return {
896
+ kind: enforcement.kind,
897
+ entries: projectedEntries,
898
+ warnings,
899
+ source: aggregateSource,
900
+ cacheAgeSeconds: aggregateCacheAge,
901
+ };
902
+ }
903
+ // Single-file branches: `'file'` (M38) / `'file_bulk'` (M42) /
904
+ // `'file_create'` (M43). Find the matching resolved entry for
905
+ // the file-column token (echo into resolved_ids downstream).
528
906
  const fileResolved = resolved.find((r) => r.columnType === 'file' &&
529
907
  r.columnId === enforcement.columnId &&
530
908
  r.rawValue === enforcement.rawValue);