@chalksurf/cli 0.2.3 → 0.2.4
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 -0
- package/dist/bin/chalksurf.js +6336 -158
- package/docs/agents.md +123 -3
- package/docs/manual.md +43 -0
- package/docs/mcp.md +88 -0
- package/package.json +3 -2
- package/dist/commands/auth.js +0 -174
- package/dist/commands/exercise.js +0 -600
- package/dist/commands/job.js +0 -172
- package/dist/commands/org.js +0 -115
- package/dist/commands/profile.js +0 -97
- package/dist/commands/sheet.js +0 -772
- package/dist/lib/api-client.js +0 -86
- package/dist/lib/cli-error.js +0 -46
- package/dist/lib/command-options.js +0 -61
- package/dist/lib/config-store.js +0 -354
- package/dist/lib/import-files.js +0 -120
- package/dist/lib/import-output.js +0 -81
- package/dist/lib/manifest.js +0 -353
- package/dist/lib/output.js +0 -57
- package/dist/lib/prompt-secret.js +0 -32
- package/dist/lib/session.js +0 -72
- package/dist/lib/source-resolver.js +0 -272
- package/dist/lib/translation-languages.js +0 -16
- package/dist/lib/user-jobs.js +0 -107
package/dist/commands/sheet.js
DELETED
|
@@ -1,772 +0,0 @@
|
|
|
1
|
-
import { z } from 'zod';
|
|
2
|
-
import { createApiClient } from '../lib/api-client.js';
|
|
3
|
-
import { CliCommandError } from '../lib/cli-error.js';
|
|
4
|
-
import { normalizeLimit, normalizeOffset, normalizeOptionalText, normalizeOwnership, toApiOwnership, } from '../lib/command-options.js';
|
|
5
|
-
import { buildFileImportCommandSources, buildFileImportFormData, normalizeWaitTimeoutMs } from '../lib/import-files.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';
|
|
9
|
-
import { cleanupResolvedSources, resolveSources } from '../lib/source-resolver.js';
|
|
10
|
-
import { resolveRequestedTranslateToLanguages, translationLanguages, } from '../lib/translation-languages.js';
|
|
11
|
-
import { formatCliJobSummary, getWaitError, getWaitExitCode, throwSilentExitCode, waitForCliJobs, } from '../lib/user-jobs.js';
|
|
12
|
-
const hasUniqueItems = (values) => new Set(values).size === values.length;
|
|
13
|
-
const sheetImportComponentIdPattern = /^[a-zA-Z0-9_-]{1,80}$/;
|
|
14
|
-
const isValidTargetFolderPath = (value) => {
|
|
15
|
-
const normalizedSegments = value
|
|
16
|
-
.replaceAll('\\', '/')
|
|
17
|
-
.split('/')
|
|
18
|
-
.filter((segment) => segment.length > 0);
|
|
19
|
-
return normalizedSegments.length > 0 && normalizedSegments.every((segment) => segment !== '.' && segment !== '..');
|
|
20
|
-
};
|
|
21
|
-
const assertValidSheetImportPlan = (plan) => {
|
|
22
|
-
if (plan.sheets.length === 0) {
|
|
23
|
-
throw new CliCommandError('Invalid sheet import plan: sheets must include at least one sheet.', 2);
|
|
24
|
-
}
|
|
25
|
-
plan.sheets.forEach((sheet) => {
|
|
26
|
-
if (sheet.sourceIndexes.length === 0) {
|
|
27
|
-
throw new CliCommandError('Invalid sheet import plan: sourceIndexes must include at least one source.', 2);
|
|
28
|
-
}
|
|
29
|
-
if (!sheet.sourceIndexes.every((sourceIndex) => Number.isInteger(sourceIndex) && sourceIndex >= 0)) {
|
|
30
|
-
throw new CliCommandError('Invalid sheet import plan: sourceIndexes must contain only non-negative integers.', 2);
|
|
31
|
-
}
|
|
32
|
-
if (!hasUniqueItems(sheet.sourceIndexes)) {
|
|
33
|
-
throw new CliCommandError('Invalid sheet import plan: sourceIndexes must be unique within a sheet.', 2);
|
|
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
|
-
}
|
|
61
|
-
if (sheet.targetFolderPath !== null && !isValidTargetFolderPath(sheet.targetFolderPath)) {
|
|
62
|
-
throw new CliCommandError('Invalid sheet import plan: targetFolderPath must be a normalized folder path without "." or ".." segments.', 2);
|
|
63
|
-
}
|
|
64
|
-
if (sheet.translateToLanguages && !hasUniqueItems(sheet.translateToLanguages)) {
|
|
65
|
-
throw new CliCommandError('Invalid sheet import plan: translateToLanguages must be unique.', 2);
|
|
66
|
-
}
|
|
67
|
-
});
|
|
68
|
-
};
|
|
69
|
-
const normalizeOptionalTitle = (value) => {
|
|
70
|
-
const normalizedValue = value?.trim();
|
|
71
|
-
if (!normalizedValue) {
|
|
72
|
-
return undefined;
|
|
73
|
-
}
|
|
74
|
-
return normalizedValue;
|
|
75
|
-
};
|
|
76
|
-
const normalizeOptionalTargetFolderPath = (value) => {
|
|
77
|
-
const normalizedValue = value?.trim();
|
|
78
|
-
if (!normalizedValue) {
|
|
79
|
-
return null;
|
|
80
|
-
}
|
|
81
|
-
return normalizedValue;
|
|
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
|
-
};
|
|
97
|
-
const getFolderPathFromRelativePath = (relativePath) => {
|
|
98
|
-
const normalizedSegments = relativePath
|
|
99
|
-
.replaceAll('\\', '/')
|
|
100
|
-
.split('/')
|
|
101
|
-
.filter((segment) => segment.length > 0);
|
|
102
|
-
if (normalizedSegments.length <= 1) {
|
|
103
|
-
return null;
|
|
104
|
-
}
|
|
105
|
-
return normalizedSegments.slice(0, -1).join('/');
|
|
106
|
-
};
|
|
107
|
-
const assertSheetImportFlagUsage = ({ manifestPath, singleSheet, sources, targetFolderPath, title, translateTo, }) => {
|
|
108
|
-
if (manifestPath && singleSheet) {
|
|
109
|
-
throw new CliCommandError('--single-sheet cannot be used with --manifest. Define grouped sheet imports in the manifest.', 2);
|
|
110
|
-
}
|
|
111
|
-
if (manifestPath && targetFolderPath !== null) {
|
|
112
|
-
throw new CliCommandError('--target-folder cannot be used with --manifest. Set targetFolderPath in the manifest.', 2);
|
|
113
|
-
}
|
|
114
|
-
if (targetFolderPath !== null && !singleSheet) {
|
|
115
|
-
throw new CliCommandError('--target-folder can only be used with --single-sheet.', 2);
|
|
116
|
-
}
|
|
117
|
-
if (!title && !translateTo) {
|
|
118
|
-
return;
|
|
119
|
-
}
|
|
120
|
-
if (manifestPath) {
|
|
121
|
-
throw new CliCommandError('--title and --translate-to cannot be used with --manifest. Set the import metadata in the manifest instead.', 2);
|
|
122
|
-
}
|
|
123
|
-
if (!singleSheet && (sources.length !== 1 || sources[0]?.kind === 'directory')) {
|
|
124
|
-
throw new CliCommandError('--title and --translate-to can only be used with a single non-directory source. Use --manifest for multi-source imports.', 2);
|
|
125
|
-
}
|
|
126
|
-
};
|
|
127
|
-
const buildSheetImportSources = ({ sources, title, translateTo, }) => {
|
|
128
|
-
if (!title && !translateTo) {
|
|
129
|
-
return sources;
|
|
130
|
-
}
|
|
131
|
-
return sources.map((source, index) => {
|
|
132
|
-
if (index !== 0) {
|
|
133
|
-
return source;
|
|
134
|
-
}
|
|
135
|
-
return {
|
|
136
|
-
...source,
|
|
137
|
-
title,
|
|
138
|
-
translateTo,
|
|
139
|
-
};
|
|
140
|
-
});
|
|
141
|
-
};
|
|
142
|
-
const buildSourceIndexesByInputIndex = (sources) => {
|
|
143
|
-
const sourceIndexesByInputIndex = new Map();
|
|
144
|
-
sources.forEach((source) => {
|
|
145
|
-
const sourceIndexes = sourceIndexesByInputIndex.get(source.sourceInputIndex) ?? [];
|
|
146
|
-
sourceIndexes.push(source.sourceIndex);
|
|
147
|
-
sourceIndexesByInputIndex.set(source.sourceInputIndex, sourceIndexes);
|
|
148
|
-
});
|
|
149
|
-
return sourceIndexesByInputIndex;
|
|
150
|
-
};
|
|
151
|
-
const resolveGroupedSourceIndexes = ({ groups, sourceIndexesByInputIndex, }) => {
|
|
152
|
-
return groups.map((group, groupIndex) => {
|
|
153
|
-
const sourceIndexes = group.sourceInputIndexes.flatMap((sourceInputIndex) => {
|
|
154
|
-
const resolvedSourceIndexes = sourceIndexesByInputIndex.get(sourceInputIndex);
|
|
155
|
-
if (!resolvedSourceIndexes || resolvedSourceIndexes.length === 0) {
|
|
156
|
-
throw new CliCommandError(`Manifest sheet at index ${groupIndex} references source index ${sourceInputIndex}, but it resolved to no files.`, 2);
|
|
157
|
-
}
|
|
158
|
-
return resolvedSourceIndexes;
|
|
159
|
-
});
|
|
160
|
-
return {
|
|
161
|
-
sourceIndexes,
|
|
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
|
-
}),
|
|
177
|
-
};
|
|
178
|
-
});
|
|
179
|
-
};
|
|
180
|
-
const buildSheetImportPlan = ({ groupedManifestSheets, requestedTargetFolderPath, requestedTitle, requestedTranslateTo, singleSheet, sources, }) => {
|
|
181
|
-
const sourceIndexesByInputIndex = buildSourceIndexesByInputIndex(sources);
|
|
182
|
-
const rawPlan = groupedManifestSheets
|
|
183
|
-
? {
|
|
184
|
-
sheets: resolveGroupedSourceIndexes({
|
|
185
|
-
groups: groupedManifestSheets,
|
|
186
|
-
sourceIndexesByInputIndex,
|
|
187
|
-
}),
|
|
188
|
-
}
|
|
189
|
-
: singleSheet
|
|
190
|
-
? {
|
|
191
|
-
sheets: [
|
|
192
|
-
{
|
|
193
|
-
sourceIndexes: sources.map((source) => source.sourceIndex),
|
|
194
|
-
targetFolderPath: requestedTargetFolderPath,
|
|
195
|
-
titleOverride: requestedTitle,
|
|
196
|
-
translateToLanguages: requestedTranslateTo,
|
|
197
|
-
},
|
|
198
|
-
],
|
|
199
|
-
}
|
|
200
|
-
: {
|
|
201
|
-
sheets: sources.map((source) => ({
|
|
202
|
-
sourceIndexes: [source.sourceIndex],
|
|
203
|
-
targetFolderPath: getFolderPathFromRelativePath(source.relativePath),
|
|
204
|
-
titleOverride: source.title,
|
|
205
|
-
translateToLanguages: source.translateTo,
|
|
206
|
-
})),
|
|
207
|
-
};
|
|
208
|
-
assertValidSheetImportPlan(rawPlan);
|
|
209
|
-
return rawPlan;
|
|
210
|
-
};
|
|
211
|
-
const buildSheetImportJobs = ({ importResultJobs, sources, waitedJobsById, }) => {
|
|
212
|
-
return importResultJobs.map((job, index) => {
|
|
213
|
-
const sourceIndexes = job.sourceIndexes ?? (sources[index] ? [sources[index].sourceIndex] : []);
|
|
214
|
-
const waitedJob = waitedJobsById?.get(job.jobId);
|
|
215
|
-
const importedSheetTranslationJobs = waitedJob?.sheets?.flatMap((sheet) => (sheet.status === 'imported' ? (sheet.translationJobs ?? []) : [])) ?? [];
|
|
216
|
-
return {
|
|
217
|
-
jobId: job.jobId,
|
|
218
|
-
sourceIndexes,
|
|
219
|
-
sourceIds: Array.from(new Set(sourceIndexes
|
|
220
|
-
.map((sourceIndex) => sources[sourceIndex]?.sourceId)
|
|
221
|
-
.filter((sourceId) => sourceId != null))),
|
|
222
|
-
status: waitedJob?.status ?? 'queued',
|
|
223
|
-
error: waitedJob?.error,
|
|
224
|
-
componentIds: job.componentIds,
|
|
225
|
-
exerciseSheetId: waitedJob?.exerciseSheetId,
|
|
226
|
-
sheets: waitedJob?.sheets,
|
|
227
|
-
translationJobs: waitedJob?.translationJobs ?? importedSheetTranslationJobs,
|
|
228
|
-
};
|
|
229
|
-
});
|
|
230
|
-
};
|
|
231
|
-
const buildSheetImportRequest = ({ inputMode, organizationId, timeoutMs, wait, }) => {
|
|
232
|
-
return {
|
|
233
|
-
organizationId,
|
|
234
|
-
inputMode,
|
|
235
|
-
wait,
|
|
236
|
-
timeoutMs,
|
|
237
|
-
target: {},
|
|
238
|
-
};
|
|
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
|
-
};
|
|
272
|
-
const formatSheetJobSourceLabel = ({ job, sources, }) => {
|
|
273
|
-
if (job.sourceIndexes.length !== 1) {
|
|
274
|
-
return `${job.sourceIndexes.length} source${job.sourceIndexes.length === 1 ? '' : 's'}`;
|
|
275
|
-
}
|
|
276
|
-
const firstSource = sources[job.sourceIndexes[0]];
|
|
277
|
-
return firstSource?.relativePath ?? 'unknown source';
|
|
278
|
-
};
|
|
279
|
-
const formatQueuedImportOutput = ({ jobs, summary, sources, }) => {
|
|
280
|
-
return [
|
|
281
|
-
`Queued ${jobs.length} import${jobs.length === 1 ? '' : 's'}.`,
|
|
282
|
-
...jobs.map((job) => `${job.jobId} ${formatSheetJobSourceLabel({ job, sources })}`),
|
|
283
|
-
jobs.length > 1 ? formatCliImportSummary(summary) : null,
|
|
284
|
-
]
|
|
285
|
-
.filter(Boolean)
|
|
286
|
-
.join('\n');
|
|
287
|
-
};
|
|
288
|
-
const formatWaitedImportOutput = ({ jobs, summary, }) => {
|
|
289
|
-
return [
|
|
290
|
-
`${jobs.length} job${jobs.length === 1 ? '' : 's'} queued.`,
|
|
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
|
-
]),
|
|
304
|
-
jobs.length > 1 ? formatCliImportSummary(summary) : null,
|
|
305
|
-
]
|
|
306
|
-
.filter(Boolean)
|
|
307
|
-
.join('\n');
|
|
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
|
-
};
|
|
335
|
-
export const registerSheetCommands = (sheetYargs, context) => {
|
|
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
|
-
})
|
|
398
|
-
.command('import [sources..]', 'Import one or more exercise sheet sources', (importYargs) => importYargs
|
|
399
|
-
.positional('sources', {
|
|
400
|
-
array: true,
|
|
401
|
-
type: 'string',
|
|
402
|
-
describe: 'Local file paths, directories, or HTTP(S) URLs',
|
|
403
|
-
})
|
|
404
|
-
.option('manifest', {
|
|
405
|
-
type: 'string',
|
|
406
|
-
describe: 'Load sources from a manifest file, or "-" for stdin',
|
|
407
|
-
})
|
|
408
|
-
.option('relative-path', {
|
|
409
|
-
type: 'string',
|
|
410
|
-
describe: 'Override the relative path for a single positional source',
|
|
411
|
-
})
|
|
412
|
-
.option('single-sheet', {
|
|
413
|
-
type: 'boolean',
|
|
414
|
-
default: false,
|
|
415
|
-
describe: 'Import all positional sources into one sheet',
|
|
416
|
-
})
|
|
417
|
-
.option('target-folder', {
|
|
418
|
-
type: 'string',
|
|
419
|
-
describe: 'Place a --single-sheet import into this destination folder (defaults to root)',
|
|
420
|
-
})
|
|
421
|
-
.option('title', {
|
|
422
|
-
type: 'string',
|
|
423
|
-
describe: 'Override the imported title for a single-sheet import',
|
|
424
|
-
})
|
|
425
|
-
.option('translate-to', {
|
|
426
|
-
array: true,
|
|
427
|
-
type: 'string',
|
|
428
|
-
describe: `Generate translated sheet content for these languages (${translationLanguages.join(', ')})`,
|
|
429
|
-
})
|
|
430
|
-
.option('timeout-ms', {
|
|
431
|
-
type: 'number',
|
|
432
|
-
default: 300000,
|
|
433
|
-
describe: 'Maximum time to wait before timing out',
|
|
434
|
-
})
|
|
435
|
-
.option('wait', {
|
|
436
|
-
type: 'boolean',
|
|
437
|
-
default: false,
|
|
438
|
-
describe: 'Wait for the queued jobs to finish',
|
|
439
|
-
})
|
|
440
|
-
.example('chalksurf sheet import ./fixtures/algebra.pdf --title "OKTV 2014 Round 1" --translate-to english --wait', 'Import a sheet, override its title, and request an English translation')
|
|
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')
|
|
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')
|
|
443
|
-
.epilogue([
|
|
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.',
|
|
446
|
-
].join('\n')), async (argv) => {
|
|
447
|
-
const config = await context.configStore.loadProfile({ profileName: argv.profile });
|
|
448
|
-
const rawSources = (argv.sources ?? []).map(String);
|
|
449
|
-
const manifestPath = argv.manifest === '' ? '-' : argv.manifest;
|
|
450
|
-
const normalizedRawSources = manifestPath === '-' && rawSources[0] === '-' ? rawSources.slice(1) : rawSources;
|
|
451
|
-
const resolvedBaseUrl = requireResolvedBaseUrl({
|
|
452
|
-
flagValue: argv.baseUrl,
|
|
453
|
-
env: context.env,
|
|
454
|
-
config,
|
|
455
|
-
});
|
|
456
|
-
const resolvedToken = requireResolvedToken({
|
|
457
|
-
env: context.env,
|
|
458
|
-
config,
|
|
459
|
-
});
|
|
460
|
-
const commandSources = await buildFileImportCommandSources({
|
|
461
|
-
cwd: context.cwd,
|
|
462
|
-
loadManifest: loadSheetImportManifest,
|
|
463
|
-
manifestPath,
|
|
464
|
-
rawSources: normalizedRawSources,
|
|
465
|
-
relativePath: argv.relativePath,
|
|
466
|
-
stdin: context.stdin,
|
|
467
|
-
wait: argv.wait,
|
|
468
|
-
});
|
|
469
|
-
const requestedTitle = normalizeOptionalTitle(argv.title);
|
|
470
|
-
const requestedTranslateTo = resolveRequestedTranslateToLanguages(argv.translateTo);
|
|
471
|
-
const requestedTargetFolderPath = normalizeOptionalTargetFolderPath(argv.targetFolder);
|
|
472
|
-
const singleSheet = argv.singleSheet === true;
|
|
473
|
-
assertSheetImportFlagUsage({
|
|
474
|
-
manifestPath,
|
|
475
|
-
singleSheet,
|
|
476
|
-
sources: commandSources.sources,
|
|
477
|
-
targetFolderPath: requestedTargetFolderPath,
|
|
478
|
-
title: requestedTitle,
|
|
479
|
-
translateTo: requestedTranslateTo,
|
|
480
|
-
});
|
|
481
|
-
const sheetSources = buildSheetImportSources({
|
|
482
|
-
sources: commandSources.sources,
|
|
483
|
-
title: requestedTitle,
|
|
484
|
-
translateTo: requestedTranslateTo,
|
|
485
|
-
});
|
|
486
|
-
const organizationId = resolveRequestedOrganizationId({
|
|
487
|
-
flagValue: argv.organization,
|
|
488
|
-
fallbackValue: commandSources.organizationId,
|
|
489
|
-
env: context.env,
|
|
490
|
-
config,
|
|
491
|
-
});
|
|
492
|
-
const apiClient = createApiClient({
|
|
493
|
-
baseUrl: resolvedBaseUrl.value,
|
|
494
|
-
token: resolvedToken.value,
|
|
495
|
-
organizationId,
|
|
496
|
-
});
|
|
497
|
-
let resolvedSources = [];
|
|
498
|
-
try {
|
|
499
|
-
resolvedSources = await resolveSources({
|
|
500
|
-
cwd: context.cwd,
|
|
501
|
-
sources: sheetSources,
|
|
502
|
-
});
|
|
503
|
-
const sources = buildCliImportSources({
|
|
504
|
-
resolvedSources,
|
|
505
|
-
getExtra: (resolvedSource) => ({
|
|
506
|
-
title: resolvedSource.sourceInput.title,
|
|
507
|
-
translateTo: resolvedSource.sourceInput.translateTo,
|
|
508
|
-
}),
|
|
509
|
-
});
|
|
510
|
-
const formData = await buildFileImportFormData(resolvedSources);
|
|
511
|
-
const sheetImportPlan = buildSheetImportPlan({
|
|
512
|
-
groupedManifestSheets: commandSources.sheetGroups,
|
|
513
|
-
requestedTargetFolderPath,
|
|
514
|
-
requestedTitle,
|
|
515
|
-
requestedTranslateTo,
|
|
516
|
-
singleSheet,
|
|
517
|
-
sources,
|
|
518
|
-
});
|
|
519
|
-
formData.set('sheetImportPlan', JSON.stringify(sheetImportPlan));
|
|
520
|
-
const request = buildSheetImportRequest({
|
|
521
|
-
inputMode: manifestPath ? 'manifest' : 'arguments',
|
|
522
|
-
organizationId: organizationId ?? null,
|
|
523
|
-
timeoutMs: commandSources.wait ? normalizeWaitTimeoutMs(argv.timeoutMs) : null,
|
|
524
|
-
wait: commandSources.wait,
|
|
525
|
-
});
|
|
526
|
-
let importResult;
|
|
527
|
-
try {
|
|
528
|
-
importResult = await apiClient.importExerciseSheet(formData);
|
|
529
|
-
}
|
|
530
|
-
catch (error) {
|
|
531
|
-
throw mapApiErrorToCliError(error);
|
|
532
|
-
}
|
|
533
|
-
if (!commandSources.wait) {
|
|
534
|
-
const jobs = buildSheetImportJobs({
|
|
535
|
-
importResultJobs: importResult.jobs,
|
|
536
|
-
sources,
|
|
537
|
-
});
|
|
538
|
-
const summary = summarizeCliImportJobs({
|
|
539
|
-
jobs,
|
|
540
|
-
sourceCount: sources.length,
|
|
541
|
-
sourceInputCount: sheetSources.length,
|
|
542
|
-
timedOut: false,
|
|
543
|
-
});
|
|
544
|
-
context.output.print({
|
|
545
|
-
request,
|
|
546
|
-
sources,
|
|
547
|
-
jobs,
|
|
548
|
-
summary,
|
|
549
|
-
}, (result) => formatQueuedImportOutput({
|
|
550
|
-
jobs: result.jobs,
|
|
551
|
-
summary: result.summary,
|
|
552
|
-
sources: result.sources,
|
|
553
|
-
}), {
|
|
554
|
-
command: 'sheet import',
|
|
555
|
-
});
|
|
556
|
-
return;
|
|
557
|
-
}
|
|
558
|
-
const waitTimeoutMs = normalizeWaitTimeoutMs(argv.timeoutMs);
|
|
559
|
-
const waitResult = await waitForCliJobs({
|
|
560
|
-
getUserJob: async (jobId) => await apiClient.getUserJob(jobId),
|
|
561
|
-
jobIds: importResult.jobs.map((job) => job.jobId),
|
|
562
|
-
now: context.now,
|
|
563
|
-
sleep: context.sleep,
|
|
564
|
-
timeoutMs: waitTimeoutMs,
|
|
565
|
-
});
|
|
566
|
-
const waitedJobsById = new Map(waitResult.jobs.map((job) => [job.id, job]));
|
|
567
|
-
const jobs = buildSheetImportJobs({
|
|
568
|
-
importResultJobs: importResult.jobs,
|
|
569
|
-
sources,
|
|
570
|
-
waitedJobsById,
|
|
571
|
-
});
|
|
572
|
-
const summary = summarizeCliImportJobs({
|
|
573
|
-
jobs,
|
|
574
|
-
sourceCount: sources.length,
|
|
575
|
-
sourceInputCount: sheetSources.length,
|
|
576
|
-
timedOut: waitResult.timedOut,
|
|
577
|
-
});
|
|
578
|
-
const waitError = getWaitError(waitResult);
|
|
579
|
-
context.output.print({
|
|
580
|
-
request: {
|
|
581
|
-
...request,
|
|
582
|
-
timeoutMs: waitTimeoutMs,
|
|
583
|
-
},
|
|
584
|
-
sources,
|
|
585
|
-
jobs,
|
|
586
|
-
summary,
|
|
587
|
-
}, (result) => formatWaitedImportOutput({
|
|
588
|
-
jobs: result.jobs,
|
|
589
|
-
summary: result.summary,
|
|
590
|
-
}), {
|
|
591
|
-
command: 'sheet import',
|
|
592
|
-
ok: waitError == null,
|
|
593
|
-
error: waitError,
|
|
594
|
-
});
|
|
595
|
-
throwSilentExitCode(getWaitExitCode(waitResult));
|
|
596
|
-
}
|
|
597
|
-
finally {
|
|
598
|
-
await cleanupResolvedSources(resolvedSources);
|
|
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
|
-
}
|
|
769
|
-
})
|
|
770
|
-
.demandCommand(1)
|
|
771
|
-
.strict();
|
|
772
|
-
};
|