@chalksurf/cli 0.1.0 → 0.2.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 +19 -85
- package/dist/bin/chalksurf.js +77 -10
- package/dist/commands/auth.js +31 -19
- package/dist/commands/exercise.js +428 -0
- package/dist/commands/job.js +18 -8
- package/dist/commands/org.js +13 -8
- package/dist/commands/sheet.js +286 -129
- package/dist/lib/api-client.js +9 -3
- package/dist/lib/cli-error.js +37 -1
- package/dist/lib/import-files.js +120 -0
- package/dist/lib/import-output.js +67 -0
- package/dist/lib/manifest.js +183 -19
- package/dist/lib/output.js +35 -2
- package/dist/lib/prompt-secret.js +32 -0
- package/dist/lib/session.js +1 -1
- package/dist/lib/source-resolver.js +72 -10
- package/dist/lib/translation-languages.js +1 -0
- package/dist/lib/user-jobs.js +16 -1
- package/docs/agents.md +195 -0
- package/docs/examples/exercise-import-manifest.json +13 -0
- package/docs/examples/exercise-solution-import-manifest.json +13 -0
- package/docs/examples/sheet-import-manifest.json +33 -0
- package/docs/exit-codes.md +57 -0
- package/docs/manifest.md +129 -0
- package/docs/manual.md +152 -0
- package/package.json +8 -4
- package/schemas/exercise-import-manifest.schema.json +104 -0
- package/schemas/exercise-solution-import-manifest.schema.json +104 -0
- package/schemas/sheet-import-manifest.schema.json +142 -0
package/README.md
CHANGED
|
@@ -1,28 +1,9 @@
|
|
|
1
1
|
# `@chalksurf/cli`
|
|
2
2
|
|
|
3
|
-
Publishable
|
|
3
|
+
Publishable ChalkSurf CLI package.
|
|
4
4
|
|
|
5
5
|
Current internal release line: `0.1.0`. Expect breaking changes while the CLI is still only used internally.
|
|
6
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
7
|
## Installation
|
|
27
8
|
|
|
28
9
|
Run the published CLI without a global install:
|
|
@@ -38,98 +19,51 @@ npm install -g @chalksurf/cli@0.1.0
|
|
|
38
19
|
chalksurf --version
|
|
39
20
|
```
|
|
40
21
|
|
|
41
|
-
##
|
|
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
|
-
```
|
|
22
|
+
## Quick Start
|
|
48
23
|
|
|
49
|
-
|
|
24
|
+
Interactive operator flow:
|
|
50
25
|
|
|
51
26
|
```bash
|
|
52
|
-
chalksurf auth
|
|
27
|
+
chalksurf auth login --base-url https://api.chalksurf.com
|
|
53
28
|
chalksurf org list
|
|
29
|
+
chalksurf sheet import ./fixtures/algebra.pdf --wait
|
|
54
30
|
```
|
|
55
31
|
|
|
56
|
-
|
|
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:
|
|
32
|
+
Headless or agent flow:
|
|
72
33
|
|
|
73
34
|
```bash
|
|
74
35
|
CHALKSURF_BASE_URL=https://api.chalksurf.com \
|
|
75
36
|
CHALKSURF_TOKEN=cs_cli_... \
|
|
76
37
|
CHALKSURF_ORGANIZATION_ID=org_123 \
|
|
77
|
-
npx @chalksurf/cli@0.1.0
|
|
38
|
+
npx @chalksurf/cli@0.1.0 sheet import --manifest - --wait --json < import.json
|
|
78
39
|
```
|
|
79
40
|
|
|
80
|
-
|
|
41
|
+
Sheet import manifests use top-level `sheets[]`, where each sheet has one `targetFolderPath` and one or more ordered `sources[]`. See [the canonical example](./docs/examples/sheet-import-manifest.json).
|
|
81
42
|
|
|
82
|
-
|
|
83
|
-
chalksurf sheet import ./fixtures/algebra.pdf
|
|
84
|
-
```
|
|
43
|
+
## Docs
|
|
85
44
|
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
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
|
-
```
|
|
45
|
+
- [Manual operator guide](./docs/manual.md)
|
|
46
|
+
- [Agent and Codex guide](./docs/agents.md)
|
|
47
|
+
- [Manifest reference](./docs/manifest.md)
|
|
48
|
+
- [Exit codes and JSON errors](./docs/exit-codes.md)
|
|
49
|
+
- [Sheet import schema](./schemas/sheet-import-manifest.schema.json)
|
|
50
|
+
- [Exercise import schema](./schemas/exercise-import-manifest.schema.json)
|
|
51
|
+
- [Exercise solution import schema](./schemas/exercise-solution-import-manifest.schema.json)
|
|
106
52
|
|
|
107
53
|
## Local And Staging Testing
|
|
108
54
|
|
|
109
55
|
Use separate config files per environment so local and staging tokens do not overwrite each other:
|
|
110
56
|
|
|
111
|
-
```bash
|
|
112
|
-
npm run cli-dev -- --help
|
|
113
|
-
```
|
|
114
|
-
|
|
115
|
-
Run the CLI directly from source during local development:
|
|
116
|
-
|
|
117
57
|
```bash
|
|
118
58
|
export CHALKSURF_CONFIG_PATH=/tmp/chalksurf-local.json
|
|
119
|
-
|
|
59
|
+
npm run cli-dev -- auth login --base-url http://localhost:8080
|
|
120
60
|
npm run cli-dev -- auth status --json
|
|
121
|
-
npm run cli-dev -- org list --json
|
|
122
61
|
npm run cli-dev -- sheet import ./fixtures/algebra.pdf --wait --json
|
|
123
|
-
npm run cli-dev -- job get job_123 --json
|
|
124
62
|
```
|
|
125
63
|
|
|
126
64
|
```bash
|
|
127
65
|
export CHALKSURF_CONFIG_PATH=/tmp/chalksurf-staging.json
|
|
128
|
-
|
|
66
|
+
npm run cli-dev -- auth login --base-url https://staging-api.chalksurf.com
|
|
129
67
|
npm run cli-dev -- auth status --json
|
|
130
|
-
npm run cli-dev --
|
|
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
|
|
68
|
+
npm run cli-dev -- sheet import https://example.com/worksheet.docx --single-sheet --target-folder Imported --json
|
|
133
69
|
```
|
|
134
|
-
|
|
135
|
-
For short-lived agent runs, skip local persistence and inject credentials through environment variables instead.
|
package/dist/bin/chalksurf.js
CHANGED
|
@@ -4,21 +4,23 @@ import { fileURLToPath } from 'node:url';
|
|
|
4
4
|
import { hideBin } from 'yargs/helpers';
|
|
5
5
|
import yargs from 'yargs/yargs';
|
|
6
6
|
import { registerAuthCommands } from '../commands/auth.js';
|
|
7
|
+
import { registerExerciseCommands } from '../commands/exercise.js';
|
|
7
8
|
import { registerJobCommands } from '../commands/job.js';
|
|
8
9
|
import { registerOrgCommands } from '../commands/org.js';
|
|
9
10
|
import { registerSheetCommands } from '../commands/sheet.js';
|
|
10
|
-
import { CliCommandError } from '../lib/cli-error.js';
|
|
11
|
+
import { CliCommandError, createSerializableCliError, serializeCliError } from '../lib/cli-error.js';
|
|
11
12
|
import { createConfigStore } from '../lib/config-store.js';
|
|
12
13
|
import { createOutput } from '../lib/output.js';
|
|
14
|
+
import { createPromptSecret } from '../lib/prompt-secret.js';
|
|
13
15
|
const packageJson = JSON.parse(readFileSync(new URL('../../package.json', import.meta.url), 'utf8'));
|
|
14
|
-
const createCli = ({ cwd, configPath, env,
|
|
16
|
+
const createCli = ({ cwd, configPath, env, now, output, sleep, stdin, promptSecret, }) => {
|
|
15
17
|
const commandContext = {
|
|
16
18
|
cwd,
|
|
17
19
|
configStore: createConfigStore({ configPath, env }),
|
|
18
20
|
env,
|
|
19
|
-
fetchImpl,
|
|
20
21
|
now,
|
|
21
22
|
output,
|
|
23
|
+
promptSecret,
|
|
22
24
|
sleep,
|
|
23
25
|
stdin,
|
|
24
26
|
};
|
|
@@ -48,37 +50,83 @@ const createCli = ({ cwd, configPath, env, fetchImpl, now, output, sleep, stdin,
|
|
|
48
50
|
})
|
|
49
51
|
.command('auth <subcommand>', 'Authentication commands', (authYargs) => registerAuthCommands(authYargs, commandContext), () => { })
|
|
50
52
|
.command('org <subcommand>', 'Organization commands', (orgYargs) => registerOrgCommands(orgYargs, commandContext), () => { })
|
|
53
|
+
.command('exercise <subcommand>', 'Exercise commands', (exerciseYargs) => registerExerciseCommands(exerciseYargs, commandContext), () => { })
|
|
51
54
|
.command('sheet <subcommand>', 'Exercise sheet commands', (sheetYargs) => registerSheetCommands(sheetYargs, commandContext), () => { })
|
|
52
55
|
.command('job <subcommand>', 'Job commands', (jobYargs) => registerJobCommands(jobYargs, commandContext), () => { })
|
|
53
56
|
.help()
|
|
54
57
|
.strict()
|
|
55
58
|
.exitProcess(false);
|
|
56
59
|
};
|
|
60
|
+
const parseCliWithCapturedOutput = async ({ cli, argv, stdout, }) => {
|
|
61
|
+
await new Promise((resolve, reject) => {
|
|
62
|
+
cli.parse(argv, (error, _parsedArgv, output) => {
|
|
63
|
+
if (output) {
|
|
64
|
+
stdout.write(output);
|
|
65
|
+
}
|
|
66
|
+
if (error) {
|
|
67
|
+
reject(error);
|
|
68
|
+
return;
|
|
69
|
+
}
|
|
70
|
+
resolve();
|
|
71
|
+
});
|
|
72
|
+
});
|
|
73
|
+
};
|
|
57
74
|
const hasFlag = (argv, flags) => {
|
|
58
75
|
return argv.some((argument) => flags.includes(argument));
|
|
59
76
|
};
|
|
60
|
-
|
|
77
|
+
const topLevelCommands = new Set(['auth', 'org', 'exercise', 'sheet', 'job']);
|
|
78
|
+
const hasTopLevelCommand = (argv) => {
|
|
79
|
+
return argv.some((argument) => topLevelCommands.has(argument));
|
|
80
|
+
};
|
|
81
|
+
const resolveCommandName = (argv) => {
|
|
82
|
+
for (let index = 0; index < argv.length; index += 1) {
|
|
83
|
+
const argument = argv[index];
|
|
84
|
+
if (!topLevelCommands.has(argument)) {
|
|
85
|
+
continue;
|
|
86
|
+
}
|
|
87
|
+
const subcommand = argv
|
|
88
|
+
.slice(index + 1)
|
|
89
|
+
.find((candidate) => candidate.length > 0 && !candidate.startsWith('-') && !candidate.startsWith('http'));
|
|
90
|
+
return subcommand ? `${argument} ${subcommand}` : argument;
|
|
91
|
+
}
|
|
92
|
+
if (hasFlag(argv, ['--version', '-v'])) {
|
|
93
|
+
return 'chalksurf version';
|
|
94
|
+
}
|
|
95
|
+
return 'chalksurf';
|
|
96
|
+
};
|
|
97
|
+
export const runCli = async ({ argv, cwd = process.cwd(), configPath, env = process.env, nowImpl = Date.now, promptSecretImpl = createPromptSecret(), sleepImpl = (milliseconds) => new Promise((resolve) => setTimeout(resolve, milliseconds)), stdin = process.stdin, stdout = process.stdout, stderr = process.stderr, }) => {
|
|
61
98
|
const output = createOutput({
|
|
62
99
|
json: hasFlag(argv, ['--json']),
|
|
63
100
|
stdout,
|
|
64
101
|
stderr,
|
|
65
102
|
});
|
|
103
|
+
const commandName = resolveCommandName(argv);
|
|
66
104
|
const cli = createCli({
|
|
67
105
|
cwd,
|
|
68
106
|
configPath,
|
|
69
107
|
env,
|
|
70
|
-
fetchImpl,
|
|
71
108
|
now: nowImpl,
|
|
72
109
|
output,
|
|
110
|
+
promptSecret: promptSecretImpl,
|
|
73
111
|
sleep: sleepImpl,
|
|
74
112
|
stdin,
|
|
75
113
|
});
|
|
76
|
-
if (argv.length === 0 || hasFlag(argv, ['--help', '-h'])) {
|
|
114
|
+
if (argv.length === 0 || (hasFlag(argv, ['--help', '-h']) && !hasTopLevelCommand(argv))) {
|
|
77
115
|
stdout.write(await cli.getHelp());
|
|
78
116
|
return 0;
|
|
79
117
|
}
|
|
80
|
-
if (hasFlag(argv, ['--
|
|
81
|
-
|
|
118
|
+
if (hasFlag(argv, ['--help', '-h'])) {
|
|
119
|
+
await parseCliWithCapturedOutput({
|
|
120
|
+
cli,
|
|
121
|
+
argv,
|
|
122
|
+
stdout,
|
|
123
|
+
});
|
|
124
|
+
return 0;
|
|
125
|
+
}
|
|
126
|
+
if (hasFlag(argv, ['--version', '-v']) && !hasTopLevelCommand(argv)) {
|
|
127
|
+
output.print({ version: packageJson.version }, packageJson.version, {
|
|
128
|
+
command: commandName,
|
|
129
|
+
});
|
|
82
130
|
return 0;
|
|
83
131
|
}
|
|
84
132
|
try {
|
|
@@ -88,12 +136,31 @@ export const runCli = async ({ argv, cwd = process.cwd(), configPath, env = proc
|
|
|
88
136
|
catch (error) {
|
|
89
137
|
if (error instanceof CliCommandError) {
|
|
90
138
|
if (error.shouldReport && error.message) {
|
|
91
|
-
output.
|
|
139
|
+
if (output.json) {
|
|
140
|
+
output.printError({
|
|
141
|
+
command: commandName,
|
|
142
|
+
error: serializeCliError(error),
|
|
143
|
+
});
|
|
144
|
+
}
|
|
145
|
+
else {
|
|
146
|
+
output.error(error.message);
|
|
147
|
+
}
|
|
92
148
|
}
|
|
93
149
|
return error.exitCode;
|
|
94
150
|
}
|
|
95
151
|
const message = error instanceof Error ? error.message : 'Unknown CLI error';
|
|
96
|
-
output.
|
|
152
|
+
if (output.json) {
|
|
153
|
+
output.printError({
|
|
154
|
+
command: commandName,
|
|
155
|
+
error: createSerializableCliError({
|
|
156
|
+
exitCode: 1,
|
|
157
|
+
message,
|
|
158
|
+
}),
|
|
159
|
+
});
|
|
160
|
+
}
|
|
161
|
+
else {
|
|
162
|
+
output.error(message);
|
|
163
|
+
}
|
|
97
164
|
return 1;
|
|
98
165
|
}
|
|
99
166
|
};
|
package/dist/commands/auth.js
CHANGED
|
@@ -31,12 +31,15 @@ const formatOrganizationSummary = (organization) => {
|
|
|
31
31
|
};
|
|
32
32
|
export const registerAuthCommands = (authYargs, context) => {
|
|
33
33
|
return authYargs
|
|
34
|
-
.command('login', 'Read a CLI token from stdin
|
|
35
|
-
return loginYargs
|
|
34
|
+
.command('login', 'Read a CLI token from stdin or prompt for it interactively', (loginYargs) => {
|
|
35
|
+
return loginYargs
|
|
36
|
+
.option('with-token', {
|
|
36
37
|
type: 'boolean',
|
|
37
|
-
|
|
38
|
-
describe: 'Read the token from stdin',
|
|
39
|
-
})
|
|
38
|
+
default: false,
|
|
39
|
+
describe: 'Read the token from stdin when piping input',
|
|
40
|
+
})
|
|
41
|
+
.example('chalksurf auth login --base-url https://api.chalksurf.com', 'Prompt for a CLI token and store it locally')
|
|
42
|
+
.example(`printf '%s' "$CHALKSURF_TOKEN" | chalksurf auth login --base-url https://api.chalksurf.com`, 'Read a CLI token from stdin instead of prompting');
|
|
40
43
|
}, async (argv) => {
|
|
41
44
|
const config = await context.configStore.load();
|
|
42
45
|
const resolvedBaseUrl = requireResolvedBaseUrl({
|
|
@@ -44,17 +47,15 @@ export const registerAuthCommands = (authYargs, context) => {
|
|
|
44
47
|
env: context.env,
|
|
45
48
|
config,
|
|
46
49
|
});
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
const token = await readTokenFromStdin(context.stdin);
|
|
50
|
+
const token = (isInteractiveStdin(context.stdin)
|
|
51
|
+
? await context.promptSecret('CLI token: ')
|
|
52
|
+
: await readTokenFromStdin(context.stdin)).trim();
|
|
51
53
|
if (!token) {
|
|
52
|
-
throw new CliCommandError('No token was provided
|
|
54
|
+
throw new CliCommandError('No token was provided.', 2);
|
|
53
55
|
}
|
|
54
56
|
const apiClient = createApiClient({
|
|
55
57
|
baseUrl: resolvedBaseUrl.value,
|
|
56
58
|
token,
|
|
57
|
-
fetchImpl: context.fetchImpl,
|
|
58
59
|
});
|
|
59
60
|
let profile;
|
|
60
61
|
try {
|
|
@@ -88,10 +89,19 @@ export const registerAuthCommands = (authYargs, context) => {
|
|
|
88
89
|
id: profile.id,
|
|
89
90
|
name: profile.name,
|
|
90
91
|
},
|
|
91
|
-
|
|
92
|
-
|
|
92
|
+
}, (result) => `Logged in as ${formatUserIdentity(result.user)}.\n${formatOrganizationSummary(result.organization)}`, {
|
|
93
|
+
command: 'auth login',
|
|
94
|
+
});
|
|
93
95
|
})
|
|
94
|
-
.command('status', 'Show the current authentication and organization state', () =>
|
|
96
|
+
.command('status', 'Show the current authentication and organization state', (statusYargs) => statusYargs
|
|
97
|
+
.example('chalksurf auth status', 'Show the current authenticated user and default organization')
|
|
98
|
+
.example('CHALKSURF_BASE_URL=https://api.chalksurf.com CHALKSURF_TOKEN=cs_cli_... chalksurf auth status --json', 'Inspect the active session in machine-readable mode')
|
|
99
|
+
.epilogue([
|
|
100
|
+
'Resolution order:',
|
|
101
|
+
' base URL: --base-url, then CHALKSURF_BASE_URL, then stored config',
|
|
102
|
+
' token: CHALKSURF_TOKEN, then stored config',
|
|
103
|
+
' organization: --organization, then CHALKSURF_ORGANIZATION_ID, then stored config',
|
|
104
|
+
].join('\n')), async (argv) => {
|
|
95
105
|
const config = await context.configStore.load();
|
|
96
106
|
const resolvedBaseUrl = requireResolvedBaseUrl({
|
|
97
107
|
flagValue: argv.baseUrl,
|
|
@@ -105,7 +115,6 @@ export const registerAuthCommands = (authYargs, context) => {
|
|
|
105
115
|
const apiClient = createApiClient({
|
|
106
116
|
baseUrl: resolvedBaseUrl.value,
|
|
107
117
|
token: resolvedToken.value,
|
|
108
|
-
fetchImpl: context.fetchImpl,
|
|
109
118
|
});
|
|
110
119
|
let profile;
|
|
111
120
|
try {
|
|
@@ -135,10 +144,11 @@ export const registerAuthCommands = (authYargs, context) => {
|
|
|
135
144
|
id: profile.id,
|
|
136
145
|
name: profile.name,
|
|
137
146
|
},
|
|
138
|
-
|
|
139
|
-
|
|
147
|
+
}, (result) => `Authenticated as ${formatUserIdentity(result.user)}.\n${formatOrganizationSummary(result.organization)}`, {
|
|
148
|
+
command: 'auth status',
|
|
149
|
+
});
|
|
140
150
|
})
|
|
141
|
-
.command('logout', 'Remove the locally stored CLI token', () =>
|
|
151
|
+
.command('logout', 'Remove the locally stored CLI token', (logoutYargs) => logoutYargs.example('chalksurf auth logout', 'Remove the stored CLI token from local config'), async () => {
|
|
142
152
|
const config = await context.configStore.load();
|
|
143
153
|
const hadStoredToken = Boolean(config.token);
|
|
144
154
|
await context.configStore.update((currentConfig) => ({
|
|
@@ -148,7 +158,9 @@ export const registerAuthCommands = (authYargs, context) => {
|
|
|
148
158
|
context.output.print({
|
|
149
159
|
clearedStoredToken: hadStoredToken,
|
|
150
160
|
loggedOut: true,
|
|
151
|
-
}, hadStoredToken ? 'Stored CLI token removed.' : 'No stored CLI token was present.'
|
|
161
|
+
}, hadStoredToken ? 'Stored CLI token removed.' : 'No stored CLI token was present.', {
|
|
162
|
+
command: 'auth logout',
|
|
163
|
+
});
|
|
152
164
|
})
|
|
153
165
|
.demandCommand(1)
|
|
154
166
|
.strict();
|