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/dist/index.js
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';
|
|
@@ -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
|
-
|
|
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
|
-
|
|
150
|
+
printLine(`\n${sanitizeTerminalText(title)}`);
|
|
137
151
|
items.forEach((item, index) => {
|
|
138
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
284
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
303
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
325
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
344
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
365
|
-
|
|
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
|
-
|
|
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
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
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
|
-
|
|
394
|
+
printLine('\nAssignees');
|
|
380
395
|
task.assignees.forEach(assignee => {
|
|
381
|
-
|
|
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
|
-
|
|
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
|
-
|
|
577
|
-
|
|
578
|
-
expiresAt: loginData.expiresAt,
|
|
594
|
+
credentialType: 'pat',
|
|
595
|
+
apiKey: loginData.apiKey,
|
|
579
596
|
});
|
|
580
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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:
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
767
|
+
printLine(`Created app ${sanitizeTerminalText(data.app.name)} (${sanitizeTerminalText(data.app.id)}) v${data.app.versionNum}`);
|
|
749
768
|
if (data.warnings?.length) {
|
|
750
|
-
|
|
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
|
-
|
|
803
|
+
printLine(`Updated app ${sanitizeTerminalText(data.app.name)} (${sanitizeTerminalText(data.app.id)}) to v${data.app.versionNum}`);
|
|
785
804
|
if (data.warnings?.length) {
|
|
786
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1014
|
+
printLine(JSON.stringify(detail, null, 2));
|
|
996
1015
|
return;
|
|
997
1016
|
}
|
|
998
|
-
|
|
999
|
-
|
|
1017
|
+
printLine(`App: ${sanitizeTerminalText(detail.name)} (${sanitizeTerminalText(detail.id)})`);
|
|
1018
|
+
printLine(` Project: ${sanitizeTerminalText(`${detail.teamSlug}/${detail.projectSlug}`)}`);
|
|
1000
1019
|
if (detail.currentVersion) {
|
|
1001
|
-
|
|
1002
|
-
|
|
1003
|
-
|
|
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
|
-
|
|
1025
|
+
printLine(` Current version: none`);
|
|
1007
1026
|
}
|
|
1008
|
-
|
|
1027
|
+
printLine(` Compatible datasets: ${detail.compatibleDatasetsCount}/${detail.totalDatasetsCount}`);
|
|
1009
1028
|
if (detail.createdByEmail) {
|
|
1010
|
-
|
|
1029
|
+
printLine(` Created by: ${sanitizeTerminalText(detail.createdByName || detail.createdByEmail)}`);
|
|
1011
1030
|
}
|
|
1012
|
-
|
|
1013
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1171
|
+
printLine(`Uploaded dataset ${sanitizeTerminalText(dataset.name)} (${sanitizeTerminalText(dataset.id)}) with ${dataset.rowCount} rows.`);
|
|
1153
1172
|
if (dataset.url) {
|
|
1154
|
-
|
|
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
|
-
|
|
1192
|
+
printLine(`Uploaded dataset ${sanitizeTerminalText(data.dataset.name)} (${sanitizeTerminalText(data.dataset.id)}) with ${data.dataset.rowCount} rows.`);
|
|
1174
1193
|
if (data.dataset.url) {
|
|
1175
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1538
|
+
printLine(`Saved ${format.toUpperCase()} export to ${sanitizeTerminalText(filename)}`);
|
|
1482
1539
|
}
|
|
1483
|
-
async function main() {
|
|
1484
|
-
const parsed = parseGlobalFlags(
|
|
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,9 @@ async function main() {
|
|
|
1614
1679
|
printUsage();
|
|
1615
1680
|
process.exit(1);
|
|
1616
1681
|
}
|
|
1617
|
-
|
|
1618
|
-
|
|
1619
|
-
|
|
1620
|
-
|
|
1682
|
+
if (process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href) {
|
|
1683
|
+
main().catch(error => {
|
|
1684
|
+
console.error(sanitizeTerminalText(error instanceof Error ? error.message : 'Unknown error'));
|
|
1685
|
+
process.exit(1);
|
|
1686
|
+
});
|
|
1687
|
+
}
|