orizu 0.5.39 → 0.5.41

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 (117) hide show
  1. package/README.md +77 -0
  2. package/dist/dataset-clone-workflow.d.ts +11 -0
  3. package/dist/dataset-clone-workflow.d.ts.map +1 -0
  4. package/dist/dataset-clone-workflow.js +100 -0
  5. package/dist/dataset-commands.d.ts +3 -1
  6. package/dist/dataset-commands.d.ts.map +1 -1
  7. package/dist/dataset-commands.js +107 -91
  8. package/dist/dataset-delete-workflow.d.ts +4 -0
  9. package/dist/dataset-delete-workflow.d.ts.map +1 -0
  10. package/dist/dataset-delete-workflow.js +75 -0
  11. package/dist/dataset-lock-workflow.d.ts +12 -0
  12. package/dist/dataset-lock-workflow.d.ts.map +1 -0
  13. package/dist/dataset-lock-workflow.js +76 -0
  14. package/dist/dataset-readme-workflow.d.ts +13 -0
  15. package/dist/dataset-readme-workflow.d.ts.map +1 -0
  16. package/dist/dataset-readme-workflow.js +82 -0
  17. package/dist/dataset-row-workflow.d.ts +14 -0
  18. package/dist/dataset-row-workflow.d.ts.map +1 -0
  19. package/dist/dataset-row-workflow.js +202 -0
  20. package/dist/dataset-split-reads.d.ts +28 -0
  21. package/dist/dataset-split-reads.d.ts.map +1 -0
  22. package/dist/dataset-split-reads.js +47 -0
  23. package/dist/dataset-transfer-upload.d.ts +2 -6
  24. package/dist/dataset-transfer-upload.d.ts.map +1 -1
  25. package/dist/dataset-transfer-upload.js +9 -2
  26. package/dist/dataset-upload-snapshot.d.ts.map +1 -1
  27. package/dist/dataset-upload-snapshot.js +7 -1
  28. package/dist/dataset-version-workflow.d.ts +24 -0
  29. package/dist/dataset-version-workflow.d.ts.map +1 -0
  30. package/dist/dataset-version-workflow.js +99 -0
  31. package/dist/feedback-cli.js +3 -3
  32. package/dist/help.d.ts.map +1 -1
  33. package/dist/help.js +29 -3
  34. package/dist/hosted-boot.d.ts +1 -0
  35. package/dist/hosted-boot.d.ts.map +1 -1
  36. package/dist/hosted-boot.js +17 -0
  37. package/dist/hosted-harness-opencode.d.ts +5 -6
  38. package/dist/hosted-harness-opencode.d.ts.map +1 -1
  39. package/dist/hosted-harness-opencode.js +73 -59
  40. package/dist/hosted-loop-lifecycle.d.ts +6 -11
  41. package/dist/hosted-loop-lifecycle.d.ts.map +1 -1
  42. package/dist/hosted-loop-lifecycle.js +6 -11
  43. package/dist/hosted-loop.d.ts +9 -0
  44. package/dist/hosted-loop.d.ts.map +1 -1
  45. package/dist/hosted-loop.js +19 -10
  46. package/dist/index.d.ts.map +1 -1
  47. package/dist/index.js +11 -3
  48. package/dist/instruction-delivery.d.ts +14 -0
  49. package/dist/instruction-delivery.d.ts.map +1 -0
  50. package/dist/instruction-delivery.js +103 -0
  51. package/dist/instruction-set-sync/index.d.ts +17 -0
  52. package/dist/instruction-set-sync/index.d.ts.map +1 -1
  53. package/dist/instruction-set-sync/index.js +44 -17
  54. package/dist/instruction-set-update/index.d.ts +2 -2
  55. package/dist/instruction-set-update/index.d.ts.map +1 -1
  56. package/dist/instruction-set-update/index.js +2 -2
  57. package/dist/instruction-sets-cli.d.ts.map +1 -1
  58. package/dist/instruction-sets-cli.js +19 -27
  59. package/dist/json-response.d.ts +1 -0
  60. package/dist/json-response.d.ts.map +1 -1
  61. package/dist/json-response.js +7 -0
  62. package/dist/last-error-record.d.ts +4 -2
  63. package/dist/last-error-record.d.ts.map +1 -1
  64. package/dist/last-error-record.js +10 -68
  65. package/dist/last-error-workflow.d.ts +4 -0
  66. package/dist/last-error-workflow.d.ts.map +1 -0
  67. package/dist/last-error-workflow.js +78 -0
  68. package/dist/preview-runtime.d.ts +2 -2
  69. package/dist/preview-runtime.d.ts.map +1 -1
  70. package/dist/preview-runtime.js +37 -7
  71. package/dist/task-commands.d.ts.map +1 -1
  72. package/dist/task-commands.js +9 -16
  73. package/dist/task-export-workflow.d.ts +4 -0
  74. package/dist/task-export-workflow.d.ts.map +1 -0
  75. package/dist/task-export-workflow.js +46 -0
  76. package/dist/task-report-workflow.d.ts +47 -0
  77. package/dist/task-report-workflow.d.ts.map +1 -0
  78. package/dist/task-report-workflow.js +160 -0
  79. package/dist/workbench-cli.js +1 -1
  80. package/package.json +1 -1
  81. package/src/dataset-clone-workflow.ts +100 -0
  82. package/src/dataset-commands.ts +95 -142
  83. package/src/dataset-delete-workflow.ts +72 -0
  84. package/src/dataset-lock-workflow.ts +74 -0
  85. package/src/dataset-readme-workflow.ts +80 -0
  86. package/src/dataset-row-workflow.ts +248 -0
  87. package/src/dataset-split-reads.ts +43 -0
  88. package/src/dataset-transfer-upload.ts +12 -5
  89. package/src/dataset-upload-snapshot.ts +8 -1
  90. package/src/dataset-version-workflow.ts +105 -0
  91. package/src/feedback-cli.ts +4 -4
  92. package/src/help.ts +29 -3
  93. package/src/hosted-boot.ts +16 -1
  94. package/src/hosted-harness-opencode.ts +74 -72
  95. package/src/hosted-loop-lifecycle.ts +6 -11
  96. package/src/hosted-loop.ts +20 -10
  97. package/src/hosted-model-cache.d.mts +2 -0
  98. package/src/hosted-model-cache.mjs +53 -0
  99. package/src/index.ts +12 -2
  100. package/src/instruction-delivery.ts +123 -0
  101. package/src/instruction-set-sync/index.ts +61 -14
  102. package/src/instruction-set-update/index.ts +4 -3
  103. package/src/instruction-sets-cli.ts +19 -27
  104. package/src/json-response.ts +8 -0
  105. package/src/last-error-record.ts +11 -85
  106. package/src/last-error-workflow.ts +121 -0
  107. package/src/preview-runtime.ts +40 -10
  108. package/src/task-commands.ts +9 -43
  109. package/src/task-export-workflow.ts +56 -0
  110. package/src/task-report-workflow.ts +179 -0
  111. package/src/workbench-cli.ts +1 -1
  112. package/vendor/skills/orizu/references/cli-reference.md +34 -0
  113. package/vendor/skills/orizu/references/dataset-design.md +1 -1
  114. package/dist/dataset-edit-rows.d.ts +0 -10
  115. package/dist/dataset-edit-rows.d.ts.map +0 -1
  116. package/dist/dataset-edit-rows.js +0 -90
  117. package/src/dataset-edit-rows.ts +0 -124
