@chalksurf/cli 0.2.1 → 0.2.3
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 +15 -16
- 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 +369 -32
- 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 +82 -0
- package/dist/lib/session.js +27 -0
- package/dist/lib/translation-languages.js +15 -0
- package/dist/lib/user-jobs.js +14 -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/examples/sheet-import-manifest.json +24 -0
- package/docs/manifest.md +49 -4
- 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/schemas/sheet-import-manifest.schema.json +78 -22
package/dist/commands/sheet.js
CHANGED
|
@@ -1,13 +1,16 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
1
2
|
import { createApiClient } from '../lib/api-client.js';
|
|
2
3
|
import { CliCommandError } from '../lib/cli-error.js';
|
|
4
|
+
import { normalizeLimit, normalizeOffset, normalizeOptionalText, normalizeOwnership, toApiOwnership, } from '../lib/command-options.js';
|
|
3
5
|
import { buildFileImportCommandSources, buildFileImportFormData, normalizeWaitTimeoutMs } from '../lib/import-files.js';
|
|
4
|
-
import { buildCliImportSources, formatCliImportSummary, summarizeCliImportJobs, } from '../lib/import-output.js';
|
|
5
|
-
import { loadSheetImportManifest, } from '../lib/manifest.js';
|
|
6
|
-
import { mapApiErrorToCliError, requireResolvedBaseUrl, requireResolvedToken, resolveRequestedOrganizationId, } from '../lib/session.js';
|
|
6
|
+
import { buildCliImportSources, formatCliImportSummary, formatCliImportTranslationJobs, summarizeCliImportJobs, } from '../lib/import-output.js';
|
|
7
|
+
import { loadExerciseSheetSolutionImportManifest, loadSheetImportManifest, } from '../lib/manifest.js';
|
|
8
|
+
import { createResolvedApiClient, mapApiErrorToCliError, requireResolvedBaseUrl, requireResolvedToken, resolveRequestedOrganizationId, } from '../lib/session.js';
|
|
7
9
|
import { cleanupResolvedSources, resolveSources } from '../lib/source-resolver.js';
|
|
8
|
-
import { translationLanguages } from '../lib/translation-languages.js';
|
|
10
|
+
import { resolveRequestedTranslateToLanguages, translationLanguages, } from '../lib/translation-languages.js';
|
|
9
11
|
import { formatCliJobSummary, getWaitError, getWaitExitCode, throwSilentExitCode, waitForCliJobs, } from '../lib/user-jobs.js';
|
|
10
12
|
const hasUniqueItems = (values) => new Set(values).size === values.length;
|
|
13
|
+
const sheetImportComponentIdPattern = /^[a-zA-Z0-9_-]{1,80}$/;
|
|
11
14
|
const isValidTargetFolderPath = (value) => {
|
|
12
15
|
const normalizedSegments = value
|
|
13
16
|
.replaceAll('\\', '/')
|
|
@@ -29,6 +32,32 @@ const assertValidSheetImportPlan = (plan) => {
|
|
|
29
32
|
if (!hasUniqueItems(sheet.sourceIndexes)) {
|
|
30
33
|
throw new CliCommandError('Invalid sheet import plan: sourceIndexes must be unique within a sheet.', 2);
|
|
31
34
|
}
|
|
35
|
+
if (sheet.components) {
|
|
36
|
+
if (sheet.targetFolderPath !== undefined || sheet.titleOverride !== undefined || sheet.translateToLanguages) {
|
|
37
|
+
throw new CliCommandError('Invalid sheet import plan: targetFolderPath, titleOverride, and translateToLanguages must be set on components when components are provided.', 2);
|
|
38
|
+
}
|
|
39
|
+
if (!hasUniqueItems(sheet.components.map((component) => component.componentId))) {
|
|
40
|
+
throw new CliCommandError('Invalid sheet import plan: componentId values must be unique within a sheet.', 2);
|
|
41
|
+
}
|
|
42
|
+
sheet.components.forEach((component) => {
|
|
43
|
+
if (!sheetImportComponentIdPattern.test(component.componentId)) {
|
|
44
|
+
throw new CliCommandError('Invalid sheet import plan: componentId must be 1-80 letters, numbers, underscores, or hyphens.', 2);
|
|
45
|
+
}
|
|
46
|
+
if (!component.description.trim()) {
|
|
47
|
+
throw new CliCommandError('Invalid sheet import plan: component descriptions must be non-empty.', 2);
|
|
48
|
+
}
|
|
49
|
+
if (component.targetFolderPath != null && !isValidTargetFolderPath(component.targetFolderPath)) {
|
|
50
|
+
throw new CliCommandError('Invalid sheet import plan: component targetFolderPath must be a normalized folder path without "." or ".." segments.', 2);
|
|
51
|
+
}
|
|
52
|
+
if (component.translateToLanguages && !hasUniqueItems(component.translateToLanguages)) {
|
|
53
|
+
throw new CliCommandError('Invalid sheet import plan: component translateToLanguages must be unique.', 2);
|
|
54
|
+
}
|
|
55
|
+
});
|
|
56
|
+
return;
|
|
57
|
+
}
|
|
58
|
+
if (sheet.targetFolderPath === undefined) {
|
|
59
|
+
throw new CliCommandError('Invalid sheet import plan: targetFolderPath is required.', 2);
|
|
60
|
+
}
|
|
32
61
|
if (sheet.targetFolderPath !== null && !isValidTargetFolderPath(sheet.targetFolderPath)) {
|
|
33
62
|
throw new CliCommandError('Invalid sheet import plan: targetFolderPath must be a normalized folder path without "." or ".." segments.', 2);
|
|
34
63
|
}
|
|
@@ -44,20 +73,6 @@ const normalizeOptionalTitle = (value) => {
|
|
|
44
73
|
}
|
|
45
74
|
return normalizedValue;
|
|
46
75
|
};
|
|
47
|
-
const resolveRequestedTranslateToLanguages = (rawValues) => {
|
|
48
|
-
if (!rawValues || rawValues.length === 0) {
|
|
49
|
-
return undefined;
|
|
50
|
-
}
|
|
51
|
-
const invalidLanguage = rawValues.find((value) => !translationLanguages.includes(value));
|
|
52
|
-
if (invalidLanguage) {
|
|
53
|
-
throw new CliCommandError(`--translate-to must be one of: ${translationLanguages.join(', ')}`, 2);
|
|
54
|
-
}
|
|
55
|
-
const languages = rawValues;
|
|
56
|
-
if (new Set(languages).size !== languages.length) {
|
|
57
|
-
throw new CliCommandError('--translate-to languages must be unique.', 2);
|
|
58
|
-
}
|
|
59
|
-
return languages;
|
|
60
|
-
};
|
|
61
76
|
const normalizeOptionalTargetFolderPath = (value) => {
|
|
62
77
|
const normalizedValue = value?.trim();
|
|
63
78
|
if (!normalizedValue) {
|
|
@@ -65,6 +80,20 @@ const normalizeOptionalTargetFolderPath = (value) => {
|
|
|
65
80
|
}
|
|
66
81
|
return normalizedValue;
|
|
67
82
|
};
|
|
83
|
+
const resolveRequestedUuid = ({ flagValue, fallbackValue, label, required = false, }) => {
|
|
84
|
+
const resolvedValue = flagValue?.trim() || fallbackValue?.trim();
|
|
85
|
+
if (!resolvedValue) {
|
|
86
|
+
if (required) {
|
|
87
|
+
throw new CliCommandError(`${label} is required.`, 2);
|
|
88
|
+
}
|
|
89
|
+
return undefined;
|
|
90
|
+
}
|
|
91
|
+
const parsedValue = z.uuid().safeParse(resolvedValue);
|
|
92
|
+
if (!parsedValue.success) {
|
|
93
|
+
throw new CliCommandError(`${label} must be a valid UUID.`, 2);
|
|
94
|
+
}
|
|
95
|
+
return parsedValue.data;
|
|
96
|
+
};
|
|
68
97
|
const getFolderPathFromRelativePath = (relativePath) => {
|
|
69
98
|
const normalizedSegments = relativePath
|
|
70
99
|
.replaceAll('\\', '/')
|
|
@@ -130,9 +159,21 @@ const resolveGroupedSourceIndexes = ({ groups, sourceIndexesByInputIndex, }) =>
|
|
|
130
159
|
});
|
|
131
160
|
return {
|
|
132
161
|
sourceIndexes,
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
162
|
+
...(group.components
|
|
163
|
+
? {
|
|
164
|
+
components: group.components.map((component) => ({
|
|
165
|
+
componentId: component.componentId,
|
|
166
|
+
description: component.description,
|
|
167
|
+
targetFolderPath: component.targetFolderPath,
|
|
168
|
+
titleOverride: component.title,
|
|
169
|
+
translateToLanguages: component.translateTo,
|
|
170
|
+
})),
|
|
171
|
+
}
|
|
172
|
+
: {
|
|
173
|
+
targetFolderPath: group.targetFolderPath,
|
|
174
|
+
titleOverride: group.title,
|
|
175
|
+
translateToLanguages: group.translateTo,
|
|
176
|
+
}),
|
|
136
177
|
};
|
|
137
178
|
});
|
|
138
179
|
};
|
|
@@ -171,6 +212,7 @@ const buildSheetImportJobs = ({ importResultJobs, sources, waitedJobsById, }) =>
|
|
|
171
212
|
return importResultJobs.map((job, index) => {
|
|
172
213
|
const sourceIndexes = job.sourceIndexes ?? (sources[index] ? [sources[index].sourceIndex] : []);
|
|
173
214
|
const waitedJob = waitedJobsById?.get(job.jobId);
|
|
215
|
+
const importedSheetTranslationJobs = waitedJob?.sheets?.flatMap((sheet) => (sheet.status === 'imported' ? (sheet.translationJobs ?? []) : [])) ?? [];
|
|
174
216
|
return {
|
|
175
217
|
jobId: job.jobId,
|
|
176
218
|
sourceIndexes,
|
|
@@ -179,7 +221,10 @@ const buildSheetImportJobs = ({ importResultJobs, sources, waitedJobsById, }) =>
|
|
|
179
221
|
.filter((sourceId) => sourceId != null))),
|
|
180
222
|
status: waitedJob?.status ?? 'queued',
|
|
181
223
|
error: waitedJob?.error,
|
|
224
|
+
componentIds: job.componentIds,
|
|
182
225
|
exerciseSheetId: waitedJob?.exerciseSheetId,
|
|
226
|
+
sheets: waitedJob?.sheets,
|
|
227
|
+
translationJobs: waitedJob?.translationJobs ?? importedSheetTranslationJobs,
|
|
183
228
|
};
|
|
184
229
|
});
|
|
185
230
|
};
|
|
@@ -192,6 +237,38 @@ const buildSheetImportRequest = ({ inputMode, organizationId, timeoutMs, wait, }
|
|
|
192
237
|
target: {},
|
|
193
238
|
};
|
|
194
239
|
};
|
|
240
|
+
const buildSheetSolutionImportRequest = ({ exerciseSheetId, inputMode, organizationId, timeoutMs, wait, }) => {
|
|
241
|
+
return {
|
|
242
|
+
organizationId,
|
|
243
|
+
inputMode,
|
|
244
|
+
wait,
|
|
245
|
+
timeoutMs,
|
|
246
|
+
target: { exerciseSheetId },
|
|
247
|
+
};
|
|
248
|
+
};
|
|
249
|
+
const buildQueuedSheetSolutionImportJob = ({ jobId, sources, }) => {
|
|
250
|
+
return {
|
|
251
|
+
jobId,
|
|
252
|
+
sourceIndexes: sources.map((source) => source.sourceIndex),
|
|
253
|
+
sourceIds: sources.map((source) => source.sourceId).filter((sourceId) => sourceId !== null),
|
|
254
|
+
status: 'queued',
|
|
255
|
+
};
|
|
256
|
+
};
|
|
257
|
+
const buildWaitedSheetSolutionImportJob = ({ jobId, sources, waitedJob, }) => {
|
|
258
|
+
return {
|
|
259
|
+
jobId,
|
|
260
|
+
sourceIndexes: sources.map((source) => source.sourceIndex),
|
|
261
|
+
sourceIds: sources.map((source) => source.sourceId).filter((sourceId) => sourceId !== null),
|
|
262
|
+
status: waitedJob?.status ?? 'pending',
|
|
263
|
+
error: waitedJob?.error,
|
|
264
|
+
eligibleExerciseCount: waitedJob?.eligibleExerciseCount,
|
|
265
|
+
exerciseSheetId: waitedJob?.exerciseSheetId,
|
|
266
|
+
nonUpdatableExerciseCount: waitedJob?.nonUpdatableExerciseCount,
|
|
267
|
+
skippedExerciseCount: waitedJob?.skippedExerciseCount,
|
|
268
|
+
unmatchedImportedSolutionCount: waitedJob?.unmatchedImportedSolutionCount,
|
|
269
|
+
updatedExerciseCount: waitedJob?.updatedExerciseCount,
|
|
270
|
+
};
|
|
271
|
+
};
|
|
195
272
|
const formatSheetJobSourceLabel = ({ job, sources, }) => {
|
|
196
273
|
if (job.sourceIndexes.length !== 1) {
|
|
197
274
|
return `${job.sourceIndexes.length} source${job.sourceIndexes.length === 1 ? '' : 's'}`;
|
|
@@ -211,22 +288,113 @@ const formatQueuedImportOutput = ({ jobs, summary, sources, }) => {
|
|
|
211
288
|
const formatWaitedImportOutput = ({ jobs, summary, }) => {
|
|
212
289
|
return [
|
|
213
290
|
`${jobs.length} job${jobs.length === 1 ? '' : 's'} queued.`,
|
|
214
|
-
...jobs.
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
291
|
+
...jobs.flatMap((job) => [
|
|
292
|
+
formatCliJobSummary({
|
|
293
|
+
id: job.jobId,
|
|
294
|
+
status: job.status === 'queued' ? 'pending' : job.status,
|
|
295
|
+
type: 'exercise_sheet_import',
|
|
296
|
+
createdAt: '',
|
|
297
|
+
updatedAt: '',
|
|
298
|
+
error: job.error,
|
|
299
|
+
exerciseSheetId: job.exerciseSheetId,
|
|
300
|
+
sheets: job.sheets,
|
|
301
|
+
}),
|
|
302
|
+
...formatCliImportTranslationJobs(job.translationJobs),
|
|
303
|
+
]),
|
|
223
304
|
jobs.length > 1 ? formatCliImportSummary(summary) : null,
|
|
224
305
|
]
|
|
225
306
|
.filter(Boolean)
|
|
226
307
|
.join('\n');
|
|
227
308
|
};
|
|
309
|
+
const formatQueuedSheetSolutionImportOutput = (job) => {
|
|
310
|
+
return [
|
|
311
|
+
'Queued exercise sheet solution import.',
|
|
312
|
+
`${job.jobId} ${job.sourceIndexes.length} source${job.sourceIndexes.length === 1 ? '' : 's'}`,
|
|
313
|
+
].join('\n');
|
|
314
|
+
};
|
|
315
|
+
const formatWaitedSheetSolutionImportOutput = (job) => {
|
|
316
|
+
return formatCliJobSummary({
|
|
317
|
+
id: job.jobId,
|
|
318
|
+
status: job.status === 'queued' ? 'pending' : job.status,
|
|
319
|
+
type: 'exercise_sheet_solution_import',
|
|
320
|
+
createdAt: '',
|
|
321
|
+
updatedAt: '',
|
|
322
|
+
error: job.error,
|
|
323
|
+
exerciseSheetId: job.exerciseSheetId,
|
|
324
|
+
});
|
|
325
|
+
};
|
|
326
|
+
const formatSheetSearchOutput = ({ exerciseSheets, totalCount, }) => {
|
|
327
|
+
if (exerciseSheets.length === 0) {
|
|
328
|
+
return 'No sheets found.';
|
|
329
|
+
}
|
|
330
|
+
return [
|
|
331
|
+
`${exerciseSheets.length} of ${totalCount} sheet${totalCount === 1 ? '' : 's'} returned.`,
|
|
332
|
+
...exerciseSheets.map((sheet) => `${sheet.id} ${sheet.name} ${sheet.exerciseCount} exercises`),
|
|
333
|
+
].join('\n');
|
|
334
|
+
};
|
|
228
335
|
export const registerSheetCommands = (sheetYargs, context) => {
|
|
229
336
|
return sheetYargs
|
|
337
|
+
.command('search', 'Search exercise sheets visible to the selected organization', (searchYargs) => searchYargs
|
|
338
|
+
.option('text', {
|
|
339
|
+
type: 'string',
|
|
340
|
+
describe: 'Search sheet names',
|
|
341
|
+
})
|
|
342
|
+
.option('ownership', {
|
|
343
|
+
type: 'string',
|
|
344
|
+
default: 'own',
|
|
345
|
+
describe: 'Visibility scope: own, public, or all',
|
|
346
|
+
})
|
|
347
|
+
.option('limit', {
|
|
348
|
+
type: 'number',
|
|
349
|
+
default: 20,
|
|
350
|
+
describe: 'Maximum number of sheets to return',
|
|
351
|
+
})
|
|
352
|
+
.option('offset', {
|
|
353
|
+
type: 'number',
|
|
354
|
+
default: 0,
|
|
355
|
+
describe: 'Number of matching sheets to skip',
|
|
356
|
+
})
|
|
357
|
+
.example('chalksurf sheet search --text "OKTV 2014" --ownership own --json', 'Verify an imported private sheet in the selected organization')
|
|
358
|
+
.example('chalksurf --profile prod-codex sheet search --ownership own --limit 10 --json', 'List recent sheets visible to the agent profile'), async (argv) => {
|
|
359
|
+
const text = normalizeOptionalText(argv.text);
|
|
360
|
+
const ownership = normalizeOwnership(argv.ownership);
|
|
361
|
+
const limit = normalizeLimit(argv.limit);
|
|
362
|
+
const offset = normalizeOffset(argv.offset);
|
|
363
|
+
const { apiClient, organizationId } = await createResolvedApiClient({
|
|
364
|
+
context,
|
|
365
|
+
baseUrlFlagValue: argv.baseUrl,
|
|
366
|
+
organizationFlagValue: argv.organization,
|
|
367
|
+
profileName: argv.profile,
|
|
368
|
+
});
|
|
369
|
+
let searchResult;
|
|
370
|
+
try {
|
|
371
|
+
searchResult = await apiClient.searchExerciseSheets({
|
|
372
|
+
text,
|
|
373
|
+
ownership: toApiOwnership(ownership),
|
|
374
|
+
limit,
|
|
375
|
+
offset,
|
|
376
|
+
});
|
|
377
|
+
}
|
|
378
|
+
catch (error) {
|
|
379
|
+
throw mapApiErrorToCliError(error);
|
|
380
|
+
}
|
|
381
|
+
context.output.print({
|
|
382
|
+
request: {
|
|
383
|
+
text,
|
|
384
|
+
ownership,
|
|
385
|
+
limit,
|
|
386
|
+
offset,
|
|
387
|
+
organizationId: organizationId ?? null,
|
|
388
|
+
},
|
|
389
|
+
exerciseSheets: searchResult.exerciseSheets,
|
|
390
|
+
totalCount: searchResult.totalCount,
|
|
391
|
+
}, (result) => formatSheetSearchOutput({
|
|
392
|
+
exerciseSheets: result.exerciseSheets,
|
|
393
|
+
totalCount: result.totalCount,
|
|
394
|
+
}), {
|
|
395
|
+
command: 'sheet search',
|
|
396
|
+
});
|
|
397
|
+
})
|
|
230
398
|
.command('import [sources..]', 'Import one or more exercise sheet sources', (importYargs) => importYargs
|
|
231
399
|
.positional('sources', {
|
|
232
400
|
array: true,
|
|
@@ -273,8 +441,8 @@ export const registerSheetCommands = (sheetYargs, context) => {
|
|
|
273
441
|
.example('chalksurf sheet import ./fixtures/round-1-a.pdf ./fixtures/round-1-b.pdf --single-sheet --target-folder Contests --wait', 'Import multiple source files into one sheet and place the result into a target folder')
|
|
274
442
|
.example('cat sheet-import.json | chalksurf --profile prod-codex sheet import --manifest - --wait --json', 'Drive multi-source sheet imports from a manifest in an agent or CI flow')
|
|
275
443
|
.epilogue([
|
|
276
|
-
'When --wait is set, the command exits
|
|
277
|
-
'For translated sheet imports,
|
|
444
|
+
'When --wait is set, the command exits after the queued import job reaches a terminal state.',
|
|
445
|
+
'For translated sheet imports, requested translations continue as separate jobs listed in the import result.',
|
|
278
446
|
].join('\n')), async (argv) => {
|
|
279
447
|
const config = await context.configStore.loadProfile({ profileName: argv.profile });
|
|
280
448
|
const rawSources = (argv.sources ?? []).map(String);
|
|
@@ -429,6 +597,175 @@ export const registerSheetCommands = (sheetYargs, context) => {
|
|
|
429
597
|
finally {
|
|
430
598
|
await cleanupResolvedSources(resolvedSources);
|
|
431
599
|
}
|
|
600
|
+
})
|
|
601
|
+
.command('import-solutions [exerciseSheetId] [sources..]', 'Import one or more solution files into an existing exercise sheet', (importSolutionsYargs) => importSolutionsYargs
|
|
602
|
+
.positional('exerciseSheetId', {
|
|
603
|
+
type: 'string',
|
|
604
|
+
describe: 'Exercise sheet ID to reconcile imported solutions into',
|
|
605
|
+
})
|
|
606
|
+
.positional('sources', {
|
|
607
|
+
array: true,
|
|
608
|
+
type: 'string',
|
|
609
|
+
describe: 'Local file paths, directories, or HTTP(S) URLs',
|
|
610
|
+
})
|
|
611
|
+
.option('manifest', {
|
|
612
|
+
type: 'string',
|
|
613
|
+
describe: 'Load sources from a manifest file, or "-" for stdin',
|
|
614
|
+
})
|
|
615
|
+
.option('relative-path', {
|
|
616
|
+
type: 'string',
|
|
617
|
+
describe: 'Override the relative path for a single positional source',
|
|
618
|
+
})
|
|
619
|
+
.option('timeout-ms', {
|
|
620
|
+
type: 'number',
|
|
621
|
+
default: 300000,
|
|
622
|
+
describe: 'Maximum time to wait before timing out',
|
|
623
|
+
})
|
|
624
|
+
.option('wait', {
|
|
625
|
+
type: 'boolean',
|
|
626
|
+
default: false,
|
|
627
|
+
describe: 'Wait for the queued job to finish',
|
|
628
|
+
})
|
|
629
|
+
.example('chalksurf sheet import-solutions 00000000-0000-4000-8000-000000000001 ./fixtures/solutions.pdf --wait', 'Attach a separate solution file to an existing exercise sheet')
|
|
630
|
+
.example('cat sheet-solution-import.json | chalksurf --profile prod-codex sheet import-solutions --manifest - --wait --json', 'Drive sheet solution imports from a manifest in an agent or CI flow')
|
|
631
|
+
.epilogue('When --wait is set, the command exits after the queued import job reaches a terminal state.'), async (argv) => {
|
|
632
|
+
const config = await context.configStore.loadProfile({ profileName: argv.profile });
|
|
633
|
+
const rawSources = (argv.sources ?? []).map(String);
|
|
634
|
+
const manifestPath = argv.manifest === '' ? '-' : argv.manifest;
|
|
635
|
+
const normalizedRawSources = manifestPath === '-' && rawSources[0] === '-' ? rawSources.slice(1) : rawSources;
|
|
636
|
+
const resolvedBaseUrl = requireResolvedBaseUrl({
|
|
637
|
+
flagValue: argv.baseUrl,
|
|
638
|
+
env: context.env,
|
|
639
|
+
config,
|
|
640
|
+
});
|
|
641
|
+
const resolvedToken = requireResolvedToken({
|
|
642
|
+
env: context.env,
|
|
643
|
+
config,
|
|
644
|
+
});
|
|
645
|
+
const commandSources = await buildFileImportCommandSources({
|
|
646
|
+
cwd: context.cwd,
|
|
647
|
+
loadManifest: loadExerciseSheetSolutionImportManifest,
|
|
648
|
+
manifestPath,
|
|
649
|
+
rawSources: normalizedRawSources,
|
|
650
|
+
relativePath: argv.relativePath,
|
|
651
|
+
stdin: context.stdin,
|
|
652
|
+
wait: argv.wait,
|
|
653
|
+
});
|
|
654
|
+
const organizationId = resolveRequestedOrganizationId({
|
|
655
|
+
flagValue: argv.organization,
|
|
656
|
+
fallbackValue: commandSources.organizationId,
|
|
657
|
+
env: context.env,
|
|
658
|
+
config,
|
|
659
|
+
});
|
|
660
|
+
const exerciseSheetId = resolveRequestedUuid({
|
|
661
|
+
flagValue: argv.exerciseSheetId,
|
|
662
|
+
fallbackValue: commandSources.exerciseSheetId,
|
|
663
|
+
label: 'exerciseSheetId',
|
|
664
|
+
required: true,
|
|
665
|
+
});
|
|
666
|
+
const apiClient = createApiClient({
|
|
667
|
+
baseUrl: resolvedBaseUrl.value,
|
|
668
|
+
token: resolvedToken.value,
|
|
669
|
+
organizationId,
|
|
670
|
+
});
|
|
671
|
+
let resolvedSources = [];
|
|
672
|
+
try {
|
|
673
|
+
resolvedSources = await resolveSources({
|
|
674
|
+
cwd: context.cwd,
|
|
675
|
+
sources: commandSources.sources,
|
|
676
|
+
});
|
|
677
|
+
const sources = buildCliImportSources({
|
|
678
|
+
resolvedSources,
|
|
679
|
+
});
|
|
680
|
+
const formData = await buildFileImportFormData(resolvedSources);
|
|
681
|
+
formData.set('exerciseSheetId', exerciseSheetId);
|
|
682
|
+
const request = buildSheetSolutionImportRequest({
|
|
683
|
+
exerciseSheetId,
|
|
684
|
+
inputMode: manifestPath ? 'manifest' : 'arguments',
|
|
685
|
+
organizationId: organizationId ?? null,
|
|
686
|
+
timeoutMs: commandSources.wait ? normalizeWaitTimeoutMs(argv.timeoutMs) : null,
|
|
687
|
+
wait: commandSources.wait,
|
|
688
|
+
});
|
|
689
|
+
let importResult;
|
|
690
|
+
try {
|
|
691
|
+
importResult = await apiClient.importExerciseSheetSolutions(formData);
|
|
692
|
+
}
|
|
693
|
+
catch (error) {
|
|
694
|
+
throw mapApiErrorToCliError(error);
|
|
695
|
+
}
|
|
696
|
+
if (!commandSources.wait) {
|
|
697
|
+
const jobs = [
|
|
698
|
+
buildQueuedSheetSolutionImportJob({
|
|
699
|
+
jobId: importResult.jobId,
|
|
700
|
+
sources,
|
|
701
|
+
}),
|
|
702
|
+
];
|
|
703
|
+
const summary = summarizeCliImportJobs({
|
|
704
|
+
jobs,
|
|
705
|
+
sourceCount: sources.length,
|
|
706
|
+
sourceInputCount: commandSources.sources.length,
|
|
707
|
+
timedOut: false,
|
|
708
|
+
});
|
|
709
|
+
context.output.print({
|
|
710
|
+
request,
|
|
711
|
+
sources,
|
|
712
|
+
jobs,
|
|
713
|
+
summary,
|
|
714
|
+
}, (result) => formatQueuedSheetSolutionImportOutput(result.jobs[0]), {
|
|
715
|
+
command: 'sheet import-solutions',
|
|
716
|
+
});
|
|
717
|
+
return;
|
|
718
|
+
}
|
|
719
|
+
const waitTimeoutMs = normalizeWaitTimeoutMs(argv.timeoutMs);
|
|
720
|
+
const waitResult = await waitForCliJobs({
|
|
721
|
+
getUserJob: async (jobId) => await apiClient.getUserJob(jobId),
|
|
722
|
+
jobIds: [importResult.jobId],
|
|
723
|
+
now: context.now,
|
|
724
|
+
sleep: context.sleep,
|
|
725
|
+
timeoutMs: waitTimeoutMs,
|
|
726
|
+
});
|
|
727
|
+
const waitedJob = waitResult.jobs[0];
|
|
728
|
+
const jobs = [
|
|
729
|
+
buildWaitedSheetSolutionImportJob({
|
|
730
|
+
jobId: importResult.jobId,
|
|
731
|
+
sources,
|
|
732
|
+
waitedJob: waitedJob == null
|
|
733
|
+
? undefined
|
|
734
|
+
: {
|
|
735
|
+
...waitedJob,
|
|
736
|
+
id: waitedJob.id,
|
|
737
|
+
status: waitedJob.status,
|
|
738
|
+
},
|
|
739
|
+
}),
|
|
740
|
+
];
|
|
741
|
+
const summary = summarizeCliImportJobs({
|
|
742
|
+
jobs,
|
|
743
|
+
sourceCount: sources.length,
|
|
744
|
+
sourceInputCount: commandSources.sources.length,
|
|
745
|
+
timedOut: waitResult.timedOut,
|
|
746
|
+
});
|
|
747
|
+
const waitError = getWaitError({
|
|
748
|
+
jobs: waitResult.jobs,
|
|
749
|
+
timedOut: waitResult.timedOut,
|
|
750
|
+
});
|
|
751
|
+
context.output.print({
|
|
752
|
+
request: {
|
|
753
|
+
...request,
|
|
754
|
+
timeoutMs: waitTimeoutMs,
|
|
755
|
+
},
|
|
756
|
+
sources,
|
|
757
|
+
jobs,
|
|
758
|
+
summary,
|
|
759
|
+
}, (result) => formatWaitedSheetSolutionImportOutput(result.jobs[0]), {
|
|
760
|
+
command: 'sheet import-solutions',
|
|
761
|
+
ok: waitError == null,
|
|
762
|
+
error: waitError,
|
|
763
|
+
});
|
|
764
|
+
throwSilentExitCode(getWaitExitCode({ jobs: waitResult.jobs, timedOut: waitResult.timedOut }));
|
|
765
|
+
}
|
|
766
|
+
finally {
|
|
767
|
+
await cleanupResolvedSources(resolvedSources);
|
|
768
|
+
}
|
|
432
769
|
})
|
|
433
770
|
.demandCommand(1)
|
|
434
771
|
.strict();
|
package/dist/lib/api-client.js
CHANGED
|
@@ -58,12 +58,18 @@ export const createApiClient = ({ baseUrl, token, organizationId, }) => {
|
|
|
58
58
|
getUserProfile: async () => {
|
|
59
59
|
return await query('getUserProfile', null);
|
|
60
60
|
},
|
|
61
|
-
listUserJobs: async () => {
|
|
62
|
-
return await query('listUserJobs', null);
|
|
61
|
+
listUserJobs: async (input) => {
|
|
62
|
+
return await query('listUserJobs', input ?? null);
|
|
63
63
|
},
|
|
64
64
|
getUserJob: async (id) => {
|
|
65
65
|
return await query('getUserJob', { id });
|
|
66
66
|
},
|
|
67
|
+
searchExercises: async (input) => {
|
|
68
|
+
return await mutation('searchExercises', input);
|
|
69
|
+
},
|
|
70
|
+
searchExerciseSheets: async (input) => {
|
|
71
|
+
return await mutation('searchExerciseSheets', input);
|
|
72
|
+
},
|
|
67
73
|
importExerciseSheet: async (formData) => {
|
|
68
74
|
return await mutation('importExerciseSheet', formData);
|
|
69
75
|
},
|
|
@@ -73,5 +79,8 @@ export const createApiClient = ({ baseUrl, token, organizationId, }) => {
|
|
|
73
79
|
importExerciseSolution: async (formData) => {
|
|
74
80
|
return await mutation('importExerciseSolution', formData);
|
|
75
81
|
},
|
|
82
|
+
importExerciseSheetSolutions: async (formData) => {
|
|
83
|
+
return await mutation('importExerciseSheetSolutions', formData);
|
|
84
|
+
},
|
|
76
85
|
};
|
|
77
86
|
};
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import { CliCommandError } from './cli-error.js';
|
|
2
|
+
export const cliDefaultLimit = 20;
|
|
3
|
+
export const cliMaxLimit = 100;
|
|
4
|
+
const normalizeOptionalString = (value) => {
|
|
5
|
+
const normalizedValue = value?.trim();
|
|
6
|
+
return normalizedValue ? normalizedValue : undefined;
|
|
7
|
+
};
|
|
8
|
+
export const normalizeOptionalText = (value) => {
|
|
9
|
+
return normalizeOptionalString(value) ?? null;
|
|
10
|
+
};
|
|
11
|
+
export const normalizeLimit = (value) => {
|
|
12
|
+
const normalizedValue = value ?? cliDefaultLimit;
|
|
13
|
+
if (!Number.isInteger(normalizedValue) || normalizedValue < 1 || normalizedValue > cliMaxLimit) {
|
|
14
|
+
throw new CliCommandError(`--limit must be an integer between 1 and ${cliMaxLimit}.`, 2);
|
|
15
|
+
}
|
|
16
|
+
return normalizedValue;
|
|
17
|
+
};
|
|
18
|
+
export const normalizeOffset = (value) => {
|
|
19
|
+
const normalizedValue = value ?? 0;
|
|
20
|
+
if (!Number.isInteger(normalizedValue) || normalizedValue < 0) {
|
|
21
|
+
throw new CliCommandError('--offset must be a non-negative integer.', 2);
|
|
22
|
+
}
|
|
23
|
+
return normalizedValue;
|
|
24
|
+
};
|
|
25
|
+
export const normalizeOptionalInteger = ({ label, value }) => {
|
|
26
|
+
if (value === undefined) {
|
|
27
|
+
return null;
|
|
28
|
+
}
|
|
29
|
+
if (!Number.isInteger(value)) {
|
|
30
|
+
throw new CliCommandError(`${label} must be an integer.`, 2);
|
|
31
|
+
}
|
|
32
|
+
return value;
|
|
33
|
+
};
|
|
34
|
+
export const normalizeChoiceValues = ({ allowedValues, label, values, }) => {
|
|
35
|
+
if (!values || values.length === 0) {
|
|
36
|
+
return undefined;
|
|
37
|
+
}
|
|
38
|
+
const normalizedValues = values.map((value) => value.trim()).filter((value) => value.length > 0);
|
|
39
|
+
const invalidValue = normalizedValues.find((value) => !allowedValues.includes(value));
|
|
40
|
+
if (invalidValue) {
|
|
41
|
+
throw new CliCommandError(`${label} must be one of: ${allowedValues.join(', ')}`, 2);
|
|
42
|
+
}
|
|
43
|
+
return Array.from(new Set(normalizedValues));
|
|
44
|
+
};
|
|
45
|
+
export const normalizeStringValues = (values) => {
|
|
46
|
+
if (!values || values.length === 0) {
|
|
47
|
+
return [];
|
|
48
|
+
}
|
|
49
|
+
return Array.from(new Set(values.map((value) => value.trim()).filter((value) => value.length > 0)));
|
|
50
|
+
};
|
|
51
|
+
export const ownershipOptions = ['own', 'public', 'all'];
|
|
52
|
+
export const normalizeOwnership = (value) => {
|
|
53
|
+
const normalizedValue = normalizeOptionalString(value) ?? 'own';
|
|
54
|
+
if (!ownershipOptions.includes(normalizedValue)) {
|
|
55
|
+
throw new CliCommandError(`--ownership must be one of: ${ownershipOptions.join(', ')}`, 2);
|
|
56
|
+
}
|
|
57
|
+
return normalizedValue;
|
|
58
|
+
};
|
|
59
|
+
export const toApiOwnership = (ownership) => {
|
|
60
|
+
return ownership === 'all' ? null : ownership;
|
|
61
|
+
};
|
|
@@ -65,3 +65,17 @@ export const formatCliImportSummary = (summary) => {
|
|
|
65
65
|
}
|
|
66
66
|
return `Summary: ${parts.join(', ') || '0 jobs'}.`;
|
|
67
67
|
};
|
|
68
|
+
export const formatCliImportTranslationJobs = (translationJobs) => {
|
|
69
|
+
if (!translationJobs || translationJobs.length === 0) {
|
|
70
|
+
return [];
|
|
71
|
+
}
|
|
72
|
+
return [
|
|
73
|
+
`Queued ${translationJobs.length} translation job${translationJobs.length === 1 ? '' : 's'}:`,
|
|
74
|
+
...translationJobs.map((job) => {
|
|
75
|
+
if (job.type === 'exercise_translation_generation') {
|
|
76
|
+
return `${job.jobId} exercise ${job.exerciseId} ${job.languages.join(', ')}`;
|
|
77
|
+
}
|
|
78
|
+
return `${job.jobId} sheet ${job.exerciseSheetId} ${job.language}`;
|
|
79
|
+
}),
|
|
80
|
+
];
|
|
81
|
+
};
|