@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.
@@ -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
- .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
- })
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() ? [ dirPath ] : fs.readdirSync(dirPath)
141
- .map(file => path.join(dirPath, file))
142
- .filter(file => fs.statSync(file).isFile());
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
- .map(subDir => path.join(dirPath, subDir))
152
- .filter(subDir => fs.statSync(subDir).isDirectory());
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
- console.log('Uploading files')
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
- console.log(`Uploading chunk ${i + 1} of ${chunks.length}`);
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
- console.log(`Finished uploading chunk ${i + 1} of ${chunks.length}`);
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
- console.log(`Finished uploading files. Total files: ${localFilePaths.length}, total chunks: ${chunks.length}`);
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
- console.log(`${fileToUpload}`)
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
- console.log(await res.json())
364
+ return await res.json()
353
365
  }
354
366
  else {
355
- console.log(res)
356
- console.log(await res.json())
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
+ });
@@ -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
- if (argv.verbose) console.info(`Installing file located at :${argv.filePath}`)
24
- await handleInstall(argv)
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
- await uploadFiles(env, user, [ resolvedPath ], 'System/AddIns/Local', false, true);
34
- await installAddin(env, user, resolvedPath, argv.queue)
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
- console.log('Installing addin')
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
- if (env.verbose) console.log(await res.json())
58
- console.log(`Addin installed`)
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
- console.log('Request failed, returned error:')
62
- console.log(await res.json())
63
- process.exit(1);
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
- console.error(err.message);
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.0",
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": "echo \"Error: no test specified\" && exit 1"
18
+ "test": "node --test"
19
19
  },
20
20
  "author": "Dynamicweb A/S (https://www.dynamicweb.com)",
21
21
  "repository": {