@chalksurf/cli 0.1.0 → 0.2.0
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 +19 -85
- package/dist/bin/chalksurf.js +77 -10
- package/dist/commands/auth.js +31 -19
- package/dist/commands/exercise.js +428 -0
- package/dist/commands/job.js +18 -8
- package/dist/commands/org.js +13 -8
- package/dist/commands/sheet.js +286 -129
- package/dist/lib/api-client.js +9 -3
- package/dist/lib/cli-error.js +37 -1
- 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 +195 -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 +152 -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,120 @@
|
|
|
1
|
+
import { openAsBlob } from 'node:fs';
|
|
2
|
+
import { stat } from 'node:fs/promises';
|
|
3
|
+
import { extname, resolve } from 'node:path';
|
|
4
|
+
import { CliCommandError } from './cli-error.js';
|
|
5
|
+
const mimeTypesByExtension = {
|
|
6
|
+
'.avif': 'image/avif',
|
|
7
|
+
'.doc': 'application/msword',
|
|
8
|
+
'.docx': 'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
|
|
9
|
+
'.gif': 'image/gif',
|
|
10
|
+
'.htm': 'text/html',
|
|
11
|
+
'.html': 'text/html',
|
|
12
|
+
'.jpeg': 'image/jpeg',
|
|
13
|
+
'.jpg': 'image/jpeg',
|
|
14
|
+
'.md': 'text/markdown',
|
|
15
|
+
'.pdf': 'application/pdf',
|
|
16
|
+
'.png': 'image/png',
|
|
17
|
+
'.svg': 'image/svg+xml',
|
|
18
|
+
'.tex': 'text/plain',
|
|
19
|
+
'.tif': 'image/tiff',
|
|
20
|
+
'.tiff': 'image/tiff',
|
|
21
|
+
'.txt': 'text/plain',
|
|
22
|
+
'.webp': 'image/webp',
|
|
23
|
+
};
|
|
24
|
+
const guessMimeType = (fileName) => {
|
|
25
|
+
return mimeTypesByExtension[extname(fileName).toLowerCase()] ?? 'application/octet-stream';
|
|
26
|
+
};
|
|
27
|
+
const isHttpUrl = (value) => {
|
|
28
|
+
try {
|
|
29
|
+
const parsedUrl = new URL(value);
|
|
30
|
+
return ['http:', 'https:'].includes(parsedUrl.protocol);
|
|
31
|
+
}
|
|
32
|
+
catch {
|
|
33
|
+
return false;
|
|
34
|
+
}
|
|
35
|
+
};
|
|
36
|
+
export const buildFileImportFormData = async (resolvedSources) => {
|
|
37
|
+
const formData = new FormData();
|
|
38
|
+
await Promise.all(resolvedSources.map(async (resolvedSource, index) => {
|
|
39
|
+
const mimeType = resolvedSource.mimeType ?? guessMimeType(resolvedSource.fileName);
|
|
40
|
+
const fileBlob = await openAsBlob(resolvedSource.filePath, { type: mimeType });
|
|
41
|
+
const file = new File([fileBlob], resolvedSource.fileName, { type: mimeType });
|
|
42
|
+
formData.set(`file-${index}`, file);
|
|
43
|
+
formData.set(`path-${index}`, resolvedSource.relativePath);
|
|
44
|
+
}));
|
|
45
|
+
return formData;
|
|
46
|
+
};
|
|
47
|
+
export const buildFileImportCommandSources = async ({ cwd, loadManifest, manifestPath, rawSources, relativePath, stdin, wait, }) => {
|
|
48
|
+
if (manifestPath && rawSources.length > 0) {
|
|
49
|
+
throw new CliCommandError('Pass positional sources or --manifest, not both.', 2);
|
|
50
|
+
}
|
|
51
|
+
if (manifestPath && relativePath) {
|
|
52
|
+
throw new CliCommandError('--relative-path can only be used with a single positional source.', 2);
|
|
53
|
+
}
|
|
54
|
+
if (manifestPath) {
|
|
55
|
+
const manifest = await loadManifest({
|
|
56
|
+
cwd,
|
|
57
|
+
manifestPath,
|
|
58
|
+
stdin,
|
|
59
|
+
});
|
|
60
|
+
const { organizationId, sources, wait: manifestWait, ...manifestExtras } = manifest;
|
|
61
|
+
return {
|
|
62
|
+
organizationId,
|
|
63
|
+
sources,
|
|
64
|
+
wait: wait || manifestWait === true,
|
|
65
|
+
...manifestExtras,
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
if (rawSources.length === 0) {
|
|
69
|
+
throw new CliCommandError('At least one source or --manifest is required.', 2);
|
|
70
|
+
}
|
|
71
|
+
if (relativePath && rawSources.length !== 1) {
|
|
72
|
+
throw new CliCommandError('--relative-path can only be used with a single positional source.', 2);
|
|
73
|
+
}
|
|
74
|
+
const sources = [];
|
|
75
|
+
for (const rawSource of rawSources) {
|
|
76
|
+
if (isHttpUrl(rawSource)) {
|
|
77
|
+
sources.push({
|
|
78
|
+
kind: 'url',
|
|
79
|
+
url: rawSource,
|
|
80
|
+
relativePath,
|
|
81
|
+
});
|
|
82
|
+
continue;
|
|
83
|
+
}
|
|
84
|
+
try {
|
|
85
|
+
const pathStats = await stat(resolve(cwd, rawSource));
|
|
86
|
+
if (pathStats.isDirectory()) {
|
|
87
|
+
if (relativePath) {
|
|
88
|
+
throw new CliCommandError('--relative-path cannot be used with directory sources.', 2);
|
|
89
|
+
}
|
|
90
|
+
sources.push({
|
|
91
|
+
kind: 'directory',
|
|
92
|
+
path: rawSource,
|
|
93
|
+
});
|
|
94
|
+
continue;
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
catch (error) {
|
|
98
|
+
if (error.code !== 'ENOENT') {
|
|
99
|
+
throw error;
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
sources.push({
|
|
103
|
+
kind: 'local',
|
|
104
|
+
path: rawSource,
|
|
105
|
+
relativePath,
|
|
106
|
+
});
|
|
107
|
+
}
|
|
108
|
+
return {
|
|
109
|
+
organizationId: undefined,
|
|
110
|
+
sources,
|
|
111
|
+
wait: wait === true,
|
|
112
|
+
};
|
|
113
|
+
};
|
|
114
|
+
export const normalizeWaitTimeoutMs = (timeoutMs) => {
|
|
115
|
+
const normalizedTimeoutMs = timeoutMs ?? 300000;
|
|
116
|
+
if (!Number.isFinite(normalizedTimeoutMs) || normalizedTimeoutMs <= 0) {
|
|
117
|
+
throw new CliCommandError('--timeout-ms must be a positive number.', 2);
|
|
118
|
+
}
|
|
119
|
+
return normalizedTimeoutMs;
|
|
120
|
+
};
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
export const buildCliImportSources = ({ resolvedSources, getExtra, }) => {
|
|
2
|
+
return resolvedSources.map((resolvedSource, sourceIndex) => ({
|
|
3
|
+
sourceIndex,
|
|
4
|
+
sourceInputIndex: resolvedSource.sourceInputIndex,
|
|
5
|
+
sourceId: resolvedSource.sourceInput.sourceId ?? null,
|
|
6
|
+
kind: resolvedSource.kind,
|
|
7
|
+
input: resolvedSource.input,
|
|
8
|
+
fileName: resolvedSource.fileName,
|
|
9
|
+
relativePath: resolvedSource.relativePath,
|
|
10
|
+
...(getExtra ? getExtra(resolvedSource) : {}),
|
|
11
|
+
}));
|
|
12
|
+
};
|
|
13
|
+
export const summarizeCliImportJobs = ({ jobs, sourceCount, sourceInputCount, timedOut, }) => {
|
|
14
|
+
const summary = {
|
|
15
|
+
sourceCount,
|
|
16
|
+
sourceInputCount,
|
|
17
|
+
jobCount: jobs.length,
|
|
18
|
+
queued: 0,
|
|
19
|
+
pending: 0,
|
|
20
|
+
inProgress: 0,
|
|
21
|
+
completed: 0,
|
|
22
|
+
failed: 0,
|
|
23
|
+
timedOut,
|
|
24
|
+
};
|
|
25
|
+
for (const job of jobs) {
|
|
26
|
+
if (job.status === 'queued') {
|
|
27
|
+
summary.queued += 1;
|
|
28
|
+
continue;
|
|
29
|
+
}
|
|
30
|
+
if (job.status === 'pending') {
|
|
31
|
+
summary.pending += 1;
|
|
32
|
+
continue;
|
|
33
|
+
}
|
|
34
|
+
if (job.status === 'in_progress') {
|
|
35
|
+
summary.inProgress += 1;
|
|
36
|
+
continue;
|
|
37
|
+
}
|
|
38
|
+
if (job.status === 'completed') {
|
|
39
|
+
summary.completed += 1;
|
|
40
|
+
continue;
|
|
41
|
+
}
|
|
42
|
+
summary.failed += 1;
|
|
43
|
+
}
|
|
44
|
+
return summary;
|
|
45
|
+
};
|
|
46
|
+
export const formatCliImportSummary = (summary) => {
|
|
47
|
+
const parts = [];
|
|
48
|
+
if (summary.queued > 0) {
|
|
49
|
+
parts.push(`${summary.queued} queued`);
|
|
50
|
+
}
|
|
51
|
+
if (summary.pending > 0) {
|
|
52
|
+
parts.push(`${summary.pending} pending`);
|
|
53
|
+
}
|
|
54
|
+
if (summary.inProgress > 0) {
|
|
55
|
+
parts.push(`${summary.inProgress} in progress`);
|
|
56
|
+
}
|
|
57
|
+
if (summary.completed > 0) {
|
|
58
|
+
parts.push(`${summary.completed} completed`);
|
|
59
|
+
}
|
|
60
|
+
if (summary.failed > 0) {
|
|
61
|
+
parts.push(`${summary.failed} failed`);
|
|
62
|
+
}
|
|
63
|
+
if (summary.timedOut) {
|
|
64
|
+
parts.push('timed out');
|
|
65
|
+
}
|
|
66
|
+
return `Summary: ${parts.join(', ') || '0 jobs'}.`;
|
|
67
|
+
};
|
package/dist/lib/manifest.js
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import { readFile, stat } from 'node:fs/promises';
|
|
2
2
|
import { resolve } from 'node:path';
|
|
3
|
+
import { z } from 'zod';
|
|
3
4
|
import { CliCommandError } from './cli-error.js';
|
|
5
|
+
import { translationLanguages } from './translation-languages.js';
|
|
4
6
|
const isObject = (value) => {
|
|
5
7
|
return typeof value === 'object' && value !== null;
|
|
6
8
|
};
|
|
@@ -11,6 +13,88 @@ const normalizeOptionalString = (value) => {
|
|
|
11
13
|
const normalizedValue = value.trim();
|
|
12
14
|
return normalizedValue.length > 0 ? normalizedValue : undefined;
|
|
13
15
|
};
|
|
16
|
+
const translationLanguageListSchema = z
|
|
17
|
+
.array(z.enum(translationLanguages))
|
|
18
|
+
.min(1, { message: 'translateTo must include at least one language.' })
|
|
19
|
+
.refine((languages) => new Set(languages).size === languages.length, {
|
|
20
|
+
message: 'translateTo languages must be unique.',
|
|
21
|
+
});
|
|
22
|
+
const parseTranslateTo = ({ value, label }) => {
|
|
23
|
+
if (value == null) {
|
|
24
|
+
return undefined;
|
|
25
|
+
}
|
|
26
|
+
const parsedLanguages = translationLanguageListSchema.safeParse(value);
|
|
27
|
+
if (!parsedLanguages.success) {
|
|
28
|
+
const issue = parsedLanguages.error.issues[0];
|
|
29
|
+
throw new CliCommandError(`${label} has invalid translateTo: ${issue?.message ?? 'Invalid languages.'}`, 2);
|
|
30
|
+
}
|
|
31
|
+
return parsedLanguages.data;
|
|
32
|
+
};
|
|
33
|
+
const parseManifestObject = (manifestText) => {
|
|
34
|
+
let parsedManifest;
|
|
35
|
+
try {
|
|
36
|
+
parsedManifest = JSON.parse(manifestText);
|
|
37
|
+
}
|
|
38
|
+
catch {
|
|
39
|
+
throw new CliCommandError('Manifest must be valid JSON.', 2);
|
|
40
|
+
}
|
|
41
|
+
if (!isObject(parsedManifest)) {
|
|
42
|
+
throw new CliCommandError('Manifest must be a JSON object.', 2);
|
|
43
|
+
}
|
|
44
|
+
return parsedManifest;
|
|
45
|
+
};
|
|
46
|
+
const parseSourceManifestObject = (manifestText) => {
|
|
47
|
+
const parsedManifest = parseManifestObject(manifestText);
|
|
48
|
+
if (!Array.isArray(parsedManifest.sources) || parsedManifest.sources.length === 0) {
|
|
49
|
+
throw new CliCommandError('Manifest must include a non-empty sources array.', 2);
|
|
50
|
+
}
|
|
51
|
+
return parsedManifest;
|
|
52
|
+
};
|
|
53
|
+
const isValidTargetFolderPath = (value) => {
|
|
54
|
+
const normalizedSegments = value
|
|
55
|
+
.replaceAll('\\', '/')
|
|
56
|
+
.split('/')
|
|
57
|
+
.filter((segment) => segment.length > 0);
|
|
58
|
+
return normalizedSegments.length > 0 && normalizedSegments.every((segment) => segment !== '.' && segment !== '..');
|
|
59
|
+
};
|
|
60
|
+
const parseTargetFolderPath = ({ value, label }) => {
|
|
61
|
+
if (value === null) {
|
|
62
|
+
return null;
|
|
63
|
+
}
|
|
64
|
+
const normalizedValue = normalizeOptionalString(value);
|
|
65
|
+
if (!normalizedValue) {
|
|
66
|
+
throw new CliCommandError(`${label} must include targetFolderPath as a string or null.`, 2);
|
|
67
|
+
}
|
|
68
|
+
if (!isValidTargetFolderPath(normalizedValue)) {
|
|
69
|
+
throw new CliCommandError(`${label} targetFolderPath must be a normalized folder path without "." or ".." segments.`, 2);
|
|
70
|
+
}
|
|
71
|
+
return normalizedValue;
|
|
72
|
+
};
|
|
73
|
+
const parseManifestMetadata = ({ parsedManifest, topLevelFields, }) => {
|
|
74
|
+
const wait = typeof parsedManifest.wait === 'boolean' ? parsedManifest.wait : undefined;
|
|
75
|
+
return {
|
|
76
|
+
organizationId: normalizeOptionalString(parsedManifest.organizationId),
|
|
77
|
+
wait,
|
|
78
|
+
...(topLevelFields.includes('exerciseId')
|
|
79
|
+
? { exerciseId: normalizeOptionalString(parsedManifest.exerciseId) }
|
|
80
|
+
: {}),
|
|
81
|
+
...(topLevelFields.includes('exerciseSheetId')
|
|
82
|
+
? { exerciseSheetId: normalizeOptionalString(parsedManifest.exerciseSheetId) }
|
|
83
|
+
: {}),
|
|
84
|
+
};
|
|
85
|
+
};
|
|
86
|
+
const assertUniqueSourceIds = (sources) => {
|
|
87
|
+
const seenSourceIds = new Set();
|
|
88
|
+
for (const source of sources) {
|
|
89
|
+
if (!source.sourceId) {
|
|
90
|
+
continue;
|
|
91
|
+
}
|
|
92
|
+
if (seenSourceIds.has(source.sourceId)) {
|
|
93
|
+
throw new CliCommandError(`Manifest sourceId "${source.sourceId}" must be unique.`, 2);
|
|
94
|
+
}
|
|
95
|
+
seenSourceIds.add(source.sourceId);
|
|
96
|
+
}
|
|
97
|
+
};
|
|
14
98
|
const isInteractiveStdin = (stdin) => {
|
|
15
99
|
return stdin.isTTY === true;
|
|
16
100
|
};
|
|
@@ -21,11 +105,15 @@ const readTextFromStdin = async (stdin) => {
|
|
|
21
105
|
}
|
|
22
106
|
return text;
|
|
23
107
|
};
|
|
24
|
-
const
|
|
108
|
+
const parseBaseManifestSource = (value, index) => {
|
|
25
109
|
if (!isObject(value)) {
|
|
26
110
|
throw new CliCommandError(`Manifest source at index ${index} must be an object.`, 2);
|
|
27
111
|
}
|
|
28
112
|
const kind = normalizeOptionalString(value.kind);
|
|
113
|
+
const sourceId = normalizeOptionalString(value.sourceId);
|
|
114
|
+
if ('title' in value || 'translateTo' in value) {
|
|
115
|
+
throw new CliCommandError(`Manifest source at index ${index} cannot include title or translateTo for this command.`, 2);
|
|
116
|
+
}
|
|
29
117
|
if (kind === 'local') {
|
|
30
118
|
const path = normalizeOptionalString(value.path);
|
|
31
119
|
if (!path) {
|
|
@@ -35,6 +123,7 @@ const parseManifestSource = (value, index) => {
|
|
|
35
123
|
kind: 'local',
|
|
36
124
|
path,
|
|
37
125
|
relativePath: normalizeOptionalString(value.relativePath),
|
|
126
|
+
sourceId,
|
|
38
127
|
};
|
|
39
128
|
}
|
|
40
129
|
if (kind === 'directory') {
|
|
@@ -46,6 +135,7 @@ const parseManifestSource = (value, index) => {
|
|
|
46
135
|
kind: 'directory',
|
|
47
136
|
path,
|
|
48
137
|
relativeRoot: normalizeOptionalString(value.relativeRoot),
|
|
138
|
+
sourceId,
|
|
49
139
|
};
|
|
50
140
|
}
|
|
51
141
|
if (kind === 'url') {
|
|
@@ -57,35 +147,85 @@ const parseManifestSource = (value, index) => {
|
|
|
57
147
|
kind: 'url',
|
|
58
148
|
url,
|
|
59
149
|
relativePath: normalizeOptionalString(value.relativePath),
|
|
150
|
+
sourceId,
|
|
60
151
|
};
|
|
61
152
|
}
|
|
62
153
|
throw new CliCommandError(`Manifest source at index ${index} has unsupported kind "${String(value.kind)}".`, 2);
|
|
63
154
|
};
|
|
64
|
-
const
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
155
|
+
const parseExerciseImportManifest = (manifestText) => {
|
|
156
|
+
const parsedManifest = parseSourceManifestObject(manifestText);
|
|
157
|
+
const sources = parsedManifest.sources.map((source, index) => parseBaseManifestSource(source, index));
|
|
158
|
+
assertUniqueSourceIds(sources);
|
|
159
|
+
return {
|
|
160
|
+
...parseManifestMetadata({
|
|
161
|
+
parsedManifest,
|
|
162
|
+
topLevelFields: ['exerciseSheetId'],
|
|
163
|
+
}),
|
|
164
|
+
sources,
|
|
165
|
+
};
|
|
166
|
+
};
|
|
167
|
+
const parseExerciseSolutionImportManifest = (manifestText) => {
|
|
168
|
+
const parsedManifest = parseSourceManifestObject(manifestText);
|
|
169
|
+
const sources = parsedManifest.sources.map((source, index) => parseBaseManifestSource(source, index));
|
|
170
|
+
assertUniqueSourceIds(sources);
|
|
171
|
+
return {
|
|
172
|
+
...parseManifestMetadata({
|
|
173
|
+
parsedManifest,
|
|
174
|
+
topLevelFields: ['exerciseId'],
|
|
175
|
+
}),
|
|
176
|
+
sources,
|
|
177
|
+
};
|
|
178
|
+
};
|
|
179
|
+
const parseSheetImportManifest = (manifestText) => {
|
|
180
|
+
const parsedManifest = parseManifestObject(manifestText);
|
|
181
|
+
if ('sources' in parsedManifest) {
|
|
182
|
+
throw new CliCommandError('Sheet import manifests must use top-level sheets[]. Top-level sources[] is no longer supported.', 2);
|
|
74
183
|
}
|
|
75
|
-
if (!Array.isArray(parsedManifest.
|
|
76
|
-
throw new CliCommandError('Manifest must include a non-empty
|
|
184
|
+
if (!Array.isArray(parsedManifest.sheets) || parsedManifest.sheets.length === 0) {
|
|
185
|
+
throw new CliCommandError('Manifest must include a non-empty sheets array.', 2);
|
|
77
186
|
}
|
|
78
|
-
const
|
|
187
|
+
const sources = [];
|
|
188
|
+
const sheetGroups = [];
|
|
189
|
+
parsedManifest.sheets.forEach((sheetValue, sheetIndex) => {
|
|
190
|
+
if (!isObject(sheetValue)) {
|
|
191
|
+
throw new CliCommandError(`Manifest sheet at index ${sheetIndex} must be an object.`, 2);
|
|
192
|
+
}
|
|
193
|
+
if (!Array.isArray(sheetValue.sources) || sheetValue.sources.length === 0) {
|
|
194
|
+
throw new CliCommandError(`Manifest sheet at index ${sheetIndex} must include a non-empty sources array.`, 2);
|
|
195
|
+
}
|
|
196
|
+
const sourceInputIndexes = sheetValue.sources.map((sourceValue) => {
|
|
197
|
+
const flattenedSourceIndex = sources.length;
|
|
198
|
+
const parsedSource = parseBaseManifestSource(sourceValue, flattenedSourceIndex);
|
|
199
|
+
sources.push(parsedSource);
|
|
200
|
+
return flattenedSourceIndex;
|
|
201
|
+
});
|
|
202
|
+
sheetGroups.push({
|
|
203
|
+
sourceInputIndexes,
|
|
204
|
+
targetFolderPath: parseTargetFolderPath({
|
|
205
|
+
value: sheetValue.targetFolderPath,
|
|
206
|
+
label: `Manifest sheet at index ${sheetIndex}`,
|
|
207
|
+
}),
|
|
208
|
+
title: normalizeOptionalString(sheetValue.title),
|
|
209
|
+
translateTo: parseTranslateTo({
|
|
210
|
+
value: sheetValue.translateTo,
|
|
211
|
+
label: `Manifest sheet at index ${sheetIndex}`,
|
|
212
|
+
}),
|
|
213
|
+
});
|
|
214
|
+
});
|
|
215
|
+
assertUniqueSourceIds(sources);
|
|
79
216
|
return {
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
217
|
+
...parseManifestMetadata({
|
|
218
|
+
parsedManifest,
|
|
219
|
+
topLevelFields: [],
|
|
220
|
+
}),
|
|
221
|
+
sources,
|
|
222
|
+
sheetGroups,
|
|
83
223
|
};
|
|
84
224
|
};
|
|
85
|
-
|
|
225
|
+
const loadManifest = async ({ cwd, manifestPath, parseManifest, stdin, }) => {
|
|
86
226
|
if (manifestPath === '-') {
|
|
87
227
|
if (isInteractiveStdin(stdin)) {
|
|
88
|
-
throw new CliCommandError('No manifest was piped on stdin. Pipe JSON into "
|
|
228
|
+
throw new CliCommandError('No manifest was piped on stdin. Pipe JSON into "--manifest -".', 2);
|
|
89
229
|
}
|
|
90
230
|
return parseManifest(await readTextFromStdin(stdin));
|
|
91
231
|
}
|
|
@@ -105,3 +245,27 @@ export const loadSheetImportManifest = async ({ cwd, manifestPath, stdin, }) =>
|
|
|
105
245
|
}
|
|
106
246
|
return parseManifest(await readFile(resolvedManifestPath, 'utf8'));
|
|
107
247
|
};
|
|
248
|
+
export const loadExerciseImportManifest = async ({ cwd, manifestPath, stdin, }) => {
|
|
249
|
+
return await loadManifest({
|
|
250
|
+
cwd,
|
|
251
|
+
manifestPath,
|
|
252
|
+
parseManifest: parseExerciseImportManifest,
|
|
253
|
+
stdin,
|
|
254
|
+
});
|
|
255
|
+
};
|
|
256
|
+
export const loadExerciseSolutionImportManifest = async ({ cwd, manifestPath, stdin, }) => {
|
|
257
|
+
return await loadManifest({
|
|
258
|
+
cwd,
|
|
259
|
+
manifestPath,
|
|
260
|
+
parseManifest: parseExerciseSolutionImportManifest,
|
|
261
|
+
stdin,
|
|
262
|
+
});
|
|
263
|
+
};
|
|
264
|
+
export const loadSheetImportManifest = async ({ cwd, manifestPath, stdin, }) => {
|
|
265
|
+
return await loadManifest({
|
|
266
|
+
cwd,
|
|
267
|
+
manifestPath,
|
|
268
|
+
parseManifest: parseSheetImportManifest,
|
|
269
|
+
stdin,
|
|
270
|
+
});
|
|
271
|
+
};
|
package/dist/lib/output.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
const cliJsonSchemaVersion = 'v1';
|
|
1
2
|
const ensureTrailingNewline = (value) => {
|
|
2
3
|
return value.endsWith('\n') ? value : `${value}\n`;
|
|
3
4
|
};
|
|
@@ -5,16 +6,48 @@ const writeLine = (writer, value) => {
|
|
|
5
6
|
writer.write(ensureTrailingNewline(value));
|
|
6
7
|
};
|
|
7
8
|
export const createOutput = ({ json, stdout, stderr, }) => {
|
|
9
|
+
let warnings = [];
|
|
10
|
+
const consumeWarnings = () => {
|
|
11
|
+
const nextWarnings = warnings;
|
|
12
|
+
warnings = [];
|
|
13
|
+
return nextWarnings;
|
|
14
|
+
};
|
|
8
15
|
return {
|
|
9
|
-
|
|
16
|
+
json,
|
|
17
|
+
print: (value, humanFormatter, options) => {
|
|
10
18
|
if (json) {
|
|
11
|
-
writeLine(stdout, JSON.stringify(
|
|
19
|
+
writeLine(stdout, JSON.stringify({
|
|
20
|
+
schemaVersion: cliJsonSchemaVersion,
|
|
21
|
+
command: options.command,
|
|
22
|
+
ok: options.ok ?? true,
|
|
23
|
+
result: value,
|
|
24
|
+
...(options.error ? { error: options.error } : {}),
|
|
25
|
+
warnings: consumeWarnings(),
|
|
26
|
+
}, null, 2));
|
|
12
27
|
return;
|
|
13
28
|
}
|
|
14
29
|
const message = typeof humanFormatter === 'function' ? humanFormatter(value) : humanFormatter;
|
|
15
30
|
writeLine(stdout, message);
|
|
16
31
|
},
|
|
32
|
+
printError: ({ command, error, result }) => {
|
|
33
|
+
if (json) {
|
|
34
|
+
writeLine(stdout, JSON.stringify({
|
|
35
|
+
schemaVersion: cliJsonSchemaVersion,
|
|
36
|
+
command,
|
|
37
|
+
ok: false,
|
|
38
|
+
...(result === undefined ? {} : { result }),
|
|
39
|
+
error,
|
|
40
|
+
warnings: consumeWarnings(),
|
|
41
|
+
}, null, 2));
|
|
42
|
+
return;
|
|
43
|
+
}
|
|
44
|
+
writeLine(stderr, error.message);
|
|
45
|
+
},
|
|
17
46
|
info: (message) => {
|
|
47
|
+
if (json) {
|
|
48
|
+
warnings.push(message);
|
|
49
|
+
return;
|
|
50
|
+
}
|
|
18
51
|
writeLine(stderr, message);
|
|
19
52
|
},
|
|
20
53
|
error: (message) => {
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { createInterface } from 'node:readline/promises';
|
|
2
|
+
import { Writable } from 'node:stream';
|
|
3
|
+
export const createPromptSecret = ({ input = process.stdin, output = process.stderr, } = {}) => {
|
|
4
|
+
return async (message) => {
|
|
5
|
+
let muted = false;
|
|
6
|
+
const maskedOutput = new Writable({
|
|
7
|
+
write(chunk, encoding, callback) {
|
|
8
|
+
if (!muted) {
|
|
9
|
+
output.write(chunk, encoding);
|
|
10
|
+
}
|
|
11
|
+
callback();
|
|
12
|
+
},
|
|
13
|
+
});
|
|
14
|
+
const readline = createInterface({
|
|
15
|
+
input,
|
|
16
|
+
output: maskedOutput,
|
|
17
|
+
terminal: true,
|
|
18
|
+
});
|
|
19
|
+
try {
|
|
20
|
+
output.write(message);
|
|
21
|
+
muted = true;
|
|
22
|
+
const value = await readline.question('');
|
|
23
|
+
muted = false;
|
|
24
|
+
output.write('\n');
|
|
25
|
+
return value.trim();
|
|
26
|
+
}
|
|
27
|
+
finally {
|
|
28
|
+
muted = false;
|
|
29
|
+
readline.close();
|
|
30
|
+
}
|
|
31
|
+
};
|
|
32
|
+
};
|
package/dist/lib/session.js
CHANGED
|
@@ -21,7 +21,7 @@ export const requireResolvedBaseUrl = ({ flagValue, env, config, }) => {
|
|
|
21
21
|
export const requireResolvedToken = ({ env, config, }) => {
|
|
22
22
|
const resolvedToken = resolveToken({ env, config });
|
|
23
23
|
if (!resolvedToken.value) {
|
|
24
|
-
throw new CliCommandError(`Not authenticated. Set ${chalksurfTokenEnvVar} or run "chalksurf auth login
|
|
24
|
+
throw new CliCommandError(`Not authenticated. Set ${chalksurfTokenEnvVar} or run "chalksurf auth login".`, 3);
|
|
25
25
|
}
|
|
26
26
|
return resolvedToken;
|
|
27
27
|
};
|