orizu 0.1.6 → 0.2.1

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/dist/index.js CHANGED
@@ -1,8 +1,9 @@
1
1
  #!/usr/bin/env node
2
2
  import { createHash, randomBytes } from 'crypto';
3
3
  import { basename, extname } from 'path';
4
+ import { fileURLToPath } from 'url';
4
5
  import { createServer } from 'http';
5
- import { readFileSync, statSync, writeFileSync } from 'fs';
6
+ import { readFileSync, realpathSync, statSync, writeFileSync } from 'fs';
6
7
  import { spawn } from 'child_process';
7
8
  import { createInterface } from 'readline/promises';
8
9
  import { stdin as input, stdout as output } from 'process';
@@ -13,8 +14,21 @@ import { parseDatasetReference } from './dataset-download.js';
13
14
  import { parseGlobalFlags } from './global-flags.js';
14
15
  import { assertSecureTokenTransport, authedFetch, getBaseUrl, resolveLoginBaseUrl, setGlobalFlags, } from './http.js';
15
16
  import { formatTaskCreateError } from './task-create-error.js';
17
+ function getCliVersion() {
18
+ const packageJson = JSON.parse(readFileSync(new URL('../package.json', import.meta.url), 'utf8'));
19
+ if (typeof packageJson.version !== 'string' || packageJson.version.length === 0) {
20
+ throw new Error('Unable to read orizu CLI version.');
21
+ }
22
+ return packageJson.version;
23
+ }
24
+ function printLine(message = '') {
25
+ output.write(`${message}\n`);
26
+ }
27
+ function printVersion() {
28
+ printLine(`orizu ${getCliVersion()}`);
29
+ }
16
30
  function printUsage() {
17
- 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>]`);
31
+ 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>]`);
18
32
  }
19
33
  let cliArgs = process.argv.slice(2);
20
34
  function getArg(name) {
@@ -24,7 +38,7 @@ function getArg(name) {
24
38
  }
25
39
  return cliArgs[index + 1];
26
40
  }
