@dynamicweb/cli 1.1.0 → 1.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -98,6 +98,7 @@ 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
101
102
 
102
103
  #### Examples
103
104
  Exporting all templates from current environment to local solution
@@ -108,6 +109,9 @@ Exporting all templates from current environment to local solution
108
109
  Listing the system files structure of the current environment
109
110
  > $ dw files system -lr
110
111
 
112
+ Uploading files with JSON output for automation
113
+ > $ dw files ./Files templates -i -r --json
114
+
111
115
  ### Files Source Type Detection
112
116
  By default, the `dw files` command automatically detects the source type based on the \<dirPath\>:
113
117
  If the path contains a file extension (e.g., 'templates/Translations.xml'), it is treated as a file.
@@ -152,6 +156,7 @@ Pulling and overriding local solution with latest nightly build
152
156
  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;
153
157
  - `-l` `--list` Will list all the properties for the given \<query\>
154
158
  - `-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
155
160
  - `--<queryParam>` Any parameter the query needs will be sent by '--key value'
156
161
 
157
162
  #### Examples
@@ -161,12 +166,16 @@ Getting all properties for a query
161
166
  Getting file information on a specific file by name
162
167
  > $ dw query FileByName --name DefaultMail.html --directorypath /Templates/Forms/Mail
163
168
 
169
+ Running a query with JSON output
170
+ > $ dw query FileByName --name DefaultMail.html --output json
171
+
164
172
  ### Command
165
173
  > $ dw command \<command\>
166
174
 
167
175
  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.
168
176
  - `-l` `--list` Lists all the properties for the command, as well as the json model required
169
177
  - `--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
170
179
 
171
180
  #### Examples
172
181
  Creating a copy of a page using a json-string
@@ -183,6 +192,9 @@ Where PageMove.json contains
183
192
  Deleting a page
184
193
  > $ dw command PageDelete --json '{ "id": "1383" }'
185
194
 
195
+ Running a command with JSON output
196
+ > $ dw command PageDelete --json '{ "id": "1383" }' --output json
197
+
186
198
  ### Install
187
199
  > $ dw install \<filePath\>
188
200
 
@@ -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']
7
+ const exclude = ['_', '$0', 'command', 'list', 'json', 'verbose', 'v', 'host', 'protocol', 'apiKey', 'env', 'output']
8
8
 
9
9
  export function commandCommand() {
10
10
  return {
@@ -22,22 +22,38 @@ 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
+ })
25
29
  },
26
30
  handler: async (argv) => {
27
- if (argv.verbose) console.info(`Running command ${argv.command}`)
28
- await handleCommand(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
+ }
29
42
  }
30
43
  }
31
44
  }
32
45
 
33
- async function handleCommand(argv) {
46
+ async function handleCommand(argv, output) {
34
47
  let env = await setupEnv(argv);
35
48
  let user = await setupUser(argv, env);
36
49
  if (argv.list) {
37
- console.log(await getProperties(env, user, argv.command))
50
+ const properties = await getProperties(env, user, argv.command);
51
+ output.addData(properties);
52
+ output.log(properties);
38
53
  } else {
39
- let response = await runCommand(env, user, argv.command, getQueryParams(argv), parseJsonOrPath(argv.json))
40
- console.log(response)
54
+ let response = await runCommand(env, user, argv.command, getQueryParams(argv), parseJsonOrPath(argv.json));
55
+ output.addData(response);
56
+ output.log(response);
41
57
  }
42
58
  }
43
59
 
@@ -82,8 +98,67 @@ async function runCommand(env, user, command, queryParams, data) {
82
98
  agent: getAgent(env.protocol)
83
99
  })
84
100
  if (!res.ok) {
85
- console.log(`Error when doing request ${res.url}`)
86
- process.exit(1);
101
+ throw createCommandError(`Error when doing request ${res.url}`, res.status, await parseJsonSafe(res));
87
102
  }
88
103
  return await res.json()
89
- }
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
+ }
@@ -75,24 +75,41 @@ export function filesCommand() {
75
75
  describe: 'Forces the command to treat the path as a directory, even if its name contains a dot.',
76
76
  conflicts: 'asFile'
77
77
  })
