orizu 0.0.9 → 0.1.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/credentials.js +10 -2
- package/dist/error-response.js +20 -0
- package/dist/index.js +360 -37
- package/dist/jsonl-stream.js +91 -0
- package/dist/task-create-error.js +60 -0
- package/package.json +1 -1
- package/src/credentials.ts +11 -2
- package/src/error-response.ts +27 -0
- package/src/index.ts +499 -51
- package/src/jsonl-stream.ts +131 -0
- package/src/task-create-error.ts +115 -0
package/dist/credentials.js
CHANGED
|
@@ -68,14 +68,22 @@ export function loadCredentialsConfig() {
|
|
|
68
68
|
return null;
|
|
69
69
|
}
|
|
70
70
|
const raw = readFileSync(path, 'utf-8');
|
|
71
|
-
|
|
71
|
+
let parsed;
|
|
72
|
+
try {
|
|
73
|
+
parsed = JSON.parse(raw);
|
|
74
|
+
}
|
|
75
|
+
catch {
|
|
76
|
+
console.warn('Warning: credentials file contains invalid JSON — please re-login with `orizu login`');
|
|
77
|
+
return null;
|
|
78
|
+
}
|
|
72
79
|
if (isStoredCredentialsV2(parsed)) {
|
|
73
80
|
return parsed;
|
|
74
81
|
}
|
|
75
82
|
if (isStoredCredentialsV1(parsed)) {
|
|
76
83
|
return migrateToV2(parsed);
|
|
77
84
|
}
|
|
78
|
-
|
|
85
|
+
console.warn('Warning: credentials file has unrecognized format — please re-login with `orizu login`');
|
|
86
|
+
return null;
|
|
79
87
|
}
|
|
80
88
|
export function getServerCredentials(baseUrl) {
|
|
81
89
|
const config = loadCredentialsConfig();
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
export async function extractErrorMessage(response) {
|
|
2
|
+
const contentType = response.headers.get('content-type') || '';
|
|
3
|
+
const rawBody = await response.text();
|
|
4
|
+
if (contentType.includes('application/json')) {
|
|
5
|
+
try {
|
|
6
|
+
const payload = JSON.parse(rawBody);
|
|
7
|
+
if (payload &&
|
|
8
|
+
typeof payload === 'object' &&
|
|
9
|
+
!Array.isArray(payload) &&
|
|
10
|
+
typeof payload.error === 'string' &&
|
|
11
|
+
payload.error.length > 0) {
|
|
12
|
+
return payload.error;
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
catch {
|
|
16
|
+
// Fall through to raw body
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
return rawBody;
|
|
20
|
+
}
|
package/dist/index.js
CHANGED
|
@@ -1,18 +1,20 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import { createHash, randomBytes } from 'crypto';
|
|
3
|
-
import { basename } from 'path';
|
|
3
|
+
import { basename, extname } from 'path';
|
|
4
4
|
import { createServer } from 'http';
|
|
5
|
-
import { readFileSync, writeFileSync } from 'fs';
|
|
5
|
+
import { readFileSync, statSync, writeFileSync } from 'fs';
|
|
6
6
|
import { spawn } from 'child_process';
|
|
7
7
|
import { createInterface } from 'readline/promises';
|
|
8
8
|
import { stdin as input, stdout as output } from 'process';
|
|
9
9
|
import { clearServerCredentials, getServerCredentials, saveServerCredentials } from './credentials.js';
|
|
10
10
|
import { parseDatasetFile } from './file-parser.js';
|
|
11
|
+
import { streamJsonlRowChunks } from './jsonl-stream.js';
|
|
11
12
|
import { parseDatasetReference } from './dataset-download.js';
|
|
12
13
|
import { parseGlobalFlags } from './global-flags.js';
|
|
13
14
|
import { authedFetch, getBaseUrl, resolveLoginBaseUrl, setGlobalFlags } from './http.js';
|
|
15
|
+
import { formatTaskCreateError } from './task-create-error.js';
|
|
14
16
|
function printUsage() {
|
|
15
|
-
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 tasks list [--project <team/project>]\n orizu tasks create --project <team/project> --dataset <datasetId> --app <appId> --title <title> --assignees <
|
|
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>]`);
|
|
16
18
|
}
|
|
17
19
|
let cliArgs = process.argv.slice(2);
|
|
18
20
|
function getArg(name) {
|
|
@@ -313,16 +315,18 @@ function printTeamMembers(members) {
|
|
|
313
315
|
}
|
|
314
316
|
const rows = members.map(member => ({
|
|
315
317
|
id: member.id,
|
|
318
|
+
userId: member.user_id || '-',
|
|
316
319
|
email: member.email || '-',
|
|
317
320
|
role: member.role || '-',
|
|
318
321
|
}));
|
|
319
322
|
const idWidth = Math.max('MEMBER ID'.length, ...rows.map(row => row.id.length));
|
|
323
|
+
const userIdWidth = Math.max('USER ID'.length, ...rows.map(row => row.userId.length));
|
|
320
324
|
const emailWidth = Math.max('EMAIL'.length, ...rows.map(row => row.email.length));
|
|
321
325
|
const roleWidth = Math.max('ROLE'.length, ...rows.map(row => row.role.length));
|
|
322
|
-
console.log(`${'MEMBER ID'.padEnd(idWidth)} ${'EMAIL'.padEnd(emailWidth)} ${'ROLE'.padEnd(roleWidth)}`);
|
|
323
|
-
console.log(`${'-'.repeat(idWidth)} ${'-'.repeat(emailWidth)} ${'-'.repeat(roleWidth)}`);
|
|
326
|
+
console.log(`${'MEMBER ID'.padEnd(idWidth)} ${'USER ID'.padEnd(userIdWidth)} ${'EMAIL'.padEnd(emailWidth)} ${'ROLE'.padEnd(roleWidth)}`);
|
|
327
|
+
console.log(`${'-'.repeat(idWidth)} ${'-'.repeat(userIdWidth)} ${'-'.repeat(emailWidth)} ${'-'.repeat(roleWidth)}`);
|
|
324
328
|
rows.forEach(row => {
|
|
325
|
-
console.log(`${row.id.padEnd(idWidth)} ${row.email.padEnd(emailWidth)} ${row.role.padEnd(roleWidth)}`);
|
|
329
|
+
console.log(`${row.id.padEnd(idWidth)} ${row.userId.padEnd(userIdWidth)} ${row.email.padEnd(emailWidth)} ${row.role.padEnd(roleWidth)}`);
|
|
326
330
|
});
|
|
327
331
|
}
|
|
328
332
|
function printTaskStatusSummary(data) {
|
|
@@ -340,14 +344,27 @@ function printTaskStatusSummary(data) {
|
|
|
340
344
|
});
|
|
341
345
|
}
|
|
342
346
|
}
|
|
347
|
+
const DEFAULT_AUTH_CALLBACK_PORT = 43123;
|
|
348
|
+
function resolveAuthCallbackPort() {
|
|
349
|
+
const envPort = process.env.ORIZU_AUTH_PORT;
|
|
350
|
+
if (!envPort) {
|
|
351
|
+
return DEFAULT_AUTH_CALLBACK_PORT;
|
|
352
|
+
}
|
|
353
|
+
const parsed = parseInt(envPort, 10);
|
|
354
|
+
if (Number.isNaN(parsed) || parsed < 1024 || parsed > 65535) {
|
|
355
|
+
throw new Error(`Invalid ORIZU_AUTH_PORT: '${envPort}'. Must be a number between 1024 and 65535.`);
|
|
356
|
+
}
|
|
357
|
+
return parsed;
|
|
358
|
+
}
|
|
343
359
|
async function login() {
|
|
344
360
|
const baseUrl = resolveLoginBaseUrl();
|
|
345
361
|
const codeVerifier = createCodeVerifier();
|
|
346
362
|
const codeChallenge = createCodeChallenge(codeVerifier);
|
|
363
|
+
const callbackPort = resolveAuthCallbackPort();
|
|
347
364
|
const callbackCode = await new Promise((resolve, reject) => {
|
|
348
365
|
const server = createServer((request, response) => {
|
|
349
366
|
try {
|
|
350
|
-
const url = new URL(request.url || '/',
|
|
367
|
+
const url = new URL(request.url || '/', `http://127.0.0.1:${callbackPort}`);
|
|
351
368
|
const code = url.searchParams.get('code');
|
|
352
369
|
if (!code) {
|
|
353
370
|
response.statusCode = 400;
|
|
@@ -365,10 +382,17 @@ async function login() {
|
|
|
365
382
|
reject(error);
|
|
366
383
|
}
|
|
367
384
|
});
|
|
368
|
-
server.on('error',
|
|
369
|
-
|
|
385
|
+
server.on('error', (error) => {
|
|
386
|
+
if (error.code === 'EADDRINUSE') {
|
|
387
|
+
reject(new Error(`Port ${callbackPort} is already in use. Set ORIZU_AUTH_PORT to a different port (1024–65535) and retry.`));
|
|
388
|
+
}
|
|
389
|
+
else {
|
|
390
|
+
reject(error);
|
|
391
|
+
}
|
|
392
|
+
});
|
|
393
|
+
server.listen(callbackPort, '127.0.0.1', async () => {
|
|
370
394
|
try {
|
|
371
|
-
const redirectUri =
|
|
395
|
+
const redirectUri = `http://127.0.0.1:${callbackPort}/callback`;
|
|
372
396
|
const response = await fetch(`${baseUrl}/api/cli/auth/start`, {
|
|
373
397
|
method: 'POST',
|
|
374
398
|
headers: { 'Content-Type': 'application/json' },
|
|
@@ -601,10 +625,14 @@ async function linkAppDataset() {
|
|
|
601
625
|
const project = getArg('--project');
|
|
602
626
|
let appId = getArg('--app');
|
|
603
627
|
const versionArg = getArg('--version');
|
|
604
|
-
const
|
|
628
|
+
const parsedVersionNum = versionArg ? Number(versionArg) : Number.NaN;
|
|
629
|
+
const versionNum = Number.isInteger(parsedVersionNum) && parsedVersionNum > 0 ? parsedVersionNum : undefined;
|
|
605
630
|
if (!datasetId) {
|
|
606
631
|
throw new Error('Usage: orizu apps link-dataset --dataset <datasetId> [--app <appId>] [--project <team/project>] [--version <n>]');
|
|
607
632
|
}
|
|
633
|
+
if (versionArg && versionNum === undefined) {
|
|
634
|
+
throw new Error('--version must be a positive integer');
|
|
635
|
+
}
|
|
608
636
|
if (!appId) {
|
|
609
637
|
const selected = await selectAppIdInteractively(project);
|
|
610
638
|
appId = selected.appId;
|
|
@@ -638,11 +666,17 @@ async function createTask() {
|
|
|
638
666
|
const appId = getArg('--app');
|
|
639
667
|
const title = getArg('--title');
|
|
640
668
|
const assignees = parseCommaSeparated(getArg('--assignees'));
|
|
669
|
+
const versionArg = getArg('--version');
|
|
641
670
|
const instructions = getArg('--instructions');
|
|
642
671
|
const labelsPerItemArg = getArg('--labels-per-item');
|
|
643
672
|
const labelsPerItem = labelsPerItemArg ? Number(labelsPerItemArg) : 1;
|
|
673
|
+
const parsedVersionNum = versionArg ? Number(versionArg) : Number.NaN;
|
|
674
|
+
const versionNum = Number.isInteger(parsedVersionNum) && parsedVersionNum > 0 ? parsedVersionNum : null;
|
|
644
675
|
if (!projectSlug || !datasetId || !appId || !title || assignees.length === 0) {
|
|
645
|
-
throw new Error('Usage: orizu tasks create --project <team/project> --dataset <datasetId> --app <appId> --title <title> --assignees <
|
|
676
|
+
throw new Error('Usage: orizu tasks create --project <team/project> --dataset <datasetId> --app <appId> --title <title> --assignees <userIdOrEmail1,userIdOrEmail2> [--version <n>] [--instructions <text>] [--labels-per-item <n>] [--json]');
|
|
677
|
+
}
|
|
678
|
+
if (versionArg && versionNum === null) {
|
|
679
|
+
throw new Error('--version must be a positive integer');
|
|
646
680
|
}
|
|
647
681
|
const response = await authedFetch('/api/cli/tasks', {
|
|
648
682
|
method: 'POST',
|
|
@@ -651,6 +685,7 @@ async function createTask() {
|
|
|
651
685
|
projectSlug,
|
|
652
686
|
datasetId,
|
|
653
687
|
appId,
|
|
688
|
+
versionNum,
|
|
654
689
|
title,
|
|
655
690
|
memberIds: assignees,
|
|
656
691
|
instructions,
|
|
@@ -658,10 +693,43 @@ async function createTask() {
|
|
|
658
693
|
}),
|
|
659
694
|
});
|
|
660
695
|
if (!response.ok) {
|
|
661
|
-
|
|
696
|
+
const cliError = await formatTaskCreateError(response);
|
|
697
|
+
if (hasArg('--json')) {
|
|
698
|
+
const payload = cliError.structuredPayload ?? { error: cliError.message };
|
|
699
|
+
console.log(JSON.stringify({
|
|
700
|
+
...payload,
|
|
701
|
+
httpStatus: cliError.httpStatus,
|
|
702
|
+
}, null, 2));
|
|
703
|
+
}
|
|
704
|
+
throw cliError;
|
|
662
705
|
}
|
|
663
706
|
const data = await parseJsonResponse(response, 'Task create');
|
|
664
|
-
|
|
707
|
+
if (hasArg('--json')) {
|
|
708
|
+
console.log(JSON.stringify({
|
|
709
|
+
taskId: data.task.id,
|
|
710
|
+
datasetId,
|
|
711
|
+
versionId: data.task.versionId,
|
|
712
|
+
versionNum: data.task.versionNum,
|
|
713
|
+
taskUrl: `${getBaseUrl()}/d/${projectSlug}/tasks/${data.task.id}`,
|
|
714
|
+
title: data.task.title,
|
|
715
|
+
status: data.task.status,
|
|
716
|
+
requiredAssignmentsPerRow: data.task.requiredAssignmentsPerRow,
|
|
717
|
+
assignmentsCreated: data.assignmentsCreated,
|
|
718
|
+
...(data.assignmentShortfall !== undefined ? { assignmentShortfall: data.assignmentShortfall } : {}),
|
|
719
|
+
...(data.warning ? { warning: data.warning } : {}),
|
|
720
|
+
}, null, 2));
|
|
721
|
+
return;
|
|
722
|
+
}
|
|
723
|
+
const baseUrl = getBaseUrl();
|
|
724
|
+
const taskUrl = `${baseUrl}/d/${projectSlug}/tasks/${data.task.id}`;
|
|
725
|
+
console.log(`Created task ${data.task.title} (${data.task.id}) [${data.task.status}]` +
|
|
726
|
+
`\n Task ID: ${data.task.id}` +
|
|
727
|
+
`\n Dataset ID: ${datasetId}` +
|
|
728
|
+
`\n Version: v${data.task.versionNum} (${data.task.versionId})` +
|
|
729
|
+
`\n Labels/row: ${data.task.requiredAssignmentsPerRow}` +
|
|
730
|
+
`\n Assignments: ${data.assignmentsCreated}` +
|
|
731
|
+
(data.warning ? `\n Warning: ${data.warning}` : '') +
|
|
732
|
+
`\n URL: ${taskUrl}`);
|
|
665
733
|
}
|
|
666
734
|
async function assignTask() {
|
|
667
735
|
const taskId = getArg('--task');
|
|
@@ -687,7 +755,32 @@ async function taskStatus() {
|
|
|
687
755
|
}
|
|
688
756
|
const response = await authedFetch(`/api/cli/tasks/${encodeURIComponent(taskId)}/status`);
|
|
689
757
|
if (!response.ok) {
|
|
690
|
-
|
|
758
|
+
const rawBody = await response.text();
|
|
759
|
+
if (hasArg('--json')) {
|
|
760
|
+
let errorPayload = { error: rawBody };
|
|
761
|
+
try {
|
|
762
|
+
const parsed = JSON.parse(rawBody);
|
|
763
|
+
if (parsed && typeof parsed === 'object' && !Array.isArray(parsed)) {
|
|
764
|
+
errorPayload = parsed;
|
|
765
|
+
}
|
|
766
|
+
}
|
|
767
|
+
catch {
|
|
768
|
+
// keep raw body as error
|
|
769
|
+
}
|
|
770
|
+
console.log(JSON.stringify({
|
|
771
|
+
...errorPayload,
|
|
772
|
+
httpStatus: response.status,
|
|
773
|
+
}, null, 2));
|
|
774
|
+
}
|
|
775
|
+
let errorMsg = rawBody;
|
|
776
|
+
try {
|
|
777
|
+
const parsed = JSON.parse(rawBody);
|
|
778
|
+
if (parsed && typeof parsed === 'object' && typeof parsed.error === 'string') {
|
|
779
|
+
errorMsg = parsed.error;
|
|
780
|
+
}
|
|
781
|
+
}
|
|
782
|
+
catch { /* use rawBody as-is */ }
|
|
783
|
+
throw new Error(`Failed to fetch task status: ${errorMsg}`);
|
|
691
784
|
}
|
|
692
785
|
const data = await parseJsonResponse(response, 'Task status');
|
|
693
786
|
if (hasArg('--json')) {
|
|
@@ -696,6 +789,63 @@ async function taskStatus() {
|
|
|
696
789
|
}
|
|
697
790
|
printTaskStatusSummary(data);
|
|
698
791
|
}
|
|
792
|
+
async function updateTaskStatus(targetStatus) {
|
|
793
|
+
const taskId = getArg('--task');
|
|
794
|
+
if (!taskId) {
|
|
795
|
+
const verb = targetStatus === 'paused' ? 'pause' : 'unpause';
|
|
796
|
+
throw new Error(`Usage: orizu tasks ${verb} --task <taskId>`);
|
|
797
|
+
}
|
|
798
|
+
const response = await authedFetch(`/api/cli/tasks/${encodeURIComponent(taskId)}/status`, {
|
|
799
|
+
method: 'PATCH',
|
|
800
|
+
headers: { 'Content-Type': 'application/json' },
|
|
801
|
+
body: JSON.stringify({ status: targetStatus }),
|
|
802
|
+
});
|
|
803
|
+
if (!response.ok) {
|
|
804
|
+
const verb = targetStatus === 'paused' ? 'pause' : 'unpause';
|
|
805
|
+
throw new Error(`Failed to ${verb} task: ${await response.text()}`);
|
|
806
|
+
}
|
|
807
|
+
const data = await parseJsonResponse(response, 'Task status update');
|
|
808
|
+
const action = targetStatus === 'paused' ? 'Paused' : 'Unpaused';
|
|
809
|
+
console.log(`${action} task ${data.task.id} [${data.task.status}]`);
|
|
810
|
+
}
|
|
811
|
+
async function appDetail() {
|
|
812
|
+
const appId = getArg('--app');
|
|
813
|
+
const project = getArg('--project');
|
|
814
|
+
if (!appId) {
|
|
815
|
+
throw new Error('Usage: orizu apps detail --app <appId> [--project <team/project>] [--json]');
|
|
816
|
+
}
|
|
817
|
+
const projectSlug = project || await resolveProjectSlug(null);
|
|
818
|
+
// Single fetch — the apps endpoint already returns full detail (ALI-544)
|
|
819
|
+
const detailResponse = await authedFetch(`/api/cli/apps?project=${encodeURIComponent(projectSlug)}`);
|
|
820
|
+
if (!detailResponse.ok) {
|
|
821
|
+
throw new Error(`Failed to fetch app detail: ${await detailResponse.text()}`);
|
|
822
|
+
}
|
|
823
|
+
const detailData = await parseJsonResponse(detailResponse, 'App detail');
|
|
824
|
+
const detail = detailData.apps.find(a => a.id === appId);
|
|
825
|
+
if (!detail) {
|
|
826
|
+
throw new Error(`App '${appId}' not found in project '${projectSlug}'`);
|
|
827
|
+
}
|
|
828
|
+
if (hasArg('--json')) {
|
|
829
|
+
console.log(JSON.stringify(detail, null, 2));
|
|
830
|
+
return;
|
|
831
|
+
}
|
|
832
|
+
console.log(`App: ${detail.name} (${detail.id})`);
|
|
833
|
+
console.log(` Project: ${detail.teamSlug}/${detail.projectSlug}`);
|
|
834
|
+
if (detail.currentVersion) {
|
|
835
|
+
console.log(` Current version: v${detail.currentVersion.versionNum} (${detail.currentVersion.versionId})`);
|
|
836
|
+
console.log(` Input schema: ${detail.currentVersion.inputJsonSchema ? 'defined' : 'none'}`);
|
|
837
|
+
console.log(` Output schema: ${detail.currentVersion.outputJsonSchema ? 'defined' : 'none'}`);
|
|
838
|
+
}
|
|
839
|
+
else {
|
|
840
|
+
console.log(` Current version: none`);
|
|
841
|
+
}
|
|
842
|
+
console.log(` Compatible datasets: ${detail.compatibleDatasetsCount}/${detail.totalDatasetsCount}`);
|
|
843
|
+
if (detail.createdByEmail) {
|
|
844
|
+
console.log(` Created by: ${detail.createdByName || detail.createdByEmail}`);
|
|
845
|
+
}
|
|
846
|
+
console.log(` Created: ${detail.createdAt}`);
|
|
847
|
+
console.log(` Updated: ${detail.updatedAt}`);
|
|
848
|
+
}
|
|
699
849
|
async function listTeamMembers() {
|
|
700
850
|
const teamSlug = await resolveTeamSlug(getArg('--team'));
|
|
701
851
|
printTeamMembers(await fetchTeamMembers(teamSlug));
|
|
@@ -759,17 +909,7 @@ async function changeTeamMemberRole() {
|
|
|
759
909
|
}
|
|
760
910
|
console.log(`Updated ${member.email} role to ${role}`);
|
|
761
911
|
}
|
|
762
|
-
async function
|
|
763
|
-
const projectArg = getArg('--project');
|
|
764
|
-
const fileArg = getArg('--file');
|
|
765
|
-
const name = getArg('--name');
|
|
766
|
-
if (!fileArg) {
|
|
767
|
-
throw new Error('Usage: orizu datasets upload --file <path> [--project <team/project>] [--name <name>]');
|
|
768
|
-
}
|
|
769
|
-
const file = expandHomePath(fileArg);
|
|
770
|
-
const project = await resolveProjectSlug(projectArg);
|
|
771
|
-
const { rows, sourceType } = parseDatasetFile(file);
|
|
772
|
-
const datasetName = name || basename(file);
|
|
912
|
+
async function createDatasetFromRows(project, name, sourceType, rows) {
|
|
773
913
|
const response = await authedFetch('/api/cli/datasets/upload', {
|
|
774
914
|
method: 'POST',
|
|
775
915
|
headers: {
|
|
@@ -777,7 +917,7 @@ async function uploadDataset() {
|
|
|
777
917
|
},
|
|
778
918
|
body: JSON.stringify({
|
|
779
919
|
projectSlug: project,
|
|
780
|
-
name
|
|
920
|
+
name,
|
|
781
921
|
rows,
|
|
782
922
|
sourceType,
|
|
783
923
|
}),
|
|
@@ -786,7 +926,83 @@ async function uploadDataset() {
|
|
|
786
926
|
const body = await parseJsonResponse(response, 'Dataset upload');
|
|
787
927
|
throw new Error(`Upload failed: ${body.error}`);
|
|
788
928
|
}
|
|
789
|
-
|
|
929
|
+
return parseJsonResponse(response, 'Dataset upload');
|
|
930
|
+
}
|
|
931
|
+
async function uploadJsonlDatasetInChunks(file, project, datasetName) {
|
|
932
|
+
let dataset = null;
|
|
933
|
+
let totalUploaded = 0;
|
|
934
|
+
let chunkIndex = 0;
|
|
935
|
+
const chunks = streamJsonlRowChunks(file)[Symbol.asyncIterator]();
|
|
936
|
+
while (true) {
|
|
937
|
+
let nextChunk;
|
|
938
|
+
try {
|
|
939
|
+
nextChunk = await chunks.next();
|
|
940
|
+
}
|
|
941
|
+
catch (error) {
|
|
942
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
943
|
+
if (!dataset) {
|
|
944
|
+
throw new Error(message);
|
|
945
|
+
}
|
|
946
|
+
throw new Error(`Upload stopped while reading the next JSONL chunk: ${message}\n` +
|
|
947
|
+
`Dataset ${dataset.name} (${dataset.id}) was created and ${totalUploaded} rows were uploaded. ` +
|
|
948
|
+
`Fix the file, remove the first ${totalUploaded} rows, and run ` +
|
|
949
|
+
`orizu datasets append --dataset ${dataset.id} --file <remaining-file>.`);
|
|
950
|
+
}
|
|
951
|
+
if (nextChunk.done) {
|
|
952
|
+
break;
|
|
953
|
+
}
|
|
954
|
+
const chunk = nextChunk.value;
|
|
955
|
+
chunkIndex += 1;
|
|
956
|
+
console.log(`Uploading chunk ${chunkIndex} (${chunk.length} rows)...`);
|
|
957
|
+
try {
|
|
958
|
+
if (!dataset) {
|
|
959
|
+
const data = await createDatasetFromRows(project, datasetName, 'jsonl', chunk);
|
|
960
|
+
dataset = data.dataset;
|
|
961
|
+
totalUploaded = data.dataset.rowCount;
|
|
962
|
+
continue;
|
|
963
|
+
}
|
|
964
|
+
const data = await appendChunk(dataset.id, chunk);
|
|
965
|
+
totalUploaded += data.appendedCount;
|
|
966
|
+
dataset = {
|
|
967
|
+
...dataset,
|
|
968
|
+
rowCount: data.dataset.rowCount,
|
|
969
|
+
};
|
|
970
|
+
}
|
|
971
|
+
catch (error) {
|
|
972
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
973
|
+
if (!dataset) {
|
|
974
|
+
throw new Error(message);
|
|
975
|
+
}
|
|
976
|
+
throw new Error(`Chunk ${chunkIndex} failed: ${message}\n` +
|
|
977
|
+
`Dataset ${dataset.name} (${dataset.id}) was created and ${totalUploaded} rows were uploaded. ` +
|
|
978
|
+
`To retry, remove the first ${totalUploaded} rows from your file and run ` +
|
|
979
|
+
`orizu datasets append --dataset ${dataset.id} --file <remaining-file>.`);
|
|
980
|
+
}
|
|
981
|
+
}
|
|
982
|
+
if (!dataset) {
|
|
983
|
+
throw new Error('Dataset file contains no rows');
|
|
984
|
+
}
|
|
985
|
+
console.log(`Uploaded dataset ${dataset.name} (${dataset.id}) with ${dataset.rowCount} rows.`);
|
|
986
|
+
if (dataset.url) {
|
|
987
|
+
console.log(`View dataset: ${formatTerminalLink(dataset.url)}`);
|
|
988
|
+
}
|
|
989
|
+
}
|
|
990
|
+
async function uploadDataset() {
|
|
991
|
+
const projectArg = getArg('--project');
|
|
992
|
+
const fileArg = getArg('--file');
|
|
993
|
+
const name = getArg('--name');
|
|
994
|
+
if (!fileArg) {
|
|
995
|
+
throw new Error('Usage: orizu datasets upload --file <path> [--project <team/project>] [--name <name>]');
|
|
996
|
+
}
|
|
997
|
+
const file = expandHomePath(fileArg);
|
|
998
|
+
const project = await resolveProjectSlug(projectArg);
|
|
999
|
+
const datasetName = name || basename(file);
|
|
1000
|
+
if (extname(file).toLowerCase() === '.jsonl') {
|
|
1001
|
+
await uploadJsonlDatasetInChunks(file, project, datasetName);
|
|
1002
|
+
return;
|
|
1003
|
+
}
|
|
1004
|
+
const { rows, sourceType } = parseDatasetFile(file);
|
|
1005
|
+
const data = await createDatasetFromRows(project, datasetName, sourceType, rows);
|
|
790
1006
|
console.log(`Uploaded dataset ${data.dataset.name} (${data.dataset.id}) with ${data.dataset.rowCount} rows.`);
|
|
791
1007
|
if (data.dataset.url) {
|
|
792
1008
|
console.log(`View dataset: ${formatTerminalLink(data.dataset.url)}`);
|
|
@@ -830,6 +1046,58 @@ async function downloadDataset() {
|
|
|
830
1046
|
writeFileSync(filename, bytes);
|
|
831
1047
|
console.log(`Saved dataset ${datasetId} (${format.toUpperCase()}) to ${filename}`);
|
|
832
1048
|
}
|
|
1049
|
+
const MAX_INPUT_FILE_SIZE_BYTES = 50 * 1024 * 1024; // 50 MB
|
|
1050
|
+
const APPEND_CHUNK_SIZE_ROWS = 500;
|
|
1051
|
+
async function appendChunk(datasetId, rows) {
|
|
1052
|
+
const response = await authedFetch(`/api/cli/datasets/${encodeURIComponent(datasetId)}/rows`, {
|
|
1053
|
+
method: 'POST',
|
|
1054
|
+
headers: { 'Content-Type': 'application/json' },
|
|
1055
|
+
body: JSON.stringify({ rows }),
|
|
1056
|
+
});
|
|
1057
|
+
if (!response.ok) {
|
|
1058
|
+
throw new Error(`Append failed: ${await response.text()}`);
|
|
1059
|
+
}
|
|
1060
|
+
return parseJsonResponse(response, 'Dataset append');
|
|
1061
|
+
}
|
|
1062
|
+
async function appendJsonlDatasetRowsInChunks(datasetId, file) {
|
|
1063
|
+
let totalAppended = 0;
|
|
1064
|
+
let lastResult = null;
|
|
1065
|
+
let chunkIndex = 0;
|
|
1066
|
+
const chunks = streamJsonlRowChunks(file)[Symbol.asyncIterator]();
|
|
1067
|
+
while (true) {
|
|
1068
|
+
let nextChunk;
|
|
1069
|
+
try {
|
|
1070
|
+
nextChunk = await chunks.next();
|
|
1071
|
+
}
|
|
1072
|
+
catch (error) {
|
|
1073
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
1074
|
+
throw new Error(`Append stopped while reading the next JSONL chunk: ${message}\n` +
|
|
1075
|
+
`${totalAppended} rows from ${chunkIndex} chunk(s) were already appended. ` +
|
|
1076
|
+
`Fix the file, remove the first ${totalAppended} rows, and re-run the command.`);
|
|
1077
|
+
}
|
|
1078
|
+
if (nextChunk.done) {
|
|
1079
|
+
break;
|
|
1080
|
+
}
|
|
1081
|
+
const chunk = nextChunk.value;
|
|
1082
|
+
chunkIndex += 1;
|
|
1083
|
+
console.log(`Uploading chunk ${chunkIndex} (${chunk.length} rows)...`);
|
|
1084
|
+
try {
|
|
1085
|
+
const data = await appendChunk(datasetId, chunk);
|
|
1086
|
+
totalAppended += data.appendedCount;
|
|
1087
|
+
lastResult = data;
|
|
1088
|
+
}
|
|
1089
|
+
catch (error) {
|
|
1090
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
1091
|
+
throw new Error(`Chunk ${chunkIndex} failed: ${message}\n` +
|
|
1092
|
+
`${totalAppended} rows from ${chunkIndex - 1} chunk(s) were already appended. ` +
|
|
1093
|
+
`To retry, remove the first ${totalAppended} rows from your file and re-run the command.`);
|
|
1094
|
+
}
|
|
1095
|
+
}
|
|
1096
|
+
if (!lastResult) {
|
|
1097
|
+
throw new Error('Dataset append file must contain at least one row');
|
|
1098
|
+
}
|
|
1099
|
+
console.log(`Appended ${totalAppended} rows to dataset ${lastResult.dataset.name} (${lastResult.dataset.id}). New row count: ${lastResult.dataset.rowCount}`);
|
|
1100
|
+
}
|
|
833
1101
|
async function appendDatasetRows() {
|
|
834
1102
|
const projectArg = getArg('--project');
|
|
835
1103
|
const datasetInput = getDatasetReferenceInput();
|
|
@@ -846,20 +1114,63 @@ async function appendDatasetRows() {
|
|
|
846
1114
|
datasetId = selected.datasetId;
|
|
847
1115
|
}
|
|
848
1116
|
const file = expandHomePath(fileArg);
|
|
1117
|
+
if (extname(file).toLowerCase() === '.jsonl') {
|
|
1118
|
+
await appendJsonlDatasetRowsInChunks(datasetId, file);
|
|
1119
|
+
return;
|
|
1120
|
+
}
|
|
1121
|
+
// Check file size before reading to prevent OOM on large files (ALI-565).
|
|
1122
|
+
// Wrap statSync in try/catch so missing/inaccessible files get friendly
|
|
1123
|
+
// errors instead of raw Node.js ENOENT/EPERM (ALI-554).
|
|
1124
|
+
let fileSizeBytes;
|
|
1125
|
+
try {
|
|
1126
|
+
fileSizeBytes = statSync(file).size;
|
|
1127
|
+
}
|
|
1128
|
+
catch (error) {
|
|
1129
|
+
const maybeError = error;
|
|
1130
|
+
if (maybeError.code === 'ENOENT') {
|
|
1131
|
+
throw new Error(`File not found: ${file}. Check the path and filename, then retry.`);
|
|
1132
|
+
}
|
|
1133
|
+
if (maybeError.code === 'EPERM' || maybeError.code === 'EACCES') {
|
|
1134
|
+
throw new Error(`Cannot read file: ${file}. Grant folder permission to your terminal app and retry.`);
|
|
1135
|
+
}
|
|
1136
|
+
throw new Error(`Failed to access file ${file}: ${maybeError.message}`);
|
|
1137
|
+
}
|
|
1138
|
+
if (fileSizeBytes > MAX_INPUT_FILE_SIZE_BYTES) {
|
|
1139
|
+
const sizeMb = (fileSizeBytes / (1024 * 1024)).toFixed(1);
|
|
1140
|
+
throw new Error(`Input file is ${sizeMb} MB, which exceeds the 50 MB limit. Split the file into smaller parts and append each separately.`);
|
|
1141
|
+
}
|
|
849
1142
|
const { rows } = parseDatasetFile(file);
|
|
850
1143
|
if (!Array.isArray(rows) || rows.length === 0) {
|
|
851
1144
|
throw new Error('Dataset append file must contain at least one row');
|
|
852
1145
|
}
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
}
|
|
858
|
-
|
|
859
|
-
|
|
1146
|
+
if (rows.length <= APPEND_CHUNK_SIZE_ROWS) {
|
|
1147
|
+
const data = await appendChunk(datasetId, rows);
|
|
1148
|
+
console.log(`Appended ${data.appendedCount} rows to dataset ${data.dataset.name} (${data.dataset.id}). New row count: ${data.dataset.rowCount}`);
|
|
1149
|
+
return;
|
|
1150
|
+
}
|
|
1151
|
+
// Chunked upload for large row counts (ALI-555: track partial progress)
|
|
1152
|
+
let totalAppended = 0;
|
|
1153
|
+
let lastResult = null;
|
|
1154
|
+
const totalChunks = Math.ceil(rows.length / APPEND_CHUNK_SIZE_ROWS);
|
|
1155
|
+
for (let offset = 0; offset < rows.length; offset += APPEND_CHUNK_SIZE_ROWS) {
|
|
1156
|
+
const chunk = rows.slice(offset, offset + APPEND_CHUNK_SIZE_ROWS);
|
|
1157
|
+
const chunkIndex = Math.floor(offset / APPEND_CHUNK_SIZE_ROWS) + 1;
|
|
1158
|
+
console.log(`Uploading chunk ${chunkIndex}/${totalChunks} (${chunk.length} rows)...`);
|
|
1159
|
+
try {
|
|
1160
|
+
const data = await appendChunk(datasetId, chunk);
|
|
1161
|
+
totalAppended += data.appendedCount;
|
|
1162
|
+
lastResult = data;
|
|
1163
|
+
}
|
|
1164
|
+
catch (error) {
|
|
1165
|
+
const msg = error instanceof Error ? error.message : String(error);
|
|
1166
|
+
throw new Error(`Chunk ${chunkIndex}/${totalChunks} failed: ${msg}\n` +
|
|
1167
|
+
`${totalAppended} rows from ${chunkIndex - 1} chunk(s) were already appended. ` +
|
|
1168
|
+
`To retry, remove the first ${totalAppended} rows from your file and re-run the command.`);
|
|
1169
|
+
}
|
|
1170
|
+
}
|
|
1171
|
+
if (lastResult) {
|
|
1172
|
+
console.log(`Appended ${totalAppended} rows to dataset ${lastResult.dataset.name} (${lastResult.dataset.id}). New row count: ${lastResult.dataset.rowCount}`);
|
|
860
1173
|
}
|
|
861
|
-
const data = await parseJsonResponse(response, 'Dataset append');
|
|
862
|
-
console.log(`Appended ${data.appendedCount} rows to dataset ${data.dataset.name} (${data.dataset.id}). New row count: ${data.dataset.rowCount}`);
|
|
863
1174
|
}
|
|
864
1175
|
async function editDatasetRows() {
|
|
865
1176
|
const projectArg = getArg('--project');
|
|
@@ -1073,6 +1384,10 @@ async function main() {
|
|
|
1073
1384
|
await linkAppDataset();
|
|
1074
1385
|
return;
|
|
1075
1386
|
}
|
|
1387
|
+
if (command === 'apps' && subcommand === 'detail') {
|
|
1388
|
+
await appDetail();
|
|
1389
|
+
return;
|
|
1390
|
+
}
|
|
1076
1391
|
if (command === 'tasks' && subcommand === 'list') {
|
|
1077
1392
|
await listTasks();
|
|
1078
1393
|
return;
|
|
@@ -1089,6 +1404,14 @@ async function main() {
|
|
|
1089
1404
|
await taskStatus();
|
|
1090
1405
|
return;
|
|
1091
1406
|
}
|
|
1407
|
+
if (command === 'tasks' && subcommand === 'pause') {
|
|
1408
|
+
await updateTaskStatus('paused');
|
|
1409
|
+
return;
|
|
1410
|
+
}
|
|
1411
|
+
if (command === 'tasks' && subcommand === 'unpause') {
|
|
1412
|
+
await updateTaskStatus('active');
|
|
1413
|
+
return;
|
|
1414
|
+
}
|
|
1092
1415
|
if (command === 'datasets' && subcommand === 'upload') {
|
|
1093
1416
|
await uploadDataset();
|
|
1094
1417
|
return;
|