@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 +0 -12
- package/bin/commands/command.js +10 -85
- package/bin/commands/files.js +165 -237
- package/bin/commands/files.test.js +76 -0
- package/bin/commands/install.js +99 -13
- package/bin/commands/install.test.js +48 -0
- package/bin/commands/query.js +22 -94
- package/package.json +3 -3
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
|
|
package/bin/commands/command.js
CHANGED
|
@@ -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']
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
+
}
|
package/bin/commands/files.js
CHANGED
|
@@ -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
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
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,
|
|
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,
|
|
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
|
-
|
|
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,
|
|
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,
|
|
140
|
-
if (argv.raw)
|
|
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
|
|
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
|
|
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() ? [
|
|
158
|
-
|
|
159
|
-
|
|
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
|
-
|
|
169
|
-
|
|
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
|
|
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
|
-
|
|
166
|
+
console.log(indentLevel + mid, dir.name)
|
|
184
167
|
} else {
|
|
185
|
-
|
|
168
|
+
console.log(indentLevel + end, dir.name)
|
|
186
169
|
indentPipe = false;
|
|
187
170
|
}
|
|
188
171
|
} else if (id != dirs.length - 1) {
|
|
189
|
-
|
|
172
|
+
console.log(indentLevel + mid, dir.name)
|
|
190
173
|
} else {
|
|
191
174
|
if (parentHasFiles) {
|
|
192
|
-
|
|
175
|
+
console.log(indentLevel + mid, dir.name)
|
|
193
176
|
} else {
|
|
194
|
-
|
|
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
|
|
201
|
-
resolveTree(dir.files?.data ?? [], indentLevel + '│\t', false
|
|
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
|
|
204
|
-
resolveTree(dir.files?.data ?? [], indentLevel + '\t', false
|
|
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,
|
|
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
|
|
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,
|
|
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 =
|
|
219
|
+
const updater = createThrottledStatusUpdater();
|
|
237
220
|
|
|
238
221
|
await downloadWithProgress(res, filePath, {
|
|
239
222
|
onData: (received) => {
|
|
240
|
-
|
|
241
|
-
updater.update(`Received:\t${formatBytes(received)}`);
|
|
242
|
-
}
|
|
223
|
+
updater.update(`Received:\t${formatBytes(received)}`);
|
|
243
224
|
}
|
|
244
225
|
});
|
|
245
226
|
|
|
246
|
-
|
|
247
|
-
updater.stop();
|
|
248
|
-
}
|
|
227
|
+
updater.stop();
|
|
249
228
|
|
|
250
229
|
if (singleFileMode) {
|
|
251
|
-
|
|
230
|
+
console.log(`Successfully downloaded: ${filename}`);
|
|
252
231
|
} else {
|
|
253
|
-
|
|
232
|
+
console.log(`Finished downloading`, dirPath === '/.' ? '.' : dirPath, 'Recursive=' + recursive);
|
|
254
233
|
}
|
|
255
234
|
|
|
256
|
-
|
|
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
|
|
252
|
+
function displayDownloadMessage(directoryPath, fileNames, recursive, singleFileMode) {
|
|
283
253
|
if (singleFileMode) {
|
|
284
254
|
const fileName = path.basename(fileNames[0] || 'unknown');
|
|
285
|
-
|
|
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
|
-
|
|
264
|
+
console.log('Downloading', directoryPathDisplayName, 'Recursive=' + recursive);
|
|
295
265
|
}
|
|
296
266
|
|
|
297
|
-
async function extractArchive(filename, filePath, outPath, raw
|
|
267
|
+
async function extractArchive(filename, filePath, outPath, raw) {
|
|
298
268
|
if (raw) {
|
|
299
269
|
return;
|
|
300
270
|
}
|
|
301
271
|
|
|
302
|
-
|
|
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 =
|
|
278
|
+
const updater = createThrottledStatusUpdater();
|
|
309
279
|
|
|
310
280
|
await extractWithProgress(filePath, destinationPath, {
|
|
311
281
|
onEntry: (processedEntries, totalEntries, percent) => {
|
|
312
|
-
|
|
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
|
-
|
|
319
|
-
|
|
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
|
-
|
|
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
|
-
|
|
399
|
-
|
|
400
|
-
}
|
|
367
|
+
if (output.structured) {
|
|
368
|
+
throw createUploadError('File upload failed.', res.status, await parseJsonSafe(res));
|
|
369
|
+
}
|
|
401
370
|
|
|
402
|
-
|
|
403
|
-
|
|
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
|
|
421
|
-
const response = {
|
|
422
|
-
|
|
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
|
-
|
|
433
|
-
verbose: Boolean(argv.verbose),
|
|
382
|
+
structured: Boolean(output),
|
|
434
383
|
response,
|
|
435
|
-
log
|
|
436
|
-
|
|
437
|
-
|
|
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
|
|
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
|
+
});
|
package/bin/commands/install.js
CHANGED
|
@@ -18,24 +18,40 @@ export function installCommand() {
|
|
|
18
18
|
type: 'boolean',
|
|
19
19
|
describe: 'Queues the install for next Dynamicweb recycle'
|
|
20
20
|
})
|
|
21
|
+
.option('output', {
|
|
22
|
+
choices: ['json'],
|
|
23
|
+
describe: 'Outputs a single JSON response for automation-friendly parsing'
|
|
24
|
+
})
|
|
21
25
|
},
|
|
22
26
|
handler: async (argv) => {
|
|
23
|
-
|
|
24
|
-
|
|
27
|
+
const output = createInstallOutput(argv);
|
|
28
|
+
|
|
29
|
+
try {
|
|
30
|
+
output.verboseLog(`Installing file located at: ${argv.filePath}`);
|
|
31
|
+
await handleInstall(argv, output)
|
|
32
|
+
} catch (err) {
|
|
33
|
+
output.fail(err);
|
|
34
|
+
process.exitCode = 1;
|
|
35
|
+
} finally {
|
|
36
|
+
output.finish();
|
|
37
|
+
}
|
|
25
38
|
}
|
|
26
39
|
}
|
|
27
40
|
}
|
|
28
41
|
|
|
29
|
-
async function handleInstall(argv) {
|
|
42
|
+
async function handleInstall(argv, output) {
|
|
30
43
|
let env = await setupEnv(argv);
|
|
31
44
|
let user = await setupUser(argv, env);
|
|
32
45
|
let resolvedPath = resolveFilePath(argv.filePath);
|
|
33
|
-
|
|
34
|
-
|
|
46
|
+
output.mergeMeta({
|
|
47
|
+
resolvedPath
|
|
48
|
+
});
|
|
49
|
+
await uploadFiles(env, user, [ resolvedPath ], 'System/AddIns/Local', false, true, output);
|
|
50
|
+
await installAddin(env, user, resolvedPath, argv.queue, output)
|
|
35
51
|
}
|
|
36
52
|
|
|
37
|
-
async function installAddin(env, user, resolvedPath, queue) {
|
|
38
|
-
|
|
53
|
+
async function installAddin(env, user, resolvedPath, queue, output) {
|
|
54
|
+
output.log('Installing addin')
|
|
39
55
|
let filename = path.basename(resolvedPath);
|
|
40
56
|
let data = {
|
|
41
57
|
'Queue': queue,
|
|
@@ -54,12 +70,82 @@ async function installAddin(env, user, resolvedPath, queue) {
|
|
|
54
70
|
});
|
|
55
71
|
|
|
56
72
|
if (res.ok) {
|
|
57
|
-
|
|
58
|
-
|
|
73
|
+
const body = await res.json();
|
|
74
|
+
output.addData({
|
|
75
|
+
type: 'install',
|
|
76
|
+
filename,
|
|
77
|
+
queued: Boolean(queue),
|
|
78
|
+
response: body
|
|
79
|
+
});
|
|
80
|
+
output.log(`Addin installed`)
|
|
59
81
|
}
|
|
60
82
|
else {
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
83
|
+
throw createInstallError('Addin install failed.', res.status, await parseJsonSafe(res));
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
export function createInstallOutput(argv) {
|
|
88
|
+
const response = {
|
|
89
|
+
ok: true,
|
|
90
|
+
command: 'install',
|
|
91
|
+
operation: 'install',
|
|
92
|
+
status: 200,
|
|
93
|
+
data: [],
|
|
94
|
+
errors: [],
|
|
95
|
+
meta: {
|
|
96
|
+
queued: Boolean(argv.queue)
|
|
97
|
+
}
|
|
98
|
+
};
|
|
99
|
+
|
|
100
|
+
return {
|
|
101
|
+
json: argv.output === 'json',
|
|
102
|
+
response,
|
|
103
|
+
log(...args) {
|
|
104
|
+
if (!this.json) {
|
|
105
|
+
console.log(...args);
|
|
106
|
+
}
|
|
107
|
+
},
|
|
108
|
+
verboseLog(...args) {
|
|
109
|
+
if (argv.verbose && !this.json) {
|
|
110
|
+
console.info(...args);
|
|
111
|
+
}
|
|
112
|
+
},
|
|
113
|
+
addData(entry) {
|
|
114
|
+
response.data.push(entry);
|
|
115
|
+
},
|
|
116
|
+
mergeMeta(meta) {
|
|
117
|
+
response.meta = {
|
|
118
|
+
...response.meta,
|
|
119
|
+
...meta
|
|
120
|
+
};
|
|
121
|
+
},
|
|
122
|
+
fail(err) {
|
|
123
|
+
response.ok = false;
|
|
124
|
+
response.status = err?.status || 1;
|
|
125
|
+
response.errors.push({
|
|
126
|
+
message: err?.message || 'Unknown install command error.',
|
|
127
|
+
details: err?.details ?? null
|
|
128
|
+
});
|
|
129
|
+
},
|
|
130
|
+
finish() {
|
|
131
|
+
if (this.json) {
|
|
132
|
+
console.log(JSON.stringify(response, null, 2));
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
};
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
function createInstallError(message, status, details = null) {
|
|
139
|
+
const error = new Error(message);
|
|
140
|
+
error.status = status;
|
|
141
|
+
error.details = details;
|
|
142
|
+
return error;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
async function parseJsonSafe(res) {
|
|
146
|
+
try {
|
|
147
|
+
return await res.json();
|
|
148
|
+
} catch {
|
|
149
|
+
return null;
|
|
64
150
|
}
|
|
65
|
-
}
|
|
151
|
+
}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import test from 'node:test';
|
|
2
|
+
import assert from 'node:assert/strict';
|
|
3
|
+
|
|
4
|
+
import { createInstallOutput } from './install.js';
|
|
5
|
+
|
|
6
|
+
test('createInstallOutput suppresses regular logs in json mode and emits the final envelope', () => {
|
|
7
|
+
const logCalls = [];
|
|
8
|
+
const infoCalls = [];
|
|
9
|
+
const originalLog = console.log;
|
|
10
|
+
const originalInfo = console.info;
|
|
11
|
+
|
|
12
|
+
console.log = (...args) => logCalls.push(args);
|
|
13
|
+
console.info = (...args) => infoCalls.push(args);
|
|
14
|
+
|
|
15
|
+
try {
|
|
16
|
+
const output = createInstallOutput({
|
|
17
|
+
output: 'json',
|
|
18
|
+
queue: true,
|
|
19
|
+
verbose: true
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
output.log('hidden');
|
|
23
|
+
output.verboseLog('hidden verbose');
|
|
24
|
+
output.addData({ type: 'install', filename: 'addon.nupkg' });
|
|
25
|
+
output.mergeMeta({ resolvedPath: '/tmp/addon.nupkg' });
|
|
26
|
+
output.finish();
|
|
27
|
+
|
|
28
|
+
assert.deepEqual(infoCalls, []);
|
|
29
|
+
assert.equal(logCalls.length, 1);
|
|
30
|
+
|
|
31
|
+
const rendered = JSON.parse(logCalls[0][0]);
|
|
32
|
+
assert.deepEqual(rendered, {
|
|
33
|
+
ok: true,
|
|
34
|
+
command: 'install',
|
|
35
|
+
operation: 'install',
|
|
36
|
+
status: 200,
|
|
37
|
+
data: [{ type: 'install', filename: 'addon.nupkg' }],
|
|
38
|
+
errors: [],
|
|
39
|
+
meta: {
|
|
40
|
+
queued: true,
|
|
41
|
+
resolvedPath: '/tmp/addon.nupkg'
|
|
42
|
+
}
|
|
43
|
+
});
|
|
44
|
+
} finally {
|
|
45
|
+
console.log = originalLog;
|
|
46
|
+
console.info = originalInfo;
|
|
47
|
+
}
|
|
48
|
+
});
|
package/bin/commands/query.js
CHANGED
|
@@ -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']
|
|
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:
|
|
31
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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(
|
|
61
|
-
let
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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.
|
|
5
|
+
"version": "1.1.2",
|
|
6
6
|
"main": "bin/index.js",
|
|
7
7
|
"files": [
|
|
8
8
|
"bin/**"
|
|
@@ -15,7 +15,7 @@
|
|
|
15
15
|
"devops"
|
|
16
16
|
],
|
|
17
17
|
"scripts": {
|
|
18
|
-
"test": "
|
|
18
|
+
"test": "node --test"
|
|
19
19
|
},
|
|
20
20
|
"author": "Dynamicweb A/S (https://www.dynamicweb.com)",
|
|
21
21
|
"repository": {
|
|
@@ -48,4 +48,4 @@
|
|
|
48
48
|
"node-fetch": "^3.2.10",
|
|
49
49
|
"yargs": "^17.5.1"
|
|
50
50
|
}
|
|
51
|
-
}
|
|
51
|
+
}
|