@dynamicweb/cli 1.1.1 → 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/README.md CHANGED
@@ -98,7 +98,6 @@ The files command is used to list out and export the structure in your Dynamicwe
98
98
  - `-e` `--export` It will export \<dirPath\> into \<outPath\> on your local machine, unzipped by default
99
99
  - `--raw` This will keep the content zipped
100
100
  - `--iamstupid` This will include the export of the /files/system/log and /files/.cache folders
101
- - `--json` This will output a single JSON object for automation-friendly parsing
102
101
 
103
102
  #### Examples
104
103
  Exporting all templates from current environment to local solution
@@ -109,9 +108,6 @@ Exporting all templates from current environment to local solution
109
108
  Listing the system files structure of the current environment
110
109
  > $ dw files system -lr
111
110
 
112
- Uploading files with JSON output for automation
113
- > $ dw files ./Files templates -i -r --json
114
-
115
111
  ### Files Source Type Detection
116
112
  By default, the `dw files` command automatically detects the source type based on the \<dirPath\>:
117
113
  If the path contains a file extension (e.g., 'templates/Translations.xml'), it is treated as a file.
@@ -156,7 +152,6 @@ Pulling and overriding local solution with latest nightly build
156
152
  The query command will fire any query towards the admin Api with the given query parameters. This means any query parameter that's necessary for the given query, is required as an option in this command. It's also possible to list which parameters is necessary for the given query through the options;
157
153
  - `-l` `--list` Will list all the properties for the given \<query\>
158
154
  - `-i` `--interactive` Will perform the \<query\> but without any parameters, as they will be asked for one by one in interactive mode
159
- - `--output json` Will output a single JSON object for automation-friendly parsing
160
155
  - `--<queryParam>` Any parameter the query needs will be sent by '--key value'
161
156
 
162
157
  #### Examples
@@ -166,16 +161,12 @@ Getting all properties for a query
166
161
  Getting file information on a specific file by name
167
162
  > $ dw query FileByName --name DefaultMail.html --directorypath /Templates/Forms/Mail
168
163
 
169
- Running a query with JSON output
170
- > $ dw query FileByName --name DefaultMail.html --output json
171
-
172
164
  ### Command
173
165
  > $ dw command \<command\>
174
166
 
175
167
  Using command will, like query, fire any given command in the solution. It works like query, given the query parameters necessary, however if a `DataModel` is required for the command, it is given in a json-format, either through a path to a .json file or a literal json-string in the command.
176
168
  - `-l` `--list` Lists all the properties for the command, as well as the json model required
177
169
  - `--json` Takes a path to a .json file or a literal json, i.e --json '{ abc: "123" }'
178
- - `--output json` Outputs a single JSON object for automation-friendly parsing
179
170
 
180
171
  #### Examples
181
172
  Creating a copy of a page using a json-string
@@ -192,9 +183,6 @@ Where PageMove.json contains
192
183
  Deleting a page
193
184
  > $ dw command PageDelete --json '{ "id": "1383" }'
194
185
 
195
- Running a command with JSON output
196
- > $ dw command PageDelete --json '{ "id": "1383" }' --output json
197
-
198
186
  ### Install
199
187
  > $ dw install \<filePath\>
200
188
 
@@ -4,7 +4,7 @@ import fs from 'fs';
4
4
  import { setupEnv, getAgent } from './env.js';
5
5
  import { setupUser } from './login.js';
6
6
 
7
- const exclude = ['_', '$0', 'command', 'list', 'json', 'verbose', 'v', 'host', 'protocol', 'apiKey', 'env', 'output']
7
+ const exclude = ['_', '$0', 'command', 'list', 'json', 'verbose', 'v', 'host', 'protocol', 'apiKey', 'env']
8
8
 
9
9
  export function commandCommand() {
10
10
  return {
@@ -22,38 +22,22 @@ export function commandCommand() {
22
22
  alias: 'l',
23
23
  describe: 'Lists all the properties for the command, currently not working'
24
24
  })
25
- .option('output', {
26
- choices: ['json'],
27
- describe: 'Outputs a single JSON response for automation-friendly parsing'
28
- })
29
25
  },
