@dynamicweb/cli 1.1.1 → 2.0.0-beta.0

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.
@@ -2,7 +2,7 @@ import fetch from 'node-fetch';
2
2
  import path from 'path';
3
3
  import fs from 'fs';
4
4
  import FormData from 'form-data';
5
- import { setupEnv, getAgent } from './env.js';
5
+ import { setupEnv, getAgent, createCommandError } from './env.js';
6
6
  import { setupUser } from './login.js';
7
7
  import { interactiveConfirm, formatBytes, createThrottledStatusUpdater } from '../utils.js';
8
8
  import { downloadWithProgress, tryGetFileNameFromResponse } from '../downloader.js';
@@ -59,9 +59,31 @@ export function filesCommand() {
59
59
  type: 'boolean',
60
60
  describe: 'Used with export, keeps zip file instead of unpacking it'
61
61
  })
62
+ .option('dangerouslyIncludeLogsAndCache', {
63
+ type: 'boolean',
64
+ describe: 'Includes log and cache folders during export. Risky and usually not recommended'
65
+ })
62
66
  .option('iamstupid', {
63
67
  type: 'boolean',
64
- describe: 'Includes export of log and cache folders, NOT RECOMMENDED'
68
+ hidden: true,
69
+ describe: 'Deprecated alias for --dangerouslyIncludeLogsAndCache'
70
+ })
71
+ .option('delete', {
72
+ alias: 'd',
73
+ type: 'boolean',
74
+ describe: 'Deletes the file or directory at [dirPath]. Detects type from path (use --asFile/--asDirectory to override)'
75
+ })
76
+ .option('empty', {
77
+ type: 'boolean',
78
+ describe: 'Used with --delete, empties a directory instead of deleting it'
79
+ })
80
+ .option('copy', {
81
+ type: 'string',
82
+ describe: 'Copies the file or directory at [dirPath] to the given destination path'
83
+ })
84
+ .option('move', {
85
+ type: 'string',
86
+ describe: 'Moves the file or directory at [dirPath] to the given destination path'
65
87
  })
66
88
  .option('asFile', {
67
89
  type: 'boolean',
@@ -75,32 +97,45 @@ export function filesCommand() {
75
97
  describe: 'Forces the command to treat the path as a directory, even if its name contains a dot.',
76
98
  conflicts: 'asFile'
77
99
  })
100
+ .option('output', {
101
+ choices: ['json'],
102
+ describe: 'Outputs a single JSON response for automation-friendly parsing'
103
+ })
78
104
  .option('json', {
79
105
  type: 'boolean',
80
- describe: 'Outputs a single JSON response for automation-friendly parsing'
106
+ hidden: true,
107
+ describe: 'Deprecated alias for --output json'
81
108
  })
82
109
  },
83
110
  handler: async (argv) => {
111
+ if (argv.json && !argv.output) {
112
+ argv.output = 'json';
113
+ console.warn('Warning: --json is deprecated and will be removed in a future release. Use --output json instead.');
114
+ }
115
+ if (argv.iamstupid && !argv.dangerouslyIncludeLogsAndCache) {
116
+ argv.dangerouslyIncludeLogsAndCache = true;
117
+ console.warn('Warning: --iamstupid is deprecated and will be removed in a future release. Use --dangerouslyIncludeLogsAndCache instead.');
118
+ }
84
119
  const output = createFilesOutput(argv);
85
120
 
86
121
  try {
87
- output.verboseLog(`Listing directory at: ${argv.dirPath}`);
88
122
  await handleFiles(argv, output);
89
- output.finish();
90
123
  } catch (err) {
91
124
  output.fail(err);
125
+ process.exitCode = 1;
126
+ } finally {
92
127
  output.finish();
93
- process.exit(1);
94
128
  }
95
129
  }
96
130
  }
97
131
  }
98
132
 