78
+ .option('json', {
79
+ type: 'boolean',
80
+ describe: 'Outputs a single JSON response for automation-friendly parsing'
81
+ })
78
82
  },
79
83
  handler: async (argv) => {
80
- if (argv.verbose) console.info(`Listing directory at: ${argv.dirPath}`)
81
- await handleFiles(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
+ }
82
95
  }
83
96
  }
84
97
  }
85
98
 
86
- async function handleFiles(argv) {
99
+ async function handleFiles(argv, output) {
87
100
  let env = await setupEnv(argv);
88
101
  let user = await setupUser(argv, env);
89
102
 
90
103
  if (argv.list) {
91
104
  let files = (await getFilesStructure(env, user, argv.dirPath, argv.recursive, argv.includeFiles)).model;
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);
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
+ }
96
113
  }
97
114
 
98
115
  if (argv.export) {
@@ -106,31 +123,31 @@ async function handleFiles(argv) {
106
123
  let parentDirectory = path.dirname(argv.dirPath);
107
124
  parentDirectory = parentDirectory === '.' ? '/' : parentDirectory;
108
125
 
109
- await download(env, user, parentDirectory, argv.outPath, false, null, true, argv.iamstupid, [argv.dirPath], true);
126
+ await download(env, user, parentDirectory, argv.outPath, false, null, true, argv.iamstupid, [argv.dirPath], true, output);
110
127
  } else {
111
- await download(env, user, argv.dirPath, argv.outPath, true, null, argv.raw, argv.iamstupid, [], false);
128
+ await download(env, user, argv.dirPath, argv.outPath, true, null, argv.raw, argv.iamstupid, [], false, output);
112
129
  }
113
130
  } else {
114
131
  await interactiveConfirm('Are you sure you want a full export of files?', async () => {
115
- console.log('Full export is starting')
132
+ output.log('Full export is starting');
116
133
  let filesStructure = (await getFilesStructure(env, user, '/', false, true)).model;
117
134
  let dirs = filesStructure.directories;
118
135
  for (let id = 0; id < dirs.length; id++) {
119
136
  const dir = dirs[id];
120
- await download(env, user, dir.name, argv.outPath, true, null, argv.raw, argv.iamstupid, [], false);
137
+ await download(env, user, dir.name, argv.outPath, true, null, argv.raw, argv.iamstupid, [], false, output);
121
138
  }
122
- await download(env, user, '/.', argv.outPath, false, 'Base.zip', argv.raw, argv.iamstupid, Array.from(filesStructure.files.data, f => f.name), false);
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')
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');
124
141
  })
125
142
  }
126
143
  } else if (argv.import) {
127
144
  if (argv.dirPath && argv.outPath) {
128
145
  let resolvedPath = path.resolve(argv.dirPath);
129
146
  if (argv.recursive) {
130
- await processDirectory(env, user, resolvedPath, argv.outPath, resolvedPath, argv.createEmpty, true, argv.overwrite);
147
+ await processDirectory(env, user, resolvedPath, argv.outPath, resolvedPath, argv.createEmpty, true, argv.overwrite, output);
131
148
  } else {
132
149
  let filesInDir = getFilesInDirectory(resolvedPath);
133
- await uploadFiles(env, user, filesInDir, argv.outPath, argv.createEmpty, argv.overwrite);
150
+ await uploadFiles(env, user, filesInDir, argv.outPath, argv.createEmpty, argv.overwrite, output);
134
151
  }
135
152
  }
136
153
  }
@@ -142,20 +159,20 @@ function getFilesInDirectory(dirPath) {
142
159
  .filter(file => fs.statSync(file).isFile());
143
160
  }
144
161
 
