@dynamicweb/cli 1.1.0 → 1.1.2
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/bin/commands/files.js +155 -103
- package/bin/commands/files.test.js +76 -0
- package/bin/commands/install.js +99 -13
- package/bin/commands/install.test.js +48 -0
- package/bin/downloader.js +8 -5
- package/package.json +2 -2
package/bin/commands/files.js
CHANGED
|
@@ -10,71 +10,71 @@ import { extractWithProgress } from '../extractor.js';
|
|
|
10
10
|
|
|
11
11
|
export function filesCommand() {
|
|
12
12
|
return {
|
|
13
|
-
command: 'files [dirPath] [outPath]',
|
|
14
|
-
describe: 'Handles files',
|
|
13
|
+
command: 'files [dirPath] [outPath]',
|
|
14
|
+
describe: 'Handles files',
|
|
15
15
|
builder: (yargs) => {
|
|
16
16
|
return yargs
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
17
|
+
.positional('dirPath', {
|
|
18
|
+
describe: 'The directory to list or export'
|
|
19
|
+
})
|
|
20
|
+
.positional('outPath', {
|
|
21
|
+
describe: 'The directory to export the specified directory to',
|
|
22
|
+
default: '.'
|
|
23
|
+
})
|
|
24
|
+
.option('list', {
|
|
25
|
+
alias: 'l',
|
|
26
|
+
type: 'boolean',
|
|
27
|
+
describe: 'Lists all directories and files'
|
|
28
|
+
})
|
|
29
|
+
.option('export', {
|
|
30
|
+
alias: 'e',
|
|
31
|
+
type: 'boolean',
|
|
32
|
+
describe: 'Exports the specified directory and all subdirectories at [dirPath] to [outPath]'
|
|
33
|
+
})
|
|
34
|
+
.option('import', {
|
|
35
|
+
alias: 'i',
|
|
36
|
+
type: 'boolean',
|
|
37
|
+
describe: 'Imports the file at [dirPath] to [outPath]'
|
|
38
|
+
})
|
|
39
|
+
.option('overwrite', {
|
|
40
|
+
alias: 'o',
|
|
41
|
+
type: 'boolean',
|
|
42
|
+
describe: 'Used with import, will overwrite existing files at destination if set to true'
|
|
43
|
+
})
|
|
44
|
+
.option('createEmpty', {
|
|
45
|
+
type: 'boolean',
|
|
46
|
+
describe: 'Used with import, will create a file even if its empty'
|
|
47
|
+
})
|
|
48
|
+
.option('includeFiles', {
|
|
49
|
+
alias: 'f',
|
|
50
|
+
type: 'boolean',
|
|
51
|
+
describe: 'Used with export, includes files in list of directories and files'
|
|
52
|
+
})
|
|
53
|
+
.option('recursive', {
|
|
54
|
+
alias: 'r',
|
|
55
|
+
type: 'boolean',
|
|
56
|
+
describe: 'Used with list, import and export, handles all directories recursively'
|
|
57
|
+
})
|
|
58
|
+
.option('raw', {
|
|
59
|
+
type: 'boolean',
|
|
60
|
+
describe: 'Used with export, keeps zip file instead of unpacking it'
|
|
61
|
+
})
|
|
62
|
+
.option('iamstupid', {
|
|
63
|
+
type: 'boolean',
|
|
64
|
+
describe: 'Includes export of log and cache folders, NOT RECOMMENDED'
|
|
65
|
+
})
|
|
66
|
+
.option('asFile', {
|
|
67
|
+
type: 'boolean',
|
|
68
|
+
alias: 'af',
|
|
69
|
+
describe: 'Forces the command to treat the path as a single file, even if it has no extension.',
|
|
70
|
+
conflicts: 'asDirectory'
|
|
71
|
+
})
|
|
72
|
+
.option('asDirectory', {
|
|
73
|
+
type: 'boolean',
|
|
74
|
+
alias: 'ad',
|
|
75
|
+
describe: 'Forces the command to treat the path as a directory, even if its name contains a dot.',
|
|
76
|
+
conflicts: 'asFile'
|
|
77
|
+
})
|
|
78
78
|
},
|
|
79
79
|
handler: async (argv) => {
|
|
80
80
|
if (argv.verbose) console.info(`Listing directory at: ${argv.dirPath}`)
|
|
@@ -97,18 +97,18 @@ async function handleFiles(argv) {
|
|
|
97
97
|
|
|
98
98
|
if (argv.export) {
|
|
99
99
|
if (argv.dirPath) {
|
|
100
|
-
|
|
100
|
+
|
|
101
101
|
const isFile = argv.asFile || argv.asDirectory
|
|
102
102
|
? argv.asFile
|
|
103
|
-
: path.extname(argv.dirPath) !== '';
|
|
103
|
+
: path.extname(argv.dirPath) !== '';
|
|
104
104
|
|
|
105
105
|
if (isFile) {
|
|
106
|
-
let parentDirectory = path.dirname(argv.dirPath);
|
|
106
|
+
let parentDirectory = path.dirname(argv.dirPath);
|
|
107
107
|
parentDirectory = parentDirectory === '.' ? '/' : parentDirectory;
|
|
108
|
-
|
|
109
|
-
await download(env, user, parentDirectory, argv.outPath, false, null, true, argv.iamstupid, [argv.dirPath], true);
|
|
108
|
+
|
|
109
|
+
await download(env, user, parentDirectory, argv.outPath, false, null, true, argv.iamstupid, [argv.dirPath], true, argv.verbose);
|
|
110
110
|
} else {
|
|
111
|
-
await download(env, user, argv.dirPath, argv.outPath, true, null, argv.raw, argv.iamstupid, [], false);
|
|
111
|
+
await download(env, user, argv.dirPath, argv.outPath, true, null, argv.raw, argv.iamstupid, [], false, argv.verbose);
|
|
112
112
|
}
|
|
113
113
|
} else {
|
|
114
114
|
await interactiveConfirm('Are you sure you want a full export of files?', async () => {
|
|
@@ -117,9 +117,9 @@ async function handleFiles(argv) {
|
|
|
117
117
|
let dirs = filesStructure.directories;
|
|
118
118
|
for (let id = 0; id < dirs.length; id++) {
|
|
119
119
|
const dir = dirs[id];
|
|
120
|
-
await download(env, user, dir.name, argv.outPath, true, null, argv.raw, argv.iamstupid, [], false);
|
|
120
|
+
await download(env, user, dir.name, argv.outPath, true, null, argv.raw, argv.iamstupid, [], false, argv.verbose);
|
|
121
121
|
}
|
|
122
|
-
await download(env, user, '/.', argv.outPath, false, 'Base.zip', argv.raw, argv.iamstupid, Array.from(filesStructure.files.data, f => f.name), false);
|
|
122
|
+
await download(env, user, '/.', argv.outPath, false, 'Base.zip', argv.raw, argv.iamstupid, Array.from(filesStructure.files.data, f => f.name), false, argv.verbose);
|
|
123
123
|
if (argv.raw) console.log('The files in the base "files" folder is in Base.zip, each directory in "files" is in its own zip')
|
|
124
124
|
})
|
|
125
125
|
}
|
|
@@ -137,21 +137,21 @@ async function handleFiles(argv) {
|
|
|
137
137
|
}
|
|
138
138
|
|
|
139
139
|
function getFilesInDirectory(dirPath) {
|
|
140
|
-
return fs.statSync(dirPath).isFile() ? [
|
|
141
|
-
|
|
142
|
-
|
|
140
|
+
return fs.statSync(dirPath).isFile() ? [dirPath] : fs.readdirSync(dirPath)
|
|
141
|
+
.map(file => path.join(dirPath, file))
|
|
142
|
+
.filter(file => fs.statSync(file).isFile());
|
|
143
143
|
}
|
|
144
144
|
|
|
145
|
-
async function processDirectory(env, user, dirPath, outPath, originalDir, createEmpty, isRoot = false, overwrite = false) {
|
|
145
|
+
async function processDirectory(env, user, dirPath, outPath, originalDir, createEmpty, isRoot = false, overwrite = false, output = console) {
|
|
146
146
|
let filesInDir = getFilesInDirectory(dirPath);
|
|
147
147
|
if (filesInDir.length > 0)
|
|
148
|
-
await uploadFiles(env, user, filesInDir, isRoot ? outPath : path.join(outPath, path.basename(dirPath)), createEmpty, overwrite);
|
|
148
|
+
await uploadFiles(env, user, filesInDir, isRoot ? outPath : path.join(outPath, path.basename(dirPath)), createEmpty, overwrite, output);
|
|
149
149
|
|
|
150
150
|
const subDirectories = fs.readdirSync(dirPath)
|
|
151
|
-
|
|
152
|
-
|
|
151
|
+
.map(subDir => path.join(dirPath, subDir))
|
|
152
|
+
.filter(subDir => fs.statSync(subDir).isDirectory());
|
|
153
153
|
for (let subDir of subDirectories) {
|
|
154
|
-
await processDirectory(env, user, subDir, isRoot ? outPath : path.join(outPath, path.basename(dirPath)), originalDir, createEmpty, false, overwrite);
|
|
154
|
+
await processDirectory(env, user, subDir, isRoot ? outPath : path.join(outPath, path.basename(dirPath)), originalDir, createEmpty, false, overwrite, output);
|
|
155
155
|
}
|
|
156
156
|
}
|
|
157
157
|
|
|
@@ -184,12 +184,12 @@ function resolveTree(dirs, indentLevel, parentHasFiles) {
|
|
|
184
184
|
resolveTree(dir.files?.data ?? [], indentLevel + '│\t', false);
|
|
185
185
|
} else {
|
|
186
186
|
resolveTree(dir.directories ?? [], indentLevel + '\t', hasFiles);
|
|
187
|
-
resolveTree(dir.files?.data ?? [], indentLevel + '\t', false);
|
|
187
|
+
resolveTree(dir.files?.data ?? [], indentLevel + '\t', false);
|
|
188
188
|
}
|
|
189
189
|
}
|
|
190
190
|
}
|
|
191
191
|
|
|
192
|
-
async function download(env, user, dirPath, outPath, recursive, outname, raw, iamstupid, fileNames, singleFileMode) {
|
|
192
|
+
async function download(env, user, dirPath, outPath, recursive, outname, raw, iamstupid, fileNames, singleFileMode, verbose = false) {
|
|
193
193
|
let excludeDirectories = '';
|
|
194
194
|
if (!iamstupid) {
|
|
195
195
|
excludeDirectories = 'system/log';
|
|
@@ -212,7 +212,7 @@ async function download(env, user, dirPath, outPath, recursive, outname, raw, ia
|
|
|
212
212
|
agent: getAgent(env.protocol)
|
|
213
213
|
});
|
|
214
214
|
|
|
215
|
-
const filename = outname || tryGetFileNameFromResponse(res, dirPath);
|
|
215
|
+
const filename = outname || tryGetFileNameFromResponse(res, dirPath, verbose);
|
|
216
216
|
if (!filename) return;
|
|
217
217
|
|
|
218
218
|
const filePath = path.resolve(`${path.resolve(outPath)}/${filename}`)
|
|
@@ -286,7 +286,7 @@ async function extractArchive(filename, filePath, outPath, raw) {
|
|
|
286
286
|
updater.stop();
|
|
287
287
|
console.log(`Finished extracting ${filename} to ${outPath}\n`);
|
|
288
288
|
|
|
289
|
-
fs.unlink(filePath, function(err) {});
|
|
289
|
+
fs.unlink(filePath, function (err) { });
|
|
290
290
|
}
|
|
291
291
|
|
|
292
292
|
async function getFilesStructure(env, user, dirPath, recursive, includeFiles) {
|
|
@@ -306,8 +306,9 @@ async function getFilesStructure(env, user, dirPath, recursive, includeFiles) {
|
|
|
306
306
|
}
|
|
307
307
|
}
|
|
308
308
|
|
|
309
|
-
export async function uploadFiles(env, user, localFilePaths, destinationPath, createEmpty = false, overwrite = false) {
|
|
310
|
-
|
|
309
|
+
export async function uploadFiles(env, user, localFilePaths, destinationPath, createEmpty = false, overwrite = false, output) {
|
|
310
|
+
output = resolveUploadOutput(output);
|
|
311
|
+
output.log('Uploading files')
|
|
311
312
|
|
|
312
313
|
const chunkSize = 300;
|
|
313
314
|
const chunks = [];
|
|
@@ -316,30 +317,41 @@ export async function uploadFiles(env, user, localFilePaths, destinationPath, cr
|
|
|
316
317
|
chunks.push(localFilePaths.slice(i, i + chunkSize));
|
|
317
318
|
}
|
|
318
319
|
|
|
320
|
+
output.mergeMeta({
|
|
321
|
+
filesProcessed: (output.response.meta.filesProcessed || 0) + localFilePaths.length,
|
|
322
|
+
chunks: (output.response.meta.chunks || 0) + chunks.length
|
|
323
|
+
});
|
|
324
|
+
|
|
319
325
|
for (let i = 0; i < chunks.length; i++) {
|
|
320
|
-
|
|
326
|
+
output.log(`Uploading chunk ${i + 1} of ${chunks.length}`);
|
|
321
327
|
|
|
322
328
|
const chunk = chunks[i];
|
|
323
|
-
await uploadChunk(env, user, chunk, destinationPath, createEmpty, overwrite);
|
|
324
|
-
|
|
325
|
-
|
|
329
|
+
const body = await uploadChunk(env, user, chunk, destinationPath, createEmpty, overwrite, output);
|
|
330
|
+
output.addData({
|
|
331
|
+
type: 'upload',
|
|
332
|
+
destinationPath,
|
|
333
|
+
files: chunk.map(filePath => path.resolve(filePath)),
|
|
334
|
+
response: body
|
|
335
|
+
});
|
|
336
|
+
|
|
337
|
+
output.log(`Finished uploading chunk ${i + 1} of ${chunks.length}`);
|
|
326
338
|
}
|
|
327
339
|
|
|
328
|
-
|
|
340
|
+
output.log(`Finished uploading files. Total files: ${localFilePaths.length}, total chunks: ${chunks.length}`);
|
|
329
341
|
}
|
|
330
342
|
|
|
331
|
-
async function uploadChunk(env, user, filePathsChunk, destinationPath, createEmpty, overwrite) {
|
|
343
|
+
async function uploadChunk(env, user, filePathsChunk, destinationPath, createEmpty, overwrite, output = console) {
|
|
332
344
|
const form = new FormData();
|
|
333
345
|
form.append('path', destinationPath);
|
|
334
346
|
form.append('skipExistingFiles', String(!overwrite));
|
|
335
347
|
form.append('allowOverwrite', String(overwrite));
|
|
336
|
-
|
|
348
|
+
|
|
337
349
|
filePathsChunk.forEach(fileToUpload => {
|
|
338
|
-
|
|
350
|
+
output.log(`${fileToUpload}`)
|
|
339
351
|
form.append('files', fs.createReadStream(path.resolve(fileToUpload)));
|
|
340
352
|
});
|
|
341
353
|
|
|
342
|
-
const res = await fetch(`${env.protocol}://${env.host}/Admin/Api/Upload?` + new URLSearchParams({"createEmptyFiles": createEmpty, "createMissingDirectories": true}), {
|
|
354
|
+
const res = await fetch(`${env.protocol}://${env.host}/Admin/Api/Upload?` + new URLSearchParams({ "createEmptyFiles": createEmpty, "createMissingDirectories": true }), {
|
|
343
355
|
method: 'POST',
|
|
344
356
|
body: form,
|
|
345
357
|
headers: {
|
|
@@ -347,25 +359,66 @@ async function uploadChunk(env, user, filePathsChunk, destinationPath, createEmp
|
|
|
347
359
|
},
|
|
348
360
|
agent: getAgent(env.protocol)
|
|
349
361
|
});
|
|
350
|
-
|
|
362
|
+
|
|
351
363
|
if (res.ok) {
|
|
352
|
-
|
|
364
|
+
return await res.json()
|
|
353
365
|
}
|
|
354
366
|
else {
|
|
355
|
-
|
|
356
|
-
|
|
367
|
+
if (output.structured) {
|
|
368
|
+
throw createUploadError('File upload failed.', res.status, await parseJsonSafe(res));
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
output.log(res)
|
|
372
|
+
output.log(await parseJsonSafe(res))
|
|
357
373
|
process.exit(1);
|
|
358
374
|
}
|
|
359
375
|
}
|
|
360
376
|
|
|
377
|
+
export function resolveUploadOutput(output) {
|
|
378
|
+
const response = output?.response ?? {};
|
|
379
|
+
response.meta = response.meta ?? {};
|
|
380
|
+
|
|
381
|
+
return {
|
|
382
|
+
structured: Boolean(output),
|
|
383
|
+
response,
|
|
384
|
+
log: typeof output?.log === 'function'
|
|
385
|
+
? output.log.bind(output)
|
|
386
|
+
: (...args) => console.log(...args),
|
|
387
|
+
addData: typeof output?.addData === 'function'
|
|
388
|
+
? output.addData.bind(output)
|
|
389
|
+
: () => { },
|
|
390
|
+
mergeMeta: typeof output?.mergeMeta === 'function'
|
|
391
|
+
? output.mergeMeta.bind(output)
|
|
392
|
+
: (meta) => {
|
|
393
|
+
response.meta = {
|
|
394
|
+
...response.meta,
|
|
395
|
+
...meta
|
|
396
|
+
};
|
|
397
|
+
}
|
|
398
|
+
};
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
function createUploadError(message, status, details = null) {
|
|
402
|
+
const error = new Error(message);
|
|
403
|
+
error.status = status;
|
|
404
|
+
error.details = details;
|
|
405
|
+
return error;
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
async function parseJsonSafe(res) {
|
|
409
|
+
try {
|
|
410
|
+
return await res.json();
|
|
411
|
+
} catch {
|
|
412
|
+
return null;
|
|
413
|
+
}
|
|
414
|
+
}
|
|
415
|
+
|
|
361
416
|
export function resolveFilePath(filePath) {
|
|
362
417
|
let p = path.parse(path.resolve(filePath))
|
|
363
418
|
let regex = wildcardToRegExp(p.base);
|
|
364
419
|
let resolvedPath = fs.readdirSync(p.dir).filter((allFilesPaths) => allFilesPaths.match(regex) !== null)[0]
|
|
365
|
-
if (resolvedPath === undefined)
|
|
366
|
-
|
|
367
|
-
console.log('Could not find any files with the name ' + filePath);
|
|
368
|
-
process.exit(1);
|
|
420
|
+
if (resolvedPath === undefined) {
|
|
421
|
+
throw new Error('Could not find any files with the name ' + filePath);
|
|
369
422
|
}
|
|
370
423
|
return path.join(p.dir, resolvedPath);
|
|
371
424
|
}
|
|
@@ -375,4 +428,3 @@ function wildcardToRegExp(wildcard) {
|
|
|
375
428
|
const escaped = wildcard.replace(/[.+?^${}()|[\]\\]/g, '\\$&');
|
|
376
429
|
return new RegExp('^' + escaped.replace(/\*/g, '.*') + '$');
|
|
377
430
|
}
|
|
378
|
-
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import test from 'node:test';
|
|
2
|
+
import assert from 'node:assert/strict';
|
|
3
|
+
import fs from 'node:fs';
|
|
4
|
+
import os from 'node:os';
|
|
5
|
+
import path from 'node:path';
|
|
6
|
+
|
|
7
|
+
import { resolveFilePath, resolveUploadOutput } from './files.js';
|
|
8
|
+
|
|
9
|
+
test('resolveUploadOutput falls back to a console-compatible output object', () => {
|
|
10
|
+
const output = resolveUploadOutput();
|
|
11
|
+
|
|
12
|
+
assert.equal(typeof output.log, 'function');
|
|
13
|
+
assert.equal(typeof output.addData, 'function');
|
|
14
|
+
assert.equal(typeof output.mergeMeta, 'function');
|
|
15
|
+
assert.deepEqual(output.response.meta, {});
|
|
16
|
+
|
|
17
|
+
output.mergeMeta({ chunks: 1, filesProcessed: 2 });
|
|
18
|
+
|
|
19
|
+
assert.deepEqual(output.response.meta, {
|
|
20
|
+
chunks: 1,
|
|
21
|
+
filesProcessed: 2
|
|
22
|
+
});
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
test('resolveUploadOutput preserves custom logging and merges meta when mergeMeta is absent', () => {
|
|
26
|
+
const calls = [];
|
|
27
|
+
const data = [];
|
|
28
|
+
const output = {
|
|
29
|
+
log: (...args) => calls.push(args),
|
|
30
|
+
addData: (entry) => data.push(entry),
|
|
31
|
+
response: {
|
|
32
|
+
meta: {
|
|
33
|
+
existing: true
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
const resolved = resolveUploadOutput(output);
|
|
39
|
+
|
|
40
|
+
resolved.log('Uploading chunk 1 of 1');
|
|
41
|
+
resolved.addData({ file: 'addon.nupkg' });
|
|
42
|
+
resolved.mergeMeta({ chunks: 1 });
|
|
43
|
+
|
|
44
|
+
assert.deepEqual(calls, [[ 'Uploading chunk 1 of 1' ]]);
|
|
45
|
+
assert.deepEqual(data, [{ file: 'addon.nupkg' }]);
|
|
46
|
+
assert.deepEqual(resolved.response.meta, {
|
|
47
|
+
existing: true,
|
|
48
|
+
chunks: 1
|
|
49
|
+
});
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
test('resolveUploadOutput initializes response.meta for partial output objects', () => {
|
|
53
|
+
const resolved = resolveUploadOutput({
|
|
54
|
+
log: () => {},
|
|
55
|
+
response: {}
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
resolved.mergeMeta({ chunks: 2 });
|
|
59
|
+
|
|
60
|
+
assert.deepEqual(resolved.response.meta, {
|
|
61
|
+
chunks: 2
|
|
62
|
+
});
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
test('resolveFilePath throws when no matching file exists', () => {
|
|
66
|
+
const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'dw-cli-files-test-'));
|
|
67
|
+
|
|
68
|
+
try {
|
|
69
|
+
assert.throws(
|
|
70
|
+
() => resolveFilePath(path.join(tempDir, 'missing*.nupkg')),
|
|
71
|
+
/Could not find any files with the name/
|
|
72
|
+
);
|
|
73
|
+
} finally {
|
|
74
|
+
fs.rmSync(tempDir, { recursive: true, force: true });
|
|
75
|
+
}
|
|
76
|
+
});
|
package/bin/commands/install.js
CHANGED
|
@@ -18,24 +18,40 @@ export function installCommand() {
|
|
|
18
18
|
type: 'boolean',
|
|
19
19
|
describe: 'Queues the install for next Dynamicweb recycle'
|
|
20
20
|
})
|
|
21
|
+
.option('output', {
|
|
22
|
+
choices: ['json'],
|
|
23
|
+
describe: 'Outputs a single JSON response for automation-friendly parsing'
|
|
24
|
+
})
|
|
21
25
|
},
|
|
22
26
|
handler: async (argv) => {
|
|
23
|
-
|
|
24
|
-
|
|
27
|
+
const output = createInstallOutput(argv);
|
|
28
|
+
|
|
29
|
+
try {
|
|
30
|
+
output.verboseLog(`Installing file located at: ${argv.filePath}`);
|
|
31
|
+
await handleInstall(argv, output)
|
|
32
|
+
} catch (err) {
|
|
33
|
+
output.fail(err);
|
|
34
|
+
process.exitCode = 1;
|
|
35
|
+
} finally {
|
|
36
|
+
output.finish();
|
|
37
|
+
}
|
|
25
38
|
}
|
|
26
39
|
}
|
|
27
40
|
}
|
|
28
41
|
|
|
29
|
-
async function handleInstall(argv) {
|
|
42
|
+
async function handleInstall(argv, output) {
|
|
30
43
|
let env = await setupEnv(argv);
|
|
31
44
|
let user = await setupUser(argv, env);
|
|
32
45
|
let resolvedPath = resolveFilePath(argv.filePath);
|
|
33
|
-
|
|
34
|
-
|
|
46
|
+
output.mergeMeta({
|
|
47
|
+
resolvedPath
|
|
48
|
+
});
|
|
49
|
+
await uploadFiles(env, user, [ resolvedPath ], 'System/AddIns/Local', false, true, output);
|
|
50
|
+
await installAddin(env, user, resolvedPath, argv.queue, output)
|
|
35
51
|
}
|
|
36
52
|
|
|
37
|
-
async function installAddin(env, user, resolvedPath, queue) {
|
|
38
|
-
|
|
53
|
+
async function installAddin(env, user, resolvedPath, queue, output) {
|
|
54
|
+
output.log('Installing addin')
|
|
39
55
|
let filename = path.basename(resolvedPath);
|
|
40
56
|
let data = {
|
|
41
57
|
'Queue': queue,
|
|
@@ -54,12 +70,82 @@ async function installAddin(env, user, resolvedPath, queue) {
|
|
|
54
70
|
});
|
|
55
71
|
|
|
56
72
|
if (res.ok) {
|
|
57
|
-
|
|
58
|
-
|
|
73
|
+
const body = await res.json();
|
|
74
|
+
output.addData({
|
|
75
|
+
type: 'install',
|
|
76
|
+
filename,
|
|
77
|
+
queued: Boolean(queue),
|
|
78
|
+
response: body
|
|
79
|
+
});
|
|
80
|
+
output.log(`Addin installed`)
|
|
59
81
|
}
|
|
60
82
|
else {
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
83
|
+
throw createInstallError('Addin install failed.', res.status, await parseJsonSafe(res));
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
export function createInstallOutput(argv) {
|
|
88
|
+
const response = {
|
|
89
|
+
ok: true,
|
|
90
|
+
command: 'install',
|
|
91
|
+
operation: 'install',
|
|
92
|
+
status: 200,
|
|
93
|
+
data: [],
|
|
94
|
+
errors: [],
|
|
95
|
+
meta: {
|
|
96
|
+
queued: Boolean(argv.queue)
|
|
97
|
+
}
|
|
98
|
+
};
|
|
99
|
+
|
|
100
|
+
return {
|
|
101
|
+
json: argv.output === 'json',
|
|
102
|
+
response,
|
|
103
|
+
log(...args) {
|
|
104
|
+
if (!this.json) {
|
|
105
|
+
console.log(...args);
|
|
106
|
+
}
|
|
107
|
+
},
|
|
108
|
+
verboseLog(...args) {
|
|
109
|
+
if (argv.verbose && !this.json) {
|
|
110
|
+
console.info(...args);
|
|
111
|
+
}
|
|
112
|
+
},
|
|
113
|
+
addData(entry) {
|
|
114
|
+
response.data.push(entry);
|
|
115
|
+
},
|
|
116
|
+
mergeMeta(meta) {
|
|
117
|
+
response.meta = {
|
|
118
|
+
...response.meta,
|
|
119
|
+
...meta
|
|
120
|
+
};
|
|
121
|
+
},
|
|
122
|
+
fail(err) {
|
|
123
|
+
response.ok = false;
|
|
124
|
+
response.status = err?.status || 1;
|
|
125
|
+
response.errors.push({
|
|
126
|
+
message: err?.message || 'Unknown install command error.',
|
|
127
|
+
details: err?.details ?? null
|
|
128
|
+
});
|
|
129
|
+
},
|
|
130
|
+
finish() {
|
|
131
|
+
if (this.json) {
|
|
132
|
+
console.log(JSON.stringify(response, null, 2));
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
};
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
function createInstallError(message, status, details = null) {
|
|
139
|
+
const error = new Error(message);
|
|
140
|
+
error.status = status;
|
|
141
|
+
error.details = details;
|
|
142
|
+
return error;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
async function parseJsonSafe(res) {
|
|
146
|
+
try {
|
|
147
|
+
return await res.json();
|
|
148
|
+
} catch {
|
|
149
|
+
return null;
|
|
64
150
|
}
|
|
65
|
-
}
|
|
151
|
+
}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import test from 'node:test';
|
|
2
|
+
import assert from 'node:assert/strict';
|
|
3
|
+
|
|
4
|
+
import { createInstallOutput } from './install.js';
|
|
5
|
+
|
|
6
|
+
test('createInstallOutput suppresses regular logs in json mode and emits the final envelope', () => {
|
|
7
|
+
const logCalls = [];
|
|
8
|
+
const infoCalls = [];
|
|
9
|
+
const originalLog = console.log;
|
|
10
|
+
const originalInfo = console.info;
|
|
11
|
+
|
|
12
|
+
console.log = (...args) => logCalls.push(args);
|
|
13
|
+
console.info = (...args) => infoCalls.push(args);
|
|
14
|
+
|
|
15
|
+
try {
|
|
16
|
+
const output = createInstallOutput({
|
|
17
|
+
output: 'json',
|
|
18
|
+
queue: true,
|
|
19
|
+
verbose: true
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
output.log('hidden');
|
|
23
|
+
output.verboseLog('hidden verbose');
|
|
24
|
+
output.addData({ type: 'install', filename: 'addon.nupkg' });
|
|
25
|
+
output.mergeMeta({ resolvedPath: '/tmp/addon.nupkg' });
|
|
26
|
+
output.finish();
|
|
27
|
+
|
|
28
|
+
assert.deepEqual(infoCalls, []);
|
|
29
|
+
assert.equal(logCalls.length, 1);
|
|
30
|
+
|
|
31
|
+
const rendered = JSON.parse(logCalls[0][0]);
|
|
32
|
+
assert.deepEqual(rendered, {
|
|
33
|
+
ok: true,
|
|
34
|
+
command: 'install',
|
|
35
|
+
operation: 'install',
|
|
36
|
+
status: 200,
|
|
37
|
+
data: [{ type: 'install', filename: 'addon.nupkg' }],
|
|
38
|
+
errors: [],
|
|
39
|
+
meta: {
|
|
40
|
+
queued: true,
|
|
41
|
+
resolvedPath: '/tmp/addon.nupkg'
|
|
42
|
+
}
|
|
43
|
+
});
|
|
44
|
+
} finally {
|
|
45
|
+
console.log = originalLog;
|
|
46
|
+
console.info = originalInfo;
|
|
47
|
+
}
|
|
48
|
+
});
|
package/bin/downloader.js
CHANGED
|
@@ -9,7 +9,7 @@ import fs from 'fs';
|
|
|
9
9
|
export function getFileNameFromResponse(res, dirPath) {
|
|
10
10
|
const header = res.headers.get('content-disposition');
|
|
11
11
|
const parts = header?.split(';');
|
|
12
|
-
|
|
12
|
+
|
|
13
13
|
if (!parts) {
|
|
14
14
|
const msg = `No files found in directory '${dirPath}', if you want to download all folders recursively include the -r flag`;
|
|
15
15
|
throw new Error(msg);
|
|
@@ -24,13 +24,16 @@ export function getFileNameFromResponse(res, dirPath) {
|
|
|
24
24
|
*
|
|
25
25
|
* @param {Object} res - The HTTP response object to extract the file name from.
|
|
26
26
|
* @param {string} dirPath - The directory path to use for file name resolution.
|
|
27
|
+
* @param {boolean} verbose - Whether to log missing download information.
|
|
27
28
|
* @returns {string|null} The extracted file name, or null if extraction fails.
|
|
28
29
|
*/
|
|
29
|
-
export function tryGetFileNameFromResponse(res, dirPath) {
|
|
30
|
+
export function tryGetFileNameFromResponse(res, dirPath, verbose = false) {
|
|
30
31
|
try {
|
|
31
32
|
return getFileNameFromResponse(res, dirPath);
|
|
32
33
|
} catch (err) {
|
|
33
|
-
|
|
34
|
+
if (verbose) {
|
|
35
|
+
console.log(err.message);
|
|
36
|
+
}
|
|
34
37
|
return null;
|
|
35
38
|
}
|
|
36
39
|
}
|
|
@@ -56,7 +59,7 @@ export function downloadWithProgress(res, filePath, options) {
|
|
|
56
59
|
res.body.on("data", chunk => {
|
|
57
60
|
const isFirstChunk = receivedBytes === 0;
|
|
58
61
|
const elapsed = Date.now() - startTime;
|
|
59
|
-
|
|
62
|
+
|
|
60
63
|
receivedBytes += chunk.length;
|
|
61
64
|
|
|
62
65
|
if (options?.onData) {
|
|
@@ -64,4 +67,4 @@ export function downloadWithProgress(res, filePath, options) {
|
|
|
64
67
|
}
|
|
65
68
|
});
|
|
66
69
|
});
|
|
67
|
-
}
|
|
70
|
+
}
|
package/package.json
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
"name": "@dynamicweb/cli",
|
|
3
3
|
"type": "module",
|
|
4
4
|
"description": "CLI for interacting with Dynamicweb 10 solutions from the command line.",
|
|
5
|
-
"version": "1.1.
|
|
5
|
+
"version": "1.1.2",
|
|
6
6
|
"main": "bin/index.js",
|
|
7
7
|
"files": [
|
|
8
8
|
"bin/**"
|
|
@@ -15,7 +15,7 @@
|
|
|
15
15
|
"devops"
|
|
16
16
|
],
|
|
17
17
|
"scripts": {
|
|
18
|
-
"test": "
|
|
18
|
+
"test": "node --test"
|
|
19
19
|
},
|
|
20
20
|
"author": "Dynamicweb A/S (https://www.dynamicweb.com)",
|
|
21
21
|
"repository": {
|