orizu 0.0.5 → 0.0.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.
@@ -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
- export function saveCredentials(credentials) {
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(credentials, null, 2), 'utf-8');
47
+ writeFileSync(path, JSON.stringify(config, null, 2), 'utf-8');
15
48
  chmodSync(path, 0o600);
16
49
  }
17
- export function loadCredentials() {
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
- return JSON.parse(raw);
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 clearCredentials() {
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 { loadCredentials, saveCredentials } from './credentials.js';
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 process.env.ORIZU_BASE_URL || 'https://orizu.ai';
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(`${credentials.baseUrl}/api/cli/auth/refresh`, {
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
- saveCredentials(refreshed);
44
+ updateServerCredentials(baseUrl, refreshed);
26
45
  return refreshed;
27
46
  }
28
47
  export async function authedFetch(path, init = {}) {
29
- const credentials = loadCredentials();
48
+ const baseUrl = resolveBaseUrl();
49
+ const credentials = getServerCredentials(baseUrl);
30
50
  if (!credentials) {
31
- throw new Error('Not logged in. Run `orizu login` first.');
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(`${activeCredentials.baseUrl}${path}`, {
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(`${activeCredentials.baseUrl}${path}`, {
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 { clearCredentials, loadCredentials, saveCredentials } from './credentials.js';
9
+ import { clearServerCredentials, getServerCredentials, saveServerCredentials } from './credentials.js';
10
10
  import { parseDatasetFile } from './file-parser.js';
11
- import { authedFetch, getBaseUrl } from './http.js';
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 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 = process.argv.indexOf(name);
17
- if (index === -1 || index + 1 >= process.argv.length) {
19
+ const index = cliArgs.indexOf(name);
20
+ if (index === -1 || index + 1 >= cliArgs.length) {
18
21
  return null;
19
22
  }
20
- return process.argv[index + 1];
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 process.argv.includes(name);
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
- saveCredentials({
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 credentials = loadCredentials();
419
+ const baseUrl = getBaseUrl();
420
+ const credentials = getServerCredentials(baseUrl);
398
421
  if (!credentials) {
399
- console.log('Already logged out.');
422
+ console.log(`Already logged out for ${baseUrl}.`);
400
423
  return;
401
424
  }
402
- await fetch(`${credentials.baseUrl}/api/cli/auth/logout`, {
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
- clearCredentials();
409
- console.log('Logged out.');
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,44 @@ async function uploadDataset() {
769
792
  console.log(`View dataset: ${formatTerminalLink(data.dataset.url)}`);
770
793
  }
771
794
  }
795
+ function getDatasetDownloadInput() {
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 = getDatasetDownloadInput();
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
+ }
772
833
  async function downloadAnnotations() {
773
834
  let taskId = getArg('--task');
774
835
  const format = (getArg('--format') || 'jsonl');
@@ -792,8 +853,11 @@ async function downloadAnnotations() {
792
853
  console.log(`Saved ${format.toUpperCase()} export to ${filename}`);
793
854
  }
794
855
  async function main() {
795
- const command = process.argv[2];
796
- const subcommand = process.argv[3];
856
+ const parsed = parseGlobalFlags(process.argv.slice(2));
857
+ setGlobalFlags(parsed.flags);
858
+ cliArgs = parsed.args;
859
+ const command = cliArgs[0];
860
+ const subcommand = cliArgs[1];
797
861
  if (!command) {
798
862
  printUsage();
799
863
  process.exit(1);
@@ -818,7 +882,7 @@ async function main() {
818
882
  await createTeam();
819
883
  return;
820
884
  }
821
- const teamsMembersAction = process.argv[4];
885
+ const teamsMembersAction = cliArgs[2];
822
886
  if (command === 'teams' && subcommand === 'members' && teamsMembersAction === 'list') {
823
887
  await listTeamMembers();
824
888
  return;
@@ -879,6 +943,10 @@ async function main() {
879
943
  await uploadDataset();
880
944
  return;
881
945
  }
946
+ if (command === 'datasets' && subcommand === 'download') {
947
+ await downloadDataset();
948
+ return;
949
+ }
882
950
  if (command === 'tasks' && subcommand === 'export') {
883
951
  await downloadAnnotations();
884
952
  return;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "orizu",
3
- "version": "0.0.5",
3
+ "version": "0.0.6",
4
4
  "private": false,
5
5
  "type": "module",
6
6
  "bin": {
@@ -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 { StoredCredentials } from './types.js'
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
- export function saveCredentials(credentials: StoredCredentials) {
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(credentials, null, 2), 'utf-8')
58
+ writeFileSync(path, JSON.stringify(config, null, 2), 'utf-8')
19
59
  chmodSync(path, 0o600)
20
60
  }
21
61
 
22
- export function loadCredentials(): StoredCredentials | null {
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
- return JSON.parse(raw) as StoredCredentials
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 clearCredentials() {
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 { loadCredentials, saveCredentials } from './credentials.js'
2
- import { LoginResponse, StoredCredentials } from './types.js'
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 process.env.ORIZU_BASE_URL || 'https://orizu.ai'
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: StoredCredentials): Promise<StoredCredentials> {
14
- const response = await fetch(`${credentials.baseUrl}/api/cli/auth/refresh`, {
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
- saveCredentials(refreshed)
56
+ updateServerCredentials(baseUrl, refreshed)
32
57
  return refreshed
33
58
  }
34
59
 
35
60
  export async function authedFetch(path: string, init: RequestInit = {}) {
36
- const credentials = loadCredentials()
61
+ const baseUrl = resolveBaseUrl()
62
+ const credentials = getServerCredentials(baseUrl)
37
63
  if (!credentials) {
38
- throw new Error('Not logged in. Run `orizu login` first.')
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(`${activeCredentials.baseUrl}${path}`, {
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(`${activeCredentials.baseUrl}${path}`, {
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 { clearCredentials, loadCredentials, saveCredentials } from './credentials.js'
9
+ import { clearServerCredentials, getServerCredentials, saveServerCredentials } from './credentials.js'
10
10
  import { parseDatasetFile } from './file-parser.js'
11
- import { authedFetch, getBaseUrl } from './http.js'
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 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 = process.argv.indexOf(name)
99
- if (index === -1 || index + 1 >= process.argv.length) {
115
+ const index = cliArgs.indexOf(name)
116
+ if (index === -1 || index + 1 >= cliArgs.length) {
100
117
  return null
101
118
  }
102
119
 
103
- return process.argv[index + 1]
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 process.argv.includes(name)
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
- saveCredentials({
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 credentials = loadCredentials()
688
+ const baseUrl = getBaseUrl()
689
+ const credentials = getServerCredentials(baseUrl)
643
690
  if (!credentials) {
644
- console.log('Already logged out.')
691
+ console.log(`Already logged out for ${baseUrl}.`)
645
692
  return
646
693
  }
647
694
 
648
- await fetch(`${credentials.baseUrl}/api/cli/auth/logout`, {
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
- clearCredentials()
656
- console.log('Logged out.')
702
+ clearServerCredentials(baseUrl)
703
+ console.log(`Logged out from ${baseUrl}.`)
657
704
  }
658
705
 
659
706
  async function listTeams() {
@@ -1126,6 +1173,55 @@ async function uploadDataset() {
1126
1173
  }
1127
1174
  }
1128
1175
 
1176
+ function getDatasetDownloadInput(): 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 = getDatasetDownloadInput()
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
+
1129
1225
  async function downloadAnnotations() {
1130
1226
  let taskId = getArg('--task')
1131
1227
  const format = (getArg('--format') || 'jsonl') as 'csv' | 'json' | 'jsonl'
@@ -1156,8 +1252,12 @@ async function downloadAnnotations() {
1156
1252
  }
1157
1253
 
1158
1254
  async function main() {
1159
- const command = process.argv[2]
1160
- const subcommand = process.argv[3]
1255
+ const parsed = parseGlobalFlags(process.argv.slice(2))
1256
+ setGlobalFlags(parsed.flags)
1257
+ cliArgs = parsed.args
1258
+
1259
+ const command = cliArgs[0]
1260
+ const subcommand = cliArgs[1]
1161
1261
 
1162
1262
  if (!command) {
1163
1263
  printUsage()
@@ -1189,7 +1289,7 @@ async function main() {
1189
1289
  return
1190
1290
  }
1191
1291
 
1192
- const teamsMembersAction = process.argv[4]
1292
+ const teamsMembersAction = cliArgs[2]
1193
1293
  if (command === 'teams' && subcommand === 'members' && teamsMembersAction === 'list') {
1194
1294
  await listTeamMembers()
1195
1295
  return
@@ -1262,6 +1362,11 @@ async function main() {
1262
1362
  return
1263
1363
  }
1264
1364
 
1365
+ if (command === 'datasets' && subcommand === 'download') {
1366
+ await downloadDataset()
1367
+ return
1368
+ }
1369
+
1265
1370
  if (command === 'tasks' && subcommand === 'export') {
1266
1371
  await downloadAnnotations()
1267
1372
  return
package/src/types.ts CHANGED
@@ -1,10 +1,25 @@
1
- export interface StoredCredentials {
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