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.
- package/README.md +77 -0
- package/dist/dataset-clone-workflow.d.ts +11 -0
- package/dist/dataset-clone-workflow.d.ts.map +1 -0
- package/dist/dataset-clone-workflow.js +100 -0
- package/dist/dataset-commands.d.ts +3 -1
- package/dist/dataset-commands.d.ts.map +1 -1
- package/dist/dataset-commands.js +107 -91
- package/dist/dataset-delete-workflow.d.ts +4 -0
- package/dist/dataset-delete-workflow.d.ts.map +1 -0
- package/dist/dataset-delete-workflow.js +75 -0
- package/dist/dataset-lock-workflow.d.ts +12 -0
- package/dist/dataset-lock-workflow.d.ts.map +1 -0
- package/dist/dataset-lock-workflow.js +76 -0
- package/dist/dataset-readme-workflow.d.ts +13 -0
- package/dist/dataset-readme-workflow.d.ts.map +1 -0
- package/dist/dataset-readme-workflow.js +82 -0
- package/dist/dataset-row-workflow.d.ts +14 -0
- package/dist/dataset-row-workflow.d.ts.map +1 -0
- package/dist/dataset-row-workflow.js +202 -0
- package/dist/dataset-split-reads.d.ts +28 -0
- package/dist/dataset-split-reads.d.ts.map +1 -0
- package/dist/dataset-split-reads.js +47 -0
- package/dist/dataset-transfer-upload.d.ts +2 -6
- package/dist/dataset-transfer-upload.d.ts.map +1 -1
- package/dist/dataset-transfer-upload.js +9 -2
- package/dist/dataset-upload-snapshot.d.ts.map +1 -1
- package/dist/dataset-upload-snapshot.js +7 -1
- package/dist/dataset-version-workflow.d.ts +24 -0
- package/dist/dataset-version-workflow.d.ts.map +1 -0
- package/dist/dataset-version-workflow.js +99 -0
- package/dist/feedback-cli.js +3 -3
- package/dist/help.d.ts.map +1 -1
- package/dist/help.js +29 -3
- package/dist/hosted-boot.d.ts +1 -0
- package/dist/hosted-boot.d.ts.map +1 -1
- package/dist/hosted-boot.js +17 -0
- package/dist/hosted-harness-opencode.d.ts +5 -6
- package/dist/hosted-harness-opencode.d.ts.map +1 -1
- package/dist/hosted-harness-opencode.js +73 -59
- package/dist/hosted-loop-lifecycle.d.ts +6 -11
- package/dist/hosted-loop-lifecycle.d.ts.map +1 -1
- package/dist/hosted-loop-lifecycle.js +6 -11
- package/dist/hosted-loop.d.ts +9 -0
- package/dist/hosted-loop.d.ts.map +1 -1
- package/dist/hosted-loop.js +19 -10
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +11 -3
- package/dist/instruction-delivery.d.ts +14 -0
- package/dist/instruction-delivery.d.ts.map +1 -0
- package/dist/instruction-delivery.js +103 -0
- package/dist/instruction-set-sync/index.d.ts +17 -0
- package/dist/instruction-set-sync/index.d.ts.map +1 -1
- package/dist/instruction-set-sync/index.js +44 -17
- package/dist/instruction-set-update/index.d.ts +2 -2
- package/dist/instruction-set-update/index.d.ts.map +1 -1
- package/dist/instruction-set-update/index.js +2 -2
- package/dist/instruction-sets-cli.d.ts.map +1 -1
- package/dist/instruction-sets-cli.js +19 -27
- package/dist/json-response.d.ts +1 -0
- package/dist/json-response.d.ts.map +1 -1
- package/dist/json-response.js +7 -0
- package/dist/last-error-record.d.ts +4 -2
- package/dist/last-error-record.d.ts.map +1 -1
- package/dist/last-error-record.js +10 -68
- package/dist/last-error-workflow.d.ts +4 -0
- package/dist/last-error-workflow.d.ts.map +1 -0
- package/dist/last-error-workflow.js +78 -0
- package/dist/preview-runtime.d.ts +2 -2
- package/dist/preview-runtime.d.ts.map +1 -1
- package/dist/preview-runtime.js +37 -7
- package/dist/task-commands.d.ts.map +1 -1
- package/dist/task-commands.js +9 -16
- package/dist/task-export-workflow.d.ts +4 -0
- package/dist/task-export-workflow.d.ts.map +1 -0
- package/dist/task-export-workflow.js +46 -0
- package/dist/task-report-workflow.d.ts +47 -0
- package/dist/task-report-workflow.d.ts.map +1 -0
- package/dist/task-report-workflow.js +160 -0
- package/dist/workbench-cli.js +1 -1
- package/package.json +1 -1
- package/src/dataset-clone-workflow.ts +100 -0
- package/src/dataset-commands.ts +95 -142
- package/src/dataset-delete-workflow.ts +72 -0
- package/src/dataset-lock-workflow.ts +74 -0
- package/src/dataset-readme-workflow.ts +80 -0
- package/src/dataset-row-workflow.ts +248 -0
- package/src/dataset-split-reads.ts +43 -0
- package/src/dataset-transfer-upload.ts +12 -5
- package/src/dataset-upload-snapshot.ts +8 -1
- package/src/dataset-version-workflow.ts +105 -0
- package/src/feedback-cli.ts +4 -4
- package/src/help.ts +29 -3
- package/src/hosted-boot.ts +16 -1
- package/src/hosted-harness-opencode.ts +74 -72
- package/src/hosted-loop-lifecycle.ts +6 -11
- package/src/hosted-loop.ts +20 -10
- package/src/hosted-model-cache.d.mts +2 -0
- package/src/hosted-model-cache.mjs +53 -0
- package/src/index.ts +12 -2
- package/src/instruction-delivery.ts +123 -0
- package/src/instruction-set-sync/index.ts +61 -14
- package/src/instruction-set-update/index.ts +4 -3
- package/src/instruction-sets-cli.ts +19 -27
- package/src/json-response.ts +8 -0
- package/src/last-error-record.ts +11 -85
- package/src/last-error-workflow.ts +121 -0
- package/src/preview-runtime.ts +40 -10
- package/src/task-commands.ts +9 -43
- package/src/task-export-workflow.ts +56 -0
- package/src/task-report-workflow.ts +179 -0
- package/src/workbench-cli.ts +1 -1
- package/vendor/skills/orizu/references/cli-reference.md +34 -0
- package/vendor/skills/orizu/references/dataset-design.md +1 -1
- package/dist/dataset-edit-rows.d.ts +0 -10
- package/dist/dataset-edit-rows.d.ts.map +0 -1
- package/dist/dataset-edit-rows.js +0 -90
- 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
|
+
}
|
package/src/dataset-commands.ts
CHANGED
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
// ORI-2044: moved from index.ts to keep dataset commands together.
|
|
2
2
|
|
|
3
|
-
import { readFileSync
|
|
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
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
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
|
-
?
|
|
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
|
|
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 &&
|
|
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
|
|
355
|
-
|
|
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:
|
|
376
|
-
readme_version_id:
|
|
377
|
-
readme_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${
|
|
384
|
-
`${sanitizeTerminalText(
|
|
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
|
|
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
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
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
|
|
554
|
-
|
|
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:
|
|
555
|
+
printJson({ dataset: deleted, deleted: true })
|
|
567
556
|
return
|
|
568
557
|
}
|
|
569
|
-
printLine(`Deleted dataset ${sanitizeTerminalText(
|
|
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
|
|
586
|
-
|
|
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
|
|
578
|
+
printJson({ dataset })
|
|
607
579
|
return
|
|
608
580
|
}
|
|
609
581
|
printLine(
|
|
610
|
-
`Locked dataset ${sanitizeTerminalText(
|
|
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
|
|
628
|
-
|
|
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
|
|
603
|
+
printJson({ dataset })
|
|
648
604
|
return
|
|
649
605
|
}
|
|
650
606
|
printLine(
|
|
651
|
-
`Cloned dataset ${sanitizeTerminalText(
|
|
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
|
|
679
|
-
|
|
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
|
+
}
|