@chalksurf/cli 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +135 -0
- package/dist/bin/chalksurf.js +121 -0
- package/dist/commands/auth.js +155 -0
- package/dist/commands/job.js +94 -0
- package/dist/commands/org.js +106 -0
- package/dist/commands/sheet.js +278 -0
- package/dist/lib/api-client.js +71 -0
- package/dist/lib/cli-error.js +10 -0
- package/dist/lib/config-store.js +176 -0
- package/dist/lib/manifest.js +107 -0
- package/dist/lib/output.js +24 -0
- package/dist/lib/session.js +45 -0
- package/dist/lib/source-resolver.js +210 -0
- package/dist/lib/user-jobs.js +78 -0
- package/package.json +43 -0
package/README.md
ADDED
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
# `@chalksurf/cli`
|
|
2
|
+
|
|
3
|
+
Publishable Chalksurf CLI package.
|
|
4
|
+
|
|
5
|
+
Current internal release line: `0.1.0`. Expect breaking changes while the CLI is still only used internally.
|
|
6
|
+
|
|
7
|
+
Current milestone scope:
|
|
8
|
+
|
|
9
|
+
- package skeleton and build output
|
|
10
|
+
- `chalksurf --help`
|
|
11
|
+
- `chalksurf --version`
|
|
12
|
+
- `chalksurf auth login --with-token`
|
|
13
|
+
- `chalksurf auth status`
|
|
14
|
+
- `chalksurf auth logout`
|
|
15
|
+
- `chalksurf org list`
|
|
16
|
+
- `chalksurf org use <organization-id>`
|
|
17
|
+
- `chalksurf sheet import [sources...]`
|
|
18
|
+
- `chalksurf job get <job-id>`
|
|
19
|
+
- `chalksurf job wait <job-id> [<job-id>...]`
|
|
20
|
+
- shared output helpers
|
|
21
|
+
- shared HTTP client wrapper
|
|
22
|
+
- secure local config persistence for CLI auth
|
|
23
|
+
- source resolution for local files, directories, and URLs
|
|
24
|
+
- manifest-driven imports and optional job waiting
|
|
25
|
+
|
|
26
|
+
## Installation
|
|
27
|
+
|
|
28
|
+
Run the published CLI without a global install:
|
|
29
|
+
|
|
30
|
+
```bash
|
|
31
|
+
npx @chalksurf/cli@0.1.0 --help
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
Install it globally when you want a persistent local binary:
|
|
35
|
+
|
|
36
|
+
```bash
|
|
37
|
+
npm install -g @chalksurf/cli@0.1.0
|
|
38
|
+
chalksurf --version
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
## Usage
|
|
42
|
+
|
|
43
|
+
Pipe a CLI token into the login command:
|
|
44
|
+
|
|
45
|
+
```bash
|
|
46
|
+
printf '%s' "$CHALKSURF_TOKEN" | chalksurf auth login --with-token --base-url https://api.chalksurf.com
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
Check the current session:
|
|
50
|
+
|
|
51
|
+
```bash
|
|
52
|
+
chalksurf auth status
|
|
53
|
+
chalksurf org list
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
Select a default organization:
|
|
57
|
+
|
|
58
|
+
```bash
|
|
59
|
+
chalksurf org use org_123
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
For headless or CI usage, prefer environment variables:
|
|
63
|
+
|
|
64
|
+
```bash
|
|
65
|
+
export CHALKSURF_BASE_URL=https://api.chalksurf.com
|
|
66
|
+
export CHALKSURF_TOKEN=cs_cli_...
|
|
67
|
+
export CHALKSURF_ORGANIZATION_ID=org_123
|
|
68
|
+
chalksurf auth status --json
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
Use the published package directly in agent or CI runs when you do not want a global install:
|
|
72
|
+
|
|
73
|
+
```bash
|
|
74
|
+
CHALKSURF_BASE_URL=https://api.chalksurf.com \
|
|
75
|
+
CHALKSURF_TOKEN=cs_cli_... \
|
|
76
|
+
CHALKSURF_ORGANIZATION_ID=org_123 \
|
|
77
|
+
npx @chalksurf/cli@0.1.0 auth status --json
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
Import a local file:
|
|
81
|
+
|
|
82
|
+
```bash
|
|
83
|
+
chalksurf sheet import ./fixtures/algebra.pdf
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
Import a URL and override the stored default organization for this run:
|
|
87
|
+
|
|
88
|
+
```bash
|
|
89
|
+
chalksurf sheet import https://example.com/trig.docx \
|
|
90
|
+
--relative-path Imported/trig.docx \
|
|
91
|
+
--organization org_123
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
Import from a manifest and wait for the jobs to finish:
|
|
95
|
+
|
|
96
|
+
```bash
|
|
97
|
+
cat import.json | chalksurf sheet import --manifest - --wait --json
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
Wait on an existing job:
|
|
101
|
+
|
|
102
|
+
```bash
|
|
103
|
+
chalksurf job get job_123 --json
|
|
104
|
+
chalksurf job wait job_123 job_124 --json
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
## Local And Staging Testing
|
|
108
|
+
|
|
109
|
+
Use separate config files per environment so local and staging tokens do not overwrite each other:
|
|
110
|
+
|
|
111
|
+
```bash
|
|
112
|
+
npm run cli-dev -- --help
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
Run the CLI directly from source during local development:
|
|
116
|
+
|
|
117
|
+
```bash
|
|
118
|
+
export CHALKSURF_CONFIG_PATH=/tmp/chalksurf-local.json
|
|
119
|
+
printf '%s' "$LOCAL_CLI_TOKEN" | npm run cli-dev -- auth login --with-token --base-url http://localhost:8080
|
|
120
|
+
npm run cli-dev -- auth status --json
|
|
121
|
+
npm run cli-dev -- org list --json
|
|
122
|
+
npm run cli-dev -- sheet import ./fixtures/algebra.pdf --wait --json
|
|
123
|
+
npm run cli-dev -- job get job_123 --json
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
```bash
|
|
127
|
+
export CHALKSURF_CONFIG_PATH=/tmp/chalksurf-staging.json
|
|
128
|
+
printf '%s' "$STAGING_CLI_TOKEN" | npm run cli-dev -- auth login --with-token --base-url https://staging-api.chalksurf.com
|
|
129
|
+
npm run cli-dev -- auth status --json
|
|
130
|
+
npm run cli-dev -- org list --json
|
|
131
|
+
npm run cli-dev -- sheet import https://example.com/worksheet.docx --relative-path Imported/worksheet.docx --json
|
|
132
|
+
npm run cli-dev -- job wait job_123 --json
|
|
133
|
+
```
|
|
134
|
+
|
|
135
|
+
For short-lived agent runs, skip local persistence and inject credentials through environment variables instead.
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { readFileSync, realpathSync } from 'node:fs';
|
|
3
|
+
import { fileURLToPath } from 'node:url';
|
|
4
|
+
import { hideBin } from 'yargs/helpers';
|
|
5
|
+
import yargs from 'yargs/yargs';
|
|
6
|
+
import { registerAuthCommands } from '../commands/auth.js';
|
|
7
|
+
import { registerJobCommands } from '../commands/job.js';
|
|
8
|
+
import { registerOrgCommands } from '../commands/org.js';
|
|
9
|
+
import { registerSheetCommands } from '../commands/sheet.js';
|
|
10
|
+
import { CliCommandError } from '../lib/cli-error.js';
|
|
11
|
+
import { createConfigStore } from '../lib/config-store.js';
|
|
12
|
+
import { createOutput } from '../lib/output.js';
|
|
13
|
+
const packageJson = JSON.parse(readFileSync(new URL('../../package.json', import.meta.url), 'utf8'));
|
|
14
|
+
const createCli = ({ cwd, configPath, env, fetchImpl, now, output, sleep, stdin, }) => {
|
|
15
|
+
const commandContext = {
|
|
16
|
+
cwd,
|
|
17
|
+
configStore: createConfigStore({ configPath, env }),
|
|
18
|
+
env,
|
|
19
|
+
fetchImpl,
|
|
20
|
+
now,
|
|
21
|
+
output,
|
|
22
|
+
sleep,
|
|
23
|
+
stdin,
|
|
24
|
+
};
|
|
25
|
+
return yargs()
|
|
26
|
+
.scriptName('chalksurf')
|
|
27
|
+
.usage('$0 <command> [options]')
|
|
28
|
+
.version(false)
|
|
29
|
+
.option('json', {
|
|
30
|
+
type: 'boolean',
|
|
31
|
+
default: false,
|
|
32
|
+
describe: 'Write machine-readable JSON to stdout',
|
|
33
|
+
})
|
|
34
|
+
.option('base-url', {
|
|
35
|
+
type: 'string',
|
|
36
|
+
global: true,
|
|
37
|
+
describe: 'Base URL of the ChalkSurf API',
|
|
38
|
+
})
|
|
39
|
+
.option('organization', {
|
|
40
|
+
type: 'string',
|
|
41
|
+
global: true,
|
|
42
|
+
describe: 'Organization ID to use for this command',
|
|
43
|
+
})
|
|
44
|
+
.option('version', {
|
|
45
|
+
type: 'boolean',
|
|
46
|
+
alias: 'v',
|
|
47
|
+
describe: 'Show the CLI version',
|
|
48
|
+
})
|
|
49
|
+
.command('auth <subcommand>', 'Authentication commands', (authYargs) => registerAuthCommands(authYargs, commandContext), () => { })
|
|
50
|
+
.command('org <subcommand>', 'Organization commands', (orgYargs) => registerOrgCommands(orgYargs, commandContext), () => { })
|
|
51
|
+
.command('sheet <subcommand>', 'Exercise sheet commands', (sheetYargs) => registerSheetCommands(sheetYargs, commandContext), () => { })
|
|
52
|
+
.command('job <subcommand>', 'Job commands', (jobYargs) => registerJobCommands(jobYargs, commandContext), () => { })
|
|
53
|
+
.help()
|
|
54
|
+
.strict()
|
|
55
|
+
.exitProcess(false);
|
|
56
|
+
};
|
|
57
|
+
const hasFlag = (argv, flags) => {
|
|
58
|
+
return argv.some((argument) => flags.includes(argument));
|
|
59
|
+
};
|
|
60
|
+
export const runCli = async ({ argv, cwd = process.cwd(), configPath, env = process.env, fetchImpl = fetch, nowImpl = Date.now, sleepImpl = (milliseconds) => new Promise((resolve) => setTimeout(resolve, milliseconds)), stdin = process.stdin, stdout = process.stdout, stderr = process.stderr, }) => {
|
|
61
|
+
const output = createOutput({
|
|
62
|
+
json: hasFlag(argv, ['--json']),
|
|
63
|
+
stdout,
|
|
64
|
+
stderr,
|
|
65
|
+
});
|
|
66
|
+
const cli = createCli({
|
|
67
|
+
cwd,
|
|
68
|
+
configPath,
|
|
69
|
+
env,
|
|
70
|
+
fetchImpl,
|
|
71
|
+
now: nowImpl,
|
|
72
|
+
output,
|
|
73
|
+
sleep: sleepImpl,
|
|
74
|
+
stdin,
|
|
75
|
+
});
|
|
76
|
+
if (argv.length === 0 || hasFlag(argv, ['--help', '-h'])) {
|
|
77
|
+
stdout.write(await cli.getHelp());
|
|
78
|
+
return 0;
|
|
79
|
+
}
|
|
80
|
+
if (hasFlag(argv, ['--version', '-v'])) {
|
|
81
|
+
output.print({ version: packageJson.version }, packageJson.version);
|
|
82
|
+
return 0;
|
|
83
|
+
}
|
|
84
|
+
try {
|
|
85
|
+
await cli.parseAsync(argv);
|
|
86
|
+
return 0;
|
|
87
|
+
}
|
|
88
|
+
catch (error) {
|
|
89
|
+
if (error instanceof CliCommandError) {
|
|
90
|
+
if (error.shouldReport && error.message) {
|
|
91
|
+
output.error(error.message);
|
|
92
|
+
}
|
|
93
|
+
return error.exitCode;
|
|
94
|
+
}
|
|
95
|
+
const message = error instanceof Error ? error.message : 'Unknown CLI error';
|
|
96
|
+
output.error(message);
|
|
97
|
+
return 1;
|
|
98
|
+
}
|
|
99
|
+
};
|
|
100
|
+
export const isDirectExecution = ({ importMetaUrl, processArgv1, }) => {
|
|
101
|
+
if (!processArgv1) {
|
|
102
|
+
return false;
|
|
103
|
+
}
|
|
104
|
+
try {
|
|
105
|
+
return realpathSync(fileURLToPath(importMetaUrl)) === realpathSync(processArgv1);
|
|
106
|
+
}
|
|
107
|
+
catch {
|
|
108
|
+
return false;
|
|
109
|
+
}
|
|
110
|
+
};
|
|
111
|
+
if (isDirectExecution({ importMetaUrl: import.meta.url, processArgv1: process.argv[1] })) {
|
|
112
|
+
void runCli({ argv: hideBin(process.argv) })
|
|
113
|
+
.then((exitCode) => {
|
|
114
|
+
process.exitCode = exitCode;
|
|
115
|
+
})
|
|
116
|
+
.catch((error) => {
|
|
117
|
+
const message = error instanceof Error ? error.message : 'Unknown CLI error';
|
|
118
|
+
process.stderr.write(`${message}\n`);
|
|
119
|
+
process.exitCode = 1;
|
|
120
|
+
});
|
|
121
|
+
}
|
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
import { createApiClient } from '../lib/api-client.js';
|
|
2
|
+
import { CliCommandError } from '../lib/cli-error.js';
|
|
3
|
+
import { resolveOrganization } from '../lib/config-store.js';
|
|
4
|
+
import { formatUserIdentity, mapApiErrorToCliError, requireResolvedBaseUrl, requireResolvedToken, } from '../lib/session.js';
|
|
5
|
+
const isInteractiveStdin = (stdin) => {
|
|
6
|
+
return stdin.isTTY === true;
|
|
7
|
+
};
|
|
8
|
+
const readTokenFromStdin = async (stdin) => {
|
|
9
|
+
let token = '';
|
|
10
|
+
for await (const chunk of stdin) {
|
|
11
|
+
token += typeof chunk === 'string' ? chunk : Buffer.from(chunk).toString('utf8');
|
|
12
|
+
}
|
|
13
|
+
return token.trim();
|
|
14
|
+
};
|
|
15
|
+
const serializeOrganization = (organization) => {
|
|
16
|
+
if (!organization) {
|
|
17
|
+
return null;
|
|
18
|
+
}
|
|
19
|
+
return {
|
|
20
|
+
id: organization.organizationId,
|
|
21
|
+
name: organization.organizationName,
|
|
22
|
+
role: organization.role,
|
|
23
|
+
type: organization.organizationType,
|
|
24
|
+
};
|
|
25
|
+
};
|
|
26
|
+
const formatOrganizationSummary = (organization) => {
|
|
27
|
+
if (!organization) {
|
|
28
|
+
return 'No default organization selected.';
|
|
29
|
+
}
|
|
30
|
+
return `Default organization: ${organization.name} (${organization.id}).`;
|
|
31
|
+
};
|
|
32
|
+
export const registerAuthCommands = (authYargs, context) => {
|
|
33
|
+
return authYargs
|
|
34
|
+
.command('login', 'Read a CLI token from stdin and store it locally', (loginYargs) => {
|
|
35
|
+
return loginYargs.option('with-token', {
|
|
36
|
+
type: 'boolean',
|
|
37
|
+
demandOption: true,
|
|
38
|
+
describe: 'Read the token from stdin',
|
|
39
|
+
});
|
|
40
|
+
}, async (argv) => {
|
|
41
|
+
const config = await context.configStore.load();
|
|
42
|
+
const resolvedBaseUrl = requireResolvedBaseUrl({
|
|
43
|
+
flagValue: argv.baseUrl,
|
|
44
|
+
env: context.env,
|
|
45
|
+
config,
|
|
46
|
+
});
|
|
47
|
+
if (isInteractiveStdin(context.stdin)) {
|
|
48
|
+
throw new CliCommandError('No token was piped on stdin. Pipe a CLI token into "chalksurf auth login --with-token".', 2);
|
|
49
|
+
}
|
|
50
|
+
const token = await readTokenFromStdin(context.stdin);
|
|
51
|
+
if (!token) {
|
|
52
|
+
throw new CliCommandError('No token was provided on stdin.', 2);
|
|
53
|
+
}
|
|
54
|
+
const apiClient = createApiClient({
|
|
55
|
+
baseUrl: resolvedBaseUrl.value,
|
|
56
|
+
token,
|
|
57
|
+
fetchImpl: context.fetchImpl,
|
|
58
|
+
});
|
|
59
|
+
let profile;
|
|
60
|
+
try {
|
|
61
|
+
profile = await apiClient.getUserProfile();
|
|
62
|
+
}
|
|
63
|
+
catch (error) {
|
|
64
|
+
throw mapApiErrorToCliError(error);
|
|
65
|
+
}
|
|
66
|
+
const resolvedOrganization = resolveOrganization({
|
|
67
|
+
flagValue: argv.organization,
|
|
68
|
+
env: context.env,
|
|
69
|
+
config,
|
|
70
|
+
profile,
|
|
71
|
+
});
|
|
72
|
+
await context.configStore.update((currentConfig) => ({
|
|
73
|
+
...currentConfig,
|
|
74
|
+
token,
|
|
75
|
+
baseUrl: resolvedBaseUrl.value,
|
|
76
|
+
organizationId: resolvedOrganization.organization?.organizationId,
|
|
77
|
+
}));
|
|
78
|
+
resolvedOrganization.warnings.forEach((warning) => {
|
|
79
|
+
context.output.info(warning);
|
|
80
|
+
});
|
|
81
|
+
context.output.print({
|
|
82
|
+
authenticated: true,
|
|
83
|
+
baseUrl: resolvedBaseUrl.value,
|
|
84
|
+
baseUrlSource: resolvedBaseUrl.source,
|
|
85
|
+
organization: serializeOrganization(resolvedOrganization.organization),
|
|
86
|
+
user: {
|
|
87
|
+
email: profile.email,
|
|
88
|
+
id: profile.id,
|
|
89
|
+
name: profile.name,
|
|
90
|
+
},
|
|
91
|
+
warnings: resolvedOrganization.warnings,
|
|
92
|
+
}, (result) => `Logged in as ${formatUserIdentity(result.user)}.\n${formatOrganizationSummary(result.organization)}`);
|
|
93
|
+
})
|
|
94
|
+
.command('status', 'Show the current authentication and organization state', () => { }, async (argv) => {
|
|
95
|
+
const config = await context.configStore.load();
|
|
96
|
+
const resolvedBaseUrl = requireResolvedBaseUrl({
|
|
97
|
+
flagValue: argv.baseUrl,
|
|
98
|
+
env: context.env,
|
|
99
|
+
config,
|
|
100
|
+
});
|
|
101
|
+
const resolvedToken = requireResolvedToken({
|
|
102
|
+
env: context.env,
|
|
103
|
+
config,
|
|
104
|
+
});
|
|
105
|
+
const apiClient = createApiClient({
|
|
106
|
+
baseUrl: resolvedBaseUrl.value,
|
|
107
|
+
token: resolvedToken.value,
|
|
108
|
+
fetchImpl: context.fetchImpl,
|
|
109
|
+
});
|
|
110
|
+
let profile;
|
|
111
|
+
try {
|
|
112
|
+
profile = await apiClient.getUserProfile();
|
|
113
|
+
}
|
|
114
|
+
catch (error) {
|
|
115
|
+
throw mapApiErrorToCliError(error);
|
|
116
|
+
}
|
|
117
|
+
const resolvedOrganization = resolveOrganization({
|
|
118
|
+
flagValue: argv.organization,
|
|
119
|
+
env: context.env,
|
|
120
|
+
config,
|
|
121
|
+
profile,
|
|
122
|
+
});
|
|
123
|
+
resolvedOrganization.warnings.forEach((warning) => {
|
|
124
|
+
context.output.info(warning);
|
|
125
|
+
});
|
|
126
|
+
context.output.print({
|
|
127
|
+
authenticated: true,
|
|
128
|
+
baseUrl: resolvedBaseUrl.value,
|
|
129
|
+
baseUrlSource: resolvedBaseUrl.source,
|
|
130
|
+
organization: serializeOrganization(resolvedOrganization.organization),
|
|
131
|
+
organizationSource: resolvedOrganization.source,
|
|
132
|
+
tokenSource: resolvedToken.source,
|
|
133
|
+
user: {
|
|
134
|
+
email: profile.email,
|
|
135
|
+
id: profile.id,
|
|
136
|
+
name: profile.name,
|
|
137
|
+
},
|
|
138
|
+
warnings: resolvedOrganization.warnings,
|
|
139
|
+
}, (result) => `Authenticated as ${formatUserIdentity(result.user)}.\n${formatOrganizationSummary(result.organization)}`);
|
|
140
|
+
})
|
|
141
|
+
.command('logout', 'Remove the locally stored CLI token', () => { }, async () => {
|
|
142
|
+
const config = await context.configStore.load();
|
|
143
|
+
const hadStoredToken = Boolean(config.token);
|
|
144
|
+
await context.configStore.update((currentConfig) => ({
|
|
145
|
+
...currentConfig,
|
|
146
|
+
token: undefined,
|
|
147
|
+
}));
|
|
148
|
+
context.output.print({
|
|
149
|
+
clearedStoredToken: hadStoredToken,
|
|
150
|
+
loggedOut: true,
|
|
151
|
+
}, hadStoredToken ? 'Stored CLI token removed.' : 'No stored CLI token was present.');
|
|
152
|
+
})
|
|
153
|
+
.demandCommand(1)
|
|
154
|
+
.strict();
|
|
155
|
+
};
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
import { createApiClient } from '../lib/api-client.js';
|
|
2
|
+
import { CliCommandError } from '../lib/cli-error.js';
|
|
3
|
+
import { mapApiErrorToCliError, requireResolvedBaseUrl, requireResolvedToken, resolveRequestedOrganizationId, } from '../lib/session.js';
|
|
4
|
+
import { formatCliJobSummary, getWaitExitCode, serializeCliJob, throwSilentExitCode, waitForCliJobs, } from '../lib/user-jobs.js';
|
|
5
|
+
const normalizeTimeoutMs = (timeoutMs) => {
|
|
6
|
+
const normalizedTimeoutMs = timeoutMs ?? 300000;
|
|
7
|
+
if (!Number.isFinite(normalizedTimeoutMs) || normalizedTimeoutMs <= 0) {
|
|
8
|
+
throw new CliCommandError('--timeout-ms must be a positive number.', 2);
|
|
9
|
+
}
|
|
10
|
+
return normalizedTimeoutMs;
|
|
11
|
+
};
|
|
12
|
+
export const registerJobCommands = (jobYargs, context) => {
|
|
13
|
+
return jobYargs
|
|
14
|
+
.command('get <jobId>', 'Fetch a single job', (getYargs) => getYargs.positional('jobId', {
|
|
15
|
+
type: 'string',
|
|
16
|
+
describe: 'Job ID to fetch',
|
|
17
|
+
}), async (argv) => {
|
|
18
|
+
const config = await context.configStore.load();
|
|
19
|
+
const resolvedBaseUrl = requireResolvedBaseUrl({
|
|
20
|
+
flagValue: argv.baseUrl,
|
|
21
|
+
env: context.env,
|
|
22
|
+
config,
|
|
23
|
+
});
|
|
24
|
+
const resolvedToken = requireResolvedToken({
|
|
25
|
+
env: context.env,
|
|
26
|
+
config,
|
|
27
|
+
});
|
|
28
|
+
const organizationId = resolveRequestedOrganizationId({
|
|
29
|
+
flagValue: argv.organization,
|
|
30
|
+
env: context.env,
|
|
31
|
+
config,
|
|
32
|
+
});
|
|
33
|
+
const apiClient = createApiClient({
|
|
34
|
+
baseUrl: resolvedBaseUrl.value,
|
|
35
|
+
token: resolvedToken.value,
|
|
36
|
+
organizationId,
|
|
37
|
+
fetchImpl: context.fetchImpl,
|
|
38
|
+
});
|
|
39
|
+
let job;
|
|
40
|
+
try {
|
|
41
|
+
job = await apiClient.getUserJob(String(argv.jobId));
|
|
42
|
+
}
|
|
43
|
+
catch (error) {
|
|
44
|
+
throw mapApiErrorToCliError(error);
|
|
45
|
+
}
|
|
46
|
+
const serializedJob = serializeCliJob(job);
|
|
47
|
+
context.output.print({ job: serializedJob }, (result) => formatCliJobSummary(result.job));
|
|
48
|
+
})
|
|
49
|
+
.command('wait <jobIds..>', 'Poll jobs until they complete, fail, or time out', (waitYargs) => waitYargs
|
|
50
|
+
.positional('jobIds', {
|
|
51
|
+
array: true,
|
|
52
|
+
type: 'string',
|
|
53
|
+
describe: 'One or more job IDs to wait for',
|
|
54
|
+
})
|
|
55
|
+
.option('timeout-ms', {
|
|
56
|
+
type: 'number',
|
|
57
|
+
default: 300000,
|
|
58
|
+
describe: 'Maximum time to wait before timing out',
|
|
59
|
+
}), async (argv) => {
|
|
60
|
+
const config = await context.configStore.load();
|
|
61
|
+
const resolvedBaseUrl = requireResolvedBaseUrl({
|
|
62
|
+
flagValue: argv.baseUrl,
|
|
63
|
+
env: context.env,
|
|
64
|
+
config,
|
|
65
|
+
});
|
|
66
|
+
const resolvedToken = requireResolvedToken({
|
|
67
|
+
env: context.env,
|
|
68
|
+
config,
|
|
69
|
+
});
|
|
70
|
+
const organizationId = resolveRequestedOrganizationId({
|
|
71
|
+
flagValue: argv.organization,
|
|
72
|
+
env: context.env,
|
|
73
|
+
config,
|
|
74
|
+
});
|
|
75
|
+
const apiClient = createApiClient({
|
|
76
|
+
baseUrl: resolvedBaseUrl.value,
|
|
77
|
+
token: resolvedToken.value,
|
|
78
|
+
organizationId,
|
|
79
|
+
fetchImpl: context.fetchImpl,
|
|
80
|
+
});
|
|
81
|
+
const timeoutMs = normalizeTimeoutMs(argv.timeoutMs);
|
|
82
|
+
const waitResult = await waitForCliJobs({
|
|
83
|
+
getUserJob: async (jobId) => await apiClient.getUserJob(jobId),
|
|
84
|
+
jobIds: (argv.jobIds ?? []).map(String),
|
|
85
|
+
now: context.now,
|
|
86
|
+
sleep: context.sleep,
|
|
87
|
+
timeoutMs,
|
|
88
|
+
});
|
|
89
|
+
context.output.print(waitResult, (result) => result.jobs.map((job) => formatCliJobSummary(job)).join('\n'));
|
|
90
|
+
throwSilentExitCode(getWaitExitCode(waitResult));
|
|
91
|
+
})
|
|
92
|
+
.demandCommand(1)
|
|
93
|
+
.strict();
|
|
94
|
+
};
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
import { createApiClient } from '../lib/api-client.js';
|
|
2
|
+
import { CliCommandError } from '../lib/cli-error.js';
|
|
3
|
+
import { resolveOrganization } from '../lib/config-store.js';
|
|
4
|
+
import { mapApiErrorToCliError, requireResolvedBaseUrl, requireResolvedToken } from '../lib/session.js';
|
|
5
|
+
const serializeOrganization = ({ organizationId, organizationName, organizationType, role, selected, }) => {
|
|
6
|
+
return {
|
|
7
|
+
id: organizationId,
|
|
8
|
+
name: organizationName,
|
|
9
|
+
role,
|
|
10
|
+
selected,
|
|
11
|
+
type: organizationType,
|
|
12
|
+
};
|
|
13
|
+
};
|
|
14
|
+
export const registerOrgCommands = (orgYargs, context) => {
|
|
15
|
+
return orgYargs
|
|
16
|
+
.command('list', 'List organizations available to the authenticated user', () => { }, async (argv) => {
|
|
17
|
+
const config = await context.configStore.load();
|
|
18
|
+
const resolvedBaseUrl = requireResolvedBaseUrl({
|
|
19
|
+
flagValue: argv.baseUrl,
|
|
20
|
+
env: context.env,
|
|
21
|
+
config,
|
|
22
|
+
});
|
|
23
|
+
const resolvedToken = requireResolvedToken({
|
|
24
|
+
env: context.env,
|
|
25
|
+
config,
|
|
26
|
+
});
|
|
27
|
+
const apiClient = createApiClient({
|
|
28
|
+
baseUrl: resolvedBaseUrl.value,
|
|
29
|
+
token: resolvedToken.value,
|
|
30
|
+
fetchImpl: context.fetchImpl,
|
|
31
|
+
});
|
|
32
|
+
let profile;
|
|
33
|
+
try {
|
|
34
|
+
profile = await apiClient.getUserProfile();
|
|
35
|
+
}
|
|
36
|
+
catch (error) {
|
|
37
|
+
throw mapApiErrorToCliError(error);
|
|
38
|
+
}
|
|
39
|
+
const resolvedOrganization = resolveOrganization({
|
|
40
|
+
flagValue: argv.organization,
|
|
41
|
+
env: context.env,
|
|
42
|
+
config,
|
|
43
|
+
profile,
|
|
44
|
+
});
|
|
45
|
+
resolvedOrganization.warnings.forEach((warning) => {
|
|
46
|
+
context.output.info(warning);
|
|
47
|
+
});
|
|
48
|
+
const organizations = profile.organizationMemberships.map((membership) => serializeOrganization({
|
|
49
|
+
...membership,
|
|
50
|
+
selected: membership.organizationId === resolvedOrganization.organization?.organizationId,
|
|
51
|
+
}));
|
|
52
|
+
context.output.print({
|
|
53
|
+
organizations,
|
|
54
|
+
selectedOrganizationId: resolvedOrganization.organization?.organizationId ?? null,
|
|
55
|
+
selectedOrganizationSource: resolvedOrganization.source,
|
|
56
|
+
warnings: resolvedOrganization.warnings,
|
|
57
|
+
}, (result) => result.organizations
|
|
58
|
+
.map((organization) => `${organization.selected ? '*' : ' '} ${organization.name} (${organization.id}) [${organization.type}, ${organization.role}]`)
|
|
59
|
+
.join('\n'));
|
|
60
|
+
})
|
|
61
|
+
.command('use <organizationId>', 'Set the default organization used by the CLI', (useYargs) => {
|
|
62
|
+
return useYargs.positional('organizationId', {
|
|
63
|
+
type: 'string',
|
|
64
|
+
describe: 'Organization ID to store as the default',
|
|
65
|
+
});
|
|
66
|
+
}, async (argv) => {
|
|
67
|
+
const config = await context.configStore.load();
|
|
68
|
+
const resolvedBaseUrl = requireResolvedBaseUrl({
|
|
69
|
+
flagValue: argv.baseUrl,
|
|
70
|
+
env: context.env,
|
|
71
|
+
config,
|
|
72
|
+
});
|
|
73
|
+
const resolvedToken = requireResolvedToken({
|
|
74
|
+
env: context.env,
|
|
75
|
+
config,
|
|
76
|
+
});
|
|
77
|
+
const apiClient = createApiClient({
|
|
78
|
+
baseUrl: resolvedBaseUrl.value,
|
|
79
|
+
token: resolvedToken.value,
|
|
80
|
+
fetchImpl: context.fetchImpl,
|
|
81
|
+
});
|
|
82
|
+
let profile;
|
|
83
|
+
try {
|
|
84
|
+
profile = await apiClient.getUserProfile();
|
|
85
|
+
}
|
|
86
|
+
catch (error) {
|
|
87
|
+
throw mapApiErrorToCliError(error);
|
|
88
|
+
}
|
|
89
|
+
const organization = profile.organizationMemberships.find((membership) => membership.organizationId === String(argv.organizationId));
|
|
90
|
+
if (!organization) {
|
|
91
|
+
throw new CliCommandError(`Organization "${String(argv.organizationId)}" is not accessible to the current user.`, 2);
|
|
92
|
+
}
|
|
93
|
+
await context.configStore.update((currentConfig) => ({
|
|
94
|
+
...currentConfig,
|
|
95
|
+
organizationId: organization.organizationId,
|
|
96
|
+
}));
|
|
97
|
+
context.output.print({
|
|
98
|
+
organization: serializeOrganization({
|
|
99
|
+
...organization,
|
|
100
|
+
selected: true,
|
|
101
|
+
}),
|
|
102
|
+
}, (result) => `Default organization set to ${result.organization.name} (${result.organization.id}).`);
|
|
103
|
+
})
|
|
104
|
+
.demandCommand(1)
|
|
105
|
+
.strict();
|
|
106
|
+
};
|