@chalksurf/cli 0.2.2 → 0.2.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +21 -8
- package/dist/bin/chalksurf.js +6336 -158
- package/docs/agents.md +123 -3
- package/docs/examples/sheet-import-manifest.json +24 -0
- package/docs/manifest.md +24 -1
- package/docs/manual.md +43 -0
- package/docs/mcp.md +88 -0
- package/package.json +3 -2
- package/schemas/sheet-import-manifest.schema.json +78 -22
- package/dist/commands/auth.js +0 -174
- package/dist/commands/exercise.js +0 -600
- package/dist/commands/job.js +0 -172
- package/dist/commands/org.js +0 -115
- package/dist/commands/profile.js +0 -97
- package/dist/commands/sheet.js +0 -729
- package/dist/lib/api-client.js +0 -86
- package/dist/lib/cli-error.js +0 -46
- package/dist/lib/command-options.js +0 -61
- package/dist/lib/config-store.js +0 -354
- package/dist/lib/import-files.js +0 -120
- package/dist/lib/import-output.js +0 -81
- package/dist/lib/manifest.js +0 -295
- package/dist/lib/output.js +0 -57
- package/dist/lib/prompt-secret.js +0 -32
- package/dist/lib/session.js +0 -72
- package/dist/lib/source-resolver.js +0 -272
- package/dist/lib/translation-languages.js +0 -16
- package/dist/lib/user-jobs.js +0 -99
|
@@ -1,272 +0,0 @@
|
|
|
1
|
-
import { createWriteStream } from 'node:fs';
|
|
2
|
-
import { mkdtemp, open, readdir, rm, stat } from 'node:fs/promises';
|
|
3
|
-
import { tmpdir } from 'node:os';
|
|
4
|
-
import { basename, isAbsolute, relative, resolve } from 'node:path';
|
|
5
|
-
import { Readable } from 'node:stream';
|
|
6
|
-
import { pipeline } from 'node:stream/promises';
|
|
7
|
-
import pLimit from 'p-limit';
|
|
8
|
-
import { CliCommandError } from './cli-error.js';
|
|
9
|
-
const sourceResolutionExitCode = 4;
|
|
10
|
-
const defaultMaxConcurrentUrlDownloads = 4;
|
|
11
|
-
const htmlSniffBytes = 1024;
|
|
12
|
-
const genericMimeTypes = new Set(['application/octet-stream', 'binary/octet-stream']);
|
|
13
|
-
const createSourceResolutionError = (message) => {
|
|
14
|
-
return new CliCommandError(message, sourceResolutionExitCode);
|
|
15
|
-
};
|
|
16
|
-
const normalizeRelativePath = (relativePath) => {
|
|
17
|
-
const normalizedSegments = relativePath
|
|
18
|
-
.replaceAll('\\', '/')
|
|
19
|
-
.split('/')
|
|
20
|
-
.filter((segment) => segment.length > 0 && segment !== '.');
|
|
21
|
-
if (normalizedSegments.some((segment) => segment === '..')) {
|
|
22
|
-
throw createSourceResolutionError(`Relative path "${relativePath}" cannot contain "..".`);
|
|
23
|
-
}
|
|
24
|
-
const normalizedRelativePath = normalizedSegments.join('/');
|
|
25
|
-
if (!normalizedRelativePath) {
|
|
26
|
-
throw createSourceResolutionError('Relative path cannot be empty.');
|
|
27
|
-
}
|
|
28
|
-
return normalizedRelativePath;
|
|
29
|
-
};
|
|
30
|
-
const ensureExistingFile = async (filePath, label) => {
|
|
31
|
-
let fileStats;
|
|
32
|
-
try {
|
|
33
|
-
fileStats = await stat(filePath);
|
|
34
|
-
}
|
|
35
|
-
catch (error) {
|
|
36
|
-
if (error.code === 'ENOENT') {
|
|
37
|
-
throw createSourceResolutionError(`${label} "${filePath}" does not exist.`);
|
|
38
|
-
}
|
|
39
|
-
throw error;
|
|
40
|
-
}
|
|
41
|
-
if (!fileStats.isFile()) {
|
|
42
|
-
throw createSourceResolutionError(`${label} "${filePath}" is not a file.`);
|
|
43
|
-
}
|
|
44
|
-
};
|
|
45
|
-
const ensureExistingDirectory = async (directoryPath, label) => {
|
|
46
|
-
let directoryStats;
|
|
47
|
-
try {
|
|
48
|
-
directoryStats = await stat(directoryPath);
|
|
49
|
-
}
|
|
50
|
-
catch (error) {
|
|
51
|
-
if (error.code === 'ENOENT') {
|
|
52
|
-
throw createSourceResolutionError(`${label} "${directoryPath}" does not exist.`);
|
|
53
|
-
}
|
|
54
|
-
throw error;
|
|
55
|
-
}
|
|
56
|
-
if (!directoryStats.isDirectory()) {
|
|
57
|
-
throw createSourceResolutionError(`${label} "${directoryPath}" is not a directory.`);
|
|
58
|
-
}
|
|
59
|
-
};
|
|
60
|
-
const isContainedWithin = ({ childPath, parentPath }) => {
|
|
61
|
-
const relativePath = relative(parentPath, childPath);
|
|
62
|
-
if (!relativePath) {
|
|
63
|
-
return true;
|
|
64
|
-
}
|
|
65
|
-
return !relativePath.startsWith('..') && !isAbsolute(relativePath);
|
|
66
|
-
};
|
|
67
|
-
const collectFilesRecursively = async (directoryPath) => {
|
|
68
|
-
const directoryEntries = await readdir(directoryPath, { withFileTypes: true });
|
|
69
|
-
const sortedEntries = directoryEntries.sort((leftEntry, rightEntry) => leftEntry.name.localeCompare(rightEntry.name));
|
|
70
|
-
const filePaths = [];
|
|
71
|
-
for (const directoryEntry of sortedEntries) {
|
|
72
|
-
const entryPath = resolve(directoryPath, directoryEntry.name);
|
|
73
|
-
if (directoryEntry.isDirectory()) {
|
|
74
|
-
filePaths.push(...(await collectFilesRecursively(entryPath)));
|
|
75
|
-
continue;
|
|
76
|
-
}
|
|
77
|
-
if (directoryEntry.isFile()) {
|
|
78
|
-
filePaths.push(entryPath);
|
|
79
|
-
}
|
|
80
|
-
}
|
|
81
|
-
return filePaths;
|
|
82
|
-
};
|
|
83
|
-
const deriveRelativePathFromUrl = (url) => {
|
|
84
|
-
const fileName = basename(decodeURIComponent(url.pathname));
|
|
85
|
-
if (!fileName) {
|
|
86
|
-
throw createSourceResolutionError(`URL source "${url.toString()}" does not include a file name. Pass a relative path explicitly.`);
|
|
87
|
-
}
|
|
88
|
-
return fileName;
|
|
89
|
-
};
|
|
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, }) => {
|
|
128
|
-
const resolvedPath = resolve(cwd, source.path);
|
|
129
|
-
await ensureExistingFile(resolvedPath, 'Local source');
|
|
130
|
-
const relativePath = normalizeRelativePath(source.relativePath ?? basename(resolvedPath));
|
|
131
|
-
return [
|
|
132
|
-
{
|
|
133
|
-
kind: 'local',
|
|
134
|
-
input: source.path,
|
|
135
|
-
filePath: resolvedPath,
|
|
136
|
-
fileName: basename(relativePath),
|
|
137
|
-
relativePath,
|
|
138
|
-
sourceInputIndex,
|
|
139
|
-
sourceInput: source,
|
|
140
|
-
},
|
|
141
|
-
];
|
|
142
|
-
};
|
|
143
|
-
const resolveDirectorySource = async ({ source, cwd, sourceInputIndex, }) => {
|
|
144
|
-
const resolvedDirectoryPath = resolve(cwd, source.path);
|
|
145
|
-
const resolvedRelativeRoot = resolve(cwd, source.relativeRoot ?? source.path);
|
|
146
|
-
await ensureExistingDirectory(resolvedDirectoryPath, 'Directory source');
|
|
147
|
-
await ensureExistingDirectory(resolvedRelativeRoot, 'Directory relativeRoot');
|
|
148
|
-
if (!isContainedWithin({ childPath: resolvedDirectoryPath, parentPath: resolvedRelativeRoot })) {
|
|
149
|
-
throw createSourceResolutionError(`Directory source "${resolvedDirectoryPath}" is not contained within relativeRoot "${resolvedRelativeRoot}".`);
|
|
150
|
-
}
|
|
151
|
-
const filePaths = await collectFilesRecursively(resolvedDirectoryPath);
|
|
152
|
-
if (filePaths.length === 0) {
|
|
153
|
-
throw createSourceResolutionError(`Directory source "${resolvedDirectoryPath}" does not contain any files.`);
|
|
154
|
-
}
|
|
155
|
-
return filePaths.map((filePath) => {
|
|
156
|
-
const relativePath = normalizeRelativePath(relative(resolvedRelativeRoot, filePath));
|
|
157
|
-
return {
|
|
158
|
-
kind: 'local',
|
|
159
|
-
input: filePath,
|
|
160
|
-
filePath,
|
|
161
|
-
fileName: basename(relativePath),
|
|
162
|
-
relativePath,
|
|
163
|
-
sourceInputIndex,
|
|
164
|
-
sourceInput: source,
|
|
165
|
-
};
|
|
166
|
-
});
|
|
167
|
-
};
|
|
168
|
-
const resolveUrlSource = async ({ source, sourceInputIndex, }) => {
|
|
169
|
-
let parsedUrl;
|
|
170
|
-
try {
|
|
171
|
-
parsedUrl = new URL(source.url);
|
|
172
|
-
}
|
|
173
|
-
catch {
|
|
174
|
-
throw createSourceResolutionError(`URL source "${source.url}" is not a valid URL.`);
|
|
175
|
-
}
|
|
176
|
-
if (!['http:', 'https:'].includes(parsedUrl.protocol)) {
|
|
177
|
-
throw createSourceResolutionError(`URL source "${source.url}" must use http or https.`);
|
|
178
|
-
}
|
|
179
|
-
const relativePath = normalizeRelativePath(source.relativePath ?? deriveRelativePathFromUrl(parsedUrl));
|
|
180
|
-
const downloadDirectoryPath = await mkdtemp(resolve(tmpdir(), 'chalksurf-cli-source-'));
|
|
181
|
-
const downloadedFilePath = resolve(downloadDirectoryPath, basename(relativePath));
|
|
182
|
-
try {
|
|
183
|
-
const response = await fetch(source.url);
|
|
184
|
-
if (!response.ok) {
|
|
185
|
-
throw createSourceResolutionError(`Failed to download "${source.url}": HTTP ${response.status}${response.statusText ? ` ${response.statusText}` : ''}.`);
|
|
186
|
-
}
|
|
187
|
-
if (!response.body) {
|
|
188
|
-
throw createSourceResolutionError(`Failed to download "${source.url}": response body was empty.`);
|
|
189
|
-
}
|
|
190
|
-
const responseMimeType = normalizeMimeType(response.headers.get('content-type'));
|
|
191
|
-
await pipeline(Readable.fromWeb(response.body), createWriteStream(downloadedFilePath));
|
|
192
|
-
const mimeType = await sniffMimeType({
|
|
193
|
-
filePath: downloadedFilePath,
|
|
194
|
-
responseMimeType,
|
|
195
|
-
});
|
|
196
|
-
return [
|
|
197
|
-
{
|
|
198
|
-
kind: 'url',
|
|
199
|
-
input: source.url,
|
|
200
|
-
filePath: downloadedFilePath,
|
|
201
|
-
fileName: basename(relativePath),
|
|
202
|
-
mimeType,
|
|
203
|
-
relativePath,
|
|
204
|
-
sourceInputIndex,
|
|
205
|
-
cleanupPath: downloadDirectoryPath,
|
|
206
|
-
sourceInput: source,
|
|
207
|
-
},
|
|
208
|
-
];
|
|
209
|
-
}
|
|
210
|
-
catch (error) {
|
|
211
|
-
await rm(downloadDirectoryPath, { force: true, recursive: true });
|
|
212
|
-
if (error instanceof CliCommandError) {
|
|
213
|
-
throw error;
|
|
214
|
-
}
|
|
215
|
-
const message = error instanceof Error ? error.message : 'Unknown download error';
|
|
216
|
-
throw createSourceResolutionError(`Failed to download "${source.url}": ${message}`);
|
|
217
|
-
}
|
|
218
|
-
};
|
|
219
|
-
const assertUniqueRelativePaths = (resolvedSources) => {
|
|
220
|
-
const seenRelativePaths = new Set();
|
|
221
|
-
for (const resolvedSource of resolvedSources) {
|
|
222
|
-
if (seenRelativePaths.has(resolvedSource.relativePath)) {
|
|
223
|
-
throw createSourceResolutionError(`Resolved sources contain duplicate relative path "${resolvedSource.relativePath}".`);
|
|
224
|
-
}
|
|
225
|
-
seenRelativePaths.add(resolvedSource.relativePath);
|
|
226
|
-
}
|
|
227
|
-
};
|
|
228
|
-
export const cleanupResolvedSources = async (resolvedSources) => {
|
|
229
|
-
const cleanupPaths = Array.from(new Set(resolvedSources
|
|
230
|
-
.map((resolvedSource) => resolvedSource.cleanupPath)
|
|
231
|
-
.filter((cleanupPath) => cleanupPath !== undefined)));
|
|
232
|
-
await Promise.all(cleanupPaths.map(async (cleanupPath) => {
|
|
233
|
-
await rm(cleanupPath, { force: true, recursive: true });
|
|
234
|
-
}));
|
|
235
|
-
};
|
|
236
|
-
export const resolveSources = async ({ sources, cwd = process.cwd(), maxConcurrentUrlDownloads = defaultMaxConcurrentUrlDownloads, }) => {
|
|
237
|
-
const limitUrlDownloads = pLimit(Math.max(1, maxConcurrentUrlDownloads));
|
|
238
|
-
const settledSourceGroups = await Promise.allSettled(sources.map(async (source, sourceInputIndex) => {
|
|
239
|
-
if (source.kind === 'local') {
|
|
240
|
-
return await resolveLocalFileSource({
|
|
241
|
-
source: source,
|
|
242
|
-
cwd,
|
|
243
|
-
sourceInputIndex,
|
|
244
|
-
});
|
|
245
|
-
}
|
|
246
|
-
if (source.kind === 'directory') {
|
|
247
|
-
return await resolveDirectorySource({
|
|
248
|
-
source: source,
|
|
249
|
-
cwd,
|
|
250
|
-
sourceInputIndex,
|
|
251
|
-
});
|
|
252
|
-
}
|
|
253
|
-
return await limitUrlDownloads(() => resolveUrlSource({
|
|
254
|
-
source: source,
|
|
255
|
-
sourceInputIndex,
|
|
256
|
-
}));
|
|
257
|
-
}));
|
|
258
|
-
const resolvedSources = settledSourceGroups.flatMap((result) => (result.status === 'fulfilled' ? result.value : []));
|
|
259
|
-
const firstRejectedResult = settledSourceGroups.find((result) => result.status === 'rejected');
|
|
260
|
-
if (firstRejectedResult?.status === 'rejected') {
|
|
261
|
-
await cleanupResolvedSources(resolvedSources);
|
|
262
|
-
throw firstRejectedResult.reason;
|
|
263
|
-
}
|
|
264
|
-
try {
|
|
265
|
-
assertUniqueRelativePaths(resolvedSources);
|
|
266
|
-
return resolvedSources;
|
|
267
|
-
}
|
|
268
|
-
catch (error) {
|
|
269
|
-
await cleanupResolvedSources(resolvedSources);
|
|
270
|
-
throw error;
|
|
271
|
-
}
|
|
272
|
-
};
|
|
@@ -1,16 +0,0 @@
|
|
|
1
|
-
import { CliCommandError } from './cli-error.js';
|
|
2
|
-
export const translationLanguages = ['english', 'hungarian', 'german', 'french', 'spanish', 'italian'];
|
|
3
|
-
export const resolveRequestedTranslateToLanguages = (rawValues) => {
|
|
4
|
-
if (!rawValues || rawValues.length === 0) {
|
|
5
|
-
return undefined;
|
|
6
|
-
}
|
|
7
|
-
const invalidLanguage = rawValues.find((value) => !translationLanguages.includes(value));
|
|
8
|
-
if (invalidLanguage) {
|
|
9
|
-
throw new CliCommandError(`--translate-to must be one of: ${translationLanguages.join(', ')}`, 2);
|
|
10
|
-
}
|
|
11
|
-
const languages = rawValues;
|
|
12
|
-
if (new Set(languages).size !== languages.length) {
|
|
13
|
-
throw new CliCommandError('--translate-to languages must be unique.', 2);
|
|
14
|
-
}
|
|
15
|
-
return languages;
|
|
16
|
-
};
|
package/dist/lib/user-jobs.js
DELETED
|
@@ -1,99 +0,0 @@
|
|
|
1
|
-
import { CliCommandError, createSerializableCliError } from './cli-error.js';
|
|
2
|
-
import { mapApiErrorToCliError } from './session.js';
|
|
3
|
-
const isTerminalJob = (job) => {
|
|
4
|
-
return ['completed', 'failed'].includes(job.status);
|
|
5
|
-
};
|
|
6
|
-
export const serializeCliJob = (job) => {
|
|
7
|
-
return {
|
|
8
|
-
id: job.id,
|
|
9
|
-
status: job.status,
|
|
10
|
-
type: job.type,
|
|
11
|
-
createdAt: job.createdAt,
|
|
12
|
-
updatedAt: job.updatedAt,
|
|
13
|
-
error: job.result?.error,
|
|
14
|
-
exerciseId: job.result?.exerciseId,
|
|
15
|
-
exerciseIds: job.result?.exerciseIds,
|
|
16
|
-
exerciseSheetId: job.result?.exerciseSheetId,
|
|
17
|
-
eligibleExerciseCount: job.result?.eligibleExerciseCount,
|
|
18
|
-
nonUpdatableExerciseCount: job.result?.nonUpdatableExerciseCount,
|
|
19
|
-
resultCode: job.result?.resultCode,
|
|
20
|
-
skippedExerciseCount: job.result?.skippedExerciseCount,
|
|
21
|
-
translationJobs: job.result?.translationJobs,
|
|
22
|
-
unmatchedImportedSolutionCount: job.result?.unmatchedImportedSolutionCount,
|
|
23
|
-
updatedExerciseCount: job.result?.updatedExerciseCount,
|
|
24
|
-
};
|
|
25
|
-
};
|
|
26
|
-
export const formatCliJobSummary = (job) => {
|
|
27
|
-
if (job.status === 'completed') {
|
|
28
|
-
if (job.exerciseSheetId) {
|
|
29
|
-
return `${job.id} completed -> ${job.exerciseSheetId}`;
|
|
30
|
-
}
|
|
31
|
-
if (job.exerciseId) {
|
|
32
|
-
return `${job.id} completed -> ${job.exerciseId}`;
|
|
33
|
-
}
|
|
34
|
-
return `${job.id} completed`;
|
|
35
|
-
}
|
|
36
|
-
if (job.status === 'failed') {
|
|
37
|
-
return `${job.id} failed -> ${job.error ?? 'Unknown error'}`;
|
|
38
|
-
}
|
|
39
|
-
return `${job.id} ${job.status}`;
|
|
40
|
-
};
|
|
41
|
-
export const getWaitExitCode = ({ jobs, timedOut }) => {
|
|
42
|
-
if (timedOut) {
|
|
43
|
-
return 6;
|
|
44
|
-
}
|
|
45
|
-
if (jobs.some((job) => job.status === 'failed')) {
|
|
46
|
-
return 7;
|
|
47
|
-
}
|
|
48
|
-
return 0;
|
|
49
|
-
};
|
|
50
|
-
export const getWaitError = ({ jobs, timedOut }) => {
|
|
51
|
-
if (timedOut) {
|
|
52
|
-
return createSerializableCliError({
|
|
53
|
-
exitCode: 6,
|
|
54
|
-
message: 'Timed out before all jobs completed.',
|
|
55
|
-
});
|
|
56
|
-
}
|
|
57
|
-
if (jobs.some((job) => job.status === 'failed')) {
|
|
58
|
-
return createSerializableCliError({
|
|
59
|
-
exitCode: 7,
|
|
60
|
-
message: 'One or more jobs failed.',
|
|
61
|
-
});
|
|
62
|
-
}
|
|
63
|
-
return undefined;
|
|
64
|
-
};
|
|
65
|
-
export const waitForCliJobs = async ({ getUserJob, jobIds, maxPollIntervalMs = 5000, now, sleep, timeoutMs = 300000, }) => {
|
|
66
|
-
const startedAt = now();
|
|
67
|
-
let nextPollDelayMs = 1000;
|
|
68
|
-
let latestJobs = [];
|
|
69
|
-
while (true) {
|
|
70
|
-
try {
|
|
71
|
-
latestJobs = await Promise.all(jobIds.map(async (jobId) => await getUserJob(jobId)));
|
|
72
|
-
}
|
|
73
|
-
catch (error) {
|
|
74
|
-
throw mapApiErrorToCliError(error);
|
|
75
|
-
}
|
|
76
|
-
if (latestJobs.every(isTerminalJob)) {
|
|
77
|
-
return {
|
|
78
|
-
jobs: latestJobs.map(serializeCliJob),
|
|
79
|
-
timedOut: false,
|
|
80
|
-
};
|
|
81
|
-
}
|
|
82
|
-
const elapsedMs = now() - startedAt;
|
|
83
|
-
if (elapsedMs >= timeoutMs) {
|
|
84
|
-
return {
|
|
85
|
-
jobs: latestJobs.map(serializeCliJob),
|
|
86
|
-
timedOut: true,
|
|
87
|
-
};
|
|
88
|
-
}
|
|
89
|
-
const remainingMs = timeoutMs - elapsedMs;
|
|
90
|
-
await sleep(Math.max(0, Math.min(nextPollDelayMs, remainingMs)));
|
|
91
|
-
nextPollDelayMs = Math.min(nextPollDelayMs * 2, maxPollIntervalMs);
|
|
92
|
-
}
|
|
93
|
-
};
|
|
94
|
-
export const throwSilentExitCode = (exitCode) => {
|
|
95
|
-
if (exitCode === 0) {
|
|
96
|
-
return;
|
|
97
|
-
}
|
|
98
|
-
throw new CliCommandError('', exitCode, false);
|
|
99
|
-
};
|