@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.
@@ -1,11 +1,13 @@
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;
11
13
  const isValidTargetFolderPath = (value) => {
@@ -44,20 +46,6 @@ const normalizeOptionalTitle = (value) => {
44
46
  }
45
47
  return normalizedValue;
46
48
  };
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
49
  const normalizeOptionalTargetFolderPath = (value) => {
62
50
  const normalizedValue = value?.trim();
63
51
  if (!normalizedValue) {
@@ -65,6 +53,20 @@ const normalizeOptionalTargetFolderPath = (value) => {
65
53
  }
66
54
  return normalizedValue;
67
55
  };
56
+ const resolveRequestedUuid = ({ flagValue, fallbackValue, label, required = false, }) => {
57
+ const resolvedValue = flagValue?.trim() || fallbackValue?.trim();
58
+ if (!resolvedValue) {
59
+ if (required) {
60
+ throw new CliCommandError(`${label} is required.`, 2);
61
+ }
62
+ return undefined;
63
+ }
64
+ const parsedValue = z.uuid().safeParse(resolvedValue);
65
+ if (!parsedValue.success) {
66
+ throw new CliCommandError(`${label} must be a valid UUID.`, 2);
67
+ }
68
+ return parsedValue.data;
69
+ };
68
70
  const getFolderPathFromRelativePath = (relativePath) => {
69
71
  const normalizedSegments = relativePath
70
72
  .replaceAll('\\', '/')
@@ -180,6 +182,7 @@ const buildSheetImportJobs = ({ importResultJobs, sources, waitedJobsById, }) =>
180
182
  status: waitedJob?.status ?? 'queued',
181
183
  error: waitedJob?.error,
182
184
  exerciseSheetId: waitedJob?.exerciseSheetId,
185
+ translationJobs: waitedJob?.translationJobs,
183
186
  };
184
187
  });
185
188
  };
@@ -192,6 +195,38 @@ const buildSheetImportRequest = ({ inputMode, organizationId, timeoutMs, wait, }
192
195
  target: {},
193
196
  };
194
197
  };