30
26
  handler: async (argv) => {
31
- const output = createCommandOutput(argv);
32
-
33
- try {
34
- output.verboseLog(`Running command ${argv.command}`);
35
- await handleCommand(argv, output);
36
- output.finish();
37
- } catch (err) {
38
- output.fail(err);
39
- output.finish();
40
- process.exit(1);
41
- }
27
+ if (argv.verbose) console.info(`Running command ${argv.command}`)
28
+ await handleCommand(argv)
42
29
  }
43
30
  }
44
31
  }
45
32
 
46
- async function handleCommand(argv, output) {
33
+ async function handleCommand(argv) {
47
34
  let env = await setupEnv(argv);
48
35
  let user = await setupUser(argv, env);
49
36
  if (argv.list) {
50
- const properties = await getProperties(env, user, argv.command);
51
- output.addData(properties);
52
- output.log(properties);
37
+ console.log(await getProperties(env, user, argv.command))
53
38
  } else {
54
- let response = await runCommand(env, user, argv.command, getQueryParams(argv), parseJsonOrPath(argv.json));
55
- output.addData(response);
56
- output.log(response);
39
+ let response = await runCommand(env, user, argv.command, getQueryParams(argv), parseJsonOrPath(argv.json))
40
+ console.log(response)
57
41
  }
58
42
  }
59
43
 
@@ -98,67 +82,8 @@ async function runCommand(env, user, command, queryParams, data) {
98
82
  agent: getAgent(env.protocol)
99
83
  })
100
84
  if (!res.ok) {
101
- throw createCommandError(`Error when doing request ${res.url}`, res.status, await parseJsonSafe(res));
85
+ console.log(`Error when doing request ${res.url}`)
86
+ process.exit(1);
102
87
  }
103
88
  return await res.json()
104
- }
105
-
106
- function createCommandOutput(argv) {
107
- const response = {
108
- ok: true,
109
- command: 'command',
110
- operation: argv.list ? 'list' : 'run',
111
- status: 200,
112
- data: [],
113
- errors: [],
114
- meta: {
115
- commandName: argv.command
116
- }
117
- };
118
-
119
- return {
120
- json: argv.output === 'json',
121
- response,
122
- log(value) {
123
- if (!this.json) {
124
- console.log(value);
125
- }
126
- },
127
- verboseLog(...args) {
128
- if (argv.verbose && !this.json) {
129
- console.info(...args);
130
- }
131
- },
132
- addData(entry) {
133
- response.data.push(entry);
134
- },
135
- fail(err) {
136
- response.ok = false;
137
- response.status = err?.status || 1;
138
- response.errors.push({
139
- message: err?.message || 'Unknown command error.',
140
- details: err?.details ?? null
141
- });
142
- },
143
- finish() {
144
- if (this.json) {
145
- console.log(JSON.stringify(response, null, 2));
146
- }
147
- }
148
- };
149
- }
150
-
151
- function createCommandError(message, status, details = null) {
152
- const error = new Error(message);
153
- error.status = status;
154
- error.details = details;
155
- return error;
156
- }
157
-
158
- async function parseJsonSafe(res) {
159
- try {
160
- return await res.json();
161
- } catch {
162
- return null;
163
- }
164
- }
89
+ }
@@ -10,169 +10,152 @@ 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
- })
78
- .option('json', {
79
- type: 'boolean',
80
- describe: 'Outputs a single JSON response for automation-friendly parsing'
81
- })
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
+ })
82
78
  },
83
79
  handler: async (argv) => {
84
- const output = createFilesOutput(argv);
85
-
86
- try {
87
- output.verboseLog(`Listing directory at: ${argv.dirPath}`);
88
- await handleFiles(argv, output);
89
- output.finish();
90
- } catch (err) {
91
- output.fail(err);
92
- output.finish();
93
- process.exit(1);
94
- }
80
+ if (argv.verbose) console.info(`Listing directory at: ${argv.dirPath}`)
81
+ await handleFiles(argv)
95
82
  }
96
83
  }
97
84
  }
98
85
 
