@dynamicweb/cli 2.0.0-beta.0 → 2.0.0-beta.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 +10 -1
- package/bin/commands/command.js +24 -21
- package/bin/commands/config.js +14 -2
- package/bin/commands/env.js +46 -20
- package/bin/commands/files.js +114 -117
- package/bin/commands/install.js +37 -23
- package/bin/commands/login.js +3 -3
- package/bin/commands/query.js +36 -12
- package/bin/index.js +6 -2
- package/package.json +3 -2
package/README.md
CHANGED
|
@@ -440,7 +440,7 @@ Download the latest Swift release, a specific tag, or the nightly build.
|
|
|
440
440
|
|
|
441
441
|
```sh
|
|
442
442
|
dw swift -l
|
|
443
|
-
dw swift . --tag
|
|
443
|
+
dw swift . --tag v2.3.0 --force
|
|
444
444
|
dw swift . --nightly --force
|
|
445
445
|
```
|
|
446
446
|
|
|
@@ -465,6 +465,15 @@ dw query HealthCheck \
|
|
|
465
465
|
|
|
466
466
|
For longer-lived runners, you can configure a saved environment once with `dw login --oauth`. Full CI/CD guidance will be expanded in the documentation.
|
|
467
467
|
|
|
468
|
+
## QA Smoke Testing
|
|
469
|
+
|
|
470
|
+
This repository now includes a reusable QA harness in [qa/README.md](qa/README.md). It runs the CLI against a real DynamicWeb solution using OAuth client credentials, keeps its own isolated `HOME`, and can exercise both:
|
|
471
|
+
|
|
472
|
+
- saved-environment developer flow
|
|
473
|
+
- ephemeral CI/CD-style flow with `--host --auth oauth`
|
|
474
|
+
|
|
475
|
+
The harness currently excludes `database` and `swift`.
|
|
476
|
+
|
|
468
477
|
## Using Git Bash
|
|
469
478
|
|
|
470
479
|
Git Bash can rewrite relative paths in a way that interferes with CLI file operations. If you see the path-conversion warning, disable it for the session before running file commands:
|
package/bin/commands/command.js
CHANGED
|
@@ -8,24 +8,24 @@ const exclude = ['_', '$0', 'command', 'list', 'json', 'verbose', 'v', 'host', '
|
|
|
8
8
|
|
|
9
9
|
export function commandCommand() {
|
|
10
10
|
return {
|
|
11
|
-
command: 'command [command]',
|
|
12
|
-
describe: 'Runs the given command',
|
|
11
|
+
command: 'command [command]',
|
|
12
|
+
describe: 'Runs the given command',
|
|
13
13
|
builder: (yargs) => {
|
|
14
14
|
return yargs
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
15
|
+
.positional('command', {
|
|
16
|
+
describe: 'The command to execute'
|
|
17
|
+
})
|
|
18
|
+
.option('json', {
|
|
19
|
+
describe: 'Literal json or location of json file to send'
|
|
20
|
+
})
|
|
21
|
+
.option('list', {
|
|
22
|
+
alias: 'l',
|
|
23
|
+
describe: 'Lists all the properties for the command, currently not working'
|
|
24
|
+
})
|
|
25
|
+
.option('output', {
|
|
26
|
+
choices: ['json'],
|
|
27
|
+
describe: 'Outputs a single JSON response for automation-friendly parsing'
|
|
28
|
+
})
|
|
29
29
|
},
|
|
30
30
|
handler: async (argv) => {
|
|
31
31
|
const output = createCommandOutput(argv);
|
|
@@ -61,19 +61,22 @@ async function getProperties(env, user, command) {
|
|
|
61
61
|
throw createCommandError('The --list option is not currently implemented for commands.');
|
|
62
62
|
}
|
|
63
63
|
|
|
64
|
-
function getQueryParams(argv) {
|
|
64
|
+
export function getQueryParams(argv) {
|
|
65
65
|
let params = {}
|
|
66
66
|
Object.keys(argv).filter(k => !exclude.includes(k)).forEach(k => params['Command.' + k] = argv[k])
|
|
67
67
|
return params
|
|
68
68
|
}
|
|
69
69
|
|
|
70
|
-
function parseJsonOrPath(json) {
|
|
70
|
+
export function parseJsonOrPath(json) {
|
|
71
71
|
if (!json) return
|
|
72
|
+
const trimmed = json.trim();
|
|
73
|
+
if (trimmed.startsWith('{') || trimmed.startsWith('[')) {
|
|
74
|
+
return JSON.parse(trimmed);
|
|
75
|
+
}
|
|
72
76
|
if (fs.existsSync(json)) {
|
|
73
|
-
return JSON.parse(fs.readFileSync(path.resolve(json)))
|
|
74
|
-
} else {
|
|
75
|
-
return JSON.parse(json)
|
|
77
|
+
return JSON.parse(fs.readFileSync(path.resolve(json)));
|
|
76
78
|
}
|
|
79
|
+
return JSON.parse(json);
|
|
77
80
|
}
|
|
78
81
|
|
|
79
82
|
async function runCommand(env, user, command, queryParams, data) {
|
package/bin/commands/config.js
CHANGED
|
@@ -27,10 +27,22 @@ export function setupConfig() {
|
|
|
27
27
|
}
|
|
28
28
|
|
|
29
29
|
export function getConfig() {
|
|
30
|
-
return localConfig;
|
|
30
|
+
return localConfig || {};
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Overrides the in-memory config for testing.
|
|
35
|
+
* @param {Object} config - Must be a plain, non-null object; handleConfig writes keys directly onto it.
|
|
36
|
+
*/
|
|
37
|
+
export function setConfigForTests(config) {
|
|
38
|
+
if (config === null || typeof config !== 'object') {
|
|
39
|
+
throw new Error('setConfigForTests: config must be a plain object');
|
|
40
|
+
}
|
|
41
|
+
localConfig = config;
|
|
31
42
|
}
|
|
32
43
|
|
|
33
44
|
export function handleConfig(argv) {
|
|
45
|
+
localConfig = localConfig || {};
|
|
34
46
|
Object.keys(argv).forEach(a => {
|
|
35
47
|
if (a != '_' && a != '$0') {
|
|
36
48
|
localConfig[a] = resolveConfig(a, argv[a], localConfig[a] || {});
|
|
@@ -52,4 +64,4 @@ function resolveConfig(key, obj, conf) {
|
|
|
52
64
|
conf[a] = resolveConfig(key, obj[a], conf[a]);
|
|
53
65
|
})
|
|
54
66
|
return conf;
|
|
55
|
-
}
|
|
67
|
+
}
|
package/bin/commands/env.js
CHANGED
|
@@ -22,6 +22,30 @@ export function getAgent(protocol) {
|
|
|
22
22
|
return protocol === 'http' ? httpAgent : httpsAgent;
|
|
23
23
|
}
|
|
24
24
|
|
|
25
|
+
export function parseHostInput(hostValue) {
|
|
26
|
+
if (!hostValue || typeof hostValue !== 'string' || !hostValue.trim()) {
|
|
27
|
+
throw createCommandError(`Invalid host value: ${hostValue}`);
|
|
28
|
+
}
|
|
29
|
+
hostValue = hostValue.trim();
|
|
30
|
+
const hostSplit = hostValue.split('://');
|
|
31
|
+
|
|
32
|
+
if (hostSplit.length === 1) {
|
|
33
|
+
return {
|
|
34
|
+
protocol: 'https',
|
|
35
|
+
host: hostSplit[0]
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
if (hostSplit.length === 2) {
|
|
40
|
+
return {
|
|
41
|
+
protocol: hostSplit[0],
|
|
42
|
+
host: hostSplit[1]
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
throw createCommandError(`Issues resolving host ${hostValue}`);
|
|
47
|
+
}
|
|
48
|
+
|
|
25
49
|
export function envCommand() {
|
|
26
50
|
return {
|
|
27
51
|
command: 'env [env]',
|
|
@@ -61,7 +85,9 @@ export function envCommand() {
|
|
|
61
85
|
}
|
|
62
86
|
}
|
|
63
87
|
|
|
64
|
-
export async function setupEnv(argv, output = null) {
|
|
88
|
+
export async function setupEnv(argv, output = null, deps = {}) {
|
|
89
|
+
const interactiveEnvFn = deps.interactiveEnvFn || interactiveEnv;
|
|
90
|
+
const cfg = getConfig();
|
|
65
91
|
let env = {};
|
|
66
92
|
let askEnv = true;
|
|
67
93
|
|
|
@@ -75,8 +101,8 @@ export async function setupEnv(argv, output = null) {
|
|
|
75
101
|
}
|
|
76
102
|
}
|
|
77
103
|
|
|
78
|
-
if (askEnv &&
|
|
79
|
-
env =
|
|
104
|
+
if (askEnv && cfg.env) {
|
|
105
|
+
env = cfg.env[argv.env] || cfg.env[cfg?.current?.env];
|
|
80
106
|
if (env && !env.protocol) {
|
|
81
107
|
logMessage(argv, 'Protocol for environment not set, defaulting to https');
|
|
82
108
|
env.protocol = 'https';
|
|
@@ -88,7 +114,7 @@ export async function setupEnv(argv, output = null) {
|
|
|
88
114
|
}
|
|
89
115
|
|
|
90
116
|
logMessage(argv, 'Current environment not set, please set it');
|
|
91
|
-
await
|
|
117
|
+
await interactiveEnvFn(argv, {
|
|
92
118
|
environment: {
|
|
93
119
|
type: 'input'
|
|
94
120
|
},
|
|
@@ -96,7 +122,8 @@ export async function setupEnv(argv, output = null) {
|
|
|
96
122
|
default: true
|
|
97
123
|
}
|
|
98
124
|
}, output)
|
|
99
|
-
|
|
125
|
+
const updatedConfig = getConfig();
|
|
126
|
+
env = updatedConfig.env?.[updatedConfig?.current?.env];
|
|
100
127
|
}
|
|
101
128
|
|
|
102
129
|
if (!env || Object.keys(env).length === 0) {
|
|
@@ -119,7 +146,9 @@ async function handleEnv(argv, output) {
|
|
|
119
146
|
output.log(`Users in environment ${env}: ${users}`);
|
|
120
147
|
} else if (argv.env) {
|
|
121
148
|
const result = await changeEnv(argv, output);
|
|
122
|
-
|
|
149
|
+
if (result !== null) {
|
|
150
|
+
output.addData(result);
|
|
151
|
+
}
|
|
123
152
|
} else if (argv.list) {
|
|
124
153
|
const environments = Object.keys(getConfig().env || {});
|
|
125
154
|
output.addData({ environments });
|
|
@@ -160,16 +189,9 @@ export async function interactiveEnv(argv, options, output) {
|
|
|
160
189
|
}
|
|
161
190
|
getConfig().env[result.environment] = getConfig().env[result.environment] || {};
|
|
162
191
|
if (result.host) {
|
|
163
|
-
const
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
getConfig().env[result.environment].host = hostSplit[0];
|
|
167
|
-
} else if (hostSplit.length == 2) {
|
|
168
|
-
getConfig().env[result.environment].protocol = hostSplit[0];
|
|
169
|
-
getConfig().env[result.environment].host = hostSplit[1];
|
|
170
|
-
} else {
|
|
171
|
-
throw createCommandError(`Issues resolving host ${result.host}`);
|
|
172
|
-
}
|
|
192
|
+
const resolvedHost = parseHostInput(result.host);
|
|
193
|
+
getConfig().env[result.environment].protocol = resolvedHost.protocol;
|
|
194
|
+
getConfig().env[result.environment].host = resolvedHost.host;
|
|
173
195
|
}
|
|
174
196
|
if (result.environment) {
|
|
175
197
|
getConfig().current = getConfig().current || {};
|
|
@@ -182,7 +204,7 @@ export async function interactiveEnv(argv, options, output) {
|
|
|
182
204
|
const currentEnv = getConfig().env[result.environment];
|
|
183
205
|
const data = {
|
|
184
206
|
environment: result.environment,
|
|
185
|
-
protocol: currentEnv.protocol,
|
|
207
|
+
protocol: currentEnv.protocol || null,
|
|
186
208
|
host: currentEnv.host || null,
|
|
187
209
|
current: getConfig().current.env
|
|
188
210
|
};
|
|
@@ -197,13 +219,13 @@ export async function interactiveEnv(argv, options, output) {
|
|
|
197
219
|
async function changeEnv(argv, output) {
|
|
198
220
|
const environments = getConfig().env || {};
|
|
199
221
|
|
|
200
|
-
if (!Object.
|
|
222
|
+
if (!Object.hasOwn(environments, argv.env)) {
|
|
201
223
|
if (isJsonOutput(argv)) {
|
|
202
224
|
throw createCommandError(`The specified environment ${argv.env} doesn't exist, please create it`, 404);
|
|
203
225
|
}
|
|
204
226
|
|
|
205
227
|
logMessage(argv, `The specified environment ${argv.env} doesn't exist, please create it`);
|
|
206
|
-
|
|
228
|
+
await interactiveEnv(argv, {
|
|
207
229
|
environment: {
|
|
208
230
|
type: 'input',
|
|
209
231
|
default: argv.env,
|
|
@@ -218,6 +240,7 @@ async function changeEnv(argv, output) {
|
|
|
218
240
|
default: true
|
|
219
241
|
}
|
|
220
242
|
}, output)
|
|
243
|
+
return null;
|
|
221
244
|
} else {
|
|
222
245
|
getConfig().current.env = argv.env;
|
|
223
246
|
updateConfig();
|
|
@@ -226,7 +249,10 @@ async function changeEnv(argv, output) {
|
|
|
226
249
|
current: getConfig().current.env
|
|
227
250
|
};
|
|
228
251
|
logMessage(argv, `Your current environment is now ${getConfig().current.env}`);
|
|
229
|
-
|
|
252
|
+
if (output) {
|
|
253
|
+
output.addData(data);
|
|
254
|
+
}
|
|
255
|
+
return null;
|
|
230
256
|
}
|
|
231
257
|
}
|
|
232
258
|
|
package/bin/commands/files.js
CHANGED
|
@@ -10,102 +10,102 @@ 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
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
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('dangerouslyIncludeLogsAndCache', {
|
|
63
|
+
type: 'boolean',
|
|
64
|
+
describe: 'Includes log and cache folders during export. Risky and usually not recommended'
|
|
65
|
+
})
|
|
66
|
+
.option('iamstupid', {
|
|
67
|
+
type: 'boolean',
|
|
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'
|
|
87
|
+
})
|
|
88
|
+
.option('asFile', {
|
|
89
|
+
type: 'boolean',
|
|
90
|
+
alias: 'af',
|
|
91
|
+
describe: 'Forces the command to treat the path as a single file, even if it has no extension.',
|
|
92
|
+
conflicts: 'asDirectory'
|
|
93
|
+
})
|
|
94
|
+
.option('asDirectory', {
|
|
95
|
+
type: 'boolean',
|
|
96
|
+
alias: 'ad',
|
|
97
|
+
describe: 'Forces the command to treat the path as a directory, even if its name contains a dot.',
|
|
98
|
+
conflicts: 'asFile'
|
|
99
|
+
})
|
|
100
|
+
.option('output', {
|
|
101
|
+
choices: ['json'],
|
|
102
|
+
describe: 'Outputs a single JSON response for automation-friendly parsing'
|
|
103
|
+
})
|
|
104
|
+
.option('json', {
|
|
105
|
+
type: 'boolean',
|
|
106
|
+
hidden: true,
|
|
107
|
+
describe: 'Deprecated alias for --output json'
|
|
108
|
+
})
|
|
109
109
|
},
|
|
110
110
|
handler: async (argv) => {
|
|
111
111
|
if (argv.json && !argv.output) {
|
|
@@ -149,13 +149,13 @@ async function handleFiles(argv, output) {
|
|
|
149
149
|
|
|
150
150
|
if (argv.export) {
|
|
151
151
|
if (argv.dirPath) {
|
|
152
|
-
|
|
152
|
+
|
|
153
153
|
const isFile = isFilePath(argv, argv.dirPath);
|
|
154
154
|
|
|
155
155
|
if (isFile) {
|
|
156
|
-
let parentDirectory = path.dirname(argv.dirPath);
|
|
156
|
+
let parentDirectory = path.dirname(argv.dirPath);
|
|
157
157
|
parentDirectory = parentDirectory === '.' ? '/' : parentDirectory;
|
|
158
|
-
|
|
158
|
+
|
|
159
159
|
await download(env, user, parentDirectory, argv.outPath, false, null, true, argv.dangerouslyIncludeLogsAndCache, [argv.dirPath], true, output);
|
|
160
160
|
} else {
|
|
161
161
|
await download(env, user, argv.dirPath, argv.outPath, true, null, argv.raw, argv.dangerouslyIncludeLogsAndCache, [], false, output);
|
|
@@ -231,9 +231,9 @@ async function handleFiles(argv, output) {
|
|
|
231
231
|
}
|
|
232
232
|
|
|
233
233
|
function getFilesInDirectory(dirPath) {
|
|
234
|
-
return fs.statSync(dirPath).isFile() ? [
|
|
235
|
-
|
|
236
|
-
|
|
234
|
+
return fs.statSync(dirPath).isFile() ? [dirPath] : fs.readdirSync(dirPath)
|
|
235
|
+
.map(file => path.join(dirPath, file))
|
|
236
|
+
.filter(file => fs.statSync(file).isFile());
|
|
237
237
|
}
|
|
238
238
|
|
|
239
239
|
async function processDirectory(env, user, dirPath, outPath, originalDir, createEmpty, isRoot = false, overwrite = false, output) {
|
|
@@ -242,8 +242,8 @@ async function processDirectory(env, user, dirPath, outPath, originalDir, create
|
|
|
242
242
|
await uploadFiles(env, user, filesInDir, isRoot ? outPath : path.join(outPath, path.basename(dirPath)), createEmpty, overwrite, output);
|
|
243
243
|
|
|
244
244
|
const subDirectories = fs.readdirSync(dirPath)
|
|
245
|
-
|
|
246
|
-
|
|
245
|
+
.map(subDir => path.join(dirPath, subDir))
|
|
246
|
+
.filter(subDir => fs.statSync(subDir).isDirectory());
|
|
247
247
|
for (let subDir of subDirectories) {
|
|
248
248
|
await processDirectory(env, user, subDir, isRoot ? outPath : path.join(outPath, path.basename(dirPath)), originalDir, createEmpty, false, overwrite, output);
|
|
249
249
|
}
|
|
@@ -278,7 +278,7 @@ function resolveTree(dirs, indentLevel, parentHasFiles, output) {
|
|
|
278
278
|
resolveTree(dir.files?.data ?? [], indentLevel + '│\t', false, output);
|
|
279
279
|
} else {
|
|
280
280
|
resolveTree(dir.directories ?? [], indentLevel + '\t', hasFiles, output);
|
|
281
|
-
resolveTree(dir.files?.data ?? [], indentLevel + '\t', false, output);
|
|
281
|
+
resolveTree(dir.files?.data ?? [], indentLevel + '\t', false, output);
|
|
282
282
|
}
|
|
283
283
|
}
|
|
284
284
|
}
|
|
@@ -342,7 +342,7 @@ async function download(env, user, dirPath, outPath, recursive, outname, raw, da
|
|
|
342
342
|
await extractArchive(filename, filePath, outPath, raw, output);
|
|
343
343
|
}
|
|
344
344
|
|
|
345
|
-
function prepareDownloadCommandData(directoryPath, excludeDirectories, fileNames, recursive, singleFileMode) {
|
|
345
|
+
export function prepareDownloadCommandData(directoryPath, excludeDirectories, fileNames, recursive, singleFileMode) {
|
|
346
346
|
const data = {
|
|
347
347
|
'DirectoryPath': directoryPath ?? '/',
|
|
348
348
|
'ExcludeDirectories': [excludeDirectories],
|
|
@@ -397,10 +397,8 @@ async function extractArchive(filename, filePath, outPath, raw, output) {
|
|
|
397
397
|
}
|
|
398
398
|
output.log(`Finished extracting ${filename} to ${outPath}\n`);
|
|
399
399
|
|
|
400
|
-
fs.unlink(filePath
|
|
401
|
-
|
|
402
|
-
output.verboseLog(`Warning: Failed to delete temporary archive ${filePath}: ${err.message}`);
|
|
403
|
-
}
|
|
400
|
+
await fs.promises.unlink(filePath).catch(err => {
|
|
401
|
+
output.verboseLog(`Warning: Failed to delete temporary archive ${filePath}: ${err.message}`);
|
|
404
402
|
});
|
|
405
403
|
}
|
|
406
404
|
|
|
@@ -577,13 +575,13 @@ async function uploadChunk(env, user, filePathsChunk, destinationPath, createEmp
|
|
|
577
575
|
form.append('path', destinationPath);
|
|
578
576
|
form.append('skipExistingFiles', String(!overwrite));
|
|
579
577
|
form.append('allowOverwrite', String(overwrite));
|
|
580
|
-
|
|
578
|
+
|
|
581
579
|
filePathsChunk.forEach(fileToUpload => {
|
|
582
580
|
output.log(`${fileToUpload}`)
|
|
583
581
|
form.append('files', fs.createReadStream(path.resolve(fileToUpload)));
|
|
584
582
|
});
|
|
585
583
|
|
|
586
|
-
const res = await fetch(`${env.protocol}://${env.host}/Admin/Api/Upload?` + new URLSearchParams({"createEmptyFiles": createEmpty, "createMissingDirectories": true}), {
|
|
584
|
+
const res = await fetch(`${env.protocol}://${env.host}/Admin/Api/Upload?` + new URLSearchParams({ "createEmptyFiles": createEmpty, "createMissingDirectories": true }), {
|
|
587
585
|
method: 'POST',
|
|
588
586
|
body: form,
|
|
589
587
|
headers: {
|
|
@@ -591,7 +589,7 @@ async function uploadChunk(env, user, filePathsChunk, destinationPath, createEmp
|
|
|
591
589
|
},
|
|
592
590
|
agent: getAgent(env.protocol)
|
|
593
591
|
});
|
|
594
|
-
|
|
592
|
+
|
|
595
593
|
if (res.ok) {
|
|
596
594
|
return await res.json();
|
|
597
595
|
}
|
|
@@ -604,22 +602,21 @@ export function resolveFilePath(filePath) {
|
|
|
604
602
|
let p = path.parse(path.resolve(filePath))
|
|
605
603
|
let regex = wildcardToRegExp(p.base);
|
|
606
604
|
let resolvedPath = fs.readdirSync(p.dir).filter((allFilesPaths) => allFilesPaths.match(regex) !== null)[0]
|
|
607
|
-
if (resolvedPath === undefined)
|
|
608
|
-
{
|
|
605
|
+
if (resolvedPath === undefined) {
|
|
609
606
|
throw createCommandError(`Could not find any files with the name ${filePath}`, 1);
|
|
610
607
|
}
|
|
611
608
|
return path.join(p.dir, resolvedPath);
|
|
612
609
|
}
|
|
613
610
|
|
|
614
611
|
|
|
615
|
-
function isFilePath(argv, dirPath) {
|
|
612
|
+
export function isFilePath(argv, dirPath) {
|
|
616
613
|
if (argv.asFile || argv.asDirectory) {
|
|
617
|
-
return argv.asFile;
|
|
614
|
+
return Boolean(argv.asFile);
|
|
618
615
|
}
|
|
619
616
|
return path.extname(dirPath) !== '';
|
|
620
617
|
}
|
|
621
618
|
|
|
622
|
-
function wildcardToRegExp(wildcard) {
|
|
619
|
+
export function wildcardToRegExp(wildcard) {
|
|
623
620
|
const escaped = wildcard.replace(/[.+?^${}()|[\]\\]/g, '\\$&');
|
|
624
621
|
return new RegExp('^' + escaped.replace(/\*/g, '.*') + '$');
|
|
625
622
|
}
|
|
@@ -678,7 +675,7 @@ function createFilesOutput(argv) {
|
|
|
678
675
|
};
|
|
679
676
|
}
|
|
680
677
|
|
|
681
|
-
function getFilesOperation(argv) {
|
|
678
|
+
export function getFilesOperation(argv) {
|
|
682
679
|
if (argv.list) {
|
|
683
680
|
return 'list';
|
|
684
681
|
}
|
package/bin/commands/install.js
CHANGED
|
@@ -6,22 +6,22 @@ import { uploadFiles, resolveFilePath } from './files.js';
|
|
|
6
6
|
|
|
7
7
|
export function installCommand() {
|
|
8
8
|
return {
|
|
9
|
-
command: 'install
|
|
9
|
+
command: 'install <filePath>',
|
|
10
10
|
describe: 'Installs the addin on the given path, allowed file extensions are .dll, .nupkg',
|
|
11
11
|
builder: (yargs) => {
|
|
12
12
|
return yargs
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
13
|
+
.positional('filePath', {
|
|
14
|
+
describe: 'Path to the file to install'
|
|
15
|
+
})
|
|
16
|
+
.option('queue', {
|
|
17
|
+
alias: 'q',
|
|
18
|
+
type: 'boolean',
|
|
19
|
+
describe: 'Queues the install for next Dynamicweb recycle'
|
|
20
|
+
})
|
|
21
|
+
.option('output', {
|
|
22
|
+
choices: ['json'],
|
|
23
|
+
describe: 'Outputs a single JSON response for automation-friendly parsing'
|
|
24
|
+
})
|
|
25
25
|
},
|
|
26
26
|
handler: async (argv) => {
|
|
27
27
|
const output = createInstallOutput(argv);
|
|
@@ -43,7 +43,7 @@ async function handleInstall(argv, output) {
|
|
|
43
43
|
let env = await setupEnv(argv, output);
|
|
44
44
|
let user = await setupUser(argv, env);
|
|
45
45
|
let resolvedPath = resolveFilePath(argv.filePath);
|
|
46
|
-
await uploadFiles(env, user, [
|
|
46
|
+
await uploadFiles(env, user, [resolvedPath], 'System/AddIns/Local', false, true, output);
|
|
47
47
|
await installAddin(env, user, resolvedPath, argv.queue, output);
|
|
48
48
|
}
|
|
49
49
|
|
|
@@ -56,15 +56,29 @@ async function installAddin(env, user, resolvedPath, queue, output) {
|
|
|
56
56
|
`${filename.substring(0, filename.lastIndexOf('.')) || filename}|${path.extname(resolvedPath)}`
|
|
57
57
|
]
|
|
58
58
|
}
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
59
|
+
const controller = new AbortController();
|
|
60
|
+
const timeoutId = setTimeout(() => controller.abort(), 30_000);
|
|
61
|
+
|
|
62
|
+
let res;
|
|
63
|
+
try {
|
|
64
|
+
res = await fetch(`${env.protocol}://${env.host}/Admin/Api/AddinInstall`, {
|
|
65
|
+
method: 'POST',
|
|
66
|
+
body: JSON.stringify(data),
|
|
67
|
+
headers: {
|
|
68
|
+
'Content-Type': 'application/json',
|
|
69
|
+
'Authorization': `Bearer ${user.apiKey}`
|
|
70
|
+
},
|
|
71
|
+
agent: getAgent(env.protocol),
|
|
72
|
+
signal: controller.signal
|
|
73
|
+
});
|
|
74
|
+
} catch (err) {
|
|
75
|
+
if (err.name === 'AbortError') {
|
|
76
|
+
throw createCommandError('Addin installation request timed out.', 408);
|
|
77
|
+
}
|
|
78
|
+
throw err;
|
|
79
|
+
} finally {
|
|
80
|
+
clearTimeout(timeoutId);
|
|
81
|
+
}
|
|
68
82
|
|
|
69
83
|
if (res.ok) {
|
|
70
84
|
const body = await parseJsonSafe(res);
|
package/bin/commands/login.js
CHANGED
|
@@ -216,7 +216,7 @@ async function login(username, password, env, protocol, verbose) {
|
|
|
216
216
|
}
|
|
217
217
|
}
|
|
218
218
|
|
|
219
|
-
function parseCookies (cookieHeader) {
|
|
219
|
+
export function parseCookies (cookieHeader) {
|
|
220
220
|
const list = {};
|
|
221
221
|
if (!cookieHeader) {
|
|
222
222
|
return list;
|
|
@@ -415,7 +415,7 @@ async function authenticateWithOAuth(argv, env) {
|
|
|
415
415
|
};
|
|
416
416
|
}
|
|
417
417
|
|
|
418
|
-
function shouldUseOAuth(argv, env = {}) {
|
|
418
|
+
export function shouldUseOAuth(argv, env = {}) {
|
|
419
419
|
if (argv.auth === 'user') {
|
|
420
420
|
return false;
|
|
421
421
|
}
|
|
@@ -435,7 +435,7 @@ function shouldUseOAuth(argv, env = {}) {
|
|
|
435
435
|
return env?.auth?.type === 'oauth_client_credentials';
|
|
436
436
|
}
|
|
437
437
|
|
|
438
|
-
function resolveOAuthConfig(argv, env = {}, requireCredentials = true) {
|
|
438
|
+
export function resolveOAuthConfig(argv, env = {}, requireCredentials = true) {
|
|
439
439
|
const authConfig = env?.auth || {};
|
|
440
440
|
const clientIdEnv = argv.clientIdEnv || authConfig.clientIdEnv || DEFAULT_CLIENT_ID_ENV;
|
|
441
441
|
const clientSecretEnv = argv.clientSecretEnv || authConfig.clientSecretEnv || DEFAULT_CLIENT_SECRET_ENV;
|
package/bin/commands/query.js
CHANGED
|
@@ -74,33 +74,57 @@ async function getProperties(env, user, query) {
|
|
|
74
74
|
if (body?.model?.properties?.groups === undefined) {
|
|
75
75
|
throw createCommandError('Unable to fetch query parameters.', res.status, body);
|
|
76
76
|
}
|
|
77
|
-
return body
|
|
77
|
+
return extractQueryPropertyPrompts(body);
|
|
78
78
|
}
|
|
79
79
|
|
|
80
80
|
throw createCommandError('Unable to fetch query parameters.', res.status, await parseJsonSafe(res));
|
|
81
81
|
}
|
|
82
82
|
|
|
83
|
-
async function getQueryParams(env, user, argv, output) {
|
|
83
|
+
export async function getQueryParams(env, user, argv, output, deps = {}) {
|
|
84
84
|
let params = {}
|
|
85
|
+
const getPropertiesFn = deps.getPropertiesFn || getProperties;
|
|
86
|
+
const promptFn = deps.promptFn || input;
|
|
85
87
|
if (argv.interactive) {
|
|
86
|
-
let properties = await
|
|
88
|
+
let properties = await getPropertiesFn(env, user, argv.query);
|
|
87
89
|
output.log('The following properties will be requested:')
|
|
88
90
|
output.log(properties)
|
|
89
|
-
|
|
90
|
-
const value = await input({ message: p });
|
|
91
|
-
if (value) {
|
|
92
|
-
const fieldName = p.split(' (')[0];
|
|
93
|
-
params[fieldName] = value;
|
|
94
|
-
}
|
|
95
|
-
}
|
|
91
|
+
params = await buildInteractiveQueryParams(properties, promptFn);
|
|
96
92
|
} else {
|
|
97
|
-
|
|
93
|
+
params = buildQueryParamsFromArgv(argv);
|
|
94
|
+
}
|
|
95
|
+
return params
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
export function extractQueryPropertyPrompts(body) {
|
|
99
|
+
const fields = body?.model?.properties?.groups?.find(g => g.name === 'Properties')?.fields || [];
|
|
100
|
+
return fields.map(field => `${field.name} (${field.typeName})`);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
export function getFieldNameFromPropertyPrompt(prompt) {
|
|
104
|
+
return prompt.replace(/\s+\([^)]+\)$/, '');
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
export async function buildInteractiveQueryParams(properties, promptFn = input) {
|
|
108
|
+
const params = {};
|
|
109
|
+
|
|
110
|
+
for (const propertyPrompt of properties) {
|
|
111
|
+
const value = await promptFn({ message: propertyPrompt });
|
|
112
|
+
if (value) {
|
|
113
|
+
params[getFieldNameFromPropertyPrompt(propertyPrompt)] = value;
|
|
114
|
+
}
|
|
98
115
|
}
|
|
116
|
+
|
|
117
|
+
return params;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
export function buildQueryParamsFromArgv(argv) {
|
|
121
|
+
let params = {}
|
|
122
|
+
Object.keys(argv).filter(k => !exclude.includes(k)).forEach(k => params[k] = argv[k])
|
|
99
123
|
return params
|
|
100
124
|
}
|
|
101
125
|
|
|
102
126
|
async function runQuery(env, user, query, params) {
|
|
103
|
-
let res = await fetch(`${env.protocol}://${env.host}/Admin/Api/${query}?` + new URLSearchParams(params), {
|
|
127
|
+
let res = await fetch(`${env.protocol}://${env.host}/Admin/Api/${encodeURIComponent(query)}?` + new URLSearchParams(params), {
|
|
104
128
|
method: 'GET',
|
|
105
129
|
headers: {
|
|
106
130
|
'Authorization': `Bearer ${user.apiKey}`
|
package/bin/index.js
CHANGED
|
@@ -84,8 +84,12 @@ function baseCommand() {
|
|
|
84
84
|
} else if (currentEnv?.current?.user) {
|
|
85
85
|
console.log(`User: ${currentEnv.current.user}`);
|
|
86
86
|
}
|
|
87
|
-
|
|
88
|
-
|
|
87
|
+
if (currentEnv.protocol) {
|
|
88
|
+
console.log(`Protocol: ${currentEnv.protocol}`);
|
|
89
|
+
}
|
|
90
|
+
if (currentEnv.host) {
|
|
91
|
+
console.log(`Host: ${currentEnv.host}`);
|
|
92
|
+
}
|
|
89
93
|
}
|
|
90
94
|
}
|
|
91
95
|
}
|
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": "2.0.0-beta.
|
|
5
|
+
"version": "2.0.0-beta.2",
|
|
6
6
|
"main": "bin/index.js",
|
|
7
7
|
"files": [
|
|
8
8
|
"bin/**"
|
|
@@ -15,7 +15,8 @@
|
|
|
15
15
|
"devops"
|
|
16
16
|
],
|
|
17
17
|
"scripts": {
|
|
18
|
-
"test": "
|
|
18
|
+
"test": "node --test",
|
|
19
|
+
"qa:smoke": "node qa/run-smoke.mjs"
|
|
19
20
|
},
|
|
20
21
|
"author": "Dynamicweb A/S (https://www.dynamicweb.com)",
|
|
21
22
|
"repository": {
|