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/src/index.ts
CHANGED
|
@@ -2,15 +2,17 @@
|
|
|
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
|
import { LoginResponse } from './types.js'
|
|
15
17
|
|
|
16
18
|
interface Team {
|
|
@@ -106,7 +108,7 @@ interface TaskStatusPayload {
|
|
|
106
108
|
}
|
|
107
109
|
|
|
108
110
|
function printUsage() {
|
|
109
|
-
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 <
|
|
111
|
+
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>]`)
|
|
110
112
|
}
|
|
111
113
|
|
|
112
114
|
let cliArgs = process.argv.slice(2)
|
|
@@ -198,7 +200,6 @@ async function parseJsonResponse<T>(response: Response, context: string): Promis
|
|
|
198
200
|
)
|
|
199
201
|
}
|
|
200
202
|
}
|
|
201
|
-
|
|
202
203
|
async function promptSelect<T>(
|
|
203
204
|
title: string,
|
|
204
205
|
items: T[],
|
|
@@ -559,18 +560,26 @@ function printTeamMembers(members: TeamMember[]) {
|
|
|
559
560
|
|
|
560
561
|
const rows = members.map(member => ({
|
|
561
562
|
id: member.id,
|
|
563
|
+
userId: member.user_id || '-',
|
|
562
564
|
email: member.email || '-',
|
|
563
565
|
role: member.role || '-',
|
|
564
566
|
}))
|
|
565
567
|
|
|
566
568
|
const idWidth = Math.max('MEMBER ID'.length, ...rows.map(row => row.id.length))
|
|
569
|
+
const userIdWidth = Math.max('USER ID'.length, ...rows.map(row => row.userId.length))
|
|
567
570
|
const emailWidth = Math.max('EMAIL'.length, ...rows.map(row => row.email.length))
|
|
568
571
|
const roleWidth = Math.max('ROLE'.length, ...rows.map(row => row.role.length))
|
|
569
572
|
|
|
570
|
-
console.log(
|
|
571
|
-
|
|
573
|
+
console.log(
|
|
574
|
+
`${'MEMBER ID'.padEnd(idWidth)} ${'USER ID'.padEnd(userIdWidth)} ${'EMAIL'.padEnd(emailWidth)} ${'ROLE'.padEnd(roleWidth)}`
|
|
575
|
+
)
|
|
576
|
+
console.log(
|
|
577
|
+
`${'-'.repeat(idWidth)} ${'-'.repeat(userIdWidth)} ${'-'.repeat(emailWidth)} ${'-'.repeat(roleWidth)}`
|
|
578
|
+
)
|
|
572
579
|
rows.forEach(row => {
|
|
573
|
-
console.log(
|
|
580
|
+
console.log(
|
|
581
|
+
`${row.id.padEnd(idWidth)} ${row.userId.padEnd(userIdWidth)} ${row.email.padEnd(emailWidth)} ${row.role.padEnd(roleWidth)}`
|
|
582
|
+
)
|
|
574
583
|
})
|
|
575
584
|
}
|
|
576
585
|
|
|
@@ -593,15 +602,34 @@ function printTaskStatusSummary(data: TaskStatusPayload) {
|
|
|
593
602
|
}
|
|
594
603
|
}
|
|
595
604
|
|
|
605
|
+
const DEFAULT_AUTH_CALLBACK_PORT = 43123
|
|
606
|
+
|
|
607
|
+
function resolveAuthCallbackPort(): number {
|
|
608
|
+
const envPort = process.env.ORIZU_AUTH_PORT
|
|
609
|
+
if (!envPort) {
|
|
610
|
+
return DEFAULT_AUTH_CALLBACK_PORT
|
|
611
|
+
}
|
|
612
|
+
|
|
613
|
+
const parsed = parseInt(envPort, 10)
|
|
614
|
+
if (Number.isNaN(parsed) || parsed < 1024 || parsed > 65535) {
|
|
615
|
+
throw new Error(
|
|
616
|
+
`Invalid ORIZU_AUTH_PORT: '${envPort}'. Must be a number between 1024 and 65535.`
|
|
617
|
+
)
|
|
618
|
+
}
|
|
619
|
+
|
|
620
|
+
return parsed
|
|
621
|
+
}
|
|
622
|
+
|
|
596
623
|
async function login() {
|
|
597
624
|
const baseUrl = resolveLoginBaseUrl()
|
|
598
625
|
const codeVerifier = createCodeVerifier()
|
|
599
626
|
const codeChallenge = createCodeChallenge(codeVerifier)
|
|
627
|
+
const callbackPort = resolveAuthCallbackPort()
|
|
600
628
|
|
|
601
629
|
const callbackCode = await new Promise<string>((resolve, reject) => {
|
|
602
630
|
const server = createServer((request, response) => {
|
|
603
631
|
try {
|
|
604
|
-
const url = new URL(request.url || '/',
|
|
632
|
+
const url = new URL(request.url || '/', `http://127.0.0.1:${callbackPort}`)
|
|
605
633
|
const code = url.searchParams.get('code')
|
|
606
634
|
|
|
607
635
|
if (!code) {
|
|
@@ -622,11 +650,19 @@ async function login() {
|
|
|
622
650
|
}
|
|
623
651
|
})
|
|
624
652
|
|
|
625
|
-
server.on('error',
|
|
653
|
+
server.on('error', (error: NodeJS.ErrnoException) => {
|
|
654
|
+
if (error.code === 'EADDRINUSE') {
|
|
655
|
+
reject(new Error(
|
|
656
|
+
`Port ${callbackPort} is already in use. Set ORIZU_AUTH_PORT to a different port (1024–65535) and retry.`
|
|
657
|
+
))
|
|
658
|
+
} else {
|
|
659
|
+
reject(error)
|
|
660
|
+
}
|
|
661
|
+
})
|
|
626
662
|
|
|
627
|
-
server.listen(
|
|
663
|
+
server.listen(callbackPort, '127.0.0.1', async () => {
|
|
628
664
|
try {
|
|
629
|
-
const redirectUri =
|
|
665
|
+
const redirectUri = `http://127.0.0.1:${callbackPort}/callback`
|
|
630
666
|
const response = await fetch(`${baseUrl}/api/cli/auth/start`, {
|
|
631
667
|
method: 'POST',
|
|
632
668
|
headers: { 'Content-Type': 'application/json' },
|
|
@@ -921,12 +957,18 @@ async function linkAppDataset() {
|
|
|
921
957
|
const project = getArg('--project')
|
|
922
958
|
let appId = getArg('--app')
|
|
923
959
|
const versionArg = getArg('--version')
|
|
924
|
-
const
|
|
960
|
+
const parsedVersionNum = versionArg ? Number(versionArg) : Number.NaN
|
|
961
|
+
const versionNum =
|
|
962
|
+
Number.isInteger(parsedVersionNum) && parsedVersionNum > 0 ? parsedVersionNum : undefined
|
|
925
963
|
|
|
926
964
|
if (!datasetId) {
|
|
927
965
|
throw new Error('Usage: orizu apps link-dataset --dataset <datasetId> [--app <appId>] [--project <team/project>] [--version <n>]')
|
|
928
966
|
}
|
|
929
967
|
|
|
968
|
+
if (versionArg && versionNum === undefined) {
|
|
969
|
+
throw new Error('--version must be a positive integer')
|
|
970
|
+
}
|
|
971
|
+
|
|
930
972
|
if (!appId) {
|
|
931
973
|
const selected = await selectAppIdInteractively(project)
|
|
932
974
|
appId = selected.appId
|
|
@@ -972,12 +1014,20 @@ async function createTask() {
|
|
|
972
1014
|
const appId = getArg('--app')
|
|
973
1015
|
const title = getArg('--title')
|
|
974
1016
|
const assignees = parseCommaSeparated(getArg('--assignees'))
|
|
1017
|
+
const versionArg = getArg('--version')
|
|
975
1018
|
const instructions = getArg('--instructions')
|
|
976
1019
|
const labelsPerItemArg = getArg('--labels-per-item')
|
|
977
1020
|
const labelsPerItem = labelsPerItemArg ? Number(labelsPerItemArg) : 1
|
|
1021
|
+
const parsedVersionNum = versionArg ? Number(versionArg) : Number.NaN
|
|
1022
|
+
const versionNum =
|
|
1023
|
+
Number.isInteger(parsedVersionNum) && parsedVersionNum > 0 ? parsedVersionNum : null
|
|
978
1024
|
|
|
979
1025
|
if (!projectSlug || !datasetId || !appId || !title || assignees.length === 0) {
|
|
980
|
-
throw new Error('Usage: orizu tasks create --project <team/project> --dataset <datasetId> --app <appId> --title <title> --assignees <
|
|
1026
|
+
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]')
|
|
1027
|
+
}
|
|
1028
|
+
|
|
1029
|
+
if (versionArg && versionNum === null) {
|
|
1030
|
+
throw new Error('--version must be a positive integer')
|
|
981
1031
|
}
|
|
982
1032
|
|
|
983
1033
|
const response = await authedFetch('/api/cli/tasks', {
|
|
@@ -987,6 +1037,7 @@ async function createTask() {
|
|
|
987
1037
|
projectSlug,
|
|
988
1038
|
datasetId,
|
|
989
1039
|
appId,
|
|
1040
|
+
versionNum,
|
|
990
1041
|
title,
|
|
991
1042
|
memberIds: assignees,
|
|
992
1043
|
instructions,
|
|
@@ -995,15 +1046,59 @@ async function createTask() {
|
|
|
995
1046
|
})
|
|
996
1047
|
|
|
997
1048
|
if (!response.ok) {
|
|
998
|
-
|
|
1049
|
+
const cliError = await formatTaskCreateError(response)
|
|
1050
|
+
if (hasArg('--json')) {
|
|
1051
|
+
const payload = cliError.structuredPayload ?? { error: cliError.message }
|
|
1052
|
+
console.log(JSON.stringify({
|
|
1053
|
+
...payload,
|
|
1054
|
+
httpStatus: cliError.httpStatus,
|
|
1055
|
+
}, null, 2))
|
|
1056
|
+
}
|
|
1057
|
+
throw cliError
|
|
999
1058
|
}
|
|
1000
1059
|
|
|
1001
1060
|
const data = await parseJsonResponse<{
|
|
1002
|
-
task: {
|
|
1061
|
+
task: {
|
|
1062
|
+
id: string
|
|
1063
|
+
title: string
|
|
1064
|
+
status: string
|
|
1065
|
+
requiredAssignmentsPerRow: number
|
|
1066
|
+
versionId: string
|
|
1067
|
+
versionNum: number
|
|
1068
|
+
}
|
|
1003
1069
|
assignmentsCreated: number
|
|
1070
|
+
assignmentShortfall?: number
|
|
1071
|
+
warning?: string
|
|
1004
1072
|
}>(response, 'Task create')
|
|
1073
|
+
|
|
1074
|
+
if (hasArg('--json')) {
|
|
1075
|
+
console.log(JSON.stringify({
|
|
1076
|
+
taskId: data.task.id,
|
|
1077
|
+
datasetId,
|
|
1078
|
+
versionId: data.task.versionId,
|
|
1079
|
+
versionNum: data.task.versionNum,
|
|
1080
|
+
taskUrl: `${getBaseUrl()}/d/${projectSlug}/tasks/${data.task.id}`,
|
|
1081
|
+
title: data.task.title,
|
|
1082
|
+
status: data.task.status,
|
|
1083
|
+
requiredAssignmentsPerRow: data.task.requiredAssignmentsPerRow,
|
|
1084
|
+
assignmentsCreated: data.assignmentsCreated,
|
|
1085
|
+
...(data.assignmentShortfall !== undefined ? { assignmentShortfall: data.assignmentShortfall } : {}),
|
|
1086
|
+
...(data.warning ? { warning: data.warning } : {}),
|
|
1087
|
+
}, null, 2))
|
|
1088
|
+
return
|
|
1089
|
+
}
|
|
1090
|
+
|
|
1091
|
+
const baseUrl = getBaseUrl()
|
|
1092
|
+
const taskUrl = `${baseUrl}/d/${projectSlug}/tasks/${data.task.id}`
|
|
1005
1093
|
console.log(
|
|
1006
|
-
`Created task ${data.task.title} (${data.task.id}) [${data.task.status}]
|
|
1094
|
+
`Created task ${data.task.title} (${data.task.id}) [${data.task.status}]` +
|
|
1095
|
+
`\n Task ID: ${data.task.id}` +
|
|
1096
|
+
`\n Dataset ID: ${datasetId}` +
|
|
1097
|
+
`\n Version: v${data.task.versionNum} (${data.task.versionId})` +
|
|
1098
|
+
`\n Labels/row: ${data.task.requiredAssignmentsPerRow}` +
|
|
1099
|
+
`\n Assignments: ${data.assignmentsCreated}` +
|
|
1100
|
+
(data.warning ? `\n Warning: ${data.warning}` : '') +
|
|
1101
|
+
`\n URL: ${taskUrl}`
|
|
1007
1102
|
)
|
|
1008
1103
|
}
|
|
1009
1104
|
|
|
@@ -1037,7 +1132,31 @@ async function taskStatus() {
|
|
|
1037
1132
|
|
|
1038
1133
|
const response = await authedFetch(`/api/cli/tasks/${encodeURIComponent(taskId)}/status`)
|
|
1039
1134
|
if (!response.ok) {
|
|
1040
|
-
|
|
1135
|
+
const rawBody = await response.text()
|
|
1136
|
+
if (hasArg('--json')) {
|
|
1137
|
+
let errorPayload: Record<string, unknown> = { error: rawBody }
|
|
1138
|
+
try {
|
|
1139
|
+
const parsed = JSON.parse(rawBody)
|
|
1140
|
+
if (parsed && typeof parsed === 'object' && !Array.isArray(parsed)) {
|
|
1141
|
+
errorPayload = parsed as Record<string, unknown>
|
|
1142
|
+
}
|
|
1143
|
+
} catch {
|
|
1144
|
+
// keep raw body as error
|
|
1145
|
+
}
|
|
1146
|
+
console.log(JSON.stringify({
|
|
1147
|
+
...errorPayload,
|
|
1148
|
+
httpStatus: response.status,
|
|
1149
|
+
}, null, 2))
|
|
1150
|
+
}
|
|
1151
|
+
|
|
1152
|
+
let errorMsg = rawBody
|
|
1153
|
+
try {
|
|
1154
|
+
const parsed = JSON.parse(rawBody)
|
|
1155
|
+
if (parsed && typeof parsed === 'object' && typeof parsed.error === 'string') {
|
|
1156
|
+
errorMsg = parsed.error
|
|
1157
|
+
}
|
|
1158
|
+
} catch { /* use rawBody as-is */ }
|
|
1159
|
+
throw new Error(`Failed to fetch task status: ${errorMsg}`)
|
|
1041
1160
|
}
|
|
1042
1161
|
|
|
1043
1162
|
const data = await parseJsonResponse<TaskStatusPayload>(response, 'Task status')
|
|
@@ -1049,6 +1168,97 @@ async function taskStatus() {
|
|
|
1049
1168
|
printTaskStatusSummary(data)
|
|
1050
1169
|
}
|
|
1051
1170
|
|
|
1171
|
+
async function updateTaskStatus(targetStatus: 'paused' | 'active') {
|
|
1172
|
+
const taskId = getArg('--task')
|
|
1173
|
+
if (!taskId) {
|
|
1174
|
+
const verb = targetStatus === 'paused' ? 'pause' : 'unpause'
|
|
1175
|
+
throw new Error(`Usage: orizu tasks ${verb} --task <taskId>`)
|
|
1176
|
+
}
|
|
1177
|
+
|
|
1178
|
+
const response = await authedFetch(`/api/cli/tasks/${encodeURIComponent(taskId)}/status`, {
|
|
1179
|
+
method: 'PATCH',
|
|
1180
|
+
headers: { 'Content-Type': 'application/json' },
|
|
1181
|
+
body: JSON.stringify({ status: targetStatus }),
|
|
1182
|
+
})
|
|
1183
|
+
|
|
1184
|
+
if (!response.ok) {
|
|
1185
|
+
const verb = targetStatus === 'paused' ? 'pause' : 'unpause'
|
|
1186
|
+
throw new Error(`Failed to ${verb} task: ${await response.text()}`)
|
|
1187
|
+
}
|
|
1188
|
+
|
|
1189
|
+
const data = await parseJsonResponse<{ task: { id: string; status: string } }>(response, 'Task status update')
|
|
1190
|
+
const action = targetStatus === 'paused' ? 'Paused' : 'Unpaused'
|
|
1191
|
+
console.log(`${action} task ${data.task.id} [${data.task.status}]`)
|
|
1192
|
+
}
|
|
1193
|
+
|
|
1194
|
+
interface AppDetailPayload {
|
|
1195
|
+
id: string
|
|
1196
|
+
name: string
|
|
1197
|
+
currentVersionNum: number | null
|
|
1198
|
+
currentVersion: {
|
|
1199
|
+
versionId: string
|
|
1200
|
+
versionNum: number
|
|
1201
|
+
inputJsonSchema: unknown
|
|
1202
|
+
outputJsonSchema: unknown
|
|
1203
|
+
} | null
|
|
1204
|
+
createdAt: string
|
|
1205
|
+
updatedAt: string
|
|
1206
|
+
projectId: string | null
|
|
1207
|
+
compatibleDatasetsCount: number
|
|
1208
|
+
totalDatasetsCount: number
|
|
1209
|
+
createdByName: string | null
|
|
1210
|
+
createdByEmail: string | null
|
|
1211
|
+
teamSlug: string
|
|
1212
|
+
teamName: string
|
|
1213
|
+
projectSlug: string
|
|
1214
|
+
projectName: string
|
|
1215
|
+
}
|
|
1216
|
+
|
|
1217
|
+
async function appDetail() {
|
|
1218
|
+
const appId = getArg('--app')
|
|
1219
|
+
const project = getArg('--project')
|
|
1220
|
+
|
|
1221
|
+
if (!appId) {
|
|
1222
|
+
throw new Error('Usage: orizu apps detail --app <appId> [--project <team/project>] [--json]')
|
|
1223
|
+
}
|
|
1224
|
+
|
|
1225
|
+
const projectSlug = project || await resolveProjectSlug(null)
|
|
1226
|
+
|
|
1227
|
+
// Single fetch — the apps endpoint already returns full detail (ALI-544)
|
|
1228
|
+
const detailResponse = await authedFetch(`/api/cli/apps?project=${encodeURIComponent(projectSlug)}`)
|
|
1229
|
+
if (!detailResponse.ok) {
|
|
1230
|
+
throw new Error(`Failed to fetch app detail: ${await detailResponse.text()}`)
|
|
1231
|
+
}
|
|
1232
|
+
|
|
1233
|
+
const detailData = await parseJsonResponse<{ apps: AppDetailPayload[] }>(detailResponse, 'App detail')
|
|
1234
|
+
const detail = detailData.apps.find(a => a.id === appId)
|
|
1235
|
+
|
|
1236
|
+
if (!detail) {
|
|
1237
|
+
throw new Error(`App '${appId}' not found in project '${projectSlug}'`)
|
|
1238
|
+
}
|
|
1239
|
+
|
|
1240
|
+
if (hasArg('--json')) {
|
|
1241
|
+
console.log(JSON.stringify(detail, null, 2))
|
|
1242
|
+
return
|
|
1243
|
+
}
|
|
1244
|
+
|
|
1245
|
+
console.log(`App: ${detail.name} (${detail.id})`)
|
|
1246
|
+
console.log(` Project: ${detail.teamSlug}/${detail.projectSlug}`)
|
|
1247
|
+
if (detail.currentVersion) {
|
|
1248
|
+
console.log(` Current version: v${detail.currentVersion.versionNum} (${detail.currentVersion.versionId})`)
|
|
1249
|
+
console.log(` Input schema: ${detail.currentVersion.inputJsonSchema ? 'defined' : 'none'}`)
|
|
1250
|
+
console.log(` Output schema: ${detail.currentVersion.outputJsonSchema ? 'defined' : 'none'}`)
|
|
1251
|
+
} else {
|
|
1252
|
+
console.log(` Current version: none`)
|
|
1253
|
+
}
|
|
1254
|
+
console.log(` Compatible datasets: ${detail.compatibleDatasetsCount}/${detail.totalDatasetsCount}`)
|
|
1255
|
+
if (detail.createdByEmail) {
|
|
1256
|
+
console.log(` Created by: ${detail.createdByName || detail.createdByEmail}`)
|
|
1257
|
+
}
|
|
1258
|
+
console.log(` Created: ${detail.createdAt}`)
|
|
1259
|
+
console.log(` Updated: ${detail.updatedAt}`)
|
|
1260
|
+
}
|
|
1261
|
+
|
|
1052
1262
|
async function listTeamMembers() {
|
|
1053
1263
|
const teamSlug = await resolveTeamSlug(getArg('--team'))
|
|
1054
1264
|
|
|
@@ -1222,6 +1432,29 @@ async function downloadDataset() {
|
|
|
1222
1432
|
console.log(`Saved dataset ${datasetId} (${format.toUpperCase()}) to ${filename}`)
|
|
1223
1433
|
}
|
|
1224
1434
|
|
|
1435
|
+
const MAX_INPUT_FILE_SIZE_BYTES = 50 * 1024 * 1024 // 50 MB
|
|
1436
|
+
const APPEND_CHUNK_SIZE_ROWS = 500
|
|
1437
|
+
|
|
1438
|
+
async function appendChunk(
|
|
1439
|
+
datasetId: string,
|
|
1440
|
+
rows: Array<Record<string, unknown>>
|
|
1441
|
+
): Promise<{ dataset: { id: string; name: string; rowCount: number }; appendedCount: number }> {
|
|
1442
|
+
const response = await authedFetch(`/api/cli/datasets/${encodeURIComponent(datasetId)}/rows`, {
|
|
1443
|
+
method: 'POST',
|
|
1444
|
+
headers: { 'Content-Type': 'application/json' },
|
|
1445
|
+
body: JSON.stringify({ rows }),
|
|
1446
|
+
})
|
|
1447
|
+
|
|
1448
|
+
if (!response.ok) {
|
|
1449
|
+
throw new Error(`Append failed: ${await response.text()}`)
|
|
1450
|
+
}
|
|
1451
|
+
|
|
1452
|
+
return parseJsonResponse<{
|
|
1453
|
+
dataset: { id: string; name: string; rowCount: number }
|
|
1454
|
+
appendedCount: number
|
|
1455
|
+
}>(response, 'Dataset append')
|
|
1456
|
+
}
|
|
1457
|
+
|
|
1225
1458
|
async function appendDatasetRows() {
|
|
1226
1459
|
const projectArg = getArg('--project')
|
|
1227
1460
|
const datasetInput = getDatasetReferenceInput()
|
|
@@ -1240,57 +1473,145 @@ async function appendDatasetRows() {
|
|
|
1240
1473
|
}
|
|
1241
1474
|
|
|
1242
1475
|
const file = expandHomePath(fileArg)
|
|
1476
|
+
|
|
1477
|
+
// Check file size before reading to prevent OOM on large files (ALI-565).
|
|
1478
|
+
// Wrap statSync in try/catch so missing/inaccessible files get friendly
|
|
1479
|
+
// errors instead of raw Node.js ENOENT/EPERM (ALI-554).
|
|
1480
|
+
let fileSizeBytes: number
|
|
1481
|
+
try {
|
|
1482
|
+
fileSizeBytes = statSync(file).size
|
|
1483
|
+
} catch (error) {
|
|
1484
|
+
const maybeError = error as NodeJS.ErrnoException
|
|
1485
|
+
if (maybeError.code === 'ENOENT') {
|
|
1486
|
+
throw new Error(
|
|
1487
|
+
`File not found: ${file}. Check the path and filename, then retry.`
|
|
1488
|
+
)
|
|
1489
|
+
}
|
|
1490
|
+
if (maybeError.code === 'EPERM' || maybeError.code === 'EACCES') {
|
|
1491
|
+
throw new Error(
|
|
1492
|
+
`Cannot read file: ${file}. Grant folder permission to your terminal app and retry.`
|
|
1493
|
+
)
|
|
1494
|
+
}
|
|
1495
|
+
throw new Error(`Failed to access file ${file}: ${maybeError.message}`)
|
|
1496
|
+
}
|
|
1497
|
+
if (fileSizeBytes > MAX_INPUT_FILE_SIZE_BYTES) {
|
|
1498
|
+
const sizeMb = (fileSizeBytes / (1024 * 1024)).toFixed(1)
|
|
1499
|
+
throw new Error(
|
|
1500
|
+
`Input file is ${sizeMb} MB, which exceeds the 50 MB limit. Split the file into smaller parts and append each separately.`
|
|
1501
|
+
)
|
|
1502
|
+
}
|
|
1503
|
+
|
|
1243
1504
|
const { rows } = parseDatasetFile(file)
|
|
1244
1505
|
if (!Array.isArray(rows) || rows.length === 0) {
|
|
1245
1506
|
throw new Error('Dataset append file must contain at least one row')
|
|
1246
1507
|
}
|
|
1247
1508
|
|
|
1509
|
+
if (rows.length <= APPEND_CHUNK_SIZE_ROWS) {
|
|
1510
|
+
const data = await appendChunk(datasetId, rows)
|
|
1511
|
+
console.log(
|
|
1512
|
+
`Appended ${data.appendedCount} rows to dataset ${data.dataset.name} (${data.dataset.id}). New row count: ${data.dataset.rowCount}`
|
|
1513
|
+
)
|
|
1514
|
+
return
|
|
1515
|
+
}
|
|
1516
|
+
|
|
1517
|
+
// Chunked upload for large row counts (ALI-555: track partial progress)
|
|
1518
|
+
let totalAppended = 0
|
|
1519
|
+
let lastResult: { dataset: { id: string; name: string; rowCount: number } } | null = null
|
|
1520
|
+
const totalChunks = Math.ceil(rows.length / APPEND_CHUNK_SIZE_ROWS)
|
|
1521
|
+
|
|
1522
|
+
for (let offset = 0; offset < rows.length; offset += APPEND_CHUNK_SIZE_ROWS) {
|
|
1523
|
+
const chunk = rows.slice(offset, offset + APPEND_CHUNK_SIZE_ROWS)
|
|
1524
|
+
const chunkIndex = Math.floor(offset / APPEND_CHUNK_SIZE_ROWS) + 1
|
|
1525
|
+
|
|
1526
|
+
console.log(`Uploading chunk ${chunkIndex}/${totalChunks} (${chunk.length} rows)...`)
|
|
1527
|
+
try {
|
|
1528
|
+
const data = await appendChunk(datasetId, chunk)
|
|
1529
|
+
totalAppended += data.appendedCount
|
|
1530
|
+
lastResult = data
|
|
1531
|
+
} catch (error) {
|
|
1532
|
+
const msg = error instanceof Error ? error.message : String(error)
|
|
1533
|
+
throw new Error(
|
|
1534
|
+
`Chunk ${chunkIndex}/${totalChunks} failed: ${msg}\n` +
|
|
1535
|
+
`${totalAppended} rows from ${chunkIndex - 1} chunk(s) were already appended. ` +
|
|
1536
|
+
`To retry, remove the first ${totalAppended} rows from your file and re-run the command.`
|
|
1537
|
+
)
|
|
1538
|
+
}
|
|
1539
|
+
}
|
|
1540
|
+
|
|
1541
|
+
if (lastResult) {
|
|
1542
|
+
console.log(
|
|
1543
|
+
`Appended ${totalAppended} rows to dataset ${lastResult.dataset.name} (${lastResult.dataset.id}). New row count: ${lastResult.dataset.rowCount}`
|
|
1544
|
+
)
|
|
1545
|
+
}
|
|
1546
|
+
}
|
|
1547
|
+
|
|
1548
|
+
async function editDatasetRows() {
|
|
1549
|
+
const projectArg = getArg('--project')
|
|
1550
|
+
const datasetInput = getDatasetReferenceInput()
|
|
1551
|
+
const fileArg = getArg('--file')
|
|
1552
|
+
|
|
1553
|
+
if (!fileArg) {
|
|
1554
|
+
throw new Error('Usage: orizu datasets edit-rows [--dataset <datasetId|datasetUrl>] [--project <team/project>] --file <path>')
|
|
1555
|
+
}
|
|
1556
|
+
|
|
1557
|
+
let datasetId: string
|
|
1558
|
+
if (datasetInput) {
|
|
1559
|
+
datasetId = parseDatasetReference(datasetInput).datasetId
|
|
1560
|
+
} else {
|
|
1561
|
+
const selected = await selectDatasetInteractively(projectArg)
|
|
1562
|
+
datasetId = selected.datasetId
|
|
1563
|
+
}
|
|
1564
|
+
|
|
1565
|
+
const file = expandHomePath(fileArg)
|
|
1566
|
+
const { rows } = parseDatasetFile(file)
|
|
1567
|
+
if (!Array.isArray(rows) || rows.length === 0) {
|
|
1568
|
+
throw new Error('Dataset edit file must contain at least one row')
|
|
1569
|
+
}
|
|
1570
|
+
|
|
1571
|
+
const normalizedRows = rows.map((row, index) => {
|
|
1572
|
+
if (typeof row !== 'object' || row === null || Array.isArray(row)) {
|
|
1573
|
+
throw new Error(`Dataset edit file rows[${index}] must be an object`)
|
|
1574
|
+
}
|
|
1575
|
+
|
|
1576
|
+
const rowRecord = row as Record<string, unknown>
|
|
1577
|
+
const rowId = typeof rowRecord.id === 'string' ? rowRecord.id.trim() : ''
|
|
1578
|
+
if (!rowId) {
|
|
1579
|
+
throw new Error(`Dataset edit file rows[${index}] must include a non-empty string id`)
|
|
1580
|
+
}
|
|
1581
|
+
|
|
1582
|
+
return {
|
|
1583
|
+
...rowRecord,
|
|
1584
|
+
id: rowId,
|
|
1585
|
+
}
|
|
1586
|
+
})
|
|
1587
|
+
|
|
1248
1588
|
const response = await authedFetch(`/api/cli/datasets/${encodeURIComponent(datasetId)}/rows`, {
|
|
1249
|
-
method: '
|
|
1589
|
+
method: 'PATCH',
|
|
1250
1590
|
headers: { 'Content-Type': 'application/json' },
|
|
1251
|
-
body: JSON.stringify({ rows }),
|
|
1591
|
+
body: JSON.stringify({ rows: normalizedRows }),
|
|
1252
1592
|
})
|
|
1253
1593
|
|
|
1254
1594
|
if (!response.ok) {
|
|
1255
|
-
throw new Error(`
|
|
1595
|
+
throw new Error(`Edit rows failed: ${await response.text()}`)
|
|
1256
1596
|
}
|
|
1257
1597
|
|
|
1258
1598
|
const data = await parseJsonResponse<{
|
|
1259
1599
|
dataset: { id: string; name: string; rowCount: number }
|
|
1260
|
-
|
|
1261
|
-
}>(response, 'Dataset
|
|
1600
|
+
updatedCount: number
|
|
1601
|
+
}>(response, 'Dataset edit rows')
|
|
1262
1602
|
|
|
1263
1603
|
console.log(
|
|
1264
|
-
`
|
|
1604
|
+
`Updated ${data.updatedCount} rows in dataset ${data.dataset.name} (${data.dataset.id}). Current row count: ${data.dataset.rowCount}`
|
|
1265
1605
|
)
|
|
1266
1606
|
}
|
|
1267
1607
|
|
|
1268
|
-
function parseCommaSeparatedIntegers(value: string | null): number[] {
|
|
1269
|
-
if (!value) {
|
|
1270
|
-
return []
|
|
1271
|
-
}
|
|
1272
|
-
|
|
1273
|
-
const rawItems = value
|
|
1274
|
-
.split(',')
|
|
1275
|
-
.map(item => item.trim())
|
|
1276
|
-
.filter(Boolean)
|
|
1277
|
-
const parsed = rawItems.map(item => Number(item))
|
|
1278
|
-
const invalid = parsed.some(item => !Number.isInteger(item) || item < 0)
|
|
1279
|
-
if (invalid) {
|
|
1280
|
-
throw new Error('row-indices must be comma-separated non-negative integers')
|
|
1281
|
-
}
|
|
1282
|
-
|
|
1283
|
-
return parsed
|
|
1284
|
-
}
|
|
1285
|
-
|
|
1286
1608
|
async function deleteDatasetRows() {
|
|
1287
1609
|
const projectArg = getArg('--project')
|
|
1288
1610
|
const datasetInput = getDatasetReferenceInput()
|
|
1289
1611
|
const rowIds = parseCommaSeparated(getArg('--row-ids'))
|
|
1290
|
-
const rowIndices = parseCommaSeparatedIntegers(getArg('--row-indices'))
|
|
1291
1612
|
|
|
1292
|
-
if (rowIds.length === 0
|
|
1293
|
-
throw new Error('Usage: orizu datasets delete-rows [--dataset <datasetId|datasetUrl>] [--project <team/project>]
|
|
1613
|
+
if (rowIds.length === 0) {
|
|
1614
|
+
throw new Error('Usage: orizu datasets delete-rows [--dataset <datasetId|datasetUrl>] [--project <team/project>] --row-ids <id1,id2>')
|
|
1294
1615
|
}
|
|
1295
1616
|
|
|
1296
1617
|
let datasetId: string
|
|
@@ -1306,7 +1627,6 @@ async function deleteDatasetRows() {
|
|
|
1306
1627
|
headers: { 'Content-Type': 'application/json' },
|
|
1307
1628
|
body: JSON.stringify({
|
|
1308
1629
|
rowIds,
|
|
1309
|
-
rowIndices,
|
|
1310
1630
|
}),
|
|
1311
1631
|
})
|
|
1312
1632
|
|
|
@@ -1324,6 +1644,81 @@ async function deleteDatasetRows() {
|
|
|
1324
1644
|
)
|
|
1325
1645
|
}
|
|
1326
1646
|
|
|
1647
|
+
async function lockDataset() {
|
|
1648
|
+
const projectArg = getArg('--project')
|
|
1649
|
+
const datasetInput = getDatasetReferenceInput()
|
|
1650
|
+
const reason = getArg('--reason')
|
|
1651
|
+
|
|
1652
|
+
let datasetId: string
|
|
1653
|
+
if (datasetInput) {
|
|
1654
|
+
datasetId = parseDatasetReference(datasetInput).datasetId
|
|
1655
|
+
} else {
|
|
1656
|
+
const selected = await selectDatasetInteractively(projectArg)
|
|
1657
|
+
datasetId = selected.datasetId
|
|
1658
|
+
}
|
|
1659
|
+
|
|
1660
|
+
const response = await authedFetch(`/api/cli/datasets/${encodeURIComponent(datasetId)}/lock`, {
|
|
1661
|
+
method: 'POST',
|
|
1662
|
+
headers: { 'Content-Type': 'application/json' },
|
|
1663
|
+
body: JSON.stringify(reason ? { reason } : {}),
|
|
1664
|
+
})
|
|
1665
|
+
|
|
1666
|
+
if (!response.ok) {
|
|
1667
|
+
throw new Error(`Lock failed: ${await response.text()}`)
|
|
1668
|
+
}
|
|
1669
|
+
|
|
1670
|
+
const data = await parseJsonResponse<{
|
|
1671
|
+
dataset: {
|
|
1672
|
+
id: string
|
|
1673
|
+
name: string
|
|
1674
|
+
rowCount: number
|
|
1675
|
+
lockedAt: string
|
|
1676
|
+
lockedBy: string | null
|
|
1677
|
+
}
|
|
1678
|
+
}>(response, 'Dataset lock')
|
|
1679
|
+
|
|
1680
|
+
console.log(
|
|
1681
|
+
`Locked dataset ${data.dataset.name} (${data.dataset.id}) at ${data.dataset.lockedAt}. Row count: ${data.dataset.rowCount}`
|
|
1682
|
+
)
|
|
1683
|
+
}
|
|
1684
|
+
|
|
1685
|
+
async function cloneDataset() {
|
|
1686
|
+
const projectArg = getArg('--project')
|
|
1687
|
+
const datasetInput = getDatasetReferenceInput()
|
|
1688
|
+
const name = getArg('--name')
|
|
1689
|
+
|
|
1690
|
+
let datasetId: string
|
|
1691
|
+
if (datasetInput) {
|
|
1692
|
+
datasetId = parseDatasetReference(datasetInput).datasetId
|
|
1693
|
+
} else {
|
|
1694
|
+
const selected = await selectDatasetInteractively(projectArg)
|
|
1695
|
+
datasetId = selected.datasetId
|
|
1696
|
+
}
|
|
1697
|
+
|
|
1698
|
+
const response = await authedFetch(`/api/cli/datasets/${encodeURIComponent(datasetId)}/clone`, {
|
|
1699
|
+
method: 'POST',
|
|
1700
|
+
headers: { 'Content-Type': 'application/json' },
|
|
1701
|
+
body: JSON.stringify(name ? { name } : {}),
|
|
1702
|
+
})
|
|
1703
|
+
|
|
1704
|
+
if (!response.ok) {
|
|
1705
|
+
throw new Error(`Clone failed: ${await response.text()}`)
|
|
1706
|
+
}
|
|
1707
|
+
|
|
1708
|
+
const data = await parseJsonResponse<{
|
|
1709
|
+
dataset: {
|
|
1710
|
+
id: string
|
|
1711
|
+
name: string
|
|
1712
|
+
rowCount: number
|
|
1713
|
+
parentDatasetId: string
|
|
1714
|
+
}
|
|
1715
|
+
}>(response, 'Dataset clone')
|
|
1716
|
+
|
|
1717
|
+
console.log(
|
|
1718
|
+
`Cloned dataset ${data.dataset.parentDatasetId} -> ${data.dataset.name} (${data.dataset.id}). Row count: ${data.dataset.rowCount}`
|
|
1719
|
+
)
|
|
1720
|
+
}
|
|
1721
|
+
|
|
1327
1722
|
async function downloadAnnotations() {
|
|
1328
1723
|
let taskId = getArg('--task')
|
|
1329
1724
|
const format = (getArg('--format') || 'jsonl') as 'csv' | 'json' | 'jsonl'
|
|
@@ -1439,6 +1834,11 @@ async function main() {
|
|
|
1439
1834
|
return
|
|
1440
1835
|
}
|
|
1441
1836
|
|
|
1837
|
+
if (command === 'apps' && subcommand === 'detail') {
|
|
1838
|
+
await appDetail()
|
|
1839
|
+
return
|
|
1840
|
+
}
|
|
1841
|
+
|
|
1442
1842
|
if (command === 'tasks' && subcommand === 'list') {
|
|
1443
1843
|
await listTasks()
|
|
1444
1844
|
return
|
|
@@ -1459,6 +1859,16 @@ async function main() {
|
|
|
1459
1859
|
return
|
|
1460
1860
|
}
|
|
1461
1861
|
|
|
1862
|
+
if (command === 'tasks' && subcommand === 'pause') {
|
|
1863
|
+
await updateTaskStatus('paused')
|
|
1864
|
+
return
|
|
1865
|
+
}
|
|
1866
|
+
|
|
1867
|
+
if (command === 'tasks' && subcommand === 'unpause') {
|
|
1868
|
+
await updateTaskStatus('active')
|
|
1869
|
+
return
|
|
1870
|
+
}
|
|
1871
|
+
|
|
1462
1872
|
if (command === 'datasets' && subcommand === 'upload') {
|
|
1463
1873
|
await uploadDataset()
|
|
1464
1874
|
return
|
|
@@ -1474,11 +1884,26 @@ async function main() {
|
|
|
1474
1884
|
return
|
|
1475
1885
|
}
|
|
1476
1886
|
|
|
1887
|
+
if (command === 'datasets' && subcommand === 'edit-rows') {
|
|
1888
|
+
await editDatasetRows()
|
|
1889
|
+
return
|
|
1890
|
+
}
|
|
1891
|
+
|
|
1477
1892
|
if (command === 'datasets' && subcommand === 'delete-rows') {
|
|
1478
1893
|
await deleteDatasetRows()
|
|
1479
1894
|
return
|
|
1480
1895
|
}
|
|
1481
1896
|
|
|
1897
|
+
if (command === 'datasets' && subcommand === 'lock') {
|
|
1898
|
+
await lockDataset()
|
|
1899
|
+
return
|
|
1900
|
+
}
|
|
1901
|
+
|
|
1902
|
+
if (command === 'datasets' && subcommand === 'clone') {
|
|
1903
|
+
await cloneDataset()
|
|
1904
|
+
return
|
|
1905
|
+
}
|
|
1906
|
+
|
|
1482
1907
|
if (command === 'tasks' && subcommand === 'export') {
|
|
1483
1908
|
await downloadAnnotations()
|
|
1484
1909
|
return
|