orizu 0.1.5 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +5 -0
- package/dist/credentials.js +55 -6
- package/dist/http.js +41 -4
- package/dist/index.js +253 -136
- package/package.json +1 -1
- package/src/credentials.ts +82 -11
- package/src/http.ts +54 -5
- package/src/index.ts +305 -147
- package/src/types.ts +19 -4
package/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';
|
|
@@ -11,10 +12,23 @@ import { parseDatasetFile } from './file-parser.js';
|
|
|
11
12
|
import { streamJsonlRowChunks } from './jsonl-stream.js';
|
|
12
13
|
import { parseDatasetReference } from './dataset-download.js';
|
|
13
14
|
import { parseGlobalFlags } from './global-flags.js';
|
|
14
|
-
import { authedFetch, getBaseUrl, resolveLoginBaseUrl, setGlobalFlags } from './http.js';
|
|
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,50 +57,84 @@ 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
|
}
|
|
63
|
+
export function sanitizeTerminalText(value) {
|
|
64
|
+
return String(value).replace(/[\u0000-\u0008\u000B\u000C\u000E-\u001F\u007F-\u009F]/g, '');
|
|
65
|
+
}
|
|
66
|
+
export function validateBrowserUrl(url, expectedOrigin) {
|
|
67
|
+
let parsed;
|
|
68
|
+
try {
|
|
69
|
+
parsed = new URL(url);
|
|
70
|
+
}
|
|
71
|
+
catch {
|
|
72
|
+
throw new Error('Server returned an invalid browser URL.');
|
|
73
|
+
}
|
|
74
|
+
if (parsed.protocol !== 'https:' && parsed.protocol !== 'http:') {
|
|
75
|
+
throw new Error('Server returned an unsupported browser URL scheme.');
|
|
76
|
+
}
|
|
77
|
+
if (parsed.username || parsed.password) {
|
|
78
|
+
throw new Error('Server returned a browser URL containing credentials.');
|
|
79
|
+
}
|
|
80
|
+
if (expectedOrigin && parsed.origin !== expectedOrigin) {
|
|
81
|
+
throw new Error('Server returned a browser URL for an unexpected origin.');
|
|
82
|
+
}
|
|
83
|
+
return parsed;
|
|
84
|
+
}
|
|
49
85
|
function openInBrowser(url) {
|
|
86
|
+
const parsed = validateBrowserUrl(url);
|
|
50
87
|
const platform = process.platform;
|
|
88
|
+
const href = parsed.href;
|
|
51
89
|
if (platform === 'darwin') {
|
|
52
|
-
spawn('open', [
|
|
90
|
+
spawn('open', [href], {
|
|
53
91
|
detached: true,
|
|
54
92
|
stdio: 'ignore',
|
|
55
93
|
}).unref();
|
|
56
94
|
return;
|
|
57
95
|
}
|
|
58
96
|
if (platform === 'win32') {
|
|
59
|
-
spawn('
|
|
97
|
+
spawn('rundll32.exe', ['url.dll,FileProtocolHandler', href], {
|
|
60
98
|
detached: true,
|
|
61
99
|
stdio: 'ignore',
|
|
62
100
|
windowsHide: true,
|
|
63
101
|
}).unref();
|
|
64
102
|
return;
|
|
65
103
|
}
|
|
66
|
-
spawn('xdg-open', [
|
|
104
|
+
spawn('xdg-open', [href], {
|
|
67
105
|
detached: true,
|
|
68
106
|
stdio: 'ignore',
|
|
69
107
|
}).unref();
|
|
70
108
|
}
|
|
71
|
-
function formatTerminalLink(url) {
|
|
109
|
+
export function formatTerminalLink(url) {
|
|
110
|
+
const safeUrl = sanitizeTerminalText(url);
|
|
111
|
+
try {
|
|
112
|
+
const parsed = validateBrowserUrl(safeUrl);
|
|
113
|
+
if (parsed.protocol !== 'https:' && parsed.protocol !== 'http:') {
|
|
114
|
+
return safeUrl;
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
catch {
|
|
118
|
+
return safeUrl;
|
|
119
|
+
}
|
|
72
120
|
if (!isInteractiveTerminal()) {
|
|
73
|
-
return
|
|
121
|
+
return safeUrl;
|
|
74
122
|
}
|
|
75
|
-
return `\u001B]8;;${
|
|
123
|
+
return `\u001B]8;;${safeUrl}\u0007${safeUrl}\u001B]8;;\u0007`;
|
|
76
124
|
}
|
|
77
|
-
async function parseJsonResponse(response, context) {
|
|
125
|
+
export async function parseJsonResponse(response, context) {
|
|
78
126
|
const contentType = response.headers.get('content-type') || '';
|
|
79
127
|
const rawBody = await response.text();
|
|
80
128
|
if (!contentType.includes('application/json')) {
|
|
81
129
|
throw new Error(`${context} returned non-JSON response (status ${response.status}). ` +
|
|
82
|
-
`Body preview: ${rawBody.slice(0, 180)}`);
|
|
130
|
+
`Body preview: ${sanitizeTerminalText(rawBody.slice(0, 180))}`);
|
|
83
131
|
}
|
|
84
132
|
try {
|
|
85
133
|
return JSON.parse(rawBody);
|
|
86
134
|
}
|
|
87
135
|
catch {
|
|
88
136
|
throw new Error(`${context} returned invalid JSON (status ${response.status}). ` +
|
|
89
|
-
`Body preview: ${rawBody.slice(0, 180)}`);
|
|
137
|
+
`Body preview: ${sanitizeTerminalText(rawBody.slice(0, 180))}`);
|
|
90
138
|
}
|
|
91
139
|
}
|
|
92
140
|
async function promptSelect(title, items, label, options) {
|
|
@@ -99,9 +147,9 @@ async function promptSelect(title, items, label, options) {
|
|
|
99
147
|
if (items.length === 1 && !options?.forcePrompt) {
|
|
100
148
|
return items[0];
|
|
101
149
|
}
|
|
102
|
-
|
|
150
|
+
printLine(`\n${sanitizeTerminalText(title)}`);
|
|
103
151
|
items.forEach((item, index) => {
|
|
104
|
-
|
|
152
|
+
printLine(` ${index + 1}. ${sanitizeTerminalText(label(item, index))}`);
|
|
105
153
|
});
|
|
106
154
|
const rl = createInterface({ input, output });
|
|
107
155
|
try {
|
|
@@ -111,7 +159,7 @@ async function promptSelect(title, items, label, options) {
|
|
|
111
159
|
if (Number.isInteger(chosenIndex) && chosenIndex >= 1 && chosenIndex <= items.length) {
|
|
112
160
|
return items[chosenIndex - 1];
|
|
113
161
|
}
|
|
114
|
-
|
|
162
|
+
printLine('Invalid selection. Enter a valid number from the list.');
|
|
115
163
|
}
|
|
116
164
|
}
|
|
117
165
|
finally {
|
|
@@ -187,7 +235,7 @@ async function resolveProjectSlug(projectArg) {
|
|
|
187
235
|
const [teamSlug, projectSlug] = segments.map(normalizeSlugInput);
|
|
188
236
|
const matchedTeam = teams.find(team => team.slug === teamSlug);
|
|
189
237
|
if (!matchedTeam) {
|
|
190
|
-
console.error(`Team '${teamSlug}' not found in your accessible teams.`);
|
|
238
|
+
console.error(`Team '${sanitizeTerminalText(teamSlug)}' not found in your accessible teams.`);
|
|
191
239
|
const selectedTeam = await promptSelect('Select a team', teams, team => `${team.name} (${team.slug})`);
|
|
192
240
|
const projects = await fetchProjects(selectedTeam.slug);
|
|
193
241
|
const selectedProject = await promptSelect(`Select a project in ${selectedTeam.slug}`, projects, project => `${project.name} (${project.teamSlug}/${project.slug})`);
|
|
@@ -196,7 +244,7 @@ async function resolveProjectSlug(projectArg) {
|
|
|
196
244
|
const projects = await fetchProjects(matchedTeam.slug);
|
|
197
245
|
const matchedProject = projects.find(project => project.slug === projectSlug);
|
|
198
246
|
if (!matchedProject) {
|
|
199
|
-
console.error(`Project '${projectSlug}' not found in team '${matchedTeam.slug}'.`);
|
|
247
|
+
console.error(`Project '${sanitizeTerminalText(projectSlug)}' not found in team '${sanitizeTerminalText(matchedTeam.slug)}'.`);
|
|
200
248
|
const selectedProject = await promptSelect(`Select a project in ${matchedTeam.slug}`, projects, project => `${project.name} (${project.teamSlug}/${project.slug})`);
|
|
201
249
|
return `${selectedProject.teamSlug}/${selectedProject.slug}`;
|
|
202
250
|
}
|
|
@@ -231,120 +279,121 @@ async function selectDatasetInteractively(projectArg) {
|
|
|
231
279
|
return {
|
|
232
280
|
datasetId: dataset.id,
|
|
233
281
|
project,
|
|
282
|
+
name: dataset.name,
|
|
234
283
|
};
|
|
235
284
|
}
|
|
236
285
|
function printTeams(teams) {
|
|
237
286
|
if (teams.length === 0) {
|
|
238
|
-
|
|
287
|
+
printLine('No teams found.');
|
|
239
288
|
return;
|
|
240
289
|
}
|
|
241
290
|
const rows = teams.map(team => ({
|
|
242
|
-
slug: team.slug,
|
|
243
|
-
name: team.name || '-',
|
|
244
|
-
role: team.role || '-',
|
|
291
|
+
slug: sanitizeTerminalText(team.slug),
|
|
292
|
+
name: sanitizeTerminalText(team.name || '-'),
|
|
293
|
+
role: sanitizeTerminalText(team.role || '-'),
|
|
245
294
|
}));
|
|
246
295
|
const slugWidth = Math.max('TEAM SLUG'.length, ...rows.map(row => row.slug.length));
|
|
247
296
|
const nameWidth = Math.max('TEAM NAME'.length, ...rows.map(row => row.name.length));
|
|
248
297
|
const roleWidth = Math.max('ROLE'.length, ...rows.map(row => row.role.length));
|
|
249
|
-
|
|
250
|
-
|
|
298
|
+
printLine(`${'TEAM SLUG'.padEnd(slugWidth)} ${'TEAM NAME'.padEnd(nameWidth)} ${'ROLE'.padEnd(roleWidth)}`);
|
|
299
|
+
printLine(`${'-'.repeat(slugWidth)} ${'-'.repeat(nameWidth)} ${'-'.repeat(roleWidth)}`);
|
|
251
300
|
rows.forEach(row => {
|
|
252
|
-
|
|
301
|
+
printLine(`${row.slug.padEnd(slugWidth)} ${row.name.padEnd(nameWidth)} ${row.role.padEnd(roleWidth)}`);
|
|
253
302
|
});
|
|
254
303
|
}
|
|
255
304
|
function printProjects(projects) {
|
|
256
305
|
if (projects.length === 0) {
|
|
257
|
-
|
|
306
|
+
printLine('No projects found.');
|
|
258
307
|
return;
|
|
259
308
|
}
|
|
260
309
|
const rows = projects.map(project => ({
|
|
261
|
-
project: `${project.teamSlug}/${project.slug}
|
|
262
|
-
name: project.name || '-',
|
|
263
|
-
role: project.role || '-',
|
|
310
|
+
project: sanitizeTerminalText(`${project.teamSlug}/${project.slug}`),
|
|
311
|
+
name: sanitizeTerminalText(project.name || '-'),
|
|
312
|
+
role: sanitizeTerminalText(project.role || '-'),
|
|
264
313
|
}));
|
|
265
314
|
const projectWidth = Math.max('TEAM/PROJECT'.length, ...rows.map(row => row.project.length));
|
|
266
315
|
const nameWidth = Math.max('PROJECT NAME'.length, ...rows.map(row => row.name.length));
|
|
267
316
|
const roleWidth = Math.max('ROLE'.length, ...rows.map(row => row.role.length));
|
|
268
|
-
|
|
269
|
-
|
|
317
|
+
printLine(`${'TEAM/PROJECT'.padEnd(projectWidth)} ${'PROJECT NAME'.padEnd(nameWidth)} ${'ROLE'.padEnd(roleWidth)}`);
|
|
318
|
+
printLine(`${'-'.repeat(projectWidth)} ${'-'.repeat(nameWidth)} ${'-'.repeat(roleWidth)}`);
|
|
270
319
|
rows.forEach(row => {
|
|
271
|
-
|
|
320
|
+
printLine(`${row.project.padEnd(projectWidth)} ${row.name.padEnd(nameWidth)} ${row.role.padEnd(roleWidth)}`);
|
|
272
321
|
});
|
|
273
322
|
}
|
|
274
323
|
function printTasks(tasks) {
|
|
275
324
|
if (tasks.length === 0) {
|
|
276
|
-
|
|
325
|
+
printLine('No tasks found.');
|
|
277
326
|
return;
|
|
278
327
|
}
|
|
279
328
|
const rows = tasks.map(task => ({
|
|
280
|
-
id: task.id,
|
|
281
|
-
name: task.title || '-',
|
|
282
|
-
status: task.status || '-',
|
|
329
|
+
id: sanitizeTerminalText(task.id),
|
|
330
|
+
name: sanitizeTerminalText(task.title || '-'),
|
|
331
|
+
status: sanitizeTerminalText(task.status || '-'),
|
|
283
332
|
project: task.teamSlug && task.projectSlug
|
|
284
|
-
? `${task.teamSlug}/${task.projectSlug}`
|
|
333
|
+
? sanitizeTerminalText(`${task.teamSlug}/${task.projectSlug}`)
|
|
285
334
|
: 'unknown-project',
|
|
286
335
|
}));
|
|
287
336
|
const idWidth = Math.max('TASK ID'.length, ...rows.map(row => row.id.length));
|
|
288
337
|
const nameWidth = Math.max('TASK NAME'.length, ...rows.map(row => row.name.length));
|
|
289
338
|
const statusWidth = Math.max('STATUS'.length, ...rows.map(row => row.status.length));
|
|
290
|
-
|
|
291
|
-
|
|
339
|
+
printLine(`${'TASK ID'.padEnd(idWidth)} ${'TASK NAME'.padEnd(nameWidth)} ${'STATUS'.padEnd(statusWidth)} TEAM/PROJECT`);
|
|
340
|
+
printLine(`${'-'.repeat(idWidth)} ${'-'.repeat(nameWidth)} ${'-'.repeat(statusWidth)} ------------`);
|
|
292
341
|
rows.forEach(row => {
|
|
293
|
-
|
|
342
|
+
printLine(`${row.id.padEnd(idWidth)} ${row.name.padEnd(nameWidth)} ${row.status.padEnd(statusWidth)} ${row.project}`);
|
|
294
343
|
});
|
|
295
344
|
}
|
|
296
345
|
function printApps(apps) {
|
|
297
346
|
if (apps.length === 0) {
|
|
298
|
-
|
|
347
|
+
printLine('No apps found.');
|
|
299
348
|
return;
|
|
300
349
|
}
|
|
301
350
|
const rows = apps.map(app => ({
|
|
302
|
-
id: app.id,
|
|
303
|
-
name: app.name || '-',
|
|
351
|
+
id: sanitizeTerminalText(app.id),
|
|
352
|
+
name: sanitizeTerminalText(app.name || '-'),
|
|
304
353
|
version: `v${app.currentVersionNum || 1}`,
|
|
305
354
|
}));
|
|
306
355
|
const idWidth = Math.max('APP ID'.length, ...rows.map(row => row.id.length));
|
|
307
356
|
const nameWidth = Math.max('APP NAME'.length, ...rows.map(row => row.name.length));
|
|
308
357
|
const versionWidth = Math.max('VERSION'.length, ...rows.map(row => row.version.length));
|
|
309
|
-
|
|
310
|
-
|
|
358
|
+
printLine(`${'APP ID'.padEnd(idWidth)} ${'APP NAME'.padEnd(nameWidth)} ${'VERSION'.padEnd(versionWidth)}`);
|
|
359
|
+
printLine(`${'-'.repeat(idWidth)} ${'-'.repeat(nameWidth)} ${'-'.repeat(versionWidth)}`);
|
|
311
360
|
rows.forEach(row => {
|
|
312
|
-
|
|
361
|
+
printLine(`${row.id.padEnd(idWidth)} ${row.name.padEnd(nameWidth)} ${row.version.padEnd(versionWidth)}`);
|
|
313
362
|
});
|
|
314
363
|
}
|
|
315
364
|
function printTeamMembers(members) {
|
|
316
365
|
if (members.length === 0) {
|
|
317
|
-
|
|
366
|
+
printLine('No team members found.');
|
|
318
367
|
return;
|
|
319
368
|
}
|
|
320
369
|
const rows = members.map(member => ({
|
|
321
|
-
id: member.id,
|
|
322
|
-
userId: member.user_id || '-',
|
|
323
|
-
email: member.email || '-',
|
|
324
|
-
role: member.role || '-',
|
|
370
|
+
id: sanitizeTerminalText(member.id),
|
|
371
|
+
userId: sanitizeTerminalText(member.user_id || '-'),
|
|
372
|
+
email: sanitizeTerminalText(member.email || '-'),
|
|
373
|
+
role: sanitizeTerminalText(member.role || '-'),
|
|
325
374
|
}));
|
|
326
375
|
const idWidth = Math.max('MEMBER ID'.length, ...rows.map(row => row.id.length));
|
|
327
376
|
const userIdWidth = Math.max('USER ID'.length, ...rows.map(row => row.userId.length));
|
|
328
377
|
const emailWidth = Math.max('EMAIL'.length, ...rows.map(row => row.email.length));
|
|
329
378
|
const roleWidth = Math.max('ROLE'.length, ...rows.map(row => row.role.length));
|
|
330
|
-
|
|
331
|
-
|
|
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)}`);
|
|
332
381
|
rows.forEach(row => {
|
|
333
|
-
|
|
382
|
+
printLine(`${row.id.padEnd(idWidth)} ${row.userId.padEnd(userIdWidth)} ${row.email.padEnd(emailWidth)} ${row.role.padEnd(roleWidth)}`);
|
|
334
383
|
});
|
|
335
384
|
}
|
|
336
385
|
function printTaskStatusSummary(data) {
|
|
337
386
|
const task = data.task;
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
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})`);
|
|
344
393
|
if (task.assignees.length > 0) {
|
|
345
|
-
|
|
394
|
+
printLine('\nAssignees');
|
|
346
395
|
task.assignees.forEach(assignee => {
|
|
347
|
-
|
|
396
|
+
printLine(` ${sanitizeTerminalText(assignee.email)}: total=${assignee.total}, completed=${assignee.completed}, in_progress=${assignee.inProgress}, pending=${assignee.pending}, skipped=${assignee.skipped}`);
|
|
348
397
|
});
|
|
349
398
|
}
|
|
350
399
|
}
|
|
@@ -469,6 +518,7 @@ function renderCliAuthBrowserPage(status) {
|
|
|
469
518
|
}
|
|
470
519
|
async function login() {
|
|
471
520
|
const baseUrl = resolveLoginBaseUrl();
|
|
521
|
+
assertSecureTokenTransport(baseUrl);
|
|
472
522
|
const codeVerifier = createCodeVerifier();
|
|
473
523
|
const codeChallenge = createCodeChallenge(codeVerifier);
|
|
474
524
|
const callbackPort = resolveAuthCallbackPort();
|
|
@@ -517,8 +567,9 @@ async function login() {
|
|
|
517
567
|
return;
|
|
518
568
|
}
|
|
519
569
|
const { authorizeUrl } = await parseJsonResponse(response, 'CLI auth start');
|
|
520
|
-
|
|
521
|
-
|
|
570
|
+
const safeAuthorizeUrl = validateBrowserUrl(authorizeUrl, baseUrl).href;
|
|
571
|
+
printLine(`Opening browser for login: ${sanitizeTerminalText(safeAuthorizeUrl)}`);
|
|
572
|
+
openInBrowser(safeAuthorizeUrl);
|
|
522
573
|
}
|
|
523
574
|
catch (error) {
|
|
524
575
|
server.close();
|
|
@@ -536,12 +587,14 @@ async function login() {
|
|
|
536
587
|
throw new Error(`Failed to exchange auth code: ${text}`);
|
|
537
588
|
}
|
|
538
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
|
+
}
|
|
539
593
|
saveServerCredentials(baseUrl, {
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
expiresAt: loginData.expiresAt,
|
|
594
|
+
credentialType: 'pat',
|
|
595
|
+
apiKey: loginData.apiKey,
|
|
543
596
|
});
|
|
544
|
-
|
|
597
|
+
printLine(`Logged in as ${sanitizeTerminalText(loginData.user.email ?? loginData.user.id)}`);
|
|
545
598
|
}
|
|
546
599
|
async function whoami() {
|
|
547
600
|
const response = await authedFetch('/api/cli/auth/whoami');
|
|
@@ -549,25 +602,41 @@ async function whoami() {
|
|
|
549
602
|
throw new Error(`whoami failed: ${await response.text()}`);
|
|
550
603
|
}
|
|
551
604
|
const data = await response.json();
|
|
552
|
-
|
|
605
|
+
printLine(sanitizeTerminalText(data.user.email ?? data.user.id));
|
|
553
606
|
}
|
|
554
607
|
async function logout() {
|
|
555
608
|
const baseUrl = getBaseUrl();
|
|
556
609
|
const credentials = getServerCredentials(baseUrl);
|
|
557
610
|
if (!credentials) {
|
|
558
|
-
|
|
611
|
+
printLine(`Already logged out for ${sanitizeTerminalText(baseUrl)}.`);
|
|
559
612
|
return;
|
|
560
613
|
}
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
614
|
+
let remoteLogoutError = null;
|
|
615
|
+
try {
|
|
616
|
+
assertSecureTokenTransport(baseUrl);
|
|
617
|
+
const response = await fetch(`${baseUrl}/api/cli/auth/logout`, {
|
|
618
|
+
method: 'POST',
|
|
619
|
+
headers: {
|
|
620
|
+
'Content-Type': 'application/json',
|
|
621
|
+
Authorization: `Bearer ${'accessToken' in credentials ? credentials.accessToken : credentials.apiKey}`,
|
|
622
|
+
},
|
|
623
|
+
body: 'refreshToken' in credentials
|
|
624
|
+
? JSON.stringify({ refreshToken: credentials.refreshToken })
|
|
625
|
+
: undefined,
|
|
626
|
+
});
|
|
627
|
+
if (!response.ok) {
|
|
628
|
+
remoteLogoutError = sanitizeTerminalText(await response.text()).slice(0, 180);
|
|
629
|
+
}
|
|
630
|
+
}
|
|
631
|
+
catch (error) {
|
|
632
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
633
|
+
remoteLogoutError = sanitizeTerminalText(message).slice(0, 180);
|
|
634
|
+
}
|
|
569
635
|
clearServerCredentials(baseUrl);
|
|
570
|
-
|
|
636
|
+
if (remoteLogoutError) {
|
|
637
|
+
console.warn(`Warning: remote logout failed: ${remoteLogoutError}`);
|
|
638
|
+
}
|
|
639
|
+
printLine(`Logged out from ${sanitizeTerminalText(baseUrl)}.`);
|
|
571
640
|
}
|
|
572
641
|
async function listTeams() {
|
|
573
642
|
printTeams(await fetchTeams());
|
|
@@ -602,7 +671,7 @@ async function createTeam() {
|
|
|
602
671
|
throw new Error(`Failed to create team: ${await response.text()}`);
|
|
603
672
|
}
|
|
604
673
|
const data = await parseJsonResponse(response, 'Team create');
|
|
605
|
-
|
|
674
|
+
printLine(`Created team: ${sanitizeTerminalText(data.team.name)} (${sanitizeTerminalText(data.team.slug)})`);
|
|
606
675
|
}
|
|
607
676
|
async function listProjects() {
|
|
608
677
|
const teamSlugArg = getArg('--team');
|
|
@@ -629,7 +698,7 @@ async function createProject() {
|
|
|
629
698
|
throw new Error(`Failed to create project: ${await response.text()}`);
|
|
630
699
|
}
|
|
631
700
|
const data = await parseJsonResponse(response, 'Project create');
|
|
632
|
-
|
|
701
|
+
printLine(`Created project ${sanitizeTerminalText(`${data.project.teamSlug}/${data.project.slug}`)}`);
|
|
633
702
|
}
|
|
634
703
|
async function listTasks() {
|
|
635
704
|
const project = getArg('--project');
|
|
@@ -695,9 +764,9 @@ async function createAppFromFile() {
|
|
|
695
764
|
throw new Error(`Failed to create app: ${await response.text()}`);
|
|
696
765
|
}
|
|
697
766
|
const data = await parseJsonResponse(response, 'App create');
|
|
698
|
-
|
|
767
|
+
printLine(`Created app ${sanitizeTerminalText(data.app.name)} (${sanitizeTerminalText(data.app.id)}) v${data.app.versionNum}`);
|
|
699
768
|
if (data.warnings?.length) {
|
|
700
|
-
|
|
769
|
+
printLine(`Warnings: ${sanitizeTerminalText(data.warnings.join('; '))}`);
|
|
701
770
|
}
|
|
702
771
|
}
|
|
703
772
|
async function updateAppFromFile() {
|
|
@@ -731,9 +800,9 @@ async function updateAppFromFile() {
|
|
|
731
800
|
throw new Error(`Failed to update app: ${await response.text()}`);
|
|
732
801
|
}
|
|
733
802
|
const data = await parseJsonResponse(response, 'App update');
|
|
734
|
-
|
|
803
|
+
printLine(`Updated app ${sanitizeTerminalText(data.app.name)} (${sanitizeTerminalText(data.app.id)}) to v${data.app.versionNum}`);
|
|
735
804
|
if (data.warnings?.length) {
|
|
736
|
-
|
|
805
|
+
printLine(`Warnings: ${sanitizeTerminalText(data.warnings.join('; '))}`);
|
|
737
806
|
}
|
|
738
807
|
}
|
|
739
808
|
async function linkAppDataset() {
|
|
@@ -765,7 +834,7 @@ async function linkAppDataset() {
|
|
|
765
834
|
throw new Error(`Failed to link dataset: ${await response.text()}`);
|
|
766
835
|
}
|
|
767
836
|
const data = await parseJsonResponse(response, 'App link dataset');
|
|
768
|
-
|
|
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}`);
|
|
769
838
|
}
|
|
770
839
|
function parseCommaSeparated(value) {
|
|
771
840
|
if (!value) {
|
|
@@ -812,7 +881,7 @@ async function createTask() {
|
|
|
812
881
|
const cliError = await formatTaskCreateError(response);
|
|
813
882
|
if (hasArg('--json')) {
|
|
814
883
|
const payload = cliError.structuredPayload ?? { error: cliError.message };
|
|
815
|
-
|
|
884
|
+
printLine(JSON.stringify({
|
|
816
885
|
...payload,
|
|
817
886
|
httpStatus: cliError.httpStatus,
|
|
818
887
|
}, null, 2));
|
|
@@ -821,7 +890,7 @@ async function createTask() {
|
|
|
821
890
|
}
|
|
822
891
|
const data = await parseJsonResponse(response, 'Task create');
|
|
823
892
|
if (hasArg('--json')) {
|
|
824
|
-
|
|
893
|
+
printLine(JSON.stringify({
|
|
825
894
|
taskId: data.task.id,
|
|
826
895
|
datasetId,
|
|
827
896
|
versionId: data.task.versionId,
|
|
@@ -838,14 +907,14 @@ async function createTask() {
|
|
|
838
907
|
}
|
|
839
908
|
const baseUrl = getBaseUrl();
|
|
840
909
|
const taskUrl = `${baseUrl}/d/${projectSlug}/tasks/${data.task.id}`;
|
|
841
|
-
|
|
842
|
-
`\n Task ID: ${data.task.id}` +
|
|
843
|
-
`\n Dataset ID: ${datasetId}` +
|
|
844
|
-
`\n Version: v${data.task.versionNum} (${data.task.versionId})` +
|
|
910
|
+
printLine(`Created task ${sanitizeTerminalText(data.task.title)} (${sanitizeTerminalText(data.task.id)}) [${sanitizeTerminalText(data.task.status)}]` +
|
|
911
|
+
`\n Task ID: ${sanitizeTerminalText(data.task.id)}` +
|
|
912
|
+
`\n Dataset ID: ${sanitizeTerminalText(datasetId)}` +
|
|
913
|
+
`\n Version: v${data.task.versionNum} (${sanitizeTerminalText(data.task.versionId)})` +
|
|
845
914
|
`\n Labels/row: ${data.task.requiredAssignmentsPerRow}` +
|
|
846
915
|
`\n Assignments: ${data.assignmentsCreated}` +
|
|
847
|
-
(data.warning ? `\n Warning: ${data.warning}` : '') +
|
|
848
|
-
`\n URL: ${taskUrl}`);
|
|
916
|
+
(data.warning ? `\n Warning: ${sanitizeTerminalText(data.warning)}` : '') +
|
|
917
|
+
`\n URL: ${sanitizeTerminalText(taskUrl)}`);
|
|
849
918
|
}
|
|
850
919
|
async function assignTask() {
|
|
851
920
|
const taskId = getArg('--task');
|
|
@@ -862,7 +931,7 @@ async function assignTask() {
|
|
|
862
931
|
throw new Error(`Failed to assign task: ${await response.text()}`);
|
|
863
932
|
}
|
|
864
933
|
const data = await parseJsonResponse(response, 'Task assign');
|
|
865
|
-
|
|
934
|
+
printLine(`Created ${data.assignmentsCreated} assignments.`);
|
|
866
935
|
}
|
|
867
936
|
async function taskStatus() {
|
|
868
937
|
const taskId = getArg('--task');
|
|
@@ -883,7 +952,7 @@ async function taskStatus() {
|
|
|
883
952
|
catch {
|
|
884
953
|
// keep raw body as error
|
|
885
954
|
}
|
|
886
|
-
|
|
955
|
+
printLine(JSON.stringify({
|
|
887
956
|
...errorPayload,
|
|
888
957
|
httpStatus: response.status,
|
|
889
958
|
}, null, 2));
|
|
@@ -900,7 +969,7 @@ async function taskStatus() {
|
|
|
900
969
|
}
|
|
901
970
|
const data = await parseJsonResponse(response, 'Task status');
|
|
902
971
|
if (hasArg('--json')) {
|
|
903
|
-
|
|
972
|
+
printLine(JSON.stringify(data, null, 2));
|
|
904
973
|
return;
|
|
905
974
|
}
|
|
906
975
|
printTaskStatusSummary(data);
|
|
@@ -922,7 +991,7 @@ async function updateTaskStatus(targetStatus) {
|
|
|
922
991
|
}
|
|
923
992
|
const data = await parseJsonResponse(response, 'Task status update');
|
|
924
993
|
const action = targetStatus === 'paused' ? 'Paused' : 'Unpaused';
|
|
925
|
-
|
|
994
|
+
printLine(`${action} task ${sanitizeTerminalText(data.task.id)} [${sanitizeTerminalText(data.task.status)}]`);
|
|
926
995
|
}
|
|
927
996
|
async function appDetail() {
|
|
928
997
|
const appId = getArg('--app');
|
|
@@ -942,25 +1011,25 @@ async function appDetail() {
|
|
|
942
1011
|
throw new Error(`App '${appId}' not found in project '${projectSlug}'`);
|
|
943
1012
|
}
|
|
944
1013
|
if (hasArg('--json')) {
|
|
945
|
-
|
|
1014
|
+
printLine(JSON.stringify(detail, null, 2));
|
|
946
1015
|
return;
|
|
947
1016
|
}
|
|
948
|
-
|
|
949
|
-
|
|
1017
|
+
printLine(`App: ${sanitizeTerminalText(detail.name)} (${sanitizeTerminalText(detail.id)})`);
|
|
1018
|
+
printLine(` Project: ${sanitizeTerminalText(`${detail.teamSlug}/${detail.projectSlug}`)}`);
|
|
950
1019
|
if (detail.currentVersion) {
|
|
951
|
-
|
|
952
|
-
|
|
953
|
-
|
|
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'}`);
|
|
954
1023
|
}
|
|
955
1024
|
else {
|
|
956
|
-
|
|
1025
|
+
printLine(` Current version: none`);
|
|
957
1026
|
}
|
|
958
|
-
|
|
1027
|
+
printLine(` Compatible datasets: ${detail.compatibleDatasetsCount}/${detail.totalDatasetsCount}`);
|
|
959
1028
|
if (detail.createdByEmail) {
|
|
960
|
-
|
|
1029
|
+
printLine(` Created by: ${sanitizeTerminalText(detail.createdByName || detail.createdByEmail)}`);
|
|
961
1030
|
}
|
|
962
|
-
|
|
963
|
-
|
|
1031
|
+
printLine(` Created: ${sanitizeTerminalText(detail.createdAt)}`);
|
|
1032
|
+
printLine(` Updated: ${sanitizeTerminalText(detail.updatedAt)}`);
|
|
964
1033
|
}
|
|
965
1034
|
async function listTeamMembers() {
|
|
966
1035
|
const teamSlug = await resolveTeamSlug(getArg('--team'));
|
|
@@ -981,7 +1050,7 @@ async function addTeamMember() {
|
|
|
981
1050
|
throw new Error(`Failed to add team member: ${await response.text()}`);
|
|
982
1051
|
}
|
|
983
1052
|
const data = await parseJsonResponse(response, 'Team member add');
|
|
984
|
-
|
|
1053
|
+
printLine(`Added team member ${sanitizeTerminalText(data.member.email)} (${sanitizeTerminalText(data.member.id)})`);
|
|
985
1054
|
}
|
|
986
1055
|
async function removeTeamMember() {
|
|
987
1056
|
const teamSlug = await resolveTeamSlug(getArg('--team'));
|
|
@@ -998,7 +1067,7 @@ async function removeTeamMember() {
|
|
|
998
1067
|
if (!response.ok) {
|
|
999
1068
|
throw new Error(`Failed to remove team member: ${await response.text()}`);
|
|
1000
1069
|
}
|
|
1001
|
-
|
|
1070
|
+
printLine(`Removed team member ${sanitizeTerminalText(member.email)}`);
|
|
1002
1071
|
}
|
|
1003
1072
|
async function changeTeamMemberRole() {
|
|
1004
1073
|
const teamSlugArg = getArg('--team');
|
|
@@ -1024,7 +1093,7 @@ async function changeTeamMemberRole() {
|
|
|
1024
1093
|
if (!response.ok) {
|
|
1025
1094
|
throw new Error(`Failed to update member role: ${await response.text()}`);
|
|
1026
1095
|
}
|
|
1027
|
-
|
|
1096
|
+
printLine(`Updated ${sanitizeTerminalText(member.email)} role to ${sanitizeTerminalText(role)}`);
|
|
1028
1097
|
}
|
|
1029
1098
|
async function createDatasetFromRows(project, name, sourceType, rows) {
|
|
1030
1099
|
const response = await authedFetch('/api/cli/datasets/upload', {
|
|
@@ -1061,7 +1130,7 @@ async function uploadJsonlDatasetInChunks(file, project, datasetName) {
|
|
|
1061
1130
|
throw new Error(message);
|
|
1062
1131
|
}
|
|
1063
1132
|
throw new Error(`Upload stopped while reading the next JSONL chunk: ${message}\n` +
|
|
1064
|
-
`Dataset ${dataset.name} (${dataset.id}) was created and ${totalUploaded} rows were uploaded. ` +
|
|
1133
|
+
`Dataset ${sanitizeTerminalText(dataset.name)} (${sanitizeTerminalText(dataset.id)}) was created and ${totalUploaded} rows were uploaded. ` +
|
|
1065
1134
|
`Fix the file, remove the first ${totalUploaded} rows, and run ` +
|
|
1066
1135
|
`orizu datasets append --dataset ${dataset.id} --file <remaining-file>.`);
|
|
1067
1136
|
}
|
|
@@ -1070,7 +1139,7 @@ async function uploadJsonlDatasetInChunks(file, project, datasetName) {
|
|
|
1070
1139
|
}
|
|
1071
1140
|
const chunk = nextChunk.value;
|
|
1072
1141
|
chunkIndex += 1;
|
|
1073
|
-
|
|
1142
|
+
printLine(`Uploading chunk ${chunkIndex} (${chunk.length} rows)...`);
|
|
1074
1143
|
try {
|
|
1075
1144
|
if (!dataset) {
|
|
1076
1145
|
const data = await createDatasetFromRows(project, datasetName, 'jsonl', chunk);
|
|
@@ -1099,9 +1168,9 @@ async function uploadJsonlDatasetInChunks(file, project, datasetName) {
|
|
|
1099
1168
|
if (!dataset) {
|
|
1100
1169
|
throw new Error('Dataset file contains no rows');
|
|
1101
1170
|
}
|
|
1102
|
-
|
|
1171
|
+
printLine(`Uploaded dataset ${sanitizeTerminalText(dataset.name)} (${sanitizeTerminalText(dataset.id)}) with ${dataset.rowCount} rows.`);
|
|
1103
1172
|
if (dataset.url) {
|
|
1104
|
-
|
|
1173
|
+
printLine(`View dataset: ${formatTerminalLink(dataset.url)}`);
|
|
1105
1174
|
}
|
|
1106
1175
|
}
|
|
1107
1176
|
async function uploadDataset() {
|
|
@@ -1120,9 +1189,9 @@ async function uploadDataset() {
|
|
|
1120
1189
|
}
|
|
1121
1190
|
const { rows, sourceType } = parseDatasetFile(file);
|
|
1122
1191
|
const data = await createDatasetFromRows(project, datasetName, sourceType, rows);
|
|
1123
|
-
|
|
1192
|
+
printLine(`Uploaded dataset ${sanitizeTerminalText(data.dataset.name)} (${sanitizeTerminalText(data.dataset.id)}) with ${data.dataset.rowCount} rows.`);
|
|
1124
1193
|
if (data.dataset.url) {
|
|
1125
|
-
|
|
1194
|
+
printLine(`View dataset: ${formatTerminalLink(data.dataset.url)}`);
|
|
1126
1195
|
}
|
|
1127
1196
|
}
|
|
1128
1197
|
function getDatasetReferenceInput() {
|
|
@@ -1161,7 +1230,7 @@ async function downloadDataset() {
|
|
|
1161
1230
|
: `${datasetId}.${format}`;
|
|
1162
1231
|
const bytes = new Uint8Array(await response.arrayBuffer());
|
|
1163
1232
|
writeFileSync(filename, bytes);
|
|
1164
|
-
|
|
1233
|
+
printLine(`Saved dataset ${sanitizeTerminalText(datasetId)} (${format.toUpperCase()}) to ${sanitizeTerminalText(filename)}`);
|
|
1165
1234
|
}
|
|
1166
1235
|
const MAX_INPUT_FILE_SIZE_BYTES = 50 * 1024 * 1024; // 50 MB
|
|
1167
1236
|
const APPEND_CHUNK_SIZE_ROWS = 500;
|
|
@@ -1197,7 +1266,7 @@ async function appendJsonlDatasetRowsInChunks(datasetId, file) {
|
|
|
1197
1266
|
}
|
|
1198
1267
|
const chunk = nextChunk.value;
|
|
1199
1268
|
chunkIndex += 1;
|
|
1200
|
-
|
|
1269
|
+
printLine(`Uploading chunk ${chunkIndex} (${chunk.length} rows)...`);
|
|
1201
1270
|
try {
|
|
1202
1271
|
const data = await appendChunk(datasetId, chunk);
|
|
1203
1272
|
totalAppended += data.appendedCount;
|
|
@@ -1213,7 +1282,7 @@ async function appendJsonlDatasetRowsInChunks(datasetId, file) {
|
|
|
1213
1282
|
if (!lastResult) {
|
|
1214
1283
|
throw new Error('Dataset append file must contain at least one row');
|
|
1215
1284
|
}
|
|
1216
|
-
|
|
1285
|
+
printLine(`Appended ${totalAppended} rows to dataset ${sanitizeTerminalText(lastResult.dataset.name)} (${sanitizeTerminalText(lastResult.dataset.id)}). New row count: ${lastResult.dataset.rowCount}`);
|
|
1217
1286
|
}
|
|
1218
1287
|
async function appendDatasetRows() {
|
|
1219
1288
|
const projectArg = getArg('--project');
|
|
@@ -1262,7 +1331,7 @@ async function appendDatasetRows() {
|
|
|
1262
1331
|
}
|
|
1263
1332
|
if (rows.length <= APPEND_CHUNK_SIZE_ROWS) {
|
|
1264
1333
|
const data = await appendChunk(datasetId, rows);
|
|
1265
|
-
|
|
1334
|
+
printLine(`Appended ${data.appendedCount} rows to dataset ${sanitizeTerminalText(data.dataset.name)} (${sanitizeTerminalText(data.dataset.id)}). New row count: ${data.dataset.rowCount}`);
|
|
1266
1335
|
return;
|
|
1267
1336
|
}
|
|
1268
1337
|
// Chunked upload for large row counts (ALI-555: track partial progress)
|
|
@@ -1272,7 +1341,7 @@ async function appendDatasetRows() {
|
|
|
1272
1341
|
for (let offset = 0; offset < rows.length; offset += APPEND_CHUNK_SIZE_ROWS) {
|
|
1273
1342
|
const chunk = rows.slice(offset, offset + APPEND_CHUNK_SIZE_ROWS);
|
|
1274
1343
|
const chunkIndex = Math.floor(offset / APPEND_CHUNK_SIZE_ROWS) + 1;
|
|
1275
|
-
|
|
1344
|
+
printLine(`Uploading chunk ${chunkIndex}/${totalChunks} (${chunk.length} rows)...`);
|
|
1276
1345
|
try {
|
|
1277
1346
|
const data = await appendChunk(datasetId, chunk);
|
|
1278
1347
|
totalAppended += data.appendedCount;
|
|
@@ -1286,7 +1355,7 @@ async function appendDatasetRows() {
|
|
|
1286
1355
|
}
|
|
1287
1356
|
}
|
|
1288
1357
|
if (lastResult) {
|
|
1289
|
-
|
|
1358
|
+
printLine(`Appended ${totalAppended} rows to dataset ${sanitizeTerminalText(lastResult.dataset.name)} (${sanitizeTerminalText(lastResult.dataset.id)}). New row count: ${lastResult.dataset.rowCount}`);
|
|
1290
1359
|
}
|
|
1291
1360
|
}
|
|
1292
1361
|
async function editDatasetRows() {
|
|
@@ -1332,7 +1401,7 @@ async function editDatasetRows() {
|
|
|
1332
1401
|
throw new Error(`Edit rows failed: ${await response.text()}`);
|
|
1333
1402
|
}
|
|
1334
1403
|
const data = await parseJsonResponse(response, 'Dataset edit rows');
|
|
1335
|
-
|
|
1404
|
+
printLine(`Updated ${data.updatedCount} rows in dataset ${sanitizeTerminalText(data.dataset.name)} (${sanitizeTerminalText(data.dataset.id)}). Current row count: ${data.dataset.rowCount}`);
|
|
1336
1405
|
}
|
|
1337
1406
|
async function deleteDatasetRows() {
|
|
1338
1407
|
const projectArg = getArg('--project');
|
|
@@ -1360,7 +1429,45 @@ async function deleteDatasetRows() {
|
|
|
1360
1429
|
throw new Error(`Delete rows failed: ${await response.text()}`);
|
|
1361
1430
|
}
|
|
1362
1431
|
const data = await parseJsonResponse(response, 'Dataset delete rows');
|
|
1363
|
-
|
|
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)}.`);
|
|
1364
1471
|
}
|
|
1365
1472
|
async function lockDataset() {
|
|
1366
1473
|
const projectArg = getArg('--project');
|
|
@@ -1383,7 +1490,7 @@ async function lockDataset() {
|
|
|
1383
1490
|
throw new Error(`Lock failed: ${await response.text()}`);
|
|
1384
1491
|
}
|
|
1385
1492
|
const data = await parseJsonResponse(response, 'Dataset lock');
|
|
1386
|
-
|
|
1493
|
+
printLine(`Locked dataset ${sanitizeTerminalText(data.dataset.name)} (${sanitizeTerminalText(data.dataset.id)}) at ${sanitizeTerminalText(data.dataset.lockedAt)}. Row count: ${data.dataset.rowCount}`);
|
|
1387
1494
|
}
|
|
1388
1495
|
async function cloneDataset() {
|
|
1389
1496
|
const projectArg = getArg('--project');
|
|
@@ -1406,7 +1513,7 @@ async function cloneDataset() {
|
|
|
1406
1513
|
throw new Error(`Clone failed: ${await response.text()}`);
|
|
1407
1514
|
}
|
|
1408
1515
|
const data = await parseJsonResponse(response, 'Dataset clone');
|
|
1409
|
-
|
|
1516
|
+
printLine(`Cloned dataset ${sanitizeTerminalText(data.dataset.parentDatasetId)} -> ${sanitizeTerminalText(data.dataset.name)} (${sanitizeTerminalText(data.dataset.id)}). Row count: ${data.dataset.rowCount}`);
|
|
1410
1517
|
}
|
|
1411
1518
|
async function downloadAnnotations() {
|
|
1412
1519
|
let taskId = getArg('--task');
|
|
@@ -1428,14 +1535,18 @@ async function downloadAnnotations() {
|
|
|
1428
1535
|
: fallbackName;
|
|
1429
1536
|
const bytes = new Uint8Array(await response.arrayBuffer());
|
|
1430
1537
|
writeFileSync(filename, bytes);
|
|
1431
|
-
|
|
1538
|
+
printLine(`Saved ${format.toUpperCase()} export to ${sanitizeTerminalText(filename)}`);
|
|
1432
1539
|
}
|
|
1433
|
-
async function main() {
|
|
1434
|
-
const parsed = parseGlobalFlags(
|
|
1540
|
+
export async function main(rawArgs = process.argv.slice(2)) {
|
|
1541
|
+
const parsed = parseGlobalFlags(rawArgs);
|
|
1435
1542
|
setGlobalFlags(parsed.flags);
|
|
1436
1543
|
cliArgs = parsed.args;
|
|
1437
1544
|
const command = cliArgs[0];
|
|
1438
1545
|
const subcommand = cliArgs[1];
|
|
1546
|
+
if (command === '--version' || command === '-v') {
|
|
1547
|
+
printVersion();
|
|
1548
|
+
return;
|
|
1549
|
+
}
|
|
1439
1550
|
if (!command) {
|
|
1440
1551
|
printUsage();
|
|
1441
1552
|
process.exit(1);
|
|
@@ -1549,6 +1660,10 @@ async function main() {
|
|
|
1549
1660
|
await deleteDatasetRows();
|
|
1550
1661
|
return;
|
|
1551
1662
|
}
|
|
1663
|
+
if (command === 'datasets' && subcommand === 'delete') {
|
|
1664
|
+
await deleteDataset();
|
|
1665
|
+
return;
|
|
1666
|
+
}
|
|
1552
1667
|
if (command === 'datasets' && subcommand === 'lock') {
|
|
1553
1668
|
await lockDataset();
|
|
1554
1669
|
return;
|
|
@@ -1564,7 +1679,9 @@ async function main() {
|
|
|
1564
1679
|
printUsage();
|
|
1565
1680
|
process.exit(1);
|
|
1566
1681
|
}
|
|
1567
|
-
|
|
1568
|
-
|
|
1569
|
-
|
|
1570
|
-
|
|
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
|
+
}
|