orizu 0.1.6 → 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 +27 -3
- package/dist/http.js +17 -4
- package/dist/index.js +166 -99
- package/package.json +1 -1
- package/src/credentials.ts +40 -8
- package/src/http.ts +22 -5
- package/src/index.ts +197 -100
- 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'
|
|
@@ -73,6 +74,12 @@ interface DatasetSummary {
|
|
|
73
74
|
teamSlug: string
|
|
74
75
|
}
|
|
75
76
|
|
|
77
|
+
interface DatasetSelection {
|
|
78
|
+
datasetId: string
|
|
79
|
+
project?: string
|
|
80
|
+
name?: string
|
|
81
|
+
}
|
|
82
|
+
|
|
76
83
|
type DatasetUploadSourceType = 'csv' | 'json' | 'jsonl'
|
|
77
84
|
|
|
78
85
|
interface DatasetUploadResponse {
|
|
@@ -125,8 +132,28 @@ interface TaskStatusPayload {
|
|
|
125
132
|
}
|
|
126
133
|
}
|
|
127
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
|
+
|
|
128
155
|
function printUsage() {
|
|
129
|
-
|
|
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>]`)
|
|
130
157
|
}
|
|
131
158
|
|
|
132
159
|
let cliArgs = process.argv.slice(2)
|
|
@@ -140,7 +167,7 @@ function getArg(name: string): string | null {
|
|
|
140
167
|
return cliArgs[index + 1]
|
|
141
168
|
}
|
|
142
169
|
|
|
143
|
-
function normalizeSlugInput(slug: string): string {
|
|
170
|
+
export function normalizeSlugInput(slug: string): string {
|
|
144
171
|
return slug.trim().toLowerCase()
|
|
145
172
|
}
|
|
146
173
|
|
|
@@ -152,7 +179,7 @@ function hasArg(name: string): boolean {
|
|
|
152
179
|
return cliArgs.includes(name)
|
|
153
180
|
}
|
|
154
181
|
|
|
155
|
-
function expandHomePath(path: string): string {
|
|
182
|
+
export function expandHomePath(path: string): string {
|
|
156
183
|
if (path.startsWith('~/')) {
|
|
157
184
|
const home = process.env.HOME || ''
|
|
158
185
|
return `${home}/${path.slice(2)}`
|
|
@@ -165,15 +192,15 @@ function createCodeVerifier(): string {
|
|
|
165
192
|
return randomBytes(32).toString('base64url')
|
|
166
193
|
}
|
|
167
194
|
|
|
168
|
-
function createCodeChallenge(verifier: string): string {
|
|
195
|
+
export function createCodeChallenge(verifier: string): string {
|
|
169
196
|
return createHash('sha256').update(verifier).digest('base64url')
|
|
170
197
|
}
|
|
171
198
|
|
|
172
|
-
function sanitizeTerminalText(value: unknown): string {
|
|
199
|
+
export function sanitizeTerminalText(value: unknown): string {
|
|
173
200
|
return String(value).replace(/[\u0000-\u0008\u000B\u000C\u000E-\u001F\u007F-\u009F]/g, '')
|
|
174
201
|
}
|
|
175
202
|
|
|
176
|
-
function validateBrowserUrl(url: string, expectedOrigin?: string): URL {
|
|
203
|
+
export function validateBrowserUrl(url: string, expectedOrigin?: string): URL {
|
|
177
204
|
let parsed: URL
|
|
178
205
|
try {
|
|
179
206
|
parsed = new URL(url)
|
|
@@ -223,7 +250,7 @@ function openInBrowser(url: string) {
|
|
|
223
250
|
}).unref()
|
|
224
251
|
}
|
|
225
252
|
|
|
226
|
-
function formatTerminalLink(url: string): string {
|
|
253
|
+
export function formatTerminalLink(url: string): string {
|
|
227
254
|
const safeUrl = sanitizeTerminalText(url)
|
|
228
255
|
try {
|
|
229
256
|
const parsed = validateBrowserUrl(safeUrl)
|
|
@@ -241,7 +268,7 @@ function formatTerminalLink(url: string): string {
|
|
|
241
268
|
return `\u001B]8;;${safeUrl}\u0007${safeUrl}\u001B]8;;\u0007`
|
|
242
269
|
}
|
|
243
270
|
|
|
244
|
-
async function parseJsonResponse<T>(response: Response, context: string): Promise<T> {
|
|
271
|
+
export async function parseJsonResponse<T>(response: Response, context: string): Promise<T> {
|
|
245
272
|
const contentType = response.headers.get('content-type') || ''
|
|
246
273
|
const rawBody = await response.text()
|
|
247
274
|
|
|
@@ -281,9 +308,9 @@ async function promptSelect<T>(
|
|
|
281
308
|
return items[0]
|
|
282
309
|
}
|
|
283
310
|
|
|
284
|
-
|
|
311
|
+
printLine(`\n${sanitizeTerminalText(title)}`)
|
|
285
312
|
items.forEach((item, index) => {
|
|
286
|
-
|
|
313
|
+
printLine(` ${index + 1}. ${sanitizeTerminalText(label(item, index))}`)
|
|
287
314
|
})
|
|
288
315
|
|
|
289
316
|
const rl = createInterface({ input, output })
|
|
@@ -295,7 +322,7 @@ async function promptSelect<T>(
|
|
|
295
322
|
return items[chosenIndex - 1]
|
|
296
323
|
}
|
|
297
324
|
|
|
298
|
-
|
|
325
|
+
printLine('Invalid selection. Enter a valid number from the list.')
|
|
299
326
|
}
|
|
300
327
|
} finally {
|
|
301
328
|
rl.close()
|
|
@@ -479,7 +506,7 @@ async function selectAppIdInteractively(projectArg: string | null): Promise<{ ap
|
|
|
479
506
|
}
|
|
480
507
|
}
|
|
481
508
|
|
|
482
|
-
async function selectDatasetInteractively(projectArg: string | null): Promise<
|
|
509
|
+
async function selectDatasetInteractively(projectArg: string | null): Promise<DatasetSelection> {
|
|
483
510
|
let project = projectArg
|
|
484
511
|
if (!project) {
|
|
485
512
|
project = await resolveProjectSlug(null)
|
|
@@ -496,12 +523,13 @@ async function selectDatasetInteractively(projectArg: string | null): Promise<{
|
|
|
496
523
|
return {
|
|
497
524
|
datasetId: dataset.id,
|
|
498
525
|
project,
|
|
526
|
+
name: dataset.name,
|
|
499
527
|
}
|
|
500
528
|
}
|
|
501
529
|
|
|
502
530
|
function printTeams(teams: Team[]) {
|
|
503
531
|
if (teams.length === 0) {
|
|
504
|
-
|
|
532
|
+
printLine('No teams found.')
|
|
505
533
|
return
|
|
506
534
|
}
|
|
507
535
|
|
|
@@ -515,21 +543,21 @@ function printTeams(teams: Team[]) {
|
|
|
515
543
|
const nameWidth = Math.max('TEAM NAME'.length, ...rows.map(row => row.name.length))
|
|
516
544
|
const roleWidth = Math.max('ROLE'.length, ...rows.map(row => row.role.length))
|
|
517
545
|
|
|
518
|
-
|
|
546
|
+
printLine(
|
|
519
547
|
`${'TEAM SLUG'.padEnd(slugWidth)} ${'TEAM NAME'.padEnd(nameWidth)} ${'ROLE'.padEnd(roleWidth)}`
|
|
520
548
|
)
|
|
521
|
-
|
|
549
|
+
printLine(
|
|
522
550
|
`${'-'.repeat(slugWidth)} ${'-'.repeat(nameWidth)} ${'-'.repeat(roleWidth)}`
|
|
523
551
|
)
|
|
524
552
|
|
|
525
553
|
rows.forEach(row => {
|
|
526
|
-
|
|
554
|
+
printLine(`${row.slug.padEnd(slugWidth)} ${row.name.padEnd(nameWidth)} ${row.role.padEnd(roleWidth)}`)
|
|
527
555
|
})
|
|
528
556
|
}
|
|
529
557
|
|
|
530
558
|
function printProjects(projects: Project[]) {
|
|
531
559
|
if (projects.length === 0) {
|
|
532
|
-
|
|
560
|
+
printLine('No projects found.')
|
|
533
561
|
return
|
|
534
562
|
}
|
|
535
563
|
|
|
@@ -543,15 +571,15 @@ function printProjects(projects: Project[]) {
|
|
|
543
571
|
const nameWidth = Math.max('PROJECT NAME'.length, ...rows.map(row => row.name.length))
|
|
544
572
|
const roleWidth = Math.max('ROLE'.length, ...rows.map(row => row.role.length))
|
|
545
573
|
|
|
546
|
-
|
|
574
|
+
printLine(
|
|
547
575
|
`${'TEAM/PROJECT'.padEnd(projectWidth)} ${'PROJECT NAME'.padEnd(nameWidth)} ${'ROLE'.padEnd(roleWidth)}`
|
|
548
576
|
)
|
|
549
|
-
|
|
577
|
+
printLine(
|
|
550
578
|
`${'-'.repeat(projectWidth)} ${'-'.repeat(nameWidth)} ${'-'.repeat(roleWidth)}`
|
|
551
579
|
)
|
|
552
580
|
|
|
553
581
|
rows.forEach(row => {
|
|
554
|
-
|
|
582
|
+
printLine(
|
|
555
583
|
`${row.project.padEnd(projectWidth)} ${row.name.padEnd(nameWidth)} ${row.role.padEnd(roleWidth)}`
|
|
556
584
|
)
|
|
557
585
|
})
|
|
@@ -559,7 +587,7 @@ function printProjects(projects: Project[]) {
|
|
|
559
587
|
|
|
560
588
|
function printTasks(tasks: Task[]) {
|
|
561
589
|
if (tasks.length === 0) {
|
|
562
|
-
|
|
590
|
+
printLine('No tasks found.')
|
|
563
591
|
return
|
|
564
592
|
}
|
|
565
593
|
|
|
@@ -576,15 +604,15 @@ function printTasks(tasks: Task[]) {
|
|
|
576
604
|
const nameWidth = Math.max('TASK NAME'.length, ...rows.map(row => row.name.length))
|
|
577
605
|
const statusWidth = Math.max('STATUS'.length, ...rows.map(row => row.status.length))
|
|
578
606
|
|
|
579
|
-
|
|
607
|
+
printLine(
|
|
580
608
|
`${'TASK ID'.padEnd(idWidth)} ${'TASK NAME'.padEnd(nameWidth)} ${'STATUS'.padEnd(statusWidth)} TEAM/PROJECT`
|
|
581
609
|
)
|
|
582
|
-
|
|
610
|
+
printLine(
|
|
583
611
|
`${'-'.repeat(idWidth)} ${'-'.repeat(nameWidth)} ${'-'.repeat(statusWidth)} ------------`
|
|
584
612
|
)
|
|
585
613
|
|
|
586
614
|
rows.forEach(row => {
|
|
587
|
-
|
|
615
|
+
printLine(
|
|
588
616
|
`${row.id.padEnd(idWidth)} ${row.name.padEnd(nameWidth)} ${row.status.padEnd(statusWidth)} ${row.project}`
|
|
589
617
|
)
|
|
590
618
|
})
|
|
@@ -592,7 +620,7 @@ function printTasks(tasks: Task[]) {
|
|
|
592
620
|
|
|
593
621
|
function printApps(apps: AppSummary[]) {
|
|
594
622
|
if (apps.length === 0) {
|
|
595
|
-
|
|
623
|
+
printLine('No apps found.')
|
|
596
624
|
return
|
|
597
625
|
}
|
|
598
626
|
|
|
@@ -606,17 +634,17 @@ function printApps(apps: AppSummary[]) {
|
|
|
606
634
|
const nameWidth = Math.max('APP NAME'.length, ...rows.map(row => row.name.length))
|
|
607
635
|
const versionWidth = Math.max('VERSION'.length, ...rows.map(row => row.version.length))
|
|
608
636
|
|
|
609
|
-
|
|
610
|
-
|
|
637
|
+
printLine(`${'APP ID'.padEnd(idWidth)} ${'APP NAME'.padEnd(nameWidth)} ${'VERSION'.padEnd(versionWidth)}`)
|
|
638
|
+
printLine(`${'-'.repeat(idWidth)} ${'-'.repeat(nameWidth)} ${'-'.repeat(versionWidth)}`)
|
|
611
639
|
|
|
612
640
|
rows.forEach(row => {
|
|
613
|
-
|
|
641
|
+
printLine(`${row.id.padEnd(idWidth)} ${row.name.padEnd(nameWidth)} ${row.version.padEnd(versionWidth)}`)
|
|
614
642
|
})
|
|
615
643
|
}
|
|
616
644
|
|
|
617
645
|
function printTeamMembers(members: TeamMember[]) {
|
|
618
646
|
if (members.length === 0) {
|
|
619
|
-
|
|
647
|
+
printLine('No team members found.')
|
|
620
648
|
return
|
|
621
649
|
}
|
|
622
650
|
|
|
@@ -632,14 +660,14 @@ function printTeamMembers(members: TeamMember[]) {
|
|
|
632
660
|
const emailWidth = Math.max('EMAIL'.length, ...rows.map(row => row.email.length))
|
|
633
661
|
const roleWidth = Math.max('ROLE'.length, ...rows.map(row => row.role.length))
|
|
634
662
|
|
|
635
|
-
|
|
663
|
+
printLine(
|
|
636
664
|
`${'MEMBER ID'.padEnd(idWidth)} ${'USER ID'.padEnd(userIdWidth)} ${'EMAIL'.padEnd(emailWidth)} ${'ROLE'.padEnd(roleWidth)}`
|
|
637
665
|
)
|
|
638
|
-
|
|
666
|
+
printLine(
|
|
639
667
|
`${'-'.repeat(idWidth)} ${'-'.repeat(userIdWidth)} ${'-'.repeat(emailWidth)} ${'-'.repeat(roleWidth)}`
|
|
640
668
|
)
|
|
641
669
|
rows.forEach(row => {
|
|
642
|
-
|
|
670
|
+
printLine(
|
|
643
671
|
`${row.id.padEnd(idWidth)} ${row.userId.padEnd(userIdWidth)} ${row.email.padEnd(emailWidth)} ${row.role.padEnd(roleWidth)}`
|
|
644
672
|
)
|
|
645
673
|
})
|
|
@@ -647,17 +675,17 @@ function printTeamMembers(members: TeamMember[]) {
|
|
|
647
675
|
|
|
648
676
|
function printTaskStatusSummary(data: TaskStatusPayload) {
|
|
649
677
|
const task = data.task
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
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})`)
|
|
656
684
|
|
|
657
685
|
if (task.assignees.length > 0) {
|
|
658
|
-
|
|
686
|
+
printLine('\nAssignees')
|
|
659
687
|
task.assignees.forEach(assignee => {
|
|
660
|
-
|
|
688
|
+
printLine(
|
|
661
689
|
` ${sanitizeTerminalText(assignee.email)}: total=${assignee.total}, completed=${assignee.completed}, in_progress=${assignee.inProgress}, pending=${assignee.pending}, skipped=${assignee.skipped}`
|
|
662
690
|
)
|
|
663
691
|
})
|
|
@@ -854,7 +882,7 @@ async function login() {
|
|
|
854
882
|
'CLI auth start'
|
|
855
883
|
)
|
|
856
884
|
const safeAuthorizeUrl = validateBrowserUrl(authorizeUrl, baseUrl).href
|
|
857
|
-
|
|
885
|
+
printLine(`Opening browser for login: ${sanitizeTerminalText(safeAuthorizeUrl)}`)
|
|
858
886
|
openInBrowser(safeAuthorizeUrl)
|
|
859
887
|
} catch (error) {
|
|
860
888
|
server.close()
|
|
@@ -875,13 +903,16 @@ async function login() {
|
|
|
875
903
|
}
|
|
876
904
|
|
|
877
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
|
+
|
|
878
910
|
saveServerCredentials(baseUrl, {
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
expiresAt: loginData.expiresAt,
|
|
911
|
+
credentialType: 'pat',
|
|
912
|
+
apiKey: loginData.apiKey,
|
|
882
913
|
})
|
|
883
914
|
|
|
884
|
-
|
|
915
|
+
printLine(`Logged in as ${sanitizeTerminalText(loginData.user.email ?? loginData.user.id)}`)
|
|
885
916
|
}
|
|
886
917
|
|
|
887
918
|
async function whoami() {
|
|
@@ -891,14 +922,14 @@ async function whoami() {
|
|
|
891
922
|
}
|
|
892
923
|
|
|
893
924
|
const data = await response.json() as { user: { id: string; email: string | null } }
|
|
894
|
-
|
|
925
|
+
printLine(sanitizeTerminalText(data.user.email ?? data.user.id))
|
|
895
926
|
}
|
|
896
927
|
|
|
897
928
|
async function logout() {
|
|
898
929
|
const baseUrl = getBaseUrl()
|
|
899
930
|
const credentials = getServerCredentials(baseUrl)
|
|
900
931
|
if (!credentials) {
|
|
901
|
-
|
|
932
|
+
printLine(`Already logged out for ${sanitizeTerminalText(baseUrl)}.`)
|
|
902
933
|
return
|
|
903
934
|
}
|
|
904
935
|
|
|
@@ -909,9 +940,11 @@ async function logout() {
|
|
|
909
940
|
method: 'POST',
|
|
910
941
|
headers: {
|
|
911
942
|
'Content-Type': 'application/json',
|
|
912
|
-
Authorization: `Bearer ${credentials.accessToken}`,
|
|
943
|
+
Authorization: `Bearer ${'accessToken' in credentials ? credentials.accessToken : credentials.apiKey}`,
|
|
913
944
|
},
|
|
914
|
-
body:
|
|
945
|
+
body: 'refreshToken' in credentials
|
|
946
|
+
? JSON.stringify({ refreshToken: credentials.refreshToken })
|
|
947
|
+
: undefined,
|
|
915
948
|
})
|
|
916
949
|
|
|
917
950
|
if (!response.ok) {
|
|
@@ -926,7 +959,7 @@ async function logout() {
|
|
|
926
959
|
if (remoteLogoutError) {
|
|
927
960
|
console.warn(`Warning: remote logout failed: ${remoteLogoutError}`)
|
|
928
961
|
}
|
|
929
|
-
|
|
962
|
+
printLine(`Logged out from ${sanitizeTerminalText(baseUrl)}.`)
|
|
930
963
|
}
|
|
931
964
|
|
|
932
965
|
async function listTeams() {
|
|
@@ -975,7 +1008,7 @@ async function createTeam() {
|
|
|
975
1008
|
}
|
|
976
1009
|
|
|
977
1010
|
const data = await parseJsonResponse<{ team: Team }>(response, 'Team create')
|
|
978
|
-
|
|
1011
|
+
printLine(`Created team: ${sanitizeTerminalText(data.team.name)} (${sanitizeTerminalText(data.team.slug)})`)
|
|
979
1012
|
}
|
|
980
1013
|
|
|
981
1014
|
async function listProjects() {
|
|
@@ -1016,7 +1049,7 @@ async function createProject() {
|
|
|
1016
1049
|
const data = await parseJsonResponse<{
|
|
1017
1050
|
project: { id: string; name: string; slug: string; teamSlug: string }
|
|
1018
1051
|
}>(response, 'Project create')
|
|
1019
|
-
|
|
1052
|
+
printLine(`Created project ${sanitizeTerminalText(`${data.project.teamSlug}/${data.project.slug}`)}`)
|
|
1020
1053
|
}
|
|
1021
1054
|
|
|
1022
1055
|
async function listTasks() {
|
|
@@ -1093,9 +1126,9 @@ async function createAppFromFile() {
|
|
|
1093
1126
|
app: { id: string; name: string; versionNum: number; componentName?: string }
|
|
1094
1127
|
warnings?: string[]
|
|
1095
1128
|
}>(response, 'App create')
|
|
1096
|
-
|
|
1129
|
+
printLine(`Created app ${sanitizeTerminalText(data.app.name)} (${sanitizeTerminalText(data.app.id)}) v${data.app.versionNum}`)
|
|
1097
1130
|
if (data.warnings?.length) {
|
|
1098
|
-
|
|
1131
|
+
printLine(`Warnings: ${sanitizeTerminalText(data.warnings.join('; '))}`)
|
|
1099
1132
|
}
|
|
1100
1133
|
}
|
|
1101
1134
|
|
|
@@ -1138,9 +1171,9 @@ async function updateAppFromFile() {
|
|
|
1138
1171
|
app: { id: string; name: string; versionNum: number; componentName?: string }
|
|
1139
1172
|
warnings?: string[]
|
|
1140
1173
|
}>(response, 'App update')
|
|
1141
|
-
|
|
1174
|
+
printLine(`Updated app ${sanitizeTerminalText(data.app.name)} (${sanitizeTerminalText(data.app.id)}) to v${data.app.versionNum}`)
|
|
1142
1175
|
if (data.warnings?.length) {
|
|
1143
|
-
|
|
1176
|
+
printLine(`Warnings: ${sanitizeTerminalText(data.warnings.join('; '))}`)
|
|
1144
1177
|
}
|
|
1145
1178
|
}
|
|
1146
1179
|
|
|
@@ -1185,7 +1218,7 @@ async function linkAppDataset() {
|
|
|
1185
1218
|
versionNum: number
|
|
1186
1219
|
}>(response, 'App link dataset')
|
|
1187
1220
|
|
|
1188
|
-
|
|
1221
|
+
printLine(
|
|
1189
1222
|
`Linked dataset ${sanitizeTerminalText(data.linkedDataset.name)} (${sanitizeTerminalText(data.linkedDataset.id)}) to app ${sanitizeTerminalText(data.app.name)} (${sanitizeTerminalText(data.app.id)}) version ${data.versionNum}`
|
|
1190
1223
|
)
|
|
1191
1224
|
}
|
|
@@ -1241,7 +1274,7 @@ async function createTask() {
|
|
|
1241
1274
|
const cliError = await formatTaskCreateError(response)
|
|
1242
1275
|
if (hasArg('--json')) {
|
|
1243
1276
|
const payload = cliError.structuredPayload ?? { error: cliError.message }
|
|
1244
|
-
|
|
1277
|
+
printLine(JSON.stringify({
|
|
1245
1278
|
...payload,
|
|
1246
1279
|
httpStatus: cliError.httpStatus,
|
|
1247
1280
|
}, null, 2))
|
|
@@ -1264,7 +1297,7 @@ async function createTask() {
|
|
|
1264
1297
|
}>(response, 'Task create')
|
|
1265
1298
|
|
|
1266
1299
|
if (hasArg('--json')) {
|
|
1267
|
-
|
|
1300
|
+
printLine(JSON.stringify({
|
|
1268
1301
|
taskId: data.task.id,
|
|
1269
1302
|
datasetId,
|
|
1270
1303
|
versionId: data.task.versionId,
|
|
@@ -1282,7 +1315,7 @@ async function createTask() {
|
|
|
1282
1315
|
|
|
1283
1316
|
const baseUrl = getBaseUrl()
|
|
1284
1317
|
const taskUrl = `${baseUrl}/d/${projectSlug}/tasks/${data.task.id}`
|
|
1285
|
-
|
|
1318
|
+
printLine(
|
|
1286
1319
|
`Created task ${sanitizeTerminalText(data.task.title)} (${sanitizeTerminalText(data.task.id)}) [${sanitizeTerminalText(data.task.status)}]` +
|
|
1287
1320
|
`\n Task ID: ${sanitizeTerminalText(data.task.id)}` +
|
|
1288
1321
|
`\n Dataset ID: ${sanitizeTerminalText(datasetId)}` +
|
|
@@ -1313,7 +1346,7 @@ async function assignTask() {
|
|
|
1313
1346
|
}
|
|
1314
1347
|
|
|
1315
1348
|
const data = await parseJsonResponse<{ assignmentsCreated: number }>(response, 'Task assign')
|
|
1316
|
-
|
|
1349
|
+
printLine(`Created ${data.assignmentsCreated} assignments.`)
|
|
1317
1350
|
}
|
|
1318
1351
|
|
|
1319
1352
|
async function taskStatus() {
|
|
@@ -1335,7 +1368,7 @@ async function taskStatus() {
|
|
|
1335
1368
|
} catch {
|
|
1336
1369
|
// keep raw body as error
|
|
1337
1370
|
}
|
|
1338
|
-
|
|
1371
|
+
printLine(JSON.stringify({
|
|
1339
1372
|
...errorPayload,
|
|
1340
1373
|
httpStatus: response.status,
|
|
1341
1374
|
}, null, 2))
|
|
@@ -1353,7 +1386,7 @@ async function taskStatus() {
|
|
|
1353
1386
|
|
|
1354
1387
|
const data = await parseJsonResponse<TaskStatusPayload>(response, 'Task status')
|
|
1355
1388
|
if (hasArg('--json')) {
|
|
1356
|
-
|
|
1389
|
+
printLine(JSON.stringify(data, null, 2))
|
|
1357
1390
|
return
|
|
1358
1391
|
}
|
|
1359
1392
|
|
|
@@ -1380,7 +1413,7 @@ async function updateTaskStatus(targetStatus: 'paused' | 'active') {
|
|
|
1380
1413
|
|
|
1381
1414
|
const data = await parseJsonResponse<{ task: { id: string; status: string } }>(response, 'Task status update')
|
|
1382
1415
|
const action = targetStatus === 'paused' ? 'Paused' : 'Unpaused'
|
|
1383
|
-
|
|
1416
|
+
printLine(`${action} task ${sanitizeTerminalText(data.task.id)} [${sanitizeTerminalText(data.task.status)}]`)
|
|
1384
1417
|
}
|
|
1385
1418
|
|
|
1386
1419
|
interface AppDetailPayload {
|
|
@@ -1430,25 +1463,25 @@ async function appDetail() {
|
|
|
1430
1463
|
}
|
|
1431
1464
|
|
|
1432
1465
|
if (hasArg('--json')) {
|
|
1433
|
-
|
|
1466
|
+
printLine(JSON.stringify(detail, null, 2))
|
|
1434
1467
|
return
|
|
1435
1468
|
}
|
|
1436
1469
|
|
|
1437
|
-
|
|
1438
|
-
|
|
1470
|
+
printLine(`App: ${sanitizeTerminalText(detail.name)} (${sanitizeTerminalText(detail.id)})`)
|
|
1471
|
+
printLine(` Project: ${sanitizeTerminalText(`${detail.teamSlug}/${detail.projectSlug}`)}`)
|
|
1439
1472
|
if (detail.currentVersion) {
|
|
1440
|
-
|
|
1441
|
-
|
|
1442
|
-
|
|
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'}`)
|
|
1443
1476
|
} else {
|
|
1444
|
-
|
|
1477
|
+
printLine(` Current version: none`)
|
|
1445
1478
|
}
|
|
1446
|
-
|
|
1479
|
+
printLine(` Compatible datasets: ${detail.compatibleDatasetsCount}/${detail.totalDatasetsCount}`)
|
|
1447
1480
|
if (detail.createdByEmail) {
|
|
1448
|
-
|
|
1481
|
+
printLine(` Created by: ${sanitizeTerminalText(detail.createdByName || detail.createdByEmail)}`)
|
|
1449
1482
|
}
|
|
1450
|
-
|
|
1451
|
-
|
|
1483
|
+
printLine(` Created: ${sanitizeTerminalText(detail.createdAt)}`)
|
|
1484
|
+
printLine(` Updated: ${sanitizeTerminalText(detail.updatedAt)}`)
|
|
1452
1485
|
}
|
|
1453
1486
|
|
|
1454
1487
|
async function listTeamMembers() {
|
|
@@ -1473,7 +1506,7 @@ async function addTeamMember() {
|
|
|
1473
1506
|
throw new Error(`Failed to add team member: ${await response.text()}`)
|
|
1474
1507
|
}
|
|
1475
1508
|
const data = await parseJsonResponse<{ member: TeamMember }>(response, 'Team member add')
|
|
1476
|
-
|
|
1509
|
+
printLine(`Added team member ${sanitizeTerminalText(data.member.email)} (${sanitizeTerminalText(data.member.id)})`)
|
|
1477
1510
|
}
|
|
1478
1511
|
|
|
1479
1512
|
async function removeTeamMember() {
|
|
@@ -1496,7 +1529,7 @@ async function removeTeamMember() {
|
|
|
1496
1529
|
if (!response.ok) {
|
|
1497
1530
|
throw new Error(`Failed to remove team member: ${await response.text()}`)
|
|
1498
1531
|
}
|
|
1499
|
-
|
|
1532
|
+
printLine(`Removed team member ${sanitizeTerminalText(member.email)}`)
|
|
1500
1533
|
}
|
|
1501
1534
|
|
|
1502
1535
|
async function changeTeamMemberRole() {
|
|
@@ -1530,7 +1563,7 @@ async function changeTeamMemberRole() {
|
|
|
1530
1563
|
throw new Error(`Failed to update member role: ${await response.text()}`)
|
|
1531
1564
|
}
|
|
1532
1565
|
|
|
1533
|
-
|
|
1566
|
+
printLine(`Updated ${sanitizeTerminalText(member.email)} role to ${sanitizeTerminalText(role)}`)
|
|
1534
1567
|
}
|
|
1535
1568
|
|
|
1536
1569
|
async function createDatasetFromRows(
|
|
@@ -1594,7 +1627,7 @@ async function uploadJsonlDatasetInChunks(
|
|
|
1594
1627
|
|
|
1595
1628
|
const chunk = nextChunk.value
|
|
1596
1629
|
chunkIndex += 1
|
|
1597
|
-
|
|
1630
|
+
printLine(`Uploading chunk ${chunkIndex} (${chunk.length} rows)...`)
|
|
1598
1631
|
|
|
1599
1632
|
try {
|
|
1600
1633
|
if (!dataset) {
|
|
@@ -1629,9 +1662,9 @@ async function uploadJsonlDatasetInChunks(
|
|
|
1629
1662
|
throw new Error('Dataset file contains no rows')
|
|
1630
1663
|
}
|
|
1631
1664
|
|
|
1632
|
-
|
|
1665
|
+
printLine(`Uploaded dataset ${sanitizeTerminalText(dataset.name)} (${sanitizeTerminalText(dataset.id)}) with ${dataset.rowCount} rows.`)
|
|
1633
1666
|
if (dataset.url) {
|
|
1634
|
-
|
|
1667
|
+
printLine(`View dataset: ${formatTerminalLink(dataset.url)}`)
|
|
1635
1668
|
}
|
|
1636
1669
|
}
|
|
1637
1670
|
|
|
@@ -1656,9 +1689,9 @@ async function uploadDataset() {
|
|
|
1656
1689
|
const { rows, sourceType } = parseDatasetFile(file)
|
|
1657
1690
|
const data = await createDatasetFromRows(project, datasetName, sourceType, rows)
|
|
1658
1691
|
|
|
1659
|
-
|
|
1692
|
+
printLine(`Uploaded dataset ${sanitizeTerminalText(data.dataset.name)} (${sanitizeTerminalText(data.dataset.id)}) with ${data.dataset.rowCount} rows.`)
|
|
1660
1693
|
if (data.dataset.url) {
|
|
1661
|
-
|
|
1694
|
+
printLine(`View dataset: ${formatTerminalLink(data.dataset.url)}`)
|
|
1662
1695
|
}
|
|
1663
1696
|
}
|
|
1664
1697
|
|
|
@@ -1708,7 +1741,7 @@ async function downloadDataset() {
|
|
|
1708
1741
|
const bytes = new Uint8Array(await response.arrayBuffer())
|
|
1709
1742
|
writeFileSync(filename, bytes)
|
|
1710
1743
|
|
|
1711
|
-
|
|
1744
|
+
printLine(`Saved dataset ${sanitizeTerminalText(datasetId)} (${format.toUpperCase()}) to ${sanitizeTerminalText(filename)}`)
|
|
1712
1745
|
}
|
|
1713
1746
|
|
|
1714
1747
|
const MAX_INPUT_FILE_SIZE_BYTES = 50 * 1024 * 1024 // 50 MB
|
|
@@ -1759,7 +1792,7 @@ async function appendJsonlDatasetRowsInChunks(datasetId: string, file: string) {
|
|
|
1759
1792
|
|
|
1760
1793
|
const chunk = nextChunk.value
|
|
1761
1794
|
chunkIndex += 1
|
|
1762
|
-
|
|
1795
|
+
printLine(`Uploading chunk ${chunkIndex} (${chunk.length} rows)...`)
|
|
1763
1796
|
|
|
1764
1797
|
try {
|
|
1765
1798
|
const data = await appendChunk(datasetId, chunk)
|
|
@@ -1779,7 +1812,7 @@ async function appendJsonlDatasetRowsInChunks(datasetId: string, file: string) {
|
|
|
1779
1812
|
throw new Error('Dataset append file must contain at least one row')
|
|
1780
1813
|
}
|
|
1781
1814
|
|
|
1782
|
-
|
|
1815
|
+
printLine(
|
|
1783
1816
|
`Appended ${totalAppended} rows to dataset ${sanitizeTerminalText(lastResult.dataset.name)} (${sanitizeTerminalText(lastResult.dataset.id)}). New row count: ${lastResult.dataset.rowCount}`
|
|
1784
1817
|
)
|
|
1785
1818
|
}
|
|
@@ -1842,7 +1875,7 @@ async function appendDatasetRows() {
|
|
|
1842
1875
|
|
|
1843
1876
|
if (rows.length <= APPEND_CHUNK_SIZE_ROWS) {
|
|
1844
1877
|
const data = await appendChunk(datasetId, rows)
|
|
1845
|
-
|
|
1878
|
+
printLine(
|
|
1846
1879
|
`Appended ${data.appendedCount} rows to dataset ${sanitizeTerminalText(data.dataset.name)} (${sanitizeTerminalText(data.dataset.id)}). New row count: ${data.dataset.rowCount}`
|
|
1847
1880
|
)
|
|
1848
1881
|
return
|
|
@@ -1857,7 +1890,7 @@ async function appendDatasetRows() {
|
|
|
1857
1890
|
const chunk = rows.slice(offset, offset + APPEND_CHUNK_SIZE_ROWS)
|
|
1858
1891
|
const chunkIndex = Math.floor(offset / APPEND_CHUNK_SIZE_ROWS) + 1
|
|
1859
1892
|
|
|
1860
|
-
|
|
1893
|
+
printLine(`Uploading chunk ${chunkIndex}/${totalChunks} (${chunk.length} rows)...`)
|
|
1861
1894
|
try {
|
|
1862
1895
|
const data = await appendChunk(datasetId, chunk)
|
|
1863
1896
|
totalAppended += data.appendedCount
|
|
@@ -1873,7 +1906,7 @@ async function appendDatasetRows() {
|
|
|
1873
1906
|
}
|
|
1874
1907
|
|
|
1875
1908
|
if (lastResult) {
|
|
1876
|
-
|
|
1909
|
+
printLine(
|
|
1877
1910
|
`Appended ${totalAppended} rows to dataset ${sanitizeTerminalText(lastResult.dataset.name)} (${sanitizeTerminalText(lastResult.dataset.id)}). New row count: ${lastResult.dataset.rowCount}`
|
|
1878
1911
|
)
|
|
1879
1912
|
}
|
|
@@ -1934,7 +1967,7 @@ async function editDatasetRows() {
|
|
|
1934
1967
|
updatedCount: number
|
|
1935
1968
|
}>(response, 'Dataset edit rows')
|
|
1936
1969
|
|
|
1937
|
-
|
|
1970
|
+
printLine(
|
|
1938
1971
|
`Updated ${data.updatedCount} rows in dataset ${sanitizeTerminalText(data.dataset.name)} (${sanitizeTerminalText(data.dataset.id)}). Current row count: ${data.dataset.rowCount}`
|
|
1939
1972
|
)
|
|
1940
1973
|
}
|
|
@@ -1973,11 +2006,63 @@ async function deleteDatasetRows() {
|
|
|
1973
2006
|
deletedCount: number
|
|
1974
2007
|
}>(response, 'Dataset delete rows')
|
|
1975
2008
|
|
|
1976
|
-
|
|
2009
|
+
printLine(
|
|
1977
2010
|
`Deleted ${data.deletedCount} rows from dataset ${sanitizeTerminalText(data.dataset.name)} (${sanitizeTerminalText(data.dataset.id)}). New row count: ${data.dataset.rowCount}`
|
|
1978
2011
|
)
|
|
1979
2012
|
}
|
|
1980
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
|
+
|
|
1981
2066
|
async function lockDataset() {
|
|
1982
2067
|
const projectArg = getArg('--project')
|
|
1983
2068
|
const datasetInput = getDatasetReferenceInput()
|
|
@@ -2011,7 +2096,7 @@ async function lockDataset() {
|
|
|
2011
2096
|
}
|
|
2012
2097
|
}>(response, 'Dataset lock')
|
|
2013
2098
|
|
|
2014
|
-
|
|
2099
|
+
printLine(
|
|
2015
2100
|
`Locked dataset ${sanitizeTerminalText(data.dataset.name)} (${sanitizeTerminalText(data.dataset.id)}) at ${sanitizeTerminalText(data.dataset.lockedAt)}. Row count: ${data.dataset.rowCount}`
|
|
2016
2101
|
)
|
|
2017
2102
|
}
|
|
@@ -2048,7 +2133,7 @@ async function cloneDataset() {
|
|
|
2048
2133
|
}
|
|
2049
2134
|
}>(response, 'Dataset clone')
|
|
2050
2135
|
|
|
2051
|
-
|
|
2136
|
+
printLine(
|
|
2052
2137
|
`Cloned dataset ${sanitizeTerminalText(data.dataset.parentDatasetId)} -> ${sanitizeTerminalText(data.dataset.name)} (${sanitizeTerminalText(data.dataset.id)}). Row count: ${data.dataset.rowCount}`
|
|
2053
2138
|
)
|
|
2054
2139
|
}
|
|
@@ -2079,17 +2164,22 @@ async function downloadAnnotations() {
|
|
|
2079
2164
|
const bytes = new Uint8Array(await response.arrayBuffer())
|
|
2080
2165
|
writeFileSync(filename, bytes)
|
|
2081
2166
|
|
|
2082
|
-
|
|
2167
|
+
printLine(`Saved ${format.toUpperCase()} export to ${sanitizeTerminalText(filename)}`)
|
|
2083
2168
|
}
|
|
2084
2169
|
|
|
2085
|
-
async function main() {
|
|
2086
|
-
const parsed = parseGlobalFlags(
|
|
2170
|
+
export async function main(rawArgs = process.argv.slice(2)) {
|
|
2171
|
+
const parsed = parseGlobalFlags(rawArgs)
|
|
2087
2172
|
setGlobalFlags(parsed.flags)
|
|
2088
2173
|
cliArgs = parsed.args
|
|
2089
2174
|
|
|
2090
2175
|
const command = cliArgs[0]
|
|
2091
2176
|
const subcommand = cliArgs[1]
|
|
2092
2177
|
|
|
2178
|
+
if (command === '--version' || command === '-v') {
|
|
2179
|
+
printVersion()
|
|
2180
|
+
return
|
|
2181
|
+
}
|
|
2182
|
+
|
|
2093
2183
|
if (!command) {
|
|
2094
2184
|
printUsage()
|
|
2095
2185
|
process.exit(1)
|
|
@@ -2228,6 +2318,11 @@ async function main() {
|
|
|
2228
2318
|
return
|
|
2229
2319
|
}
|
|
2230
2320
|
|
|
2321
|
+
if (command === 'datasets' && subcommand === 'delete') {
|
|
2322
|
+
await deleteDataset()
|
|
2323
|
+
return
|
|
2324
|
+
}
|
|
2325
|
+
|
|
2231
2326
|
if (command === 'datasets' && subcommand === 'lock') {
|
|
2232
2327
|
await lockDataset()
|
|
2233
2328
|
return
|
|
@@ -2247,7 +2342,9 @@ async function main() {
|
|
|
2247
2342
|
process.exit(1)
|
|
2248
2343
|
}
|
|
2249
2344
|
|
|
2250
|
-
|
|
2251
|
-
|
|
2252
|
-
|
|
2253
|
-
|
|
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
|
+
}
|