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/src/index.ts
CHANGED
|
@@ -1,16 +1,18 @@
|
|
|
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
|
import { LoginResponse } from './types.js'
|
|
15
17
|
|
|
16
18
|
interface Team {
|
|
@@ -65,6 +67,18 @@ interface DatasetSummary {
|
|
|
65
67
|
teamSlug: string
|
|
66
68
|
}
|
|
67
69
|
|
|
70
|
+
type DatasetUploadSourceType = 'csv' | 'json' | 'jsonl'
|
|
71
|
+
|
|
72
|
+
interface DatasetUploadResponse {
|
|
73
|
+
dataset: {
|
|
74
|
+
id: string
|
|
75
|
+
name: string
|
|
76
|
+
rowCount: number
|
|
77
|
+
sourceType: string
|
|
78
|
+
url?: string
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
68
82
|
interface TeamMember {
|
|
69
83
|
id: string
|
|
70
84
|
user_id: string | null
|
|
@@ -106,7 +120,7 @@ interface TaskStatusPayload {
|
|
|
106
120
|
}
|
|
107
121
|
|
|
108
122
|
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 <
|
|
123
|
+
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
124
|
}
|
|
111
125
|
|
|
112
126
|
let cliArgs = process.argv.slice(2)
|
|
@@ -198,7 +212,6 @@ async function parseJsonResponse<T>(response: Response, context: string): Promis
|
|
|
198
212
|
)
|
|
199
213
|
}
|
|
200
214
|
}
|
|
201
|
-
|
|
202
215
|
async function promptSelect<T>(
|
|
203
216
|
title: string,
|
|
204
217
|
items: T[],
|
|
@@ -559,18 +572,26 @@ function printTeamMembers(members: TeamMember[]) {
|
|
|
559
572
|
|
|
560
573
|
const rows = members.map(member => ({
|
|
561
574
|
id: member.id,
|
|
575
|
+
userId: member.user_id || '-',
|
|
562
576
|
email: member.email || '-',
|
|
563
577
|
role: member.role || '-',
|
|
564
578
|
}))
|
|
565
579
|
|
|
566
580
|
const idWidth = Math.max('MEMBER ID'.length, ...rows.map(row => row.id.length))
|
|
581
|
+
const userIdWidth = Math.max('USER ID'.length, ...rows.map(row => row.userId.length))
|
|
567
582
|
const emailWidth = Math.max('EMAIL'.length, ...rows.map(row => row.email.length))
|
|
568
583
|
const roleWidth = Math.max('ROLE'.length, ...rows.map(row => row.role.length))
|
|
569
584
|
|
|
570
|
-
console.log(
|
|
571
|
-
|
|
585
|
+
console.log(
|
|
586
|
+
`${'MEMBER ID'.padEnd(idWidth)} ${'USER ID'.padEnd(userIdWidth)} ${'EMAIL'.padEnd(emailWidth)} ${'ROLE'.padEnd(roleWidth)}`
|
|
587
|
+
)
|
|
588
|
+
console.log(
|
|
589
|
+
`${'-'.repeat(idWidth)} ${'-'.repeat(userIdWidth)} ${'-'.repeat(emailWidth)} ${'-'.repeat(roleWidth)}`
|
|
590
|
+
)
|
|
572
591
|
rows.forEach(row => {
|
|
573
|
-
console.log(
|
|
592
|
+
console.log(
|
|
593
|
+
`${row.id.padEnd(idWidth)} ${row.userId.padEnd(userIdWidth)} ${row.email.padEnd(emailWidth)} ${row.role.padEnd(roleWidth)}`
|
|
594
|
+
)
|
|
574
595
|
})
|
|
575
596
|
}
|
|
576
597
|
|
|
@@ -593,15 +614,34 @@ function printTaskStatusSummary(data: TaskStatusPayload) {
|
|
|
593
614
|
}
|
|
594
615
|
}
|
|
595
616
|
|
|
617
|
+
const DEFAULT_AUTH_CALLBACK_PORT = 43123
|
|
618
|
+
|
|
619
|
+
function resolveAuthCallbackPort(): number {
|
|
620
|
+
const envPort = process.env.ORIZU_AUTH_PORT
|
|
621
|
+
if (!envPort) {
|
|
622
|
+
return DEFAULT_AUTH_CALLBACK_PORT
|
|
623
|
+
}
|
|
624
|
+
|
|
625
|
+
const parsed = parseInt(envPort, 10)
|
|
626
|
+
if (Number.isNaN(parsed) || parsed < 1024 || parsed > 65535) {
|
|
627
|
+
throw new Error(
|
|
628
|
+
`Invalid ORIZU_AUTH_PORT: '${envPort}'. Must be a number between 1024 and 65535.`
|
|
629
|
+
)
|
|
630
|
+
}
|
|
631
|
+
|
|
632
|
+
return parsed
|
|
633
|
+
}
|
|
634
|
+
|
|
596
635
|
async function login() {
|
|
597
636
|
const baseUrl = resolveLoginBaseUrl()
|
|
598
637
|
const codeVerifier = createCodeVerifier()
|
|
599
638
|
const codeChallenge = createCodeChallenge(codeVerifier)
|
|
639
|
+
const callbackPort = resolveAuthCallbackPort()
|
|
600
640
|
|
|
601
641
|
const callbackCode = await new Promise<string>((resolve, reject) => {
|
|
602
642
|
const server = createServer((request, response) => {
|
|
603
643
|
try {
|
|
604
|
-
const url = new URL(request.url || '/',
|
|
644
|
+
const url = new URL(request.url || '/', `http://127.0.0.1:${callbackPort}`)
|
|
605
645
|
const code = url.searchParams.get('code')
|
|
606
646
|
|
|
607
647
|
if (!code) {
|
|
@@ -622,11 +662,19 @@ async function login() {
|
|
|
622
662
|
}
|
|
623
663
|
})
|
|
624
664
|
|
|
625
|
-
server.on('error',
|
|
665
|
+
server.on('error', (error: NodeJS.ErrnoException) => {
|
|
666
|
+
if (error.code === 'EADDRINUSE') {
|
|
667
|
+
reject(new Error(
|
|
668
|
+
`Port ${callbackPort} is already in use. Set ORIZU_AUTH_PORT to a different port (1024–65535) and retry.`
|
|
669
|
+
))
|
|
670
|
+
} else {
|
|
671
|
+
reject(error)
|
|
672
|
+
}
|
|
673
|
+
})
|
|
626
674
|
|
|
627
|
-
server.listen(
|
|
675
|
+
server.listen(callbackPort, '127.0.0.1', async () => {
|
|
628
676
|
try {
|
|
629
|
-
const redirectUri =
|
|
677
|
+
const redirectUri = `http://127.0.0.1:${callbackPort}/callback`
|
|
630
678
|
const response = await fetch(`${baseUrl}/api/cli/auth/start`, {
|
|
631
679
|
method: 'POST',
|
|
632
680
|
headers: { 'Content-Type': 'application/json' },
|
|
@@ -921,12 +969,18 @@ async function linkAppDataset() {
|
|
|
921
969
|
const project = getArg('--project')
|
|
922
970
|
let appId = getArg('--app')
|
|
923
971
|
const versionArg = getArg('--version')
|
|
924
|
-
const
|
|
972
|
+
const parsedVersionNum = versionArg ? Number(versionArg) : Number.NaN
|
|
973
|
+
const versionNum =
|
|
974
|
+
Number.isInteger(parsedVersionNum) && parsedVersionNum > 0 ? parsedVersionNum : undefined
|
|
925
975
|
|
|
926
976
|
if (!datasetId) {
|
|
927
977
|
throw new Error('Usage: orizu apps link-dataset --dataset <datasetId> [--app <appId>] [--project <team/project>] [--version <n>]')
|
|
928
978
|
}
|
|
929
979
|
|
|
980
|
+
if (versionArg && versionNum === undefined) {
|
|
981
|
+
throw new Error('--version must be a positive integer')
|
|
982
|
+
}
|
|
983
|
+
|
|
930
984
|
if (!appId) {
|
|
931
985
|
const selected = await selectAppIdInteractively(project)
|
|
932
986
|
appId = selected.appId
|
|
@@ -972,12 +1026,20 @@ async function createTask() {
|
|
|
972
1026
|
const appId = getArg('--app')
|
|
973
1027
|
const title = getArg('--title')
|
|
974
1028
|
const assignees = parseCommaSeparated(getArg('--assignees'))
|
|
1029
|
+
const versionArg = getArg('--version')
|
|
975
1030
|
const instructions = getArg('--instructions')
|
|
976
1031
|
const labelsPerItemArg = getArg('--labels-per-item')
|
|
977
1032
|
const labelsPerItem = labelsPerItemArg ? Number(labelsPerItemArg) : 1
|
|
1033
|
+
const parsedVersionNum = versionArg ? Number(versionArg) : Number.NaN
|
|
1034
|
+
const versionNum =
|
|
1035
|
+
Number.isInteger(parsedVersionNum) && parsedVersionNum > 0 ? parsedVersionNum : null
|
|
978
1036
|
|
|
979
1037
|
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 <
|
|
1038
|
+
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]')
|
|
1039
|
+
}
|
|
1040
|
+
|
|
1041
|
+
if (versionArg && versionNum === null) {
|
|
1042
|
+
throw new Error('--version must be a positive integer')
|
|
981
1043
|
}
|
|
982
1044
|
|
|
983
1045
|
const response = await authedFetch('/api/cli/tasks', {
|
|
@@ -987,6 +1049,7 @@ async function createTask() {
|
|
|
987
1049
|
projectSlug,
|
|
988
1050
|
datasetId,
|
|
989
1051
|
appId,
|
|
1052
|
+
versionNum,
|
|
990
1053
|
title,
|
|
991
1054
|
memberIds: assignees,
|
|
992
1055
|
instructions,
|
|
@@ -995,15 +1058,59 @@ async function createTask() {
|
|
|
995
1058
|
})
|
|
996
1059
|
|
|
997
1060
|
if (!response.ok) {
|
|
998
|
-
|
|
1061
|
+
const cliError = await formatTaskCreateError(response)
|
|
1062
|
+
if (hasArg('--json')) {
|
|
1063
|
+
const payload = cliError.structuredPayload ?? { error: cliError.message }
|
|
1064
|
+
console.log(JSON.stringify({
|
|
1065
|
+
...payload,
|
|
1066
|
+
httpStatus: cliError.httpStatus,
|
|
1067
|
+
}, null, 2))
|
|
1068
|
+
}
|
|
1069
|
+
throw cliError
|
|
999
1070
|
}
|
|
1000
1071
|
|
|
1001
1072
|
const data = await parseJsonResponse<{
|
|
1002
|
-
task: {
|
|
1073
|
+
task: {
|
|
1074
|
+
id: string
|
|
1075
|
+
title: string
|
|
1076
|
+
status: string
|
|
1077
|
+
requiredAssignmentsPerRow: number
|
|
1078
|
+
versionId: string
|
|
1079
|
+
versionNum: number
|
|
1080
|
+
}
|
|
1003
1081
|
assignmentsCreated: number
|
|
1082
|
+
assignmentShortfall?: number
|
|
1083
|
+
warning?: string
|
|
1004
1084
|
}>(response, 'Task create')
|
|
1085
|
+
|
|
1086
|
+
if (hasArg('--json')) {
|
|
1087
|
+
console.log(JSON.stringify({
|
|
1088
|
+
taskId: data.task.id,
|
|
1089
|
+
datasetId,
|
|
1090
|
+
versionId: data.task.versionId,
|
|
1091
|
+
versionNum: data.task.versionNum,
|
|
1092
|
+
taskUrl: `${getBaseUrl()}/d/${projectSlug}/tasks/${data.task.id}`,
|
|
1093
|
+
title: data.task.title,
|
|
1094
|
+
status: data.task.status,
|
|
1095
|
+
requiredAssignmentsPerRow: data.task.requiredAssignmentsPerRow,
|
|
1096
|
+
assignmentsCreated: data.assignmentsCreated,
|
|
1097
|
+
...(data.assignmentShortfall !== undefined ? { assignmentShortfall: data.assignmentShortfall } : {}),
|
|
1098
|
+
...(data.warning ? { warning: data.warning } : {}),
|
|
1099
|
+
}, null, 2))
|
|
1100
|
+
return
|
|
1101
|
+
}
|
|
1102
|
+
|
|
1103
|
+
const baseUrl = getBaseUrl()
|
|
1104
|
+
const taskUrl = `${baseUrl}/d/${projectSlug}/tasks/${data.task.id}`
|
|
1005
1105
|
console.log(
|
|
1006
|
-
`Created task ${data.task.title} (${data.task.id}) [${data.task.status}]
|
|
1106
|
+
`Created task ${data.task.title} (${data.task.id}) [${data.task.status}]` +
|
|
1107
|
+
`\n Task ID: ${data.task.id}` +
|
|
1108
|
+
`\n Dataset ID: ${datasetId}` +
|
|
1109
|
+
`\n Version: v${data.task.versionNum} (${data.task.versionId})` +
|
|
1110
|
+
`\n Labels/row: ${data.task.requiredAssignmentsPerRow}` +
|
|
1111
|
+
`\n Assignments: ${data.assignmentsCreated}` +
|
|
1112
|
+
(data.warning ? `\n Warning: ${data.warning}` : '') +
|
|
1113
|
+
`\n URL: ${taskUrl}`
|
|
1007
1114
|
)
|
|
1008
1115
|
}
|
|
1009
1116
|
|
|
@@ -1037,7 +1144,31 @@ async function taskStatus() {
|
|
|
1037
1144
|
|
|
1038
1145
|
const response = await authedFetch(`/api/cli/tasks/${encodeURIComponent(taskId)}/status`)
|
|
1039
1146
|
if (!response.ok) {
|
|
1040
|
-
|
|
1147
|
+
const rawBody = await response.text()
|
|
1148
|
+
if (hasArg('--json')) {
|
|
1149
|
+
let errorPayload: Record<string, unknown> = { error: rawBody }
|
|
1150
|
+
try {
|
|
1151
|
+
const parsed = JSON.parse(rawBody)
|
|
1152
|
+
if (parsed && typeof parsed === 'object' && !Array.isArray(parsed)) {
|
|
1153
|
+
errorPayload = parsed as Record<string, unknown>
|
|
1154
|
+
}
|
|
1155
|
+
} catch {
|
|
1156
|
+
// keep raw body as error
|
|
1157
|
+
}
|
|
1158
|
+
console.log(JSON.stringify({
|
|
1159
|
+
...errorPayload,
|
|
1160
|
+
httpStatus: response.status,
|
|
1161
|
+
}, null, 2))
|
|
1162
|
+
}
|
|
1163
|
+
|
|
1164
|
+
let errorMsg = rawBody
|
|
1165
|
+
try {
|
|
1166
|
+
const parsed = JSON.parse(rawBody)
|
|
1167
|
+
if (parsed && typeof parsed === 'object' && typeof parsed.error === 'string') {
|
|
1168
|
+
errorMsg = parsed.error
|
|
1169
|
+
}
|
|
1170
|
+
} catch { /* use rawBody as-is */ }
|
|
1171
|
+
throw new Error(`Failed to fetch task status: ${errorMsg}`)
|
|
1041
1172
|
}
|
|
1042
1173
|
|
|
1043
1174
|
const data = await parseJsonResponse<TaskStatusPayload>(response, 'Task status')
|
|
@@ -1049,6 +1180,97 @@ async function taskStatus() {
|
|
|
1049
1180
|
printTaskStatusSummary(data)
|
|
1050
1181
|
}
|
|
1051
1182
|
|
|
1183
|
+
async function updateTaskStatus(targetStatus: 'paused' | 'active') {
|
|
1184
|
+
const taskId = getArg('--task')
|
|
1185
|
+
if (!taskId) {
|
|
1186
|
+
const verb = targetStatus === 'paused' ? 'pause' : 'unpause'
|
|
1187
|
+
throw new Error(`Usage: orizu tasks ${verb} --task <taskId>`)
|
|
1188
|
+
}
|
|
1189
|
+
|
|
1190
|
+
const response = await authedFetch(`/api/cli/tasks/${encodeURIComponent(taskId)}/status`, {
|
|
1191
|
+
method: 'PATCH',
|
|
1192
|
+
headers: { 'Content-Type': 'application/json' },
|
|
1193
|
+
body: JSON.stringify({ status: targetStatus }),
|
|
1194
|
+
})
|
|
1195
|
+
|
|
1196
|
+
if (!response.ok) {
|
|
1197
|
+
const verb = targetStatus === 'paused' ? 'pause' : 'unpause'
|
|
1198
|
+
throw new Error(`Failed to ${verb} task: ${await response.text()}`)
|
|
1199
|
+
}
|
|
1200
|
+
|
|
1201
|
+
const data = await parseJsonResponse<{ task: { id: string; status: string } }>(response, 'Task status update')
|
|
1202
|
+
const action = targetStatus === 'paused' ? 'Paused' : 'Unpaused'
|
|
1203
|
+
console.log(`${action} task ${data.task.id} [${data.task.status}]`)
|
|
1204
|
+
}
|
|
1205
|
+
|
|
1206
|
+
interface AppDetailPayload {
|
|
1207
|
+
id: string
|
|
1208
|
+
name: string
|
|
1209
|
+
currentVersionNum: number | null
|
|
1210
|
+
currentVersion: {
|
|
1211
|
+
versionId: string
|
|
1212
|
+
versionNum: number
|
|
1213
|
+
inputJsonSchema: unknown
|
|
1214
|
+
outputJsonSchema: unknown
|
|
1215
|
+
} | null
|
|
1216
|
+
createdAt: string
|
|
1217
|
+
updatedAt: string
|
|
1218
|
+
projectId: string | null
|
|
1219
|
+
compatibleDatasetsCount: number
|
|
1220
|
+
totalDatasetsCount: number
|
|
1221
|
+
createdByName: string | null
|
|
1222
|
+
createdByEmail: string | null
|
|
1223
|
+
teamSlug: string
|
|
1224
|
+
teamName: string
|
|
1225
|
+
projectSlug: string
|
|
1226
|
+
projectName: string
|
|
1227
|
+
}
|
|
1228
|
+
|
|
1229
|
+
async function appDetail() {
|
|
1230
|
+
const appId = getArg('--app')
|
|
1231
|
+
const project = getArg('--project')
|
|
1232
|
+
|
|
1233
|
+
if (!appId) {
|
|
1234
|
+
throw new Error('Usage: orizu apps detail --app <appId> [--project <team/project>] [--json]')
|
|
1235
|
+
}
|
|
1236
|
+
|
|
1237
|
+
const projectSlug = project || await resolveProjectSlug(null)
|
|
1238
|
+
|
|
1239
|
+
// Single fetch — the apps endpoint already returns full detail (ALI-544)
|
|
1240
|
+
const detailResponse = await authedFetch(`/api/cli/apps?project=${encodeURIComponent(projectSlug)}`)
|
|
1241
|
+
if (!detailResponse.ok) {
|
|
1242
|
+
throw new Error(`Failed to fetch app detail: ${await detailResponse.text()}`)
|
|
1243
|
+
}
|
|
1244
|
+
|
|
1245
|
+
const detailData = await parseJsonResponse<{ apps: AppDetailPayload[] }>(detailResponse, 'App detail')
|
|
1246
|
+
const detail = detailData.apps.find(a => a.id === appId)
|
|
1247
|
+
|
|
1248
|
+
if (!detail) {
|
|
1249
|
+
throw new Error(`App '${appId}' not found in project '${projectSlug}'`)
|
|
1250
|
+
}
|
|
1251
|
+
|
|
1252
|
+
if (hasArg('--json')) {
|
|
1253
|
+
console.log(JSON.stringify(detail, null, 2))
|
|
1254
|
+
return
|
|
1255
|
+
}
|
|
1256
|
+
|
|
1257
|
+
console.log(`App: ${detail.name} (${detail.id})`)
|
|
1258
|
+
console.log(` Project: ${detail.teamSlug}/${detail.projectSlug}`)
|
|
1259
|
+
if (detail.currentVersion) {
|
|
1260
|
+
console.log(` Current version: v${detail.currentVersion.versionNum} (${detail.currentVersion.versionId})`)
|
|
1261
|
+
console.log(` Input schema: ${detail.currentVersion.inputJsonSchema ? 'defined' : 'none'}`)
|
|
1262
|
+
console.log(` Output schema: ${detail.currentVersion.outputJsonSchema ? 'defined' : 'none'}`)
|
|
1263
|
+
} else {
|
|
1264
|
+
console.log(` Current version: none`)
|
|
1265
|
+
}
|
|
1266
|
+
console.log(` Compatible datasets: ${detail.compatibleDatasetsCount}/${detail.totalDatasetsCount}`)
|
|
1267
|
+
if (detail.createdByEmail) {
|
|
1268
|
+
console.log(` Created by: ${detail.createdByName || detail.createdByEmail}`)
|
|
1269
|
+
}
|
|
1270
|
+
console.log(` Created: ${detail.createdAt}`)
|
|
1271
|
+
console.log(` Updated: ${detail.updatedAt}`)
|
|
1272
|
+
}
|
|
1273
|
+
|
|
1052
1274
|
async function listTeamMembers() {
|
|
1053
1275
|
const teamSlug = await resolveTeamSlug(getArg('--team'))
|
|
1054
1276
|
|
|
@@ -1130,21 +1352,12 @@ async function changeTeamMemberRole() {
|
|
|
1130
1352
|
console.log(`Updated ${member.email} role to ${role}`)
|
|
1131
1353
|
}
|
|
1132
1354
|
|
|
1133
|
-
async function
|
|
1134
|
-
|
|
1135
|
-
|
|
1136
|
-
|
|
1137
|
-
|
|
1138
|
-
|
|
1139
|
-
throw new Error('Usage: orizu datasets upload --file <path> [--project <team/project>] [--name <name>]')
|
|
1140
|
-
}
|
|
1141
|
-
|
|
1142
|
-
const file = expandHomePath(fileArg)
|
|
1143
|
-
const project = await resolveProjectSlug(projectArg)
|
|
1144
|
-
|
|
1145
|
-
const { rows, sourceType } = parseDatasetFile(file)
|
|
1146
|
-
const datasetName = name || basename(file)
|
|
1147
|
-
|
|
1355
|
+
async function createDatasetFromRows(
|
|
1356
|
+
project: string,
|
|
1357
|
+
name: string,
|
|
1358
|
+
sourceType: DatasetUploadSourceType,
|
|
1359
|
+
rows: Array<Record<string, unknown>>
|
|
1360
|
+
): Promise<DatasetUploadResponse> {
|
|
1148
1361
|
const response = await authedFetch('/api/cli/datasets/upload', {
|
|
1149
1362
|
method: 'POST',
|
|
1150
1363
|
headers: {
|
|
@@ -1152,7 +1365,7 @@ async function uploadDataset() {
|
|
|
1152
1365
|
},
|
|
1153
1366
|
body: JSON.stringify({
|
|
1154
1367
|
projectSlug: project,
|
|
1155
|
-
name
|
|
1368
|
+
name,
|
|
1156
1369
|
rows,
|
|
1157
1370
|
sourceType,
|
|
1158
1371
|
}),
|
|
@@ -1163,9 +1376,104 @@ async function uploadDataset() {
|
|
|
1163
1376
|
throw new Error(`Upload failed: ${body.error}`)
|
|
1164
1377
|
}
|
|
1165
1378
|
|
|
1166
|
-
|
|
1167
|
-
|
|
1168
|
-
|
|
1379
|
+
return parseJsonResponse<DatasetUploadResponse>(response, 'Dataset upload')
|
|
1380
|
+
}
|
|
1381
|
+
|
|
1382
|
+
async function uploadJsonlDatasetInChunks(
|
|
1383
|
+
file: string,
|
|
1384
|
+
project: string,
|
|
1385
|
+
datasetName: string
|
|
1386
|
+
) {
|
|
1387
|
+
let dataset: DatasetUploadResponse['dataset'] | null = null
|
|
1388
|
+
let totalUploaded = 0
|
|
1389
|
+
let chunkIndex = 0
|
|
1390
|
+
const chunks = streamJsonlRowChunks(file)[Symbol.asyncIterator]()
|
|
1391
|
+
|
|
1392
|
+
while (true) {
|
|
1393
|
+
let nextChunk: IteratorResult<Array<Record<string, unknown>>>
|
|
1394
|
+
try {
|
|
1395
|
+
nextChunk = await chunks.next()
|
|
1396
|
+
} catch (error) {
|
|
1397
|
+
const message = error instanceof Error ? error.message : String(error)
|
|
1398
|
+
if (!dataset) {
|
|
1399
|
+
throw new Error(message)
|
|
1400
|
+
}
|
|
1401
|
+
|
|
1402
|
+
throw new Error(
|
|
1403
|
+
`Upload stopped while reading the next JSONL chunk: ${message}\n` +
|
|
1404
|
+
`Dataset ${dataset.name} (${dataset.id}) was created and ${totalUploaded} rows were uploaded. ` +
|
|
1405
|
+
`Fix the file, remove the first ${totalUploaded} rows, and run ` +
|
|
1406
|
+
`orizu datasets append --dataset ${dataset.id} --file <remaining-file>.`
|
|
1407
|
+
)
|
|
1408
|
+
}
|
|
1409
|
+
|
|
1410
|
+
if (nextChunk.done) {
|
|
1411
|
+
break
|
|
1412
|
+
}
|
|
1413
|
+
|
|
1414
|
+
const chunk = nextChunk.value
|
|
1415
|
+
chunkIndex += 1
|
|
1416
|
+
console.log(`Uploading chunk ${chunkIndex} (${chunk.length} rows)...`)
|
|
1417
|
+
|
|
1418
|
+
try {
|
|
1419
|
+
if (!dataset) {
|
|
1420
|
+
const data = await createDatasetFromRows(project, datasetName, 'jsonl', chunk)
|
|
1421
|
+
dataset = data.dataset
|
|
1422
|
+
totalUploaded = data.dataset.rowCount
|
|
1423
|
+
continue
|
|
1424
|
+
}
|
|
1425
|
+
|
|
1426
|
+
const data = await appendChunk(dataset.id, chunk)
|
|
1427
|
+
totalUploaded += data.appendedCount
|
|
1428
|
+
dataset = {
|
|
1429
|
+
...dataset,
|
|
1430
|
+
rowCount: data.dataset.rowCount,
|
|
1431
|
+
}
|
|
1432
|
+
} catch (error) {
|
|
1433
|
+
const message = error instanceof Error ? error.message : String(error)
|
|
1434
|
+
if (!dataset) {
|
|
1435
|
+
throw new Error(message)
|
|
1436
|
+
}
|
|
1437
|
+
|
|
1438
|
+
throw new Error(
|
|
1439
|
+
`Chunk ${chunkIndex} failed: ${message}\n` +
|
|
1440
|
+
`Dataset ${dataset.name} (${dataset.id}) was created and ${totalUploaded} rows were uploaded. ` +
|
|
1441
|
+
`To retry, remove the first ${totalUploaded} rows from your file and run ` +
|
|
1442
|
+
`orizu datasets append --dataset ${dataset.id} --file <remaining-file>.`
|
|
1443
|
+
)
|
|
1444
|
+
}
|
|
1445
|
+
}
|
|
1446
|
+
|
|
1447
|
+
if (!dataset) {
|
|
1448
|
+
throw new Error('Dataset file contains no rows')
|
|
1449
|
+
}
|
|
1450
|
+
|
|
1451
|
+
console.log(`Uploaded dataset ${dataset.name} (${dataset.id}) with ${dataset.rowCount} rows.`)
|
|
1452
|
+
if (dataset.url) {
|
|
1453
|
+
console.log(`View dataset: ${formatTerminalLink(dataset.url)}`)
|
|
1454
|
+
}
|
|
1455
|
+
}
|
|
1456
|
+
|
|
1457
|
+
async function uploadDataset() {
|
|
1458
|
+
const projectArg = getArg('--project')
|
|
1459
|
+
const fileArg = getArg('--file')
|
|
1460
|
+
const name = getArg('--name')
|
|
1461
|
+
|
|
1462
|
+
if (!fileArg) {
|
|
1463
|
+
throw new Error('Usage: orizu datasets upload --file <path> [--project <team/project>] [--name <name>]')
|
|
1464
|
+
}
|
|
1465
|
+
|
|
1466
|
+
const file = expandHomePath(fileArg)
|
|
1467
|
+
const project = await resolveProjectSlug(projectArg)
|
|
1468
|
+
const datasetName = name || basename(file)
|
|
1469
|
+
|
|
1470
|
+
if (extname(file).toLowerCase() === '.jsonl') {
|
|
1471
|
+
await uploadJsonlDatasetInChunks(file, project, datasetName)
|
|
1472
|
+
return
|
|
1473
|
+
}
|
|
1474
|
+
|
|
1475
|
+
const { rows, sourceType } = parseDatasetFile(file)
|
|
1476
|
+
const data = await createDatasetFromRows(project, datasetName, sourceType, rows)
|
|
1169
1477
|
|
|
1170
1478
|
console.log(`Uploaded dataset ${data.dataset.name} (${data.dataset.id}) with ${data.dataset.rowCount} rows.`)
|
|
1171
1479
|
if (data.dataset.url) {
|
|
@@ -1222,6 +1530,79 @@ async function downloadDataset() {
|
|
|
1222
1530
|
console.log(`Saved dataset ${datasetId} (${format.toUpperCase()}) to ${filename}`)
|
|
1223
1531
|
}
|
|
1224
1532
|
|
|
1533
|
+
const MAX_INPUT_FILE_SIZE_BYTES = 50 * 1024 * 1024 // 50 MB
|
|
1534
|
+
const APPEND_CHUNK_SIZE_ROWS = 500
|
|
1535
|
+
|
|
1536
|
+
async function appendChunk(
|
|
1537
|
+
datasetId: string,
|
|
1538
|
+
rows: Array<Record<string, unknown>>
|
|
1539
|
+
): Promise<{ dataset: { id: string; name: string; rowCount: number }; appendedCount: number }> {
|
|
1540
|
+
const response = await authedFetch(`/api/cli/datasets/${encodeURIComponent(datasetId)}/rows`, {
|
|
1541
|
+
method: 'POST',
|
|
1542
|
+
headers: { 'Content-Type': 'application/json' },
|
|
1543
|
+
body: JSON.stringify({ rows }),
|
|
1544
|
+
})
|
|
1545
|
+
|
|
1546
|
+
if (!response.ok) {
|
|
1547
|
+
throw new Error(`Append failed: ${await response.text()}`)
|
|
1548
|
+
}
|
|
1549
|
+
|
|
1550
|
+
return parseJsonResponse<{
|
|
1551
|
+
dataset: { id: string; name: string; rowCount: number }
|
|
1552
|
+
appendedCount: number
|
|
1553
|
+
}>(response, 'Dataset append')
|
|
1554
|
+
}
|
|
1555
|
+
|
|
1556
|
+
async function appendJsonlDatasetRowsInChunks(datasetId: string, file: string) {
|
|
1557
|
+
let totalAppended = 0
|
|
1558
|
+
let lastResult: { dataset: { id: string; name: string; rowCount: number } } | null = null
|
|
1559
|
+
let chunkIndex = 0
|
|
1560
|
+
const chunks = streamJsonlRowChunks(file)[Symbol.asyncIterator]()
|
|
1561
|
+
|
|
1562
|
+
while (true) {
|
|
1563
|
+
let nextChunk: IteratorResult<Array<Record<string, unknown>>>
|
|
1564
|
+
try {
|
|
1565
|
+
nextChunk = await chunks.next()
|
|
1566
|
+
} catch (error) {
|
|
1567
|
+
const message = error instanceof Error ? error.message : String(error)
|
|
1568
|
+
throw new Error(
|
|
1569
|
+
`Append stopped while reading the next JSONL chunk: ${message}\n` +
|
|
1570
|
+
`${totalAppended} rows from ${chunkIndex} chunk(s) were already appended. ` +
|
|
1571
|
+
`Fix the file, remove the first ${totalAppended} rows, and re-run the command.`
|
|
1572
|
+
)
|
|
1573
|
+
}
|
|
1574
|
+
|
|
1575
|
+
if (nextChunk.done) {
|
|
1576
|
+
break
|
|
1577
|
+
}
|
|
1578
|
+
|
|
1579
|
+
const chunk = nextChunk.value
|
|
1580
|
+
chunkIndex += 1
|
|
1581
|
+
console.log(`Uploading chunk ${chunkIndex} (${chunk.length} rows)...`)
|
|
1582
|
+
|
|
1583
|
+
try {
|
|
1584
|
+
const data = await appendChunk(datasetId, chunk)
|
|
1585
|
+
totalAppended += data.appendedCount
|
|
1586
|
+
lastResult = data
|
|
1587
|
+
} catch (error) {
|
|
1588
|
+
const message = error instanceof Error ? error.message : String(error)
|
|
1589
|
+
throw new Error(
|
|
1590
|
+
`Chunk ${chunkIndex} failed: ${message}\n` +
|
|
1591
|
+
`${totalAppended} rows from ${chunkIndex - 1} chunk(s) were already appended. ` +
|
|
1592
|
+
`To retry, remove the first ${totalAppended} rows from your file and re-run the command.`
|
|
1593
|
+
)
|
|
1594
|
+
}
|
|
1595
|
+
}
|
|
1596
|
+
|
|
1597
|
+
if (!lastResult) {
|
|
1598
|
+
throw new Error('Dataset append file must contain at least one row')
|
|
1599
|
+
}
|
|
1600
|
+
|
|
1601
|
+
console.log(
|
|
1602
|
+
`Appended ${totalAppended} rows to dataset ${lastResult.dataset.name} (${lastResult.dataset.id}). New row count: ${lastResult.dataset.rowCount}`
|
|
1603
|
+
)
|
|
1604
|
+
}
|
|
1605
|
+
|
|
1225
1606
|
async function appendDatasetRows() {
|
|
1226
1607
|
const projectArg = getArg('--project')
|
|
1227
1608
|
const datasetInput = getDatasetReferenceInput()
|
|
@@ -1240,29 +1621,81 @@ async function appendDatasetRows() {
|
|
|
1240
1621
|
}
|
|
1241
1622
|
|
|
1242
1623
|
const file = expandHomePath(fileArg)
|
|
1624
|
+
|
|
1625
|
+
if (extname(file).toLowerCase() === '.jsonl') {
|
|
1626
|
+
await appendJsonlDatasetRowsInChunks(datasetId, file)
|
|
1627
|
+
return
|
|
1628
|
+
}
|
|
1629
|
+
|
|
1630
|
+
// Check file size before reading to prevent OOM on large files (ALI-565).
|
|
1631
|
+
// Wrap statSync in try/catch so missing/inaccessible files get friendly
|
|
1632
|
+
// errors instead of raw Node.js ENOENT/EPERM (ALI-554).
|
|
1633
|
+
let fileSizeBytes: number
|
|
1634
|
+
try {
|
|
1635
|
+
fileSizeBytes = statSync(file).size
|
|
1636
|
+
} catch (error) {
|
|
1637
|
+
const maybeError = error as NodeJS.ErrnoException
|
|
1638
|
+
if (maybeError.code === 'ENOENT') {
|
|
1639
|
+
throw new Error(
|
|
1640
|
+
`File not found: ${file}. Check the path and filename, then retry.`
|
|
1641
|
+
)
|
|
1642
|
+
}
|
|
1643
|
+
if (maybeError.code === 'EPERM' || maybeError.code === 'EACCES') {
|
|
1644
|
+
throw new Error(
|
|
1645
|
+
`Cannot read file: ${file}. Grant folder permission to your terminal app and retry.`
|
|
1646
|
+
)
|
|
1647
|
+
}
|
|
1648
|
+
throw new Error(`Failed to access file ${file}: ${maybeError.message}`)
|
|
1649
|
+
}
|
|
1650
|
+
if (fileSizeBytes > MAX_INPUT_FILE_SIZE_BYTES) {
|
|
1651
|
+
const sizeMb = (fileSizeBytes / (1024 * 1024)).toFixed(1)
|
|
1652
|
+
throw new Error(
|
|
1653
|
+
`Input file is ${sizeMb} MB, which exceeds the 50 MB limit. Split the file into smaller parts and append each separately.`
|
|
1654
|
+
)
|
|
1655
|
+
}
|
|
1656
|
+
|
|
1243
1657
|
const { rows } = parseDatasetFile(file)
|
|
1244
1658
|
if (!Array.isArray(rows) || rows.length === 0) {
|
|
1245
1659
|
throw new Error('Dataset append file must contain at least one row')
|
|
1246
1660
|
}
|
|
1247
1661
|
|
|
1248
|
-
|
|
1249
|
-
|
|
1250
|
-
|
|
1251
|
-
|
|
1252
|
-
|
|
1253
|
-
|
|
1254
|
-
if (!response.ok) {
|
|
1255
|
-
throw new Error(`Append failed: ${await response.text()}`)
|
|
1662
|
+
if (rows.length <= APPEND_CHUNK_SIZE_ROWS) {
|
|
1663
|
+
const data = await appendChunk(datasetId, rows)
|
|
1664
|
+
console.log(
|
|
1665
|
+
`Appended ${data.appendedCount} rows to dataset ${data.dataset.name} (${data.dataset.id}). New row count: ${data.dataset.rowCount}`
|
|
1666
|
+
)
|
|
1667
|
+
return
|
|
1256
1668
|
}
|
|
1257
1669
|
|
|
1258
|
-
|
|
1259
|
-
|
|
1260
|
-
|
|
1261
|
-
|
|
1670
|
+
// Chunked upload for large row counts (ALI-555: track partial progress)
|
|
1671
|
+
let totalAppended = 0
|
|
1672
|
+
let lastResult: { dataset: { id: string; name: string; rowCount: number } } | null = null
|
|
1673
|
+
const totalChunks = Math.ceil(rows.length / APPEND_CHUNK_SIZE_ROWS)
|
|
1262
1674
|
|
|
1263
|
-
|
|
1264
|
-
|
|
1265
|
-
|
|
1675
|
+
for (let offset = 0; offset < rows.length; offset += APPEND_CHUNK_SIZE_ROWS) {
|
|
1676
|
+
const chunk = rows.slice(offset, offset + APPEND_CHUNK_SIZE_ROWS)
|
|
1677
|
+
const chunkIndex = Math.floor(offset / APPEND_CHUNK_SIZE_ROWS) + 1
|
|
1678
|
+
|
|
1679
|
+
console.log(`Uploading chunk ${chunkIndex}/${totalChunks} (${chunk.length} rows)...`)
|
|
1680
|
+
try {
|
|
1681
|
+
const data = await appendChunk(datasetId, chunk)
|
|
1682
|
+
totalAppended += data.appendedCount
|
|
1683
|
+
lastResult = data
|
|
1684
|
+
} catch (error) {
|
|
1685
|
+
const msg = error instanceof Error ? error.message : String(error)
|
|
1686
|
+
throw new Error(
|
|
1687
|
+
`Chunk ${chunkIndex}/${totalChunks} failed: ${msg}\n` +
|
|
1688
|
+
`${totalAppended} rows from ${chunkIndex - 1} chunk(s) were already appended. ` +
|
|
1689
|
+
`To retry, remove the first ${totalAppended} rows from your file and re-run the command.`
|
|
1690
|
+
)
|
|
1691
|
+
}
|
|
1692
|
+
}
|
|
1693
|
+
|
|
1694
|
+
if (lastResult) {
|
|
1695
|
+
console.log(
|
|
1696
|
+
`Appended ${totalAppended} rows to dataset ${lastResult.dataset.name} (${lastResult.dataset.id}). New row count: ${lastResult.dataset.rowCount}`
|
|
1697
|
+
)
|
|
1698
|
+
}
|
|
1266
1699
|
}
|
|
1267
1700
|
|
|
1268
1701
|
async function editDatasetRows() {
|
|
@@ -1554,6 +1987,11 @@ async function main() {
|
|
|
1554
1987
|
return
|
|
1555
1988
|
}
|
|
1556
1989
|
|
|
1990
|
+
if (command === 'apps' && subcommand === 'detail') {
|
|
1991
|
+
await appDetail()
|
|
1992
|
+
return
|
|
1993
|
+
}
|
|
1994
|
+
|
|
1557
1995
|
if (command === 'tasks' && subcommand === 'list') {
|
|
1558
1996
|
await listTasks()
|
|
1559
1997
|
return
|
|
@@ -1574,6 +2012,16 @@ async function main() {
|
|
|
1574
2012
|
return
|
|
1575
2013
|
}
|
|
1576
2014
|
|
|
2015
|
+
if (command === 'tasks' && subcommand === 'pause') {
|
|
2016
|
+
await updateTaskStatus('paused')
|
|
2017
|
+
return
|
|
2018
|
+
}
|
|
2019
|
+
|
|
2020
|
+
if (command === 'tasks' && subcommand === 'unpause') {
|
|
2021
|
+
await updateTaskStatus('active')
|
|
2022
|
+
return
|
|
2023
|
+
}
|
|
2024
|
+
|
|
1577
2025
|
if (command === 'datasets' && subcommand === 'upload') {
|
|
1578
2026
|
await uploadDataset()
|
|
1579
2027
|
return
|