@@ -0,0 +1,100 @@
1
+ import { Cause, Effect, Exit, Result, Schema } from 'effect'
2
+
3
+ import { datasetTransferTimeoutMs } from './dataset-transfer-policy.js'
4
+ import { authedFetch } from './http.js'
5
+ import { parseJsonResponse } from './json-response.js'
6
+
7
+ // Loaded only through `await import(...)` from dataset-commands, like lock.
8
+
9
+ const SERVER_MAY_BE_CREATED = 'Dataset clone may already be created'
10
+
11
+ const ClonedDataset = Schema.Struct({
12
+ id: Schema.String,
13
+ name: Schema.String,
14
+ rowCount: Schema.Number,
15
+ parentDatasetId: Schema.String,
16
+ })
17
+ const CloneReply = Schema.Struct({ dataset: ClonedDataset })
18
+ const ErrorReply = Schema.Struct({ error: Schema.String })
19
+ export type ClonedDataset = typeof ClonedDataset.Type
20
+
21
+ // A refusal means nothing was cloned. After any other failure once the request
22
+ // was sent, the clone may exist. Unlike lock, cloning again is not harmless: it
23
+ // makes another copy, so the clone is never retried and the user is told to look.
24
+ class CloneFailure extends Error {
25
+ constructor(message: string, readonly mayBeCreated: boolean) { super(message) }
26
+ }
27
+
28
+ function mayBeCreatedGuidance(name: string | null): string {
29
+ const target = name ? `a dataset named ${JSON.stringify(name)}` : 'a new dataset named after the source with " Copy" at the end'
30
+ return `The clone may already have been created. Run \`orizu datasets list\` and look for ${target} before cloning again: running clone again makes another copy.`
31
+ }
32
+
33
+ // authedFetch raises this code only when it sent nothing, or when the server
34
+ // answered 401 and the changed credentials stopped a replay. Nothing was cloned.
35
+ function isAuthContextChange(cause: unknown): boolean {
36
+ return cause instanceof Error && cause.message.startsWith('ORIZU_AUTH_CONTEXT_CHANGED:')
37
+ }
38
+
39
+ function serverError(text: string): string | null {
40
+ try {
41
+ const decoded = Schema.decodeUnknownResult(ErrorReply)(JSON.parse(text))
42
+ return Result.isSuccess(decoded) ? decoded.success.error : null
43
+ } catch {
44
+ return null
45
+ }
46
+ }
47
+
48
+ function cloneRequest(datasetId: string, name: string | null, timeoutMs: number) {
49
+ return Effect.suspend(() => {
50
+ let sent = false
51
+ let timedOut = false
52
+ return Effect.tryPromise({
53
+ try: async signal => {
54
+ const request = new AbortController()
55
+ const abort = () => request.abort()
56
+ const timer = setTimeout(() => { timedOut = true; request.abort() }, timeoutMs)
57
+ if (signal.aborted) abort()
58
+ else signal.addEventListener('abort', abort, { once: true })
59
+ try {
60
+ const response = await authedFetch(`/api/cli/datasets/${encodeURIComponent(datasetId)}/clone`, {
61
+ method: 'POST',
62
+ headers: { 'Content-Type': 'application/json' },
63
+ body: JSON.stringify(name ? { name } : {}),
64
+ signal: request.signal,
65
+ }, () => { sent = true })
66
+ if (!response.ok) {
67
+ const text = await response.text()
68
+ const error = serverError(text)
69
+ // The app's own 5xx bodies name what happened; anything else at 5xx
70
+ // (a gateway page, an empty body) says nothing about the clone.
71
+ const mayBeCreated = response.status < 400 || error === SERVER_MAY_BE_CREATED || (response.status >= 500 && error === null)
72
+ throw new CloneFailure(`Clone failed: ${text}`, mayBeCreated)
73
+ }
74
+ const data = await parseJsonResponse<unknown>(response, 'Dataset clone')
75
+ const decoded = Schema.decodeUnknownResult(CloneReply)(data)
76
+ if (Result.isFailure(decoded)) {
77
+ throw new CloneFailure(`Dataset clone returned an unexpected response (status ${response.status}).`, true)
78
+ }
79
+ return decoded.success.dataset
80
+ } finally {
81
+ clearTimeout(timer)
82
+ signal.removeEventListener('abort', abort)
83
+ request.abort()
84
+ }
85
+ },
86
+ catch: cause => {
87
+ if (timedOut) return new CloneFailure('DATASET_TRANSFER_TIMEOUT: Dataset clone exceeded its deadline', true)
88
+ if (cause instanceof CloneFailure) return cause
89
+ return new CloneFailure(cause instanceof Error ? cause.message : String(cause), sent && !isAuthContextChange(cause))
90
+ },
91
+ })
92
+ }).pipe(Effect.mapError(failure => new Error(failure.mayBeCreated ? `${failure.message}\n${mayBeCreatedGuidance(name)}` : failure.message)))
93
+ }
94
+
95
+ // Nothing to clean up, so signals keep Node's default handling.
96
+ export async function cloneDataset(datasetId: string, name: string | null): Promise<ClonedDataset> {
97
+ const exit = await Effect.runPromiseExit(cloneRequest(datasetId, name, datasetTransferTimeoutMs()))
98
+ if (Exit.isSuccess(exit)) return exit.value
99
+ throw Cause.squash(exit.cause)
100
+ }
@@ -1,11 +1,12 @@
1
1
  // ORI-2044: moved from index.ts to keep dataset commands together.
