@chalksurf/cli 0.1.0 → 0.2.1
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 +31 -94
- package/dist/bin/chalksurf.js +84 -10
- package/dist/commands/auth.js +43 -24
- package/dist/commands/exercise.js +428 -0
- package/dist/commands/job.js +20 -10
- package/dist/commands/org.js +20 -11
- package/dist/commands/profile.js +97 -0
- package/dist/commands/sheet.js +287 -130
- package/dist/lib/api-client.js +9 -3
- package/dist/lib/cli-error.js +37 -1
- package/dist/lib/config-store.js +188 -10
- package/dist/lib/import-files.js +120 -0
- package/dist/lib/import-output.js +67 -0
- package/dist/lib/manifest.js +183 -19
- package/dist/lib/output.js +35 -2
- package/dist/lib/prompt-secret.js +32 -0
- package/dist/lib/session.js +1 -1
- package/dist/lib/source-resolver.js +72 -10
- package/dist/lib/translation-languages.js +1 -0
- package/dist/lib/user-jobs.js +16 -1
- package/docs/agents.md +203 -0
- package/docs/examples/exercise-import-manifest.json +13 -0
- package/docs/examples/exercise-solution-import-manifest.json +13 -0
- package/docs/examples/sheet-import-manifest.json +33 -0
- package/docs/exit-codes.md +57 -0
- package/docs/manifest.md +129 -0
- package/docs/manual.md +169 -0
- package/package.json +8 -4
- package/schemas/exercise-import-manifest.schema.json +104 -0
- package/schemas/exercise-solution-import-manifest.schema.json +104 -0
- package/schemas/sheet-import-manifest.schema.json +142 -0
package/dist/commands/sheet.js
CHANGED
|
@@ -1,163 +1,226 @@
|
|
|
1
|
-
import { openAsBlob } from 'node:fs';
|
|
2
|
-
import { stat } from 'node:fs/promises';
|
|
3
|
-
import { extname, resolve } from 'node:path';
|
|
4
1
|
import { createApiClient } from '../lib/api-client.js';
|
|
5
2
|
import { CliCommandError } from '../lib/cli-error.js';
|
|
6
|
-
import {
|
|
3
|
+
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';
|
|
7
6
|
import { mapApiErrorToCliError, requireResolvedBaseUrl, requireResolvedToken, resolveRequestedOrganizationId, } from '../lib/session.js';
|
|
8
|
-
import { cleanupResolvedSources, resolveSources
|
|
9
|
-
import {
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
'.
|
|
18
|
-
'.pdf': 'application/pdf',
|
|
19
|
-
'.png': 'image/png',
|
|
20
|
-
'.svg': 'image/svg+xml',
|
|
21
|
-
'.tex': 'text/plain',
|
|
22
|
-
'.tif': 'image/tiff',
|
|
23
|
-
'.tiff': 'image/tiff',
|
|
24
|
-
'.txt': 'text/plain',
|
|
25
|
-
'.webp': 'image/webp',
|
|
7
|
+
import { cleanupResolvedSources, resolveSources } from '../lib/source-resolver.js';
|
|
8
|
+
import { translationLanguages } from '../lib/translation-languages.js';
|
|
9
|
+
import { formatCliJobSummary, getWaitError, getWaitExitCode, throwSilentExitCode, waitForCliJobs, } from '../lib/user-jobs.js';
|
|
10
|
+
const hasUniqueItems = (values) => new Set(values).size === values.length;
|
|
11
|
+
const isValidTargetFolderPath = (value) => {
|
|
12
|
+
const normalizedSegments = value
|
|
13
|
+
.replaceAll('\\', '/')
|
|
14
|
+
.split('/')
|
|
15
|
+
.filter((segment) => segment.length > 0);
|
|
16
|
+
return normalizedSegments.length > 0 && normalizedSegments.every((segment) => segment !== '.' && segment !== '..');
|
|
26
17
|
};
|
|
27
|
-
const
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
throw new CliCommandError('--timeout-ms must be a positive number.', 2);
|
|
18
|
+
const assertValidSheetImportPlan = (plan) => {
|
|
19
|
+
if (plan.sheets.length === 0) {
|
|
20
|
+
throw new CliCommandError('Invalid sheet import plan: sheets must include at least one sheet.', 2);
|
|
31
21
|
}
|
|
32
|
-
|
|
22
|
+
plan.sheets.forEach((sheet) => {
|
|
23
|
+
if (sheet.sourceIndexes.length === 0) {
|
|
24
|
+
throw new CliCommandError('Invalid sheet import plan: sourceIndexes must include at least one source.', 2);
|
|
25
|
+
}
|
|
26
|
+
if (!sheet.sourceIndexes.every((sourceIndex) => Number.isInteger(sourceIndex) && sourceIndex >= 0)) {
|
|
27
|
+
throw new CliCommandError('Invalid sheet import plan: sourceIndexes must contain only non-negative integers.', 2);
|
|
28
|
+
}
|
|
29
|
+
if (!hasUniqueItems(sheet.sourceIndexes)) {
|
|
30
|
+
throw new CliCommandError('Invalid sheet import plan: sourceIndexes must be unique within a sheet.', 2);
|
|
31
|
+
}
|
|
32
|
+
if (sheet.targetFolderPath !== null && !isValidTargetFolderPath(sheet.targetFolderPath)) {
|
|
33
|
+
throw new CliCommandError('Invalid sheet import plan: targetFolderPath must be a normalized folder path without "." or ".." segments.', 2);
|
|
34
|
+
}
|
|
35
|
+
if (sheet.translateToLanguages && !hasUniqueItems(sheet.translateToLanguages)) {
|
|
36
|
+
throw new CliCommandError('Invalid sheet import plan: translateToLanguages must be unique.', 2);
|
|
37
|
+
}
|
|
38
|
+
});
|
|
33
39
|
};
|
|
34
|
-
const
|
|
35
|
-
|
|
40
|
+
const normalizeOptionalTitle = (value) => {
|
|
41
|
+
const normalizedValue = value?.trim();
|
|
42
|
+
if (!normalizedValue) {
|
|
43
|
+
return undefined;
|
|
44
|
+
}
|
|
45
|
+
return normalizedValue;
|
|
36
46
|
};
|
|
37
|
-
const
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
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);
|
|
41
58
|
}
|
|
42
|
-
|
|
43
|
-
|
|
59
|
+
return languages;
|
|
60
|
+
};
|
|
61
|
+
const normalizeOptionalTargetFolderPath = (value) => {
|
|
62
|
+
const normalizedValue = value?.trim();
|
|
63
|
+
if (!normalizedValue) {
|
|
64
|
+
return null;
|
|
44
65
|
}
|
|
66
|
+
return normalizedValue;
|
|
45
67
|
};
|
|
46
|
-
const
|
|
47
|
-
const
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
return formData;
|
|
68
|
+
const getFolderPathFromRelativePath = (relativePath) => {
|
|
69
|
+
const normalizedSegments = relativePath
|
|
70
|
+
.replaceAll('\\', '/')
|
|
71
|
+
.split('/')
|
|
72
|
+
.filter((segment) => segment.length > 0);
|
|
73
|
+
if (normalizedSegments.length <= 1) {
|
|
74
|
+
return null;
|
|
75
|
+
}
|
|
76
|
+
return normalizedSegments.slice(0, -1).join('/');
|
|
56
77
|
};
|
|
57
|
-
const
|
|
58
|
-
if (manifestPath &&
|
|
59
|
-
throw new CliCommandError('
|
|
78
|
+
const assertSheetImportFlagUsage = ({ manifestPath, singleSheet, sources, targetFolderPath, title, translateTo, }) => {
|
|
79
|
+
if (manifestPath && singleSheet) {
|
|
80
|
+
throw new CliCommandError('--single-sheet cannot be used with --manifest. Define grouped sheet imports in the manifest.', 2);
|
|
81
|
+
}
|
|
82
|
+
if (manifestPath && targetFolderPath !== null) {
|
|
83
|
+
throw new CliCommandError('--target-folder cannot be used with --manifest. Set targetFolderPath in the manifest.', 2);
|
|
60
84
|
}
|
|
61
|
-
if (
|
|
62
|
-
throw new CliCommandError('--
|
|
85
|
+
if (targetFolderPath !== null && !singleSheet) {
|
|
86
|
+
throw new CliCommandError('--target-folder can only be used with --single-sheet.', 2);
|
|
87
|
+
}
|
|
88
|
+
if (!title && !translateTo) {
|
|
89
|
+
return;
|
|
63
90
|
}
|
|
64
91
|
if (manifestPath) {
|
|
65
|
-
|
|
66
|
-
cwd,
|
|
67
|
-
manifestPath,
|
|
68
|
-
stdin,
|
|
69
|
-
});
|
|
70
|
-
return {
|
|
71
|
-
organizationId: manifest.organizationId,
|
|
72
|
-
sources: manifest.sources,
|
|
73
|
-
wait: wait || manifest.wait === true,
|
|
74
|
-
};
|
|
92
|
+
throw new CliCommandError('--title and --translate-to cannot be used with --manifest. Set the import metadata in the manifest instead.', 2);
|
|
75
93
|
}
|
|
76
|
-
if (
|
|
77
|
-
throw new CliCommandError('
|
|
94
|
+
if (!singleSheet && (sources.length !== 1 || sources[0]?.kind === 'directory')) {
|
|
95
|
+
throw new CliCommandError('--title and --translate-to can only be used with a single non-directory source. Use --manifest for multi-source imports.', 2);
|
|
78
96
|
}
|
|
79
|
-
|
|
80
|
-
|
|
97
|
+
};
|
|
98
|
+
const buildSheetImportSources = ({ sources, title, translateTo, }) => {
|
|
99
|
+
if (!title && !translateTo) {
|
|
100
|
+
return sources;
|
|
81
101
|
}
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
sources.push({
|
|
86
|
-
kind: 'url',
|
|
87
|
-
url: rawSource,
|
|
88
|
-
relativePath,
|
|
89
|
-
});
|
|
90
|
-
continue;
|
|
102
|
+
return sources.map((source, index) => {
|
|
103
|
+
if (index !== 0) {
|
|
104
|
+
return source;
|
|
91
105
|
}
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
106
|
+
return {
|
|
107
|
+
...source,
|
|
108
|
+
title,
|
|
109
|
+
translateTo,
|
|
110
|
+
};
|
|
111
|
+
});
|
|
112
|
+
};
|
|
113
|
+
const buildSourceIndexesByInputIndex = (sources) => {
|
|
114
|
+
const sourceIndexesByInputIndex = new Map();
|
|
115
|
+
sources.forEach((source) => {
|
|
116
|
+
const sourceIndexes = sourceIndexesByInputIndex.get(source.sourceInputIndex) ?? [];
|
|
117
|
+
sourceIndexes.push(source.sourceIndex);
|
|
118
|
+
sourceIndexesByInputIndex.set(source.sourceInputIndex, sourceIndexes);
|
|
119
|
+
});
|
|
120
|
+
return sourceIndexesByInputIndex;
|
|
121
|
+
};
|
|
122
|
+
const resolveGroupedSourceIndexes = ({ groups, sourceIndexesByInputIndex, }) => {
|
|
123
|
+
return groups.map((group, groupIndex) => {
|
|
124
|
+
const sourceIndexes = group.sourceInputIndexes.flatMap((sourceInputIndex) => {
|
|
125
|
+
const resolvedSourceIndexes = sourceIndexesByInputIndex.get(sourceInputIndex);
|
|
126
|
+
if (!resolvedSourceIndexes || resolvedSourceIndexes.length === 0) {
|
|
127
|
+
throw new CliCommandError(`Manifest sheet at index ${groupIndex} references source index ${sourceInputIndex}, but it resolved to no files.`, 2);
|
|
103
128
|
}
|
|
129
|
+
return resolvedSourceIndexes;
|
|
130
|
+
});
|
|
131
|
+
return {
|
|
132
|
+
sourceIndexes,
|
|
133
|
+
targetFolderPath: group.targetFolderPath,
|
|
134
|
+
titleOverride: group.title,
|
|
135
|
+
translateToLanguages: group.translateTo,
|
|
136
|
+
};
|
|
137
|
+
});
|
|
138
|
+
};
|
|
139
|
+
const buildSheetImportPlan = ({ groupedManifestSheets, requestedTargetFolderPath, requestedTitle, requestedTranslateTo, singleSheet, sources, }) => {
|
|
140
|
+
const sourceIndexesByInputIndex = buildSourceIndexesByInputIndex(sources);
|
|
141
|
+
const rawPlan = groupedManifestSheets
|
|
142
|
+
? {
|
|
143
|
+
sheets: resolveGroupedSourceIndexes({
|
|
144
|
+
groups: groupedManifestSheets,
|
|
145
|
+
sourceIndexesByInputIndex,
|
|
146
|
+
}),
|
|
104
147
|
}
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
148
|
+
: singleSheet
|
|
149
|
+
? {
|
|
150
|
+
sheets: [
|
|
151
|
+
{
|
|
152
|
+
sourceIndexes: sources.map((source) => source.sourceIndex),
|
|
153
|
+
targetFolderPath: requestedTargetFolderPath,
|
|
154
|
+
titleOverride: requestedTitle,
|
|
155
|
+
translateToLanguages: requestedTranslateTo,
|
|
156
|
+
},
|
|
157
|
+
],
|
|
108
158
|
}
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
wait: wait === true,
|
|
120
|
-
};
|
|
159
|
+
: {
|
|
160
|
+
sheets: sources.map((source) => ({
|
|
161
|
+
sourceIndexes: [source.sourceIndex],
|
|
162
|
+
targetFolderPath: getFolderPathFromRelativePath(source.relativePath),
|
|
163
|
+
titleOverride: source.title,
|
|
164
|
+
translateToLanguages: source.translateTo,
|
|
165
|
+
})),
|
|
166
|
+
};
|
|
167
|
+
assertValidSheetImportPlan(rawPlan);
|
|
168
|
+
return rawPlan;
|
|
121
169
|
};
|
|
122
|
-
const buildSheetImportJobs = ({ importResultJobs,
|
|
170
|
+
const buildSheetImportJobs = ({ importResultJobs, sources, waitedJobsById, }) => {
|
|
123
171
|
return importResultJobs.map((job, index) => {
|
|
124
|
-
const
|
|
172
|
+
const sourceIndexes = job.sourceIndexes ?? (sources[index] ? [sources[index].sourceIndex] : []);
|
|
125
173
|
const waitedJob = waitedJobsById?.get(job.jobId);
|
|
126
174
|
return {
|
|
127
175
|
jobId: job.jobId,
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
relativePath: source?.relativePath ?? job.relativePath,
|
|
134
|
-
},
|
|
135
|
-
status: waitedJob?.status,
|
|
176
|
+
sourceIndexes,
|
|
177
|
+
sourceIds: Array.from(new Set(sourceIndexes
|
|
178
|
+
.map((sourceIndex) => sources[sourceIndex]?.sourceId)
|
|
179
|
+
.filter((sourceId) => sourceId != null))),
|
|
180
|
+
status: waitedJob?.status ?? 'queued',
|
|
136
181
|
error: waitedJob?.error,
|
|
137
182
|
exerciseSheetId: waitedJob?.exerciseSheetId,
|
|
138
183
|
};
|
|
139
184
|
});
|
|
140
185
|
};
|
|
141
|
-
const
|
|
186
|
+
const buildSheetImportRequest = ({ inputMode, organizationId, timeoutMs, wait, }) => {
|
|
187
|
+
return {
|
|
188
|
+
organizationId,
|
|
189
|
+
inputMode,
|
|
190
|
+
wait,
|
|
191
|
+
timeoutMs,
|
|
192
|
+
target: {},
|
|
193
|
+
};
|
|
194
|
+
};
|
|
195
|
+
const formatSheetJobSourceLabel = ({ job, sources, }) => {
|
|
196
|
+
if (job.sourceIndexes.length !== 1) {
|
|
197
|
+
return `${job.sourceIndexes.length} source${job.sourceIndexes.length === 1 ? '' : 's'}`;
|
|
198
|
+
}
|
|
199
|
+
const firstSource = sources[job.sourceIndexes[0]];
|
|
200
|
+
return firstSource?.relativePath ?? 'unknown source';
|
|
201
|
+
};
|
|
202
|
+
const formatQueuedImportOutput = ({ jobs, summary, sources, }) => {
|
|
142
203
|
return [
|
|
143
204
|
`Queued ${jobs.length} import${jobs.length === 1 ? '' : 's'}.`,
|
|
144
|
-
...jobs.map((job) => `${job.jobId} ${job
|
|
205
|
+
...jobs.map((job) => `${job.jobId} ${formatSheetJobSourceLabel({ job, sources })}`),
|
|
206
|
+
jobs.length > 1 ? formatCliImportSummary(summary) : null,
|
|
145
207
|
]
|
|
146
208
|
.filter(Boolean)
|
|
147
209
|
.join('\n');
|
|
148
210
|
};
|
|
149
|
-
const formatWaitedImportOutput = (jobs) => {
|
|
211
|
+
const formatWaitedImportOutput = ({ jobs, summary, }) => {
|
|
150
212
|
return [
|
|
151
213
|
`${jobs.length} job${jobs.length === 1 ? '' : 's'} queued.`,
|
|
152
214
|
...jobs.map((job) => formatCliJobSummary({
|
|
153
215
|
id: job.jobId,
|
|
154
|
-
status: job.status
|
|
216
|
+
status: job.status === 'queued' ? 'pending' : job.status,
|
|
155
217
|
type: 'exercise_sheet_import',
|
|
156
218
|
createdAt: '',
|
|
157
219
|
updatedAt: '',
|
|
158
220
|
error: job.error,
|
|
159
221
|
exerciseSheetId: job.exerciseSheetId,
|
|
160
222
|
})),
|
|
223
|
+
jobs.length > 1 ? formatCliImportSummary(summary) : null,
|
|
161
224
|
]
|
|
162
225
|
.filter(Boolean)
|
|
163
226
|
.join('\n');
|
|
@@ -177,6 +240,24 @@ export const registerSheetCommands = (sheetYargs, context) => {
|
|
|
177
240
|
.option('relative-path', {
|
|
178
241
|
type: 'string',
|
|
179
242
|
describe: 'Override the relative path for a single positional source',
|
|
243
|
+
})
|
|
244
|
+
.option('single-sheet', {
|
|
245
|
+
type: 'boolean',
|
|
246
|
+
default: false,
|
|
247
|
+
describe: 'Import all positional sources into one sheet',
|
|
248
|
+
})
|
|
249
|
+
.option('target-folder', {
|
|
250
|
+
type: 'string',
|
|
251
|
+
describe: 'Place a --single-sheet import into this destination folder (defaults to root)',
|
|
252
|
+
})
|
|
253
|
+
.option('title', {
|
|
254
|
+
type: 'string',
|
|
255
|
+
describe: 'Override the imported title for a single-sheet import',
|
|
256
|
+
})
|
|
257
|
+
.option('translate-to', {
|
|
258
|
+
array: true,
|
|
259
|
+
type: 'string',
|
|
260
|
+
describe: `Generate translated sheet content for these languages (${translationLanguages.join(', ')})`,
|
|
180
261
|
})
|
|
181
262
|
.option('timeout-ms', {
|
|
182
263
|
type: 'number',
|
|
@@ -187,8 +268,15 @@ export const registerSheetCommands = (sheetYargs, context) => {
|
|
|
187
268
|
type: 'boolean',
|
|
188
269
|
default: false,
|
|
189
270
|
describe: 'Wait for the queued jobs to finish',
|
|
190
|
-
})
|
|
191
|
-
|
|
271
|
+
})
|
|
272
|
+
.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')
|
|
273
|
+
.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
|
+
.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
|
+
.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.',
|
|
278
|
+
].join('\n')), async (argv) => {
|
|
279
|
+
const config = await context.configStore.loadProfile({ profileName: argv.profile });
|
|
192
280
|
const rawSources = (argv.sources ?? []).map(String);
|
|
193
281
|
const manifestPath = argv.manifest === '' ? '-' : argv.manifest;
|
|
194
282
|
const normalizedRawSources = manifestPath === '-' && rawSources[0] === '-' ? rawSources.slice(1) : rawSources;
|
|
@@ -201,14 +289,32 @@ export const registerSheetCommands = (sheetYargs, context) => {
|
|
|
201
289
|
env: context.env,
|
|
202
290
|
config,
|
|
203
291
|
});
|
|
204
|
-
const commandSources = await
|
|
292
|
+
const commandSources = await buildFileImportCommandSources({
|
|
205
293
|
cwd: context.cwd,
|
|
294
|
+
loadManifest: loadSheetImportManifest,
|
|
206
295
|
manifestPath,
|
|
207
296
|
rawSources: normalizedRawSources,
|
|
208
297
|
relativePath: argv.relativePath,
|
|
209
298
|
stdin: context.stdin,
|
|
210
299
|
wait: argv.wait,
|
|
211
300
|
});
|
|
301
|
+
const requestedTitle = normalizeOptionalTitle(argv.title);
|
|
302
|
+
const requestedTranslateTo = resolveRequestedTranslateToLanguages(argv.translateTo);
|
|
303
|
+
const requestedTargetFolderPath = normalizeOptionalTargetFolderPath(argv.targetFolder);
|
|
304
|
+
const singleSheet = argv.singleSheet === true;
|
|
305
|
+
assertSheetImportFlagUsage({
|
|
306
|
+
manifestPath,
|
|
307
|
+
singleSheet,
|
|
308
|
+
sources: commandSources.sources,
|
|
309
|
+
targetFolderPath: requestedTargetFolderPath,
|
|
310
|
+
title: requestedTitle,
|
|
311
|
+
translateTo: requestedTranslateTo,
|
|
312
|
+
});
|
|
313
|
+
const sheetSources = buildSheetImportSources({
|
|
314
|
+
sources: commandSources.sources,
|
|
315
|
+
title: requestedTitle,
|
|
316
|
+
translateTo: requestedTranslateTo,
|
|
317
|
+
});
|
|
212
318
|
const organizationId = resolveRequestedOrganizationId({
|
|
213
319
|
flagValue: argv.organization,
|
|
214
320
|
fallbackValue: commandSources.organizationId,
|
|
@@ -219,16 +325,36 @@ export const registerSheetCommands = (sheetYargs, context) => {
|
|
|
219
325
|
baseUrl: resolvedBaseUrl.value,
|
|
220
326
|
token: resolvedToken.value,
|
|
221
327
|
organizationId,
|
|
222
|
-
fetchImpl: context.fetchImpl,
|
|
223
328
|
});
|
|
224
329
|
let resolvedSources = [];
|
|
225
330
|
try {
|
|
226
331
|
resolvedSources = await resolveSources({
|
|
227
332
|
cwd: context.cwd,
|
|
228
|
-
|
|
229
|
-
|
|
333
|
+
sources: sheetSources,
|
|
334
|
+
});
|
|
335
|
+
const sources = buildCliImportSources({
|
|
336
|
+
resolvedSources,
|
|
337
|
+
getExtra: (resolvedSource) => ({
|
|
338
|
+
title: resolvedSource.sourceInput.title,
|
|
339
|
+
translateTo: resolvedSource.sourceInput.translateTo,
|
|
340
|
+
}),
|
|
341
|
+
});
|
|
342
|
+
const formData = await buildFileImportFormData(resolvedSources);
|
|
343
|
+
const sheetImportPlan = buildSheetImportPlan({
|
|
344
|
+
groupedManifestSheets: commandSources.sheetGroups,
|
|
345
|
+
requestedTargetFolderPath,
|
|
346
|
+
requestedTitle,
|
|
347
|
+
requestedTranslateTo,
|
|
348
|
+
singleSheet,
|
|
349
|
+
sources,
|
|
350
|
+
});
|
|
351
|
+
formData.set('sheetImportPlan', JSON.stringify(sheetImportPlan));
|
|
352
|
+
const request = buildSheetImportRequest({
|
|
353
|
+
inputMode: manifestPath ? 'manifest' : 'arguments',
|
|
354
|
+
organizationId: organizationId ?? null,
|
|
355
|
+
timeoutMs: commandSources.wait ? normalizeWaitTimeoutMs(argv.timeoutMs) : null,
|
|
356
|
+
wait: commandSources.wait,
|
|
230
357
|
});
|
|
231
|
-
const formData = await buildFormData(resolvedSources);
|
|
232
358
|
let importResult;
|
|
233
359
|
try {
|
|
234
360
|
importResult = await apiClient.importExerciseSheet(formData);
|
|
@@ -239,34 +365,65 @@ export const registerSheetCommands = (sheetYargs, context) => {
|
|
|
239
365
|
if (!commandSources.wait) {
|
|
240
366
|
const jobs = buildSheetImportJobs({
|
|
241
367
|
importResultJobs: importResult.jobs,
|
|
242
|
-
|
|
368
|
+
sources,
|
|
369
|
+
});
|
|
370
|
+
const summary = summarizeCliImportJobs({
|
|
371
|
+
jobs,
|
|
372
|
+
sourceCount: sources.length,
|
|
373
|
+
sourceInputCount: sheetSources.length,
|
|
374
|
+
timedOut: false,
|
|
243
375
|
});
|
|
244
376
|
context.output.print({
|
|
245
|
-
|
|
377
|
+
request,
|
|
378
|
+
sources,
|
|
246
379
|
jobs,
|
|
247
|
-
|
|
248
|
-
}, (result) => formatQueuedImportOutput(
|
|
380
|
+
summary,
|
|
381
|
+
}, (result) => formatQueuedImportOutput({
|
|
382
|
+
jobs: result.jobs,
|
|
383
|
+
summary: result.summary,
|
|
384
|
+
sources: result.sources,
|
|
385
|
+
}), {
|
|
386
|
+
command: 'sheet import',
|
|
387
|
+
});
|
|
249
388
|
return;
|
|
250
389
|
}
|
|
390
|
+
const waitTimeoutMs = normalizeWaitTimeoutMs(argv.timeoutMs);
|
|
251
391
|
const waitResult = await waitForCliJobs({
|
|
252
392
|
getUserJob: async (jobId) => await apiClient.getUserJob(jobId),
|
|
253
393
|
jobIds: importResult.jobs.map((job) => job.jobId),
|
|
254
394
|
now: context.now,
|
|
255
395
|
sleep: context.sleep,
|
|
256
|
-
timeoutMs:
|
|
396
|
+
timeoutMs: waitTimeoutMs,
|
|
257
397
|
});
|
|
258
398
|
const waitedJobsById = new Map(waitResult.jobs.map((job) => [job.id, job]));
|
|
259
399
|
const jobs = buildSheetImportJobs({
|
|
260
400
|
importResultJobs: importResult.jobs,
|
|
261
|
-
|
|
401
|
+
sources,
|
|
262
402
|
waitedJobsById,
|
|
263
403
|
});
|
|
264
|
-
|
|
265
|
-
organizationId: organizationId ?? null,
|
|
404
|
+
const summary = summarizeCliImportJobs({
|
|
266
405
|
jobs,
|
|
406
|
+
sourceCount: sources.length,
|
|
407
|
+
sourceInputCount: sheetSources.length,
|
|
267
408
|
timedOut: waitResult.timedOut,
|
|
268
|
-
|
|
269
|
-
|
|
409
|
+
});
|
|
410
|
+
const waitError = getWaitError(waitResult);
|
|
411
|
+
context.output.print({
|
|
412
|
+
request: {
|
|
413
|
+
...request,
|
|
414
|
+
timeoutMs: waitTimeoutMs,
|
|
415
|
+
},
|
|
416
|
+
sources,
|
|
417
|
+
jobs,
|
|
418
|
+
summary,
|
|
419
|
+
}, (result) => formatWaitedImportOutput({
|
|
420
|
+
jobs: result.jobs,
|
|
421
|
+
summary: result.summary,
|
|
422
|
+
}), {
|
|
423
|
+
command: 'sheet import',
|
|
424
|
+
ok: waitError == null,
|
|
425
|
+
error: waitError,
|
|
426
|
+
});
|
|
270
427
|
throwSilentExitCode(getWaitExitCode(waitResult));
|
|
271
428
|
}
|
|
272
429
|
finally {
|
package/dist/lib/api-client.js
CHANGED
|
@@ -18,7 +18,7 @@ const getResponseData = async ({ response, batched }) => {
|
|
|
18
18
|
}
|
|
19
19
|
return payload.result.data;
|
|
20
20
|
};
|
|
21
|
-
export const createApiClient = ({ baseUrl, token, organizationId,
|
|
21
|
+
export const createApiClient = ({ baseUrl, token, organizationId, }) => {
|
|
22
22
|
const normalizedBaseUrl = normalizeBaseUrl(baseUrl);
|
|
23
23
|
const createHeaders = ({ includeJsonContentType }) => {
|
|
24
24
|
const headers = new Headers();
|
|
@@ -37,7 +37,7 @@ export const createApiClient = ({ baseUrl, token, organizationId, fetchImpl = fe
|
|
|
37
37
|
const url = new URL(`trpc/${procedureName}`, normalizedBaseUrl);
|
|
38
38
|
url.searchParams.set('batch', '1');
|
|
39
39
|
url.searchParams.set('input', JSON.stringify({ 0: input ?? null }));
|
|
40
|
-
const response = await
|
|
40
|
+
const response = await fetch(url.toString(), {
|
|
41
41
|
method: 'GET',
|
|
42
42
|
headers: createHeaders({ includeJsonContentType: false }),
|
|
43
43
|
});
|
|
@@ -45,7 +45,7 @@ export const createApiClient = ({ baseUrl, token, organizationId, fetchImpl = fe
|
|
|
45
45
|
};
|
|
46
46
|
const mutation = async (procedureName, input) => {
|
|
47
47
|
const isFormDataInput = input instanceof FormData;
|
|
48
|
-
const response = await
|
|
48
|
+
const response = await fetch(new URL(`trpc/${procedureName}`, normalizedBaseUrl).toString(), {
|
|
49
49
|
method: 'POST',
|
|
50
50
|
headers: createHeaders({ includeJsonContentType: !isFormDataInput }),
|
|
51
51
|
body: isFormDataInput ? input : JSON.stringify(input ?? {}),
|
|
@@ -67,5 +67,11 @@ export const createApiClient = ({ baseUrl, token, organizationId, fetchImpl = fe
|
|
|
67
67
|
importExerciseSheet: async (formData) => {
|
|
68
68
|
return await mutation('importExerciseSheet', formData);
|
|
69
69
|
},
|
|
70
|
+
importExercise: async (formData) => {
|
|
71
|
+
return await mutation('importExercise', formData);
|
|
72
|
+
},
|
|
73
|
+
importExerciseSolution: async (formData) => {
|
|
74
|
+
return await mutation('importExerciseSolution', formData);
|
|
75
|
+
},
|
|
70
76
|
};
|
|
71
77
|
};
|
package/dist/lib/cli-error.js
CHANGED
|
@@ -1,10 +1,46 @@
|
|
|
1
|
+
const cliErrorCodeByExitCode = {
|
|
2
|
+
1: 'unexpected_error',
|
|
3
|
+
2: 'usage_error',
|
|
4
|
+
3: 'not_authenticated',
|
|
5
|
+
4: 'source_resolution_failed',
|
|
6
|
+
5: 'api_error',
|
|
7
|
+
6: 'wait_timed_out',
|
|
8
|
+
7: 'job_failed',
|
|
9
|
+
};
|
|
10
|
+
const isRetryableCliErrorCode = (code) => {
|
|
11
|
+
return ['api_error', 'wait_timed_out'].includes(code);
|
|
12
|
+
};
|
|
13
|
+
export const getCliErrorCode = (exitCode) => {
|
|
14
|
+
return cliErrorCodeByExitCode[exitCode] ?? 'unexpected_error';
|
|
15
|
+
};
|
|
16
|
+
export const createSerializableCliError = ({ code, exitCode, message, retryable, }) => {
|
|
17
|
+
const resolvedCode = code ?? getCliErrorCode(exitCode);
|
|
18
|
+
return {
|
|
19
|
+
code: resolvedCode,
|
|
20
|
+
exitCode,
|
|
21
|
+
message,
|
|
22
|
+
retryable: retryable ?? isRetryableCliErrorCode(resolvedCode),
|
|
23
|
+
};
|
|
24
|
+
};
|
|
1
25
|
export class CliCommandError extends Error {
|
|
26
|
+
code;
|
|
2
27
|
exitCode;
|
|
28
|
+
retryable;
|
|
3
29
|
shouldReport;
|
|
4
|
-
constructor(message, exitCode, shouldReport = true) {
|
|
30
|
+
constructor(message, exitCode, shouldReport = true, options = {}) {
|
|
5
31
|
super(message);
|
|
6
32
|
this.name = 'CliCommandError';
|
|
33
|
+
this.code = options.code ?? getCliErrorCode(exitCode);
|
|
7
34
|
this.exitCode = exitCode;
|
|
35
|
+
this.retryable = options.retryable ?? isRetryableCliErrorCode(this.code);
|
|
8
36
|
this.shouldReport = shouldReport;
|
|
9
37
|
}
|
|
10
38
|
}
|
|
39
|
+
export const serializeCliError = (error) => {
|
|
40
|
+
return createSerializableCliError({
|
|
41
|
+
code: error.code,
|
|
42
|
+
exitCode: error.exitCode,
|
|
43
|
+
message: error.message,
|
|
44
|
+
retryable: error.retryable,
|
|
45
|
+
});
|
|
46
|
+
};
|