145
- async function processDirectory(env, user, dirPath, outPath, originalDir, createEmpty, isRoot = false, overwrite = false) {
162
+ async function processDirectory(env, user, dirPath, outPath, originalDir, createEmpty, isRoot = false, overwrite = false, output) {
146
163
  let filesInDir = getFilesInDirectory(dirPath);
147
164
  if (filesInDir.length > 0)
148
- await uploadFiles(env, user, filesInDir, isRoot ? outPath : path.join(outPath, path.basename(dirPath)), createEmpty, overwrite);
165
+ await uploadFiles(env, user, filesInDir, isRoot ? outPath : path.join(outPath, path.basename(dirPath)), createEmpty, overwrite, output);
149
166
 
150
167
  const subDirectories = fs.readdirSync(dirPath)
151
168
  .map(subDir => path.join(dirPath, subDir))
152
169
  .filter(subDir => fs.statSync(subDir).isDirectory());
153
170
  for (let subDir of subDirectories) {
154
- await processDirectory(env, user, subDir, isRoot ? outPath : path.join(outPath, path.basename(dirPath)), originalDir, createEmpty, false, overwrite);
171
+ await processDirectory(env, user, subDir, isRoot ? outPath : path.join(outPath, path.basename(dirPath)), originalDir, createEmpty, false, overwrite, output);
155
172
  }
156
173
  }
157
174
 
158
- function resolveTree(dirs, indentLevel, parentHasFiles) {
175
+ function resolveTree(dirs, indentLevel, parentHasFiles, output) {
159
176
  let end = `└──`
160
177
  let mid = `├──`
161
178
  for (let id = 0; id < dirs.length; id++) {
@@ -163,33 +180,33 @@ function resolveTree(dirs, indentLevel, parentHasFiles) {
163
180
  let indentPipe = true;
164
181
  if (dirs.length == 1) {
165
182
  if (parentHasFiles) {
166
- console.log(indentLevel + mid, dir.name)
183
+ output.log(indentLevel + mid, dir.name)
167
184
  } else {
168
- console.log(indentLevel + end, dir.name)
185
+ output.log(indentLevel + end, dir.name)
169
186
  indentPipe = false;
170
187
  }
171
188
  } else if (id != dirs.length - 1) {
172
- console.log(indentLevel + mid, dir.name)
189
+ output.log(indentLevel + mid, dir.name)
173
190
  } else {
174
191
  if (parentHasFiles) {
175
- console.log(indentLevel + mid, dir.name)
192
+ output.log(indentLevel + mid, dir.name)
176
193
  } else {
177
- console.log(indentLevel + end, dir.name)
194
+ output.log(indentLevel + end, dir.name)
178
195
  indentPipe = false;
179
196
  }
180
197
  }
181
198
  let hasFiles = dir.files?.data && dir.files?.data.length !== 0;
182
199
  if (indentPipe) {
183
- resolveTree(dir.directories ?? [], indentLevel + '│\t', hasFiles);
184
- resolveTree(dir.files?.data ?? [], indentLevel + '│\t', false);
200
+ resolveTree(dir.directories ?? [], indentLevel + '│\t', hasFiles, output);
201
+ resolveTree(dir.files?.data ?? [], indentLevel + '│\t', false, output);
185
202
  } else {
186
- resolveTree(dir.directories ?? [], indentLevel + '\t', hasFiles);
187
- resolveTree(dir.files?.data ?? [], indentLevel + '\t', false);
203
+ resolveTree(dir.directories ?? [], indentLevel + '\t', hasFiles, output);
204
+ resolveTree(dir.files?.data ?? [], indentLevel + '\t', false, output);
188
205
  }
189
206
  }
190
207
  }
191
208
 
192
- async function download(env, user, dirPath, outPath, recursive, outname, raw, iamstupid, fileNames, singleFileMode) {
209
+ async function download(env, user, dirPath, outPath, recursive, outname, raw, iamstupid, fileNames, singleFileMode, output) {
193
210
  let excludeDirectories = '';
194
211
  if (!iamstupid) {
195
212
  excludeDirectories = 'system/log';
@@ -200,7 +217,7 @@ async function download(env, user, dirPath, outPath, recursive, outname, raw, ia
200
217
 
201
218
  const { endpoint, data } = prepareDownloadCommandData(dirPath, excludeDirectories, fileNames, recursive, singleFileMode);
202
219
 
203
- displayDownloadMessage(dirPath, fileNames, recursive, singleFileMode);
220
+ displayDownloadMessage(dirPath, fileNames, recursive, singleFileMode, output);
204
221
 
205
222
  const res = await fetch(`${env.protocol}://${env.host}/Admin/Api/${endpoint}`, {
206
223
  method: 'POST',
@@ -212,27 +229,40 @@ async function download(env, user, dirPath, outPath, recursive, outname, raw, ia
212
229
  agent: getAgent(env.protocol)
213
230
  });
214
231
 
215
- const filename = outname || tryGetFileNameFromResponse(res, dirPath);
232
+ const filename = outname || tryGetFileNameFromResponse(res, dirPath, output.verbose);
216
233
  if (!filename) return;
217
234
 
218
235
  const filePath = path.resolve(`${path.resolve(outPath)}/${filename}`)
219
- const updater = createThrottledStatusUpdater();
236
+ const updater = output.json ? null : createThrottledStatusUpdater();
220
237
 
221
238
  await downloadWithProgress(res, filePath, {
222
239
  onData: (received) => {
223
- updater.update(`Received:\t${formatBytes(received)}`);
240
+ if (updater) {
241
+ updater.update(`Received:\t${formatBytes(received)}`);
242
+ }
224
243
  }
225
244
  });
226
245
 
227
- updater.stop();
246
+ if (updater) {
247
+ updater.stop();
248
+ }
228
249
 
229
250
  if (singleFileMode) {
230
- console.log(`Successfully downloaded: ${filename}`);
251
+ output.log(`Successfully downloaded: ${filename}`);
231
252
  } else {
232
- console.log(`Finished downloading`, dirPath === '/.' ? '.' : dirPath, 'Recursive=' + recursive);
253
+ output.log(`Finished downloading`, dirPath === '/.' ? '.' : dirPath, 'Recursive=' + recursive);
233
254
  }
234
255
 
235
- await extractArchive(filename, filePath, outPath, raw);
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);
236
266
  }
237
267
 
238
268
  function prepareDownloadCommandData(directoryPath, excludeDirectories, fileNames, recursive, singleFileMode) {
@@ -249,10 +279,10 @@ function prepareDownloadCommandData(directoryPath, excludeDirectories, fileNames
249
279
  return { endpoint: 'FileDownload', data };
250
280
  }
251
281
 
252
- function displayDownloadMessage(directoryPath, fileNames, recursive, singleFileMode) {
282
+ function displayDownloadMessage(directoryPath, fileNames, recursive, singleFileMode, output) {
253
283
  if (singleFileMode) {
254
284
  const fileName = path.basename(fileNames[0] || 'unknown');
255
- console.log('Downloading file: ' + fileName);
285
+ output.log('Downloading file: ' + fileName);
256
286
 
257
287
  return;
258
288
  }
@@ -261,30 +291,34 @@ function displayDownloadMessage(directoryPath, fileNames, recursive, singleFileM
261
291
  ? 'Base'
262
292
  : directoryPath;
263
293
 
264
- console.log('Downloading', directoryPathDisplayName, 'Recursive=' + recursive);
294
+ output.log('Downloading', directoryPathDisplayName, 'Recursive=' + recursive);
265
295
  }
266
296
 
267
- async function extractArchive(filename, filePath, outPath, raw) {
297
+ async function extractArchive(filename, filePath, outPath, raw, output) {
268
298
  if (raw) {
269
299
  return;
270
300
  }
271
301
 
272
- console.log(`\nExtracting ${filename} to ${outPath}`);
302
+ output.log(`\nExtracting ${filename} to ${outPath}`);
273
303
  let destinationFilename = filename.replace('.zip', '');
274
304
  if (destinationFilename === 'Base')
275
305
  destinationFilename = '';
276
306
 
277
307
  const destinationPath = `${path.resolve(outPath)}/${destinationFilename}`;
278
- const updater = createThrottledStatusUpdater();
308
+ const updater = output.json ? null : createThrottledStatusUpdater();
279
309
 
280
310
  await extractWithProgress(filePath, destinationPath, {
281
311
  onEntry: (processedEntries, totalEntries, percent) => {
282
- updater.update(`Extracted:\t${processedEntries} of ${totalEntries} files (${percent}%)`);
312
+ if (updater) {
313
+ updater.update(`Extracted:\t${processedEntries} of ${totalEntries} files (${percent}%)`);
314
+ }
283
315
  }
284
316
  });
285
317
 
286
- updater.stop();
287
- console.log(`Finished extracting ${filename} to ${outPath}\n`);
318
+ if (updater) {
319
+ updater.stop();
320
+ }
321
+ output.log(`Finished extracting ${filename} to ${outPath}\n`);
288
322
 
289
323
  fs.unlink(filePath, function(err) {});
290
324
  }
@@ -300,14 +334,12 @@ async function getFilesStructure(env, user, dirPath, recursive, includeFiles) {
300
334
  if (res.ok) {
301
335
  return await res.json();
302
336
  } else {
303
- console.log(res);
304
- console.log(await res.json());
305
- process.exit(1);
337
+ throw createCommandError('Unable to fetch file structure.', res.status, await parseJsonSafe(res));
306
338
  }
307
339
  }
308
340
 
309
- export async function uploadFiles(env, user, localFilePaths, destinationPath, createEmpty = false, overwrite = false) {
310
- console.log('Uploading files')
341
+ export async function uploadFiles(env, user, localFilePaths, destinationPath, createEmpty = false, overwrite = false, output) {
342
+ output.log('Uploading files')
311
343
 
312
344
  const chunkSize = 300;
313
345
  const chunks = [];
@@ -316,26 +348,37 @@ export async function uploadFiles(env, user, localFilePaths, destinationPath, cr
316
348
  chunks.push(localFilePaths.slice(i, i + chunkSize));
317
349
  }
318
350
 
351
+ output.mergeMeta({
352
+ filesProcessed: (output.response.meta.filesProcessed || 0) + localFilePaths.length,
353
+ chunks: (output.response.meta.chunks || 0) + chunks.length
354
+ });
355
+
319
356
  for (let i = 0; i < chunks.length; i++) {
320
- console.log(`Uploading chunk ${i + 1} of ${chunks.length}`);
357
+ output.log(`Uploading chunk ${i + 1} of ${chunks.length}`);
321
358
 
322
359
  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}`);
360
+ const body = await uploadChunk(env, user, chunk, destinationPath, createEmpty, overwrite, output);
361
+ output.addData({
362
+ type: 'upload',
363
+ destinationPath,
364
+ files: chunk.map(filePath => path.resolve(filePath)),
365
+ response: body
366
+ });
367
+
368
+ output.log(`Finished uploading chunk ${i + 1} of ${chunks.length}`);
326
369
  }
327
370
 
328
- console.log(`Finished uploading files. Total files: ${localFilePaths.length}, total chunks: ${chunks.length}`);
371
+ output.log(`Finished uploading files. Total files: ${localFilePaths.length}, total chunks: ${chunks.length}`);
329
372
  }
330
373
 
331
- async function uploadChunk(env, user, filePathsChunk, destinationPath, createEmpty, overwrite) {
374
+ async function uploadChunk(env, user, filePathsChunk, destinationPath, createEmpty, overwrite, output) {
332
375
  const form = new FormData();
333
376
  form.append('path', destinationPath);
334
377
  form.append('skipExistingFiles', String(!overwrite));
335
378
  form.append('allowOverwrite', String(overwrite));
336
379
 
337
380
  filePathsChunk.forEach(fileToUpload => {
338
- console.log(`${fileToUpload}`)
381
+ output.log(`${fileToUpload}`)
339
382
  form.append('files', fs.createReadStream(path.resolve(fileToUpload)));
340
383
  });
341
384
 
@@ -349,12 +392,10 @@ async function uploadChunk(env, user, filePathsChunk, destinationPath, createEmp
349
392
  });
350
393
 
351
394
  if (res.ok) {
352
- console.log(await res.json())
395
+ return await res.json();
353
396
  }
354
397
  else {
355
- console.log(res)
356
- console.log(await res.json())
357
- process.exit(1);
398
+ throw createCommandError('File upload failed.', res.status, await parseJsonSafe(res));
358
399
  }
359
400
  }
360
401
 
@@ -376,3 +417,86 @@ function wildcardToRegExp(wildcard) {
376
417
  return new RegExp('^' + escaped.replace(/\*/g, '.*') + '$');
377
418
  }
378
419
 
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
+ };
430
+
431
+ return {
432
+ json: Boolean(argv.json),
433
+ verbose: Boolean(argv.verbose),
434
+ response,
435
+ log(...args) {
436
+ if (!this.json) {
437
+ console.log(...args);
438
+ }
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
+ };
471
+ }
472
+
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) {
490
+ const error = new Error(message);
491
+ error.status = status;
492
+ error.details = details;
493
+ return error;
494
+ }
495
+
496
+ async function parseJsonSafe(res) {
497
+ try {
498
+ return await res.json();
499
+ } catch {
500
+ return null;
501
+ }
502
+ }
@@ -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']
6
+ const exclude = ['_', '$0', 'query', 'list', 'i', 'l', 'interactive', 'verbose', 'v', 'host', 'protocol', 'apiKey', 'env', 'output']
7
7
 
8
8
  export function queryCommand() {
9
9
  return {
@@ -22,30 +22,43 @@ 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
+ })
25
29
  },
26
- handler: (argv) => {
27
- if (argv.verbose) console.info(`Running query ${argv.query}`)
28
- handleQuery(argv)
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
+ }
29
42
  }
30
43
  }
31
44
  }
32
45
 
33
- async function handleQuery(argv) {
46
+ async function handleQuery(argv, output) {
34
47
  let env = await setupEnv(argv);
35
48
  let user = await setupUser(argv, env);
36
49
  if (argv.list) {
37
- console.log(await getProperties(argv))
50
+ const properties = await getProperties(env, user, argv.query);
51
+ output.addData(properties);
52
+ output.log(properties);
38
53
  } else {
39
- let response = await runQuery(env, user, argv.query, await getQueryParams(argv))
40
- console.log(response)
54
+ let response = await runQuery(env, user, argv.query, await getQueryParams(argv));
55
+ output.addData(response);
56
+ output.log(response);
41
57
  }
42
58
  }
43
59
 
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}`, {
60
+ async function getProperties(env, user, query) {
61
+ let res = await fetch(`${env.protocol}://${env.host}/Admin/Api/QueryByName?name=${query}`, {
49
62
  method: 'GET',
50
63
  headers: {
51
64
  'Authorization': `Bearer ${user.apiKey}`
@@ -55,20 +68,20 @@ async function getProperties(argv) {
55
68
  if (res.ok) {
56
69
  let body = await res.json()
57
70
  if (body.model.properties.groups === undefined) {
58
- console.log('Unable to fetch query parameters');
59
- process.exit(1);
71
+ throw createCommandError('Unable to fetch query parameters.', res.status, body);
60
72
  }
61
73
  return body.model.properties.groups.filter(g => g.name === 'Properties')[0].fields.map(field => `${field.name} (${field.typeName})`)
62
74
  }
63
- console.log('Unable to fetch query parameters');
64
- console.log(res);
65
- process.exit(1);
75
+
76
+ throw createCommandError('Unable to fetch query parameters.', res.status, await parseJsonSafe(res));
66
77
  }
67
78
 
68
79
  async function getQueryParams(argv) {
69
80
  let params = {}
70
81
  if (argv.interactive) {
71
- let properties = await getProperties(argv);
82
+ let env = await setupEnv(argv);
83
+ let user = await setupUser(argv, env);
84
+ let properties = await getProperties(env, user, argv.query);
72
85
  console.log('The following properties will be requested:')
73
86
  console.log(properties)
74
87
  for (const p of properties) {
@@ -93,8 +106,67 @@ async function runQuery(env, user, query, params) {
93
106
  agent: getAgent(env.protocol)
94
107
  })
95
108
  if (!res.ok) {
96
- console.log(`Error when doing request ${res.url}`)
97
- process.exit(1);
109
+ throw createCommandError(`Error when doing request ${res.url}`, res.status, await parseJsonSafe(res));
98
110
  }
99
111
  return await res.json()
100
- }
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
+ }
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.1",
6
6
  "main": "bin/index.js",
7
7
  "files": [
8
8
  "bin/**"
@@ -48,4 +48,4 @@
48
48
  "node-fetch": "^3.2.10",
49
49
  "yargs": "^17.5.1"
50
50
  }
51
- }
51
+ }