@dynamicweb/cli 1.0.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.
- package/README.md +171 -0
- package/bin/commands/command.js +88 -0
- package/bin/commands/config.js +55 -0
- package/bin/commands/database.js +72 -0
- package/bin/commands/env.js +133 -0
- package/bin/commands/files.js +232 -0
- package/bin/commands/install.js +55 -0
- package/bin/commands/login.js +190 -0
- package/bin/commands/query.js +92 -0
- package/bin/commands/swift.js +79 -0
- package/bin/index.js +48 -0
- package/bin/utils.js +20 -0
- package/package.json +41 -0
package/README.md
ADDED
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
# DynamicWeb CLI
|
|
2
|
+
|
|
3
|
+
## What is it?
|
|
4
|
+
DynamicWeb CLI is a powerful command line tool designed to help developers quickly and efficiently manage any given DynamicWeb 10 solution they may have access to. These tools inclues an easy setup and handling of different environments, access to the Management API and an easy way to update a Swift solution.
|
|
5
|
+
|
|
6
|
+
Logging into a DynamicWeb 10 solution through the DynamicWeb CLI will create an API Key for the given user, which in turn lets you use any Queries and Commands the solution had, meaning you can control everything you can do in the backend, from your command line.
|
|
7
|
+
With this, you can hook it up to your own build pipelines and processes, if certain requests needs to happen before or after deployments or changes.
|
|
8
|
+
|
|
9
|
+
The DynamicWeb CLI can also help with active development of custom addins to solutions. With a simple `dw install` command it will upload and install your custom code to a solution.
|
|
10
|
+
|
|
11
|
+
Extracting files from solutions is just as easy as well, with the DynamicWeb CLI you can list out the structure of a solution and get full exports of the files structure and the database. Importing files into a solution is just as easy as well, as long as you have access to the files and the solution, they can be imported with a simple command using the DynamicWeb CLI.
|
|
12
|
+
|
|
13
|
+
## Get started
|
|
14
|
+
To install after cloning, move to project dir and run
|
|
15
|
+
> $ npm install -g .
|
|
16
|
+
>
|
|
17
|
+
> $ npm install
|
|
18
|
+
|
|
19
|
+
Note that specific installations might be necessary if you're faced with errors such as 'Error [ERR_MODULE_NOT_FOUND]: Cannot find package 'yargs''
|
|
20
|
+
In which case try installing that module specifically;
|
|
21
|
+
> $ npm install yargs
|
|
22
|
+
|
|
23
|
+
## Commands
|
|
24
|
+
All commands and options can be viewed by running
|
|
25
|
+
> $ dw --help
|
|
26
|
+
>
|
|
27
|
+
> $ dw \<command\> --help
|
|
28
|
+
|
|
29
|
+
### Users and environments
|
|
30
|
+
As most commands are pulling or pushing data from the DW admin API, the necessary authorization is required.
|
|
31
|
+
|
|
32
|
+
To generate an Api-key that the CLI will use, login to your environment
|
|
33
|
+
> $ dw login
|
|
34
|
+
|
|
35
|
+
This will start an interactive session asking for username and password, as well as the name of the environment, so it's possible to switch between different environments easily.
|
|
36
|
+
It will also ask for a host, if you're running a local environment, set this to the host it starts up with, i.e `localhost:6001`.
|
|
37
|
+
|
|
38
|
+
Each environment has its own users, and each user has its own Api-key assigned to it, swap between environments by using
|
|
39
|
+
> $ dw env \<env\>
|
|
40
|
+
|
|
41
|
+
and swap between users by simply supplying the name of the user in the login command
|
|
42
|
+
> $ dw login \<username\>
|
|
43
|
+
|
|
44
|
+
You can view the current environment and user being used by simply typing
|
|
45
|
+
> $ dw
|
|
46
|
+
|
|
47
|
+
The configuration will automatically be created when setting up your first environment, but if you already have an Api-key you want to use for a user, you can modify the config directly in the file located in `usr/.dwc`. The structure should look like the following
|
|
48
|
+
```json
|
|
49
|
+
{
|
|
50
|
+
"env": {
|
|
51
|
+
"dev": {
|
|
52
|
+
"host": "localhost:6001",
|
|
53
|
+
"users": {
|
|
54
|
+
"DemoUser": {
|
|
55
|
+
"apiKey": "<keyPrefix>.<key>"
|
|
56
|
+
}
|
|
57
|
+
},
|
|
58
|
+
"current": {
|
|
59
|
+
"user": "DemoUser"
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
},
|
|
63
|
+
"current": {
|
|
64
|
+
"env": "dev"
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
### Files
|
|
70
|
+
> $ dw files \<dirPath\> \<outPath\>
|
|
71
|
+
|
|
72
|
+
The files command is used to list out and export the structure in your Dynamicweb files archive, as such is has multiple options;
|
|
73
|
+
- `-l` `--list` This will list the directory given in \<dirPath\>
|
|
74
|
+
- `-f` `--includeFiles` The list will now also show all files in the directories
|
|
75
|
+
- `-r` `--recursive` By default it only handles the \<dirPath\>, but with this option it will handle all directories under this recursively
|
|
76
|
+
- `-e` `--export` It will export \<dirPath\> into \<outPath\> on your local machine, unzipped by default
|
|
77
|
+
- `--raw` This will keep the content zipped
|
|
78
|
+
- `--iamstupid` This will include the export of the /files/system/log and /files/.cache folders
|
|
79
|
+
|
|
80
|
+
#### Examples
|
|
81
|
+
Exporting all templates from current environment to local solution
|
|
82
|
+
> $ cd DynamicWebSolution/Files
|
|
83
|
+
>
|
|
84
|
+
> $ dw files templates ./templates -fre
|
|
85
|
+
|
|
86
|
+
Listing the system files structure of the current environment
|
|
87
|
+
> $ dw files system -lr
|
|
88
|
+
|
|
89
|
+
### Swift
|
|
90
|
+
> $ dw swift \<outPath\>
|
|
91
|
+
|
|
92
|
+
The swift command is used to easily get your local environment up to date with the latest swift release. It will override all existing directories and content in those, which can then be adjusted in your source control afterwards. It has multiple options to specify which tag or branch to pull;
|
|
93
|
+
- `-t` `--tag <tag>` The tag/branch/release to pull
|
|
94
|
+
- `-l` `--list` Will list all the release versions
|
|
95
|
+
- `-n` `--nightly` Will pull #HEAD, as default is latest release
|
|
96
|
+
- `--force` Used if \<outPath\> is not an empty folder, to override all the content
|
|
97
|
+
|
|
98
|
+
#### Examples
|
|
99
|
+
Getting all the available releases
|
|
100
|
+
> $ dw swift -l
|
|
101
|
+
|
|
102
|
+
Pulling and overriding local solution with latest nightly build
|
|
103
|
+
> $ cd DynamicWebSolution/Swift
|
|
104
|
+
>
|
|
105
|
+
> $ dw swift . -n --force
|
|
106
|
+
|
|
107
|
+
### Query
|
|
108
|
+
> $ dw query \<query\>
|
|
109
|
+
|
|
110
|
+
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;
|
|
111
|
+
- `-l` `--list` Will list all the properties for the given \<query\>
|
|
112
|
+
- `-i` `--interactive` Will perform the \<query\> but without any parameters, as they will be asked for one by one in interactive mode
|
|
113
|
+
- `--<queryParam>` Any parameter the query needs will be sent by '--key value'
|
|
114
|
+
|
|
115
|
+
#### Examples
|
|
116
|
+
Getting all properties for a query
|
|
117
|
+
> $ dw query FileByName -l
|
|
118
|
+
|
|
119
|
+
Getting file information on a specific file by name
|
|
120
|
+
> $ dw query FileByName --name DefaultMail.html --directorypath /Templates/Forms/Mail
|
|
121
|
+
|
|
122
|
+
### Command
|
|
123
|
+
> $ dw command \<command\>
|
|
124
|
+
|
|
125
|
+
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.
|
|
126
|
+
- `-l` `--list` Lists all the properties for the command, as well as the json model required **currently not working**
|
|
127
|
+
- `--json` Takes a path to a .json file or a literal json, i.e --json '{ abc: "123" }'
|
|
128
|
+
|
|
129
|
+
#### Examples
|
|
130
|
+
Creating a copy of a page using a json-string
|
|
131
|
+
> $ dw command PageCopy --json '{ "model": { "SourcePageId": 1189, "DestinationParentPageId": 1129 } }'
|
|
132
|
+
|
|
133
|
+
Removing a page using a json file
|
|
134
|
+
> $ dw command PageMove --json ./PageMove.json
|
|
135
|
+
|
|
136
|
+
Where PageMove.json contains
|
|
137
|
+
```json
|
|
138
|
+
{ "model": { "SourcePageId": 1383, "DestinationParentPageId": 1376 } }
|
|
139
|
+
```
|
|
140
|
+
|
|
141
|
+
Deleting a page
|
|
142
|
+
> $ dw command PageDelete --json '{ "id": "1383" }'
|
|
143
|
+
|
|
144
|
+
### Install
|
|
145
|
+
> $ dw install \<filePath\>
|
|
146
|
+
|
|
147
|
+
Install is somewhat of a shorthand for a few commands. It will upload and install a given .dll or .nupkg addin to your current environment.
|
|
148
|
+
|
|
149
|
+
It's meant to be used to easily apply custom dlls to a given project, it being local or otherwise, so after having a dotnet library built locally, this command can be run, pointing to the built .dll and it will handle the rest with all the addin installation, and it will be available in the DynamicWeb solution as soon as the command finishes.
|
|
150
|
+
|
|
151
|
+
#### Examples
|
|
152
|
+
> $ dw install ./bin/Release/net6.0/CustomProject.dll
|
|
153
|
+
|
|
154
|
+
### Database
|
|
155
|
+
> $ dw database \<outPath\>
|
|
156
|
+
|
|
157
|
+
This command is used for various actions towards your current environments database.
|
|
158
|
+
- `-e` `--export` Exports your current environments database to a .bacpac file at \<outPath\>
|
|
159
|
+
|
|
160
|
+
#### Examples
|
|
161
|
+
> $ dw database -e ./backup
|
|
162
|
+
|
|
163
|
+
### Config
|
|
164
|
+
> $ dw config
|
|
165
|
+
|
|
166
|
+
Config is used to manage the .dwc file through the CLI, given any prop it will create the key/value with the path to it.
|
|
167
|
+
- `--<property>` The path and name of the property to set
|
|
168
|
+
|
|
169
|
+
#### Examples
|
|
170
|
+
Changing the host for the dev environment
|
|
171
|
+
> $ dw config --env.dev.host localhost:6001
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
import fetch from 'node-fetch';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
import fs from 'fs';
|
|
4
|
+
import { setupEnv, getAgent } from './env.js';
|
|
5
|
+
import { setupUser } from './login.js';
|
|
6
|
+
|
|
7
|
+
const exclude = ['_', '$0', 'command', 'list', 'json']
|
|
8
|
+
|
|
9
|
+
export function commandCommand() {
|
|
10
|
+
return {
|
|
11
|
+
command: 'command [command]',
|
|
12
|
+
describe: 'Runs the given command',
|
|
13
|
+
builder: (yargs) => {
|
|
14
|
+
return yargs
|
|
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
|
+
},
|
|
26
|
+
handler: (argv) => {
|
|
27
|
+
if (argv.verbose) console.info(`Running command ${argv.command}`)
|
|
28
|
+
handleCommand(argv)
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
async function handleCommand(argv) {
|
|
34
|
+
let env = await setupEnv(argv);
|
|
35
|
+
let user = await setupUser(argv, env);
|
|
36
|
+
if (argv.list) {
|
|
37
|
+
console.log(await getProperties(env, user, argv.command))
|
|
38
|
+
} else {
|
|
39
|
+
let response = await runCommand(env, user, argv.command, getQueryParams(argv), parseJsonOrPath(argv.json))
|
|
40
|
+
console.log(response)
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
async function getProperties(env, user, command) {
|
|
45
|
+
return `This option currently doesn't work`
|
|
46
|
+
let res = await fetch(`${env.protocol}://${env.host}/Admin/Api/CommandByName?name=${command}`, {
|
|
47
|
+
method: 'GET',
|
|
48
|
+
headers: {
|
|
49
|
+
'Authorization': `Bearer ${user.apiKey}`
|
|
50
|
+
},
|
|
51
|
+
agent: getAgent(env.protocol)
|
|
52
|
+
})
|
|
53
|
+
if (res.ok) {
|
|
54
|
+
let body = await res.json()
|
|
55
|
+
return body.model.propertyNames
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function getQueryParams(argv) {
|
|
60
|
+
let params = {}
|
|
61
|
+
Object.keys(argv).filter(k => !exclude.includes(k)).forEach(k => params['Command.' + k] = argv[k])
|
|
62
|
+
return params
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function parseJsonOrPath(json) {
|
|
66
|
+
if (!json) return
|
|
67
|
+
if (fs.existsSync(json)) {
|
|
68
|
+
return JSON.parse(fs.readFileSync(path.resolve(json)))
|
|
69
|
+
} else {
|
|
70
|
+
return JSON.parse(json)
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
async function runCommand(env, user, command, queryParams, data) {
|
|
75
|
+
let res = await fetch(`${env.protocol}://${env.host}/Admin/Api/${command}?` + new URLSearchParams(queryParams), {
|
|
76
|
+
method: 'POST',
|
|
77
|
+
body: JSON.stringify(data),
|
|
78
|
+
headers: {
|
|
79
|
+
'Authorization': `Bearer ${user.apiKey}`,
|
|
80
|
+
'Content-Type': 'application/json'
|
|
81
|
+
},
|
|
82
|
+
agent: getAgent(env.protocol)
|
|
83
|
+
})
|
|
84
|
+
if (!res.ok) {
|
|
85
|
+
console.log(`Error when doing request ${res.url}`)
|
|
86
|
+
}
|
|
87
|
+
return await res.json()
|
|
88
|
+
}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import os from 'os';
|
|
2
|
+
import fs from 'fs';
|
|
3
|
+
|
|
4
|
+
const configLocation = os.homedir() + '/.dwc';
|
|
5
|
+
let localConfig;
|
|
6
|
+
|
|
7
|
+
export function configCommand() {
|
|
8
|
+
return {
|
|
9
|
+
command: 'config',
|
|
10
|
+
describe: 'Edit the configs located in usr/.dwc',
|
|
11
|
+
handler: (argv) => handleConfig(argv),
|
|
12
|
+
builder: {
|
|
13
|
+
prop: {
|
|
14
|
+
type: 'string',
|
|
15
|
+
describe: 'Path to your config property, i.e --env.dev.host=newHost:1000'
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export function setupConfig() {
|
|
22
|
+
try {
|
|
23
|
+
localConfig = JSON.parse(fs.readFileSync(configLocation));
|
|
24
|
+
} catch (e) {
|
|
25
|
+
localConfig = {}
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export function getConfig() {
|
|
30
|
+
return localConfig;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export function handleConfig(argv) {
|
|
34
|
+
Object.keys(argv).forEach(a => {
|
|
35
|
+
if (a != '_' && a != '$0') {
|
|
36
|
+
resolveConfig(a, argv[a], config[a]);
|
|
37
|
+
updateConfig();
|
|
38
|
+
}
|
|
39
|
+
})
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export function updateConfig() {
|
|
43
|
+
fs.writeFileSync(configLocation, JSON.stringify(localConfig));
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function resolveConfig(key, obj, conf) {
|
|
47
|
+
if (typeof obj !== 'object' || !(obj instanceof Object)) {
|
|
48
|
+
return obj;
|
|
49
|
+
}
|
|
50
|
+
Object.keys(obj).forEach(a => {
|
|
51
|
+
conf[a] = conf[a] || {};
|
|
52
|
+
conf[a] = resolveConfig(key, obj[a], conf[a]);
|
|
53
|
+
})
|
|
54
|
+
return conf;
|
|
55
|
+
}
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import fetch from 'node-fetch';
|
|
2
|
+
import fs from 'fs';
|
|
3
|
+
import _path from 'path';
|
|
4
|
+
import { setupEnv, getAgent } from './env.js';
|
|
5
|
+
import { setupUser } from './login.js';
|
|
6
|
+
|
|
7
|
+
export function databaseCommand() {
|
|
8
|
+
return {
|
|
9
|
+
command: 'database [path]',
|
|
10
|
+
describe: 'Handles database',
|
|
11
|
+
builder: (yargs) => {
|
|
12
|
+
return yargs
|
|
13
|
+
.positional('path', {
|
|
14
|
+
describe: 'Path to the .bacpac file',
|
|
15
|
+
default: '.'
|
|
16
|
+
})
|
|
17
|
+
.option('export', {
|
|
18
|
+
alias: 'e',
|
|
19
|
+
type: 'boolean',
|
|
20
|
+
description: 'Exports the solutions database to a .bacpac file at [path]'
|
|
21
|
+
})
|
|
22
|
+
},
|
|
23
|
+
handler: (argv) => {
|
|
24
|
+
if (argv.verbose) console.info(`Handling database with path: ${argv.path}`)
|
|
25
|
+
handleDatabase(argv)
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
async function handleDatabase(argv) {
|
|
31
|
+
let env = await setupEnv(argv);
|
|
32
|
+
let user = await setupUser(argv, env);
|
|
33
|
+
|
|
34
|
+
if (argv.export) {
|
|
35
|
+
await download(env, user, argv.path, argv.verbose);
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
async function download(env, user, path, verbose) {
|
|
40
|
+
let filename = 'database.bacpac';
|
|
41
|
+
fetch(`${env.protocol}://${env.host}/Admin/Api/DatabaseDownload`, {
|
|
42
|
+
method: 'POST',
|
|
43
|
+
headers: {
|
|
44
|
+
'Authorization': `Bearer ${user.apiKey}`,
|
|
45
|
+
'content-type': 'application/json'
|
|
46
|
+
},
|
|
47
|
+
agent: getAgent(env.protocol)
|
|
48
|
+
}).then(async (res) => {
|
|
49
|
+
if (verbose) console.log(res)
|
|
50
|
+
const header = res.headers.get('Content-Disposition');
|
|
51
|
+
const parts = header?.split(';');
|
|
52
|
+
if (!parts || !header.includes('attachment')) {
|
|
53
|
+
console.log('Failed download, check users database permissions')
|
|
54
|
+
if (verbose) console.log(await res.json())
|
|
55
|
+
return;
|
|
56
|
+
}
|
|
57
|
+
filename = parts[1].split('=')[1];
|
|
58
|
+
return res;
|
|
59
|
+
}).then(async (res) => {
|
|
60
|
+
if (!res) {
|
|
61
|
+
return;
|
|
62
|
+
}
|
|
63
|
+
const fileStream = fs.createWriteStream(_path.resolve(`${_path.resolve(path)}/${filename}`));
|
|
64
|
+
await new Promise((resolve, reject) => {
|
|
65
|
+
res.body.pipe(fileStream);
|
|
66
|
+
res.body.on("error", reject);
|
|
67
|
+
fileStream.on("finish", resolve);
|
|
68
|
+
});
|
|
69
|
+
console.log(`Finished downloading`);
|
|
70
|
+
return res;
|
|
71
|
+
});
|
|
72
|
+
}
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
import { updateConfig, getConfig } from './config.js'
|
|
2
|
+
import { Agent as HttpAgent } from 'http';
|
|
3
|
+
import { Agent as HttpsAgent } from 'https';
|
|
4
|
+
import yargsInteractive from 'yargs-interactive';
|
|
5
|
+
|
|
6
|
+
const httpAgent = new HttpAgent({
|
|
7
|
+
rejectUnauthorized: false
|
|
8
|
+
})
|
|
9
|
+
|
|
10
|
+
const httpsAgent = new HttpsAgent({
|
|
11
|
+
rejectUnauthorized: false
|
|
12
|
+
})
|
|
13
|
+
|
|
14
|
+
export function getAgent(protocol) {
|
|
15
|
+
return protocol === 'http' ? httpAgent : httpsAgent;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export function envCommand() {
|
|
19
|
+
return {
|
|
20
|
+
command: 'env [env]',
|
|
21
|
+
describe: 'If environment is specified, changes the current environment to the environment specified, otherwise sets up the config for a new environment',
|
|
22
|
+
builder: (yargs) => {
|
|
23
|
+
return yargs
|
|
24
|
+
.positional('env', {
|
|
25
|
+
describe: 'Environment'
|
|
26
|
+
})
|
|
27
|
+
.option('list', {
|
|
28
|
+
alias: 'l',
|
|
29
|
+
type: 'boolean',
|
|
30
|
+
description: 'List all existing environments'
|
|
31
|
+
})
|
|
32
|
+
.option('users', {
|
|
33
|
+
alias: 'u',
|
|
34
|
+
type: 'boolean',
|
|
35
|
+
description: 'List all users in environment, uses positional [env] if used, otherwise current env'
|
|
36
|
+
})
|
|
37
|
+
},
|
|
38
|
+
handler: (argv) => handleEnv(argv)
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export async function setupEnv(argv) {
|
|
43
|
+
let env;
|
|
44
|
+
if (getConfig().env) {
|
|
45
|
+
env = getConfig().env[argv.env] || getConfig().env[getConfig()?.current?.env];
|
|
46
|
+
if (!env.protocol) {
|
|
47
|
+
console.log('Protocol for environment not set, defaulting to https');
|
|
48
|
+
env.protocol = 'https';
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
if (!env) {
|
|
52
|
+
console.log('Current environment not set, please set it')
|
|
53
|
+
await interactiveEnv(argv, {
|
|
54
|
+
environment: {
|
|
55
|
+
type: 'input'
|
|
56
|
+
},
|
|
57
|
+
interactive: {
|
|
58
|
+
default: true
|
|
59
|
+
}
|
|
60
|
+
})
|
|
61
|
+
env = getConfig().env[getConfig()?.current?.env];
|
|
62
|
+
}
|
|
63
|
+
return env;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
async function handleEnv(argv) {
|
|
67
|
+
if (argv.users) {
|
|
68
|
+
let env = argv.env || getConfig().current.env;
|
|
69
|
+
console.log(`Users in environment ${env}: ${Object.keys(getConfig().env[env].users || {})}`);
|
|
70
|
+
} else if (argv.env) {
|
|
71
|
+
changeEnv(argv)
|
|
72
|
+
} else if (argv.list) {
|
|
73
|
+
console.log(`Existing environments: ${Object.keys(getConfig().env || {})}`)
|
|
74
|
+
} else {
|
|
75
|
+
interactiveEnv(argv, {
|
|
76
|
+
environment: {
|
|
77
|
+
type: 'input'
|
|
78
|
+
},
|
|
79
|
+
protocol: {
|
|
80
|
+
type: 'input'
|
|
81
|
+
},
|
|
82
|
+
host: {
|
|
83
|
+
type: 'input'
|
|
84
|
+
},
|
|
85
|
+
interactive: {
|
|
86
|
+
default: true
|
|
87
|
+
}
|
|
88
|
+
})
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
export async function interactiveEnv(argv, options) {
|
|
93
|
+
if (argv.verbose) console.info('Setting up new environment')
|
|
94
|
+
await yargsInteractive()
|
|
95
|
+
.interactive(options)
|
|
96
|
+
.then(async (result) => {
|
|
97
|
+
getConfig().env = getConfig().env || {};
|
|
98
|
+
getConfig().env[result.environment] = getConfig().env[result.environment] || {};
|
|
99
|
+
getConfig().env[result.environment].protocol = result.protocol || 'https';
|
|
100
|
+
if (result.host)
|
|
101
|
+
getConfig().env[result.environment].host = result.host;
|
|
102
|
+
if (result.environment) {
|
|
103
|
+
getConfig().current = getConfig().current || {};
|
|
104
|
+
getConfig().current.env = result.environment;
|
|
105
|
+
}
|
|
106
|
+
updateConfig();
|
|
107
|
+
console.log(`Your current environment is now ${getConfig().current.env}`);
|
|
108
|
+
});
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
async function changeEnv(argv) {
|
|
112
|
+
if (!Object.keys(getConfig().env).includes(argv.env)) {
|
|
113
|
+
console.log(`The specified environment ${argv.env} doesn't exist, please create it`);
|
|
114
|
+
await interactiveEnv(argv, {
|
|
115
|
+
environment: {
|
|
116
|
+
type: 'input',
|
|
117
|
+
default: argv.env,
|
|
118
|
+
prompt: 'never'
|
|
119
|
+
},
|
|
120
|
+
host: {
|
|
121
|
+
type: 'input',
|
|
122
|
+
prompt: 'always'
|
|
123
|
+
},
|
|
124
|
+
interactive: {
|
|
125
|
+
default: true
|
|
126
|
+
}
|
|
127
|
+
})
|
|
128
|
+
} else {
|
|
129
|
+
getConfig().current.env = argv.env;
|
|
130
|
+
updateConfig();
|
|
131
|
+
console.log(`Your current environment is now ${getConfig().current.env}`);
|
|
132
|
+
}
|
|
133
|
+
}
|
|
@@ -0,0 +1,232 @@
|
|
|
1
|
+
import fetch from 'node-fetch';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
import fs from 'fs';
|
|
4
|
+
import extract from 'extract-zip';
|
|
5
|
+
import FormData from 'form-data';
|
|
6
|
+
import { setupEnv, getAgent } from './env.js';
|
|
7
|
+
import { setupUser } from './login.js';
|
|
8
|
+
import { interactiveConfirm } from '../utils.js';
|
|
9
|
+
|
|
10
|
+
export function filesCommand() {
|
|
11
|
+
return {
|
|
12
|
+
command: 'files [dirPath] [outPath]',
|
|
13
|
+
describe: 'Handles files',
|
|
14
|
+
builder: (yargs) => {
|
|
15
|
+
return yargs
|
|
16
|
+
.positional('dirPath', {
|
|
17
|
+
describe: 'The directory to list or export'
|
|
18
|
+
})
|
|
19
|
+
.positional('outPath', {
|
|
20
|
+
describe: 'The directory to export the specified directory to',
|
|
21
|
+
default: '.'
|
|
22
|
+
})
|
|
23
|
+
.option('list', {
|
|
24
|
+
alias: 'l',
|
|
25
|
+
type: 'boolean',
|
|
26
|
+
describe: 'Lists all directories and files'
|
|
27
|
+
})
|
|
28
|
+
.option('export', {
|
|
29
|
+
alias: 'e',
|
|
30
|
+
type: 'boolean',
|
|
31
|
+
describe: 'Exports the directory at [dirPath] to [outPath]'
|
|
32
|
+
})
|
|
33
|
+
.option('import', {
|
|
34
|
+
alias: 'i',
|
|
35
|
+
type: 'boolean',
|
|
36
|
+
describe: 'Imports the file at [dirPath] to [outPath]'
|
|
37
|
+
})
|
|
38
|
+
.option('includeFiles', {
|
|
39
|
+
alias: 'f',
|
|
40
|
+
type: 'boolean',
|
|
41
|
+
describe: 'Includes files in list of directories and files'
|
|
42
|
+
})
|
|
43
|
+
.option('recursive', {
|
|
44
|
+
alias: 'r',
|
|
45
|
+
type: 'boolean',
|
|
46
|
+
describe: 'Handles all directories recursively'
|
|
47
|
+
})
|
|
48
|
+
.option('raw', {
|
|
49
|
+
type: 'boolean',
|
|
50
|
+
describe: 'Keeps zip file instead of unpacking it'
|
|
51
|
+
})
|
|
52
|
+
.option('iamstupid', {
|
|
53
|
+
type: 'boolean',
|
|
54
|
+
describe: 'Includes export of log and cache folders, NOT RECOMMENDED'
|
|
55
|
+
})
|
|
56
|
+
},
|
|
57
|
+
handler: (argv) => {
|
|
58
|
+
if (argv.verbose) console.info(`Listing directory at: ${argv.dirPath}`)
|
|
59
|
+
handleFiles(argv)
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
async function handleFiles(argv) {
|
|
65
|
+
let env = await setupEnv(argv);
|
|
66
|
+
let user = await setupUser(argv, env);
|
|
67
|
+
|
|
68
|
+
if (argv.list) {
|
|
69
|
+
let files = (await getFilesStructure(env, user, argv.dirPath, argv.recursive, argv.includeFiles)).model;
|
|
70
|
+
console.log(files.name)
|
|
71
|
+
let hasFiles = files.files?.data && files.files?.data.length !== 0;
|
|
72
|
+
resolveTree(files.directories, '', hasFiles);
|
|
73
|
+
resolveTree(files.files?.data ?? [], '', false);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
if (argv.export) {
|
|
77
|
+
if (argv.dirPath) {
|
|
78
|
+
await download(env, user, argv.dirPath, argv.outPath, argv.recursive, null, argv.raw, []);
|
|
79
|
+
} else {
|
|
80
|
+
await interactiveConfirm('Are you sure you want a full export of files?', async () => {
|
|
81
|
+
console.log('Full export is starting')
|
|
82
|
+
let filesStructure = (await getFilesStructure(env, user, '/', false, argv.includeFiles)).model;
|
|
83
|
+
let dirs = filesStructure.directories;
|
|
84
|
+
for (let id = 0; id < dirs.length; id++) {
|
|
85
|
+
const dir = dirs[id];
|
|
86
|
+
await download(env, user, dir.name, argv.outPath, true, null, argv.raw, argv.iamstupid, []);
|
|
87
|
+
}
|
|
88
|
+
await download(env, user, '/.', argv.outPath, false, 'Base.zip', argv.raw, argv.iamstupid, Array.from(filesStructure.files.data, f => f.name));
|
|
89
|
+
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')
|
|
90
|
+
})
|
|
91
|
+
}
|
|
92
|
+
} else if (argv.import) {
|
|
93
|
+
if (argv.dirPath && argv.outPath) {
|
|
94
|
+
let resolvedPath = path.resolve(argv.dirPath)
|
|
95
|
+
await uploadFile(env, user, resolvedPath, argv.outPath);
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
function resolveTree(dirs, indentLevel, parentHasFiles) {
|
|
101
|
+
let end = `└──`
|
|
102
|
+
let mid = `├──`
|
|
103
|
+
for (let id = 0; id < dirs.length; id++) {
|
|
104
|
+
const dir = dirs[id];
|
|
105
|
+
let indentPipe = true;
|
|
106
|
+
if (dirs.length == 1) {
|
|
107
|
+
if (parentHasFiles) {
|
|
108
|
+
console.log(indentLevel + mid, dir.name)
|
|
109
|
+
} else {
|
|
110
|
+
console.log(indentLevel + end, dir.name)
|
|
111
|
+
indentPipe = false;
|
|
112
|
+
}
|
|
113
|
+
} else if (id != dirs.length - 1) {
|
|
114
|
+
console.log(indentLevel + mid, dir.name)
|
|
115
|
+
} else {
|
|
116
|
+
if (parentHasFiles) {
|
|
117
|
+
console.log(indentLevel + mid, dir.name)
|
|
118
|
+
} else {
|
|
119
|
+
console.log(indentLevel + end, dir.name)
|
|
120
|
+
indentPipe = false;
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
let hasFiles = dir.files?.data && dir.files?.data.length !== 0;
|
|
124
|
+
if (indentPipe) {
|
|
125
|
+
resolveTree(dir.directories ?? [], indentLevel + '│\t', hasFiles);
|
|
126
|
+
resolveTree(dir.files?.data ?? [], indentLevel + '│\t', false);
|
|
127
|
+
} else {
|
|
128
|
+
resolveTree(dir.directories ?? [], indentLevel + '\t', hasFiles);
|
|
129
|
+
resolveTree(dir.files?.data ?? [], indentLevel + '\t', false);
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
async function download(env, user, dirPath, outPath, recursive, outname, raw, iamstupid, fileNames) {
|
|
135
|
+
let endpoint;
|
|
136
|
+
let excludeDirectories = '';
|
|
137
|
+
if (!iamstupid) {
|
|
138
|
+
excludeDirectories = 'system/log';
|
|
139
|
+
if (dirPath === 'cache.net') {
|
|
140
|
+
return;
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
let data = {
|
|
144
|
+
'DirectoryPath': dirPath ?? '/',
|
|
145
|
+
'ExcludeDirectories': [ excludeDirectories ],
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
if (recursive) {
|
|
149
|
+
endpoint = 'DirectoryDownload';
|
|
150
|
+
} else {
|
|
151
|
+
endpoint = 'FileDownload'
|
|
152
|
+
data['Ids'] = fileNames
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
console.log('Downloading', dirPath === '/.' ? 'Base' : dirPath, 'Recursive=' + recursive);
|
|
156
|
+
|
|
157
|
+
let filename;
|
|
158
|
+
|
|
159
|
+
fetch(`${env.protocol}://${env.host}/Admin/Api/${endpoint}`, {
|
|
160
|
+
method: 'POST',
|
|
161
|
+
body: JSON.stringify(data),
|
|
162
|
+
headers: {
|
|
163
|
+
'Authorization': `Bearer ${user.apiKey}`,
|
|
164
|
+
'Content-Type': 'application/json'
|
|
165
|
+
},
|
|
166
|
+
agent: getAgent(env.protocol)
|
|
167
|
+
}).then((res) => {
|
|
168
|
+
const header = res.headers.get('content-disposition');
|
|
169
|
+
const parts = header?.split(';');
|
|
170
|
+
if (!parts) {
|
|
171
|
+
console.log(`No files found in directory '${dirPath}', if you want to download all folders recursively include the -r flag`);
|
|
172
|
+
return;
|
|
173
|
+
}
|
|
174
|
+
filename = parts[1].split('=')[1].replace('+', ' ');
|
|
175
|
+
if (outname) filename = outname;
|
|
176
|
+
return res;
|
|
177
|
+
}).then(async (res) => {
|
|
178
|
+
if (!filename) return;
|
|
179
|
+
let filePath = path.resolve(`${path.resolve(outPath)}/${filename}`)
|
|
180
|
+
const fileStream = fs.createWriteStream(filePath);
|
|
181
|
+
await new Promise((resolve, reject) => {
|
|
182
|
+
res.body.pipe(fileStream);
|
|
183
|
+
res.body.on("error", reject);
|
|
184
|
+
fileStream.on("finish", resolve);
|
|
185
|
+
});
|
|
186
|
+
console.log(`Finished downloading`, dirPath === '/.' ? '.' : dirPath, 'Recursive=' + recursive);
|
|
187
|
+
if (!raw) {
|
|
188
|
+
let filenameWithoutExtension = filename.replace('.zip', '')
|
|
189
|
+
await extract(filePath, { dir: `${path.resolve(outPath)}/${filenameWithoutExtension === 'Base' ? '' : filenameWithoutExtension}` }, function (err) {})
|
|
190
|
+
fs.unlink(filePath, function(err) {})
|
|
191
|
+
}
|
|
192
|
+
return res;
|
|
193
|
+
});
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
async function getFilesStructure(env, user, dirPath, recursive, includeFiles) {
|
|
197
|
+
let res = await fetch(`${env.protocol}://${env.host}/Admin/Api/DirectoryAll?DirectoryPath=${dirPath ?? '/'}&recursive=${recursive ?? 'false'}&includeFiles=${includeFiles ?? 'false'}`, {
|
|
198
|
+
method: 'GET',
|
|
199
|
+
headers: {
|
|
200
|
+
'Authorization': `Bearer ${user.apiKey}`
|
|
201
|
+
},
|
|
202
|
+
agent: getAgent(env.protocol)
|
|
203
|
+
});
|
|
204
|
+
if (res.ok) {
|
|
205
|
+
return await res.json();
|
|
206
|
+
} else {
|
|
207
|
+
console.log(res);
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
export async function uploadFile(env, user, localFilePath, destinationPath) {
|
|
212
|
+
console.log('Uploading file')
|
|
213
|
+
let form = new FormData();
|
|
214
|
+
form.append('path', destinationPath);
|
|
215
|
+
form.append('files', fs.createReadStream(localFilePath));
|
|
216
|
+
let res = await fetch(`${env.protocol}://${env.host}/Admin/Api/Upload`, {
|
|
217
|
+
method: 'POST',
|
|
218
|
+
body: form,
|
|
219
|
+
headers: {
|
|
220
|
+
'Authorization': `Bearer ${user.apiKey}`
|
|
221
|
+
},
|
|
222
|
+
agent: getAgent(env.protocol)
|
|
223
|
+
});
|
|
224
|
+
if (res.ok) {
|
|
225
|
+
if (env.verbose) console.log(await res.json())
|
|
226
|
+
console.log(`File uploaded`)
|
|
227
|
+
}
|
|
228
|
+
else {
|
|
229
|
+
console.log(res)
|
|
230
|
+
return;
|
|
231
|
+
}
|
|
232
|
+
}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import fetch from 'node-fetch';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
import { setupEnv, getAgent } from './env.js';
|
|
4
|
+
import { setupUser } from './login.js';
|
|
5
|
+
import { uploadFile } from './files.js';
|
|
6
|
+
|
|
7
|
+
export function installCommand() {
|
|
8
|
+
return {
|
|
9
|
+
command: 'install [filePath]',
|
|
10
|
+
describe: 'Installs the addin on the given path, allowed file extensions are .dll, .nupkg',
|
|
11
|
+
builder: (yargs) => {
|
|
12
|
+
return yargs
|
|
13
|
+
.positional('filePath', {
|
|
14
|
+
describe: 'Path to the file to install'
|
|
15
|
+
})
|
|
16
|
+
},
|
|
17
|
+
handler: (argv) => {
|
|
18
|
+
if (argv.verbose) console.info(`Installing file located at :${argv.filePath}`)
|
|
19
|
+
handleInstall(argv)
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
async function handleInstall(argv) {
|
|
25
|
+
let env = await setupEnv(argv);
|
|
26
|
+
let user = await setupUser(argv, env);
|
|
27
|
+
let resolvedPath = path.resolve(argv.filePath)
|
|
28
|
+
await uploadFile(env, user, resolvedPath, 'System/AddIns/Local');
|
|
29
|
+
await installAddin(env, user, resolvedPath)
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
async function installAddin(env, user, resolvedPath) {
|
|
33
|
+
console.log('Installing addin')
|
|
34
|
+
let data = {
|
|
35
|
+
'AddinProvider': 'Dynamicweb.Marketplace.Providers.LocalAddinProvider',
|
|
36
|
+
'Package': path.basename(resolvedPath)
|
|
37
|
+
}
|
|
38
|
+
let res = await fetch(`${env.protocol}://${env.host}/Admin/Api/AddinInstall`, {
|
|
39
|
+
method: 'POST',
|
|
40
|
+
body: JSON.stringify(data),
|
|
41
|
+
headers: {
|
|
42
|
+
'Content-Type': 'application/json',
|
|
43
|
+
'Authorization': `Bearer ${user.apiKey}`
|
|
44
|
+
},
|
|
45
|
+
agent: getAgent(env.protocol)
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
if (res.ok) {
|
|
49
|
+
if (env.verbose) console.log(await res.json())
|
|
50
|
+
console.log(`Addin installed`)
|
|
51
|
+
}
|
|
52
|
+
else {
|
|
53
|
+
console.log(res)
|
|
54
|
+
}
|
|
55
|
+
}
|
|
@@ -0,0 +1,190 @@
|
|
|
1
|
+
import fetch from 'node-fetch';
|
|
2
|
+
import { interactiveEnv, getAgent } from './env.js'
|
|
3
|
+
import { updateConfig, getConfig } from './config.js';
|
|
4
|
+
import yargsInteractive from 'yargs-interactive';
|
|
5
|
+
|
|
6
|
+
export function loginCommand() {
|
|
7
|
+
return {
|
|
8
|
+
command: 'login [user]',
|
|
9
|
+
describe: 'If user is specified, changes the current user to the user specified, otherwise fetches an API-key that will be used to upload files and trigger addin installs, this runs with interactive mode',
|
|
10
|
+
builder: (yargs) => {
|
|
11
|
+
return yargs
|
|
12
|
+
.positional('user', {
|
|
13
|
+
describe: 'user'
|
|
14
|
+
})
|
|
15
|
+
},
|
|
16
|
+
handler: (argv) => handleLogin(argv)
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export async function setupUser(argv, env) {
|
|
21
|
+
let user;
|
|
22
|
+
if (env.users) {
|
|
23
|
+
user = env.users[argv.user] || env.users[env.current.user];
|
|
24
|
+
}
|
|
25
|
+
if (!user) {
|
|
26
|
+
console.log('Current user not set, please login')
|
|
27
|
+
await interactiveLogin(argv, {
|
|
28
|
+
environment: {
|
|
29
|
+
type: 'input',
|
|
30
|
+
default: getConfig()?.current?.env,
|
|
31
|
+
prompt: 'never'
|
|
32
|
+
},
|
|
33
|
+
username: {
|
|
34
|
+
type: 'input'
|
|
35
|
+
},
|
|
36
|
+
password: {
|
|
37
|
+
type: 'password'
|
|
38
|
+
},
|
|
39
|
+
interactive: {
|
|
40
|
+
default: true
|
|
41
|
+
}
|
|
42
|
+
})
|
|
43
|
+
user = env.users[env.current.user];
|
|
44
|
+
}
|
|
45
|
+
return user;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
async function handleLogin(argv) {
|
|
49
|
+
argv.user ? changeUser(argv) : interactiveLogin(argv, {
|
|
50
|
+
environment: {
|
|
51
|
+
type: 'input',
|
|
52
|
+
default: getConfig()?.current?.env || 'dev',
|
|
53
|
+
prompt: 'if-no-arg'
|
|
54
|
+
},
|
|
55
|
+
username: {
|
|
56
|
+
type: 'input'
|
|
57
|
+
},
|
|
58
|
+
password: {
|
|
59
|
+
type: 'password'
|
|
60
|
+
},
|
|
61
|
+
interactive: {
|
|
62
|
+
default: true
|
|
63
|
+
}
|
|
64
|
+
})
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
export async function interactiveLogin(argv, options) {
|
|
68
|
+
if (argv.verbose) console.info('Now logging in')
|
|
69
|
+
await yargsInteractive()
|
|
70
|
+
.interactive(options)
|
|
71
|
+
.then(async (result) => {
|
|
72
|
+
if (!getConfig().env || !getConfig().env[result.environment] || !getConfig().env[result.environment].host || !getConfig().env[result.environment].protocol) {
|
|
73
|
+
if (!argv.host || !argv.protocol)
|
|
74
|
+
console.log(`The environment specified is missing parameters, please specify them`)
|
|
75
|
+
await interactiveEnv(argv, {
|
|
76
|
+
environment: {
|
|
77
|
+
type: 'input',
|
|
78
|
+
default: result.environment,
|
|
79
|
+
prompt: 'never'
|
|
80
|
+
},
|
|
81
|
+
protocol: {
|
|
82
|
+
type: 'input',
|
|
83
|
+
prompt: 'if-no-arg'
|
|
84
|
+
},
|
|
85
|
+
host: {
|
|
86
|
+
type: 'input',
|
|
87
|
+
prompt: 'if-no-arg'
|
|
88
|
+
},
|
|
89
|
+
interactive: {
|
|
90
|
+
default: true
|
|
91
|
+
}
|
|
92
|
+
})
|
|
93
|
+
}
|
|
94
|
+
await loginInteractive(result);
|
|
95
|
+
});
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
async function loginInteractive(result) {
|
|
99
|
+
var protocol = getConfig().env[result.environment].protocol;
|
|
100
|
+
var token = await login(result.username, result.password, result.environment, protocol);
|
|
101
|
+
var apiKey = await getApiKey(token, result.environment, protocol)
|
|
102
|
+
getConfig().env = getConfig().env || {};
|
|
103
|
+
getConfig().env[result.environment].users = getConfig().env[result.environment].users || {};
|
|
104
|
+
getConfig().env[result.environment].users[result.username] = getConfig().env[result.environment].users[result.username] || {};
|
|
105
|
+
getConfig().env[result.environment].users[result.username].apiKey = apiKey;
|
|
106
|
+
getConfig().env[result.environment].current = getConfig().env[result.environment].current || {};
|
|
107
|
+
getConfig().env[result.environment].current.user = result.username;
|
|
108
|
+
updateConfig();
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
async function login(username, password, env, protocol) {
|
|
112
|
+
let data = new URLSearchParams();
|
|
113
|
+
data.append('Username', username);
|
|
114
|
+
data.append('Password', password);
|
|
115
|
+
var res = await fetch(`${protocol}://${getConfig().env[env].host}/Admin/Authentication/Login`, {
|
|
116
|
+
method: 'POST',
|
|
117
|
+
body: data,
|
|
118
|
+
headers: {
|
|
119
|
+
'Content-Type': 'application/x-www-form-urlencoded'
|
|
120
|
+
},
|
|
121
|
+
agent: getAgent(protocol)
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
if (res.ok) {
|
|
125
|
+
let user = parseCookies(res.headers.get('set-cookie')).user;
|
|
126
|
+
return await getToken(user, env, protocol)
|
|
127
|
+
}
|
|
128
|
+
else {
|
|
129
|
+
console.log(res)
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
function parseCookies (cookieHeader) {
|
|
134
|
+
const list = {};
|
|
135
|
+
if (!cookieHeader) return list;
|
|
136
|
+
|
|
137
|
+
cookieHeader.replace('httponly, ', '').replace('Dynamicweb.Admin', 'user').split(`;`).forEach(cookie => {
|
|
138
|
+
let [ name, ...rest] = cookie.split(`=`);
|
|
139
|
+
name = name?.trim();
|
|
140
|
+
if (!name) return;
|
|
141
|
+
const value = rest.join(`=`).trim();
|
|
142
|
+
if (!value) return;
|
|
143
|
+
list[name] = decodeURIComponent(value);
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
return list;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
async function getToken(user, env, protocol) {
|
|
150
|
+
var res = await fetch(`${getConfig().env[env].protocol}://${getConfig().env[env].host}/Admin/Authentication/Token`, {
|
|
151
|
+
method: 'GET',
|
|
152
|
+
headers: {
|
|
153
|
+
'cookie': `Dynamicweb.Admin=${user}`
|
|
154
|
+
},
|
|
155
|
+
agent: getAgent(protocol)
|
|
156
|
+
});
|
|
157
|
+
if (res.ok) {
|
|
158
|
+
return (await res.json()).token
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
async function getApiKey(token, env, protocol) {
|
|
163
|
+
let data = {
|
|
164
|
+
'Name': 'addin',
|
|
165
|
+
'Prefix': 'addin',
|
|
166
|
+
'Description': 'Auto-generated ApiKey by DW CLI'
|
|
167
|
+
};
|
|
168
|
+
var res = await fetch(`${protocol}://${getConfig().env[env].host}/Admin/Api/ApiKeySave`, {
|
|
169
|
+
method: 'POST',
|
|
170
|
+
body: JSON.stringify( { 'model': data } ),
|
|
171
|
+
headers: {
|
|
172
|
+
'Content-Type': 'application/json',
|
|
173
|
+
'Authorization': `Bearer ${token}`
|
|
174
|
+
},
|
|
175
|
+
agent: getAgent(protocol)
|
|
176
|
+
});
|
|
177
|
+
|
|
178
|
+
if (res.ok) {
|
|
179
|
+
return (await res.json()).message
|
|
180
|
+
}
|
|
181
|
+
else {
|
|
182
|
+
console.log(await res.json())
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
async function changeUser(argv) {
|
|
187
|
+
getConfig().env[getConfig().current.env].current.user = argv.user;
|
|
188
|
+
updateConfig();
|
|
189
|
+
console.log(`You're now logged in as ${getConfig().env[getConfig().current.env].current.user}`);
|
|
190
|
+
}
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
import fetch from 'node-fetch';
|
|
2
|
+
import { setupEnv, getAgent } from './env.js';
|
|
3
|
+
import { setupUser } from './login.js';
|
|
4
|
+
import yargsInteractive from 'yargs-interactive';
|
|
5
|
+
|
|
6
|
+
const exclude = ['_', '$0', 'query', 'list', 'i', 'l', 'interactive']
|
|
7
|
+
|
|
8
|
+
export function queryCommand() {
|
|
9
|
+
return {
|
|
10
|
+
command: 'query [query]',
|
|
11
|
+
describe: 'Runs the given query',
|
|
12
|
+
builder: (yargs) => {
|
|
13
|
+
return yargs
|
|
14
|
+
.positional('query', {
|
|
15
|
+
describe: 'The query to execute'
|
|
16
|
+
})
|
|
17
|
+
.option('list', {
|
|
18
|
+
alias: 'l',
|
|
19
|
+
describe: 'Lists all the properties for the query'
|
|
20
|
+
})
|
|
21
|
+
.option('interactive', {
|
|
22
|
+
alias: 'i',
|
|
23
|
+
describe: 'Runs in interactive mode to ask for query parameters one by one'
|
|
24
|
+
})
|
|
25
|
+
},
|
|
26
|
+
handler: (argv) => {
|
|
27
|
+
if (argv.verbose) console.info(`Running query ${argv.query}`)
|
|
28
|
+
handleQuery(argv)
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
async function handleQuery(argv) {
|
|
34
|
+
let env = await setupEnv(argv);
|
|
35
|
+
let user = await setupUser(argv, env);
|
|
36
|
+
if (argv.list) {
|
|
37
|
+
console.log(await getProperties(argv))
|
|
38
|
+
} else {
|
|
39
|
+
let response = await runQuery(env, user, argv.query, await getQueryParams(argv))
|
|
40
|
+
console.log(response)
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
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}`, {
|
|
49
|
+
method: 'GET',
|
|
50
|
+
headers: {
|
|
51
|
+
'Authorization': `Bearer ${user.apiKey}`
|
|
52
|
+
},
|
|
53
|
+
agent: getAgent(env.protocol)
|
|
54
|
+
})
|
|
55
|
+
if (res.ok) {
|
|
56
|
+
let body = await res.json()
|
|
57
|
+
return body.model.propertyNames
|
|
58
|
+
}
|
|
59
|
+
console.log(res)
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
async function getQueryParams(argv) {
|
|
63
|
+
let params = {}
|
|
64
|
+
if (argv.interactive) {
|
|
65
|
+
let props = { interactive: { default: true }}
|
|
66
|
+
Array.from(await getProperties(argv)).forEach(p => props[p] = { type: 'input', prompt: 'if-no-arg'})
|
|
67
|
+
await yargsInteractive()
|
|
68
|
+
.interactive(props)
|
|
69
|
+
.then((result) => {
|
|
70
|
+
Object.keys(result).filter(k => !exclude.includes(k)).forEach(k => {
|
|
71
|
+
if (result[k]) params[k] = result[k]
|
|
72
|
+
})
|
|
73
|
+
});
|
|
74
|
+
} else {
|
|
75
|
+
Object.keys(argv).filter(k => !exclude.includes(k)).forEach(k => params[k] = argv[k])
|
|
76
|
+
}
|
|
77
|
+
return params
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
async function runQuery(env, user, query, params) {
|
|
81
|
+
let res = await fetch(`${env.protocol}://${env.host}/Admin/Api/${query}?` + new URLSearchParams(params), {
|
|
82
|
+
method: 'GET',
|
|
83
|
+
headers: {
|
|
84
|
+
'Authorization': `Bearer ${user.apiKey}`
|
|
85
|
+
},
|
|
86
|
+
agent: getAgent(env.protocol)
|
|
87
|
+
})
|
|
88
|
+
if (!res.ok) {
|
|
89
|
+
console.log(`Error when doing request ${res.url}`)
|
|
90
|
+
}
|
|
91
|
+
return await res.json()
|
|
92
|
+
}
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
import { exec } from 'child_process';
|
|
2
|
+
import { Agent } from 'https';
|
|
3
|
+
import path from 'path';
|
|
4
|
+
import fetch from 'node-fetch';
|
|
5
|
+
|
|
6
|
+
const agent = new Agent({
|
|
7
|
+
rejectUnauthorized: false
|
|
8
|
+
})
|
|
9
|
+
|
|
10
|
+
export function swiftCommand() {
|
|
11
|
+
return {
|
|
12
|
+
command: 'swift [outPath]',
|
|
13
|
+
describe: 'Downloads latest swift version to outPath',
|
|
14
|
+
builder: (yargs) => {
|
|
15
|
+
return yargs
|
|
16
|
+
.positional('outPath', {
|
|
17
|
+
default: '.',
|
|
18
|
+
describe: 'Location for the swift solution'
|
|
19
|
+
})
|
|
20
|
+
.option('tag', {
|
|
21
|
+
alias: 't',
|
|
22
|
+
describe: 'The version tag or branch to clone'
|
|
23
|
+
})
|
|
24
|
+
.option('list', {
|
|
25
|
+
alias: 'l',
|
|
26
|
+
describe: 'Lists all release versions'
|
|
27
|
+
})
|
|
28
|
+
.option('nightly', {
|
|
29
|
+
alias: 'n',
|
|
30
|
+
describe: 'Will pull #HEAD, as default is latest release'
|
|
31
|
+
})
|
|
32
|
+
.option('force', {})
|
|
33
|
+
},
|
|
34
|
+
handler: (argv) => {
|
|
35
|
+
if (argv.verbose) console.info(`Downloading latest swift to :${argv.outPath}`)
|
|
36
|
+
handleSwift(argv)
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
async function handleSwift(argv) {
|
|
42
|
+
if (argv.list) {
|
|
43
|
+
console.log(await getVersions(false))
|
|
44
|
+
} else {
|
|
45
|
+
let degitCommand
|
|
46
|
+
if (argv.nightly) {
|
|
47
|
+
degitCommand = `npx degit dynamicweb/swift ${argv.force ? '--force' : ''} "${path.resolve(argv.outPath)}"`
|
|
48
|
+
} else {
|
|
49
|
+
degitCommand = `npx degit dynamicweb/swift#${argv.tag ? argv.tag : await getVersions(true)} ${argv.force ? '--force' : ''} "${path.resolve(argv.outPath)}"`
|
|
50
|
+
}
|
|
51
|
+
if (argv.verbose) console.info(`Executing command: ${degitCommand}`)
|
|
52
|
+
exec(degitCommand, (error, stdout, stderr) => {
|
|
53
|
+
if (error) {
|
|
54
|
+
console.log(`error: ${error.message}`);
|
|
55
|
+
return;
|
|
56
|
+
}
|
|
57
|
+
if (stderr) {
|
|
58
|
+
console.log(stderr);
|
|
59
|
+
return;
|
|
60
|
+
}
|
|
61
|
+
console.log(stdout);
|
|
62
|
+
});
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
async function getVersions(latest) {
|
|
67
|
+
let res = await fetch(`https://api.github.com/repos/dynamicweb/swift/releases${latest ? '/latest' : ''}`, {
|
|
68
|
+
method: 'GET',
|
|
69
|
+
agent: agent
|
|
70
|
+
});
|
|
71
|
+
if (res.ok) {
|
|
72
|
+
let body = await res.json()
|
|
73
|
+
if (latest) {
|
|
74
|
+
return body.tag_name
|
|
75
|
+
} else {
|
|
76
|
+
return body.map(a => a.tag_name)
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
}
|
package/bin/index.js
ADDED
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
#! /usr/bin/env node
|
|
2
|
+
|
|
3
|
+
import yargs from 'yargs/yargs';
|
|
4
|
+
import { hideBin } from 'yargs/helpers';
|
|
5
|
+
import { loginCommand } from './commands/login.js';
|
|
6
|
+
import { envCommand } from './commands/env.js';
|
|
7
|
+
import { configCommand, setupConfig, getConfig } from './commands/config.js';
|
|
8
|
+
import { installCommand } from './commands/install.js';
|
|
9
|
+
import { filesCommand } from './commands/files.js';
|
|
10
|
+
import { swiftCommand } from './commands/swift.js';
|
|
11
|
+
import { databaseCommand } from './commands/database.js';
|
|
12
|
+
import { queryCommand } from './commands/query.js';
|
|
13
|
+
import { commandCommand } from './commands/command.js';
|
|
14
|
+
|
|
15
|
+
setupConfig();
|
|
16
|
+
|
|
17
|
+
yargs(hideBin(process.argv))
|
|
18
|
+
.scriptName('dw')
|
|
19
|
+
.command(baseCommand())
|
|
20
|
+
.command(loginCommand())
|
|
21
|
+
.command(envCommand())
|
|
22
|
+
.command(installCommand())
|
|
23
|
+
.command(configCommand())
|
|
24
|
+
.command(filesCommand())
|
|
25
|
+
.command(swiftCommand())
|
|
26
|
+
.command(databaseCommand())
|
|
27
|
+
.command(queryCommand())
|
|
28
|
+
.command(commandCommand())
|
|
29
|
+
.option('verbose', {
|
|
30
|
+
alias: 'v',
|
|
31
|
+
type: 'boolean',
|
|
32
|
+
description: 'Run with verbose logging'
|
|
33
|
+
})
|
|
34
|
+
.demandCommand()
|
|
35
|
+
.parse()
|
|
36
|
+
|
|
37
|
+
function baseCommand() {
|
|
38
|
+
return {
|
|
39
|
+
command: '$0',
|
|
40
|
+
describe: 'Shows the current env and user being used',
|
|
41
|
+
handler: () => {
|
|
42
|
+
console.log(`Environment: ${getConfig()?.current?.env}`)
|
|
43
|
+
console.log(`User: ${getConfig()?.env[getConfig()?.current?.env]?.current?.user}`)
|
|
44
|
+
console.log(`Protocol: ${getConfig()?.env[getConfig()?.current?.env]?.protocol}`)
|
|
45
|
+
console.log(`Host: ${getConfig()?.env[getConfig()?.current?.env]?.host}`)
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
}
|
package/bin/utils.js
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import yargsInteractive from 'yargs-interactive';
|
|
2
|
+
|
|
3
|
+
export async function interactiveConfirm(question, func) {
|
|
4
|
+
await yargsInteractive()
|
|
5
|
+
.interactive({
|
|
6
|
+
confirm: {
|
|
7
|
+
type: 'confirm',
|
|
8
|
+
default: false,
|
|
9
|
+
describe: question,
|
|
10
|
+
prompt: 'always'
|
|
11
|
+
},
|
|
12
|
+
interactive: {
|
|
13
|
+
default: true
|
|
14
|
+
}
|
|
15
|
+
})
|
|
16
|
+
.then(async (result) => {
|
|
17
|
+
if (!result.confirm) return;
|
|
18
|
+
func()
|
|
19
|
+
});
|
|
20
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@dynamicweb/cli",
|
|
3
|
+
"type": "module",
|
|
4
|
+
"description": "Dynamicweb CLI is a commandline tool for interacting with Dynamicweb 10 solutions.",
|
|
5
|
+
"version": "1.0.0",
|
|
6
|
+
"main": "bin/index.js",
|
|
7
|
+
"files": [
|
|
8
|
+
"bin/*"
|
|
9
|
+
],
|
|
10
|
+
"keywords": [],
|
|
11
|
+
"scripts": {
|
|
12
|
+
"test": "echo \"Error: no test specified\" && exit 1"
|
|
13
|
+
},
|
|
14
|
+
"author": "Dynamicweb A/S (https://www.dynamicweb.com)",
|
|
15
|
+
"repository": {
|
|
16
|
+
"type": "git",
|
|
17
|
+
"url": "git+https://github.com/dynamicweb/CLI.git"
|
|
18
|
+
},
|
|
19
|
+
"contributors": [
|
|
20
|
+
"Dynamicweb A/S"
|
|
21
|
+
],
|
|
22
|
+
"license": "MIT",
|
|
23
|
+
"bin": {
|
|
24
|
+
"dw": "bin/index.js"
|
|
25
|
+
},
|
|
26
|
+
"bugs": {
|
|
27
|
+
"url": "https://github.com/dynamicweb/CLI/issues"
|
|
28
|
+
},
|
|
29
|
+
"dependencies": {
|
|
30
|
+
"child_process": "^1.0.2",
|
|
31
|
+
"degit": "^2.8.4",
|
|
32
|
+
"extract-zip": "^2.0.1",
|
|
33
|
+
"fetch": "^1.1.0",
|
|
34
|
+
"form-data": "^4.0.0",
|
|
35
|
+
"https": "^1.0.0",
|
|
36
|
+
"node-fetch": "^3.2.10",
|
|
37
|
+
"path": "^0.12.7",
|
|
38
|
+
"yargs": "^17.5.1",
|
|
39
|
+
"yargs-interactive": "^3.0.1"
|
|
40
|
+
}
|
|
41
|
+
}
|