2
2
 
3
- import { readFileSync, writeFileSync } from 'fs'
3
+ import { readFileSync } from 'fs'
4
4
  import { basename, extname } from 'path'
5
5
  import { createInterface } from 'readline/promises'
6
6
  import { stdin as input, stdout as output } from 'process'
7
7
  import {
8
8
  getArg,
9
+ hasArg,
9
10
  expandHomePath,
10
11
  getPositionalArg,
11
12
  hasJsonFlag,
@@ -21,14 +22,12 @@ import {
21
22
  } from './account-directory.js'
22
23
  import { sanitizeTerminalText, parseJsonResponse } from './json-response.js'
23
24
  import { authedFetch } from './http.js'
24
- import { extractErrorMessage } from './error-response.js'
25
25
  import { printJson, printLine } from './cli-console-output.js'
26
26
  import { readJsonFile } from './app-commands.js'
27
27
  import { printDatasetSummaries } from './archive-artifacts-cli.js'
28
28
  import { formatTerminalLink } from './auth-commands.js'
29
29
  import { parseDatasetFile } from './file-parser.js'
30
30
  import { parseDatasetReference } from './dataset-download.js'
31
- import { editDatasetRows as runEditDatasetRows } from './dataset-edit-rows.js'
32
31
  import { parseCommaSeparated } from './task-commands.js'
33
32
  import type { DatasetSelection } from './account-directory.js'
34
33
 
@@ -36,12 +35,13 @@ import type { DatasetSelection } from './account-directory.js'
36
35
 
37
36
  const MAX_README_LENGTH = 200_000
38
37
 
39
- interface DatasetVersionResponse {
40
- datasetVersion: {
41
- id: string
42
- rowCount: number
43
- artifactFormat?: string
44
- artifactStoragePath?: string
38
+ function readReadmeFile(readmeFile: string): string {
39
+ try {
40
+ return readFileSync(expandHomePath(readmeFile), 'utf8')
41
+ } catch (error) {
42
+ const missing = error instanceof Error && 'code' in error && error.code === 'ENOENT'
43
+ const reason = missing ? 'file not found' : error instanceof Error ? error.message : String(error)
44
+ throw new Error(`Could not read README file ${readmeFile}: ${reason}`)
45
45
  }
46
46
  }
47
47
 
@@ -54,7 +54,7 @@ function readReadmeMarkdownFromArgs(usage: string): string | null {
54
54
  }
55
55
 
56
56
  const markdown = readmeFile !== null
57
- ? readFileSync(expandHomePath(readmeFile), 'utf8')
57
+ ? readReadmeFile(readmeFile)
58
58
  : readmeText
59
59
 
60
60
  if (markdown !== null && markdown.length > MAX_README_LENGTH) {
@@ -117,28 +117,6 @@ async function resolveDatasetIdForProject(datasetRef: string, projectArg: string
117
117
  return matches[0].id
118
118
  }
119
119
 
120
- async function requestDatasetVersionSnapshot(
121
- datasetId: string,
122
- options: { versionLabel?: string | null; readmeMarkdown?: string | null } = {}
123
- ): Promise<DatasetVersionResponse> {
124
- const response = await authedFetch(`/api/cli/datasets/${encodeURIComponent(datasetId)}/versions`, {
125
- method: 'POST',
126
- headers: { 'Content-Type': 'application/json' },
127
- body: JSON.stringify({
128
- ...(options.versionLabel ? { versionLabel: options.versionLabel } : {}),
129
- ...(options.readmeMarkdown !== undefined && options.readmeMarkdown !== null
130
- ? { readmeMarkdown: options.readmeMarkdown }
131
- : {}),
132
- }),
133
- })
134
-
135
- if (!response.ok) {
136
- throw new Error(`Failed to create dataset version: ${await extractErrorMessage(response)}`)
137
- }
138
-
139
- return parseJsonResponse<DatasetVersionResponse>(response, 'Dataset version create')
140
- }
141
-
142
120
  async function createDatasetVersion() {
143
121
  const datasetId = getPositionalArg(3) || getArg('--dataset')
144
122
  const versionLabel = getArg('--label') || getArg('--version-label') || null
@@ -151,7 +129,8 @@ async function createDatasetVersion() {
151
129
 
152
130
  const readmeMarkdown = readReadmeMarkdownFromArgs(usage)
153
131
  const resolvedDatasetId = await resolveDatasetIdForProject(datasetId, project)
154
- const data = await requestDatasetVersionSnapshot(resolvedDatasetId, {
132
+ const { createDatasetVersionSnapshot } = await import('./dataset-version-workflow.js')
133
+ const data = await createDatasetVersionSnapshot(resolvedDatasetId, {
155
134
  versionLabel,
156
135
  readmeMarkdown,
157
136
  })
@@ -174,6 +153,36 @@ async function createDatasetVersion() {
174
153
  )
175
154
  }
176
155
 
156
+ async function listDatasetSplitSets() {
157
+ const versionId = getPositionalArg(3)
158
+ if (!versionId) throw new Error('Usage: orizu datasets splits list <datasetVersionId> [--json]')
159
+ const { listDatasetSplits } = await import('./dataset-split-reads.js')
160
+ const data = await listDatasetSplits(versionId)
161
+ if (hasJsonFlag()) return printJson({ ...data })
162
+ printLine(`Dataset version ${sanitizeTerminalText(data.dataset_version_id)}`)
163
+ for (const set of data.split_sets) {
164
+ printLine(`${sanitizeTerminalText(set.name)} (${sanitizeTerminalText(set.id)})`)
165
+ for (const partition of set.partitions) printLine(` ${sanitizeTerminalText(partition.name)}: ${partition.row_count} rows`)
166
+ }
167
+ if (!data.split_sets.length) printLine('No saved split sets.')
168
+ }
169
+
170
+ async function showDatasetSplitSet() {
171
+ const setId = getPositionalArg(3)
172
+ if (!setId) throw new Error('Usage: orizu datasets splits show <splitSetId> [--json]')
173
+ const { showDatasetSplit } = await import('./dataset-split-reads.js')
174
+ const data = await showDatasetSplit(setId)
175
+ if (hasJsonFlag()) return printJson({ ...data })
176
+ printLine(`${sanitizeTerminalText(data.name)} (${sanitizeTerminalText(data.split_set_id)})`)
177
+ printLine(`Dataset version: ${sanitizeTerminalText(data.dataset_version_id)}`)
178
+ printLine(`Strategy: ${sanitizeTerminalText(data.strategy)}; seed: ${data.seed ?? 'none'}`)
179
+ printLine(`Metadata: ${sanitizeTerminalText(JSON.stringify(data.metadata))}`)
180
+ for (const partition of data.partitions) {
181
+ printLine(`${sanitizeTerminalText(partition.name)}: ${partition.row_ids.length} rows`)
182
+ for (const id of partition.row_ids) printLine(` ${sanitizeTerminalText(id)}`)
183
+ }
184
+ }
185
+
177
186
  async function createDatasetSplitSet() {
178
187
  const datasetVersionId = getPositionalArg(3) || getArg('--dataset-version')
179
188
  const splitFile = getArg('--from-file')
@@ -194,7 +203,7 @@ async function createDatasetSplitSet() {
194
203
  }
195
204
 
196
205
  const partitions = splitSpec && Array.isArray(splitSpec.partitions) ? splitSpec.partitions : undefined
197
- const metadata = splitSpec && typeof splitSpec.metadata === 'object' && splitSpec.metadata !== null
206
+ const metadata = splitSpec && Object.prototype.hasOwnProperty.call(splitSpec, 'metadata')
198
207
  ? splitSpec.metadata
199
208
  : undefined
200
209
 
@@ -351,37 +360,21 @@ async function setDatasetReadme() {
351
360
  }
352
361
 
353
362
  const resolvedDatasetId = await resolveDatasetIdForProject(datasetId, project)
354
- const response = await authedFetch(`/api/cli/datasets/${encodeURIComponent(resolvedDatasetId)}/readme`, {
355
- method: 'POST',
356
- headers: { 'Content-Type': 'application/json' },
357
- body: JSON.stringify({ markdown: readmeMarkdown }),
358
- })
359
-
360
- if (!response.ok) {
361
- throw new Error(`Failed to save dataset README: ${await response.text()}`)
362
- }
363
-
364
- const data = await parseJsonResponse<{
365
- version: {
366
- id: string
367
- dataset_id: string
368
- version_num: number
369
- created_at: string
370
- }
371
- }>(response, 'Dataset README save')
363
+ const { saveDatasetReadme } = await import('./dataset-readme-workflow.js')
364
+ const version = await saveDatasetReadme(resolvedDatasetId, readmeMarkdown)
372
365
 
373
366
  if (hasJsonFlag()) {
374
367
  printJson({
375
- dataset_id: data.version.dataset_id,
376
- readme_version_id: data.version.id,
377
- readme_version_num: data.version.version_num,
368
+ dataset_id: version.dataset_id,
369
+ readme_version_id: version.id,
370
+ readme_version_num: version.version_num,
378
371
  })
379
372
  return
380
373
  }
381
374
 
382
375
  printLine(
383
- `Saved README v${data.version.version_num} for dataset ` +
384
- `${sanitizeTerminalText(data.version.dataset_id)} (${sanitizeTerminalText(data.version.id)}).`
376
+ `Saved README v${version.version_num} for dataset ` +
377
+ `${sanitizeTerminalText(version.dataset_id)} (${sanitizeTerminalText(version.id)}).`
385
378
  )
386
379
  }
387
380
 
@@ -409,6 +402,21 @@ async function downloadDataset() {
409
402
  throw new Error('format must be one of: csv, json, jsonl')
410
403
  }
411
404
 
405
+ const versionId = getArg('--dataset-version'), setId = getArg('--split-set')
406
+ const inlineSplit = cliArgs.find(argument => argument.startsWith('--split='))
407
+ const split = inlineSplit === undefined ? getArg('--split') : inlineSplit.slice('--split='.length)
408
+ if (['--dataset-version', '--split-set', '--split'].some(flag => hasArg(flag) || cliArgs.some(argument => argument.startsWith(`${flag}=`)))) {
409
+ if (!versionId || !setId || !split || [versionId, setId].some(value => value.startsWith('--')) || (inlineSplit === undefined && split.startsWith('--'))) throw new Error('Pinned download requires --dataset-version, --split-set and --split together')
410
+ if (datasetInput) throw new Error('Use --dataset-version with split selectors without a live --dataset or dataset argument')
411
+ if (format === 'csv') throw new Error('Pinned download format must be json or jsonl')
412
+ const filename = outPathArg ? expandHomePath(outPathArg) : `${versionId}.${format}`
413
+ const { downloadPinnedDatasetSplit } = await import('./dataset-split-reads.js')
414
+ await downloadPinnedDatasetSplit(versionId, setId, split, format, filename)
415
+ if (hasJsonFlag()) return printJson({ dataset_version_id: versionId, split_set_id: setId, split, format, savedTo: filename })
416
+ printLine(`Saved split ${sanitizeTerminalText(split)} from dataset version ${sanitizeTerminalText(versionId)} (${format.toUpperCase()}) to ${sanitizeTerminalText(filename)}`)
417
+ return
418
+ }
419
+
412
420
  let datasetId: string
413
421
  if (datasetInput) {
414
422
  datasetId = parseDatasetReference(datasetInput).datasetId
@@ -460,7 +468,8 @@ async function editDatasetRows() {
460
468
  datasetId = selected.datasetId
461
469
  }
462
470
 
463
- await runEditDatasetRows({
471
+ const { editDatasetRows: runEditDatasetRows } = await import('./dataset-row-workflow.js')
472
+ const exitCode = await runEditDatasetRows({
464
473
  datasetId,
465
474
  file: expandHomePath(fileArg),
466
475
  json: hasJsonFlag(),
@@ -468,6 +477,7 @@ async function editDatasetRows() {
468
477
  printLine,
469
478
  sanitize: sanitizeTerminalText,
470
479
  })
480
+ if (exitCode !== 0) process.exitCode = exitCode
471
481
  }
472
482
 
473
483
  async function deleteDatasetRows() {
@@ -487,30 +497,16 @@ async function deleteDatasetRows() {
487
497
  datasetId = selected.datasetId
488
498
  }
489
499
 
490
- const response = await authedFetch(`/api/cli/datasets/${encodeURIComponent(datasetId)}/rows`, {
491
- method: 'DELETE',
492
- headers: { 'Content-Type': 'application/json' },
493
- body: JSON.stringify({
494
- rowIds,
495
- }),
500
+ const { deleteDatasetRows: runDeleteDatasetRows } = await import('./dataset-row-workflow.js')
501
+ const exitCode = await runDeleteDatasetRows({
502
+ datasetId,
503
+ rowIds,
504
+ json: hasJsonFlag(),
505
+ printJson,
506
+ printLine,
507
+ sanitize: sanitizeTerminalText,
496
508
  })
497
-
498
- if (!response.ok) {
499
- throw new Error(`Delete rows failed: ${await response.text()}`)
500
- }
501
-
502
- const data = await parseJsonResponse<{
503
- dataset: { id: string; name: string; rowCount: number }
504
- deletedCount: number
505
- }>(response, 'Dataset delete rows')
506
-
507
- if (hasJsonFlag()) {
508
- printJson({ dataset: data.dataset, deletedCount: data.deletedCount })
509
- return
510
- }
511
- printLine(
512
- `Deleted ${data.deletedCount} rows from dataset ${sanitizeTerminalText(data.dataset.name)} (${sanitizeTerminalText(data.dataset.id)}). New row count: ${data.dataset.rowCount}`
513
- )
509
+ if (exitCode !== 0) process.exitCode = exitCode
514
510
  }
515
511
 
516
512
  async function confirmDatasetDeletion(dataset: DatasetSelection) {
@@ -550,23 +546,16 @@ async function deleteDataset() {
550
546
 
551
547
  await confirmDatasetDeletion(dataset)
552
548
 
553
- const response = await authedFetch(`/api/cli/datasets/${encodeURIComponent(dataset.datasetId)}`, {
554
- method: 'DELETE',
555
- })
556
-
557
- if (!response.ok) {
558
- throw new Error(`Delete failed: ${await response.text()}`)
559
- }
560
-
561
- const data = await parseJsonResponse<{
562
- dataset: { id: string }
563
- }>(response, 'Dataset delete')
549
+ const { deleteDataset: requestDelete } = await import('./dataset-delete-workflow.js')
550
+ // Only a dataset picked from a project's list has a known project; with
551
+ // --dataset, --project is never checked against the dataset.
552
+ const deleted = await requestDelete(dataset.datasetId, dataset.project ?? null)
564
553
 
565
554
  if (hasJsonFlag()) {
566
- printJson({ dataset: data.dataset, deleted: true })
555
+ printJson({ dataset: deleted, deleted: true })
567
556
  return
568
557
  }
569
- printLine(`Deleted dataset ${sanitizeTerminalText(data.dataset.id)}.`)
558
+ printLine(`Deleted dataset ${sanitizeTerminalText(deleted.id)}.`)
570
559
  }
571
560
 
572
561
  async function lockDataset() {
@@ -582,32 +571,15 @@ async function lockDataset() {
582
571
  datasetId = selected.datasetId
583
572
  }
584
573
 
585
- const response = await authedFetch(`/api/cli/datasets/${encodeURIComponent(datasetId)}/lock`, {
586
- method: 'POST',
587
- headers: { 'Content-Type': 'application/json' },
588
- body: JSON.stringify(reason ? { reason } : {}),
589
- })
590
-
591
- if (!response.ok) {
592
- throw new Error(`Lock failed: ${await response.text()}`)
593
- }
594
-
595
- const data = await parseJsonResponse<{
596
- dataset: {
597
- id: string
598
- name: string
599
- rowCount: number
600
- lockedAt: string
601
- lockedBy: string | null
602
- }
603
- }>(response, 'Dataset lock')
574
+ const { lockDataset: requestLock } = await import('./dataset-lock-workflow.js')
575
+ const dataset = await requestLock(datasetId, reason)
604
576
 
605
577
  if (hasJsonFlag()) {
606
- printJson({ dataset: data.dataset })
578
+ printJson({ dataset })
607
579
  return
608
580
  }
609
581
  printLine(
610
- `Locked dataset ${sanitizeTerminalText(data.dataset.name)} (${sanitizeTerminalText(data.dataset.id)}) at ${sanitizeTerminalText(data.dataset.lockedAt)}. Row count: ${data.dataset.rowCount}`
582
+ `Locked dataset ${sanitizeTerminalText(dataset.name)} (${sanitizeTerminalText(dataset.id)}) at ${sanitizeTerminalText(dataset.lockedAt)}. Row count: ${dataset.rowCount}`
611
583
  )
612
584
  }
613
585
 
@@ -624,31 +596,15 @@ async function cloneDataset() {
624
596
  datasetId = selected.datasetId
625
597
  }
626
598
 
627
- const response = await authedFetch(`/api/cli/datasets/${encodeURIComponent(datasetId)}/clone`, {
628
- method: 'POST',
629
- headers: { 'Content-Type': 'application/json' },
630
- body: JSON.stringify(name ? { name } : {}),
631
- })
632
-
633
- if (!response.ok) {
634
- throw new Error(`Clone failed: ${await response.text()}`)
635
- }
636
-
637
- const data = await parseJsonResponse<{
638
- dataset: {
639
- id: string
640
- name: string
641
- rowCount: number
642
- parentDatasetId: string
643
- }
644
- }>(response, 'Dataset clone')
599
+ const { cloneDataset: requestClone } = await import('./dataset-clone-workflow.js')
600
+ const dataset = await requestClone(datasetId, name ?? null)
645
601
 
646
602
  if (hasJsonFlag()) {
647
- printJson({ dataset: data.dataset })
603
+ printJson({ dataset })
648
604
  return
649
605
  }
650
606
  printLine(
651
- `Cloned dataset ${sanitizeTerminalText(data.dataset.parentDatasetId)} -> ${sanitizeTerminalText(data.dataset.name)} (${sanitizeTerminalText(data.dataset.id)}). Row count: ${data.dataset.rowCount}`
607
+ `Cloned dataset ${sanitizeTerminalText(dataset.parentDatasetId)} -> ${sanitizeTerminalText(dataset.name)} (${sanitizeTerminalText(dataset.id)}). Row count: ${dataset.rowCount}`
652
608
  )
653
609
  }
654
610
 
@@ -665,18 +621,13 @@ async function downloadAnnotations() {
665
621
  taskId = await selectTaskIdInteractively()
666
622
  }
667
623
 
668
- const response = await authedFetch(`/api/cli/tasks/${taskId}/export?format=${format}`)
669
- if (!response.ok) {
670
- throw new Error(`Download failed: ${await response.text()}`)
671
- }
672
-
673
624
  const fallbackName = `${taskId}.${format}`
674
625
  const filename = outPathArg
675
626
  ? expandHomePath(outPathArg)
676
627
  : fallbackName
677
628
 
678
- const bytes = new Uint8Array(await response.arrayBuffer())
679
- writeFileSync(filename, bytes)
629
+ const { runTaskExportDownload } = await import('./task-export-workflow.js')
630
+ await runTaskExportDownload(taskId, format, filename)
680
631
 
681
632
  if (hasJsonFlag()) {
682
633
  printJson({ taskId, format, savedTo: filename })
@@ -694,6 +645,8 @@ export {
694
645
  setDatasetReadme,
695
646
  createDatasetVersion,
696
647
  createDatasetSplitSet,
648
+ listDatasetSplitSets,
649
+ showDatasetSplitSet,
697
650
  editDatasetRows,
698
651
  deleteDatasetRows,
699
652
  deleteDataset,
@@ -0,0 +1,72 @@
1
+ import { Cause, Effect, Exit, Result, Schema } from 'effect'
2
+
3
+ import { datasetTransferTimeoutMs } from './dataset-transfer-policy.js'
4
+ import { authedFetch } from './http.js'
5
+ import { parseJsonResponse } from './json-response.js'
6
+
7
+ // Loaded only through `await import(...)` from dataset-commands, like append.
8
+
9
+ const MAY_BE_DELETED = 'The dataset may already be deleted: check with `orizu datasets list --status all` before retrying.'
10
+
11
+ const DeleteReply = Schema.Struct({ dataset: Schema.Struct({ id: Schema.String }) })
12
+
13
+ // A refusal (4xx) means nothing was deleted. After any other failure once the
14
+ // request was sent, the delete may have committed, and it cannot be undone.
15
+ class DeleteFailure extends Error {
16
+ constructor(message: string, readonly mayBeDeleted: boolean) { super(message) }
17
+ }
18
+
19
+ function deleteRequest(datasetId: string, project: string | null, timeoutMs: number) {
20
+ return Effect.suspend(() => {
21
+ let sent = false
22
+ let timedOut = false
23
+ return Effect.tryPromise({
24
+ try: async signal => {
25
+ const request = new AbortController()
26
+ const abort = () => request.abort()
27
+ const timer = setTimeout(() => { timedOut = true; request.abort() }, timeoutMs)
28
+ if (signal.aborted) abort()
29
+ else signal.addEventListener('abort', abort, { once: true })
30
+ try {
31
+ const response = await authedFetch(`/api/cli/datasets/${encodeURIComponent(datasetId)}`, {
32
+ method: 'DELETE',
33
+ signal: request.signal,
34
+ }, () => { sent = true })
35
+ if (!response.ok) {
36
+ const message = `Delete failed: ${await response.text()}`
37
+ // A dependency refusal: archiving keeps the dataset and its references.
38
+ if (response.status === 409) {
39
+ const archive = `orizu datasets archive ${datasetId}${project ? ` --project ${project}` : ''}`
40
+ throw new DeleteFailure(`${message}\nArchive it instead: \`${archive}\``, false)
41
+ }
42
+ throw new DeleteFailure(message, response.status < 400 || response.status >= 500)
43
+ }
44
+ const data = await parseJsonResponse<unknown>(response, 'Dataset delete')
45
+ const decoded = Schema.decodeUnknownResult(DeleteReply)(data)
46
+ if (Result.isFailure(decoded)) {
47
+ throw new DeleteFailure(`Dataset delete returned an unexpected response (status ${response.status}).`, true)
48
+ }
49
+ return decoded.success.dataset
50
+ } finally {
51
+ clearTimeout(timer)
52
+ signal.removeEventListener('abort', abort)
53
+ request.abort()
54
+ }
55
+ },
56
+ catch: cause => {
57
+ if (timedOut) return new DeleteFailure('DATASET_TRANSFER_TIMEOUT: Dataset delete exceeded its deadline', true)
58
+ if (cause instanceof DeleteFailure) return cause
59
+ return new DeleteFailure(cause instanceof Error ? cause.message : String(cause), sent)
60
+ },
61
+ })
62
+ }).pipe(Effect.mapError(failure => new Error(failure.mayBeDeleted ? `${failure.message}\n${MAY_BE_DELETED}` : failure.message)))
63
+ }
64
+
65
+ // Runs after the typed confirmation. Nothing to clean up, so signals keep
66
+ // Node's default handling.
67
+ // `project` (team/project), when known, goes into the archive hint.
68
+ export async function deleteDataset(datasetId: string, project: string | null): Promise<{ id: string }> {
69
+ const exit = await Effect.runPromiseExit(deleteRequest(datasetId, project, datasetTransferTimeoutMs()))
70
+ if (Exit.isSuccess(exit)) return exit.value
71
+ throw Cause.squash(exit.cause)
72
+ }
@@ -0,0 +1,74 @@
1
+ import { Cause, Effect, Exit, Result, Schema } from 'effect'
2
+
3
+ import { datasetTransferTimeoutMs } from './dataset-transfer-policy.js'
4
+ import { authedFetch } from './http.js'
5
+ import { parseJsonResponse } from './json-response.js'
6
+
7
+ // Loaded only through `await import(...)` from dataset-commands, like append.
8
+
9
+ const MAY_BE_LOCKED = 'The dataset may already be locked: running `orizu datasets lock` again is safe and shows its current state.'
10
+
11
+ const LockedDataset = Schema.Struct({
12
+ id: Schema.String,
13
+ name: Schema.String,
14
+ rowCount: Schema.Number,
15
+ lockedAt: Schema.String,
16
+ lockedBy: Schema.NullOr(Schema.String),
17
+ })
18
+ const LockReply = Schema.Struct({ dataset: LockedDataset })
19
+ export type LockedDataset = typeof LockedDataset.Type
20
+
21
+ // A refusal (4xx) means nothing was locked. After any other failure once the
22
+ // request was sent, the lock may have committed; locking again is harmless.
23
+ class LockFailure extends Error {
24
+ constructor(message: string, readonly mayBeLocked: boolean) { super(message) }
25
+ }
26
+
27
+ function lockRequest(datasetId: string, reason: string | null, timeoutMs: number) {
28
+ return Effect.suspend(() => {
29
+ let sent = false
30
+ let timedOut = false
31
+ return Effect.tryPromise({
32
+ try: async signal => {
33
+ const request = new AbortController()
34
+ const abort = () => request.abort()
35
+ const timer = setTimeout(() => { timedOut = true; request.abort() }, timeoutMs)
36
+ if (signal.aborted) abort()
37
+ else signal.addEventListener('abort', abort, { once: true })
38
+ try {
39
+ const response = await authedFetch(`/api/cli/datasets/${encodeURIComponent(datasetId)}/lock`, {
40
+ method: 'POST',
41
+ headers: { 'Content-Type': 'application/json' },
42
+ body: JSON.stringify(reason ? { reason } : {}),
43
+ signal: request.signal,
44
+ }, () => { sent = true })
45
+ if (!response.ok) {
46
+ throw new LockFailure(`Lock failed: ${await response.text()}`, response.status < 400 || response.status >= 500)
47
+ }
48
+ const data = await parseJsonResponse<unknown>(response, 'Dataset lock')
49
+ const decoded = Schema.decodeUnknownResult(LockReply)(data)
50
+ if (Result.isFailure(decoded)) {
51
+ throw new LockFailure(`Dataset lock returned an unexpected response (status ${response.status}).`, true)
52
+ }
53
+ return decoded.success.dataset
54
+ } finally {
55
+ clearTimeout(timer)
56
+ signal.removeEventListener('abort', abort)
57
+ request.abort()
58
+ }
59
+ },
60
+ catch: cause => {
61
+ if (timedOut) return new LockFailure('DATASET_TRANSFER_TIMEOUT: Dataset lock exceeded its deadline', true)
62
+ if (cause instanceof LockFailure) return cause
63
+ return new LockFailure(cause instanceof Error ? cause.message : String(cause), sent)
64
+ },
65
+ })
66
+ }).pipe(Effect.mapError(failure => new Error(failure.mayBeLocked ? `${failure.message}\n${MAY_BE_LOCKED}` : failure.message)))
67
+ }
68
+
69
+ // Nothing to clean up, so signals keep Node's default handling.
70
+ export async function lockDataset(datasetId: string, reason: string | null): Promise<LockedDataset> {
71
+ const exit = await Effect.runPromiseExit(lockRequest(datasetId, reason, datasetTransferTimeoutMs()))
72
+ if (Exit.isSuccess(exit)) return exit.value
73
+ throw Cause.squash(exit.cause)
74
+ }