@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
|
@@ -0,0 +1,428 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
import { createApiClient } from '../lib/api-client.js';
|
|
3
|
+
import { CliCommandError } from '../lib/cli-error.js';
|
|
4
|
+
import { buildFileImportCommandSources, buildFileImportFormData, normalizeWaitTimeoutMs } from '../lib/import-files.js';
|
|
5
|
+
import { buildCliImportSources, summarizeCliImportJobs, } from '../lib/import-output.js';
|
|
6
|
+
import { loadExerciseImportManifest, loadExerciseSolutionImportManifest } from '../lib/manifest.js';
|
|
7
|
+
import { mapApiErrorToCliError, requireResolvedBaseUrl, requireResolvedToken, resolveRequestedOrganizationId, } from '../lib/session.js';
|
|
8
|
+
import { cleanupResolvedSources, resolveSources } from '../lib/source-resolver.js';
|
|
9
|
+
import { getWaitError, getWaitExitCode, throwSilentExitCode, waitForCliJobs } from '../lib/user-jobs.js';
|
|
10
|
+
const resolveRequestedUuid = ({ flagValue, fallbackValue, label, required = false, }) => {
|
|
11
|
+
const resolvedValue = flagValue?.trim() || fallbackValue?.trim();
|
|
12
|
+
if (!resolvedValue) {
|
|
13
|
+
if (required) {
|
|
14
|
+
throw new CliCommandError(`${label} is required.`, 2);
|
|
15
|
+
}
|
|
16
|
+
return undefined;
|
|
17
|
+
}
|
|
18
|
+
const parsedValue = z.uuid().safeParse(resolvedValue);
|
|
19
|
+
if (!parsedValue.success) {
|
|
20
|
+
throw new CliCommandError(`${label} must be a valid UUID.`, 2);
|
|
21
|
+
}
|
|
22
|
+
return parsedValue.data;
|
|
23
|
+
};
|
|
24
|
+
const buildExerciseImportRequest = ({ exerciseId, exerciseSheetId, inputMode, organizationId, timeoutMs, wait, }) => {
|
|
25
|
+
return {
|
|
26
|
+
organizationId,
|
|
27
|
+
inputMode,
|
|
28
|
+
wait,
|
|
29
|
+
timeoutMs,
|
|
30
|
+
target: {
|
|
31
|
+
...(exerciseId === undefined ? {} : { exerciseId }),
|
|
32
|
+
...(exerciseSheetId === undefined ? {} : { exerciseSheetId }),
|
|
33
|
+
},
|
|
34
|
+
};
|
|
35
|
+
};
|
|
36
|
+
const buildQueuedExerciseImportJob = ({ jobId, sources, }) => {
|
|
37
|
+
return {
|
|
38
|
+
jobId,
|
|
39
|
+
sourceIndexes: sources.map((source) => source.sourceIndex),
|
|
40
|
+
sourceIds: sources.map((source) => source.sourceId).filter((sourceId) => sourceId !== null),
|
|
41
|
+
status: 'queued',
|
|
42
|
+
};
|
|
43
|
+
};
|
|
44
|
+
const buildWaitedExerciseImportJob = ({ jobId, sources, waitedJob, }) => {
|
|
45
|
+
return {
|
|
46
|
+
jobId,
|
|
47
|
+
sourceIndexes: sources.map((source) => source.sourceIndex),
|
|
48
|
+
sourceIds: sources.map((source) => source.sourceId).filter((sourceId) => sourceId !== null),
|
|
49
|
+
status: waitedJob?.status ?? 'pending',
|
|
50
|
+
error: waitedJob?.error,
|
|
51
|
+
exerciseId: waitedJob?.exerciseId,
|
|
52
|
+
exerciseIds: waitedJob?.exerciseIds,
|
|
53
|
+
};
|
|
54
|
+
};
|
|
55
|
+
const formatQueuedImportOutput = (job) => {
|
|
56
|
+
return [
|
|
57
|
+
'Queued exercise import.',
|
|
58
|
+
`${job.jobId} ${job.sourceIndexes.length} source${job.sourceIndexes.length === 1 ? '' : 's'}`,
|
|
59
|
+
].join('\n');
|
|
60
|
+
};
|
|
61
|
+
const formatQueuedSolutionImportOutput = (job) => {
|
|
62
|
+
return [
|
|
63
|
+
'Queued exercise solution import.',
|
|
64
|
+
`${job.jobId} ${job.sourceIndexes.length} source${job.sourceIndexes.length === 1 ? '' : 's'}`,
|
|
65
|
+
].join('\n');
|
|
66
|
+
};
|
|
67
|
+
const formatWaitedImportOutput = (job) => {
|
|
68
|
+
if (job.status === 'completed') {
|
|
69
|
+
if (job.exerciseIds && job.exerciseIds.length > 0) {
|
|
70
|
+
return `${job.jobId} completed -> ${job.exerciseIds.length} exercise${job.exerciseIds.length === 1 ? '' : 's'}`;
|
|
71
|
+
}
|
|
72
|
+
if (job.exerciseId) {
|
|
73
|
+
return `${job.jobId} completed -> ${job.exerciseId}`;
|
|
74
|
+
}
|
|
75
|
+
return `${job.jobId} completed`;
|
|
76
|
+
}
|
|
77
|
+
if (job.status === 'failed') {
|
|
78
|
+
return `${job.jobId} failed -> ${job.error ?? 'Unknown error'}`;
|
|
79
|
+
}
|
|
80
|
+
return `${job.jobId} ${job.status ?? 'pending'}`;
|
|
81
|
+
};
|
|
82
|
+
export const registerExerciseCommands = (exerciseYargs, context) => {
|
|
83
|
+
return exerciseYargs
|
|
84
|
+
.command('import [sources..]', 'Import one or more exercises', (importYargs) => importYargs
|
|
85
|
+
.positional('sources', {
|
|
86
|
+
array: true,
|
|
87
|
+
type: 'string',
|
|
88
|
+
describe: 'Local file paths, directories, or HTTP(S) URLs',
|
|
89
|
+
})
|
|
90
|
+
.option('manifest', {
|
|
91
|
+
type: 'string',
|
|
92
|
+
describe: 'Load sources from a manifest file, or "-" for stdin',
|
|
93
|
+
})
|
|
94
|
+
.option('relative-path', {
|
|
95
|
+
type: 'string',
|
|
96
|
+
describe: 'Override the relative path for a single positional source',
|
|
97
|
+
})
|
|
98
|
+
.option('sheet-id', {
|
|
99
|
+
type: 'string',
|
|
100
|
+
describe: 'Append the imported exercises to an existing sheet',
|
|
101
|
+
})
|
|
102
|
+
.option('timeout-ms', {
|
|
103
|
+
type: 'number',
|
|
104
|
+
default: 300000,
|
|
105
|
+
describe: 'Maximum time to wait before timing out',
|
|
106
|
+
})
|
|
107
|
+
.option('wait', {
|
|
108
|
+
type: 'boolean',
|
|
109
|
+
default: false,
|
|
110
|
+
describe: 'Wait for the queued job to finish',
|
|
111
|
+
})
|
|
112
|
+
.example('chalksurf exercise import ./fixtures/problem-set.pdf --sheet-id 00000000-0000-4000-8000-000000000001 --wait', 'Import a problem set into an existing sheet')
|
|
113
|
+
.example('cat exercise-import.json | chalksurf --profile prod-codex exercise import --manifest - --wait --json', 'Drive imports from a manifest in an agent or CI flow')
|
|
114
|
+
.epilogue('When --wait is set, the command exits after the queued import job reaches a terminal state.'), async (argv) => {
|
|
115
|
+
const config = await context.configStore.loadProfile({ profileName: argv.profile });
|
|
116
|
+
const rawSources = (argv.sources ?? []).map(String);
|
|
117
|
+
const manifestPath = argv.manifest === '' ? '-' : argv.manifest;
|
|
118
|
+
const normalizedRawSources = manifestPath === '-' && rawSources[0] === '-' ? rawSources.slice(1) : rawSources;
|
|
119
|
+
const resolvedBaseUrl = requireResolvedBaseUrl({
|
|
120
|
+
flagValue: argv.baseUrl,
|
|
121
|
+
env: context.env,
|
|
122
|
+
config,
|
|
123
|
+
});
|
|
124
|
+
const resolvedToken = requireResolvedToken({
|
|
125
|
+
env: context.env,
|
|
126
|
+
config,
|
|
127
|
+
});
|
|
128
|
+
const commandSources = await buildFileImportCommandSources({
|
|
129
|
+
cwd: context.cwd,
|
|
130
|
+
loadManifest: loadExerciseImportManifest,
|
|
131
|
+
manifestPath,
|
|
132
|
+
rawSources: normalizedRawSources,
|
|
133
|
+
relativePath: argv.relativePath,
|
|
134
|
+
stdin: context.stdin,
|
|
135
|
+
wait: argv.wait,
|
|
136
|
+
});
|
|
137
|
+
const organizationId = resolveRequestedOrganizationId({
|
|
138
|
+
flagValue: argv.organization,
|
|
139
|
+
fallbackValue: commandSources.organizationId,
|
|
140
|
+
env: context.env,
|
|
141
|
+
config,
|
|
142
|
+
});
|
|
143
|
+
const exerciseSheetId = resolveRequestedUuid({
|
|
144
|
+
flagValue: argv.sheetId,
|
|
145
|
+
fallbackValue: commandSources.exerciseSheetId,
|
|
146
|
+
label: '--sheet-id',
|
|
147
|
+
});
|
|
148
|
+
const apiClient = createApiClient({
|
|
149
|
+
baseUrl: resolvedBaseUrl.value,
|
|
150
|
+
token: resolvedToken.value,
|
|
151
|
+
organizationId,
|
|
152
|
+
});
|
|
153
|
+
let resolvedSources = [];
|
|
154
|
+
try {
|
|
155
|
+
resolvedSources = await resolveSources({
|
|
156
|
+
cwd: context.cwd,
|
|
157
|
+
sources: commandSources.sources,
|
|
158
|
+
});
|
|
159
|
+
const sources = buildCliImportSources({
|
|
160
|
+
resolvedSources,
|
|
161
|
+
});
|
|
162
|
+
const formData = await buildFileImportFormData(resolvedSources);
|
|
163
|
+
const request = buildExerciseImportRequest({
|
|
164
|
+
exerciseSheetId: exerciseSheetId ?? null,
|
|
165
|
+
inputMode: manifestPath ? 'manifest' : 'arguments',
|
|
166
|
+
organizationId: organizationId ?? null,
|
|
167
|
+
timeoutMs: commandSources.wait ? normalizeWaitTimeoutMs(argv.timeoutMs) : null,
|
|
168
|
+
wait: commandSources.wait,
|
|
169
|
+
});
|
|
170
|
+
if (exerciseSheetId) {
|
|
171
|
+
formData.set('exerciseSheetId', exerciseSheetId);
|
|
172
|
+
}
|
|
173
|
+
let importResult;
|
|
174
|
+
try {
|
|
175
|
+
importResult = await apiClient.importExercise(formData);
|
|
176
|
+
}
|
|
177
|
+
catch (error) {
|
|
178
|
+
throw mapApiErrorToCliError(error);
|
|
179
|
+
}
|
|
180
|
+
if (!commandSources.wait) {
|
|
181
|
+
const jobs = [
|
|
182
|
+
buildQueuedExerciseImportJob({
|
|
183
|
+
jobId: importResult.jobId,
|
|
184
|
+
sources,
|
|
185
|
+
}),
|
|
186
|
+
];
|
|
187
|
+
const summary = summarizeCliImportJobs({
|
|
188
|
+
jobs,
|
|
189
|
+
sourceCount: sources.length,
|
|
190
|
+
sourceInputCount: commandSources.sources.length,
|
|
191
|
+
timedOut: false,
|
|
192
|
+
});
|
|
193
|
+
context.output.print({
|
|
194
|
+
request,
|
|
195
|
+
sources,
|
|
196
|
+
jobs,
|
|
197
|
+
summary,
|
|
198
|
+
}, (result) => formatQueuedImportOutput(result.jobs[0]), {
|
|
199
|
+
command: 'exercise import',
|
|
200
|
+
});
|
|
201
|
+
return;
|
|
202
|
+
}
|
|
203
|
+
const waitTimeoutMs = normalizeWaitTimeoutMs(argv.timeoutMs);
|
|
204
|
+
const waitResult = await waitForCliJobs({
|
|
205
|
+
getUserJob: async (jobId) => await apiClient.getUserJob(jobId),
|
|
206
|
+
jobIds: [importResult.jobId],
|
|
207
|
+
now: context.now,
|
|
208
|
+
sleep: context.sleep,
|
|
209
|
+
timeoutMs: waitTimeoutMs,
|
|
210
|
+
});
|
|
211
|
+
const waitedJob = waitResult.jobs[0];
|
|
212
|
+
const jobs = [
|
|
213
|
+
buildWaitedExerciseImportJob({
|
|
214
|
+
jobId: importResult.jobId,
|
|
215
|
+
sources,
|
|
216
|
+
waitedJob: waitedJob == null
|
|
217
|
+
? undefined
|
|
218
|
+
: {
|
|
219
|
+
...waitedJob,
|
|
220
|
+
id: waitedJob.id,
|
|
221
|
+
status: waitedJob.status,
|
|
222
|
+
},
|
|
223
|
+
}),
|
|
224
|
+
];
|
|
225
|
+
const summary = summarizeCliImportJobs({
|
|
226
|
+
jobs,
|
|
227
|
+
sourceCount: sources.length,
|
|
228
|
+
sourceInputCount: commandSources.sources.length,
|
|
229
|
+
timedOut: waitResult.timedOut,
|
|
230
|
+
});
|
|
231
|
+
const waitError = getWaitError({
|
|
232
|
+
jobs: waitResult.jobs,
|
|
233
|
+
timedOut: waitResult.timedOut,
|
|
234
|
+
});
|
|
235
|
+
context.output.print({
|
|
236
|
+
request: {
|
|
237
|
+
...request,
|
|
238
|
+
timeoutMs: waitTimeoutMs,
|
|
239
|
+
},
|
|
240
|
+
sources,
|
|
241
|
+
jobs,
|
|
242
|
+
summary,
|
|
243
|
+
}, (result) => formatWaitedImportOutput(result.jobs[0]), {
|
|
244
|
+
command: 'exercise import',
|
|
245
|
+
ok: waitError == null,
|
|
246
|
+
error: waitError,
|
|
247
|
+
});
|
|
248
|
+
throwSilentExitCode(getWaitExitCode({ jobs: waitResult.jobs, timedOut: waitResult.timedOut }));
|
|
249
|
+
}
|
|
250
|
+
catch (error) {
|
|
251
|
+
throw error;
|
|
252
|
+
}
|
|
253
|
+
finally {
|
|
254
|
+
await cleanupResolvedSources(resolvedSources);
|
|
255
|
+
}
|
|
256
|
+
})
|
|
257
|
+
.command('import-solution [exerciseId] [sources..]', 'Import one or more exercise solution files', (importSolutionYargs) => importSolutionYargs
|
|
258
|
+
.positional('exerciseId', {
|
|
259
|
+
type: 'string',
|
|
260
|
+
describe: 'Exercise ID to attach the imported solution to',
|
|
261
|
+
})
|
|
262
|
+
.positional('sources', {
|
|
263
|
+
array: true,
|
|
264
|
+
type: 'string',
|
|
265
|
+
describe: 'Local file paths, directories, or HTTP(S) URLs',
|
|
266
|
+
})
|
|
267
|
+
.option('manifest', {
|
|
268
|
+
type: 'string',
|
|
269
|
+
describe: 'Load sources from a manifest file, or "-" for stdin',
|
|
270
|
+
})
|
|
271
|
+
.option('relative-path', {
|
|
272
|
+
type: 'string',
|
|
273
|
+
describe: 'Override the relative path for a single positional source',
|
|
274
|
+
})
|
|
275
|
+
.option('timeout-ms', {
|
|
276
|
+
type: 'number',
|
|
277
|
+
default: 300000,
|
|
278
|
+
describe: 'Maximum time to wait before timing out',
|
|
279
|
+
})
|
|
280
|
+
.option('wait', {
|
|
281
|
+
type: 'boolean',
|
|
282
|
+
default: false,
|
|
283
|
+
describe: 'Wait for the queued job to finish',
|
|
284
|
+
})
|
|
285
|
+
.example('chalksurf exercise import-solution 00000000-0000-4000-8000-000000000001 ./fixtures/solution.pdf --wait', 'Attach a solution file to an existing exercise')
|
|
286
|
+
.example('cat exercise-solution-import.json | chalksurf --profile prod-codex exercise import-solution --manifest - --wait --json', 'Drive solution imports from a manifest in an agent or CI flow')
|
|
287
|
+
.epilogue('When --wait is set, the command exits after the queued import job reaches a terminal state.'), async (argv) => {
|
|
288
|
+
const config = await context.configStore.loadProfile({ profileName: argv.profile });
|
|
289
|
+
const rawSources = (argv.sources ?? []).map(String);
|
|
290
|
+
const manifestPath = argv.manifest === '' ? '-' : argv.manifest;
|
|
291
|
+
const normalizedRawSources = manifestPath === '-' && rawSources[0] === '-' ? rawSources.slice(1) : rawSources;
|
|
292
|
+
const resolvedBaseUrl = requireResolvedBaseUrl({
|
|
293
|
+
flagValue: argv.baseUrl,
|
|
294
|
+
env: context.env,
|
|
295
|
+
config,
|
|
296
|
+
});
|
|
297
|
+
const resolvedToken = requireResolvedToken({
|
|
298
|
+
env: context.env,
|
|
299
|
+
config,
|
|
300
|
+
});
|
|
301
|
+
const commandSources = await buildFileImportCommandSources({
|
|
302
|
+
cwd: context.cwd,
|
|
303
|
+
loadManifest: loadExerciseSolutionImportManifest,
|
|
304
|
+
manifestPath,
|
|
305
|
+
rawSources: normalizedRawSources,
|
|
306
|
+
relativePath: argv.relativePath,
|
|
307
|
+
stdin: context.stdin,
|
|
308
|
+
wait: argv.wait,
|
|
309
|
+
});
|
|
310
|
+
const organizationId = resolveRequestedOrganizationId({
|
|
311
|
+
flagValue: argv.organization,
|
|
312
|
+
fallbackValue: commandSources.organizationId,
|
|
313
|
+
env: context.env,
|
|
314
|
+
config,
|
|
315
|
+
});
|
|
316
|
+
const exerciseId = resolveRequestedUuid({
|
|
317
|
+
flagValue: argv.exerciseId,
|
|
318
|
+
fallbackValue: commandSources.exerciseId,
|
|
319
|
+
label: 'exerciseId',
|
|
320
|
+
required: true,
|
|
321
|
+
});
|
|
322
|
+
const apiClient = createApiClient({
|
|
323
|
+
baseUrl: resolvedBaseUrl.value,
|
|
324
|
+
token: resolvedToken.value,
|
|
325
|
+
organizationId,
|
|
326
|
+
});
|
|
327
|
+
let resolvedSources = [];
|
|
328
|
+
try {
|
|
329
|
+
resolvedSources = await resolveSources({
|
|
330
|
+
cwd: context.cwd,
|
|
331
|
+
sources: commandSources.sources,
|
|
332
|
+
});
|
|
333
|
+
const sources = buildCliImportSources({
|
|
334
|
+
resolvedSources,
|
|
335
|
+
});
|
|
336
|
+
const formData = await buildFileImportFormData(resolvedSources);
|
|
337
|
+
formData.set('exerciseId', exerciseId);
|
|
338
|
+
const request = buildExerciseImportRequest({
|
|
339
|
+
exerciseId,
|
|
340
|
+
inputMode: manifestPath ? 'manifest' : 'arguments',
|
|
341
|
+
organizationId: organizationId ?? null,
|
|
342
|
+
timeoutMs: commandSources.wait ? normalizeWaitTimeoutMs(argv.timeoutMs) : null,
|
|
343
|
+
wait: commandSources.wait,
|
|
344
|
+
});
|
|
345
|
+
let importResult;
|
|
346
|
+
try {
|
|
347
|
+
importResult = await apiClient.importExerciseSolution(formData);
|
|
348
|
+
}
|
|
349
|
+
catch (error) {
|
|
350
|
+
throw mapApiErrorToCliError(error);
|
|
351
|
+
}
|
|
352
|
+
if (!commandSources.wait) {
|
|
353
|
+
const jobs = [
|
|
354
|
+
buildQueuedExerciseImportJob({
|
|
355
|
+
jobId: importResult.jobId,
|
|
356
|
+
sources,
|
|
357
|
+
}),
|
|
358
|
+
];
|
|
359
|
+
const summary = summarizeCliImportJobs({
|
|
360
|
+
jobs,
|
|
361
|
+
sourceCount: sources.length,
|
|
362
|
+
sourceInputCount: commandSources.sources.length,
|
|
363
|
+
timedOut: false,
|
|
364
|
+
});
|
|
365
|
+
context.output.print({
|
|
366
|
+
request,
|
|
367
|
+
sources,
|
|
368
|
+
jobs,
|
|
369
|
+
summary,
|
|
370
|
+
}, (result) => formatQueuedSolutionImportOutput(result.jobs[0]), {
|
|
371
|
+
command: 'exercise import-solution',
|
|
372
|
+
});
|
|
373
|
+
return;
|
|
374
|
+
}
|
|
375
|
+
const waitTimeoutMs = normalizeWaitTimeoutMs(argv.timeoutMs);
|
|
376
|
+
const waitResult = await waitForCliJobs({
|
|
377
|
+
getUserJob: async (jobId) => await apiClient.getUserJob(jobId),
|
|
378
|
+
jobIds: [importResult.jobId],
|
|
379
|
+
now: context.now,
|
|
380
|
+
sleep: context.sleep,
|
|
381
|
+
timeoutMs: waitTimeoutMs,
|
|
382
|
+
});
|
|
383
|
+
const waitedJob = waitResult.jobs[0];
|
|
384
|
+
const jobs = [
|
|
385
|
+
buildWaitedExerciseImportJob({
|
|
386
|
+
jobId: importResult.jobId,
|
|
387
|
+
sources,
|
|
388
|
+
waitedJob: waitedJob == null
|
|
389
|
+
? undefined
|
|
390
|
+
: {
|
|
391
|
+
...waitedJob,
|
|
392
|
+
id: waitedJob.id,
|
|
393
|
+
status: waitedJob.status,
|
|
394
|
+
},
|
|
395
|
+
}),
|
|
396
|
+
];
|
|
397
|
+
const summary = summarizeCliImportJobs({
|
|
398
|
+
jobs,
|
|
399
|
+
sourceCount: sources.length,
|
|
400
|
+
sourceInputCount: commandSources.sources.length,
|
|
401
|
+
timedOut: waitResult.timedOut,
|
|
402
|
+
});
|
|
403
|
+
const waitError = getWaitError({
|
|
404
|
+
jobs: waitResult.jobs,
|
|
405
|
+
timedOut: waitResult.timedOut,
|
|
406
|
+
});
|
|
407
|
+
context.output.print({
|
|
408
|
+
request: {
|
|
409
|
+
...request,
|
|
410
|
+
timeoutMs: waitTimeoutMs,
|
|
411
|
+
},
|
|
412
|
+
sources,
|
|
413
|
+
jobs,
|
|
414
|
+
summary,
|
|
415
|
+
}, (result) => formatWaitedImportOutput(result.jobs[0]), {
|
|
416
|
+
command: 'exercise import-solution',
|
|
417
|
+
ok: waitError == null,
|
|
418
|
+
error: waitError,
|
|
419
|
+
});
|
|
420
|
+
throwSilentExitCode(getWaitExitCode({ jobs: waitResult.jobs, timedOut: waitResult.timedOut }));
|
|
421
|
+
}
|
|
422
|
+
finally {
|
|
423
|
+
await cleanupResolvedSources(resolvedSources);
|
|
424
|
+
}
|
|
425
|
+
})
|
|
426
|
+
.demandCommand(1)
|
|
427
|
+
.strict();
|
|
428
|
+
};
|
package/dist/commands/job.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { createApiClient } from '../lib/api-client.js';
|
|
2
2
|
import { CliCommandError } from '../lib/cli-error.js';
|
|
3
3
|
import { mapApiErrorToCliError, requireResolvedBaseUrl, requireResolvedToken, resolveRequestedOrganizationId, } from '../lib/session.js';
|
|
4
|
-
import { formatCliJobSummary, getWaitExitCode, serializeCliJob, throwSilentExitCode, waitForCliJobs, } from '../lib/user-jobs.js';
|
|
4
|
+
import { formatCliJobSummary, getWaitError, getWaitExitCode, serializeCliJob, throwSilentExitCode, waitForCliJobs, } from '../lib/user-jobs.js';
|
|
5
5
|
const normalizeTimeoutMs = (timeoutMs) => {
|
|
6
6
|
const normalizedTimeoutMs = timeoutMs ?? 300000;
|
|
7
7
|
if (!Number.isFinite(normalizedTimeoutMs) || normalizedTimeoutMs <= 0) {
|
|
@@ -11,11 +11,14 @@ const normalizeTimeoutMs = (timeoutMs) => {
|
|
|
11
11
|
};
|
|
12
12
|
export const registerJobCommands = (jobYargs, context) => {
|
|
13
13
|
return jobYargs
|
|
14
|
-
.command('get <jobId>', 'Fetch a single job', (getYargs) => getYargs
|
|
14
|
+
.command('get <jobId>', 'Fetch a single job', (getYargs) => getYargs
|
|
15
|
+
.positional('jobId', {
|
|
15
16
|
type: 'string',
|
|
16
17
|
describe: 'Job ID to fetch',
|
|
17
|
-
})
|
|
18
|
-
|
|
18
|
+
})
|
|
19
|
+
.example('chalksurf job get job_123', 'Read one job in human-readable mode')
|
|
20
|
+
.example('chalksurf job get job_123 --json', 'Read one job in machine-readable mode'), async (argv) => {
|
|
21
|
+
const config = await context.configStore.loadProfile({ profileName: argv.profile });
|
|
19
22
|
const resolvedBaseUrl = requireResolvedBaseUrl({
|
|
20
23
|
flagValue: argv.baseUrl,
|
|
21
24
|
env: context.env,
|
|
@@ -34,7 +37,6 @@ export const registerJobCommands = (jobYargs, context) => {
|
|
|
34
37
|
baseUrl: resolvedBaseUrl.value,
|
|
35
38
|
token: resolvedToken.value,
|
|
36
39
|
organizationId,
|
|
37
|
-
fetchImpl: context.fetchImpl,
|
|
38
40
|
});
|
|
39
41
|
let job;
|
|
40
42
|
try {
|
|
@@ -44,7 +46,9 @@ export const registerJobCommands = (jobYargs, context) => {
|
|
|
44
46
|
throw mapApiErrorToCliError(error);
|
|
45
47
|
}
|
|
46
48
|
const serializedJob = serializeCliJob(job);
|
|
47
|
-
context.output.print({ job: serializedJob }, (result) => formatCliJobSummary(result.job)
|
|
49
|
+
context.output.print({ job: serializedJob }, (result) => formatCliJobSummary(result.job), {
|
|
50
|
+
command: 'job get',
|
|
51
|
+
});
|
|
48
52
|
})
|
|
49
53
|
.command('wait <jobIds..>', 'Poll jobs until they complete, fail, or time out', (waitYargs) => waitYargs
|
|
50
54
|
.positional('jobIds', {
|
|
@@ -56,8 +60,10 @@ export const registerJobCommands = (jobYargs, context) => {
|
|
|
56
60
|
type: 'number',
|
|
57
61
|
default: 300000,
|
|
58
62
|
describe: 'Maximum time to wait before timing out',
|
|
59
|
-
})
|
|
60
|
-
|
|
63
|
+
})
|
|
64
|
+
.example('chalksurf job wait job_123 job_124', 'Wait for one or more jobs to reach a terminal state')
|
|
65
|
+
.example('chalksurf job wait job_123 --timeout-ms 60000 --json', 'Poll a job for up to 60 seconds'), async (argv) => {
|
|
66
|
+
const config = await context.configStore.loadProfile({ profileName: argv.profile });
|
|
61
67
|
const resolvedBaseUrl = requireResolvedBaseUrl({
|
|
62
68
|
flagValue: argv.baseUrl,
|
|
63
69
|
env: context.env,
|
|
@@ -76,7 +82,6 @@ export const registerJobCommands = (jobYargs, context) => {
|
|
|
76
82
|
baseUrl: resolvedBaseUrl.value,
|
|
77
83
|
token: resolvedToken.value,
|
|
78
84
|
organizationId,
|
|
79
|
-
fetchImpl: context.fetchImpl,
|
|
80
85
|
});
|
|
81
86
|
const timeoutMs = normalizeTimeoutMs(argv.timeoutMs);
|
|
82
87
|
const waitResult = await waitForCliJobs({
|
|
@@ -86,7 +91,12 @@ export const registerJobCommands = (jobYargs, context) => {
|
|
|
86
91
|
sleep: context.sleep,
|
|
87
92
|
timeoutMs,
|
|
88
93
|
});
|
|
89
|
-
|
|
94
|
+
const waitError = getWaitError(waitResult);
|
|
95
|
+
context.output.print(waitResult, (result) => result.jobs.map((job) => formatCliJobSummary(job)).join('\n'), {
|
|
96
|
+
command: 'job wait',
|
|
97
|
+
ok: waitError == null,
|
|
98
|
+
error: waitError,
|
|
99
|
+
});
|
|
90
100
|
throwSilentExitCode(getWaitExitCode(waitResult));
|
|
91
101
|
})
|
|
92
102
|
.demandCommand(1)
|
package/dist/commands/org.js
CHANGED
|
@@ -13,8 +13,10 @@ const serializeOrganization = ({ organizationId, organizationName, organizationT
|
|
|
13
13
|
};
|
|
14
14
|
export const registerOrgCommands = (orgYargs, context) => {
|
|
15
15
|
return orgYargs
|
|
16
|
-
.command('list', 'List organizations available to the authenticated user', () =>
|
|
17
|
-
|
|
16
|
+
.command('list', 'List organizations available to the authenticated user', (listYargs) => listYargs
|
|
17
|
+
.example('chalksurf org list', 'List organizations for the default profile')
|
|
18
|
+
.example('chalksurf --profile prod-codex org list --json', 'Inspect organizations for a specific profile'), async (argv) => {
|
|
19
|
+
const config = await context.configStore.loadProfile({ profileName: argv.profile });
|
|
18
20
|
const resolvedBaseUrl = requireResolvedBaseUrl({
|
|
19
21
|
flagValue: argv.baseUrl,
|
|
20
22
|
env: context.env,
|
|
@@ -27,7 +29,6 @@ export const registerOrgCommands = (orgYargs, context) => {
|
|
|
27
29
|
const apiClient = createApiClient({
|
|
28
30
|
baseUrl: resolvedBaseUrl.value,
|
|
29
31
|
token: resolvedToken.value,
|
|
30
|
-
fetchImpl: context.fetchImpl,
|
|
31
32
|
});
|
|
32
33
|
let profile;
|
|
33
34
|
try {
|
|
@@ -51,20 +52,25 @@ export const registerOrgCommands = (orgYargs, context) => {
|
|
|
51
52
|
}));
|
|
52
53
|
context.output.print({
|
|
53
54
|
organizations,
|
|
55
|
+
profile: config.profileName,
|
|
56
|
+
profileSource: config.profileSource,
|
|
54
57
|
selectedOrganizationId: resolvedOrganization.organization?.organizationId ?? null,
|
|
55
58
|
selectedOrganizationSource: resolvedOrganization.source,
|
|
56
|
-
warnings: resolvedOrganization.warnings,
|
|
57
59
|
}, (result) => result.organizations
|
|
58
60
|
.map((organization) => `${organization.selected ? '*' : ' '} ${organization.name} (${organization.id}) [${organization.type}, ${organization.role}]`)
|
|
59
|
-
.join('\n')
|
|
61
|
+
.join('\n'), {
|
|
62
|
+
command: 'org list',
|
|
63
|
+
});
|
|
60
64
|
})
|
|
61
65
|
.command('use <organizationId>', 'Set the default organization used by the CLI', (useYargs) => {
|
|
62
|
-
return useYargs
|
|
66
|
+
return useYargs
|
|
67
|
+
.positional('organizationId', {
|
|
63
68
|
type: 'string',
|
|
64
69
|
describe: 'Organization ID to store as the default',
|
|
65
|
-
})
|
|
70
|
+
})
|
|
71
|
+
.example('chalksurf --profile prod-codex org use org_123', 'Store org_123 in the prod-codex profile');
|
|
66
72
|
}, async (argv) => {
|
|
67
|
-
const config = await context.configStore.
|
|
73
|
+
const config = await context.configStore.loadProfile({ profileName: argv.profile });
|
|
68
74
|
const resolvedBaseUrl = requireResolvedBaseUrl({
|
|
69
75
|
flagValue: argv.baseUrl,
|
|
70
76
|
env: context.env,
|
|
@@ -77,7 +83,6 @@ export const registerOrgCommands = (orgYargs, context) => {
|
|
|
77
83
|
const apiClient = createApiClient({
|
|
78
84
|
baseUrl: resolvedBaseUrl.value,
|
|
79
85
|
token: resolvedToken.value,
|
|
80
|
-
fetchImpl: context.fetchImpl,
|
|
81
86
|
});
|
|
82
87
|
let profile;
|
|
83
88
|
try {
|
|
@@ -90,7 +95,7 @@ export const registerOrgCommands = (orgYargs, context) => {
|
|
|
90
95
|
if (!organization) {
|
|
91
96
|
throw new CliCommandError(`Organization "${String(argv.organizationId)}" is not accessible to the current user.`, 2);
|
|
92
97
|
}
|
|
93
|
-
await context.configStore.update((currentConfig) => ({
|
|
98
|
+
await context.configStore.update({ profileName: argv.profile }, (currentConfig) => ({
|
|
94
99
|
...currentConfig,
|
|
95
100
|
organizationId: organization.organizationId,
|
|
96
101
|
}));
|
|
@@ -99,7 +104,11 @@ export const registerOrgCommands = (orgYargs, context) => {
|
|
|
99
104
|
...organization,
|
|
100
105
|
selected: true,
|
|
101
106
|
}),
|
|
102
|
-
|
|
107
|
+
profile: config.profileName,
|
|
108
|
+
profileSource: config.profileSource,
|
|
109
|
+
}, (result) => `Default organization set to ${result.organization.name} (${result.organization.id}).`, {
|
|
110
|
+
command: 'org use',
|
|
111
|
+
});
|
|
103
112
|
})
|
|
104
113
|
.demandCommand(1)
|
|
105
114
|
.strict();
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
import { CliCommandError } from '../lib/cli-error.js';
|
|
2
|
+
import { assertValidProfileName, resolveProfile } from '../lib/config-store.js';
|
|
3
|
+
const serializeProfile = ({ profileName, profileConfig, selected, }) => {
|
|
4
|
+
return {
|
|
5
|
+
baseUrl: profileConfig.baseUrl ?? null,
|
|
6
|
+
hasToken: Boolean(profileConfig.token),
|
|
7
|
+
name: profileName,
|
|
8
|
+
organizationId: profileConfig.organizationId ?? null,
|
|
9
|
+
selected,
|
|
10
|
+
};
|
|
11
|
+
};
|
|
12
|
+
const formatProfile = (profile) => {
|
|
13
|
+
const marker = profile.selected ? '*' : ' ';
|
|
14
|
+
const baseUrl = profile.baseUrl ?? 'no base URL';
|
|
15
|
+
const token = profile.hasToken ? 'token' : 'no token';
|
|
16
|
+
const organization = profile.organizationId ? `org ${profile.organizationId}` : 'no org';
|
|
17
|
+
return `${marker} ${profile.name} (${baseUrl}, ${token}, ${organization})`;
|
|
18
|
+
};
|
|
19
|
+
export const registerProfileCommands = (profileYargs, context) => {
|
|
20
|
+
return profileYargs
|
|
21
|
+
.command('list', 'List locally stored CLI profiles', (listYargs) => listYargs
|
|
22
|
+
.example('chalksurf profile list', 'List locally stored profiles')
|
|
23
|
+
.example('chalksurf --profile prod-codex profile list --json', 'Show the active profile in JSON mode'), async (argv) => {
|
|
24
|
+
const rootConfig = await context.configStore.loadRoot();
|
|
25
|
+
let activeProfile = null;
|
|
26
|
+
try {
|
|
27
|
+
activeProfile =
|
|
28
|
+
Object.keys(rootConfig.profiles).length === 0
|
|
29
|
+
? null
|
|
30
|
+
: resolveProfile({
|
|
31
|
+
flagValue: argv.profile,
|
|
32
|
+
env: context.env,
|
|
33
|
+
config: rootConfig,
|
|
34
|
+
});
|
|
35
|
+
}
|
|
36
|
+
catch (error) {
|
|
37
|
+
if (!(error instanceof CliCommandError) || !error.message.startsWith('No active ChalkSurf profile')) {
|
|
38
|
+
throw error;
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
const profiles = Object.entries(rootConfig.profiles).map(([profileName, profileConfig]) => serializeProfile({
|
|
42
|
+
profileName,
|
|
43
|
+
profileConfig,
|
|
44
|
+
selected: profileName === activeProfile?.profileName,
|
|
45
|
+
}));
|
|
46
|
+
context.output.print({
|
|
47
|
+
activeProfile: activeProfile?.profileName ?? null,
|
|
48
|
+
activeProfileSource: activeProfile?.profileSource ?? null,
|
|
49
|
+
defaultProfile: rootConfig.defaultProfile ?? null,
|
|
50
|
+
profiles,
|
|
51
|
+
}, (result) => result.profiles.length === 0 ? 'No profiles configured.' : result.profiles.map(formatProfile).join('\n'), {
|
|
52
|
+
command: 'profile list',
|
|
53
|
+
});
|
|
54
|
+
})
|
|
55
|
+
.command('use <profileName>', 'Set the default profile used by the CLI', (useYargs) => useYargs
|
|
56
|
+
.positional('profileName', {
|
|
57
|
+
type: 'string',
|
|
58
|
+
describe: 'Profile name to make the default',
|
|
59
|
+
})
|
|
60
|
+
.example('chalksurf profile use prod-cztamas', 'Use prod-cztamas by default for future commands'), async (argv) => {
|
|
61
|
+
const profileName = String(argv.profileName ?? '');
|
|
62
|
+
assertValidProfileName(profileName);
|
|
63
|
+
const rootConfig = await context.configStore.setDefaultProfile(profileName);
|
|
64
|
+
const profileConfig = rootConfig.profiles[profileName];
|
|
65
|
+
if (!profileConfig) {
|
|
66
|
+
throw new CliCommandError(`Profile "${profileName}" does not exist.`, 2);
|
|
67
|
+
}
|
|
68
|
+
context.output.print({
|
|
69
|
+
defaultProfile: profileName,
|
|
70
|
+
profile: serializeProfile({
|
|
71
|
+
profileName,
|
|
72
|
+
profileConfig,
|
|
73
|
+
selected: true,
|
|
74
|
+
}),
|
|
75
|
+
}, (result) => `Default profile set to ${result.defaultProfile}.`, {
|
|
76
|
+
command: 'profile use',
|
|
77
|
+
});
|
|
78
|
+
})
|
|
79
|
+
.command('delete <profileName>', 'Delete a locally stored CLI profile', (deleteYargs) => deleteYargs
|
|
80
|
+
.positional('profileName', {
|
|
81
|
+
type: 'string',
|
|
82
|
+
describe: 'Profile name to delete',
|
|
83
|
+
})
|
|
84
|
+
.example('chalksurf profile delete prod-codex', 'Delete the prod-codex profile'), async (argv) => {
|
|
85
|
+
const profileName = String(argv.profileName ?? '');
|
|
86
|
+
assertValidProfileName(profileName);
|
|
87
|
+
const rootConfig = await context.configStore.deleteProfile(profileName);
|
|
88
|
+
context.output.print({
|
|
89
|
+
deletedProfile: profileName,
|
|
90
|
+
defaultProfile: rootConfig.defaultProfile ?? null,
|
|
91
|
+
}, (result) => `Deleted profile ${result.deletedProfile}.`, {
|
|
92
|
+
command: 'profile delete',
|
|
93
|
+
});
|
|
94
|
+
})
|
|
95
|
+
.demandCommand(1)
|
|
96
|
+
.strict();
|
|
97
|
+
};
|