orizu 0.1.5 → 0.1.6
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 +28 -3
- package/dist/http.js +24 -0
- package/dist/index.js +132 -82
- package/package.json +1 -1
- package/src/credentials.ts +42 -3
- package/src/http.ts +32 -0
- package/src/index.ts +143 -82
package/dist/credentials.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
|
-
import { mkdirSync, readFileSync, rmSync, writeFileSync,
|
|
1
|
+
import { chmodSync, closeSync, existsSync, fsyncSync, mkdirSync, openSync, readFileSync, renameSync, rmSync, writeFileSync, } from 'fs';
|
|
2
2
|
import { join } from 'path';
|
|
3
3
|
import { homedir } from 'os';
|
|
4
|
+
import { randomBytes } from 'crypto';
|
|
4
5
|
function getConfigDir() {
|
|
5
6
|
if (process.env.ORIZU_CONFIG_DIR) {
|
|
6
7
|
return process.env.ORIZU_CONFIG_DIR;
|
|
@@ -42,9 +43,33 @@ function migrateToV2(stored) {
|
|
|
42
43
|
}
|
|
43
44
|
function writeCredentials(config) {
|
|
44
45
|
const dir = getConfigDir();
|
|
45
|
-
mkdirSync(dir, { recursive: true });
|
|
46
|
+
mkdirSync(dir, { recursive: true, mode: 0o700 });
|
|
47
|
+
chmodSync(dir, 0o700);
|
|
46
48
|
const path = getCredentialsPath();
|
|
47
|
-
|
|
49
|
+
const tempPath = join(dir, `.credentials.json.${process.pid}.${randomBytes(8).toString('hex')}.tmp`);
|
|
50
|
+
const payload = JSON.stringify(config, null, 2) + '\n';
|
|
51
|
+
const fd = openSync(tempPath, 'wx', 0o600);
|
|
52
|
+
try {
|
|
53
|
+
writeFileSync(fd, payload, 'utf-8');
|
|
54
|
+
fsyncSync(fd);
|
|
55
|
+
}
|
|
56
|
+
finally {
|
|
57
|
+
closeSync(fd);
|
|
58
|
+
}
|
|
59
|
+
chmodSync(tempPath, 0o600);
|
|
60
|
+
try {
|
|
61
|
+
renameSync(tempPath, path);
|
|
62
|
+
}
|
|
63
|
+
catch (error) {
|
|
64
|
+
if (process.platform === 'win32' && error?.code === 'EEXIST') {
|
|
65
|
+
rmSync(path, { force: true });
|
|
66
|
+
renameSync(tempPath, path);
|
|
67
|
+
}
|
|
68
|
+
else {
|
|
69
|
+
rmSync(tempPath, { force: true });
|
|
70
|
+
throw error;
|
|
71
|
+
}
|
|
72
|
+
}
|
|
48
73
|
chmodSync(path, 0o600);
|
|
49
74
|
}
|
|
50
75
|
function createEmptyCredentialsConfig() {
|
package/dist/http.js
CHANGED
|
@@ -33,11 +33,34 @@ export function resolveLoginBaseUrl(flags = runtimeFlags) {
|
|
|
33
33
|
export function getBaseUrl() {
|
|
34
34
|
return resolveBaseUrl();
|
|
35
35
|
}
|
|
36
|
+
function isLoopbackHostname(hostname) {
|
|
37
|
+
const normalized = hostname.toLowerCase();
|
|
38
|
+
return (normalized === 'localhost' ||
|
|
39
|
+
normalized === '[::1]' ||
|
|
40
|
+
/^127(?:\.\d{1,3}){3}$/.test(normalized));
|
|
41
|
+
}
|
|
42
|
+
export function assertSecureTokenTransport(baseUrl) {
|
|
43
|
+
let parsed;
|
|
44
|
+
try {
|
|
45
|
+
parsed = new URL(baseUrl);
|
|
46
|
+
}
|
|
47
|
+
catch {
|
|
48
|
+
throw new Error(`Invalid server URL: '${baseUrl}'`);
|
|
49
|
+
}
|
|
50
|
+
if (parsed.protocol === 'https:') {
|
|
51
|
+
return;
|
|
52
|
+
}
|
|
53
|
+
if (parsed.protocol === 'http:' && isLoopbackHostname(parsed.hostname)) {
|
|
54
|
+
return;
|
|
55
|
+
}
|
|
56
|
+
throw new Error(`Refusing to send CLI tokens to ${baseUrl}. Use HTTPS, or --local for loopback development.`);
|
|
57
|
+
}
|
|
36
58
|
function isExpired(expiresAt) {
|
|
37
59
|
const nowUnix = Math.floor(Date.now() / 1000);
|
|
38
60
|
return expiresAt <= nowUnix + 30;
|
|
39
61
|
}
|
|
40
62
|
async function refreshCredentials(baseUrl, credentials) {
|
|
63
|
+
assertSecureTokenTransport(baseUrl);
|
|
41
64
|
const response = await fetch(`${baseUrl}/api/cli/auth/refresh`, {
|
|
42
65
|
method: 'POST',
|
|
43
66
|
headers: { 'Content-Type': 'application/json' },
|
|
@@ -57,6 +80,7 @@ async function refreshCredentials(baseUrl, credentials) {
|
|
|
57
80
|
}
|
|
58
81
|
export async function authedFetch(path, init = {}) {
|
|
59
82
|
const baseUrl = resolveBaseUrl();
|
|
83
|
+
assertSecureTokenTransport(baseUrl);
|
|
60
84
|
const credentials = getServerCredentials(baseUrl);
|
|
61
85
|
if (!credentials) {
|
|
62
86
|
throw new Error(`Not logged in for ${baseUrl}. Run \`orizu login --server ${baseUrl}\` (or \`--local\`) first.`);
|
package/dist/index.js
CHANGED
|
@@ -11,7 +11,7 @@ import { parseDatasetFile } from './file-parser.js';
|
|
|
11
11
|
import { streamJsonlRowChunks } from './jsonl-stream.js';
|
|
12
12
|
import { parseDatasetReference } from './dataset-download.js';
|
|
13
13
|
import { parseGlobalFlags } from './global-flags.js';
|
|
14
|
-
import { authedFetch, getBaseUrl, resolveLoginBaseUrl, setGlobalFlags } from './http.js';
|
|
14
|
+
import { assertSecureTokenTransport, authedFetch, getBaseUrl, resolveLoginBaseUrl, setGlobalFlags, } from './http.js';
|
|
15
15
|
import { formatTaskCreateError } from './task-create-error.js';
|
|
16
16
|
function printUsage() {
|
|
17
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>]`);
|
|
@@ -46,47 +46,81 @@ function createCodeVerifier() {
|
|
|
46
46
|
function createCodeChallenge(verifier) {
|
|
47
47
|
return createHash('sha256').update(verifier).digest('base64url');
|
|
48
48
|
}
|
|
49
|
+
function sanitizeTerminalText(value) {
|
|
50
|
+
return String(value).replace(/[\u0000-\u0008\u000B\u000C\u000E-\u001F\u007F-\u009F]/g, '');
|
|
51
|
+
}
|
|
52
|
+
function validateBrowserUrl(url, expectedOrigin) {
|
|
53
|
+
let parsed;
|
|
54
|
+
try {
|
|
55
|
+
parsed = new URL(url);
|
|
56
|
+
}
|
|
57
|
+
catch {
|
|
58
|
+
throw new Error('Server returned an invalid browser URL.');
|
|
59
|
+
}
|
|
60
|
+
if (parsed.protocol !== 'https:' && parsed.protocol !== 'http:') {
|
|
61
|
+
throw new Error('Server returned an unsupported browser URL scheme.');
|
|
62
|
+
}
|
|
63
|
+
if (parsed.username || parsed.password) {
|
|
64
|
+
throw new Error('Server returned a browser URL containing credentials.');
|
|
65
|
+
}
|
|
66
|
+
if (expectedOrigin && parsed.origin !== expectedOrigin) {
|
|
67
|
+
throw new Error('Server returned a browser URL for an unexpected origin.');
|
|
68
|
+
}
|
|
69
|
+
return parsed;
|
|
70
|
+
}
|
|
49
71
|
function openInBrowser(url) {
|
|
72
|
+
const parsed = validateBrowserUrl(url);
|
|
50
73
|
const platform = process.platform;
|
|
74
|
+
const href = parsed.href;
|
|
51
75
|
if (platform === 'darwin') {
|
|
52
|
-
spawn('open', [
|
|
76
|
+
spawn('open', [href], {
|
|
53
77
|
detached: true,
|
|
54
78
|
stdio: 'ignore',
|
|
55
79
|
}).unref();
|
|
56
80
|
return;
|
|
57
81
|
}
|
|
58
82
|
if (platform === 'win32') {
|
|
59
|
-
spawn('
|
|
83
|
+
spawn('rundll32.exe', ['url.dll,FileProtocolHandler', href], {
|
|
60
84
|
detached: true,
|
|
61
85
|
stdio: 'ignore',
|
|
62
86
|
windowsHide: true,
|
|
63
87
|
}).unref();
|
|
64
88
|
return;
|
|
65
89
|
}
|
|
66
|
-
spawn('xdg-open', [
|
|
90
|
+
spawn('xdg-open', [href], {
|
|
67
91
|
detached: true,
|
|
68
92
|
stdio: 'ignore',
|
|
69
93
|
}).unref();
|
|
70
94
|
}
|
|
71
95
|
function formatTerminalLink(url) {
|
|
96
|
+
const safeUrl = sanitizeTerminalText(url);
|
|
97
|
+
try {
|
|
98
|
+
const parsed = validateBrowserUrl(safeUrl);
|
|
99
|
+
if (parsed.protocol !== 'https:' && parsed.protocol !== 'http:') {
|
|
100
|
+
return safeUrl;
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
catch {
|
|
104
|
+
return safeUrl;
|
|
105
|
+
}
|
|
72
106
|
if (!isInteractiveTerminal()) {
|
|
73
|
-
return
|
|
107
|
+
return safeUrl;
|
|
74
108
|
}
|
|
75
|
-
return `\u001B]8;;${
|
|
109
|
+
return `\u001B]8;;${safeUrl}\u0007${safeUrl}\u001B]8;;\u0007`;
|
|
76
110
|
}
|
|
77
111
|
async function parseJsonResponse(response, context) {
|
|
78
112
|
const contentType = response.headers.get('content-type') || '';
|
|
79
113
|
const rawBody = await response.text();
|
|
80
114
|
if (!contentType.includes('application/json')) {
|
|
81
115
|
throw new Error(`${context} returned non-JSON response (status ${response.status}). ` +
|
|
82
|
-
`Body preview: ${rawBody.slice(0, 180)}`);
|
|
116
|
+
`Body preview: ${sanitizeTerminalText(rawBody.slice(0, 180))}`);
|
|
83
117
|
}
|
|
84
118
|
try {
|
|
85
119
|
return JSON.parse(rawBody);
|
|
86
120
|
}
|
|
87
121
|
catch {
|
|
88
122
|
throw new Error(`${context} returned invalid JSON (status ${response.status}). ` +
|
|
89
|
-
`Body preview: ${rawBody.slice(0, 180)}`);
|
|
123
|
+
`Body preview: ${sanitizeTerminalText(rawBody.slice(0, 180))}`);
|
|
90
124
|
}
|
|
91
125
|
}
|
|
92
126
|
async function promptSelect(title, items, label, options) {
|
|
@@ -99,9 +133,9 @@ async function promptSelect(title, items, label, options) {
|
|
|
99
133
|
if (items.length === 1 && !options?.forcePrompt) {
|
|
100
134
|
return items[0];
|
|
101
135
|
}
|
|
102
|
-
console.log(`\n${title}`);
|
|
136
|
+
console.log(`\n${sanitizeTerminalText(title)}`);
|
|
103
137
|
items.forEach((item, index) => {
|
|
104
|
-
console.log(` ${index + 1}. ${label(item, index)}`);
|
|
138
|
+
console.log(` ${index + 1}. ${sanitizeTerminalText(label(item, index))}`);
|
|
105
139
|
});
|
|
106
140
|
const rl = createInterface({ input, output });
|
|
107
141
|
try {
|
|
@@ -187,7 +221,7 @@ async function resolveProjectSlug(projectArg) {
|
|
|
187
221
|
const [teamSlug, projectSlug] = segments.map(normalizeSlugInput);
|
|
188
222
|
const matchedTeam = teams.find(team => team.slug === teamSlug);
|
|
189
223
|
if (!matchedTeam) {
|
|
190
|
-
console.error(`Team '${teamSlug}' not found in your accessible teams.`);
|
|
224
|
+
console.error(`Team '${sanitizeTerminalText(teamSlug)}' not found in your accessible teams.`);
|
|
191
225
|
const selectedTeam = await promptSelect('Select a team', teams, team => `${team.name} (${team.slug})`);
|
|
192
226
|
const projects = await fetchProjects(selectedTeam.slug);
|
|
193
227
|
const selectedProject = await promptSelect(`Select a project in ${selectedTeam.slug}`, projects, project => `${project.name} (${project.teamSlug}/${project.slug})`);
|
|
@@ -196,7 +230,7 @@ async function resolveProjectSlug(projectArg) {
|
|
|
196
230
|
const projects = await fetchProjects(matchedTeam.slug);
|
|
197
231
|
const matchedProject = projects.find(project => project.slug === projectSlug);
|
|
198
232
|
if (!matchedProject) {
|
|
199
|
-
console.error(`Project '${projectSlug}' not found in team '${matchedTeam.slug}'.`);
|
|
233
|
+
console.error(`Project '${sanitizeTerminalText(projectSlug)}' not found in team '${sanitizeTerminalText(matchedTeam.slug)}'.`);
|
|
200
234
|
const selectedProject = await promptSelect(`Select a project in ${matchedTeam.slug}`, projects, project => `${project.name} (${project.teamSlug}/${project.slug})`);
|
|
201
235
|
return `${selectedProject.teamSlug}/${selectedProject.slug}`;
|
|
202
236
|
}
|
|
@@ -239,9 +273,9 @@ function printTeams(teams) {
|
|
|
239
273
|
return;
|
|
240
274
|
}
|
|
241
275
|
const rows = teams.map(team => ({
|
|
242
|
-
slug: team.slug,
|
|
243
|
-
name: team.name || '-',
|
|
244
|
-
role: team.role || '-',
|
|
276
|
+
slug: sanitizeTerminalText(team.slug),
|
|
277
|
+
name: sanitizeTerminalText(team.name || '-'),
|
|
278
|
+
role: sanitizeTerminalText(team.role || '-'),
|
|
245
279
|
}));
|
|
246
280
|
const slugWidth = Math.max('TEAM SLUG'.length, ...rows.map(row => row.slug.length));
|
|
247
281
|
const nameWidth = Math.max('TEAM NAME'.length, ...rows.map(row => row.name.length));
|
|
@@ -258,9 +292,9 @@ function printProjects(projects) {
|
|
|
258
292
|
return;
|
|
259
293
|
}
|
|
260
294
|
const rows = projects.map(project => ({
|
|
261
|
-
project: `${project.teamSlug}/${project.slug}
|
|
262
|
-
name: project.name || '-',
|
|
263
|
-
role: project.role || '-',
|
|
295
|
+
project: sanitizeTerminalText(`${project.teamSlug}/${project.slug}`),
|
|
296
|
+
name: sanitizeTerminalText(project.name || '-'),
|
|
297
|
+
role: sanitizeTerminalText(project.role || '-'),
|
|
264
298
|
}));
|
|
265
299
|
const projectWidth = Math.max('TEAM/PROJECT'.length, ...rows.map(row => row.project.length));
|
|
266
300
|
const nameWidth = Math.max('PROJECT NAME'.length, ...rows.map(row => row.name.length));
|
|
@@ -277,11 +311,11 @@ function printTasks(tasks) {
|
|
|
277
311
|
return;
|
|
278
312
|
}
|
|
279
313
|
const rows = tasks.map(task => ({
|
|
280
|
-
id: task.id,
|
|
281
|
-
name: task.title || '-',
|
|
282
|
-
status: task.status || '-',
|
|
314
|
+
id: sanitizeTerminalText(task.id),
|
|
315
|
+
name: sanitizeTerminalText(task.title || '-'),
|
|
316
|
+
status: sanitizeTerminalText(task.status || '-'),
|
|
283
317
|
project: task.teamSlug && task.projectSlug
|
|
284
|
-
? `${task.teamSlug}/${task.projectSlug}`
|
|
318
|
+
? sanitizeTerminalText(`${task.teamSlug}/${task.projectSlug}`)
|
|
285
319
|
: 'unknown-project',
|
|
286
320
|
}));
|
|
287
321
|
const idWidth = Math.max('TASK ID'.length, ...rows.map(row => row.id.length));
|
|
@@ -299,8 +333,8 @@ function printApps(apps) {
|
|
|
299
333
|
return;
|
|
300
334
|
}
|
|
301
335
|
const rows = apps.map(app => ({
|
|
302
|
-
id: app.id,
|
|
303
|
-
name: app.name || '-',
|
|
336
|
+
id: sanitizeTerminalText(app.id),
|
|
337
|
+
name: sanitizeTerminalText(app.name || '-'),
|
|
304
338
|
version: `v${app.currentVersionNum || 1}`,
|
|
305
339
|
}));
|
|
306
340
|
const idWidth = Math.max('APP ID'.length, ...rows.map(row => row.id.length));
|
|
@@ -318,10 +352,10 @@ function printTeamMembers(members) {
|
|
|
318
352
|
return;
|
|
319
353
|
}
|
|
320
354
|
const rows = members.map(member => ({
|
|
321
|
-
id: member.id,
|
|
322
|
-
userId: member.user_id || '-',
|
|
323
|
-
email: member.email || '-',
|
|
324
|
-
role: member.role || '-',
|
|
355
|
+
id: sanitizeTerminalText(member.id),
|
|
356
|
+
userId: sanitizeTerminalText(member.user_id || '-'),
|
|
357
|
+
email: sanitizeTerminalText(member.email || '-'),
|
|
358
|
+
role: sanitizeTerminalText(member.role || '-'),
|
|
325
359
|
}));
|
|
326
360
|
const idWidth = Math.max('MEMBER ID'.length, ...rows.map(row => row.id.length));
|
|
327
361
|
const userIdWidth = Math.max('USER ID'.length, ...rows.map(row => row.userId.length));
|
|
@@ -335,16 +369,16 @@ function printTeamMembers(members) {
|
|
|
335
369
|
}
|
|
336
370
|
function printTaskStatusSummary(data) {
|
|
337
371
|
const task = data.task;
|
|
338
|
-
console.log(`Task: ${task.title} (${task.id})`);
|
|
339
|
-
console.log(`Status: ${task.status}`);
|
|
340
|
-
console.log(`Project: ${task.teamSlug}/${task.projectSlug}`);
|
|
372
|
+
console.log(`Task: ${sanitizeTerminalText(task.title)} (${sanitizeTerminalText(task.id)})`);
|
|
373
|
+
console.log(`Status: ${sanitizeTerminalText(task.status)}`);
|
|
374
|
+
console.log(`Project: ${sanitizeTerminalText(`${task.teamSlug}/${task.projectSlug}`)}`);
|
|
341
375
|
console.log(`Progress: ${task.progressPercentage}%`);
|
|
342
376
|
console.log(`Counts: completed=${task.counts.completed}, in_progress=${task.counts.inProgress}, pending=${task.counts.pending}, skipped=${task.counts.skipped}`);
|
|
343
377
|
console.log(`Required assignments: ${task.totalRequiredAssignments} (${task.datasetRowCount} rows x ${task.requiredAssignmentsPerRow})`);
|
|
344
378
|
if (task.assignees.length > 0) {
|
|
345
379
|
console.log('\nAssignees');
|
|
346
380
|
task.assignees.forEach(assignee => {
|
|
347
|
-
console.log(` ${assignee.email}: total=${assignee.total}, completed=${assignee.completed}, in_progress=${assignee.inProgress}, pending=${assignee.pending}, skipped=${assignee.skipped}`);
|
|
381
|
+
console.log(` ${sanitizeTerminalText(assignee.email)}: total=${assignee.total}, completed=${assignee.completed}, in_progress=${assignee.inProgress}, pending=${assignee.pending}, skipped=${assignee.skipped}`);
|
|
348
382
|
});
|
|
349
383
|
}
|
|
350
384
|
}
|
|
@@ -469,6 +503,7 @@ function renderCliAuthBrowserPage(status) {
|
|
|
469
503
|
}
|
|
470
504
|
async function login() {
|
|
471
505
|
const baseUrl = resolveLoginBaseUrl();
|
|
506
|
+
assertSecureTokenTransport(baseUrl);
|
|
472
507
|
const codeVerifier = createCodeVerifier();
|
|
473
508
|
const codeChallenge = createCodeChallenge(codeVerifier);
|
|
474
509
|
const callbackPort = resolveAuthCallbackPort();
|
|
@@ -517,8 +552,9 @@ async function login() {
|
|
|
517
552
|
return;
|
|
518
553
|
}
|
|
519
554
|
const { authorizeUrl } = await parseJsonResponse(response, 'CLI auth start');
|
|
520
|
-
|
|
521
|
-
|
|
555
|
+
const safeAuthorizeUrl = validateBrowserUrl(authorizeUrl, baseUrl).href;
|
|
556
|
+
console.log(`Opening browser for login: ${sanitizeTerminalText(safeAuthorizeUrl)}`);
|
|
557
|
+
openInBrowser(safeAuthorizeUrl);
|
|
522
558
|
}
|
|
523
559
|
catch (error) {
|
|
524
560
|
server.close();
|
|
@@ -541,7 +577,7 @@ async function login() {
|
|
|
541
577
|
refreshToken: loginData.refreshToken,
|
|
542
578
|
expiresAt: loginData.expiresAt,
|
|
543
579
|
});
|
|
544
|
-
console.log(`Logged in as ${loginData.user.email ?? loginData.user.id}`);
|
|
580
|
+
console.log(`Logged in as ${sanitizeTerminalText(loginData.user.email ?? loginData.user.id)}`);
|
|
545
581
|
}
|
|
546
582
|
async function whoami() {
|
|
547
583
|
const response = await authedFetch('/api/cli/auth/whoami');
|
|
@@ -549,25 +585,39 @@ async function whoami() {
|
|
|
549
585
|
throw new Error(`whoami failed: ${await response.text()}`);
|
|
550
586
|
}
|
|
551
587
|
const data = await response.json();
|
|
552
|
-
console.log(data.user.email ?? data.user.id);
|
|
588
|
+
console.log(sanitizeTerminalText(data.user.email ?? data.user.id));
|
|
553
589
|
}
|
|
554
590
|
async function logout() {
|
|
555
591
|
const baseUrl = getBaseUrl();
|
|
556
592
|
const credentials = getServerCredentials(baseUrl);
|
|
557
593
|
if (!credentials) {
|
|
558
|
-
console.log(`Already logged out for ${baseUrl}.`);
|
|
594
|
+
console.log(`Already logged out for ${sanitizeTerminalText(baseUrl)}.`);
|
|
559
595
|
return;
|
|
560
596
|
}
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
597
|
+
let remoteLogoutError = null;
|
|
598
|
+
try {
|
|
599
|
+
assertSecureTokenTransport(baseUrl);
|
|
600
|
+
const response = await fetch(`${baseUrl}/api/cli/auth/logout`, {
|
|
601
|
+
method: 'POST',
|
|
602
|
+
headers: {
|
|
603
|
+
'Content-Type': 'application/json',
|
|
604
|
+
Authorization: `Bearer ${credentials.accessToken}`,
|
|
605
|
+
},
|
|
606
|
+
body: JSON.stringify({ refreshToken: credentials.refreshToken }),
|
|
607
|
+
});
|
|
608
|
+
if (!response.ok) {
|
|
609
|
+
remoteLogoutError = sanitizeTerminalText(await response.text()).slice(0, 180);
|
|
610
|
+
}
|
|
611
|
+
}
|
|
612
|
+
catch (error) {
|
|
613
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
614
|
+
remoteLogoutError = sanitizeTerminalText(message).slice(0, 180);
|
|
615
|
+
}
|
|
569
616
|
clearServerCredentials(baseUrl);
|
|
570
|
-
|
|
617
|
+
if (remoteLogoutError) {
|
|
618
|
+
console.warn(`Warning: remote logout failed: ${remoteLogoutError}`);
|
|
619
|
+
}
|
|
620
|
+
console.log(`Logged out from ${sanitizeTerminalText(baseUrl)}.`);
|
|
571
621
|
}
|
|
572
622
|
async function listTeams() {
|
|
573
623
|
printTeams(await fetchTeams());
|
|
@@ -602,7 +652,7 @@ async function createTeam() {
|
|
|
602
652
|
throw new Error(`Failed to create team: ${await response.text()}`);
|
|
603
653
|
}
|
|
604
654
|
const data = await parseJsonResponse(response, 'Team create');
|
|
605
|
-
console.log(`Created team: ${data.team.name} (${data.team.slug})`);
|
|
655
|
+
console.log(`Created team: ${sanitizeTerminalText(data.team.name)} (${sanitizeTerminalText(data.team.slug)})`);
|
|
606
656
|
}
|
|
607
657
|
async function listProjects() {
|
|
608
658
|
const teamSlugArg = getArg('--team');
|
|
@@ -629,7 +679,7 @@ async function createProject() {
|
|
|
629
679
|
throw new Error(`Failed to create project: ${await response.text()}`);
|
|
630
680
|
}
|
|
631
681
|
const data = await parseJsonResponse(response, 'Project create');
|
|
632
|
-
console.log(`Created project ${data.project.teamSlug}/${data.project.slug}`);
|
|
682
|
+
console.log(`Created project ${sanitizeTerminalText(`${data.project.teamSlug}/${data.project.slug}`)}`);
|
|
633
683
|
}
|
|
634
684
|
async function listTasks() {
|
|
635
685
|
const project = getArg('--project');
|
|
@@ -695,9 +745,9 @@ async function createAppFromFile() {
|
|
|
695
745
|
throw new Error(`Failed to create app: ${await response.text()}`);
|
|
696
746
|
}
|
|
697
747
|
const data = await parseJsonResponse(response, 'App create');
|
|
698
|
-
console.log(`Created app ${data.app.name} (${data.app.id}) v${data.app.versionNum}`);
|
|
748
|
+
console.log(`Created app ${sanitizeTerminalText(data.app.name)} (${sanitizeTerminalText(data.app.id)}) v${data.app.versionNum}`);
|
|
699
749
|
if (data.warnings?.length) {
|
|
700
|
-
console.log(`Warnings: ${data.warnings.join('; ')}`);
|
|
750
|
+
console.log(`Warnings: ${sanitizeTerminalText(data.warnings.join('; '))}`);
|
|
701
751
|
}
|
|
702
752
|
}
|
|
703
753
|
async function updateAppFromFile() {
|
|
@@ -731,9 +781,9 @@ async function updateAppFromFile() {
|
|
|
731
781
|
throw new Error(`Failed to update app: ${await response.text()}`);
|
|
732
782
|
}
|
|
733
783
|
const data = await parseJsonResponse(response, 'App update');
|
|
734
|
-
console.log(`Updated app ${data.app.name} (${data.app.id}) to v${data.app.versionNum}`);
|
|
784
|
+
console.log(`Updated app ${sanitizeTerminalText(data.app.name)} (${sanitizeTerminalText(data.app.id)}) to v${data.app.versionNum}`);
|
|
735
785
|
if (data.warnings?.length) {
|
|
736
|
-
console.log(`Warnings: ${data.warnings.join('; ')}`);
|
|
786
|
+
console.log(`Warnings: ${sanitizeTerminalText(data.warnings.join('; '))}`);
|
|
737
787
|
}
|
|
738
788
|
}
|
|
739
789
|
async function linkAppDataset() {
|
|
@@ -765,7 +815,7 @@ async function linkAppDataset() {
|
|
|
765
815
|
throw new Error(`Failed to link dataset: ${await response.text()}`);
|
|
766
816
|
}
|
|
767
817
|
const data = await parseJsonResponse(response, 'App link dataset');
|
|
768
|
-
console.log(`Linked dataset ${data.linkedDataset.name} (${data.linkedDataset.id}) to app ${data.app.name} (${data.app.id}) version ${data.versionNum}`);
|
|
818
|
+
console.log(`Linked dataset ${sanitizeTerminalText(data.linkedDataset.name)} (${sanitizeTerminalText(data.linkedDataset.id)}) to app ${sanitizeTerminalText(data.app.name)} (${sanitizeTerminalText(data.app.id)}) version ${data.versionNum}`);
|
|
769
819
|
}
|
|
770
820
|
function parseCommaSeparated(value) {
|
|
771
821
|
if (!value) {
|
|
@@ -838,14 +888,14 @@ async function createTask() {
|
|
|
838
888
|
}
|
|
839
889
|
const baseUrl = getBaseUrl();
|
|
840
890
|
const taskUrl = `${baseUrl}/d/${projectSlug}/tasks/${data.task.id}`;
|
|
841
|
-
console.log(`Created task ${data.task.title} (${data.task.id}) [${data.task.status}]` +
|
|
842
|
-
`\n Task ID: ${data.task.id}` +
|
|
843
|
-
`\n Dataset ID: ${datasetId}` +
|
|
844
|
-
`\n Version: v${data.task.versionNum} (${data.task.versionId})` +
|
|
891
|
+
console.log(`Created task ${sanitizeTerminalText(data.task.title)} (${sanitizeTerminalText(data.task.id)}) [${sanitizeTerminalText(data.task.status)}]` +
|
|
892
|
+
`\n Task ID: ${sanitizeTerminalText(data.task.id)}` +
|
|
893
|
+
`\n Dataset ID: ${sanitizeTerminalText(datasetId)}` +
|
|
894
|
+
`\n Version: v${data.task.versionNum} (${sanitizeTerminalText(data.task.versionId)})` +
|
|
845
895
|
`\n Labels/row: ${data.task.requiredAssignmentsPerRow}` +
|
|
846
896
|
`\n Assignments: ${data.assignmentsCreated}` +
|
|
847
|
-
(data.warning ? `\n Warning: ${data.warning}` : '') +
|
|
848
|
-
`\n URL: ${taskUrl}`);
|
|
897
|
+
(data.warning ? `\n Warning: ${sanitizeTerminalText(data.warning)}` : '') +
|
|
898
|
+
`\n URL: ${sanitizeTerminalText(taskUrl)}`);
|
|
849
899
|
}
|
|
850
900
|
async function assignTask() {
|
|
851
901
|
const taskId = getArg('--task');
|
|
@@ -922,7 +972,7 @@ async function updateTaskStatus(targetStatus) {
|
|
|
922
972
|
}
|
|
923
973
|
const data = await parseJsonResponse(response, 'Task status update');
|
|
924
974
|
const action = targetStatus === 'paused' ? 'Paused' : 'Unpaused';
|
|
925
|
-
console.log(`${action} task ${data.task.id} [${data.task.status}]`);
|
|
975
|
+
console.log(`${action} task ${sanitizeTerminalText(data.task.id)} [${sanitizeTerminalText(data.task.status)}]`);
|
|
926
976
|
}
|
|
927
977
|
async function appDetail() {
|
|
928
978
|
const appId = getArg('--app');
|
|
@@ -945,10 +995,10 @@ async function appDetail() {
|
|
|
945
995
|
console.log(JSON.stringify(detail, null, 2));
|
|
946
996
|
return;
|
|
947
997
|
}
|
|
948
|
-
console.log(`App: ${detail.name} (${detail.id})`);
|
|
949
|
-
console.log(` Project: ${detail.teamSlug}/${detail.projectSlug}`);
|
|
998
|
+
console.log(`App: ${sanitizeTerminalText(detail.name)} (${sanitizeTerminalText(detail.id)})`);
|
|
999
|
+
console.log(` Project: ${sanitizeTerminalText(`${detail.teamSlug}/${detail.projectSlug}`)}`);
|
|
950
1000
|
if (detail.currentVersion) {
|
|
951
|
-
console.log(` Current version: v${detail.currentVersion.versionNum} (${detail.currentVersion.versionId})`);
|
|
1001
|
+
console.log(` Current version: v${detail.currentVersion.versionNum} (${sanitizeTerminalText(detail.currentVersion.versionId)})`);
|
|
952
1002
|
console.log(` Input schema: ${detail.currentVersion.inputJsonSchema ? 'defined' : 'none'}`);
|
|
953
1003
|
console.log(` Output schema: ${detail.currentVersion.outputJsonSchema ? 'defined' : 'none'}`);
|
|
954
1004
|
}
|
|
@@ -957,10 +1007,10 @@ async function appDetail() {
|
|
|
957
1007
|
}
|
|
958
1008
|
console.log(` Compatible datasets: ${detail.compatibleDatasetsCount}/${detail.totalDatasetsCount}`);
|
|
959
1009
|
if (detail.createdByEmail) {
|
|
960
|
-
console.log(` Created by: ${detail.createdByName || detail.createdByEmail}`);
|
|
1010
|
+
console.log(` Created by: ${sanitizeTerminalText(detail.createdByName || detail.createdByEmail)}`);
|
|
961
1011
|
}
|
|
962
|
-
console.log(` Created: ${detail.createdAt}`);
|
|
963
|
-
console.log(` Updated: ${detail.updatedAt}`);
|
|
1012
|
+
console.log(` Created: ${sanitizeTerminalText(detail.createdAt)}`);
|
|
1013
|
+
console.log(` Updated: ${sanitizeTerminalText(detail.updatedAt)}`);
|
|
964
1014
|
}
|
|
965
1015
|
async function listTeamMembers() {
|
|
966
1016
|
const teamSlug = await resolveTeamSlug(getArg('--team'));
|
|
@@ -981,7 +1031,7 @@ async function addTeamMember() {
|
|
|
981
1031
|
throw new Error(`Failed to add team member: ${await response.text()}`);
|
|
982
1032
|
}
|
|
983
1033
|
const data = await parseJsonResponse(response, 'Team member add');
|
|
984
|
-
console.log(`Added team member ${data.member.email} (${data.member.id})`);
|
|
1034
|
+
console.log(`Added team member ${sanitizeTerminalText(data.member.email)} (${sanitizeTerminalText(data.member.id)})`);
|
|
985
1035
|
}
|
|
986
1036
|
async function removeTeamMember() {
|
|
987
1037
|
const teamSlug = await resolveTeamSlug(getArg('--team'));
|
|
@@ -998,7 +1048,7 @@ async function removeTeamMember() {
|
|
|
998
1048
|
if (!response.ok) {
|
|
999
1049
|
throw new Error(`Failed to remove team member: ${await response.text()}`);
|
|
1000
1050
|
}
|
|
1001
|
-
console.log(`Removed team member ${member.email}`);
|
|
1051
|
+
console.log(`Removed team member ${sanitizeTerminalText(member.email)}`);
|
|
1002
1052
|
}
|
|
1003
1053
|
async function changeTeamMemberRole() {
|
|
1004
1054
|
const teamSlugArg = getArg('--team');
|
|
@@ -1024,7 +1074,7 @@ async function changeTeamMemberRole() {
|
|
|
1024
1074
|
if (!response.ok) {
|
|
1025
1075
|
throw new Error(`Failed to update member role: ${await response.text()}`);
|
|
1026
1076
|
}
|
|
1027
|
-
console.log(`Updated ${member.email} role to ${role}`);
|
|
1077
|
+
console.log(`Updated ${sanitizeTerminalText(member.email)} role to ${sanitizeTerminalText(role)}`);
|
|
1028
1078
|
}
|
|
1029
1079
|
async function createDatasetFromRows(project, name, sourceType, rows) {
|
|
1030
1080
|
const response = await authedFetch('/api/cli/datasets/upload', {
|
|
@@ -1061,7 +1111,7 @@ async function uploadJsonlDatasetInChunks(file, project, datasetName) {
|
|
|
1061
1111
|
throw new Error(message);
|
|
1062
1112
|
}
|
|
1063
1113
|
throw new Error(`Upload stopped while reading the next JSONL chunk: ${message}\n` +
|
|
1064
|
-
`Dataset ${dataset.name} (${dataset.id}) was created and ${totalUploaded} rows were uploaded. ` +
|
|
1114
|
+
`Dataset ${sanitizeTerminalText(dataset.name)} (${sanitizeTerminalText(dataset.id)}) was created and ${totalUploaded} rows were uploaded. ` +
|
|
1065
1115
|
`Fix the file, remove the first ${totalUploaded} rows, and run ` +
|
|
1066
1116
|
`orizu datasets append --dataset ${dataset.id} --file <remaining-file>.`);
|
|
1067
1117
|
}
|
|
@@ -1099,7 +1149,7 @@ async function uploadJsonlDatasetInChunks(file, project, datasetName) {
|
|
|
1099
1149
|
if (!dataset) {
|
|
1100
1150
|
throw new Error('Dataset file contains no rows');
|
|
1101
1151
|
}
|
|
1102
|
-
console.log(`Uploaded dataset ${dataset.name} (${dataset.id}) with ${dataset.rowCount} rows.`);
|
|
1152
|
+
console.log(`Uploaded dataset ${sanitizeTerminalText(dataset.name)} (${sanitizeTerminalText(dataset.id)}) with ${dataset.rowCount} rows.`);
|
|
1103
1153
|
if (dataset.url) {
|
|
1104
1154
|
console.log(`View dataset: ${formatTerminalLink(dataset.url)}`);
|
|
1105
1155
|
}
|
|
@@ -1120,7 +1170,7 @@ async function uploadDataset() {
|
|
|
1120
1170
|
}
|
|
1121
1171
|
const { rows, sourceType } = parseDatasetFile(file);
|
|
1122
1172
|
const data = await createDatasetFromRows(project, datasetName, sourceType, rows);
|
|
1123
|
-
console.log(`Uploaded dataset ${data.dataset.name} (${data.dataset.id}) with ${data.dataset.rowCount} rows.`);
|
|
1173
|
+
console.log(`Uploaded dataset ${sanitizeTerminalText(data.dataset.name)} (${sanitizeTerminalText(data.dataset.id)}) with ${data.dataset.rowCount} rows.`);
|
|
1124
1174
|
if (data.dataset.url) {
|
|
1125
1175
|
console.log(`View dataset: ${formatTerminalLink(data.dataset.url)}`);
|
|
1126
1176
|
}
|
|
@@ -1161,7 +1211,7 @@ async function downloadDataset() {
|
|
|
1161
1211
|
: `${datasetId}.${format}`;
|
|
1162
1212
|
const bytes = new Uint8Array(await response.arrayBuffer());
|
|
1163
1213
|
writeFileSync(filename, bytes);
|
|
1164
|
-
console.log(`Saved dataset ${datasetId} (${format.toUpperCase()}) to ${filename}`);
|
|
1214
|
+
console.log(`Saved dataset ${sanitizeTerminalText(datasetId)} (${format.toUpperCase()}) to ${sanitizeTerminalText(filename)}`);
|
|
1165
1215
|
}
|
|
1166
1216
|
const MAX_INPUT_FILE_SIZE_BYTES = 50 * 1024 * 1024; // 50 MB
|
|
1167
1217
|
const APPEND_CHUNK_SIZE_ROWS = 500;
|
|
@@ -1213,7 +1263,7 @@ async function appendJsonlDatasetRowsInChunks(datasetId, file) {
|
|
|
1213
1263
|
if (!lastResult) {
|
|
1214
1264
|
throw new Error('Dataset append file must contain at least one row');
|
|
1215
1265
|
}
|
|
1216
|
-
console.log(`Appended ${totalAppended} rows to dataset ${lastResult.dataset.name} (${lastResult.dataset.id}). New row count: ${lastResult.dataset.rowCount}`);
|
|
1266
|
+
console.log(`Appended ${totalAppended} rows to dataset ${sanitizeTerminalText(lastResult.dataset.name)} (${sanitizeTerminalText(lastResult.dataset.id)}). New row count: ${lastResult.dataset.rowCount}`);
|
|
1217
1267
|
}
|
|
1218
1268
|
async function appendDatasetRows() {
|
|
1219
1269
|
const projectArg = getArg('--project');
|
|
@@ -1262,7 +1312,7 @@ async function appendDatasetRows() {
|
|
|
1262
1312
|
}
|
|
1263
1313
|
if (rows.length <= APPEND_CHUNK_SIZE_ROWS) {
|
|
1264
1314
|
const data = await appendChunk(datasetId, rows);
|
|
1265
|
-
console.log(`Appended ${data.appendedCount} rows to dataset ${data.dataset.name} (${data.dataset.id}). New row count: ${data.dataset.rowCount}`);
|
|
1315
|
+
console.log(`Appended ${data.appendedCount} rows to dataset ${sanitizeTerminalText(data.dataset.name)} (${sanitizeTerminalText(data.dataset.id)}). New row count: ${data.dataset.rowCount}`);
|
|
1266
1316
|
return;
|
|
1267
1317
|
}
|
|
1268
1318
|
// Chunked upload for large row counts (ALI-555: track partial progress)
|
|
@@ -1286,7 +1336,7 @@ async function appendDatasetRows() {
|
|
|
1286
1336
|
}
|
|
1287
1337
|
}
|
|
1288
1338
|
if (lastResult) {
|
|
1289
|
-
console.log(`Appended ${totalAppended} rows to dataset ${lastResult.dataset.name} (${lastResult.dataset.id}). New row count: ${lastResult.dataset.rowCount}`);
|
|
1339
|
+
console.log(`Appended ${totalAppended} rows to dataset ${sanitizeTerminalText(lastResult.dataset.name)} (${sanitizeTerminalText(lastResult.dataset.id)}). New row count: ${lastResult.dataset.rowCount}`);
|
|
1290
1340
|
}
|
|
1291
1341
|
}
|
|
1292
1342
|
async function editDatasetRows() {
|
|
@@ -1332,7 +1382,7 @@ async function editDatasetRows() {
|
|
|
1332
1382
|
throw new Error(`Edit rows failed: ${await response.text()}`);
|
|
1333
1383
|
}
|
|
1334
1384
|
const data = await parseJsonResponse(response, 'Dataset edit rows');
|
|
1335
|
-
console.log(`Updated ${data.updatedCount} rows in dataset ${data.dataset.name} (${data.dataset.id}). Current row count: ${data.dataset.rowCount}`);
|
|
1385
|
+
console.log(`Updated ${data.updatedCount} rows in dataset ${sanitizeTerminalText(data.dataset.name)} (${sanitizeTerminalText(data.dataset.id)}). Current row count: ${data.dataset.rowCount}`);
|
|
1336
1386
|
}
|
|
1337
1387
|
async function deleteDatasetRows() {
|
|
1338
1388
|
const projectArg = getArg('--project');
|
|
@@ -1360,7 +1410,7 @@ async function deleteDatasetRows() {
|
|
|
1360
1410
|
throw new Error(`Delete rows failed: ${await response.text()}`);
|
|
1361
1411
|
}
|
|
1362
1412
|
const data = await parseJsonResponse(response, 'Dataset delete rows');
|
|
1363
|
-
console.log(`Deleted ${data.deletedCount} rows from dataset ${data.dataset.name} (${data.dataset.id}). New row count: ${data.dataset.rowCount}`);
|
|
1413
|
+
console.log(`Deleted ${data.deletedCount} rows from dataset ${sanitizeTerminalText(data.dataset.name)} (${sanitizeTerminalText(data.dataset.id)}). New row count: ${data.dataset.rowCount}`);
|
|
1364
1414
|
}
|
|
1365
1415
|
async function lockDataset() {
|
|
1366
1416
|
const projectArg = getArg('--project');
|
|
@@ -1383,7 +1433,7 @@ async function lockDataset() {
|
|
|
1383
1433
|
throw new Error(`Lock failed: ${await response.text()}`);
|
|
1384
1434
|
}
|
|
1385
1435
|
const data = await parseJsonResponse(response, 'Dataset lock');
|
|
1386
|
-
console.log(`Locked dataset ${data.dataset.name} (${data.dataset.id}) at ${data.dataset.lockedAt}. Row count: ${data.dataset.rowCount}`);
|
|
1436
|
+
console.log(`Locked dataset ${sanitizeTerminalText(data.dataset.name)} (${sanitizeTerminalText(data.dataset.id)}) at ${sanitizeTerminalText(data.dataset.lockedAt)}. Row count: ${data.dataset.rowCount}`);
|
|
1387
1437
|
}
|
|
1388
1438
|
async function cloneDataset() {
|
|
1389
1439
|
const projectArg = getArg('--project');
|
|
@@ -1406,7 +1456,7 @@ async function cloneDataset() {
|
|
|
1406
1456
|
throw new Error(`Clone failed: ${await response.text()}`);
|
|
1407
1457
|
}
|
|
1408
1458
|
const data = await parseJsonResponse(response, 'Dataset clone');
|
|
1409
|
-
console.log(`Cloned dataset ${data.dataset.parentDatasetId} -> ${data.dataset.name} (${data.dataset.id}). Row count: ${data.dataset.rowCount}`);
|
|
1459
|
+
console.log(`Cloned dataset ${sanitizeTerminalText(data.dataset.parentDatasetId)} -> ${sanitizeTerminalText(data.dataset.name)} (${sanitizeTerminalText(data.dataset.id)}). Row count: ${data.dataset.rowCount}`);
|
|
1410
1460
|
}
|
|
1411
1461
|
async function downloadAnnotations() {
|
|
1412
1462
|
let taskId = getArg('--task');
|
|
@@ -1428,7 +1478,7 @@ async function downloadAnnotations() {
|
|
|
1428
1478
|
: fallbackName;
|
|
1429
1479
|
const bytes = new Uint8Array(await response.arrayBuffer());
|
|
1430
1480
|
writeFileSync(filename, bytes);
|
|
1431
|
-
console.log(`Saved ${format.toUpperCase()} export to ${filename}`);
|
|
1481
|
+
console.log(`Saved ${format.toUpperCase()} export to ${sanitizeTerminalText(filename)}`);
|
|
1432
1482
|
}
|
|
1433
1483
|
async function main() {
|
|
1434
1484
|
const parsed = parseGlobalFlags(process.argv.slice(2));
|
|
@@ -1565,6 +1615,6 @@ async function main() {
|
|
|
1565
1615
|
process.exit(1);
|
|
1566
1616
|
}
|
|
1567
1617
|
main().catch(error => {
|
|
1568
|
-
console.error(error instanceof Error ? error.message : 'Unknown error');
|
|
1618
|
+
console.error(sanitizeTerminalText(error instanceof Error ? error.message : 'Unknown error'));
|
|
1569
1619
|
process.exit(1);
|
|
1570
1620
|
});
|
package/package.json
CHANGED
package/src/credentials.ts
CHANGED
|
@@ -1,6 +1,18 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import {
|
|
2
|
+
chmodSync,
|
|
3
|
+
closeSync,
|
|
4
|
+
existsSync,
|
|
5
|
+
fsyncSync,
|
|
6
|
+
mkdirSync,
|
|
7
|
+
openSync,
|
|
8
|
+
readFileSync,
|
|
9
|
+
renameSync,
|
|
10
|
+
rmSync,
|
|
11
|
+
writeFileSync,
|
|
12
|
+
} from 'fs'
|
|
2
13
|
import { join } from 'path'
|
|
3
14
|
import { homedir } from 'os'
|
|
15
|
+
import { randomBytes } from 'crypto'
|
|
4
16
|
import { ServerCredentials, StoredCredentialsV1, StoredCredentialsV2 } from './types.js'
|
|
5
17
|
|
|
6
18
|
function getConfigDir(): string {
|
|
@@ -53,9 +65,36 @@ function migrateToV2(stored: StoredCredentialsV1): StoredCredentialsV2 {
|
|
|
53
65
|
|
|
54
66
|
function writeCredentials(config: StoredCredentialsV2) {
|
|
55
67
|
const dir = getConfigDir()
|
|
56
|
-
mkdirSync(dir, { recursive: true })
|
|
68
|
+
mkdirSync(dir, { recursive: true, mode: 0o700 })
|
|
69
|
+
chmodSync(dir, 0o700)
|
|
70
|
+
|
|
57
71
|
const path = getCredentialsPath()
|
|
58
|
-
|
|
72
|
+
const tempPath = join(
|
|
73
|
+
dir,
|
|
74
|
+
`.credentials.json.${process.pid}.${randomBytes(8).toString('hex')}.tmp`
|
|
75
|
+
)
|
|
76
|
+
const payload = JSON.stringify(config, null, 2) + '\n'
|
|
77
|
+
const fd = openSync(tempPath, 'wx', 0o600)
|
|
78
|
+
|
|
79
|
+
try {
|
|
80
|
+
writeFileSync(fd, payload, 'utf-8')
|
|
81
|
+
fsyncSync(fd)
|
|
82
|
+
} finally {
|
|
83
|
+
closeSync(fd)
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
chmodSync(tempPath, 0o600)
|
|
87
|
+
try {
|
|
88
|
+
renameSync(tempPath, path)
|
|
89
|
+
} catch (error: any) {
|
|
90
|
+
if (process.platform === 'win32' && error?.code === 'EEXIST') {
|
|
91
|
+
rmSync(path, { force: true })
|
|
92
|
+
renameSync(tempPath, path)
|
|
93
|
+
} else {
|
|
94
|
+
rmSync(tempPath, { force: true })
|
|
95
|
+
throw error
|
|
96
|
+
}
|
|
97
|
+
}
|
|
59
98
|
chmodSync(path, 0o600)
|
|
60
99
|
}
|
|
61
100
|
|
package/src/http.ts
CHANGED
|
@@ -45,12 +45,43 @@ export function getBaseUrl(): string {
|
|
|
45
45
|
return resolveBaseUrl()
|
|
46
46
|
}
|
|
47
47
|
|
|
48
|
+
function isLoopbackHostname(hostname: string): boolean {
|
|
49
|
+
const normalized = hostname.toLowerCase()
|
|
50
|
+
return (
|
|
51
|
+
normalized === 'localhost' ||
|
|
52
|
+
normalized === '[::1]' ||
|
|
53
|
+
/^127(?:\.\d{1,3}){3}$/.test(normalized)
|
|
54
|
+
)
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export function assertSecureTokenTransport(baseUrl: string) {
|
|
58
|
+
let parsed: URL
|
|
59
|
+
try {
|
|
60
|
+
parsed = new URL(baseUrl)
|
|
61
|
+
} catch {
|
|
62
|
+
throw new Error(`Invalid server URL: '${baseUrl}'`)
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
if (parsed.protocol === 'https:') {
|
|
66
|
+
return
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
if (parsed.protocol === 'http:' && isLoopbackHostname(parsed.hostname)) {
|
|
70
|
+
return
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
throw new Error(
|
|
74
|
+
`Refusing to send CLI tokens to ${baseUrl}. Use HTTPS, or --local for loopback development.`
|
|
75
|
+
)
|
|
76
|
+
}
|
|
77
|
+
|
|
48
78
|
function isExpired(expiresAt: number): boolean {
|
|
49
79
|
const nowUnix = Math.floor(Date.now() / 1000)
|
|
50
80
|
return expiresAt <= nowUnix + 30
|
|
51
81
|
}
|
|
52
82
|
|
|
53
83
|
async function refreshCredentials(baseUrl: string, credentials: ServerCredentials): Promise<ServerCredentials> {
|
|
84
|
+
assertSecureTokenTransport(baseUrl)
|
|
54
85
|
const response = await fetch(`${baseUrl}/api/cli/auth/refresh`, {
|
|
55
86
|
method: 'POST',
|
|
56
87
|
headers: { 'Content-Type': 'application/json' },
|
|
@@ -73,6 +104,7 @@ async function refreshCredentials(baseUrl: string, credentials: ServerCredential
|
|
|
73
104
|
|
|
74
105
|
export async function authedFetch(path: string, init: RequestInit = {}) {
|
|
75
106
|
const baseUrl = resolveBaseUrl()
|
|
107
|
+
assertSecureTokenTransport(baseUrl)
|
|
76
108
|
const credentials = getServerCredentials(baseUrl)
|
|
77
109
|
if (!credentials) {
|
|
78
110
|
throw new Error(`Not logged in for ${baseUrl}. Run \`orizu login --server ${baseUrl}\` (or \`--local\`) first.`)
|
package/src/index.ts
CHANGED
|
@@ -11,7 +11,13 @@ import { parseDatasetFile } from './file-parser.js'
|
|
|
11
11
|
import { streamJsonlRowChunks } from './jsonl-stream.js'
|
|
12
12
|
import { parseDatasetReference } from './dataset-download.js'
|
|
13
13
|
import { parseGlobalFlags } from './global-flags.js'
|
|
14
|
-
import {
|
|
14
|
+
import {
|
|
15
|
+
assertSecureTokenTransport,
|
|
16
|
+
authedFetch,
|
|
17
|
+
getBaseUrl,
|
|
18
|
+
resolveLoginBaseUrl,
|
|
19
|
+
setGlobalFlags,
|
|
20
|
+
} from './http.js'
|
|
15
21
|
import { formatTaskCreateError } from './task-create-error.js'
|
|
16
22
|
import { LoginResponse } from './types.js'
|
|
17
23
|
|
|
@@ -163,10 +169,39 @@ function createCodeChallenge(verifier: string): string {
|
|
|
163
169
|
return createHash('sha256').update(verifier).digest('base64url')
|
|
164
170
|
}
|
|
165
171
|
|
|
172
|
+
function sanitizeTerminalText(value: unknown): string {
|
|
173
|
+
return String(value).replace(/[\u0000-\u0008\u000B\u000C\u000E-\u001F\u007F-\u009F]/g, '')
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
function validateBrowserUrl(url: string, expectedOrigin?: string): URL {
|
|
177
|
+
let parsed: URL
|
|
178
|
+
try {
|
|
179
|
+
parsed = new URL(url)
|
|
180
|
+
} catch {
|
|
181
|
+
throw new Error('Server returned an invalid browser URL.')
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
if (parsed.protocol !== 'https:' && parsed.protocol !== 'http:') {
|
|
185
|
+
throw new Error('Server returned an unsupported browser URL scheme.')
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
if (parsed.username || parsed.password) {
|
|
189
|
+
throw new Error('Server returned a browser URL containing credentials.')
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
if (expectedOrigin && parsed.origin !== expectedOrigin) {
|
|
193
|
+
throw new Error('Server returned a browser URL for an unexpected origin.')
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
return parsed
|
|
197
|
+
}
|
|
198
|
+
|
|
166
199
|
function openInBrowser(url: string) {
|
|
200
|
+
const parsed = validateBrowserUrl(url)
|
|
167
201
|
const platform = process.platform
|
|
202
|
+
const href = parsed.href
|
|
168
203
|
if (platform === 'darwin') {
|
|
169
|
-
spawn('open', [
|
|
204
|
+
spawn('open', [href], {
|
|
170
205
|
detached: true,
|
|
171
206
|
stdio: 'ignore',
|
|
172
207
|
}).unref()
|
|
@@ -174,7 +209,7 @@ function openInBrowser(url: string) {
|
|
|
174
209
|
}
|
|
175
210
|
|
|
176
211
|
if (platform === 'win32') {
|
|
177
|
-
spawn('
|
|
212
|
+
spawn('rundll32.exe', ['url.dll,FileProtocolHandler', href], {
|
|
178
213
|
detached: true,
|
|
179
214
|
stdio: 'ignore',
|
|
180
215
|
windowsHide: true,
|
|
@@ -182,18 +217,28 @@ function openInBrowser(url: string) {
|
|
|
182
217
|
return
|
|
183
218
|
}
|
|
184
219
|
|
|
185
|
-
spawn('xdg-open', [
|
|
220
|
+
spawn('xdg-open', [href], {
|
|
186
221
|
detached: true,
|
|
187
222
|
stdio: 'ignore',
|
|
188
223
|
}).unref()
|
|
189
224
|
}
|
|
190
225
|
|
|
191
226
|
function formatTerminalLink(url: string): string {
|
|
227
|
+
const safeUrl = sanitizeTerminalText(url)
|
|
228
|
+
try {
|
|
229
|
+
const parsed = validateBrowserUrl(safeUrl)
|
|
230
|
+
if (parsed.protocol !== 'https:' && parsed.protocol !== 'http:') {
|
|
231
|
+
return safeUrl
|
|
232
|
+
}
|
|
233
|
+
} catch {
|
|
234
|
+
return safeUrl
|
|
235
|
+
}
|
|
236
|
+
|
|
192
237
|
if (!isInteractiveTerminal()) {
|
|
193
|
-
return
|
|
238
|
+
return safeUrl
|
|
194
239
|
}
|
|
195
240
|
|
|
196
|
-
return `\u001B]8;;${
|
|
241
|
+
return `\u001B]8;;${safeUrl}\u0007${safeUrl}\u001B]8;;\u0007`
|
|
197
242
|
}
|
|
198
243
|
|
|
199
244
|
async function parseJsonResponse<T>(response: Response, context: string): Promise<T> {
|
|
@@ -203,7 +248,7 @@ async function parseJsonResponse<T>(response: Response, context: string): Promis
|
|
|
203
248
|
if (!contentType.includes('application/json')) {
|
|
204
249
|
throw new Error(
|
|
205
250
|
`${context} returned non-JSON response (status ${response.status}). ` +
|
|
206
|
-
`Body preview: ${rawBody.slice(0, 180)}`
|
|
251
|
+
`Body preview: ${sanitizeTerminalText(rawBody.slice(0, 180))}`
|
|
207
252
|
)
|
|
208
253
|
}
|
|
209
254
|
|
|
@@ -212,7 +257,7 @@ async function parseJsonResponse<T>(response: Response, context: string): Promis
|
|
|
212
257
|
} catch {
|
|
213
258
|
throw new Error(
|
|
214
259
|
`${context} returned invalid JSON (status ${response.status}). ` +
|
|
215
|
-
`Body preview: ${rawBody.slice(0, 180)}`
|
|
260
|
+
`Body preview: ${sanitizeTerminalText(rawBody.slice(0, 180))}`
|
|
216
261
|
)
|
|
217
262
|
}
|
|
218
263
|
}
|
|
@@ -236,9 +281,9 @@ async function promptSelect<T>(
|
|
|
236
281
|
return items[0]
|
|
237
282
|
}
|
|
238
283
|
|
|
239
|
-
console.log(`\n${title}`)
|
|
284
|
+
console.log(`\n${sanitizeTerminalText(title)}`)
|
|
240
285
|
items.forEach((item, index) => {
|
|
241
|
-
console.log(` ${index + 1}. ${label(item, index)}`)
|
|
286
|
+
console.log(` ${index + 1}. ${sanitizeTerminalText(label(item, index))}`)
|
|
242
287
|
})
|
|
243
288
|
|
|
244
289
|
const rl = createInterface({ input, output })
|
|
@@ -354,7 +399,7 @@ async function resolveProjectSlug(projectArg: string | null): Promise<string> {
|
|
|
354
399
|
|
|
355
400
|
const matchedTeam = teams.find(team => team.slug === teamSlug)
|
|
356
401
|
if (!matchedTeam) {
|
|
357
|
-
console.error(`Team '${teamSlug}' not found in your accessible teams.`)
|
|
402
|
+
console.error(`Team '${sanitizeTerminalText(teamSlug)}' not found in your accessible teams.`)
|
|
358
403
|
const selectedTeam = await promptSelect(
|
|
359
404
|
'Select a team',
|
|
360
405
|
teams,
|
|
@@ -375,7 +420,7 @@ async function resolveProjectSlug(projectArg: string | null): Promise<string> {
|
|
|
375
420
|
const matchedProject = projects.find(project => project.slug === projectSlug)
|
|
376
421
|
|
|
377
422
|
if (!matchedProject) {
|
|
378
|
-
console.error(`Project '${projectSlug}' not found in team '${matchedTeam.slug}'.`)
|
|
423
|
+
console.error(`Project '${sanitizeTerminalText(projectSlug)}' not found in team '${sanitizeTerminalText(matchedTeam.slug)}'.`)
|
|
379
424
|
const selectedProject = await promptSelect(
|
|
380
425
|
`Select a project in ${matchedTeam.slug}`,
|
|
381
426
|
projects,
|
|
@@ -461,9 +506,9 @@ function printTeams(teams: Team[]) {
|
|
|
461
506
|
}
|
|
462
507
|
|
|
463
508
|
const rows = teams.map(team => ({
|
|
464
|
-
slug: team.slug,
|
|
465
|
-
name: team.name || '-',
|
|
466
|
-
role: team.role || '-',
|
|
509
|
+
slug: sanitizeTerminalText(team.slug),
|
|
510
|
+
name: sanitizeTerminalText(team.name || '-'),
|
|
511
|
+
role: sanitizeTerminalText(team.role || '-'),
|
|
467
512
|
}))
|
|
468
513
|
|
|
469
514
|
const slugWidth = Math.max('TEAM SLUG'.length, ...rows.map(row => row.slug.length))
|
|
@@ -489,9 +534,9 @@ function printProjects(projects: Project[]) {
|
|
|
489
534
|
}
|
|
490
535
|
|
|
491
536
|
const rows = projects.map(project => ({
|
|
492
|
-
project: `${project.teamSlug}/${project.slug}
|
|
493
|
-
name: project.name || '-',
|
|
494
|
-
role: project.role || '-',
|
|
537
|
+
project: sanitizeTerminalText(`${project.teamSlug}/${project.slug}`),
|
|
538
|
+
name: sanitizeTerminalText(project.name || '-'),
|
|
539
|
+
role: sanitizeTerminalText(project.role || '-'),
|
|
495
540
|
}))
|
|
496
541
|
|
|
497
542
|
const projectWidth = Math.max('TEAM/PROJECT'.length, ...rows.map(row => row.project.length))
|
|
@@ -519,11 +564,11 @@ function printTasks(tasks: Task[]) {
|
|
|
519
564
|
}
|
|
520
565
|
|
|
521
566
|
const rows = tasks.map(task => ({
|
|
522
|
-
id: task.id,
|
|
523
|
-
name: task.title || '-',
|
|
524
|
-
status: task.status || '-',
|
|
567
|
+
id: sanitizeTerminalText(task.id),
|
|
568
|
+
name: sanitizeTerminalText(task.title || '-'),
|
|
569
|
+
status: sanitizeTerminalText(task.status || '-'),
|
|
525
570
|
project: task.teamSlug && task.projectSlug
|
|
526
|
-
? `${task.teamSlug}/${task.projectSlug}`
|
|
571
|
+
? sanitizeTerminalText(`${task.teamSlug}/${task.projectSlug}`)
|
|
527
572
|
: 'unknown-project',
|
|
528
573
|
}))
|
|
529
574
|
|
|
@@ -552,8 +597,8 @@ function printApps(apps: AppSummary[]) {
|
|
|
552
597
|
}
|
|
553
598
|
|
|
554
599
|
const rows = apps.map(app => ({
|
|
555
|
-
id: app.id,
|
|
556
|
-
name: app.name || '-',
|
|
600
|
+
id: sanitizeTerminalText(app.id),
|
|
601
|
+
name: sanitizeTerminalText(app.name || '-'),
|
|
557
602
|
version: `v${app.currentVersionNum || 1}`,
|
|
558
603
|
}))
|
|
559
604
|
|
|
@@ -576,10 +621,10 @@ function printTeamMembers(members: TeamMember[]) {
|
|
|
576
621
|
}
|
|
577
622
|
|
|
578
623
|
const rows = members.map(member => ({
|
|
579
|
-
id: member.id,
|
|
580
|
-
userId: member.user_id || '-',
|
|
581
|
-
email: member.email || '-',
|
|
582
|
-
role: member.role || '-',
|
|
624
|
+
id: sanitizeTerminalText(member.id),
|
|
625
|
+
userId: sanitizeTerminalText(member.user_id || '-'),
|
|
626
|
+
email: sanitizeTerminalText(member.email || '-'),
|
|
627
|
+
role: sanitizeTerminalText(member.role || '-'),
|
|
583
628
|
}))
|
|
584
629
|
|
|
585
630
|
const idWidth = Math.max('MEMBER ID'.length, ...rows.map(row => row.id.length))
|
|
@@ -602,9 +647,9 @@ function printTeamMembers(members: TeamMember[]) {
|
|
|
602
647
|
|
|
603
648
|
function printTaskStatusSummary(data: TaskStatusPayload) {
|
|
604
649
|
const task = data.task
|
|
605
|
-
console.log(`Task: ${task.title} (${task.id})`)
|
|
606
|
-
console.log(`Status: ${task.status}`)
|
|
607
|
-
console.log(`Project: ${task.teamSlug}/${task.projectSlug}`)
|
|
650
|
+
console.log(`Task: ${sanitizeTerminalText(task.title)} (${sanitizeTerminalText(task.id)})`)
|
|
651
|
+
console.log(`Status: ${sanitizeTerminalText(task.status)}`)
|
|
652
|
+
console.log(`Project: ${sanitizeTerminalText(`${task.teamSlug}/${task.projectSlug}`)}`)
|
|
608
653
|
console.log(`Progress: ${task.progressPercentage}%`)
|
|
609
654
|
console.log(`Counts: completed=${task.counts.completed}, in_progress=${task.counts.inProgress}, pending=${task.counts.pending}, skipped=${task.counts.skipped}`)
|
|
610
655
|
console.log(`Required assignments: ${task.totalRequiredAssignments} (${task.datasetRowCount} rows x ${task.requiredAssignmentsPerRow})`)
|
|
@@ -613,7 +658,7 @@ function printTaskStatusSummary(data: TaskStatusPayload) {
|
|
|
613
658
|
console.log('\nAssignees')
|
|
614
659
|
task.assignees.forEach(assignee => {
|
|
615
660
|
console.log(
|
|
616
|
-
` ${assignee.email}: total=${assignee.total}, completed=${assignee.completed}, in_progress=${assignee.inProgress}, pending=${assignee.pending}, skipped=${assignee.skipped}`
|
|
661
|
+
` ${sanitizeTerminalText(assignee.email)}: total=${assignee.total}, completed=${assignee.completed}, in_progress=${assignee.inProgress}, pending=${assignee.pending}, skipped=${assignee.skipped}`
|
|
617
662
|
)
|
|
618
663
|
})
|
|
619
664
|
}
|
|
@@ -748,6 +793,7 @@ function renderCliAuthBrowserPage(status: 'success' | 'error'): string {
|
|
|
748
793
|
|
|
749
794
|
async function login() {
|
|
750
795
|
const baseUrl = resolveLoginBaseUrl()
|
|
796
|
+
assertSecureTokenTransport(baseUrl)
|
|
751
797
|
const codeVerifier = createCodeVerifier()
|
|
752
798
|
const codeChallenge = createCodeChallenge(codeVerifier)
|
|
753
799
|
const callbackPort = resolveAuthCallbackPort()
|
|
@@ -807,8 +853,9 @@ async function login() {
|
|
|
807
853
|
response,
|
|
808
854
|
'CLI auth start'
|
|
809
855
|
)
|
|
810
|
-
|
|
811
|
-
|
|
856
|
+
const safeAuthorizeUrl = validateBrowserUrl(authorizeUrl, baseUrl).href
|
|
857
|
+
console.log(`Opening browser for login: ${sanitizeTerminalText(safeAuthorizeUrl)}`)
|
|
858
|
+
openInBrowser(safeAuthorizeUrl)
|
|
812
859
|
} catch (error) {
|
|
813
860
|
server.close()
|
|
814
861
|
reject(error)
|
|
@@ -834,7 +881,7 @@ async function login() {
|
|
|
834
881
|
expiresAt: loginData.expiresAt,
|
|
835
882
|
})
|
|
836
883
|
|
|
837
|
-
console.log(`Logged in as ${loginData.user.email ?? loginData.user.id}`)
|
|
884
|
+
console.log(`Logged in as ${sanitizeTerminalText(loginData.user.email ?? loginData.user.id)}`)
|
|
838
885
|
}
|
|
839
886
|
|
|
840
887
|
async function whoami() {
|
|
@@ -844,28 +891,42 @@ async function whoami() {
|
|
|
844
891
|
}
|
|
845
892
|
|
|
846
893
|
const data = await response.json() as { user: { id: string; email: string | null } }
|
|
847
|
-
console.log(data.user.email ?? data.user.id)
|
|
894
|
+
console.log(sanitizeTerminalText(data.user.email ?? data.user.id))
|
|
848
895
|
}
|
|
849
896
|
|
|
850
897
|
async function logout() {
|
|
851
898
|
const baseUrl = getBaseUrl()
|
|
852
899
|
const credentials = getServerCredentials(baseUrl)
|
|
853
900
|
if (!credentials) {
|
|
854
|
-
console.log(`Already logged out for ${baseUrl}.`)
|
|
901
|
+
console.log(`Already logged out for ${sanitizeTerminalText(baseUrl)}.`)
|
|
855
902
|
return
|
|
856
903
|
}
|
|
857
904
|
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
|
|
905
|
+
let remoteLogoutError: string | null = null
|
|
906
|
+
try {
|
|
907
|
+
assertSecureTokenTransport(baseUrl)
|
|
908
|
+
const response = await fetch(`${baseUrl}/api/cli/auth/logout`, {
|
|
909
|
+
method: 'POST',
|
|
910
|
+
headers: {
|
|
911
|
+
'Content-Type': 'application/json',
|
|
912
|
+
Authorization: `Bearer ${credentials.accessToken}`,
|
|
913
|
+
},
|
|
914
|
+
body: JSON.stringify({ refreshToken: credentials.refreshToken }),
|
|
915
|
+
})
|
|
916
|
+
|
|
917
|
+
if (!response.ok) {
|
|
918
|
+
remoteLogoutError = sanitizeTerminalText(await response.text()).slice(0, 180)
|
|
919
|
+
}
|
|
920
|
+
} catch (error) {
|
|
921
|
+
const message = error instanceof Error ? error.message : String(error)
|
|
922
|
+
remoteLogoutError = sanitizeTerminalText(message).slice(0, 180)
|
|
923
|
+
}
|
|
866
924
|
|
|
867
925
|
clearServerCredentials(baseUrl)
|
|
868
|
-
|
|
926
|
+
if (remoteLogoutError) {
|
|
927
|
+
console.warn(`Warning: remote logout failed: ${remoteLogoutError}`)
|
|
928
|
+
}
|
|
929
|
+
console.log(`Logged out from ${sanitizeTerminalText(baseUrl)}.`)
|
|
869
930
|
}
|
|
870
931
|
|
|
871
932
|
async function listTeams() {
|
|
@@ -914,7 +975,7 @@ async function createTeam() {
|
|
|
914
975
|
}
|
|
915
976
|
|
|
916
977
|
const data = await parseJsonResponse<{ team: Team }>(response, 'Team create')
|
|
917
|
-
console.log(`Created team: ${data.team.name} (${data.team.slug})`)
|
|
978
|
+
console.log(`Created team: ${sanitizeTerminalText(data.team.name)} (${sanitizeTerminalText(data.team.slug)})`)
|
|
918
979
|
}
|
|
919
980
|
|
|
920
981
|
async function listProjects() {
|
|
@@ -955,7 +1016,7 @@ async function createProject() {
|
|
|
955
1016
|
const data = await parseJsonResponse<{
|
|
956
1017
|
project: { id: string; name: string; slug: string; teamSlug: string }
|
|
957
1018
|
}>(response, 'Project create')
|
|
958
|
-
console.log(`Created project ${data.project.teamSlug}/${data.project.slug}`)
|
|
1019
|
+
console.log(`Created project ${sanitizeTerminalText(`${data.project.teamSlug}/${data.project.slug}`)}`)
|
|
959
1020
|
}
|
|
960
1021
|
|
|
961
1022
|
async function listTasks() {
|
|
@@ -1032,9 +1093,9 @@ async function createAppFromFile() {
|
|
|
1032
1093
|
app: { id: string; name: string; versionNum: number; componentName?: string }
|
|
1033
1094
|
warnings?: string[]
|
|
1034
1095
|
}>(response, 'App create')
|
|
1035
|
-
console.log(`Created app ${data.app.name} (${data.app.id}) v${data.app.versionNum}`)
|
|
1096
|
+
console.log(`Created app ${sanitizeTerminalText(data.app.name)} (${sanitizeTerminalText(data.app.id)}) v${data.app.versionNum}`)
|
|
1036
1097
|
if (data.warnings?.length) {
|
|
1037
|
-
console.log(`Warnings: ${data.warnings.join('; ')}`)
|
|
1098
|
+
console.log(`Warnings: ${sanitizeTerminalText(data.warnings.join('; '))}`)
|
|
1038
1099
|
}
|
|
1039
1100
|
}
|
|
1040
1101
|
|
|
@@ -1077,9 +1138,9 @@ async function updateAppFromFile() {
|
|
|
1077
1138
|
app: { id: string; name: string; versionNum: number; componentName?: string }
|
|
1078
1139
|
warnings?: string[]
|
|
1079
1140
|
}>(response, 'App update')
|
|
1080
|
-
console.log(`Updated app ${data.app.name} (${data.app.id}) to v${data.app.versionNum}`)
|
|
1141
|
+
console.log(`Updated app ${sanitizeTerminalText(data.app.name)} (${sanitizeTerminalText(data.app.id)}) to v${data.app.versionNum}`)
|
|
1081
1142
|
if (data.warnings?.length) {
|
|
1082
|
-
console.log(`Warnings: ${data.warnings.join('; ')}`)
|
|
1143
|
+
console.log(`Warnings: ${sanitizeTerminalText(data.warnings.join('; '))}`)
|
|
1083
1144
|
}
|
|
1084
1145
|
}
|
|
1085
1146
|
|
|
@@ -1125,7 +1186,7 @@ async function linkAppDataset() {
|
|
|
1125
1186
|
}>(response, 'App link dataset')
|
|
1126
1187
|
|
|
1127
1188
|
console.log(
|
|
1128
|
-
`Linked dataset ${data.linkedDataset.name} (${data.linkedDataset.id}) to app ${data.app.name} (${data.app.id}) version ${data.versionNum}`
|
|
1189
|
+
`Linked dataset ${sanitizeTerminalText(data.linkedDataset.name)} (${sanitizeTerminalText(data.linkedDataset.id)}) to app ${sanitizeTerminalText(data.app.name)} (${sanitizeTerminalText(data.app.id)}) version ${data.versionNum}`
|
|
1129
1190
|
)
|
|
1130
1191
|
}
|
|
1131
1192
|
|
|
@@ -1222,14 +1283,14 @@ async function createTask() {
|
|
|
1222
1283
|
const baseUrl = getBaseUrl()
|
|
1223
1284
|
const taskUrl = `${baseUrl}/d/${projectSlug}/tasks/${data.task.id}`
|
|
1224
1285
|
console.log(
|
|
1225
|
-
`Created task ${data.task.title} (${data.task.id}) [${data.task.status}]` +
|
|
1226
|
-
`\n Task ID: ${data.task.id}` +
|
|
1227
|
-
`\n Dataset ID: ${datasetId}` +
|
|
1228
|
-
`\n Version: v${data.task.versionNum} (${data.task.versionId})` +
|
|
1286
|
+
`Created task ${sanitizeTerminalText(data.task.title)} (${sanitizeTerminalText(data.task.id)}) [${sanitizeTerminalText(data.task.status)}]` +
|
|
1287
|
+
`\n Task ID: ${sanitizeTerminalText(data.task.id)}` +
|
|
1288
|
+
`\n Dataset ID: ${sanitizeTerminalText(datasetId)}` +
|
|
1289
|
+
`\n Version: v${data.task.versionNum} (${sanitizeTerminalText(data.task.versionId)})` +
|
|
1229
1290
|
`\n Labels/row: ${data.task.requiredAssignmentsPerRow}` +
|
|
1230
1291
|
`\n Assignments: ${data.assignmentsCreated}` +
|
|
1231
|
-
(data.warning ? `\n Warning: ${data.warning}` : '') +
|
|
1232
|
-
`\n URL: ${taskUrl}`
|
|
1292
|
+
(data.warning ? `\n Warning: ${sanitizeTerminalText(data.warning)}` : '') +
|
|
1293
|
+
`\n URL: ${sanitizeTerminalText(taskUrl)}`
|
|
1233
1294
|
)
|
|
1234
1295
|
}
|
|
1235
1296
|
|
|
@@ -1319,7 +1380,7 @@ async function updateTaskStatus(targetStatus: 'paused' | 'active') {
|
|
|
1319
1380
|
|
|
1320
1381
|
const data = await parseJsonResponse<{ task: { id: string; status: string } }>(response, 'Task status update')
|
|
1321
1382
|
const action = targetStatus === 'paused' ? 'Paused' : 'Unpaused'
|
|
1322
|
-
console.log(`${action} task ${data.task.id} [${data.task.status}]`)
|
|
1383
|
+
console.log(`${action} task ${sanitizeTerminalText(data.task.id)} [${sanitizeTerminalText(data.task.status)}]`)
|
|
1323
1384
|
}
|
|
1324
1385
|
|
|
1325
1386
|
interface AppDetailPayload {
|
|
@@ -1373,10 +1434,10 @@ async function appDetail() {
|
|
|
1373
1434
|
return
|
|
1374
1435
|
}
|
|
1375
1436
|
|
|
1376
|
-
console.log(`App: ${detail.name} (${detail.id})`)
|
|
1377
|
-
console.log(` Project: ${detail.teamSlug}/${detail.projectSlug}`)
|
|
1437
|
+
console.log(`App: ${sanitizeTerminalText(detail.name)} (${sanitizeTerminalText(detail.id)})`)
|
|
1438
|
+
console.log(` Project: ${sanitizeTerminalText(`${detail.teamSlug}/${detail.projectSlug}`)}`)
|
|
1378
1439
|
if (detail.currentVersion) {
|
|
1379
|
-
console.log(` Current version: v${detail.currentVersion.versionNum} (${detail.currentVersion.versionId})`)
|
|
1440
|
+
console.log(` Current version: v${detail.currentVersion.versionNum} (${sanitizeTerminalText(detail.currentVersion.versionId)})`)
|
|
1380
1441
|
console.log(` Input schema: ${detail.currentVersion.inputJsonSchema ? 'defined' : 'none'}`)
|
|
1381
1442
|
console.log(` Output schema: ${detail.currentVersion.outputJsonSchema ? 'defined' : 'none'}`)
|
|
1382
1443
|
} else {
|
|
@@ -1384,10 +1445,10 @@ async function appDetail() {
|
|
|
1384
1445
|
}
|
|
1385
1446
|
console.log(` Compatible datasets: ${detail.compatibleDatasetsCount}/${detail.totalDatasetsCount}`)
|
|
1386
1447
|
if (detail.createdByEmail) {
|
|
1387
|
-
console.log(` Created by: ${detail.createdByName || detail.createdByEmail}`)
|
|
1448
|
+
console.log(` Created by: ${sanitizeTerminalText(detail.createdByName || detail.createdByEmail)}`)
|
|
1388
1449
|
}
|
|
1389
|
-
console.log(` Created: ${detail.createdAt}`)
|
|
1390
|
-
console.log(` Updated: ${detail.updatedAt}`)
|
|
1450
|
+
console.log(` Created: ${sanitizeTerminalText(detail.createdAt)}`)
|
|
1451
|
+
console.log(` Updated: ${sanitizeTerminalText(detail.updatedAt)}`)
|
|
1391
1452
|
}
|
|
1392
1453
|
|
|
1393
1454
|
async function listTeamMembers() {
|
|
@@ -1412,7 +1473,7 @@ async function addTeamMember() {
|
|
|
1412
1473
|
throw new Error(`Failed to add team member: ${await response.text()}`)
|
|
1413
1474
|
}
|
|
1414
1475
|
const data = await parseJsonResponse<{ member: TeamMember }>(response, 'Team member add')
|
|
1415
|
-
console.log(`Added team member ${data.member.email} (${data.member.id})`)
|
|
1476
|
+
console.log(`Added team member ${sanitizeTerminalText(data.member.email)} (${sanitizeTerminalText(data.member.id)})`)
|
|
1416
1477
|
}
|
|
1417
1478
|
|
|
1418
1479
|
async function removeTeamMember() {
|
|
@@ -1435,7 +1496,7 @@ async function removeTeamMember() {
|
|
|
1435
1496
|
if (!response.ok) {
|
|
1436
1497
|
throw new Error(`Failed to remove team member: ${await response.text()}`)
|
|
1437
1498
|
}
|
|
1438
|
-
console.log(`Removed team member ${member.email}`)
|
|
1499
|
+
console.log(`Removed team member ${sanitizeTerminalText(member.email)}`)
|
|
1439
1500
|
}
|
|
1440
1501
|
|
|
1441
1502
|
async function changeTeamMemberRole() {
|
|
@@ -1469,7 +1530,7 @@ async function changeTeamMemberRole() {
|
|
|
1469
1530
|
throw new Error(`Failed to update member role: ${await response.text()}`)
|
|
1470
1531
|
}
|
|
1471
1532
|
|
|
1472
|
-
console.log(`Updated ${member.email} role to ${role}`)
|
|
1533
|
+
console.log(`Updated ${sanitizeTerminalText(member.email)} role to ${sanitizeTerminalText(role)}`)
|
|
1473
1534
|
}
|
|
1474
1535
|
|
|
1475
1536
|
async function createDatasetFromRows(
|
|
@@ -1521,7 +1582,7 @@ async function uploadJsonlDatasetInChunks(
|
|
|
1521
1582
|
|
|
1522
1583
|
throw new Error(
|
|
1523
1584
|
`Upload stopped while reading the next JSONL chunk: ${message}\n` +
|
|
1524
|
-
`Dataset ${dataset.name} (${dataset.id}) was created and ${totalUploaded} rows were uploaded. ` +
|
|
1585
|
+
`Dataset ${sanitizeTerminalText(dataset.name)} (${sanitizeTerminalText(dataset.id)}) was created and ${totalUploaded} rows were uploaded. ` +
|
|
1525
1586
|
`Fix the file, remove the first ${totalUploaded} rows, and run ` +
|
|
1526
1587
|
`orizu datasets append --dataset ${dataset.id} --file <remaining-file>.`
|
|
1527
1588
|
)
|
|
@@ -1568,7 +1629,7 @@ async function uploadJsonlDatasetInChunks(
|
|
|
1568
1629
|
throw new Error('Dataset file contains no rows')
|
|
1569
1630
|
}
|
|
1570
1631
|
|
|
1571
|
-
console.log(`Uploaded dataset ${dataset.name} (${dataset.id}) with ${dataset.rowCount} rows.`)
|
|
1632
|
+
console.log(`Uploaded dataset ${sanitizeTerminalText(dataset.name)} (${sanitizeTerminalText(dataset.id)}) with ${dataset.rowCount} rows.`)
|
|
1572
1633
|
if (dataset.url) {
|
|
1573
1634
|
console.log(`View dataset: ${formatTerminalLink(dataset.url)}`)
|
|
1574
1635
|
}
|
|
@@ -1595,7 +1656,7 @@ async function uploadDataset() {
|
|
|
1595
1656
|
const { rows, sourceType } = parseDatasetFile(file)
|
|
1596
1657
|
const data = await createDatasetFromRows(project, datasetName, sourceType, rows)
|
|
1597
1658
|
|
|
1598
|
-
console.log(`Uploaded dataset ${data.dataset.name} (${data.dataset.id}) with ${data.dataset.rowCount} rows.`)
|
|
1659
|
+
console.log(`Uploaded dataset ${sanitizeTerminalText(data.dataset.name)} (${sanitizeTerminalText(data.dataset.id)}) with ${data.dataset.rowCount} rows.`)
|
|
1599
1660
|
if (data.dataset.url) {
|
|
1600
1661
|
console.log(`View dataset: ${formatTerminalLink(data.dataset.url)}`)
|
|
1601
1662
|
}
|
|
@@ -1647,7 +1708,7 @@ async function downloadDataset() {
|
|
|
1647
1708
|
const bytes = new Uint8Array(await response.arrayBuffer())
|
|
1648
1709
|
writeFileSync(filename, bytes)
|
|
1649
1710
|
|
|
1650
|
-
console.log(`Saved dataset ${datasetId} (${format.toUpperCase()}) to ${filename}`)
|
|
1711
|
+
console.log(`Saved dataset ${sanitizeTerminalText(datasetId)} (${format.toUpperCase()}) to ${sanitizeTerminalText(filename)}`)
|
|
1651
1712
|
}
|
|
1652
1713
|
|
|
1653
1714
|
const MAX_INPUT_FILE_SIZE_BYTES = 50 * 1024 * 1024 // 50 MB
|
|
@@ -1719,7 +1780,7 @@ async function appendJsonlDatasetRowsInChunks(datasetId: string, file: string) {
|
|
|
1719
1780
|
}
|
|
1720
1781
|
|
|
1721
1782
|
console.log(
|
|
1722
|
-
`Appended ${totalAppended} rows to dataset ${lastResult.dataset.name} (${lastResult.dataset.id}). New row count: ${lastResult.dataset.rowCount}`
|
|
1783
|
+
`Appended ${totalAppended} rows to dataset ${sanitizeTerminalText(lastResult.dataset.name)} (${sanitizeTerminalText(lastResult.dataset.id)}). New row count: ${lastResult.dataset.rowCount}`
|
|
1723
1784
|
)
|
|
1724
1785
|
}
|
|
1725
1786
|
|
|
@@ -1782,7 +1843,7 @@ async function appendDatasetRows() {
|
|
|
1782
1843
|
if (rows.length <= APPEND_CHUNK_SIZE_ROWS) {
|
|
1783
1844
|
const data = await appendChunk(datasetId, rows)
|
|
1784
1845
|
console.log(
|
|
1785
|
-
`Appended ${data.appendedCount} rows to dataset ${data.dataset.name} (${data.dataset.id}). New row count: ${data.dataset.rowCount}`
|
|
1846
|
+
`Appended ${data.appendedCount} rows to dataset ${sanitizeTerminalText(data.dataset.name)} (${sanitizeTerminalText(data.dataset.id)}). New row count: ${data.dataset.rowCount}`
|
|
1786
1847
|
)
|
|
1787
1848
|
return
|
|
1788
1849
|
}
|
|
@@ -1813,7 +1874,7 @@ async function appendDatasetRows() {
|
|
|
1813
1874
|
|
|
1814
1875
|
if (lastResult) {
|
|
1815
1876
|
console.log(
|
|
1816
|
-
`Appended ${totalAppended} rows to dataset ${lastResult.dataset.name} (${lastResult.dataset.id}). New row count: ${lastResult.dataset.rowCount}`
|
|
1877
|
+
`Appended ${totalAppended} rows to dataset ${sanitizeTerminalText(lastResult.dataset.name)} (${sanitizeTerminalText(lastResult.dataset.id)}). New row count: ${lastResult.dataset.rowCount}`
|
|
1817
1878
|
)
|
|
1818
1879
|
}
|
|
1819
1880
|
}
|
|
@@ -1874,7 +1935,7 @@ async function editDatasetRows() {
|
|
|
1874
1935
|
}>(response, 'Dataset edit rows')
|
|
1875
1936
|
|
|
1876
1937
|
console.log(
|
|
1877
|
-
`Updated ${data.updatedCount} rows in dataset ${data.dataset.name} (${data.dataset.id}). Current row count: ${data.dataset.rowCount}`
|
|
1938
|
+
`Updated ${data.updatedCount} rows in dataset ${sanitizeTerminalText(data.dataset.name)} (${sanitizeTerminalText(data.dataset.id)}). Current row count: ${data.dataset.rowCount}`
|
|
1878
1939
|
)
|
|
1879
1940
|
}
|
|
1880
1941
|
|
|
@@ -1913,7 +1974,7 @@ async function deleteDatasetRows() {
|
|
|
1913
1974
|
}>(response, 'Dataset delete rows')
|
|
1914
1975
|
|
|
1915
1976
|
console.log(
|
|
1916
|
-
`Deleted ${data.deletedCount} rows from dataset ${data.dataset.name} (${data.dataset.id}). New row count: ${data.dataset.rowCount}`
|
|
1977
|
+
`Deleted ${data.deletedCount} rows from dataset ${sanitizeTerminalText(data.dataset.name)} (${sanitizeTerminalText(data.dataset.id)}). New row count: ${data.dataset.rowCount}`
|
|
1917
1978
|
)
|
|
1918
1979
|
}
|
|
1919
1980
|
|
|
@@ -1951,7 +2012,7 @@ async function lockDataset() {
|
|
|
1951
2012
|
}>(response, 'Dataset lock')
|
|
1952
2013
|
|
|
1953
2014
|
console.log(
|
|
1954
|
-
`Locked dataset ${data.dataset.name} (${data.dataset.id}) at ${data.dataset.lockedAt}. Row count: ${data.dataset.rowCount}`
|
|
2015
|
+
`Locked dataset ${sanitizeTerminalText(data.dataset.name)} (${sanitizeTerminalText(data.dataset.id)}) at ${sanitizeTerminalText(data.dataset.lockedAt)}. Row count: ${data.dataset.rowCount}`
|
|
1955
2016
|
)
|
|
1956
2017
|
}
|
|
1957
2018
|
|
|
@@ -1988,7 +2049,7 @@ async function cloneDataset() {
|
|
|
1988
2049
|
}>(response, 'Dataset clone')
|
|
1989
2050
|
|
|
1990
2051
|
console.log(
|
|
1991
|
-
`Cloned dataset ${data.dataset.parentDatasetId} -> ${data.dataset.name} (${data.dataset.id}). Row count: ${data.dataset.rowCount}`
|
|
2052
|
+
`Cloned dataset ${sanitizeTerminalText(data.dataset.parentDatasetId)} -> ${sanitizeTerminalText(data.dataset.name)} (${sanitizeTerminalText(data.dataset.id)}). Row count: ${data.dataset.rowCount}`
|
|
1992
2053
|
)
|
|
1993
2054
|
}
|
|
1994
2055
|
|
|
@@ -2018,7 +2079,7 @@ async function downloadAnnotations() {
|
|
|
2018
2079
|
const bytes = new Uint8Array(await response.arrayBuffer())
|
|
2019
2080
|
writeFileSync(filename, bytes)
|
|
2020
2081
|
|
|
2021
|
-
console.log(`Saved ${format.toUpperCase()} export to ${filename}`)
|
|
2082
|
+
console.log(`Saved ${format.toUpperCase()} export to ${sanitizeTerminalText(filename)}`)
|
|
2022
2083
|
}
|
|
2023
2084
|
|
|
2024
2085
|
async function main() {
|
|
@@ -2187,6 +2248,6 @@ async function main() {
|
|
|
2187
2248
|
}
|
|
2188
2249
|
|
|
2189
2250
|
main().catch(error => {
|
|
2190
|
-
console.error(error instanceof Error ? error.message : 'Unknown error')
|
|
2251
|
+
console.error(sanitizeTerminalText(error instanceof Error ? error.message : 'Unknown error'))
|
|
2191
2252
|
process.exit(1)
|
|
2192
2253
|
})
|