orizu 0.0.5 → 0.0.7
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 +102 -5
- package/dist/dataset-download.js +24 -0
- package/dist/global-flags.js +57 -0
- package/dist/http.js +32 -12
- package/dist/index.js +169 -17
- package/package.json +1 -1
- package/src/credentials.ts +123 -6
- package/src/dataset-download.ts +36 -0
- package/src/global-flags.ts +81 -0
- package/src/http.ts +39 -13
- package/src/index.ts +234 -17
- package/src/types.ts +16 -1
package/dist/credentials.js
CHANGED
|
@@ -2,27 +2,124 @@ import { mkdirSync, readFileSync, rmSync, writeFileSync, chmodSync, existsSync }
|
|
|
2
2
|
import { join } from 'path';
|
|
3
3
|
import { homedir } from 'os';
|
|
4
4
|
function getConfigDir() {
|
|
5
|
+
if (process.env.ORIZU_CONFIG_DIR) {
|
|
6
|
+
return process.env.ORIZU_CONFIG_DIR;
|
|
7
|
+
}
|
|
5
8
|
return join(homedir(), '.config', 'orizu');
|
|
6
9
|
}
|
|
7
10
|
function getCredentialsPath() {
|
|
8
11
|
return join(getConfigDir(), 'credentials.json');
|
|
9
12
|
}
|
|
10
|
-
|
|
13
|
+
function isStoredCredentialsV2(value) {
|
|
14
|
+
if (!value || typeof value !== 'object') {
|
|
15
|
+
return false;
|
|
16
|
+
}
|
|
17
|
+
const typed = value;
|
|
18
|
+
return typed.version === 2 && !!typed.servers && typeof typed.servers === 'object';
|
|
19
|
+
}
|
|
20
|
+
function isStoredCredentialsV1(value) {
|
|
21
|
+
if (!value || typeof value !== 'object') {
|
|
22
|
+
return false;
|
|
23
|
+
}
|
|
24
|
+
const typed = value;
|
|
25
|
+
return (typeof typed.baseUrl === 'string' &&
|
|
26
|
+
typeof typed.accessToken === 'string' &&
|
|
27
|
+
typeof typed.refreshToken === 'string' &&
|
|
28
|
+
typeof typed.expiresAt === 'number');
|
|
29
|
+
}
|
|
30
|
+
function migrateToV2(stored) {
|
|
31
|
+
return {
|
|
32
|
+
version: 2,
|
|
33
|
+
activeBaseUrl: stored.baseUrl,
|
|
34
|
+
servers: {
|
|
35
|
+
[stored.baseUrl]: {
|
|
36
|
+
accessToken: stored.accessToken,
|
|
37
|
+
refreshToken: stored.refreshToken,
|
|
38
|
+
expiresAt: stored.expiresAt,
|
|
39
|
+
},
|
|
40
|
+
},
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
function writeCredentials(config) {
|
|
11
44
|
const dir = getConfigDir();
|
|
12
45
|
mkdirSync(dir, { recursive: true });
|
|
13
46
|
const path = getCredentialsPath();
|
|
14
|
-
writeFileSync(path, JSON.stringify(
|
|
47
|
+
writeFileSync(path, JSON.stringify(config, null, 2), 'utf-8');
|
|
15
48
|
chmodSync(path, 0o600);
|
|
16
49
|
}
|
|
17
|
-
|
|
50
|
+
function createEmptyCredentialsConfig() {
|
|
51
|
+
return {
|
|
52
|
+
version: 2,
|
|
53
|
+
activeBaseUrl: null,
|
|
54
|
+
servers: {},
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
function loadCredentialsConfigForWrite() {
|
|
58
|
+
try {
|
|
59
|
+
return loadCredentialsConfig() || createEmptyCredentialsConfig();
|
|
60
|
+
}
|
|
61
|
+
catch {
|
|
62
|
+
return createEmptyCredentialsConfig();
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
export function loadCredentialsConfig() {
|
|
18
66
|
const path = getCredentialsPath();
|
|
19
67
|
if (!existsSync(path)) {
|
|
20
68
|
return null;
|
|
21
69
|
}
|
|
22
70
|
const raw = readFileSync(path, 'utf-8');
|
|
23
|
-
|
|
71
|
+
const parsed = JSON.parse(raw);
|
|
72
|
+
if (isStoredCredentialsV2(parsed)) {
|
|
73
|
+
return parsed;
|
|
74
|
+
}
|
|
75
|
+
if (isStoredCredentialsV1(parsed)) {
|
|
76
|
+
return migrateToV2(parsed);
|
|
77
|
+
}
|
|
78
|
+
throw new Error('Invalid credentials file format.');
|
|
79
|
+
}
|
|
80
|
+
export function getServerCredentials(baseUrl) {
|
|
81
|
+
const config = loadCredentialsConfig();
|
|
82
|
+
if (!config) {
|
|
83
|
+
return null;
|
|
84
|
+
}
|
|
85
|
+
if (!Object.hasOwn(config.servers, baseUrl)) {
|
|
86
|
+
return null;
|
|
87
|
+
}
|
|
88
|
+
return config.servers[baseUrl] || null;
|
|
89
|
+
}
|
|
90
|
+
export function saveServerCredentials(baseUrl, credentials) {
|
|
91
|
+
const config = loadCredentialsConfigForWrite();
|
|
92
|
+
config.servers[baseUrl] = credentials;
|
|
93
|
+
config.activeBaseUrl = baseUrl;
|
|
94
|
+
writeCredentials(config);
|
|
95
|
+
}
|
|
96
|
+
export function updateServerCredentials(baseUrl, credentials) {
|
|
97
|
+
const config = loadCredentialsConfigForWrite();
|
|
98
|
+
config.servers[baseUrl] = credentials;
|
|
99
|
+
writeCredentials(config);
|
|
100
|
+
}
|
|
101
|
+
export function getActiveBaseUrl() {
|
|
102
|
+
const config = loadCredentialsConfig();
|
|
103
|
+
return config?.activeBaseUrl || null;
|
|
104
|
+
}
|
|
105
|
+
export function setActiveBaseUrl(baseUrl) {
|
|
106
|
+
const config = loadCredentialsConfigForWrite();
|
|
107
|
+
config.activeBaseUrl = baseUrl;
|
|
108
|
+
writeCredentials(config);
|
|
109
|
+
}
|
|
110
|
+
export function clearServerCredentials(baseUrl) {
|
|
111
|
+
const config = loadCredentialsConfig();
|
|
112
|
+
if (!config || !Object.hasOwn(config.servers, baseUrl)) {
|
|
113
|
+
return false;
|
|
114
|
+
}
|
|
115
|
+
delete config.servers[baseUrl];
|
|
116
|
+
if (config.activeBaseUrl === baseUrl) {
|
|
117
|
+
config.activeBaseUrl = null;
|
|
118
|
+
}
|
|
119
|
+
writeCredentials(config);
|
|
120
|
+
return true;
|
|
24
121
|
}
|
|
25
|
-
export function
|
|
122
|
+
export function clearCredentialsFile() {
|
|
26
123
|
const path = getCredentialsPath();
|
|
27
124
|
if (existsSync(path)) {
|
|
28
125
|
rmSync(path);
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
const DATASET_URL_PATH = /^\/d\/([^/]+)\/([^/]+)\/datasets\/([^/?#]+)\/?$/;
|
|
2
|
+
export function parseDatasetReference(input) {
|
|
3
|
+
const value = input.trim();
|
|
4
|
+
if (!value) {
|
|
5
|
+
throw new Error('Dataset value cannot be empty');
|
|
6
|
+
}
|
|
7
|
+
try {
|
|
8
|
+
const url = new URL(value);
|
|
9
|
+
const match = DATASET_URL_PATH.exec(url.pathname);
|
|
10
|
+
if (match) {
|
|
11
|
+
return {
|
|
12
|
+
project: `${match[1]}/${match[2]}`,
|
|
13
|
+
datasetId: decodeURIComponent(match[3]),
|
|
14
|
+
};
|
|
15
|
+
}
|
|
16
|
+
throw new Error('Dataset URL must be in format https://<host>/d/<teamSlug>/<projectSlug>/datasets/<datasetId>');
|
|
17
|
+
}
|
|
18
|
+
catch {
|
|
19
|
+
if (value.startsWith('http://') || value.startsWith('https://')) {
|
|
20
|
+
throw new Error('Dataset URL must be in format https://<host>/d/<teamSlug>/<projectSlug>/datasets/<datasetId>');
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
return { datasetId: value };
|
|
24
|
+
}
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
const LOCALHOST_BASE_URL = 'http://localhost:3000';
|
|
2
|
+
export function normalizeBaseUrl(url) {
|
|
3
|
+
let parsed;
|
|
4
|
+
try {
|
|
5
|
+
parsed = new URL(url);
|
|
6
|
+
}
|
|
7
|
+
catch {
|
|
8
|
+
throw new Error(`Invalid server URL: '${url}'`);
|
|
9
|
+
}
|
|
10
|
+
if (parsed.protocol !== 'http:' && parsed.protocol !== 'https:') {
|
|
11
|
+
throw new Error('Server URL must use http or https.');
|
|
12
|
+
}
|
|
13
|
+
if (parsed.username || parsed.password) {
|
|
14
|
+
throw new Error('Server URL must not contain credentials.');
|
|
15
|
+
}
|
|
16
|
+
if (parsed.pathname !== '/' || parsed.search || parsed.hash) {
|
|
17
|
+
throw new Error('Server URL must be an origin only (no path, query, or hash).');
|
|
18
|
+
}
|
|
19
|
+
return parsed.origin;
|
|
20
|
+
}
|
|
21
|
+
export function parseGlobalFlags(argv) {
|
|
22
|
+
const args = [];
|
|
23
|
+
const flags = {
|
|
24
|
+
local: false,
|
|
25
|
+
server: null,
|
|
26
|
+
};
|
|
27
|
+
for (let index = 0; index < argv.length; index += 1) {
|
|
28
|
+
const value = argv[index];
|
|
29
|
+
if (value === '--local') {
|
|
30
|
+
flags.local = true;
|
|
31
|
+
continue;
|
|
32
|
+
}
|
|
33
|
+
if (value === '--server') {
|
|
34
|
+
const next = argv[index + 1];
|
|
35
|
+
if (!next || next.startsWith('--')) {
|
|
36
|
+
throw new Error('Usage: --server <url>');
|
|
37
|
+
}
|
|
38
|
+
flags.server = normalizeBaseUrl(next);
|
|
39
|
+
index += 1;
|
|
40
|
+
continue;
|
|
41
|
+
}
|
|
42
|
+
args.push(value);
|
|
43
|
+
}
|
|
44
|
+
if (flags.local && flags.server) {
|
|
45
|
+
throw new Error('Use either --local or --server <url>, not both.');
|
|
46
|
+
}
|
|
47
|
+
return { flags, args };
|
|
48
|
+
}
|
|
49
|
+
export function getFlagBaseUrl(flags) {
|
|
50
|
+
if (flags.local) {
|
|
51
|
+
return LOCALHOST_BASE_URL;
|
|
52
|
+
}
|
|
53
|
+
if (flags.server) {
|
|
54
|
+
return flags.server;
|
|
55
|
+
}
|
|
56
|
+
return null;
|
|
57
|
+
}
|
package/dist/http.js
CHANGED
|
@@ -1,13 +1,33 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { getActiveBaseUrl, getServerCredentials, updateServerCredentials } from './credentials.js';
|
|
2
|
+
import { getFlagBaseUrl, normalizeBaseUrl } from './global-flags.js';
|
|
3
|
+
let runtimeFlags = { local: false, server: null };
|
|
4
|
+
export function setGlobalFlags(flags) {
|
|
5
|
+
runtimeFlags = flags;
|
|
6
|
+
}
|
|
7
|
+
export function resolveBaseUrl(flags = runtimeFlags) {
|
|
8
|
+
const fromFlags = getFlagBaseUrl(flags);
|
|
9
|
+
if (fromFlags) {
|
|
10
|
+
return fromFlags;
|
|
11
|
+
}
|
|
12
|
+
const fromEnv = process.env.ORIZU_BASE_URL;
|
|
13
|
+
if (fromEnv) {
|
|
14
|
+
return normalizeBaseUrl(fromEnv);
|
|
15
|
+
}
|
|
16
|
+
const fromStored = getActiveBaseUrl();
|
|
17
|
+
if (fromStored) {
|
|
18
|
+
return fromStored;
|
|
19
|
+
}
|
|
20
|
+
return 'https://orizu.ai';
|
|
21
|
+
}
|
|
2
22
|
export function getBaseUrl() {
|
|
3
|
-
return
|
|
23
|
+
return resolveBaseUrl();
|
|
4
24
|
}
|
|
5
25
|
function isExpired(expiresAt) {
|
|
6
26
|
const nowUnix = Math.floor(Date.now() / 1000);
|
|
7
27
|
return expiresAt <= nowUnix + 30;
|
|
8
28
|
}
|
|
9
|
-
async function refreshCredentials(credentials) {
|
|
10
|
-
const response = await fetch(`${
|
|
29
|
+
async function refreshCredentials(baseUrl, credentials) {
|
|
30
|
+
const response = await fetch(`${baseUrl}/api/cli/auth/refresh`, {
|
|
11
31
|
method: 'POST',
|
|
12
32
|
headers: { 'Content-Type': 'application/json' },
|
|
13
33
|
body: JSON.stringify({ refreshToken: credentials.refreshToken }),
|
|
@@ -20,21 +40,21 @@ async function refreshCredentials(credentials) {
|
|
|
20
40
|
accessToken: data.accessToken,
|
|
21
41
|
refreshToken: data.refreshToken,
|
|
22
42
|
expiresAt: data.expiresAt,
|
|
23
|
-
baseUrl: credentials.baseUrl,
|
|
24
43
|
};
|
|
25
|
-
|
|
44
|
+
updateServerCredentials(baseUrl, refreshed);
|
|
26
45
|
return refreshed;
|
|
27
46
|
}
|
|
28
47
|
export async function authedFetch(path, init = {}) {
|
|
29
|
-
const
|
|
48
|
+
const baseUrl = resolveBaseUrl();
|
|
49
|
+
const credentials = getServerCredentials(baseUrl);
|
|
30
50
|
if (!credentials) {
|
|
31
|
-
throw new Error(
|
|
51
|
+
throw new Error(`Not logged in for ${baseUrl}. Run \`orizu login --server ${baseUrl}\` (or \`--local\`) first.`);
|
|
32
52
|
}
|
|
33
53
|
let activeCredentials = credentials;
|
|
34
54
|
if (isExpired(activeCredentials.expiresAt)) {
|
|
35
|
-
activeCredentials = await refreshCredentials(activeCredentials);
|
|
55
|
+
activeCredentials = await refreshCredentials(baseUrl, activeCredentials);
|
|
36
56
|
}
|
|
37
|
-
let response = await fetch(`${
|
|
57
|
+
let response = await fetch(`${baseUrl}${path}`, {
|
|
38
58
|
...init,
|
|
39
59
|
headers: {
|
|
40
60
|
...(init.headers || {}),
|
|
@@ -42,8 +62,8 @@ export async function authedFetch(path, init = {}) {
|
|
|
42
62
|
},
|
|
43
63
|
});
|
|
44
64
|
if (response.status === 401) {
|
|
45
|
-
activeCredentials = await refreshCredentials(activeCredentials);
|
|
46
|
-
response = await fetch(`${
|
|
65
|
+
activeCredentials = await refreshCredentials(baseUrl, activeCredentials);
|
|
66
|
+
response = await fetch(`${baseUrl}${path}`, {
|
|
47
67
|
...init,
|
|
48
68
|
headers: {
|
|
49
69
|
...(init.headers || {}),
|
package/dist/index.js
CHANGED
|
@@ -6,24 +6,27 @@ import { readFileSync, 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
|
-
import {
|
|
9
|
+
import { clearServerCredentials, getServerCredentials, saveServerCredentials } from './credentials.js';
|
|
10
10
|
import { parseDatasetFile } from './file-parser.js';
|
|
11
|
-
import {
|
|
11
|
+
import { parseDatasetReference } from './dataset-download.js';
|
|
12
|
+
import { parseGlobalFlags } from './global-flags.js';
|
|
13
|
+
import { authedFetch, getBaseUrl, setGlobalFlags } from './http.js';
|
|
12
14
|
function printUsage() {
|
|
13
|
-
console.log(`orizu 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 <userId1,userId2> [--instructions <text>] [--labels-per-item <n>]\n orizu tasks assign --task <taskId> --assignees <userId1,userId2>\n orizu tasks status --task <taskId> [--json]\n orizu datasets upload --file <path> [--project <team/project>] [--name <name>]\n orizu tasks export [--task <taskId>] [--format <csv|json|jsonl>] [--out <path>]`);
|
|
15
|
+
console.log(`orizu global options:\n\n --local Use http://localhost:3000\n --server <url> Use a specific server origin (for example: https://preview.example.com)\n\norizu commands:\n\n orizu login\n orizu logout\n orizu whoami\n orizu teams list\n orizu teams create [--name <name>]\n orizu teams members list [--team <teamSlug>]\n orizu teams members add --email <email> [--team <teamSlug>]\n orizu teams members remove --email <email> [--team <teamSlug>]\n orizu teams members role --team <teamSlug> --email <email> --role <admin|member>\n orizu projects list [--team <teamSlug>]\n orizu projects create --name <name> [--team <teamSlug>]\n orizu apps list [--project <team/project>]\n orizu apps create --project <team/project> --name <name> --dataset <datasetId> --file <path> --input-schema <json-path> --output-schema <json-path> [--component <name>]\n orizu apps update [--app <appId>] [--project <team/project>] --file <path> --input-schema <json-path> --output-schema <json-path> [--component <name>]\n orizu apps link-dataset --dataset <datasetId> [--app <appId>] [--project <team/project>] [--version <n>]\n orizu tasks list [--project <team/project>]\n orizu tasks create --project <team/project> --dataset <datasetId> --app <appId> --title <title> --assignees <userId1,userId2> [--instructions <text>] [--labels-per-item <n>]\n orizu tasks assign --task <taskId> --assignees <userId1,userId2>\n orizu tasks status --task <taskId> [--json]\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 delete-rows [--dataset <datasetId|datasetUrl>] [--project <team/project>] [--row-ids <id1,id2>] [--row-indices <n1,n2>]\n orizu tasks export [--task <taskId>] [--format <csv|json|jsonl>] [--out <path>]`);
|
|
14
16
|
}
|
|
17
|
+
let cliArgs = process.argv.slice(2);
|
|
15
18
|
function getArg(name) {
|
|
16
|
-
const index =
|
|
17
|
-
if (index === -1 || index + 1 >=
|
|
19
|
+
const index = cliArgs.indexOf(name);
|
|
20
|
+
if (index === -1 || index + 1 >= cliArgs.length) {
|
|
18
21
|
return null;
|
|
19
22
|
}
|
|
20
|
-
return
|
|
23
|
+
return cliArgs[index + 1];
|
|
21
24
|
}
|
|
22
25
|
function isInteractiveTerminal() {
|
|
23
26
|
return Boolean(process.stdin.isTTY && process.stdout.isTTY);
|
|
24
27
|
}
|
|
25
28
|
function hasArg(name) {
|
|
26
|
-
return
|
|
29
|
+
return cliArgs.includes(name);
|
|
27
30
|
}
|
|
28
31
|
function expandHomePath(path) {
|
|
29
32
|
if (path.startsWith('~/')) {
|
|
@@ -144,6 +147,14 @@ async function fetchApps(project) {
|
|
|
144
147
|
const data = await parseJsonResponse(response, 'Apps list');
|
|
145
148
|
return data.apps;
|
|
146
149
|
}
|
|
150
|
+
async function fetchDatasets(project) {
|
|
151
|
+
const response = await authedFetch(`/api/cli/datasets?project=${encodeURIComponent(project)}`);
|
|
152
|
+
if (!response.ok) {
|
|
153
|
+
throw new Error(`Failed to fetch datasets: ${await response.text()}`);
|
|
154
|
+
}
|
|
155
|
+
const data = await parseJsonResponse(response, 'Datasets list');
|
|
156
|
+
return data.datasets;
|
|
157
|
+
}
|
|
147
158
|
async function fetchTeamMembers(teamSlug) {
|
|
148
159
|
const response = await authedFetch(`/api/cli/teams/${encodeURIComponent(teamSlug)}/members`);
|
|
149
160
|
if (!response.ok) {
|
|
@@ -204,6 +215,18 @@ async function selectAppIdInteractively(projectArg) {
|
|
|
204
215
|
project,
|
|
205
216
|
};
|
|
206
217
|
}
|
|
218
|
+
async function selectDatasetInteractively(projectArg) {
|
|
219
|
+
let project = projectArg;
|
|
220
|
+
if (!project) {
|
|
221
|
+
project = await resolveProjectSlug(null);
|
|
222
|
+
}
|
|
223
|
+
const datasets = await fetchDatasets(project);
|
|
224
|
+
const dataset = await promptSelect(`Select a dataset in ${project}`, datasets, item => `${item.name} (id=${item.id}, rows=${item.rowCount})`, { forcePrompt: true });
|
|
225
|
+
return {
|
|
226
|
+
datasetId: dataset.id,
|
|
227
|
+
project,
|
|
228
|
+
};
|
|
229
|
+
}
|
|
207
230
|
function printTeams(teams) {
|
|
208
231
|
if (teams.length === 0) {
|
|
209
232
|
console.log('No teams found.');
|
|
@@ -377,11 +400,10 @@ async function login() {
|
|
|
377
400
|
throw new Error(`Failed to exchange auth code: ${text}`);
|
|
378
401
|
}
|
|
379
402
|
const loginData = await parseJsonResponse(exchangeResponse, 'CLI auth exchange');
|
|
380
|
-
|
|
403
|
+
saveServerCredentials(baseUrl, {
|
|
381
404
|
accessToken: loginData.accessToken,
|
|
382
405
|
refreshToken: loginData.refreshToken,
|
|
383
406
|
expiresAt: loginData.expiresAt,
|
|
384
|
-
baseUrl,
|
|
385
407
|
});
|
|
386
408
|
console.log(`Logged in as ${loginData.user.email ?? loginData.user.id}`);
|
|
387
409
|
}
|
|
@@ -394,19 +416,20 @@ async function whoami() {
|
|
|
394
416
|
console.log(data.user.email ?? data.user.id);
|
|
395
417
|
}
|
|
396
418
|
async function logout() {
|
|
397
|
-
const
|
|
419
|
+
const baseUrl = getBaseUrl();
|
|
420
|
+
const credentials = getServerCredentials(baseUrl);
|
|
398
421
|
if (!credentials) {
|
|
399
|
-
console.log(
|
|
422
|
+
console.log(`Already logged out for ${baseUrl}.`);
|
|
400
423
|
return;
|
|
401
424
|
}
|
|
402
|
-
await fetch(`${
|
|
425
|
+
await fetch(`${baseUrl}/api/cli/auth/logout`, {
|
|
403
426
|
method: 'POST',
|
|
404
427
|
headers: {
|
|
405
428
|
Authorization: `Bearer ${credentials.accessToken}`,
|
|
406
429
|
},
|
|
407
430
|
}).catch(() => undefined);
|
|
408
|
-
|
|
409
|
-
console.log(
|
|
431
|
+
clearServerCredentials(baseUrl);
|
|
432
|
+
console.log(`Logged out from ${baseUrl}.`);
|
|
410
433
|
}
|
|
411
434
|
async function listTeams() {
|
|
412
435
|
printTeams(await fetchTeams());
|
|
@@ -769,6 +792,120 @@ async function uploadDataset() {
|
|
|
769
792
|
console.log(`View dataset: ${formatTerminalLink(data.dataset.url)}`);
|
|
770
793
|
}
|
|
771
794
|
}
|
|
795
|
+
function getDatasetReferenceInput() {
|
|
796
|
+
const fromFlag = getArg('--dataset');
|
|
797
|
+
if (fromFlag) {
|
|
798
|
+
return fromFlag;
|
|
799
|
+
}
|
|
800
|
+
const positional = cliArgs[2];
|
|
801
|
+
if (positional && !positional.startsWith('--')) {
|
|
802
|
+
return positional;
|
|
803
|
+
}
|
|
804
|
+
return null;
|
|
805
|
+
}
|
|
806
|
+
async function downloadDataset() {
|
|
807
|
+
const projectArg = getArg('--project');
|
|
808
|
+
const datasetInput = getDatasetReferenceInput();
|
|
809
|
+
const format = (getArg('--format') || 'jsonl');
|
|
810
|
+
const outPathArg = getArg('--out');
|
|
811
|
+
if (!['csv', 'json', 'jsonl'].includes(format)) {
|
|
812
|
+
throw new Error('format must be one of: csv, json, jsonl');
|
|
813
|
+
}
|
|
814
|
+
let datasetId;
|
|
815
|
+
if (datasetInput) {
|
|
816
|
+
datasetId = parseDatasetReference(datasetInput).datasetId;
|
|
817
|
+
}
|
|
818
|
+
else {
|
|
819
|
+
const selected = await selectDatasetInteractively(projectArg);
|
|
820
|
+
datasetId = selected.datasetId;
|
|
821
|
+
}
|
|
822
|
+
const response = await authedFetch(`/api/cli/datasets/${encodeURIComponent(datasetId)}/download?format=${encodeURIComponent(format)}`);
|
|
823
|
+
if (!response.ok) {
|
|
824
|
+
throw new Error(`Download failed: ${await response.text()}`);
|
|
825
|
+
}
|
|
826
|
+
const filename = outPathArg
|
|
827
|
+
? expandHomePath(outPathArg)
|
|
828
|
+
: `${datasetId}.${format}`;
|
|
829
|
+
const bytes = new Uint8Array(await response.arrayBuffer());
|
|
830
|
+
writeFileSync(filename, bytes);
|
|
831
|
+
console.log(`Saved dataset ${datasetId} (${format.toUpperCase()}) to ${filename}`);
|
|
832
|
+
}
|
|
833
|
+
async function appendDatasetRows() {
|
|
834
|
+
const projectArg = getArg('--project');
|
|
835
|
+
const datasetInput = getDatasetReferenceInput();
|
|
836
|
+
const fileArg = getArg('--file');
|
|
837
|
+
if (!fileArg) {
|
|
838
|
+
throw new Error('Usage: orizu datasets append [--dataset <datasetId|datasetUrl>] [--project <team/project>] --file <path>');
|
|
839
|
+
}
|
|
840
|
+
let datasetId;
|
|
841
|
+
if (datasetInput) {
|
|
842
|
+
datasetId = parseDatasetReference(datasetInput).datasetId;
|
|
843
|
+
}
|
|
844
|
+
else {
|
|
845
|
+
const selected = await selectDatasetInteractively(projectArg);
|
|
846
|
+
datasetId = selected.datasetId;
|
|
847
|
+
}
|
|
848
|
+
const file = expandHomePath(fileArg);
|
|
849
|
+
const { rows } = parseDatasetFile(file);
|
|
850
|
+
if (!Array.isArray(rows) || rows.length === 0) {
|
|
851
|
+
throw new Error('Dataset append file must contain at least one row');
|
|
852
|
+
}
|
|
853
|
+
const response = await authedFetch(`/api/cli/datasets/${encodeURIComponent(datasetId)}/rows`, {
|
|
854
|
+
method: 'POST',
|
|
855
|
+
headers: { 'Content-Type': 'application/json' },
|
|
856
|
+
body: JSON.stringify({ rows }),
|
|
857
|
+
});
|
|
858
|
+
if (!response.ok) {
|
|
859
|
+
throw new Error(`Append failed: ${await response.text()}`);
|
|
860
|
+
}
|
|
861
|
+
const data = await parseJsonResponse(response, 'Dataset append');
|
|
862
|
+
console.log(`Appended ${data.appendedCount} rows to dataset ${data.dataset.name} (${data.dataset.id}). New row count: ${data.dataset.rowCount}`);
|
|
863
|
+
}
|
|
864
|
+
function parseCommaSeparatedIntegers(value) {
|
|
865
|
+
if (!value) {
|
|
866
|
+
return [];
|
|
867
|
+
}
|
|
868
|
+
const rawItems = value
|
|
869
|
+
.split(',')
|
|
870
|
+
.map(item => item.trim())
|
|
871
|
+
.filter(Boolean);
|
|
872
|
+
const parsed = rawItems.map(item => Number(item));
|
|
873
|
+
const invalid = parsed.some(item => !Number.isInteger(item) || item < 0);
|
|
874
|
+
if (invalid) {
|
|
875
|
+
throw new Error('row-indices must be comma-separated non-negative integers');
|
|
876
|
+
}
|
|
877
|
+
return parsed;
|
|
878
|
+
}
|
|
879
|
+
async function deleteDatasetRows() {
|
|
880
|
+
const projectArg = getArg('--project');
|
|
881
|
+
const datasetInput = getDatasetReferenceInput();
|
|
882
|
+
const rowIds = parseCommaSeparated(getArg('--row-ids'));
|
|
883
|
+
const rowIndices = parseCommaSeparatedIntegers(getArg('--row-indices'));
|
|
884
|
+
if (rowIds.length === 0 && rowIndices.length === 0) {
|
|
885
|
+
throw new Error('Usage: orizu datasets delete-rows [--dataset <datasetId|datasetUrl>] [--project <team/project>] [--row-ids <id1,id2>] [--row-indices <n1,n2>]');
|
|
886
|
+
}
|
|
887
|
+
let datasetId;
|
|
888
|
+
if (datasetInput) {
|
|
889
|
+
datasetId = parseDatasetReference(datasetInput).datasetId;
|
|
890
|
+
}
|
|
891
|
+
else {
|
|
892
|
+
const selected = await selectDatasetInteractively(projectArg);
|
|
893
|
+
datasetId = selected.datasetId;
|
|
894
|
+
}
|
|
895
|
+
const response = await authedFetch(`/api/cli/datasets/${encodeURIComponent(datasetId)}/rows`, {
|
|
896
|
+
method: 'DELETE',
|
|
897
|
+
headers: { 'Content-Type': 'application/json' },
|
|
898
|
+
body: JSON.stringify({
|
|
899
|
+
rowIds,
|
|
900
|
+
rowIndices,
|
|
901
|
+
}),
|
|
902
|
+
});
|
|
903
|
+
if (!response.ok) {
|
|
904
|
+
throw new Error(`Delete rows failed: ${await response.text()}`);
|
|
905
|
+
}
|
|
906
|
+
const data = await parseJsonResponse(response, 'Dataset delete rows');
|
|
907
|
+
console.log(`Deleted ${data.deletedCount} rows from dataset ${data.dataset.name} (${data.dataset.id}). New row count: ${data.dataset.rowCount}`);
|
|
908
|
+
}
|
|
772
909
|
async function downloadAnnotations() {
|
|
773
910
|
let taskId = getArg('--task');
|
|
774
911
|
const format = (getArg('--format') || 'jsonl');
|
|
@@ -792,8 +929,11 @@ async function downloadAnnotations() {
|
|
|
792
929
|
console.log(`Saved ${format.toUpperCase()} export to ${filename}`);
|
|
793
930
|
}
|
|
794
931
|
async function main() {
|
|
795
|
-
const
|
|
796
|
-
|
|
932
|
+
const parsed = parseGlobalFlags(process.argv.slice(2));
|
|
933
|
+
setGlobalFlags(parsed.flags);
|
|
934
|
+
cliArgs = parsed.args;
|
|
935
|
+
const command = cliArgs[0];
|
|
936
|
+
const subcommand = cliArgs[1];
|
|
797
937
|
if (!command) {
|
|
798
938
|
printUsage();
|
|
799
939
|
process.exit(1);
|
|
@@ -818,7 +958,7 @@ async function main() {
|
|
|
818
958
|
await createTeam();
|
|
819
959
|
return;
|
|
820
960
|
}
|
|
821
|
-
const teamsMembersAction =
|
|
961
|
+
const teamsMembersAction = cliArgs[2];
|
|
822
962
|
if (command === 'teams' && subcommand === 'members' && teamsMembersAction === 'list') {
|
|
823
963
|
await listTeamMembers();
|
|
824
964
|
return;
|
|
@@ -879,6 +1019,18 @@ async function main() {
|
|
|
879
1019
|
await uploadDataset();
|
|
880
1020
|
return;
|
|
881
1021
|
}
|
|
1022
|
+
if (command === 'datasets' && subcommand === 'download') {
|
|
1023
|
+
await downloadDataset();
|
|
1024
|
+
return;
|
|
1025
|
+
}
|
|
1026
|
+
if (command === 'datasets' && subcommand === 'append') {
|
|
1027
|
+
await appendDatasetRows();
|
|
1028
|
+
return;
|
|
1029
|
+
}
|
|
1030
|
+
if (command === 'datasets' && subcommand === 'delete-rows') {
|
|
1031
|
+
await deleteDatasetRows();
|
|
1032
|
+
return;
|
|
1033
|
+
}
|
|
882
1034
|
if (command === 'tasks' && subcommand === 'export') {
|
|
883
1035
|
await downloadAnnotations();
|
|
884
1036
|
return;
|
package/package.json
CHANGED
package/src/credentials.ts
CHANGED
|
@@ -1,9 +1,12 @@
|
|
|
1
1
|
import { mkdirSync, readFileSync, rmSync, writeFileSync, chmodSync, existsSync } from 'fs'
|
|
2
2
|
import { join } from 'path'
|
|
3
3
|
import { homedir } from 'os'
|
|
4
|
-
import {
|
|
4
|
+
import { ServerCredentials, StoredCredentialsV1, StoredCredentialsV2 } from './types.js'
|
|
5
5
|
|
|
6
6
|
function getConfigDir(): string {
|
|
7
|
+
if (process.env.ORIZU_CONFIG_DIR) {
|
|
8
|
+
return process.env.ORIZU_CONFIG_DIR
|
|
9
|
+
}
|
|
7
10
|
return join(homedir(), '.config', 'orizu')
|
|
8
11
|
}
|
|
9
12
|
|
|
@@ -11,25 +14,139 @@ function getCredentialsPath(): string {
|
|
|
11
14
|
return join(getConfigDir(), 'credentials.json')
|
|
12
15
|
}
|
|
13
16
|
|
|
14
|
-
|
|
17
|
+
function isStoredCredentialsV2(value: unknown): value is StoredCredentialsV2 {
|
|
18
|
+
if (!value || typeof value !== 'object') {
|
|
19
|
+
return false
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
const typed = value as Partial<StoredCredentialsV2>
|
|
23
|
+
return typed.version === 2 && !!typed.servers && typeof typed.servers === 'object'
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function isStoredCredentialsV1(value: unknown): value is StoredCredentialsV1 {
|
|
27
|
+
if (!value || typeof value !== 'object') {
|
|
28
|
+
return false
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
const typed = value as Partial<StoredCredentialsV1>
|
|
32
|
+
return (
|
|
33
|
+
typeof typed.baseUrl === 'string' &&
|
|
34
|
+
typeof typed.accessToken === 'string' &&
|
|
35
|
+
typeof typed.refreshToken === 'string' &&
|
|
36
|
+
typeof typed.expiresAt === 'number'
|
|
37
|
+
)
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function migrateToV2(stored: StoredCredentialsV1): StoredCredentialsV2 {
|
|
41
|
+
return {
|
|
42
|
+
version: 2,
|
|
43
|
+
activeBaseUrl: stored.baseUrl,
|
|
44
|
+
servers: {
|
|
45
|
+
[stored.baseUrl]: {
|
|
46
|
+
accessToken: stored.accessToken,
|
|
47
|
+
refreshToken: stored.refreshToken,
|
|
48
|
+
expiresAt: stored.expiresAt,
|
|
49
|
+
},
|
|
50
|
+
},
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function writeCredentials(config: StoredCredentialsV2) {
|
|
15
55
|
const dir = getConfigDir()
|
|
16
56
|
mkdirSync(dir, { recursive: true })
|
|
17
57
|
const path = getCredentialsPath()
|
|
18
|
-
writeFileSync(path, JSON.stringify(
|
|
58
|
+
writeFileSync(path, JSON.stringify(config, null, 2), 'utf-8')
|
|
19
59
|
chmodSync(path, 0o600)
|
|
20
60
|
}
|
|
21
61
|
|
|
22
|
-
|
|
62
|
+
function createEmptyCredentialsConfig(): StoredCredentialsV2 {
|
|
63
|
+
return {
|
|
64
|
+
version: 2 as const,
|
|
65
|
+
activeBaseUrl: null,
|
|
66
|
+
servers: {},
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function loadCredentialsConfigForWrite(): StoredCredentialsV2 {
|
|
71
|
+
try {
|
|
72
|
+
return loadCredentialsConfig() || createEmptyCredentialsConfig()
|
|
73
|
+
} catch {
|
|
74
|
+
return createEmptyCredentialsConfig()
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
export function loadCredentialsConfig(): StoredCredentialsV2 | null {
|
|
23
79
|
const path = getCredentialsPath()
|
|
24
80
|
if (!existsSync(path)) {
|
|
25
81
|
return null
|
|
26
82
|
}
|
|
27
83
|
|
|
28
84
|
const raw = readFileSync(path, 'utf-8')
|
|
29
|
-
|
|
85
|
+
const parsed = JSON.parse(raw) as unknown
|
|
86
|
+
if (isStoredCredentialsV2(parsed)) {
|
|
87
|
+
return parsed
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
if (isStoredCredentialsV1(parsed)) {
|
|
91
|
+
return migrateToV2(parsed)
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
throw new Error('Invalid credentials file format.')
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
export function getServerCredentials(baseUrl: string): ServerCredentials | null {
|
|
98
|
+
const config = loadCredentialsConfig()
|
|
99
|
+
if (!config) {
|
|
100
|
+
return null
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
if (!Object.hasOwn(config.servers, baseUrl)) {
|
|
104
|
+
return null
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
return config.servers[baseUrl] || null
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
export function saveServerCredentials(baseUrl: string, credentials: ServerCredentials) {
|
|
111
|
+
const config = loadCredentialsConfigForWrite()
|
|
112
|
+
|
|
113
|
+
config.servers[baseUrl] = credentials
|
|
114
|
+
config.activeBaseUrl = baseUrl
|
|
115
|
+
writeCredentials(config)
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
export function updateServerCredentials(baseUrl: string, credentials: ServerCredentials) {
|
|
119
|
+
const config = loadCredentialsConfigForWrite()
|
|
120
|
+
config.servers[baseUrl] = credentials
|
|
121
|
+
writeCredentials(config)
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
export function getActiveBaseUrl(): string | null {
|
|
125
|
+
const config = loadCredentialsConfig()
|
|
126
|
+
return config?.activeBaseUrl || null
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
export function setActiveBaseUrl(baseUrl: string | null) {
|
|
130
|
+
const config = loadCredentialsConfigForWrite()
|
|
131
|
+
config.activeBaseUrl = baseUrl
|
|
132
|
+
writeCredentials(config)
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
export function clearServerCredentials(baseUrl: string): boolean {
|
|
136
|
+
const config = loadCredentialsConfig()
|
|
137
|
+
if (!config || !Object.hasOwn(config.servers, baseUrl)) {
|
|
138
|
+
return false
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
delete config.servers[baseUrl]
|
|
142
|
+
if (config.activeBaseUrl === baseUrl) {
|
|
143
|
+
config.activeBaseUrl = null
|
|
144
|
+
}
|
|
145
|
+
writeCredentials(config)
|
|
146
|
+
return true
|
|
30
147
|
}
|
|
31
148
|
|
|
32
|
-
export function
|
|
149
|
+
export function clearCredentialsFile() {
|
|
33
150
|
const path = getCredentialsPath()
|
|
34
151
|
if (existsSync(path)) {
|
|
35
152
|
rmSync(path)
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
export interface ParsedDatasetReference {
|
|
2
|
+
datasetId: string
|
|
3
|
+
project?: string
|
|
4
|
+
}
|
|
5
|
+
|
|
6
|
+
const DATASET_URL_PATH = /^\/d\/([^/]+)\/([^/]+)\/datasets\/([^/?#]+)\/?$/
|
|
7
|
+
|
|
8
|
+
export function parseDatasetReference(input: string): ParsedDatasetReference {
|
|
9
|
+
const value = input.trim()
|
|
10
|
+
if (!value) {
|
|
11
|
+
throw new Error('Dataset value cannot be empty')
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
try {
|
|
15
|
+
const url = new URL(value)
|
|
16
|
+
const match = DATASET_URL_PATH.exec(url.pathname)
|
|
17
|
+
if (match) {
|
|
18
|
+
return {
|
|
19
|
+
project: `${match[1]}/${match[2]}`,
|
|
20
|
+
datasetId: decodeURIComponent(match[3]),
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
throw new Error(
|
|
25
|
+
'Dataset URL must be in format https://<host>/d/<teamSlug>/<projectSlug>/datasets/<datasetId>'
|
|
26
|
+
)
|
|
27
|
+
} catch {
|
|
28
|
+
if (value.startsWith('http://') || value.startsWith('https://')) {
|
|
29
|
+
throw new Error(
|
|
30
|
+
'Dataset URL must be in format https://<host>/d/<teamSlug>/<projectSlug>/datasets/<datasetId>'
|
|
31
|
+
)
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
return { datasetId: value }
|
|
36
|
+
}
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
const LOCALHOST_BASE_URL = 'http://localhost:3000'
|
|
2
|
+
|
|
3
|
+
export interface GlobalFlags {
|
|
4
|
+
local: boolean
|
|
5
|
+
server: string | null
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export interface ParsedGlobalFlags {
|
|
9
|
+
flags: GlobalFlags
|
|
10
|
+
args: string[]
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export function normalizeBaseUrl(url: string): string {
|
|
14
|
+
let parsed: URL
|
|
15
|
+
try {
|
|
16
|
+
parsed = new URL(url)
|
|
17
|
+
} catch {
|
|
18
|
+
throw new Error(`Invalid server URL: '${url}'`)
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
if (parsed.protocol !== 'http:' && parsed.protocol !== 'https:') {
|
|
22
|
+
throw new Error('Server URL must use http or https.')
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
if (parsed.username || parsed.password) {
|
|
26
|
+
throw new Error('Server URL must not contain credentials.')
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
if (parsed.pathname !== '/' || parsed.search || parsed.hash) {
|
|
30
|
+
throw new Error('Server URL must be an origin only (no path, query, or hash).')
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
return parsed.origin
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export function parseGlobalFlags(argv: string[]): ParsedGlobalFlags {
|
|
37
|
+
const args: string[] = []
|
|
38
|
+
const flags: GlobalFlags = {
|
|
39
|
+
local: false,
|
|
40
|
+
server: null,
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
for (let index = 0; index < argv.length; index += 1) {
|
|
44
|
+
const value = argv[index]
|
|
45
|
+
if (value === '--local') {
|
|
46
|
+
flags.local = true
|
|
47
|
+
continue
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
if (value === '--server') {
|
|
51
|
+
const next = argv[index + 1]
|
|
52
|
+
if (!next || next.startsWith('--')) {
|
|
53
|
+
throw new Error('Usage: --server <url>')
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
flags.server = normalizeBaseUrl(next)
|
|
57
|
+
index += 1
|
|
58
|
+
continue
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
args.push(value)
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
if (flags.local && flags.server) {
|
|
65
|
+
throw new Error('Use either --local or --server <url>, not both.')
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
return { flags, args }
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
export function getFlagBaseUrl(flags: GlobalFlags): string | null {
|
|
72
|
+
if (flags.local) {
|
|
73
|
+
return LOCALHOST_BASE_URL
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
if (flags.server) {
|
|
77
|
+
return flags.server
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
return null
|
|
81
|
+
}
|
package/src/http.ts
CHANGED
|
@@ -1,8 +1,34 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
1
|
+
import { getActiveBaseUrl, getServerCredentials, updateServerCredentials } from './credentials.js'
|
|
2
|
+
import { getFlagBaseUrl, GlobalFlags, normalizeBaseUrl } from './global-flags.js'
|
|
3
|
+
import { LoginResponse, ServerCredentials } from './types.js'
|
|
4
|
+
|
|
5
|
+
let runtimeFlags: GlobalFlags = { local: false, server: null }
|
|
6
|
+
|
|
7
|
+
export function setGlobalFlags(flags: GlobalFlags) {
|
|
8
|
+
runtimeFlags = flags
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export function resolveBaseUrl(flags: GlobalFlags = runtimeFlags): string {
|
|
12
|
+
const fromFlags = getFlagBaseUrl(flags)
|
|
13
|
+
if (fromFlags) {
|
|
14
|
+
return fromFlags
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
const fromEnv = process.env.ORIZU_BASE_URL
|
|
18
|
+
if (fromEnv) {
|
|
19
|
+
return normalizeBaseUrl(fromEnv)
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
const fromStored = getActiveBaseUrl()
|
|
23
|
+
if (fromStored) {
|
|
24
|
+
return fromStored
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
return 'https://orizu.ai'
|
|
28
|
+
}
|
|
3
29
|
|
|
4
30
|
export function getBaseUrl(): string {
|
|
5
|
-
return
|
|
31
|
+
return resolveBaseUrl()
|
|
6
32
|
}
|
|
7
33
|
|
|
8
34
|
function isExpired(expiresAt: number): boolean {
|
|
@@ -10,8 +36,8 @@ function isExpired(expiresAt: number): boolean {
|
|
|
10
36
|
return expiresAt <= nowUnix + 30
|
|
11
37
|
}
|
|
12
38
|
|
|
13
|
-
async function refreshCredentials(credentials:
|
|
14
|
-
const response = await fetch(`${
|
|
39
|
+
async function refreshCredentials(baseUrl: string, credentials: ServerCredentials): Promise<ServerCredentials> {
|
|
40
|
+
const response = await fetch(`${baseUrl}/api/cli/auth/refresh`, {
|
|
15
41
|
method: 'POST',
|
|
16
42
|
headers: { 'Content-Type': 'application/json' },
|
|
17
43
|
body: JSON.stringify({ refreshToken: credentials.refreshToken }),
|
|
@@ -26,24 +52,24 @@ async function refreshCredentials(credentials: StoredCredentials): Promise<Store
|
|
|
26
52
|
accessToken: data.accessToken,
|
|
27
53
|
refreshToken: data.refreshToken,
|
|
28
54
|
expiresAt: data.expiresAt,
|
|
29
|
-
baseUrl: credentials.baseUrl,
|
|
30
55
|
}
|
|
31
|
-
|
|
56
|
+
updateServerCredentials(baseUrl, refreshed)
|
|
32
57
|
return refreshed
|
|
33
58
|
}
|
|
34
59
|
|
|
35
60
|
export async function authedFetch(path: string, init: RequestInit = {}) {
|
|
36
|
-
const
|
|
61
|
+
const baseUrl = resolveBaseUrl()
|
|
62
|
+
const credentials = getServerCredentials(baseUrl)
|
|
37
63
|
if (!credentials) {
|
|
38
|
-
throw new Error(
|
|
64
|
+
throw new Error(`Not logged in for ${baseUrl}. Run \`orizu login --server ${baseUrl}\` (or \`--local\`) first.`)
|
|
39
65
|
}
|
|
40
66
|
|
|
41
67
|
let activeCredentials = credentials
|
|
42
68
|
if (isExpired(activeCredentials.expiresAt)) {
|
|
43
|
-
activeCredentials = await refreshCredentials(activeCredentials)
|
|
69
|
+
activeCredentials = await refreshCredentials(baseUrl, activeCredentials)
|
|
44
70
|
}
|
|
45
71
|
|
|
46
|
-
let response = await fetch(`${
|
|
72
|
+
let response = await fetch(`${baseUrl}${path}`, {
|
|
47
73
|
...init,
|
|
48
74
|
headers: {
|
|
49
75
|
...(init.headers || {}),
|
|
@@ -52,8 +78,8 @@ export async function authedFetch(path: string, init: RequestInit = {}) {
|
|
|
52
78
|
})
|
|
53
79
|
|
|
54
80
|
if (response.status === 401) {
|
|
55
|
-
activeCredentials = await refreshCredentials(activeCredentials)
|
|
56
|
-
response = await fetch(`${
|
|
81
|
+
activeCredentials = await refreshCredentials(baseUrl, activeCredentials)
|
|
82
|
+
response = await fetch(`${baseUrl}${path}`, {
|
|
57
83
|
...init,
|
|
58
84
|
headers: {
|
|
59
85
|
...(init.headers || {}),
|
package/src/index.ts
CHANGED
|
@@ -6,9 +6,11 @@ import { readFileSync, 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
|
-
import {
|
|
9
|
+
import { clearServerCredentials, getServerCredentials, saveServerCredentials } from './credentials.js'
|
|
10
10
|
import { parseDatasetFile } from './file-parser.js'
|
|
11
|
-
import {
|
|
11
|
+
import { parseDatasetReference } from './dataset-download.js'
|
|
12
|
+
import { parseGlobalFlags } from './global-flags.js'
|
|
13
|
+
import { authedFetch, getBaseUrl, setGlobalFlags } from './http.js'
|
|
12
14
|
import { LoginResponse } from './types.js'
|
|
13
15
|
|
|
14
16
|
interface Team {
|
|
@@ -50,6 +52,19 @@ interface AppSummary {
|
|
|
50
52
|
projectName: string
|
|
51
53
|
}
|
|
52
54
|
|
|
55
|
+
interface DatasetSummary {
|
|
56
|
+
id: string
|
|
57
|
+
name: string
|
|
58
|
+
rowCount: number
|
|
59
|
+
sourceType: string
|
|
60
|
+
createdAt: string
|
|
61
|
+
projectId: string
|
|
62
|
+
projectName: string
|
|
63
|
+
projectSlug: string
|
|
64
|
+
teamName: string
|
|
65
|
+
teamSlug: string
|
|
66
|
+
}
|
|
67
|
+
|
|
53
68
|
interface TeamMember {
|
|
54
69
|
id: string
|
|
55
70
|
user_id: string | null
|
|
@@ -91,16 +106,18 @@ interface TaskStatusPayload {
|
|
|
91
106
|
}
|
|
92
107
|
|
|
93
108
|
function printUsage() {
|
|
94
|
-
console.log(`orizu 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 <userId1,userId2> [--instructions <text>] [--labels-per-item <n>]\n orizu tasks assign --task <taskId> --assignees <userId1,userId2>\n orizu tasks status --task <taskId> [--json]\n orizu datasets upload --file <path> [--project <team/project>] [--name <name>]\n orizu tasks export [--task <taskId>] [--format <csv|json|jsonl>] [--out <path>]`)
|
|
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 <userId1,userId2> [--instructions <text>] [--labels-per-item <n>]\n orizu tasks assign --task <taskId> --assignees <userId1,userId2>\n orizu tasks status --task <taskId> [--json]\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 delete-rows [--dataset <datasetId|datasetUrl>] [--project <team/project>] [--row-ids <id1,id2>] [--row-indices <n1,n2>]\n orizu tasks export [--task <taskId>] [--format <csv|json|jsonl>] [--out <path>]`)
|
|
95
110
|
}
|
|
96
111
|
|
|
112
|
+
let cliArgs = process.argv.slice(2)
|
|
113
|
+
|
|
97
114
|
function getArg(name: string): string | null {
|
|
98
|
-
const index =
|
|
99
|
-
if (index === -1 || index + 1 >=
|
|
115
|
+
const index = cliArgs.indexOf(name)
|
|
116
|
+
if (index === -1 || index + 1 >= cliArgs.length) {
|
|
100
117
|
return null
|
|
101
118
|
}
|
|
102
119
|
|
|
103
|
-
return
|
|
120
|
+
return cliArgs[index + 1]
|
|
104
121
|
}
|
|
105
122
|
|
|
106
123
|
function isInteractiveTerminal() {
|
|
@@ -108,7 +125,7 @@ function isInteractiveTerminal() {
|
|
|
108
125
|
}
|
|
109
126
|
|
|
110
127
|
function hasArg(name: string): boolean {
|
|
111
|
-
return
|
|
128
|
+
return cliArgs.includes(name)
|
|
112
129
|
}
|
|
113
130
|
|
|
114
131
|
function expandHomePath(path: string): string {
|
|
@@ -265,6 +282,16 @@ async function fetchApps(project: string): Promise<AppSummary[]> {
|
|
|
265
282
|
return data.apps
|
|
266
283
|
}
|
|
267
284
|
|
|
285
|
+
async function fetchDatasets(project: string): Promise<DatasetSummary[]> {
|
|
286
|
+
const response = await authedFetch(`/api/cli/datasets?project=${encodeURIComponent(project)}`)
|
|
287
|
+
if (!response.ok) {
|
|
288
|
+
throw new Error(`Failed to fetch datasets: ${await response.text()}`)
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
const data = await parseJsonResponse<{ datasets: DatasetSummary[] }>(response, 'Datasets list')
|
|
292
|
+
return data.datasets
|
|
293
|
+
}
|
|
294
|
+
|
|
268
295
|
async function fetchTeamMembers(teamSlug: string): Promise<TeamMember[]> {
|
|
269
296
|
const response = await authedFetch(`/api/cli/teams/${encodeURIComponent(teamSlug)}/members`)
|
|
270
297
|
if (!response.ok) {
|
|
@@ -389,6 +416,26 @@ async function selectAppIdInteractively(projectArg: string | null): Promise<{ ap
|
|
|
389
416
|
}
|
|
390
417
|
}
|
|
391
418
|
|
|
419
|
+
async function selectDatasetInteractively(projectArg: string | null): Promise<{ datasetId: string; project: string }> {
|
|
420
|
+
let project = projectArg
|
|
421
|
+
if (!project) {
|
|
422
|
+
project = await resolveProjectSlug(null)
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
const datasets = await fetchDatasets(project)
|
|
426
|
+
const dataset = await promptSelect(
|
|
427
|
+
`Select a dataset in ${project}`,
|
|
428
|
+
datasets,
|
|
429
|
+
item => `${item.name} (id=${item.id}, rows=${item.rowCount})`,
|
|
430
|
+
{ forcePrompt: true }
|
|
431
|
+
)
|
|
432
|
+
|
|
433
|
+
return {
|
|
434
|
+
datasetId: dataset.id,
|
|
435
|
+
project,
|
|
436
|
+
}
|
|
437
|
+
}
|
|
438
|
+
|
|
392
439
|
function printTeams(teams: Team[]) {
|
|
393
440
|
if (teams.length === 0) {
|
|
394
441
|
console.log('No teams found.')
|
|
@@ -618,11 +665,10 @@ async function login() {
|
|
|
618
665
|
}
|
|
619
666
|
|
|
620
667
|
const loginData = await parseJsonResponse<LoginResponse>(exchangeResponse, 'CLI auth exchange')
|
|
621
|
-
|
|
668
|
+
saveServerCredentials(baseUrl, {
|
|
622
669
|
accessToken: loginData.accessToken,
|
|
623
670
|
refreshToken: loginData.refreshToken,
|
|
624
671
|
expiresAt: loginData.expiresAt,
|
|
625
|
-
baseUrl,
|
|
626
672
|
})
|
|
627
673
|
|
|
628
674
|
console.log(`Logged in as ${loginData.user.email ?? loginData.user.id}`)
|
|
@@ -639,21 +685,22 @@ async function whoami() {
|
|
|
639
685
|
}
|
|
640
686
|
|
|
641
687
|
async function logout() {
|
|
642
|
-
const
|
|
688
|
+
const baseUrl = getBaseUrl()
|
|
689
|
+
const credentials = getServerCredentials(baseUrl)
|
|
643
690
|
if (!credentials) {
|
|
644
|
-
console.log(
|
|
691
|
+
console.log(`Already logged out for ${baseUrl}.`)
|
|
645
692
|
return
|
|
646
693
|
}
|
|
647
694
|
|
|
648
|
-
await fetch(`${
|
|
695
|
+
await fetch(`${baseUrl}/api/cli/auth/logout`, {
|
|
649
696
|
method: 'POST',
|
|
650
697
|
headers: {
|
|
651
698
|
Authorization: `Bearer ${credentials.accessToken}`,
|
|
652
699
|
},
|
|
653
700
|
}).catch(() => undefined)
|
|
654
701
|
|
|
655
|
-
|
|
656
|
-
console.log(
|
|
702
|
+
clearServerCredentials(baseUrl)
|
|
703
|
+
console.log(`Logged out from ${baseUrl}.`)
|
|
657
704
|
}
|
|
658
705
|
|
|
659
706
|
async function listTeams() {
|
|
@@ -1126,6 +1173,157 @@ async function uploadDataset() {
|
|
|
1126
1173
|
}
|
|
1127
1174
|
}
|
|
1128
1175
|
|
|
1176
|
+
function getDatasetReferenceInput(): string | null {
|
|
1177
|
+
const fromFlag = getArg('--dataset')
|
|
1178
|
+
if (fromFlag) {
|
|
1179
|
+
return fromFlag
|
|
1180
|
+
}
|
|
1181
|
+
|
|
1182
|
+
const positional = cliArgs[2]
|
|
1183
|
+
if (positional && !positional.startsWith('--')) {
|
|
1184
|
+
return positional
|
|
1185
|
+
}
|
|
1186
|
+
|
|
1187
|
+
return null
|
|
1188
|
+
}
|
|
1189
|
+
|
|
1190
|
+
async function downloadDataset() {
|
|
1191
|
+
const projectArg = getArg('--project')
|
|
1192
|
+
const datasetInput = getDatasetReferenceInput()
|
|
1193
|
+
const format = (getArg('--format') || 'jsonl') as 'csv' | 'json' | 'jsonl'
|
|
1194
|
+
const outPathArg = getArg('--out')
|
|
1195
|
+
|
|
1196
|
+
if (!['csv', 'json', 'jsonl'].includes(format)) {
|
|
1197
|
+
throw new Error('format must be one of: csv, json, jsonl')
|
|
1198
|
+
}
|
|
1199
|
+
|
|
1200
|
+
let datasetId: string
|
|
1201
|
+
if (datasetInput) {
|
|
1202
|
+
datasetId = parseDatasetReference(datasetInput).datasetId
|
|
1203
|
+
} else {
|
|
1204
|
+
const selected = await selectDatasetInteractively(projectArg)
|
|
1205
|
+
datasetId = selected.datasetId
|
|
1206
|
+
}
|
|
1207
|
+
|
|
1208
|
+
const response = await authedFetch(
|
|
1209
|
+
`/api/cli/datasets/${encodeURIComponent(datasetId)}/download?format=${encodeURIComponent(format)}`
|
|
1210
|
+
)
|
|
1211
|
+
if (!response.ok) {
|
|
1212
|
+
throw new Error(`Download failed: ${await response.text()}`)
|
|
1213
|
+
}
|
|
1214
|
+
|
|
1215
|
+
const filename = outPathArg
|
|
1216
|
+
? expandHomePath(outPathArg)
|
|
1217
|
+
: `${datasetId}.${format}`
|
|
1218
|
+
|
|
1219
|
+
const bytes = new Uint8Array(await response.arrayBuffer())
|
|
1220
|
+
writeFileSync(filename, bytes)
|
|
1221
|
+
|
|
1222
|
+
console.log(`Saved dataset ${datasetId} (${format.toUpperCase()}) to ${filename}`)
|
|
1223
|
+
}
|
|
1224
|
+
|
|
1225
|
+
async function appendDatasetRows() {
|
|
1226
|
+
const projectArg = getArg('--project')
|
|
1227
|
+
const datasetInput = getDatasetReferenceInput()
|
|
1228
|
+
const fileArg = getArg('--file')
|
|
1229
|
+
|
|
1230
|
+
if (!fileArg) {
|
|
1231
|
+
throw new Error('Usage: orizu datasets append [--dataset <datasetId|datasetUrl>] [--project <team/project>] --file <path>')
|
|
1232
|
+
}
|
|
1233
|
+
|
|
1234
|
+
let datasetId: string
|
|
1235
|
+
if (datasetInput) {
|
|
1236
|
+
datasetId = parseDatasetReference(datasetInput).datasetId
|
|
1237
|
+
} else {
|
|
1238
|
+
const selected = await selectDatasetInteractively(projectArg)
|
|
1239
|
+
datasetId = selected.datasetId
|
|
1240
|
+
}
|
|
1241
|
+
|
|
1242
|
+
const file = expandHomePath(fileArg)
|
|
1243
|
+
const { rows } = parseDatasetFile(file)
|
|
1244
|
+
if (!Array.isArray(rows) || rows.length === 0) {
|
|
1245
|
+
throw new Error('Dataset append file must contain at least one row')
|
|
1246
|
+
}
|
|
1247
|
+
|
|
1248
|
+
const response = await authedFetch(`/api/cli/datasets/${encodeURIComponent(datasetId)}/rows`, {
|
|
1249
|
+
method: 'POST',
|
|
1250
|
+
headers: { 'Content-Type': 'application/json' },
|
|
1251
|
+
body: JSON.stringify({ rows }),
|
|
1252
|
+
})
|
|
1253
|
+
|
|
1254
|
+
if (!response.ok) {
|
|
1255
|
+
throw new Error(`Append failed: ${await response.text()}`)
|
|
1256
|
+
}
|
|
1257
|
+
|
|
1258
|
+
const data = await parseJsonResponse<{
|
|
1259
|
+
dataset: { id: string; name: string; rowCount: number }
|
|
1260
|
+
appendedCount: number
|
|
1261
|
+
}>(response, 'Dataset append')
|
|
1262
|
+
|
|
1263
|
+
console.log(
|
|
1264
|
+
`Appended ${data.appendedCount} rows to dataset ${data.dataset.name} (${data.dataset.id}). New row count: ${data.dataset.rowCount}`
|
|
1265
|
+
)
|
|
1266
|
+
}
|
|
1267
|
+
|
|
1268
|
+
function parseCommaSeparatedIntegers(value: string | null): number[] {
|
|
1269
|
+
if (!value) {
|
|
1270
|
+
return []
|
|
1271
|
+
}
|
|
1272
|
+
|
|
1273
|
+
const rawItems = value
|
|
1274
|
+
.split(',')
|
|
1275
|
+
.map(item => item.trim())
|
|
1276
|
+
.filter(Boolean)
|
|
1277
|
+
const parsed = rawItems.map(item => Number(item))
|
|
1278
|
+
const invalid = parsed.some(item => !Number.isInteger(item) || item < 0)
|
|
1279
|
+
if (invalid) {
|
|
1280
|
+
throw new Error('row-indices must be comma-separated non-negative integers')
|
|
1281
|
+
}
|
|
1282
|
+
|
|
1283
|
+
return parsed
|
|
1284
|
+
}
|
|
1285
|
+
|
|
1286
|
+
async function deleteDatasetRows() {
|
|
1287
|
+
const projectArg = getArg('--project')
|
|
1288
|
+
const datasetInput = getDatasetReferenceInput()
|
|
1289
|
+
const rowIds = parseCommaSeparated(getArg('--row-ids'))
|
|
1290
|
+
const rowIndices = parseCommaSeparatedIntegers(getArg('--row-indices'))
|
|
1291
|
+
|
|
1292
|
+
if (rowIds.length === 0 && rowIndices.length === 0) {
|
|
1293
|
+
throw new Error('Usage: orizu datasets delete-rows [--dataset <datasetId|datasetUrl>] [--project <team/project>] [--row-ids <id1,id2>] [--row-indices <n1,n2>]')
|
|
1294
|
+
}
|
|
1295
|
+
|
|
1296
|
+
let datasetId: string
|
|
1297
|
+
if (datasetInput) {
|
|
1298
|
+
datasetId = parseDatasetReference(datasetInput).datasetId
|
|
1299
|
+
} else {
|
|
1300
|
+
const selected = await selectDatasetInteractively(projectArg)
|
|
1301
|
+
datasetId = selected.datasetId
|
|
1302
|
+
}
|
|
1303
|
+
|
|
1304
|
+
const response = await authedFetch(`/api/cli/datasets/${encodeURIComponent(datasetId)}/rows`, {
|
|
1305
|
+
method: 'DELETE',
|
|
1306
|
+
headers: { 'Content-Type': 'application/json' },
|
|
1307
|
+
body: JSON.stringify({
|
|
1308
|
+
rowIds,
|
|
1309
|
+
rowIndices,
|
|
1310
|
+
}),
|
|
1311
|
+
})
|
|
1312
|
+
|
|
1313
|
+
if (!response.ok) {
|
|
1314
|
+
throw new Error(`Delete rows failed: ${await response.text()}`)
|
|
1315
|
+
}
|
|
1316
|
+
|
|
1317
|
+
const data = await parseJsonResponse<{
|
|
1318
|
+
dataset: { id: string; name: string; rowCount: number }
|
|
1319
|
+
deletedCount: number
|
|
1320
|
+
}>(response, 'Dataset delete rows')
|
|
1321
|
+
|
|
1322
|
+
console.log(
|
|
1323
|
+
`Deleted ${data.deletedCount} rows from dataset ${data.dataset.name} (${data.dataset.id}). New row count: ${data.dataset.rowCount}`
|
|
1324
|
+
)
|
|
1325
|
+
}
|
|
1326
|
+
|
|
1129
1327
|
async function downloadAnnotations() {
|
|
1130
1328
|
let taskId = getArg('--task')
|
|
1131
1329
|
const format = (getArg('--format') || 'jsonl') as 'csv' | 'json' | 'jsonl'
|
|
@@ -1156,8 +1354,12 @@ async function downloadAnnotations() {
|
|
|
1156
1354
|
}
|
|
1157
1355
|
|
|
1158
1356
|
async function main() {
|
|
1159
|
-
const
|
|
1160
|
-
|
|
1357
|
+
const parsed = parseGlobalFlags(process.argv.slice(2))
|
|
1358
|
+
setGlobalFlags(parsed.flags)
|
|
1359
|
+
cliArgs = parsed.args
|
|
1360
|
+
|
|
1361
|
+
const command = cliArgs[0]
|
|
1362
|
+
const subcommand = cliArgs[1]
|
|
1161
1363
|
|
|
1162
1364
|
if (!command) {
|
|
1163
1365
|
printUsage()
|
|
@@ -1189,7 +1391,7 @@ async function main() {
|
|
|
1189
1391
|
return
|
|
1190
1392
|
}
|
|
1191
1393
|
|
|
1192
|
-
const teamsMembersAction =
|
|
1394
|
+
const teamsMembersAction = cliArgs[2]
|
|
1193
1395
|
if (command === 'teams' && subcommand === 'members' && teamsMembersAction === 'list') {
|
|
1194
1396
|
await listTeamMembers()
|
|
1195
1397
|
return
|
|
@@ -1262,6 +1464,21 @@ async function main() {
|
|
|
1262
1464
|
return
|
|
1263
1465
|
}
|
|
1264
1466
|
|
|
1467
|
+
if (command === 'datasets' && subcommand === 'download') {
|
|
1468
|
+
await downloadDataset()
|
|
1469
|
+
return
|
|
1470
|
+
}
|
|
1471
|
+
|
|
1472
|
+
if (command === 'datasets' && subcommand === 'append') {
|
|
1473
|
+
await appendDatasetRows()
|
|
1474
|
+
return
|
|
1475
|
+
}
|
|
1476
|
+
|
|
1477
|
+
if (command === 'datasets' && subcommand === 'delete-rows') {
|
|
1478
|
+
await deleteDatasetRows()
|
|
1479
|
+
return
|
|
1480
|
+
}
|
|
1481
|
+
|
|
1265
1482
|
if (command === 'tasks' && subcommand === 'export') {
|
|
1266
1483
|
await downloadAnnotations()
|
|
1267
1484
|
return
|
package/src/types.ts
CHANGED
|
@@ -1,10 +1,25 @@
|
|
|
1
|
-
export interface
|
|
1
|
+
export interface ServerCredentials {
|
|
2
2
|
accessToken: string
|
|
3
3
|
refreshToken: string
|
|
4
4
|
expiresAt: number
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
export interface StoredCredentialsV1 {
|
|
5
8
|
baseUrl: string
|
|
9
|
+
accessToken: string
|
|
10
|
+
refreshToken: string
|
|
11
|
+
expiresAt: number
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export interface StoredCredentialsV2 {
|
|
15
|
+
version: 2
|
|
16
|
+
activeBaseUrl: string | null
|
|
17
|
+
servers: Record<string, ServerCredentials>
|
|
6
18
|
}
|
|
7
19
|
|
|
20
|
+
// Backward-compatible alias for legacy callers.
|
|
21
|
+
export type StoredCredentials = StoredCredentialsV1
|
|
22
|
+
|
|
8
23
|
export interface LoginResponse {
|
|
9
24
|
accessToken: string
|
|
10
25
|
refreshToken: string
|