@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/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
|
};
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { createWriteStream } from 'node:fs';
|
|
2
|
-
import { mkdtemp, readdir, rm, stat } from 'node:fs/promises';
|
|
2
|
+
import { mkdtemp, open, readdir, rm, stat } from 'node:fs/promises';
|
|
3
3
|
import { tmpdir } from 'node:os';
|
|
4
4
|
import { basename, isAbsolute, relative, resolve } from 'node:path';
|
|
5
5
|
import { Readable } from 'node:stream';
|
|
@@ -8,6 +8,8 @@ import pLimit from 'p-limit';
|
|
|
8
8
|
import { CliCommandError } from './cli-error.js';
|
|
9
9
|
const sourceResolutionExitCode = 4;
|
|
10
10
|
const defaultMaxConcurrentUrlDownloads = 4;
|
|
11
|
+
const htmlSniffBytes = 1024;
|
|
12
|
+
const genericMimeTypes = new Set(['application/octet-stream', 'binary/octet-stream']);
|
|
11
13
|
const createSourceResolutionError = (message) => {
|
|
12
14
|
return new CliCommandError(message, sourceResolutionExitCode);
|
|
13
15
|
};
|
|
@@ -85,7 +87,44 @@ const deriveRelativePathFromUrl = (url) => {
|
|
|
85
87
|
}
|
|
86
88
|
return fileName;
|
|
87
89
|
};
|
|
88
|
-
const
|
|
90
|
+
const normalizeMimeType = (contentTypeHeaderValue) => {
|
|
91
|
+
const mimeType = contentTypeHeaderValue?.split(';', 1)[0]?.trim().toLowerCase();
|
|
92
|
+
return mimeType || undefined;
|
|
93
|
+
};
|
|
94
|
+
const stripLeadingHtmlComments = (value) => {
|
|
95
|
+
let normalizedValue = value.trimStart();
|
|
96
|
+
while (normalizedValue.startsWith('<!--')) {
|
|
97
|
+
const commentEndIndex = normalizedValue.indexOf('-->');
|
|
98
|
+
if (commentEndIndex === -1) {
|
|
99
|
+
return normalizedValue;
|
|
100
|
+
}
|
|
101
|
+
normalizedValue = normalizedValue.slice(commentEndIndex + 3).trimStart();
|
|
102
|
+
}
|
|
103
|
+
return normalizedValue;
|
|
104
|
+
};
|
|
105
|
+
const looksLikeHtmlDocument = (value) => {
|
|
106
|
+
const normalizedValue = stripLeadingHtmlComments(value.replace(/^\uFEFF/, ''));
|
|
107
|
+
return /^<(?:!doctype html|html|head|body)\b/i.test(normalizedValue);
|
|
108
|
+
};
|
|
109
|
+
const sniffMimeType = async ({ filePath, responseMimeType }) => {
|
|
110
|
+
if (responseMimeType && !genericMimeTypes.has(responseMimeType)) {
|
|
111
|
+
return responseMimeType;
|
|
112
|
+
}
|
|
113
|
+
const fileHandle = await open(filePath, 'r');
|
|
114
|
+
try {
|
|
115
|
+
const buffer = Buffer.alloc(htmlSniffBytes);
|
|
116
|
+
const { bytesRead } = await fileHandle.read(buffer, 0, buffer.length, 0);
|
|
117
|
+
const filePrefix = buffer.subarray(0, bytesRead).toString('utf8');
|
|
118
|
+
if (looksLikeHtmlDocument(filePrefix)) {
|
|
119
|
+
return 'text/html';
|
|
120
|
+
}
|
|
121
|
+
return responseMimeType;
|
|
122
|
+
}
|
|
123
|
+
finally {
|
|
124
|
+
await fileHandle.close();
|
|
125
|
+
}
|
|
126
|
+
};
|
|
127
|
+
const resolveLocalFileSource = async ({ source, cwd, sourceInputIndex, }) => {
|
|
89
128
|
const resolvedPath = resolve(cwd, source.path);
|
|
90
129
|
await ensureExistingFile(resolvedPath, 'Local source');
|
|
91
130
|
const relativePath = normalizeRelativePath(source.relativePath ?? basename(resolvedPath));
|
|
@@ -96,10 +135,12 @@ const resolveLocalFileSource = async ({ source, cwd, }) => {
|
|
|
96
135
|
filePath: resolvedPath,
|
|
97
136
|
fileName: basename(relativePath),
|
|
98
137
|
relativePath,
|
|
138
|
+
sourceInputIndex,
|
|
139
|
+
sourceInput: source,
|
|
99
140
|
},
|
|
100
141
|
];
|
|
101
142
|
};
|
|
102
|
-
const resolveDirectorySource = async ({ source, cwd, }) => {
|
|
143
|
+
const resolveDirectorySource = async ({ source, cwd, sourceInputIndex, }) => {
|
|
103
144
|
const resolvedDirectoryPath = resolve(cwd, source.path);
|
|
104
145
|
const resolvedRelativeRoot = resolve(cwd, source.relativeRoot ?? source.path);
|
|
105
146
|
await ensureExistingDirectory(resolvedDirectoryPath, 'Directory source');
|
|
@@ -119,10 +160,12 @@ const resolveDirectorySource = async ({ source, cwd, }) => {
|
|
|
119
160
|
filePath,
|
|
120
161
|
fileName: basename(relativePath),
|
|
121
162
|
relativePath,
|
|
163
|
+
sourceInputIndex,
|
|
164
|
+
sourceInput: source,
|
|
122
165
|
};
|
|
123
166
|
});
|
|
124
167
|
};
|
|
125
|
-
const resolveUrlSource = async ({ source,
|
|
168
|
+
const resolveUrlSource = async ({ source, sourceInputIndex, }) => {
|
|
126
169
|
let parsedUrl;
|
|
127
170
|
try {
|
|
128
171
|
parsedUrl = new URL(source.url);
|
|
@@ -137,22 +180,30 @@ const resolveUrlSource = async ({ source, fetchImpl, }) => {
|
|
|
137
180
|
const downloadDirectoryPath = await mkdtemp(resolve(tmpdir(), 'chalksurf-cli-source-'));
|
|
138
181
|
const downloadedFilePath = resolve(downloadDirectoryPath, basename(relativePath));
|
|
139
182
|
try {
|
|
140
|
-
const response = await
|
|
183
|
+
const response = await fetch(source.url);
|
|
141
184
|
if (!response.ok) {
|
|
142
185
|
throw createSourceResolutionError(`Failed to download "${source.url}": HTTP ${response.status}${response.statusText ? ` ${response.statusText}` : ''}.`);
|
|
143
186
|
}
|
|
144
187
|
if (!response.body) {
|
|
145
188
|
throw createSourceResolutionError(`Failed to download "${source.url}": response body was empty.`);
|
|
146
189
|
}
|
|
190
|
+
const responseMimeType = normalizeMimeType(response.headers.get('content-type'));
|
|
147
191
|
await pipeline(Readable.fromWeb(response.body), createWriteStream(downloadedFilePath));
|
|
192
|
+
const mimeType = await sniffMimeType({
|
|
193
|
+
filePath: downloadedFilePath,
|
|
194
|
+
responseMimeType,
|
|
195
|
+
});
|
|
148
196
|
return [
|
|
149
197
|
{
|
|
150
198
|
kind: 'url',
|
|
151
199
|
input: source.url,
|
|
152
200
|
filePath: downloadedFilePath,
|
|
153
201
|
fileName: basename(relativePath),
|
|
202
|
+
mimeType,
|
|
154
203
|
relativePath,
|
|
204
|
+
sourceInputIndex,
|
|
155
205
|
cleanupPath: downloadDirectoryPath,
|
|
206
|
+
sourceInput: source,
|
|
156
207
|
},
|
|
157
208
|
];
|
|
158
209
|
}
|
|
@@ -182,16 +233,27 @@ export const cleanupResolvedSources = async (resolvedSources) => {
|
|
|
182
233
|
await rm(cleanupPath, { force: true, recursive: true });
|
|
183
234
|
}));
|
|
184
235
|
};
|
|
185
|
-
export const resolveSources = async ({ sources, cwd = process.cwd(),
|
|
236
|
+
export const resolveSources = async ({ sources, cwd = process.cwd(), maxConcurrentUrlDownloads = defaultMaxConcurrentUrlDownloads, }) => {
|
|
186
237
|
const limitUrlDownloads = pLimit(Math.max(1, maxConcurrentUrlDownloads));
|
|
187
|
-
const settledSourceGroups = await Promise.allSettled(sources.map(async (source) => {
|
|
238
|
+
const settledSourceGroups = await Promise.allSettled(sources.map(async (source, sourceInputIndex) => {
|
|
188
239
|
if (source.kind === 'local') {
|
|
189
|
-
return await resolveLocalFileSource({
|
|
240
|
+
return await resolveLocalFileSource({
|
|
241
|
+
source: source,
|
|
242
|
+
cwd,
|
|
243
|
+
sourceInputIndex,
|
|
244
|
+
});
|
|
190
245
|
}
|
|
191
246
|
if (source.kind === 'directory') {
|
|
192
|
-
return await resolveDirectorySource({
|
|
247
|
+
return await resolveDirectorySource({
|
|
248
|
+
source: source,
|
|
249
|
+
cwd,
|
|
250
|
+
sourceInputIndex,
|
|
251
|
+
});
|
|
193
252
|
}
|
|
194
|
-
return await limitUrlDownloads(() => resolveUrlSource({
|
|
253
|
+
return await limitUrlDownloads(() => resolveUrlSource({
|
|
254
|
+
source: source,
|
|
255
|
+
sourceInputIndex,
|
|
256
|
+
}));
|
|
195
257
|
}));
|
|
196
258
|
const resolvedSources = settledSourceGroups.flatMap((result) => (result.status === 'fulfilled' ? result.value : []));
|
|
197
259
|
const firstRejectedResult = settledSourceGroups.find((result) => result.status === 'rejected');
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export const translationLanguages = ['english', 'hungarian', 'german', 'french', 'spanish', 'italian'];
|
package/dist/lib/user-jobs.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { CliCommandError } from './cli-error.js';
|
|
1
|
+
import { CliCommandError, createSerializableCliError } from './cli-error.js';
|
|
2
2
|
import { mapApiErrorToCliError } from './session.js';
|
|
3
3
|
const isTerminalJob = (job) => {
|
|
4
4
|
return ['completed', 'failed'].includes(job.status);
|
|
@@ -41,6 +41,21 @@ export const getWaitExitCode = ({ jobs, timedOut }) => {
|
|
|
41
41
|
}
|
|
42
42
|
return 0;
|
|
43
43
|
};
|
|
44
|
+
export const getWaitError = ({ jobs, timedOut }) => {
|
|
45
|
+
if (timedOut) {
|
|
46
|
+
return createSerializableCliError({
|
|
47
|
+
exitCode: 6,
|
|
48
|
+
message: 'Timed out before all jobs completed.',
|
|
49
|
+
});
|
|
50
|
+
}
|
|
51
|
+
if (jobs.some((job) => job.status === 'failed')) {
|
|
52
|
+
return createSerializableCliError({
|
|
53
|
+
exitCode: 7,
|
|
54
|
+
message: 'One or more jobs failed.',
|
|
55
|
+
});
|
|
56
|
+
}
|
|
57
|
+
return undefined;
|
|
58
|
+
};
|
|
44
59
|
export const waitForCliJobs = async ({ getUserJob, jobIds, maxPollIntervalMs = 5000, now, sleep, timeoutMs = 300000, }) => {
|
|
45
60
|
const startedAt = now();
|
|
46
61
|
let nextPollDelayMs = 1000;
|