@chalksurf/cli 0.2.0 → 0.2.2
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 +26 -22
- package/dist/bin/chalksurf.js +11 -4
- package/dist/commands/auth.js +19 -12
- package/dist/commands/exercise.js +182 -10
- package/dist/commands/job.js +108 -40
- package/dist/commands/org.js +10 -6
- package/dist/commands/profile.js +97 -0
- package/dist/commands/sheet.js +325 -31
- package/dist/lib/api-client.js +11 -2
- package/dist/lib/command-options.js +61 -0
- package/dist/lib/config-store.js +188 -10
- package/dist/lib/import-output.js +14 -0
- package/dist/lib/manifest.js +24 -0
- package/dist/lib/session.js +27 -0
- package/dist/lib/translation-languages.js +15 -0
- package/dist/lib/user-jobs.js +6 -0
- package/docs/agents.md +76 -14
- package/docs/examples/exercise-import-manifest.json +1 -0
- package/docs/examples/exercise-sheet-solution-import-manifest.json +13 -0
- package/docs/manifest.md +25 -3
- package/docs/manual.md +72 -11
- package/package.json +1 -1
- package/schemas/exercise-import-manifest.schema.json +12 -0
- package/schemas/exercise-sheet-solution-import-manifest.schema.json +104 -0
package/README.md
CHANGED
|
@@ -2,20 +2,20 @@
|
|
|
2
2
|
|
|
3
3
|
Publishable ChalkSurf CLI package.
|
|
4
4
|
|
|
5
|
-
Current internal release line: `0.
|
|
5
|
+
Current internal release line: `0.2.2`. Expect breaking changes while the CLI is still only used internally.
|
|
6
6
|
|
|
7
7
|
## Installation
|
|
8
8
|
|
|
9
9
|
Run the published CLI without a global install:
|
|
10
10
|
|
|
11
11
|
```bash
|
|
12
|
-
npx @chalksurf/cli@0.
|
|
12
|
+
npx @chalksurf/cli@0.2.2 --help
|
|
13
13
|
```
|
|
14
14
|
|
|
15
15
|
Install it globally when you want a persistent local binary:
|
|
16
16
|
|
|
17
17
|
```bash
|
|
18
|
-
npm install -g @chalksurf/cli@0.
|
|
18
|
+
npm install -g @chalksurf/cli@0.2.2
|
|
19
19
|
chalksurf --version
|
|
20
20
|
```
|
|
21
21
|
|
|
@@ -24,7 +24,8 @@ chalksurf --version
|
|
|
24
24
|
Interactive operator flow:
|
|
25
25
|
|
|
26
26
|
```bash
|
|
27
|
-
chalksurf auth login --base-url https://api.
|
|
27
|
+
chalksurf auth login --profile prod-cztamas --base-url https://chalksurf-api.fly.dev
|
|
28
|
+
chalksurf profile use prod-cztamas
|
|
28
29
|
chalksurf org list
|
|
29
30
|
chalksurf sheet import ./fixtures/algebra.pdf --wait
|
|
30
31
|
```
|
|
@@ -32,38 +33,41 @@ chalksurf sheet import ./fixtures/algebra.pdf --wait
|
|
|
32
33
|
Headless or agent flow:
|
|
33
34
|
|
|
34
35
|
```bash
|
|
35
|
-
CHALKSURF_BASE_URL=https://api.chalksurf.com \
|
|
36
36
|
CHALKSURF_TOKEN=cs_cli_... \
|
|
37
|
-
|
|
38
|
-
|
|
37
|
+
printf '%s' "$CHALKSURF_TOKEN" | npx @chalksurf/cli@0.2.2 auth login \
|
|
38
|
+
--profile prod-codex \
|
|
39
|
+
--base-url https://chalksurf-api.fly.dev \
|
|
40
|
+
--with-token
|
|
41
|
+
|
|
42
|
+
npx @chalksurf/cli@0.2.2 --profile prod-codex sheet import --manifest - --wait --json < import.json
|
|
39
43
|
```
|
|
40
44
|
|
|
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).
|
|
45
|
+
Sheet import manifests use top-level `sheets[]`, where each sheet has one `targetFolderPath` and one or more ordered `sources[]`. See [the canonical example](./packages/cli/docs/examples/sheet-import-manifest.json).
|
|
42
46
|
|
|
43
47
|
## Docs
|
|
44
48
|
|
|
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)
|
|
49
|
+
- [Manual operator guide](./packages/cli/docs/manual.md)
|
|
50
|
+
- [Agent and Codex guide](./packages/cli/docs/agents.md)
|
|
51
|
+
- [Manifest reference](./packages/cli/docs/manifest.md)
|
|
52
|
+
- [Exit codes and JSON errors](./packages/cli/docs/exit-codes.md)
|
|
53
|
+
- [Sheet import schema](./packages/cli/schemas/sheet-import-manifest.schema.json)
|
|
54
|
+
- [Exercise import schema](./packages/cli/schemas/exercise-import-manifest.schema.json)
|
|
55
|
+
- [Exercise solution import schema](./packages/cli/schemas/exercise-solution-import-manifest.schema.json)
|
|
56
|
+
- [Exercise sheet solution import schema](./packages/cli/schemas/exercise-sheet-solution-import-manifest.schema.json)
|
|
52
57
|
|
|
53
58
|
## Local And Staging Testing
|
|
54
59
|
|
|
55
|
-
Use separate
|
|
60
|
+
Use separate profiles so local, staging, production, human, and agent tokens do not overwrite each other:
|
|
56
61
|
|
|
57
62
|
```bash
|
|
58
|
-
|
|
59
|
-
npm run cli-dev --
|
|
63
|
+
npm run cli-dev -- auth login --profile dev-cztamas --base-url http://localhost:8080
|
|
64
|
+
npm run cli-dev -- profile use dev-cztamas
|
|
60
65
|
npm run cli-dev -- auth status --json
|
|
61
66
|
npm run cli-dev -- sheet import ./fixtures/algebra.pdf --wait --json
|
|
62
67
|
```
|
|
63
68
|
|
|
64
69
|
```bash
|
|
65
|
-
|
|
66
|
-
npm run cli-dev -- auth
|
|
67
|
-
npm run cli-dev --
|
|
68
|
-
npm run cli-dev -- sheet import https://example.com/worksheet.docx --single-sheet --target-folder Imported --json
|
|
70
|
+
npm run cli-dev -- auth login --profile staging-codex --base-url https://chalksurf-api-staging.fly.dev
|
|
71
|
+
npm run cli-dev -- --profile staging-codex auth status --json
|
|
72
|
+
npm run cli-dev -- --profile staging-codex sheet import https://example.com/worksheet.docx --single-sheet --target-folder Imported --json
|
|
69
73
|
```
|
package/dist/bin/chalksurf.js
CHANGED
|
@@ -7,13 +7,15 @@ import { registerAuthCommands } from '../commands/auth.js';
|
|
|
7
7
|
import { registerExerciseCommands } from '../commands/exercise.js';
|
|
8
8
|
import { registerJobCommands } from '../commands/job.js';
|
|
9
9
|
import { registerOrgCommands } from '../commands/org.js';
|
|
10
|
+
import { registerProfileCommands } from '../commands/profile.js';
|
|
10
11
|
import { registerSheetCommands } from '../commands/sheet.js';
|
|
11
12
|
import { CliCommandError, createSerializableCliError, serializeCliError } from '../lib/cli-error.js';
|
|
12
13
|
import { createConfigStore } from '../lib/config-store.js';
|
|
13
14
|
import { createOutput } from '../lib/output.js';
|
|
14
15
|
import { createPromptSecret } from '../lib/prompt-secret.js';
|
|
15
16
|
const packageJson = JSON.parse(readFileSync(new URL('../../package.json', import.meta.url), 'utf8'));
|
|
16
|
-
const createCli = ({ cwd, configPath, env, now, output, sleep, stdin,
|
|
17
|
+
const createCli = ({ cwd, configPath, env, now, output, sleep, stdin, }) => {
|
|
18
|
+
const promptSecret = createPromptSecret();
|
|
17
19
|
const commandContext = {
|
|
18
20
|
cwd,
|
|
19
21
|
configStore: createConfigStore({ configPath, env }),
|
|
@@ -42,6 +44,11 @@ const createCli = ({ cwd, configPath, env, now, output, sleep, stdin, promptSecr
|
|
|
42
44
|
type: 'string',
|
|
43
45
|
global: true,
|
|
44
46
|
describe: 'Organization ID to use for this command',
|
|
47
|
+
})
|
|
48
|
+
.option('profile', {
|
|
49
|
+
type: 'string',
|
|
50
|
+
global: true,
|
|
51
|
+
describe: 'CLI profile to use for this command',
|
|
45
52
|
})
|
|
46
53
|
.option('version', {
|
|
47
54
|
type: 'boolean',
|
|
@@ -50,6 +57,7 @@ const createCli = ({ cwd, configPath, env, now, output, sleep, stdin, promptSecr
|
|
|
50
57
|
})
|
|
51
58
|
.command('auth <subcommand>', 'Authentication commands', (authYargs) => registerAuthCommands(authYargs, commandContext), () => { })
|
|
52
59
|
.command('org <subcommand>', 'Organization commands', (orgYargs) => registerOrgCommands(orgYargs, commandContext), () => { })
|
|
60
|
+
.command('profile <subcommand>', 'Profile commands', (profileYargs) => registerProfileCommands(profileYargs, commandContext), () => { })
|
|
53
61
|
.command('exercise <subcommand>', 'Exercise commands', (exerciseYargs) => registerExerciseCommands(exerciseYargs, commandContext), () => { })
|
|
54
62
|
.command('sheet <subcommand>', 'Exercise sheet commands', (sheetYargs) => registerSheetCommands(sheetYargs, commandContext), () => { })
|
|
55
63
|
.command('job <subcommand>', 'Job commands', (jobYargs) => registerJobCommands(jobYargs, commandContext), () => { })
|
|
@@ -74,7 +82,7 @@ const parseCliWithCapturedOutput = async ({ cli, argv, stdout, }) => {
|
|
|
74
82
|
const hasFlag = (argv, flags) => {
|
|
75
83
|
return argv.some((argument) => flags.includes(argument));
|
|
76
84
|
};
|
|
77
|
-
const topLevelCommands = new Set(['auth', 'org', 'exercise', 'sheet', 'job']);
|
|
85
|
+
const topLevelCommands = new Set(['auth', 'org', 'profile', 'exercise', 'sheet', 'job']);
|
|
78
86
|
const hasTopLevelCommand = (argv) => {
|
|
79
87
|
return argv.some((argument) => topLevelCommands.has(argument));
|
|
80
88
|
};
|
|
@@ -94,7 +102,7 @@ const resolveCommandName = (argv) => {
|
|
|
94
102
|
}
|
|
95
103
|
return 'chalksurf';
|
|
96
104
|
};
|
|
97
|
-
export const runCli = async ({ argv, cwd = process.cwd(), configPath, env = process.env, nowImpl = Date.now,
|
|
105
|
+
export const runCli = async ({ argv, cwd = process.cwd(), configPath, env = process.env, nowImpl = Date.now, sleepImpl = (milliseconds) => new Promise((resolve) => setTimeout(resolve, milliseconds)), stdin = process.stdin, stdout = process.stdout, stderr = process.stderr, }) => {
|
|
98
106
|
const output = createOutput({
|
|
99
107
|
json: hasFlag(argv, ['--json']),
|
|
100
108
|
stdout,
|
|
@@ -107,7 +115,6 @@ export const runCli = async ({ argv, cwd = process.cwd(), configPath, env = proc
|
|
|
107
115
|
env,
|
|
108
116
|
now: nowImpl,
|
|
109
117
|
output,
|
|
110
|
-
promptSecret: promptSecretImpl,
|
|
111
118
|
sleep: sleepImpl,
|
|
112
119
|
stdin,
|
|
113
120
|
});
|
package/dist/commands/auth.js
CHANGED
|
@@ -38,10 +38,10 @@ export const registerAuthCommands = (authYargs, context) => {
|
|
|
38
38
|
default: false,
|
|
39
39
|
describe: 'Read the token from stdin when piping input',
|
|
40
40
|
})
|
|
41
|
-
.example('chalksurf auth login --base-url https://api.
|
|
42
|
-
.example(`printf '%s' "$CHALKSURF_TOKEN" | chalksurf auth login --base-url https://api.
|
|
41
|
+
.example('chalksurf auth login --profile prod-cztamas --base-url https://chalksurf-api.fly.dev', 'Prompt for a CLI token and store it in the prod-cztamas profile')
|
|
42
|
+
.example(`printf '%s' "$CHALKSURF_TOKEN" | chalksurf auth login --profile prod-codex --base-url https://chalksurf-api.fly.dev`, 'Read a CLI token from stdin into an explicit profile');
|
|
43
43
|
}, async (argv) => {
|
|
44
|
-
const config = await context.configStore.
|
|
44
|
+
const config = await context.configStore.loadProfile({ profileName: argv.profile });
|
|
45
45
|
const resolvedBaseUrl = requireResolvedBaseUrl({
|
|
46
46
|
flagValue: argv.baseUrl,
|
|
47
47
|
env: context.env,
|
|
@@ -70,7 +70,7 @@ export const registerAuthCommands = (authYargs, context) => {
|
|
|
70
70
|
config,
|
|
71
71
|
profile,
|
|
72
72
|
});
|
|
73
|
-
await context.configStore.update((currentConfig) => ({
|
|
73
|
+
await context.configStore.update({ profileName: argv.profile }, (currentConfig) => ({
|
|
74
74
|
...currentConfig,
|
|
75
75
|
token,
|
|
76
76
|
baseUrl: resolvedBaseUrl.value,
|
|
@@ -84,6 +84,8 @@ export const registerAuthCommands = (authYargs, context) => {
|
|
|
84
84
|
baseUrl: resolvedBaseUrl.value,
|
|
85
85
|
baseUrlSource: resolvedBaseUrl.source,
|
|
86
86
|
organization: serializeOrganization(resolvedOrganization.organization),
|
|
87
|
+
profile: config.profileName,
|
|
88
|
+
profileSource: config.profileSource,
|
|
87
89
|
user: {
|
|
88
90
|
email: profile.email,
|
|
89
91
|
id: profile.id,
|
|
@@ -95,14 +97,15 @@ export const registerAuthCommands = (authYargs, context) => {
|
|
|
95
97
|
})
|
|
96
98
|
.command('status', 'Show the current authentication and organization state', (statusYargs) => statusYargs
|
|
97
99
|
.example('chalksurf auth status', 'Show the current authenticated user and default organization')
|
|
98
|
-
.example('
|
|
100
|
+
.example('chalksurf --profile prod-codex auth status --json', 'Inspect a specific profile in machine-readable mode')
|
|
99
101
|
.epilogue([
|
|
100
102
|
'Resolution order:',
|
|
101
|
-
'
|
|
102
|
-
'
|
|
103
|
-
'
|
|
103
|
+
' profile: --profile, then CHALKSURF_PROFILE, then stored default profile',
|
|
104
|
+
' base URL: --base-url, then CHALKSURF_BASE_URL, then active profile',
|
|
105
|
+
' token: CHALKSURF_TOKEN, then active profile',
|
|
106
|
+
' organization: --organization, then CHALKSURF_ORGANIZATION_ID, then active profile',
|
|
104
107
|
].join('\n')), async (argv) => {
|
|
105
|
-
const config = await context.configStore.
|
|
108
|
+
const config = await context.configStore.loadProfile({ profileName: argv.profile });
|
|
106
109
|
const resolvedBaseUrl = requireResolvedBaseUrl({
|
|
107
110
|
flagValue: argv.baseUrl,
|
|
108
111
|
env: context.env,
|
|
@@ -138,6 +141,8 @@ export const registerAuthCommands = (authYargs, context) => {
|
|
|
138
141
|
baseUrlSource: resolvedBaseUrl.source,
|
|
139
142
|
organization: serializeOrganization(resolvedOrganization.organization),
|
|
140
143
|
organizationSource: resolvedOrganization.source,
|
|
144
|
+
profile: config.profileName,
|
|
145
|
+
profileSource: config.profileSource,
|
|
141
146
|
tokenSource: resolvedToken.source,
|
|
142
147
|
user: {
|
|
143
148
|
email: profile.email,
|
|
@@ -148,16 +153,18 @@ export const registerAuthCommands = (authYargs, context) => {
|
|
|
148
153
|
command: 'auth status',
|
|
149
154
|
});
|
|
150
155
|
})
|
|
151
|
-
.command('logout', 'Remove the locally stored CLI token', (logoutYargs) => logoutYargs.example('chalksurf auth logout', 'Remove the stored CLI token from local config'), async () => {
|
|
152
|
-
const config = await context.configStore.
|
|
156
|
+
.command('logout', 'Remove the locally stored CLI token', (logoutYargs) => logoutYargs.example('chalksurf auth logout', 'Remove the stored CLI token from local config'), async (argv) => {
|
|
157
|
+
const config = await context.configStore.loadProfile({ profileName: argv.profile });
|
|
153
158
|
const hadStoredToken = Boolean(config.token);
|
|
154
|
-
await context.configStore.update((currentConfig) => ({
|
|
159
|
+
await context.configStore.update({ profileName: argv.profile }, (currentConfig) => ({
|
|
155
160
|
...currentConfig,
|
|
156
161
|
token: undefined,
|
|
157
162
|
}));
|
|
158
163
|
context.output.print({
|
|
159
164
|
clearedStoredToken: hadStoredToken,
|
|
160
165
|
loggedOut: true,
|
|
166
|
+
profile: config.profileName,
|
|
167
|
+
profileSource: config.profileSource,
|
|
161
168
|
}, hadStoredToken ? 'Stored CLI token removed.' : 'No stored CLI token was present.', {
|
|
162
169
|
command: 'auth logout',
|
|
163
170
|
});
|
|
@@ -1,12 +1,39 @@
|
|
|
1
1
|
import { z } from 'zod';
|
|
2
2
|
import { createApiClient } from '../lib/api-client.js';
|
|
3
3
|
import { CliCommandError } from '../lib/cli-error.js';
|
|
4
|
+
import { normalizeChoiceValues, normalizeLimit, normalizeOffset, normalizeOptionalInteger, normalizeOptionalText, normalizeOwnership, normalizeStringValues, toApiOwnership, } from '../lib/command-options.js';
|
|
4
5
|
import { buildFileImportCommandSources, buildFileImportFormData, normalizeWaitTimeoutMs } from '../lib/import-files.js';
|
|
5
|
-
import { buildCliImportSources, summarizeCliImportJobs, } from '../lib/import-output.js';
|
|
6
|
+
import { buildCliImportSources, formatCliImportTranslationJobs, summarizeCliImportJobs, } from '../lib/import-output.js';
|
|
6
7
|
import { loadExerciseImportManifest, loadExerciseSolutionImportManifest } from '../lib/manifest.js';
|
|
7
|
-
import { mapApiErrorToCliError, requireResolvedBaseUrl, requireResolvedToken, resolveRequestedOrganizationId, } from '../lib/session.js';
|
|
8
|
+
import { createResolvedApiClient, mapApiErrorToCliError, requireResolvedBaseUrl, requireResolvedToken, resolveRequestedOrganizationId, } from '../lib/session.js';
|
|
8
9
|
import { cleanupResolvedSources, resolveSources } from '../lib/source-resolver.js';
|
|
10
|
+
import { resolveRequestedTranslateToLanguages, translationLanguages, } from '../lib/translation-languages.js';
|
|
9
11
|
import { getWaitError, getWaitExitCode, throwSilentExitCode, waitForCliJobs } from '../lib/user-jobs.js';
|
|
12
|
+
const exerciseStatusOptions = ['verified', 'unverified', 'invalid'];
|
|
13
|
+
const getExerciseTextPreview = ({ exercise, language }) => {
|
|
14
|
+
if (language && exercise.text[language]) {
|
|
15
|
+
return exercise.text[language];
|
|
16
|
+
}
|
|
17
|
+
return Object.values(exercise.text)[0] ?? '';
|
|
18
|
+
};
|
|
19
|
+
const formatExerciseSearchOutput = ({ exercises, language, totalCount, }) => {
|
|
20
|
+
if (exercises.length === 0) {
|
|
21
|
+
return 'No exercises found.';
|
|
22
|
+
}
|
|
23
|
+
return [
|
|
24
|
+
`${exercises.length} of ${totalCount} exercise${totalCount === 1 ? '' : 's'} returned.`,
|
|
25
|
+
...exercises.map((exercise) => {
|
|
26
|
+
const preview = getExerciseTextPreview({ exercise, language }).replace(/\s+/g, ' ').trim();
|
|
27
|
+
const labels = exercise.labels.length > 0 ? ` [${exercise.labels.join(', ')}]` : '';
|
|
28
|
+
return `${exercise.id} ${exercise.status}${labels}${preview ? ` ${preview}` : ''}`;
|
|
29
|
+
}),
|
|
30
|
+
].join('\n');
|
|
31
|
+
};
|
|
32
|
+
const normalizeSearchNumberRange = ({ maxDifficulty, minDifficulty, }) => {
|
|
33
|
+
if (minDifficulty !== null && maxDifficulty !== null && minDifficulty > maxDifficulty) {
|
|
34
|
+
throw new CliCommandError('--min-difficulty cannot be greater than --max-difficulty.', 2);
|
|
35
|
+
}
|
|
36
|
+
};
|
|
10
37
|
const resolveRequestedUuid = ({ flagValue, fallbackValue, label, required = false, }) => {
|
|
11
38
|
const resolvedValue = flagValue?.trim() || fallbackValue?.trim();
|
|
12
39
|
if (!resolvedValue) {
|
|
@@ -50,6 +77,7 @@ const buildWaitedExerciseImportJob = ({ jobId, sources, waitedJob, }) => {
|
|
|
50
77
|
error: waitedJob?.error,
|
|
51
78
|
exerciseId: waitedJob?.exerciseId,
|
|
52
79
|
exerciseIds: waitedJob?.exerciseIds,
|
|
80
|
+
translationJobs: waitedJob?.translationJobs,
|
|
53
81
|
};
|
|
54
82
|
};
|
|
55
83
|
const formatQueuedImportOutput = (job) => {
|
|
@@ -66,13 +94,17 @@ const formatQueuedSolutionImportOutput = (job) => {
|
|
|
66
94
|
};
|
|
67
95
|
const formatWaitedImportOutput = (job) => {
|
|
68
96
|
if (job.status === 'completed') {
|
|
97
|
+
const translationJobLines = formatCliImportTranslationJobs(job.translationJobs);
|
|
69
98
|
if (job.exerciseIds && job.exerciseIds.length > 0) {
|
|
70
|
-
return
|
|
99
|
+
return [
|
|
100
|
+
`${job.jobId} completed -> ${job.exerciseIds.length} exercise${job.exerciseIds.length === 1 ? '' : 's'}`,
|
|
101
|
+
...translationJobLines,
|
|
102
|
+
].join('\n');
|
|
71
103
|
}
|
|
72
104
|
if (job.exerciseId) {
|
|
73
|
-
return `${job.jobId} completed -> ${job.exerciseId}
|
|
105
|
+
return [`${job.jobId} completed -> ${job.exerciseId}`, ...translationJobLines].join('\n');
|
|
74
106
|
}
|
|
75
|
-
return `${job.jobId} completed
|
|
107
|
+
return [`${job.jobId} completed`, ...translationJobLines].join('\n');
|
|
76
108
|
}
|
|
77
109
|
if (job.status === 'failed') {
|
|
78
110
|
return `${job.jobId} failed -> ${job.error ?? 'Unknown error'}`;
|
|
@@ -81,6 +113,133 @@ const formatWaitedImportOutput = (job) => {
|
|
|
81
113
|
};
|
|
82
114
|
export const registerExerciseCommands = (exerciseYargs, context) => {
|
|
83
115
|
return exerciseYargs
|
|
116
|
+
.command('search', 'Search exercises visible to the selected organization', (searchYargs) => searchYargs
|
|
117
|
+
.option('text', {
|
|
118
|
+
type: 'string',
|
|
119
|
+
describe: 'Search exercise text. Requires --language.',
|
|
120
|
+
})
|
|
121
|
+
.option('language', {
|
|
122
|
+
type: 'string',
|
|
123
|
+
describe: 'Language to search or preview, such as english or hungarian',
|
|
124
|
+
})
|
|
125
|
+
.option('semantic', {
|
|
126
|
+
type: 'string',
|
|
127
|
+
describe: 'Run semantic search with this query text',
|
|
128
|
+
})
|
|
129
|
+
.option('label', {
|
|
130
|
+
array: true,
|
|
131
|
+
type: 'string',
|
|
132
|
+
describe: 'Require an exercise label or label prefix',
|
|
133
|
+
})
|
|
134
|
+
.option('age', {
|
|
135
|
+
type: 'number',
|
|
136
|
+
describe: 'Filter exercises appropriate for this age',
|
|
137
|
+
})
|
|
138
|
+
.option('min-difficulty', {
|
|
139
|
+
type: 'number',
|
|
140
|
+
describe: 'Minimum difficulty',
|
|
141
|
+
})
|
|
142
|
+
.option('max-difficulty', {
|
|
143
|
+
type: 'number',
|
|
144
|
+
describe: 'Maximum difficulty',
|
|
145
|
+
})
|
|
146
|
+
.option('status', {
|
|
147
|
+
array: true,
|
|
148
|
+
type: 'string',
|
|
149
|
+
describe: `Only include exercises with these statuses (${exerciseStatusOptions.join(', ')})`,
|
|
150
|
+
})
|
|
151
|
+
.option('ownership', {
|
|
152
|
+
type: 'string',
|
|
153
|
+
default: 'own',
|
|
154
|
+
describe: 'Visibility scope: own, public, or all',
|
|
155
|
+
})
|
|
156
|
+
.option('limit', {
|
|
157
|
+
type: 'number',
|
|
158
|
+
default: 20,
|
|
159
|
+
describe: 'Maximum number of exercises to return',
|
|
160
|
+
})
|
|
161
|
+
.option('offset', {
|
|
162
|
+
type: 'number',
|
|
163
|
+
default: 0,
|
|
164
|
+
describe: 'Number of matching exercises to skip',
|
|
165
|
+
})
|
|
166
|
+
.example('chalksurf exercise search --text "binomial theorem" --language english --ownership own --json', 'Verify imported exercises in the selected organization')
|
|
167
|
+
.example('chalksurf --profile prod-codex exercise search --label geometry.triangles --status unverified --json', 'Find agent-created exercises that still need review'), async (argv) => {
|
|
168
|
+
const text = normalizeOptionalText(argv.text);
|
|
169
|
+
const language = normalizeOptionalText(argv.language);
|
|
170
|
+
const semanticSearch = normalizeOptionalText(argv.semantic);
|
|
171
|
+
const ownership = normalizeOwnership(argv.ownership);
|
|
172
|
+
const labels = normalizeStringValues(argv.label);
|
|
173
|
+
const status = normalizeChoiceValues({
|
|
174
|
+
allowedValues: exerciseStatusOptions,
|
|
175
|
+
label: '--status',
|
|
176
|
+
values: argv.status,
|
|
177
|
+
});
|
|
178
|
+
const age = normalizeOptionalInteger({ label: '--age', value: argv.age });
|
|
179
|
+
const minDifficulty = normalizeOptionalInteger({
|
|
180
|
+
label: '--min-difficulty',
|
|
181
|
+
value: argv.minDifficulty,
|
|
182
|
+
});
|
|
183
|
+
const maxDifficulty = normalizeOptionalInteger({
|
|
184
|
+
label: '--max-difficulty',
|
|
185
|
+
value: argv.maxDifficulty,
|
|
186
|
+
});
|
|
187
|
+
normalizeSearchNumberRange({ minDifficulty, maxDifficulty });
|
|
188
|
+
if (text !== null && language === null) {
|
|
189
|
+
throw new CliCommandError('--language is required when --text is provided.', 2);
|
|
190
|
+
}
|
|
191
|
+
const limit = normalizeLimit(argv.limit);
|
|
192
|
+
const offset = normalizeOffset(argv.offset);
|
|
193
|
+
const { apiClient, organizationId } = await createResolvedApiClient({
|
|
194
|
+
context,
|
|
195
|
+
baseUrlFlagValue: argv.baseUrl,
|
|
196
|
+
organizationFlagValue: argv.organization,
|
|
197
|
+
profileName: argv.profile,
|
|
198
|
+
});
|
|
199
|
+
let searchResult;
|
|
200
|
+
try {
|
|
201
|
+
searchResult = await apiClient.searchExercises({
|
|
202
|
+
age,
|
|
203
|
+
language,
|
|
204
|
+
labels,
|
|
205
|
+
minDifficulty,
|
|
206
|
+
maxDifficulty,
|
|
207
|
+
text,
|
|
208
|
+
semanticSearch,
|
|
209
|
+
status,
|
|
210
|
+
ownership: toApiOwnership(ownership),
|
|
211
|
+
limit,
|
|
212
|
+
offset,
|
|
213
|
+
});
|
|
214
|
+
}
|
|
215
|
+
catch (error) {
|
|
216
|
+
throw mapApiErrorToCliError(error);
|
|
217
|
+
}
|
|
218
|
+
context.output.print({
|
|
219
|
+
request: {
|
|
220
|
+
age,
|
|
221
|
+
language,
|
|
222
|
+
labels,
|
|
223
|
+
minDifficulty,
|
|
224
|
+
maxDifficulty,
|
|
225
|
+
text,
|
|
226
|
+
semanticSearch,
|
|
227
|
+
status: status ?? [],
|
|
228
|
+
ownership,
|
|
229
|
+
limit,
|
|
230
|
+
offset,
|
|
231
|
+
organizationId: organizationId ?? null,
|
|
232
|
+
},
|
|
233
|
+
exercises: searchResult.exercises,
|
|
234
|
+
totalCount: searchResult.totalCount,
|
|
235
|
+
}, (result) => formatExerciseSearchOutput({
|
|
236
|
+
exercises: result.exercises,
|
|
237
|
+
language: result.request.language ?? undefined,
|
|
238
|
+
totalCount: result.totalCount,
|
|
239
|
+
}), {
|
|
240
|
+
command: 'exercise search',
|
|
241
|
+
});
|
|
242
|
+
})
|
|
84
243
|
.command('import [sources..]', 'Import one or more exercises', (importYargs) => importYargs
|
|
85
244
|
.positional('sources', {
|
|
86
245
|
array: true,
|
|
@@ -103,6 +262,11 @@ export const registerExerciseCommands = (exerciseYargs, context) => {
|
|
|
103
262
|
type: 'number',
|
|
104
263
|
default: 300000,
|
|
105
264
|
describe: 'Maximum time to wait before timing out',
|
|
265
|
+
})
|
|
266
|
+
.option('translate-to', {
|
|
267
|
+
array: true,
|
|
268
|
+
type: 'string',
|
|
269
|
+
describe: `Generate translated exercise content for these languages (${translationLanguages.join(', ')})`,
|
|
106
270
|
})
|
|
107
271
|
.option('wait', {
|
|
108
272
|
type: 'boolean',
|
|
@@ -110,12 +274,16 @@ export const registerExerciseCommands = (exerciseYargs, context) => {
|
|
|
110
274
|
describe: 'Wait for the queued job to finish',
|
|
111
275
|
})
|
|
112
276
|
.example('chalksurf exercise import ./fixtures/problem-set.pdf --sheet-id 00000000-0000-4000-8000-000000000001 --wait', 'Import a problem set into an existing sheet')
|
|
113
|
-
.example('cat exercise-import.json | chalksurf exercise import --manifest - --wait --json', 'Drive imports from a manifest in an agent or CI flow')
|
|
114
|
-
.epilogue(
|
|
115
|
-
|
|
277
|
+
.example('cat exercise-import.json | chalksurf --profile prod-codex exercise import --manifest - --wait --json', 'Drive imports from a manifest in an agent or CI flow')
|
|
278
|
+
.epilogue([
|
|
279
|
+
'When --wait is set, the command exits after the queued import job reaches a terminal state.',
|
|
280
|
+
'For translated exercise imports, requested translations continue as separate jobs listed in the import result.',
|
|
281
|
+
].join('\n')), async (argv) => {
|
|
282
|
+
const config = await context.configStore.loadProfile({ profileName: argv.profile });
|
|
116
283
|
const rawSources = (argv.sources ?? []).map(String);
|
|
117
284
|
const manifestPath = argv.manifest === '' ? '-' : argv.manifest;
|
|
118
285
|
const normalizedRawSources = manifestPath === '-' && rawSources[0] === '-' ? rawSources.slice(1) : rawSources;
|
|
286
|
+
const requestedTranslateTo = resolveRequestedTranslateToLanguages(argv.translateTo);
|
|
119
287
|
const resolvedBaseUrl = requireResolvedBaseUrl({
|
|
120
288
|
flagValue: argv.baseUrl,
|
|
121
289
|
env: context.env,
|
|
@@ -145,6 +313,7 @@ export const registerExerciseCommands = (exerciseYargs, context) => {
|
|
|
145
313
|
fallbackValue: commandSources.exerciseSheetId,
|
|
146
314
|
label: '--sheet-id',
|
|
147
315
|
});
|
|
316
|
+
const translateToLanguages = requestedTranslateTo ?? commandSources.translateTo;
|
|
148
317
|
const apiClient = createApiClient({
|
|
149
318
|
baseUrl: resolvedBaseUrl.value,
|
|
150
319
|
token: resolvedToken.value,
|
|
@@ -170,6 +339,9 @@ export const registerExerciseCommands = (exerciseYargs, context) => {
|
|
|
170
339
|
if (exerciseSheetId) {
|
|
171
340
|
formData.set('exerciseSheetId', exerciseSheetId);
|
|
172
341
|
}
|
|
342
|
+
if (translateToLanguages && translateToLanguages.length > 0) {
|
|
343
|
+
formData.set('translateToLanguages', JSON.stringify(translateToLanguages));
|
|
344
|
+
}
|
|
173
345
|
let importResult;
|
|
174
346
|
try {
|
|
175
347
|
importResult = await apiClient.importExercise(formData);
|
|
@@ -283,9 +455,9 @@ export const registerExerciseCommands = (exerciseYargs, context) => {
|
|
|
283
455
|
describe: 'Wait for the queued job to finish',
|
|
284
456
|
})
|
|
285
457
|
.example('chalksurf exercise import-solution 00000000-0000-4000-8000-000000000001 ./fixtures/solution.pdf --wait', 'Attach a solution file to an existing exercise')
|
|
286
|
-
.example('cat exercise-solution-import.json | chalksurf exercise import-solution --manifest - --wait --json', 'Drive solution imports from a manifest in an agent or CI flow')
|
|
458
|
+
.example('cat exercise-solution-import.json | chalksurf --profile prod-codex exercise import-solution --manifest - --wait --json', 'Drive solution imports from a manifest in an agent or CI flow')
|
|
287
459
|
.epilogue('When --wait is set, the command exits after the queued import job reaches a terminal state.'), async (argv) => {
|
|
288
|
-
const config = await context.configStore.
|
|
460
|
+
const config = await context.configStore.loadProfile({ profileName: argv.profile });
|
|
289
461
|
const rawSources = (argv.sources ?? []).map(String);
|
|
290
462
|
const manifestPath = argv.manifest === '' ? '-' : argv.manifest;
|
|
291
463
|
const normalizedRawSources = manifestPath === '-' && rawSources[0] === '-' ? rawSources.slice(1) : rawSources;
|