99
133
  async function handleFiles(argv, output) {
100
- let env = await setupEnv(argv);
134
+ let env = await setupEnv(argv, output);
101
135
  let user = await setupUser(argv, env);
102
136
 
103
137
  if (argv.list) {
138
+ output.verboseLog(`Listing directory at: ${argv.dirPath}`);
104
139
  let files = (await getFilesStructure(env, user, argv.dirPath, argv.recursive, argv.includeFiles)).model;
105
140
  output.setStatus(200);
106
141
  output.addData(files);
@@ -115,30 +150,34 @@ async function handleFiles(argv, output) {
115
150
  if (argv.export) {
116
151
  if (argv.dirPath) {
117
152
 
118
- const isFile = argv.asFile || argv.asDirectory
119
- ? argv.asFile
120
- : path.extname(argv.dirPath) !== '';
153
+ const isFile = isFilePath(argv, argv.dirPath);
121
154
 
122
155
  if (isFile) {
123
156
  let parentDirectory = path.dirname(argv.dirPath);
124
157
  parentDirectory = parentDirectory === '.' ? '/' : parentDirectory;
125
158
 
126
- await download(env, user, parentDirectory, argv.outPath, false, null, true, argv.iamstupid, [argv.dirPath], true, output);
159
+ await download(env, user, parentDirectory, argv.outPath, false, null, true, argv.dangerouslyIncludeLogsAndCache, [argv.dirPath], true, output);
127
160
  } else {
128
- await download(env, user, argv.dirPath, argv.outPath, true, null, argv.raw, argv.iamstupid, [], false, output);
161
+ await download(env, user, argv.dirPath, argv.outPath, true, null, argv.raw, argv.dangerouslyIncludeLogsAndCache, [], false, output);
129
162
  }
130
163
  } else {
131
- await interactiveConfirm('Are you sure you want a full export of files?', async () => {
164
+ const fullExport = async () => {
132
165
  output.log('Full export is starting');
133
166
  let filesStructure = (await getFilesStructure(env, user, '/', false, true)).model;
134
167
  let dirs = filesStructure.directories;
135
168
  for (let id = 0; id < dirs.length; id++) {
136
169
  const dir = dirs[id];
137
- await download(env, user, dir.name, argv.outPath, true, null, argv.raw, argv.iamstupid, [], false, output);
170
+ await download(env, user, dir.name, argv.outPath, true, null, argv.raw, argv.dangerouslyIncludeLogsAndCache, [], false, output);
138
171
  }
139
- await download(env, user, '/.', argv.outPath, false, 'Base.zip', argv.raw, argv.iamstupid, Array.from(filesStructure.files.data, f => f.name), false, output);
172
+ await download(env, user, '/.', argv.outPath, false, 'Base.zip', argv.raw, argv.dangerouslyIncludeLogsAndCache, Array.from(filesStructure.files.data, f => f.name), false, output);
140
173
  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');
141
- })
174
+ };
175
+
176
+ if (output.json) {
177
+ await fullExport();
178
+ } else {
179
+ await interactiveConfirm('Are you sure you want a full export of files?', fullExport);
180
+ }
142
181
  }
143
182
  } else if (argv.import) {
144
183
  if (argv.dirPath && argv.outPath) {
@@ -150,6 +189,44 @@ async function handleFiles(argv, output) {
150
189
  await uploadFiles(env, user, filesInDir, argv.outPath, argv.createEmpty, argv.overwrite, output);
151
190
  }
152
191
  }
192
+ } else if (argv.delete) {
193
+ if (!argv.dirPath) {
194
+ throw createCommandError('A path is required for delete operations.', 400);
195
+ }
196
+
197
+ const isFile = isFilePath(argv, argv.dirPath);
198
+
199
+ if (argv.empty && isFile) {
200
+ throw createCommandError('--empty can only be used with directories.', 400);
201
+ }
202
+
203
+ const shouldConfirm = !output.json;
204
+
205
+ if (shouldConfirm) {
206
+ const action = argv.empty
207
+ ? `empty directory "${argv.dirPath}"`
208
+ : isFile
209
+ ? `delete file "${argv.dirPath}"`
210
+ : `delete directory "${argv.dirPath}"`;
211
+
212
+ await interactiveConfirm(`Are you sure you want to ${action}?`, async () => {
213
+ await deleteRemote(env, user, argv.dirPath, isFile, argv.empty, output);
214
+ });
215
+ } else {
216
+ await deleteRemote(env, user, argv.dirPath, isFile, argv.empty, output);
217
+ }
218
+ } else if (argv.copy) {
219
+ if (!argv.dirPath) {
220
+ throw createCommandError('A source path [dirPath] is required for copy operations.', 400);
221
+ }
222
+
223
+ await copyRemote(env, user, argv.dirPath, argv.copy, output);
224
+ } else if (argv.move) {
225
+ if (!argv.dirPath) {
226
+ throw createCommandError('A source path [dirPath] is required for move operations.', 400);
227
+ }
228
+
229
+ await moveRemote(env, user, argv.dirPath, argv.move, argv.overwrite, output);
153
230
  }
154
231
  }
155
232
 
@@ -206,9 +283,9 @@ function resolveTree(dirs, indentLevel, parentHasFiles, output) {
206
283
  }
207
284
  }
208
285
 
209
- async function download(env, user, dirPath, outPath, recursive, outname, raw, iamstupid, fileNames, singleFileMode, output) {
286
+ async function download(env, user, dirPath, outPath, recursive, outname, raw, dangerouslyIncludeLogsAndCache, fileNames, singleFileMode, output) {
210
287
  let excludeDirectories = '';
211
- if (!iamstupid) {
288
+ if (!dangerouslyIncludeLogsAndCache) {
212
289
  excludeDirectories = 'system/log';
213
290
  if (dirPath === 'cache.net') {
214
291
  return;
@@ -320,7 +397,11 @@ async function extractArchive(filename, filePath, outPath, raw, output) {
320
397
  }
321
398
  output.log(`Finished extracting ${filename} to ${outPath}\n`);
322
399
 
323
- fs.unlink(filePath, function(err) {});
400
+ fs.unlink(filePath, function(err) {
401
+ if (err) {
402
+ output.verboseLog(`Warning: Failed to delete temporary archive ${filePath}: ${err.message}`);
403
+ }
404
+ });
324
405
  }
325
406
 
326
407
  async function getFilesStructure(env, user, dirPath, recursive, includeFiles) {
@@ -338,7 +419,127 @@ async function getFilesStructure(env, user, dirPath, recursive, includeFiles) {
338
419
  }
339
420
  }
340
421
 
341
- export async function uploadFiles(env, user, localFilePaths, destinationPath, createEmpty = false, overwrite = false, output) {
422
+ async function deleteRemote(env, user, remotePath, isFile, empty, output) {
423
+ let endpoint;
424
+ let mode;
425
+ let data;
426
+
427
+ if (isFile) {
428
+ endpoint = 'FileDelete';
429
+ mode = 'file';
430
+ const parentDir = path.posix.dirname(remotePath);
431
+ data = {
432
+ DirectoryPath: parentDir === '.' ? '/' : parentDir,
433
+ Ids: [remotePath]
434
+ };
435
+ } else if (empty) {
436
+ endpoint = 'DirectoryEmpty';
437
+ mode = 'empty';
438
+ data = { Path: remotePath };
439
+ } else {
440
+ endpoint = 'DirectoryDelete';
441
+ mode = 'directory';
442
+ data = { Path: remotePath };
443
+ }
444
+
445
+ output.log(`${mode === 'empty' ? 'Emptying' : 'Deleting'} ${mode === 'file' ? 'file' : 'directory'}: ${remotePath}`);
446
+
447
+ const res = await fetch(`${env.protocol}://${env.host}/Admin/Api/${endpoint}`, {
448
+ method: 'POST',
449
+ body: JSON.stringify(data),
450
+ headers: {
451
+ 'Authorization': `Bearer ${user.apiKey}`,
452
+ 'Content-Type': 'application/json'
453
+ },
454
+ agent: getAgent(env.protocol)
455
+ });
456
+
457
+ if (!res.ok) {
458
+ throw createCommandError(`Failed to ${mode === 'empty' ? 'empty' : 'delete'} "${remotePath}".`, res.status, await parseJsonSafe(res));
459
+ }
460
+
461
+ const body = await parseJsonSafe(res);
462
+
463
+ output.setStatus(200);
464
+ output.addData({
465
+ type: 'delete',
466
+ path: remotePath,
467
+ mode,
468
+ response: body
469
+ });
470
+
471
+ output.log(`Successfully ${mode === 'empty' ? 'emptied' : 'deleted'}: ${remotePath}`);
472
+ }
473
+
474
+ async function copyRemote(env, user, sourcePath, destination, output) {
475
+ output.log(`Copying ${sourcePath} to ${destination}`);
476
+
477
+ const res = await fetch(`${env.protocol}://${env.host}/Admin/Api/AssetCopy`, {
478
+ method: 'POST',
479
+ body: JSON.stringify({
480
+ Destination: destination,
481
+ Ids: [sourcePath]
482
+ }),
483
+ headers: {
484
+ 'Authorization': `Bearer ${user.apiKey}`,
485
+ 'Content-Type': 'application/json'
486
+ },
487
+ agent: getAgent(env.protocol)
488
+ });
489
+
490
+ if (!res.ok) {
491
+ throw createCommandError(`Failed to copy "${sourcePath}" to "${destination}".`, res.status, await parseJsonSafe(res));
492
+ }
493
+
494
+ const body = await parseJsonSafe(res);
495
+
496
+ output.setStatus(200);
497
+ output.addData({
498
+ type: 'copy',
499
+ sourcePath,
500
+ destination,
501
+ response: body
502
+ });
503
+
504
+ output.log(`Successfully copied ${sourcePath} to ${destination}`);
505
+ }
506
+
507
+ async function moveRemote(env, user, sourcePath, destination, overwrite, output) {
508
+ output.log(`Moving ${sourcePath} to ${destination}`);
509
+
510
+ const res = await fetch(`${env.protocol}://${env.host}/Admin/Api/AssetMove`, {
511
+ method: 'POST',
512
+ body: JSON.stringify({
513
+ Destination: destination,
514
+ Overwrite: Boolean(overwrite),
515
+ Ids: [sourcePath]
516
+ }),
517
+ headers: {
518
+ 'Authorization': `Bearer ${user.apiKey}`,
519
+ 'Content-Type': 'application/json'
520
+ },
521
+ agent: getAgent(env.protocol)
522
+ });
523
+
524
+ if (!res.ok) {
525
+ throw createCommandError(`Failed to move "${sourcePath}" to "${destination}".`, res.status, await parseJsonSafe(res));
526
+ }
527
+
528
+ const body = await parseJsonSafe(res);
529
+
530
+ output.setStatus(200);
531
+ output.addData({
532
+ type: 'move',
533
+ sourcePath,
534
+ destination,
535
+ overwrite: Boolean(overwrite),
536
+ response: body
537
+ });
538
+
539
+ output.log(`Successfully moved ${sourcePath} to ${destination}`);
540
+ }
541
+
542
+ export async function uploadFiles(env, user, localFilePaths, destinationPath, createEmpty = false, overwrite = false, output = createFilesOutput({})) {
342
543
  output.log('Uploading files')
343
544
 
344
545
  const chunkSize = 300;
@@ -348,10 +549,10 @@ export async function uploadFiles(env, user, localFilePaths, destinationPath, cr
348
549
  chunks.push(localFilePaths.slice(i, i + chunkSize));
349
550
  }
350
551
 
351
- output.mergeMeta({
352
- filesProcessed: (output.response.meta.filesProcessed || 0) + localFilePaths.length,
353
- chunks: (output.response.meta.chunks || 0) + chunks.length
354
- });
552
+ output.mergeMeta((meta) => ({
553
+ filesProcessed: (meta.filesProcessed || 0) + localFilePaths.length,
554
+ chunks: (meta.chunks || 0) + chunks.length
555
+ }));
355
556
 
356
557
  for (let i = 0; i < chunks.length; i++) {
357
558
  output.log(`Uploading chunk ${i + 1} of ${chunks.length}`);
@@ -405,13 +606,19 @@ export function resolveFilePath(filePath) {
405
606
  let resolvedPath = fs.readdirSync(p.dir).filter((allFilesPaths) => allFilesPaths.match(regex) !== null)[0]
406
607
  if (resolvedPath === undefined)
407
608
  {
408
- console.log('Could not find any files with the name ' + filePath);
409
- process.exit(1);
609
+ throw createCommandError(`Could not find any files with the name ${filePath}`, 1);
410
610
  }
411
611
  return path.join(p.dir, resolvedPath);
412
612
  }
413
613
 
414
614
 
615
+ function isFilePath(argv, dirPath) {
616
+ if (argv.asFile || argv.asDirectory) {
617
+ return argv.asFile;
618
+ }
619
+ return path.extname(dirPath) !== '';
620
+ }
621
+
415
622
  function wildcardToRegExp(wildcard) {
416
623
  const escaped = wildcard.replace(/[.+?^${}()|[\]\\]/g, '\\$&');
417
624
  return new RegExp('^' + escaped.replace(/\*/g, '.*') + '$');
@@ -429,7 +636,7 @@ function createFilesOutput(argv) {
429
636
  };
430
637
 
431
638
  return {
432
- json: Boolean(argv.json),
639
+ json: argv.output === 'json' || Boolean(argv.json),
433
640
  verbose: Boolean(argv.verbose),
434
641
  response,
435
642
  log(...args) {
@@ -445,7 +652,8 @@ function createFilesOutput(argv) {
445
652
  addData(entry) {
446
653
  response.data.push(entry);
447
654
  },
448
- mergeMeta(meta) {
655
+ mergeMeta(metaOrFn) {
656
+ const meta = typeof metaOrFn === 'function' ? metaOrFn(response.meta) : metaOrFn;
449
657
  response.meta = {
450
658
  ...response.meta,
451
659
  ...meta
@@ -483,15 +691,21 @@ function getFilesOperation(argv) {
483
691
  return 'import';
484
692
  }
485
693
 
694
+ if (argv.delete) {
695
+ return 'delete';
696
+ }
697
+
698
+ if (argv.copy) {
699
+ return 'copy';
700
+ }
701
+
702
+ if (argv.move) {
703
+ return 'move';
704
+ }
705
+
486
706
  return 'unknown';
487
707
  }
488
708
 
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
709
 
496
710
  async function parseJsonSafe(res) {
497
711
  try {
@@ -1,13 +1,13 @@
1
1
  import fetch from 'node-fetch';
2
2
  import path from 'path';
3
- import { setupEnv, getAgent } from './env.js';
3
+ import { setupEnv, getAgent, createCommandError } from './env.js';
4
4
  import { setupUser } from './login.js';
5
5
  import { uploadFiles, resolveFilePath } from './files.js';
6
6
 
7
7
  export function installCommand() {
8
8
  return {
9
- command: 'install [filePath]',
10
- describe: 'Installs the addin on the given path, allowed file extensions are .dll, .nupkg',
9
+ command: 'install [filePath]',
10
+ describe: 'Installs the addin on the given path, allowed file extensions are .dll, .nupkg',
11
11
  builder: (yargs) => {
12
12
  return yargs
13
13
  .positional('filePath', {
@@ -18,24 +18,37 @@ 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) {
30
- let env = await setupEnv(argv);
42
+ async function handleInstall(argv, output) {
43
+ let env = await setupEnv(argv, output);
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
+ await uploadFiles(env, user, [ resolvedPath ], 'System/AddIns/Local', false, true, output);
47
+ await installAddin(env, user, resolvedPath, argv.queue, output);
35
48
  }
36
49
 
37
- async function installAddin(env, user, resolvedPath, queue) {
38
- console.log('Installing addin')
50
+ async function installAddin(env, user, resolvedPath, queue, output) {
51
+ output.log('Installing addin');
39
52
  let filename = path.basename(resolvedPath);
40
53
  let data = {
41
54
  'Queue': queue,
@@ -54,12 +67,79 @@ async function installAddin(env, user, resolvedPath, queue) {
54
67
  });
55
68
 
56
69
  if (res.ok) {
57
- if (env.verbose) console.log(await res.json())
58
- console.log(`Addin installed`)
70
+ const body = await parseJsonSafe(res);
71
+ output.verboseLog(body);
72
+ output.addData({
73
+ type: 'install',
74
+ filePath: resolvedPath,
75
+ filename,
76
+ queued: Boolean(queue),
77
+ response: body
78
+ });
79
+ output.log('Addin installed');
80
+ } else {
81
+ const body = await parseJsonSafe(res);
82
+ throw createCommandError('Addin installation failed.', res.status, body);
59
83
  }
60
- else {
61
- console.log('Request failed, returned error:')
62
- console.log(await res.json())
63
- process.exit(1);
84
+ }
85
+
86
+ function createInstallOutput(argv) {
87
+ const response = {
88
+ ok: true,
89
+ command: 'install',
90
+ operation: argv.queue ? 'queue' : 'install',
91
+ status: 200,
92
+ data: [],
93
+ errors: [],
94
+ meta: {
95
+ filePath: argv.filePath
96
+ }
97
+ };
98
+
99
+ return {
100
+ json: argv.output === 'json',
101
+ response,
102
+ log(...args) {
103
+ if (!this.json) {
104
+ console.log(...args);
105
+ }
106
+ },
107
+ verboseLog(...args) {
108
+ if (argv.verbose && !this.json) {
109
+ console.info(...args);
110
+ }
111
+ },
112
+ addData(entry) {
113
+ response.data.push(entry);
114
+ },
115
+ mergeMeta(metaOrFn) {
116
+ const meta = typeof metaOrFn === 'function' ? metaOrFn(response.meta) : metaOrFn;
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
+
139
+ async function parseJsonSafe(res) {
140
+ try {
141
+ return await res.json();
142
+ } catch {
143
+ return null;
64
144
  }
65
- }
145
+ }