orizu 0.1.5 → 0.2.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.
- package/README.md +5 -0
- package/dist/credentials.js +55 -6
- package/dist/http.js +41 -4
- package/dist/index.js +253 -136
- package/package.json +1 -1
- package/src/credentials.ts +82 -11
- package/src/http.ts +54 -5
- package/src/index.ts +305 -147
- package/src/types.ts +19 -4
package/src/index.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import { createHash, randomBytes } from 'crypto'
|
|
3
3
|
import { basename, extname } from 'path'
|
|
4
|
+
import { pathToFileURL } from 'url'
|
|
4
5
|
import { createServer } from 'http'
|
|
5
6
|
import { readFileSync, statSync, writeFileSync } from 'fs'
|
|
6
7
|
import { spawn } from 'child_process'
|
|
@@ -11,7 +12,13 @@ import { parseDatasetFile } from './file-parser.js'
|
|
|
11
12
|
import { streamJsonlRowChunks } from './jsonl-stream.js'
|
|
12
13
|
import { parseDatasetReference } from './dataset-download.js'
|
|
13
14
|
import { parseGlobalFlags } from './global-flags.js'
|
|
14
|
-
import {
|
|
15
|
+
import {
|
|
16
|
+
assertSecureTokenTransport,
|
|
17
|
+
authedFetch,
|
|
18
|
+
getBaseUrl,
|
|
19
|
+
resolveLoginBaseUrl,
|
|
20
|
+
setGlobalFlags,
|
|
21
|
+
} from './http.js'
|
|
15
22
|
import { formatTaskCreateError } from './task-create-error.js'
|
|
16
23
|
import { LoginResponse } from './types.js'
|
|
17
24
|
|
|
@@ -67,6 +74,12 @@ interface DatasetSummary {
|
|
|
67
74
|
teamSlug: string
|
|
68
75
|
}
|
|
69
76
|
|
|
77
|
+
interface DatasetSelection {
|
|
78
|
+
datasetId: string
|
|
79
|
+
project?: string
|
|
80
|
+
name?: string
|
|
81
|
+
}
|
|
82
|
+
|
|
70
83
|
type DatasetUploadSourceType = 'csv' | 'json' | 'jsonl'
|
|
71
84
|
|
|
72
85
|
interface DatasetUploadResponse {
|
|
@@ -119,8 +132,28 @@ interface TaskStatusPayload {
|
|
|
119
132
|
}
|
|
120
133
|
}
|
|
121
134
|
|
|
135
|
+
function getCliVersion(): string {
|
|
136
|
+
const packageJson = JSON.parse(
|
|
137
|
+
readFileSync(new URL('../package.json', import.meta.url), 'utf8')
|
|
138
|
+
) as { version?: unknown }
|
|
139
|
+
|
|
140
|
+
if (typeof packageJson.version !== 'string' || packageJson.version.length === 0) {
|
|
141
|
+
throw new Error('Unable to read orizu CLI version.')
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
return packageJson.version
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
function printLine(message = '') {
|
|
148
|
+
output.write(`${message}\n`)
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
function printVersion() {
|
|
152
|
+
printLine(`orizu ${getCliVersion()}`)
|
|
153
|
+
}
|
|
154
|
+
|
|
122
155
|
function printUsage() {
|
|
123
|
-
|
|
156
|
+
printLine(`orizu global options:\n\n --local Use http://localhost:3000\n --server <url> Use a specific server origin (for example: https://preview.example.com)\n --version, -v Print the orizu CLI version\n\norizu commands:\n\n orizu login\n orizu logout\n orizu whoami\n orizu teams list\n orizu teams create [--name <name>]\n orizu teams members list [--team <teamSlug>]\n orizu teams members add --email <email> [--team <teamSlug>]\n orizu teams members remove --email <email> [--team <teamSlug>]\n orizu teams members role --team <teamSlug> --email <email> --role <admin|member>\n orizu projects list [--team <teamSlug>]\n orizu projects create --name <name> [--team <teamSlug>]\n orizu apps list [--project <team/project>]\n orizu apps create --project <team/project> --name <name> --dataset <datasetId> --file <path> --input-schema <json-path> --output-schema <json-path> [--component <name>]\n orizu apps update [--app <appId>] [--project <team/project>] --file <path> --input-schema <json-path> --output-schema <json-path> [--component <name>]\n orizu apps link-dataset --dataset <datasetId> [--app <appId>] [--project <team/project>] [--version <n>]\n orizu apps detail --app <appId> [--project <team/project>] [--json]\n orizu tasks list [--project <team/project>]\n orizu tasks create --project <team/project> --dataset <datasetId> --app <appId> --title <title> --assignees <userIdOrEmail1,userIdOrEmail2> [--version <n>] [--instructions <text>] [--labels-per-item <n>] [--json]\n orizu tasks assign --task <taskId> --assignees <userId1,userId2>\n orizu tasks status --task <taskId> [--json]\n orizu tasks pause --task <taskId>\n orizu tasks unpause --task <taskId>\n orizu datasets upload --file <path> [--project <team/project>] [--name <name>]\n orizu datasets download [--dataset <datasetId|datasetUrl>] [--project <team/project>] [--format <csv|json|jsonl>] [--out <path>]\n orizu datasets append [--dataset <datasetId|datasetUrl>] [--project <team/project>] --file <path>\n orizu datasets edit-rows [--dataset <datasetId|datasetUrl>] [--project <team/project>] --file <path>\n orizu datasets delete-rows [--dataset <datasetId|datasetUrl>] [--project <team/project>] --row-ids <id1,id2>\n orizu datasets delete [--dataset <datasetId|datasetUrl>] [--project <team/project>]\n orizu datasets lock [--dataset <datasetId|datasetUrl>] [--project <team/project>] [--reason <text>]\n orizu datasets clone [--dataset <datasetId|datasetUrl>] [--project <team/project>] [--name <name>]\n orizu tasks export [--task <taskId>] [--format <csv|json|jsonl>] [--out <path>]`)
|
|
124
157
|
}
|
|
125
158
|
|
|
126
159
|
let cliArgs = process.argv.slice(2)
|
|
@@ -134,7 +167,7 @@ function getArg(name: string): string | null {
|
|
|
134
167
|
return cliArgs[index + 1]
|
|
135
168
|
}
|
|
136
169
|
|
|
137
|
-
function normalizeSlugInput(slug: string): string {
|
|
170
|
+
export function normalizeSlugInput(slug: string): string {
|
|
138
171
|
return slug.trim().toLowerCase()
|
|
139
172
|
}
|
|
140
173
|
|
|
@@ -146,7 +179,7 @@ function hasArg(name: string): boolean {
|
|
|
146
179
|
return cliArgs.includes(name)
|
|
147
180
|
}
|
|
148
181
|
|
|
149
|
-
function expandHomePath(path: string): string {
|
|
182
|
+
export function expandHomePath(path: string): string {
|
|
150
183
|
if (path.startsWith('~/')) {
|
|
151
184
|
const home = process.env.HOME || ''
|
|
152
185
|
return `${home}/${path.slice(2)}`
|
|
@@ -159,14 +192,43 @@ function createCodeVerifier(): string {
|
|
|
159
192
|
return randomBytes(32).toString('base64url')
|
|
160
193
|
}
|
|
161
194
|
|
|
162
|
-
function createCodeChallenge(verifier: string): string {
|
|
195
|
+
export function createCodeChallenge(verifier: string): string {
|
|
163
196
|
return createHash('sha256').update(verifier).digest('base64url')
|
|
164
197
|
}
|
|
165
198
|
|
|
199
|
+
export function sanitizeTerminalText(value: unknown): string {
|
|
200
|
+
return String(value).replace(/[\u0000-\u0008\u000B\u000C\u000E-\u001F\u007F-\u009F]/g, '')
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
export function validateBrowserUrl(url: string, expectedOrigin?: string): URL {
|
|
204
|
+
let parsed: URL
|
|
205
|
+
try {
|
|
206
|
+
parsed = new URL(url)
|
|
207
|
+
} catch {
|
|
208
|
+
throw new Error('Server returned an invalid browser URL.')
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
if (parsed.protocol !== 'https:' && parsed.protocol !== 'http:') {
|
|
212
|
+
throw new Error('Server returned an unsupported browser URL scheme.')
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
if (parsed.username || parsed.password) {
|
|
216
|
+
throw new Error('Server returned a browser URL containing credentials.')
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
if (expectedOrigin && parsed.origin !== expectedOrigin) {
|
|
220
|
+
throw new Error('Server returned a browser URL for an unexpected origin.')
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
return parsed
|
|
224
|
+
}
|
|
225
|
+
|
|
166
226
|
function openInBrowser(url: string) {
|
|
227
|
+
const parsed = validateBrowserUrl(url)
|
|
167
228
|
const platform = process.platform
|
|
229
|
+
const href = parsed.href
|
|
168
230
|
if (platform === 'darwin') {
|
|
169
|
-
spawn('open', [
|
|
231
|
+
spawn('open', [href], {
|
|
170
232
|
detached: true,
|
|
171
233
|
stdio: 'ignore',
|
|
172
234
|
}).unref()
|
|
@@ -174,7 +236,7 @@ function openInBrowser(url: string) {
|
|
|
174
236
|
}
|
|
175
237
|
|
|
176
238
|
if (platform === 'win32') {
|
|
177
|
-
spawn('
|
|
239
|
+
spawn('rundll32.exe', ['url.dll,FileProtocolHandler', href], {
|
|
178
240
|
detached: true,
|
|
179
241
|
stdio: 'ignore',
|
|
180
242
|
windowsHide: true,
|
|
@@ -182,28 +244,38 @@ function openInBrowser(url: string) {
|
|
|
182
244
|
return
|
|
183
245
|
}
|
|
184
246
|
|
|
185
|
-
spawn('xdg-open', [
|
|
247
|
+
spawn('xdg-open', [href], {
|
|
186
248
|
detached: true,
|
|
187
249
|
stdio: 'ignore',
|
|
188
250
|
}).unref()
|
|
189
251
|
}
|
|
190
252
|
|
|
191
|
-
function formatTerminalLink(url: string): string {
|
|
253
|
+
export function formatTerminalLink(url: string): string {
|
|
254
|
+
const safeUrl = sanitizeTerminalText(url)
|
|
255
|
+
try {
|
|
256
|
+
const parsed = validateBrowserUrl(safeUrl)
|
|
257
|
+
if (parsed.protocol !== 'https:' && parsed.protocol !== 'http:') {
|
|
258
|
+
return safeUrl
|
|
259
|
+
}
|
|
260
|
+
} catch {
|
|
261
|
+
return safeUrl
|
|
262
|
+
}
|
|
263
|
+
|
|
192
264
|
if (!isInteractiveTerminal()) {
|
|
193
|
-
return
|
|
265
|
+
return safeUrl
|
|
194
266
|
}
|
|
195
267
|
|
|
196
|
-
return `\u001B]8;;${
|
|
268
|
+
return `\u001B]8;;${safeUrl}\u0007${safeUrl}\u001B]8;;\u0007`
|
|
197
269
|
}
|
|
198
270
|
|
|
199
|
-
async function parseJsonResponse<T>(response: Response, context: string): Promise<T> {
|
|
271
|
+
export async function parseJsonResponse<T>(response: Response, context: string): Promise<T> {
|
|
200
272
|
const contentType = response.headers.get('content-type') || ''
|
|
201
273
|
const rawBody = await response.text()
|
|
202
274
|
|
|
203
275
|
if (!contentType.includes('application/json')) {
|
|
204
276
|
throw new Error(
|
|
205
277
|
`${context} returned non-JSON response (status ${response.status}). ` +
|
|
206
|
-
`Body preview: ${rawBody.slice(0, 180)}`
|
|
278
|
+
`Body preview: ${sanitizeTerminalText(rawBody.slice(0, 180))}`
|
|
207
279
|
)
|
|
208
280
|
}
|
|
209
281
|
|
|
@@ -212,7 +284,7 @@ async function parseJsonResponse<T>(response: Response, context: string): Promis
|
|
|
212
284
|
} catch {
|
|
213
285
|
throw new Error(
|
|
214
286
|
`${context} returned invalid JSON (status ${response.status}). ` +
|
|
215
|
-
`Body preview: ${rawBody.slice(0, 180)}`
|
|
287
|
+
`Body preview: ${sanitizeTerminalText(rawBody.slice(0, 180))}`
|
|
216
288
|
)
|
|
217
289
|
}
|
|
218
290
|
}
|
|
@@ -236,9 +308,9 @@ async function promptSelect<T>(
|
|
|
236
308
|
return items[0]
|
|
237
309
|
}
|
|
238
310
|
|
|
239
|
-
|
|
311
|
+
printLine(`\n${sanitizeTerminalText(title)}`)
|
|
240
312
|
items.forEach((item, index) => {
|
|
241
|
-
|
|
313
|
+
printLine(` ${index + 1}. ${sanitizeTerminalText(label(item, index))}`)
|
|
242
314
|
})
|
|
243
315
|
|
|
244
316
|
const rl = createInterface({ input, output })
|
|
@@ -250,7 +322,7 @@ async function promptSelect<T>(
|
|
|
250
322
|
return items[chosenIndex - 1]
|
|
251
323
|
}
|
|
252
324
|
|
|
253
|
-
|
|
325
|
+
printLine('Invalid selection. Enter a valid number from the list.')
|
|
254
326
|
}
|
|
255
327
|
} finally {
|
|
256
328
|
rl.close()
|
|
@@ -354,7 +426,7 @@ async function resolveProjectSlug(projectArg: string | null): Promise<string> {
|
|
|
354
426
|
|
|
355
427
|
const matchedTeam = teams.find(team => team.slug === teamSlug)
|
|
356
428
|
if (!matchedTeam) {
|
|
357
|
-
console.error(`Team '${teamSlug}' not found in your accessible teams.`)
|
|
429
|
+
console.error(`Team '${sanitizeTerminalText(teamSlug)}' not found in your accessible teams.`)
|
|
358
430
|
const selectedTeam = await promptSelect(
|
|
359
431
|
'Select a team',
|
|
360
432
|
teams,
|
|
@@ -375,7 +447,7 @@ async function resolveProjectSlug(projectArg: string | null): Promise<string> {
|
|
|
375
447
|
const matchedProject = projects.find(project => project.slug === projectSlug)
|
|
376
448
|
|
|
377
449
|
if (!matchedProject) {
|
|
378
|
-
console.error(`Project '${projectSlug}' not found in team '${matchedTeam.slug}'.`)
|
|
450
|
+
console.error(`Project '${sanitizeTerminalText(projectSlug)}' not found in team '${sanitizeTerminalText(matchedTeam.slug)}'.`)
|
|
379
451
|
const selectedProject = await promptSelect(
|
|
380
452
|
`Select a project in ${matchedTeam.slug}`,
|
|
381
453
|
projects,
|
|
@@ -434,7 +506,7 @@ async function selectAppIdInteractively(projectArg: string | null): Promise<{ ap
|
|
|
434
506
|
}
|
|
435
507
|
}
|
|
436
508
|
|
|
437
|
-
async function selectDatasetInteractively(projectArg: string | null): Promise<
|
|
509
|
+
async function selectDatasetInteractively(projectArg: string | null): Promise<DatasetSelection> {
|
|
438
510
|
let project = projectArg
|
|
439
511
|
if (!project) {
|
|
440
512
|
project = await resolveProjectSlug(null)
|
|
@@ -451,62 +523,63 @@ async function selectDatasetInteractively(projectArg: string | null): Promise<{
|
|
|
451
523
|
return {
|
|
452
524
|
datasetId: dataset.id,
|
|
453
525
|
project,
|
|
526
|
+
name: dataset.name,
|
|
454
527
|
}
|
|
455
528
|
}
|
|
456
529
|
|
|
457
530
|
function printTeams(teams: Team[]) {
|
|
458
531
|
if (teams.length === 0) {
|
|
459
|
-
|
|
532
|
+
printLine('No teams found.')
|
|
460
533
|
return
|
|
461
534
|
}
|
|
462
535
|
|
|
463
536
|
const rows = teams.map(team => ({
|
|
464
|
-
slug: team.slug,
|
|
465
|
-
name: team.name || '-',
|
|
466
|
-
role: team.role || '-',
|
|
537
|
+
slug: sanitizeTerminalText(team.slug),
|
|
538
|
+
name: sanitizeTerminalText(team.name || '-'),
|
|
539
|
+
role: sanitizeTerminalText(team.role || '-'),
|
|
467
540
|
}))
|
|
468
541
|
|
|
469
542
|
const slugWidth = Math.max('TEAM SLUG'.length, ...rows.map(row => row.slug.length))
|
|
470
543
|
const nameWidth = Math.max('TEAM NAME'.length, ...rows.map(row => row.name.length))
|
|
471
544
|
const roleWidth = Math.max('ROLE'.length, ...rows.map(row => row.role.length))
|
|
472
545
|
|
|
473
|
-
|
|
546
|
+
printLine(
|
|
474
547
|
`${'TEAM SLUG'.padEnd(slugWidth)} ${'TEAM NAME'.padEnd(nameWidth)} ${'ROLE'.padEnd(roleWidth)}`
|
|
475
548
|
)
|
|
476
|
-
|
|
549
|
+
printLine(
|
|
477
550
|
`${'-'.repeat(slugWidth)} ${'-'.repeat(nameWidth)} ${'-'.repeat(roleWidth)}`
|
|
478
551
|
)
|
|
479
552
|
|
|
480
553
|
rows.forEach(row => {
|
|
481
|
-
|
|
554
|
+
printLine(`${row.slug.padEnd(slugWidth)} ${row.name.padEnd(nameWidth)} ${row.role.padEnd(roleWidth)}`)
|
|
482
555
|
})
|
|
483
556
|
}
|
|
484
557
|
|
|
485
558
|
function printProjects(projects: Project[]) {
|
|
486
559
|
if (projects.length === 0) {
|
|
487
|
-
|
|
560
|
+
printLine('No projects found.')
|
|
488
561
|
return
|
|
489
562
|
}
|
|
490
563
|
|
|
491
564
|
const rows = projects.map(project => ({
|
|
492
|
-
project: `${project.teamSlug}/${project.slug}
|
|
493
|
-
name: project.name || '-',
|
|
494
|
-
role: project.role || '-',
|
|
565
|
+
project: sanitizeTerminalText(`${project.teamSlug}/${project.slug}`),
|
|
566
|
+
name: sanitizeTerminalText(project.name || '-'),
|
|
567
|
+
role: sanitizeTerminalText(project.role || '-'),
|
|
495
568
|
}))
|
|
496
569
|
|
|
497
570
|
const projectWidth = Math.max('TEAM/PROJECT'.length, ...rows.map(row => row.project.length))
|
|
498
571
|
const nameWidth = Math.max('PROJECT NAME'.length, ...rows.map(row => row.name.length))
|
|
499
572
|
const roleWidth = Math.max('ROLE'.length, ...rows.map(row => row.role.length))
|
|
500
573
|
|
|
501
|
-
|
|
574
|
+
printLine(
|
|
502
575
|
`${'TEAM/PROJECT'.padEnd(projectWidth)} ${'PROJECT NAME'.padEnd(nameWidth)} ${'ROLE'.padEnd(roleWidth)}`
|
|
503
576
|
)
|
|
504
|
-
|
|
577
|
+
printLine(
|
|
505
578
|
`${'-'.repeat(projectWidth)} ${'-'.repeat(nameWidth)} ${'-'.repeat(roleWidth)}`
|
|
506
579
|
)
|
|
507
580
|
|
|
508
581
|
rows.forEach(row => {
|
|
509
|
-
|
|
582
|
+
printLine(
|
|
510
583
|
`${row.project.padEnd(projectWidth)} ${row.name.padEnd(nameWidth)} ${row.role.padEnd(roleWidth)}`
|
|
511
584
|
)
|
|
512
585
|
})
|
|
@@ -514,16 +587,16 @@ function printProjects(projects: Project[]) {
|
|
|
514
587
|
|
|
515
588
|
function printTasks(tasks: Task[]) {
|
|
516
589
|
if (tasks.length === 0) {
|
|
517
|
-
|
|
590
|
+
printLine('No tasks found.')
|
|
518
591
|
return
|
|
519
592
|
}
|
|
520
593
|
|
|
521
594
|
const rows = tasks.map(task => ({
|
|
522
|
-
id: task.id,
|
|
523
|
-
name: task.title || '-',
|
|
524
|
-
status: task.status || '-',
|
|
595
|
+
id: sanitizeTerminalText(task.id),
|
|
596
|
+
name: sanitizeTerminalText(task.title || '-'),
|
|
597
|
+
status: sanitizeTerminalText(task.status || '-'),
|
|
525
598
|
project: task.teamSlug && task.projectSlug
|
|
526
|
-
? `${task.teamSlug}/${task.projectSlug}`
|
|
599
|
+
? sanitizeTerminalText(`${task.teamSlug}/${task.projectSlug}`)
|
|
527
600
|
: 'unknown-project',
|
|
528
601
|
}))
|
|
529
602
|
|
|
@@ -531,15 +604,15 @@ function printTasks(tasks: Task[]) {
|
|
|
531
604
|
const nameWidth = Math.max('TASK NAME'.length, ...rows.map(row => row.name.length))
|
|
532
605
|
const statusWidth = Math.max('STATUS'.length, ...rows.map(row => row.status.length))
|
|
533
606
|
|
|
534
|
-
|
|
607
|
+
printLine(
|
|
535
608
|
`${'TASK ID'.padEnd(idWidth)} ${'TASK NAME'.padEnd(nameWidth)} ${'STATUS'.padEnd(statusWidth)} TEAM/PROJECT`
|
|
536
609
|
)
|
|
537
|
-
|
|
610
|
+
printLine(
|
|
538
611
|
`${'-'.repeat(idWidth)} ${'-'.repeat(nameWidth)} ${'-'.repeat(statusWidth)} ------------`
|
|
539
612
|
)
|
|
540
613
|
|
|
541
614
|
rows.forEach(row => {
|
|
542
|
-
|
|
615
|
+
printLine(
|
|
543
616
|
`${row.id.padEnd(idWidth)} ${row.name.padEnd(nameWidth)} ${row.status.padEnd(statusWidth)} ${row.project}`
|
|
544
617
|
)
|
|
545
618
|
})
|
|
@@ -547,13 +620,13 @@ function printTasks(tasks: Task[]) {
|
|
|
547
620
|
|
|
548
621
|
function printApps(apps: AppSummary[]) {
|
|
549
622
|
if (apps.length === 0) {
|
|
550
|
-
|
|
623
|
+
printLine('No apps found.')
|
|
551
624
|
return
|
|
552
625
|
}
|
|
553
626
|
|
|
554
627
|
const rows = apps.map(app => ({
|
|
555
|
-
id: app.id,
|
|
556
|
-
name: app.name || '-',
|
|
628
|
+
id: sanitizeTerminalText(app.id),
|
|
629
|
+
name: sanitizeTerminalText(app.name || '-'),
|
|
557
630
|
version: `v${app.currentVersionNum || 1}`,
|
|
558
631
|
}))
|
|
559
632
|
|
|
@@ -561,25 +634,25 @@ function printApps(apps: AppSummary[]) {
|
|
|
561
634
|
const nameWidth = Math.max('APP NAME'.length, ...rows.map(row => row.name.length))
|
|
562
635
|
const versionWidth = Math.max('VERSION'.length, ...rows.map(row => row.version.length))
|
|
563
636
|
|
|
564
|
-
|
|
565
|
-
|
|
637
|
+
printLine(`${'APP ID'.padEnd(idWidth)} ${'APP NAME'.padEnd(nameWidth)} ${'VERSION'.padEnd(versionWidth)}`)
|
|
638
|
+
printLine(`${'-'.repeat(idWidth)} ${'-'.repeat(nameWidth)} ${'-'.repeat(versionWidth)}`)
|
|
566
639
|
|
|
567
640
|
rows.forEach(row => {
|
|
568
|
-
|
|
641
|
+
printLine(`${row.id.padEnd(idWidth)} ${row.name.padEnd(nameWidth)} ${row.version.padEnd(versionWidth)}`)
|
|
569
642
|
})
|
|
570
643
|
}
|
|
571
644
|
|
|
572
645
|
function printTeamMembers(members: TeamMember[]) {
|
|
573
646
|
if (members.length === 0) {
|
|
574
|
-
|
|
647
|
+
printLine('No team members found.')
|
|
575
648
|
return
|
|
576
649
|
}
|
|
577
650
|
|
|
578
651
|
const rows = members.map(member => ({
|
|
579
|
-
id: member.id,
|
|
580
|
-
userId: member.user_id || '-',
|
|
581
|
-
email: member.email || '-',
|
|
582
|
-
role: member.role || '-',
|
|
652
|
+
id: sanitizeTerminalText(member.id),
|
|
653
|
+
userId: sanitizeTerminalText(member.user_id || '-'),
|
|
654
|
+
email: sanitizeTerminalText(member.email || '-'),
|
|
655
|
+
role: sanitizeTerminalText(member.role || '-'),
|
|
583
656
|
}))
|
|
584
657
|
|
|
585
658
|
const idWidth = Math.max('MEMBER ID'.length, ...rows.map(row => row.id.length))
|
|
@@ -587,14 +660,14 @@ function printTeamMembers(members: TeamMember[]) {
|
|
|
587
660
|
const emailWidth = Math.max('EMAIL'.length, ...rows.map(row => row.email.length))
|
|
588
661
|
const roleWidth = Math.max('ROLE'.length, ...rows.map(row => row.role.length))
|
|
589
662
|
|
|
590
|
-
|
|
663
|
+
printLine(
|
|
591
664
|
`${'MEMBER ID'.padEnd(idWidth)} ${'USER ID'.padEnd(userIdWidth)} ${'EMAIL'.padEnd(emailWidth)} ${'ROLE'.padEnd(roleWidth)}`
|
|
592
665
|
)
|
|
593
|
-
|
|
666
|
+
printLine(
|
|
594
667
|
`${'-'.repeat(idWidth)} ${'-'.repeat(userIdWidth)} ${'-'.repeat(emailWidth)} ${'-'.repeat(roleWidth)}`
|
|
595
668
|
)
|
|
596
669
|
rows.forEach(row => {
|
|
597
|
-
|
|
670
|
+
printLine(
|
|
598
671
|
`${row.id.padEnd(idWidth)} ${row.userId.padEnd(userIdWidth)} ${row.email.padEnd(emailWidth)} ${row.role.padEnd(roleWidth)}`
|
|
599
672
|
)
|
|
600
673
|
})
|
|
@@ -602,18 +675,18 @@ function printTeamMembers(members: TeamMember[]) {
|
|
|
602
675
|
|
|
603
676
|
function printTaskStatusSummary(data: TaskStatusPayload) {
|
|
604
677
|
const task = data.task
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
678
|
+
printLine(`Task: ${sanitizeTerminalText(task.title)} (${sanitizeTerminalText(task.id)})`)
|
|
679
|
+
printLine(`Status: ${sanitizeTerminalText(task.status)}`)
|
|
680
|
+
printLine(`Project: ${sanitizeTerminalText(`${task.teamSlug}/${task.projectSlug}`)}`)
|
|
681
|
+
printLine(`Progress: ${task.progressPercentage}%`)
|
|
682
|
+
printLine(`Counts: completed=${task.counts.completed}, in_progress=${task.counts.inProgress}, pending=${task.counts.pending}, skipped=${task.counts.skipped}`)
|
|
683
|
+
printLine(`Required assignments: ${task.totalRequiredAssignments} (${task.datasetRowCount} rows x ${task.requiredAssignmentsPerRow})`)
|
|
611
684
|
|
|
612
685
|
if (task.assignees.length > 0) {
|
|
613
|
-
|
|
686
|
+
printLine('\nAssignees')
|
|
614
687
|
task.assignees.forEach(assignee => {
|
|
615
|
-
|
|
616
|
-
` ${assignee.email}: total=${assignee.total}, completed=${assignee.completed}, in_progress=${assignee.inProgress}, pending=${assignee.pending}, skipped=${assignee.skipped}`
|
|
688
|
+
printLine(
|
|
689
|
+
` ${sanitizeTerminalText(assignee.email)}: total=${assignee.total}, completed=${assignee.completed}, in_progress=${assignee.inProgress}, pending=${assignee.pending}, skipped=${assignee.skipped}`
|
|
617
690
|
)
|
|
618
691
|
})
|
|
619
692
|
}
|
|
@@ -748,6 +821,7 @@ function renderCliAuthBrowserPage(status: 'success' | 'error'): string {
|
|
|
748
821
|
|
|
749
822
|
async function login() {
|
|
750
823
|
const baseUrl = resolveLoginBaseUrl()
|
|
824
|
+
assertSecureTokenTransport(baseUrl)
|
|
751
825
|
const codeVerifier = createCodeVerifier()
|
|
752
826
|
const codeChallenge = createCodeChallenge(codeVerifier)
|
|
753
827
|
const callbackPort = resolveAuthCallbackPort()
|
|
@@ -807,8 +881,9 @@ async function login() {
|
|
|
807
881
|
response,
|
|
808
882
|
'CLI auth start'
|
|
809
883
|
)
|
|
810
|
-
|
|
811
|
-
|
|
884
|
+
const safeAuthorizeUrl = validateBrowserUrl(authorizeUrl, baseUrl).href
|
|
885
|
+
printLine(`Opening browser for login: ${sanitizeTerminalText(safeAuthorizeUrl)}`)
|
|
886
|
+
openInBrowser(safeAuthorizeUrl)
|
|
812
887
|
} catch (error) {
|
|
813
888
|
server.close()
|
|
814
889
|
reject(error)
|
|
@@ -828,13 +903,16 @@ async function login() {
|
|
|
828
903
|
}
|
|
829
904
|
|
|
830
905
|
const loginData = await parseJsonResponse<LoginResponse>(exchangeResponse, 'CLI auth exchange')
|
|
906
|
+
if (!loginData.apiKey) {
|
|
907
|
+
throw new Error('Server did not return an API key. Upgrade the Orizu server and run `orizu login` again.')
|
|
908
|
+
}
|
|
909
|
+
|
|
831
910
|
saveServerCredentials(baseUrl, {
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
expiresAt: loginData.expiresAt,
|
|
911
|
+
credentialType: 'pat',
|
|
912
|
+
apiKey: loginData.apiKey,
|
|
835
913
|
})
|
|
836
914
|
|
|
837
|
-
|
|
915
|
+
printLine(`Logged in as ${sanitizeTerminalText(loginData.user.email ?? loginData.user.id)}`)
|
|
838
916
|
}
|
|
839
917
|
|
|
840
918
|
async function whoami() {
|
|
@@ -844,28 +922,44 @@ async function whoami() {
|
|
|
844
922
|
}
|
|
845
923
|
|
|
846
924
|
const data = await response.json() as { user: { id: string; email: string | null } }
|
|
847
|
-
|
|
925
|
+
printLine(sanitizeTerminalText(data.user.email ?? data.user.id))
|
|
848
926
|
}
|
|
849
927
|
|
|
850
928
|
async function logout() {
|
|
851
929
|
const baseUrl = getBaseUrl()
|
|
852
930
|
const credentials = getServerCredentials(baseUrl)
|
|
853
931
|
if (!credentials) {
|
|
854
|
-
|
|
932
|
+
printLine(`Already logged out for ${sanitizeTerminalText(baseUrl)}.`)
|
|
855
933
|
return
|
|
856
934
|
}
|
|
857
935
|
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
|
|
936
|
+
let remoteLogoutError: string | null = null
|
|
937
|
+
try {
|
|
938
|
+
assertSecureTokenTransport(baseUrl)
|
|
939
|
+
const response = await fetch(`${baseUrl}/api/cli/auth/logout`, {
|
|
940
|
+
method: 'POST',
|
|
941
|
+
headers: {
|
|
942
|
+
'Content-Type': 'application/json',
|
|
943
|
+
Authorization: `Bearer ${'accessToken' in credentials ? credentials.accessToken : credentials.apiKey}`,
|
|
944
|
+
},
|
|
945
|
+
body: 'refreshToken' in credentials
|
|
946
|
+
? JSON.stringify({ refreshToken: credentials.refreshToken })
|
|
947
|
+
: undefined,
|
|
948
|
+
})
|
|
949
|
+
|
|
950
|
+
if (!response.ok) {
|
|
951
|
+
remoteLogoutError = sanitizeTerminalText(await response.text()).slice(0, 180)
|
|
952
|
+
}
|
|
953
|
+
} catch (error) {
|
|
954
|
+
const message = error instanceof Error ? error.message : String(error)
|
|
955
|
+
remoteLogoutError = sanitizeTerminalText(message).slice(0, 180)
|
|
956
|
+
}
|
|
866
957
|
|
|
867
958
|
clearServerCredentials(baseUrl)
|
|
868
|
-
|
|
959
|
+
if (remoteLogoutError) {
|
|
960
|
+
console.warn(`Warning: remote logout failed: ${remoteLogoutError}`)
|
|
961
|
+
}
|
|
962
|
+
printLine(`Logged out from ${sanitizeTerminalText(baseUrl)}.`)
|
|
869
963
|
}
|
|
870
964
|
|
|
871
965
|
async function listTeams() {
|
|
@@ -914,7 +1008,7 @@ async function createTeam() {
|
|
|
914
1008
|
}
|
|
915
1009
|
|
|
916
1010
|
const data = await parseJsonResponse<{ team: Team }>(response, 'Team create')
|
|
917
|
-
|
|
1011
|
+
printLine(`Created team: ${sanitizeTerminalText(data.team.name)} (${sanitizeTerminalText(data.team.slug)})`)
|
|
918
1012
|
}
|
|
919
1013
|
|
|
920
1014
|
async function listProjects() {
|
|
@@ -955,7 +1049,7 @@ async function createProject() {
|
|
|
955
1049
|
const data = await parseJsonResponse<{
|
|
956
1050
|
project: { id: string; name: string; slug: string; teamSlug: string }
|
|
957
1051
|
}>(response, 'Project create')
|
|
958
|
-
|
|
1052
|
+
printLine(`Created project ${sanitizeTerminalText(`${data.project.teamSlug}/${data.project.slug}`)}`)
|
|
959
1053
|
}
|
|
960
1054
|
|
|
961
1055
|
async function listTasks() {
|
|
@@ -1032,9 +1126,9 @@ async function createAppFromFile() {
|
|
|
1032
1126
|
app: { id: string; name: string; versionNum: number; componentName?: string }
|
|
1033
1127
|
warnings?: string[]
|
|
1034
1128
|
}>(response, 'App create')
|
|
1035
|
-
|
|
1129
|
+
printLine(`Created app ${sanitizeTerminalText(data.app.name)} (${sanitizeTerminalText(data.app.id)}) v${data.app.versionNum}`)
|
|
1036
1130
|
if (data.warnings?.length) {
|
|
1037
|
-
|
|
1131
|
+
printLine(`Warnings: ${sanitizeTerminalText(data.warnings.join('; '))}`)
|
|
1038
1132
|
}
|
|
1039
1133
|
}
|
|
1040
1134
|
|
|
@@ -1077,9 +1171,9 @@ async function updateAppFromFile() {
|
|
|
1077
1171
|
app: { id: string; name: string; versionNum: number; componentName?: string }
|
|
1078
1172
|
warnings?: string[]
|
|
1079
1173
|
}>(response, 'App update')
|
|
1080
|
-
|
|
1174
|
+
printLine(`Updated app ${sanitizeTerminalText(data.app.name)} (${sanitizeTerminalText(data.app.id)}) to v${data.app.versionNum}`)
|
|
1081
1175
|
if (data.warnings?.length) {
|
|
1082
|
-
|
|
1176
|
+
printLine(`Warnings: ${sanitizeTerminalText(data.warnings.join('; '))}`)
|
|
1083
1177
|
}
|
|
1084
1178
|
}
|
|
1085
1179
|
|
|
@@ -1124,8 +1218,8 @@ async function linkAppDataset() {
|
|
|
1124
1218
|
versionNum: number
|
|
1125
1219
|
}>(response, 'App link dataset')
|
|
1126
1220
|
|
|
1127
|
-
|
|
1128
|
-
`Linked dataset ${data.linkedDataset.name} (${data.linkedDataset.id}) to app ${data.app.name} (${data.app.id}) version ${data.versionNum}`
|
|
1221
|
+
printLine(
|
|
1222
|
+
`Linked dataset ${sanitizeTerminalText(data.linkedDataset.name)} (${sanitizeTerminalText(data.linkedDataset.id)}) to app ${sanitizeTerminalText(data.app.name)} (${sanitizeTerminalText(data.app.id)}) version ${data.versionNum}`
|
|
1129
1223
|
)
|
|
1130
1224
|
}
|
|
1131
1225
|
|
|
@@ -1180,7 +1274,7 @@ async function createTask() {
|
|
|
1180
1274
|
const cliError = await formatTaskCreateError(response)
|
|
1181
1275
|
if (hasArg('--json')) {
|
|
1182
1276
|
const payload = cliError.structuredPayload ?? { error: cliError.message }
|
|
1183
|
-
|
|
1277
|
+
printLine(JSON.stringify({
|
|
1184
1278
|
...payload,
|
|
1185
1279
|
httpStatus: cliError.httpStatus,
|
|
1186
1280
|
}, null, 2))
|
|
@@ -1203,7 +1297,7 @@ async function createTask() {
|
|
|
1203
1297
|
}>(response, 'Task create')
|
|
1204
1298
|
|
|
1205
1299
|
if (hasArg('--json')) {
|
|
1206
|
-
|
|
1300
|
+
printLine(JSON.stringify({
|
|
1207
1301
|
taskId: data.task.id,
|
|
1208
1302
|
datasetId,
|
|
1209
1303
|
versionId: data.task.versionId,
|
|
@@ -1221,15 +1315,15 @@ async function createTask() {
|
|
|
1221
1315
|
|
|
1222
1316
|
const baseUrl = getBaseUrl()
|
|
1223
1317
|
const taskUrl = `${baseUrl}/d/${projectSlug}/tasks/${data.task.id}`
|
|
1224
|
-
|
|
1225
|
-
`Created task ${data.task.title} (${data.task.id}) [${data.task.status}]` +
|
|
1226
|
-
`\n Task ID: ${data.task.id}` +
|
|
1227
|
-
`\n Dataset ID: ${datasetId}` +
|
|
1228
|
-
`\n Version: v${data.task.versionNum} (${data.task.versionId})` +
|
|
1318
|
+
printLine(
|
|
1319
|
+
`Created task ${sanitizeTerminalText(data.task.title)} (${sanitizeTerminalText(data.task.id)}) [${sanitizeTerminalText(data.task.status)}]` +
|
|
1320
|
+
`\n Task ID: ${sanitizeTerminalText(data.task.id)}` +
|
|
1321
|
+
`\n Dataset ID: ${sanitizeTerminalText(datasetId)}` +
|
|
1322
|
+
`\n Version: v${data.task.versionNum} (${sanitizeTerminalText(data.task.versionId)})` +
|
|
1229
1323
|
`\n Labels/row: ${data.task.requiredAssignmentsPerRow}` +
|
|
1230
1324
|
`\n Assignments: ${data.assignmentsCreated}` +
|
|
1231
|
-
(data.warning ? `\n Warning: ${data.warning}` : '') +
|
|
1232
|
-
`\n URL: ${taskUrl}`
|
|
1325
|
+
(data.warning ? `\n Warning: ${sanitizeTerminalText(data.warning)}` : '') +
|
|
1326
|
+
`\n URL: ${sanitizeTerminalText(taskUrl)}`
|
|
1233
1327
|
)
|
|
1234
1328
|
}
|
|
1235
1329
|
|
|
@@ -1252,7 +1346,7 @@ async function assignTask() {
|
|
|
1252
1346
|
}
|
|
1253
1347
|
|
|
1254
1348
|
const data = await parseJsonResponse<{ assignmentsCreated: number }>(response, 'Task assign')
|
|
1255
|
-
|
|
1349
|
+
printLine(`Created ${data.assignmentsCreated} assignments.`)
|
|
1256
1350
|
}
|
|
1257
1351
|
|
|
1258
1352
|
async function taskStatus() {
|
|
@@ -1274,7 +1368,7 @@ async function taskStatus() {
|
|
|
1274
1368
|
} catch {
|
|
1275
1369
|
// keep raw body as error
|
|
1276
1370
|
}
|
|
1277
|
-
|
|
1371
|
+
printLine(JSON.stringify({
|
|
1278
1372
|
...errorPayload,
|
|
1279
1373
|
httpStatus: response.status,
|
|
1280
1374
|
}, null, 2))
|
|
@@ -1292,7 +1386,7 @@ async function taskStatus() {
|
|
|
1292
1386
|
|
|
1293
1387
|
const data = await parseJsonResponse<TaskStatusPayload>(response, 'Task status')
|
|
1294
1388
|
if (hasArg('--json')) {
|
|
1295
|
-
|
|
1389
|
+
printLine(JSON.stringify(data, null, 2))
|
|
1296
1390
|
return
|
|
1297
1391
|
}
|
|
1298
1392
|
|
|
@@ -1319,7 +1413,7 @@ async function updateTaskStatus(targetStatus: 'paused' | 'active') {
|
|
|
1319
1413
|
|
|
1320
1414
|
const data = await parseJsonResponse<{ task: { id: string; status: string } }>(response, 'Task status update')
|
|
1321
1415
|
const action = targetStatus === 'paused' ? 'Paused' : 'Unpaused'
|
|
1322
|
-
|
|
1416
|
+
printLine(`${action} task ${sanitizeTerminalText(data.task.id)} [${sanitizeTerminalText(data.task.status)}]`)
|
|
1323
1417
|
}
|
|
1324
1418
|
|
|
1325
1419
|
interface AppDetailPayload {
|
|
@@ -1369,25 +1463,25 @@ async function appDetail() {
|
|
|
1369
1463
|
}
|
|
1370
1464
|
|
|
1371
1465
|
if (hasArg('--json')) {
|
|
1372
|
-
|
|
1466
|
+
printLine(JSON.stringify(detail, null, 2))
|
|
1373
1467
|
return
|
|
1374
1468
|
}
|
|
1375
1469
|
|
|
1376
|
-
|
|
1377
|
-
|
|
1470
|
+
printLine(`App: ${sanitizeTerminalText(detail.name)} (${sanitizeTerminalText(detail.id)})`)
|
|
1471
|
+
printLine(` Project: ${sanitizeTerminalText(`${detail.teamSlug}/${detail.projectSlug}`)}`)
|
|
1378
1472
|
if (detail.currentVersion) {
|
|
1379
|
-
|
|
1380
|
-
|
|
1381
|
-
|
|
1473
|
+
printLine(` Current version: v${detail.currentVersion.versionNum} (${sanitizeTerminalText(detail.currentVersion.versionId)})`)
|
|
1474
|
+
printLine(` Input schema: ${detail.currentVersion.inputJsonSchema ? 'defined' : 'none'}`)
|
|
1475
|
+
printLine(` Output schema: ${detail.currentVersion.outputJsonSchema ? 'defined' : 'none'}`)
|
|
1382
1476
|
} else {
|
|
1383
|
-
|
|
1477
|
+
printLine(` Current version: none`)
|
|
1384
1478
|
}
|
|
1385
|
-
|
|
1479
|
+
printLine(` Compatible datasets: ${detail.compatibleDatasetsCount}/${detail.totalDatasetsCount}`)
|
|
1386
1480
|
if (detail.createdByEmail) {
|
|
1387
|
-
|
|
1481
|
+
printLine(` Created by: ${sanitizeTerminalText(detail.createdByName || detail.createdByEmail)}`)
|
|
1388
1482
|
}
|
|
1389
|
-
|
|
1390
|
-
|
|
1483
|
+
printLine(` Created: ${sanitizeTerminalText(detail.createdAt)}`)
|
|
1484
|
+
printLine(` Updated: ${sanitizeTerminalText(detail.updatedAt)}`)
|
|
1391
1485
|
}
|
|
1392
1486
|
|
|
1393
1487
|
async function listTeamMembers() {
|
|
@@ -1412,7 +1506,7 @@ async function addTeamMember() {
|
|
|
1412
1506
|
throw new Error(`Failed to add team member: ${await response.text()}`)
|
|
1413
1507
|
}
|
|
1414
1508
|
const data = await parseJsonResponse<{ member: TeamMember }>(response, 'Team member add')
|
|
1415
|
-
|
|
1509
|
+
printLine(`Added team member ${sanitizeTerminalText(data.member.email)} (${sanitizeTerminalText(data.member.id)})`)
|
|
1416
1510
|
}
|
|
1417
1511
|
|
|
1418
1512
|
async function removeTeamMember() {
|
|
@@ -1435,7 +1529,7 @@ async function removeTeamMember() {
|
|
|
1435
1529
|
if (!response.ok) {
|
|
1436
1530
|
throw new Error(`Failed to remove team member: ${await response.text()}`)
|
|
1437
1531
|
}
|
|
1438
|
-
|
|
1532
|
+
printLine(`Removed team member ${sanitizeTerminalText(member.email)}`)
|
|
1439
1533
|
}
|
|
1440
1534
|
|
|
1441
1535
|
async function changeTeamMemberRole() {
|
|
@@ -1469,7 +1563,7 @@ async function changeTeamMemberRole() {
|
|
|
1469
1563
|
throw new Error(`Failed to update member role: ${await response.text()}`)
|
|
1470
1564
|
}
|
|
1471
1565
|
|
|
1472
|
-
|
|
1566
|
+
printLine(`Updated ${sanitizeTerminalText(member.email)} role to ${sanitizeTerminalText(role)}`)
|
|
1473
1567
|
}
|
|
1474
1568
|
|
|
1475
1569
|
async function createDatasetFromRows(
|
|
@@ -1521,7 +1615,7 @@ async function uploadJsonlDatasetInChunks(
|
|
|
1521
1615
|
|
|
1522
1616
|
throw new Error(
|
|
1523
1617
|
`Upload stopped while reading the next JSONL chunk: ${message}\n` +
|
|
1524
|
-
`Dataset ${dataset.name} (${dataset.id}) was created and ${totalUploaded} rows were uploaded. ` +
|
|
1618
|
+
`Dataset ${sanitizeTerminalText(dataset.name)} (${sanitizeTerminalText(dataset.id)}) was created and ${totalUploaded} rows were uploaded. ` +
|
|
1525
1619
|
`Fix the file, remove the first ${totalUploaded} rows, and run ` +
|
|
1526
1620
|
`orizu datasets append --dataset ${dataset.id} --file <remaining-file>.`
|
|
1527
1621
|
)
|
|
@@ -1533,7 +1627,7 @@ async function uploadJsonlDatasetInChunks(
|
|
|
1533
1627
|
|
|
1534
1628
|
const chunk = nextChunk.value
|
|
1535
1629
|
chunkIndex += 1
|
|
1536
|
-
|
|
1630
|
+
printLine(`Uploading chunk ${chunkIndex} (${chunk.length} rows)...`)
|
|
1537
1631
|
|
|
1538
1632
|
try {
|
|
1539
1633
|
if (!dataset) {
|
|
@@ -1568,9 +1662,9 @@ async function uploadJsonlDatasetInChunks(
|
|
|
1568
1662
|
throw new Error('Dataset file contains no rows')
|
|
1569
1663
|
}
|
|
1570
1664
|
|
|
1571
|
-
|
|
1665
|
+
printLine(`Uploaded dataset ${sanitizeTerminalText(dataset.name)} (${sanitizeTerminalText(dataset.id)}) with ${dataset.rowCount} rows.`)
|
|
1572
1666
|
if (dataset.url) {
|
|
1573
|
-
|
|
1667
|
+
printLine(`View dataset: ${formatTerminalLink(dataset.url)}`)
|
|
1574
1668
|
}
|
|
1575
1669
|
}
|
|
1576
1670
|
|
|
@@ -1595,9 +1689,9 @@ async function uploadDataset() {
|
|
|
1595
1689
|
const { rows, sourceType } = parseDatasetFile(file)
|
|
1596
1690
|
const data = await createDatasetFromRows(project, datasetName, sourceType, rows)
|
|
1597
1691
|
|
|
1598
|
-
|
|
1692
|
+
printLine(`Uploaded dataset ${sanitizeTerminalText(data.dataset.name)} (${sanitizeTerminalText(data.dataset.id)}) with ${data.dataset.rowCount} rows.`)
|
|
1599
1693
|
if (data.dataset.url) {
|
|
1600
|
-
|
|
1694
|
+
printLine(`View dataset: ${formatTerminalLink(data.dataset.url)}`)
|
|
1601
1695
|
}
|
|
1602
1696
|
}
|
|
1603
1697
|
|
|
@@ -1647,7 +1741,7 @@ async function downloadDataset() {
|
|
|
1647
1741
|
const bytes = new Uint8Array(await response.arrayBuffer())
|
|
1648
1742
|
writeFileSync(filename, bytes)
|
|
1649
1743
|
|
|
1650
|
-
|
|
1744
|
+
printLine(`Saved dataset ${sanitizeTerminalText(datasetId)} (${format.toUpperCase()}) to ${sanitizeTerminalText(filename)}`)
|
|
1651
1745
|
}
|
|
1652
1746
|
|
|
1653
1747
|
const MAX_INPUT_FILE_SIZE_BYTES = 50 * 1024 * 1024 // 50 MB
|
|
@@ -1698,7 +1792,7 @@ async function appendJsonlDatasetRowsInChunks(datasetId: string, file: string) {
|
|
|
1698
1792
|
|
|
1699
1793
|
const chunk = nextChunk.value
|
|
1700
1794
|
chunkIndex += 1
|
|
1701
|
-
|
|
1795
|
+
printLine(`Uploading chunk ${chunkIndex} (${chunk.length} rows)...`)
|
|
1702
1796
|
|
|
1703
1797
|
try {
|
|
1704
1798
|
const data = await appendChunk(datasetId, chunk)
|
|
@@ -1718,8 +1812,8 @@ async function appendJsonlDatasetRowsInChunks(datasetId: string, file: string) {
|
|
|
1718
1812
|
throw new Error('Dataset append file must contain at least one row')
|
|
1719
1813
|
}
|
|
1720
1814
|
|
|
1721
|
-
|
|
1722
|
-
`Appended ${totalAppended} rows to dataset ${lastResult.dataset.name} (${lastResult.dataset.id}). New row count: ${lastResult.dataset.rowCount}`
|
|
1815
|
+
printLine(
|
|
1816
|
+
`Appended ${totalAppended} rows to dataset ${sanitizeTerminalText(lastResult.dataset.name)} (${sanitizeTerminalText(lastResult.dataset.id)}). New row count: ${lastResult.dataset.rowCount}`
|
|
1723
1817
|
)
|
|
1724
1818
|
}
|
|
1725
1819
|
|
|
@@ -1781,8 +1875,8 @@ async function appendDatasetRows() {
|
|
|
1781
1875
|
|
|
1782
1876
|
if (rows.length <= APPEND_CHUNK_SIZE_ROWS) {
|
|
1783
1877
|
const data = await appendChunk(datasetId, rows)
|
|
1784
|
-
|
|
1785
|
-
`Appended ${data.appendedCount} rows to dataset ${data.dataset.name} (${data.dataset.id}). New row count: ${data.dataset.rowCount}`
|
|
1878
|
+
printLine(
|
|
1879
|
+
`Appended ${data.appendedCount} rows to dataset ${sanitizeTerminalText(data.dataset.name)} (${sanitizeTerminalText(data.dataset.id)}). New row count: ${data.dataset.rowCount}`
|
|
1786
1880
|
)
|
|
1787
1881
|
return
|
|
1788
1882
|
}
|
|
@@ -1796,7 +1890,7 @@ async function appendDatasetRows() {
|
|
|
1796
1890
|
const chunk = rows.slice(offset, offset + APPEND_CHUNK_SIZE_ROWS)
|
|
1797
1891
|
const chunkIndex = Math.floor(offset / APPEND_CHUNK_SIZE_ROWS) + 1
|
|
1798
1892
|
|
|
1799
|
-
|
|
1893
|
+
printLine(`Uploading chunk ${chunkIndex}/${totalChunks} (${chunk.length} rows)...`)
|
|
1800
1894
|
try {
|
|
1801
1895
|
const data = await appendChunk(datasetId, chunk)
|
|
1802
1896
|
totalAppended += data.appendedCount
|
|
@@ -1812,8 +1906,8 @@ async function appendDatasetRows() {
|
|
|
1812
1906
|
}
|
|
1813
1907
|
|
|
1814
1908
|
if (lastResult) {
|
|
1815
|
-
|
|
1816
|
-
`Appended ${totalAppended} rows to dataset ${lastResult.dataset.name} (${lastResult.dataset.id}). New row count: ${lastResult.dataset.rowCount}`
|
|
1909
|
+
printLine(
|
|
1910
|
+
`Appended ${totalAppended} rows to dataset ${sanitizeTerminalText(lastResult.dataset.name)} (${sanitizeTerminalText(lastResult.dataset.id)}). New row count: ${lastResult.dataset.rowCount}`
|
|
1817
1911
|
)
|
|
1818
1912
|
}
|
|
1819
1913
|
}
|
|
@@ -1873,8 +1967,8 @@ async function editDatasetRows() {
|
|
|
1873
1967
|
updatedCount: number
|
|
1874
1968
|
}>(response, 'Dataset edit rows')
|
|
1875
1969
|
|
|
1876
|
-
|
|
1877
|
-
`Updated ${data.updatedCount} rows in dataset ${data.dataset.name} (${data.dataset.id}). Current row count: ${data.dataset.rowCount}`
|
|
1970
|
+
printLine(
|
|
1971
|
+
`Updated ${data.updatedCount} rows in dataset ${sanitizeTerminalText(data.dataset.name)} (${sanitizeTerminalText(data.dataset.id)}). Current row count: ${data.dataset.rowCount}`
|
|
1878
1972
|
)
|
|
1879
1973
|
}
|
|
1880
1974
|
|
|
@@ -1912,11 +2006,63 @@ async function deleteDatasetRows() {
|
|
|
1912
2006
|
deletedCount: number
|
|
1913
2007
|
}>(response, 'Dataset delete rows')
|
|
1914
2008
|
|
|
1915
|
-
|
|
1916
|
-
`Deleted ${data.deletedCount} rows from dataset ${data.dataset.name} (${data.dataset.id}). New row count: ${data.dataset.rowCount}`
|
|
2009
|
+
printLine(
|
|
2010
|
+
`Deleted ${data.deletedCount} rows from dataset ${sanitizeTerminalText(data.dataset.name)} (${sanitizeTerminalText(data.dataset.id)}). New row count: ${data.dataset.rowCount}`
|
|
1917
2011
|
)
|
|
1918
2012
|
}
|
|
1919
2013
|
|
|
2014
|
+
async function confirmDatasetDeletion(dataset: DatasetSelection) {
|
|
2015
|
+
if (!isInteractiveTerminal()) {
|
|
2016
|
+
throw new Error(
|
|
2017
|
+
'Dataset deletion requires an interactive terminal confirmation. There is no non-interactive delete option.'
|
|
2018
|
+
)
|
|
2019
|
+
}
|
|
2020
|
+
|
|
2021
|
+
const safeName = dataset.name ? ` (${sanitizeTerminalText(dataset.name)})` : ''
|
|
2022
|
+
printLine(
|
|
2023
|
+
`This will permanently delete dataset ${sanitizeTerminalText(dataset.datasetId)}${safeName}.`
|
|
2024
|
+
)
|
|
2025
|
+
printLine('Type the dataset id exactly to confirm.')
|
|
2026
|
+
|
|
2027
|
+
const rl = createInterface({ input, output })
|
|
2028
|
+
try {
|
|
2029
|
+
const answer = (await rl.question('Dataset id: ')).trim()
|
|
2030
|
+
if (answer !== dataset.datasetId) {
|
|
2031
|
+
throw new Error('Dataset deletion cancelled.')
|
|
2032
|
+
}
|
|
2033
|
+
} finally {
|
|
2034
|
+
rl.close()
|
|
2035
|
+
}
|
|
2036
|
+
}
|
|
2037
|
+
|
|
2038
|
+
async function deleteDataset() {
|
|
2039
|
+
const projectArg = getArg('--project')
|
|
2040
|
+
const datasetInput = getDatasetReferenceInput()
|
|
2041
|
+
|
|
2042
|
+
let dataset: DatasetSelection
|
|
2043
|
+
if (datasetInput) {
|
|
2044
|
+
dataset = { datasetId: parseDatasetReference(datasetInput).datasetId }
|
|
2045
|
+
} else {
|
|
2046
|
+
dataset = await selectDatasetInteractively(projectArg)
|
|
2047
|
+
}
|
|
2048
|
+
|
|
2049
|
+
await confirmDatasetDeletion(dataset)
|
|
2050
|
+
|
|
2051
|
+
const response = await authedFetch(`/api/cli/datasets/${encodeURIComponent(dataset.datasetId)}`, {
|
|
2052
|
+
method: 'DELETE',
|
|
2053
|
+
})
|
|
2054
|
+
|
|
2055
|
+
if (!response.ok) {
|
|
2056
|
+
throw new Error(`Delete failed: ${await response.text()}`)
|
|
2057
|
+
}
|
|
2058
|
+
|
|
2059
|
+
const data = await parseJsonResponse<{
|
|
2060
|
+
dataset: { id: string }
|
|
2061
|
+
}>(response, 'Dataset delete')
|
|
2062
|
+
|
|
2063
|
+
printLine(`Deleted dataset ${sanitizeTerminalText(data.dataset.id)}.`)
|
|
2064
|
+
}
|
|
2065
|
+
|
|
1920
2066
|
async function lockDataset() {
|
|
1921
2067
|
const projectArg = getArg('--project')
|
|
1922
2068
|
const datasetInput = getDatasetReferenceInput()
|
|
@@ -1950,8 +2096,8 @@ async function lockDataset() {
|
|
|
1950
2096
|
}
|
|
1951
2097
|
}>(response, 'Dataset lock')
|
|
1952
2098
|
|
|
1953
|
-
|
|
1954
|
-
`Locked dataset ${data.dataset.name} (${data.dataset.id}) at ${data.dataset.lockedAt}. Row count: ${data.dataset.rowCount}`
|
|
2099
|
+
printLine(
|
|
2100
|
+
`Locked dataset ${sanitizeTerminalText(data.dataset.name)} (${sanitizeTerminalText(data.dataset.id)}) at ${sanitizeTerminalText(data.dataset.lockedAt)}. Row count: ${data.dataset.rowCount}`
|
|
1955
2101
|
)
|
|
1956
2102
|
}
|
|
1957
2103
|
|
|
@@ -1987,8 +2133,8 @@ async function cloneDataset() {
|
|
|
1987
2133
|
}
|
|
1988
2134
|
}>(response, 'Dataset clone')
|
|
1989
2135
|
|
|
1990
|
-
|
|
1991
|
-
`Cloned dataset ${data.dataset.parentDatasetId} -> ${data.dataset.name} (${data.dataset.id}). Row count: ${data.dataset.rowCount}`
|
|
2136
|
+
printLine(
|
|
2137
|
+
`Cloned dataset ${sanitizeTerminalText(data.dataset.parentDatasetId)} -> ${sanitizeTerminalText(data.dataset.name)} (${sanitizeTerminalText(data.dataset.id)}). Row count: ${data.dataset.rowCount}`
|
|
1992
2138
|
)
|
|
1993
2139
|
}
|
|
1994
2140
|
|
|
@@ -2018,17 +2164,22 @@ async function downloadAnnotations() {
|
|
|
2018
2164
|
const bytes = new Uint8Array(await response.arrayBuffer())
|
|
2019
2165
|
writeFileSync(filename, bytes)
|
|
2020
2166
|
|
|
2021
|
-
|
|
2167
|
+
printLine(`Saved ${format.toUpperCase()} export to ${sanitizeTerminalText(filename)}`)
|
|
2022
2168
|
}
|
|
2023
2169
|
|
|
2024
|
-
async function main() {
|
|
2025
|
-
const parsed = parseGlobalFlags(
|
|
2170
|
+
export async function main(rawArgs = process.argv.slice(2)) {
|
|
2171
|
+
const parsed = parseGlobalFlags(rawArgs)
|
|
2026
2172
|
setGlobalFlags(parsed.flags)
|
|
2027
2173
|
cliArgs = parsed.args
|
|
2028
2174
|
|
|
2029
2175
|
const command = cliArgs[0]
|
|
2030
2176
|
const subcommand = cliArgs[1]
|
|
2031
2177
|
|
|
2178
|
+
if (command === '--version' || command === '-v') {
|
|
2179
|
+
printVersion()
|
|
2180
|
+
return
|
|
2181
|
+
}
|
|
2182
|
+
|
|
2032
2183
|
if (!command) {
|
|
2033
2184
|
printUsage()
|
|
2034
2185
|
process.exit(1)
|
|
@@ -2167,6 +2318,11 @@ async function main() {
|
|
|
2167
2318
|
return
|
|
2168
2319
|
}
|
|
2169
2320
|
|
|
2321
|
+
if (command === 'datasets' && subcommand === 'delete') {
|
|
2322
|
+
await deleteDataset()
|
|
2323
|
+
return
|
|
2324
|
+
}
|
|
2325
|
+
|
|
2170
2326
|
if (command === 'datasets' && subcommand === 'lock') {
|
|
2171
2327
|
await lockDataset()
|
|
2172
2328
|
return
|
|
@@ -2186,7 +2342,9 @@ async function main() {
|
|
|
2186
2342
|
process.exit(1)
|
|
2187
2343
|
}
|
|
2188
2344
|
|
|
2189
|
-
|
|
2190
|
-
|
|
2191
|
-
|
|
2192
|
-
|
|
2345
|
+
if (process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href) {
|
|
2346
|
+
main().catch(error => {
|
|
2347
|
+
console.error(sanitizeTerminalText(error instanceof Error ? error.message : 'Unknown error'))
|
|
2348
|
+
process.exit(1)
|
|
2349
|
+
})
|
|
2350
|
+
}
|