27
- function normalizeSlugInput(slug) {
41
+ export function normalizeSlugInput(slug) {
28
42
  return slug.trim().toLowerCase();
29
43
  }
30
44
  function isInteractiveTerminal() {
@@ -33,7 +47,7 @@ function isInteractiveTerminal() {
33
47
  function hasArg(name) {
34
48
  return cliArgs.includes(name);
35
49
  }
36
- function expandHomePath(path) {
50
+ export function expandHomePath(path) {
37
51
  if (path.startsWith('~/')) {
38
52
  const home = process.env.HOME || '';
39
53
  return `${home}/${path.slice(2)}`;
@@ -43,13 +57,13 @@ function expandHomePath(path) {
43
57
  function createCodeVerifier() {
44
58
  return randomBytes(32).toString('base64url');
45
59
  }
46
- function createCodeChallenge(verifier) {
60
+ export function createCodeChallenge(verifier) {
47
61
  return createHash('sha256').update(verifier).digest('base64url');
48
62
  }
49
- function sanitizeTerminalText(value) {
63
+ export function sanitizeTerminalText(value) {
50
64
  return String(value).replace(/[\u0000-\u0008\u000B\u000C\u000E-\u001F\u007F-\u009F]/g, '');
51
65
  }
52
- function validateBrowserUrl(url, expectedOrigin) {
66
+ export function validateBrowserUrl(url, expectedOrigin) {
53
67
  let parsed;
54
68
  try {
55
69
  parsed = new URL(url);
@@ -92,7 +106,7 @@ function openInBrowser(url) {
92
106
  stdio: 'ignore',
93
107
  }).unref();
94
108
  }
95
- function formatTerminalLink(url) {
109
+ export function formatTerminalLink(url) {
96
110
  const safeUrl = sanitizeTerminalText(url);
97
111
  try {
98
112
  const parsed = validateBrowserUrl(safeUrl);
@@ -108,7 +122,7 @@ function formatTerminalLink(url) {
108
122
  }
109
123
  return `\u001B]8;;${safeUrl}\u0007${safeUrl}\u001B]8;;\u0007`;
110
124
  }
111
- async function parseJsonResponse(response, context) {
125
+ export async function parseJsonResponse(response, context) {
112
126
  const contentType = response.headers.get('content-type') || '';
113
127
  const rawBody = await response.text();
114
128
  if (!contentType.includes('application/json')) {
@@ -133,9 +147,9 @@ async function promptSelect(title, items, label, options) {
133
147
  if (items.length === 1 && !options?.forcePrompt) {
134
148
  return items[0];
135
149
  }
136
- console.log(`\n${sanitizeTerminalText(title)}`);
150
+ printLine(`\n${sanitizeTerminalText(title)}`);
137
151
  items.forEach((item, index) => {
138
- console.log(` ${index + 1}. ${sanitizeTerminalText(label(item, index))}`);
152
+ printLine(` ${index + 1}. ${sanitizeTerminalText(label(item, index))}`);
139
153
  });
140
154
  const rl = createInterface({ input, output });
141
155
  try {
@@ -145,7 +159,7 @@ async function promptSelect(title, items, label, options) {
145
159
  if (Number.isInteger(chosenIndex) && chosenIndex >= 1 && chosenIndex <= items.length) {
146
160
  return items[chosenIndex - 1];
147
161
  }
148
- console.log('Invalid selection. Enter a valid number from the list.');
162
+ printLine('Invalid selection. Enter a valid number from the list.');
149
163
  }
150
164
  }
151
165
  finally {
@@ -265,11 +279,12 @@ async function selectDatasetInteractively(projectArg) {
265
279
  return {
266
280
  datasetId: dataset.id,
267
281
  project,
282
+ name: dataset.name,
268
283
  };
269
284
  }
270
285
  function printTeams(teams) {
271
286
  if (teams.length === 0) {
272
- console.log('No teams found.');
287
+ printLine('No teams found.');
273
288
  return;
274
289
  }
275
290
  const rows = teams.map(team => ({
@@ -280,15 +295,15 @@ function printTeams(teams) {
280
295
  const slugWidth = Math.max('TEAM SLUG'.length, ...rows.map(row => row.slug.length));
281
296
  const nameWidth = Math.max('TEAM NAME'.length, ...rows.map(row => row.name.length));
282
297
  const roleWidth = Math.max('ROLE'.length, ...rows.map(row => row.role.length));
283
- console.log(`${'TEAM SLUG'.padEnd(slugWidth)} ${'TEAM NAME'.padEnd(nameWidth)} ${'ROLE'.padEnd(roleWidth)}`);
284
- console.log(`${'-'.repeat(slugWidth)} ${'-'.repeat(nameWidth)} ${'-'.repeat(roleWidth)}`);
298
+ printLine(`${'TEAM SLUG'.padEnd(slugWidth)} ${'TEAM NAME'.padEnd(nameWidth)} ${'ROLE'.padEnd(roleWidth)}`);
299
+ printLine(`${'-'.repeat(slugWidth)} ${'-'.repeat(nameWidth)} ${'-'.repeat(roleWidth)}`);
285
300
  rows.forEach(row => {
286
- console.log(`${row.slug.padEnd(slugWidth)} ${row.name.padEnd(nameWidth)} ${row.role.padEnd(roleWidth)}`);
301
+ printLine(`${row.slug.padEnd(slugWidth)} ${row.name.padEnd(nameWidth)} ${row.role.padEnd(roleWidth)}`);
287
302
  });
288
303
  }
289
304
  function printProjects(projects) {
290
305
  if (projects.length === 0) {
291
- console.log('No projects found.');
306
+ printLine('No projects found.');
292
307
  return;
293
308
  }
294
309
  const rows = projects.map(project => ({
@@ -299,15 +314,15 @@ function printProjects(projects) {
299
314
  const projectWidth = Math.max('TEAM/PROJECT'.length, ...rows.map(row => row.project.length));
300
315
  const nameWidth = Math.max('PROJECT NAME'.length, ...rows.map(row => row.name.length));
301
316
  const roleWidth = Math.max('ROLE'.length, ...rows.map(row => row.role.length));
302
- console.log(`${'TEAM/PROJECT'.padEnd(projectWidth)} ${'PROJECT NAME'.padEnd(nameWidth)} ${'ROLE'.padEnd(roleWidth)}`);
303
- console.log(`${'-'.repeat(projectWidth)} ${'-'.repeat(nameWidth)} ${'-'.repeat(roleWidth)}`);
317
+ printLine(`${'TEAM/PROJECT'.padEnd(projectWidth)} ${'PROJECT NAME'.padEnd(nameWidth)} ${'ROLE'.padEnd(roleWidth)}`);
318
+ printLine(`${'-'.repeat(projectWidth)} ${'-'.repeat(nameWidth)} ${'-'.repeat(roleWidth)}`);
304
319
  rows.forEach(row => {
305
- console.log(`${row.project.padEnd(projectWidth)} ${row.name.padEnd(nameWidth)} ${row.role.padEnd(roleWidth)}`);
320
+ printLine(`${row.project.padEnd(projectWidth)} ${row.name.padEnd(nameWidth)} ${row.role.padEnd(roleWidth)}`);
306
321
  });
307
322
  }
308
323
  function printTasks(tasks) {
309
324
  if (tasks.length === 0) {
310
- console.log('No tasks found.');
325
+ printLine('No tasks found.');
311
326
  return;
312
327
  }
313
328
  const rows = tasks.map(task => ({
@@ -321,15 +336,15 @@ function printTasks(tasks) {
321
336
  const idWidth = Math.max('TASK ID'.length, ...rows.map(row => row.id.length));
322
337
  const nameWidth = Math.max('TASK NAME'.length, ...rows.map(row => row.name.length));
323
338
  const statusWidth = Math.max('STATUS'.length, ...rows.map(row => row.status.length));
324
- console.log(`${'TASK ID'.padEnd(idWidth)} ${'TASK NAME'.padEnd(nameWidth)} ${'STATUS'.padEnd(statusWidth)} TEAM/PROJECT`);
325
- console.log(`${'-'.repeat(idWidth)} ${'-'.repeat(nameWidth)} ${'-'.repeat(statusWidth)} ------------`);
339
+ printLine(`${'TASK ID'.padEnd(idWidth)} ${'TASK NAME'.padEnd(nameWidth)} ${'STATUS'.padEnd(statusWidth)} TEAM/PROJECT`);
340
+ printLine(`${'-'.repeat(idWidth)} ${'-'.repeat(nameWidth)} ${'-'.repeat(statusWidth)} ------------`);
326
341
  rows.forEach(row => {
327
- console.log(`${row.id.padEnd(idWidth)} ${row.name.padEnd(nameWidth)} ${row.status.padEnd(statusWidth)} ${row.project}`);
342
+ printLine(`${row.id.padEnd(idWidth)} ${row.name.padEnd(nameWidth)} ${row.status.padEnd(statusWidth)} ${row.project}`);
328
343
  });
329
344
  }
330
345
  function printApps(apps) {
331
346
  if (apps.length === 0) {
332
- console.log('No apps found.');
347
+ printLine('No apps found.');
333
348
  return;
334
349
  }
335
350
  const rows = apps.map(app => ({
@@ -340,15 +355,15 @@ function printApps(apps) {
340
355
  const idWidth = Math.max('APP ID'.length, ...rows.map(row => row.id.length));
341
356
  const nameWidth = Math.max('APP NAME'.length, ...rows.map(row => row.name.length));
342
357
  const versionWidth = Math.max('VERSION'.length, ...rows.map(row => row.version.length));
343
- console.log(`${'APP ID'.padEnd(idWidth)} ${'APP NAME'.padEnd(nameWidth)} ${'VERSION'.padEnd(versionWidth)}`);
344
- console.log(`${'-'.repeat(idWidth)} ${'-'.repeat(nameWidth)} ${'-'.repeat(versionWidth)}`);
358
+ printLine(`${'APP ID'.padEnd(idWidth)} ${'APP NAME'.padEnd(nameWidth)} ${'VERSION'.padEnd(versionWidth)}`);
359
+ printLine(`${'-'.repeat(idWidth)} ${'-'.repeat(nameWidth)} ${'-'.repeat(versionWidth)}`);
345
360
  rows.forEach(row => {
346
- console.log(`${row.id.padEnd(idWidth)} ${row.name.padEnd(nameWidth)} ${row.version.padEnd(versionWidth)}`);
361
+ printLine(`${row.id.padEnd(idWidth)} ${row.name.padEnd(nameWidth)} ${row.version.padEnd(versionWidth)}`);
347
362
  });
348
363
  }
349
364
  function printTeamMembers(members) {
350
365
  if (members.length === 0) {
351
- console.log('No team members found.');
366
+ printLine('No team members found.');
352
367
  return;
353
368
  }
354
369
  const rows = members.map(member => ({
@@ -361,24 +376,24 @@ function printTeamMembers(members) {
361
376
  const userIdWidth = Math.max('USER ID'.length, ...rows.map(row => row.userId.length));
362
377
  const emailWidth = Math.max('EMAIL'.length, ...rows.map(row => row.email.length));
363
378
  const roleWidth = Math.max('ROLE'.length, ...rows.map(row => row.role.length));
364
- console.log(`${'MEMBER ID'.padEnd(idWidth)} ${'USER ID'.padEnd(userIdWidth)} ${'EMAIL'.padEnd(emailWidth)} ${'ROLE'.padEnd(roleWidth)}`);
365
- console.log(`${'-'.repeat(idWidth)} ${'-'.repeat(userIdWidth)} ${'-'.repeat(emailWidth)} ${'-'.repeat(roleWidth)}`);
379
+ printLine(`${'MEMBER ID'.padEnd(idWidth)} ${'USER ID'.padEnd(userIdWidth)} ${'EMAIL'.padEnd(emailWidth)} ${'ROLE'.padEnd(roleWidth)}`);
380
+ printLine(`${'-'.repeat(idWidth)} ${'-'.repeat(userIdWidth)} ${'-'.repeat(emailWidth)} ${'-'.repeat(roleWidth)}`);
366
381
  rows.forEach(row => {
367
- console.log(`${row.id.padEnd(idWidth)} ${row.userId.padEnd(userIdWidth)} ${row.email.padEnd(emailWidth)} ${row.role.padEnd(roleWidth)}`);
382
+ printLine(`${row.id.padEnd(idWidth)} ${row.userId.padEnd(userIdWidth)} ${row.email.padEnd(emailWidth)} ${row.role.padEnd(roleWidth)}`);
368
383
  });
369
384
  }
370
385
  function printTaskStatusSummary(data) {
371
386
  const task = data.task;
372
- console.log(`Task: ${sanitizeTerminalText(task.title)} (${sanitizeTerminalText(task.id)})`);
373
- console.log(`Status: ${sanitizeTerminalText(task.status)}`);
374
- console.log(`Project: ${sanitizeTerminalText(`${task.teamSlug}/${task.projectSlug}`)}`);
375
- console.log(`Progress: ${task.progressPercentage}%`);
376
- console.log(`Counts: completed=${task.counts.completed}, in_progress=${task.counts.inProgress}, pending=${task.counts.pending}, skipped=${task.counts.skipped}`);
377
- console.log(`Required assignments: ${task.totalRequiredAssignments} (${task.datasetRowCount} rows x ${task.requiredAssignmentsPerRow})`);
387
+ printLine(`Task: ${sanitizeTerminalText(task.title)} (${sanitizeTerminalText(task.id)})`);
388
+ printLine(`Status: ${sanitizeTerminalText(task.status)}`);
389
+ printLine(`Project: ${sanitizeTerminalText(`${task.teamSlug}/${task.projectSlug}`)}`);
390
+ printLine(`Progress: ${task.progressPercentage}%`);
391
+ printLine(`Counts: completed=${task.counts.completed}, in_progress=${task.counts.inProgress}, pending=${task.counts.pending}, skipped=${task.counts.skipped}`);
392
+ printLine(`Required assignments: ${task.totalRequiredAssignments} (${task.datasetRowCount} rows x ${task.requiredAssignmentsPerRow})`);
378
393
  if (task.assignees.length > 0) {
379
- console.log('\nAssignees');
394
+ printLine('\nAssignees');
380
395
  task.assignees.forEach(assignee => {
381
- console.log(` ${sanitizeTerminalText(assignee.email)}: total=${assignee.total}, completed=${assignee.completed}, in_progress=${assignee.inProgress}, pending=${assignee.pending}, skipped=${assignee.skipped}`);
396
+ printLine(` ${sanitizeTerminalText(assignee.email)}: total=${assignee.total}, completed=${assignee.completed}, in_progress=${assignee.inProgress}, pending=${assignee.pending}, skipped=${assignee.skipped}`);
382
397
  });
383
398
  }
384
399
  }
@@ -553,7 +568,7 @@ async function login() {
553
568
  }
554
569
  const { authorizeUrl } = await parseJsonResponse(response, 'CLI auth start');
555
570
  const safeAuthorizeUrl = validateBrowserUrl(authorizeUrl, baseUrl).href;
556
- console.log(`Opening browser for login: ${sanitizeTerminalText(safeAuthorizeUrl)}`);
571
+ printLine(`Opening browser for login: ${sanitizeTerminalText(safeAuthorizeUrl)}`);
557
572
  openInBrowser(safeAuthorizeUrl);
558
573
  }
559
574
  catch (error) {
@@ -572,12 +587,14 @@ async function login() {
572
587
  throw new Error(`Failed to exchange auth code: ${text}`);
573
588
  }
574
589
  const loginData = await parseJsonResponse(exchangeResponse, 'CLI auth exchange');
590
+ if (!loginData.apiKey) {
591
+ throw new Error('Server did not return an API key. Upgrade the Orizu server and run `orizu login` again.');
592
+ }
575
593
  saveServerCredentials(baseUrl, {
576
- accessToken: loginData.accessToken,
577
- refreshToken: loginData.refreshToken,
578
- expiresAt: loginData.expiresAt,
594
+ credentialType: 'pat',
595
+ apiKey: loginData.apiKey,
579
596
  });
580
- console.log(`Logged in as ${sanitizeTerminalText(loginData.user.email ?? loginData.user.id)}`);
597
+ printLine(`Logged in as ${sanitizeTerminalText(loginData.user.email ?? loginData.user.id)}`);
581
598
  }
582
599
  async function whoami() {
583
600
  const response = await authedFetch('/api/cli/auth/whoami');
@@ -585,13 +602,13 @@ async function whoami() {
585
602
  throw new Error(`whoami failed: ${await response.text()}`);
586
603
  }
587
604
  const data = await response.json();
588
- console.log(sanitizeTerminalText(data.user.email ?? data.user.id));
605
+ printLine(sanitizeTerminalText(data.user.email ?? data.user.id));
589
606
  }
590
607
  async function logout() {
591
608
  const baseUrl = getBaseUrl();
592
609
  const credentials = getServerCredentials(baseUrl);
593
610
  if (!credentials) {
594
- console.log(`Already logged out for ${sanitizeTerminalText(baseUrl)}.`);
611
+ printLine(`Already logged out for ${sanitizeTerminalText(baseUrl)}.`);
595
612
  return;
596
613
  }
597
614
  let remoteLogoutError = null;
@@ -601,9 +618,11 @@ async function logout() {
601
618
  method: 'POST',
602
619
  headers: {
603
620
  'Content-Type': 'application/json',
604
- Authorization: `Bearer ${credentials.accessToken}`,
621
+ Authorization: `Bearer ${'accessToken' in credentials ? credentials.accessToken : credentials.apiKey}`,
605
622
  },
606
- body: JSON.stringify({ refreshToken: credentials.refreshToken }),
623
+ body: 'refreshToken' in credentials
624
+ ? JSON.stringify({ refreshToken: credentials.refreshToken })
625
+ : undefined,
607
626
  });
608
627
  if (!response.ok) {
609
628
  remoteLogoutError = sanitizeTerminalText(await response.text()).slice(0, 180);
@@ -617,7 +636,7 @@ async function logout() {
617
636
  if (remoteLogoutError) {
618
637
  console.warn(`Warning: remote logout failed: ${remoteLogoutError}`);
619
638
  }
620
- console.log(`Logged out from ${sanitizeTerminalText(baseUrl)}.`);
639
+ printLine(`Logged out from ${sanitizeTerminalText(baseUrl)}.`);
621
640
  }
622
641
  async function listTeams() {
623
642
  printTeams(await fetchTeams());
@@ -652,7 +671,7 @@ async function createTeam() {
652
671
  throw new Error(`Failed to create team: ${await response.text()}`);
653
672
  }
654
673
  const data = await parseJsonResponse(response, 'Team create');
655
- console.log(`Created team: ${sanitizeTerminalText(data.team.name)} (${sanitizeTerminalText(data.team.slug)})`);
674
+ printLine(`Created team: ${sanitizeTerminalText(data.team.name)} (${sanitizeTerminalText(data.team.slug)})`);
656
675
  }
657
676
  async function listProjects() {
658
677
  const teamSlugArg = getArg('--team');
@@ -679,7 +698,7 @@ async function createProject() {
679
698
  throw new Error(`Failed to create project: ${await response.text()}`);
680
699
  }
681
700
  const data = await parseJsonResponse(response, 'Project create');
682
- console.log(`Created project ${sanitizeTerminalText(`${data.project.teamSlug}/${data.project.slug}`)}`);
701
+ printLine(`Created project ${sanitizeTerminalText(`${data.project.teamSlug}/${data.project.slug}`)}`);
683
702
  }
684
703
  async function listTasks() {
685
704
  const project = getArg('--project');
@@ -745,9 +764,9 @@ async function createAppFromFile() {
745
764
  throw new Error(`Failed to create app: ${await response.text()}`);
746
765
  }
747
766
  const data = await parseJsonResponse(response, 'App create');
748
- console.log(`Created app ${sanitizeTerminalText(data.app.name)} (${sanitizeTerminalText(data.app.id)}) v${data.app.versionNum}`);
767
+ printLine(`Created app ${sanitizeTerminalText(data.app.name)} (${sanitizeTerminalText(data.app.id)}) v${data.app.versionNum}`);
749
768
  if (data.warnings?.length) {
750
- console.log(`Warnings: ${sanitizeTerminalText(data.warnings.join('; '))}`);
769
+ printLine(`Warnings: ${sanitizeTerminalText(data.warnings.join('; '))}`);
751
770
  }
752
771
  }
753
772
  async function updateAppFromFile() {
@@ -781,9 +800,9 @@ async function updateAppFromFile() {
781
800
  throw new Error(`Failed to update app: ${await response.text()}`);
782
801
  }
783
802
  const data = await parseJsonResponse(response, 'App update');
784
- console.log(`Updated app ${sanitizeTerminalText(data.app.name)} (${sanitizeTerminalText(data.app.id)}) to v${data.app.versionNum}`);
803
+ printLine(`Updated app ${sanitizeTerminalText(data.app.name)} (${sanitizeTerminalText(data.app.id)}) to v${data.app.versionNum}`);
785
804
  if (data.warnings?.length) {
786
- console.log(`Warnings: ${sanitizeTerminalText(data.warnings.join('; '))}`);
805
+ printLine(`Warnings: ${sanitizeTerminalText(data.warnings.join('; '))}`);
787
806
  }
788
807
  }
789
808
  async function linkAppDataset() {
@@ -815,7 +834,7 @@ async function linkAppDataset() {
815
834
  throw new Error(`Failed to link dataset: ${await response.text()}`);
816
835
  }
817
836
  const data = await parseJsonResponse(response, 'App link dataset');
818
- console.log(`Linked dataset ${sanitizeTerminalText(data.linkedDataset.name)} (${sanitizeTerminalText(data.linkedDataset.id)}) to app ${sanitizeTerminalText(data.app.name)} (${sanitizeTerminalText(data.app.id)}) version ${data.versionNum}`);
837
+ printLine(`Linked dataset ${sanitizeTerminalText(data.linkedDataset.name)} (${sanitizeTerminalText(data.linkedDataset.id)}) to app ${sanitizeTerminalText(data.app.name)} (${sanitizeTerminalText(data.app.id)}) version ${data.versionNum}`);
819
838
  }
820
839
  function parseCommaSeparated(value) {
821
840
  if (!value) {
@@ -862,7 +881,7 @@ async function createTask() {
862
881
  const cliError = await formatTaskCreateError(response);
863
882
  if (hasArg('--json')) {
864
883
  const payload = cliError.structuredPayload ?? { error: cliError.message };
865
- console.log(JSON.stringify({
884
+ printLine(JSON.stringify({
866
885
  ...payload,
867
886
  httpStatus: cliError.httpStatus,
868
887
  }, null, 2));
@@ -871,7 +890,7 @@ async function createTask() {
871
890
  }
872
891
  const data = await parseJsonResponse(response, 'Task create');
873
892
  if (hasArg('--json')) {
874
- console.log(JSON.stringify({
893
+ printLine(JSON.stringify({
875
894
  taskId: data.task.id,
876
895
  datasetId,
877
896
  versionId: data.task.versionId,
@@ -888,7 +907,7 @@ async function createTask() {
888
907
  }
889
908
  const baseUrl = getBaseUrl();
890
909
  const taskUrl = `${baseUrl}/d/${projectSlug}/tasks/${data.task.id}`;
891
- console.log(`Created task ${sanitizeTerminalText(data.task.title)} (${sanitizeTerminalText(data.task.id)}) [${sanitizeTerminalText(data.task.status)}]` +
910
+ printLine(`Created task ${sanitizeTerminalText(data.task.title)} (${sanitizeTerminalText(data.task.id)}) [${sanitizeTerminalText(data.task.status)}]` +
892
911
  `\n Task ID: ${sanitizeTerminalText(data.task.id)}` +
893
912
  `\n Dataset ID: ${sanitizeTerminalText(datasetId)}` +
894
913
  `\n Version: v${data.task.versionNum} (${sanitizeTerminalText(data.task.versionId)})` +
@@ -912,7 +931,7 @@ async function assignTask() {
912
931
  throw new Error(`Failed to assign task: ${await response.text()}`);
913
932
  }
914
933
  const data = await parseJsonResponse(response, 'Task assign');
915
- console.log(`Created ${data.assignmentsCreated} assignments.`);
934
+ printLine(`Created ${data.assignmentsCreated} assignments.`);
916
935
  }
917
936
  async function taskStatus() {
918
937
  const taskId = getArg('--task');
@@ -933,7 +952,7 @@ async function taskStatus() {
933
952
  catch {
934
953
  // keep raw body as error
935
954
  }
936
- console.log(JSON.stringify({
955
+ printLine(JSON.stringify({
937
956
  ...errorPayload,
938
957
  httpStatus: response.status,
939
958
  }, null, 2));
@@ -950,7 +969,7 @@ async function taskStatus() {
950
969
  }
951
970
  const data = await parseJsonResponse(response, 'Task status');
952
971
  if (hasArg('--json')) {
953
- console.log(JSON.stringify(data, null, 2));
972
+ printLine(JSON.stringify(data, null, 2));
954
973
  return;
955
974
  }
956
975
  printTaskStatusSummary(data);
@@ -972,7 +991,7 @@ async function updateTaskStatus(targetStatus) {
972
991
  }
973
992
  const data = await parseJsonResponse(response, 'Task status update');
974
993
  const action = targetStatus === 'paused' ? 'Paused' : 'Unpaused';
975
- console.log(`${action} task ${sanitizeTerminalText(data.task.id)} [${sanitizeTerminalText(data.task.status)}]`);
994
+ printLine(`${action} task ${sanitizeTerminalText(data.task.id)} [${sanitizeTerminalText(data.task.status)}]`);
976
995
  }
977
996
  async function appDetail() {
978
997
  const appId = getArg('--app');
@@ -992,25 +1011,25 @@ async function appDetail() {
992
1011
  throw new Error(`App '${appId}' not found in project '${projectSlug}'`);
993
1012
  }
994
1013
  if (hasArg('--json')) {
995
- console.log(JSON.stringify(detail, null, 2));
1014
+ printLine(JSON.stringify(detail, null, 2));
996
1015
  return;
997
1016
  }
998
- console.log(`App: ${sanitizeTerminalText(detail.name)} (${sanitizeTerminalText(detail.id)})`);
999
- console.log(` Project: ${sanitizeTerminalText(`${detail.teamSlug}/${detail.projectSlug}`)}`);
1017
+ printLine(`App: ${sanitizeTerminalText(detail.name)} (${sanitizeTerminalText(detail.id)})`);
1018
+ printLine(` Project: ${sanitizeTerminalText(`${detail.teamSlug}/${detail.projectSlug}`)}`);
1000
1019
  if (detail.currentVersion) {
1001
- console.log(` Current version: v${detail.currentVersion.versionNum} (${sanitizeTerminalText(detail.currentVersion.versionId)})`);
1002
- console.log(` Input schema: ${detail.currentVersion.inputJsonSchema ? 'defined' : 'none'}`);
1003
- console.log(` Output schema: ${detail.currentVersion.outputJsonSchema ? 'defined' : 'none'}`);
1020
+ printLine(` Current version: v${detail.currentVersion.versionNum} (${sanitizeTerminalText(detail.currentVersion.versionId)})`);
1021
+ printLine(` Input schema: ${detail.currentVersion.inputJsonSchema ? 'defined' : 'none'}`);
1022
+ printLine(` Output schema: ${detail.currentVersion.outputJsonSchema ? 'defined' : 'none'}`);
1004
1023
  }
1005
1024
  else {
1006
- console.log(` Current version: none`);
1025
+ printLine(` Current version: none`);
1007
1026
  }
1008
- console.log(` Compatible datasets: ${detail.compatibleDatasetsCount}/${detail.totalDatasetsCount}`);
1027
+ printLine(` Compatible datasets: ${detail.compatibleDatasetsCount}/${detail.totalDatasetsCount}`);
1009
1028
  if (detail.createdByEmail) {
1010
- console.log(` Created by: ${sanitizeTerminalText(detail.createdByName || detail.createdByEmail)}`);
1029
+ printLine(` Created by: ${sanitizeTerminalText(detail.createdByName || detail.createdByEmail)}`);
1011
1030
  }
1012
- console.log(` Created: ${sanitizeTerminalText(detail.createdAt)}`);
1013
- console.log(` Updated: ${sanitizeTerminalText(detail.updatedAt)}`);
1031
+ printLine(` Created: ${sanitizeTerminalText(detail.createdAt)}`);
1032
+ printLine(` Updated: ${sanitizeTerminalText(detail.updatedAt)}`);
1014
1033
  }
1015
1034
  async function listTeamMembers() {
1016
1035
  const teamSlug = await resolveTeamSlug(getArg('--team'));
@@ -1031,7 +1050,7 @@ async function addTeamMember() {
1031
1050
  throw new Error(`Failed to add team member: ${await response.text()}`);
1032
1051
  }
1033
1052
  const data = await parseJsonResponse(response, 'Team member add');
1034
- console.log(`Added team member ${sanitizeTerminalText(data.member.email)} (${sanitizeTerminalText(data.member.id)})`);
1053
+ printLine(`Added team member ${sanitizeTerminalText(data.member.email)} (${sanitizeTerminalText(data.member.id)})`);
1035
1054
  }
1036
1055
  async function removeTeamMember() {
1037
1056
  const teamSlug = await resolveTeamSlug(getArg('--team'));
@@ -1048,7 +1067,7 @@ async function removeTeamMember() {
1048
1067
  if (!response.ok) {
1049
1068
  throw new Error(`Failed to remove team member: ${await response.text()}`);
1050
1069
  }
1051
- console.log(`Removed team member ${sanitizeTerminalText(member.email)}`);
1070
+ printLine(`Removed team member ${sanitizeTerminalText(member.email)}`);
1052
1071
  }
1053
1072
  async function changeTeamMemberRole() {
1054
1073
  const teamSlugArg = getArg('--team');
@@ -1074,7 +1093,7 @@ async function changeTeamMemberRole() {
1074
1093
  if (!response.ok) {
1075
1094
  throw new Error(`Failed to update member role: ${await response.text()}`);
1076
1095
  }
1077
- console.log(`Updated ${sanitizeTerminalText(member.email)} role to ${sanitizeTerminalText(role)}`);
1096
+ printLine(`Updated ${sanitizeTerminalText(member.email)} role to ${sanitizeTerminalText(role)}`);
1078
1097
  }
1079
1098
  async function createDatasetFromRows(project, name, sourceType, rows) {
1080
1099
  const response = await authedFetch('/api/cli/datasets/upload', {
@@ -1120,7 +1139,7 @@ async function uploadJsonlDatasetInChunks(file, project, datasetName) {
1120
1139
  }
1121
1140
  const chunk = nextChunk.value;
1122
1141
  chunkIndex += 1;
1123
- console.log(`Uploading chunk ${chunkIndex} (${chunk.length} rows)...`);
1142
+ printLine(`Uploading chunk ${chunkIndex} (${chunk.length} rows)...`);
1124
1143
  try {
1125
1144
  if (!dataset) {
1126
1145
  const data = await createDatasetFromRows(project, datasetName, 'jsonl', chunk);
@@ -1149,9 +1168,9 @@ async function uploadJsonlDatasetInChunks(file, project, datasetName) {
1149
1168
  if (!dataset) {
1150
1169
  throw new Error('Dataset file contains no rows');
1151
1170
  }
1152
- console.log(`Uploaded dataset ${sanitizeTerminalText(dataset.name)} (${sanitizeTerminalText(dataset.id)}) with ${dataset.rowCount} rows.`);
1171
+ printLine(`Uploaded dataset ${sanitizeTerminalText(dataset.name)} (${sanitizeTerminalText(dataset.id)}) with ${dataset.rowCount} rows.`);
1153
1172
  if (dataset.url) {
1154
- console.log(`View dataset: ${formatTerminalLink(dataset.url)}`);
1173
+ printLine(`View dataset: ${formatTerminalLink(dataset.url)}`);
1155
1174
  }
1156
1175
  }
1157
1176
  async function uploadDataset() {
@@ -1170,9 +1189,9 @@ async function uploadDataset() {
1170
1189
  }
1171
1190
  const { rows, sourceType } = parseDatasetFile(file);
1172
1191
  const data = await createDatasetFromRows(project, datasetName, sourceType, rows);
1173
- console.log(`Uploaded dataset ${sanitizeTerminalText(data.dataset.name)} (${sanitizeTerminalText(data.dataset.id)}) with ${data.dataset.rowCount} rows.`);
1192
+ printLine(`Uploaded dataset ${sanitizeTerminalText(data.dataset.name)} (${sanitizeTerminalText(data.dataset.id)}) with ${data.dataset.rowCount} rows.`);
1174
1193
  if (data.dataset.url) {
1175
- console.log(`View dataset: ${formatTerminalLink(data.dataset.url)}`);
1194
+ printLine(`View dataset: ${formatTerminalLink(data.dataset.url)}`);
1176
1195
  }
1177
1196
  }
1178
1197
  function getDatasetReferenceInput() {
@@ -1211,7 +1230,7 @@ async function downloadDataset() {
1211
1230
  : `${datasetId}.${format}`;
1212
1231
  const bytes = new Uint8Array(await response.arrayBuffer());
1213
1232
  writeFileSync(filename, bytes);
1214
- console.log(`Saved dataset ${sanitizeTerminalText(datasetId)} (${format.toUpperCase()}) to ${sanitizeTerminalText(filename)}`);
1233
+ printLine(`Saved dataset ${sanitizeTerminalText(datasetId)} (${format.toUpperCase()}) to ${sanitizeTerminalText(filename)}`);
1215
1234
  }
1216
1235
  const MAX_INPUT_FILE_SIZE_BYTES = 50 * 1024 * 1024; // 50 MB
1217
1236
  const APPEND_CHUNK_SIZE_ROWS = 500;
@@ -1247,7 +1266,7 @@ async function appendJsonlDatasetRowsInChunks(datasetId, file) {
1247
1266
  }
1248
1267
  const chunk = nextChunk.value;
1249
1268
  chunkIndex += 1;
1250
- console.log(`Uploading chunk ${chunkIndex} (${chunk.length} rows)...`);
1269
+ printLine(`Uploading chunk ${chunkIndex} (${chunk.length} rows)...`);
1251
1270
  try {
1252
1271
  const data = await appendChunk(datasetId, chunk);
1253
1272
  totalAppended += data.appendedCount;
@@ -1263,7 +1282,7 @@ async function appendJsonlDatasetRowsInChunks(datasetId, file) {
1263
1282
  if (!lastResult) {
1264
1283
  throw new Error('Dataset append file must contain at least one row');
1265
1284
  }
1266
- console.log(`Appended ${totalAppended} rows to dataset ${sanitizeTerminalText(lastResult.dataset.name)} (${sanitizeTerminalText(lastResult.dataset.id)}). New row count: ${lastResult.dataset.rowCount}`);
1285
+ printLine(`Appended ${totalAppended} rows to dataset ${sanitizeTerminalText(lastResult.dataset.name)} (${sanitizeTerminalText(lastResult.dataset.id)}). New row count: ${lastResult.dataset.rowCount}`);
1267
1286
  }
1268
1287
  async function appendDatasetRows() {
1269
1288
  const projectArg = getArg('--project');
@@ -1312,7 +1331,7 @@ async function appendDatasetRows() {
1312
1331
  }
1313
1332
  if (rows.length <= APPEND_CHUNK_SIZE_ROWS) {
1314
1333
  const data = await appendChunk(datasetId, rows);
1315
- console.log(`Appended ${data.appendedCount} rows to dataset ${sanitizeTerminalText(data.dataset.name)} (${sanitizeTerminalText(data.dataset.id)}). New row count: ${data.dataset.rowCount}`);
1334
+ printLine(`Appended ${data.appendedCount} rows to dataset ${sanitizeTerminalText(data.dataset.name)} (${sanitizeTerminalText(data.dataset.id)}). New row count: ${data.dataset.rowCount}`);
1316
1335
  return;
1317
1336
  }
1318
1337
  // Chunked upload for large row counts (ALI-555: track partial progress)
@@ -1322,7 +1341,7 @@ async function appendDatasetRows() {
1322
1341
  for (let offset = 0; offset < rows.length; offset += APPEND_CHUNK_SIZE_ROWS) {
1323
1342
  const chunk = rows.slice(offset, offset + APPEND_CHUNK_SIZE_ROWS);
1324
1343
  const chunkIndex = Math.floor(offset / APPEND_CHUNK_SIZE_ROWS) + 1;
1325
- console.log(`Uploading chunk ${chunkIndex}/${totalChunks} (${chunk.length} rows)...`);
1344
+ printLine(`Uploading chunk ${chunkIndex}/${totalChunks} (${chunk.length} rows)...`);
1326
1345
  try {
1327
1346
  const data = await appendChunk(datasetId, chunk);
1328
1347
  totalAppended += data.appendedCount;
@@ -1336,7 +1355,7 @@ async function appendDatasetRows() {
1336
1355
  }
1337
1356
  }
1338
1357
  if (lastResult) {
1339
- console.log(`Appended ${totalAppended} rows to dataset ${sanitizeTerminalText(lastResult.dataset.name)} (${sanitizeTerminalText(lastResult.dataset.id)}). New row count: ${lastResult.dataset.rowCount}`);
1358
+ printLine(`Appended ${totalAppended} rows to dataset ${sanitizeTerminalText(lastResult.dataset.name)} (${sanitizeTerminalText(lastResult.dataset.id)}). New row count: ${lastResult.dataset.rowCount}`);
1340
1359
  }
1341
1360
  }
1342
1361
  async function editDatasetRows() {
@@ -1382,7 +1401,7 @@ async function editDatasetRows() {
1382
1401
  throw new Error(`Edit rows failed: ${await response.text()}`);
1383
1402
  }
1384
1403
  const data = await parseJsonResponse(response, 'Dataset edit rows');
1385
- console.log(`Updated ${data.updatedCount} rows in dataset ${sanitizeTerminalText(data.dataset.name)} (${sanitizeTerminalText(data.dataset.id)}). Current row count: ${data.dataset.rowCount}`);
1404
+ printLine(`Updated ${data.updatedCount} rows in dataset ${sanitizeTerminalText(data.dataset.name)} (${sanitizeTerminalText(data.dataset.id)}). Current row count: ${data.dataset.rowCount}`);
1386
1405
  }
1387
1406
  async function deleteDatasetRows() {
1388
1407
  const projectArg = getArg('--project');
@@ -1410,7 +1429,45 @@ async function deleteDatasetRows() {
1410
1429
  throw new Error(`Delete rows failed: ${await response.text()}`);
1411
1430
  }
1412
1431
  const data = await parseJsonResponse(response, 'Dataset delete rows');
1413
- console.log(`Deleted ${data.deletedCount} rows from dataset ${sanitizeTerminalText(data.dataset.name)} (${sanitizeTerminalText(data.dataset.id)}). New row count: ${data.dataset.rowCount}`);
1432
+ printLine(`Deleted ${data.deletedCount} rows from dataset ${sanitizeTerminalText(data.dataset.name)} (${sanitizeTerminalText(data.dataset.id)}). New row count: ${data.dataset.rowCount}`);
1433
+ }
1434
+ async function confirmDatasetDeletion(dataset) {
1435
+ if (!isInteractiveTerminal()) {
1436
+ throw new Error('Dataset deletion requires an interactive terminal confirmation. There is no non-interactive delete option.');
1437
+ }
1438
+ const safeName = dataset.name ? ` (${sanitizeTerminalText(dataset.name)})` : '';
1439
+ printLine(`This will permanently delete dataset ${sanitizeTerminalText(dataset.datasetId)}${safeName}.`);
1440
+ printLine('Type the dataset id exactly to confirm.');
1441
+ const rl = createInterface({ input, output });
1442
+ try {
1443
+ const answer = (await rl.question('Dataset id: ')).trim();
1444
+ if (answer !== dataset.datasetId) {
1445
+ throw new Error('Dataset deletion cancelled.');
1446
+ }
1447
+ }
1448
+ finally {
1449
+ rl.close();
1450
+ }
1451
+ }
1452
+ async function deleteDataset() {
1453
+ const projectArg = getArg('--project');
1454
+ const datasetInput = getDatasetReferenceInput();
1455
+ let dataset;
1456
+ if (datasetInput) {
1457
+ dataset = { datasetId: parseDatasetReference(datasetInput).datasetId };
1458
+ }
1459
+ else {
1460
+ dataset = await selectDatasetInteractively(projectArg);
1461
+ }
1462
+ await confirmDatasetDeletion(dataset);
1463
+ const response = await authedFetch(`/api/cli/datasets/${encodeURIComponent(dataset.datasetId)}`, {
1464
+ method: 'DELETE',
1465
+ });
1466
+ if (!response.ok) {
1467
+ throw new Error(`Delete failed: ${await response.text()}`);
1468
+ }
1469
+ const data = await parseJsonResponse(response, 'Dataset delete');
1470
+ printLine(`Deleted dataset ${sanitizeTerminalText(data.dataset.id)}.`);
1414
1471
  }
1415
1472
  async function lockDataset() {
1416
1473
  const projectArg = getArg('--project');
@@ -1433,7 +1490,7 @@ async function lockDataset() {
1433
1490
  throw new Error(`Lock failed: ${await response.text()}`);
1434
1491
  }
1435
1492
  const data = await parseJsonResponse(response, 'Dataset lock');
1436
- console.log(`Locked dataset ${sanitizeTerminalText(data.dataset.name)} (${sanitizeTerminalText(data.dataset.id)}) at ${sanitizeTerminalText(data.dataset.lockedAt)}. Row count: ${data.dataset.rowCount}`);
1493
+ printLine(`Locked dataset ${sanitizeTerminalText(data.dataset.name)} (${sanitizeTerminalText(data.dataset.id)}) at ${sanitizeTerminalText(data.dataset.lockedAt)}. Row count: ${data.dataset.rowCount}`);
1437
1494
  }
1438
1495
  async function cloneDataset() {
1439
1496
  const projectArg = getArg('--project');
@@ -1456,7 +1513,7 @@ async function cloneDataset() {
1456
1513
  throw new Error(`Clone failed: ${await response.text()}`);
1457
1514
  }
1458
1515
  const data = await parseJsonResponse(response, 'Dataset clone');
1459
- console.log(`Cloned dataset ${sanitizeTerminalText(data.dataset.parentDatasetId)} -> ${sanitizeTerminalText(data.dataset.name)} (${sanitizeTerminalText(data.dataset.id)}). Row count: ${data.dataset.rowCount}`);
1516
+ printLine(`Cloned dataset ${sanitizeTerminalText(data.dataset.parentDatasetId)} -> ${sanitizeTerminalText(data.dataset.name)} (${sanitizeTerminalText(data.dataset.id)}). Row count: ${data.dataset.rowCount}`);
1460
1517
  }
1461
1518
  async function downloadAnnotations() {
1462
1519
  let taskId = getArg('--task');
@@ -1478,14 +1535,18 @@ async function downloadAnnotations() {
1478
1535
  : fallbackName;
1479
1536
  const bytes = new Uint8Array(await response.arrayBuffer());
1480
1537
  writeFileSync(filename, bytes);
1481
- console.log(`Saved ${format.toUpperCase()} export to ${sanitizeTerminalText(filename)}`);
1538
+ printLine(`Saved ${format.toUpperCase()} export to ${sanitizeTerminalText(filename)}`);
1482
1539
  }
1483
- async function main() {
1484
- const parsed = parseGlobalFlags(process.argv.slice(2));
1540
+ export async function main(rawArgs = process.argv.slice(2)) {
1541
+ const parsed = parseGlobalFlags(rawArgs);
1485
1542
  setGlobalFlags(parsed.flags);
1486
1543
  cliArgs = parsed.args;
1487
1544
  const command = cliArgs[0];
1488
1545
  const subcommand = cliArgs[1];
1546
+ if (command === '--version' || command === '-v') {
1547
+ printVersion();
1548
+ return;
1549
+ }
1489
1550
  if (!command) {
1490
1551
  printUsage();
1491
1552
  process.exit(1);
@@ -1599,6 +1660,10 @@ async function main() {
1599
1660
  await deleteDatasetRows();
1600
1661
  return;
1601
1662
  }
1663
+ if (command === 'datasets' && subcommand === 'delete') {
1664
+ await deleteDataset();
1665
+ return;
1666
+ }
1602
1667
  if (command === 'datasets' && subcommand === 'lock') {
1603
1668
  await lockDataset();
1604
1669
  return;
@@ -1614,7 +1679,21 @@ async function main() {
1614
1679
  printUsage();
1615
1680
  process.exit(1);
1616
1681
  }
1617
- main().catch(error => {
1618
- console.error(sanitizeTerminalText(error instanceof Error ? error.message : 'Unknown error'));
1619
- process.exit(1);
1620
- });
1682
+ function isCliEntrypoint() {
1683
+ const entry = process.argv[1];
1684
+ if (!entry) {
1685
+ return false;
1686
+ }
1687
+ try {
1688
+ return realpathSync(entry) === realpathSync(fileURLToPath(import.meta.url));
1689
+ }
1690
+ catch {
1691
+ return false;
1692
+ }
1693
+ }
1694
+ if (isCliEntrypoint()) {
1695
+ main().catch(error => {
1696
+ console.error(sanitizeTerminalText(error instanceof Error ? error.message : 'Unknown error'));
1697
+ process.exit(1);
1698
+ });
1699
+ }