orizu 0.0.8 → 0.1.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/dist/credentials.js +10 -2
- package/dist/error-response.js +20 -0
- package/dist/index.js +324 -38
- 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 +469 -44
- 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
|
@@ -2,17 +2,19 @@
|
|
|
2
2
|
import { createHash, randomBytes } from 'crypto';
|
|
3
3
|
import { basename } 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
11
|
import { parseDatasetReference } from './dataset-download.js';
|
|
12
|
+
import { extractErrorMessage } from './error-response.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,44 @@ 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
|
+
process.exit(1);
|
|
704
|
+
}
|
|
705
|
+
throw cliError;
|
|
662
706
|
}
|
|
663
707
|
const data = await parseJsonResponse(response, 'Task create');
|
|
664
|
-
|
|
708
|
+
if (hasArg('--json')) {
|
|
709
|
+
console.log(JSON.stringify({
|
|
710
|
+
taskId: data.task.id,
|
|
711
|
+
datasetId,
|
|
712
|
+
versionId: data.task.versionId,
|
|
713
|
+
versionNum: data.task.versionNum,
|
|
714
|
+
taskUrl: `${getBaseUrl()}/d/${projectSlug}/tasks/${data.task.id}`,
|
|
715
|
+
title: data.task.title,
|
|
716
|
+
status: data.task.status,
|
|
717
|
+
requiredAssignmentsPerRow: data.task.requiredAssignmentsPerRow,
|
|
718
|
+
assignmentsCreated: data.assignmentsCreated,
|
|
719
|
+
...(data.assignmentShortfall !== undefined ? { assignmentShortfall: data.assignmentShortfall } : {}),
|
|
720
|
+
...(data.warning ? { warning: data.warning } : {}),
|
|
721
|
+
}, null, 2));
|
|
722
|
+
return;
|
|
723
|
+
}
|
|
724
|
+
const baseUrl = getBaseUrl();
|
|
725
|
+
const taskUrl = `${baseUrl}/d/${projectSlug}/tasks/${data.task.id}`;
|
|
726
|
+
console.log(`Created task ${data.task.title} (${data.task.id}) [${data.task.status}]` +
|
|
727
|
+
`\n Task ID: ${data.task.id}` +
|
|
728
|
+
`\n Dataset ID: ${datasetId}` +
|
|
729
|
+
`\n Version: v${data.task.versionNum} (${data.task.versionId})` +
|
|
730
|
+
`\n Labels/row: ${data.task.requiredAssignmentsPerRow}` +
|
|
731
|
+
`\n Assignments: ${data.assignmentsCreated}` +
|
|
732
|
+
(data.warning ? `\n Warning: ${data.warning}` : '') +
|
|
733
|
+
`\n URL: ${taskUrl}`);
|
|
665
734
|
}
|
|
666
735
|
async function assignTask() {
|
|
667
736
|
const taskId = getArg('--task');
|
|
@@ -687,7 +756,25 @@ async function taskStatus() {
|
|
|
687
756
|
}
|
|
688
757
|
const response = await authedFetch(`/api/cli/tasks/${encodeURIComponent(taskId)}/status`);
|
|
689
758
|
if (!response.ok) {
|
|
690
|
-
|
|
759
|
+
if (hasArg('--json')) {
|
|
760
|
+
const rawBody = await response.text();
|
|
761
|
+
let errorPayload = { error: rawBody };
|
|
762
|
+
try {
|
|
763
|
+
const parsed = JSON.parse(rawBody);
|
|
764
|
+
if (parsed && typeof parsed === 'object' && !Array.isArray(parsed)) {
|
|
765
|
+
errorPayload = parsed;
|
|
766
|
+
}
|
|
767
|
+
}
|
|
768
|
+
catch {
|
|
769
|
+
// keep raw body as error
|
|
770
|
+
}
|
|
771
|
+
console.log(JSON.stringify({
|
|
772
|
+
...errorPayload,
|
|
773
|
+
httpStatus: response.status,
|
|
774
|
+
}, null, 2));
|
|
775
|
+
process.exit(1);
|
|
776
|
+
}
|
|
777
|
+
throw new Error(`Failed to fetch task status: ${await extractErrorMessage(response)}`);
|
|
691
778
|
}
|
|
692
779
|
const data = await parseJsonResponse(response, 'Task status');
|
|
693
780
|
if (hasArg('--json')) {
|
|
@@ -696,6 +783,68 @@ async function taskStatus() {
|
|
|
696
783
|
}
|
|
697
784
|
printTaskStatusSummary(data);
|
|
698
785
|
}
|
|
786
|
+
async function updateTaskStatus(targetStatus) {
|
|
787
|
+
const taskId = getArg('--task');
|
|
788
|
+
if (!taskId) {
|
|
789
|
+
const verb = targetStatus === 'paused' ? 'pause' : 'unpause';
|
|
790
|
+
throw new Error(`Usage: orizu tasks ${verb} --task <taskId>`);
|
|
791
|
+
}
|
|
792
|
+
const response = await authedFetch(`/api/cli/tasks/${encodeURIComponent(taskId)}/status`, {
|
|
793
|
+
method: 'PATCH',
|
|
794
|
+
headers: { 'Content-Type': 'application/json' },
|
|
795
|
+
body: JSON.stringify({ status: targetStatus }),
|
|
796
|
+
});
|
|
797
|
+
if (!response.ok) {
|
|
798
|
+
const verb = targetStatus === 'paused' ? 'pause' : 'unpause';
|
|
799
|
+
throw new Error(`Failed to ${verb} task: ${await response.text()}`);
|
|
800
|
+
}
|
|
801
|
+
const data = await parseJsonResponse(response, 'Task status update');
|
|
802
|
+
const action = targetStatus === 'paused' ? 'Paused' : 'Unpaused';
|
|
803
|
+
console.log(`${action} task ${data.task.id} [${data.task.status}]`);
|
|
804
|
+
}
|
|
805
|
+
async function appDetail() {
|
|
806
|
+
const appId = getArg('--app');
|
|
807
|
+
const project = getArg('--project');
|
|
808
|
+
if (!appId) {
|
|
809
|
+
throw new Error('Usage: orizu apps detail --app <appId> [--project <team/project>] [--json]');
|
|
810
|
+
}
|
|
811
|
+
const projectSlug = project || await resolveProjectSlug(null);
|
|
812
|
+
const apps = await fetchApps(projectSlug);
|
|
813
|
+
const app = apps.find(a => a.id === appId);
|
|
814
|
+
if (!app) {
|
|
815
|
+
throw new Error(`App '${appId}' not found in project '${projectSlug}'`);
|
|
816
|
+
}
|
|
817
|
+
// Re-fetch full detail from the list (it already returns full data)
|
|
818
|
+
const detailResponse = await authedFetch(`/api/cli/apps?project=${encodeURIComponent(projectSlug)}`);
|
|
819
|
+
if (!detailResponse.ok) {
|
|
820
|
+
throw new Error(`Failed to fetch app detail: ${await detailResponse.text()}`);
|
|
821
|
+
}
|
|
822
|
+
const detailData = await parseJsonResponse(detailResponse, 'App detail');
|
|
823
|
+
const detail = detailData.apps.find(a => a.id === appId);
|
|
824
|
+
if (!detail) {
|
|
825
|
+
throw new Error(`App '${appId}' not found`);
|
|
826
|
+
}
|
|
827
|
+
if (hasArg('--json')) {
|
|
828
|
+
console.log(JSON.stringify(detail, null, 2));
|
|
829
|
+
return;
|
|
830
|
+
}
|
|
831
|
+
console.log(`App: ${detail.name} (${detail.id})`);
|
|
832
|
+
console.log(` Project: ${detail.teamSlug}/${detail.projectSlug}`);
|
|
833
|
+
if (detail.currentVersion) {
|
|
834
|
+
console.log(` Current version: v${detail.currentVersion.versionNum} (${detail.currentVersion.versionId})`);
|
|
835
|
+
console.log(` Input schema: ${detail.currentVersion.inputJsonSchema ? 'defined' : 'none'}`);
|
|
836
|
+
console.log(` Output schema: ${detail.currentVersion.outputJsonSchema ? 'defined' : 'none'}`);
|
|
837
|
+
}
|
|
838
|
+
else {
|
|
839
|
+
console.log(` Current version: none`);
|
|
840
|
+
}
|
|
841
|
+
console.log(` Compatible datasets: ${detail.compatibleDatasetsCount}/${detail.totalDatasetsCount}`);
|
|
842
|
+
if (detail.createdByEmail) {
|
|
843
|
+
console.log(` Created by: ${detail.createdByName || detail.createdByEmail}`);
|
|
844
|
+
}
|
|
845
|
+
console.log(` Created: ${detail.createdAt}`);
|
|
846
|
+
console.log(` Updated: ${detail.updatedAt}`);
|
|
847
|
+
}
|
|
699
848
|
async function listTeamMembers() {
|
|
700
849
|
const teamSlug = await resolveTeamSlug(getArg('--team'));
|
|
701
850
|
printTeamMembers(await fetchTeamMembers(teamSlug));
|
|
@@ -830,6 +979,19 @@ async function downloadDataset() {
|
|
|
830
979
|
writeFileSync(filename, bytes);
|
|
831
980
|
console.log(`Saved dataset ${datasetId} (${format.toUpperCase()}) to ${filename}`);
|
|
832
981
|
}
|
|
982
|
+
const MAX_INPUT_FILE_SIZE_BYTES = 50 * 1024 * 1024; // 50 MB
|
|
983
|
+
const APPEND_CHUNK_SIZE_ROWS = 500;
|
|
984
|
+
async function appendChunk(datasetId, rows) {
|
|
985
|
+
const response = await authedFetch(`/api/cli/datasets/${encodeURIComponent(datasetId)}/rows`, {
|
|
986
|
+
method: 'POST',
|
|
987
|
+
headers: { 'Content-Type': 'application/json' },
|
|
988
|
+
body: JSON.stringify({ rows }),
|
|
989
|
+
});
|
|
990
|
+
if (!response.ok) {
|
|
991
|
+
throw new Error(`Append failed: ${await response.text()}`);
|
|
992
|
+
}
|
|
993
|
+
return parseJsonResponse(response, 'Dataset append');
|
|
994
|
+
}
|
|
833
995
|
async function appendDatasetRows() {
|
|
834
996
|
const projectArg = getArg('--project');
|
|
835
997
|
const datasetInput = getDatasetReferenceInput();
|
|
@@ -846,43 +1008,98 @@ async function appendDatasetRows() {
|
|
|
846
1008
|
datasetId = selected.datasetId;
|
|
847
1009
|
}
|
|
848
1010
|
const file = expandHomePath(fileArg);
|
|
1011
|
+
// Parse the file first — parseDatasetFile provides user-friendly errors for
|
|
1012
|
+
// ENOENT, EPERM, and EACCES. We check the file size afterward using the
|
|
1013
|
+
// already-read content length to avoid a raw statSync ENOENT (ALI-554).
|
|
849
1014
|
const { rows } = parseDatasetFile(file);
|
|
850
1015
|
if (!Array.isArray(rows) || rows.length === 0) {
|
|
851
1016
|
throw new Error('Dataset append file must contain at least one row');
|
|
852
1017
|
}
|
|
1018
|
+
const fileSizeBytes = statSync(file).size;
|
|
1019
|
+
if (fileSizeBytes > MAX_INPUT_FILE_SIZE_BYTES) {
|
|
1020
|
+
const sizeMb = (fileSizeBytes / (1024 * 1024)).toFixed(1);
|
|
1021
|
+
throw new Error(`Input file is ${sizeMb} MB, which exceeds the 50 MB limit. Split the file into smaller parts and append each separately.`);
|
|
1022
|
+
}
|
|
1023
|
+
if (rows.length <= APPEND_CHUNK_SIZE_ROWS) {
|
|
1024
|
+
const data = await appendChunk(datasetId, rows);
|
|
1025
|
+
console.log(`Appended ${data.appendedCount} rows to dataset ${data.dataset.name} (${data.dataset.id}). New row count: ${data.dataset.rowCount}`);
|
|
1026
|
+
return;
|
|
1027
|
+
}
|
|
1028
|
+
// Chunked upload for large row counts (ALI-555: track partial progress)
|
|
1029
|
+
let totalAppended = 0;
|
|
1030
|
+
let lastResult = null;
|
|
1031
|
+
const totalChunks = Math.ceil(rows.length / APPEND_CHUNK_SIZE_ROWS);
|
|
1032
|
+
for (let offset = 0; offset < rows.length; offset += APPEND_CHUNK_SIZE_ROWS) {
|
|
1033
|
+
const chunk = rows.slice(offset, offset + APPEND_CHUNK_SIZE_ROWS);
|
|
1034
|
+
const chunkIndex = Math.floor(offset / APPEND_CHUNK_SIZE_ROWS) + 1;
|
|
1035
|
+
console.log(`Uploading chunk ${chunkIndex}/${totalChunks} (${chunk.length} rows)...`);
|
|
1036
|
+
try {
|
|
1037
|
+
const data = await appendChunk(datasetId, chunk);
|
|
1038
|
+
totalAppended += data.appendedCount;
|
|
1039
|
+
lastResult = data;
|
|
1040
|
+
}
|
|
1041
|
+
catch (error) {
|
|
1042
|
+
const msg = error instanceof Error ? error.message : String(error);
|
|
1043
|
+
throw new Error(`Chunk ${chunkIndex}/${totalChunks} failed: ${msg}\n` +
|
|
1044
|
+
`${totalAppended} rows from ${chunkIndex - 1} chunk(s) were already appended. ` +
|
|
1045
|
+
`To retry, remove the first ${totalAppended} rows from your file and re-run the command.`);
|
|
1046
|
+
}
|
|
1047
|
+
}
|
|
1048
|
+
if (lastResult) {
|
|
1049
|
+
console.log(`Appended ${totalAppended} rows to dataset ${lastResult.dataset.name} (${lastResult.dataset.id}). New row count: ${lastResult.dataset.rowCount}`);
|
|
1050
|
+
}
|
|
1051
|
+
}
|
|
1052
|
+
async function editDatasetRows() {
|
|
1053
|
+
const projectArg = getArg('--project');
|
|
1054
|
+
const datasetInput = getDatasetReferenceInput();
|
|
1055
|
+
const fileArg = getArg('--file');
|
|
1056
|
+
if (!fileArg) {
|
|
1057
|
+
throw new Error('Usage: orizu datasets edit-rows [--dataset <datasetId|datasetUrl>] [--project <team/project>] --file <path>');
|
|
1058
|
+
}
|
|
1059
|
+
let datasetId;
|
|
1060
|
+
if (datasetInput) {
|
|
1061
|
+
datasetId = parseDatasetReference(datasetInput).datasetId;
|
|
1062
|
+
}
|
|
1063
|
+
else {
|
|
1064
|
+
const selected = await selectDatasetInteractively(projectArg);
|
|
1065
|
+
datasetId = selected.datasetId;
|
|
1066
|
+
}
|
|
1067
|
+
const file = expandHomePath(fileArg);
|
|
1068
|
+
const { rows } = parseDatasetFile(file);
|
|
1069
|
+
if (!Array.isArray(rows) || rows.length === 0) {
|
|
1070
|
+
throw new Error('Dataset edit file must contain at least one row');
|
|
1071
|
+
}
|
|
1072
|
+
const normalizedRows = rows.map((row, index) => {
|
|
1073
|
+
if (typeof row !== 'object' || row === null || Array.isArray(row)) {
|
|
1074
|
+
throw new Error(`Dataset edit file rows[${index}] must be an object`);
|
|
1075
|
+
}
|
|
1076
|
+
const rowRecord = row;
|
|
1077
|
+
const rowId = typeof rowRecord.id === 'string' ? rowRecord.id.trim() : '';
|
|
1078
|
+
if (!rowId) {
|
|
1079
|
+
throw new Error(`Dataset edit file rows[${index}] must include a non-empty string id`);
|
|
1080
|
+
}
|
|
1081
|
+
return {
|
|
1082
|
+
...rowRecord,
|
|
1083
|
+
id: rowId,
|
|
1084
|
+
};
|
|
1085
|
+
});
|
|
853
1086
|
const response = await authedFetch(`/api/cli/datasets/${encodeURIComponent(datasetId)}/rows`, {
|
|
854
|
-
method: '
|
|
1087
|
+
method: 'PATCH',
|
|
855
1088
|
headers: { 'Content-Type': 'application/json' },
|
|
856
|
-
body: JSON.stringify({ rows }),
|
|
1089
|
+
body: JSON.stringify({ rows: normalizedRows }),
|
|
857
1090
|
});
|
|
858
1091
|
if (!response.ok) {
|
|
859
|
-
throw new Error(`
|
|
860
|
-
}
|
|
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
|
-
}
|
|
864
|
-
function parseCommaSeparatedIntegers(value) {
|
|
865
|
-
if (!value) {
|
|
866
|
-
return [];
|
|
1092
|
+
throw new Error(`Edit rows failed: ${await response.text()}`);
|
|
867
1093
|
}
|
|
868
|
-
const
|
|
869
|
-
|
|
870
|
-
.map(item => item.trim())
|
|
871
|
-
.filter(Boolean);
|
|
872
|
-
const parsed = rawItems.map(item => Number(item));
|
|
873
|
-
const invalid = parsed.some(item => !Number.isInteger(item) || item < 0);
|
|
874
|
-
if (invalid) {
|
|
875
|
-
throw new Error('row-indices must be comma-separated non-negative integers');
|
|
876
|
-
}
|
|
877
|
-
return parsed;
|
|
1094
|
+
const data = await parseJsonResponse(response, 'Dataset edit rows');
|
|
1095
|
+
console.log(`Updated ${data.updatedCount} rows in dataset ${data.dataset.name} (${data.dataset.id}). Current row count: ${data.dataset.rowCount}`);
|
|
878
1096
|
}
|
|
879
1097
|
async function deleteDatasetRows() {
|
|
880
1098
|
const projectArg = getArg('--project');
|
|
881
1099
|
const datasetInput = getDatasetReferenceInput();
|
|
882
1100
|
const rowIds = parseCommaSeparated(getArg('--row-ids'));
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
throw new Error('Usage: orizu datasets delete-rows [--dataset <datasetId|datasetUrl>] [--project <team/project>] [--row-ids <id1,id2>] [--row-indices <n1,n2>]');
|
|
1101
|
+
if (rowIds.length === 0) {
|
|
1102
|
+
throw new Error('Usage: orizu datasets delete-rows [--dataset <datasetId|datasetUrl>] [--project <team/project>] --row-ids <id1,id2>');
|
|
886
1103
|
}
|
|
887
1104
|
let datasetId;
|
|
888
1105
|
if (datasetInput) {
|
|
@@ -897,7 +1114,6 @@ async function deleteDatasetRows() {
|
|
|
897
1114
|
headers: { 'Content-Type': 'application/json' },
|
|
898
1115
|
body: JSON.stringify({
|
|
899
1116
|
rowIds,
|
|
900
|
-
rowIndices,
|
|
901
1117
|
}),
|
|
902
1118
|
});
|
|
903
1119
|
if (!response.ok) {
|
|
@@ -906,6 +1122,52 @@ async function deleteDatasetRows() {
|
|
|
906
1122
|
const data = await parseJsonResponse(response, 'Dataset delete rows');
|
|
907
1123
|
console.log(`Deleted ${data.deletedCount} rows from dataset ${data.dataset.name} (${data.dataset.id}). New row count: ${data.dataset.rowCount}`);
|
|
908
1124
|
}
|
|
1125
|
+
async function lockDataset() {
|
|
1126
|
+
const projectArg = getArg('--project');
|
|
1127
|
+
const datasetInput = getDatasetReferenceInput();
|
|
1128
|
+
const reason = getArg('--reason');
|
|
1129
|
+
let datasetId;
|
|
1130
|
+
if (datasetInput) {
|
|
1131
|
+
datasetId = parseDatasetReference(datasetInput).datasetId;
|
|
1132
|
+
}
|
|
1133
|
+
else {
|
|
1134
|
+
const selected = await selectDatasetInteractively(projectArg);
|
|
1135
|
+
datasetId = selected.datasetId;
|
|
1136
|
+
}
|
|
1137
|
+
const response = await authedFetch(`/api/cli/datasets/${encodeURIComponent(datasetId)}/lock`, {
|
|
1138
|
+
method: 'POST',
|
|
1139
|
+
headers: { 'Content-Type': 'application/json' },
|
|
1140
|
+
body: JSON.stringify(reason ? { reason } : {}),
|
|
1141
|
+
});
|
|
1142
|
+
if (!response.ok) {
|
|
1143
|
+
throw new Error(`Lock failed: ${await response.text()}`);
|
|
1144
|
+
}
|
|
1145
|
+
const data = await parseJsonResponse(response, 'Dataset lock');
|
|
1146
|
+
console.log(`Locked dataset ${data.dataset.name} (${data.dataset.id}) at ${data.dataset.lockedAt}. Row count: ${data.dataset.rowCount}`);
|
|
1147
|
+
}
|
|
1148
|
+
async function cloneDataset() {
|
|
1149
|
+
const projectArg = getArg('--project');
|
|
1150
|
+
const datasetInput = getDatasetReferenceInput();
|
|
1151
|
+
const name = getArg('--name');
|
|
1152
|
+
let datasetId;
|
|
1153
|
+
if (datasetInput) {
|
|
1154
|
+
datasetId = parseDatasetReference(datasetInput).datasetId;
|
|
1155
|
+
}
|
|
1156
|
+
else {
|
|
1157
|
+
const selected = await selectDatasetInteractively(projectArg);
|
|
1158
|
+
datasetId = selected.datasetId;
|
|
1159
|
+
}
|
|
1160
|
+
const response = await authedFetch(`/api/cli/datasets/${encodeURIComponent(datasetId)}/clone`, {
|
|
1161
|
+
method: 'POST',
|
|
1162
|
+
headers: { 'Content-Type': 'application/json' },
|
|
1163
|
+
body: JSON.stringify(name ? { name } : {}),
|
|
1164
|
+
});
|
|
1165
|
+
if (!response.ok) {
|
|
1166
|
+
throw new Error(`Clone failed: ${await response.text()}`);
|
|
1167
|
+
}
|
|
1168
|
+
const data = await parseJsonResponse(response, 'Dataset clone');
|
|
1169
|
+
console.log(`Cloned dataset ${data.dataset.parentDatasetId} -> ${data.dataset.name} (${data.dataset.id}). Row count: ${data.dataset.rowCount}`);
|
|
1170
|
+
}
|
|
909
1171
|
async function downloadAnnotations() {
|
|
910
1172
|
let taskId = getArg('--task');
|
|
911
1173
|
const format = (getArg('--format') || 'jsonl');
|
|
@@ -999,6 +1261,10 @@ async function main() {
|
|
|
999
1261
|
await linkAppDataset();
|
|
1000
1262
|
return;
|
|
1001
1263
|
}
|
|
1264
|
+
if (command === 'apps' && subcommand === 'detail') {
|
|
1265
|
+
await appDetail();
|
|
1266
|
+
return;
|
|
1267
|
+
}
|
|
1002
1268
|
if (command === 'tasks' && subcommand === 'list') {
|
|
1003
1269
|
await listTasks();
|
|
1004
1270
|
return;
|
|
@@ -1015,6 +1281,14 @@ async function main() {
|
|
|
1015
1281
|
await taskStatus();
|
|
1016
1282
|
return;
|
|
1017
1283
|
}
|
|
1284
|
+
if (command === 'tasks' && subcommand === 'pause') {
|
|
1285
|
+
await updateTaskStatus('paused');
|
|
1286
|
+
return;
|
|
1287
|
+
}
|
|
1288
|
+
if (command === 'tasks' && subcommand === 'unpause') {
|
|
1289
|
+
await updateTaskStatus('active');
|
|
1290
|
+
return;
|
|
1291
|
+
}
|
|
1018
1292
|
if (command === 'datasets' && subcommand === 'upload') {
|
|
1019
1293
|
await uploadDataset();
|
|
1020
1294
|
return;
|
|
@@ -1027,10 +1301,22 @@ async function main() {
|
|
|
1027
1301
|
await appendDatasetRows();
|
|
1028
1302
|
return;
|
|
1029
1303
|
}
|
|
1304
|
+
if (command === 'datasets' && subcommand === 'edit-rows') {
|
|
1305
|
+
await editDatasetRows();
|
|
1306
|
+
return;
|
|
1307
|
+
}
|
|
1030
1308
|
if (command === 'datasets' && subcommand === 'delete-rows') {
|
|
1031
1309
|
await deleteDatasetRows();
|
|
1032
1310
|
return;
|
|
1033
1311
|
}
|
|
1312
|
+
if (command === 'datasets' && subcommand === 'lock') {
|
|
1313
|
+
await lockDataset();
|
|
1314
|
+
return;
|
|
1315
|
+
}
|
|
1316
|
+
if (command === 'datasets' && subcommand === 'clone') {
|
|
1317
|
+
await cloneDataset();
|
|
1318
|
+
return;
|
|
1319
|
+
}
|
|
1034
1320
|
if (command === 'tasks' && subcommand === 'export') {
|
|
1035
1321
|
await downloadAnnotations();
|
|
1036
1322
|
return;
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
function createCliError(message, httpStatus, structuredPayload) {
|
|
2
|
+
const error = new Error(message);
|
|
3
|
+
error.httpStatus = httpStatus;
|
|
4
|
+
error.structuredPayload = structuredPayload;
|
|
5
|
+
return error;
|
|
6
|
+
}
|
|
7
|
+
export async function formatTaskCreateError(response) {
|
|
8
|
+
const contentType = response.headers.get('content-type') || '';
|
|
9
|
+
const rawBody = await response.text();
|
|
10
|
+
const httpStatus = response.status;
|
|
11
|
+
let parsedPayload;
|
|
12
|
+
if (!contentType.includes('application/json')) {
|
|
13
|
+
return createCliError(`Failed to create task: ${rawBody}`, httpStatus);
|
|
14
|
+
}
|
|
15
|
+
try {
|
|
16
|
+
const payload = JSON.parse(rawBody);
|
|
17
|
+
parsedPayload = payload;
|
|
18
|
+
if (Array.isArray(payload.invalidAssigneeErrors) &&
|
|
19
|
+
payload.invalidAssigneeErrors.length > 0) {
|
|
20
|
+
const details = payload.invalidAssigneeErrors
|
|
21
|
+
.filter(item => typeof item.assignee === 'string' &&
|
|
22
|
+
item.assignee.length > 0 &&
|
|
23
|
+
typeof item.reason === 'string' &&
|
|
24
|
+
item.reason.length > 0)
|
|
25
|
+
.map(item => ` - ${item.assignee}: ${item.reason}`)
|
|
26
|
+
.join('\n');
|
|
27
|
+
if (details) {
|
|
28
|
+
return createCliError(`Failed to create task: ${payload.error || 'Invalid assignees'}\n${details}`, httpStatus, parsedPayload);
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
if (Array.isArray(payload.missingRequiredFields) &&
|
|
32
|
+
payload.missingRequiredFields.length > 0) {
|
|
33
|
+
const lines = [
|
|
34
|
+
`Failed to create task: ${payload.error || 'Dataset incompatible'}`,
|
|
35
|
+
];
|
|
36
|
+
if (payload.versionNum !== undefined) {
|
|
37
|
+
lines.push(` Version: v${payload.versionNum} (${payload.versionId || 'unknown'})`);
|
|
38
|
+
}
|
|
39
|
+
lines.push(` Missing required fields: ${payload.missingRequiredFields.join(', ')}`);
|
|
40
|
+
if (typeof payload.incompatibleRowCount === 'number') {
|
|
41
|
+
lines.push(` Incompatible rows: ${payload.incompatibleRowCount}`);
|
|
42
|
+
}
|
|
43
|
+
if (payload.missingRequiredFieldCounts &&
|
|
44
|
+
Object.keys(payload.missingRequiredFieldCounts).length > 0) {
|
|
45
|
+
const fieldCounts = Object.entries(payload.missingRequiredFieldCounts)
|
|
46
|
+
.map(([field, count]) => ` ${field}: ${count} rows`)
|
|
47
|
+
.join('\n');
|
|
48
|
+
lines.push(` Missing field counts:\n${fieldCounts}`);
|
|
49
|
+
}
|
|
50
|
+
return createCliError(lines.join('\n'), httpStatus, parsedPayload);
|
|
51
|
+
}
|
|
52
|
+
if (typeof payload.error === 'string' && payload.error.length > 0) {
|
|
53
|
+
return createCliError(`Failed to create task: ${payload.error}`, httpStatus, parsedPayload);
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
catch {
|
|
57
|
+
return createCliError(`Failed to create task: ${rawBody}`, httpStatus);
|
|
58
|
+
}
|
|
59
|
+
return createCliError(`Failed to create task: ${rawBody}`, httpStatus, parsedPayload);
|
|
60
|
+
}
|