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/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
- console.log(`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\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 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>]`)
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
- console.log(`\n${sanitizeTerminalText(title)}`)
311
+ printLine(`\n${sanitizeTerminalText(title)}`)
285
312
  items.forEach((item, index) => {
286
- console.log(` ${index + 1}. ${sanitizeTerminalText(label(item, index))}`)
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
- console.log('Invalid selection. Enter a valid number from the list.')
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<{ datasetId: string; project: string }> {
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
- console.log('No teams found.')
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
- console.log(
546
+ printLine(
519
547
  `${'TEAM SLUG'.padEnd(slugWidth)} ${'TEAM NAME'.padEnd(nameWidth)} ${'ROLE'.padEnd(roleWidth)}`
520
548
  )
521
- console.log(
549
+ printLine(
522
550
  `${'-'.repeat(slugWidth)} ${'-'.repeat(nameWidth)} ${'-'.repeat(roleWidth)}`
523
551
  )
524
552
 
525
553
  rows.forEach(row => {
526
- console.log(`${row.slug.padEnd(slugWidth)} ${row.name.padEnd(nameWidth)} ${row.role.padEnd(roleWidth)}`)
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
- console.log('No projects found.')
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
- console.log(
574
+ printLine(
547
575
  `${'TEAM/PROJECT'.padEnd(projectWidth)} ${'PROJECT NAME'.padEnd(nameWidth)} ${'ROLE'.padEnd(roleWidth)}`
548
576
  )
549
- console.log(
577
+ printLine(
550
578
  `${'-'.repeat(projectWidth)} ${'-'.repeat(nameWidth)} ${'-'.repeat(roleWidth)}`
551
579
  )
552
580
 
553
581
  rows.forEach(row => {
554
- console.log(
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
- console.log('No tasks found.')
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
- console.log(
607
+ printLine(
580
608
  `${'TASK ID'.padEnd(idWidth)} ${'TASK NAME'.padEnd(nameWidth)} ${'STATUS'.padEnd(statusWidth)} TEAM/PROJECT`
581
609
  )
582
- console.log(
610
+ printLine(
583
611
  `${'-'.repeat(idWidth)} ${'-'.repeat(nameWidth)} ${'-'.repeat(statusWidth)} ------------`
584
612
  )
585
613
 
586
614
  rows.forEach(row => {
587
- console.log(
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
- console.log('No apps found.')
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
- console.log(`${'APP ID'.padEnd(idWidth)} ${'APP NAME'.padEnd(nameWidth)} ${'VERSION'.padEnd(versionWidth)}`)
610
- console.log(`${'-'.repeat(idWidth)} ${'-'.repeat(nameWidth)} ${'-'.repeat(versionWidth)}`)
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
- console.log(`${row.id.padEnd(idWidth)} ${row.name.padEnd(nameWidth)} ${row.version.padEnd(versionWidth)}`)
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
- console.log('No team members found.')
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
- console.log(
663
+ printLine(
636
664
  `${'MEMBER ID'.padEnd(idWidth)} ${'USER ID'.padEnd(userIdWidth)} ${'EMAIL'.padEnd(emailWidth)} ${'ROLE'.padEnd(roleWidth)}`
637
665
  )
638
- console.log(
666
+ printLine(
639
667
  `${'-'.repeat(idWidth)} ${'-'.repeat(userIdWidth)} ${'-'.repeat(emailWidth)} ${'-'.repeat(roleWidth)}`
640
668
  )
641
669
  rows.forEach(row => {
642
- console.log(
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
- console.log(`Task: ${sanitizeTerminalText(task.title)} (${sanitizeTerminalText(task.id)})`)
651
- console.log(`Status: ${sanitizeTerminalText(task.status)}`)
652
- console.log(`Project: ${sanitizeTerminalText(`${task.teamSlug}/${task.projectSlug}`)}`)
653
- console.log(`Progress: ${task.progressPercentage}%`)
654
- console.log(`Counts: completed=${task.counts.completed}, in_progress=${task.counts.inProgress}, pending=${task.counts.pending}, skipped=${task.counts.skipped}`)
655
- console.log(`Required assignments: ${task.totalRequiredAssignments} (${task.datasetRowCount} rows x ${task.requiredAssignmentsPerRow})`)
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
- console.log('\nAssignees')
686
+ printLine('\nAssignees')
659
687
  task.assignees.forEach(assignee => {
660
- console.log(
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
- console.log(`Opening browser for login: ${sanitizeTerminalText(safeAuthorizeUrl)}`)
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
- accessToken: loginData.accessToken,
880
- refreshToken: loginData.refreshToken,
881
- expiresAt: loginData.expiresAt,
911
+ credentialType: 'pat',
912
+ apiKey: loginData.apiKey,
882
913
  })
883
914
 
884
- console.log(`Logged in as ${sanitizeTerminalText(loginData.user.email ?? loginData.user.id)}`)
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
- console.log(sanitizeTerminalText(data.user.email ?? data.user.id))
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
- console.log(`Already logged out for ${sanitizeTerminalText(baseUrl)}.`)
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: JSON.stringify({ refreshToken: credentials.refreshToken }),
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
- console.log(`Logged out from ${sanitizeTerminalText(baseUrl)}.`)
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
- console.log(`Created team: ${sanitizeTerminalText(data.team.name)} (${sanitizeTerminalText(data.team.slug)})`)
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
- console.log(`Created project ${sanitizeTerminalText(`${data.project.teamSlug}/${data.project.slug}`)}`)
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
- console.log(`Created app ${sanitizeTerminalText(data.app.name)} (${sanitizeTerminalText(data.app.id)}) v${data.app.versionNum}`)
1129
+ printLine(`Created app ${sanitizeTerminalText(data.app.name)} (${sanitizeTerminalText(data.app.id)}) v${data.app.versionNum}`)
1097
1130
  if (data.warnings?.length) {
1098
- console.log(`Warnings: ${sanitizeTerminalText(data.warnings.join('; '))}`)
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
- console.log(`Updated app ${sanitizeTerminalText(data.app.name)} (${sanitizeTerminalText(data.app.id)}) to v${data.app.versionNum}`)
1174
+ printLine(`Updated app ${sanitizeTerminalText(data.app.name)} (${sanitizeTerminalText(data.app.id)}) to v${data.app.versionNum}`)
1142
1175
  if (data.warnings?.length) {
1143
- console.log(`Warnings: ${sanitizeTerminalText(data.warnings.join('; '))}`)
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
- console.log(
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
- console.log(JSON.stringify({
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
- console.log(JSON.stringify({
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
- console.log(
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
- console.log(`Created ${data.assignmentsCreated} assignments.`)
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
- console.log(JSON.stringify({
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
- console.log(JSON.stringify(data, null, 2))
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
- console.log(`${action} task ${sanitizeTerminalText(data.task.id)} [${sanitizeTerminalText(data.task.status)}]`)
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
- console.log(JSON.stringify(detail, null, 2))
1466
+ printLine(JSON.stringify(detail, null, 2))
1434
1467
  return
1435
1468
  }
1436
1469
 
1437
- console.log(`App: ${sanitizeTerminalText(detail.name)} (${sanitizeTerminalText(detail.id)})`)
1438
- console.log(` Project: ${sanitizeTerminalText(`${detail.teamSlug}/${detail.projectSlug}`)}`)
1470
+ printLine(`App: ${sanitizeTerminalText(detail.name)} (${sanitizeTerminalText(detail.id)})`)
1471
+ printLine(` Project: ${sanitizeTerminalText(`${detail.teamSlug}/${detail.projectSlug}`)}`)
1439
1472
  if (detail.currentVersion) {
1440
- console.log(` Current version: v${detail.currentVersion.versionNum} (${sanitizeTerminalText(detail.currentVersion.versionId)})`)
1441
- console.log(` Input schema: ${detail.currentVersion.inputJsonSchema ? 'defined' : 'none'}`)
1442
- console.log(` Output schema: ${detail.currentVersion.outputJsonSchema ? 'defined' : 'none'}`)
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
- console.log(` Current version: none`)
1477
+ printLine(` Current version: none`)
1445
1478
  }
1446
- console.log(` Compatible datasets: ${detail.compatibleDatasetsCount}/${detail.totalDatasetsCount}`)
1479
+ printLine(` Compatible datasets: ${detail.compatibleDatasetsCount}/${detail.totalDatasetsCount}`)
1447
1480
  if (detail.createdByEmail) {
1448
- console.log(` Created by: ${sanitizeTerminalText(detail.createdByName || detail.createdByEmail)}`)
1481
+ printLine(` Created by: ${sanitizeTerminalText(detail.createdByName || detail.createdByEmail)}`)
1449
1482
  }
1450
- console.log(` Created: ${sanitizeTerminalText(detail.createdAt)}`)
1451
- console.log(` Updated: ${sanitizeTerminalText(detail.updatedAt)}`)
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
- console.log(`Added team member ${sanitizeTerminalText(data.member.email)} (${sanitizeTerminalText(data.member.id)})`)
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
- console.log(`Removed team member ${sanitizeTerminalText(member.email)}`)
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
- console.log(`Updated ${sanitizeTerminalText(member.email)} role to ${sanitizeTerminalText(role)}`)
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
- console.log(`Uploading chunk ${chunkIndex} (${chunk.length} rows)...`)
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
- console.log(`Uploaded dataset ${sanitizeTerminalText(dataset.name)} (${sanitizeTerminalText(dataset.id)}) with ${dataset.rowCount} rows.`)
1665
+ printLine(`Uploaded dataset ${sanitizeTerminalText(dataset.name)} (${sanitizeTerminalText(dataset.id)}) with ${dataset.rowCount} rows.`)
1633
1666
  if (dataset.url) {
1634
- console.log(`View dataset: ${formatTerminalLink(dataset.url)}`)
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
- console.log(`Uploaded dataset ${sanitizeTerminalText(data.dataset.name)} (${sanitizeTerminalText(data.dataset.id)}) with ${data.dataset.rowCount} rows.`)
1692
+ printLine(`Uploaded dataset ${sanitizeTerminalText(data.dataset.name)} (${sanitizeTerminalText(data.dataset.id)}) with ${data.dataset.rowCount} rows.`)
1660
1693
  if (data.dataset.url) {
1661
- console.log(`View dataset: ${formatTerminalLink(data.dataset.url)}`)
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
- console.log(`Saved dataset ${sanitizeTerminalText(datasetId)} (${format.toUpperCase()}) to ${sanitizeTerminalText(filename)}`)
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
- console.log(`Uploading chunk ${chunkIndex} (${chunk.length} rows)...`)
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
- console.log(
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
- console.log(
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
- console.log(`Uploading chunk ${chunkIndex}/${totalChunks} (${chunk.length} rows)...`)
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
- console.log(
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
- console.log(
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
- console.log(
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
- console.log(
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
- console.log(
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
- console.log(`Saved ${format.toUpperCase()} export to ${sanitizeTerminalText(filename)}`)
2167
+ printLine(`Saved ${format.toUpperCase()} export to ${sanitizeTerminalText(filename)}`)
2083
2168
  }
2084
2169
 
2085
- async function main() {
2086
- const parsed = parseGlobalFlags(process.argv.slice(2))
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
- main().catch(error => {
2251
- console.error(sanitizeTerminalText(error instanceof Error ? error.message : 'Unknown error'))
2252
- process.exit(1)
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
+ }