198
+ const buildSheetSolutionImportRequest = ({ exerciseSheetId, inputMode, organizationId, timeoutMs, wait, }) => {
199
+ return {
200
+ organizationId,
201
+ inputMode,
202
+ wait,
203
+ timeoutMs,
204
+ target: { exerciseSheetId },
205
+ };
206
+ };
207
+ const buildQueuedSheetSolutionImportJob = ({ jobId, sources, }) => {
208
+ return {
209
+ jobId,
210
+ sourceIndexes: sources.map((source) => source.sourceIndex),
211
+ sourceIds: sources.map((source) => source.sourceId).filter((sourceId) => sourceId !== null),
212
+ status: 'queued',
213
+ };
214
+ };
215
+ const buildWaitedSheetSolutionImportJob = ({ jobId, sources, waitedJob, }) => {
216
+ return {
217
+ jobId,
218
+ sourceIndexes: sources.map((source) => source.sourceIndex),
219
+ sourceIds: sources.map((source) => source.sourceId).filter((sourceId) => sourceId !== null),
220
+ status: waitedJob?.status ?? 'pending',
221
+ error: waitedJob?.error,
222
+ eligibleExerciseCount: waitedJob?.eligibleExerciseCount,
223
+ exerciseSheetId: waitedJob?.exerciseSheetId,
224
+ nonUpdatableExerciseCount: waitedJob?.nonUpdatableExerciseCount,
225
+ skippedExerciseCount: waitedJob?.skippedExerciseCount,
226
+ unmatchedImportedSolutionCount: waitedJob?.unmatchedImportedSolutionCount,
227
+ updatedExerciseCount: waitedJob?.updatedExerciseCount,
228
+ };
229
+ };
195
230
  const formatSheetJobSourceLabel = ({ job, sources, }) => {
196
231
  if (job.sourceIndexes.length !== 1) {
197
232
  return `${job.sourceIndexes.length} source${job.sourceIndexes.length === 1 ? '' : 's'}`;
@@ -211,22 +246,112 @@ const formatQueuedImportOutput = ({ jobs, summary, sources, }) => {
211
246
  const formatWaitedImportOutput = ({ jobs, summary, }) => {
212
247
  return [
213
248
  `${jobs.length} job${jobs.length === 1 ? '' : 's'} queued.`,
214
- ...jobs.map((job) => formatCliJobSummary({
215
- id: job.jobId,
216
- status: job.status === 'queued' ? 'pending' : job.status,
217
- type: 'exercise_sheet_import',
218
- createdAt: '',
219
- updatedAt: '',
220
- error: job.error,
221
- exerciseSheetId: job.exerciseSheetId,
222
- })),
249
+ ...jobs.flatMap((job) => [
250
+ formatCliJobSummary({
251
+ id: job.jobId,
252
+ status: job.status === 'queued' ? 'pending' : job.status,
253
+ type: 'exercise_sheet_import',
254
+ createdAt: '',
255
+ updatedAt: '',
256
+ error: job.error,
257
+ exerciseSheetId: job.exerciseSheetId,
258
+ }),
259
+ ...formatCliImportTranslationJobs(job.translationJobs),
260
+ ]),
223
261
  jobs.length > 1 ? formatCliImportSummary(summary) : null,
224
262
  ]
225
263
  .filter(Boolean)
226
264
  .join('\n');
227
265
  };
266
+ const formatQueuedSheetSolutionImportOutput = (job) => {
267
+ return [
268
+ 'Queued exercise sheet solution import.',
269
+ `${job.jobId} ${job.sourceIndexes.length} source${job.sourceIndexes.length === 1 ? '' : 's'}`,
270
+ ].join('\n');
271
+ };
272
+ const formatWaitedSheetSolutionImportOutput = (job) => {
273
+ return formatCliJobSummary({
274
+ id: job.jobId,
275
+ status: job.status === 'queued' ? 'pending' : job.status,
276
+ type: 'exercise_sheet_solution_import',
277
+ createdAt: '',
278
+ updatedAt: '',
279
+ error: job.error,
280
+ exerciseSheetId: job.exerciseSheetId,
281
+ });
282
+ };
283
+ const formatSheetSearchOutput = ({ exerciseSheets, totalCount, }) => {
284
+ if (exerciseSheets.length === 0) {
285
+ return 'No sheets found.';
286
+ }
287
+ return [
288
+ `${exerciseSheets.length} of ${totalCount} sheet${totalCount === 1 ? '' : 's'} returned.`,
289
+ ...exerciseSheets.map((sheet) => `${sheet.id} ${sheet.name} ${sheet.exerciseCount} exercises`),
290
+ ].join('\n');
291
+ };
228
292
  export const registerSheetCommands = (sheetYargs, context) => {
229
293
  return sheetYargs
294
+ .command('search', 'Search exercise sheets visible to the selected organization', (searchYargs) => searchYargs
295
+ .option('text', {
296
+ type: 'string',
297
+ describe: 'Search sheet names',
298
+ })
299
+ .option('ownership', {
300
+ type: 'string',
301
+ default: 'own',
302
+ describe: 'Visibility scope: own, public, or all',
303
+ })
304
+ .option('limit', {
305
+ type: 'number',
306
+ default: 20,
307
+ describe: 'Maximum number of sheets to return',
308
+ })
309
+ .option('offset', {
310
+ type: 'number',
311
+ default: 0,
312
+ describe: 'Number of matching sheets to skip',
313
+ })
314
+ .example('chalksurf sheet search --text "OKTV 2014" --ownership own --json', 'Verify an imported private sheet in the selected organization')
315
+ .example('chalksurf --profile prod-codex sheet search --ownership own --limit 10 --json', 'List recent sheets visible to the agent profile'), async (argv) => {
316
+ const text = normalizeOptionalText(argv.text);
317
+ const ownership = normalizeOwnership(argv.ownership);
318
+ const limit = normalizeLimit(argv.limit);
319
+ const offset = normalizeOffset(argv.offset);
320
+ const { apiClient, organizationId } = await createResolvedApiClient({
321
+ context,
322
+ baseUrlFlagValue: argv.baseUrl,
323
+ organizationFlagValue: argv.organization,
324
+ profileName: argv.profile,
325
+ });
326
+ let searchResult;
327
+ try {
328
+ searchResult = await apiClient.searchExerciseSheets({
329
+ text,
330
+ ownership: toApiOwnership(ownership),
331
+ limit,
332
+ offset,
333
+ });
334
+ }
335
+ catch (error) {
336
+ throw mapApiErrorToCliError(error);
337
+ }
338
+ context.output.print({
339
+ request: {
340
+ text,
341
+ ownership,
342
+ limit,
343
+ offset,
344
+ organizationId: organizationId ?? null,
345
+ },
346
+ exerciseSheets: searchResult.exerciseSheets,
347
+ totalCount: searchResult.totalCount,
348
+ }, (result) => formatSheetSearchOutput({
349
+ exerciseSheets: result.exerciseSheets,
350
+ totalCount: result.totalCount,
351
+ }), {
352
+ command: 'sheet search',
353
+ });
354
+ })
230
355
  .command('import [sources..]', 'Import one or more exercise sheet sources', (importYargs) => importYargs
231
356
  .positional('sources', {
232
357
  array: true,
@@ -273,8 +398,8 @@ export const registerSheetCommands = (sheetYargs, context) => {
273
398
  .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
399
  .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
400
  .epilogue([
276
- 'When --wait is set, the command exits only after the full requested import is complete.',
277
- 'For translated sheet imports, that includes generating and storing the requested translations.',
401
+ 'When --wait is set, the command exits after the queued import job reaches a terminal state.',
402
+ 'For translated sheet imports, requested translations continue as separate jobs listed in the import result.',
278
403
  ].join('\n')), async (argv) => {
279
404
  const config = await context.configStore.loadProfile({ profileName: argv.profile });
280
405
  const rawSources = (argv.sources ?? []).map(String);
@@ -429,6 +554,175 @@ export const registerSheetCommands = (sheetYargs, context) => {
429
554
  finally {
430
555
  await cleanupResolvedSources(resolvedSources);
431
556
  }
557
+ })
558
+ .command('import-solutions [exerciseSheetId] [sources..]', 'Import one or more solution files into an existing exercise sheet', (importSolutionsYargs) => importSolutionsYargs
559
+ .positional('exerciseSheetId', {
560
+ type: 'string',
561
+ describe: 'Exercise sheet ID to reconcile imported solutions into',
562
+ })
563
+ .positional('sources', {
564
+ array: true,
565
+ type: 'string',
566
+ describe: 'Local file paths, directories, or HTTP(S) URLs',
567
+ })
568
+ .option('manifest', {
569
+ type: 'string',
570
+ describe: 'Load sources from a manifest file, or "-" for stdin',
571
+ })
572
+ .option('relative-path', {
573
+ type: 'string',
574
+ describe: 'Override the relative path for a single positional source',
575
+ })
576
+ .option('timeout-ms', {
577
+ type: 'number',
578
+ default: 300000,
579
+ describe: 'Maximum time to wait before timing out',
580
+ })
581
+ .option('wait', {
582
+ type: 'boolean',
583
+ default: false,
584
+ describe: 'Wait for the queued job to finish',
585
+ })
586
+ .example('chalksurf sheet import-solutions 00000000-0000-4000-8000-000000000001 ./fixtures/solutions.pdf --wait', 'Attach a separate solution file to an existing exercise sheet')
587
+ .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')
588
+ .epilogue('When --wait is set, the command exits after the queued import job reaches a terminal state.'), async (argv) => {
589
+ const config = await context.configStore.loadProfile({ profileName: argv.profile });
590
+ const rawSources = (argv.sources ?? []).map(String);
591
+ const manifestPath = argv.manifest === '' ? '-' : argv.manifest;
592
+ const normalizedRawSources = manifestPath === '-' && rawSources[0] === '-' ? rawSources.slice(1) : rawSources;
593
+ const resolvedBaseUrl = requireResolvedBaseUrl({
594
+ flagValue: argv.baseUrl,
595
+ env: context.env,
596
+ config,
597
+ });
598
+ const resolvedToken = requireResolvedToken({
599
+ env: context.env,
600
+ config,
601
+ });
602
+ const commandSources = await buildFileImportCommandSources({
603
+ cwd: context.cwd,
604
+ loadManifest: loadExerciseSheetSolutionImportManifest,
605
+ manifestPath,
606
+ rawSources: normalizedRawSources,
607
+ relativePath: argv.relativePath,
608
+ stdin: context.stdin,
609
+ wait: argv.wait,
610
+ });
611
+ const organizationId = resolveRequestedOrganizationId({
612
+ flagValue: argv.organization,
613
+ fallbackValue: commandSources.organizationId,
614
+ env: context.env,
615
+ config,
616
+ });
617
+ const exerciseSheetId = resolveRequestedUuid({
618
+ flagValue: argv.exerciseSheetId,
619
+ fallbackValue: commandSources.exerciseSheetId,
620
+ label: 'exerciseSheetId',
621
+ required: true,
622
+ });
623
+ const apiClient = createApiClient({
624
+ baseUrl: resolvedBaseUrl.value,
625
+ token: resolvedToken.value,
626
+ organizationId,
627
+ });
628
+ let resolvedSources = [];
629
+ try {
630
+ resolvedSources = await resolveSources({
631
+ cwd: context.cwd,
632
+ sources: commandSources.sources,
633
+ });
634
+ const sources = buildCliImportSources({
635
+ resolvedSources,
636
+ });
637
+ const formData = await buildFileImportFormData(resolvedSources);
638
+ formData.set('exerciseSheetId', exerciseSheetId);
639
+ const request = buildSheetSolutionImportRequest({
640
+ exerciseSheetId,
641
+ inputMode: manifestPath ? 'manifest' : 'arguments',
642
+ organizationId: organizationId ?? null,
643
+ timeoutMs: commandSources.wait ? normalizeWaitTimeoutMs(argv.timeoutMs) : null,
644
+ wait: commandSources.wait,
645
+ });
646
+ let importResult;
647
+ try {
648
+ importResult = await apiClient.importExerciseSheetSolutions(formData);
649
+ }
650
+ catch (error) {
651
+ throw mapApiErrorToCliError(error);
652
+ }
653
+ if (!commandSources.wait) {
654
+ const jobs = [
655
+ buildQueuedSheetSolutionImportJob({
656
+ jobId: importResult.jobId,
657
+ sources,
658
+ }),
659
+ ];
660
+ const summary = summarizeCliImportJobs({
661
+ jobs,
662
+ sourceCount: sources.length,
663
+ sourceInputCount: commandSources.sources.length,
664
+ timedOut: false,
665
+ });
666
+ context.output.print({
667
+ request,
668
+ sources,
669
+ jobs,
670
+ summary,
671
+ }, (result) => formatQueuedSheetSolutionImportOutput(result.jobs[0]), {
672
+ command: 'sheet import-solutions',
673
+ });
674
+ return;
675
+ }
676
+ const waitTimeoutMs = normalizeWaitTimeoutMs(argv.timeoutMs);
677
+ const waitResult = await waitForCliJobs({
678
+ getUserJob: async (jobId) => await apiClient.getUserJob(jobId),
679
+ jobIds: [importResult.jobId],
680
+ now: context.now,
681
+ sleep: context.sleep,
682
+ timeoutMs: waitTimeoutMs,
683
+ });
684
+ const waitedJob = waitResult.jobs[0];
685
+ const jobs = [
686
+ buildWaitedSheetSolutionImportJob({
687
+ jobId: importResult.jobId,
688
+ sources,
689
+ waitedJob: waitedJob == null
690
+ ? undefined
691
+ : {
692
+ ...waitedJob,
693
+ id: waitedJob.id,
694
+ status: waitedJob.status,
695
+ },
696
+ }),
697
+ ];
698
+ const summary = summarizeCliImportJobs({
699
+ jobs,
700
+ sourceCount: sources.length,
701
+ sourceInputCount: commandSources.sources.length,
702
+ timedOut: waitResult.timedOut,
703
+ });
704
+ const waitError = getWaitError({
705
+ jobs: waitResult.jobs,
706
+ timedOut: waitResult.timedOut,
707
+ });
708
+ context.output.print({
709
+ request: {
710
+ ...request,
711
+ timeoutMs: waitTimeoutMs,
712
+ },
713
+ sources,
714
+ jobs,
715
+ summary,
716
+ }, (result) => formatWaitedSheetSolutionImportOutput(result.jobs[0]), {
717
+ command: 'sheet import-solutions',
718
+ ok: waitError == null,
719
+ error: waitError,
720
+ });
721
+ throwSilentExitCode(getWaitExitCode({ jobs: waitResult.jobs, timedOut: waitResult.timedOut }));
722
+ }
723
+ finally {
724
+ await cleanupResolvedSources(resolvedSources);
725
+ }
432
726
  })
433
727
  .demandCommand(1)
434
728
  .strict();
@@ -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
+ };
@@ -161,6 +161,10 @@ const parseExerciseImportManifest = (manifestText) => {
161
161
  parsedManifest,
162
162
  topLevelFields: ['exerciseSheetId'],
163
163
  }),
164
+ translateTo: parseTranslateTo({
165
+ value: parsedManifest.translateTo,
166
+ label: 'Manifest',
167
+ }),
164
168
  sources,
165
169
  };
166
170
  };
@@ -176,6 +180,18 @@ const parseExerciseSolutionImportManifest = (manifestText) => {
176
180
  sources,
177
181
  };
178
182
  };
183
+ const parseExerciseSheetSolutionImportManifest = (manifestText) => {
184
+ const parsedManifest = parseSourceManifestObject(manifestText);
185
+ const sources = parsedManifest.sources.map((source, index) => parseBaseManifestSource(source, index));
186
+ assertUniqueSourceIds(sources);
187
+ return {
188
+ ...parseManifestMetadata({
189
+ parsedManifest,
190
+ topLevelFields: ['exerciseSheetId'],
191
+ }),
192
+ sources,
193
+ };
194
+ };
179
195
  const parseSheetImportManifest = (manifestText) => {
180
196
  const parsedManifest = parseManifestObject(manifestText);
181
197
  if ('sources' in parsedManifest) {
@@ -261,6 +277,14 @@ export const loadExerciseSolutionImportManifest = async ({ cwd, manifestPath, st
261
277
  stdin,
262
278
  });
263
279
  };
280
+ export const loadExerciseSheetSolutionImportManifest = async ({ cwd, manifestPath, stdin, }) => {
281
+ return await loadManifest({
282
+ cwd,
283
+ manifestPath,
284
+ parseManifest: parseExerciseSheetSolutionImportManifest,
285
+ stdin,
286
+ });
287
+ };
264
288
  export const loadSheetImportManifest = async ({ cwd, manifestPath, stdin, }) => {
265
289
  return await loadManifest({
266
290
  cwd,
@@ -1,3 +1,4 @@
1
+ import { createApiClient } from './api-client.js';
1
2
  import { CliCommandError } from './cli-error.js';
2
3
  import { chalksurfBaseUrlEnvVar, chalksurfOrganizationIdEnvVar, chalksurfTokenEnvVar, resolveBaseUrl, resolveToken, } from './config-store.js';
3
4
  const normalizeOptionalString = (value) => {
@@ -31,6 +32,32 @@ export const resolveRequestedOrganizationId = ({ config, env, fallbackValue, fla
31
32
  normalizeOptionalString(env[chalksurfOrganizationIdEnvVar]) ??
32
33
  normalizeOptionalString(config.organizationId));
33
34
  };
35
+ export const createResolvedApiClient = async ({ context, baseUrlFlagValue, organizationFallbackValue, organizationFlagValue, profileName, }) => {
36
+ const config = await context.configStore.loadProfile({ profileName });
37
+ const resolvedBaseUrl = requireResolvedBaseUrl({
38
+ flagValue: baseUrlFlagValue,
39
+ env: context.env,
40
+ config,
41
+ });
42
+ const resolvedToken = requireResolvedToken({
43
+ env: context.env,
44
+ config,
45
+ });
46
+ const organizationId = resolveRequestedOrganizationId({
47
+ flagValue: organizationFlagValue,
48
+ fallbackValue: organizationFallbackValue,
49
+ env: context.env,
50
+ config,
51
+ });
52
+ return {
53
+ apiClient: createApiClient({
54
+ baseUrl: resolvedBaseUrl.value,
55
+ token: resolvedToken.value,
56
+ organizationId,
57
+ }),
58
+ organizationId,
59
+ };
60
+ };
34
61
  export const formatUserIdentity = (profile) => {
35
62
  if (profile.name && profile.email) {
36
63
  return `${profile.name} <${profile.email}>`;
@@ -1 +1,16 @@
1
+ import { CliCommandError } from './cli-error.js';
1
2
  export const translationLanguages = ['english', 'hungarian', 'german', 'french', 'spanish', 'italian'];
3
+ export const resolveRequestedTranslateToLanguages = (rawValues) => {
4
+ if (!rawValues || rawValues.length === 0) {
5
+ return undefined;
6
+ }
7
+ const invalidLanguage = rawValues.find((value) => !translationLanguages.includes(value));
8
+ if (invalidLanguage) {
9
+ throw new CliCommandError(`--translate-to must be one of: ${translationLanguages.join(', ')}`, 2);
10
+ }
11
+ const languages = rawValues;
12
+ if (new Set(languages).size !== languages.length) {
13
+ throw new CliCommandError('--translate-to languages must be unique.', 2);
14
+ }
15
+ return languages;
16
+ };
@@ -14,7 +14,13 @@ export const serializeCliJob = (job) => {
14
14
  exerciseId: job.result?.exerciseId,
15
15
  exerciseIds: job.result?.exerciseIds,
16
16
  exerciseSheetId: job.result?.exerciseSheetId,
17
+ eligibleExerciseCount: job.result?.eligibleExerciseCount,
18
+ nonUpdatableExerciseCount: job.result?.nonUpdatableExerciseCount,
17
19
  resultCode: job.result?.resultCode,
20
+ skippedExerciseCount: job.result?.skippedExerciseCount,
21
+ translationJobs: job.result?.translationJobs,
22
+ unmatchedImportedSolutionCount: job.result?.unmatchedImportedSolutionCount,
23
+ updatedExerciseCount: job.result?.updatedExerciseCount,
18
24
  };
19
25
  };
20
26
  export const formatCliJobSummary = (job) => {