99
- async function handleFiles(argv, output) {
86
+ async function handleFiles(argv) {
100
87
  let env = await setupEnv(argv);
101
88
  let user = await setupUser(argv, env);
102
89
 
103
90
  if (argv.list) {
104
91
  let files = (await getFilesStructure(env, user, argv.dirPath, argv.recursive, argv.includeFiles)).model;
105
- output.setStatus(200);
106
- output.addData(files);
107
- if (!output.json) {
108
- output.log(files.name);
109
- let hasFiles = files.files?.data && files.files?.data.length !== 0;
110
- resolveTree(files.directories, '', hasFiles, output);
111
- resolveTree(files.files?.data ?? [], '', false, output);
112
- }
92
+ console.log(files.name)
93
+ let hasFiles = files.files?.data && files.files?.data.length !== 0;
94
+ resolveTree(files.directories, '', hasFiles);
95
+ resolveTree(files.files?.data ?? [], '', false);
113
96
  }
114
97
 
115
98
  if (argv.export) {
116
99
  if (argv.dirPath) {
117
-
100
+
118
101
  const isFile = argv.asFile || argv.asDirectory
119
102
  ? argv.asFile
120
- : path.extname(argv.dirPath) !== '';
103
+ : path.extname(argv.dirPath) !== '';
121
104
 
122
105
  if (isFile) {
123
- let parentDirectory = path.dirname(argv.dirPath);
106
+ let parentDirectory = path.dirname(argv.dirPath);
124
107
  parentDirectory = parentDirectory === '.' ? '/' : parentDirectory;
125
-
126
- await download(env, user, parentDirectory, argv.outPath, false, null, true, argv.iamstupid, [argv.dirPath], true, output);
108
+
109
+ await download(env, user, parentDirectory, argv.outPath, false, null, true, argv.iamstupid, [argv.dirPath], true, argv.verbose);
127
110
  } else {
128
- await download(env, user, argv.dirPath, argv.outPath, true, null, argv.raw, argv.iamstupid, [], false, output);
111
+ await download(env, user, argv.dirPath, argv.outPath, true, null, argv.raw, argv.iamstupid, [], false, argv.verbose);
129
112
  }
130
113
  } else {
131
114
  await interactiveConfirm('Are you sure you want a full export of files?', async () => {
132
- output.log('Full export is starting');
115
+ console.log('Full export is starting')
133
116
  let filesStructure = (await getFilesStructure(env, user, '/', false, true)).model;
134
117
  let dirs = filesStructure.directories;
135
118
  for (let id = 0; id < dirs.length; id++) {
136
119
  const dir = dirs[id];
137
- await download(env, user, dir.name, argv.outPath, true, null, argv.raw, argv.iamstupid, [], false, output);
120
+ await download(env, user, dir.name, argv.outPath, true, null, argv.raw, argv.iamstupid, [], false, argv.verbose);
138
121
  }
139
- await download(env, user, '/.', argv.outPath, false, 'Base.zip', argv.raw, argv.iamstupid, Array.from(filesStructure.files.data, f => f.name), false, output);
140
- if (argv.raw) output.log('The files in the base "files" folder is in Base.zip, each directory in "files" is in its own zip');
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
+ 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')
141
124
  })
142
125
  }
143
126
  } else if (argv.import) {
144
127
  if (argv.dirPath && argv.outPath) {
145
128
  let resolvedPath = path.resolve(argv.dirPath);
146
129
  if (argv.recursive) {
147
- await processDirectory(env, user, resolvedPath, argv.outPath, resolvedPath, argv.createEmpty, true, argv.overwrite, output);
130
+ await processDirectory(env, user, resolvedPath, argv.outPath, resolvedPath, argv.createEmpty, true, argv.overwrite);
148
131
  } else {
149
132
  let filesInDir = getFilesInDirectory(resolvedPath);
150
- await uploadFiles(env, user, filesInDir, argv.outPath, argv.createEmpty, argv.overwrite, output);
133
+ await uploadFiles(env, user, filesInDir, argv.outPath, argv.createEmpty, argv.overwrite);
151
134
  }
152
135
  }
153
136
  }
154
137
  }
155
138
 
156
139
  function getFilesInDirectory(dirPath) {
157
- return fs.statSync(dirPath).isFile() ? [ dirPath ] : fs.readdirSync(dirPath)
158
- .map(file => path.join(dirPath, file))
159
- .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());
160
143
  }
161
144
 
