@chalksurf/cli 0.2.1 → 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 +14 -13
- package/dist/bin/chalksurf.js +3 -3
- package/dist/commands/exercise.js +178 -6
- package/dist/commands/job.js +108 -40
- package/dist/commands/sheet.js +323 -29
- package/dist/lib/api-client.js +11 -2
- package/dist/lib/command-options.js +61 -0
- 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 +57 -3
- 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 +46 -2
- 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.2.
|
|
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.2.
|
|
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.2.
|
|
18
|
+
npm install -g @chalksurf/cli@0.2.2
|
|
19
19
|
chalksurf --version
|
|
20
20
|
```
|
|
21
21
|
|
|
@@ -34,25 +34,26 @@ Headless or agent flow:
|
|
|
34
34
|
|
|
35
35
|
```bash
|
|
36
36
|
CHALKSURF_TOKEN=cs_cli_... \
|
|
37
|
-
printf '%s' "$CHALKSURF_TOKEN" | npx @chalksurf/cli@0.2.
|
|
37
|
+
printf '%s' "$CHALKSURF_TOKEN" | npx @chalksurf/cli@0.2.2 auth login \
|
|
38
38
|
--profile prod-codex \
|
|
39
39
|
--base-url https://chalksurf-api.fly.dev \
|
|
40
40
|
--with-token
|
|
41
41
|
|
|
42
|
-
npx @chalksurf/cli@0.2.
|
|
42
|
+
npx @chalksurf/cli@0.2.2 --profile prod-codex sheet import --manifest - --wait --json < import.json
|
|
43
43
|
```
|
|
44
44
|
|
|
45
|
-
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).
|
|
46
46
|
|
|
47
47
|
## Docs
|
|
48
48
|
|
|
49
|
-
- [Manual operator guide](./docs/manual.md)
|
|
50
|
-
- [Agent and Codex guide](./docs/agents.md)
|
|
51
|
-
- [Manifest reference](./docs/manifest.md)
|
|
52
|
-
- [Exit codes and JSON errors](./docs/exit-codes.md)
|
|
53
|
-
- [Sheet import schema](./schemas/sheet-import-manifest.schema.json)
|
|
54
|
-
- [Exercise import schema](./schemas/exercise-import-manifest.schema.json)
|
|
55
|
-
- [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)
|
|
56
57
|
|
|
57
58
|
## Local And Staging Testing
|
|
58
59
|
|
package/dist/bin/chalksurf.js
CHANGED
|
@@ -14,7 +14,8 @@ import { createConfigStore } from '../lib/config-store.js';
|
|
|
14
14
|
import { createOutput } from '../lib/output.js';
|
|
15
15
|
import { createPromptSecret } from '../lib/prompt-secret.js';
|
|
16
16
|
const packageJson = JSON.parse(readFileSync(new URL('../../package.json', import.meta.url), 'utf8'));
|
|
17
|
-
const createCli = ({ cwd, configPath, env, now, output, sleep, stdin,
|
|
17
|
+
const createCli = ({ cwd, configPath, env, now, output, sleep, stdin, }) => {
|
|
18
|
+
const promptSecret = createPromptSecret();
|
|
18
19
|
const commandContext = {
|
|
19
20
|
cwd,
|
|
20
21
|
configStore: createConfigStore({ configPath, env }),
|
|
@@ -101,7 +102,7 @@ const resolveCommandName = (argv) => {
|
|
|
101
102
|
}
|
|
102
103
|
return 'chalksurf';
|
|
103
104
|
};
|
|
104
|
-
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, }) => {
|
|
105
106
|
const output = createOutput({
|
|
106
107
|
json: hasFlag(argv, ['--json']),
|
|
107
108
|
stdout,
|
|
@@ -114,7 +115,6 @@ export const runCli = async ({ argv, cwd = process.cwd(), configPath, env = proc
|
|
|
114
115
|
env,
|
|
115
116
|
now: nowImpl,
|
|
116
117
|
output,
|
|
117
|
-
promptSecret: promptSecretImpl,
|
|
118
118
|
sleep: sleepImpl,
|
|
119
119
|
stdin,
|
|
120
120
|
});
|
|
@@ -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',
|
|
@@ -111,11 +275,15 @@ export const registerExerciseCommands = (exerciseYargs, context) => {
|
|
|
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
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')
|
|
114
|
-
.epilogue(
|
|
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) => {
|
|
115
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);
|
package/dist/commands/job.js
CHANGED
|
@@ -1,7 +1,16 @@
|
|
|
1
|
-
import { createApiClient } from '../lib/api-client.js';
|
|
2
1
|
import { CliCommandError } from '../lib/cli-error.js';
|
|
3
|
-
import {
|
|
2
|
+
import { normalizeChoiceValues, normalizeLimit, normalizeOffset, normalizeOptionalText, } from '../lib/command-options.js';
|
|
3
|
+
import { createResolvedApiClient, mapApiErrorToCliError } from '../lib/session.js';
|
|
4
4
|
import { formatCliJobSummary, getWaitError, getWaitExitCode, serializeCliJob, throwSilentExitCode, waitForCliJobs, } from '../lib/user-jobs.js';
|
|
5
|
+
const jobStatusOptions = ['pending', 'in_progress', 'completed', 'failed'];
|
|
6
|
+
const jobTypeOptions = [
|
|
7
|
+
'exercise_sheet_import',
|
|
8
|
+
'exercise_import',
|
|
9
|
+
'exercise_solution_import',
|
|
10
|
+
'exercise_solution_generation',
|
|
11
|
+
'exercise_translation_generation',
|
|
12
|
+
'exercise_sheet_translation_generation',
|
|
13
|
+
];
|
|
5
14
|
const normalizeTimeoutMs = (timeoutMs) => {
|
|
6
15
|
const normalizedTimeoutMs = timeoutMs ?? 300000;
|
|
7
16
|
if (!Number.isFinite(normalizedTimeoutMs) || normalizedTimeoutMs <= 0) {
|
|
@@ -9,8 +18,95 @@ const normalizeTimeoutMs = (timeoutMs) => {
|
|
|
9
18
|
}
|
|
10
19
|
return normalizedTimeoutMs;
|
|
11
20
|
};
|
|
21
|
+
const formatJobListOutput = ({ jobs }) => {
|
|
22
|
+
if (jobs.length === 0) {
|
|
23
|
+
return 'No jobs found.';
|
|
24
|
+
}
|
|
25
|
+
return jobs.map((job) => formatCliJobSummary(job)).join('\n');
|
|
26
|
+
};
|
|
12
27
|
export const registerJobCommands = (jobYargs, context) => {
|
|
13
28
|
return jobYargs
|
|
29
|
+
.command('list', 'List jobs for the current user', (listYargs) => listYargs
|
|
30
|
+
.option('status', {
|
|
31
|
+
array: true,
|
|
32
|
+
type: 'string',
|
|
33
|
+
describe: `Only include jobs with these statuses (${jobStatusOptions.join(', ')})`,
|
|
34
|
+
})
|
|
35
|
+
.option('type', {
|
|
36
|
+
array: true,
|
|
37
|
+
type: 'string',
|
|
38
|
+
describe: `Only include jobs with these types (${jobTypeOptions.join(', ')})`,
|
|
39
|
+
})
|
|
40
|
+
.option('include-viewed', {
|
|
41
|
+
type: 'boolean',
|
|
42
|
+
default: false,
|
|
43
|
+
describe: 'Include jobs already marked as viewed',
|
|
44
|
+
})
|
|
45
|
+
.option('include-dismissed', {
|
|
46
|
+
type: 'boolean',
|
|
47
|
+
default: false,
|
|
48
|
+
describe: 'Include jobs already dismissed in the UI',
|
|
49
|
+
})
|
|
50
|
+
.option('limit', {
|
|
51
|
+
type: 'number',
|
|
52
|
+
default: 20,
|
|
53
|
+
describe: 'Maximum number of jobs to return',
|
|
54
|
+
})
|
|
55
|
+
.option('offset', {
|
|
56
|
+
type: 'number',
|
|
57
|
+
default: 0,
|
|
58
|
+
describe: 'Number of matching jobs to skip',
|
|
59
|
+
})
|
|
60
|
+
.example('chalksurf job list --status failed --include-dismissed --json', 'Find failed jobs for retry')
|
|
61
|
+
.example('chalksurf --profile prod-codex job list --type exercise_sheet_import --json', 'Inspect agent imports'), async (argv) => {
|
|
62
|
+
const statuses = normalizeChoiceValues({
|
|
63
|
+
allowedValues: jobStatusOptions,
|
|
64
|
+
label: '--status',
|
|
65
|
+
values: argv.status,
|
|
66
|
+
});
|
|
67
|
+
const types = normalizeChoiceValues({
|
|
68
|
+
allowedValues: jobTypeOptions,
|
|
69
|
+
label: '--type',
|
|
70
|
+
values: argv.type,
|
|
71
|
+
});
|
|
72
|
+
const limit = normalizeLimit(argv.limit);
|
|
73
|
+
const offset = normalizeOffset(argv.offset);
|
|
74
|
+
const { apiClient } = await createResolvedApiClient({
|
|
75
|
+
context,
|
|
76
|
+
baseUrlFlagValue: normalizeOptionalText(argv.baseUrl) ?? undefined,
|
|
77
|
+
organizationFlagValue: normalizeOptionalText(argv.organization) ?? undefined,
|
|
78
|
+
profileName: argv.profile,
|
|
79
|
+
});
|
|
80
|
+
let jobs;
|
|
81
|
+
try {
|
|
82
|
+
jobs = await apiClient.listUserJobs({
|
|
83
|
+
statuses,
|
|
84
|
+
types,
|
|
85
|
+
includeViewed: argv.includeViewed === true,
|
|
86
|
+
includeDismissed: argv.includeDismissed === true,
|
|
87
|
+
limit,
|
|
88
|
+
offset,
|
|
89
|
+
});
|
|
90
|
+
}
|
|
91
|
+
catch (error) {
|
|
92
|
+
throw mapApiErrorToCliError(error);
|
|
93
|
+
}
|
|
94
|
+
const serializedJobs = jobs.map(serializeCliJob);
|
|
95
|
+
context.output.print({
|
|
96
|
+
request: {
|
|
97
|
+
statuses: statuses ?? [],
|
|
98
|
+
types: types ?? [],
|
|
99
|
+
includeViewed: argv.includeViewed === true,
|
|
100
|
+
includeDismissed: argv.includeDismissed === true,
|
|
101
|
+
limit,
|
|
102
|
+
offset,
|
|
103
|
+
},
|
|
104
|
+
jobs: serializedJobs,
|
|
105
|
+
count: serializedJobs.length,
|
|
106
|
+
}, (result) => formatJobListOutput({ jobs: result.jobs }), {
|
|
107
|
+
command: 'job list',
|
|
108
|
+
});
|
|
109
|
+
})
|
|
14
110
|
.command('get <jobId>', 'Fetch a single job', (getYargs) => getYargs
|
|
15
111
|
.positional('jobId', {
|
|
16
112
|
type: 'string',
|
|
@@ -18,25 +114,11 @@ export const registerJobCommands = (jobYargs, context) => {
|
|
|
18
114
|
})
|
|
19
115
|
.example('chalksurf job get job_123', 'Read one job in human-readable mode')
|
|
20
116
|
.example('chalksurf job get job_123 --json', 'Read one job in machine-readable mode'), async (argv) => {
|
|
21
|
-
const
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
});
|
|
27
|
-
const resolvedToken = requireResolvedToken({
|
|
28
|
-
env: context.env,
|
|
29
|
-
config,
|
|
30
|
-
});
|
|
31
|
-
const organizationId = resolveRequestedOrganizationId({
|
|
32
|
-
flagValue: argv.organization,
|
|
33
|
-
env: context.env,
|
|
34
|
-
config,
|
|
35
|
-
});
|
|
36
|
-
const apiClient = createApiClient({
|
|
37
|
-
baseUrl: resolvedBaseUrl.value,
|
|
38
|
-
token: resolvedToken.value,
|
|
39
|
-
organizationId,
|
|
117
|
+
const { apiClient } = await createResolvedApiClient({
|
|
118
|
+
context,
|
|
119
|
+
baseUrlFlagValue: argv.baseUrl,
|
|
120
|
+
organizationFlagValue: argv.organization,
|
|
121
|
+
profileName: argv.profile,
|
|
40
122
|
});
|
|
41
123
|
let job;
|
|
42
124
|
try {
|
|
@@ -63,25 +145,11 @@ export const registerJobCommands = (jobYargs, context) => {
|
|
|
63
145
|
})
|
|
64
146
|
.example('chalksurf job wait job_123 job_124', 'Wait for one or more jobs to reach a terminal state')
|
|
65
147
|
.example('chalksurf job wait job_123 --timeout-ms 60000 --json', 'Poll a job for up to 60 seconds'), async (argv) => {
|
|
66
|
-
const
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
});
|
|
72
|
-
const resolvedToken = requireResolvedToken({
|
|
73
|
-
env: context.env,
|
|
74
|
-
config,
|
|
75
|
-
});
|
|
76
|
-
const organizationId = resolveRequestedOrganizationId({
|
|
77
|
-
flagValue: argv.organization,
|
|
78
|
-
env: context.env,
|
|
79
|
-
config,
|
|
80
|
-
});
|
|
81
|
-
const apiClient = createApiClient({
|
|
82
|
-
baseUrl: resolvedBaseUrl.value,
|
|
83
|
-
token: resolvedToken.value,
|
|
84
|
-
organizationId,
|
|
148
|
+
const { apiClient } = await createResolvedApiClient({
|
|
149
|
+
context,
|
|
150
|
+
baseUrlFlagValue: argv.baseUrl,
|
|
151
|
+
organizationFlagValue: argv.organization,
|
|
152
|
+
profileName: argv.profile,
|
|
85
153
|
});
|
|
86
154
|
const timeoutMs = normalizeTimeoutMs(argv.timeoutMs);
|
|
87
155
|
const waitResult = await waitForCliJobs({
|