162
- async function processDirectory(env, user, dirPath, outPath, originalDir, createEmpty, isRoot = false, overwrite = false, output) {
145
+ async function processDirectory(env, user, dirPath, outPath, originalDir, createEmpty, isRoot = false, overwrite = false, output = console) {
163
146
  let filesInDir = getFilesInDirectory(dirPath);
164
147
  if (filesInDir.length > 0)
165
148
  await uploadFiles(env, user, filesInDir, isRoot ? outPath : path.join(outPath, path.basename(dirPath)), createEmpty, overwrite, output);
166
149
 
167
150
  const subDirectories = fs.readdirSync(dirPath)
168
- .map(subDir => path.join(dirPath, subDir))
169
- .filter(subDir => fs.statSync(subDir).isDirectory());
151
+ .map(subDir => path.join(dirPath, subDir))
152
+ .filter(subDir => fs.statSync(subDir).isDirectory());
170
153
  for (let subDir of subDirectories) {
171
154
  await processDirectory(env, user, subDir, isRoot ? outPath : path.join(outPath, path.basename(dirPath)), originalDir, createEmpty, false, overwrite, output);
172
155
  }
173
156
  }
174
157
 
175
- function resolveTree(dirs, indentLevel, parentHasFiles, output) {
158
+ function resolveTree(dirs, indentLevel, parentHasFiles) {
176
159
  let end = `└──`
177
160
  let mid = `├──`
178
161
  for (let id = 0; id < dirs.length; id++) {
@@ -180,33 +163,33 @@ function resolveTree(dirs, indentLevel, parentHasFiles, output) {
180
163
  let indentPipe = true;
181
164
  if (dirs.length == 1) {
182
165
  if (parentHasFiles) {
183
- output.log(indentLevel + mid, dir.name)
166
+ console.log(indentLevel + mid, dir.name)
184
167
  } else {
185
- output.log(indentLevel + end, dir.name)
168
+ console.log(indentLevel + end, dir.name)
186
169
  indentPipe = false;
187
170
  }
188
171
  } else if (id != dirs.length - 1) {
189
- output.log(indentLevel + mid, dir.name)
172
+ console.log(indentLevel + mid, dir.name)
190
173
  } else {
191
174
  if (parentHasFiles) {
192
- output.log(indentLevel + mid, dir.name)
175
+ console.log(indentLevel + mid, dir.name)
193
176
  } else {
194
- output.log(indentLevel + end, dir.name)
177
+ console.log(indentLevel + end, dir.name)
195
178
  indentPipe = false;
196
179
  }
197
180
  }
198
181
  let hasFiles = dir.files?.data && dir.files?.data.length !== 0;
199
182
  if (indentPipe) {
200
- resolveTree(dir.directories ?? [], indentLevel + '│\t', hasFiles, output);
201
- resolveTree(dir.files?.data ?? [], indentLevel + '│\t', false, output);
183
+ resolveTree(dir.directories ?? [], indentLevel + '│\t', hasFiles);
184
+ resolveTree(dir.files?.data ?? [], indentLevel + '│\t', false);
202
185
  } else {
203
- resolveTree(dir.directories ?? [], indentLevel + '\t', hasFiles, output);
204
- resolveTree(dir.files?.data ?? [], indentLevel + '\t', false, output);
186
+ resolveTree(dir.directories ?? [], indentLevel + '\t', hasFiles);
187
+ resolveTree(dir.files?.data ?? [], indentLevel + '\t', false);
205
188
  }
206
189
  }
207
190
  }
208
191
 
209
- async function download(env, user, dirPath, outPath, recursive, outname, raw, iamstupid, fileNames, singleFileMode, output) {
192
+ async function download(env, user, dirPath, outPath, recursive, outname, raw, iamstupid, fileNames, singleFileMode, verbose = false) {
210
193
  let excludeDirectories = '';
211
194
  if (!iamstupid) {
212
195
  excludeDirectories = 'system/log';
@@ -217,7 +200,7 @@ async function download(env, user, dirPath, outPath, recursive, outname, raw, ia
217
200
 
218
201
  const { endpoint, data } = prepareDownloadCommandData(dirPath, excludeDirectories, fileNames, recursive, singleFileMode);
219
202
 
220
- displayDownloadMessage(dirPath, fileNames, recursive, singleFileMode, output);
203
+ displayDownloadMessage(dirPath, fileNames, recursive, singleFileMode);
221
204
 
222
205
  const res = await fetch(`${env.protocol}://${env.host}/Admin/Api/${endpoint}`, {
223
206
  method: 'POST',
@@ -229,40 +212,27 @@ async function download(env, user, dirPath, outPath, recursive, outname, raw, ia
229
212
  agent: getAgent(env.protocol)
230
213
  });
231
214
 
232
- const filename = outname || tryGetFileNameFromResponse(res, dirPath, output.verbose);
215
+ const filename = outname || tryGetFileNameFromResponse(res, dirPath, verbose);
233
216
  if (!filename) return;
234
217
 
235
218
  const filePath = path.resolve(`${path.resolve(outPath)}/${filename}`)
236
- const updater = output.json ? null : createThrottledStatusUpdater();
219
+ const updater = createThrottledStatusUpdater();
237
220
 
238
221
  await downloadWithProgress(res, filePath, {
239
222
  onData: (received) => {
240
- if (updater) {
241
- updater.update(`Received:\t${formatBytes(received)}`);
242
- }
223
+ updater.update(`Received:\t${formatBytes(received)}`);
243
224
  }
244
225
  });
245
226
 
246
- if (updater) {
247
- updater.stop();
248
- }
227
+ updater.stop();
249
228
 
250
229
  if (singleFileMode) {
251
- output.log(`Successfully downloaded: ${filename}`);
230
+ console.log(`Successfully downloaded: ${filename}`);
252
231
  } else {
253
- output.log(`Finished downloading`, dirPath === '/.' ? '.' : dirPath, 'Recursive=' + recursive);
232
+ console.log(`Finished downloading`, dirPath === '/.' ? '.' : dirPath, 'Recursive=' + recursive);
254
233
  }
255
234
 
256
- output.addData({
257
- type: 'download',
258
- directoryPath: dirPath,
259
- filename,
260
- outPath: path.resolve(outPath),
261
- recursive,
262
- raw
263
- });
264
-
265
- await extractArchive(filename, filePath, outPath, raw, output);
235
+ await extractArchive(filename, filePath, outPath, raw);
266
236
  }
267
237
 
268
238
  function prepareDownloadCommandData(directoryPath, excludeDirectories, fileNames, recursive, singleFileMode) {
@@ -279,10 +249,10 @@ function prepareDownloadCommandData(directoryPath, excludeDirectories, fileNames
279
249
  return { endpoint: 'FileDownload', data };
280
250
  }
281
251
 
282
- function displayDownloadMessage(directoryPath, fileNames, recursive, singleFileMode, output) {
252
+ function displayDownloadMessage(directoryPath, fileNames, recursive, singleFileMode) {
283
253
  if (singleFileMode) {
284
254
  const fileName = path.basename(fileNames[0] || 'unknown');
285
- output.log('Downloading file: ' + fileName);
255
+ console.log('Downloading file: ' + fileName);
286
256
 
287
257
  return;
288
258
  }
@@ -291,36 +261,32 @@ function displayDownloadMessage(directoryPath, fileNames, recursive, singleFileM
291
261
  ? 'Base'
292
262
  : directoryPath;
293
263
 
294
- output.log('Downloading', directoryPathDisplayName, 'Recursive=' + recursive);
264
+ console.log('Downloading', directoryPathDisplayName, 'Recursive=' + recursive);
295
265
  }
296
266
 
297
- async function extractArchive(filename, filePath, outPath, raw, output) {
267
+ async function extractArchive(filename, filePath, outPath, raw) {
298
268
  if (raw) {
299
269
  return;
300
270
  }
301
271
 
302
- output.log(`\nExtracting ${filename} to ${outPath}`);
272
+ console.log(`\nExtracting ${filename} to ${outPath}`);
303
273
  let destinationFilename = filename.replace('.zip', '');
304
274
  if (destinationFilename === 'Base')
305
275
  destinationFilename = '';
306
276
 
307
277
  const destinationPath = `${path.resolve(outPath)}/${destinationFilename}`;
308
- const updater = output.json ? null : createThrottledStatusUpdater();
278
+ const updater = createThrottledStatusUpdater();
309
279
 
310
280
  await extractWithProgress(filePath, destinationPath, {
311
281
  onEntry: (processedEntries, totalEntries, percent) => {
312
- if (updater) {
313
- updater.update(`Extracted:\t${processedEntries} of ${totalEntries} files (${percent}%)`);
314
- }
282
+ updater.update(`Extracted:\t${processedEntries} of ${totalEntries} files (${percent}%)`);
315
283
  }
316
284
  });
317
285
 
318
- if (updater) {
319
- updater.stop();
320
- }
321
- output.log(`Finished extracting ${filename} to ${outPath}\n`);
286
+ updater.stop();
287
+ console.log(`Finished extracting ${filename} to ${outPath}\n`);
322
288
 
323
- fs.unlink(filePath, function(err) {});
289
+ fs.unlink(filePath, function (err) { });
324
290
  }
325
291
 
326
292
  async function getFilesStructure(env, user, dirPath, recursive, includeFiles) {
@@ -334,11 +300,14 @@ async function getFilesStructure(env, user, dirPath, recursive, includeFiles) {
334
300
  if (res.ok) {
335
301
  return await res.json();
336
302
  } else {
337
- throw createCommandError('Unable to fetch file structure.', res.status, await parseJsonSafe(res));
303
+ console.log(res);
304
+ console.log(await res.json());
305
+ process.exit(1);
338
306
  }
339
307
  }
340
308
 
341
309
  export async function uploadFiles(env, user, localFilePaths, destinationPath, createEmpty = false, overwrite = false, output) {
310
+ output = resolveUploadOutput(output);
342
311
  output.log('Uploading files')
343
312
 
344
313
  const chunkSize = 300;
@@ -371,18 +340,18 @@ export async function uploadFiles(env, user, localFilePaths, destinationPath, cr
371
340
  output.log(`Finished uploading files. Total files: ${localFilePaths.length}, total chunks: ${chunks.length}`);
372
341
  }
373
342
 
374
- async function uploadChunk(env, user, filePathsChunk, destinationPath, createEmpty, overwrite, output) {
343
+ async function uploadChunk(env, user, filePathsChunk, destinationPath, createEmpty, overwrite, output = console) {
375
344
  const form = new FormData();
376
345
  form.append('path', destinationPath);
377
346
  form.append('skipExistingFiles', String(!overwrite));
378
347
  form.append('allowOverwrite', String(overwrite));
379
-
348
+
380
349
  filePathsChunk.forEach(fileToUpload => {
381
350
  output.log(`${fileToUpload}`)
382
351
  form.append('files', fs.createReadStream(path.resolve(fileToUpload)));
383
352
  });
384
353
 
385
- 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 }), {
386
355
  method: 'POST',
387
356
  body: form,
388
357
  headers: {
@@ -390,103 +359,46 @@ async function uploadChunk(env, user, filePathsChunk, destinationPath, createEmp
390
359
  },
391
360
  agent: getAgent(env.protocol)
392
361
  });
393
-
362
+
394
363
  if (res.ok) {
395
- return await res.json();
364
+ return await res.json()
396
365
  }
397
366
  else {
398
- throw createCommandError('File upload failed.', res.status, await parseJsonSafe(res));
399
- }
400
- }
367
+ if (output.structured) {
368
+ throw createUploadError('File upload failed.', res.status, await parseJsonSafe(res));
369
+ }
401
370
 
402
- export function resolveFilePath(filePath) {
403
- let p = path.parse(path.resolve(filePath))
404
- let regex = wildcardToRegExp(p.base);
405
- let resolvedPath = fs.readdirSync(p.dir).filter((allFilesPaths) => allFilesPaths.match(regex) !== null)[0]
406
- if (resolvedPath === undefined)
407
- {
408
- console.log('Could not find any files with the name ' + filePath);
371
+ output.log(res)
372
+ output.log(await parseJsonSafe(res))
409
373
  process.exit(1);
410
374
  }
411
- return path.join(p.dir, resolvedPath);
412
- }
413
-
414
-
415
- function wildcardToRegExp(wildcard) {
416
- const escaped = wildcard.replace(/[.+?^${}()|[\]\\]/g, '\\$&');
417
- return new RegExp('^' + escaped.replace(/\*/g, '.*') + '$');
418
375
  }
419
376
 
420
- function createFilesOutput(argv) {
421
- const response = {
422
- ok: true,
423
- command: 'files',
424
- operation: getFilesOperation(argv),
425
- status: 200,
426
- data: [],
427
- errors: [],
428
- meta: {}
429
- };
377
+ export function resolveUploadOutput(output) {
378
+ const response = output?.response ?? {};
379
+ response.meta = response.meta ?? {};
430
380
 
431
381
  return {
432
- json: Boolean(argv.json),
433
- verbose: Boolean(argv.verbose),
382
+ structured: Boolean(output),
434
383
  response,
435
- log(...args) {
436
- if (!this.json) {
437
- console.log(...args);
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
+ };
438
397
  }
439
- },
440
- verboseLog(...args) {
441
- if (this.verbose && !this.json) {
442
- console.info(...args);
443
- }
444
- },
445
- addData(entry) {
446
- response.data.push(entry);
447
- },
448
- mergeMeta(meta) {
449
- response.meta = {
450
- ...response.meta,
451
- ...meta
452
- };
453
- },
454
- setStatus(status) {
455
- response.status = status;
456
- },
457
- fail(err) {
458
- response.ok = false;
459
- response.status = err?.status || 1;
460
- response.errors.push({
461
- message: err?.message || 'Unknown files command error.',
462
- details: err?.details ?? null
463
- });
464
- },
465
- finish() {
466
- if (this.json) {
467
- console.log(JSON.stringify(response, null, 2));
468
- }
469
- }
470
398
  };
471
399
  }
472
400
 
473
- function getFilesOperation(argv) {
474
- if (argv.list) {
475
- return 'list';
476
- }
477
-
478
- if (argv.export) {
479
- return 'export';
480
- }
481
-
482
- if (argv.import) {
483
- return 'import';
484
- }
485
-
486
- return 'unknown';
487
- }
488
-
489
- function createCommandError(message, status, details = null) {
401
+ function createUploadError(message, status, details = null) {
490
402
  const error = new Error(message);
491
403
  error.status = status;
492
404
  error.details = details;
@@ -500,3 +412,19 @@ async function parseJsonSafe(res) {
500
412
  return null;
501
413
  }
502
414
  }
415
+
416
+ export function resolveFilePath(filePath) {
417
+ let p = path.parse(path.resolve(filePath))
418
+ let regex = wildcardToRegExp(p.base);
419
+ let resolvedPath = fs.readdirSync(p.dir).filter((allFilesPaths) => allFilesPaths.match(regex) !== null)[0]
420
+ if (resolvedPath === undefined) {
421
+ throw new Error('Could not find any files with the name ' + filePath);
422
+ }
423
+ return path.join(p.dir, resolvedPath);
424
+ }
425
+
426
+
427
+ function wildcardToRegExp(wildcard) {
428
+ const escaped = wildcard.replace(/[.+?^${}()|[\]\\]/g, '\\$&');
429
+ return new RegExp('^' + escaped.replace(/\*/g, '.*') + '$');
430
+ }
@@ -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
+ });
@@ -3,7 +3,7 @@ import { setupEnv, getAgent } from './env.js';
3
3
  import { setupUser } from './login.js';
4
4
  import { input } from '@inquirer/prompts';
5
5
 
6
- const exclude = ['_', '$0', 'query', 'list', 'i', 'l', 'interactive', 'verbose', 'v', 'host', 'protocol', 'apiKey', 'env', 'output']
6
+ const exclude = ['_', '$0', 'query', 'list', 'i', 'l', 'interactive', 'verbose', 'v', 'host', 'protocol', 'apiKey', 'env']
7
7
 
8
8
  export function queryCommand() {
9
9
  return {
@@ -22,43 +22,30 @@ export function queryCommand() {
22
22
  alias: 'i',
23
23
  describe: 'Runs in interactive mode to ask for query parameters one by one'
24
24
  })
25
- .option('output', {
26
- choices: ['json'],
27
- describe: 'Outputs a single JSON response for automation-friendly parsing'
28
- })
29
25
  },
30
- handler: async (argv) => {
31
- const output = createQueryOutput(argv);
32
-
33
- try {
34
- output.verboseLog(`Running query ${argv.query}`);
35
- await handleQuery(argv, output);
36
- output.finish();
37
- } catch (err) {
38
- output.fail(err);
39
- output.finish();
40
- process.exit(1);
41
- }
26
+ handler: (argv) => {
27
+ if (argv.verbose) console.info(`Running query ${argv.query}`)
28
+ handleQuery(argv)
42
29
  }
43
30
  }
44
31
  }
45
32
 
46
- async function handleQuery(argv, output) {
33
+ async function handleQuery(argv) {
47
34
  let env = await setupEnv(argv);
48
35
  let user = await setupUser(argv, env);
49
36
  if (argv.list) {
50
- const properties = await getProperties(env, user, argv.query);
51
- output.addData(properties);
52
- output.log(properties);
37
+ console.log(await getProperties(argv))
53
38
  } else {
54
- let response = await runQuery(env, user, argv.query, await getQueryParams(argv));
55
- output.addData(response);
56
- output.log(response);
39
+ let response = await runQuery(env, user, argv.query, await getQueryParams(argv))
40
+ console.log(response)
57
41
  }
58
42
  }
59
43
 
60
- async function getProperties(env, user, query) {
61
- let res = await fetch(`${env.protocol}://${env.host}/Admin/Api/QueryByName?name=${query}`, {
44
+ async function getProperties(argv) {
45
+ let env = await setupEnv(argv);
46
+ let user = await setupUser(argv, env);
47
+
48
+ let res = await fetch(`${env.protocol}://${env.host}/Admin/Api/QueryByName?name=${argv.query}`, {
62
49
  method: 'GET',
63
50
  headers: {
64
51
  'Authorization': `Bearer ${user.apiKey}`
@@ -68,20 +55,20 @@ async function getProperties(env, user, query) {
68
55
  if (res.ok) {
69
56
  let body = await res.json()
70
57
  if (body.model.properties.groups === undefined) {
71
- throw createCommandError('Unable to fetch query parameters.', res.status, body);
58
+ console.log('Unable to fetch query parameters');
59
+ process.exit(1);
72
60
  }
73
61
  return body.model.properties.groups.filter(g => g.name === 'Properties')[0].fields.map(field => `${field.name} (${field.typeName})`)
74
62
  }
75
-
76
- throw createCommandError('Unable to fetch query parameters.', res.status, await parseJsonSafe(res));
63
+ console.log('Unable to fetch query parameters');
64
+ console.log(res);
65
+ process.exit(1);
77
66
  }
78
67
 
79
68
  async function getQueryParams(argv) {
80
69
  let params = {}
81
70
  if (argv.interactive) {
82
- let env = await setupEnv(argv);
83
- let user = await setupUser(argv, env);
84
- let properties = await getProperties(env, user, argv.query);
71
+ let properties = await getProperties(argv);
85
72
  console.log('The following properties will be requested:')
86
73
  console.log(properties)
87
74
  for (const p of properties) {
@@ -106,67 +93,8 @@ async function runQuery(env, user, query, params) {
106
93
  agent: getAgent(env.protocol)
107
94
  })
108
95
  if (!res.ok) {
109
- throw createCommandError(`Error when doing request ${res.url}`, res.status, await parseJsonSafe(res));
96
+ console.log(`Error when doing request ${res.url}`)
97
+ process.exit(1);
110
98
  }
111
99
  return await res.json()
112
- }
113
-
114
- function createQueryOutput(argv) {
115
- const response = {
116
- ok: true,
117
- command: 'query',
118
- operation: argv.list ? 'list' : 'run',
119
- status: 200,
120
- data: [],
121
- errors: [],
122
- meta: {
123
- query: argv.query
124
- }
125
- };
126
-
127
- return {
128
- json: argv.output === 'json',
129
- response,
130
- log(value) {
131
- if (!this.json) {
132
- console.log(value);
133
- }
134
- },
135
- verboseLog(...args) {
136
- if (argv.verbose && !this.json) {
137
- console.info(...args);
138
- }
139
- },
140
- addData(entry) {
141
- response.data.push(entry);
142
- },
143
- fail(err) {
144
- response.ok = false;
145
- response.status = err?.status || 1;
146
- response.errors.push({
147
- message: err?.message || 'Unknown query command error.',
148
- details: err?.details ?? null
149
- });
150
- },
151
- finish() {
152
- if (this.json) {
153
- console.log(JSON.stringify(response, null, 2));
154
- }
155
- }
156
- };
157
- }
158
-
159
- function createCommandError(message, status, details = null) {
160
- const error = new Error(message);
161
- error.status = status;
162
- error.details = details;
163
- return error;
164
- }
165
-
166
- async function parseJsonSafe(res) {
167
- try {
168
- return await res.json();
169
- } catch {
170
- return null;
171
- }
172
- }
100
+ }
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.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": "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": {
@@ -48,4 +48,4 @@
48
48
  "node-fetch": "^3.2.10",
49
49
  "yargs": "^17.5.1"
50
50
  }
51
- }
51
+ }