From ed74ff55ed998382d70476361d36d861652df3c2 Mon Sep 17 00:00:00 2001 From: frederik5480 Date: Fri, 19 Aug 2022 15:00:00 +0200 Subject: [PATCH 01/86] Create README.md --- README.md | 6 ++++++ 1 file changed, 6 insertions(+) create mode 100644 README.md diff --git a/README.md b/README.md new file mode 100644 index 0000000..bb3a955 --- /dev/null +++ b/README.md @@ -0,0 +1,6 @@ +# DW CLI + +## Get started +To install after cloning, move to project dir and run +> npm install -g . + From eb1bfa71d07bb8f018072048825ffcf488736bc1 Mon Sep 17 00:00:00 2001 From: Frederik Nielsen Date: Tue, 23 Aug 2022 09:40:13 +0200 Subject: [PATCH 02/86] Fixing bug in login command, adding query and command commands for generic, UserStory: 8747 --- bin/commands/command.js | 93 +++++++++++++++++++++++++++++++++++++++++ bin/commands/env.js | 6 --- bin/commands/files.js | 10 ++--- bin/commands/login.js | 6 +++ bin/commands/query.js | 76 +++++++++++++++++++++++++++++++++ bin/commands/swift.js | 60 +++++++++++++++++++++----- bin/index.js | 4 ++ 7 files changed, 233 insertions(+), 22 deletions(-) create mode 100644 bin/commands/command.js create mode 100644 bin/commands/query.js diff --git a/bin/commands/command.js b/bin/commands/command.js new file mode 100644 index 0000000..6f260fa --- /dev/null +++ b/bin/commands/command.js @@ -0,0 +1,93 @@ +import { Agent } from 'https'; +import fetch from 'node-fetch'; +import path from 'path'; +import fs from 'fs'; +import { setupEnv } from './env.js'; +import { setupUser } from './login.js'; + +const agent = new Agent({ + rejectUnauthorized: false +}) + +const exclude = ['_', '$0', 'command', 'list', 'json'] + +export function commandCommand() { + return { + command: 'command [command]', + describe: 'Runs the given command', + builder: (yargs) => { + return yargs + .positional('command', { + describe: 'The command to execute' + }) + .option('json', { + describe: 'Literal json or location of json file to send' + }) + .option('list', { + alias: 'l', + describe: 'Lists all the properties for the command, currently not working' + }) + }, + handler: (argv) => { + if (argv.verbose) console.info(`Running command ${argv.command}`) + handleCommand(argv) + } + } +} + +async function handleCommand(argv) { + let env = await setupEnv(argv); + let user = await setupUser(argv, env); + if (argv.list) { + console.log(await getProperties(env, user, argv.command)) + } else { + let response = await runCommand(env, user, argv.command, getQueryParams(argv), getFormParams(argv.json)) + console.log(response) + } +} + +async function getProperties(env, user, command) { + return `This option currently doesn't work` + let res = await fetch(`https://${env.host}/Admin/Api/CommandByName?name=${command}`, { + method: 'GET', + headers: { + 'Authorization': `Bearer ${user.apiKey}` + }, + agent: agent + }) + if (res.ok) { + let body = await res.json() + return body.model.propertyNames + } +} + +function getQueryParams(argv) { + let params = {} + Object.keys(argv).filter(k => !exclude.includes(k)).forEach(k => params['Command.' + k] = argv[k]) + return params +} + +function getFormParams(json) { + if (!json) return + if (fs.existsSync(json)) { + return JSON.parse(fs.readFileSync(path.resolve(json))) + } else { + return JSON.parse(json) + } +} + +async function runCommand(env, user, command, queryParams, formParams) { + let res = await fetch(`https://${env.host}/Admin/Api/${command}?` + new URLSearchParams(queryParams), { + method: 'POST', + body: new URLSearchParams(formParams), + headers: { + 'Authorization': `Bearer ${user.apiKey}`, + 'Content-Type': 'application/x-www-form-urlencoded' + }, + agent: agent + }) + if (!res.ok) { + console.log(`Error when doing request ${res.url}`) + } + return await res.json() +} \ No newline at end of file diff --git a/bin/commands/env.js b/bin/commands/env.js index ef6e555..31a672b 100644 --- a/bin/commands/env.js +++ b/bin/commands/env.js @@ -108,10 +108,4 @@ async function changeEnv(argv) { updateConfig(); console.log(`Your current environment is now ${getConfig().current.env}`); } -} - -async function changeUser(argv) { - getConfig().env[getConfig().current.env].current.user = argv.user; - updateConfig(); - console.log(`You're now logged in as ${getConfig().env[getConfig().current.env].current.user}`); } \ No newline at end of file diff --git a/bin/commands/files.js b/bin/commands/files.js index 18bafc7..55994ff 100644 --- a/bin/commands/files.js +++ b/bin/commands/files.js @@ -26,26 +26,26 @@ export function filesCommand() { .option('list', { alias: 'l', type: 'boolean', - description: 'Lists all directories and files' + describe: 'Lists all directories and files' }) .option('export', { alias: 'e', type: 'boolean', - description: 'Exports the directory at [dirPath] to [outPath]' + describe: 'Exports the directory at [dirPath] to [outPath]' }) .option('includeFiles', { alias: 'f', type: 'boolean', - description: 'Includes files in list of directories and files' + describe: 'Includes files in list of directories and files' }) .option('recursive', { alias: 'r', type: 'boolean', - description: 'Handles all directories recursively' + describe: 'Handles all directories recursively' }) .option('iamstupid', { type: 'boolean', - description: 'Includes export of log and cache folders, NOT RECOMMENDED' + describe: 'Includes export of log and cache folders, NOT RECOMMENDED' }) }, handler: (argv) => { diff --git a/bin/commands/login.js b/bin/commands/login.js index 58d8756..bf13f90 100644 --- a/bin/commands/login.js +++ b/bin/commands/login.js @@ -180,4 +180,10 @@ async function getApiKey(token, env) { else { console.log(res) } +} + +async function changeUser(argv) { + getConfig().env[getConfig().current.env].current.user = argv.user; + updateConfig(); + console.log(`You're now logged in as ${getConfig().env[getConfig().current.env].current.user}`); } \ No newline at end of file diff --git a/bin/commands/query.js b/bin/commands/query.js new file mode 100644 index 0000000..d52b6f6 --- /dev/null +++ b/bin/commands/query.js @@ -0,0 +1,76 @@ +import { Agent } from 'https'; +import fetch from 'node-fetch'; +import { setupEnv } from './env.js'; +import { setupUser } from './login.js'; + +const agent = new Agent({ + rejectUnauthorized: false +}) + +const exclude = ['_', '$0', 'query', 'list'] + +export function queryCommand() { + return { + command: 'query [query]', + describe: 'Runs the given query', + builder: (yargs) => { + return yargs + .positional('query', { + describe: 'The query to execute' + }) + .option('list', { + alias: 'l', + describe: 'Lists all the properties for the query' + }) + }, + handler: (argv) => { + if (argv.verbose) console.info(`Running query ${argv.query}`) + handleQuery(argv) + } + } +} + +async function handleQuery(argv) { + let env = await setupEnv(argv); + let user = await setupUser(argv, env); + if (argv.list) { + console.log(await getProperties(env, user, argv.query)) + } else { + let response = await runQuery(env, user, argv.query, getQueryParams(argv)) + console.log(response) + } +} + +async function getProperties(env, user, query) { + let res = await fetch(`https://${env.host}/Admin/Api/QueryByName?name=${query}`, { + method: 'GET', + headers: { + 'Authorization': `Bearer ${user.apiKey}` + }, + agent: agent + }) + if (res.ok) { + let body = await res.json() + return body.model.propertyNames + } +} + +function getQueryParams(argv) { + let params = {} + Object.keys(argv).filter(k => !exclude.includes(k)).forEach(k => params[k] = argv[k]) + return params +} + +async function runQuery(env, user, query, params) { + let res = await fetch(`https://${env.host}/Admin/Api/${query}?` + new URLSearchParams(params), { + method: 'GET', + headers: { + 'Authorization': `Bearer ${user.apiKey}` + }, + agent: agent + }) + if (!res.ok) { + console.log(`Error when doing request ${res.url}`) + } + return await res.json() +} \ No newline at end of file diff --git a/bin/commands/swift.js b/bin/commands/swift.js index dffb3eb..d0ff6e5 100644 --- a/bin/commands/swift.js +++ b/bin/commands/swift.js @@ -1,4 +1,10 @@ import { exec } from 'child_process'; +import { Agent } from 'https'; +import fetch from 'node-fetch'; + +const agent = new Agent({ + rejectUnauthorized: false +}) export function swiftCommand() { return { @@ -11,7 +17,14 @@ export function swiftCommand() { }) .option('tag', { alias: 't', - description: 'The version tag or branch to clone' + describe: 'The version tag or branch to clone' + }) + .option('list', { + alias: 'l', + describe: 'Lists all release versions' + }) + .option('latest', { + describe: 'Only covers latest and overrides tag with latest' }) .option('force', {}) }, @@ -23,16 +36,41 @@ export function swiftCommand() { } async function handleSwift(argv) { - if (argv.verbose) console.info(`Executing command: degit dynamicweb/swift${argv.tag ? '#' + argv.tag : ''} ${argv.force ? '--force' : ''} ${argv.outPath}`) - exec(`npx degit dynamicweb/swift${argv.tag ? '#' + argv.tag : ''} ${argv.force ? '--force' : ''} ${argv.outPath}`, (error, stdout, stderr) => { - if (error) { - console.log(`error: ${error.message}`); - return; + if (argv.list) { + console.log(await getVersions(argv.latest)) + } else { + let degitCommand + if (argv.latest) { + degitCommand = `npx degit dynamicweb/swift#${await getVersions(argv.latest)} ${argv.force ? '--force' : ''} ${argv.outPath}` + } else { + degitCommand = `npx degit dynamicweb/swift${argv.tag ? '#' + argv.tag : ''} ${argv.force ? '--force' : ''} ${argv.outPath}` } - if (stderr) { - console.log(stderr); - return; - } - console.log(stdout); + if (argv.verbose) console.info(`Executing command: ${degitCommand}`) + exec(`${degitCommand}`, (error, stdout, stderr) => { + if (error) { + console.log(`error: ${error.message}`); + return; + } + if (stderr) { + console.log(stderr); + return; + } + console.log(stdout); + }); + } +} + +async function getVersions(latest) { + let res = await fetch(`https://api.github.com/repos/dynamicweb/swift/releases${latest ? '/latest' : ''}`, { + method: 'GET', + agent: agent }); + if (res.ok) { + let body = await res.json() + if (latest) { + return body.tag_name + } else { + return body.map(a => a.tag_name) + } + } } \ No newline at end of file diff --git a/bin/index.js b/bin/index.js index a9753c8..43ea10e 100644 --- a/bin/index.js +++ b/bin/index.js @@ -9,6 +9,8 @@ import { installCommand } from './commands/install.js'; import { filesCommand } from './commands/files.js'; import { swiftCommand } from './commands/swift.js'; import { databaseCommand } from './commands/database.js'; +import { queryCommand } from './commands/query.js'; +import { commandCommand } from './commands/command.js'; setupConfig(); @@ -22,6 +24,8 @@ yargs(hideBin(process.argv)) .command(filesCommand()) .command(swiftCommand()) .command(databaseCommand()) + .command(queryCommand()) + .command(commandCommand()) .option('verbose', { alias: 'v', type: 'boolean', From 8154759b4c20be5d3462d71df4bbd259ee03a8d7 Mon Sep 17 00:00:00 2001 From: Frederik Nielsen Date: Tue, 23 Aug 2022 12:25:49 +0200 Subject: [PATCH 03/86] Added interactive mode for queries, this will ask for each query param one by one without the user having to specify them --- bin/commands/query.js | 35 ++++++++++++++++++++++++++++------- 1 file changed, 28 insertions(+), 7 deletions(-) diff --git a/bin/commands/query.js b/bin/commands/query.js index d52b6f6..691f5f7 100644 --- a/bin/commands/query.js +++ b/bin/commands/query.js @@ -2,12 +2,13 @@ import { Agent } from 'https'; import fetch from 'node-fetch'; import { setupEnv } from './env.js'; import { setupUser } from './login.js'; +import yargsInteractive from 'yargs-interactive'; const agent = new Agent({ rejectUnauthorized: false }) -const exclude = ['_', '$0', 'query', 'list'] +const exclude = ['_', '$0', 'query', 'list', 'i', 'l', 'interactive'] export function queryCommand() { return { @@ -22,6 +23,10 @@ export function queryCommand() { alias: 'l', describe: 'Lists all the properties for the query' }) + .option('interactive', { + alias: 'i', + describe: 'Runs in interactive mode to ask for query parameters one by one' + }) }, handler: (argv) => { if (argv.verbose) console.info(`Running query ${argv.query}`) @@ -34,15 +39,18 @@ async function handleQuery(argv) { let env = await setupEnv(argv); let user = await setupUser(argv, env); if (argv.list) { - console.log(await getProperties(env, user, argv.query)) + console.log(await getProperties(argv)) } else { - let response = await runQuery(env, user, argv.query, getQueryParams(argv)) + let response = await runQuery(env, user, argv.query, await getQueryParams(argv)) console.log(response) } } -async function getProperties(env, user, query) { - let res = await fetch(`https://${env.host}/Admin/Api/QueryByName?name=${query}`, { +async function getProperties(argv) { + let env = await setupEnv(argv); + let user = await setupUser(argv, env); + + let res = await fetch(`https://${env.host}/Admin/Api/QueryByName?name=${argv.query}`, { method: 'GET', headers: { 'Authorization': `Bearer ${user.apiKey}` @@ -53,11 +61,24 @@ async function getProperties(env, user, query) { let body = await res.json() return body.model.propertyNames } + console.log(res) } -function getQueryParams(argv) { +async function getQueryParams(argv) { let params = {} - Object.keys(argv).filter(k => !exclude.includes(k)).forEach(k => params[k] = argv[k]) + if (argv.interactive) { + let props = { interactive: { default: true }} + Array.from(await getProperties(argv)).forEach(p => props[p] = { type: 'input', prompt: 'if-no-arg'}) + await yargsInteractive() + .interactive(props) + .then((result) => { + Object.keys(result).filter(k => !exclude.includes(k)).forEach(k => { + if (result[k]) params[k] = result[k] + }) + }); + } else { + Object.keys(argv).filter(k => !exclude.includes(k)).forEach(k => params[k] = argv[k]) + } return params } From cf36e84f250c27b80bbef1fbf703e0f137b61dea Mon Sep 17 00:00:00 2001 From: Frederik Nielsen Date: Tue, 23 Aug 2022 14:25:48 +0200 Subject: [PATCH 04/86] Updating readme --- README.md | 153 +++++++++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 151 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index bb3a955..cb889a4 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,155 @@ -# DW CLI +# DynamicWeb CLI ## Get started To install after cloning, move to project dir and run -> npm install -g . +> $ npm install -g . +## Commands +All commands and options can be viewed by running +> $ dw --help +> +> $ dw \ --help + +### Users and environments +As most commands are pulling or pushing data from the DW admin API, the necessary authorization is required. + +To generate an Api-key that the CLI will use, login to your environment +> $ dw login + +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. +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`. + +Each environment has its own users, and each user has its own Api-key assigned to it, swap between environments by using +> $ dw env \ + +and swap between users by simply supplying the name of the user in the login command +> $ dw login \ + +You can view the current environment and user being used by simply typing +> $ dw + +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 +```json +{ + "env": { + "dev": { + "host": "localhost:6001", + "users": { + "DemoUser": { + "apiKey": "." + } + }, + "current": { + "user": "DemoUser" + } + } + }, + "current": { + "env": "dev" + } +} +``` + +### Files +> $ dw files \ \ + +The files command is used to list out and export the structure in your Dynamicweb files archive, as such is has multiple options; +- -l --list This will list the directory given in \ +- -f --includeFiles The list will now also show all files in the directories +- -r --recursive By default it only handles the \, but with this option it will handle all directories under this recursively +- -e --export It will export \ into \ on your local machine, unzipped by default +- --raw This will keep the content zipped +- --iamstupid This will include the export of the /files/system/log and /files/.cache folders + +#### Examples +Exporting all templates from current environment to local solution +> $ cd DynamicWebSolution/Files +> +> $ dw files /templates ./templates + +Listing the system files structure of the current environment +> $ dw files system -lr + +### Swift +> $ dw swift \ + +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; +- -t --tag \ The tag/branch/release to pull +- -l --list Will list all the release versions +- -n --nightly Will pull #HEAD, as default is latest release +- --force Used if \ is not an empty folder, to override all the content + +#### Examples +Getting all the available releases +> $ dw swift -l + +Pulling and overriding local solution with latest nightly build +> $ cd DynamicWebSolution/Swift +> +> $ dw swift . -n --force + +### Query +> $ dw query \ + +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; +- -l --list Will list all the properties for the given \ +- -i --interactive Will perform the \ but without any parameters, as they will be asked for one by one in interactive mode +- --\ Any parameter the query needs will be sent by '--key value' + +#### Examples +Getting all properties for a query +> $ dw query FileByName -l + +Getting file information on a specific file by name +> $ dw query FileByName --name DefaultMail.html --directorypath /Templates/Forms/Mail + +### Command +> $ dw command \ + +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. +- -l --list Lists all the properties for the command, as well as the json model required **currently not working** +- --json Takes a path to a .json file or a literal json, i.e --json '{ abc: "123" }' + +#### Examples +Creating a copy of a page using a json-string +> $ dw command PageCopy --json '{ "SourcePageId": 1189, "DestinationParentPageId": 1129 }' + +Removing a page using a json file +> $ dw command PageMove --json ./PageMove.json + +Where PageMove.json contains +```json +{ "SourcePageId": 1383, "DestinationParentPageId": 1376 } +``` + +Deleting a page +> $ dw command PageDelete --id 1383 + +### Install +> $ dw install \ + +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. + +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. + +#### Examples +> $ dw install ./bin/Release/net6.0/CustomProject.dll + +### Database +> $ dw database \ + +This command is used for various actions towards your current environments database. +- -e --export Exports your current environments database to a .bacpac file at \ + +#### Examples +> $ dw database -e ./backup + +### Config +> $ dw config + +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. +- --\ The path and name of the property to set + +#### Examples +Changing the host for the dev environment +> $ dw config --env.dev.host localhost:6001 \ No newline at end of file From fdd6ca3dfc89c17daf3370f41f09d95016b77366 Mon Sep 17 00:00:00 2001 From: Frederik Nielsen Date: Tue, 23 Aug 2022 14:31:20 +0200 Subject: [PATCH 05/86] Removing latest option from swift, adding nightly option to swift --- bin/commands/swift.js | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/bin/commands/swift.js b/bin/commands/swift.js index d0ff6e5..21a29fb 100644 --- a/bin/commands/swift.js +++ b/bin/commands/swift.js @@ -13,6 +13,7 @@ export function swiftCommand() { builder: (yargs) => { return yargs .positional('outPath', { + default: '.', describe: 'Location for the swift solution' }) .option('tag', { @@ -23,8 +24,9 @@ export function swiftCommand() { alias: 'l', describe: 'Lists all release versions' }) - .option('latest', { - describe: 'Only covers latest and overrides tag with latest' + .option('nightly', { + alias: 'n', + describe: 'Will pull #HEAD, as default is latest release' }) .option('force', {}) }, @@ -37,13 +39,13 @@ export function swiftCommand() { async function handleSwift(argv) { if (argv.list) { - console.log(await getVersions(argv.latest)) + console.log(await getVersions(false)) } else { let degitCommand - if (argv.latest) { - degitCommand = `npx degit dynamicweb/swift#${await getVersions(argv.latest)} ${argv.force ? '--force' : ''} ${argv.outPath}` + if (argv.nightly) { + degitCommand = `npx degit dynamicweb/swift ${argv.force ? '--force' : ''} ${argv.outPath}` } else { - degitCommand = `npx degit dynamicweb/swift${argv.tag ? '#' + argv.tag : ''} ${argv.force ? '--force' : ''} ${argv.outPath}` + degitCommand = `npx degit dynamicweb/swift#${argv.tag ? argv.tag : await getVersions(true)} ${argv.force ? '--force' : ''} ${argv.outPath}` } if (argv.verbose) console.info(`Executing command: ${degitCommand}`) exec(`${degitCommand}`, (error, stdout, stderr) => { From 05268d560ea62d76ca7d0eb323e8d1159dc0e1a5 Mon Sep 17 00:00:00 2001 From: Frederik Nielsen Date: Tue, 23 Aug 2022 15:22:30 +0200 Subject: [PATCH 06/86] Files are now being unzipped when extracting them --- bin/commands/files.js | 12 ++- package-lock.json | 237 +++++++++++++++++++++++++++++++++++++++++- package.json | 1 + 3 files changed, 246 insertions(+), 4 deletions(-) diff --git a/bin/commands/files.js b/bin/commands/files.js index 55994ff..87cbbc3 100644 --- a/bin/commands/files.js +++ b/bin/commands/files.js @@ -1,6 +1,7 @@ import fetch from 'node-fetch'; import path from 'path'; import fs from 'fs'; +import extract from 'extract-zip'; import { Agent } from 'https'; import { setupEnv } from './env.js'; import { setupUser } from './login.js'; @@ -43,6 +44,10 @@ export function filesCommand() { type: 'boolean', describe: 'Handles all directories recursively' }) + .option('raw', { + type: 'boolean', + describe: 'Keeps zip file instead of unpacking it' + }) .option('iamstupid', { type: 'boolean', describe: 'Includes export of log and cache folders, NOT RECOMMENDED' @@ -78,7 +83,7 @@ async function handleFiles(argv) { const dir = dirs[id]; await download(env, user, dir.name, argv.outPath, true, null, argv.iamstupid); } - await download(env, user, '', argv.outPath, false, 'Base.zip'); + await download(env, user, '', argv.outPath, false, 'Base.zip', argv.iamstupid); console.log('The files in the base "files" folder is in Base.zip, each directory in "files" is in its own zip') }) } @@ -150,7 +155,7 @@ async function download(env, user, dirPath, outPath, recursive, outname, iamstup console.log(`No files found in directory '${dirPath}', if you want to download all folders recursively include the -r flag`); return; } - filename = parts[1].split('=')[1]; + filename = parts[1].split('=')[1].replace('+', ' '); if (outname) filename = outname; return res; }).then(async (res) => { @@ -162,6 +167,9 @@ async function download(env, user, dirPath, outPath, recursive, outname, iamstup fileStream.on("finish", resolve); }); console.log(`Finished downloading`, dirPath === '' ? '.' : dirPath, 'Recursive=' + recursive); + let filenameWithoutExtension = filename.replace('.zip', '') + await extract(filename, { dir: `${path.resolve(outPath)}/${filenameWithoutExtension === 'Base' ? '' : filenameWithoutExtension}` }, function (err) {}) + fs.unlink(filename, function(err) {}) return res; }); } diff --git a/package-lock.json b/package-lock.json index c62a2cd..aef1aa8 100644 --- a/package-lock.json +++ b/package-lock.json @@ -11,6 +11,7 @@ "dependencies": { "child_process": "^1.0.2", "degit": "^2.8.4", + "extract-zip": "^2.0.1", "fetch": "^1.1.0", "form-data": "^4.0.0", "https": "^1.0.0", @@ -21,8 +22,22 @@ }, "bin": { "dw": "bin/index.js" - }, - "devDependencies": {} + } + }, + "node_modules/@types/node": { + "version": "18.7.11", + "resolved": "https://registry.npmjs.org/@types/node/-/node-18.7.11.tgz", + "integrity": "sha512-KZhFpSLlmK/sdocfSAjqPETTMd0ug6HIMIAwkwUpU79olnZdQtMxpQP+G1wDzCH7na+FltSIhbaZuKdwZ8RDrw==", + "optional": true + }, + "node_modules/@types/yauzl": { + "version": "2.10.0", + "resolved": "https://registry.npmjs.org/@types/yauzl/-/yauzl-2.10.0.tgz", + "integrity": "sha512-Cn6WYCm0tXv8p6k+A8PvbDG763EDpBoTzHdA+Q/MF6H3sapGjCm9NzoaJncJS9tUKSuCoDs9XHxYYsQDgxR6kw==", + "optional": true, + "dependencies": { + "@types/node": "*" + } }, "node_modules/ansi-escapes": { "version": "4.3.2", @@ -76,6 +91,14 @@ "node": ">=1.0.0" } }, + "node_modules/buffer-crc32": { + "version": "0.2.13", + "resolved": "https://registry.npmjs.org/buffer-crc32/-/buffer-crc32-0.2.13.tgz", + "integrity": "sha512-VO9Ht/+p3SN7SKWqcrgEzjGbRSJYTx+Q1pTQC0wrWqHx0vpJraQ6GtHx8tvcg1rlK1byhU5gccxgOgj7B0TDkQ==", + "engines": { + "node": "*" + } + }, "node_modules/camelcase": { "version": "5.3.1", "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-5.3.1.tgz", @@ -173,6 +196,22 @@ "node": ">= 12" } }, + "node_modules/debug": { + "version": "4.3.4", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", + "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", + "dependencies": { + "ms": "2.1.2" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, "node_modules/decamelize": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/decamelize/-/decamelize-1.2.0.tgz", @@ -213,6 +252,14 @@ "iconv-lite": "~0.4.13" } }, + "node_modules/end-of-stream": { + "version": "1.4.4", + "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.4.tgz", + "integrity": "sha512-+uw1inIHVPQoaVuHzRyXd21icM+cnt4CzD5rW+NC1wjOUSTOs+Te7FOv7AhN7vS9x/oIyhLP5PR1H+phQAHu5Q==", + "dependencies": { + "once": "^1.4.0" + } + }, "node_modules/escalade": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.1.1.tgz", @@ -242,6 +289,33 @@ "node": ">=4" } }, + "node_modules/extract-zip": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/extract-zip/-/extract-zip-2.0.1.tgz", + "integrity": "sha512-GDhU9ntwuKyGXdZBUgTIe+vXnWj0fppUEtMDL0+idd5Sta8TGpHssn/eusA9mrPr9qNDym6SxAYZjNvCn/9RBg==", + "dependencies": { + "debug": "^4.1.1", + "get-stream": "^5.1.0", + "yauzl": "^2.10.0" + }, + "bin": { + "extract-zip": "cli.js" + }, + "engines": { + "node": ">= 10.17.0" + }, + "optionalDependencies": { + "@types/yauzl": "^2.9.1" + } + }, + "node_modules/fd-slicer": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/fd-slicer/-/fd-slicer-1.1.0.tgz", + "integrity": "sha512-cE1qsB/VwyQozZ+q1dGxR8LBYNZeofhEdUNGSMbQD3Gw2lAzX9Zb3uIU6Ebc/Fmyjo9AWWfnn0AUCHqtevs/8g==", + "dependencies": { + "pend": "~1.2.0" + } + }, "node_modules/fetch": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/fetch/-/fetch-1.1.0.tgz", @@ -330,6 +404,20 @@ "node": "6.* || 8.* || >= 10.*" } }, + "node_modules/get-stream": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-5.2.0.tgz", + "integrity": "sha512-nBF+F1rAZVCu/p7rjzgA+Yb4lfYXrpl7a6VmJrU8wF9I1CKvP/QwPNZHnOlwbTkY6dvtFIzFMSyQXbLoTQPRpA==", + "dependencies": { + "pump": "^3.0.0" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/has-flag": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", @@ -434,6 +522,11 @@ "node": ">=6" } }, + "node_modules/ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" + }, "node_modules/mute-stream": { "version": "0.0.8", "resolved": "https://registry.npmjs.org/mute-stream/-/mute-stream-0.0.8.tgz", @@ -474,6 +567,14 @@ "url": "https://opencollective.com/node-fetch" } }, + "node_modules/once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", + "dependencies": { + "wrappy": "1" + } + }, "node_modules/onetime": { "version": "5.1.2", "resolved": "https://registry.npmjs.org/onetime/-/onetime-5.1.2.tgz", @@ -546,6 +647,11 @@ "node": ">=4" } }, + "node_modules/pend": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/pend/-/pend-1.2.0.tgz", + "integrity": "sha512-F3asv42UuXchdzt+xXqfW1OGlVBe+mxa2mqI0pg5yAHZPvFmY3Y6drSf/GQ1A86WgWEN9Kzh/WrgKa6iGcHXLg==" + }, "node_modules/process": { "version": "0.11.10", "resolved": "https://registry.npmjs.org/process/-/process-0.11.10.tgz", @@ -559,6 +665,15 @@ "resolved": "https://registry.npmjs.org/psl/-/psl-1.9.0.tgz", "integrity": "sha512-E/ZsdU4HLs/68gYzgGTkMicWTLPdAftJLfJFlLUAAKZGkStNU72sZjT66SnMDVOfOWY/YAoiD7Jxa9iHvngcag==" }, + "node_modules/pump": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.0.tgz", + "integrity": "sha512-LwZy+p3SFs1Pytd/jYct4wpv49HiYCqd9Rlc5ZVdk0V+8Yzv6jR5Blk3TRmPL1ft69TxP0IMZGJ+WPFU2BFhww==", + "dependencies": { + "end-of-stream": "^1.1.0", + "once": "^1.3.1" + } + }, "node_modules/require-directory": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", @@ -722,6 +837,11 @@ "url": "https://github.com/chalk/wrap-ansi?sponsor=1" } }, + "node_modules/wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==" + }, "node_modules/y18n": { "version": "5.0.8", "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", @@ -891,9 +1011,33 @@ "engines": { "node": ">=12" } + }, + "node_modules/yauzl": { + "version": "2.10.0", + "resolved": "https://registry.npmjs.org/yauzl/-/yauzl-2.10.0.tgz", + "integrity": "sha512-p4a9I6X6nu6IhoGmBqAcbJy1mlC4j27vEPZX9F4L4/vZT3Lyq1VkFHw/V/PUcB9Buo+DG3iHkT0x3Qya58zc3g==", + "dependencies": { + "buffer-crc32": "~0.2.3", + "fd-slicer": "~1.1.0" + } } }, "dependencies": { + "@types/node": { + "version": "18.7.11", + "resolved": "https://registry.npmjs.org/@types/node/-/node-18.7.11.tgz", + "integrity": "sha512-KZhFpSLlmK/sdocfSAjqPETTMd0ug6HIMIAwkwUpU79olnZdQtMxpQP+G1wDzCH7na+FltSIhbaZuKdwZ8RDrw==", + "optional": true + }, + "@types/yauzl": { + "version": "2.10.0", + "resolved": "https://registry.npmjs.org/@types/yauzl/-/yauzl-2.10.0.tgz", + "integrity": "sha512-Cn6WYCm0tXv8p6k+A8PvbDG763EDpBoTzHdA+Q/MF6H3sapGjCm9NzoaJncJS9tUKSuCoDs9XHxYYsQDgxR6kw==", + "optional": true, + "requires": { + "@types/node": "*" + } + }, "ansi-escapes": { "version": "4.3.2", "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-4.3.2.tgz", @@ -928,6 +1072,11 @@ "psl": "^1.1.7" } }, + "buffer-crc32": { + "version": "0.2.13", + "resolved": "https://registry.npmjs.org/buffer-crc32/-/buffer-crc32-0.2.13.tgz", + "integrity": "sha512-VO9Ht/+p3SN7SKWqcrgEzjGbRSJYTx+Q1pTQC0wrWqHx0vpJraQ6GtHx8tvcg1rlK1byhU5gccxgOgj7B0TDkQ==" + }, "camelcase": { "version": "5.3.1", "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-5.3.1.tgz", @@ -1001,6 +1150,14 @@ "resolved": "https://registry.npmjs.org/data-uri-to-buffer/-/data-uri-to-buffer-4.0.0.tgz", "integrity": "sha512-Vr3mLBA8qWmcuschSLAOogKgQ/Jwxulv3RNE4FXnYWRGujzrRWQI4m12fQqRkwX06C0KanhLr4hK+GydchZsaA==" }, + "debug": { + "version": "4.3.4", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", + "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", + "requires": { + "ms": "2.1.2" + } + }, "decamelize": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/decamelize/-/decamelize-1.2.0.tgz", @@ -1029,6 +1186,14 @@ "iconv-lite": "~0.4.13" } }, + "end-of-stream": { + "version": "1.4.4", + "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.4.tgz", + "integrity": "sha512-+uw1inIHVPQoaVuHzRyXd21icM+cnt4CzD5rW+NC1wjOUSTOs+Te7FOv7AhN7vS9x/oIyhLP5PR1H+phQAHu5Q==", + "requires": { + "once": "^1.4.0" + } + }, "escalade": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.1.1.tgz", @@ -1049,6 +1214,25 @@ "tmp": "^0.0.33" } }, + "extract-zip": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/extract-zip/-/extract-zip-2.0.1.tgz", + "integrity": "sha512-GDhU9ntwuKyGXdZBUgTIe+vXnWj0fppUEtMDL0+idd5Sta8TGpHssn/eusA9mrPr9qNDym6SxAYZjNvCn/9RBg==", + "requires": { + "@types/yauzl": "^2.9.1", + "debug": "^4.1.1", + "get-stream": "^5.1.0", + "yauzl": "^2.10.0" + } + }, + "fd-slicer": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/fd-slicer/-/fd-slicer-1.1.0.tgz", + "integrity": "sha512-cE1qsB/VwyQozZ+q1dGxR8LBYNZeofhEdUNGSMbQD3Gw2lAzX9Zb3uIU6Ebc/Fmyjo9AWWfnn0AUCHqtevs/8g==", + "requires": { + "pend": "~1.2.0" + } + }, "fetch": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/fetch/-/fetch-1.1.0.tgz", @@ -1106,6 +1290,14 @@ "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==" }, + "get-stream": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-5.2.0.tgz", + "integrity": "sha512-nBF+F1rAZVCu/p7rjzgA+Yb4lfYXrpl7a6VmJrU8wF9I1CKvP/QwPNZHnOlwbTkY6dvtFIzFMSyQXbLoTQPRpA==", + "requires": { + "pump": "^3.0.0" + } + }, "has-flag": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", @@ -1186,6 +1378,11 @@ "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-2.1.0.tgz", "integrity": "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==" }, + "ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" + }, "mute-stream": { "version": "0.0.8", "resolved": "https://registry.npmjs.org/mute-stream/-/mute-stream-0.0.8.tgz", @@ -1206,6 +1403,14 @@ "formdata-polyfill": "^4.0.10" } }, + "once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", + "requires": { + "wrappy": "1" + } + }, "onetime": { "version": "5.1.2", "resolved": "https://registry.npmjs.org/onetime/-/onetime-5.1.2.tgz", @@ -1254,6 +1459,11 @@ "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-3.0.0.tgz", "integrity": "sha512-bpC7GYwiDYQ4wYLe+FA8lhRjhQCMcQGuSgGGqDkg/QerRWw9CmGRT0iSOVRSZJ29NMLZgIzqaljJ63oaL4NIJQ==" }, + "pend": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/pend/-/pend-1.2.0.tgz", + "integrity": "sha512-F3asv42UuXchdzt+xXqfW1OGlVBe+mxa2mqI0pg5yAHZPvFmY3Y6drSf/GQ1A86WgWEN9Kzh/WrgKa6iGcHXLg==" + }, "process": { "version": "0.11.10", "resolved": "https://registry.npmjs.org/process/-/process-0.11.10.tgz", @@ -1264,6 +1474,15 @@ "resolved": "https://registry.npmjs.org/psl/-/psl-1.9.0.tgz", "integrity": "sha512-E/ZsdU4HLs/68gYzgGTkMicWTLPdAftJLfJFlLUAAKZGkStNU72sZjT66SnMDVOfOWY/YAoiD7Jxa9iHvngcag==" }, + "pump": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.0.tgz", + "integrity": "sha512-LwZy+p3SFs1Pytd/jYct4wpv49HiYCqd9Rlc5ZVdk0V+8Yzv6jR5Blk3TRmPL1ft69TxP0IMZGJ+WPFU2BFhww==", + "requires": { + "end-of-stream": "^1.1.0", + "once": "^1.3.1" + } + }, "require-directory": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", @@ -1388,6 +1607,11 @@ "strip-ansi": "^6.0.0" } }, + "wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==" + }, "y18n": { "version": "5.0.8", "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", @@ -1528,6 +1752,15 @@ "version": "21.1.1", "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz", "integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==" + }, + "yauzl": { + "version": "2.10.0", + "resolved": "https://registry.npmjs.org/yauzl/-/yauzl-2.10.0.tgz", + "integrity": "sha512-p4a9I6X6nu6IhoGmBqAcbJy1mlC4j27vEPZX9F4L4/vZT3Lyq1VkFHw/V/PUcB9Buo+DG3iHkT0x3Qya58zc3g==", + "requires": { + "buffer-crc32": "~0.2.3", + "fd-slicer": "~1.1.0" + } } } } diff --git a/package.json b/package.json index fc4b6b5..f099608 100644 --- a/package.json +++ b/package.json @@ -14,6 +14,7 @@ "dependencies": { "child_process": "^1.0.2", "degit": "^2.8.4", + "extract-zip": "^2.0.1", "fetch": "^1.1.0", "form-data": "^4.0.0", "https": "^1.0.0", From 85d99fb78cc81835f2d24faa8113210a422a8a91 Mon Sep 17 00:00:00 2001 From: Frederik Nielsen Date: Wed, 31 Aug 2022 16:20:04 +0200 Subject: [PATCH 07/86] Fixing extract bug and changing addinprovider in install command to properly use the structure in main --- bin/commands/files.js | 7 ++++--- bin/commands/install.js | 2 +- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/bin/commands/files.js b/bin/commands/files.js index 87cbbc3..f6753d7 100644 --- a/bin/commands/files.js +++ b/bin/commands/files.js @@ -160,7 +160,8 @@ async function download(env, user, dirPath, outPath, recursive, outname, iamstup return res; }).then(async (res) => { if (!filename) return; - const fileStream = fs.createWriteStream(path.resolve(`${path.resolve(outPath)}/${filename}`)); + let filePath = path.resolve(`${path.resolve(outPath)}/${filename}`) + const fileStream = fs.createWriteStream(filePath); await new Promise((resolve, reject) => { res.body.pipe(fileStream); res.body.on("error", reject); @@ -168,8 +169,8 @@ async function download(env, user, dirPath, outPath, recursive, outname, iamstup }); console.log(`Finished downloading`, dirPath === '' ? '.' : dirPath, 'Recursive=' + recursive); let filenameWithoutExtension = filename.replace('.zip', '') - await extract(filename, { dir: `${path.resolve(outPath)}/${filenameWithoutExtension === 'Base' ? '' : filenameWithoutExtension}` }, function (err) {}) - fs.unlink(filename, function(err) {}) + await extract(filePath, { dir: `${path.resolve(outPath)}/${filenameWithoutExtension === 'Base' ? '' : filenameWithoutExtension}` }, function (err) {}) + fs.unlink(filePath, function(err) {}) return res; }); } diff --git a/bin/commands/install.js b/bin/commands/install.js index 70dff20..09bb333 100644 --- a/bin/commands/install.js +++ b/bin/commands/install.js @@ -60,7 +60,7 @@ async function uploadFile(env, user, resolvedPath) { async function installAddin(env, user, resolvedPath) { console.log('Installing addin') let data = new URLSearchParams(); - data.append('AddinProvider', 'Dynamicweb.Marketplace.NuGet.LocalAddinProvider'); + data.append('AddinProvider', 'Dynamicweb.Marketplace.Providers.LocalAddinProvider'); data.append('Package', path.basename(resolvedPath)); let res = await fetch(`https://${env.host}/Admin/Api/AddinInstall`, { method: 'POST', From ad789a4b8152299e99f9c0f35fe8de4bbe07089a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nicolai=20H=C3=B8eg=20Pedersen?= Date: Thu, 1 Sep 2022 11:27:10 +0200 Subject: [PATCH 08/86] Suggestion to code highlight command parameters --- README.md | 32 ++++++++++++++++---------------- 1 file changed, 16 insertions(+), 16 deletions(-) diff --git a/README.md b/README.md index cb889a4..bff77ab 100644 --- a/README.md +++ b/README.md @@ -54,12 +54,12 @@ The configuration will automatically be created when setting up your first envir > $ dw files \ \ The files command is used to list out and export the structure in your Dynamicweb files archive, as such is has multiple options; -- -l --list This will list the directory given in \ -- -f --includeFiles The list will now also show all files in the directories -- -r --recursive By default it only handles the \, but with this option it will handle all directories under this recursively -- -e --export It will export \ into \ on your local machine, unzipped by default -- --raw This will keep the content zipped -- --iamstupid This will include the export of the /files/system/log and /files/.cache folders +- `-l` `--list` This will list the directory given in \ +- `-f` `--includeFiles` The list will now also show all files in the directories +- `-r` `--recursive` By default it only handles the \, but with this option it will handle all directories under this recursively +- `-e` `--export` It will export \ into \ on your local machine, unzipped by default +- `--raw` This will keep the content zipped +- `--iamstupid` This will include the export of the /files/system/log and /files/.cache folders #### Examples Exporting all templates from current environment to local solution @@ -74,10 +74,10 @@ Listing the system files structure of the current environment > $ dw swift \ 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; -- -t --tag \ The tag/branch/release to pull -- -l --list Will list all the release versions -- -n --nightly Will pull #HEAD, as default is latest release -- --force Used if \ is not an empty folder, to override all the content +- `-t` `--tag ` The tag/branch/release to pull +- `-l` `--list` Will list all the release versions +- `-n` `--nightly` Will pull #HEAD, as default is latest release +- `--force` Used if \ is not an empty folder, to override all the content #### Examples Getting all the available releases @@ -92,9 +92,9 @@ Pulling and overriding local solution with latest nightly build > $ dw query \ 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; -- -l --list Will list all the properties for the given \ -- -i --interactive Will perform the \ but without any parameters, as they will be asked for one by one in interactive mode -- --\ Any parameter the query needs will be sent by '--key value' +- `-l` `--list` Will list all the properties for the given \ +- `-i` `--interactive` Will perform the \ but without any parameters, as they will be asked for one by one in interactive mode +- `--` Any parameter the query needs will be sent by '--key value' #### Examples Getting all properties for a query @@ -107,8 +107,8 @@ Getting file information on a specific file by name > $ dw command \ 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. -- -l --list Lists all the properties for the command, as well as the json model required **currently not working** -- --json Takes a path to a .json file or a literal json, i.e --json '{ abc: "123" }' +- `-l` `--list` Lists all the properties for the command, as well as the json model required **currently not working** +- `--json` Takes a path to a .json file or a literal json, i.e --json '{ abc: "123" }' #### Examples Creating a copy of a page using a json-string @@ -139,7 +139,7 @@ It's meant to be used to easily apply custom dlls to a given project, it being l > $ dw database \ This command is used for various actions towards your current environments database. -- -e --export Exports your current environments database to a .bacpac file at \ +- `-e` `--export` Exports your current environments database to a .bacpac file at \ #### Examples > $ dw database -e ./backup From c2f0abab528e66c58a3babb544ec0ba4eadabab0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nicolai=20H=C3=B8eg=20Pedersen?= Date: Thu, 1 Sep 2022 13:48:59 +0200 Subject: [PATCH 09/86] Fix missing --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index bff77ab..3cc6334 100644 --- a/README.md +++ b/README.md @@ -148,7 +148,7 @@ This command is used for various actions towards your current environments datab > $ dw config 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. -- --\ The path and name of the property to set +- `--` The path and name of the property to set #### Examples Changing the host for the dev environment From 967a6fadcac2ef4b16ee8032b3721d248241385b Mon Sep 17 00:00:00 2001 From: Frederik Nielsen Date: Tue, 13 Sep 2022 08:19:35 +0200 Subject: [PATCH 10/86] Moving uploadFile to files, adding flag to use with file and destination --- bin/commands/files.js | 32 ++++++++++++++++++++++++++++++++ bin/commands/install.js | 25 ++----------------------- 2 files changed, 34 insertions(+), 23 deletions(-) diff --git a/bin/commands/files.js b/bin/commands/files.js index f6753d7..fa94595 100644 --- a/bin/commands/files.js +++ b/bin/commands/files.js @@ -2,6 +2,7 @@ import fetch from 'node-fetch'; import path from 'path'; import fs from 'fs'; import extract from 'extract-zip'; +import FormData from 'form-data'; import { Agent } from 'https'; import { setupEnv } from './env.js'; import { setupUser } from './login.js'; @@ -34,6 +35,11 @@ export function filesCommand() { type: 'boolean', describe: 'Exports the directory at [dirPath] to [outPath]' }) + .option('import', { + alias: 'i', + type: 'boolean', + describe: 'Imports the file at [dirPath] to [outPath]' + }) .option('includeFiles', { alias: 'f', type: 'boolean', @@ -87,6 +93,10 @@ async function handleFiles(argv) { console.log('The files in the base "files" folder is in Base.zip, each directory in "files" is in its own zip') }) } + } else if (argv.import) { + if (argv.dirPath && argv.outPath) { + await uploadFile(env, user, argv.dirPath, argv.outPath); + } } } @@ -188,4 +198,26 @@ async function getFilesStructure(env, user, dirPath, recursive, includeFiles) { } else { console.log(res); } +} + +export async function uploadFile(env, user, localFilePath, destinationPath) { + console.log('Uploading file') + let files = new FormData(); + files.append('files', fs.createReadStream(localFilePath)); + let res = await fetch(`https://${env.host}/Admin/Api/FileUpload?Command.Path=${destinationPath}`, { + method: 'POST', + body: files, + headers: { + 'Authorization': `Bearer ${user.apiKey}` + }, + agent: agent + }); + if (res.ok) { + if (env.verbose) console.log(await res.json()) + console.log(`File uploaded`) + } + else { + console.log(res) + return; + } } \ No newline at end of file diff --git a/bin/commands/install.js b/bin/commands/install.js index 09bb333..d74cc19 100644 --- a/bin/commands/install.js +++ b/bin/commands/install.js @@ -5,6 +5,7 @@ import FormData from 'form-data'; import { Agent } from 'https'; import { setupEnv } from './env.js'; import { setupUser } from './login.js'; +import { uploadFile } from './files.js'; const agent = new Agent({ rejectUnauthorized: false @@ -31,32 +32,10 @@ async function handleInstall(argv) { let env = await setupEnv(argv); let user = await setupUser(argv, env); let resolvedPath = path.resolve(argv.filePath) - await uploadFile(env, user, resolvedPath); + await uploadFile(env, user, resolvedPath, 'System/AddIns/Local'); await installAddin(env, user, resolvedPath) } -async function uploadFile(env, user, resolvedPath) { - console.log('Uploading file') - let files = new FormData(); - files.append('files', fs.createReadStream(resolvedPath)); - let res = await fetch(`https://${env.host}/Admin/Api/FileUpload?Command.Path=System/AddIns/Local`, { - method: 'POST', - body: files, - headers: { - 'Authorization': `Bearer ${user.apiKey}` - }, - agent: agent - }); - if (res.ok) { - if (env.verbose) console.log(await res.json()) - console.log(`File uploaded`) - } - else { - console.log(res) - return; - } -} - async function installAddin(env, user, resolvedPath) { console.log('Installing addin') let data = new URLSearchParams(); From 0afd0b347155f1ed17de369863d8c4aff03ef38a Mon Sep 17 00:00:00 2001 From: Frederik Nielsen Date: Wed, 12 Oct 2022 15:18:10 +0200 Subject: [PATCH 11/86] Changing where we send form data to now send the new structure of { model: data } --- bin/commands/command.js | 10 +++++----- bin/commands/install.js | 13 ++++++------- bin/commands/login.js | 15 ++++++++------- 3 files changed, 19 insertions(+), 19 deletions(-) diff --git a/bin/commands/command.js b/bin/commands/command.js index 6f260fa..24c14a5 100644 --- a/bin/commands/command.js +++ b/bin/commands/command.js @@ -41,7 +41,7 @@ async function handleCommand(argv) { if (argv.list) { console.log(await getProperties(env, user, argv.command)) } else { - let response = await runCommand(env, user, argv.command, getQueryParams(argv), getFormParams(argv.json)) + let response = await runCommand(env, user, argv.command, getQueryParams(argv), parseJsonOrPath(argv.json)) console.log(response) } } @@ -67,7 +67,7 @@ function getQueryParams(argv) { return params } -function getFormParams(json) { +function parseJsonOrPath(json) { if (!json) return if (fs.existsSync(json)) { return JSON.parse(fs.readFileSync(path.resolve(json))) @@ -76,13 +76,13 @@ function getFormParams(json) { } } -async function runCommand(env, user, command, queryParams, formParams) { +async function runCommand(env, user, command, queryParams, data) { let res = await fetch(`https://${env.host}/Admin/Api/${command}?` + new URLSearchParams(queryParams), { method: 'POST', - body: new URLSearchParams(formParams), + body: JSON.stringify( { 'model': data } ), headers: { 'Authorization': `Bearer ${user.apiKey}`, - 'Content-Type': 'application/x-www-form-urlencoded' + 'Content-Type': 'application/json' }, agent: agent }) diff --git a/bin/commands/install.js b/bin/commands/install.js index d74cc19..f51145c 100644 --- a/bin/commands/install.js +++ b/bin/commands/install.js @@ -1,7 +1,5 @@ import fetch from 'node-fetch'; import path from 'path'; -import fs from 'fs'; -import FormData from 'form-data'; import { Agent } from 'https'; import { setupEnv } from './env.js'; import { setupUser } from './login.js'; @@ -38,14 +36,15 @@ async function handleInstall(argv) { async function installAddin(env, user, resolvedPath) { console.log('Installing addin') - let data = new URLSearchParams(); - data.append('AddinProvider', 'Dynamicweb.Marketplace.Providers.LocalAddinProvider'); - data.append('Package', path.basename(resolvedPath)); + let data = { + 'AddinProvider': 'Dynamicweb.Marketplace.Providers.LocalAddinProvider', + 'Package': path.basename(resolvedPath) + } let res = await fetch(`https://${env.host}/Admin/Api/AddinInstall`, { method: 'POST', - body: data, + body: JSON.stringify( { 'model': data } ), headers: { - 'Content-Type': 'application/x-www-form-urlencoded', + 'Content-Type': 'application/json', 'Authorization': `Bearer ${user.apiKey}` }, agent: agent diff --git a/bin/commands/login.js b/bin/commands/login.js index bf13f90..356bcf6 100644 --- a/bin/commands/login.js +++ b/bin/commands/login.js @@ -160,15 +160,16 @@ async function getToken(user, env) { } async function getApiKey(token, env) { - let data = new URLSearchParams(); - data.append('Name', 'addin'); - data.append('Prefix', 'addin'); - data.append('Description', 'Auto-generated ApiKey by DW CLI'); + let data = { + 'Name': 'addin', + 'Prefix': 'addin', + 'Description': 'Auto-generated ApiKey by DW CLI' + }; var res = await fetch(`https://${getConfig().env[env].host}/Admin/Api/ApiKeySave`, { method: 'POST', - body: data, + body: JSON.stringify( { 'model': data } ), headers: { - 'Content-Type': 'application/x-www-form-urlencoded', + 'Content-Type': 'application/json', 'Authorization': `Bearer ${token}` }, agent: agent @@ -178,7 +179,7 @@ async function getApiKey(token, env) { return (await res.json()).message } else { - console.log(res) + console.log(await res.json()) } } From 4f6dd0a8dafd0321b1c52daac437d7f62a1a6870 Mon Sep 17 00:00:00 2001 From: Frederik Nielsen Date: Wed, 12 Oct 2022 15:22:29 +0200 Subject: [PATCH 12/86] Changing back the command command to simply just use the structure given, so given commands will have to follow the proper command structure instead of only supplying a model --- bin/commands/command.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bin/commands/command.js b/bin/commands/command.js index 24c14a5..f0cbb35 100644 --- a/bin/commands/command.js +++ b/bin/commands/command.js @@ -79,7 +79,7 @@ function parseJsonOrPath(json) { async function runCommand(env, user, command, queryParams, data) { let res = await fetch(`https://${env.host}/Admin/Api/${command}?` + new URLSearchParams(queryParams), { method: 'POST', - body: JSON.stringify( { 'model': data } ), + body: JSON.stringify(data), headers: { 'Authorization': `Bearer ${user.apiKey}`, 'Content-Type': 'application/json' From a708b722d4c9409ee05d7879fbbc0f11919edc0c Mon Sep 17 00:00:00 2001 From: frederik5480 Date: Wed, 12 Oct 2022 23:16:09 +0200 Subject: [PATCH 13/86] Update readme.md to reflect the management API json changes --- README.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 3cc6334..5ea1bd4 100644 --- a/README.md +++ b/README.md @@ -112,14 +112,14 @@ Using command will, like query, fire any given command in the solution. It works #### Examples Creating a copy of a page using a json-string -> $ dw command PageCopy --json '{ "SourcePageId": 1189, "DestinationParentPageId": 1129 }' +> $ dw command PageCopy --json '{ "model": { "SourcePageId": 1189, "DestinationParentPageId": 1129 } }' Removing a page using a json file > $ dw command PageMove --json ./PageMove.json Where PageMove.json contains ```json -{ "SourcePageId": 1383, "DestinationParentPageId": 1376 } +{ "model": { "SourcePageId": 1383, "DestinationParentPageId": 1376 } } ``` Deleting a page @@ -152,4 +152,4 @@ Config is used to manage the .dwc file through the CLI, given any prop it will c #### Examples Changing the host for the dev environment -> $ dw config --env.dev.host localhost:6001 \ No newline at end of file +> $ dw config --env.dev.host localhost:6001 From e645f23c350758386ffc6bbc1f3653fd447080b4 Mon Sep 17 00:00:00 2001 From: Frederik Nielsen Date: Wed, 12 Oct 2022 23:38:44 +0200 Subject: [PATCH 14/86] Fixes #8 --- bin/commands/files.js | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/bin/commands/files.js b/bin/commands/files.js index fa94595..d3f1345 100644 --- a/bin/commands/files.js +++ b/bin/commands/files.js @@ -143,7 +143,7 @@ async function download(env, user, dirPath, outPath, recursive, outname, iamstup } let excludeDirectories = ''; if (!iamstupid) { - excludeDirectories = '&Command.ExcludeDirectories=system/log'; + excludeDirectories = 'system/log'; if (dirPath === 'cache.net') { return; } @@ -152,14 +152,20 @@ async function download(env, user, dirPath, outPath, recursive, outname, iamstup console.log('Downloading', dirPath === '' ? 'Base' : dirPath, 'Recursive=' + recursive); let filename; - fetch(`https://${env.host}/Admin/Api/${endpoint}?Command.DirectoryPath=${dirPath ?? ''}${excludeDirectories}`, { + let data = { + 'DirectoryPath': dirPath ?? '', + 'ExcludeDirectories': [ excludeDirectories ], + } + fetch(`https://${env.host}/Admin/Api/${endpoint}`, { method: 'POST', + body: JSON.stringify(data), headers: { - 'Authorization': `Bearer ${user.apiKey}` + 'Authorization': `Bearer ${user.apiKey}`, + 'Content-Type': 'application/json' }, agent: agent }).then((res) => { - const header = res.headers.get('Content-Disposition'); + const header = res.headers.get('content-disposition'); const parts = header?.split(';'); if (!parts) { console.log(`No files found in directory '${dirPath}', if you want to download all folders recursively include the -r flag`); From bf1c74ac0c5756fd52cc3b337e39fd3bac18bc49 Mon Sep 17 00:00:00 2001 From: frederik5480 Date: Wed, 12 Oct 2022 23:44:21 +0200 Subject: [PATCH 15/86] Updated readme.md Reflects page delete example properly, fixes #10 --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 5ea1bd4..a09cf3a 100644 --- a/README.md +++ b/README.md @@ -123,7 +123,7 @@ Where PageMove.json contains ``` Deleting a page -> $ dw command PageDelete --id 1383 +> $ dw command PageDelete --json '{ "id": "1383" }' ### Install > $ dw install \ From 6b7b56ae07302217e3b3878e71fe7ba60bfbb817 Mon Sep 17 00:00:00 2001 From: frederik5480 Date: Wed, 12 Oct 2022 23:49:40 +0200 Subject: [PATCH 16/86] Update readme.md Fixes #5 --- README.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/README.md b/README.md index a09cf3a..5c26ab4 100644 --- a/README.md +++ b/README.md @@ -3,6 +3,12 @@ ## Get started To install after cloning, move to project dir and run > $ npm install -g . +> +> $ npm install + +Note that specific installations might be necessary if you're faced with errors such as 'Error [ERR_MODULE_NOT_FOUND]: Cannot find package 'yargs'' +In which case try installing that module specifically; +> $ npm install yargs ## Commands All commands and options can be viewed by running From fec28e96d00beab0e90e1d748639577bb066aaf6 Mon Sep 17 00:00:00 2001 From: frederik5480 Date: Wed, 12 Oct 2022 23:55:49 +0200 Subject: [PATCH 17/86] Update readme.me Fixes #6 --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 5c26ab4..54d77ba 100644 --- a/README.md +++ b/README.md @@ -71,7 +71,7 @@ The files command is used to list out and export the structure in your Dynamicwe Exporting all templates from current environment to local solution > $ cd DynamicWebSolution/Files > -> $ dw files /templates ./templates +> $ dw files templates ./templates -fre Listing the system files structure of the current environment > $ dw files system -lr From ea6133b5dc0c2e33478d0d39c250ae029e568c4f Mon Sep 17 00:00:00 2001 From: Frederik Nielsen Date: Thu, 13 Oct 2022 00:17:25 +0200 Subject: [PATCH 18/86] Fixes #4 --- bin/commands/swift.js | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/bin/commands/swift.js b/bin/commands/swift.js index 21a29fb..de3e85b 100644 --- a/bin/commands/swift.js +++ b/bin/commands/swift.js @@ -1,5 +1,6 @@ import { exec } from 'child_process'; import { Agent } from 'https'; +import path from 'path'; import fetch from 'node-fetch'; const agent = new Agent({ @@ -43,12 +44,12 @@ async function handleSwift(argv) { } else { let degitCommand if (argv.nightly) { - degitCommand = `npx degit dynamicweb/swift ${argv.force ? '--force' : ''} ${argv.outPath}` + degitCommand = `npx degit dynamicweb/swift ${argv.force ? '--force' : ''} "${path.resolve(argv.outPath)}"` } else { - degitCommand = `npx degit dynamicweb/swift#${argv.tag ? argv.tag : await getVersions(true)} ${argv.force ? '--force' : ''} ${argv.outPath}` + degitCommand = `npx degit dynamicweb/swift#${argv.tag ? argv.tag : await getVersions(true)} ${argv.force ? '--force' : ''} "${path.resolve(argv.outPath)}"` } if (argv.verbose) console.info(`Executing command: ${degitCommand}`) - exec(`${degitCommand}`, (error, stdout, stderr) => { + exec(degitCommand, (error, stdout, stderr) => { if (error) { console.log(`error: ${error.message}`); return; From 9171a1c566f23d6c22777f8cba8fadab24452fdd Mon Sep 17 00:00:00 2001 From: Frederik Nielsen Date: Thu, 13 Oct 2022 00:25:22 +0200 Subject: [PATCH 19/86] Fixes #7 --- bin/index.js | 1 + 1 file changed, 1 insertion(+) diff --git a/bin/index.js b/bin/index.js index 43ea10e..7ed9986 100644 --- a/bin/index.js +++ b/bin/index.js @@ -41,6 +41,7 @@ function baseCommand() { handler: () => { console.log(`Environment: ${getConfig()?.current?.env}`) console.log(`User: ${getConfig()?.env[getConfig()?.current?.env]?.current?.user}`) + console.log(`Host: ${getConfig()?.env[getConfig()?.current?.env]?.host}`) } } } \ No newline at end of file From f68bdeac4a453d2ab90ee977f986774025653e44 Mon Sep 17 00:00:00 2001 From: Frederik Nielsen Date: Mon, 17 Oct 2022 12:52:18 +0200 Subject: [PATCH 20/86] Adding proper content-type header for database command --- bin/commands/database.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/bin/commands/database.js b/bin/commands/database.js index fad72fd..53b3ae8 100644 --- a/bin/commands/database.js +++ b/bin/commands/database.js @@ -46,7 +46,8 @@ async function download(env, user, path, verbose) { fetch(`https://${env.host}/Admin/Api/DatabaseDownload`, { method: 'POST', headers: { - 'Authorization': `Bearer ${user.apiKey}` + 'Authorization': `Bearer ${user.apiKey}`, + 'content-type': 'application/json' }, agent: agent }).then(async (res) => { From 1d6b9cd146b793b6b00c6eaa64f1e569b453328e Mon Sep 17 00:00:00 2001 From: Frederik Nielsen Date: Mon, 17 Oct 2022 13:10:38 +0200 Subject: [PATCH 21/86] Fixes #15 --- bin/commands/files.js | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/bin/commands/files.js b/bin/commands/files.js index d3f1345..76b2709 100644 --- a/bin/commands/files.js +++ b/bin/commands/files.js @@ -80,16 +80,16 @@ async function handleFiles(argv) { if (argv.export) { if (argv.dirPath) { - await download(env, user, argv.dirPath, argv.outPath, argv.recursive); + await download(env, user, argv.dirPath, argv.outPath, argv.recursive, null, argv.raw); } else { await interactiveConfirm('Are you sure you want a full export of files?', async () => { console.log('Full export is starting') let dirs = (await getFilesStructure(env, user, '', false, false)).model.directories; for (let id = 0; id < dirs.length; id++) { const dir = dirs[id]; - await download(env, user, dir.name, argv.outPath, true, null, argv.iamstupid); + await download(env, user, dir.name, argv.outPath, true, null, argv.raw, argv.iamstupid); } - await download(env, user, '', argv.outPath, false, 'Base.zip', argv.iamstupid); + await download(env, user, '', argv.outPath, false, 'Base.zip', argv.raw, argv.iamstupid); console.log('The files in the base "files" folder is in Base.zip, each directory in "files" is in its own zip') }) } @@ -134,7 +134,7 @@ function resolveTree(dirs, indentLevel, parentHasFiles) { } } -async function download(env, user, dirPath, outPath, recursive, outname, iamstupid) { +async function download(env, user, dirPath, outPath, recursive, outname, raw, iamstupid) { let endpoint; if (recursive) { endpoint = 'DirectoryDownload'; @@ -184,9 +184,11 @@ async function download(env, user, dirPath, outPath, recursive, outname, iamstup fileStream.on("finish", resolve); }); console.log(`Finished downloading`, dirPath === '' ? '.' : dirPath, 'Recursive=' + recursive); - let filenameWithoutExtension = filename.replace('.zip', '') - await extract(filePath, { dir: `${path.resolve(outPath)}/${filenameWithoutExtension === 'Base' ? '' : filenameWithoutExtension}` }, function (err) {}) - fs.unlink(filePath, function(err) {}) + if (!raw) { + let filenameWithoutExtension = filename.replace('.zip', '') + await extract(filePath, { dir: `${path.resolve(outPath)}/${filenameWithoutExtension === 'Base' ? '' : filenameWithoutExtension}` }, function (err) {}) + fs.unlink(filePath, function(err) {}) + } return res; }); } From a07e7c88bb43bcd39ad62f18a473ecd490c16c81 Mon Sep 17 00:00:00 2001 From: frederik5480 Date: Wed, 1 Feb 2023 13:32:01 +0100 Subject: [PATCH 22/86] Update README.md Adding short explanation of what the CLI is and can be used for in simpler terms. --- README.md | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/README.md b/README.md index 54d77ba..506dfa9 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,15 @@ # DynamicWeb CLI +## What is it? +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. + +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. +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. + +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. + +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. + ## Get started To install after cloning, move to project dir and run > $ npm install -g . From e374b6d612b499537ad68ce0e07ce3cb59d899a0 Mon Sep 17 00:00:00 2001 From: frederik5480 Date: Wed, 1 Feb 2023 14:16:36 +0100 Subject: [PATCH 23/86] Fixing directory and file listing query not working --- bin/commands/files.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/bin/commands/files.js b/bin/commands/files.js index 76b2709..a77b1c2 100644 --- a/bin/commands/files.js +++ b/bin/commands/files.js @@ -84,7 +84,7 @@ async function handleFiles(argv) { } else { await interactiveConfirm('Are you sure you want a full export of files?', async () => { console.log('Full export is starting') - let dirs = (await getFilesStructure(env, user, '', false, false)).model.directories; + let dirs = (await getFilesStructure(env, user, '/', false, false)).model.directories; for (let id = 0; id < dirs.length; id++) { const dir = dirs[id]; await download(env, user, dir.name, argv.outPath, true, null, argv.raw, argv.iamstupid); @@ -194,7 +194,7 @@ async function download(env, user, dirPath, outPath, recursive, outname, raw, ia } async function getFilesStructure(env, user, dirPath, recursive, includeFiles) { - let res = await fetch(`https://${env.host}/Admin/Api/DirectoryAll?DirectoryPath=${dirPath ?? ''}&recursive=${recursive ?? 'false'}&includeFiles=${includeFiles ?? 'false'}`, { + let res = await fetch(`https://${env.host}/Admin/Api/DirectoryAll?DirectoryPath=${dirPath ?? '/'}&recursive=${recursive ?? 'false'}&includeFiles=${includeFiles ?? 'false'}`, { method: 'GET', headers: { 'Authorization': `Bearer ${user.apiKey}` @@ -228,4 +228,4 @@ export async function uploadFile(env, user, localFilePath, destinationPath) { console.log(res) return; } -} \ No newline at end of file +} From 0841d038879229d540ee3d8694f4e3835c3bfdf7 Mon Sep 17 00:00:00 2001 From: Frederik Nielsen Date: Wed, 1 Feb 2023 14:45:14 +0100 Subject: [PATCH 24/86] Allowing setting protocol when setting up environments, https by default --- bin/commands/command.js | 15 +++++---------- bin/commands/database.js | 11 +++-------- bin/commands/env.js | 22 ++++++++++++++++++++++ bin/commands/files.js | 19 +++++++------------ bin/commands/install.js | 11 +++-------- bin/commands/login.js | 40 ++++++++++++++++++++-------------------- bin/commands/query.js | 15 +++++---------- bin/index.js | 1 + 8 files changed, 66 insertions(+), 68 deletions(-) diff --git a/bin/commands/command.js b/bin/commands/command.js index f0cbb35..427e74a 100644 --- a/bin/commands/command.js +++ b/bin/commands/command.js @@ -1,14 +1,9 @@ -import { Agent } from 'https'; import fetch from 'node-fetch'; import path from 'path'; import fs from 'fs'; -import { setupEnv } from './env.js'; +import { setupEnv, getAgent } from './env.js'; import { setupUser } from './login.js'; -const agent = new Agent({ - rejectUnauthorized: false -}) - const exclude = ['_', '$0', 'command', 'list', 'json'] export function commandCommand() { @@ -48,12 +43,12 @@ async function handleCommand(argv) { async function getProperties(env, user, command) { return `This option currently doesn't work` - let res = await fetch(`https://${env.host}/Admin/Api/CommandByName?name=${command}`, { + let res = await fetch(`${env.protocol}://${env.host}/Admin/Api/CommandByName?name=${command}`, { method: 'GET', headers: { 'Authorization': `Bearer ${user.apiKey}` }, - agent: agent + agent: getAgent(env.protocol) }) if (res.ok) { let body = await res.json() @@ -77,14 +72,14 @@ function parseJsonOrPath(json) { } async function runCommand(env, user, command, queryParams, data) { - let res = await fetch(`https://${env.host}/Admin/Api/${command}?` + new URLSearchParams(queryParams), { + let res = await fetch(`${env.protocol}://${env.host}/Admin/Api/${command}?` + new URLSearchParams(queryParams), { method: 'POST', body: JSON.stringify(data), headers: { 'Authorization': `Bearer ${user.apiKey}`, 'Content-Type': 'application/json' }, - agent: agent + agent: getAgent(env.protocol) }) if (!res.ok) { console.log(`Error when doing request ${res.url}`) diff --git a/bin/commands/database.js b/bin/commands/database.js index 53b3ae8..50ee437 100644 --- a/bin/commands/database.js +++ b/bin/commands/database.js @@ -1,14 +1,9 @@ import fetch from 'node-fetch'; import fs from 'fs'; import _path from 'path'; -import { Agent } from 'https'; -import { setupEnv } from './env.js'; +import { setupEnv, getAgent } from './env.js'; import { setupUser } from './login.js'; -const agent = new Agent({ - rejectUnauthorized: false -}) - export function databaseCommand() { return { command: 'database [path]', @@ -43,13 +38,13 @@ async function handleDatabase(argv) { async function download(env, user, path, verbose) { let filename = 'database.bacpac'; - fetch(`https://${env.host}/Admin/Api/DatabaseDownload`, { + fetch(`${env.protocol}://${env.host}/Admin/Api/DatabaseDownload`, { method: 'POST', headers: { 'Authorization': `Bearer ${user.apiKey}`, 'content-type': 'application/json' }, - agent: agent + agent: getAgent(env.protocol) }).then(async (res) => { if (verbose) console.log(res) const header = res.headers.get('Content-Disposition'); diff --git a/bin/commands/env.js b/bin/commands/env.js index 31a672b..585e03f 100644 --- a/bin/commands/env.js +++ b/bin/commands/env.js @@ -1,6 +1,20 @@ import { updateConfig, getConfig } from './config.js' +import { Agent as HttpAgent } from 'http'; +import { Agent as HttpsAgent } from 'https'; import yargsInteractive from 'yargs-interactive'; +const httpAgent = new HttpAgent({ + rejectUnauthorized: false +}) + +const httpsAgent = new HttpsAgent({ + rejectUnauthorized: false +}) + +export function getAgent(protocol) { + return protocol === 'http' ? httpAgent : httpsAgent; +} + export function envCommand() { return { command: 'env [env]', @@ -29,6 +43,10 @@ export async function setupEnv(argv) { let env; if (getConfig().env) { env = getConfig().env[argv.env] || getConfig().env[getConfig()?.current?.env]; + if (!env.protocol) { + console.log('Protocol for environment not set, defaulting to https'); + env.protocol = 'https'; + } } if (!env) { console.log('Current environment not set, please set it') @@ -58,6 +76,9 @@ async function handleEnv(argv) { environment: { type: 'input' }, + protocol: { + type: 'input' + }, host: { type: 'input' }, @@ -75,6 +96,7 @@ export async function interactiveEnv(argv, options) { .then(async (result) => { getConfig().env = getConfig().env || {}; getConfig().env[result.environment] = getConfig().env[result.environment] || {}; + getConfig().env[result.environment].protocol = result.protocol || 'https'; if (result.host) getConfig().env[result.environment].host = result.host; if (result.environment) { diff --git a/bin/commands/files.js b/bin/commands/files.js index 76b2709..ae7dd8b 100644 --- a/bin/commands/files.js +++ b/bin/commands/files.js @@ -3,15 +3,10 @@ import path from 'path'; import fs from 'fs'; import extract from 'extract-zip'; import FormData from 'form-data'; -import { Agent } from 'https'; -import { setupEnv } from './env.js'; +import { setupEnv, getAgent } from './env.js'; import { setupUser } from './login.js'; import { interactiveConfirm } from '../utils.js'; -const agent = new Agent({ - rejectUnauthorized: false -}) - export function filesCommand() { return { command: 'files [dirPath] [outPath]', @@ -156,14 +151,14 @@ async function download(env, user, dirPath, outPath, recursive, outname, raw, ia 'DirectoryPath': dirPath ?? '', 'ExcludeDirectories': [ excludeDirectories ], } - fetch(`https://${env.host}/Admin/Api/${endpoint}`, { + fetch(`${env.protocol}://${env.host}/Admin/Api/${endpoint}`, { method: 'POST', body: JSON.stringify(data), headers: { 'Authorization': `Bearer ${user.apiKey}`, 'Content-Type': 'application/json' }, - agent: agent + agent: getAgent(env.protocol) }).then((res) => { const header = res.headers.get('content-disposition'); const parts = header?.split(';'); @@ -194,12 +189,12 @@ async function download(env, user, dirPath, outPath, recursive, outname, raw, ia } async function getFilesStructure(env, user, dirPath, recursive, includeFiles) { - let res = await fetch(`https://${env.host}/Admin/Api/DirectoryAll?DirectoryPath=${dirPath ?? ''}&recursive=${recursive ?? 'false'}&includeFiles=${includeFiles ?? 'false'}`, { + let res = await fetch(`${env.protocol}://${env.host}/Admin/Api/DirectoryAll?DirectoryPath=${dirPath ?? ''}&recursive=${recursive ?? 'false'}&includeFiles=${includeFiles ?? 'false'}`, { method: 'GET', headers: { 'Authorization': `Bearer ${user.apiKey}` }, - agent: agent + agent: getAgent(env.protocol) }); if (res.ok) { return await res.json(); @@ -212,13 +207,13 @@ export async function uploadFile(env, user, localFilePath, destinationPath) { console.log('Uploading file') let files = new FormData(); files.append('files', fs.createReadStream(localFilePath)); - let res = await fetch(`https://${env.host}/Admin/Api/FileUpload?Command.Path=${destinationPath}`, { + let res = await fetch(`${env.protocol}://${env.host}/Admin/Api/FileUpload?Command.Path=${destinationPath}`, { method: 'POST', body: files, headers: { 'Authorization': `Bearer ${user.apiKey}` }, - agent: agent + agent: getAgent(env.protocol) }); if (res.ok) { if (env.verbose) console.log(await res.json()) diff --git a/bin/commands/install.js b/bin/commands/install.js index f51145c..03ea110 100644 --- a/bin/commands/install.js +++ b/bin/commands/install.js @@ -1,14 +1,9 @@ import fetch from 'node-fetch'; import path from 'path'; -import { Agent } from 'https'; -import { setupEnv } from './env.js'; +import { setupEnv, getAgent } from './env.js'; import { setupUser } from './login.js'; import { uploadFile } from './files.js'; -const agent = new Agent({ - rejectUnauthorized: false -}) - export function installCommand() { return { command: 'install [filePath]', @@ -40,14 +35,14 @@ async function installAddin(env, user, resolvedPath) { 'AddinProvider': 'Dynamicweb.Marketplace.Providers.LocalAddinProvider', 'Package': path.basename(resolvedPath) } - let res = await fetch(`https://${env.host}/Admin/Api/AddinInstall`, { + let res = await fetch(`${env.protocol}://${env.host}/Admin/Api/AddinInstall`, { method: 'POST', body: JSON.stringify( { 'model': data } ), headers: { 'Content-Type': 'application/json', 'Authorization': `Bearer ${user.apiKey}` }, - agent: agent + agent: getAgent(env.protocol) }); if (res.ok) { diff --git a/bin/commands/login.js b/bin/commands/login.js index 356bcf6..f51fa23 100644 --- a/bin/commands/login.js +++ b/bin/commands/login.js @@ -1,12 +1,7 @@ import fetch from 'node-fetch'; -import { interactiveEnv } from './env.js' +import { interactiveEnv, getAgent } from './env.js' import { updateConfig, getConfig } from './config.js'; import yargsInteractive from 'yargs-interactive'; -import { Agent } from 'https'; - -const agent = new Agent({ - rejectUnauthorized: false -}) export function loginCommand() { return { @@ -74,8 +69,8 @@ export async function interactiveLogin(argv, options) { await yargsInteractive() .interactive(options) .then(async (result) => { - if (!getConfig().env || !getConfig().env[result.environment] || !getConfig().env[result.environment].host) { - if (!argv.host) + if (!getConfig().env || !getConfig().env[result.environment] || !getConfig().env[result.environment].host || !getConfig().env[result.environment].protocol) { + if (!argv.host || !argv.protocol) console.log(`The environment specified is missing parameters, please specify them`) await interactiveEnv(argv, { environment: { @@ -83,6 +78,10 @@ export async function interactiveLogin(argv, options) { default: result.environment, prompt: 'never' }, + protocol: { + type: 'input', + prompt: 'if-no-arg' + }, host: { type: 'input', prompt: 'if-no-arg' @@ -97,8 +96,9 @@ export async function interactiveLogin(argv, options) { } async function loginInteractive(result) { - var token = await login(result.username, result.password, result.environment); - var apiKey = await getApiKey(token, result.environment) + var protocol = getConfig().env[result.environment].protocol; + var token = await login(result.username, result.password, result.environment, protocol); + var apiKey = await getApiKey(token, result.environment, protocol) getConfig().env = getConfig().env || {}; getConfig().env[result.environment].users = getConfig().env[result.environment].users || {}; getConfig().env[result.environment].users[result.username] = getConfig().env[result.environment].users[result.username] || {}; @@ -108,22 +108,22 @@ async function loginInteractive(result) { updateConfig(); } -async function login(username, password, env) { +async function login(username, password, env, protocol) { let data = new URLSearchParams(); data.append('Username', username); data.append('Password', password); - var res = await fetch(`https://${getConfig().env[env].host}/Admin/Authentication/Login`, { + var res = await fetch(`${protocol}://${getConfig().env[env].host}/Admin/Authentication/Login`, { method: 'POST', body: data, headers: { 'Content-Type': 'application/x-www-form-urlencoded' }, - agent: agent + agent: getAgent(protocol) }); if (res.ok) { let user = parseCookies(res.headers.get('set-cookie')).user; - return await getToken(user, env) + return await getToken(user, env, protocol) } else { console.log(res) @@ -146,33 +146,33 @@ function parseCookies (cookieHeader) { return list; } -async function getToken(user, env) { - var res = await fetch(`https://${getConfig().env[env].host}/Admin/Authentication/Token`, { +async function getToken(user, env, protocol) { + var res = await fetch(`${getConfig().env[env].protocol}://${getConfig().env[env].host}/Admin/Authentication/Token`, { method: 'GET', headers: { 'cookie': `Dynamicweb.Admin=${user}` }, - agent: agent + agent: getAgent(protocol) }); if (res.ok) { return (await res.json()).token } } -async function getApiKey(token, env) { +async function getApiKey(token, env, protocol) { let data = { 'Name': 'addin', 'Prefix': 'addin', 'Description': 'Auto-generated ApiKey by DW CLI' }; - var res = await fetch(`https://${getConfig().env[env].host}/Admin/Api/ApiKeySave`, { + var res = await fetch(`${protocol}://${getConfig().env[env].host}/Admin/Api/ApiKeySave`, { method: 'POST', body: JSON.stringify( { 'model': data } ), headers: { 'Content-Type': 'application/json', 'Authorization': `Bearer ${token}` }, - agent: agent + agent: getAgent(protocol) }); if (res.ok) { diff --git a/bin/commands/query.js b/bin/commands/query.js index 691f5f7..85744a3 100644 --- a/bin/commands/query.js +++ b/bin/commands/query.js @@ -1,13 +1,8 @@ -import { Agent } from 'https'; import fetch from 'node-fetch'; -import { setupEnv } from './env.js'; +import { setupEnv, getAgent } from './env.js'; import { setupUser } from './login.js'; import yargsInteractive from 'yargs-interactive'; -const agent = new Agent({ - rejectUnauthorized: false -}) - const exclude = ['_', '$0', 'query', 'list', 'i', 'l', 'interactive'] export function queryCommand() { @@ -50,12 +45,12 @@ async function getProperties(argv) { let env = await setupEnv(argv); let user = await setupUser(argv, env); - let res = await fetch(`https://${env.host}/Admin/Api/QueryByName?name=${argv.query}`, { + let res = await fetch(`${env.protocol}://${env.host}/Admin/Api/QueryByName?name=${argv.query}`, { method: 'GET', headers: { 'Authorization': `Bearer ${user.apiKey}` }, - agent: agent + agent: getAgent(env.protocol) }) if (res.ok) { let body = await res.json() @@ -83,12 +78,12 @@ async function getQueryParams(argv) { } async function runQuery(env, user, query, params) { - let res = await fetch(`https://${env.host}/Admin/Api/${query}?` + new URLSearchParams(params), { + let res = await fetch(`${env.protocol}://${env.host}/Admin/Api/${query}?` + new URLSearchParams(params), { method: 'GET', headers: { 'Authorization': `Bearer ${user.apiKey}` }, - agent: agent + agent: getAgent(env.protocol) }) if (!res.ok) { console.log(`Error when doing request ${res.url}`) diff --git a/bin/index.js b/bin/index.js index 7ed9986..a587900 100644 --- a/bin/index.js +++ b/bin/index.js @@ -41,6 +41,7 @@ function baseCommand() { handler: () => { console.log(`Environment: ${getConfig()?.current?.env}`) console.log(`User: ${getConfig()?.env[getConfig()?.current?.env]?.current?.user}`) + console.log(`Protocol: ${getConfig()?.env[getConfig()?.current?.env]?.protocol}`) console.log(`Host: ${getConfig()?.env[getConfig()?.current?.env]?.host}`) } } From 480b8fd272d9003ef5be562444a106a7f0c92e4a Mon Sep 17 00:00:00 2001 From: Frederik Nielsen Date: Wed, 1 Feb 2023 22:54:03 +0100 Subject: [PATCH 25/86] Changing upload to use another endpoint specific for file upload instead of a command-specific endpoint --- bin/commands/files.js | 12 +++++++----- bin/commands/install.js | 2 +- 2 files changed, 8 insertions(+), 6 deletions(-) diff --git a/bin/commands/files.js b/bin/commands/files.js index 76b2709..10f97b8 100644 --- a/bin/commands/files.js +++ b/bin/commands/files.js @@ -95,7 +95,8 @@ async function handleFiles(argv) { } } else if (argv.import) { if (argv.dirPath && argv.outPath) { - await uploadFile(env, user, argv.dirPath, argv.outPath); + let resolvedPath = path.resolve(argv.dirPath) + await uploadFile(env, user, resolvedPath, argv.outPath); } } } @@ -210,11 +211,12 @@ async function getFilesStructure(env, user, dirPath, recursive, includeFiles) { export async function uploadFile(env, user, localFilePath, destinationPath) { console.log('Uploading file') - let files = new FormData(); - files.append('files', fs.createReadStream(localFilePath)); - let res = await fetch(`https://${env.host}/Admin/Api/FileUpload?Command.Path=${destinationPath}`, { + let form = new FormData(); + form.append('path', destinationPath); + form.append('files', fs.createReadStream(localFilePath)); + let res = await fetch(`https://${env.host}/Admin/Api/Upload`, { method: 'POST', - body: files, + body: form, headers: { 'Authorization': `Bearer ${user.apiKey}` }, diff --git a/bin/commands/install.js b/bin/commands/install.js index f51145c..a24ca2b 100644 --- a/bin/commands/install.js +++ b/bin/commands/install.js @@ -42,7 +42,7 @@ async function installAddin(env, user, resolvedPath) { } let res = await fetch(`https://${env.host}/Admin/Api/AddinInstall`, { method: 'POST', - body: JSON.stringify( { 'model': data } ), + body: JSON.stringify(data), headers: { 'Content-Type': 'application/json', 'Authorization': `Bearer ${user.apiKey}` From 76e4434d34e811cca5e5f33529d8ae62d92f5404 Mon Sep 17 00:00:00 2001 From: Frederik Nielsen Date: Tue, 2 May 2023 10:48:30 +0200 Subject: [PATCH 26/86] Fixing downloading of root files folder --- bin/commands/files.js | 38 +++++++++++++++++++++----------------- 1 file changed, 21 insertions(+), 17 deletions(-) diff --git a/bin/commands/files.js b/bin/commands/files.js index 54ad1a0..74a1626 100644 --- a/bin/commands/files.js +++ b/bin/commands/files.js @@ -75,17 +75,18 @@ async function handleFiles(argv) { if (argv.export) { if (argv.dirPath) { - await download(env, user, argv.dirPath, argv.outPath, argv.recursive, null, argv.raw); + await download(env, user, argv.dirPath, argv.outPath, argv.recursive, null, argv.raw, []); } else { await interactiveConfirm('Are you sure you want a full export of files?', async () => { console.log('Full export is starting') - let dirs = (await getFilesStructure(env, user, '/', false, false)).model.directories; + let filesStructure = (await getFilesStructure(env, user, '/', false, argv.includeFiles)).model; + let dirs = filesStructure.directories; for (let id = 0; id < dirs.length; id++) { const dir = dirs[id]; - await download(env, user, dir.name, argv.outPath, true, null, argv.raw, argv.iamstupid); + await download(env, user, dir.name, argv.outPath, true, null, argv.raw, argv.iamstupid, []); } - await download(env, user, '', argv.outPath, false, 'Base.zip', argv.raw, argv.iamstupid); - console.log('The files in the base "files" folder is in Base.zip, each directory in "files" is in its own zip') + await download(env, user, '/.', argv.outPath, false, 'Base.zip', argv.raw, argv.iamstupid, Array.from(filesStructure.files.data, f => f.name)); + 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') }) } } else if (argv.import) { @@ -130,13 +131,8 @@ function resolveTree(dirs, indentLevel, parentHasFiles) { } } -async function download(env, user, dirPath, outPath, recursive, outname, raw, iamstupid) { +async function download(env, user, dirPath, outPath, recursive, outname, raw, iamstupid, fileNames) { let endpoint; - if (recursive) { - endpoint = 'DirectoryDownload'; - } else { - endpoint = 'FileDownload' - } let excludeDirectories = ''; if (!iamstupid) { excludeDirectories = 'system/log'; @@ -144,14 +140,22 @@ async function download(env, user, dirPath, outPath, recursive, outname, raw, ia return; } } - - console.log('Downloading', dirPath === '' ? 'Base' : dirPath, 'Recursive=' + recursive); - - let filename; let data = { - 'DirectoryPath': dirPath ?? '', + 'DirectoryPath': dirPath ?? '/', 'ExcludeDirectories': [ excludeDirectories ], } + + if (recursive) { + endpoint = 'DirectoryDownload'; + } else { + endpoint = 'FileDownload' + data['Ids'] = fileNames + } + + console.log('Downloading', dirPath === '/.' ? 'Base' : dirPath, 'Recursive=' + recursive); + + let filename; + fetch(`${env.protocol}://${env.host}/Admin/Api/${endpoint}`, { method: 'POST', body: JSON.stringify(data), @@ -179,7 +183,7 @@ async function download(env, user, dirPath, outPath, recursive, outname, raw, ia res.body.on("error", reject); fileStream.on("finish", resolve); }); - console.log(`Finished downloading`, dirPath === '' ? '.' : dirPath, 'Recursive=' + recursive); + console.log(`Finished downloading`, dirPath === '/.' ? '.' : dirPath, 'Recursive=' + recursive); if (!raw) { let filenameWithoutExtension = filename.replace('.zip', '') await extract(filePath, { dir: `${path.resolve(outPath)}/${filenameWithoutExtension === 'Base' ? '' : filenameWithoutExtension}` }, function (err) {}) From 91cda9a4a83b5fb49730233db556c4ae21b9c011 Mon Sep 17 00:00:00 2001 From: Frederik Nielsen Date: Wed, 24 May 2023 11:03:42 +0200 Subject: [PATCH 27/86] Combining protocl and host in interactive, adding user messages when login goes wrong, updating package.json --- bin/commands/env.js | 22 ++++++++++++++------ bin/commands/login.js | 47 +++++++++++++++++++++++++++---------------- package.json | 22 ++++++++++++++++---- 3 files changed, 64 insertions(+), 27 deletions(-) diff --git a/bin/commands/env.js b/bin/commands/env.js index 585e03f..8a6586f 100644 --- a/bin/commands/env.js +++ b/bin/commands/env.js @@ -76,10 +76,8 @@ async function handleEnv(argv) { environment: { type: 'input' }, - protocol: { - type: 'input' - }, host: { + describe: 'Enter your host including protocol, i.e "https://yourHost.com":', type: 'input' }, interactive: { @@ -96,15 +94,26 @@ export async function interactiveEnv(argv, options) { .then(async (result) => { getConfig().env = getConfig().env || {}; getConfig().env[result.environment] = getConfig().env[result.environment] || {}; - getConfig().env[result.environment].protocol = result.protocol || 'https'; - if (result.host) - getConfig().env[result.environment].host = result.host; + if (result.host) { + var hostSplit = result.host.split("://"); + if (hostSplit.length == 1) { + getConfig().env[result.environment].protocol = 'https'; + getConfig().env[result.environment].host = hostSplit[0]; + } else if (hostSplit.length == 2) { + getConfig().env[result.environment].protocol = hostSplit[0]; + getConfig().env[result.environment].host = hostSplit[1]; + } else { + console.log(`Issues resolving host ${result.host}`); + return; + } + } if (result.environment) { getConfig().current = getConfig().current || {}; getConfig().current.env = result.environment; } updateConfig(); console.log(`Your current environment is now ${getConfig().current.env}`); + console.log(`To change the host of your environment, use the command 'dw env'`) }); } @@ -118,6 +127,7 @@ async function changeEnv(argv) { prompt: 'never' }, host: { + describe: 'Enter your host including protocol, i.e "https://yourHost.com":', type: 'input', prompt: 'always' }, diff --git a/bin/commands/login.js b/bin/commands/login.js index f51fa23..8df36b6 100644 --- a/bin/commands/login.js +++ b/bin/commands/login.js @@ -70,7 +70,7 @@ export async function interactiveLogin(argv, options) { .interactive(options) .then(async (result) => { if (!getConfig().env || !getConfig().env[result.environment] || !getConfig().env[result.environment].host || !getConfig().env[result.environment].protocol) { - if (!argv.host || !argv.protocol) + if (!argv.host) console.log(`The environment specified is missing parameters, please specify them`) await interactiveEnv(argv, { environment: { @@ -78,11 +78,8 @@ export async function interactiveLogin(argv, options) { default: result.environment, prompt: 'never' }, - protocol: { - type: 'input', - prompt: 'if-no-arg' - }, host: { + describe: 'Enter your host including protocol, i.e "https://yourHost.com":', type: 'input', prompt: 'if-no-arg' }, @@ -91,14 +88,16 @@ export async function interactiveLogin(argv, options) { } }) } - await loginInteractive(result); + await loginInteractive(result, argv.verbose); }); } -async function loginInteractive(result) { +async function loginInteractive(result, verbose) { var protocol = getConfig().env[result.environment].protocol; - var token = await login(result.username, result.password, result.environment, protocol); - var apiKey = await getApiKey(token, result.environment, protocol) + var token = await login(result.username, result.password, result.environment, protocol, verbose); + if (!token) return; + var apiKey = await getApiKey(token, result.environment, protocol, verbose) + if (!apiKey) return; getConfig().env = getConfig().env || {}; getConfig().env[result.environment].users = getConfig().env[result.environment].users || {}; getConfig().env[result.environment].users[result.username] = getConfig().env[result.environment].users[result.username] || {}; @@ -108,7 +107,7 @@ async function loginInteractive(result) { updateConfig(); } -async function login(username, password, env, protocol) { +async function login(username, password, env, protocol, verbose) { let data = new URLSearchParams(); data.append('Username', username); data.append('Password', password); @@ -123,16 +122,21 @@ async function login(username, password, env, protocol) { if (res.ok) { let user = parseCookies(res.headers.get('set-cookie')).user; - return await getToken(user, env, protocol) + if (!user) return; + return await getToken(user, env, protocol, verbose) } else { - console.log(res) + if (verbose) console.info(res) + console.log(`Login attempt failed with username ${username}, please verify its a valid user in your Dynamicweb solution.`) } } function parseCookies (cookieHeader) { const list = {}; - if (!cookieHeader) return list; + if (!cookieHeader) { + console.log(`Could not get the necessary information from the login request, please verify its a valid user in your Dynamicweb solution.`) + return list; + } cookieHeader.replace('httponly, ', '').replace('Dynamicweb.Admin', 'user').split(`;`).forEach(cookie => { let [ name, ...rest] = cookie.split(`=`); @@ -143,11 +147,15 @@ function parseCookies (cookieHeader) { list[name] = decodeURIComponent(value); }); + if (!list.user) { + console.log(`Could not get the necessary information from the login request, please verify its a valid user in your Dynamicweb solution.`) + } + return list; } -async function getToken(user, env, protocol) { - var res = await fetch(`${getConfig().env[env].protocol}://${getConfig().env[env].host}/Admin/Authentication/Token`, { +async function getToken(user, env, protocol, verbose) { + var res = await fetch(`${protocol}://${getConfig().env[env].host}/Admin/Authentication/Token`, { method: 'GET', headers: { 'cookie': `Dynamicweb.Admin=${user}` @@ -157,9 +165,13 @@ async function getToken(user, env, protocol) { if (res.ok) { return (await res.json()).token } + else { + if (verbose) console.info(res) + console.log(`Could not fetch the token for the logged in user ${user}, please verify its a valid user in your Dynamicweb solution.`) + } } -async function getApiKey(token, env, protocol) { +async function getApiKey(token, env, protocol, verbose) { let data = { 'Name': 'addin', 'Prefix': 'addin', @@ -179,7 +191,8 @@ async function getApiKey(token, env, protocol) { return (await res.json()).message } else { - console.log(await res.json()) + if (verbose) console.info(res) + console.log(`Could not create an API Key for the logged in user, please verify its a valid user in your Dynamicweb solution.`) } } diff --git a/package.json b/package.json index f099608..6cb7870 100644 --- a/package.json +++ b/package.json @@ -1,16 +1,31 @@ { "name": "@dynamicweb/cli", "type": "module", + "description": "Dynamicweb CLI is a commandline tool for interacting with Dynamicweb 10 solutions.", "version": "1.0.0", "main": "bin/index.js", + "files": [ + "bin/*" + ], + "keywords": [], "scripts": { "test": "echo \"Error: no test specified\" && exit 1" }, - "author": "", - "license": "ISC", + "author": "Dynamicweb A/S (https://www.dynamicweb.com)", + "repository": { + "type": "git", + "url": "git+https://github.com/dynamicweb/CLI.git" + }, + "contributors": [ + "Dynamicweb A/S" + ], + "license": "MIT", "bin": { "dw": "bin/index.js" }, + "bugs": { + "url": "https://github.com/dynamicweb/CLI/issues" + }, "dependencies": { "child_process": "^1.0.2", "degit": "^2.8.4", @@ -22,6 +37,5 @@ "path": "^0.12.7", "yargs": "^17.5.1", "yargs-interactive": "^3.0.1" - }, - "description": "" + } } From 3b8df1043c921921400c375c2c8bdcb3e0f6461d Mon Sep 17 00:00:00 2001 From: Frederik Nielsen Date: Fri, 26 May 2023 14:26:10 +0200 Subject: [PATCH 28/86] Changing api key to detail CLI instead of addin, letting files export assume files and recursive as directory is always root or specified, defaulting full files confirmation to yes --- bin/commands/files.js | 6 +++--- bin/commands/login.js | 4 ++-- bin/utils.js | 2 +- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/bin/commands/files.js b/bin/commands/files.js index 74a1626..4d3bea0 100644 --- a/bin/commands/files.js +++ b/bin/commands/files.js @@ -28,7 +28,7 @@ export function filesCommand() { .option('export', { alias: 'e', type: 'boolean', - describe: 'Exports the directory at [dirPath] to [outPath]' + describe: 'Exports the specified directory and all subdirectories at [dirPath] to [outPath]' }) .option('import', { alias: 'i', @@ -75,11 +75,11 @@ async function handleFiles(argv) { if (argv.export) { if (argv.dirPath) { - await download(env, user, argv.dirPath, argv.outPath, argv.recursive, null, argv.raw, []); + await download(env, user, argv.dirPath, argv.outPath, true, null, argv.raw, argv.iamstupid, []); } else { await interactiveConfirm('Are you sure you want a full export of files?', async () => { console.log('Full export is starting') - let filesStructure = (await getFilesStructure(env, user, '/', false, argv.includeFiles)).model; + let filesStructure = (await getFilesStructure(env, user, '/', false, true)).model; let dirs = filesStructure.directories; for (let id = 0; id < dirs.length; id++) { const dir = dirs[id]; diff --git a/bin/commands/login.js b/bin/commands/login.js index f51fa23..bb1bebc 100644 --- a/bin/commands/login.js +++ b/bin/commands/login.js @@ -161,8 +161,8 @@ async function getToken(user, env, protocol) { async function getApiKey(token, env, protocol) { let data = { - 'Name': 'addin', - 'Prefix': 'addin', + 'Name': 'DW CLI', + 'Prefix': 'CLI', 'Description': 'Auto-generated ApiKey by DW CLI' }; var res = await fetch(`${protocol}://${getConfig().env[env].host}/Admin/Api/ApiKeySave`, { diff --git a/bin/utils.js b/bin/utils.js index 3a77177..3ebbc55 100644 --- a/bin/utils.js +++ b/bin/utils.js @@ -5,7 +5,7 @@ export async function interactiveConfirm(question, func) { .interactive({ confirm: { type: 'confirm', - default: false, + default: true, describe: question, prompt: 'always' }, From 49d36116eaddc43133dd89ed745aee87bea54605 Mon Sep 17 00:00:00 2001 From: Frederik Nielsen Date: Tue, 24 Oct 2023 11:17:13 +0200 Subject: [PATCH 29/86] Fixing dw install to the adjusted AddinInstall --- bin/commands/install.js | 8 ++++++-- bin/index.js | 4 ++++ 2 files changed, 10 insertions(+), 2 deletions(-) diff --git a/bin/commands/install.js b/bin/commands/install.js index 153eee6..c0ddfa4 100644 --- a/bin/commands/install.js +++ b/bin/commands/install.js @@ -31,10 +31,14 @@ async function handleInstall(argv) { async function installAddin(env, user, resolvedPath) { console.log('Installing addin') + console.log(path.basename(resolvedPath)) + let filename = path.basename(resolvedPath); let data = { - 'AddinProvider': 'Dynamicweb.Marketplace.Providers.LocalAddinProvider', - 'Package': path.basename(resolvedPath) + 'Ids': [ + `${filename.substring(0, filename.lastIndexOf('.')) || filename}|${path.extname(resolvedPath)}` + ] } + console.log(data) let res = await fetch(`${env.protocol}://${env.host}/Admin/Api/AddinInstall`, { method: 'POST', body: JSON.stringify(data), diff --git a/bin/index.js b/bin/index.js index a587900..9ed1d6b 100644 --- a/bin/index.js +++ b/bin/index.js @@ -39,6 +39,10 @@ function baseCommand() { command: '$0', describe: 'Shows the current env and user being used', handler: () => { + if (Object.keys(getConfig()).length === 0) { + console.log('To login to a solution use `dw login`') + return; + } console.log(`Environment: ${getConfig()?.current?.env}`) console.log(`User: ${getConfig()?.env[getConfig()?.current?.env]?.current?.user}`) console.log(`Protocol: ${getConfig()?.env[getConfig()?.current?.env]?.protocol}`) From 70d27f26fb8f4e54acd44353a9fff23464b87583 Mon Sep 17 00:00:00 2001 From: Frederik Nielsen Date: Tue, 24 Oct 2023 13:40:05 +0200 Subject: [PATCH 30/86] Adding error response --- bin/commands/install.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/bin/commands/install.js b/bin/commands/install.js index c0ddfa4..60340cb 100644 --- a/bin/commands/install.js +++ b/bin/commands/install.js @@ -54,6 +54,7 @@ async function installAddin(env, user, resolvedPath) { console.log(`Addin installed`) } else { - console.log(res) + console.log('Request failed, returned error:') + console.log(await res.json()) } } \ No newline at end of file From 7377db471f5a96d5acf3d72f03b1ac5503f3f399 Mon Sep 17 00:00:00 2001 From: Frederik Nielsen Date: Fri, 27 Oct 2023 09:55:21 +0200 Subject: [PATCH 31/86] Removing console log line --- bin/commands/install.js | 1 - 1 file changed, 1 deletion(-) diff --git a/bin/commands/install.js b/bin/commands/install.js index 60340cb..4fb5da7 100644 --- a/bin/commands/install.js +++ b/bin/commands/install.js @@ -31,7 +31,6 @@ async function handleInstall(argv) { async function installAddin(env, user, resolvedPath) { console.log('Installing addin') - console.log(path.basename(resolvedPath)) let filename = path.basename(resolvedPath); let data = { 'Ids': [ From c5ae1fccf79ab692f3a30d6fb544af8fcee468d2 Mon Sep 17 00:00:00 2001 From: Frederik Nielsen Date: Fri, 27 Oct 2023 09:58:45 +0200 Subject: [PATCH 32/86] Removing another log line --- bin/commands/install.js | 1 - 1 file changed, 1 deletion(-) diff --git a/bin/commands/install.js b/bin/commands/install.js index 4fb5da7..daef531 100644 --- a/bin/commands/install.js +++ b/bin/commands/install.js @@ -37,7 +37,6 @@ async function installAddin(env, user, resolvedPath) { `${filename.substring(0, filename.lastIndexOf('.')) || filename}|${path.extname(resolvedPath)}` ] } - console.log(data) let res = await fetch(`${env.protocol}://${env.host}/Admin/Api/AddinInstall`, { method: 'POST', body: JSON.stringify(data), From 2151ae20f0db0eb969c312d22e0d13cac61980b7 Mon Sep 17 00:00:00 2001 From: frederik5480 Date: Fri, 27 Oct 2023 10:30:26 +0200 Subject: [PATCH 33/86] Update package.json --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 6cb7870..a7c16af 100644 --- a/package.json +++ b/package.json @@ -2,7 +2,7 @@ "name": "@dynamicweb/cli", "type": "module", "description": "Dynamicweb CLI is a commandline tool for interacting with Dynamicweb 10 solutions.", - "version": "1.0.0", + "version": "1.0.1", "main": "bin/index.js", "files": [ "bin/*" From 9528e80a05ce5c7bade3e357f9e4110452252f4b Mon Sep 17 00:00:00 2001 From: Frederik Nielsen Date: Fri, 3 Nov 2023 09:43:07 +0100 Subject: [PATCH 34/86] Allowing importing a folder to DW --- bin/commands/files.js | 81 ++++++++++++++++++++++++++++++++++++----- bin/commands/install.js | 4 +- 2 files changed, 73 insertions(+), 12 deletions(-) diff --git a/bin/commands/files.js b/bin/commands/files.js index 4d3bea0..554d005 100644 --- a/bin/commands/files.js +++ b/bin/commands/files.js @@ -35,19 +35,28 @@ export function filesCommand() { type: 'boolean', describe: 'Imports the file at [dirPath] to [outPath]' }) + .option('overwrite', { + alias: 'o', + type: 'boolean', + describe: 'Used with import, will overwrite existing files at destrination if set to true' + }) + .option('createEmpty', { + type: 'boolean', + describe: 'Used with import, will create a file even if its empty' + }) .option('includeFiles', { alias: 'f', type: 'boolean', - describe: 'Includes files in list of directories and files' + describe: 'Used with export, includes files in list of directories and files' }) .option('recursive', { alias: 'r', type: 'boolean', - describe: 'Handles all directories recursively' + describe: 'Used with list, import and export, handles all directories recursively' }) .option('raw', { type: 'boolean', - describe: 'Keeps zip file instead of unpacking it' + describe: 'Used with export, keeps zip file instead of unpacking it' }) .option('iamstupid', { type: 'boolean', @@ -91,12 +100,61 @@ async function handleFiles(argv) { } } else if (argv.import) { if (argv.dirPath && argv.outPath) { - let resolvedPath = path.resolve(argv.dirPath) - await uploadFile(env, user, resolvedPath, argv.outPath); + let resolvedPath = path.resolve(argv.dirPath); + let files; + if (!argv.overwrite) { + files = (await getFilesStructure(env, user, argv.outPath, argv.recursive, true)).model; + } + if (argv.recursive) { + await processDirectory(env, user, resolvedPath, argv.outPath, files, resolvedPath, argv.createEmpty, true); + } else { + let filesInDir = getFilesInDirectory(resolvedPath); + if (files) + filesInDir = getFilesNotInData(filesInDir, files.files.data, resolvedPath); + await uploadFiles(env, user, filesInDir, argv.outPath, argv.createEmpty); + } } } } +function convertToDataFormat(filePath, resolvedPath) { + const relativePath = `/Files${filePath.substring(resolvedPath.length)}`; + return path.format(path.parse(relativePath)).replace(/\\/g, '/'); +} + +function getFilesNotInData(filesInDir, data, resolvedPath) { + const existingPaths = data.map(file => file.filePath); + return filesInDir.filter(filePath => { + const convertedPath = convertToDataFormat(filePath, resolvedPath); + return !existingPaths.includes(convertedPath); + }); +} + +function getFilesInDirectory(dirPath) { + return fs.readdirSync(dirPath) + .map(file => path.join(dirPath, file)) + .filter(file => fs.statSync(file).isFile()); +} + +async function processDirectory(env, user, dirPath, outPath, files, originalDir, createEmpty, isRoot = false) { + let filesInDir = getFilesInDirectory(dirPath); + let missingFiles; + if (files === undefined) + missingFiles = filesInDir; + else + missingFiles = getFilesNotInData(filesInDir, files.files.data, originalDir); + if (missingFiles.length > 0) + await uploadFiles(env, user, missingFiles, isRoot ? outPath : path.join(outPath, path.basename(dirPath)), createEmpty); + + const subDirectories = fs.readdirSync(dirPath) + .map(subDir => path.join(dirPath, subDir)) + .filter(subDir => fs.statSync(subDir).isDirectory()); + for (let subDir of subDirectories) { + const remoteSubDir = files?.directories.find(dir => dir.name === path.basename(subDir)); + await processDirectory(env, user, subDir, isRoot ? outPath : path.join(outPath, path.basename(dirPath)), remoteSubDir, originalDir, createEmpty); + } +} + function resolveTree(dirs, indentLevel, parentHasFiles) { let end = `└──` let mid = `├──` @@ -208,12 +266,15 @@ async function getFilesStructure(env, user, dirPath, recursive, includeFiles) { } } -export async function uploadFile(env, user, localFilePath, destinationPath) { - console.log('Uploading file') +export async function uploadFiles(env, user, localFilePaths, destinationPath, createEmpty = false) { + console.log('Uploading files') let form = new FormData(); form.append('path', destinationPath); - form.append('files', fs.createReadStream(localFilePath)); - let res = await fetch(`${env.protocol}://${env.host}/Admin/Api/Upload`, { + localFilePaths.forEach((localPath, index) => { + console.log(localPath) + form.append('files', fs.createReadStream(path.resolve(localPath))); + }); + let res = await fetch(`${env.protocol}://${env.host}/Admin/Api/Upload?` + new URLSearchParams({"createEmptyFiles": createEmpty}), { method: 'POST', body: form, headers: { @@ -223,7 +284,7 @@ export async function uploadFile(env, user, localFilePath, destinationPath) { }); if (res.ok) { if (env.verbose) console.log(await res.json()) - console.log(`File uploaded`) + console.log(`Files uploaded`) } else { console.log(res) diff --git a/bin/commands/install.js b/bin/commands/install.js index daef531..2456cb2 100644 --- a/bin/commands/install.js +++ b/bin/commands/install.js @@ -2,7 +2,7 @@ import fetch from 'node-fetch'; import path from 'path'; import { setupEnv, getAgent } from './env.js'; import { setupUser } from './login.js'; -import { uploadFile } from './files.js'; +import { uploadFiles } from './files.js'; export function installCommand() { return { @@ -25,7 +25,7 @@ async function handleInstall(argv) { let env = await setupEnv(argv); let user = await setupUser(argv, env); let resolvedPath = path.resolve(argv.filePath) - await uploadFile(env, user, resolvedPath, 'System/AddIns/Local'); + await uploadFiles(env, user, [resolvedPath], 'System/AddIns/Local'); await installAddin(env, user, resolvedPath) } From 7e72957a19c9045a7abdb76ffd67143d44574605 Mon Sep 17 00:00:00 2001 From: Frederik Nielsen Date: Fri, 3 Nov 2023 09:44:45 +0100 Subject: [PATCH 35/86] Upping version --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index a7c16af..ec93eae 100644 --- a/package.json +++ b/package.json @@ -2,7 +2,7 @@ "name": "@dynamicweb/cli", "type": "module", "description": "Dynamicweb CLI is a commandline tool for interacting with Dynamicweb 10 solutions.", - "version": "1.0.1", + "version": "1.0.2", "main": "bin/index.js", "files": [ "bin/*" From 24b0d0052a75b8d41fda017553cfd8503fb1007b Mon Sep 17 00:00:00 2001 From: Frederik Nielsen Date: Fri, 17 Nov 2023 10:09:53 +0100 Subject: [PATCH 36/86] Allowing specifically setting protocol, host and apiKey for a CLI command --- bin/commands/env.js | 18 +++++++++++++++--- bin/commands/login.js | 20 +++++++++++++++++--- bin/index.js | 9 +++++++++ package.json | 2 +- 4 files changed, 42 insertions(+), 7 deletions(-) diff --git a/bin/commands/env.js b/bin/commands/env.js index 8a6586f..435aa8e 100644 --- a/bin/commands/env.js +++ b/bin/commands/env.js @@ -40,15 +40,27 @@ export function envCommand() { } export async function setupEnv(argv) { - let env; - if (getConfig().env) { + let env = {}; + let askEnv = true; + + if (argv.host) { + askEnv = false; + env.host = argv.host; + if (argv.protocol) { + env.protocol = argv.protocol; + } else { + env.protocol = 'https'; + } + } + + if (askEnv && getConfig().env) { env = getConfig().env[argv.env] || getConfig().env[getConfig()?.current?.env]; if (!env.protocol) { console.log('Protocol for environment not set, defaulting to https'); env.protocol = 'https'; } } - if (!env) { + else if (askEnv) { console.log('Current environment not set, please set it') await interactiveEnv(argv, { environment: { diff --git a/bin/commands/login.js b/bin/commands/login.js index e71ee24..7bed23c 100644 --- a/bin/commands/login.js +++ b/bin/commands/login.js @@ -18,11 +18,24 @@ export function loginCommand() { } export async function setupUser(argv, env) { - let user; - if (env.users) { + let user = {}; + let askLogin = true; + + if (argv.apiKey) { + user.apiKey = argv.apiKey; + askLogin = false; + } + + if (!user.apiKey && env.users) { user = env.users[argv.user] || env.users[env.current.user]; + askLogin = false; } - if (!user) { + + if (askLogin && argv.host) { + console.log('Please add an --apiKey to the command as overriding the host requires that.') + process.exit(); + } + else if (askLogin) { console.log('Current user not set, please login') await interactiveLogin(argv, { environment: { @@ -42,6 +55,7 @@ export async function setupUser(argv, env) { }) user = env.users[env.current.user]; } + return user; } diff --git a/bin/index.js b/bin/index.js index 9ed1d6b..32f98c7 100644 --- a/bin/index.js +++ b/bin/index.js @@ -31,6 +31,15 @@ yargs(hideBin(process.argv)) type: 'boolean', description: 'Run with verbose logging' }) + .option('protocol', { + description: 'Allows setting the protocol used, only used together with --host, defaulting to https' + }) + .option('host', { + description: 'Allows setting the host used, only allowed if an --apiKey is specified' + }) + .option('apiKey', { + description: 'Allows setting the apiKey for an environmentless execution of the CLI command' + }) .demandCommand() .parse() diff --git a/package.json b/package.json index ec93eae..458756c 100644 --- a/package.json +++ b/package.json @@ -2,7 +2,7 @@ "name": "@dynamicweb/cli", "type": "module", "description": "Dynamicweb CLI is a commandline tool for interacting with Dynamicweb 10 solutions.", - "version": "1.0.2", + "version": "1.0.3", "main": "bin/index.js", "files": [ "bin/*" From e176f2677c0fe64902bc8f2c111cfb2de7c0823d Mon Sep 17 00:00:00 2001 From: Frederik Nielsen Date: Thu, 30 May 2024 12:34:44 +0200 Subject: [PATCH 37/86] Creating missing directories when uploading files --- bin/commands/files.js | 4 ++-- package.json | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/bin/commands/files.js b/bin/commands/files.js index 554d005..2985881 100644 --- a/bin/commands/files.js +++ b/bin/commands/files.js @@ -213,7 +213,6 @@ async function download(env, user, dirPath, outPath, recursive, outname, raw, ia console.log('Downloading', dirPath === '/.' ? 'Base' : dirPath, 'Recursive=' + recursive); let filename; - fetch(`${env.protocol}://${env.host}/Admin/Api/${endpoint}`, { method: 'POST', body: JSON.stringify(data), @@ -274,7 +273,7 @@ export async function uploadFiles(env, user, localFilePaths, destinationPath, cr console.log(localPath) form.append('files', fs.createReadStream(path.resolve(localPath))); }); - let res = await fetch(`${env.protocol}://${env.host}/Admin/Api/Upload?` + new URLSearchParams({"createEmptyFiles": createEmpty}), { + let res = await fetch(`${env.protocol}://${env.host}/Admin/Api/Upload?` + new URLSearchParams({"createEmptyFiles": createEmpty, "createMissingDirectories": true}), { method: 'POST', body: form, headers: { @@ -288,6 +287,7 @@ export async function uploadFiles(env, user, localFilePaths, destinationPath, cr } else { console.log(res) + console.log(res.json()) return; } } diff --git a/package.json b/package.json index 458756c..4c0deba 100644 --- a/package.json +++ b/package.json @@ -2,7 +2,7 @@ "name": "@dynamicweb/cli", "type": "module", "description": "Dynamicweb CLI is a commandline tool for interacting with Dynamicweb 10 solutions.", - "version": "1.0.3", + "version": "1.0.5", "main": "bin/index.js", "files": [ "bin/*" From 06eb6b9f7ea11b001195040abee22c25c03020b1 Mon Sep 17 00:00:00 2001 From: Frederik Nielsen Date: Mon, 29 Jul 2024 15:09:52 +0200 Subject: [PATCH 38/86] Removing old overwrite functionality, now relying on dw upload endpoint logic --- bin/commands/files.js | 38 ++++++++++---------------------------- package.json | 2 +- 2 files changed, 11 insertions(+), 29 deletions(-) diff --git a/bin/commands/files.js b/bin/commands/files.js index 2985881..5854b6b 100644 --- a/bin/commands/files.js +++ b/bin/commands/files.js @@ -101,17 +101,11 @@ async function handleFiles(argv) { } else if (argv.import) { if (argv.dirPath && argv.outPath) { let resolvedPath = path.resolve(argv.dirPath); - let files; - if (!argv.overwrite) { - files = (await getFilesStructure(env, user, argv.outPath, argv.recursive, true)).model; - } if (argv.recursive) { - await processDirectory(env, user, resolvedPath, argv.outPath, files, resolvedPath, argv.createEmpty, true); + await processDirectory(env, user, resolvedPath, argv.outPath, resolvedPath, argv.createEmpty, true, argv.overwrite); } else { let filesInDir = getFilesInDirectory(resolvedPath); - if (files) - filesInDir = getFilesNotInData(filesInDir, files.files.data, resolvedPath); - await uploadFiles(env, user, filesInDir, argv.outPath, argv.createEmpty); + await uploadFiles(env, user, filesInDir, argv.outPath, argv.createEmpty, argv.overwrite); } } } @@ -122,36 +116,22 @@ function convertToDataFormat(filePath, resolvedPath) { return path.format(path.parse(relativePath)).replace(/\\/g, '/'); } -function getFilesNotInData(filesInDir, data, resolvedPath) { - const existingPaths = data.map(file => file.filePath); - return filesInDir.filter(filePath => { - const convertedPath = convertToDataFormat(filePath, resolvedPath); - return !existingPaths.includes(convertedPath); - }); -} - function getFilesInDirectory(dirPath) { return fs.readdirSync(dirPath) .map(file => path.join(dirPath, file)) .filter(file => fs.statSync(file).isFile()); } -async function processDirectory(env, user, dirPath, outPath, files, originalDir, createEmpty, isRoot = false) { +async function processDirectory(env, user, dirPath, outPath, originalDir, createEmpty, isRoot = false, overwrite = false) { let filesInDir = getFilesInDirectory(dirPath); - let missingFiles; - if (files === undefined) - missingFiles = filesInDir; - else - missingFiles = getFilesNotInData(filesInDir, files.files.data, originalDir); - if (missingFiles.length > 0) - await uploadFiles(env, user, missingFiles, isRoot ? outPath : path.join(outPath, path.basename(dirPath)), createEmpty); + if (filesInDir.length > 0) + await uploadFiles(env, user, filesInDir, isRoot ? outPath : path.join(outPath, path.basename(dirPath)), createEmpty, overwrite); const subDirectories = fs.readdirSync(dirPath) .map(subDir => path.join(dirPath, subDir)) .filter(subDir => fs.statSync(subDir).isDirectory()); for (let subDir of subDirectories) { - const remoteSubDir = files?.directories.find(dir => dir.name === path.basename(subDir)); - await processDirectory(env, user, subDir, isRoot ? outPath : path.join(outPath, path.basename(dirPath)), remoteSubDir, originalDir, createEmpty); + await processDirectory(env, user, subDir, isRoot ? outPath : path.join(outPath, path.basename(dirPath)), originalDir, createEmpty, overwrite); } } @@ -265,10 +245,12 @@ async function getFilesStructure(env, user, dirPath, recursive, includeFiles) { } } -export async function uploadFiles(env, user, localFilePaths, destinationPath, createEmpty = false) { +export async function uploadFiles(env, user, localFilePaths, destinationPath, createEmpty = false, overwrite = false) { console.log('Uploading files') let form = new FormData(); form.append('path', destinationPath); + form.append('skipExistingFiles', String(!overwrite)); + form.append('allowOverwrite', String(overwrite)); localFilePaths.forEach((localPath, index) => { console.log(localPath) form.append('files', fs.createReadStream(path.resolve(localPath))); @@ -282,7 +264,7 @@ export async function uploadFiles(env, user, localFilePaths, destinationPath, cr agent: getAgent(env.protocol) }); if (res.ok) { - if (env.verbose) console.log(await res.json()) + console.log(await res.json()) console.log(`Files uploaded`) } else { diff --git a/package.json b/package.json index 4c0deba..1c6efd3 100644 --- a/package.json +++ b/package.json @@ -2,7 +2,7 @@ "name": "@dynamicweb/cli", "type": "module", "description": "Dynamicweb CLI is a commandline tool for interacting with Dynamicweb 10 solutions.", - "version": "1.0.5", + "version": "1.0.6", "main": "bin/index.js", "files": [ "bin/*" From d27e082dd0f750e0b1fbc51c46bca1c1843ddb8d Mon Sep 17 00:00:00 2001 From: Frederik Nielsen Date: Tue, 30 Jul 2024 13:52:26 +0200 Subject: [PATCH 39/86] Adding missing argument for recursive import --- bin/commands/files.js | 7 +------ package.json | 2 +- 2 files changed, 2 insertions(+), 7 deletions(-) diff --git a/bin/commands/files.js b/bin/commands/files.js index 5854b6b..a4f28e1 100644 --- a/bin/commands/files.js +++ b/bin/commands/files.js @@ -111,11 +111,6 @@ async function handleFiles(argv) { } } -function convertToDataFormat(filePath, resolvedPath) { - const relativePath = `/Files${filePath.substring(resolvedPath.length)}`; - return path.format(path.parse(relativePath)).replace(/\\/g, '/'); -} - function getFilesInDirectory(dirPath) { return fs.readdirSync(dirPath) .map(file => path.join(dirPath, file)) @@ -131,7 +126,7 @@ async function processDirectory(env, user, dirPath, outPath, originalDir, create .map(subDir => path.join(dirPath, subDir)) .filter(subDir => fs.statSync(subDir).isDirectory()); for (let subDir of subDirectories) { - await processDirectory(env, user, subDir, isRoot ? outPath : path.join(outPath, path.basename(dirPath)), originalDir, createEmpty, overwrite); + await processDirectory(env, user, subDir, isRoot ? outPath : path.join(outPath, path.basename(dirPath)), originalDir, createEmpty, false, overwrite); } } diff --git a/package.json b/package.json index 1c6efd3..aef7c1e 100644 --- a/package.json +++ b/package.json @@ -2,7 +2,7 @@ "name": "@dynamicweb/cli", "type": "module", "description": "Dynamicweb CLI is a commandline tool for interacting with Dynamicweb 10 solutions.", - "version": "1.0.6", + "version": "1.0.7", "main": "bin/index.js", "files": [ "bin/*" From 1495f8b1e9399644d033ef1ad952b4a18404f8ba Mon Sep 17 00:00:00 2001 From: Frederik Nielsen Date: Wed, 4 Sep 2024 13:46:03 +0200 Subject: [PATCH 40/86] Adding overwrite when uploading addin when using dw install --- bin/commands/install.js | 2 +- package.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/bin/commands/install.js b/bin/commands/install.js index 2456cb2..2a1dee1 100644 --- a/bin/commands/install.js +++ b/bin/commands/install.js @@ -25,7 +25,7 @@ async function handleInstall(argv) { let env = await setupEnv(argv); let user = await setupUser(argv, env); let resolvedPath = path.resolve(argv.filePath) - await uploadFiles(env, user, [resolvedPath], 'System/AddIns/Local'); + await uploadFiles(env, user, [resolvedPath], 'System/AddIns/Local', false, true); await installAddin(env, user, resolvedPath) } diff --git a/package.json b/package.json index aef7c1e..7864c74 100644 --- a/package.json +++ b/package.json @@ -2,7 +2,7 @@ "name": "@dynamicweb/cli", "type": "module", "description": "Dynamicweb CLI is a commandline tool for interacting with Dynamicweb 10 solutions.", - "version": "1.0.7", + "version": "1.0.8", "main": "bin/index.js", "files": [ "bin/*" From 08446a58ed49376bfd27e0110c0a79282e2f1c8f Mon Sep 17 00:00:00 2001 From: Frederik Nielsen Date: Thu, 26 Sep 2024 14:16:16 +0200 Subject: [PATCH 41/86] Adding exit code on errors and allowing wildcard for filenames in dw install --- bin/commands/command.js | 1 + bin/commands/database.js | 2 +- bin/commands/files.js | 31 ++++++++++++++++++++++++++++--- bin/commands/install.js | 8 ++++---- bin/commands/login.js | 2 ++ bin/commands/query.js | 1 + package.json | 2 +- 7 files changed, 38 insertions(+), 9 deletions(-) diff --git a/bin/commands/command.js b/bin/commands/command.js index 427e74a..cb6507c 100644 --- a/bin/commands/command.js +++ b/bin/commands/command.js @@ -83,6 +83,7 @@ async function runCommand(env, user, command, queryParams, data) { }) if (!res.ok) { console.log(`Error when doing request ${res.url}`) + process.exit(1); } return await res.json() } \ No newline at end of file diff --git a/bin/commands/database.js b/bin/commands/database.js index 50ee437..4ae4357 100644 --- a/bin/commands/database.js +++ b/bin/commands/database.js @@ -58,7 +58,7 @@ async function download(env, user, path, verbose) { return res; }).then(async (res) => { if (!res) { - return; + process.exit(1); } const fileStream = fs.createWriteStream(_path.resolve(`${_path.resolve(path)}/${filename}`)); await new Promise((resolve, reject) => { diff --git a/bin/commands/files.js b/bin/commands/files.js index a4f28e1..ad4e523 100644 --- a/bin/commands/files.js +++ b/bin/commands/files.js @@ -237,6 +237,8 @@ async function getFilesStructure(env, user, dirPath, recursive, includeFiles) { return await res.json(); } else { console.log(res); + console.log(res.json()); + process.exit(1); } } @@ -247,8 +249,11 @@ export async function uploadFiles(env, user, localFilePaths, destinationPath, cr form.append('skipExistingFiles', String(!overwrite)); form.append('allowOverwrite', String(overwrite)); localFilePaths.forEach((localPath, index) => { - console.log(localPath) - form.append('files', fs.createReadStream(path.resolve(localPath))); + let fileToUpload = resolveFilePath(localPath) + console.log(fileToUpload) + if (fileToUpload === undefined) + return; + form.append('files', fs.createReadStream(path.resolve(fileToUpload))); }); let res = await fetch(`${env.protocol}://${env.host}/Admin/Api/Upload?` + new URLSearchParams({"createEmptyFiles": createEmpty, "createMissingDirectories": true}), { method: 'POST', @@ -265,6 +270,26 @@ export async function uploadFiles(env, user, localFilePaths, destinationPath, cr else { console.log(res) console.log(res.json()) - return; + process.exit(1); + } +} + +export function resolveFilePath(filePath) { + let p = path.parse(path.resolve(filePath)) + let regex = wildcardToRegExp(p.base); + let resolvedPath = fs.readdirSync(p.dir).filter((allFilesPaths) => allFilesPaths.match(regex) !== null)[0] + if (resolvedPath === undefined) + { + console.log('Could not find any files with the name ' + filePath); + process.exit(1); } + return resolvedPath; +} + +function wildcardToRegExp(wildcard) { + return new RegExp('^' + wildcard + .replace(/\./g, '\\.') + .replace(/\*/g, '.*') + .replace(/\?/g, '.') + + '$'); } diff --git a/bin/commands/install.js b/bin/commands/install.js index 2456cb2..c6be1cb 100644 --- a/bin/commands/install.js +++ b/bin/commands/install.js @@ -2,7 +2,7 @@ import fetch from 'node-fetch'; import path from 'path'; import { setupEnv, getAgent } from './env.js'; import { setupUser } from './login.js'; -import { uploadFiles } from './files.js'; +import { uploadFiles, resolveFilePath } from './files.js'; export function installCommand() { return { @@ -24,9 +24,8 @@ export function installCommand() { async function handleInstall(argv) { let env = await setupEnv(argv); let user = await setupUser(argv, env); - let resolvedPath = path.resolve(argv.filePath) - await uploadFiles(env, user, [resolvedPath], 'System/AddIns/Local'); - await installAddin(env, user, resolvedPath) + await uploadFiles(env, user, [ argv.filePath ], 'System/AddIns/Local', false, true); + await installAddin(env, user, resolveFilePath(argv.filePath)) } async function installAddin(env, user, resolvedPath) { @@ -54,5 +53,6 @@ async function installAddin(env, user, resolvedPath) { else { console.log('Request failed, returned error:') console.log(await res.json()) + process.exit(1); } } \ No newline at end of file diff --git a/bin/commands/login.js b/bin/commands/login.js index 7bed23c..60b0358 100644 --- a/bin/commands/login.js +++ b/bin/commands/login.js @@ -135,6 +135,8 @@ async function login(username, password, env, protocol, verbose) { }); if (res.ok) { + console.log(res) + console.log(res.json()) let user = parseCookies(res.headers.get('set-cookie')).user; if (!user) return; return await getToken(user, env, protocol, verbose) diff --git a/bin/commands/query.js b/bin/commands/query.js index 85744a3..f542da6 100644 --- a/bin/commands/query.js +++ b/bin/commands/query.js @@ -87,6 +87,7 @@ async function runQuery(env, user, query, params) { }) if (!res.ok) { console.log(`Error when doing request ${res.url}`) + process.exit(1); } return await res.json() } \ No newline at end of file diff --git a/package.json b/package.json index aef7c1e..2671965 100644 --- a/package.json +++ b/package.json @@ -2,7 +2,7 @@ "name": "@dynamicweb/cli", "type": "module", "description": "Dynamicweb CLI is a commandline tool for interacting with Dynamicweb 10 solutions.", - "version": "1.0.7", + "version": "1.0.9", "main": "bin/index.js", "files": [ "bin/*" From fcfc9879047adf50a934fc3ae4c84fd0253dbb27 Mon Sep 17 00:00:00 2001 From: Frederik Nielsen Date: Fri, 27 Sep 2024 13:11:40 +0200 Subject: [PATCH 42/86] Fixing importing files and folders --- bin/commands/files.js | 4 ++-- package.json | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/bin/commands/files.js b/bin/commands/files.js index ad4e523..c8f8a12 100644 --- a/bin/commands/files.js +++ b/bin/commands/files.js @@ -112,7 +112,7 @@ async function handleFiles(argv) { } function getFilesInDirectory(dirPath) { - return fs.readdirSync(dirPath) + return fs.statSync(dirPath).isFile() ? [ dirPath ] : fs.readdirSync(dirPath) .map(file => path.join(dirPath, file)) .filter(file => fs.statSync(file).isFile()); } @@ -283,7 +283,7 @@ export function resolveFilePath(filePath) { console.log('Could not find any files with the name ' + filePath); process.exit(1); } - return resolvedPath; + return path.join(p.dir, resolvedPath); } function wildcardToRegExp(wildcard) { diff --git a/package.json b/package.json index 2671965..85ed533 100644 --- a/package.json +++ b/package.json @@ -2,7 +2,7 @@ "name": "@dynamicweb/cli", "type": "module", "description": "Dynamicweb CLI is a commandline tool for interacting with Dynamicweb 10 solutions.", - "version": "1.0.9", + "version": "1.0.10", "main": "bin/index.js", "files": [ "bin/*" From 2cd44fda5d33df540f1e8d7f348cc1a6fedd0968 Mon Sep 17 00:00:00 2001 From: Frederik Nielsen Date: Wed, 2 Oct 2024 11:23:40 +0200 Subject: [PATCH 43/86] Adding + as a valid filename when uploading and fixing i and l for query together with new dw version --- bin/commands/files.js | 1 + bin/commands/query.js | 15 ++++++++++++--- package.json | 2 +- 3 files changed, 14 insertions(+), 4 deletions(-) diff --git a/bin/commands/files.js b/bin/commands/files.js index c8f8a12..a3fba9a 100644 --- a/bin/commands/files.js +++ b/bin/commands/files.js @@ -290,6 +290,7 @@ function wildcardToRegExp(wildcard) { return new RegExp('^' + wildcard .replace(/\./g, '\\.') .replace(/\*/g, '.*') + .replace(/\+/g, '.+') .replace(/\?/g, '.') + '$'); } diff --git a/bin/commands/query.js b/bin/commands/query.js index f542da6..460f5ed 100644 --- a/bin/commands/query.js +++ b/bin/commands/query.js @@ -54,16 +54,25 @@ async function getProperties(argv) { }) if (res.ok) { let body = await res.json() - return body.model.propertyNames + if (body.model.properties.groups === undefined) { + console.log('Unable to fetch query parameters'); + process.exit(1); + } + return body.model.properties.groups.filter(g => g.name === 'Properties')[0].fields.map(field => `${field.name} (${field.typeName})`) } - console.log(res) + console.log('Unable to fetch query parameters'); + console.log(res); + process.exit(1); } async function getQueryParams(argv) { let params = {} if (argv.interactive) { let props = { interactive: { default: true }} - Array.from(await getProperties(argv)).forEach(p => props[p] = { type: 'input', prompt: 'if-no-arg'}) + let properties = await getProperties(argv); + console.log('The following properties will be requested:') + console.log(properties) + Array.from(properties).forEach(p => props[p] = { type: 'input', prompt: 'if-no-arg'}) await yargsInteractive() .interactive(props) .then((result) => { diff --git a/package.json b/package.json index 85ed533..cdf5787 100644 --- a/package.json +++ b/package.json @@ -2,7 +2,7 @@ "name": "@dynamicweb/cli", "type": "module", "description": "Dynamicweb CLI is a commandline tool for interacting with Dynamicweb 10 solutions.", - "version": "1.0.10", + "version": "1.0.11", "main": "bin/index.js", "files": [ "bin/*" From b0cde46d363c9dfb6fd74d28b2b3dde492262d0a Mon Sep 17 00:00:00 2001 From: Jeppe Eriksson Agger Date: Fri, 18 Oct 2024 00:20:14 +0200 Subject: [PATCH 44/86] Fixed wildcard replace logic Wildcards did not translate correctly to regex as not all escape characters were handled. --- bin/commands/files.js | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/bin/commands/files.js b/bin/commands/files.js index a3fba9a..65a9d7e 100644 --- a/bin/commands/files.js +++ b/bin/commands/files.js @@ -286,11 +286,8 @@ export function resolveFilePath(filePath) { return path.join(p.dir, resolvedPath); } + function wildcardToRegExp(wildcard) { - return new RegExp('^' + wildcard - .replace(/\./g, '\\.') - .replace(/\*/g, '.*') - .replace(/\+/g, '.+') - .replace(/\?/g, '.') - + '$'); + return new RegExp('^' + wildcard.replace(/[.*+?^${}()|[\]\\]/g, '\\$&') + '$'); } + From 1aefd1671c17076aa68bd738f76a3723e6bd8d37 Mon Sep 17 00:00:00 2001 From: Frederik Nielsen Date: Fri, 18 Oct 2024 08:08:44 +0200 Subject: [PATCH 45/86] Upping version --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index cdf5787..24adedb 100644 --- a/package.json +++ b/package.json @@ -2,7 +2,7 @@ "name": "@dynamicweb/cli", "type": "module", "description": "Dynamicweb CLI is a commandline tool for interacting with Dynamicweb 10 solutions.", - "version": "1.0.11", + "version": "1.0.12", "main": "bin/index.js", "files": [ "bin/*" From 6d581e301cacff90720ca59f50910f1e4d85d7a6 Mon Sep 17 00:00:00 2001 From: Frederik Nielsen Date: Mon, 20 Jan 2025 11:33:15 +0100 Subject: [PATCH 46/86] Fixing dw login and adding queue property to dw install --- bin/commands/install.js | 10 ++++++++-- bin/commands/login.js | 8 ++++---- package.json | 2 +- 3 files changed, 13 insertions(+), 7 deletions(-) diff --git a/bin/commands/install.js b/bin/commands/install.js index c6be1cb..836e707 100644 --- a/bin/commands/install.js +++ b/bin/commands/install.js @@ -13,6 +13,11 @@ export function installCommand() { .positional('filePath', { describe: 'Path to the file to install' }) + .option('queue', { + alias: 'q', + type: 'boolean', + describe: 'Queues the install for next Dynamicweb recycle' + }) }, handler: (argv) => { if (argv.verbose) console.info(`Installing file located at :${argv.filePath}`) @@ -25,13 +30,14 @@ async function handleInstall(argv) { let env = await setupEnv(argv); let user = await setupUser(argv, env); await uploadFiles(env, user, [ argv.filePath ], 'System/AddIns/Local', false, true); - await installAddin(env, user, resolveFilePath(argv.filePath)) + await installAddin(env, user, resolveFilePath(argv.filePath), argv.queue) } -async function installAddin(env, user, resolvedPath) { +async function installAddin(env, user, resolvedPath, queue) { console.log('Installing addin') let filename = path.basename(resolvedPath); let data = { + 'Queue': queue, 'Ids': [ `${filename.substring(0, filename.lastIndexOf('.')) || filename}|${path.extname(resolvedPath)}` ] diff --git a/bin/commands/login.js b/bin/commands/login.js index 60b0358..af692b2 100644 --- a/bin/commands/login.js +++ b/bin/commands/login.js @@ -118,6 +118,7 @@ async function loginInteractive(result, verbose) { getConfig().env[result.environment].users[result.username].apiKey = apiKey; getConfig().env[result.environment].current = getConfig().env[result.environment].current || {}; getConfig().env[result.environment].current.user = result.username; + console.log("You're now logged in as " + result.username) updateConfig(); } @@ -131,12 +132,11 @@ async function login(username, password, env, protocol, verbose) { headers: { 'Content-Type': 'application/x-www-form-urlencoded' }, - agent: getAgent(protocol) + agent: getAgent(protocol), + redirect: "manual" }); - if (res.ok) { - console.log(res) - console.log(res.json()) + if (res.ok || res.status == 302) { let user = parseCookies(res.headers.get('set-cookie')).user; if (!user) return; return await getToken(user, env, protocol, verbose) diff --git a/package.json b/package.json index 24adedb..717b6ea 100644 --- a/package.json +++ b/package.json @@ -2,7 +2,7 @@ "name": "@dynamicweb/cli", "type": "module", "description": "Dynamicweb CLI is a commandline tool for interacting with Dynamicweb 10 solutions.", - "version": "1.0.12", + "version": "1.0.13", "main": "bin/index.js", "files": [ "bin/*" From e3b35e2ecbd6946611c434fb539eb6ed6f5c9dae Mon Sep 17 00:00:00 2001 From: Jeppe Eriksson Agger Date: Thu, 29 May 2025 14:51:04 +0200 Subject: [PATCH 47/86] Improved the download experience --- bin/commands/files.js | 67 ++-- bin/downloader.js | 49 +++ bin/extractor.js | 28 ++ bin/utils.js | 107 +++++- package-lock.json | 755 +++++++++++++----------------------------- package.json | 1 + 6 files changed, 452 insertions(+), 555 deletions(-) create mode 100644 bin/downloader.js create mode 100644 bin/extractor.js diff --git a/bin/commands/files.js b/bin/commands/files.js index 65a9d7e..79f9d48 100644 --- a/bin/commands/files.js +++ b/bin/commands/files.js @@ -1,11 +1,12 @@ import fetch from 'node-fetch'; import path from 'path'; import fs from 'fs'; -import extract from 'extract-zip'; import FormData from 'form-data'; import { setupEnv, getAgent } from './env.js'; import { setupUser } from './login.js'; -import { interactiveConfirm } from '../utils.js'; +import { interactiveConfirm, formatBytes, createThrottledStatusUpdater } from '../utils.js'; +import { downloadWithProgress, getFileNameFromResponse } from '../downloader.js'; +import { extractWithProgress } from '../extractor.js'; export function filesCommand() { return { @@ -38,7 +39,7 @@ export function filesCommand() { .option('overwrite', { alias: 'o', type: 'boolean', - describe: 'Used with import, will overwrite existing files at destrination if set to true' + describe: 'Used with import, will overwrite existing files at destination if set to true' }) .option('createEmpty', { type: 'boolean', @@ -187,8 +188,7 @@ async function download(env, user, dirPath, outPath, recursive, outname, raw, ia console.log('Downloading', dirPath === '/.' ? 'Base' : dirPath, 'Recursive=' + recursive); - let filename; - fetch(`${env.protocol}://${env.host}/Admin/Api/${endpoint}`, { + const res = await fetch(`${env.protocol}://${env.host}/Admin/Api/${endpoint}`, { method: 'POST', body: JSON.stringify(data), headers: { @@ -196,33 +196,40 @@ async function download(env, user, dirPath, outPath, recursive, outname, raw, ia 'Content-Type': 'application/json' }, agent: getAgent(env.protocol) - }).then((res) => { - const header = res.headers.get('content-disposition'); - const parts = header?.split(';'); - if (!parts) { - console.log(`No files found in directory '${dirPath}', if you want to download all folders recursively include the -r flag`); - return; - } - filename = parts[1].split('=')[1].replace('+', ' '); - if (outname) filename = outname; - return res; - }).then(async (res) => { - if (!filename) return; - let filePath = path.resolve(`${path.resolve(outPath)}/${filename}`) - const fileStream = fs.createWriteStream(filePath); - await new Promise((resolve, reject) => { - res.body.pipe(fileStream); - res.body.on("error", reject); - fileStream.on("finish", resolve); - }); - console.log(`Finished downloading`, dirPath === '/.' ? '.' : dirPath, 'Recursive=' + recursive); - if (!raw) { - let filenameWithoutExtension = filename.replace('.zip', '') - await extract(filePath, { dir: `${path.resolve(outPath)}/${filenameWithoutExtension === 'Base' ? '' : filenameWithoutExtension}` }, function (err) {}) - fs.unlink(filePath, function(err) {}) + }); + + const filename = outname || getFileNameFromResponse(res); + if (!filename) return; + + let filePath = path.resolve(`${path.resolve(outPath)}/${filename}`) + let updater = createThrottledStatusUpdater(); + await downloadWithProgress(res, filePath, { + onData: (received) => { + updater.update(`Received:\t${formatBytes(received)}`); } - return res; }); + updater.stop(); + + console.log(`Finished downloading`, dirPath === '/.' ? '.' : dirPath, 'Recursive=' + recursive); + + if (!raw) { + console.log(`\nExtracting ${filename} to ${outPath}`); + + const filenameWithoutExtension = filename.replace('.zip', ''); + const destinationPath = `${path.resolve(outPath)}/${filenameWithoutExtension === 'Base' ? '' : filenameWithoutExtension}`; + + updater = createThrottledStatusUpdater(); + await extractWithProgress(filePath, destinationPath, { + onEntry: (processedEntries, totalEntries, percent) => { + updater.update(`Extracted:\t${processedEntries} of ${totalEntries} files (${percent}%)`); + } + }); + updater.stop(); + + console.log(`Finished extracting ${filename} to ${outPath}\n`); + + fs.unlink(filePath, function(err) {}); + } } async function getFilesStructure(env, user, dirPath, recursive, includeFiles) { diff --git a/bin/downloader.js b/bin/downloader.js new file mode 100644 index 0000000..2f1cb3d --- /dev/null +++ b/bin/downloader.js @@ -0,0 +1,49 @@ +import { Response } from 'node-fetch'; +import fs from 'fs'; + +/** + * Extracts the file name from the HTTP response. + * @param {Response} res - The HTTP response object. + * @returns {string} The extracted file name. + */ +export function getFileNameFromResponse(res) { + const header = res.headers.get('content-disposition'); + const parts = header?.split(';'); + + if (!parts) { + throw new Error(`No files found in directory '${dirPath}', if you want to download all folders recursively include the -r flag`); + } + + return parts[1].split('=')[1].replace('+', ' '); +} + +/** + * Downloads a file with progress reporting. + * @param {Response} res - The response from which to read the stream data. + * @param {string} filePath - The path to write the file to. + * @param {Object} options - Options for the download. + * @param {(receivedBytes: number, elapsedMillis: number, isFirstChunk: boolean) => void} options.onData - Callback invoked with each data chunk. + * @returns {Promise} Resolves when the download is complete. + */ +export function downloadWithProgress(res, filePath, options) { + const fileStream = fs.createWriteStream(filePath); + return new Promise((resolve, reject) => { + let receivedBytes = 0; + let startTime = Date.now(); + + res.body.pipe(fileStream); + res.body.on("error", reject); + fileStream.on("finish", resolve); + + res.body.on("data", chunk => { + const isFirstChunk = receivedBytes === 0; + const elapsed = Date.now() - startTime; + + receivedBytes += chunk.length; + + if (options?.onData) { + options.onData(receivedBytes, elapsed, isFirstChunk); + } + }); + }); +} \ No newline at end of file diff --git a/bin/extractor.js b/bin/extractor.js new file mode 100644 index 0000000..653ae72 --- /dev/null +++ b/bin/extractor.js @@ -0,0 +1,28 @@ +import extract from 'extract-zip'; + +/** + * Extracts files from a zip archive with progress reporting. + * + * @param {string} filePath - The path to the zip file to extract. + * @param {string} destinationPath - The directory where files will be extracted. + * @param {Object} [options] - Optional settings. + * @param {(processedEntries: number, totalEntries: number, percent: string) => void} [options.onEntry] - Callback invoked on each entry extracted. + * Receives the number of processed files, total entry count, and percent complete as arguments. + * @returns {Promise} A promise that resolves when extraction is complete. + */ +export function extractWithProgress(filePath, destinationPath, options) { + let processedEntries = 0; + + return extract(filePath, { + dir: destinationPath, + onEntry: (_, zipFile) => { + processedEntries++; + + const percent = Math.floor((processedEntries / zipFile.entryCount) * 100).toFixed(0); + + if (options?.onEntry) { + options.onEntry(processedEntries, zipFile.entryCount, percent); + } + } + }, function (err) {}); +} diff --git a/bin/utils.js b/bin/utils.js index 3ebbc55..ec0a628 100644 --- a/bin/utils.js +++ b/bin/utils.js @@ -1,4 +1,7 @@ import yargsInteractive from 'yargs-interactive'; +import logUpdate from 'log-update'; + +const WRITE_THROTTLE_MS = 500; export async function interactiveConfirm(question, func) { await yargsInteractive() @@ -17,4 +20,106 @@ export async function interactiveConfirm(question, func) { if (!result.confirm) return; func() }); -} \ No newline at end of file +} + +/** + * Creates a throttled status updater that periodically logs status messages and elapsed time. + * + * The updater allows you to update the displayed status message at any time, but throttles the actual log updates + * to the specified interval. When called with no message, the updater stops and finalizes the log output. + * + * @param {number} [intervalMs=WRITE_THROTTLE_MS] - The interval in milliseconds at which to update the log output. + * @returns {{ + * update: (message?: string) => void, + * stop: () => void + * }} An object with `update` and `stop` methods: + * - `update(message)`: Updates the status message and starts the updater if not already running. If called with no message, stops the updater. + * - `stop()`: Stops the updater and finalizes the log output. + * + * @example + * const updater = createThrottledStatusUpdater(500); + * updater.update('Processing...'); + * // ... later + * updater.update('Still working...'); + * // ... when done + * updater.update(); // or updater.stop(); + */ +export function createThrottledStatusUpdater(intervalMs = WRITE_THROTTLE_MS) { + let latestMessage; + let timer = null; + let stopped = false; + let startTime; + + function write() { + const elapsed = `Elapsed:\t${formatElapsed(Date.now() - startTime)}`; + const lines = latestMessage ? [latestMessage, elapsed] : [elapsed]; + logUpdate(lines.join('\n')); + } + + function start() { + if (timer) return; + + startTime = Date.now(); + + timer = setInterval(() => { + if (stopped) return; + write(); + }, intervalMs); + } + + function stop() { + stopped = true; + if (timer) clearInterval(timer); + write(); + logUpdate.done(); + } + + // The updater function you call from anywhere + function update(message) { + if (!message) { + stop(); + return; + } + latestMessage = message; + start(); + } + + return { + update, + stop + }; +} + +/** + * Converts a number of bytes into a human-readable string with appropriate units. + * + * @param {number} bytes - The number of bytes to format. + * @returns {string} The formatted string representing the size in appropriate units (Bytes, KB, MB, GB, TB, PB). + */ +export function formatBytes(bytes) { + if (bytes === 0) return '0 Bytes'; + const k = 1024; + const sizes = ['Bytes', 'KB', 'MB', 'GB', 'TB', 'PB']; + const i = Math.floor(Math.log(bytes) / Math.log(k)); + return (bytes / Math.pow(k, i)).toFixed(2) + ' ' + sizes[i]; +} + +/** + * Formats a duration given in milliseconds into a human-readable string. + * + * The output is in the format: + * - "Xh Ym Zs" if the duration is at least 1 hour, + * - "Ym Zs" if the duration is at least 1 minute but less than 1 hour, + * - "Zs" if the duration is less than 1 minute. + * + * @param {number} ms - The duration in milliseconds. + * @returns {string} The formatted elapsed time string. + */ +export function formatElapsed(ms) { + const sec = Math.floor(ms / 1000); + const min = Math.floor(sec / 60); + const hr = Math.floor(min / 60); + if (hr > 0) return `${hr}h ${min % 60}m ${sec % 60}s`; + if (min > 0) return `${min}m ${sec % 60}s`; + return `${sec}s`; +} diff --git a/package-lock.json b/package-lock.json index 6bbefbb..689e592 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@dynamicweb/cli", - "version": "1.0.8", + "version": "1.0.13", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "@dynamicweb/cli", - "version": "1.0.8", + "version": "1.0.13", "license": "MIT", "dependencies": { "child_process": "^1.0.2", @@ -15,6 +15,7 @@ "fetch": "^1.1.0", "form-data": "^4.0.0", "https": "^1.0.0", + "log-update": "^6.1.0", "node-fetch": "^3.2.10", "path": "^0.12.7", "yargs": "^17.5.1", @@ -24,22 +25,6 @@ "dw": "bin/index.js" } }, - "node_modules/@isaacs/cliui": { - "version": "8.0.2", - "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz", - "integrity": "sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==", - "dependencies": { - "string-width": "^5.1.2", - "string-width-cjs": "npm:string-width@^4.2.0", - "strip-ansi": "^7.0.1", - "strip-ansi-cjs": "npm:strip-ansi@^6.0.1", - "wrap-ansi": "^8.1.0", - "wrap-ansi-cjs": "npm:wrap-ansi@^7.0.0" - }, - "engines": { - "node": ">=12" - } - }, "node_modules/@types/node": { "version": "22.7.2", "resolved": "https://registry.npmjs.org/@types/node/-/node-22.7.2.tgz", @@ -99,11 +84,6 @@ "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==" }, - "node_modules/balanced-match": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", - "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==" - }, "node_modules/biskviit": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/biskviit/-/biskviit-1.0.1.tgz", @@ -115,14 +95,6 @@ "node": ">=1.0.0" } }, - "node_modules/brace-expansion": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", - "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", - "dependencies": { - "balanced-match": "^1.0.0" - } - }, "node_modules/buffer-crc32": { "version": "0.2.13", "resolved": "https://registry.npmjs.org/buffer-crc32/-/buffer-crc32-0.2.13.tgz", @@ -304,19 +276,6 @@ "node": ">= 0.8" } }, - "node_modules/cross-spawn": { - "version": "7.0.3", - "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz", - "integrity": "sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==", - "dependencies": { - "path-key": "^3.1.0", - "shebang-command": "^2.0.0", - "which": "^2.0.1" - }, - "engines": { - "node": ">= 8" - } - }, "node_modules/data-uri-to-buffer": { "version": "4.0.1", "resolved": "https://registry.npmjs.org/data-uri-to-buffer/-/data-uri-to-buffer-4.0.1.tgz", @@ -368,15 +327,10 @@ "node": ">=0.4.0" } }, - "node_modules/eastasianwidth": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz", - "integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==" - }, "node_modules/emoji-regex": { - "version": "9.2.2", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz", - "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==" + "version": "10.4.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-10.4.0.tgz", + "integrity": "sha512-EC+0oUMY1Rqm4O6LLrgjtYDvcVYTy7chDnM4Q7030tP4Kwj3u/pR6gP9ygnp2CJMK5Gq+9Q2oqmrFJAz01DXjw==" }, "node_modules/encoding": { "version": "0.1.12", @@ -394,6 +348,17 @@ "once": "^1.4.0" } }, + "node_modules/environment": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/environment/-/environment-1.1.0.tgz", + "integrity": "sha512-xUtoPkMggbz0MPyPiIWr1Kp4aeWJjDZ6SMvURhimjdZgsRuDplF5/s9hcgGhyXMhs+6vpnuoiZ2kFiu3FMnS8Q==", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/escalade": { "version": "3.2.0", "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", @@ -506,21 +471,6 @@ "node": ">=6" } }, - "node_modules/foreground-child": { - "version": "3.3.0", - "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.3.0.tgz", - "integrity": "sha512-Ld2g8rrAyMYFXBhEqMz8ZAHBi4J4uS1i/CxGMDnjyFWddMXLVcDp051DZfu+t7+ab7Wv6SMqpWmyFIj5UbfFvg==", - "dependencies": { - "cross-spawn": "^7.0.0", - "signal-exit": "^4.0.1" - }, - "engines": { - "node": ">=14" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, "node_modules/form-data": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.0.tgz", @@ -553,6 +503,17 @@ "node": "6.* || 8.* || >= 10.*" } }, + "node_modules/get-east-asian-width": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/get-east-asian-width/-/get-east-asian-width-1.3.0.tgz", + "integrity": "sha512-vpeMIQKxczTD/0s2CdEWHcb0eeJe6TFjxb+J5xgX7hScxqrGuyjmv4c1D4A/gelKfyox0gJJwIHF+fLjeaM8kQ==", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/get-stream": { "version": "5.2.0", "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-5.2.0.tgz", @@ -664,25 +625,6 @@ "node": ">=8" } }, - "node_modules/isexe": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", - "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==" - }, - "node_modules/jackspeak": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-4.0.2.tgz", - "integrity": "sha512-bZsjR/iRjl1Nk1UkjGpAzLNfQtzuijhn2g+pbZb98HQ1Gk8vM9hfbxeMBP+M2/UUdwj0RqGG3mlvk2MsAqwvEw==", - "dependencies": { - "@isaacs/cliui": "^8.0.2" - }, - "engines": { - "node": "20 || >=22" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, "node_modules/locate-path": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-3.0.0.tgz", @@ -700,12 +642,79 @@ "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==" }, - "node_modules/lru-cache": { - "version": "11.0.1", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.0.1.tgz", - "integrity": "sha512-CgeuL5uom6j/ZVrg7G/+1IXqRY8JXX4Hghfy5YE0EhoYQWvndP1kufu58cmZLNIDKnRhZrXfdS9urVWx98AipQ==", + "node_modules/log-update": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/log-update/-/log-update-6.1.0.tgz", + "integrity": "sha512-9ie8ItPR6tjY5uYJh8K/Zrv/RMZ5VOlOWvtZdEHYSTFKZfIBPQa9tOAEeAWhd+AnIneLJ22w5fjOYtoutpWq5w==", + "dependencies": { + "ansi-escapes": "^7.0.0", + "cli-cursor": "^5.0.0", + "slice-ansi": "^7.1.0", + "strip-ansi": "^7.1.0", + "wrap-ansi": "^9.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/log-update/node_modules/ansi-escapes": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-7.0.0.tgz", + "integrity": "sha512-GdYO7a61mR0fOlAsvC9/rIHf7L96sBc6dEWzeOu+KAea5bZyQRPIpojrVoI4AXGJS/ycu/fBTdLrUkA4ODrvjw==", + "dependencies": { + "environment": "^1.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/log-update/node_modules/cli-cursor": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-5.0.0.tgz", + "integrity": "sha512-aCj4O5wKyszjMmDT4tZj93kxyydN/K5zPWSCe6/0AV/AA1pqe5ZBIw0a2ZfPQV7lL5/yb5HsUreJ6UFAF1tEQw==", + "dependencies": { + "restore-cursor": "^5.0.0" + }, "engines": { - "node": "20 || >=22" + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/log-update/node_modules/onetime": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/onetime/-/onetime-7.0.0.tgz", + "integrity": "sha512-VXJjc87FScF88uafS3JllDgvAm+c/Slfz06lorj2uAY34rlUu0Nt+v8wreiImcrgAjjIHp1rXpTDlLOGw29WwQ==", + "dependencies": { + "mimic-function": "^5.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/log-update/node_modules/restore-cursor": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-5.1.0.tgz", + "integrity": "sha512-oMA2dcrw6u0YfxJQXm342bFKX/E4sG9rbTzO9ptUcR/e8A33cHuvStiYOwH7fszkZlZ1z/ta9AAoPk2F4qIOHA==", + "dependencies": { + "onetime": "^7.0.0", + "signal-exit": "^4.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, "node_modules/mime-db": { @@ -735,26 +744,15 @@ "node": ">=6" } }, - "node_modules/minimatch": { - "version": "10.0.1", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.0.1.tgz", - "integrity": "sha512-ethXTt3SGGR+95gudmqJ1eNhRO7eGEGIgYA9vnPatK4/etz2MEVDno5GMCibdMTuBMyElzIlgxMna3K94XDIDQ==", - "dependencies": { - "brace-expansion": "^2.0.1" - }, + "node_modules/mimic-function": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/mimic-function/-/mimic-function-5.0.1.tgz", + "integrity": "sha512-VP79XUPxV2CigYP3jWwAUFSku2aKqBH7uTAapFWCBqutsbmDo96KY5o8uh6U+/YSIn5OxJnXp73beVkpqMIGhA==", "engines": { - "node": "20 || >=22" + "node": ">=18" }, "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/minipass": { - "version": "7.1.2", - "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.2.tgz", - "integrity": "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==", - "engines": { - "node": ">=16 || 14 >=14.17" + "url": "https://github.com/sponsors/sindresorhus" } }, "node_modules/ms": { @@ -865,11 +863,6 @@ "node": ">=6" } }, - "node_modules/package-json-from-dist": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/package-json-from-dist/-/package-json-from-dist-1.0.0.tgz", - "integrity": "sha512-dATvCeZN/8wQsGywez1mzHtTlP22H8OEfPrVMLNr4/eGa+ijtLn/6M5f0dY8UKNrC2O9UCU6SSoG3qRKnt7STw==" - }, "node_modules/path": { "version": "0.12.7", "resolved": "https://registry.npmjs.org/path/-/path-0.12.7.tgz", @@ -887,29 +880,6 @@ "node": ">=4" } }, - "node_modules/path-key": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", - "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", - "engines": { - "node": ">=8" - } - }, - "node_modules/path-scurry": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-2.0.0.tgz", - "integrity": "sha512-ypGJsmGtdXUOeM5u93TyeIEfEhM6s+ljAhrk5vAvSx8uyY/02OvrZnA0YNGUrPXfpJMgI1ODd3nwz8Npx4O4cg==", - "dependencies": { - "lru-cache": "^11.0.0", - "minipass": "^7.1.2" - }, - "engines": { - "node": "20 || >=22" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, "node_modules/pend": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/pend/-/pend-1.2.0.tgz", @@ -996,25 +966,6 @@ "resolved": "https://registry.npmjs.org/set-blocking/-/set-blocking-2.0.0.tgz", "integrity": "sha512-KiKBS8AnWGEyLzofFfmvKwpdPzqiy16LvQfK3yv/fVH7Bj13/wl3JSR1J+rfgRE9q7xUJK4qvgS8raSOeLUehw==" }, - "node_modules/shebang-command": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", - "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", - "dependencies": { - "shebang-regex": "^3.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/shebang-regex": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", - "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", - "engines": { - "node": ">=8" - } - }, "node_modules/signal-exit": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", @@ -1026,58 +977,49 @@ "url": "https://github.com/sponsors/isaacs" } }, - "node_modules/string-width": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz", - "integrity": "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==", + "node_modules/slice-ansi": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-7.1.0.tgz", + "integrity": "sha512-bSiSngZ/jWeX93BqeIAbImyTbEihizcwNjFoRUIY/T1wWQsfsm2Vw1agPKylXvQTU7iASGdHhyqRlqQzfz+Htg==", "dependencies": { - "eastasianwidth": "^0.2.0", - "emoji-regex": "^9.2.2", - "strip-ansi": "^7.0.1" + "ansi-styles": "^6.2.1", + "is-fullwidth-code-point": "^5.0.0" }, "engines": { - "node": ">=12" + "node": ">=18" }, "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "url": "https://github.com/chalk/slice-ansi?sponsor=1" } }, - "node_modules/string-width-cjs": { - "name": "string-width", - "version": "4.2.3", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", - "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "node_modules/slice-ansi/node_modules/is-fullwidth-code-point": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-5.0.0.tgz", + "integrity": "sha512-OVa3u9kkBbw7b8Xw5F9P+D/T9X+Z4+JruYVNapTjPYZYUznQ5YfWeFkOj606XYYW8yugTfC8Pj0hYqvi4ryAhA==", "dependencies": { - "emoji-regex": "^8.0.0", - "is-fullwidth-code-point": "^3.0.0", - "strip-ansi": "^6.0.1" + "get-east-asian-width": "^1.0.0" }, "engines": { - "node": ">=8" - } - }, - "node_modules/string-width-cjs/node_modules/ansi-regex": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", - "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", - "engines": { - "node": ">=8" + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/string-width-cjs/node_modules/emoji-regex": { - "version": "8.0.0", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", - "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==" - }, - "node_modules/string-width-cjs/node_modules/strip-ansi": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", - "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "node_modules/string-width": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-7.2.0.tgz", + "integrity": "sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ==", "dependencies": { - "ansi-regex": "^5.0.1" + "emoji-regex": "^10.3.0", + "get-east-asian-width": "^1.0.0", + "strip-ansi": "^7.1.0" }, "engines": { - "node": ">=8" + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, "node_modules/strip-ansi": { @@ -1094,26 +1036,6 @@ "url": "https://github.com/chalk/strip-ansi?sponsor=1" } }, - "node_modules/strip-ansi-cjs": { - "name": "strip-ansi", - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", - "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", - "dependencies": { - "ansi-regex": "^5.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/strip-ansi-cjs/node_modules/ansi-regex": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", - "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", - "engines": { - "node": ">=8" - } - }, "node_modules/supports-color": { "version": "7.2.0", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", @@ -1179,109 +1101,27 @@ "node": ">= 8" } }, - "node_modules/which": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", - "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", - "dependencies": { - "isexe": "^2.0.0" - }, - "bin": { - "node-which": "bin/node-which" - }, - "engines": { - "node": ">= 8" - } - }, "node_modules/which-module": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/which-module/-/which-module-2.0.1.tgz", "integrity": "sha512-iBdZ57RDvnOR9AGBhML2vFZf7h8vmBjhoaZqODJBFWHVtKkDmKuHai3cx5PgVMrX5YDNp27AofYbAwctSS+vhQ==" }, "node_modules/wrap-ansi": { - "version": "8.1.0", - "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-8.1.0.tgz", - "integrity": "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==", + "version": "9.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-9.0.0.tgz", + "integrity": "sha512-G8ura3S+3Z2G+mkgNRq8dqaFZAuxfsxpBB8OCTGRTCtp+l/v9nbFNmCUP1BZMts3G1142MsZfn6eeUKrr4PD1Q==", "dependencies": { - "ansi-styles": "^6.1.0", - "string-width": "^5.0.1", - "strip-ansi": "^7.0.1" - }, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/wrap-ansi?sponsor=1" - } - }, - "node_modules/wrap-ansi-cjs": { - "name": "wrap-ansi", - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", - "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", - "dependencies": { - "ansi-styles": "^4.0.0", - "string-width": "^4.1.0", - "strip-ansi": "^6.0.0" + "ansi-styles": "^6.2.1", + "string-width": "^7.0.0", + "strip-ansi": "^7.1.0" }, "engines": { - "node": ">=10" + "node": ">=18" }, "funding": { "url": "https://github.com/chalk/wrap-ansi?sponsor=1" } }, - "node_modules/wrap-ansi-cjs/node_modules/ansi-regex": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", - "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", - "engines": { - "node": ">=8" - } - }, - "node_modules/wrap-ansi-cjs/node_modules/ansi-styles": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", - "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", - "dependencies": { - "color-convert": "^2.0.1" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, - "node_modules/wrap-ansi-cjs/node_modules/emoji-regex": { - "version": "8.0.0", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", - "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==" - }, - "node_modules/wrap-ansi-cjs/node_modules/string-width": { - "version": "4.2.3", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", - "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", - "dependencies": { - "emoji-regex": "^8.0.0", - "is-fullwidth-code-point": "^3.0.0", - "strip-ansi": "^6.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/wrap-ansi-cjs/node_modules/strip-ansi": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", - "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", - "dependencies": { - "ansi-regex": "^5.0.1" - }, - "engines": { - "node": ">=8" - } - }, "node_modules/wrappy": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", @@ -1505,19 +1345,6 @@ } }, "dependencies": { - "@isaacs/cliui": { - "version": "8.0.2", - "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz", - "integrity": "sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==", - "requires": { - "string-width": "^5.1.2", - "string-width-cjs": "npm:string-width@^4.2.0", - "strip-ansi": "^7.0.1", - "strip-ansi-cjs": "npm:strip-ansi@^6.0.1", - "wrap-ansi": "^8.1.0", - "wrap-ansi-cjs": "npm:wrap-ansi@^7.0.0" - } - }, "@types/node": { "version": "22.7.2", "resolved": "https://registry.npmjs.org/@types/node/-/node-22.7.2.tgz", @@ -1559,11 +1386,6 @@ "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==" }, - "balanced-match": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", - "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==" - }, "biskviit": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/biskviit/-/biskviit-1.0.1.tgz", @@ -1572,14 +1394,6 @@ "psl": "^1.1.7" } }, - "brace-expansion": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", - "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", - "requires": { - "balanced-match": "^1.0.0" - } - }, "buffer-crc32": { "version": "0.2.13", "resolved": "https://registry.npmjs.org/buffer-crc32/-/buffer-crc32-0.2.13.tgz", @@ -1711,16 +1525,6 @@ "delayed-stream": "~1.0.0" } }, - "cross-spawn": { - "version": "7.0.3", - "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz", - "integrity": "sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==", - "requires": { - "path-key": "^3.1.0", - "shebang-command": "^2.0.0", - "which": "^2.0.1" - } - }, "data-uri-to-buffer": { "version": "4.0.1", "resolved": "https://registry.npmjs.org/data-uri-to-buffer/-/data-uri-to-buffer-4.0.1.tgz", @@ -1749,15 +1553,10 @@ "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==" }, - "eastasianwidth": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz", - "integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==" - }, "emoji-regex": { - "version": "9.2.2", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz", - "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==" + "version": "10.4.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-10.4.0.tgz", + "integrity": "sha512-EC+0oUMY1Rqm4O6LLrgjtYDvcVYTy7chDnM4Q7030tP4Kwj3u/pR6gP9ygnp2CJMK5Gq+9Q2oqmrFJAz01DXjw==" }, "encoding": { "version": "0.1.12", @@ -1775,6 +1574,11 @@ "once": "^1.4.0" } }, + "environment": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/environment/-/environment-1.1.0.tgz", + "integrity": "sha512-xUtoPkMggbz0MPyPiIWr1Kp4aeWJjDZ6SMvURhimjdZgsRuDplF5/s9hcgGhyXMhs+6vpnuoiZ2kFiu3FMnS8Q==" + }, "escalade": { "version": "3.2.0", "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", @@ -1848,15 +1652,6 @@ "locate-path": "^3.0.0" } }, - "foreground-child": { - "version": "3.3.0", - "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.3.0.tgz", - "integrity": "sha512-Ld2g8rrAyMYFXBhEqMz8ZAHBi4J4uS1i/CxGMDnjyFWddMXLVcDp051DZfu+t7+ab7Wv6SMqpWmyFIj5UbfFvg==", - "requires": { - "cross-spawn": "^7.0.0", - "signal-exit": "^4.0.1" - } - }, "form-data": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.0.tgz", @@ -1880,6 +1675,11 @@ "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==" }, + "get-east-asian-width": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/get-east-asian-width/-/get-east-asian-width-1.3.0.tgz", + "integrity": "sha512-vpeMIQKxczTD/0s2CdEWHcb0eeJe6TFjxb+J5xgX7hScxqrGuyjmv4c1D4A/gelKfyox0gJJwIHF+fLjeaM8kQ==" + }, "get-stream": { "version": "5.2.0", "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-5.2.0.tgz", @@ -1966,19 +1766,6 @@ "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==" }, - "isexe": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", - "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==" - }, - "jackspeak": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-4.0.2.tgz", - "integrity": "sha512-bZsjR/iRjl1Nk1UkjGpAzLNfQtzuijhn2g+pbZb98HQ1Gk8vM9hfbxeMBP+M2/UUdwj0RqGG3mlvk2MsAqwvEw==", - "requires": { - "@isaacs/cliui": "^8.0.2" - } - }, "locate-path": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-3.0.0.tgz", @@ -1993,10 +1780,52 @@ "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==" }, - "lru-cache": { - "version": "11.0.1", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.0.1.tgz", - "integrity": "sha512-CgeuL5uom6j/ZVrg7G/+1IXqRY8JXX4Hghfy5YE0EhoYQWvndP1kufu58cmZLNIDKnRhZrXfdS9urVWx98AipQ==" + "log-update": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/log-update/-/log-update-6.1.0.tgz", + "integrity": "sha512-9ie8ItPR6tjY5uYJh8K/Zrv/RMZ5VOlOWvtZdEHYSTFKZfIBPQa9tOAEeAWhd+AnIneLJ22w5fjOYtoutpWq5w==", + "requires": { + "ansi-escapes": "^7.0.0", + "cli-cursor": "^5.0.0", + "slice-ansi": "^7.1.0", + "strip-ansi": "^7.1.0", + "wrap-ansi": "^9.0.0" + }, + "dependencies": { + "ansi-escapes": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-7.0.0.tgz", + "integrity": "sha512-GdYO7a61mR0fOlAsvC9/rIHf7L96sBc6dEWzeOu+KAea5bZyQRPIpojrVoI4AXGJS/ycu/fBTdLrUkA4ODrvjw==", + "requires": { + "environment": "^1.0.0" + } + }, + "cli-cursor": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-5.0.0.tgz", + "integrity": "sha512-aCj4O5wKyszjMmDT4tZj93kxyydN/K5zPWSCe6/0AV/AA1pqe5ZBIw0a2ZfPQV7lL5/yb5HsUreJ6UFAF1tEQw==", + "requires": { + "restore-cursor": "^5.0.0" + } + }, + "onetime": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/onetime/-/onetime-7.0.0.tgz", + "integrity": "sha512-VXJjc87FScF88uafS3JllDgvAm+c/Slfz06lorj2uAY34rlUu0Nt+v8wreiImcrgAjjIHp1rXpTDlLOGw29WwQ==", + "requires": { + "mimic-function": "^5.0.0" + } + }, + "restore-cursor": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-5.1.0.tgz", + "integrity": "sha512-oMA2dcrw6u0YfxJQXm342bFKX/E4sG9rbTzO9ptUcR/e8A33cHuvStiYOwH7fszkZlZ1z/ta9AAoPk2F4qIOHA==", + "requires": { + "onetime": "^7.0.0", + "signal-exit": "^4.1.0" + } + } + } }, "mime-db": { "version": "1.52.0", @@ -2016,18 +1845,10 @@ "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-2.1.0.tgz", "integrity": "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==" }, - "minimatch": { - "version": "10.0.1", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.0.1.tgz", - "integrity": "sha512-ethXTt3SGGR+95gudmqJ1eNhRO7eGEGIgYA9vnPatK4/etz2MEVDno5GMCibdMTuBMyElzIlgxMna3K94XDIDQ==", - "requires": { - "brace-expansion": "^2.0.1" - } - }, - "minipass": { - "version": "7.1.2", - "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.2.tgz", - "integrity": "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==" + "mimic-function": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/mimic-function/-/mimic-function-5.0.1.tgz", + "integrity": "sha512-VP79XUPxV2CigYP3jWwAUFSku2aKqBH7uTAapFWCBqutsbmDo96KY5o8uh6U+/YSIn5OxJnXp73beVkpqMIGhA==" }, "ms": { "version": "2.1.3", @@ -2096,11 +1917,6 @@ "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz", "integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==" }, - "package-json-from-dist": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/package-json-from-dist/-/package-json-from-dist-1.0.0.tgz", - "integrity": "sha512-dATvCeZN/8wQsGywez1mzHtTlP22H8OEfPrVMLNr4/eGa+ijtLn/6M5f0dY8UKNrC2O9UCU6SSoG3qRKnt7STw==" - }, "path": { "version": "0.12.7", "resolved": "https://registry.npmjs.org/path/-/path-0.12.7.tgz", @@ -2115,20 +1931,6 @@ "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-3.0.0.tgz", "integrity": "sha512-bpC7GYwiDYQ4wYLe+FA8lhRjhQCMcQGuSgGGqDkg/QerRWw9CmGRT0iSOVRSZJ29NMLZgIzqaljJ63oaL4NIJQ==" }, - "path-key": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", - "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==" - }, - "path-scurry": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-2.0.0.tgz", - "integrity": "sha512-ypGJsmGtdXUOeM5u93TyeIEfEhM6s+ljAhrk5vAvSx8uyY/02OvrZnA0YNGUrPXfpJMgI1ODd3nwz8Npx4O4cg==", - "requires": { - "lru-cache": "^11.0.0", - "minipass": "^7.1.2" - } - }, "pend": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/pend/-/pend-1.2.0.tgz", @@ -2202,64 +2004,40 @@ "resolved": "https://registry.npmjs.org/set-blocking/-/set-blocking-2.0.0.tgz", "integrity": "sha512-KiKBS8AnWGEyLzofFfmvKwpdPzqiy16LvQfK3yv/fVH7Bj13/wl3JSR1J+rfgRE9q7xUJK4qvgS8raSOeLUehw==" }, - "shebang-command": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", - "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", - "requires": { - "shebang-regex": "^3.0.0" - } - }, - "shebang-regex": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", - "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==" - }, "signal-exit": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==" }, - "string-width": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz", - "integrity": "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==", - "requires": { - "eastasianwidth": "^0.2.0", - "emoji-regex": "^9.2.2", - "strip-ansi": "^7.0.1" - } - }, - "string-width-cjs": { - "version": "npm:string-width@4.2.3", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", - "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "slice-ansi": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-7.1.0.tgz", + "integrity": "sha512-bSiSngZ/jWeX93BqeIAbImyTbEihizcwNjFoRUIY/T1wWQsfsm2Vw1agPKylXvQTU7iASGdHhyqRlqQzfz+Htg==", "requires": { - "emoji-regex": "^8.0.0", - "is-fullwidth-code-point": "^3.0.0", - "strip-ansi": "^6.0.1" + "ansi-styles": "^6.2.1", + "is-fullwidth-code-point": "^5.0.0" }, "dependencies": { - "ansi-regex": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", - "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==" - }, - "emoji-regex": { - "version": "8.0.0", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", - "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==" - }, - "strip-ansi": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", - "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "is-fullwidth-code-point": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-5.0.0.tgz", + "integrity": "sha512-OVa3u9kkBbw7b8Xw5F9P+D/T9X+Z4+JruYVNapTjPYZYUznQ5YfWeFkOj606XYYW8yugTfC8Pj0hYqvi4ryAhA==", "requires": { - "ansi-regex": "^5.0.1" + "get-east-asian-width": "^1.0.0" } } } }, + "string-width": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-7.2.0.tgz", + "integrity": "sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ==", + "requires": { + "emoji-regex": "^10.3.0", + "get-east-asian-width": "^1.0.0", + "strip-ansi": "^7.1.0" + } + }, "strip-ansi": { "version": "7.1.0", "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.0.tgz", @@ -2268,21 +2046,6 @@ "ansi-regex": "^6.0.1" } }, - "strip-ansi-cjs": { - "version": "npm:strip-ansi@6.0.1", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", - "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", - "requires": { - "ansi-regex": "^5.0.1" - }, - "dependencies": { - "ansi-regex": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", - "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==" - } - } - }, "supports-color": { "version": "7.2.0", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", @@ -2333,75 +2096,19 @@ "resolved": "https://registry.npmjs.org/web-streams-polyfill/-/web-streams-polyfill-3.3.3.tgz", "integrity": "sha512-d2JWLCivmZYTSIoge9MsgFCZrt571BikcWGYkjC1khllbTeDlGqZ2D8vD8E/lJa8WGWbb7Plm8/XJYV7IJHZZw==" }, - "which": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", - "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", - "requires": { - "isexe": "^2.0.0" - } - }, "which-module": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/which-module/-/which-module-2.0.1.tgz", "integrity": "sha512-iBdZ57RDvnOR9AGBhML2vFZf7h8vmBjhoaZqODJBFWHVtKkDmKuHai3cx5PgVMrX5YDNp27AofYbAwctSS+vhQ==" }, "wrap-ansi": { - "version": "8.1.0", - "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-8.1.0.tgz", - "integrity": "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==", + "version": "9.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-9.0.0.tgz", + "integrity": "sha512-G8ura3S+3Z2G+mkgNRq8dqaFZAuxfsxpBB8OCTGRTCtp+l/v9nbFNmCUP1BZMts3G1142MsZfn6eeUKrr4PD1Q==", "requires": { - "ansi-styles": "^6.1.0", - "string-width": "^5.0.1", - "strip-ansi": "^7.0.1" - } - }, - "wrap-ansi-cjs": { - "version": "npm:wrap-ansi@7.0.0", - "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", - "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", - "requires": { - "ansi-styles": "^4.0.0", - "string-width": "^4.1.0", - "strip-ansi": "^6.0.0" - }, - "dependencies": { - "ansi-regex": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", - "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==" - }, - "ansi-styles": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", - "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", - "requires": { - "color-convert": "^2.0.1" - } - }, - "emoji-regex": { - "version": "8.0.0", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", - "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==" - }, - "string-width": { - "version": "4.2.3", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", - "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", - "requires": { - "emoji-regex": "^8.0.0", - "is-fullwidth-code-point": "^3.0.0", - "strip-ansi": "^6.0.1" - } - }, - "strip-ansi": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", - "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", - "requires": { - "ansi-regex": "^5.0.1" - } - } + "ansi-styles": "^6.2.1", + "string-width": "^7.0.0", + "strip-ansi": "^7.1.0" } }, "wrappy": { diff --git a/package.json b/package.json index 717b6ea..06851f8 100644 --- a/package.json +++ b/package.json @@ -33,6 +33,7 @@ "fetch": "^1.1.0", "form-data": "^4.0.0", "https": "^1.0.0", + "log-update": "^6.1.0", "node-fetch": "^3.2.10", "path": "^0.12.7", "yargs": "^17.5.1", From 80ee293491c1809524c57bb31e168b19d1cd9b9b Mon Sep 17 00:00:00 2001 From: Frederik Nielsen Date: Mon, 7 Jul 2025 11:13:36 +0200 Subject: [PATCH 48/86] Upping to v 1.0.14 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 06851f8..203901b 100644 --- a/package.json +++ b/package.json @@ -2,7 +2,7 @@ "name": "@dynamicweb/cli", "type": "module", "description": "Dynamicweb CLI is a commandline tool for interacting with Dynamicweb 10 solutions.", - "version": "1.0.13", + "version": "1.0.14", "main": "bin/index.js", "files": [ "bin/*" From ef8d03e29a71478e66c19dc5688601959aa207cd Mon Sep 17 00:00:00 2001 From: Jeppe Eriksson Agger Date: Tue, 29 Jul 2025 15:18:51 +0200 Subject: [PATCH 49/86] Do not throw error on download --- bin/commands/files.js | 4 ++-- bin/downloader.js | 22 ++++++++++++++++++++-- 2 files changed, 22 insertions(+), 4 deletions(-) diff --git a/bin/commands/files.js b/bin/commands/files.js index 79f9d48..d917621 100644 --- a/bin/commands/files.js +++ b/bin/commands/files.js @@ -5,7 +5,7 @@ import FormData from 'form-data'; import { setupEnv, getAgent } from './env.js'; import { setupUser } from './login.js'; import { interactiveConfirm, formatBytes, createThrottledStatusUpdater } from '../utils.js'; -import { downloadWithProgress, getFileNameFromResponse } from '../downloader.js'; +import { downloadWithProgress, tryGetFileNameFromResponse } from '../downloader.js'; import { extractWithProgress } from '../extractor.js'; export function filesCommand() { @@ -198,7 +198,7 @@ async function download(env, user, dirPath, outPath, recursive, outname, raw, ia agent: getAgent(env.protocol) }); - const filename = outname || getFileNameFromResponse(res); + const filename = outname || tryGetFileNameFromResponse(res, dirPath); if (!filename) return; let filePath = path.resolve(`${path.resolve(outPath)}/${filename}`) diff --git a/bin/downloader.js b/bin/downloader.js index 2f1cb3d..86df65b 100644 --- a/bin/downloader.js +++ b/bin/downloader.js @@ -6,17 +6,35 @@ import fs from 'fs'; * @param {Response} res - The HTTP response object. * @returns {string} The extracted file name. */ -export function getFileNameFromResponse(res) { +export function getFileNameFromResponse(res, dirPath) { const header = res.headers.get('content-disposition'); const parts = header?.split(';'); if (!parts) { - throw new Error(`No files found in directory '${dirPath}', if you want to download all folders recursively include the -r flag`); + const msg = `No files found in directory '${dirPath}', if you want to download all folders recursively include the -r flag`; + throw new Error(msg); } return parts[1].split('=')[1].replace('+', ' '); } +/** + * Attempts to extract the file name from an HTTP response. + * If extraction fails, logs the error message to the console. + * + * @param {Object} res - The HTTP response object to extract the file name from. + * @param {string} dirPath - The directory path to use for file name resolution. + * @returns {string|null} The extracted file name, or null if extraction fails. + */ +export function tryGetFileNameFromResponse(res, dirPath) { + try { + return getFileNameFromResponse(res, dirPath); + } catch (err) { + console.error(err.message); + return null; + } +} + /** * Downloads a file with progress reporting. * @param {Response} res - The response from which to read the stream data. From 2944cbb0c8dbc9bd595d93b095c4a765a0c88a61 Mon Sep 17 00:00:00 2001 From: Jeppe Eriksson Agger Date: Wed, 30 Jul 2025 11:42:08 +0200 Subject: [PATCH 50/86] Version change to 1.0.15 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 203901b..ac9eed6 100644 --- a/package.json +++ b/package.json @@ -2,7 +2,7 @@ "name": "@dynamicweb/cli", "type": "module", "description": "Dynamicweb CLI is a commandline tool for interacting with Dynamicweb 10 solutions.", - "version": "1.0.14", + "version": "1.0.15", "main": "bin/index.js", "files": [ "bin/*" From d76ff39452d816f01c7a4ead019840be58a01dc9 Mon Sep 17 00:00:00 2001 From: Stanislav Smetanin Date: Fri, 8 Aug 2025 23:12:09 +1000 Subject: [PATCH 51/86] Added possibility to export single file by using 'dw files {file path} {outPath} -e'. Added two optional options: --asDirectory - Forces the command to treat the path as a directory, even if its name contains a dot. --asFile - Forces the command to treat the path as a single file, even if it has no extension. They are needed in cases if automatic detection ('do we want to export the file or directory?') is incorrect, usually because of strange file/directory names. --- .gitignore | 5 +- README.md | 20 +++++++ bin/commands/files.js | 121 +++++++++++++++++++++++++++++++----------- 3 files changed, 113 insertions(+), 33 deletions(-) diff --git a/.gitignore b/.gitignore index bb6fa4d..09d90be 100644 --- a/.gitignore +++ b/.gitignore @@ -101,4 +101,7 @@ dist .dynamodb/ # TernJS port file -.tern-port \ No newline at end of file +.tern-port + +# Visual Studio +/.vs \ No newline at end of file diff --git a/README.md b/README.md index 506dfa9..0f23971 100644 --- a/README.md +++ b/README.md @@ -86,6 +86,26 @@ Exporting all templates from current environment to local solution Listing the system files structure of the current environment > $ dw files system -lr +### Files Source Type Detection +By default, the `dw files` command automatically detects the source type based on the \: +If the path contains a file extension (e.g., 'templates/Translations.xml'), it is treated as a file. +Otherwise, it is treated as a directory. +In cases where this detection is incorrect, you can force the type using these flags: + +- `-ad` `--asDirectory` Forces the command to treat the path as a directory, even if its name contains a dot. +- `-af` `--asFile` Forces the command to treat the path as a single file, even if it has no extension. + +#### Examples + +Exporting single file from current environment to local solution +> $ dw files templates/Translations.xml ./templates -e + +Exporting a directory that looks like a file +> $ dw files templates/templates.v1 ./templates -e -ad + +Exporting a file that has no extension +> $ dw files templates/testfile ./templates -e -af + ### Swift > $ dw swift \ diff --git a/bin/commands/files.js b/bin/commands/files.js index d917621..cbc0f07 100644 --- a/bin/commands/files.js +++ b/bin/commands/files.js @@ -63,6 +63,18 @@ export function filesCommand() { type: 'boolean', describe: 'Includes export of log and cache folders, NOT RECOMMENDED' }) + .option('asFile', { + type: 'boolean', + alias: 'af', + describe: 'Forces the command to treat the path as a single file, even if it has no extension.', + conflicts: 'asDirectory' + }) + .option('asDirectory', { + type: 'boolean', + alias: 'ad', + describe: 'Forces the command to treat the path as a directory, even if its name contains a dot.', + conflicts: 'asFile' + }) }, handler: (argv) => { if (argv.verbose) console.info(`Listing directory at: ${argv.dirPath}`) @@ -85,7 +97,19 @@ async function handleFiles(argv) { if (argv.export) { if (argv.dirPath) { - await download(env, user, argv.dirPath, argv.outPath, true, null, argv.raw, argv.iamstupid, []); + + const isFile = argv.asFile || argv.asDirectory + ? argv.asFile + : path.extname(argv.dirPath) !== ''; + + if (isFile) { + let parentDirectory = path.dirname(argv.dirPath); + parentDirectory = parentDirectory === '.' ? '/' : parentDirectory; + + await download(env, user, parentDirectory, argv.outPath, false, null, true, argv.iamstupid, [argv.dirPath], true); + } else { + await download(env, user, argv.dirPath, argv.outPath, true, null, argv.raw, argv.iamstupid, [], false); + } } else { await interactiveConfirm('Are you sure you want a full export of files?', async () => { console.log('Full export is starting') @@ -93,9 +117,9 @@ async function handleFiles(argv) { let dirs = filesStructure.directories; for (let id = 0; id < dirs.length; id++) { const dir = dirs[id]; - await download(env, user, dir.name, argv.outPath, true, null, argv.raw, argv.iamstupid, []); + await download(env, user, dir.name, argv.outPath, true, null, argv.raw, argv.iamstupid, [], false); } - await download(env, user, '/.', argv.outPath, false, 'Base.zip', argv.raw, argv.iamstupid, Array.from(filesStructure.files.data, f => f.name)); + await download(env, user, '/.', argv.outPath, false, 'Base.zip', argv.raw, argv.iamstupid, Array.from(filesStructure.files.data, f => f.name), false); 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') }) } @@ -165,8 +189,7 @@ function resolveTree(dirs, indentLevel, parentHasFiles) { } } -async function download(env, user, dirPath, outPath, recursive, outname, raw, iamstupid, fileNames) { - let endpoint; +async function download(env, user, dirPath, outPath, recursive, outname, raw, iamstupid, fileNames, singleFileMode) { let excludeDirectories = ''; if (!iamstupid) { excludeDirectories = 'system/log'; @@ -174,19 +197,10 @@ async function download(env, user, dirPath, outPath, recursive, outname, raw, ia return; } } - let data = { - 'DirectoryPath': dirPath ?? '/', - 'ExcludeDirectories': [ excludeDirectories ], - } - if (recursive) { - endpoint = 'DirectoryDownload'; - } else { - endpoint = 'FileDownload' - data['Ids'] = fileNames - } + const { endpoint, data } = prepareDownloadCommandData(dirPath, excludeDirectories, fileNames, recursive, singleFileMode); - console.log('Downloading', dirPath === '/.' ? 'Base' : dirPath, 'Recursive=' + recursive); + displayDownloadMessage(dirPath, fileNames, recursive, singleFileMode); const res = await fetch(`${env.protocol}://${env.host}/Admin/Api/${endpoint}`, { method: 'POST', @@ -201,35 +215,78 @@ async function download(env, user, dirPath, outPath, recursive, outname, raw, ia const filename = outname || tryGetFileNameFromResponse(res, dirPath); if (!filename) return; - let filePath = path.resolve(`${path.resolve(outPath)}/${filename}`) - let updater = createThrottledStatusUpdater(); + const filePath = path.resolve(`${path.resolve(outPath)}/${filename}`) + const updater = createThrottledStatusUpdater(); + await downloadWithProgress(res, filePath, { onData: (received) => { updater.update(`Received:\t${formatBytes(received)}`); } }); + updater.stop(); - console.log(`Finished downloading`, dirPath === '/.' ? '.' : dirPath, 'Recursive=' + recursive); + if (singleFileMode) { + console.log(`Successfully downloaded: ${filename}`); + } else { + console.log(`Finished downloading`, dirPath === '/.' ? '.' : dirPath, 'Recursive=' + recursive); + } - if (!raw) { - console.log(`\nExtracting ${filename} to ${outPath}`); + await extractArchive(filename, filePath, outPath, raw); +} - const filenameWithoutExtension = filename.replace('.zip', ''); - const destinationPath = `${path.resolve(outPath)}/${filenameWithoutExtension === 'Base' ? '' : filenameWithoutExtension}`; +function prepareDownloadCommandData(directoryPath, excludeDirectories, fileNames, recursive, singleFileMode) { + const data = { + 'DirectoryPath': directoryPath ?? '/', + 'ExcludeDirectories': [excludeDirectories], + }; - updater = createThrottledStatusUpdater(); - await extractWithProgress(filePath, destinationPath, { - onEntry: (processedEntries, totalEntries, percent) => { - updater.update(`Extracted:\t${processedEntries} of ${totalEntries} files (${percent}%)`); - } - }); - updater.stop(); + if (recursive && !singleFileMode) { + return { endpoint: 'DirectoryDownload', data }; + } + + data['Ids'] = fileNames; + return { endpoint: 'FileDownload', data }; +} + +function displayDownloadMessage(directoryPath, fileNames, recursive, singleFileMode) { + if (singleFileMode) { + const fileName = path.basename(fileNames[0] || 'unknown'); + console.log('Downloading file: ' + fileName); - console.log(`Finished extracting ${filename} to ${outPath}\n`); + return; + } + + const directoryPathDisplayName = directoryPath === '/.' + ? 'Base' + : directoryPath; + + console.log('Downloading', directoryPathDisplayName, 'Recursive=' + recursive); +} - fs.unlink(filePath, function(err) {}); +async function extractArchive(filename, filePath, outPath, raw) { + if (raw) { + return; } + + console.log(`\nExtracting ${filename} to ${outPath}`); + let destinationFilename = filename.replace('.zip', ''); + if (destinationFilename === 'Base') + destinationFilename = ''; + + const destinationPath = `${path.resolve(outPath)}/${destinationFilename}`; + const updater = createThrottledStatusUpdater(); + + await extractWithProgress(filePath, destinationPath, { + onEntry: (processedEntries, totalEntries, percent) => { + updater.update(`Extracted:\t${processedEntries} of ${totalEntries} files (${percent}%)`); + } + }); + + updater.stop(); + console.log(`Finished extracting ${filename} to ${outPath}\n`); + + fs.unlink(filePath, function(err) {}); } async function getFilesStructure(env, user, dirPath, recursive, includeFiles) { From ad8cc9fed8661011bee4224cc29af6096eb3f8d7 Mon Sep 17 00:00:00 2001 From: Stanislav Smetanin Date: Tue, 7 Oct 2025 20:57:11 +1000 Subject: [PATCH 52/86] Added warning which shows when open CLI from Git Bash with enabled path conversion setting. Updated documentation to aware customer about Git Bash features --- .gitignore | 3 ++- README.md | 30 ++++++++++++++++++++++++++++++ bin/index.js | 14 +++++++++++++- 3 files changed, 45 insertions(+), 2 deletions(-) diff --git a/.gitignore b/.gitignore index bb6fa4d..cfe6ffb 100644 --- a/.gitignore +++ b/.gitignore @@ -101,4 +101,5 @@ dist .dynamodb/ # TernJS port file -.tern-port \ No newline at end of file +.tern-port +/.vs diff --git a/README.md b/README.md index 506dfa9..dc9ebee 100644 --- a/README.md +++ b/README.md @@ -169,3 +169,33 @@ Config is used to manage the .dwc file through the CLI, given any prop it will c #### Examples Changing the host for the dev environment > $ dw config --env.dev.host localhost:6001 + +## Using Git Bash +If you're using Git Bash, you may encounter issues with path conversion that can interfere with relative paths used in commands. + +### Path Conversion Issues +Git Bash automatically converts relative paths to absolute paths, which can cause problems. +You'll see a warning message if the conversion setting is not disabled: + +"You appear to have path conversion turned on in your shell. If you are using relative paths, this may interfere. Please see doc.dynamicweb.dev for more information." + +### Solution +To resolve this issue, disable path conversion by setting the `MSYS_NO_PATHCONV` environment variable (current session only): + +> $ export MSYS_NO_PATHCONV=1 + +#### Examples + +> $ export MSYS_NO_PATHCONV=1 +> $ dw files -iro ./ /TestFolder --host \ --apiKey \ + +### Alternative Solutions +If you prefer not to disable path conversion globally, you can: + +1. Prefix problematic paths with `//` to prevent conversion for specific commands. +2. Use PowerShell or CMD instead of Git Bash. + +#### Examples + +> $ dw files -iro ./ //TestFolder --host \ --apiKey \ + diff --git a/bin/index.js b/bin/index.js index 32f98c7..bde255e 100644 --- a/bin/index.js +++ b/bin/index.js @@ -13,6 +13,7 @@ import { queryCommand } from './commands/query.js'; import { commandCommand } from './commands/command.js'; setupConfig(); +showGitBashRelativePathWarning(); yargs(hideBin(process.argv)) .scriptName('dw') @@ -58,4 +59,15 @@ function baseCommand() { console.log(`Host: ${getConfig()?.env[getConfig()?.current?.env]?.host}`) } } -} \ No newline at end of file +} + +function showGitBashRelativePathWarning() { + const isGitBash = !!process.env.MSYSTEM; + const pathConversionDisabled = process.env.MSYS_NO_PATHCONV === '1'; + + if (isGitBash && !pathConversionDisabled) { + console.warn('You appear to have path conversion turned on in your shell.'); + console.warn('If you are using relative paths, this may interfere.'); + console.warn('Please see doc.dynamicweb.dev for more information.'); + } +} From f394e031d07a3fb7ce4563f5b257b84070d413ca Mon Sep 17 00:00:00 2001 From: Stanislav Smetanin Date: Tue, 7 Oct 2025 22:31:15 +1000 Subject: [PATCH 53/86] Changes according to code review. --- README.md | 9 +++++---- bin/index.js | 2 +- 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index dc9ebee..1c39cfe 100644 --- a/README.md +++ b/README.md @@ -177,7 +177,9 @@ If you're using Git Bash, you may encounter issues with path conversion that can Git Bash automatically converts relative paths to absolute paths, which can cause problems. You'll see a warning message if the conversion setting is not disabled: -"You appear to have path conversion turned on in your shell. If you are using relative paths, this may interfere. Please see doc.dynamicweb.dev for more information." +"You appear to have path conversion turned on in your shell. +If you are using relative paths, this may interfere. +Please see https://doc.dynamicweb.dev/documentation/fundamentals/code/CLI.html for more information." ### Solution To resolve this issue, disable path conversion by setting the `MSYS_NO_PATHCONV` environment variable (current session only): @@ -192,10 +194,9 @@ To resolve this issue, disable path conversion by setting the `MSYS_NO_PATHCONV` ### Alternative Solutions If you prefer not to disable path conversion globally, you can: -1. Prefix problematic paths with `//` to prevent conversion for specific commands. +1. Prefix relative paths with `./` instead of just `/` to prevent conversion for specific commands. 2. Use PowerShell or CMD instead of Git Bash. #### Examples -> $ dw files -iro ./ //TestFolder --host \ --apiKey \ - +> $ dw files -iro ./ ./TestFolder --host \ --apiKey \ \ No newline at end of file diff --git a/bin/index.js b/bin/index.js index bde255e..1bb81bb 100644 --- a/bin/index.js +++ b/bin/index.js @@ -68,6 +68,6 @@ function showGitBashRelativePathWarning() { if (isGitBash && !pathConversionDisabled) { console.warn('You appear to have path conversion turned on in your shell.'); console.warn('If you are using relative paths, this may interfere.'); - console.warn('Please see doc.dynamicweb.dev for more information.'); + console.warn('Please see https://doc.dynamicweb.dev/documentation/fundamentals/code/CLI.html for more information.'); } } From e9dfe4c52af0677f8cf4ebf1a035726745f6c1e0 Mon Sep 17 00:00:00 2001 From: Jeppe Eriksson Agger Date: Mon, 13 Oct 2025 16:15:55 +0200 Subject: [PATCH 54/86] Implemented chunked upload --- bin/commands/files.js | 38 +++++++++++++++++++++++++++++--------- 1 file changed, 29 insertions(+), 9 deletions(-) diff --git a/bin/commands/files.js b/bin/commands/files.js index cbc0f07..f3bffa0 100644 --- a/bin/commands/files.js +++ b/bin/commands/files.js @@ -308,18 +308,38 @@ async function getFilesStructure(env, user, dirPath, recursive, includeFiles) { export async function uploadFiles(env, user, localFilePaths, destinationPath, createEmpty = false, overwrite = false) { console.log('Uploading files') - let form = new FormData(); + + const chunkSize = 1000; + const chunks = []; + + for (let i = 0; i < localFilePaths.length; i += chunkSize) { + chunks.push(localFilePaths.slice(i, i + chunkSize)); + } + + for (let i = 0; i < chunks.length; i++) { + console.log(`Uploading chunk ${i + 1} of ${chunks.length}`); + + const chunk = chunks[i]; + await uploadChunk(env, user, chunk, destinationPath, createEmpty, overwrite); + + console.log(`Finished uploading chunk ${i + 1} of ${chunks.length}`); + } + + console.log(`Finished uploading files. Total files: ${localFilePaths.length}, total chunks: ${chunks.length}`); +} + +async function uploadChunk(env, user, filePathsChunk, destinationPath, createEmpty, overwrite) { + const form = new FormData(); form.append('path', destinationPath); form.append('skipExistingFiles', String(!overwrite)); form.append('allowOverwrite', String(overwrite)); - localFilePaths.forEach((localPath, index) => { - let fileToUpload = resolveFilePath(localPath) - console.log(fileToUpload) - if (fileToUpload === undefined) - return; + + filePathsChunk.forEach(fileToUpload => { + console.log(`${fileToUpload}`) form.append('files', fs.createReadStream(path.resolve(fileToUpload))); }); - let res = await fetch(`${env.protocol}://${env.host}/Admin/Api/Upload?` + new URLSearchParams({"createEmptyFiles": createEmpty, "createMissingDirectories": true}), { + + const res = await fetch(`${env.protocol}://${env.host}/Admin/Api/Upload?` + new URLSearchParams({"createEmptyFiles": createEmpty, "createMissingDirectories": true}), { method: 'POST', body: form, headers: { @@ -327,13 +347,13 @@ export async function uploadFiles(env, user, localFilePaths, destinationPath, cr }, agent: getAgent(env.protocol) }); + if (res.ok) { console.log(await res.json()) - console.log(`Files uploaded`) } else { console.log(res) - console.log(res.json()) + console.log(await res.json()) process.exit(1); } } From ba771aad37196578f2017d4ef4019ab4e10722e3 Mon Sep 17 00:00:00 2001 From: Jeppe Eriksson Agger Date: Tue, 14 Oct 2025 15:17:56 +0200 Subject: [PATCH 55/86] Updated version number to 1.0.16 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index ac9eed6..937f344 100644 --- a/package.json +++ b/package.json @@ -2,7 +2,7 @@ "name": "@dynamicweb/cli", "type": "module", "description": "Dynamicweb CLI is a commandline tool for interacting with Dynamicweb 10 solutions.", - "version": "1.0.15", + "version": "1.0.16", "main": "bin/index.js", "files": [ "bin/*" From bd79e86ed4277b6d5a569c827a8bac5ec8d2d058 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nicolai=20H=C3=B8eg=20Pedersen?= Date: Thu, 8 Jan 2026 14:49:04 +0100 Subject: [PATCH 56/86] Update env.js Try to handle timeouts --- bin/commands/env.js | 15 +++++++++++---- 1 file changed, 11 insertions(+), 4 deletions(-) diff --git a/bin/commands/env.js b/bin/commands/env.js index 435aa8e..bf2ebd3 100644 --- a/bin/commands/env.js +++ b/bin/commands/env.js @@ -4,12 +4,19 @@ import { Agent as HttpsAgent } from 'https'; import yargsInteractive from 'yargs-interactive'; const httpAgent = new HttpAgent({ - rejectUnauthorized: false -}) + keepAlive: true, + maxSockets, + maxFreeSockets: 8, + keepAliveMsecs: 10_000 +}); const httpsAgent = new HttpsAgent({ + keepAlive: true, + maxSockets, + maxFreeSockets: 8, + keepAliveMsecs: 10_000, rejectUnauthorized: false -}) +}); export function getAgent(protocol) { return protocol === 'http' ? httpAgent : httpsAgent; @@ -152,4 +159,4 @@ async function changeEnv(argv) { updateConfig(); console.log(`Your current environment is now ${getConfig().current.env}`); } -} \ No newline at end of file +} From 9a49729ec955de06463419b5ed38dc4b834386bb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nicolai=20H=C3=B8eg=20Pedersen?= Date: Thu, 8 Jan 2026 14:52:23 +0100 Subject: [PATCH 57/86] Update files.js --- bin/commands/files.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bin/commands/files.js b/bin/commands/files.js index f3bffa0..b235ead 100644 --- a/bin/commands/files.js +++ b/bin/commands/files.js @@ -309,7 +309,7 @@ async function getFilesStructure(env, user, dirPath, recursive, includeFiles) { export async function uploadFiles(env, user, localFilePaths, destinationPath, createEmpty = false, overwrite = false) { console.log('Uploading files') - const chunkSize = 1000; + const chunkSize = 300; const chunks = []; for (let i = 0; i < localFilePaths.length; i += chunkSize) { From c62ff4917235e9cdc0b32c2699bb5394ae7791a5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nicolai=20H=C3=B8eg=20Pedersen?= Date: Mon, 12 Jan 2026 12:47:25 +0100 Subject: [PATCH 58/86] Update env.js --- bin/commands/env.js | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/bin/commands/env.js b/bin/commands/env.js index bf2ebd3..076126c 100644 --- a/bin/commands/env.js +++ b/bin/commands/env.js @@ -5,15 +5,15 @@ import yargsInteractive from 'yargs-interactive'; const httpAgent = new HttpAgent({ keepAlive: true, - maxSockets, - maxFreeSockets: 8, + maxSockets: 8, + maxFreeSockets: 4, keepAliveMsecs: 10_000 }); const httpsAgent = new HttpsAgent({ keepAlive: true, - maxSockets, - maxFreeSockets: 8, + maxSockets: 8, + maxFreeSockets: 4, keepAliveMsecs: 10_000, rejectUnauthorized: false }); From a1959a255955f68aa68521e448829fd06bc33cae Mon Sep 17 00:00:00 2001 From: nicped Date: Mon, 13 Apr 2026 12:31:38 +0200 Subject: [PATCH 59/86] Replace yargs-interactive with @inquirer/prompts and clean up dependencies Migrate interactive prompts from the deprecated yargs-interactive package to @inquirer/prompts in env, query, and utils. Remove unused dependencies (child_process, https, path, fetch shims) and add Node >=20.12.0 engine requirement. Fix missing await and var usage in env.js. Update README with corrected config path, global options docs, .NET 10 example, and typo fixes. --- README.md | 15 +- bin/commands/config.js | 2 +- bin/commands/env.js | 62 +- bin/commands/query.js | 41 +- bin/utils.js | 21 +- package-lock.json | 1945 ++++++++++++++++++---------------------- package.json | 13 +- 7 files changed, 952 insertions(+), 1147 deletions(-) diff --git a/README.md b/README.md index 3643e64..e11a11c 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,7 @@ # DynamicWeb CLI ## What is it? -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. +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 include an easy setup and handling of different environments, access to the Management API and an easy way to update a Swift solution. 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. 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. @@ -26,6 +26,13 @@ All commands and options can be viewed by running > > $ dw \ --help +### Global options +Most commands support the following global options: +- `-v` `--verbose` Run with verbose logging +- `--host` Set the host directly, bypassing environment config (requires `--apiKey`) +- `--apiKey` Set the API key for an environmentless execution +- `--protocol` Set the protocol used (only with `--host`, defaults to `https`) + ### Users and environments As most commands are pulling or pushing data from the DW admin API, the necessary authorization is required. @@ -44,7 +51,7 @@ and swap between users by simply supplying the name of the user in the login com You can view the current environment and user being used by simply typing > $ dw -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 +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 `~/.dwc`. The structure should look like the following ```json { "env": { @@ -143,7 +150,7 @@ Getting file information on a specific file by name > $ dw command \ 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. -- `-l` `--list` Lists all the properties for the command, as well as the json model required **currently not working** +- `-l` `--list` Lists all the properties for the command, as well as the json model required - `--json` Takes a path to a .json file or a literal json, i.e --json '{ abc: "123" }' #### Examples @@ -169,7 +176,7 @@ Install is somewhat of a shorthand for a few commands. It will upload and instal 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. #### Examples -> $ dw install ./bin/Release/net6.0/CustomProject.dll +> $ dw install ./bin/Release/net10.0/CustomProject.dll ### Database > $ dw database \ diff --git a/bin/commands/config.js b/bin/commands/config.js index c9d9c22..0c9ad69 100644 --- a/bin/commands/config.js +++ b/bin/commands/config.js @@ -7,7 +7,7 @@ let localConfig; export function configCommand() { return { command: 'config', - describe: 'Edit the configs located in usr/.dwc', + describe: 'Edit the configs located in ~/.dwc', handler: (argv) => handleConfig(argv), builder: { prop: { diff --git a/bin/commands/env.js b/bin/commands/env.js index 435aa8e..389a9a0 100644 --- a/bin/commands/env.js +++ b/bin/commands/env.js @@ -1,7 +1,7 @@ import { updateConfig, getConfig } from './config.js' import { Agent as HttpAgent } from 'http'; import { Agent as HttpsAgent } from 'https'; -import yargsInteractive from 'yargs-interactive'; +import { input } from '@inquirer/prompts'; const httpAgent = new HttpAgent({ rejectUnauthorized: false @@ -84,7 +84,7 @@ async function handleEnv(argv) { } else if (argv.list) { console.log(`Existing environments: ${Object.keys(getConfig().env || {})}`) } else { - interactiveEnv(argv, { + await interactiveEnv(argv, { environment: { type: 'input' }, @@ -101,32 +101,40 @@ async function handleEnv(argv) { export async function interactiveEnv(argv, options) { if (argv.verbose) console.info('Setting up new environment') - await yargsInteractive() - .interactive(options) - .then(async (result) => { - getConfig().env = getConfig().env || {}; - getConfig().env[result.environment] = getConfig().env[result.environment] || {}; - if (result.host) { - var hostSplit = result.host.split("://"); - if (hostSplit.length == 1) { - getConfig().env[result.environment].protocol = 'https'; - getConfig().env[result.environment].host = hostSplit[0]; - } else if (hostSplit.length == 2) { - getConfig().env[result.environment].protocol = hostSplit[0]; - getConfig().env[result.environment].host = hostSplit[1]; - } else { - console.log(`Issues resolving host ${result.host}`); - return; - } - } - if (result.environment) { - getConfig().current = getConfig().current || {}; - getConfig().current.env = result.environment; - } - updateConfig(); - console.log(`Your current environment is now ${getConfig().current.env}`); - console.log(`To change the host of your environment, use the command 'dw env'`) + const result = {}; + for (const [key, config] of Object.entries(options)) { + if (key === 'interactive') continue; + if (config.prompt === 'never') { + result[key] = config.default; + continue; + } + result[key] = await input({ + message: config.describe || key, + default: config.default }); + } + getConfig().env = getConfig().env || {}; + getConfig().env[result.environment] = getConfig().env[result.environment] || {}; + if (result.host) { + const hostSplit = result.host.split("://"); + if (hostSplit.length == 1) { + getConfig().env[result.environment].protocol = 'https'; + getConfig().env[result.environment].host = hostSplit[0]; + } else if (hostSplit.length == 2) { + getConfig().env[result.environment].protocol = hostSplit[0]; + getConfig().env[result.environment].host = hostSplit[1]; + } else { + console.log(`Issues resolving host ${result.host}`); + return; + } + } + if (result.environment) { + getConfig().current = getConfig().current || {}; + getConfig().current.env = result.environment; + } + updateConfig(); + console.log(`Your current environment is now ${getConfig().current.env}`); + console.log(`To change the host of your environment, use the command 'dw env'`) } async function changeEnv(argv) { diff --git a/bin/commands/query.js b/bin/commands/query.js index 460f5ed..2f720d2 100644 --- a/bin/commands/query.js +++ b/bin/commands/query.js @@ -1,27 +1,27 @@ import fetch from 'node-fetch'; import { setupEnv, getAgent } from './env.js'; import { setupUser } from './login.js'; -import yargsInteractive from 'yargs-interactive'; +import { input } from '@inquirer/prompts'; const exclude = ['_', '$0', 'query', 'list', 'i', 'l', 'interactive'] export function queryCommand() { return { - command: 'query [query]', - describe: 'Runs the given query', + command: 'query [query]', + describe: 'Runs the given query', builder: (yargs) => { return yargs - .positional('query', { - describe: 'The query to execute' - }) - .option('list', { - alias: 'l', - describe: 'Lists all the properties for the query' - }) - .option('interactive', { - alias: 'i', - describe: 'Runs in interactive mode to ask for query parameters one by one' - }) + .positional('query', { + describe: 'The query to execute' + }) + .option('list', { + alias: 'l', + describe: 'Lists all the properties for the query' + }) + .option('interactive', { + alias: 'i', + describe: 'Runs in interactive mode to ask for query parameters one by one' + }) }, handler: (argv) => { if (argv.verbose) console.info(`Running query ${argv.query}`) @@ -68,18 +68,13 @@ async function getProperties(argv) { async function getQueryParams(argv) { let params = {} if (argv.interactive) { - let props = { interactive: { default: true }} let properties = await getProperties(argv); console.log('The following properties will be requested:') console.log(properties) - Array.from(properties).forEach(p => props[p] = { type: 'input', prompt: 'if-no-arg'}) - await yargsInteractive() - .interactive(props) - .then((result) => { - Object.keys(result).filter(k => !exclude.includes(k)).forEach(k => { - if (result[k]) params[k] = result[k] - }) - }); + for (const p of properties) { + const value = await input({ message: p }); + if (value) params[p] = value; + } } else { Object.keys(argv).filter(k => !exclude.includes(k)).forEach(k => params[k] = argv[k]) } diff --git a/bin/utils.js b/bin/utils.js index ec0a628..967e4fc 100644 --- a/bin/utils.js +++ b/bin/utils.js @@ -1,25 +1,12 @@ -import yargsInteractive from 'yargs-interactive'; +import { confirm } from '@inquirer/prompts'; import logUpdate from 'log-update'; const WRITE_THROTTLE_MS = 500; export async function interactiveConfirm(question, func) { - await yargsInteractive() - .interactive({ - confirm: { - type: 'confirm', - default: true, - describe: question, - prompt: 'always' - }, - interactive: { - default: true - } - }) - .then(async (result) => { - if (!result.confirm) return; - func() - }); + const result = await confirm({ message: question, default: true }); + if (!result) return; + await func(); } /** diff --git a/package-lock.json b/package-lock.json index 689e592..c43e5f4 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,28 +1,395 @@ { "name": "@dynamicweb/cli", - "version": "1.0.13", + "version": "1.0.16", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "@dynamicweb/cli", - "version": "1.0.13", + "version": "1.0.16", "license": "MIT", "dependencies": { - "child_process": "^1.0.2", + "@inquirer/prompts": "^8.4.1", "degit": "^2.8.4", "extract-zip": "^2.0.1", - "fetch": "^1.1.0", "form-data": "^4.0.0", - "https": "^1.0.0", "log-update": "^6.1.0", "node-fetch": "^3.2.10", - "path": "^0.12.7", - "yargs": "^17.5.1", - "yargs-interactive": "^3.0.1" + "yargs": "^17.5.1" }, "bin": { "dw": "bin/index.js" + }, + "engines": { + "node": ">=20.12.0" + } + }, + "node_modules/@inquirer/ansi": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@inquirer/ansi/-/ansi-2.0.5.tgz", + "integrity": "sha512-doc2sWgJpbFQ64UflSVd17ibMGDuxO1yKgOgLMwavzESnXjFWJqUeG8saYosqKpHp4kWiM5x1nXvEjbpx90gzw==", + "license": "MIT", + "engines": { + "node": ">=23.5.0 || ^22.13.0 || ^21.7.0 || ^20.12.0" + } + }, + "node_modules/@inquirer/checkbox": { + "version": "5.1.3", + "resolved": "https://registry.npmjs.org/@inquirer/checkbox/-/checkbox-5.1.3.tgz", + "integrity": "sha512-+G7I8CT+EHv/hasNfUl3P37DVoMoZfpA+2FXmM54dA8MxYle1YqucxbacxHalw1iAFSdKNEDTGNV7F+j1Ldqcg==", + "license": "MIT", + "dependencies": { + "@inquirer/ansi": "^2.0.5", + "@inquirer/core": "^11.1.8", + "@inquirer/figures": "^2.0.5", + "@inquirer/type": "^4.0.5" + }, + "engines": { + "node": ">=23.5.0 || ^22.13.0 || ^21.7.0 || ^20.12.0" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/confirm": { + "version": "6.0.11", + "resolved": "https://registry.npmjs.org/@inquirer/confirm/-/confirm-6.0.11.tgz", + "integrity": "sha512-pTpHjg0iEIRMYV/7oCZUMf27/383E6Wyhfc/MY+AVQGEoUobffIYWOK9YLP2XFRGz/9i6WlTQh1CkFVIo2Y7XA==", + "license": "MIT", + "dependencies": { + "@inquirer/core": "^11.1.8", + "@inquirer/type": "^4.0.5" + }, + "engines": { + "node": ">=23.5.0 || ^22.13.0 || ^21.7.0 || ^20.12.0" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/core": { + "version": "11.1.8", + "resolved": "https://registry.npmjs.org/@inquirer/core/-/core-11.1.8.tgz", + "integrity": "sha512-/u+yJk2pOKNDOh1ZgdUH2RQaRx6OOH4I0uwL95qPvTFTIL38YBsuSC4r1yXBB3Q6JvNqFFc202gk0Ew79rrcjA==", + "license": "MIT", + "dependencies": { + "@inquirer/ansi": "^2.0.5", + "@inquirer/figures": "^2.0.5", + "@inquirer/type": "^4.0.5", + "cli-width": "^4.1.0", + "fast-wrap-ansi": "^0.2.0", + "mute-stream": "^3.0.0", + "signal-exit": "^4.1.0" + }, + "engines": { + "node": ">=23.5.0 || ^22.13.0 || ^21.7.0 || ^20.12.0" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/core/node_modules/cli-width": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/cli-width/-/cli-width-4.1.0.tgz", + "integrity": "sha512-ouuZd4/dm2Sw5Gmqy6bGyNNNe1qt9RpmxveLSO7KcgsTnU7RXfsw+/bukWGo1abgBiMAic068rclZsO4IWmmxQ==", + "license": "ISC", + "engines": { + "node": ">= 12" + } + }, + "node_modules/@inquirer/core/node_modules/mute-stream": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/mute-stream/-/mute-stream-3.0.0.tgz", + "integrity": "sha512-dkEJPVvun4FryqBmZ5KhDo0K9iDXAwn08tMLDinNdRBNPcYEDiWYysLcc6k3mjTMlbP9KyylvRpd4wFtwrT9rw==", + "license": "ISC", + "engines": { + "node": "^20.17.0 || >=22.9.0" + } + }, + "node_modules/@inquirer/editor": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/@inquirer/editor/-/editor-5.1.0.tgz", + "integrity": "sha512-6wlkYl65Qfayy48gPCfU4D7li6KCAGN79mLXa/tYHZH99OfZ820yY+HA+DgE88r8YwwgeuY6PQgNqMeK6LuMmw==", + "license": "MIT", + "dependencies": { + "@inquirer/core": "^11.1.8", + "@inquirer/external-editor": "^3.0.0", + "@inquirer/type": "^4.0.5" + }, + "engines": { + "node": ">=23.5.0 || ^22.13.0 || ^21.7.0 || ^20.12.0" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/expand": { + "version": "5.0.12", + "resolved": "https://registry.npmjs.org/@inquirer/expand/-/expand-5.0.12.tgz", + "integrity": "sha512-vOfrB33b7YIZfDauXS8vNNz2Z86FozTZLIt7e+7/dCaPJ1RXZsHCuI9TlcERzEUq57vkM+UdnBgxP0rFd23JYQ==", + "license": "MIT", + "dependencies": { + "@inquirer/core": "^11.1.8", + "@inquirer/type": "^4.0.5" + }, + "engines": { + "node": ">=23.5.0 || ^22.13.0 || ^21.7.0 || ^20.12.0" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/external-editor": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@inquirer/external-editor/-/external-editor-3.0.0.tgz", + "integrity": "sha512-lDSwMgg+M5rq6JKBYaJwSX6T9e/HK2qqZ1oxmOwn4AQoJE5D+7TumsxLGC02PWS//rkIVqbZv3XA3ejsc9FYvg==", + "license": "MIT", + "dependencies": { + "chardet": "^2.1.1", + "iconv-lite": "^0.7.2" + }, + "engines": { + "node": ">=23.5.0 || ^22.13.0 || ^21.7.0 || ^20.12.0" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/external-editor/node_modules/chardet": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/chardet/-/chardet-2.1.1.tgz", + "integrity": "sha512-PsezH1rqdV9VvyNhxxOW32/d75r01NY7TQCmOqomRo15ZSOKbpTFVsfjghxo6JloQUCGnH4k1LGu0R4yCLlWQQ==", + "license": "MIT" + }, + "node_modules/@inquirer/external-editor/node_modules/iconv-lite": { + "version": "0.7.2", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.7.2.tgz", + "integrity": "sha512-im9DjEDQ55s9fL4EYzOAv0yMqmMBSZp6G0VvFyTMPKWxiSBHUj9NW/qqLmXUwXrrM7AvqSlTCfvqRb0cM8yYqw==", + "license": "MIT", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + }, + "engines": { + "node": ">=0.10.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/@inquirer/figures": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@inquirer/figures/-/figures-2.0.5.tgz", + "integrity": "sha512-NsSs4kzfm12lNetHwAn3GEuH317IzpwrMCbOuMIVytpjnJ90YYHNwdRgYGuKmVxwuIqSgqk3M5qqQt1cDk0tGQ==", + "license": "MIT", + "engines": { + "node": ">=23.5.0 || ^22.13.0 || ^21.7.0 || ^20.12.0" + } + }, + "node_modules/@inquirer/input": { + "version": "5.0.11", + "resolved": "https://registry.npmjs.org/@inquirer/input/-/input-5.0.11.tgz", + "integrity": "sha512-twUWidn4ocPO8qi6fRM7tNWt7W1FOnOZqQ+/+PsfLUacMR5rFLDPK9ql0nBPwxi0oELbo8T5NhRs8B2+qQEqFQ==", + "license": "MIT", + "dependencies": { + "@inquirer/core": "^11.1.8", + "@inquirer/type": "^4.0.5" + }, + "engines": { + "node": ">=23.5.0 || ^22.13.0 || ^21.7.0 || ^20.12.0" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/number": { + "version": "4.0.11", + "resolved": "https://registry.npmjs.org/@inquirer/number/-/number-4.0.11.tgz", + "integrity": "sha512-Vscmim9TCksQsfjPtka/JwPUcbLhqWYrgfPf1cHrCm24X/F2joFwnageD50yMKsaX14oNGOyKf/RNXAFkNjWpA==", + "license": "MIT", + "dependencies": { + "@inquirer/core": "^11.1.8", + "@inquirer/type": "^4.0.5" + }, + "engines": { + "node": ">=23.5.0 || ^22.13.0 || ^21.7.0 || ^20.12.0" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/password": { + "version": "5.0.11", + "resolved": "https://registry.npmjs.org/@inquirer/password/-/password-5.0.11.tgz", + "integrity": "sha512-9KZFeRaNHIcejtPb0wN4ddFc7EvobVoAFa049eS3LrDZFxI8O7xUXiITEOinBzkZFAIwY5V4yzQae/QfO9cbbg==", + "license": "MIT", + "dependencies": { + "@inquirer/ansi": "^2.0.5", + "@inquirer/core": "^11.1.8", + "@inquirer/type": "^4.0.5" + }, + "engines": { + "node": ">=23.5.0 || ^22.13.0 || ^21.7.0 || ^20.12.0" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/prompts": { + "version": "8.4.1", + "resolved": "https://registry.npmjs.org/@inquirer/prompts/-/prompts-8.4.1.tgz", + "integrity": "sha512-AH5xPQ997K7e0F0vulPlteIHke2awMkFi8F0dBemrDfmvtPmHJo82mdHbONC4F/t8d1NHwrbI5cGVI+RbLWdoQ==", + "license": "MIT", + "dependencies": { + "@inquirer/checkbox": "^5.1.3", + "@inquirer/confirm": "^6.0.11", + "@inquirer/editor": "^5.1.0", + "@inquirer/expand": "^5.0.12", + "@inquirer/input": "^5.0.11", + "@inquirer/number": "^4.0.11", + "@inquirer/password": "^5.0.11", + "@inquirer/rawlist": "^5.2.7", + "@inquirer/search": "^4.1.7", + "@inquirer/select": "^5.1.3" + }, + "engines": { + "node": ">=23.5.0 || ^22.13.0 || ^21.7.0 || ^20.12.0" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/rawlist": { + "version": "5.2.7", + "resolved": "https://registry.npmjs.org/@inquirer/rawlist/-/rawlist-5.2.7.tgz", + "integrity": "sha512-AqRMiD9+uE1lskDPrdqHwrV/EUmxKEBLX44SR7uxK3vD2413AmVfE5EQaPeNzYf5Pq5SitHJDYUFVF0poIr09w==", + "license": "MIT", + "dependencies": { + "@inquirer/core": "^11.1.8", + "@inquirer/type": "^4.0.5" + }, + "engines": { + "node": ">=23.5.0 || ^22.13.0 || ^21.7.0 || ^20.12.0" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/search": { + "version": "4.1.7", + "resolved": "https://registry.npmjs.org/@inquirer/search/-/search-4.1.7.tgz", + "integrity": "sha512-1y7+0N65AWk5RdlXH/Kn13txf3IjIQ7OEfhCEkDTU+h5wKMLq8DUF3P6z+/kLSxDGDtQT1dRBWEUC3o/VvImsQ==", + "license": "MIT", + "dependencies": { + "@inquirer/core": "^11.1.8", + "@inquirer/figures": "^2.0.5", + "@inquirer/type": "^4.0.5" + }, + "engines": { + "node": ">=23.5.0 || ^22.13.0 || ^21.7.0 || ^20.12.0" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/select": { + "version": "5.1.3", + "resolved": "https://registry.npmjs.org/@inquirer/select/-/select-5.1.3.tgz", + "integrity": "sha512-zYyqWgGQi3NhBcNq4Isc5rB3oEdQEh1Q/EcAnOW0FK4MpnXWkvSBYgA4cYrTM4A9UB573omouZbnL9JJ74Mq3A==", + "license": "MIT", + "dependencies": { + "@inquirer/ansi": "^2.0.5", + "@inquirer/core": "^11.1.8", + "@inquirer/figures": "^2.0.5", + "@inquirer/type": "^4.0.5" + }, + "engines": { + "node": ">=23.5.0 || ^22.13.0 || ^21.7.0 || ^20.12.0" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/type": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/@inquirer/type/-/type-4.0.5.tgz", + "integrity": "sha512-aetVUNeKNc/VriqXlw1NRSW0zhMBB0W4bNbWRJgzRl/3d0QNDQFfk0GO5SDdtjMZVg6o8ZKEiadd7SCCzoOn5Q==", + "license": "MIT", + "engines": { + "node": ">=23.5.0 || ^22.13.0 || ^21.7.0 || ^20.12.0" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } } }, "node_modules/@types/node": { @@ -43,20 +410,6 @@ "@types/node": "*" } }, - "node_modules/ansi-escapes": { - "version": "4.3.2", - "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-4.3.2.tgz", - "integrity": "sha512-gKXj5ALrKWQLsYG9jlTRmR/xKluxHV+Z9QEwNIgCfM1/uwPMCuzVVnh5mwTd+OuBZcwSIMbqssNWRm1lE51QaQ==", - "dependencies": { - "type-fest": "^0.21.3" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/ansi-regex": { "version": "6.1.0", "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.1.0.tgz", @@ -84,17 +437,6 @@ "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==" }, - "node_modules/biskviit": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/biskviit/-/biskviit-1.0.1.tgz", - "integrity": "sha512-VGCXdHbdbpEkFgtjkeoBN8vRlbj1ZRX2/mxhE8asCCRalUx2nBzOomLJv8Aw/nRt5+ccDb+tPKidg4XxcfGW4w==", - "dependencies": { - "psl": "^1.1.7" - }, - "engines": { - "node": ">=1.0.0" - } - }, "node_modules/buffer-crc32": { "version": "0.2.13", "resolved": "https://registry.npmjs.org/buffer-crc32/-/buffer-crc32-0.2.13.tgz", @@ -103,70 +445,17 @@ "node": "*" } }, - "node_modules/camelcase": { - "version": "5.3.1", - "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-5.3.1.tgz", - "integrity": "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==", - "engines": { - "node": ">=6" - } - }, - "node_modules/chalk": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", - "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", - "dependencies": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/chalk?sponsor=1" - } - }, - "node_modules/chalk/node_modules/ansi-styles": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", - "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", - "dependencies": { - "color-convert": "^2.0.1" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, - "node_modules/chardet": { - "version": "0.7.0", - "resolved": "https://registry.npmjs.org/chardet/-/chardet-0.7.0.tgz", - "integrity": "sha512-mT8iDcrh03qDGRRmoA2hmBJnxpllMR+0/0qlzjqZES6NdiWDcZkCNAk4rPFZ9Q85r27unkiNNg8ZOiwZXBHwcA==" - }, - "node_modules/child_process": { + "node_modules/call-bind-apply-helpers": { "version": "1.0.2", - "resolved": "https://registry.npmjs.org/child_process/-/child_process-1.0.2.tgz", - "integrity": "sha512-Wmza/JzL0SiWz7kl6MhIKT5ceIlnFPJX+lwUGj7Clhy5MMldsSoJR0+uvRzOS5Kv45Mq7t1PoE8TsOA9bzvb6g==" - }, - "node_modules/cli-cursor": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-3.1.0.tgz", - "integrity": "sha512-I/zHAwsKf9FqGoXM4WWRACob9+SNukZTd94DWF57E4toouRulbCxcUh6RKUEOQlYTHJnzkPMySvPNaaSLNfLZw==", + "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", + "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", + "license": "MIT", "dependencies": { - "restore-cursor": "^3.1.0" + "es-errors": "^1.3.0", + "function-bind": "^1.1.2" }, "engines": { - "node": ">=8" - } - }, - "node_modules/cli-width": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/cli-width/-/cli-width-3.0.0.tgz", - "integrity": "sha512-FxqpkPPwu1HjuN93Omfm4h8uIanXofW0RxVEW3k5RKx+mJJYSthzNhp32Kzxxy3YAEZ/Dc/EWN1vZRY0+kOhbw==", - "engines": { - "node": ">= 10" + "node": ">= 0.4" } }, "node_modules/cliui": { @@ -300,14 +589,6 @@ } } }, - "node_modules/decamelize": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/decamelize/-/decamelize-1.2.0.tgz", - "integrity": "sha512-z2S+W9X73hAUUki+N+9Za2lBlun89zigOyGrsax+KUQ6wKW4ZoWpEYBkGhQjwAjjDCkWxhY0VKEhk8wzY7F5cA==", - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/degit": { "version": "2.8.4", "resolved": "https://registry.npmjs.org/degit/-/degit-2.8.4.tgz", @@ -327,19 +608,25 @@ "node": ">=0.4.0" } }, + "node_modules/dunder-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", + "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.1", + "es-errors": "^1.3.0", + "gopd": "^1.2.0" + }, + "engines": { + "node": ">= 0.4" + } + }, "node_modules/emoji-regex": { "version": "10.4.0", "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-10.4.0.tgz", "integrity": "sha512-EC+0oUMY1Rqm4O6LLrgjtYDvcVYTy7chDnM4Q7030tP4Kwj3u/pR6gP9ygnp2CJMK5Gq+9Q2oqmrFJAz01DXjw==" }, - "node_modules/encoding": { - "version": "0.1.12", - "resolved": "https://registry.npmjs.org/encoding/-/encoding-0.1.12.tgz", - "integrity": "sha512-bl1LAgiQc4ZWr++pNYUdRe/alecaHFeHxIJ/pNciqGdKXghaTCOwKkbKp6ye7pKZGu/GcaSXFk8PBVhgs+dJdA==", - "dependencies": { - "iconv-lite": "~0.4.13" - } - }, "node_modules/end-of-stream": { "version": "1.4.4", "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.4.tgz", @@ -359,33 +646,57 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/escalade": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", - "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", + "node_modules/es-define-property": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", + "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", + "license": "MIT", "engines": { - "node": ">=6" + "node": ">= 0.4" + } + }, + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "license": "MIT", + "engines": { + "node": ">= 0.4" } }, - "node_modules/escape-string-regexp": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", - "integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==", + "node_modules/es-object-atoms": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", + "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0" + }, "engines": { - "node": ">=0.8.0" + "node": ">= 0.4" } }, - "node_modules/external-editor": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/external-editor/-/external-editor-3.1.0.tgz", - "integrity": "sha512-hMQ4CX1p1izmuLYyZqLMO/qGNw10wSv9QDCPfzXfyFrOaCSSoRfqE1Kf1s5an66J5JZC62NewG+mK49jOCtQew==", + "node_modules/es-set-tostringtag": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz", + "integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==", + "license": "MIT", "dependencies": { - "chardet": "^0.7.0", - "iconv-lite": "^0.4.24", - "tmp": "^0.0.33" + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.6", + "has-tostringtag": "^1.0.2", + "hasown": "^2.0.2" }, "engines": { - "node": ">=4" + "node": ">= 0.4" + } + }, + "node_modules/escalade": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", + "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", + "engines": { + "node": ">=6" } }, "node_modules/extract-zip": { @@ -407,6 +718,30 @@ "@types/yauzl": "^2.9.1" } }, + "node_modules/fast-string-truncated-width": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/fast-string-truncated-width/-/fast-string-truncated-width-3.0.3.tgz", + "integrity": "sha512-0jjjIEL6+0jag3l2XWWizO64/aZVtpiGE3t0Zgqxv0DPuxiMjvB3M24fCyhZUO4KomJQPj3LTSUnDP3GpdwC0g==", + "license": "MIT" + }, + "node_modules/fast-string-width": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/fast-string-width/-/fast-string-width-3.0.2.tgz", + "integrity": "sha512-gX8LrtNEI5hq8DVUfRQMbr5lpaS4nMIWV+7XEbXk2b8kiQIizgnlr12B4dA3ZEx3308ze0O4Q1R+cHts8kyUJg==", + "license": "MIT", + "dependencies": { + "fast-string-truncated-width": "^3.0.2" + } + }, + "node_modules/fast-wrap-ansi": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/fast-wrap-ansi/-/fast-wrap-ansi-0.2.0.tgz", + "integrity": "sha512-rLV8JHxTyhVmFYhBJuMujcrHqOT2cnO5Zxj37qROj23CP39GXubJRBUFF0z8KFK77Uc0SukZUf7JZhsVEQ6n8w==", + "license": "MIT", + "dependencies": { + "fast-string-width": "^3.0.2" + } + }, "node_modules/fd-slicer": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/fd-slicer/-/fd-slicer-1.1.0.tgz", @@ -415,15 +750,6 @@ "pend": "~1.2.0" } }, - "node_modules/fetch": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/fetch/-/fetch-1.1.0.tgz", - "integrity": "sha512-5O8TwrGzoNblBG/jtK4NFuZwNCkZX6s5GfRNOaGtm+QGJEuNakSC/i2RW0R93KX6E0jVjNXm6O3CRN4Ql3K+yA==", - "dependencies": { - "biskviit": "1.0.1", - "encoding": "0.1.12" - } - }, "node_modules/fetch-blob": { "version": "3.2.0", "resolved": "https://registry.npmjs.org/fetch-blob/-/fetch-blob-3.2.0.tgz", @@ -446,38 +772,16 @@ "node": "^12.20 || >= 14.13" } }, - "node_modules/figures": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/figures/-/figures-3.2.0.tgz", - "integrity": "sha512-yaduQFRKLXYOGgEn6AZau90j3ggSOyiqXU0F9JZfeXYhNa+Jk4X+s45A2zg5jns87GAFa34BBm2kXw4XpNcbdg==", - "dependencies": { - "escape-string-regexp": "^1.0.5" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/find-up": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/find-up/-/find-up-3.0.0.tgz", - "integrity": "sha512-1yD6RmLI1XBfxugvORwlck6f75tYL+iR0jqwsOrOxMZyGYqUuDhJ0l4AXdO1iX/FTs9cBAMEk1gWSEx1kSbylg==", - "dependencies": { - "locate-path": "^3.0.0" - }, - "engines": { - "node": ">=6" - } - }, "node_modules/form-data": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.0.tgz", - "integrity": "sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==", + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.5.tgz", + "integrity": "sha512-8RipRLol37bNs2bhoV67fiTEvdTrbMUYcFTiy3+wuuOnUog2QBHCZWXDRijWQfAkhBj2Uf5UnVaiWwA5vdd82w==", + "license": "MIT", "dependencies": { "asynckit": "^0.4.0", "combined-stream": "^1.0.8", + "es-set-tostringtag": "^2.1.0", + "hasown": "^2.0.2", "mime-types": "^2.1.12" }, "engines": { @@ -495,6 +799,15 @@ "node": ">=12.20.0" } }, + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/get-caller-file": { "version": "2.0.5", "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", @@ -511,7 +824,44 @@ "node": ">=18" }, "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/get-intrinsic": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", + "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", + "function-bind": "^1.1.2", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "math-intrinsics": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", + "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" } }, "node_modules/get-stream": { @@ -528,93 +878,55 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/has-flag": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", - "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", - "engines": { - "node": ">=8" - } - }, - "node_modules/https": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/https/-/https-1.0.0.tgz", - "integrity": "sha512-4EC57ddXrkaF0x83Oj8sM6SLQHAWXw90Skqu2M4AEWENZ3F02dFJE/GARA8igO79tcgYqGrD7ae4f5L3um2lgg==" - }, - "node_modules/iconv-lite": { - "version": "0.4.24", - "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", - "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", - "dependencies": { - "safer-buffer": ">= 2.1.2 < 3" - }, + "node_modules/gopd": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", + "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", + "license": "MIT", "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/inherits": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz", - "integrity": "sha512-x00IRNXNy63jwGkJmzPigoySHbaqpNuzKbBOmzK+g2OdZpQ9w+sxCN+VSB3ja7IAge2OP2qpfxTjeNcyjmW1uw==" - }, - "node_modules/inquirer": { - "version": "7.3.3", - "resolved": "https://registry.npmjs.org/inquirer/-/inquirer-7.3.3.tgz", - "integrity": "sha512-JG3eIAj5V9CwcGvuOmoo6LB9kbAYT8HXffUl6memuszlwDC/qvFAJw49XJ5NROSFNPxp3iQg1GqkFhaY/CR0IA==", - "dependencies": { - "ansi-escapes": "^4.2.1", - "chalk": "^4.1.0", - "cli-cursor": "^3.1.0", - "cli-width": "^3.0.0", - "external-editor": "^3.0.3", - "figures": "^3.0.0", - "lodash": "^4.17.19", - "mute-stream": "0.0.8", - "run-async": "^2.4.0", - "rxjs": "^6.6.0", - "string-width": "^4.1.0", - "strip-ansi": "^6.0.0", - "through": "^2.3.6" + "node": ">= 0.4" }, - "engines": { - "node": ">=8.0.0" + "funding": { + "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/inquirer/node_modules/ansi-regex": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", - "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "node_modules/has-symbols": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", + "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", + "license": "MIT", "engines": { - "node": ">=8" + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/inquirer/node_modules/emoji-regex": { - "version": "8.0.0", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", - "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==" - }, - "node_modules/inquirer/node_modules/string-width": { - "version": "4.2.3", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", - "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "node_modules/has-tostringtag": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz", + "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==", + "license": "MIT", "dependencies": { - "emoji-regex": "^8.0.0", - "is-fullwidth-code-point": "^3.0.0", - "strip-ansi": "^6.0.1" + "has-symbols": "^1.0.3" }, "engines": { - "node": ">=8" + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/inquirer/node_modules/strip-ansi": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", - "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "node_modules/hasown": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", + "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", + "license": "MIT", "dependencies": { - "ansi-regex": "^5.0.1" + "function-bind": "^1.1.2" }, "engines": { - "node": ">=8" + "node": ">= 0.4" } }, "node_modules/is-fullwidth-code-point": { @@ -625,23 +937,6 @@ "node": ">=8" } }, - "node_modules/locate-path": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-3.0.0.tgz", - "integrity": "sha512-7AO748wWnIhNqAuaty2ZWHkQHRSNfPVIsPIfwEOWO22AmaoVrWavlOcMR5nzTLNYvp36X220/maaRsrec1G65A==", - "dependencies": { - "p-locate": "^3.0.0", - "path-exists": "^3.0.0" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/lodash": { - "version": "4.17.21", - "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", - "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==" - }, "node_modules/log-update": { "version": "6.1.0", "resolved": "https://registry.npmjs.org/log-update/-/log-update-6.1.0.tgz", @@ -717,6 +1012,15 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/math-intrinsics": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", + "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, "node_modules/mime-db": { "version": "1.52.0", "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", @@ -736,14 +1040,6 @@ "node": ">= 0.6" } }, - "node_modules/mimic-fn": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-2.1.0.tgz", - "integrity": "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==", - "engines": { - "node": ">=6" - } - }, "node_modules/mimic-function": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/mimic-function/-/mimic-function-5.0.1.tgz", @@ -760,11 +1056,6 @@ "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==" }, - "node_modules/mute-stream": { - "version": "0.0.8", - "resolved": "https://registry.npmjs.org/mute-stream/-/mute-stream-0.0.8.tgz", - "integrity": "sha512-nnbWWOkoWyUsTjKrhgD0dcz22mdkSnpYqbEjIm2nhwhuxlSkpywJmBo8h0ZqJdkp73mb90SssHkN4rsRaBAfAA==" - }, "node_modules/node-domexception": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/node-domexception/-/node-domexception-1.0.0.tgz", @@ -808,96 +1099,11 @@ "wrappy": "1" } }, - "node_modules/onetime": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/onetime/-/onetime-5.1.2.tgz", - "integrity": "sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==", - "dependencies": { - "mimic-fn": "^2.1.0" - }, - "engines": { - "node": ">=6" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/os-tmpdir": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/os-tmpdir/-/os-tmpdir-1.0.2.tgz", - "integrity": "sha512-D2FR03Vir7FIu45XBY20mTb+/ZSWB00sjU9jdQXt83gDrI4Ztz5Fs7/yy74g2N5SVQY4xY1qDr4rNddwYRVX0g==", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/p-limit": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", - "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", - "dependencies": { - "p-try": "^2.0.0" - }, - "engines": { - "node": ">=6" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/p-locate": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-3.0.0.tgz", - "integrity": "sha512-x+12w/To+4GFfgJhBEpiDcLozRJGegY+Ei7/z0tSLkMmxGZNybVMSfWj9aJn8Z5Fc7dBUNJOOVgPv2H7IwulSQ==", - "dependencies": { - "p-limit": "^2.0.0" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/p-try": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz", - "integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==", - "engines": { - "node": ">=6" - } - }, - "node_modules/path": { - "version": "0.12.7", - "resolved": "https://registry.npmjs.org/path/-/path-0.12.7.tgz", - "integrity": "sha512-aXXC6s+1w7otVF9UletFkFcDsJeO7lSZBPUQhtb5O0xJe8LtYhj/GxldoL09bBj9+ZmE2hNoHqQSFMN5fikh4Q==", - "dependencies": { - "process": "^0.11.1", - "util": "^0.10.3" - } - }, - "node_modules/path-exists": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-3.0.0.tgz", - "integrity": "sha512-bpC7GYwiDYQ4wYLe+FA8lhRjhQCMcQGuSgGGqDkg/QerRWw9CmGRT0iSOVRSZJ29NMLZgIzqaljJ63oaL4NIJQ==", - "engines": { - "node": ">=4" - } - }, "node_modules/pend": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/pend/-/pend-1.2.0.tgz", "integrity": "sha512-F3asv42UuXchdzt+xXqfW1OGlVBe+mxa2mqI0pg5yAHZPvFmY3Y6drSf/GQ1A86WgWEN9Kzh/WrgKa6iGcHXLg==" }, - "node_modules/process": { - "version": "0.11.10", - "resolved": "https://registry.npmjs.org/process/-/process-0.11.10.tgz", - "integrity": "sha512-cdGef/drWFoydD1JsMzuFf8100nZl+GT+yacc2bEced5f9Rjk4z+WtFUTBu9PhOi9j/jfmBPu0mMEY4wIdAF8A==", - "engines": { - "node": ">= 0.6.0" - } - }, - "node_modules/psl": { - "version": "1.9.0", - "resolved": "https://registry.npmjs.org/psl/-/psl-1.9.0.tgz", - "integrity": "sha512-E/ZsdU4HLs/68gYzgGTkMicWTLPdAftJLfJFlLUAAKZGkStNU72sZjT66SnMDVOfOWY/YAoiD7Jxa9iHvngcag==" - }, "node_modules/pump": { "version": "3.0.2", "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.2.tgz", @@ -915,57 +1121,11 @@ "node": ">=0.10.0" } }, - "node_modules/require-main-filename": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/require-main-filename/-/require-main-filename-2.0.0.tgz", - "integrity": "sha512-NKN5kMDylKuldxYLSUfrbo5Tuzh4hd+2E8NPPX02mZtn1VuREQToYe/ZdlJy+J3uCpfaiGF05e7B8W0iXbQHmg==" - }, - "node_modules/restore-cursor": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-3.1.0.tgz", - "integrity": "sha512-l+sSefzHpj5qimhFSE5a8nufZYAM3sBSVMAPtYkmC+4EH2anSGaEMXSD0izRQbu9nfyQ9y5JrVmp7E8oZrUjvA==", - "dependencies": { - "onetime": "^5.1.0", - "signal-exit": "^3.0.2" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/restore-cursor/node_modules/signal-exit": { - "version": "3.0.7", - "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", - "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==" - }, - "node_modules/run-async": { - "version": "2.4.1", - "resolved": "https://registry.npmjs.org/run-async/-/run-async-2.4.1.tgz", - "integrity": "sha512-tvVnVv01b8c1RrA6Ep7JkStj85Guv/YrMcwqYQnwjsAS2cTmmPGBBjAjpCW7RrSodNSoE2/qg9O4bceNvUuDgQ==", - "engines": { - "node": ">=0.12.0" - } - }, - "node_modules/rxjs": { - "version": "6.6.7", - "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-6.6.7.tgz", - "integrity": "sha512-hTdwr+7yYNIT5n4AMYp85KA6yw2Va0FLa3Rguvbpa4W3I5xynaBZo41cM3XM+4Q6fRMj3sBYIR1VAmZMXYJvRQ==", - "dependencies": { - "tslib": "^1.9.0" - }, - "engines": { - "npm": ">=2.0.0" - } - }, "node_modules/safer-buffer": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==" }, - "node_modules/set-blocking": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/set-blocking/-/set-blocking-2.0.0.tgz", - "integrity": "sha512-KiKBS8AnWGEyLzofFfmvKwpdPzqiy16LvQfK3yv/fVH7Bj13/wl3JSR1J+rfgRE9q7xUJK4qvgS8raSOeLUehw==" - }, "node_modules/signal-exit": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", @@ -1036,63 +1196,12 @@ "url": "https://github.com/chalk/strip-ansi?sponsor=1" } }, - "node_modules/supports-color": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", - "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", - "dependencies": { - "has-flag": "^4.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/through": { - "version": "2.3.8", - "resolved": "https://registry.npmjs.org/through/-/through-2.3.8.tgz", - "integrity": "sha512-w89qg7PI8wAdvX60bMDP+bFoD5Dvhm9oLheFp5O4a2QF0cSBGsBX4qZmadPMvVqlLJBBci+WqGGOAPvcDeNSVg==" - }, - "node_modules/tmp": { - "version": "0.0.33", - "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.0.33.tgz", - "integrity": "sha512-jRCJlojKnZ3addtTOjdIqoRuPEKBvNXcGYqzO6zWZX8KfKEpnGY5jfggJQ3EjKuu8D4bJRr0y+cYJFmYbImXGw==", - "dependencies": { - "os-tmpdir": "~1.0.2" - }, - "engines": { - "node": ">=0.6.0" - } - }, - "node_modules/tslib": { - "version": "1.14.1", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz", - "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==" - }, - "node_modules/type-fest": { - "version": "0.21.3", - "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.21.3.tgz", - "integrity": "sha512-t0rzBq87m3fVcduHDUFhKmyyX+9eo6WQjZvf51Ea/M0Q7+T374Jp1aUiyUl0GKxp8M/OETVHSDvmkyPgvX+X2w==", - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/undici-types": { "version": "6.19.8", "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.19.8.tgz", "integrity": "sha512-ve2KP6f/JnbPBFyobGHuerC9g1FYGn/F8n1LWTwNxCEzd6IfqTwUQcNXgEtmmQ6DlRrC1hrSrBnCZPokRrDHjw==", "optional": true }, - "node_modules/util": { - "version": "0.10.4", - "resolved": "https://registry.npmjs.org/util/-/util-0.10.4.tgz", - "integrity": "sha512-0Pm9hTQ3se5ll1XihRic3FDIku70C+iHUdT/W926rSgHV5QgXsYbKZN8MSC3tJtSkhuROzvsQjAaFENRXr+19A==", - "dependencies": { - "inherits": "2.0.3" - } - }, "node_modules/web-streams-polyfill": { "version": "3.3.3", "resolved": "https://registry.npmjs.org/web-streams-polyfill/-/web-streams-polyfill-3.3.3.tgz", @@ -1101,11 +1210,6 @@ "node": ">= 8" } }, - "node_modules/which-module": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/which-module/-/which-module-2.0.1.tgz", - "integrity": "sha512-iBdZ57RDvnOR9AGBhML2vFZf7h8vmBjhoaZqODJBFWHVtKkDmKuHai3cx5PgVMrX5YDNp27AofYbAwctSS+vhQ==" - }, "node_modules/wrap-ansi": { "version": "9.0.0", "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-9.0.0.tgz", @@ -1152,199 +1256,242 @@ "node": ">=12" } }, - "node_modules/yargs-interactive": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/yargs-interactive/-/yargs-interactive-3.0.1.tgz", - "integrity": "sha512-Jnp88uiuz+ZRpM10Lwvs0nRetWPog+6lcgQrhwKsyEanAe3wgTlaPPzcYlZWp53aOMTzOcR5wEpEsFOMOPmLlw==", - "dependencies": { - "inquirer": "^7.0.0", - "yargs": "^14.0.0" - }, + "node_modules/yargs-parser": { + "version": "21.1.1", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz", + "integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==", "engines": { - "node": ">=8", - "npm": ">=6" + "node": ">=12" } }, - "node_modules/yargs-interactive/node_modules/ansi-regex": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-4.1.1.tgz", - "integrity": "sha512-ILlv4k/3f6vfQ4OoP2AGvirOktlQ98ZEL1k9FaQjxa3L1abBgbuTDAdPOpvbGncC0BTVQrl+OM8xZGK6tWXt7g==", + "node_modules/yargs/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", "engines": { - "node": ">=6" + "node": ">=8" } }, - "node_modules/yargs-interactive/node_modules/ansi-styles": { - "version": "3.2.1", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", - "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", + "node_modules/yargs/node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==" + }, + "node_modules/yargs/node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", "dependencies": { - "color-convert": "^1.9.0" + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" }, "engines": { - "node": ">=4" + "node": ">=8" } }, - "node_modules/yargs-interactive/node_modules/cliui": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/cliui/-/cliui-5.0.0.tgz", - "integrity": "sha512-PYeGSEmmHM6zvoef2w8TPzlrnNpXIjTipYK780YswmIP9vjxmd6Y2a3CB2Ks6/AU8NHjZugXvo8w3oWM2qnwXA==", + "node_modules/yargs/node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", "dependencies": { - "string-width": "^3.1.0", - "strip-ansi": "^5.2.0", - "wrap-ansi": "^5.1.0" + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" } }, - "node_modules/yargs-interactive/node_modules/color-convert": { - "version": "1.9.3", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", - "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", + "node_modules/yauzl": { + "version": "2.10.0", + "resolved": "https://registry.npmjs.org/yauzl/-/yauzl-2.10.0.tgz", + "integrity": "sha512-p4a9I6X6nu6IhoGmBqAcbJy1mlC4j27vEPZX9F4L4/vZT3Lyq1VkFHw/V/PUcB9Buo+DG3iHkT0x3Qya58zc3g==", "dependencies": { - "color-name": "1.1.3" + "buffer-crc32": "~0.2.3", + "fd-slicer": "~1.1.0" } + } + }, + "dependencies": { + "@inquirer/ansi": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@inquirer/ansi/-/ansi-2.0.5.tgz", + "integrity": "sha512-doc2sWgJpbFQ64UflSVd17ibMGDuxO1yKgOgLMwavzESnXjFWJqUeG8saYosqKpHp4kWiM5x1nXvEjbpx90gzw==" }, - "node_modules/yargs-interactive/node_modules/color-name": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", - "integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==" - }, - "node_modules/yargs-interactive/node_modules/emoji-regex": { - "version": "7.0.3", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-7.0.3.tgz", - "integrity": "sha512-CwBLREIQ7LvYFB0WyRvwhq5N5qPhc6PMjD6bYggFlI5YyDgl+0vxq5VHbMOFqLg7hfWzmu8T5Z1QofhmTIhItA==" - }, - "node_modules/yargs-interactive/node_modules/is-fullwidth-code-point": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-2.0.0.tgz", - "integrity": "sha512-VHskAKYM8RfSFXwee5t5cbN5PZeq1Wrh6qd5bkyiXIf6UQcN6w/A0eXM9r6t8d+GYOh+o6ZhiEnb88LN/Y8m2w==", - "engines": { - "node": ">=4" + "@inquirer/checkbox": { + "version": "5.1.3", + "resolved": "https://registry.npmjs.org/@inquirer/checkbox/-/checkbox-5.1.3.tgz", + "integrity": "sha512-+G7I8CT+EHv/hasNfUl3P37DVoMoZfpA+2FXmM54dA8MxYle1YqucxbacxHalw1iAFSdKNEDTGNV7F+j1Ldqcg==", + "requires": { + "@inquirer/ansi": "^2.0.5", + "@inquirer/core": "^11.1.8", + "@inquirer/figures": "^2.0.5", + "@inquirer/type": "^4.0.5" } }, - "node_modules/yargs-interactive/node_modules/string-width": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-3.1.0.tgz", - "integrity": "sha512-vafcv6KjVZKSgz06oM/H6GDBrAtz8vdhQakGjFIvNrHA6y3HCF1CInLy+QLq8dTJPQ1b+KDUqDFctkdRW44e1w==", - "dependencies": { - "emoji-regex": "^7.0.1", - "is-fullwidth-code-point": "^2.0.0", - "strip-ansi": "^5.1.0" - }, - "engines": { - "node": ">=6" + "@inquirer/confirm": { + "version": "6.0.11", + "resolved": "https://registry.npmjs.org/@inquirer/confirm/-/confirm-6.0.11.tgz", + "integrity": "sha512-pTpHjg0iEIRMYV/7oCZUMf27/383E6Wyhfc/MY+AVQGEoUobffIYWOK9YLP2XFRGz/9i6WlTQh1CkFVIo2Y7XA==", + "requires": { + "@inquirer/core": "^11.1.8", + "@inquirer/type": "^4.0.5" } }, - "node_modules/yargs-interactive/node_modules/strip-ansi": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-5.2.0.tgz", - "integrity": "sha512-DuRs1gKbBqsMKIZlrffwlug8MHkcnpjs5VPmL1PAh+mA30U0DTotfDZ0d2UUsXpPmPmMMJ6W773MaA3J+lbiWA==", - "dependencies": { - "ansi-regex": "^4.1.0" + "@inquirer/core": { + "version": "11.1.8", + "resolved": "https://registry.npmjs.org/@inquirer/core/-/core-11.1.8.tgz", + "integrity": "sha512-/u+yJk2pOKNDOh1ZgdUH2RQaRx6OOH4I0uwL95qPvTFTIL38YBsuSC4r1yXBB3Q6JvNqFFc202gk0Ew79rrcjA==", + "requires": { + "@inquirer/ansi": "^2.0.5", + "@inquirer/figures": "^2.0.5", + "@inquirer/type": "^4.0.5", + "cli-width": "^4.1.0", + "fast-wrap-ansi": "^0.2.0", + "mute-stream": "^3.0.0", + "signal-exit": "^4.1.0" }, - "engines": { - "node": ">=6" + "dependencies": { + "cli-width": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/cli-width/-/cli-width-4.1.0.tgz", + "integrity": "sha512-ouuZd4/dm2Sw5Gmqy6bGyNNNe1qt9RpmxveLSO7KcgsTnU7RXfsw+/bukWGo1abgBiMAic068rclZsO4IWmmxQ==" + }, + "mute-stream": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/mute-stream/-/mute-stream-3.0.0.tgz", + "integrity": "sha512-dkEJPVvun4FryqBmZ5KhDo0K9iDXAwn08tMLDinNdRBNPcYEDiWYysLcc6k3mjTMlbP9KyylvRpd4wFtwrT9rw==" + } } }, - "node_modules/yargs-interactive/node_modules/wrap-ansi": { + "@inquirer/editor": { "version": "5.1.0", - "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-5.1.0.tgz", - "integrity": "sha512-QC1/iN/2/RPVJ5jYK8BGttj5z83LmSKmvbvrXPNCLZSEb32KKVDJDl/MOt2N01qU2H/FkzEa9PKto1BqDjtd7Q==", - "dependencies": { - "ansi-styles": "^3.2.0", - "string-width": "^3.0.0", - "strip-ansi": "^5.0.0" - }, - "engines": { - "node": ">=6" + "resolved": "https://registry.npmjs.org/@inquirer/editor/-/editor-5.1.0.tgz", + "integrity": "sha512-6wlkYl65Qfayy48gPCfU4D7li6KCAGN79mLXa/tYHZH99OfZ820yY+HA+DgE88r8YwwgeuY6PQgNqMeK6LuMmw==", + "requires": { + "@inquirer/core": "^11.1.8", + "@inquirer/external-editor": "^3.0.0", + "@inquirer/type": "^4.0.5" } }, - "node_modules/yargs-interactive/node_modules/y18n": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/y18n/-/y18n-4.0.3.tgz", - "integrity": "sha512-JKhqTOwSrqNA1NY5lSztJ1GrBiUodLMmIZuLiDaMRJ+itFd+ABVE8XBjOvIWL+rSqNDC74LCSFmlb/U4UZ4hJQ==" - }, - "node_modules/yargs-interactive/node_modules/yargs": { - "version": "14.2.3", - "resolved": "https://registry.npmjs.org/yargs/-/yargs-14.2.3.tgz", - "integrity": "sha512-ZbotRWhF+lkjijC/VhmOT9wSgyBQ7+zr13+YLkhfsSiTriYsMzkTUFP18pFhWwBeMa5gUc1MzbhrO6/VB7c9Xg==", - "dependencies": { - "cliui": "^5.0.0", - "decamelize": "^1.2.0", - "find-up": "^3.0.0", - "get-caller-file": "^2.0.1", - "require-directory": "^2.1.1", - "require-main-filename": "^2.0.0", - "set-blocking": "^2.0.0", - "string-width": "^3.0.0", - "which-module": "^2.0.0", - "y18n": "^4.0.0", - "yargs-parser": "^15.0.1" + "@inquirer/expand": { + "version": "5.0.12", + "resolved": "https://registry.npmjs.org/@inquirer/expand/-/expand-5.0.12.tgz", + "integrity": "sha512-vOfrB33b7YIZfDauXS8vNNz2Z86FozTZLIt7e+7/dCaPJ1RXZsHCuI9TlcERzEUq57vkM+UdnBgxP0rFd23JYQ==", + "requires": { + "@inquirer/core": "^11.1.8", + "@inquirer/type": "^4.0.5" } }, - "node_modules/yargs-interactive/node_modules/yargs-parser": { - "version": "15.0.3", - "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-15.0.3.tgz", - "integrity": "sha512-/MVEVjTXy/cGAjdtQf8dW3V9b97bPN7rNn8ETj6BmAQL7ibC7O1Q9SPJbGjgh3SlwoBNXMzj/ZGIj8mBgl12YA==", + "@inquirer/external-editor": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@inquirer/external-editor/-/external-editor-3.0.0.tgz", + "integrity": "sha512-lDSwMgg+M5rq6JKBYaJwSX6T9e/HK2qqZ1oxmOwn4AQoJE5D+7TumsxLGC02PWS//rkIVqbZv3XA3ejsc9FYvg==", + "requires": { + "chardet": "^2.1.1", + "iconv-lite": "^0.7.2" + }, "dependencies": { - "camelcase": "^5.0.0", - "decamelize": "^1.2.0" + "chardet": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/chardet/-/chardet-2.1.1.tgz", + "integrity": "sha512-PsezH1rqdV9VvyNhxxOW32/d75r01NY7TQCmOqomRo15ZSOKbpTFVsfjghxo6JloQUCGnH4k1LGu0R4yCLlWQQ==" + }, + "iconv-lite": { + "version": "0.7.2", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.7.2.tgz", + "integrity": "sha512-im9DjEDQ55s9fL4EYzOAv0yMqmMBSZp6G0VvFyTMPKWxiSBHUj9NW/qqLmXUwXrrM7AvqSlTCfvqRb0cM8yYqw==", + "requires": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + } + } } }, - "node_modules/yargs-parser": { - "version": "21.1.1", - "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz", - "integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==", - "engines": { - "node": ">=12" - } + "@inquirer/figures": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@inquirer/figures/-/figures-2.0.5.tgz", + "integrity": "sha512-NsSs4kzfm12lNetHwAn3GEuH317IzpwrMCbOuMIVytpjnJ90YYHNwdRgYGuKmVxwuIqSgqk3M5qqQt1cDk0tGQ==" }, - "node_modules/yargs/node_modules/ansi-regex": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", - "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", - "engines": { - "node": ">=8" + "@inquirer/input": { + "version": "5.0.11", + "resolved": "https://registry.npmjs.org/@inquirer/input/-/input-5.0.11.tgz", + "integrity": "sha512-twUWidn4ocPO8qi6fRM7tNWt7W1FOnOZqQ+/+PsfLUacMR5rFLDPK9ql0nBPwxi0oELbo8T5NhRs8B2+qQEqFQ==", + "requires": { + "@inquirer/core": "^11.1.8", + "@inquirer/type": "^4.0.5" + } + }, + "@inquirer/number": { + "version": "4.0.11", + "resolved": "https://registry.npmjs.org/@inquirer/number/-/number-4.0.11.tgz", + "integrity": "sha512-Vscmim9TCksQsfjPtka/JwPUcbLhqWYrgfPf1cHrCm24X/F2joFwnageD50yMKsaX14oNGOyKf/RNXAFkNjWpA==", + "requires": { + "@inquirer/core": "^11.1.8", + "@inquirer/type": "^4.0.5" } }, - "node_modules/yargs/node_modules/emoji-regex": { - "version": "8.0.0", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", - "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==" + "@inquirer/password": { + "version": "5.0.11", + "resolved": "https://registry.npmjs.org/@inquirer/password/-/password-5.0.11.tgz", + "integrity": "sha512-9KZFeRaNHIcejtPb0wN4ddFc7EvobVoAFa049eS3LrDZFxI8O7xUXiITEOinBzkZFAIwY5V4yzQae/QfO9cbbg==", + "requires": { + "@inquirer/ansi": "^2.0.5", + "@inquirer/core": "^11.1.8", + "@inquirer/type": "^4.0.5" + } }, - "node_modules/yargs/node_modules/string-width": { - "version": "4.2.3", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", - "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", - "dependencies": { - "emoji-regex": "^8.0.0", - "is-fullwidth-code-point": "^3.0.0", - "strip-ansi": "^6.0.1" - }, - "engines": { - "node": ">=8" + "@inquirer/prompts": { + "version": "8.4.1", + "resolved": "https://registry.npmjs.org/@inquirer/prompts/-/prompts-8.4.1.tgz", + "integrity": "sha512-AH5xPQ997K7e0F0vulPlteIHke2awMkFi8F0dBemrDfmvtPmHJo82mdHbONC4F/t8d1NHwrbI5cGVI+RbLWdoQ==", + "requires": { + "@inquirer/checkbox": "^5.1.3", + "@inquirer/confirm": "^6.0.11", + "@inquirer/editor": "^5.1.0", + "@inquirer/expand": "^5.0.12", + "@inquirer/input": "^5.0.11", + "@inquirer/number": "^4.0.11", + "@inquirer/password": "^5.0.11", + "@inquirer/rawlist": "^5.2.7", + "@inquirer/search": "^4.1.7", + "@inquirer/select": "^5.1.3" + } + }, + "@inquirer/rawlist": { + "version": "5.2.7", + "resolved": "https://registry.npmjs.org/@inquirer/rawlist/-/rawlist-5.2.7.tgz", + "integrity": "sha512-AqRMiD9+uE1lskDPrdqHwrV/EUmxKEBLX44SR7uxK3vD2413AmVfE5EQaPeNzYf5Pq5SitHJDYUFVF0poIr09w==", + "requires": { + "@inquirer/core": "^11.1.8", + "@inquirer/type": "^4.0.5" } }, - "node_modules/yargs/node_modules/strip-ansi": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", - "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", - "dependencies": { - "ansi-regex": "^5.0.1" - }, - "engines": { - "node": ">=8" + "@inquirer/search": { + "version": "4.1.7", + "resolved": "https://registry.npmjs.org/@inquirer/search/-/search-4.1.7.tgz", + "integrity": "sha512-1y7+0N65AWk5RdlXH/Kn13txf3IjIQ7OEfhCEkDTU+h5wKMLq8DUF3P6z+/kLSxDGDtQT1dRBWEUC3o/VvImsQ==", + "requires": { + "@inquirer/core": "^11.1.8", + "@inquirer/figures": "^2.0.5", + "@inquirer/type": "^4.0.5" } }, - "node_modules/yauzl": { - "version": "2.10.0", - "resolved": "https://registry.npmjs.org/yauzl/-/yauzl-2.10.0.tgz", - "integrity": "sha512-p4a9I6X6nu6IhoGmBqAcbJy1mlC4j27vEPZX9F4L4/vZT3Lyq1VkFHw/V/PUcB9Buo+DG3iHkT0x3Qya58zc3g==", - "dependencies": { - "buffer-crc32": "~0.2.3", - "fd-slicer": "~1.1.0" + "@inquirer/select": { + "version": "5.1.3", + "resolved": "https://registry.npmjs.org/@inquirer/select/-/select-5.1.3.tgz", + "integrity": "sha512-zYyqWgGQi3NhBcNq4Isc5rB3oEdQEh1Q/EcAnOW0FK4MpnXWkvSBYgA4cYrTM4A9UB573omouZbnL9JJ74Mq3A==", + "requires": { + "@inquirer/ansi": "^2.0.5", + "@inquirer/core": "^11.1.8", + "@inquirer/figures": "^2.0.5", + "@inquirer/type": "^4.0.5" } - } - }, - "dependencies": { + }, + "@inquirer/type": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/@inquirer/type/-/type-4.0.5.tgz", + "integrity": "sha512-aetVUNeKNc/VriqXlw1NRSW0zhMBB0W4bNbWRJgzRl/3d0QNDQFfk0GO5SDdtjMZVg6o8ZKEiadd7SCCzoOn5Q==", + "requires": {} + }, "@types/node": { "version": "22.7.2", "resolved": "https://registry.npmjs.org/@types/node/-/node-22.7.2.tgz", @@ -1363,14 +1510,6 @@ "@types/node": "*" } }, - "ansi-escapes": { - "version": "4.3.2", - "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-4.3.2.tgz", - "integrity": "sha512-gKXj5ALrKWQLsYG9jlTRmR/xKluxHV+Z9QEwNIgCfM1/uwPMCuzVVnh5mwTd+OuBZcwSIMbqssNWRm1lE51QaQ==", - "requires": { - "type-fest": "^0.21.3" - } - }, "ansi-regex": { "version": "6.1.0", "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.1.0.tgz", @@ -1386,66 +1525,20 @@ "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==" }, - "biskviit": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/biskviit/-/biskviit-1.0.1.tgz", - "integrity": "sha512-VGCXdHbdbpEkFgtjkeoBN8vRlbj1ZRX2/mxhE8asCCRalUx2nBzOomLJv8Aw/nRt5+ccDb+tPKidg4XxcfGW4w==", - "requires": { - "psl": "^1.1.7" - } - }, "buffer-crc32": { "version": "0.2.13", "resolved": "https://registry.npmjs.org/buffer-crc32/-/buffer-crc32-0.2.13.tgz", "integrity": "sha512-VO9Ht/+p3SN7SKWqcrgEzjGbRSJYTx+Q1pTQC0wrWqHx0vpJraQ6GtHx8tvcg1rlK1byhU5gccxgOgj7B0TDkQ==" }, - "camelcase": { - "version": "5.3.1", - "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-5.3.1.tgz", - "integrity": "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==" - }, - "chalk": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", - "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", - "requires": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" - }, - "dependencies": { - "ansi-styles": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", - "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", - "requires": { - "color-convert": "^2.0.1" - } - } - } - }, - "chardet": { - "version": "0.7.0", - "resolved": "https://registry.npmjs.org/chardet/-/chardet-0.7.0.tgz", - "integrity": "sha512-mT8iDcrh03qDGRRmoA2hmBJnxpllMR+0/0qlzjqZES6NdiWDcZkCNAk4rPFZ9Q85r27unkiNNg8ZOiwZXBHwcA==" - }, - "child_process": { + "call-bind-apply-helpers": { "version": "1.0.2", - "resolved": "https://registry.npmjs.org/child_process/-/child_process-1.0.2.tgz", - "integrity": "sha512-Wmza/JzL0SiWz7kl6MhIKT5ceIlnFPJX+lwUGj7Clhy5MMldsSoJR0+uvRzOS5Kv45Mq7t1PoE8TsOA9bzvb6g==" - }, - "cli-cursor": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-3.1.0.tgz", - "integrity": "sha512-I/zHAwsKf9FqGoXM4WWRACob9+SNukZTd94DWF57E4toouRulbCxcUh6RKUEOQlYTHJnzkPMySvPNaaSLNfLZw==", + "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", + "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", "requires": { - "restore-cursor": "^3.1.0" + "es-errors": "^1.3.0", + "function-bind": "^1.1.2" } }, - "cli-width": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/cli-width/-/cli-width-3.0.0.tgz", - "integrity": "sha512-FxqpkPPwu1HjuN93Omfm4h8uIanXofW0RxVEW3k5RKx+mJJYSthzNhp32Kzxxy3YAEZ/Dc/EWN1vZRY0+kOhbw==" - }, "cliui": { "version": "8.0.1", "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz", @@ -1538,11 +1631,6 @@ "ms": "^2.1.3" } }, - "decamelize": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/decamelize/-/decamelize-1.2.0.tgz", - "integrity": "sha512-z2S+W9X73hAUUki+N+9Za2lBlun89zigOyGrsax+KUQ6wKW4ZoWpEYBkGhQjwAjjDCkWxhY0VKEhk8wzY7F5cA==" - }, "degit": { "version": "2.8.4", "resolved": "https://registry.npmjs.org/degit/-/degit-2.8.4.tgz", @@ -1553,19 +1641,21 @@ "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==" }, + "dunder-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", + "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", + "requires": { + "call-bind-apply-helpers": "^1.0.1", + "es-errors": "^1.3.0", + "gopd": "^1.2.0" + } + }, "emoji-regex": { "version": "10.4.0", "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-10.4.0.tgz", "integrity": "sha512-EC+0oUMY1Rqm4O6LLrgjtYDvcVYTy7chDnM4Q7030tP4Kwj3u/pR6gP9ygnp2CJMK5Gq+9Q2oqmrFJAz01DXjw==" }, - "encoding": { - "version": "0.1.12", - "resolved": "https://registry.npmjs.org/encoding/-/encoding-0.1.12.tgz", - "integrity": "sha512-bl1LAgiQc4ZWr++pNYUdRe/alecaHFeHxIJ/pNciqGdKXghaTCOwKkbKp6ye7pKZGu/GcaSXFk8PBVhgs+dJdA==", - "requires": { - "iconv-lite": "~0.4.13" - } - }, "end-of-stream": { "version": "1.4.4", "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.4.tgz", @@ -1579,26 +1669,40 @@ "resolved": "https://registry.npmjs.org/environment/-/environment-1.1.0.tgz", "integrity": "sha512-xUtoPkMggbz0MPyPiIWr1Kp4aeWJjDZ6SMvURhimjdZgsRuDplF5/s9hcgGhyXMhs+6vpnuoiZ2kFiu3FMnS8Q==" }, - "escalade": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", - "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==" + "es-define-property": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", + "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==" + }, + "es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==" }, - "escape-string-regexp": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", - "integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==" + "es-object-atoms": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", + "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", + "requires": { + "es-errors": "^1.3.0" + } }, - "external-editor": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/external-editor/-/external-editor-3.1.0.tgz", - "integrity": "sha512-hMQ4CX1p1izmuLYyZqLMO/qGNw10wSv9QDCPfzXfyFrOaCSSoRfqE1Kf1s5an66J5JZC62NewG+mK49jOCtQew==", + "es-set-tostringtag": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz", + "integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==", "requires": { - "chardet": "^0.7.0", - "iconv-lite": "^0.4.24", - "tmp": "^0.0.33" + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.6", + "has-tostringtag": "^1.0.2", + "hasown": "^2.0.2" } }, + "escalade": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", + "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==" + }, "extract-zip": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/extract-zip/-/extract-zip-2.0.1.tgz", @@ -1610,6 +1714,27 @@ "yauzl": "^2.10.0" } }, + "fast-string-truncated-width": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/fast-string-truncated-width/-/fast-string-truncated-width-3.0.3.tgz", + "integrity": "sha512-0jjjIEL6+0jag3l2XWWizO64/aZVtpiGE3t0Zgqxv0DPuxiMjvB3M24fCyhZUO4KomJQPj3LTSUnDP3GpdwC0g==" + }, + "fast-string-width": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/fast-string-width/-/fast-string-width-3.0.2.tgz", + "integrity": "sha512-gX8LrtNEI5hq8DVUfRQMbr5lpaS4nMIWV+7XEbXk2b8kiQIizgnlr12B4dA3ZEx3308ze0O4Q1R+cHts8kyUJg==", + "requires": { + "fast-string-truncated-width": "^3.0.2" + } + }, + "fast-wrap-ansi": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/fast-wrap-ansi/-/fast-wrap-ansi-0.2.0.tgz", + "integrity": "sha512-rLV8JHxTyhVmFYhBJuMujcrHqOT2cnO5Zxj37qROj23CP39GXubJRBUFF0z8KFK77Uc0SukZUf7JZhsVEQ6n8w==", + "requires": { + "fast-string-width": "^3.0.2" + } + }, "fd-slicer": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/fd-slicer/-/fd-slicer-1.1.0.tgz", @@ -1618,15 +1743,6 @@ "pend": "~1.2.0" } }, - "fetch": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/fetch/-/fetch-1.1.0.tgz", - "integrity": "sha512-5O8TwrGzoNblBG/jtK4NFuZwNCkZX6s5GfRNOaGtm+QGJEuNakSC/i2RW0R93KX6E0jVjNXm6O3CRN4Ql3K+yA==", - "requires": { - "biskviit": "1.0.1", - "encoding": "0.1.12" - } - }, "fetch-blob": { "version": "3.2.0", "resolved": "https://registry.npmjs.org/fetch-blob/-/fetch-blob-3.2.0.tgz", @@ -1636,29 +1752,15 @@ "web-streams-polyfill": "^3.0.3" } }, - "figures": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/figures/-/figures-3.2.0.tgz", - "integrity": "sha512-yaduQFRKLXYOGgEn6AZau90j3ggSOyiqXU0F9JZfeXYhNa+Jk4X+s45A2zg5jns87GAFa34BBm2kXw4XpNcbdg==", - "requires": { - "escape-string-regexp": "^1.0.5" - } - }, - "find-up": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/find-up/-/find-up-3.0.0.tgz", - "integrity": "sha512-1yD6RmLI1XBfxugvORwlck6f75tYL+iR0jqwsOrOxMZyGYqUuDhJ0l4AXdO1iX/FTs9cBAMEk1gWSEx1kSbylg==", - "requires": { - "locate-path": "^3.0.0" - } - }, "form-data": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.0.tgz", - "integrity": "sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==", + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.5.tgz", + "integrity": "sha512-8RipRLol37bNs2bhoV67fiTEvdTrbMUYcFTiy3+wuuOnUog2QBHCZWXDRijWQfAkhBj2Uf5UnVaiWwA5vdd82w==", "requires": { "asynckit": "^0.4.0", "combined-stream": "^1.0.8", + "es-set-tostringtag": "^2.1.0", + "hasown": "^2.0.2", "mime-types": "^2.1.12" } }, @@ -1670,6 +1772,11 @@ "fetch-blob": "^3.1.2" } }, + "function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==" + }, "get-caller-file": { "version": "2.0.5", "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", @@ -1680,6 +1787,32 @@ "resolved": "https://registry.npmjs.org/get-east-asian-width/-/get-east-asian-width-1.3.0.tgz", "integrity": "sha512-vpeMIQKxczTD/0s2CdEWHcb0eeJe6TFjxb+J5xgX7hScxqrGuyjmv4c1D4A/gelKfyox0gJJwIHF+fLjeaM8kQ==" }, + "get-intrinsic": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", + "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", + "requires": { + "call-bind-apply-helpers": "^1.0.2", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", + "function-bind": "^1.1.2", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "math-intrinsics": "^1.1.0" + } + }, + "get-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", + "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", + "requires": { + "dunder-proto": "^1.0.1", + "es-object-atoms": "^1.0.0" + } + }, "get-stream": { "version": "5.2.0", "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-5.2.0.tgz", @@ -1688,77 +1821,30 @@ "pump": "^3.0.0" } }, - "has-flag": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", - "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==" + "gopd": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", + "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==" }, - "https": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/https/-/https-1.0.0.tgz", - "integrity": "sha512-4EC57ddXrkaF0x83Oj8sM6SLQHAWXw90Skqu2M4AEWENZ3F02dFJE/GARA8igO79tcgYqGrD7ae4f5L3um2lgg==" + "has-symbols": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", + "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==" }, - "iconv-lite": { - "version": "0.4.24", - "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", - "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", + "has-tostringtag": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz", + "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==", "requires": { - "safer-buffer": ">= 2.1.2 < 3" + "has-symbols": "^1.0.3" } }, - "inherits": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz", - "integrity": "sha512-x00IRNXNy63jwGkJmzPigoySHbaqpNuzKbBOmzK+g2OdZpQ9w+sxCN+VSB3ja7IAge2OP2qpfxTjeNcyjmW1uw==" - }, - "inquirer": { - "version": "7.3.3", - "resolved": "https://registry.npmjs.org/inquirer/-/inquirer-7.3.3.tgz", - "integrity": "sha512-JG3eIAj5V9CwcGvuOmoo6LB9kbAYT8HXffUl6memuszlwDC/qvFAJw49XJ5NROSFNPxp3iQg1GqkFhaY/CR0IA==", + "hasown": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", + "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", "requires": { - "ansi-escapes": "^4.2.1", - "chalk": "^4.1.0", - "cli-cursor": "^3.1.0", - "cli-width": "^3.0.0", - "external-editor": "^3.0.3", - "figures": "^3.0.0", - "lodash": "^4.17.19", - "mute-stream": "0.0.8", - "run-async": "^2.4.0", - "rxjs": "^6.6.0", - "string-width": "^4.1.0", - "strip-ansi": "^6.0.0", - "through": "^2.3.6" - }, - "dependencies": { - "ansi-regex": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", - "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==" - }, - "emoji-regex": { - "version": "8.0.0", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", - "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==" - }, - "string-width": { - "version": "4.2.3", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", - "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", - "requires": { - "emoji-regex": "^8.0.0", - "is-fullwidth-code-point": "^3.0.0", - "strip-ansi": "^6.0.1" - } - }, - "strip-ansi": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", - "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", - "requires": { - "ansi-regex": "^5.0.1" - } - } + "function-bind": "^1.1.2" } }, "is-fullwidth-code-point": { @@ -1766,20 +1852,6 @@ "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==" }, - "locate-path": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-3.0.0.tgz", - "integrity": "sha512-7AO748wWnIhNqAuaty2ZWHkQHRSNfPVIsPIfwEOWO22AmaoVrWavlOcMR5nzTLNYvp36X220/maaRsrec1G65A==", - "requires": { - "p-locate": "^3.0.0", - "path-exists": "^3.0.0" - } - }, - "lodash": { - "version": "4.17.21", - "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", - "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==" - }, "log-update": { "version": "6.1.0", "resolved": "https://registry.npmjs.org/log-update/-/log-update-6.1.0.tgz", @@ -1827,6 +1899,11 @@ } } }, + "math-intrinsics": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", + "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==" + }, "mime-db": { "version": "1.52.0", "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", @@ -1840,11 +1917,6 @@ "mime-db": "1.52.0" } }, - "mimic-fn": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-2.1.0.tgz", - "integrity": "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==" - }, "mimic-function": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/mimic-function/-/mimic-function-5.0.1.tgz", @@ -1855,11 +1927,6 @@ "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==" }, - "mute-stream": { - "version": "0.0.8", - "resolved": "https://registry.npmjs.org/mute-stream/-/mute-stream-0.0.8.tgz", - "integrity": "sha512-nnbWWOkoWyUsTjKrhgD0dcz22mdkSnpYqbEjIm2nhwhuxlSkpywJmBo8h0ZqJdkp73mb90SssHkN4rsRaBAfAA==" - }, "node-domexception": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/node-domexception/-/node-domexception-1.0.0.tgz", @@ -1883,69 +1950,11 @@ "wrappy": "1" } }, - "onetime": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/onetime/-/onetime-5.1.2.tgz", - "integrity": "sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==", - "requires": { - "mimic-fn": "^2.1.0" - } - }, - "os-tmpdir": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/os-tmpdir/-/os-tmpdir-1.0.2.tgz", - "integrity": "sha512-D2FR03Vir7FIu45XBY20mTb+/ZSWB00sjU9jdQXt83gDrI4Ztz5Fs7/yy74g2N5SVQY4xY1qDr4rNddwYRVX0g==" - }, - "p-limit": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", - "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", - "requires": { - "p-try": "^2.0.0" - } - }, - "p-locate": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-3.0.0.tgz", - "integrity": "sha512-x+12w/To+4GFfgJhBEpiDcLozRJGegY+Ei7/z0tSLkMmxGZNybVMSfWj9aJn8Z5Fc7dBUNJOOVgPv2H7IwulSQ==", - "requires": { - "p-limit": "^2.0.0" - } - }, - "p-try": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz", - "integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==" - }, - "path": { - "version": "0.12.7", - "resolved": "https://registry.npmjs.org/path/-/path-0.12.7.tgz", - "integrity": "sha512-aXXC6s+1w7otVF9UletFkFcDsJeO7lSZBPUQhtb5O0xJe8LtYhj/GxldoL09bBj9+ZmE2hNoHqQSFMN5fikh4Q==", - "requires": { - "process": "^0.11.1", - "util": "^0.10.3" - } - }, - "path-exists": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-3.0.0.tgz", - "integrity": "sha512-bpC7GYwiDYQ4wYLe+FA8lhRjhQCMcQGuSgGGqDkg/QerRWw9CmGRT0iSOVRSZJ29NMLZgIzqaljJ63oaL4NIJQ==" - }, "pend": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/pend/-/pend-1.2.0.tgz", "integrity": "sha512-F3asv42UuXchdzt+xXqfW1OGlVBe+mxa2mqI0pg5yAHZPvFmY3Y6drSf/GQ1A86WgWEN9Kzh/WrgKa6iGcHXLg==" }, - "process": { - "version": "0.11.10", - "resolved": "https://registry.npmjs.org/process/-/process-0.11.10.tgz", - "integrity": "sha512-cdGef/drWFoydD1JsMzuFf8100nZl+GT+yacc2bEced5f9Rjk4z+WtFUTBu9PhOi9j/jfmBPu0mMEY4wIdAF8A==" - }, - "psl": { - "version": "1.9.0", - "resolved": "https://registry.npmjs.org/psl/-/psl-1.9.0.tgz", - "integrity": "sha512-E/ZsdU4HLs/68gYzgGTkMicWTLPdAftJLfJFlLUAAKZGkStNU72sZjT66SnMDVOfOWY/YAoiD7Jxa9iHvngcag==" - }, "pump": { "version": "3.0.2", "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.2.tgz", @@ -1960,50 +1969,11 @@ "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==" }, - "require-main-filename": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/require-main-filename/-/require-main-filename-2.0.0.tgz", - "integrity": "sha512-NKN5kMDylKuldxYLSUfrbo5Tuzh4hd+2E8NPPX02mZtn1VuREQToYe/ZdlJy+J3uCpfaiGF05e7B8W0iXbQHmg==" - }, - "restore-cursor": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-3.1.0.tgz", - "integrity": "sha512-l+sSefzHpj5qimhFSE5a8nufZYAM3sBSVMAPtYkmC+4EH2anSGaEMXSD0izRQbu9nfyQ9y5JrVmp7E8oZrUjvA==", - "requires": { - "onetime": "^5.1.0", - "signal-exit": "^3.0.2" - }, - "dependencies": { - "signal-exit": { - "version": "3.0.7", - "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", - "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==" - } - } - }, - "run-async": { - "version": "2.4.1", - "resolved": "https://registry.npmjs.org/run-async/-/run-async-2.4.1.tgz", - "integrity": "sha512-tvVnVv01b8c1RrA6Ep7JkStj85Guv/YrMcwqYQnwjsAS2cTmmPGBBjAjpCW7RrSodNSoE2/qg9O4bceNvUuDgQ==" - }, - "rxjs": { - "version": "6.6.7", - "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-6.6.7.tgz", - "integrity": "sha512-hTdwr+7yYNIT5n4AMYp85KA6yw2Va0FLa3Rguvbpa4W3I5xynaBZo41cM3XM+4Q6fRMj3sBYIR1VAmZMXYJvRQ==", - "requires": { - "tslib": "^1.9.0" - } - }, "safer-buffer": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==" }, - "set-blocking": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/set-blocking/-/set-blocking-2.0.0.tgz", - "integrity": "sha512-KiKBS8AnWGEyLzofFfmvKwpdPzqiy16LvQfK3yv/fVH7Bj13/wl3JSR1J+rfgRE9q7xUJK4qvgS8raSOeLUehw==" - }, "signal-exit": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", @@ -2046,61 +2016,17 @@ "ansi-regex": "^6.0.1" } }, - "supports-color": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", - "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", - "requires": { - "has-flag": "^4.0.0" - } - }, - "through": { - "version": "2.3.8", - "resolved": "https://registry.npmjs.org/through/-/through-2.3.8.tgz", - "integrity": "sha512-w89qg7PI8wAdvX60bMDP+bFoD5Dvhm9oLheFp5O4a2QF0cSBGsBX4qZmadPMvVqlLJBBci+WqGGOAPvcDeNSVg==" - }, - "tmp": { - "version": "0.0.33", - "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.0.33.tgz", - "integrity": "sha512-jRCJlojKnZ3addtTOjdIqoRuPEKBvNXcGYqzO6zWZX8KfKEpnGY5jfggJQ3EjKuu8D4bJRr0y+cYJFmYbImXGw==", - "requires": { - "os-tmpdir": "~1.0.2" - } - }, - "tslib": { - "version": "1.14.1", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz", - "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==" - }, - "type-fest": { - "version": "0.21.3", - "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.21.3.tgz", - "integrity": "sha512-t0rzBq87m3fVcduHDUFhKmyyX+9eo6WQjZvf51Ea/M0Q7+T374Jp1aUiyUl0GKxp8M/OETVHSDvmkyPgvX+X2w==" - }, "undici-types": { "version": "6.19.8", "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.19.8.tgz", "integrity": "sha512-ve2KP6f/JnbPBFyobGHuerC9g1FYGn/F8n1LWTwNxCEzd6IfqTwUQcNXgEtmmQ6DlRrC1hrSrBnCZPokRrDHjw==", "optional": true }, - "util": { - "version": "0.10.4", - "resolved": "https://registry.npmjs.org/util/-/util-0.10.4.tgz", - "integrity": "sha512-0Pm9hTQ3se5ll1XihRic3FDIku70C+iHUdT/W926rSgHV5QgXsYbKZN8MSC3tJtSkhuROzvsQjAaFENRXr+19A==", - "requires": { - "inherits": "2.0.3" - } - }, "web-streams-polyfill": { "version": "3.3.3", "resolved": "https://registry.npmjs.org/web-streams-polyfill/-/web-streams-polyfill-3.3.3.tgz", "integrity": "sha512-d2JWLCivmZYTSIoge9MsgFCZrt571BikcWGYkjC1khllbTeDlGqZ2D8vD8E/lJa8WGWbb7Plm8/XJYV7IJHZZw==" }, - "which-module": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/which-module/-/which-module-2.0.1.tgz", - "integrity": "sha512-iBdZ57RDvnOR9AGBhML2vFZf7h8vmBjhoaZqODJBFWHVtKkDmKuHai3cx5PgVMrX5YDNp27AofYbAwctSS+vhQ==" - }, "wrap-ansi": { "version": "9.0.0", "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-9.0.0.tgz", @@ -2165,123 +2091,6 @@ } } }, - "yargs-interactive": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/yargs-interactive/-/yargs-interactive-3.0.1.tgz", - "integrity": "sha512-Jnp88uiuz+ZRpM10Lwvs0nRetWPog+6lcgQrhwKsyEanAe3wgTlaPPzcYlZWp53aOMTzOcR5wEpEsFOMOPmLlw==", - "requires": { - "inquirer": "^7.0.0", - "yargs": "^14.0.0" - }, - "dependencies": { - "ansi-regex": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-4.1.1.tgz", - "integrity": "sha512-ILlv4k/3f6vfQ4OoP2AGvirOktlQ98ZEL1k9FaQjxa3L1abBgbuTDAdPOpvbGncC0BTVQrl+OM8xZGK6tWXt7g==" - }, - "ansi-styles": { - "version": "3.2.1", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", - "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", - "requires": { - "color-convert": "^1.9.0" - } - }, - "cliui": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/cliui/-/cliui-5.0.0.tgz", - "integrity": "sha512-PYeGSEmmHM6zvoef2w8TPzlrnNpXIjTipYK780YswmIP9vjxmd6Y2a3CB2Ks6/AU8NHjZugXvo8w3oWM2qnwXA==", - "requires": { - "string-width": "^3.1.0", - "strip-ansi": "^5.2.0", - "wrap-ansi": "^5.1.0" - } - }, - "color-convert": { - "version": "1.9.3", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", - "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", - "requires": { - "color-name": "1.1.3" - } - }, - "color-name": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", - "integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==" - }, - "emoji-regex": { - "version": "7.0.3", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-7.0.3.tgz", - "integrity": "sha512-CwBLREIQ7LvYFB0WyRvwhq5N5qPhc6PMjD6bYggFlI5YyDgl+0vxq5VHbMOFqLg7hfWzmu8T5Z1QofhmTIhItA==" - }, - "is-fullwidth-code-point": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-2.0.0.tgz", - "integrity": "sha512-VHskAKYM8RfSFXwee5t5cbN5PZeq1Wrh6qd5bkyiXIf6UQcN6w/A0eXM9r6t8d+GYOh+o6ZhiEnb88LN/Y8m2w==" - }, - "string-width": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-3.1.0.tgz", - "integrity": "sha512-vafcv6KjVZKSgz06oM/H6GDBrAtz8vdhQakGjFIvNrHA6y3HCF1CInLy+QLq8dTJPQ1b+KDUqDFctkdRW44e1w==", - "requires": { - "emoji-regex": "^7.0.1", - "is-fullwidth-code-point": "^2.0.0", - "strip-ansi": "^5.1.0" - } - }, - "strip-ansi": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-5.2.0.tgz", - "integrity": "sha512-DuRs1gKbBqsMKIZlrffwlug8MHkcnpjs5VPmL1PAh+mA30U0DTotfDZ0d2UUsXpPmPmMMJ6W773MaA3J+lbiWA==", - "requires": { - "ansi-regex": "^4.1.0" - } - }, - "wrap-ansi": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-5.1.0.tgz", - "integrity": "sha512-QC1/iN/2/RPVJ5jYK8BGttj5z83LmSKmvbvrXPNCLZSEb32KKVDJDl/MOt2N01qU2H/FkzEa9PKto1BqDjtd7Q==", - "requires": { - "ansi-styles": "^3.2.0", - "string-width": "^3.0.0", - "strip-ansi": "^5.0.0" - } - }, - "y18n": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/y18n/-/y18n-4.0.3.tgz", - "integrity": "sha512-JKhqTOwSrqNA1NY5lSztJ1GrBiUodLMmIZuLiDaMRJ+itFd+ABVE8XBjOvIWL+rSqNDC74LCSFmlb/U4UZ4hJQ==" - }, - "yargs": { - "version": "14.2.3", - "resolved": "https://registry.npmjs.org/yargs/-/yargs-14.2.3.tgz", - "integrity": "sha512-ZbotRWhF+lkjijC/VhmOT9wSgyBQ7+zr13+YLkhfsSiTriYsMzkTUFP18pFhWwBeMa5gUc1MzbhrO6/VB7c9Xg==", - "requires": { - "cliui": "^5.0.0", - "decamelize": "^1.2.0", - "find-up": "^3.0.0", - "get-caller-file": "^2.0.1", - "require-directory": "^2.1.1", - "require-main-filename": "^2.0.0", - "set-blocking": "^2.0.0", - "string-width": "^3.0.0", - "which-module": "^2.0.0", - "y18n": "^4.0.0", - "yargs-parser": "^15.0.1" - } - }, - "yargs-parser": { - "version": "15.0.3", - "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-15.0.3.tgz", - "integrity": "sha512-/MVEVjTXy/cGAjdtQf8dW3V9b97bPN7rNn8ETj6BmAQL7ibC7O1Q9SPJbGjgh3SlwoBNXMzj/ZGIj8mBgl12YA==", - "requires": { - "camelcase": "^5.0.0", - "decamelize": "^1.2.0" - } - } - } - }, "yargs-parser": { "version": "21.1.1", "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz", diff --git a/package.json b/package.json index 937f344..268f9d9 100644 --- a/package.json +++ b/package.json @@ -26,17 +26,16 @@ "bugs": { "url": "https://github.com/dynamicweb/CLI/issues" }, + "engines": { + "node": ">=20.12.0" + }, "dependencies": { - "child_process": "^1.0.2", + "@inquirer/prompts": "^8.4.1", "degit": "^2.8.4", "extract-zip": "^2.0.1", - "fetch": "^1.1.0", "form-data": "^4.0.0", - "https": "^1.0.0", "log-update": "^6.1.0", "node-fetch": "^3.2.10", - "path": "^0.12.7", - "yargs": "^17.5.1", - "yargs-interactive": "^3.0.1" + "yargs": "^17.5.1" } -} +} \ No newline at end of file From 7796637850820b32f7c5a9c6bd27bc730c242d72 Mon Sep 17 00:00:00 2001 From: nicped Date: Mon, 13 Apr 2026 14:08:03 +0200 Subject: [PATCH 60/86] Fix multiple bugs across the CLI codebase Critical: - login.js: Complete migration from yargs-interactive to @inquirer/prompts, matching the migration done for env/query/utils in the previous commit. Without this, `dw login` would crash on missing dependency. - config.js: Fix `handleConfig` referencing undefined `config` variable instead of `localConfig`, which crashed `dw config` on every invocation. High: - database.js: Rewrite detached `.then()` promise chain to async/await so `dw database -e` actually waits for the download to complete before exiting. Medium: - command.js, query.js: Add global flags (verbose, host, protocol, apiKey, env) to exclude lists so they are not sent as API query parameters. - install.js: Resolve wildcard file path before upload, not after, so both upload and install use the same resolved path. - downloader.js: Use replaceAll instead of replace for '+' to ' ' in filenames from Content-Disposition headers. - files.js: Fix wildcardToRegExp to convert * to .* instead of escaping it, so wildcard matching actually works. Low: - Add async/await to all yargs command handlers so errors propagate instead of becoming silent unhandled rejections. - login.js: Use optional chaining on env.current?.user to prevent crash when an environment exists but no user has logged in yet. - files.js: Add missing await on res.json() in error path so the actual error body is logged instead of Promise { }. - swift.js: Replace exec with execFile to prevent potential command injection via argv.tag or argv.outPath shell metacharacters. --- bin/commands/command.js | 6 ++-- bin/commands/config.js | 2 +- bin/commands/database.js | 48 +++++++++++++----------------- bin/commands/env.js | 4 +++ bin/commands/files.js | 9 +++--- bin/commands/install.js | 9 +++--- bin/commands/login.js | 64 +++++++++++++++++++++++----------------- bin/commands/query.js | 7 +++-- bin/commands/swift.js | 22 +++++++------- bin/downloader.js | 2 +- 10 files changed, 93 insertions(+), 80 deletions(-) diff --git a/bin/commands/command.js b/bin/commands/command.js index cb6507c..75fbfaf 100644 --- a/bin/commands/command.js +++ b/bin/commands/command.js @@ -4,7 +4,7 @@ import fs from 'fs'; import { setupEnv, getAgent } from './env.js'; import { setupUser } from './login.js'; -const exclude = ['_', '$0', 'command', 'list', 'json'] +const exclude = ['_', '$0', 'command', 'list', 'json', 'verbose', 'v', 'host', 'protocol', 'apiKey', 'env'] export function commandCommand() { return { @@ -23,9 +23,9 @@ export function commandCommand() { describe: 'Lists all the properties for the command, currently not working' }) }, - handler: (argv) => { + handler: async (argv) => { if (argv.verbose) console.info(`Running command ${argv.command}`) - handleCommand(argv) + await handleCommand(argv) } } } diff --git a/bin/commands/config.js b/bin/commands/config.js index 0c9ad69..89afed0 100644 --- a/bin/commands/config.js +++ b/bin/commands/config.js @@ -33,7 +33,7 @@ export function getConfig() { export function handleConfig(argv) { Object.keys(argv).forEach(a => { if (a != '_' && a != '$0') { - resolveConfig(a, argv[a], config[a]); + localConfig[a] = resolveConfig(a, argv[a], localConfig[a] || {}); updateConfig(); } }) diff --git a/bin/commands/database.js b/bin/commands/database.js index 4ae4357..adab4d3 100644 --- a/bin/commands/database.js +++ b/bin/commands/database.js @@ -20,9 +20,9 @@ export function databaseCommand() { description: 'Exports the solutions database to a .bacpac file at [path]' }) }, - handler: (argv) => { + handler: async (argv) => { if (argv.verbose) console.info(`Handling database with path: ${argv.path}`) - handleDatabase(argv) + await handleDatabase(argv) } } } @@ -37,36 +37,30 @@ async function handleDatabase(argv) { } async function download(env, user, path, verbose) { - let filename = 'database.bacpac'; - fetch(`${env.protocol}://${env.host}/Admin/Api/DatabaseDownload`, { + const res = await fetch(`${env.protocol}://${env.host}/Admin/Api/DatabaseDownload`, { method: 'POST', headers: { 'Authorization': `Bearer ${user.apiKey}`, 'content-type': 'application/json' }, agent: getAgent(env.protocol) - }).then(async (res) => { - if (verbose) console.log(res) - const header = res.headers.get('Content-Disposition'); - const parts = header?.split(';'); - if (!parts || !header.includes('attachment')) { - console.log('Failed download, check users database permissions') - if (verbose) console.log(await res.json()) - return; - } - filename = parts[1].split('=')[1]; - return res; - }).then(async (res) => { - if (!res) { - process.exit(1); - } - const fileStream = fs.createWriteStream(_path.resolve(`${_path.resolve(path)}/${filename}`)); - await new Promise((resolve, reject) => { - res.body.pipe(fileStream); - res.body.on("error", reject); - fileStream.on("finish", resolve); - }); - console.log(`Finished downloading`); - return res; }); + + if (verbose) console.log(res) + const header = res.headers.get('Content-Disposition'); + const parts = header?.split(';'); + if (!parts || !header.includes('attachment')) { + console.log('Failed download, check users database permissions') + if (verbose) console.log(await res.json()) + process.exit(1); + } + + const filename = parts[1].split('=')[1]; + const fileStream = fs.createWriteStream(_path.resolve(`${_path.resolve(path)}/${filename}`)); + await new Promise((resolve, reject) => { + res.body.pipe(fileStream); + res.body.on("error", reject); + fileStream.on("finish", resolve); + }); + console.log(`Finished downloading`); } \ No newline at end of file diff --git a/bin/commands/env.js b/bin/commands/env.js index 389a9a0..404936f 100644 --- a/bin/commands/env.js +++ b/bin/commands/env.js @@ -114,6 +114,10 @@ export async function interactiveEnv(argv, options) { }); } getConfig().env = getConfig().env || {}; + if (!result.environment || !result.environment.trim()) { + console.log('Environment name cannot be empty'); + return; + } getConfig().env[result.environment] = getConfig().env[result.environment] || {}; if (result.host) { const hostSplit = result.host.split("://"); diff --git a/bin/commands/files.js b/bin/commands/files.js index f3bffa0..4f54753 100644 --- a/bin/commands/files.js +++ b/bin/commands/files.js @@ -76,9 +76,9 @@ export function filesCommand() { conflicts: 'asFile' }) }, - handler: (argv) => { + handler: async (argv) => { if (argv.verbose) console.info(`Listing directory at: ${argv.dirPath}`) - handleFiles(argv) + await handleFiles(argv) } } } @@ -301,7 +301,7 @@ async function getFilesStructure(env, user, dirPath, recursive, includeFiles) { return await res.json(); } else { console.log(res); - console.log(res.json()); + console.log(await res.json()); process.exit(1); } } @@ -372,6 +372,7 @@ export function resolveFilePath(filePath) { function wildcardToRegExp(wildcard) { - return new RegExp('^' + wildcard.replace(/[.*+?^${}()|[\]\\]/g, '\\$&') + '$'); + const escaped = wildcard.replace(/[.+?^${}()|[\]\\]/g, '\\$&'); + return new RegExp('^' + escaped.replace(/\*/g, '.*') + '$'); } diff --git a/bin/commands/install.js b/bin/commands/install.js index 836e707..61aaa2d 100644 --- a/bin/commands/install.js +++ b/bin/commands/install.js @@ -19,9 +19,9 @@ export function installCommand() { describe: 'Queues the install for next Dynamicweb recycle' }) }, - handler: (argv) => { + handler: async (argv) => { if (argv.verbose) console.info(`Installing file located at :${argv.filePath}`) - handleInstall(argv) + await handleInstall(argv) } } } @@ -29,8 +29,9 @@ export function installCommand() { async function handleInstall(argv) { let env = await setupEnv(argv); let user = await setupUser(argv, env); - await uploadFiles(env, user, [ argv.filePath ], 'System/AddIns/Local', false, true); - await installAddin(env, user, resolveFilePath(argv.filePath), argv.queue) + let resolvedPath = resolveFilePath(argv.filePath); + await uploadFiles(env, user, [ resolvedPath ], 'System/AddIns/Local', false, true); + await installAddin(env, user, resolvedPath, argv.queue) } async function installAddin(env, user, resolvedPath, queue) { diff --git a/bin/commands/login.js b/bin/commands/login.js index af692b2..20874ba 100644 --- a/bin/commands/login.js +++ b/bin/commands/login.js @@ -1,7 +1,7 @@ import fetch from 'node-fetch'; import { interactiveEnv, getAgent } from './env.js' import { updateConfig, getConfig } from './config.js'; -import yargsInteractive from 'yargs-interactive'; +import { input, password } from '@inquirer/prompts'; export function loginCommand() { return { @@ -26,8 +26,8 @@ export async function setupUser(argv, env) { askLogin = false; } - if (!user.apiKey && env.users) { - user = env.users[argv.user] || env.users[env.current.user]; + if (!user.apiKey && env.users && (argv.user || env.current?.user)) { + user = env.users[argv.user] || env.users[env.current?.user]; askLogin = false; } @@ -60,7 +60,7 @@ export async function setupUser(argv, env) { } async function handleLogin(argv) { - argv.user ? changeUser(argv) : interactiveLogin(argv, { + argv.user ? changeUser(argv) : await interactiveLogin(argv, { environment: { type: 'input', default: getConfig()?.current?.env || 'dev', @@ -80,30 +80,40 @@ async function handleLogin(argv) { export async function interactiveLogin(argv, options) { if (argv.verbose) console.info('Now logging in') - await yargsInteractive() - .interactive(options) - .then(async (result) => { - if (!getConfig().env || !getConfig().env[result.environment] || !getConfig().env[result.environment].host || !getConfig().env[result.environment].protocol) { - if (!argv.host) - console.log(`The environment specified is missing parameters, please specify them`) - await interactiveEnv(argv, { - environment: { - type: 'input', - default: result.environment, - prompt: 'never' - }, - host: { - describe: 'Enter your host including protocol, i.e "https://yourHost.com":', - type: 'input', - prompt: 'if-no-arg' - }, - interactive: { - default: true - } - }) - } - await loginInteractive(result, argv.verbose); + const result = {}; + for (const [key, config] of Object.entries(options)) { + if (key === 'interactive') continue; + if (config.prompt === 'never') { + result[key] = config.default; + continue; + } + const promptFn = config.type === 'password' ? password : input; + result[key] = await promptFn({ + message: config.describe || key, + default: config.default, + mask: config.type === 'password' ? '*' : undefined }); + } + if (!getConfig().env || !getConfig().env[result.environment] || !getConfig().env[result.environment].host || !getConfig().env[result.environment].protocol) { + if (!argv.host) + console.log(`The environment specified is missing parameters, please specify them`) + await interactiveEnv(argv, { + environment: { + type: 'input', + default: result.environment, + prompt: 'never' + }, + host: { + describe: 'Enter your host including protocol, i.e "https://yourHost.com":', + type: 'input', + prompt: 'always' + }, + interactive: { + default: true + } + }) + } + await loginInteractive(result, argv.verbose); } async function loginInteractive(result, verbose) { diff --git a/bin/commands/query.js b/bin/commands/query.js index 2f720d2..baa93b0 100644 --- a/bin/commands/query.js +++ b/bin/commands/query.js @@ -3,7 +3,7 @@ import { setupEnv, getAgent } from './env.js'; import { setupUser } from './login.js'; import { input } from '@inquirer/prompts'; -const exclude = ['_', '$0', 'query', 'list', 'i', 'l', 'interactive'] +const exclude = ['_', '$0', 'query', 'list', 'i', 'l', 'interactive', 'verbose', 'v', 'host', 'protocol', 'apiKey', 'env'] export function queryCommand() { return { @@ -73,7 +73,10 @@ async function getQueryParams(argv) { console.log(properties) for (const p of properties) { const value = await input({ message: p }); - if (value) params[p] = value; + if (value) { + const fieldName = p.split(' (')[0]; + params[fieldName] = value; + } } } else { Object.keys(argv).filter(k => !exclude.includes(k)).forEach(k => params[k] = argv[k]) diff --git a/bin/commands/swift.js b/bin/commands/swift.js index de3e85b..42408cf 100644 --- a/bin/commands/swift.js +++ b/bin/commands/swift.js @@ -1,4 +1,4 @@ -import { exec } from 'child_process'; +import { execFile } from 'child_process'; import { Agent } from 'https'; import path from 'path'; import fetch from 'node-fetch'; @@ -31,9 +31,9 @@ export function swiftCommand() { }) .option('force', {}) }, - handler: (argv) => { + handler: async (argv) => { if (argv.verbose) console.info(`Downloading latest swift to :${argv.outPath}`) - handleSwift(argv) + await handleSwift(argv) } } } @@ -42,14 +42,14 @@ async function handleSwift(argv) { if (argv.list) { console.log(await getVersions(false)) } else { - let degitCommand - if (argv.nightly) { - degitCommand = `npx degit dynamicweb/swift ${argv.force ? '--force' : ''} "${path.resolve(argv.outPath)}"` - } else { - degitCommand = `npx degit dynamicweb/swift#${argv.tag ? argv.tag : await getVersions(true)} ${argv.force ? '--force' : ''} "${path.resolve(argv.outPath)}"` - } - if (argv.verbose) console.info(`Executing command: ${degitCommand}`) - exec(degitCommand, (error, stdout, stderr) => { + const repo = argv.nightly + ? 'dynamicweb/swift' + : `dynamicweb/swift#${argv.tag ? argv.tag : await getVersions(true)}`; + const args = ['degit', repo]; + if (argv.force) args.push('--force'); + args.push(path.resolve(argv.outPath)); + if (argv.verbose) console.info(`Executing: npx ${args.join(' ')}`) + execFile('npx', args, (error, stdout, stderr) => { if (error) { console.log(`error: ${error.message}`); return; diff --git a/bin/downloader.js b/bin/downloader.js index 86df65b..b87337d 100644 --- a/bin/downloader.js +++ b/bin/downloader.js @@ -15,7 +15,7 @@ export function getFileNameFromResponse(res, dirPath) { throw new Error(msg); } - return parts[1].split('=')[1].replace('+', ' '); + return parts[1].split('=')[1].replaceAll('+', ' '); } /** From 5a0b98a519551c37030cdc54199fccb30a62a63b Mon Sep 17 00:00:00 2001 From: nicped Date: Mon, 13 Apr 2026 14:12:47 +0200 Subject: [PATCH 61/86] Bump version to 1.1.0 --- package-lock.json | 4 ++-- package.json | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/package-lock.json b/package-lock.json index c43e5f4..aa1e289 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@dynamicweb/cli", - "version": "1.0.16", + "version": "1.1.0", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "@dynamicweb/cli", - "version": "1.0.16", + "version": "1.1.0", "license": "MIT", "dependencies": { "@inquirer/prompts": "^8.4.1", diff --git a/package.json b/package.json index 268f9d9..252c590 100644 --- a/package.json +++ b/package.json @@ -2,7 +2,7 @@ "name": "@dynamicweb/cli", "type": "module", "description": "Dynamicweb CLI is a commandline tool for interacting with Dynamicweb 10 solutions.", - "version": "1.0.16", + "version": "1.1.0", "main": "bin/index.js", "files": [ "bin/*" From 57b753c3f6c1e76dd24ab08573e2c924a4131fa3 Mon Sep 17 00:00:00 2001 From: nicped Date: Mon, 13 Apr 2026 14:31:05 +0200 Subject: [PATCH 62/86] Prepare npm package for publish --- README.md | 29 ++++++++++++++++++++++------- package.json | 18 ++++++++++++++---- 2 files changed, 36 insertions(+), 11 deletions(-) diff --git a/README.md b/README.md index e11a11c..873e31b 100644 --- a/README.md +++ b/README.md @@ -11,14 +11,29 @@ The DynamicWeb CLI can also help with active development of custom addins to sol 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. ## Get started -To install after cloning, move to project dir and run -> $ npm install -g . -> +### Install from npm +Install the published CLI globally: +> $ npm install -g @dynamicweb/cli + +Then verify the command is available: +> $ dw --help + +### Install from source +If you're working on the CLI locally, clone the repository, move into the project directory, and run: > $ npm install +> +> $ npm install -g . + +This installs dependencies for development and links the current checkout as the global `dw` command. + +### Publish a new version +Before publishing, make sure the version in `package.json` has been bumped: +> $ npm version patch -Note that specific installations might be necessary if you're faced with errors such as 'Error [ERR_MODULE_NOT_FOUND]: Cannot find package 'yargs'' -In which case try installing that module specifically; -> $ npm install yargs +Log in to npm and publish the scoped package publicly: +> $ npm login +> +> $ npm publish --access public ## Commands All commands and options can be viewed by running @@ -226,4 +241,4 @@ If you prefer not to disable path conversion globally, you can: #### Examples -> $ dw files -iro ./ ./TestFolder --host \ --apiKey \ \ No newline at end of file +> $ dw files -iro ./ ./TestFolder --host \ --apiKey \ diff --git a/package.json b/package.json index 252c590..2ea2d73 100644 --- a/package.json +++ b/package.json @@ -1,13 +1,19 @@ { "name": "@dynamicweb/cli", "type": "module", - "description": "Dynamicweb CLI is a commandline tool for interacting with Dynamicweb 10 solutions.", + "description": "CLI for interacting with Dynamicweb 10 solutions from the command line.", "version": "1.1.0", "main": "bin/index.js", "files": [ - "bin/*" + "bin/**" + ], + "keywords": [ + "dynamicweb", + "cli", + "dynamicweb10", + "cms", + "devops" ], - "keywords": [], "scripts": { "test": "echo \"Error: no test specified\" && exit 1" }, @@ -16,6 +22,7 @@ "type": "git", "url": "git+https://github.com/dynamicweb/CLI.git" }, + "homepage": "https://github.com/dynamicweb/CLI#readme", "contributors": [ "Dynamicweb A/S" ], @@ -26,6 +33,9 @@ "bugs": { "url": "https://github.com/dynamicweb/CLI/issues" }, + "publishConfig": { + "access": "public" + }, "engines": { "node": ">=20.12.0" }, @@ -38,4 +48,4 @@ "node-fetch": "^3.2.10", "yargs": "^17.5.1" } -} \ No newline at end of file +} From eb1cf1d8a1c462fa7cf8568e8ebafd957b3829f9 Mon Sep 17 00:00:00 2001 From: nicped Date: Mon, 13 Apr 2026 15:05:05 +0200 Subject: [PATCH 63/86] Enhance download functionality with verbose logging and parameter updates --- bin/commands/files.js | 13 ++++++------- bin/downloader.js | 13 ++++++++----- 2 files changed, 14 insertions(+), 12 deletions(-) diff --git a/bin/commands/files.js b/bin/commands/files.js index b3249be..f8f4445 100644 --- a/bin/commands/files.js +++ b/bin/commands/files.js @@ -106,9 +106,9 @@ async function handleFiles(argv) { let parentDirectory = path.dirname(argv.dirPath); parentDirectory = parentDirectory === '.' ? '/' : parentDirectory; - await download(env, user, parentDirectory, argv.outPath, false, null, true, argv.iamstupid, [argv.dirPath], true); + await download(env, user, parentDirectory, argv.outPath, false, null, true, argv.iamstupid, [argv.dirPath], true, argv.verbose); } else { - await download(env, user, argv.dirPath, argv.outPath, true, null, argv.raw, argv.iamstupid, [], false); + await download(env, user, argv.dirPath, argv.outPath, true, null, argv.raw, argv.iamstupid, [], false, argv.verbose); } } else { await interactiveConfirm('Are you sure you want a full export of files?', async () => { @@ -117,9 +117,9 @@ async function handleFiles(argv) { let dirs = filesStructure.directories; for (let id = 0; id < dirs.length; id++) { const dir = dirs[id]; - await download(env, user, dir.name, argv.outPath, true, null, argv.raw, argv.iamstupid, [], false); + await download(env, user, dir.name, argv.outPath, true, null, argv.raw, argv.iamstupid, [], false, argv.verbose); } - await download(env, user, '/.', argv.outPath, false, 'Base.zip', argv.raw, argv.iamstupid, Array.from(filesStructure.files.data, f => f.name), false); + await download(env, user, '/.', argv.outPath, false, 'Base.zip', argv.raw, argv.iamstupid, Array.from(filesStructure.files.data, f => f.name), false, argv.verbose); 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') }) } @@ -189,7 +189,7 @@ function resolveTree(dirs, indentLevel, parentHasFiles) { } } -async function download(env, user, dirPath, outPath, recursive, outname, raw, iamstupid, fileNames, singleFileMode) { +async function download(env, user, dirPath, outPath, recursive, outname, raw, iamstupid, fileNames, singleFileMode, verbose = false) { let excludeDirectories = ''; if (!iamstupid) { excludeDirectories = 'system/log'; @@ -212,7 +212,7 @@ async function download(env, user, dirPath, outPath, recursive, outname, raw, ia agent: getAgent(env.protocol) }); - const filename = outname || tryGetFileNameFromResponse(res, dirPath); + const filename = outname || tryGetFileNameFromResponse(res, dirPath, verbose); if (!filename) return; const filePath = path.resolve(`${path.resolve(outPath)}/${filename}`) @@ -375,4 +375,3 @@ function wildcardToRegExp(wildcard) { const escaped = wildcard.replace(/[.+?^${}()|[\]\\]/g, '\\$&'); return new RegExp('^' + escaped.replace(/\*/g, '.*') + '$'); } - diff --git a/bin/downloader.js b/bin/downloader.js index b87337d..0bf8664 100644 --- a/bin/downloader.js +++ b/bin/downloader.js @@ -9,7 +9,7 @@ import fs from 'fs'; export function getFileNameFromResponse(res, dirPath) { const header = res.headers.get('content-disposition'); const parts = header?.split(';'); - + if (!parts) { const msg = `No files found in directory '${dirPath}', if you want to download all folders recursively include the -r flag`; throw new Error(msg); @@ -24,13 +24,16 @@ export function getFileNameFromResponse(res, dirPath) { * * @param {Object} res - The HTTP response object to extract the file name from. * @param {string} dirPath - The directory path to use for file name resolution. + * @param {boolean} verbose - Whether to log missing download information. * @returns {string|null} The extracted file name, or null if extraction fails. */ -export function tryGetFileNameFromResponse(res, dirPath) { +export function tryGetFileNameFromResponse(res, dirPath, verbose = false) { try { return getFileNameFromResponse(res, dirPath); } catch (err) { - console.error(err.message); + if (verbose) { + console.log(err.message); + } return null; } } @@ -56,7 +59,7 @@ export function downloadWithProgress(res, filePath, options) { res.body.on("data", chunk => { const isFirstChunk = receivedBytes === 0; const elapsed = Date.now() - startTime; - + receivedBytes += chunk.length; if (options?.onData) { @@ -64,4 +67,4 @@ export function downloadWithProgress(res, filePath, options) { } }); }); -} \ No newline at end of file +} From 4866e98ae79d703401a7e85e3d9ae3ef7caa23a1 Mon Sep 17 00:00:00 2001 From: nicped Date: Mon, 13 Apr 2026 15:09:26 +0200 Subject: [PATCH 64/86] Bump --- package.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/package.json b/package.json index 2ea2d73..31cef17 100644 --- a/package.json +++ b/package.json @@ -2,7 +2,7 @@ "name": "@dynamicweb/cli", "type": "module", "description": "CLI for interacting with Dynamicweb 10 solutions from the command line.", - "version": "1.1.0", + "version": "1.1.1", "main": "bin/index.js", "files": [ "bin/**" @@ -48,4 +48,4 @@ "node-fetch": "^3.2.10", "yargs": "^17.5.1" } -} +} \ No newline at end of file From dbeed54dbf4407d59bdb159a1c6cb3d50aed006b Mon Sep 17 00:00:00 2001 From: nicped Date: Mon, 13 Apr 2026 16:48:33 +0200 Subject: [PATCH 65/86] Add CLI v2 documentation article Complete rewrite of the CLI documentation for v2, structured around the automation-first identity: OAuth client credentials, JSON output, CI/CD patterns, and the updated config shape. --- cliv2-documentation.md | 686 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 686 insertions(+) create mode 100644 cliv2-documentation.md diff --git a/cliv2-documentation.md b/cliv2-documentation.md new file mode 100644 index 0000000..424c828 --- /dev/null +++ b/cliv2-documentation.md @@ -0,0 +1,686 @@ +--- +title: DynamicWeb CLI +_description: Automate and manage DynamicWeb 10 solutions from the command line. +uid: cli +--- + +# DynamicWeb CLI + +The DynamicWeb CLI is a command-line tool for automating and managing DynamicWeb 10 solutions. It connects to the [Management API](xref:dw10-webapis#management-api) to run queries and commands, move files in and out of a solution, install add-ins, export databases, and pull Swift releases. + +The CLI is designed to be composable. Every API-driven command supports structured JSON output, and authentication can be fully configured through environment variables and flags -- no interactive prompts required. This makes the CLI equally useful for one-off tasks at a developer's terminal and for scripted steps in a CI/CD pipeline. + +If you need to do something once, interactively, the DynamicWeb backend UI is usually faster. If you need to do it repeatedly, across environments, or as part of a build -- that is what the CLI is for. + +## Installation + +The CLI requires **Node.js 20.12.0 or later**. + +Install from npm: + +```sh +npm install -g @dynamicweb/cli +dw --help +``` + +To install from source (for contributors): + +```sh +git clone https://github.com/dynamicweb/CLI.git +cd CLI +npm install +npm install -g . +``` + +## Authentication + +The CLI supports two authentication modes. Which one you use depends on whether a human is present. + +| Mode | Mechanism | Best for | +|------|-----------|----------| +| **User login** | Interactive prompt, API key stored in `~/.dwc` | Local development, exploration | +| **OAuth client credentials** | Client ID + secret via environment variables | CI/CD, service accounts, headless automation | + +Both modes can be overridden on any command with `--apiKey` for direct, environmentless execution using a [manually generated API key](xref:settings-apikeys). + +### Interactive user login + +The default login flow creates a DynamicWeb API key for a backend user and stores it in `~/.dwc`. + +```sh +dw env dev # create or switch to an environment +dw login # interactive prompt for username and password +dw login DemoUser # switch to a previously saved user +``` + +The resulting `~/.dwc` config looks like this: + +```json +{ + "env": { + "dev": { + "host": "localhost:6001", + "protocol": "https", + "users": { + "DemoUser": { + "apiKey": "." + } + }, + "current": { + "user": "DemoUser", + "authType": "user" + } + } + }, + "current": { + "env": "dev" + } +} +``` + +> [!WARNING] +> The interactive login prompt is not verified to work against all DynamicWeb authentication setups. +> +> If `dw login` does not work in your environment, [generate an API key manually](xref:settings-apikeys) and use `--apiKey ` with `--host` and `--protocol` instead. + +### OAuth client credentials + +For service accounts, automation, and any scenario where no human is available to enter a password, the CLI supports [OAuth 2.0 client credentials](xref:oauth). This section covers how to use OAuth with the CLI -- see the linked article for how to set up an OAuth client in DynamicWeb. + +**Configure a saved environment for OAuth:** + +```sh +export DW_CLIENT_ID=my-client-id +export DW_CLIENT_SECRET=my-client-secret + +dw login --oauth +``` + +This stores the environment variable names (not the secrets themselves) in `~/.dwc`: + +```json +{ + "env": { + "dev": { + "host": "localhost:6001", + "protocol": "https", + "auth": { + "type": "oauth_client_credentials", + "clientIdEnv": "DW_CLIENT_ID", + "clientSecretEnv": "DW_CLIENT_SECRET" + }, + "current": { + "authType": "oauth_client_credentials" + } + } + }, + "current": { + "env": "dev" + } +} +``` + +**Run a one-off command without saved config:** + +```sh +dw query HealthCheck \ + --host your-solution.example.com \ + --auth oauth \ + --clientIdEnv DW_CLIENT_ID \ + --clientSecretEnv DW_CLIENT_SECRET +``` + +You can also pass credentials directly with `--clientId` and `--clientSecret`, but environment variable references (`--clientIdEnv` / `--clientSecretEnv`) are preferred because they keep secrets out of shell history and process lists. + +### Authentication precedence + +When multiple auth indicators are present, the CLI resolves them in this order: + +1. `--apiKey` -- used directly, no environment required +2. OAuth -- if `--auth oauth`, `--clientId`/`--clientSecret`, `--clientIdEnv`/`--clientSecretEnv`, or the environment is configured for OAuth +3. Saved user -- from `~/.dwc` +4. Interactive prompt -- if nothing else is configured + +Use `--auth user` to force user authentication even when an environment is configured for OAuth. + +## Automation and JSON output + +Commands that talk to the Management API -- `env`, `login`, `files`, `query`, and `command` -- support `--output json`. This returns a structured envelope instead of human-readable console output: + +```json +{ + "ok": true, + "command": "query", + "operation": "run", + "status": 200, + "data": [ + { + "name": "DefaultMail.html", + "path": "/Templates/Forms/Mail/DefaultMail.html" + } + ], + "errors": [], + "meta": { + "query": "FileByName" + } +} +``` + +| Field | Type | Description | +|-------|------|-------------| +| `ok` | `boolean` | `true` if the operation succeeded | +| `command` | `string` | Which CLI command ran | +| `operation` | `string` | The specific operation within that command | +| `status` | `number` | HTTP-style status code | +| `data` | `array` | The result payload | +| `errors` | `array` | Error objects with `message` and `details` if the operation failed | +| `meta` | `object` | Command-specific metadata (query name, file counts, etc.) | + +When `--output json` is active, all human-readable output to stdout is suppressed. Only the JSON envelope is written to stdout, which makes it safe to pipe. + +### Parsing output in scripts + +Extract data with `jq` or any JSON parser: + +```sh +# Check if a query succeeded +dw query HealthCheck --output json | jq '.ok' + +# Get the list of configured environments +dw env --list --output json | jq -r '.data[0].environments[]' + +# Count files processed during an import +dw files ./Files templates -i -r --output json | jq '.meta.filesProcessed' +``` + +### Error handling in JSON mode + +When a command fails, `ok` is `false` and the `errors` array contains structured error information: + +```json +{ + "ok": false, + "command": "query", + "operation": "run", + "status": 404, + "data": [], + "errors": [ + { + "message": "Query not found", + "details": null + } + ], + "meta": {} +} +``` + +The CLI exits with code `1` on any error, so you can use standard shell exit-code checks alongside JSON parsing. + +## CI/CD + +The recommended CI/CD setup combines OAuth client credentials, `--output json`, and `--host` overrides. This section gives you the patterns for both ephemeral and persistent runners. + +### Secrets + +Store your OAuth credentials in your pipeline's secret store: + +- `DW_CLIENT_ID` -- the OAuth client ID +- `DW_CLIENT_SECRET` -- the OAuth client secret + +These should never be hardcoded in scripts or committed to source control. + +### Ephemeral runners (no saved config) + +On runners that start fresh each time (most cloud CI), pass everything inline. No `~/.dwc` file needed: + +```sh +#!/bin/sh +# Deploy templates and verify with a health check. +# DW_CLIENT_ID and DW_CLIENT_SECRET are set by the pipeline secret store. + +TARGET_HOST="your-solution.example.com" +AUTH_FLAGS="--auth oauth --clientIdEnv DW_CLIENT_ID --clientSecretEnv DW_CLIENT_SECRET" + +# Import templates +dw files ./Files/Templates /Templates -i -r \ + --host "$TARGET_HOST" $AUTH_FLAGS \ + --output json + +# Verify the environment is healthy +RESULT=$(dw query HealthCheck \ + --host "$TARGET_HOST" $AUTH_FLAGS \ + --output json) + +echo "$RESULT" | jq '.ok' +``` + +> [!TIP] +> In GitHub Actions, set `DW_CLIENT_ID` and `DW_CLIENT_SECRET` as repository secrets and reference them with `${{ secrets.DW_CLIENT_ID }}` in the `env` block of your step. In Azure Pipelines, add them as secret variables and they will be available as environment variables. + +### Persistent runners (saved config) + +On long-lived runners, configure the environment once: + +```sh +# One-time setup +dw env production +dw config --env.production.host your-solution.example.com +dw config --env.production.protocol https +export DW_CLIENT_ID=my-client-id +export DW_CLIENT_SECRET=my-client-secret +dw login --oauth +``` + +Then subsequent pipeline steps only need: + +```sh +dw env production +dw query HealthCheck --output json +``` + +### Installing add-ins in pipelines + +Use `dw install --queue` when deploying add-ins in automated pipelines. This defers activation until all files are in place, which avoids partial-load issues when multiple add-ins depend on each other: + +```sh +dw install ./bin/Release/net10.0/MyAddin.dll \ + --queue \ + --host "$TARGET_HOST" $AUTH_FLAGS +``` + +See the [install command reference](#install) for the full explanation of immediate vs. queued installation. + +## Command reference + +### Global options + +These options are available on all API-driven commands: + +| Option | Description | +|--------|-------------| +| `-v`, `--verbose` | Enable verbose logging | +| `--host ` | Use a specific host instead of the saved environment | +| `--protocol ` | Protocol for `--host` (defaults to `https`) | +| `--apiKey ` | Use an API key directly, no saved environment needed | +| `--auth ` | Override authentication mode: `user` or `oauth` | +| `--clientId ` | OAuth client ID (direct value) | +| `--clientSecret ` | OAuth client secret (direct value) | +| `--clientIdEnv ` | Environment variable containing the OAuth client ID | +| `--clientSecretEnv ` | Environment variable containing the OAuth client secret | +| `--output json` | Return structured JSON instead of human-readable output | + +### env + +Create, select, or inspect saved environments. + +```sh +dw env dev # switch to (or create) the "dev" environment +dw env # interactive setup for a new environment +dw env --list # list all configured environments +dw env production --users # list saved users for "production" +``` + +**JSON output:** + +```sh +dw env --list --output json +``` + +```json +{ + "ok": true, + "command": "env", + "operation": "list", + "status": 200, + "data": [ + { + "environments": ["dev", "staging", "production"] + } + ], + "errors": [], + "meta": {} +} +``` + +### login + +Log in interactively, configure OAuth, or switch between saved users. + +```sh +dw login # interactive username/password prompt +dw login DemoUser # switch to a saved user +dw login --oauth # configure OAuth for the current environment +``` + +The interactive login requires a DynamicWeb user with backend access and administrator privileges. The CLI creates an API key named "DW CLI" and stores it in `~/.dwc`. + +**JSON output:** + +```sh +dw login --oauth --output json +``` + +```json +{ + "ok": true, + "command": "login", + "operation": "oauth-login", + "status": 200, + "data": [ + { + "environment": "dev", + "authType": "oauth_client_credentials", + "clientIdEnv": "DW_CLIENT_ID", + "clientSecretEnv": "DW_CLIENT_SECRET", + "expires": "2026-04-13T14:22:31Z" + } + ], + "errors": [], + "meta": {} +} +``` + +### files + +List, export, and import files from the DynamicWeb file archive. + +```sh +dw files [dirPath] [outPath] [options] +``` + +**Key options:** + +| Option | Description | +|--------|-------------| +| `-l`, `--list` | List directories (add `-f` to include files) | +| `-f`, `--includeFiles` | Include files in listings | +| `-e`, `--export` | Export from the environment to disk | +| `-i`, `--import` | Import from disk to the environment | +| `-r`, `--recursive` | Recurse through subdirectories | +| `-o`, `--overwrite` | Allow overwriting existing files on import | +| `--createEmpty` | Create files even if the source file is empty | +| `--raw` | Keep exported archives zipped instead of extracting | +| `-af`, `--asFile` | Force the source path to be treated as a file | +| `-ad`, `--asDirectory` | Force the source path to be treated as a directory | +| `--dangerouslyIncludeLogsAndCache` | Include log and cache folders in export | + +**Examples:** + +```sh +# List the system folder structure recursively +dw files system -lr + +# Export templates recursively, including files +dw files templates ./templates -fre + +# Export a single file +dw files templates/Translations.xml ./templates -e + +# Import files from disk, recursively with overwrite +dw files ./Files templates -iro +``` + +#### Source type detection + +The CLI infers whether a path is a file or directory based on whether it contains a file extension. This is usually correct, but some paths are ambiguous: + +- A directory named `templates.v1` looks like a file +- A file without an extension looks like a directory + +Use `--asFile` or `--asDirectory` to override the detection: + +```sh +dw files templates/templates.v1 ./templates -e -ad # it's a directory, not a file +dw files templates/Translations.xml ./templates -e -af # force file mode +``` + +> [!NOTE] +> `--asFile` and `--asDirectory` cannot be used together. + +#### Deploying files between environments + +A common workflow is exporting files from one environment and importing them to another: + +```sh +# Export from development +dw env development +dw files templates ./templates -fre + +# Import to staging +dw env staging +dw files ./templates /templates -iro +``` + +This pattern works for any part of the file tree -- templates, designs, integration files, or the entire file archive. + +**JSON output:** + +```sh +dw files ./Files templates -i -r --output json +``` + +```json +{ + "ok": true, + "command": "files", + "operation": "import", + "status": 200, + "data": [ + { + "type": "upload", + "destinationPath": "templates", + "files": [ + "/workspace/Files/Templates/DefaultMail.html" + ], + "response": { + "message": "Upload completed" + } + } + ], + "errors": [], + "meta": { + "filesProcessed": 1, + "chunks": 1 + } +} +``` + +### query + +Run Management API queries, inspect available parameters, or prompt for them interactively. + +```sh +dw query [-- ...] [options] +``` + +**Key options:** + +| Option | Description | +|--------|-------------| +| `-l`, `--list` | List the query's properties and their types | +| `-i`, `--interactive` | Prompt for each parameter interactively | +| `-- ` | Pass query parameters directly | + +**Examples:** + +```sh +# List properties for a query +dw query FileByName -l + +# Run a query with parameters +dw query FileByName --name DefaultMail.html --directorypath /Templates/Forms/Mail + +# Run interactively (prompts for each parameter) +dw query FileByName --interactive +``` + +**JSON output:** + +```sh +dw query FileByName --name DefaultMail.html --output json +``` + +```json +{ + "ok": true, + "command": "query", + "operation": "run", + "status": 200, + "data": [ + { + "name": "DefaultMail.html", + "path": "/Templates/Forms/Mail/DefaultMail.html" + } + ], + "errors": [], + "meta": { + "query": "FileByName" + } +} +``` + +### command + +Run Management API commands with a JSON payload. + +```sh +dw command --json '' [options] +dw command --json ./payload.json [options] +``` + +The `--json` flag accepts either an inline JSON string or a path to a `.json` file. + +**Examples:** + +```sh +# Copy a page using an inline JSON payload +dw command PageCopy --json '{ "model": { "SourcePageId": 1189, "DestinationParentPageId": 1129 } }' + +# Move a page using a JSON file +dw command PageMove --json ./PageMove.json + +# Delete a page with JSON output +dw command PageDelete --json '{ "id": "1383" }' --output json +``` + +**JSON output:** + +```json +{ + "ok": true, + "command": "command", + "operation": "run", + "status": 200, + "data": [ + { + "success": true, + "message": "Command executed" + } + ], + "errors": [], + "meta": { + "commandName": "PageDelete" + } +} +``` + +> [!NOTE] +> `dw command --list` is reserved for command metadata but is not fully implemented yet. + +### install + +Upload and install a `.dll` or `.nupkg` add-in into the current environment. + +```sh +dw install [--queue] +``` + +**Immediate installation (default):** + +```sh +dw install ./bin/Release/net10.0/CustomProject.dll +``` + +The add-in is uploaded, installed, and activated immediately. This is the right choice for local development and iterative testing. + +**Queued installation:** + +```sh +dw install ./bin/Release/net10.0/CustomProject.dll --queue +``` + +The add-in is uploaded and installed but **not activated** until the next application recycle. Use `--queue` when: + +- Installing multiple add-ins in sequence +- Deploying add-ins that depend on shared libraries or other add-ins +- Running in a CI/CD pipeline where you want all changes to take effect together +- Preparing an environment before a planned restart + +Queued installation ensures all dependencies are in place before any add-in is activated, which avoids partial-load failures. + +> [!NOTE] +> Some add-in types require an application restart regardless of installation mode. In hosted or cloud environments, queued installation is the preferred approach -- see [DynamicWeb Cloud](xref:hosting-dynamicweb-cloud) for guidance on restarts and deployment workflows. + +### database + +Export the current environment's database to a `.bacpac` file. + +```sh +dw database ./backups --export +``` + +The database user needs `db_backupoperator` permissions. To grant them: + +```sql +USE [yourDwDatabaseName] +GO +ALTER ROLE [db_backupoperator] ADD MEMBER [yourDwDbUserName] +GO +``` + +### swift + +Download a Swift release from GitHub. + +```sh +dw swift [outPath] [options] +``` + +| Option | Description | +|--------|-------------| +| `-l`, `--list` | List all available release versions | +| `-t`, `--tag ` | Download a specific version tag | +| `-n`, `--nightly` | Download the latest commit (HEAD) instead of the latest release | +| `--force` | Overwrite the output directory if it is not empty | + +**Examples:** + +```sh +dw swift -l # list available versions +dw swift . --tag v1.25.1 --force # download a specific version +dw swift . --nightly --force # download the latest nightly build +``` + +### config + +Write values directly into `~/.dwc` using dot-notation paths. + +```sh +dw config --env.dev.host localhost:6001 +dw config --env.production.protocol https +``` + +This is useful for scripting config updates without editing the JSON file manually. + +## Troubleshooting + +### Git Bash path conversion + +Git Bash on Windows automatically converts paths that look like Unix paths, which can interfere with file operations. If you see unexpected path-conversion behavior, disable it for the session: + +```sh +export MSYS_NO_PATHCONV=1 +dw files -iro ./ ./TestFolder --host --apiKey +``` + +Alternatively, prefix paths with `./` or use PowerShell or CMD instead of Git Bash. From 0cb0502892af3e27cc18e7aef219a6f7fbc18f22 Mon Sep 17 00:00:00 2001 From: nicped Date: Mon, 13 Apr 2026 16:56:54 +0200 Subject: [PATCH 66/86] CLI v2: OAuth, JSON output, automation-first overhaul Adds OAuth client credentials support, --output json on all API-driven commands, improved env/auth config shape, file workflow improvements (-af, -ad, progress reporting), and structured error responses. --- README.md | 535 +++++++++++++++++++++++++++------------- bin/commands/command.js | 95 ++++++- bin/commands/env.js | 165 +++++++++++-- bin/commands/files.js | 271 +++++++++++++++----- bin/commands/login.js | 392 +++++++++++++++++++++++++---- bin/commands/query.js | 121 +++++++-- bin/index.js | 36 ++- package-lock.json | 4 +- package.json | 4 +- 9 files changed, 1278 insertions(+), 345 deletions(-) diff --git a/README.md b/README.md index 873e31b..558321c 100644 --- a/README.md +++ b/README.md @@ -1,244 +1,437 @@ # DynamicWeb CLI -## What is it? -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 include an easy setup and handling of different environments, access to the Management API and an easy way to update a Swift solution. +DynamicWeb CLI is the command-line interface for working with DynamicWeb 10 solutions. It helps you manage environments, authenticate against the admin API, run queries and commands, move files in and out of a solution, install add-ins, export databases, and pull Swift solutions. -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. -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. +This branch now targets `2.0.0-beta.0`. -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. +## Requirements -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. +- Node.js `>=20.12.0` -## Get started -### Install from npm -Install the published CLI globally: -> $ npm install -g @dynamicweb/cli +## Install -Then verify the command is available: -> $ dw --help +Install from npm: -### Install from source -If you're working on the CLI locally, clone the repository, move into the project directory, and run: -> $ npm install -> -> $ npm install -g . +```sh +npm install -g @dynamicweb/cli +dw --help +``` -This installs dependencies for development and links the current checkout as the global `dw` command. +Install from source: -### Publish a new version -Before publishing, make sure the version in `package.json` has been bumped: -> $ npm version patch +```sh +npm install +npm install -g . +``` -Log in to npm and publish the scoped package publicly: -> $ npm login -> -> $ npm publish --access public +## What Changed -## Commands -All commands and options can be viewed by running -> $ dw --help -> -> $ dw \ --help +The `2.0` beta is a substantial overhaul focused on automation and modern authentication. + +- Automation-first command output: `env`, `login`, `files`, `query`, and `command` now support `--output json` so scripts and pipelines can consume structured results instead of plain console logs. +- OAuth client credentials support: the CLI can now authenticate with OAuth 2.0 `client_credentials`, which makes headless and CI/CD usage much easier. +- Better environment handling: protocol, host, and auth details are stored more cleanly in `~/.dwc`, while one-off runs can still override host and credentials directly. +- Improved file workflows: file import, export, recursive sync, raw archive export, progress reporting, and source-type override flags make file operations more predictable. +- Clearer error reporting: commands can now return structured failures in JSON mode, which is much easier to handle in automation. + +## Quick Start + +View all available commands: + +```sh +dw --help +dw --help +``` + +Set up an environment and log in with a user: + +```sh +dw env dev +dw login +dw +``` -### Global options -Most commands support the following global options: -- `-v` `--verbose` Run with verbose logging -- `--host` Set the host directly, bypassing environment config (requires `--apiKey`) -- `--apiKey` Set the API key for an environmentless execution -- `--protocol` Set the protocol used (only with `--host`, defaults to `https`) +Run a query: -### Users and environments -As most commands are pulling or pushing data from the DW admin API, the necessary authorization is required. +```sh +dw query HealthCheck +``` -To generate an Api-key that the CLI will use, login to your environment -> $ dw login +## Authentication -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. -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`. +### Interactive User Login -Each environment has its own users, and each user has its own Api-key assigned to it, swap between environments by using -> $ dw env \ +The default login flow uses a DynamicWeb user account. The CLI logs in, creates an API key, and stores it in `~/.dwc`. -and swap between users by simply supplying the name of the user in the login command -> $ dw login \ +```sh +dw login +dw login +dw env +``` -You can view the current environment and user being used by simply typing -> $ dw +A user-authenticated config typically looks like this: -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 `~/.dwc`. The structure should look like the following ```json { - "env": { - "dev": { - "host": "localhost:6001", - "users": { - "DemoUser": { - "apiKey": "." - } - }, - "current": { - "user": "DemoUser" - } + "env": { + "dev": { + "host": "localhost:6001", + "protocol": "https", + "users": { + "DemoUser": { + "apiKey": "." } - }, - "current": { - "env": "dev" + }, + "current": { + "user": "DemoUser", + "authType": "user" + } } + }, + "current": { + "env": "dev" + } } ``` -### Files -> $ dw files \ \ +### OAuth Client Credentials -The files command is used to list out and export the structure in your Dynamicweb files archive, as such is has multiple options; -- `-l` `--list` This will list the directory given in \ -- `-f` `--includeFiles` The list will now also show all files in the directories -- `-r` `--recursive` By default it only handles the \, but with this option it will handle all directories under this recursively -- `-e` `--export` It will export \ into \ on your local machine, unzipped by default -- `--raw` This will keep the content zipped -- `--iamstupid` This will include the export of the /files/system/log and /files/.cache folders +For service accounts, automation, and headless usage, the CLI also supports OAuth 2.0 `client_credentials`. -#### Examples -Exporting all templates from current environment to local solution -> $ cd DynamicWebSolution/Files -> -> $ dw files templates ./templates -fre +Configure an environment for OAuth: -Listing the system files structure of the current environment -> $ dw files system -lr +```sh +export DW_CLIENT_ID=my-client-id +export DW_CLIENT_SECRET=my-client-secret -### Files Source Type Detection -By default, the `dw files` command automatically detects the source type based on the \: -If the path contains a file extension (e.g., 'templates/Translations.xml'), it is treated as a file. -Otherwise, it is treated as a directory. -In cases where this detection is incorrect, you can force the type using these flags: +dw login --oauth +``` -- `-ad` `--asDirectory` Forces the command to treat the path as a directory, even if its name contains a dot. -- `-af` `--asFile` Forces the command to treat the path as a single file, even if it has no extension. +Run a one-off command with OAuth flags instead of saved config: -#### Examples +```sh +dw query HealthCheck \ + --host your-solution.example.com \ + --auth oauth \ + --clientIdEnv DW_CLIENT_ID \ + --clientSecretEnv DW_CLIENT_SECRET \ + --output json +``` -Exporting single file from current environment to local solution -> $ dw files templates/Translations.xml ./templates -e +An OAuth-enabled environment in `~/.dwc` looks like this: -Exporting a directory that looks like a file -> $ dw files templates/templates.v1 ./templates -e -ad +```json +{ + "env": { + "dev": { + "host": "localhost:6001", + "protocol": "https", + "auth": { + "type": "oauth_client_credentials", + "clientIdEnv": "DW_CLIENT_ID", + "clientSecretEnv": "DW_CLIENT_SECRET" + }, + "current": { + "authType": "oauth_client_credentials" + } + } + }, + "current": { + "env": "dev" + } +} +``` -Exporting a file that has no extension -> $ dw files templates/testfile ./templates -e -af +## Global Options -### Swift -> $ dw swift \ +Most API-driven commands support these global options: -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; -- `-t` `--tag ` The tag/branch/release to pull -- `-l` `--list` Will list all the release versions -- `-n` `--nightly` Will pull #HEAD, as default is latest release -- `--force` Used if \ is not an empty folder, to override all the content +- `-v`, `--verbose`: enable verbose logging +- `--host`: use a host directly instead of the saved environment +- `--protocol`: set the protocol used with `--host` and default to `https` +- `--apiKey`: use an API key for environmentless execution +- `--auth`: override authentication mode with `user` or `oauth` +- `--clientId`: pass an OAuth client ID directly +- `--clientSecret`: pass an OAuth client secret directly +- `--clientIdEnv`: read the OAuth client ID from an environment variable +- `--clientSecretEnv`: read the OAuth client secret from an environment variable -#### Examples -Getting all the available releases -> $ dw swift -l +## JSON Output for Automation -Pulling and overriding local solution with latest nightly build -> $ cd DynamicWebSolution/Swift -> -> $ dw swift . -n --force +Commands that support `--output json` return a machine-readable envelope with `ok`, `status`, `data`, `errors`, and `meta` fields. -### Query -> $ dw query \ +Examples: + +```sh +dw env --list --output json +dw login --output json +dw query FileByName --name DefaultMail.html --output json +``` -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; -- `-l` `--list` Will list all the properties for the given \ -- `-i` `--interactive` Will perform the \ but without any parameters, as they will be asked for one by one in interactive mode -- `--` Any parameter the query needs will be sent by '--key value' +Representative output: -#### Examples -Getting all properties for a query -> $ dw query FileByName -l +```json +{ + "ok": true, + "command": "env", + "operation": "list", + "status": 200, + "data": [ + { + "environments": ["dev", "staging"] + } + ], + "errors": [], + "meta": {} +} +``` -Getting file information on a specific file by name -> $ dw query FileByName --name DefaultMail.html --directorypath /Templates/Forms/Mail +## Commands -### Command -> $ dw command \ +### `dw env [env]` -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. -- `-l` `--list` Lists all the properties for the command, as well as the json model required -- `--json` Takes a path to a .json file or a literal json, i.e --json '{ abc: "123" }' +Create, select, or inspect saved environments. -#### Examples -Creating a copy of a page using a json-string -> $ dw command PageCopy --json '{ "model": { "SourcePageId": 1189, "DestinationParentPageId": 1129 } }' +```sh +dw env dev +dw env --list +dw env --users +dw env --list --output json +``` -Removing a page using a json file -> $ dw command PageMove --json ./PageMove.json +Example JSON output: -Where PageMove.json contains ```json -{ "model": { "SourcePageId": 1383, "DestinationParentPageId": 1376 } } +{ + "ok": true, + "command": "env", + "operation": "select", + "status": 200, + "data": [ + { + "environment": "dev", + "current": "dev" + } + ], + "errors": [], + "meta": {} +} ``` -Deleting a page -> $ dw command PageDelete --json '{ "id": "1383" }' +### `dw login [user]` + +Log in interactively, configure OAuth, or switch between saved users for the current environment. -### Install -> $ dw install \ +```sh +dw login +dw login DemoUser +dw login --oauth +dw login --output json +``` -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. +Example JSON output: -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. +```json +{ + "ok": true, + "command": "login", + "operation": "oauth-login", + "status": 200, + "data": [ + { + "environment": "dev", + "authType": "oauth_client_credentials", + "clientIdEnv": "DW_CLIENT_ID", + "clientSecretEnv": "DW_CLIENT_SECRET", + "expires": "2026-04-13T14:22:31Z" + } + ], + "errors": [], + "meta": {} +} +``` -#### Examples -> $ dw install ./bin/Release/net10.0/CustomProject.dll +### `dw files [dirPath] [outPath]` -### Database -> $ dw database \ +List, export, and import files from the DynamicWeb file archive. -This command is used for various actions towards your current environments database. -- `-e` `--export` Exports your current environments database to a .bacpac file at \ +Useful flags: -#### Examples -> $ dw database -e ./backup +- `-l`, `--list`: list directories +- `-f`, `--includeFiles`: include files in listings +- `-e`, `--export`: export from the environment to disk +- `-i`, `--import`: import from disk to the environment +- `-r`, `--recursive`: recurse through subdirectories +- `--raw`: keep downloaded archives zipped +- `--dangerouslyIncludeLogsAndCache`: include log and cache folders during export, which is risky and usually not recommended +- `-af`, `--asFile`: force the source path to be treated as a file +- `-ad`, `--asDirectory`: force the source path to be treated as a directory -### Config -> $ dw config +Examples: -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. -- `--` The path and name of the property to set +```sh +dw files templates ./templates -fre +dw files system -lr +dw files templates/Translations.xml ./templates -e +dw files templates/templates.v1 ./templates -e -ad +dw files ./Files templates -i -r --output json +``` -#### Examples -Changing the host for the dev environment -> $ dw config --env.dev.host localhost:6001 +Example JSON output: -## Using Git Bash -If you're using Git Bash, you may encounter issues with path conversion that can interfere with relative paths used in commands. +```json +{ + "ok": true, + "command": "files", + "operation": "import", + "status": 200, + "data": [ + { + "type": "upload", + "destinationPath": "templates", + "files": [ + "/workspace/Files/Templates/DefaultMail.html" + ], + "response": { + "message": "Upload completed" + } + } + ], + "errors": [], + "meta": { + "filesProcessed": 1, + "chunks": 1 + } +} +``` + +### `dw query [query]` + +Run admin API queries, inspect available parameters, or prompt for them interactively. + +```sh +dw query FileByName -l +dw query FileByName --name DefaultMail.html --directorypath /Templates/Forms/Mail +dw query FileByName --interactive +dw query FileByName --name DefaultMail.html --output json +``` + +Example JSON output: + +```json +{ + "ok": true, + "command": "query", + "operation": "run", + "status": 200, + "data": [ + { + "name": "DefaultMail.html", + "path": "/Templates/Forms/Mail/DefaultMail.html" + } + ], + "errors": [], + "meta": { + "query": "FileByName" + } +} +``` + +### `dw command [command]` + +Run admin API commands and pass a JSON payload either inline or by file path. + +```sh +dw command PageCopy --json '{ "model": { "SourcePageId": 1189, "DestinationParentPageId": 1129 } }' +dw command PageMove --json ./PageMove.json +dw command PageDelete --json '{ "id": "1383" }' --output json +``` + +Example JSON output: + +```json +{ + "ok": true, + "command": "command", + "operation": "run", + "status": 200, + "data": [ + { + "success": true, + "message": "Command executed" + } + ], + "errors": [], + "meta": { + "commandName": "PageDelete" + } +} +``` + +`dw command --list` is reserved for command metadata, but it is not fully implemented yet. + +### `dw install [filePath]` + +Upload and install a `.dll` or `.nupkg` add-in into the current environment. + +```sh +dw install ./bin/Release/net10.0/CustomProject.dll +``` + +### `dw config` + +Write values directly into `~/.dwc` when you want to script config updates. + +```sh +dw config --env.dev.host localhost:6001 +``` -### Path Conversion Issues -Git Bash automatically converts relative paths to absolute paths, which can cause problems. -You'll see a warning message if the conversion setting is not disabled: +### `dw database [path] --export` -"You appear to have path conversion turned on in your shell. -If you are using relative paths, this may interfere. -Please see https://doc.dynamicweb.dev/documentation/fundamentals/code/CLI.html for more information." +Export the current environment database to a `.bacpac` file. -### Solution -To resolve this issue, disable path conversion by setting the `MSYS_NO_PATHCONV` environment variable (current session only): +```sh +dw database ./backups --export +``` + +### `dw swift [outPath]` + +Download the latest Swift release, a specific tag, or the nightly build. -> $ export MSYS_NO_PATHCONV=1 +```sh +dw swift -l +dw swift . --tag v1.25.1 --force +dw swift . --nightly --force +``` + +## CI/CD -#### Examples +For CI/CD, prefer OAuth client credentials and JSON output. -> $ export MSYS_NO_PATHCONV=1 -> $ dw files -iro ./ /TestFolder --host \ --apiKey \ +- Store `DW_CLIENT_ID` and `DW_CLIENT_SECRET` in your pipeline secret store. +- Use `--host` together with `--auth oauth` for ephemeral runners. +- Add `--output json` when you want reliable parsing in scripts. -### Alternative Solutions -If you prefer not to disable path conversion globally, you can: +Example: -1. Prefix relative paths with `./` instead of just `/` to prevent conversion for specific commands. -2. Use PowerShell or CMD instead of Git Bash. +```sh +dw query HealthCheck \ + --host your-solution.example.com \ + --auth oauth \ + --clientIdEnv DW_CLIENT_ID \ + --clientSecretEnv DW_CLIENT_SECRET \ + --output json +``` -#### Examples +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. + +## Using Git Bash + +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: + +```sh +export MSYS_NO_PATHCONV=1 +dw files -iro ./ ./TestFolder --host --apiKey +``` -> $ dw files -iro ./ ./TestFolder --host \ --apiKey \ +If you do not want to change that setting, prefer `./`-prefixed paths or use PowerShell or CMD instead. diff --git a/bin/commands/command.js b/bin/commands/command.js index 75fbfaf..05e1648 100644 --- a/bin/commands/command.js +++ b/bin/commands/command.js @@ -4,7 +4,7 @@ import fs from 'fs'; import { setupEnv, getAgent } from './env.js'; import { setupUser } from './login.js'; -const exclude = ['_', '$0', 'command', 'list', 'json', 'verbose', 'v', 'host', 'protocol', 'apiKey', 'env'] +const exclude = ['_', '$0', 'command', 'list', 'json', 'verbose', 'v', 'host', 'protocol', 'apiKey', 'env', 'output', 'auth', 'clientId', 'clientSecret', 'clientIdEnv', 'clientSecretEnv', 'oauth'] export function commandCommand() { return { @@ -22,22 +22,38 @@ export function commandCommand() { alias: 'l', describe: 'Lists all the properties for the command, currently not working' }) + .option('output', { + choices: ['json'], + describe: 'Outputs a single JSON response for automation-friendly parsing' + }) }, handler: async (argv) => { - if (argv.verbose) console.info(`Running command ${argv.command}`) - await handleCommand(argv) + const output = createCommandOutput(argv); + + try { + output.verboseLog(`Running command ${argv.command}`); + await handleCommand(argv, output); + output.finish(); + } catch (err) { + output.fail(err); + output.finish(); + process.exit(1); + } } } } -async function handleCommand(argv) { +async function handleCommand(argv, output) { let env = await setupEnv(argv); let user = await setupUser(argv, env); if (argv.list) { - console.log(await getProperties(env, user, argv.command)) + const properties = await getProperties(env, user, argv.command); + output.addData(properties); + output.log(properties); } else { - let response = await runCommand(env, user, argv.command, getQueryParams(argv), parseJsonOrPath(argv.json)) - console.log(response) + let response = await runCommand(env, user, argv.command, getQueryParams(argv), parseJsonOrPath(argv.json)); + output.addData(response); + output.log(response); } } @@ -82,8 +98,67 @@ async function runCommand(env, user, command, queryParams, data) { agent: getAgent(env.protocol) }) if (!res.ok) { - console.log(`Error when doing request ${res.url}`) - process.exit(1); + throw createCommandError(`Error when doing request ${res.url}`, res.status, await parseJsonSafe(res)); } return await res.json() -} \ No newline at end of file +} + +function createCommandOutput(argv) { + const response = { + ok: true, + command: 'command', + operation: argv.list ? 'list' : 'run', + status: 200, + data: [], + errors: [], + meta: { + commandName: argv.command + } + }; + + return { + json: argv.output === 'json', + response, + log(value) { + if (!this.json) { + console.log(value); + } + }, + verboseLog(...args) { + if (argv.verbose && !this.json) { + console.info(...args); + } + }, + addData(entry) { + response.data.push(entry); + }, + fail(err) { + response.ok = false; + response.status = err?.status || 1; + response.errors.push({ + message: err?.message || 'Unknown command error.', + details: err?.details ?? null + }); + }, + finish() { + if (this.json) { + console.log(JSON.stringify(response, null, 2)); + } + } + }; +} + +function createCommandError(message, status, details = null) { + const error = new Error(message); + error.status = status; + error.details = details; + return error; +} + +async function parseJsonSafe(res) { + try { + return await res.json(); + } catch { + return null; + } +} diff --git a/bin/commands/env.js b/bin/commands/env.js index e3dbb4e..987a560 100644 --- a/bin/commands/env.js +++ b/bin/commands/env.js @@ -41,8 +41,23 @@ export function envCommand() { type: 'boolean', description: 'List all users in environment, uses positional [env] if used, otherwise current env' }) + .option('output', { + choices: ['json'], + describe: 'Outputs a single JSON response for automation-friendly parsing' + }) }, - handler: (argv) => handleEnv(argv) + handler: async (argv) => { + const output = createEnvOutput(argv); + + try { + await handleEnv(argv, output); + output.finish(); + } catch (err) { + output.fail(err); + output.finish(); + process.exit(1); + } + } } } @@ -62,13 +77,17 @@ export async function setupEnv(argv) { if (askEnv && getConfig().env) { env = getConfig().env[argv.env] || getConfig().env[getConfig()?.current?.env]; - if (!env.protocol) { - console.log('Protocol for environment not set, defaulting to https'); + if (env && !env.protocol) { + logMessage(argv, 'Protocol for environment not set, defaulting to https'); env.protocol = 'https'; } } else if (askEnv) { - console.log('Current environment not set, please set it') + if (isJsonOutput(argv)) { + throw createCommandError('Current environment not set, please set it'); + } + + logMessage(argv, 'Current environment not set, please set it'); await interactiveEnv(argv, { environment: { type: 'input' @@ -79,17 +98,32 @@ export async function setupEnv(argv) { }) env = getConfig().env[getConfig()?.current?.env]; } + + if (!env || Object.keys(env).length === 0) { + throw createCommandError('Unable to resolve the current environment.'); + } + return env; } -async function handleEnv(argv) { +async function handleEnv(argv, output) { if (argv.users) { - let env = argv.env || getConfig().current.env; - console.log(`Users in environment ${env}: ${Object.keys(getConfig().env[env].users || {})}`); + const cfg = getConfig(); + let env = argv.env || cfg.current?.env; + const envConfig = cfg.env?.[env]; + if (!envConfig) { + throw createCommandError(`Environment '${env}' does not exist`, 404); + } + const users = Object.keys(envConfig.users || {}); + output.addData({ environment: env, users }); + output.log(`Users in environment ${env}: ${users}`); } else if (argv.env) { - changeEnv(argv) + const result = await changeEnv(argv, output); + output.addData(result); } else if (argv.list) { - console.log(`Existing environments: ${Object.keys(getConfig().env || {})}`) + const environments = Object.keys(getConfig().env || {}); + output.addData({ environments }); + output.log(`Existing environments: ${environments}`); } else { await interactiveEnv(argv, { environment: { @@ -102,12 +136,12 @@ async function handleEnv(argv) { interactive: { default: true } - }) + }, output) } } -export async function interactiveEnv(argv, options) { - if (argv.verbose) console.info('Setting up new environment') +export async function interactiveEnv(argv, options, output) { + verboseLog(argv, 'Setting up new environment'); const result = {}; for (const [key, config] of Object.entries(options)) { if (key === 'interactive') continue; @@ -122,8 +156,7 @@ export async function interactiveEnv(argv, options) { } getConfig().env = getConfig().env || {}; if (!result.environment || !result.environment.trim()) { - console.log('Environment name cannot be empty'); - return; + throw createCommandError('Environment name cannot be empty'); } getConfig().env[result.environment] = getConfig().env[result.environment] || {}; if (result.host) { @@ -135,8 +168,7 @@ export async function interactiveEnv(argv, options) { getConfig().env[result.environment].protocol = hostSplit[0]; getConfig().env[result.environment].host = hostSplit[1]; } else { - console.log(`Issues resolving host ${result.host}`); - return; + throw createCommandError(`Issues resolving host ${result.host}`); } } if (result.environment) { @@ -144,14 +176,34 @@ export async function interactiveEnv(argv, options) { getConfig().current.env = result.environment; } updateConfig(); - console.log(`Your current environment is now ${getConfig().current.env}`); - console.log(`To change the host of your environment, use the command 'dw env'`) + logMessage(argv, `Your current environment is now ${getConfig().current.env}`); + logMessage(argv, `To change the host of your environment, use the command 'dw env'`); + + const currentEnv = getConfig().env[result.environment]; + const data = { + environment: result.environment, + protocol: currentEnv.protocol, + host: currentEnv.host || null, + current: getConfig().current.env + }; + + if (output) { + output.addData(data); + } + + return data; } -async function changeEnv(argv) { - if (!Object.keys(getConfig().env).includes(argv.env)) { - console.log(`The specified environment ${argv.env} doesn't exist, please create it`); - await interactiveEnv(argv, { +async function changeEnv(argv, output) { + const environments = getConfig().env || {}; + + if (!Object.keys(environments).includes(argv.env)) { + if (isJsonOutput(argv)) { + throw createCommandError(`The specified environment ${argv.env} doesn't exist, please create it`, 404); + } + + logMessage(argv, `The specified environment ${argv.env} doesn't exist, please create it`); + return await interactiveEnv(argv, { environment: { type: 'input', default: argv.env, @@ -165,10 +217,75 @@ async function changeEnv(argv) { interactive: { default: true } - }) + }, output) } else { getConfig().current.env = argv.env; updateConfig(); - console.log(`Your current environment is now ${getConfig().current.env}`); + const data = { + environment: argv.env, + current: getConfig().current.env + }; + logMessage(argv, `Your current environment is now ${getConfig().current.env}`); + return data; } } + +export function isJsonOutput(argv) { + return argv?.json === true || argv?.output === 'json'; +} + +export function createCommandError(message, status = 1, details = null) { + const error = new Error(message); + error.status = status; + error.details = details; + return error; +} + +function logMessage(argv, ...args) { + if (!isJsonOutput(argv)) { + console.log(...args); + } +} + +function verboseLog(argv, ...args) { + if (argv?.verbose && !isJsonOutput(argv)) { + console.info(...args); + } +} + +function createEnvOutput(argv) { + const response = { + ok: true, + command: 'env', + operation: argv.users ? 'users' : argv.list ? 'list' : argv.env ? 'select' : 'setup', + status: 200, + data: [], + errors: [], + meta: {} + }; + + return { + json: isJsonOutput(argv), + addData(entry) { + response.data.push(entry); + }, + log(...args) { + if (!this.json) { + console.log(...args); + } + }, + fail(err) { + response.ok = false; + response.status = err?.status || 1; + response.errors.push({ + message: err?.message || 'Unknown env command error.', + details: err?.details ?? null + }); + }, + finish() { + if (this.json) { + console.log(JSON.stringify(response, null, 2)); + } + } + }; +} diff --git a/bin/commands/files.js b/bin/commands/files.js index f8f4445..6e3fc00 100644 --- a/bin/commands/files.js +++ b/bin/commands/files.js @@ -59,9 +59,14 @@ export function filesCommand() { type: 'boolean', describe: 'Used with export, keeps zip file instead of unpacking it' }) + .option('dangerouslyIncludeLogsAndCache', { + type: 'boolean', + describe: 'Includes log and cache folders during export. Risky and usually not recommended' + }) .option('iamstupid', { type: 'boolean', - describe: 'Includes export of log and cache folders, NOT RECOMMENDED' + hidden: true, + describe: 'Deprecated alias for --dangerouslyIncludeLogsAndCache' }) .option('asFile', { type: 'boolean', @@ -75,24 +80,54 @@ export function filesCommand() { describe: 'Forces the command to treat the path as a directory, even if its name contains a dot.', conflicts: 'asFile' }) + .option('output', { + choices: ['json'], + describe: 'Outputs a single JSON response for automation-friendly parsing' + }) + .option('json', { + type: 'boolean', + hidden: true, + describe: 'Deprecated alias for --output json' + }) }, handler: async (argv) => { - if (argv.verbose) console.info(`Listing directory at: ${argv.dirPath}`) - await handleFiles(argv) + if (argv.json && !argv.output) { + argv.output = 'json'; + console.warn('Warning: --json is deprecated and will be removed in a future release. Use --output json instead.'); + } + if (argv.iamstupid && !argv.dangerouslyIncludeLogsAndCache) { + argv.dangerouslyIncludeLogsAndCache = true; + console.warn('Warning: --iamstupid is deprecated and will be removed in a future release. Use --dangerouslyIncludeLogsAndCache instead.'); + } + const output = createFilesOutput(argv); + + try { + output.verboseLog(`Listing directory at: ${argv.dirPath}`); + await handleFiles(argv, output); + output.finish(); + } catch (err) { + output.fail(err); + output.finish(); + process.exit(1); + } } } } -async function handleFiles(argv) { +async function handleFiles(argv, output) { let env = await setupEnv(argv); let user = await setupUser(argv, env); if (argv.list) { let files = (await getFilesStructure(env, user, argv.dirPath, argv.recursive, argv.includeFiles)).model; - console.log(files.name) - let hasFiles = files.files?.data && files.files?.data.length !== 0; - resolveTree(files.directories, '', hasFiles); - resolveTree(files.files?.data ?? [], '', false); + output.setStatus(200); + output.addData(files); + if (!output.json) { + output.log(files.name); + let hasFiles = files.files?.data && files.files?.data.length !== 0; + resolveTree(files.directories, '', hasFiles, output); + resolveTree(files.files?.data ?? [], '', false, output); + } } if (argv.export) { @@ -106,31 +141,31 @@ async function handleFiles(argv) { let parentDirectory = path.dirname(argv.dirPath); parentDirectory = parentDirectory === '.' ? '/' : parentDirectory; - await download(env, user, parentDirectory, argv.outPath, false, null, true, argv.iamstupid, [argv.dirPath], true, argv.verbose); + await download(env, user, parentDirectory, argv.outPath, false, null, true, argv.dangerouslyIncludeLogsAndCache, [argv.dirPath], true, output); } else { - await download(env, user, argv.dirPath, argv.outPath, true, null, argv.raw, argv.iamstupid, [], false, argv.verbose); + await download(env, user, argv.dirPath, argv.outPath, true, null, argv.raw, argv.dangerouslyIncludeLogsAndCache, [], false, output); } } else { await interactiveConfirm('Are you sure you want a full export of files?', async () => { - console.log('Full export is starting') + output.log('Full export is starting'); let filesStructure = (await getFilesStructure(env, user, '/', false, true)).model; let dirs = filesStructure.directories; for (let id = 0; id < dirs.length; id++) { const dir = dirs[id]; - await download(env, user, dir.name, argv.outPath, true, null, argv.raw, argv.iamstupid, [], false, argv.verbose); + await download(env, user, dir.name, argv.outPath, true, null, argv.raw, argv.dangerouslyIncludeLogsAndCache, [], false, output); } - await download(env, user, '/.', argv.outPath, false, 'Base.zip', argv.raw, argv.iamstupid, Array.from(filesStructure.files.data, f => f.name), false, argv.verbose); - 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') + await download(env, user, '/.', argv.outPath, false, 'Base.zip', argv.raw, argv.dangerouslyIncludeLogsAndCache, Array.from(filesStructure.files.data, f => f.name), false, output); + if (argv.raw) output.log('The files in the base "files" folder is in Base.zip, each directory in "files" is in its own zip'); }) } } else if (argv.import) { if (argv.dirPath && argv.outPath) { let resolvedPath = path.resolve(argv.dirPath); if (argv.recursive) { - await processDirectory(env, user, resolvedPath, argv.outPath, resolvedPath, argv.createEmpty, true, argv.overwrite); + await processDirectory(env, user, resolvedPath, argv.outPath, resolvedPath, argv.createEmpty, true, argv.overwrite, output); } else { let filesInDir = getFilesInDirectory(resolvedPath); - await uploadFiles(env, user, filesInDir, argv.outPath, argv.createEmpty, argv.overwrite); + await uploadFiles(env, user, filesInDir, argv.outPath, argv.createEmpty, argv.overwrite, output); } } } @@ -142,20 +177,20 @@ function getFilesInDirectory(dirPath) { .filter(file => fs.statSync(file).isFile()); } -async function processDirectory(env, user, dirPath, outPath, originalDir, createEmpty, isRoot = false, overwrite = false) { +async function processDirectory(env, user, dirPath, outPath, originalDir, createEmpty, isRoot = false, overwrite = false, output) { let filesInDir = getFilesInDirectory(dirPath); if (filesInDir.length > 0) - await uploadFiles(env, user, filesInDir, isRoot ? outPath : path.join(outPath, path.basename(dirPath)), createEmpty, overwrite); + await uploadFiles(env, user, filesInDir, isRoot ? outPath : path.join(outPath, path.basename(dirPath)), createEmpty, overwrite, output); const subDirectories = fs.readdirSync(dirPath) .map(subDir => path.join(dirPath, subDir)) .filter(subDir => fs.statSync(subDir).isDirectory()); for (let subDir of subDirectories) { - await processDirectory(env, user, subDir, isRoot ? outPath : path.join(outPath, path.basename(dirPath)), originalDir, createEmpty, false, overwrite); + await processDirectory(env, user, subDir, isRoot ? outPath : path.join(outPath, path.basename(dirPath)), originalDir, createEmpty, false, overwrite, output); } } -function resolveTree(dirs, indentLevel, parentHasFiles) { +function resolveTree(dirs, indentLevel, parentHasFiles, output) { let end = `└──` let mid = `├──` for (let id = 0; id < dirs.length; id++) { @@ -163,35 +198,35 @@ function resolveTree(dirs, indentLevel, parentHasFiles) { let indentPipe = true; if (dirs.length == 1) { if (parentHasFiles) { - console.log(indentLevel + mid, dir.name) + output.log(indentLevel + mid, dir.name) } else { - console.log(indentLevel + end, dir.name) + output.log(indentLevel + end, dir.name) indentPipe = false; } } else if (id != dirs.length - 1) { - console.log(indentLevel + mid, dir.name) + output.log(indentLevel + mid, dir.name) } else { if (parentHasFiles) { - console.log(indentLevel + mid, dir.name) + output.log(indentLevel + mid, dir.name) } else { - console.log(indentLevel + end, dir.name) + output.log(indentLevel + end, dir.name) indentPipe = false; } } let hasFiles = dir.files?.data && dir.files?.data.length !== 0; if (indentPipe) { - resolveTree(dir.directories ?? [], indentLevel + '│\t', hasFiles); - resolveTree(dir.files?.data ?? [], indentLevel + '│\t', false); + resolveTree(dir.directories ?? [], indentLevel + '│\t', hasFiles, output); + resolveTree(dir.files?.data ?? [], indentLevel + '│\t', false, output); } else { - resolveTree(dir.directories ?? [], indentLevel + '\t', hasFiles); - resolveTree(dir.files?.data ?? [], indentLevel + '\t', false); + resolveTree(dir.directories ?? [], indentLevel + '\t', hasFiles, output); + resolveTree(dir.files?.data ?? [], indentLevel + '\t', false, output); } } } -async function download(env, user, dirPath, outPath, recursive, outname, raw, iamstupid, fileNames, singleFileMode, verbose = false) { +async function download(env, user, dirPath, outPath, recursive, outname, raw, dangerouslyIncludeLogsAndCache, fileNames, singleFileMode, output) { let excludeDirectories = ''; - if (!iamstupid) { + if (!dangerouslyIncludeLogsAndCache) { excludeDirectories = 'system/log'; if (dirPath === 'cache.net') { return; @@ -200,7 +235,7 @@ async function download(env, user, dirPath, outPath, recursive, outname, raw, ia const { endpoint, data } = prepareDownloadCommandData(dirPath, excludeDirectories, fileNames, recursive, singleFileMode); - displayDownloadMessage(dirPath, fileNames, recursive, singleFileMode); + displayDownloadMessage(dirPath, fileNames, recursive, singleFileMode, output); const res = await fetch(`${env.protocol}://${env.host}/Admin/Api/${endpoint}`, { method: 'POST', @@ -212,27 +247,40 @@ async function download(env, user, dirPath, outPath, recursive, outname, raw, ia agent: getAgent(env.protocol) }); - const filename = outname || tryGetFileNameFromResponse(res, dirPath, verbose); + const filename = outname || tryGetFileNameFromResponse(res, dirPath, output.verbose); if (!filename) return; const filePath = path.resolve(`${path.resolve(outPath)}/${filename}`) - const updater = createThrottledStatusUpdater(); + const updater = output.json ? null : createThrottledStatusUpdater(); await downloadWithProgress(res, filePath, { onData: (received) => { - updater.update(`Received:\t${formatBytes(received)}`); + if (updater) { + updater.update(`Received:\t${formatBytes(received)}`); + } } }); - updater.stop(); + if (updater) { + updater.stop(); + } if (singleFileMode) { - console.log(`Successfully downloaded: ${filename}`); + output.log(`Successfully downloaded: ${filename}`); } else { - console.log(`Finished downloading`, dirPath === '/.' ? '.' : dirPath, 'Recursive=' + recursive); + output.log(`Finished downloading`, dirPath === '/.' ? '.' : dirPath, 'Recursive=' + recursive); } - await extractArchive(filename, filePath, outPath, raw); + output.addData({ + type: 'download', + directoryPath: dirPath, + filename, + outPath: path.resolve(outPath), + recursive, + raw + }); + + await extractArchive(filename, filePath, outPath, raw, output); } function prepareDownloadCommandData(directoryPath, excludeDirectories, fileNames, recursive, singleFileMode) { @@ -249,10 +297,10 @@ function prepareDownloadCommandData(directoryPath, excludeDirectories, fileNames return { endpoint: 'FileDownload', data }; } -function displayDownloadMessage(directoryPath, fileNames, recursive, singleFileMode) { +function displayDownloadMessage(directoryPath, fileNames, recursive, singleFileMode, output) { if (singleFileMode) { const fileName = path.basename(fileNames[0] || 'unknown'); - console.log('Downloading file: ' + fileName); + output.log('Downloading file: ' + fileName); return; } @@ -261,30 +309,34 @@ function displayDownloadMessage(directoryPath, fileNames, recursive, singleFileM ? 'Base' : directoryPath; - console.log('Downloading', directoryPathDisplayName, 'Recursive=' + recursive); + output.log('Downloading', directoryPathDisplayName, 'Recursive=' + recursive); } -async function extractArchive(filename, filePath, outPath, raw) { +async function extractArchive(filename, filePath, outPath, raw, output) { if (raw) { return; } - console.log(`\nExtracting ${filename} to ${outPath}`); + output.log(`\nExtracting ${filename} to ${outPath}`); let destinationFilename = filename.replace('.zip', ''); if (destinationFilename === 'Base') destinationFilename = ''; const destinationPath = `${path.resolve(outPath)}/${destinationFilename}`; - const updater = createThrottledStatusUpdater(); + const updater = output.json ? null : createThrottledStatusUpdater(); await extractWithProgress(filePath, destinationPath, { onEntry: (processedEntries, totalEntries, percent) => { - updater.update(`Extracted:\t${processedEntries} of ${totalEntries} files (${percent}%)`); + if (updater) { + updater.update(`Extracted:\t${processedEntries} of ${totalEntries} files (${percent}%)`); + } } }); - updater.stop(); - console.log(`Finished extracting ${filename} to ${outPath}\n`); + if (updater) { + updater.stop(); + } + output.log(`Finished extracting ${filename} to ${outPath}\n`); fs.unlink(filePath, function(err) {}); } @@ -300,14 +352,12 @@ async function getFilesStructure(env, user, dirPath, recursive, includeFiles) { if (res.ok) { return await res.json(); } else { - console.log(res); - console.log(await res.json()); - process.exit(1); + throw createCommandError('Unable to fetch file structure.', res.status, await parseJsonSafe(res)); } } -export async function uploadFiles(env, user, localFilePaths, destinationPath, createEmpty = false, overwrite = false) { - console.log('Uploading files') +export async function uploadFiles(env, user, localFilePaths, destinationPath, createEmpty = false, overwrite = false, output = createFilesOutput({})) { + output.log('Uploading files') const chunkSize = 300; const chunks = []; @@ -316,26 +366,37 @@ export async function uploadFiles(env, user, localFilePaths, destinationPath, cr chunks.push(localFilePaths.slice(i, i + chunkSize)); } + output.mergeMeta({ + filesProcessed: (output.response.meta.filesProcessed || 0) + localFilePaths.length, + chunks: (output.response.meta.chunks || 0) + chunks.length + }); + for (let i = 0; i < chunks.length; i++) { - console.log(`Uploading chunk ${i + 1} of ${chunks.length}`); + output.log(`Uploading chunk ${i + 1} of ${chunks.length}`); const chunk = chunks[i]; - await uploadChunk(env, user, chunk, destinationPath, createEmpty, overwrite); - - console.log(`Finished uploading chunk ${i + 1} of ${chunks.length}`); + const body = await uploadChunk(env, user, chunk, destinationPath, createEmpty, overwrite, output); + output.addData({ + type: 'upload', + destinationPath, + files: chunk.map(filePath => path.resolve(filePath)), + response: body + }); + + output.log(`Finished uploading chunk ${i + 1} of ${chunks.length}`); } - console.log(`Finished uploading files. Total files: ${localFilePaths.length}, total chunks: ${chunks.length}`); + output.log(`Finished uploading files. Total files: ${localFilePaths.length}, total chunks: ${chunks.length}`); } -async function uploadChunk(env, user, filePathsChunk, destinationPath, createEmpty, overwrite) { +async function uploadChunk(env, user, filePathsChunk, destinationPath, createEmpty, overwrite, output) { const form = new FormData(); form.append('path', destinationPath); form.append('skipExistingFiles', String(!overwrite)); form.append('allowOverwrite', String(overwrite)); filePathsChunk.forEach(fileToUpload => { - console.log(`${fileToUpload}`) + output.log(`${fileToUpload}`) form.append('files', fs.createReadStream(path.resolve(fileToUpload))); }); @@ -349,12 +410,10 @@ async function uploadChunk(env, user, filePathsChunk, destinationPath, createEmp }); if (res.ok) { - console.log(await res.json()) + return await res.json(); } else { - console.log(res) - console.log(await res.json()) - process.exit(1); + throw createCommandError('File upload failed.', res.status, await parseJsonSafe(res)); } } @@ -375,3 +434,87 @@ function wildcardToRegExp(wildcard) { const escaped = wildcard.replace(/[.+?^${}()|[\]\\]/g, '\\$&'); return new RegExp('^' + escaped.replace(/\*/g, '.*') + '$'); } + +function createFilesOutput(argv) { + const response = { + ok: true, + command: 'files', + operation: getFilesOperation(argv), + status: 200, + data: [], + errors: [], + meta: {} + }; + + return { + json: argv.output === 'json' || Boolean(argv.json), + verbose: Boolean(argv.verbose), + response, + log(...args) { + if (!this.json) { + console.log(...args); + } + }, + verboseLog(...args) { + if (this.verbose && !this.json) { + console.info(...args); + } + }, + addData(entry) { + response.data.push(entry); + }, + mergeMeta(meta) { + response.meta = { + ...response.meta, + ...meta + }; + }, + setStatus(status) { + response.status = status; + }, + fail(err) { + response.ok = false; + response.status = err?.status || 1; + response.errors.push({ + message: err?.message || 'Unknown files command error.', + details: err?.details ?? null + }); + }, + finish() { + if (this.json) { + console.log(JSON.stringify(response, null, 2)); + } + } + }; +} + +function getFilesOperation(argv) { + if (argv.list) { + return 'list'; + } + + if (argv.export) { + return 'export'; + } + + if (argv.import) { + return 'import'; + } + + return 'unknown'; +} + +function createCommandError(message, status, details = null) { + const error = new Error(message); + error.status = status; + error.details = details; + return error; +} + +async function parseJsonSafe(res) { + try { + return await res.json(); + } catch { + return null; + } +} diff --git a/bin/commands/login.js b/bin/commands/login.js index 20874ba..f4d4a35 100644 --- a/bin/commands/login.js +++ b/bin/commands/login.js @@ -1,8 +1,12 @@ import fetch from 'node-fetch'; -import { interactiveEnv, getAgent } from './env.js' +import { interactiveEnv, getAgent, isJsonOutput, createCommandError } from './env.js' import { updateConfig, getConfig } from './config.js'; import { input, password } from '@inquirer/prompts'; +const DEFAULT_OAUTH_TOKEN_PATH = '/Admin/OAuth/token'; +const DEFAULT_CLIENT_ID_ENV = 'DW_CLIENT_ID'; +const DEFAULT_CLIENT_SECRET_ENV = 'DW_CLIENT_SECRET'; + export function loginCommand() { return { command: 'login [user]', @@ -12,8 +16,27 @@ export function loginCommand() { .positional('user', { describe: 'user' }) + .option('oauth', { + type: 'boolean', + describe: 'Configures OAuth client_credentials authentication for the current environment' + }) + .option('output', { + choices: ['json'], + describe: 'Outputs a single JSON response for automation-friendly parsing' + }) }, - handler: (argv) => handleLogin(argv) + handler: async (argv) => { + const output = createLoginOutput(argv); + + try { + await handleLogin(argv, output); + output.finish(); + } catch (err) { + output.fail(err); + output.finish(); + process.exit(1); + } + } } } @@ -26,17 +49,24 @@ export async function setupUser(argv, env) { askLogin = false; } + if (!user.apiKey && shouldUseOAuth(argv, env)) { + return await authenticateWithOAuth(argv, env); + } + if (!user.apiKey && env.users && (argv.user || env.current?.user)) { user = env.users[argv.user] || env.users[env.current?.user]; askLogin = false; } if (askLogin && argv.host) { - console.log('Please add an --apiKey to the command as overriding the host requires that.') - process.exit(); + throw createCommandError('Please add an --apiKey, or provide OAuth client credentials when overriding the host.'); } else if (askLogin) { - console.log('Current user not set, please login') + if (isJsonOutput(argv)) { + throw createCommandError('Current user not set, please login'); + } + + logMessage(argv, 'Current user not set, please login'); await interactiveLogin(argv, { environment: { type: 'input', @@ -59,8 +89,13 @@ export async function setupUser(argv, env) { return user; } -async function handleLogin(argv) { - argv.user ? changeUser(argv) : await interactiveLogin(argv, { +async function handleLogin(argv, output) { + if (shouldUseOAuth(argv, getCurrentEnv(argv))) { + output.addData(await interactiveOAuthLogin(argv)); + } else if (argv.user) { + output.addData(await changeUser(argv)); + } else { + output.addData(await interactiveLogin(argv, { environment: { type: 'input', default: getConfig()?.current?.env || 'dev', @@ -75,11 +110,12 @@ async function handleLogin(argv) { interactive: { default: true } - }) + }, output)) + } } -export async function interactiveLogin(argv, options) { - if (argv.verbose) console.info('Now logging in') +export async function interactiveLogin(argv, options, output) { + verboseLog(argv, 'Now logging in'); const result = {}; for (const [key, config] of Object.entries(options)) { if (key === 'interactive') continue; @@ -95,41 +131,57 @@ export async function interactiveLogin(argv, options) { }); } if (!getConfig().env || !getConfig().env[result.environment] || !getConfig().env[result.environment].host || !getConfig().env[result.environment].protocol) { - if (!argv.host) - console.log(`The environment specified is missing parameters, please specify them`) - await interactiveEnv(argv, { - environment: { - type: 'input', - default: result.environment, - prompt: 'never' - }, - host: { - describe: 'Enter your host including protocol, i.e "https://yourHost.com":', - type: 'input', - prompt: 'always' - }, - interactive: { - default: true - } - }) + if (argv.host) { + ensureEnvironmentFromArgs(result.environment, argv); + } else { + logMessage(argv, `The environment specified is missing parameters, please specify them`); + await interactiveEnv(argv, { + environment: { + type: 'input', + default: result.environment, + prompt: 'never' + }, + host: { + describe: 'Enter your host including protocol, i.e "https://yourHost.com":', + type: 'input', + prompt: 'always' + }, + interactive: { + default: true + } + }, output) + } } - await loginInteractive(result, argv.verbose); + return await loginInteractive(result, argv.verbose, argv); } -async function loginInteractive(result, verbose) { +async function loginInteractive(result, verbose, argv) { var protocol = getConfig().env[result.environment].protocol; var token = await login(result.username, result.password, result.environment, protocol, verbose); - if (!token) return; + if (!token) { + throw createCommandError(`Could not fetch a login token for user ${result.username}.`); + } var apiKey = await getApiKey(token, result.environment, protocol, verbose) - if (!apiKey) return; + if (!apiKey) { + throw createCommandError(`Could not create an API Key for the logged in user ${result.username}.`); + } getConfig().env = getConfig().env || {}; getConfig().env[result.environment].users = getConfig().env[result.environment].users || {}; getConfig().env[result.environment].users[result.username] = getConfig().env[result.environment].users[result.username] || {}; getConfig().env[result.environment].users[result.username].apiKey = apiKey; getConfig().env[result.environment].current = getConfig().env[result.environment].current || {}; getConfig().env[result.environment].current.user = result.username; - console.log("You're now logged in as " + result.username) + getConfig().env[result.environment].current.authType = 'user'; + logMessage(argv, "You're now logged in as " + result.username); updateConfig(); + + return { + environment: result.environment, + username: result.username, + apiKey, + host: getConfig().env[result.environment].host, + protocol + }; } async function login(username, password, env, protocol, verbose) { @@ -153,14 +205,13 @@ async function login(username, password, env, protocol, verbose) { } else { if (verbose) console.info(res) - console.log(`Login attempt failed with username ${username}, please verify its a valid user in your Dynamicweb solution.`) + throw createCommandError(`Login attempt failed with username ${username}, please verify its a valid user in your Dynamicweb solution.`, res.status) } } function parseCookies (cookieHeader) { const list = {}; if (!cookieHeader) { - console.log(`Could not get the necessary information from the login request, please verify its a valid user in your Dynamicweb solution.`) return list; } @@ -173,10 +224,6 @@ function parseCookies (cookieHeader) { list[name] = decodeURIComponent(value); }); - if (!list.user) { - console.log(`Could not get the necessary information from the login request, please verify its a valid user in your Dynamicweb solution.`) - } - return list; } @@ -193,7 +240,7 @@ async function getToken(user, env, protocol, verbose) { } else { if (verbose) console.info(res) - console.log(`Could not fetch the token for the logged in user ${user}, please verify its a valid user in your Dynamicweb solution.`) + throw createCommandError(`Could not fetch the token for the logged in user ${user}, please verify its a valid user in your Dynamicweb solution.`, res.status) } } @@ -218,12 +265,273 @@ async function getApiKey(token, env, protocol, verbose) { } else { if (verbose) console.info(res) - console.log(`Could not create an API Key for the logged in user, please verify its a valid user in your Dynamicweb solution.`) + throw createCommandError(`Could not create an API Key for the logged in user, please verify its a valid user in your Dynamicweb solution.`, res.status) } } async function changeUser(argv) { + if (!getConfig().current?.env || !getConfig().env?.[getConfig().current.env]) { + throw createCommandError('Current environment not set, please set it before changing user.'); + } + + getConfig().env[getConfig().current.env].current = getConfig().env[getConfig().current.env].current || {}; getConfig().env[getConfig().current.env].current.user = argv.user; + getConfig().env[getConfig().current.env].current.authType = 'user'; + updateConfig(); + logMessage(argv, `You're now logged in as ${getConfig().env[getConfig().current.env].current.user}`); + + return { + environment: getConfig().current.env, + username: getConfig().env[getConfig().current.env].current.user + }; +} + +async function interactiveOAuthLogin(argv) { + verboseLog(argv, 'Configuring OAuth client credentials authentication'); + + const currentEnvName = getConfig()?.current?.env || 'dev'; + const environment = await input({ + message: 'environment', + default: currentEnvName + }); + const existingEnv = getConfig()?.env?.[environment] || {}; + const existingAuth = existingEnv.auth || {}; + + const result = { + environment, + clientIdEnv: argv.clientIdEnv || existingAuth.clientIdEnv || DEFAULT_CLIENT_ID_ENV, + clientSecretEnv: argv.clientSecretEnv || existingAuth.clientSecretEnv || DEFAULT_CLIENT_SECRET_ENV + }; + + if (!argv.clientIdEnv) { + result.clientIdEnv = await input({ + message: 'clientIdEnv', + default: result.clientIdEnv + }); + } + + if (!argv.clientSecretEnv) { + result.clientSecretEnv = await input({ + message: 'clientSecretEnv', + default: result.clientSecretEnv + }); + } + + if (!getConfig().env || !getConfig().env[result.environment] || !getConfig().env[result.environment].host || !getConfig().env[result.environment].protocol) { + if (argv.host) { + ensureEnvironmentFromArgs(result.environment, argv); + } else { + logMessage(argv, 'The environment specified is missing parameters, please specify them'); + await interactiveEnv(argv, { + environment: { + type: 'input', + default: result.environment, + prompt: 'never' + }, + host: { + describe: 'Enter your host including protocol, i.e "https://yourHost.com":', + type: 'input', + prompt: 'always' + }, + interactive: { + default: true + } + }); + } + } + + const env = getConfig().env[result.environment]; + const oauthConfig = resolveOAuthConfig({ + ...argv, + clientIdEnv: result.clientIdEnv, + clientSecretEnv: result.clientSecretEnv, + oauth: true + }, env); + + const tokenResult = await fetchOAuthToken(env, oauthConfig, argv.verbose); + + getConfig().current = getConfig().current || {}; + getConfig().current.env = result.environment; + env.auth = { + type: 'oauth_client_credentials', + clientIdEnv: result.clientIdEnv, + clientSecretEnv: result.clientSecretEnv + }; + env.current = env.current || {}; + env.current.authType = 'oauth_client_credentials'; + delete env.current.user; updateConfig(); - console.log(`You're now logged in as ${getConfig().env[getConfig().current.env].current.user}`); -} \ No newline at end of file + + logMessage(argv, `OAuth authentication is now configured for ${result.environment}`); + + return { + environment: result.environment, + authType: 'oauth_client_credentials', + clientIdEnv: result.clientIdEnv, + clientSecretEnv: result.clientSecretEnv, + expires: tokenResult.expires || null + }; +} + +async function authenticateWithOAuth(argv, env) { + const oauthConfig = resolveOAuthConfig(argv, env, true); + const tokenResult = await fetchOAuthToken(env, oauthConfig, argv.verbose); + + return { + apiKey: tokenResult.token, + authType: 'oauth_client_credentials', + expires: tokenResult.expires || null + }; +} + +function shouldUseOAuth(argv, env = {}) { + if (argv.auth === 'user') { + return false; + } + + if (argv.oauth || argv.auth === 'oauth') { + return true; + } + + if (argv.clientId || argv.clientSecret || argv.clientIdEnv || argv.clientSecretEnv) { + return true; + } + + if (env?.current?.authType) { + return env.current.authType === 'oauth_client_credentials'; + } + + return env?.auth?.type === 'oauth_client_credentials'; +} + +function resolveOAuthConfig(argv, env = {}, requireCredentials = true) { + const authConfig = env?.auth || {}; + const clientIdEnv = argv.clientIdEnv || authConfig.clientIdEnv || DEFAULT_CLIENT_ID_ENV; + const clientSecretEnv = argv.clientSecretEnv || authConfig.clientSecretEnv || DEFAULT_CLIENT_SECRET_ENV; + const clientId = argv.clientId || process.env[clientIdEnv]; + const clientSecret = argv.clientSecret || process.env[clientSecretEnv]; + + if (requireCredentials) { + if (!clientId) { + throw createCommandError(`OAuth client ID not found. Set --clientId or export ${clientIdEnv}.`); + } + + if (!clientSecret) { + throw createCommandError(`OAuth client secret not found. Set --clientSecret or export ${clientSecretEnv}.`); + } + } + + return { + clientId, + clientSecret, + clientIdEnv, + clientSecretEnv + }; +} + +async function fetchOAuthToken(env, oauthConfig, verbose) { + const res = await fetch(`${env.protocol}://${env.host}${DEFAULT_OAUTH_TOKEN_PATH}`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json' + }, + body: JSON.stringify({ + grant_type: 'client_credentials', + client_id: oauthConfig.clientId, + client_secret: oauthConfig.clientSecret + }), + agent: getAgent(env.protocol) + }); + + const body = await parseJsonSafe(res); + + if (!res.ok) { + if (verbose) { + console.info(res); + } + + throw createCommandError(`OAuth token request failed at ${DEFAULT_OAUTH_TOKEN_PATH}.`, res.status, body); + } + + const token = body?.token || body?.Token; + const expires = body?.expires || body?.Expires || null; + + if (!token) { + throw createCommandError('OAuth token response did not include a token.', res.status, body); + } + + return { token, expires }; +} + +function getCurrentEnv(argv) { + if (argv.host) { + return { + host: argv.host, + protocol: argv.protocol || 'https' + }; + } + + return getConfig()?.env?.[getConfig()?.current?.env] || {}; +} + +function ensureEnvironmentFromArgs(environment, argv) { + getConfig().env = getConfig().env || {}; + getConfig().env[environment] = getConfig().env[environment] || {}; + getConfig().env[environment].host = argv.host; + getConfig().env[environment].protocol = argv.protocol || 'https'; + getConfig().current = getConfig().current || {}; + getConfig().current.env = environment; + updateConfig(); +} + +function logMessage(argv, ...args) { + if (!isJsonOutput(argv)) { + console.log(...args); + } +} + +function verboseLog(argv, ...args) { + if (argv?.verbose && !isJsonOutput(argv)) { + console.info(...args); + } +} + +function createLoginOutput(argv) { + const response = { + ok: true, + command: 'login', + operation: shouldUseOAuth(argv, getCurrentEnv(argv)) ? 'oauth-login' : argv.user ? 'select-user' : 'login', + status: 200, + data: [], + errors: [], + meta: {} + }; + + return { + json: isJsonOutput(argv), + addData(entry) { + response.data.push(entry); + }, + fail(err) { + response.ok = false; + response.status = err?.status || 1; + response.errors.push({ + message: err?.message || 'Unknown login command error.', + details: err?.details ?? null + }); + }, + finish() { + if (this.json) { + console.log(JSON.stringify(response, null, 2)); + } + } + }; +} + +async function parseJsonSafe(res) { + try { + return await res.json(); + } catch { + return null; + } +} diff --git a/bin/commands/query.js b/bin/commands/query.js index baa93b0..e9e356c 100644 --- a/bin/commands/query.js +++ b/bin/commands/query.js @@ -3,7 +3,7 @@ import { setupEnv, getAgent } from './env.js'; import { setupUser } from './login.js'; import { input } from '@inquirer/prompts'; -const exclude = ['_', '$0', 'query', 'list', 'i', 'l', 'interactive', 'verbose', 'v', 'host', 'protocol', 'apiKey', 'env'] +const exclude = ['_', '$0', 'query', 'list', 'i', 'l', 'interactive', 'verbose', 'v', 'host', 'protocol', 'apiKey', 'env', 'output', 'auth', 'clientId', 'clientSecret', 'clientIdEnv', 'clientSecretEnv', 'oauth'] export function queryCommand() { return { @@ -22,30 +22,46 @@ export function queryCommand() { alias: 'i', describe: 'Runs in interactive mode to ask for query parameters one by one' }) + .option('output', { + choices: ['json'], + describe: 'Outputs a single JSON response for automation-friendly parsing' + }) }, - handler: (argv) => { - if (argv.verbose) console.info(`Running query ${argv.query}`) - handleQuery(argv) + handler: async (argv) => { + const output = createQueryOutput(argv); + + try { + output.verboseLog(`Running query ${argv.query}`); + await handleQuery(argv, output); + output.finish(); + } catch (err) { + output.fail(err); + if (!output.json) { + console.error(err.stack || err.message || String(err)); + } + output.finish(); + process.exit(1); + } } } } -async function handleQuery(argv) { +async function handleQuery(argv, output) { let env = await setupEnv(argv); let user = await setupUser(argv, env); if (argv.list) { - console.log(await getProperties(argv)) + const properties = await getProperties(env, user, argv.query); + output.addData(properties); + output.log(properties); } else { - let response = await runQuery(env, user, argv.query, await getQueryParams(argv)) - console.log(response) + let response = await runQuery(env, user, argv.query, await getQueryParams(env, user, argv)); + output.addData(response); + output.log(response); } } -async function getProperties(argv) { - let env = await setupEnv(argv); - let user = await setupUser(argv, env); - - let res = await fetch(`${env.protocol}://${env.host}/Admin/Api/QueryByName?name=${argv.query}`, { +async function getProperties(env, user, query) { + let res = await fetch(`${env.protocol}://${env.host}/Admin/Api/QueryByName?name=${query}`, { method: 'GET', headers: { 'Authorization': `Bearer ${user.apiKey}` @@ -54,21 +70,19 @@ async function getProperties(argv) { }) if (res.ok) { let body = await res.json() - if (body.model.properties.groups === undefined) { - console.log('Unable to fetch query parameters'); - process.exit(1); + if (body?.model?.properties?.groups === undefined) { + throw createCommandError('Unable to fetch query parameters.', res.status, body); } return body.model.properties.groups.filter(g => g.name === 'Properties')[0].fields.map(field => `${field.name} (${field.typeName})`) } - console.log('Unable to fetch query parameters'); - console.log(res); - process.exit(1); + + throw createCommandError('Unable to fetch query parameters.', res.status, await parseJsonSafe(res)); } -async function getQueryParams(argv) { +async function getQueryParams(env, user, argv) { let params = {} if (argv.interactive) { - let properties = await getProperties(argv); + let properties = await getProperties(env, user, argv.query); console.log('The following properties will be requested:') console.log(properties) for (const p of properties) { @@ -93,8 +107,67 @@ async function runQuery(env, user, query, params) { agent: getAgent(env.protocol) }) if (!res.ok) { - console.log(`Error when doing request ${res.url}`) - process.exit(1); + throw createCommandError(`Error when doing request ${res.url}`, res.status, await parseJsonSafe(res)); } return await res.json() -} \ No newline at end of file +} + +function createQueryOutput(argv) { + const response = { + ok: true, + command: 'query', + operation: argv.list ? 'list' : 'run', + status: 200, + data: [], + errors: [], + meta: { + query: argv.query + } + }; + + return { + json: argv.output === 'json', + response, + log(value) { + if (!this.json) { + console.log(value); + } + }, + verboseLog(...args) { + if (argv.verbose && !this.json) { + console.info(...args); + } + }, + addData(entry) { + response.data.push(entry); + }, + fail(err) { + response.ok = false; + response.status = err?.status || 1; + response.errors.push({ + message: err?.message || 'Unknown query command error.', + details: err?.details ?? null + }); + }, + finish() { + if (this.json) { + console.log(JSON.stringify(response, null, 2)); + } + } + }; +} + +function createCommandError(message, status, details = null) { + const error = new Error(message); + error.status = status; + error.details = details; + return error; +} + +async function parseJsonSafe(res) { + try { + return await res.json(); + } catch { + return null; + } +} diff --git a/bin/index.js b/bin/index.js index 1bb81bb..954755b 100644 --- a/bin/index.js +++ b/bin/index.js @@ -36,11 +36,27 @@ yargs(hideBin(process.argv)) description: 'Allows setting the protocol used, only used together with --host, defaulting to https' }) .option('host', { - description: 'Allows setting the host used, only allowed if an --apiKey is specified' + description: 'Allows setting the host used, only allowed if an --apiKey or OAuth client credentials are specified' }) .option('apiKey', { description: 'Allows setting the apiKey for an environmentless execution of the CLI command' }) + .option('auth', { + choices: ['user', 'oauth'], + description: 'Overrides the authentication mode for the command' + }) + .option('clientId', { + description: 'OAuth client ID used together with --auth oauth' + }) + .option('clientSecret', { + description: 'OAuth client secret used together with --auth oauth. WARNING: passing this on the command line can expose the secret via shell history and process listings. Prefer using --clientSecretEnv to reference a secret stored in an environment variable instead.' + }) + .option('clientIdEnv', { + description: 'Environment variable name that contains the OAuth client ID' + }) + .option('clientSecretEnv', { + description: 'Environment variable name that contains the OAuth client secret' + }) .demandCommand() .parse() @@ -52,11 +68,19 @@ function baseCommand() { if (Object.keys(getConfig()).length === 0) { console.log('To login to a solution use `dw login`') return; - } - console.log(`Environment: ${getConfig()?.current?.env}`) - console.log(`User: ${getConfig()?.env[getConfig()?.current?.env]?.current?.user}`) - console.log(`Protocol: ${getConfig()?.env[getConfig()?.current?.env]?.protocol}`) - console.log(`Host: ${getConfig()?.env[getConfig()?.current?.env]?.host}`) + } + const cfg = getConfig(); + const currentEnv = cfg?.env?.[cfg?.current?.env]; + const authType = currentEnv?.current?.authType; + + console.log(`Environment: ${cfg?.current?.env}`); + if (authType === 'oauth_client_credentials') { + console.log('Authentication: OAuth client credentials'); + } else if (currentEnv?.current?.user) { + console.log(`User: ${currentEnv.current.user}`); + } + console.log(`Protocol: ${currentEnv?.protocol}`); + console.log(`Host: ${currentEnv?.host}`); } } } diff --git a/package-lock.json b/package-lock.json index aa1e289..b1bdc7d 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@dynamicweb/cli", - "version": "1.1.0", + "version": "2.0.0-beta.0", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "@dynamicweb/cli", - "version": "1.1.0", + "version": "2.0.0-beta.0", "license": "MIT", "dependencies": { "@inquirer/prompts": "^8.4.1", diff --git a/package.json b/package.json index 31cef17..470030e 100644 --- a/package.json +++ b/package.json @@ -2,7 +2,7 @@ "name": "@dynamicweb/cli", "type": "module", "description": "CLI for interacting with Dynamicweb 10 solutions from the command line.", - "version": "1.1.1", + "version": "2.0.0-beta.0", "main": "bin/index.js", "files": [ "bin/**" @@ -48,4 +48,4 @@ "node-fetch": "^3.2.10", "yargs": "^17.5.1" } -} \ No newline at end of file +} From f4b3fb52df4482504013c5a5e83a27c0dac619f8 Mon Sep 17 00:00:00 2001 From: nicped Date: Mon, 13 Apr 2026 17:17:11 +0200 Subject: [PATCH 67/86] Add delete, copy, and move operations to the files command Supports deployment scenarios with dynamically named files that accumulate over time. Adds --delete (with --empty modifier), --copy, and --move flags backed by the DirectoryDelete, DirectoryEmpty, FileDelete, AssetCopy, and AssetMove APIs. --- bin/commands/files.js | 189 +++++++++++++++++++++++++++++++++++++++++ cliv2-documentation.md | 127 ++++++++++++++++++++++++++- 2 files changed, 315 insertions(+), 1 deletion(-) diff --git a/bin/commands/files.js b/bin/commands/files.js index 6e3fc00..ca6af3f 100644 --- a/bin/commands/files.js +++ b/bin/commands/files.js @@ -68,6 +68,23 @@ export function filesCommand() { hidden: true, describe: 'Deprecated alias for --dangerouslyIncludeLogsAndCache' }) + .option('delete', { + alias: 'd', + type: 'boolean', + describe: 'Deletes the file or directory at [dirPath]. Detects type from path (use --asFile/--asDirectory to override)' + }) + .option('empty', { + type: 'boolean', + describe: 'Used with --delete, empties a directory instead of deleting it' + }) + .option('copy', { + type: 'string', + describe: 'Copies the file or directory at [dirPath] to the given destination path' + }) + .option('move', { + type: 'string', + describe: 'Moves the file or directory at [dirPath] to the given destination path' + }) .option('asFile', { type: 'boolean', alias: 'af', @@ -168,6 +185,46 @@ async function handleFiles(argv, output) { await uploadFiles(env, user, filesInDir, argv.outPath, argv.createEmpty, argv.overwrite, output); } } + } else if (argv.delete) { + if (!argv.dirPath) { + throw createCommandError('A path is required for delete operations.', 400); + } + + const isFile = argv.asFile || argv.asDirectory + ? argv.asFile + : path.extname(argv.dirPath) !== ''; + + if (argv.empty && isFile) { + throw createCommandError('--empty can only be used with directories.', 400); + } + + const shouldConfirm = !output.json; + + if (shouldConfirm) { + const action = argv.empty + ? `empty directory "${argv.dirPath}"` + : isFile + ? `delete file "${argv.dirPath}"` + : `delete directory "${argv.dirPath}"`; + + await interactiveConfirm(`Are you sure you want to ${action}?`, async () => { + await deleteRemote(env, user, argv.dirPath, isFile, argv.empty, output); + }); + } else { + await deleteRemote(env, user, argv.dirPath, isFile, argv.empty, output); + } + } else if (argv.copy) { + if (!argv.dirPath) { + throw createCommandError('A source path [dirPath] is required for copy operations.', 400); + } + + await copyRemote(env, user, argv.dirPath, argv.copy, output); + } else if (argv.move) { + if (!argv.dirPath) { + throw createCommandError('A source path [dirPath] is required for move operations.', 400); + } + + await moveRemote(env, user, argv.dirPath, argv.move, argv.overwrite, output); } } @@ -356,6 +413,126 @@ async function getFilesStructure(env, user, dirPath, recursive, includeFiles) { } } +async function deleteRemote(env, user, remotePath, isFile, empty, output) { + let endpoint; + let mode; + let data; + + if (isFile) { + endpoint = 'FileDelete'; + mode = 'file'; + const parentDir = path.posix.dirname(remotePath); + data = { + DirectoryPath: parentDir === '.' ? '/' : parentDir, + Ids: [remotePath] + }; + } else if (empty) { + endpoint = 'DirectoryEmpty'; + mode = 'empty'; + data = { Path: remotePath }; + } else { + endpoint = 'DirectoryDelete'; + mode = 'directory'; + data = { Path: remotePath }; + } + + output.log(`${mode === 'empty' ? 'Emptying' : 'Deleting'} ${mode === 'file' ? 'file' : 'directory'}: ${remotePath}`); + + const res = await fetch(`${env.protocol}://${env.host}/Admin/Api/${endpoint}`, { + method: 'POST', + body: JSON.stringify(data), + headers: { + 'Authorization': `Bearer ${user.apiKey}`, + 'Content-Type': 'application/json' + }, + agent: getAgent(env.protocol) + }); + + if (!res.ok) { + throw createCommandError(`Failed to ${mode === 'empty' ? 'empty' : 'delete'} "${remotePath}".`, res.status, await parseJsonSafe(res)); + } + + const body = await parseJsonSafe(res); + + output.setStatus(200); + output.addData({ + type: 'delete', + path: remotePath, + mode, + response: body + }); + + output.log(`Successfully ${mode === 'empty' ? 'emptied' : 'deleted'}: ${remotePath}`); +} + +async function copyRemote(env, user, sourcePath, destination, output) { + output.log(`Copying ${sourcePath} to ${destination}`); + + const res = await fetch(`${env.protocol}://${env.host}/Admin/Api/AssetCopy`, { + method: 'POST', + body: JSON.stringify({ + Destination: destination, + Ids: [sourcePath] + }), + headers: { + 'Authorization': `Bearer ${user.apiKey}`, + 'Content-Type': 'application/json' + }, + agent: getAgent(env.protocol) + }); + + if (!res.ok) { + throw createCommandError(`Failed to copy "${sourcePath}" to "${destination}".`, res.status, await parseJsonSafe(res)); + } + + const body = await parseJsonSafe(res); + + output.setStatus(200); + output.addData({ + type: 'copy', + sourcePath, + destination, + response: body + }); + + output.log(`Successfully copied ${sourcePath} to ${destination}`); +} + +async function moveRemote(env, user, sourcePath, destination, overwrite, output) { + output.log(`Moving ${sourcePath} to ${destination}`); + + const res = await fetch(`${env.protocol}://${env.host}/Admin/Api/AssetMove`, { + method: 'POST', + body: JSON.stringify({ + Destination: destination, + Overwrite: Boolean(overwrite), + Ids: [sourcePath] + }), + headers: { + 'Authorization': `Bearer ${user.apiKey}`, + 'Content-Type': 'application/json' + }, + agent: getAgent(env.protocol) + }); + + if (!res.ok) { + throw createCommandError(`Failed to move "${sourcePath}" to "${destination}".`, res.status, await parseJsonSafe(res)); + } + + const body = await parseJsonSafe(res); + + output.setStatus(200); + output.addData({ + type: 'move', + sourcePath, + destination, + overwrite: Boolean(overwrite), + response: body + }); + + output.log(`Successfully moved ${sourcePath} to ${destination}`); +} + export async function uploadFiles(env, user, localFilePaths, destinationPath, createEmpty = false, overwrite = false, output = createFilesOutput({})) { output.log('Uploading files') @@ -501,6 +678,18 @@ function getFilesOperation(argv) { return 'import'; } + if (argv.delete) { + return 'delete'; + } + + if (argv.copy) { + return 'copy'; + } + + if (argv.move) { + return 'move'; + } + return 'unknown'; } diff --git a/cliv2-documentation.md b/cliv2-documentation.md index 424c828..f09dd95 100644 --- a/cliv2-documentation.md +++ b/cliv2-documentation.md @@ -396,8 +396,12 @@ dw files [dirPath] [outPath] [options] | `-f`, `--includeFiles` | Include files in listings | | `-e`, `--export` | Export from the environment to disk | | `-i`, `--import` | Import from disk to the environment | +| `-d`, `--delete` | Delete a file or directory on the environment | +| `--empty` | Used with `--delete`, empties a directory instead of removing it | +| `--copy ` | Copy a file or directory to the given destination path on the environment | +| `--move ` | Move a file or directory to the given destination path on the environment | | `-r`, `--recursive` | Recurse through subdirectories | -| `-o`, `--overwrite` | Allow overwriting existing files on import | +| `-o`, `--overwrite` | Allow overwriting existing files on import or move | | `--createEmpty` | Create files even if the source file is empty | | `--raw` | Keep exported archives zipped instead of extracting | | `-af`, `--asFile` | Force the source path to be treated as a file | @@ -418,6 +422,60 @@ dw files templates/Translations.xml ./templates -e # Import files from disk, recursively with overwrite dw files ./Files templates -iro + +# Delete a file +dw files /Templates/Designs/old-bundle.js --delete + +# Delete a directory +dw files /Templates/Designs/OldDesign --delete + +# Empty a directory (remove contents, keep the directory) +dw files /Templates/Designs/MyDesign --delete --empty + +# Copy a directory within the environment +dw files /Templates/Designs/MyDesign --copy /Templates/Designs/MyDesign-backup + +# Copy a file to another directory +dw files /Templates/config.json --copy /Templates/Backups + +# Move a directory +dw files /Templates/Designs/OldName --move /Templates/Designs/Archive + +# Move a file with overwrite +dw files /Templates/config.json --move /Templates/Backups --overwrite +``` + +#### Deleting files and directories + +The `--delete` flag removes files and directories from the environment. The CLI uses the same path detection as other operations -- paths with a file extension are treated as files, paths without are treated as directories. Use `--asFile` or `--asDirectory` to override when needed. + +When used interactively, the CLI prompts for confirmation before deleting. In JSON output mode (`--output json`), the confirmation is skipped to support scripted and CI/CD usage. + +The `--empty` flag can be combined with `--delete` to remove the contents of a directory without removing the directory itself. This is useful for cleaning a deployment target before importing fresh files: + +```sh +# Clean and redeploy +dw files /Templates/Designs/MyDesign --delete --empty \ + --host "$TARGET_HOST" $AUTH_FLAGS --output json + +dw files ./dist /Templates/Designs/MyDesign -iro \ + --host "$TARGET_HOST" $AUTH_FLAGS --output json +``` + +#### Copying and moving files and directories + +The `--copy` and `--move` flags operate on files and directories within the environment. Both accept a destination path as their value. These work with both files and directories -- the server handles detection. + +The `--overwrite` flag can be combined with `--move` to overwrite existing files at the destination. + +```sh +# Back up a design before deploying a new version +dw files /Templates/Designs/MyDesign --copy /Templates/Designs/MyDesign-backup \ + --host "$TARGET_HOST" $AUTH_FLAGS --output json + +# Reorganize files on the server +dw files /Templates/OldLocation/config.json --move /Templates/NewLocation --overwrite \ + --host "$TARGET_HOST" $AUTH_FLAGS --output json ``` #### Source type detection @@ -485,6 +543,73 @@ dw files ./Files templates -i -r --output json } ``` +```sh +dw files /Templates/Designs/OldDesign --delete --output json +``` + +```json +{ + "ok": true, + "command": "files", + "operation": "delete", + "status": 200, + "data": [ + { + "type": "delete", + "path": "/Templates/Designs/OldDesign", + "mode": "directory" + } + ], + "errors": [], + "meta": {} +} +``` + +```sh +dw files /Templates/Designs/MyDesign --copy /Templates/Designs/MyDesign-backup --output json +``` + +```json +{ + "ok": true, + "command": "files", + "operation": "copy", + "status": 200, + "data": [ + { + "type": "copy", + "sourcePath": "/Templates/Designs/MyDesign", + "destination": "/Templates/Designs/MyDesign-backup" + } + ], + "errors": [], + "meta": {} +} +``` + +```sh +dw files /Templates/config.json --move /Templates/Backups --output json +``` + +```json +{ + "ok": true, + "command": "files", + "operation": "move", + "status": 200, + "data": [ + { + "type": "move", + "sourcePath": "/Templates/config.json", + "destination": "/Templates/Backups", + "overwrite": false + } + ], + "errors": [], + "meta": {} +} +``` + ### query Run Management API queries, inspect available parameters, or prompt for them interactively. From 9b366ed92551ea786d6dec4de584b2f3b60a2d26 Mon Sep 17 00:00:00 2001 From: nicped Date: Mon, 13 Apr 2026 17:20:38 +0200 Subject: [PATCH 68/86] Add changelog and what's new section for v2 --- CHANGELOG.md | 29 +++++++++++++++++++++++++++++ cliv2-documentation.md | 20 ++++++++++++++++++++ 2 files changed, 49 insertions(+) create mode 100644 CHANGELOG.md diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..a0e0f61 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,29 @@ +# Changelog + +## 2.0.0-beta.0 + +### Breaking changes + +- **Version bumped to 2.0.0** -- this release contains breaking changes to command output, option names, and authentication behavior. +- **`--json` flag replaced by `--output json`** -- the old `--json` flag is deprecated and will be removed in a future release. All commands now use `--output json` for structured output. +- **`--iamstupid` replaced by `--dangerouslyIncludeLogsAndCache`** -- the old flag is deprecated and will be removed in a future release. +- **Error handling changed** -- commands that previously printed an error and exited silently now throw structured errors. In JSON mode, errors are returned in the `errors` array with `ok: false`. The CLI exits with code `1` on any error. + +### New features + +- **OAuth client credentials authentication** -- the CLI now supports OAuth 2.0 client credentials for headless and CI/CD authentication. Use `dw login --oauth` to configure an environment, or pass `--auth oauth` with `--clientIdEnv`/`--clientSecretEnv` (or `--clientId`/`--clientSecret`) on any command. +- **Structured JSON output on all API commands** -- `env`, `login`, `files`, `query`, and `command` all support `--output json`, returning a consistent envelope with `ok`, `command`, `operation`, `status`, `data`, `errors`, and `meta` fields. +- **File delete operations** -- `dw files --delete` removes files and directories from the environment. Combine with `--empty` to clear a directory without removing it. +- **File copy operations** -- `dw files --copy ` copies files and directories within the environment. +- **File move operations** -- `dw files --move ` moves files and directories within the environment. Combine with `--overwrite` to replace existing files at the destination. +- **Global OAuth flags** -- `--auth`, `--clientId`, `--clientSecret`, `--clientIdEnv`, and `--clientSecretEnv` are available as global options on all commands. +- **Authentication precedence** -- when multiple auth indicators are present, the CLI resolves them in order: `--apiKey` > OAuth > saved user > interactive prompt. Use `--auth user` to force user auth when an environment is configured for OAuth. +- **Base command shows auth type** -- `dw` with no arguments now displays the current authentication type (OAuth or user) alongside environment info. + +### Improvements + +- **Consistent error model** -- all commands use a shared `createCommandError` helper that produces errors with `message`, `status`, and `details`. In JSON mode these are serialized into the `errors` array. +- **Human output suppressed in JSON mode** -- when `--output json` is active, all `console.log` output is suppressed. Only the JSON envelope is written to stdout, making it safe to pipe. +- **Interactive prompts skipped in JSON mode** -- delete confirmations and other interactive prompts are skipped when `--output json` is set, enabling fully non-interactive scripted usage. +- **Better host override handling** -- `--host` now works with OAuth credentials, not just `--apiKey`. +- **Null-safe config access** -- environment and user lookups use optional chaining to avoid crashes on missing config keys. diff --git a/cliv2-documentation.md b/cliv2-documentation.md index f09dd95..0451efe 100644 --- a/cliv2-documentation.md +++ b/cliv2-documentation.md @@ -12,6 +12,26 @@ The CLI is designed to be composable. Every API-driven command supports structur If you need to do something once, interactively, the DynamicWeb backend UI is usually faster. If you need to do it repeatedly, across environments, or as part of a build -- that is what the CLI is for. +## What's new in v2 + +Version 2 is an automation-first overhaul. The headline changes: + +- **OAuth client credentials** -- the CLI can now authenticate with an OAuth 2.0 client ID and secret, removing the need for an interactive login in CI/CD pipelines and service-account scenarios. See [Authentication](#authentication) for setup details. +- **Structured JSON output** -- every API-driven command (`env`, `login`, `files`, `query`, `command`) supports `--output json`, returning a consistent envelope with `ok`, `status`, `data`, `errors`, and `meta` fields. Interactive prompts are suppressed in JSON mode so output is safe to pipe. See [Automation and JSON output](#automation-and-json-output). +- **File delete, copy, and move** -- the `files` command can now delete, copy, and move files and directories on the environment in addition to listing, exporting, and importing. See the [files command reference](#files). +- **Consistent error model** -- commands that previously printed a message and exited silently now return structured errors (in JSON mode) or throw with a non-zero exit code. Scripts can rely on exit code `1` and the `errors` array for programmatic error handling. + +### Migrating from v1 + +| v1 | v2 | Notes | +|----|-----|-------| +| `--json` | `--output json` | `--json` still works but is deprecated | +| `--iamstupid` | `--dangerouslyIncludeLogsAndCache` | `--iamstupid` still works but is deprecated | +| `--host` (required `--apiKey`) | `--host` (works with `--apiKey` or OAuth) | OAuth credentials are now accepted alongside `--host` | +| Errors printed to stdout | Errors in `errors` array (JSON) or thrown (non-JSON) | Scripts should check exit codes and/or `ok` field | + +No changes are required for existing scripts that do not use `--json` or `--iamstupid`. Both deprecated flags continue to work and emit a warning. + ## Installation The CLI requires **Node.js 20.12.0 or later**. From 3fd5ce9e95421320f57fdfa010dda7d73b1b7afe Mon Sep 17 00:00:00 2001 From: nicped Date: Mon, 13 Apr 2026 17:32:34 +0200 Subject: [PATCH 69/86] Refactor command handling and improve output logging across CLI commands --- README.md | 2 +- bin/commands/command.js | 17 +++-------------- bin/commands/env.js | 10 +++++----- bin/commands/files.js | 40 ++++++++++++++++++++++++---------------- bin/commands/login.js | 10 +++++----- bin/commands/query.js | 19 ++++++++++--------- bin/index.js | 15 ++++++++++----- 7 files changed, 58 insertions(+), 55 deletions(-) diff --git a/README.md b/README.md index 558321c..8cf2155 100644 --- a/README.md +++ b/README.md @@ -48,7 +48,7 @@ Set up an environment and log in with a user: ```sh dw env dev dw login -dw +dw # shows current environment, user, protocol, and host ``` Run a query: diff --git a/bin/commands/command.js b/bin/commands/command.js index 05e1648..a52f764 100644 --- a/bin/commands/command.js +++ b/bin/commands/command.js @@ -33,18 +33,18 @@ export function commandCommand() { try { output.verboseLog(`Running command ${argv.command}`); await handleCommand(argv, output); - output.finish(); } catch (err) { output.fail(err); - output.finish(); process.exit(1); + } finally { + output.finish(); } } } } async function handleCommand(argv, output) { - let env = await setupEnv(argv); + let env = await setupEnv(argv, output); let user = await setupUser(argv, env); if (argv.list) { const properties = await getProperties(env, user, argv.command); @@ -59,17 +59,6 @@ async function handleCommand(argv, output) { async function getProperties(env, user, command) { return `This option currently doesn't work` - let res = await fetch(`${env.protocol}://${env.host}/Admin/Api/CommandByName?name=${command}`, { - method: 'GET', - headers: { - 'Authorization': `Bearer ${user.apiKey}` - }, - agent: getAgent(env.protocol) - }) - if (res.ok) { - let body = await res.json() - return body.model.propertyNames - } } function getQueryParams(argv) { diff --git a/bin/commands/env.js b/bin/commands/env.js index 987a560..2bfe50d 100644 --- a/bin/commands/env.js +++ b/bin/commands/env.js @@ -51,17 +51,17 @@ export function envCommand() { try { await handleEnv(argv, output); - output.finish(); } catch (err) { output.fail(err); - output.finish(); process.exit(1); + } finally { + output.finish(); } } } } -export async function setupEnv(argv) { +export async function setupEnv(argv, output = null) { let env = {}; let askEnv = true; @@ -95,7 +95,7 @@ export async function setupEnv(argv) { interactive: { default: true } - }) + }, output) env = getConfig().env[getConfig()?.current?.env]; } @@ -231,7 +231,7 @@ async function changeEnv(argv, output) { } export function isJsonOutput(argv) { - return argv?.json === true || argv?.output === 'json'; + return argv?.output === 'json'; } export function createCommandError(message, status = 1, details = null) { diff --git a/bin/commands/files.js b/bin/commands/files.js index ca6af3f..e3498c9 100644 --- a/bin/commands/files.js +++ b/bin/commands/files.js @@ -119,23 +119,23 @@ export function filesCommand() { const output = createFilesOutput(argv); try { - output.verboseLog(`Listing directory at: ${argv.dirPath}`); await handleFiles(argv, output); - output.finish(); } catch (err) { output.fail(err); - output.finish(); process.exit(1); + } finally { + output.finish(); } } } } async function handleFiles(argv, output) { - let env = await setupEnv(argv); + let env = await setupEnv(argv, output); let user = await setupUser(argv, env); if (argv.list) { + output.verboseLog(`Listing directory at: ${argv.dirPath}`); let files = (await getFilesStructure(env, user, argv.dirPath, argv.recursive, argv.includeFiles)).model; output.setStatus(200); output.addData(files); @@ -150,9 +150,7 @@ async function handleFiles(argv, output) { if (argv.export) { if (argv.dirPath) { - const isFile = argv.asFile || argv.asDirectory - ? argv.asFile - : path.extname(argv.dirPath) !== ''; + const isFile = isFilePath(argv, argv.dirPath); if (isFile) { let parentDirectory = path.dirname(argv.dirPath); @@ -190,9 +188,7 @@ async function handleFiles(argv, output) { throw createCommandError('A path is required for delete operations.', 400); } - const isFile = argv.asFile || argv.asDirectory - ? argv.asFile - : path.extname(argv.dirPath) !== ''; + const isFile = isFilePath(argv, argv.dirPath); if (argv.empty && isFile) { throw createCommandError('--empty can only be used with directories.', 400); @@ -395,7 +391,11 @@ async function extractArchive(filename, filePath, outPath, raw, output) { } output.log(`Finished extracting ${filename} to ${outPath}\n`); - fs.unlink(filePath, function(err) {}); + fs.unlink(filePath, function(err) { + if (err) { + output.verboseLog(`Warning: Failed to delete temporary archive ${filePath}: ${err.message}`); + } + }); } async function getFilesStructure(env, user, dirPath, recursive, includeFiles) { @@ -543,10 +543,10 @@ export async function uploadFiles(env, user, localFilePaths, destinationPath, cr chunks.push(localFilePaths.slice(i, i + chunkSize)); } - output.mergeMeta({ - filesProcessed: (output.response.meta.filesProcessed || 0) + localFilePaths.length, - chunks: (output.response.meta.chunks || 0) + chunks.length - }); + output.mergeMeta((meta) => ({ + filesProcessed: (meta.filesProcessed || 0) + localFilePaths.length, + chunks: (meta.chunks || 0) + chunks.length + })); for (let i = 0; i < chunks.length; i++) { output.log(`Uploading chunk ${i + 1} of ${chunks.length}`); @@ -607,6 +607,13 @@ export function resolveFilePath(filePath) { } +function isFilePath(argv, dirPath) { + if (argv.asFile || argv.asDirectory) { + return argv.asFile; + } + return path.extname(dirPath) !== ''; +} + function wildcardToRegExp(wildcard) { const escaped = wildcard.replace(/[.+?^${}()|[\]\\]/g, '\\$&'); return new RegExp('^' + escaped.replace(/\*/g, '.*') + '$'); @@ -640,7 +647,8 @@ function createFilesOutput(argv) { addData(entry) { response.data.push(entry); }, - mergeMeta(meta) { + mergeMeta(metaOrFn) { + const meta = typeof metaOrFn === 'function' ? metaOrFn(response.meta) : metaOrFn; response.meta = { ...response.meta, ...meta diff --git a/bin/commands/login.js b/bin/commands/login.js index f4d4a35..883dced 100644 --- a/bin/commands/login.js +++ b/bin/commands/login.js @@ -30,11 +30,11 @@ export function loginCommand() { try { await handleLogin(argv, output); - output.finish(); } catch (err) { output.fail(err); - output.finish(); process.exit(1); + } finally { + output.finish(); } } } @@ -91,7 +91,7 @@ export async function setupUser(argv, env) { async function handleLogin(argv, output) { if (shouldUseOAuth(argv, getCurrentEnv(argv))) { - output.addData(await interactiveOAuthLogin(argv)); + output.addData(await interactiveOAuthLogin(argv, output)); } else if (argv.user) { output.addData(await changeUser(argv)); } else { @@ -286,7 +286,7 @@ async function changeUser(argv) { }; } -async function interactiveOAuthLogin(argv) { +async function interactiveOAuthLogin(argv, output) { verboseLog(argv, 'Configuring OAuth client credentials authentication'); const currentEnvName = getConfig()?.current?.env || 'dev'; @@ -336,7 +336,7 @@ async function interactiveOAuthLogin(argv) { interactive: { default: true } - }); + }, output); } } diff --git a/bin/commands/query.js b/bin/commands/query.js index e9e356c..98ae5ad 100644 --- a/bin/commands/query.js +++ b/bin/commands/query.js @@ -24,7 +24,8 @@ export function queryCommand() { }) .option('output', { choices: ['json'], - describe: 'Outputs a single JSON response for automation-friendly parsing' + describe: 'Outputs a single JSON response for automation-friendly parsing', + conflicts: 'interactive' }) }, handler: async (argv) => { @@ -33,35 +34,35 @@ export function queryCommand() { try { output.verboseLog(`Running query ${argv.query}`); await handleQuery(argv, output); - output.finish(); } catch (err) { output.fail(err); if (!output.json) { console.error(err.stack || err.message || String(err)); } - output.finish(); process.exit(1); + } finally { + output.finish(); } } } } async function handleQuery(argv, output) { - let env = await setupEnv(argv); + let env = await setupEnv(argv, output); let user = await setupUser(argv, env); if (argv.list) { const properties = await getProperties(env, user, argv.query); output.addData(properties); output.log(properties); } else { - let response = await runQuery(env, user, argv.query, await getQueryParams(env, user, argv)); + let response = await runQuery(env, user, argv.query, await getQueryParams(env, user, argv, output)); output.addData(response); output.log(response); } } async function getProperties(env, user, query) { - let res = await fetch(`${env.protocol}://${env.host}/Admin/Api/QueryByName?name=${query}`, { + let res = await fetch(`${env.protocol}://${env.host}/Admin/Api/QueryByName?name=${encodeURIComponent(query)}`, { method: 'GET', headers: { 'Authorization': `Bearer ${user.apiKey}` @@ -79,12 +80,12 @@ async function getProperties(env, user, query) { throw createCommandError('Unable to fetch query parameters.', res.status, await parseJsonSafe(res)); } -async function getQueryParams(env, user, argv) { +async function getQueryParams(env, user, argv, output) { let params = {} if (argv.interactive) { let properties = await getProperties(env, user, argv.query); - console.log('The following properties will be requested:') - console.log(properties) + output.log('The following properties will be requested:') + output.log(properties) for (const p of properties) { const value = await input({ message: p }); if (value) { diff --git a/bin/index.js b/bin/index.js index 954755b..48cac19 100644 --- a/bin/index.js +++ b/bin/index.js @@ -33,7 +33,7 @@ yargs(hideBin(process.argv)) description: 'Run with verbose logging' }) .option('protocol', { - description: 'Allows setting the protocol used, only used together with --host, defaulting to https' + description: 'Set the protocol used with --host (defaults to https)' }) .option('host', { description: 'Allows setting the host used, only allowed if an --apiKey or OAuth client credentials are specified' @@ -65,12 +65,17 @@ function baseCommand() { command: '$0', describe: 'Shows the current env and user being used', handler: () => { - if (Object.keys(getConfig()).length === 0) { + const cfg = getConfig(); + if (Object.keys(cfg).length === 0) { console.log('To login to a solution use `dw login`') return; } - const cfg = getConfig(); const currentEnv = cfg?.env?.[cfg?.current?.env]; + if (!currentEnv) { + console.log(`Environment '${cfg?.current?.env}' is not configured.`); + console.log('To login to a solution use `dw login`'); + return; + } const authType = currentEnv?.current?.authType; console.log(`Environment: ${cfg?.current?.env}`); @@ -79,8 +84,8 @@ function baseCommand() { } else if (currentEnv?.current?.user) { console.log(`User: ${currentEnv.current.user}`); } - console.log(`Protocol: ${currentEnv?.protocol}`); - console.log(`Host: ${currentEnv?.host}`); + console.log(`Protocol: ${currentEnv.protocol}`); + console.log(`Host: ${currentEnv.host}`); } } } From a66429467eb3f74096be7a3a70dd01d745d9b594 Mon Sep 17 00:00:00 2001 From: nicped Date: Mon, 13 Apr 2026 22:58:31 +0200 Subject: [PATCH 70/86] Fix JSON mode issues: skip interactive prompts, respect --host override, improve error handling - Login: block interactive prompts in JSON mode; add non-interactive OAuth login path; always apply --host override on existing environments - Files: skip full-export confirmation in JSON mode - Command: throw error for unimplemented --list instead of returning string - Login: extract shared OAuth finalization into finalizeOAuthLogin helper - Install: add JSON output example to README for documentation consistency - Docs: include install in --output json command lists --- CHANGELOG.md | 2 +- README.md | 42 +++++++++++++- bin/commands/command.js | 4 +- bin/commands/env.js | 2 +- bin/commands/files.js | 15 +++-- bin/commands/install.js | 120 ++++++++++++++++++++++++++++++++++------ bin/commands/login.js | 103 ++++++++++++++++++++++------------ bin/commands/query.js | 2 +- cliv2-documentation.md | 6 +- 9 files changed, 229 insertions(+), 67 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index a0e0f61..657eb47 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,7 +12,7 @@ ### New features - **OAuth client credentials authentication** -- the CLI now supports OAuth 2.0 client credentials for headless and CI/CD authentication. Use `dw login --oauth` to configure an environment, or pass `--auth oauth` with `--clientIdEnv`/`--clientSecretEnv` (or `--clientId`/`--clientSecret`) on any command. -- **Structured JSON output on all API commands** -- `env`, `login`, `files`, `query`, and `command` all support `--output json`, returning a consistent envelope with `ok`, `command`, `operation`, `status`, `data`, `errors`, and `meta` fields. +- **Structured JSON output on all API commands** -- `env`, `login`, `files`, `query`, `command`, and `install` all support `--output json`, returning a consistent envelope with `ok`, `command`, `operation`, `status`, `data`, `errors`, and `meta` fields. - **File delete operations** -- `dw files --delete` removes files and directories from the environment. Combine with `--empty` to clear a directory without removing it. - **File copy operations** -- `dw files --copy ` copies files and directories within the environment. - **File move operations** -- `dw files --move ` moves files and directories within the environment. Combine with `--overwrite` to replace existing files at the destination. diff --git a/README.md b/README.md index 8cf2155..d86785b 100644 --- a/README.md +++ b/README.md @@ -28,7 +28,7 @@ npm install -g . The `2.0` beta is a substantial overhaul focused on automation and modern authentication. -- Automation-first command output: `env`, `login`, `files`, `query`, and `command` now support `--output json` so scripts and pipelines can consume structured results instead of plain console logs. +- Automation-first command output: `env`, `login`, `files`, `query`, `command`, and `install` now support `--output json` so scripts and pipelines can consume structured results instead of plain console logs. - OAuth client credentials support: the CLI can now authenticate with OAuth 2.0 `client_credentials`, which makes headless and CI/CD usage much easier. - Better environment handling: protocol, host, and auth details are stored more cleanly in `~/.dwc`, while one-off runs can still override host and credentials directly. - Improved file workflows: file import, export, recursive sync, raw archive export, progress reporting, and source-type override flags make file operations more predictable. @@ -376,6 +376,46 @@ Upload and install a `.dll` or `.nupkg` add-in into the current environment. ```sh dw install ./bin/Release/net10.0/CustomProject.dll +dw install ./bin/Release/net10.0/CustomProject.dll --queue --output json +``` + +Example JSON output: + +```json +{ + "ok": true, + "command": "install", + "operation": "queue", + "status": 200, + "data": [ + { + "type": "upload", + "destinationPath": "System/AddIns/Local", + "files": [ + "/workspace/bin/Release/net10.0/CustomProject.dll" + ], + "response": { + "message": "Upload completed" + } + }, + { + "type": "install", + "filePath": "/workspace/bin/Release/net10.0/CustomProject.dll", + "filename": "CustomProject.dll", + "queued": true, + "response": { + "success": true, + "message": "Addin installed" + } + } + ], + "errors": [], + "meta": { + "filePath": "./bin/Release/net10.0/CustomProject.dll", + "filesProcessed": 1, + "chunks": 1 + } +} ``` ### `dw config` diff --git a/bin/commands/command.js b/bin/commands/command.js index a52f764..3c072f8 100644 --- a/bin/commands/command.js +++ b/bin/commands/command.js @@ -35,7 +35,7 @@ export function commandCommand() { await handleCommand(argv, output); } catch (err) { output.fail(err); - process.exit(1); + process.exitCode = 1; } finally { output.finish(); } @@ -58,7 +58,7 @@ async function handleCommand(argv, output) { } async function getProperties(env, user, command) { - return `This option currently doesn't work` + throw createCommandError('The --list option is not currently implemented for commands.'); } function getQueryParams(argv) { diff --git a/bin/commands/env.js b/bin/commands/env.js index 2bfe50d..ad21d22 100644 --- a/bin/commands/env.js +++ b/bin/commands/env.js @@ -53,7 +53,7 @@ export function envCommand() { await handleEnv(argv, output); } catch (err) { output.fail(err); - process.exit(1); + process.exitCode = 1; } finally { output.finish(); } diff --git a/bin/commands/files.js b/bin/commands/files.js index e3498c9..8f66df5 100644 --- a/bin/commands/files.js +++ b/bin/commands/files.js @@ -122,7 +122,7 @@ export function filesCommand() { await handleFiles(argv, output); } catch (err) { output.fail(err); - process.exit(1); + process.exitCode = 1; } finally { output.finish(); } @@ -161,7 +161,7 @@ async function handleFiles(argv, output) { await download(env, user, argv.dirPath, argv.outPath, true, null, argv.raw, argv.dangerouslyIncludeLogsAndCache, [], false, output); } } else { - await interactiveConfirm('Are you sure you want a full export of files?', async () => { + const fullExport = async () => { output.log('Full export is starting'); let filesStructure = (await getFilesStructure(env, user, '/', false, true)).model; let dirs = filesStructure.directories; @@ -171,7 +171,13 @@ async function handleFiles(argv, output) { } await download(env, user, '/.', argv.outPath, false, 'Base.zip', argv.raw, argv.dangerouslyIncludeLogsAndCache, Array.from(filesStructure.files.data, f => f.name), false, output); if (argv.raw) output.log('The files in the base "files" folder is in Base.zip, each directory in "files" is in its own zip'); - }) + }; + + if (output.json) { + await fullExport(); + } else { + await interactiveConfirm('Are you sure you want a full export of files?', fullExport); + } } } else if (argv.import) { if (argv.dirPath && argv.outPath) { @@ -600,8 +606,7 @@ export function resolveFilePath(filePath) { let resolvedPath = fs.readdirSync(p.dir).filter((allFilesPaths) => allFilesPaths.match(regex) !== null)[0] if (resolvedPath === undefined) { - console.log('Could not find any files with the name ' + filePath); - process.exit(1); + throw createCommandError(`Could not find any files with the name ${filePath}`, 1); } return path.join(p.dir, resolvedPath); } diff --git a/bin/commands/install.js b/bin/commands/install.js index 61aaa2d..fe9dc82 100644 --- a/bin/commands/install.js +++ b/bin/commands/install.js @@ -6,8 +6,8 @@ import { uploadFiles, resolveFilePath } from './files.js'; export function installCommand() { return { - command: 'install [filePath]', - describe: 'Installs the addin on the given path, allowed file extensions are .dll, .nupkg', + command: 'install [filePath]', + describe: 'Installs the addin on the given path, allowed file extensions are .dll, .nupkg', builder: (yargs) => { return yargs .positional('filePath', { @@ -18,24 +18,37 @@ export function installCommand() { type: 'boolean', describe: 'Queues the install for next Dynamicweb recycle' }) + .option('output', { + choices: ['json'], + describe: 'Outputs a single JSON response for automation-friendly parsing' + }) }, handler: async (argv) => { - if (argv.verbose) console.info(`Installing file located at :${argv.filePath}`) - await handleInstall(argv) + const output = createInstallOutput(argv); + + try { + output.verboseLog(`Installing file located at: ${argv.filePath}`); + await handleInstall(argv, output); + } catch (err) { + output.fail(err); + process.exitCode = 1; + } finally { + output.finish(); + } } } } -async function handleInstall(argv) { - let env = await setupEnv(argv); +async function handleInstall(argv, output) { + let env = await setupEnv(argv, output); let user = await setupUser(argv, env); let resolvedPath = resolveFilePath(argv.filePath); - await uploadFiles(env, user, [ resolvedPath ], 'System/AddIns/Local', false, true); - await installAddin(env, user, resolvedPath, argv.queue) + await uploadFiles(env, user, [ resolvedPath ], 'System/AddIns/Local', false, true, output); + await installAddin(env, user, resolvedPath, argv.queue, output); } -async function installAddin(env, user, resolvedPath, queue) { - console.log('Installing addin') +async function installAddin(env, user, resolvedPath, queue, output) { + output.log('Installing addin'); let filename = path.basename(resolvedPath); let data = { 'Queue': queue, @@ -54,12 +67,85 @@ async function installAddin(env, user, resolvedPath, queue) { }); if (res.ok) { - if (env.verbose) console.log(await res.json()) - console.log(`Addin installed`) + const body = await parseJsonSafe(res); + output.verboseLog(body); + output.addData({ + type: 'install', + filePath: resolvedPath, + filename, + queued: Boolean(queue), + response: body + }); + output.log('Addin installed'); + } else { + const body = await parseJsonSafe(res); + throw createCommandError('Addin installation failed.', res.status, body); } - else { - console.log('Request failed, returned error:') - console.log(await res.json()) - process.exit(1); +} + +function createInstallOutput(argv) { + const response = { + ok: true, + command: 'install', + operation: argv.queue ? 'queue' : 'install', + status: 200, + data: [], + errors: [], + meta: { + filePath: argv.filePath + } + }; + + return { + json: argv.output === 'json', + response, + log(...args) { + if (!this.json) { + console.log(...args); + } + }, + verboseLog(...args) { + if (argv.verbose && !this.json) { + console.info(...args); + } + }, + addData(entry) { + response.data.push(entry); + }, + mergeMeta(metaOrFn) { + const meta = typeof metaOrFn === 'function' ? metaOrFn(response.meta) : metaOrFn; + response.meta = { + ...response.meta, + ...meta + }; + }, + fail(err) { + response.ok = false; + response.status = err?.status || 1; + response.errors.push({ + message: err?.message || 'Unknown install command error.', + details: err?.details ?? null + }); + }, + finish() { + if (this.json) { + console.log(JSON.stringify(response, null, 2)); + } + } + }; +} + +function createCommandError(message, status, details = null) { + const error = new Error(message); + error.status = status; + error.details = details; + return error; +} + +async function parseJsonSafe(res) { + try { + return await res.json(); + } catch { + return null; } -} \ No newline at end of file +} diff --git a/bin/commands/login.js b/bin/commands/login.js index 883dced..8b678ea 100644 --- a/bin/commands/login.js +++ b/bin/commands/login.js @@ -32,7 +32,7 @@ export function loginCommand() { await handleLogin(argv, output); } catch (err) { output.fail(err); - process.exit(1); + process.exitCode = 1; } finally { output.finish(); } @@ -91,20 +91,27 @@ export async function setupUser(argv, env) { async function handleLogin(argv, output) { if (shouldUseOAuth(argv, getCurrentEnv(argv))) { - output.addData(await interactiveOAuthLogin(argv, output)); + if (isJsonOutput(argv)) { + output.addData(await nonInteractiveOAuthLogin(argv)); + } else { + output.addData(await interactiveOAuthLogin(argv, output)); + } } else if (argv.user) { output.addData(await changeUser(argv)); } else { + if (isJsonOutput(argv)) { + throw createCommandError('Interactive login is not supported with --output json. Use --apiKey, or configure OAuth with --oauth --clientIdEnv/--clientSecretEnv.'); + } output.addData(await interactiveLogin(argv, { environment: { type: 'input', default: getConfig()?.current?.env || 'dev', prompt: 'if-no-arg' }, - username: { + username: { type: 'input' }, - password: { + password: { type: 'password' }, interactive: { @@ -317,58 +324,82 @@ async function interactiveOAuthLogin(argv, output) { }); } - if (!getConfig().env || !getConfig().env[result.environment] || !getConfig().env[result.environment].host || !getConfig().env[result.environment].protocol) { - if (argv.host) { - ensureEnvironmentFromArgs(result.environment, argv); - } else { - logMessage(argv, 'The environment specified is missing parameters, please specify them'); - await interactiveEnv(argv, { - environment: { - type: 'input', - default: result.environment, - prompt: 'never' - }, - host: { - describe: 'Enter your host including protocol, i.e "https://yourHost.com":', - type: 'input', - prompt: 'always' - }, - interactive: { - default: true - } - }, output); - } + if (argv.host) { + ensureEnvironmentFromArgs(result.environment, argv); + } else if (!getConfig().env || !getConfig().env[result.environment] || !getConfig().env[result.environment].host || !getConfig().env[result.environment].protocol) { + logMessage(argv, 'The environment specified is missing parameters, please specify them'); + await interactiveEnv(argv, { + environment: { + type: 'input', + default: result.environment, + prompt: 'never' + }, + host: { + describe: 'Enter your host including protocol, i.e "https://yourHost.com":', + type: 'input', + prompt: 'always' + }, + interactive: { + default: true + } + }, output); + } + + const oauthResult = await finalizeOAuthLogin(result.environment, result.clientIdEnv, result.clientSecretEnv, argv); + + logMessage(argv, `OAuth authentication is now configured for ${result.environment}`); + + return oauthResult; +} + +async function nonInteractiveOAuthLogin(argv) { + verboseLog(argv, 'Configuring OAuth client credentials authentication (non-interactive)'); + + const environment = getConfig()?.current?.env; + if (!environment) { + throw createCommandError('No environment set. Configure one with "dw env" first.'); + } + + if (argv.host) { + ensureEnvironmentFromArgs(environment, argv); + } else if (!getConfig().env?.[environment]?.host) { + throw createCommandError(`Environment "${environment}" has no host configured. Pass --host or set it up with "dw env" first.`); } - const env = getConfig().env[result.environment]; + const clientIdEnv = argv.clientIdEnv || getConfig().env?.[environment]?.auth?.clientIdEnv || DEFAULT_CLIENT_ID_ENV; + const clientSecretEnv = argv.clientSecretEnv || getConfig().env?.[environment]?.auth?.clientSecretEnv || DEFAULT_CLIENT_SECRET_ENV; + + return await finalizeOAuthLogin(environment, clientIdEnv, clientSecretEnv, argv); +} + +async function finalizeOAuthLogin(environment, clientIdEnv, clientSecretEnv, argv) { + const env = getConfig().env[environment]; const oauthConfig = resolveOAuthConfig({ ...argv, - clientIdEnv: result.clientIdEnv, - clientSecretEnv: result.clientSecretEnv, + clientIdEnv, + clientSecretEnv, oauth: true }, env); const tokenResult = await fetchOAuthToken(env, oauthConfig, argv.verbose); getConfig().current = getConfig().current || {}; - getConfig().current.env = result.environment; + getConfig().current.env = environment; env.auth = { type: 'oauth_client_credentials', - clientIdEnv: result.clientIdEnv, - clientSecretEnv: result.clientSecretEnv + clientIdEnv, + clientSecretEnv }; env.current = env.current || {}; env.current.authType = 'oauth_client_credentials'; delete env.current.user; updateConfig(); - logMessage(argv, `OAuth authentication is now configured for ${result.environment}`); - return { - environment: result.environment, + environment, authType: 'oauth_client_credentials', - clientIdEnv: result.clientIdEnv, - clientSecretEnv: result.clientSecretEnv, + clientIdEnv, + clientSecretEnv, expires: tokenResult.expires || null }; } diff --git a/bin/commands/query.js b/bin/commands/query.js index 98ae5ad..de7d61d 100644 --- a/bin/commands/query.js +++ b/bin/commands/query.js @@ -39,7 +39,7 @@ export function queryCommand() { if (!output.json) { console.error(err.stack || err.message || String(err)); } - process.exit(1); + process.exitCode = 1; } finally { output.finish(); } diff --git a/cliv2-documentation.md b/cliv2-documentation.md index 0451efe..13d5d2f 100644 --- a/cliv2-documentation.md +++ b/cliv2-documentation.md @@ -17,7 +17,7 @@ If you need to do something once, interactively, the DynamicWeb backend UI is us Version 2 is an automation-first overhaul. The headline changes: - **OAuth client credentials** -- the CLI can now authenticate with an OAuth 2.0 client ID and secret, removing the need for an interactive login in CI/CD pipelines and service-account scenarios. See [Authentication](#authentication) for setup details. -- **Structured JSON output** -- every API-driven command (`env`, `login`, `files`, `query`, `command`) supports `--output json`, returning a consistent envelope with `ok`, `status`, `data`, `errors`, and `meta` fields. Interactive prompts are suppressed in JSON mode so output is safe to pipe. See [Automation and JSON output](#automation-and-json-output). +- **Structured JSON output** -- every API-driven command (`env`, `login`, `files`, `query`, `command`, `install`) supports `--output json`, returning a consistent envelope with `ok`, `status`, `data`, `errors`, and `meta` fields. Interactive prompts are suppressed in JSON mode so output is safe to pipe. See [Automation and JSON output](#automation-and-json-output). - **File delete, copy, and move** -- the `files` command can now delete, copy, and move files and directories on the environment in addition to listing, exporting, and importing. See the [files command reference](#files). - **Consistent error model** -- commands that previously printed a message and exited silently now return structured errors (in JSON mode) or throw with a non-zero exit code. Scripts can rely on exit code `1` and the `errors` array for programmatic error handling. @@ -165,7 +165,7 @@ Use `--auth user` to force user authentication even when an environment is confi ## Automation and JSON output -Commands that talk to the Management API -- `env`, `login`, `files`, `query`, and `command` -- support `--output json`. This returns a structured envelope instead of human-readable console output: +Commands that talk to the Management API -- `env`, `login`, `files`, `query`, `command`, and `install` -- support `--output json`. This returns a structured envelope instead of human-readable console output: ```json { @@ -737,7 +737,7 @@ dw command PageDelete --json '{ "id": "1383" }' --output json Upload and install a `.dll` or `.nupkg` add-in into the current environment. ```sh -dw install [--queue] +dw install [--queue] [--output json] ``` **Immediate installation (default):** From 472778df98610544f7209dad0672bdd301fb4f92 Mon Sep 17 00:00:00 2001 From: nicped Date: Mon, 13 Apr 2026 23:03:29 +0200 Subject: [PATCH 71/86] Refactor command error handling: consolidate createCommandError function into env.js --- bin/commands/command.js | 8 +------- bin/commands/files.js | 8 +------- bin/commands/install.js | 8 +------- bin/commands/query.js | 8 +------- 4 files changed, 4 insertions(+), 28 deletions(-) diff --git a/bin/commands/command.js b/bin/commands/command.js index 3c072f8..72fd603 100644 --- a/bin/commands/command.js +++ b/bin/commands/command.js @@ -1,7 +1,7 @@ import fetch from 'node-fetch'; import path from 'path'; import fs from 'fs'; -import { setupEnv, getAgent } from './env.js'; +import { setupEnv, getAgent, createCommandError } from './env.js'; import { setupUser } from './login.js'; const exclude = ['_', '$0', 'command', 'list', 'json', 'verbose', 'v', 'host', 'protocol', 'apiKey', 'env', 'output', 'auth', 'clientId', 'clientSecret', 'clientIdEnv', 'clientSecretEnv', 'oauth'] @@ -137,12 +137,6 @@ function createCommandOutput(argv) { }; } -function createCommandError(message, status, details = null) { - const error = new Error(message); - error.status = status; - error.details = details; - return error; -} async function parseJsonSafe(res) { try { diff --git a/bin/commands/files.js b/bin/commands/files.js index 8f66df5..ccf1ff8 100644 --- a/bin/commands/files.js +++ b/bin/commands/files.js @@ -2,7 +2,7 @@ import fetch from 'node-fetch'; import path from 'path'; import fs from 'fs'; import FormData from 'form-data'; -import { setupEnv, getAgent } from './env.js'; +import { setupEnv, getAgent, createCommandError } from './env.js'; import { setupUser } from './login.js'; import { interactiveConfirm, formatBytes, createThrottledStatusUpdater } from '../utils.js'; import { downloadWithProgress, tryGetFileNameFromResponse } from '../downloader.js'; @@ -706,12 +706,6 @@ function getFilesOperation(argv) { return 'unknown'; } -function createCommandError(message, status, details = null) { - const error = new Error(message); - error.status = status; - error.details = details; - return error; -} async function parseJsonSafe(res) { try { diff --git a/bin/commands/install.js b/bin/commands/install.js index fe9dc82..6bc6a2f 100644 --- a/bin/commands/install.js +++ b/bin/commands/install.js @@ -1,6 +1,6 @@ import fetch from 'node-fetch'; import path from 'path'; -import { setupEnv, getAgent } from './env.js'; +import { setupEnv, getAgent, createCommandError } from './env.js'; import { setupUser } from './login.js'; import { uploadFiles, resolveFilePath } from './files.js'; @@ -135,12 +135,6 @@ function createInstallOutput(argv) { }; } -function createCommandError(message, status, details = null) { - const error = new Error(message); - error.status = status; - error.details = details; - return error; -} async function parseJsonSafe(res) { try { diff --git a/bin/commands/query.js b/bin/commands/query.js index de7d61d..fd9e31b 100644 --- a/bin/commands/query.js +++ b/bin/commands/query.js @@ -1,5 +1,5 @@ import fetch from 'node-fetch'; -import { setupEnv, getAgent } from './env.js'; +import { setupEnv, getAgent, createCommandError } from './env.js'; import { setupUser } from './login.js'; import { input } from '@inquirer/prompts'; @@ -158,12 +158,6 @@ function createQueryOutput(argv) { }; } -function createCommandError(message, status, details = null) { - const error = new Error(message); - error.status = status; - error.details = details; - return error; -} async function parseJsonSafe(res) { try { From a7c20e299ba76ea99eee80d4cb1ab79c54670ec8 Mon Sep 17 00:00:00 2001 From: nicped Date: Mon, 13 Apr 2026 23:27:17 +0200 Subject: [PATCH 72/86] Update Swift version in documentation examples to v2.3.0 --- README.md | 2 +- cliv2-documentation.md | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index d86785b..06df8c7 100644 --- a/README.md +++ b/README.md @@ -440,7 +440,7 @@ Download the latest Swift release, a specific tag, or the nightly build. ```sh dw swift -l -dw swift . --tag v1.25.1 --force +dw swift . --tag v2.3.0 --force dw swift . --nightly --force ``` diff --git a/cliv2-documentation.md b/cliv2-documentation.md index 13d5d2f..6bf21a6 100644 --- a/cliv2-documentation.md +++ b/cliv2-documentation.md @@ -802,7 +802,7 @@ dw swift [outPath] [options] ```sh dw swift -l # list available versions -dw swift . --tag v1.25.1 --force # download a specific version +dw swift . --tag v2.3.0 --force # download a specific version example dw swift . --nightly --force # download the latest nightly build ``` From 4219653d985bb05503a34264074f0c7fd9d2e494 Mon Sep 17 00:00:00 2001 From: nicped Date: Tue, 14 Apr 2026 09:49:24 +0200 Subject: [PATCH 73/86] Add CLAUDE.md for project documentation and enhance file upload logging --- CLAUDE.md | 107 +++++++++++++++++++++++++++++++++++++ bin/commands/files.js | 56 +++++++++++++------ bin/commands/files.test.js | 60 +++++++++++++++++++++ bin/commands/install.js | 4 +- package.json | 4 +- 5 files changed, 211 insertions(+), 20 deletions(-) create mode 100644 CLAUDE.md create mode 100644 bin/commands/files.test.js diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000..33b283f --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,107 @@ +# CLAUDE.md + +This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository. + +## Project Overview + +`@dynamicweb/cli` is a Node.js CLI (`dw`) for managing DynamicWeb 10 CMS solutions. It handles authentication, file archive operations, Admin API queries/commands, add-in installation, database exports, and Swift release downloads. The binary is registered as `dw`. + +## Development Setup + +```bash +npm install +npm install -g . # Makes 'dw' available globally from source +dw --help +``` + +No build step — pure ESM JavaScript (`"type": "module"`), Node.js >=20.12.0 required. + +No test framework or linting is configured yet. + +## Code Architecture + +### Entry Point + +[bin/index.js](bin/index.js) bootstraps yargs with global options and registers all commands. `setupConfig()` runs at startup to initialize `~/.dwc`. + +### Command Structure + +All commands live in [bin/commands/](bin/commands/). Each exports a `*Command()` function returning a yargs command object with `command`, `describe`, `builder`, and `handler` properties. + +Every command handler follows the same pattern: + +```js +handler: async (argv) => { + const output = createXxxOutput(argv); // local output envelope + try { + let env = await setupEnv(argv, output); // from env.js + let user = await setupUser(argv, env); // from login.js + // ... API calls with node-fetch + } catch (err) { + output.fail(err); + process.exitCode = 1; + } finally { + output.finish(); // prints JSON if --output json + } +} +``` + +### Key Shared Modules + +- **[bin/commands/env.js](bin/commands/env.js)** — `setupEnv()`, `getAgent()` (keep-alive HTTP/HTTPS agents), `createCommandError()`, `isJsonOutput()`, `interactiveEnv()` +- **[bin/commands/login.js](bin/commands/login.js)** — `setupUser()`, OAuth token fetch, interactive login, API key creation +- **[bin/commands/config.js](bin/commands/config.js)** — `getConfig()`, `updateConfig()`, `setupConfig()` — manages `~/.dwc` JSON file +- **[bin/utils.js](bin/utils.js)** — `createThrottledStatusUpdater()` (500ms throttle), `formatBytes()`, `formatElapsed()` +- **[bin/downloader.js](bin/downloader.js)** — streams HTTP responses with a progress callback +- **[bin/extractor.js](bin/extractor.js)** — ZIP extraction with progress callback + +### Output Envelope + +Each command creates a local output object (see `createEnvOutput` in [bin/commands/env.js:256](bin/commands/env.js#L256) as the canonical example): + +```js +{ ok, command, operation, status, data: [], errors: [], meta: {} } +``` + +- `output.addData(entry)` — push to `data[]` +- `output.log(...args)` — console.log only when not in JSON mode +- `output.fail(err)` — sets `ok: false`, pushes to `errors[]` +- `output.finish()` — prints `JSON.stringify(response)` if `--output json` + +### Authentication + +`shouldUseOAuth()` in [bin/commands/login.js](bin/commands/login.js) decides auth mode from flags, env config, or CLI args. Both paths converge to `user.apiKey` for API calls. + +- **User auth**: interactive login → creates a DW API key stored in `~/.dwc` +- **OAuth**: fetches access token from `/Admin/OAuth/token`; token not cached between commands + +### Config File (`~/.dwc`) + +```json +{ + "env": { + "": { + "host": "localhost:6001", + "protocol": "https", + "users": { "": { "apiKey": "prefix.key" } }, + "auth": { "type": "oauth_client_credentials", "clientIdEnv": "...", "clientSecretEnv": "..." }, + "current": { "user": "...", "authType": "user|oauth_client_credentials" } + } + }, + "current": { "env": "" } +} +``` + +### Global CLI Flags + +All commands inherit: `--verbose/-v`, `--host`, `--protocol`, `--apiKey`, `--auth user|oauth`, `--clientId`, `--clientSecret`, `--clientIdEnv`, `--clientSecretEnv`, `--output json`. + +When `--output json` is set, interactive prompts are skipped and the structured envelope is printed to stdout. All other logging must go through `output.log()` (not `console.log()` directly) so it is suppressed in JSON mode. + +### HTTPS Agent + +The HTTPS agent in [bin/commands/env.js:13](bin/commands/env.js#L13) sets `rejectUnauthorized: false` intentionally to support self-signed certificates in dev environments. + +### Git Bash Warning + +On Windows Git Bash (`MSYSTEM` env var set), the CLI warns about path conversion unless `MSYS_NO_PATHCONV=1`. diff --git a/bin/commands/files.js b/bin/commands/files.js index f8f4445..845261b 100644 --- a/bin/commands/files.js +++ b/bin/commands/files.js @@ -127,10 +127,10 @@ async function handleFiles(argv) { if (argv.dirPath && argv.outPath) { let resolvedPath = path.resolve(argv.dirPath); if (argv.recursive) { - await processDirectory(env, user, resolvedPath, argv.outPath, resolvedPath, argv.createEmpty, true, argv.overwrite); + await processDirectory(env, user, resolvedPath, argv.outPath, resolvedPath, argv.createEmpty, true, argv.overwrite, console); } else { let filesInDir = getFilesInDirectory(resolvedPath); - await uploadFiles(env, user, filesInDir, argv.outPath, argv.createEmpty, argv.overwrite); + await uploadFiles(env, user, filesInDir, argv.outPath, argv.createEmpty, argv.overwrite, console); } } } @@ -142,16 +142,16 @@ function getFilesInDirectory(dirPath) { .filter(file => fs.statSync(file).isFile()); } -async function processDirectory(env, user, dirPath, outPath, originalDir, createEmpty, isRoot = false, overwrite = false) { +async function processDirectory(env, user, dirPath, outPath, originalDir, createEmpty, isRoot = false, overwrite = false, output = console) { let filesInDir = getFilesInDirectory(dirPath); if (filesInDir.length > 0) - await uploadFiles(env, user, filesInDir, isRoot ? outPath : path.join(outPath, path.basename(dirPath)), createEmpty, overwrite); + await uploadFiles(env, user, filesInDir, isRoot ? outPath : path.join(outPath, path.basename(dirPath)), createEmpty, overwrite, output); const subDirectories = fs.readdirSync(dirPath) .map(subDir => path.join(dirPath, subDir)) .filter(subDir => fs.statSync(subDir).isDirectory()); for (let subDir of subDirectories) { - await processDirectory(env, user, subDir, isRoot ? outPath : path.join(outPath, path.basename(dirPath)), originalDir, createEmpty, false, overwrite); + await processDirectory(env, user, subDir, isRoot ? outPath : path.join(outPath, path.basename(dirPath)), originalDir, createEmpty, false, overwrite, output); } } @@ -306,8 +306,9 @@ async function getFilesStructure(env, user, dirPath, recursive, includeFiles) { } } -export async function uploadFiles(env, user, localFilePaths, destinationPath, createEmpty = false, overwrite = false) { - console.log('Uploading files') +export async function uploadFiles(env, user, localFilePaths, destinationPath, createEmpty = false, overwrite = false, output = console) { + output = resolveUploadOutput(output); + output.log('Uploading files') const chunkSize = 300; const chunks = []; @@ -317,25 +318,25 @@ export async function uploadFiles(env, user, localFilePaths, destinationPath, cr } for (let i = 0; i < chunks.length; i++) { - console.log(`Uploading chunk ${i + 1} of ${chunks.length}`); + output.log(`Uploading chunk ${i + 1} of ${chunks.length}`); const chunk = chunks[i]; - await uploadChunk(env, user, chunk, destinationPath, createEmpty, overwrite); + await uploadChunk(env, user, chunk, destinationPath, createEmpty, overwrite, output); - console.log(`Finished uploading chunk ${i + 1} of ${chunks.length}`); + output.log(`Finished uploading chunk ${i + 1} of ${chunks.length}`); } - console.log(`Finished uploading files. Total files: ${localFilePaths.length}, total chunks: ${chunks.length}`); + output.log(`Finished uploading files. Total files: ${localFilePaths.length}, total chunks: ${chunks.length}`); } -async function uploadChunk(env, user, filePathsChunk, destinationPath, createEmpty, overwrite) { +async function uploadChunk(env, user, filePathsChunk, destinationPath, createEmpty, overwrite, output = console) { const form = new FormData(); form.append('path', destinationPath); form.append('skipExistingFiles', String(!overwrite)); form.append('allowOverwrite', String(overwrite)); filePathsChunk.forEach(fileToUpload => { - console.log(`${fileToUpload}`) + output.log(`${fileToUpload}`) form.append('files', fs.createReadStream(path.resolve(fileToUpload))); }); @@ -349,15 +350,38 @@ async function uploadChunk(env, user, filePathsChunk, destinationPath, createEmp }); if (res.ok) { - console.log(await res.json()) + output.log(await res.json()) } else { - console.log(res) - console.log(await res.json()) + output.log(res) + output.log(await res.json()) process.exit(1); } } +export function resolveUploadOutput(output) { + const response = output?.response ?? {}; + response.meta = response.meta ?? {}; + + return { + response, + log: typeof output?.log === 'function' + ? output.log.bind(output) + : (...args) => console.log(...args), + addData: typeof output?.addData === 'function' + ? output.addData.bind(output) + : () => {}, + mergeMeta: typeof output?.mergeMeta === 'function' + ? output.mergeMeta.bind(output) + : (meta) => { + response.meta = { + ...response.meta, + ...meta + }; + } + }; +} + export function resolveFilePath(filePath) { let p = path.parse(path.resolve(filePath)) let regex = wildcardToRegExp(p.base); diff --git a/bin/commands/files.test.js b/bin/commands/files.test.js new file mode 100644 index 0000000..8d44129 --- /dev/null +++ b/bin/commands/files.test.js @@ -0,0 +1,60 @@ +import test from 'node:test'; +import assert from 'node:assert/strict'; + +import { resolveUploadOutput } from './files.js'; + +test('resolveUploadOutput falls back to a console-compatible output object', () => { + const output = resolveUploadOutput(); + + assert.equal(typeof output.log, 'function'); + assert.equal(typeof output.addData, 'function'); + assert.equal(typeof output.mergeMeta, 'function'); + assert.deepEqual(output.response.meta, {}); + + output.mergeMeta({ chunks: 1, filesProcessed: 2 }); + + assert.deepEqual(output.response.meta, { + chunks: 1, + filesProcessed: 2 + }); +}); + +test('resolveUploadOutput preserves custom logging and merges meta when mergeMeta is absent', () => { + const calls = []; + const data = []; + const output = { + log: (...args) => calls.push(args), + addData: (entry) => data.push(entry), + response: { + meta: { + existing: true + } + } + }; + + const resolved = resolveUploadOutput(output); + + resolved.log('Uploading chunk 1 of 1'); + resolved.addData({ file: 'addon.nupkg' }); + resolved.mergeMeta({ chunks: 1 }); + + assert.deepEqual(calls, [[ 'Uploading chunk 1 of 1' ]]); + assert.deepEqual(data, [{ file: 'addon.nupkg' }]); + assert.deepEqual(resolved.response.meta, { + existing: true, + chunks: 1 + }); +}); + +test('resolveUploadOutput initializes response.meta for partial output objects', () => { + const resolved = resolveUploadOutput({ + log: () => {}, + response: {} + }); + + resolved.mergeMeta({ chunks: 2 }); + + assert.deepEqual(resolved.response.meta, { + chunks: 2 + }); +}); diff --git a/bin/commands/install.js b/bin/commands/install.js index 61aaa2d..400f364 100644 --- a/bin/commands/install.js +++ b/bin/commands/install.js @@ -30,7 +30,7 @@ async function handleInstall(argv) { let env = await setupEnv(argv); let user = await setupUser(argv, env); let resolvedPath = resolveFilePath(argv.filePath); - await uploadFiles(env, user, [ resolvedPath ], 'System/AddIns/Local', false, true); + await uploadFiles(env, user, [ resolvedPath ], 'System/AddIns/Local', false, true, console); await installAddin(env, user, resolvedPath, argv.queue) } @@ -62,4 +62,4 @@ async function installAddin(env, user, resolvedPath, queue) { console.log(await res.json()) process.exit(1); } -} \ No newline at end of file +} diff --git a/package.json b/package.json index 31cef17..86c98cb 100644 --- a/package.json +++ b/package.json @@ -15,7 +15,7 @@ "devops" ], "scripts": { - "test": "echo \"Error: no test specified\" && exit 1" + "test": "node --test" }, "author": "Dynamicweb A/S (https://www.dynamicweb.com)", "repository": { @@ -48,4 +48,4 @@ "node-fetch": "^3.2.10", "yargs": "^17.5.1" } -} \ No newline at end of file +} From 54749832a172f4a6a20a8fe6ffeebb88b559e5cb Mon Sep 17 00:00:00 2001 From: nicped Date: Tue, 14 Apr 2026 10:12:30 +0200 Subject: [PATCH 74/86] Fix install upload output handling --- bin/commands/files.js | 205 ++++++++++++++++++++--------------- bin/commands/files.test.js | 18 ++- bin/commands/install.js | 110 +++++++++++++++++-- bin/commands/install.test.js | 48 ++++++++ 4 files changed, 280 insertions(+), 101 deletions(-) create mode 100644 bin/commands/install.test.js diff --git a/bin/commands/files.js b/bin/commands/files.js index 845261b..2e9606b 100644 --- a/bin/commands/files.js +++ b/bin/commands/files.js @@ -10,71 +10,71 @@ import { extractWithProgress } from '../extractor.js'; export function filesCommand() { return { - command: 'files [dirPath] [outPath]', - describe: 'Handles files', + command: 'files [dirPath] [outPath]', + describe: 'Handles files', builder: (yargs) => { return yargs - .positional('dirPath', { - describe: 'The directory to list or export' - }) - .positional('outPath', { - describe: 'The directory to export the specified directory to', - default: '.' - }) - .option('list', { - alias: 'l', - type: 'boolean', - describe: 'Lists all directories and files' - }) - .option('export', { - alias: 'e', - type: 'boolean', - describe: 'Exports the specified directory and all subdirectories at [dirPath] to [outPath]' - }) - .option('import', { - alias: 'i', - type: 'boolean', - describe: 'Imports the file at [dirPath] to [outPath]' - }) - .option('overwrite', { - alias: 'o', - type: 'boolean', - describe: 'Used with import, will overwrite existing files at destination if set to true' - }) - .option('createEmpty', { - type: 'boolean', - describe: 'Used with import, will create a file even if its empty' - }) - .option('includeFiles', { - alias: 'f', - type: 'boolean', - describe: 'Used with export, includes files in list of directories and files' - }) - .option('recursive', { - alias: 'r', - type: 'boolean', - describe: 'Used with list, import and export, handles all directories recursively' - }) - .option('raw', { - type: 'boolean', - describe: 'Used with export, keeps zip file instead of unpacking it' - }) - .option('iamstupid', { - type: 'boolean', - describe: 'Includes export of log and cache folders, NOT RECOMMENDED' - }) - .option('asFile', { - type: 'boolean', - alias: 'af', - describe: 'Forces the command to treat the path as a single file, even if it has no extension.', - conflicts: 'asDirectory' - }) - .option('asDirectory', { - type: 'boolean', - alias: 'ad', - describe: 'Forces the command to treat the path as a directory, even if its name contains a dot.', - conflicts: 'asFile' - }) + .positional('dirPath', { + describe: 'The directory to list or export' + }) + .positional('outPath', { + describe: 'The directory to export the specified directory to', + default: '.' + }) + .option('list', { + alias: 'l', + type: 'boolean', + describe: 'Lists all directories and files' + }) + .option('export', { + alias: 'e', + type: 'boolean', + describe: 'Exports the specified directory and all subdirectories at [dirPath] to [outPath]' + }) + .option('import', { + alias: 'i', + type: 'boolean', + describe: 'Imports the file at [dirPath] to [outPath]' + }) + .option('overwrite', { + alias: 'o', + type: 'boolean', + describe: 'Used with import, will overwrite existing files at destination if set to true' + }) + .option('createEmpty', { + type: 'boolean', + describe: 'Used with import, will create a file even if its empty' + }) + .option('includeFiles', { + alias: 'f', + type: 'boolean', + describe: 'Used with export, includes files in list of directories and files' + }) + .option('recursive', { + alias: 'r', + type: 'boolean', + describe: 'Used with list, import and export, handles all directories recursively' + }) + .option('raw', { + type: 'boolean', + describe: 'Used with export, keeps zip file instead of unpacking it' + }) + .option('iamstupid', { + type: 'boolean', + describe: 'Includes export of log and cache folders, NOT RECOMMENDED' + }) + .option('asFile', { + type: 'boolean', + alias: 'af', + describe: 'Forces the command to treat the path as a single file, even if it has no extension.', + conflicts: 'asDirectory' + }) + .option('asDirectory', { + type: 'boolean', + alias: 'ad', + describe: 'Forces the command to treat the path as a directory, even if its name contains a dot.', + conflicts: 'asFile' + }) }, handler: async (argv) => { if (argv.verbose) console.info(`Listing directory at: ${argv.dirPath}`) @@ -97,15 +97,15 @@ async function handleFiles(argv) { if (argv.export) { if (argv.dirPath) { - + const isFile = argv.asFile || argv.asDirectory ? argv.asFile - : path.extname(argv.dirPath) !== ''; + : path.extname(argv.dirPath) !== ''; if (isFile) { - let parentDirectory = path.dirname(argv.dirPath); + let parentDirectory = path.dirname(argv.dirPath); parentDirectory = parentDirectory === '.' ? '/' : parentDirectory; - + await download(env, user, parentDirectory, argv.outPath, false, null, true, argv.iamstupid, [argv.dirPath], true, argv.verbose); } else { await download(env, user, argv.dirPath, argv.outPath, true, null, argv.raw, argv.iamstupid, [], false, argv.verbose); @@ -127,19 +127,19 @@ async function handleFiles(argv) { if (argv.dirPath && argv.outPath) { let resolvedPath = path.resolve(argv.dirPath); if (argv.recursive) { - await processDirectory(env, user, resolvedPath, argv.outPath, resolvedPath, argv.createEmpty, true, argv.overwrite, console); + await processDirectory(env, user, resolvedPath, argv.outPath, resolvedPath, argv.createEmpty, true, argv.overwrite); } else { let filesInDir = getFilesInDirectory(resolvedPath); - await uploadFiles(env, user, filesInDir, argv.outPath, argv.createEmpty, argv.overwrite, console); + await uploadFiles(env, user, filesInDir, argv.outPath, argv.createEmpty, argv.overwrite); } } } } function getFilesInDirectory(dirPath) { - return fs.statSync(dirPath).isFile() ? [ dirPath ] : fs.readdirSync(dirPath) - .map(file => path.join(dirPath, file)) - .filter(file => fs.statSync(file).isFile()); + return fs.statSync(dirPath).isFile() ? [dirPath] : fs.readdirSync(dirPath) + .map(file => path.join(dirPath, file)) + .filter(file => fs.statSync(file).isFile()); } async function processDirectory(env, user, dirPath, outPath, originalDir, createEmpty, isRoot = false, overwrite = false, output = console) { @@ -148,8 +148,8 @@ async function processDirectory(env, user, dirPath, outPath, originalDir, create await uploadFiles(env, user, filesInDir, isRoot ? outPath : path.join(outPath, path.basename(dirPath)), createEmpty, overwrite, output); const subDirectories = fs.readdirSync(dirPath) - .map(subDir => path.join(dirPath, subDir)) - .filter(subDir => fs.statSync(subDir).isDirectory()); + .map(subDir => path.join(dirPath, subDir)) + .filter(subDir => fs.statSync(subDir).isDirectory()); for (let subDir of subDirectories) { await processDirectory(env, user, subDir, isRoot ? outPath : path.join(outPath, path.basename(dirPath)), originalDir, createEmpty, false, overwrite, output); } @@ -184,7 +184,7 @@ function resolveTree(dirs, indentLevel, parentHasFiles) { resolveTree(dir.files?.data ?? [], indentLevel + '│\t', false); } else { resolveTree(dir.directories ?? [], indentLevel + '\t', hasFiles); - resolveTree(dir.files?.data ?? [], indentLevel + '\t', false); + resolveTree(dir.files?.data ?? [], indentLevel + '\t', false); } } } @@ -286,7 +286,7 @@ async function extractArchive(filename, filePath, outPath, raw) { updater.stop(); console.log(`Finished extracting ${filename} to ${outPath}\n`); - fs.unlink(filePath, function(err) {}); + fs.unlink(filePath, function (err) { }); } async function getFilesStructure(env, user, dirPath, recursive, includeFiles) { @@ -306,7 +306,7 @@ async function getFilesStructure(env, user, dirPath, recursive, includeFiles) { } } -export async function uploadFiles(env, user, localFilePaths, destinationPath, createEmpty = false, overwrite = false, output = console) { +export async function uploadFiles(env, user, localFilePaths, destinationPath, createEmpty = false, overwrite = false, output) { output = resolveUploadOutput(output); output.log('Uploading files') @@ -317,11 +317,22 @@ export async function uploadFiles(env, user, localFilePaths, destinationPath, cr chunks.push(localFilePaths.slice(i, i + chunkSize)); } + output.mergeMeta({ + filesProcessed: (output.response.meta.filesProcessed || 0) + localFilePaths.length, + chunks: (output.response.meta.chunks || 0) + chunks.length + }); + for (let i = 0; i < chunks.length; i++) { output.log(`Uploading chunk ${i + 1} of ${chunks.length}`); const chunk = chunks[i]; - await uploadChunk(env, user, chunk, destinationPath, createEmpty, overwrite, output); + const body = await uploadChunk(env, user, chunk, destinationPath, createEmpty, overwrite, output); + output.addData({ + type: 'upload', + destinationPath, + files: chunk.map(filePath => path.resolve(filePath)), + response: body + }); output.log(`Finished uploading chunk ${i + 1} of ${chunks.length}`); } @@ -334,13 +345,13 @@ async function uploadChunk(env, user, filePathsChunk, destinationPath, createEmp form.append('path', destinationPath); form.append('skipExistingFiles', String(!overwrite)); form.append('allowOverwrite', String(overwrite)); - + filePathsChunk.forEach(fileToUpload => { output.log(`${fileToUpload}`) form.append('files', fs.createReadStream(path.resolve(fileToUpload))); }); - const res = await fetch(`${env.protocol}://${env.host}/Admin/Api/Upload?` + new URLSearchParams({"createEmptyFiles": createEmpty, "createMissingDirectories": true}), { + const res = await fetch(`${env.protocol}://${env.host}/Admin/Api/Upload?` + new URLSearchParams({ "createEmptyFiles": createEmpty, "createMissingDirectories": true }), { method: 'POST', body: form, headers: { @@ -348,13 +359,17 @@ async function uploadChunk(env, user, filePathsChunk, destinationPath, createEmp }, agent: getAgent(env.protocol) }); - + if (res.ok) { - output.log(await res.json()) + return await res.json() } else { + if (output.structured) { + throw createUploadError('File upload failed.', res.status, await parseJsonSafe(res)); + } + output.log(res) - output.log(await res.json()) + output.log(await parseJsonSafe(res)) process.exit(1); } } @@ -364,13 +379,14 @@ export function resolveUploadOutput(output) { response.meta = response.meta ?? {}; return { + structured: Boolean(output), response, log: typeof output?.log === 'function' ? output.log.bind(output) : (...args) => console.log(...args), addData: typeof output?.addData === 'function' ? output.addData.bind(output) - : () => {}, + : () => { }, mergeMeta: typeof output?.mergeMeta === 'function' ? output.mergeMeta.bind(output) : (meta) => { @@ -382,14 +398,27 @@ export function resolveUploadOutput(output) { }; } +function createUploadError(message, status, details = null) { + const error = new Error(message); + error.status = status; + error.details = details; + return error; +} + +async function parseJsonSafe(res) { + try { + return await res.json(); + } catch { + return null; + } +} + export function resolveFilePath(filePath) { let p = path.parse(path.resolve(filePath)) let regex = wildcardToRegExp(p.base); let resolvedPath = fs.readdirSync(p.dir).filter((allFilesPaths) => allFilesPaths.match(regex) !== null)[0] - if (resolvedPath === undefined) - { - console.log('Could not find any files with the name ' + filePath); - process.exit(1); + if (resolvedPath === undefined) { + throw new Error('Could not find any files with the name ' + filePath); } return path.join(p.dir, resolvedPath); } diff --git a/bin/commands/files.test.js b/bin/commands/files.test.js index 8d44129..2bf02b4 100644 --- a/bin/commands/files.test.js +++ b/bin/commands/files.test.js @@ -1,7 +1,10 @@ import test from 'node:test'; import assert from 'node:assert/strict'; +import fs from 'node:fs'; +import os from 'node:os'; +import path from 'node:path'; -import { resolveUploadOutput } from './files.js'; +import { resolveFilePath, resolveUploadOutput } from './files.js'; test('resolveUploadOutput falls back to a console-compatible output object', () => { const output = resolveUploadOutput(); @@ -58,3 +61,16 @@ test('resolveUploadOutput initializes response.meta for partial output objects', chunks: 2 }); }); + +test('resolveFilePath throws when no matching file exists', () => { + const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'dw-cli-files-test-')); + + try { + assert.throws( + () => resolveFilePath(path.join(tempDir, 'missing*.nupkg')), + /Could not find any files with the name/ + ); + } finally { + fs.rmSync(tempDir, { recursive: true, force: true }); + } +}); diff --git a/bin/commands/install.js b/bin/commands/install.js index 400f364..f4da776 100644 --- a/bin/commands/install.js +++ b/bin/commands/install.js @@ -18,24 +18,40 @@ export function installCommand() { type: 'boolean', describe: 'Queues the install for next Dynamicweb recycle' }) + .option('output', { + choices: ['json'], + describe: 'Outputs a single JSON response for automation-friendly parsing' + }) }, handler: async (argv) => { - if (argv.verbose) console.info(`Installing file located at :${argv.filePath}`) - await handleInstall(argv) + const output = createInstallOutput(argv); + + try { + output.verboseLog(`Installing file located at: ${argv.filePath}`); + await handleInstall(argv, output) + } catch (err) { + output.fail(err); + process.exitCode = 1; + } finally { + output.finish(); + } } } } -async function handleInstall(argv) { +async function handleInstall(argv, output) { let env = await setupEnv(argv); let user = await setupUser(argv, env); let resolvedPath = resolveFilePath(argv.filePath); - await uploadFiles(env, user, [ resolvedPath ], 'System/AddIns/Local', false, true, console); - await installAddin(env, user, resolvedPath, argv.queue) + output.mergeMeta({ + resolvedPath + }); + await uploadFiles(env, user, [ resolvedPath ], 'System/AddIns/Local', false, true, output); + await installAddin(env, user, resolvedPath, argv.queue, output) } -async function installAddin(env, user, resolvedPath, queue) { - console.log('Installing addin') +async function installAddin(env, user, resolvedPath, queue, output) { + output.log('Installing addin') let filename = path.basename(resolvedPath); let data = { 'Queue': queue, @@ -54,12 +70,82 @@ async function installAddin(env, user, resolvedPath, queue) { }); if (res.ok) { - if (env.verbose) console.log(await res.json()) - console.log(`Addin installed`) + const body = await res.json(); + output.addData({ + type: 'install', + filename, + queued: Boolean(queue), + response: body + }); + output.log(`Addin installed`) } else { - console.log('Request failed, returned error:') - console.log(await res.json()) - process.exit(1); + throw createInstallError('Addin install failed.', res.status, await parseJsonSafe(res)); + } +} + +export function createInstallOutput(argv) { + const response = { + ok: true, + command: 'install', + operation: 'install', + status: 200, + data: [], + errors: [], + meta: { + queued: Boolean(argv.queue) + } + }; + + return { + json: argv.output === 'json', + response, + log(...args) { + if (!this.json) { + console.log(...args); + } + }, + verboseLog(...args) { + if (argv.verbose && !this.json) { + console.info(...args); + } + }, + addData(entry) { + response.data.push(entry); + }, + mergeMeta(meta) { + response.meta = { + ...response.meta, + ...meta + }; + }, + fail(err) { + response.ok = false; + response.status = err?.status || 1; + response.errors.push({ + message: err?.message || 'Unknown install command error.', + details: err?.details ?? null + }); + }, + finish() { + if (this.json) { + console.log(JSON.stringify(response, null, 2)); + } + } + }; +} + +function createInstallError(message, status, details = null) { + const error = new Error(message); + error.status = status; + error.details = details; + return error; +} + +async function parseJsonSafe(res) { + try { + return await res.json(); + } catch { + return null; } } diff --git a/bin/commands/install.test.js b/bin/commands/install.test.js new file mode 100644 index 0000000..0f2e265 --- /dev/null +++ b/bin/commands/install.test.js @@ -0,0 +1,48 @@ +import test from 'node:test'; +import assert from 'node:assert/strict'; + +import { createInstallOutput } from './install.js'; + +test('createInstallOutput suppresses regular logs in json mode and emits the final envelope', () => { + const logCalls = []; + const infoCalls = []; + const originalLog = console.log; + const originalInfo = console.info; + + console.log = (...args) => logCalls.push(args); + console.info = (...args) => infoCalls.push(args); + + try { + const output = createInstallOutput({ + output: 'json', + queue: true, + verbose: true + }); + + output.log('hidden'); + output.verboseLog('hidden verbose'); + output.addData({ type: 'install', filename: 'addon.nupkg' }); + output.mergeMeta({ resolvedPath: '/tmp/addon.nupkg' }); + output.finish(); + + assert.deepEqual(infoCalls, []); + assert.equal(logCalls.length, 1); + + const rendered = JSON.parse(logCalls[0][0]); + assert.deepEqual(rendered, { + ok: true, + command: 'install', + operation: 'install', + status: 200, + data: [{ type: 'install', filename: 'addon.nupkg' }], + errors: [], + meta: { + queued: true, + resolvedPath: '/tmp/addon.nupkg' + } + }); + } finally { + console.log = originalLog; + console.info = originalInfo; + } +}); From a08d5ea61a1326fe6fd5a863f5f0b28e4c391a02 Mon Sep 17 00:00:00 2001 From: nicped Date: Tue, 14 Apr 2026 10:17:09 +0200 Subject: [PATCH 75/86] Bump version to 1.1.2 Patch release fixing TypeError in uploadFiles when called from install command without a structured output object. --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 86c98cb..1c2aac2 100644 --- a/package.json +++ b/package.json @@ -2,7 +2,7 @@ "name": "@dynamicweb/cli", "type": "module", "description": "CLI for interacting with Dynamicweb 10 solutions from the command line.", - "version": "1.1.1", + "version": "1.1.2", "main": "bin/index.js", "files": [ "bin/**" From 32ba49c7db46122f3f30a334a9ad1c03c37fa45c Mon Sep 17 00:00:00 2001 From: nicped Date: Wed, 15 Apr 2026 12:28:56 +0200 Subject: [PATCH 76/86] Add unit test suite and QA smoke harness; export internal functions for testability - Export parseHostInput, getQueryParams, buildInteractiveQueryParams, buildQueryParamsFromArgv, extractQueryPropertyPrompts, getFieldNameFromPropertyPrompt, parseCookies, shouldUseOAuth, resolveOAuthConfig, prepareDownloadCommandData, isFilePath, wildcardToRegExp, getFilesOperation from their respective modules - Add setConfigForTests() to config.js and guard getConfig() against null return - Inject deps (interactiveEnvFn, getPropertiesFn, promptFn) via optional deps param in setupEnv and getQueryParams for easier unit testing - Add test/ suite covering command, env, files, login, query, and utility helpers - Add qa/ smoke harness (run-smoke.mjs) with fixtures, profile.example.json, and README covering saved-env and ephemeral CI flows against a real DynamicWeb solution - Wire npm run test (node --test) and npm run qa:smoke scripts in package.json - Bump version to 2.0.0-beta.1 - Ignore qa/artifacts/ and qa/profile.json in .gitignore --- .gitignore | 4 + README.md | 9 + bin/commands/command.js | 4 +- bin/commands/config.js | 8 +- bin/commands/env.js | 46 +- bin/commands/files.js | 10 +- bin/commands/login.js | 6 +- bin/commands/query.js | 46 +- package.json | 7 +- qa/README.md | 139 +++ qa/fixtures/files/source/copied/keep.txt | 1 + qa/fixtures/files/source/moved/keep.txt | 1 + .../files/source/nested/nested-upload.txt | 1 + qa/fixtures/files/source/smoke-upload.txt | 1 + qa/profile.example.json | 15 + qa/run-smoke.mjs | 1027 +++++++++++++++++ test/command.test.js | 52 + test/downloader.test.js | 62 + test/env.test.js | 170 +++ test/files.test.js | 87 ++ test/login.test.js | 88 ++ test/query.test.js | 140 +++ test/utils.test.js | 20 + 23 files changed, 1903 insertions(+), 41 deletions(-) create mode 100644 qa/README.md create mode 100644 qa/fixtures/files/source/copied/keep.txt create mode 100644 qa/fixtures/files/source/moved/keep.txt create mode 100644 qa/fixtures/files/source/nested/nested-upload.txt create mode 100644 qa/fixtures/files/source/smoke-upload.txt create mode 100644 qa/profile.example.json create mode 100644 qa/run-smoke.mjs create mode 100644 test/command.test.js create mode 100644 test/downloader.test.js create mode 100644 test/env.test.js create mode 100644 test/files.test.js create mode 100644 test/login.test.js create mode 100644 test/query.test.js create mode 100644 test/utils.test.js diff --git a/.gitignore b/.gitignore index f43b8ba..2c8fe2e 100644 --- a/.gitignore +++ b/.gitignore @@ -105,3 +105,7 @@ dist # Visual Studio /.vs + +# QA harness +/qa/artifacts/ +/qa/profile.json diff --git a/README.md b/README.md index 06df8c7..7787840 100644 --- a/README.md +++ b/README.md @@ -465,6 +465,15 @@ dw query HealthCheck \ 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. +## QA Smoke Testing + +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: + +- saved-environment developer flow +- ephemeral CI/CD-style flow with `--host --auth oauth` + +The harness currently excludes `database` and `swift`. + ## Using Git Bash 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: diff --git a/bin/commands/command.js b/bin/commands/command.js index 72fd603..8a13197 100644 --- a/bin/commands/command.js +++ b/bin/commands/command.js @@ -61,13 +61,13 @@ async function getProperties(env, user, command) { throw createCommandError('The --list option is not currently implemented for commands.'); } -function getQueryParams(argv) { +export function getQueryParams(argv) { let params = {} Object.keys(argv).filter(k => !exclude.includes(k)).forEach(k => params['Command.' + k] = argv[k]) return params } -function parseJsonOrPath(json) { +export function parseJsonOrPath(json) { if (!json) return if (fs.existsSync(json)) { return JSON.parse(fs.readFileSync(path.resolve(json))) diff --git a/bin/commands/config.js b/bin/commands/config.js index 89afed0..7ef298a 100644 --- a/bin/commands/config.js +++ b/bin/commands/config.js @@ -27,7 +27,11 @@ export function setupConfig() { } export function getConfig() { - return localConfig; + return localConfig || {}; +} + +export function setConfigForTests(config) { + localConfig = config; } export function handleConfig(argv) { @@ -52,4 +56,4 @@ function resolveConfig(key, obj, conf) { conf[a] = resolveConfig(key, obj[a], conf[a]); }) return conf; -} \ No newline at end of file +} diff --git a/bin/commands/env.js b/bin/commands/env.js index ad21d22..6b79328 100644 --- a/bin/commands/env.js +++ b/bin/commands/env.js @@ -22,6 +22,26 @@ export function getAgent(protocol) { return protocol === 'http' ? httpAgent : httpsAgent; } +export function parseHostInput(hostValue) { + const hostSplit = hostValue.split('://'); + + if (hostSplit.length === 1) { + return { + protocol: 'https', + host: hostSplit[0] + }; + } + + if (hostSplit.length === 2) { + return { + protocol: hostSplit[0], + host: hostSplit[1] + }; + } + + throw createCommandError(`Issues resolving host ${hostValue}`); +} + export function envCommand() { return { command: 'env [env]', @@ -61,7 +81,9 @@ export function envCommand() { } } -export async function setupEnv(argv, output = null) { +export async function setupEnv(argv, output = null, deps = {}) { + const interactiveEnvFn = deps.interactiveEnvFn || interactiveEnv; + const cfg = getConfig(); let env = {}; let askEnv = true; @@ -75,8 +97,8 @@ export async function setupEnv(argv, output = null) { } } - if (askEnv && getConfig().env) { - env = getConfig().env[argv.env] || getConfig().env[getConfig()?.current?.env]; + if (askEnv && cfg.env) { + env = cfg.env[argv.env] || cfg.env[cfg?.current?.env]; if (env && !env.protocol) { logMessage(argv, 'Protocol for environment not set, defaulting to https'); env.protocol = 'https'; @@ -88,7 +110,7 @@ export async function setupEnv(argv, output = null) { } logMessage(argv, 'Current environment not set, please set it'); - await interactiveEnv(argv, { + await interactiveEnvFn(argv, { environment: { type: 'input' }, @@ -96,7 +118,8 @@ export async function setupEnv(argv, output = null) { default: true } }, output) - env = getConfig().env[getConfig()?.current?.env]; + const updatedConfig = getConfig(); + env = updatedConfig.env?.[updatedConfig?.current?.env]; } if (!env || Object.keys(env).length === 0) { @@ -160,16 +183,9 @@ export async function interactiveEnv(argv, options, output) { } getConfig().env[result.environment] = getConfig().env[result.environment] || {}; if (result.host) { - const hostSplit = result.host.split("://"); - if (hostSplit.length == 1) { - getConfig().env[result.environment].protocol = 'https'; - getConfig().env[result.environment].host = hostSplit[0]; - } else if (hostSplit.length == 2) { - getConfig().env[result.environment].protocol = hostSplit[0]; - getConfig().env[result.environment].host = hostSplit[1]; - } else { - throw createCommandError(`Issues resolving host ${result.host}`); - } + const resolvedHost = parseHostInput(result.host); + getConfig().env[result.environment].protocol = resolvedHost.protocol; + getConfig().env[result.environment].host = resolvedHost.host; } if (result.environment) { getConfig().current = getConfig().current || {}; diff --git a/bin/commands/files.js b/bin/commands/files.js index ccf1ff8..c0b77cf 100644 --- a/bin/commands/files.js +++ b/bin/commands/files.js @@ -342,7 +342,7 @@ async function download(env, user, dirPath, outPath, recursive, outname, raw, da await extractArchive(filename, filePath, outPath, raw, output); } -function prepareDownloadCommandData(directoryPath, excludeDirectories, fileNames, recursive, singleFileMode) { +export function prepareDownloadCommandData(directoryPath, excludeDirectories, fileNames, recursive, singleFileMode) { const data = { 'DirectoryPath': directoryPath ?? '/', 'ExcludeDirectories': [excludeDirectories], @@ -612,14 +612,14 @@ export function resolveFilePath(filePath) { } -function isFilePath(argv, dirPath) { +export function isFilePath(argv, dirPath) { if (argv.asFile || argv.asDirectory) { - return argv.asFile; + return Boolean(argv.asFile); } return path.extname(dirPath) !== ''; } -function wildcardToRegExp(wildcard) { +export function wildcardToRegExp(wildcard) { const escaped = wildcard.replace(/[.+?^${}()|[\]\\]/g, '\\$&'); return new RegExp('^' + escaped.replace(/\*/g, '.*') + '$'); } @@ -678,7 +678,7 @@ function createFilesOutput(argv) { }; } -function getFilesOperation(argv) { +export function getFilesOperation(argv) { if (argv.list) { return 'list'; } diff --git a/bin/commands/login.js b/bin/commands/login.js index 8b678ea..3254d41 100644 --- a/bin/commands/login.js +++ b/bin/commands/login.js @@ -216,7 +216,7 @@ async function login(username, password, env, protocol, verbose) { } } -function parseCookies (cookieHeader) { +export function parseCookies (cookieHeader) { const list = {}; if (!cookieHeader) { return list; @@ -415,7 +415,7 @@ async function authenticateWithOAuth(argv, env) { }; } -function shouldUseOAuth(argv, env = {}) { +export function shouldUseOAuth(argv, env = {}) { if (argv.auth === 'user') { return false; } @@ -435,7 +435,7 @@ function shouldUseOAuth(argv, env = {}) { return env?.auth?.type === 'oauth_client_credentials'; } -function resolveOAuthConfig(argv, env = {}, requireCredentials = true) { +export function resolveOAuthConfig(argv, env = {}, requireCredentials = true) { const authConfig = env?.auth || {}; const clientIdEnv = argv.clientIdEnv || authConfig.clientIdEnv || DEFAULT_CLIENT_ID_ENV; const clientSecretEnv = argv.clientSecretEnv || authConfig.clientSecretEnv || DEFAULT_CLIENT_SECRET_ENV; diff --git a/bin/commands/query.js b/bin/commands/query.js index fd9e31b..fd3fb11 100644 --- a/bin/commands/query.js +++ b/bin/commands/query.js @@ -74,28 +74,52 @@ async function getProperties(env, user, query) { if (body?.model?.properties?.groups === undefined) { throw createCommandError('Unable to fetch query parameters.', res.status, body); } - return body.model.properties.groups.filter(g => g.name === 'Properties')[0].fields.map(field => `${field.name} (${field.typeName})`) + return extractQueryPropertyPrompts(body); } throw createCommandError('Unable to fetch query parameters.', res.status, await parseJsonSafe(res)); } -async function getQueryParams(env, user, argv, output) { +export async function getQueryParams(env, user, argv, output, deps = {}) { let params = {} + const getPropertiesFn = deps.getPropertiesFn || getProperties; + const promptFn = deps.promptFn || input; if (argv.interactive) { - let properties = await getProperties(env, user, argv.query); + let properties = await getPropertiesFn(env, user, argv.query); output.log('The following properties will be requested:') output.log(properties) - for (const p of properties) { - const value = await input({ message: p }); - if (value) { - const fieldName = p.split(' (')[0]; - params[fieldName] = value; - } - } + params = await buildInteractiveQueryParams(properties, promptFn); } else { - Object.keys(argv).filter(k => !exclude.includes(k)).forEach(k => params[k] = argv[k]) + params = buildQueryParamsFromArgv(argv); + } + return params +} + +export function extractQueryPropertyPrompts(body) { + const fields = body?.model?.properties?.groups?.find(g => g.name === 'Properties')?.fields || []; + return fields.map(field => `${field.name} (${field.typeName})`); +} + +export function getFieldNameFromPropertyPrompt(prompt) { + return prompt.replace(/\s+\([^)]+\)$/, ''); +} + +export async function buildInteractiveQueryParams(properties, promptFn = input) { + const params = {}; + + for (const propertyPrompt of properties) { + const value = await promptFn({ message: propertyPrompt }); + if (value) { + params[getFieldNameFromPropertyPrompt(propertyPrompt)] = value; + } } + + return params; +} + +export function buildQueryParamsFromArgv(argv) { + let params = {} + Object.keys(argv).filter(k => !exclude.includes(k)).forEach(k => params[k] = argv[k]) return params } diff --git a/package.json b/package.json index 470030e..93b116e 100644 --- a/package.json +++ b/package.json @@ -2,7 +2,7 @@ "name": "@dynamicweb/cli", "type": "module", "description": "CLI for interacting with Dynamicweb 10 solutions from the command line.", - "version": "2.0.0-beta.0", + "version": "2.0.0-beta.1", "main": "bin/index.js", "files": [ "bin/**" @@ -15,7 +15,8 @@ "devops" ], "scripts": { - "test": "echo \"Error: no test specified\" && exit 1" + "test": "node --test", + "qa:smoke": "node qa/run-smoke.mjs" }, "author": "Dynamicweb A/S (https://www.dynamicweb.com)", "repository": { @@ -48,4 +49,4 @@ "node-fetch": "^3.2.10", "yargs": "^17.5.1" } -} +} \ No newline at end of file diff --git a/qa/README.md b/qa/README.md new file mode 100644 index 0000000..78e9d38 --- /dev/null +++ b/qa/README.md @@ -0,0 +1,139 @@ +# QA Smoke Harness + +This folder contains a smoke-test harness for running the CLI against a real DynamicWeb solution. + +It is designed to cover both: + +- developer-style usage with a saved environment and `dw login --oauth` +- CI/CD-style usage with `--host --auth oauth` + +The harness currently excludes `database` and `swift`. + +## What It Covers + +- config seeding in an isolated home directory +- `dw env --list` +- `dw env ` +- `dw login --oauth --output json` +- base `dw` output after saved OAuth login +- `dw files` list, import, export, copy, move, and delete +- configured `dw query` smoke checks +- configured `dw command` smoke checks +- optional `dw install` if you provide a real `.dll` or `.nupkg` + +## Safety Model + +- The runner sets its own temporary `HOME`, so it does not touch the engineer's real `~/.dwc`. +- File tests use a unique remote folder under `remoteRoot//...`. +- The runner attempts remote cleanup automatically unless you pass `--keep-remote`. + +## Quick Start + +Set the required credentials: + +```sh +export DW_BASE_URL=https://your-solution.example.com +export DW_CLIENT_ID=your-client-id +export DW_CLIENT_SECRET=your-client-secret +``` + +Run with defaults: + +```sh +npm run qa:smoke +``` + +Run with a custom profile: + +```sh +npm run qa:smoke -- --profile qa/profile.json +``` + +Run only one mode: + +```sh +npm run qa:smoke -- --mode saved-env +npm run qa:smoke -- --mode ephemeral +``` + +Artifacts are written to `qa/artifacts//`. + +## Configuration + +The runner works without a profile if these environment variables are present: + +- `DW_BASE_URL` +- `DW_CLIENT_ID` +- `DW_CLIENT_SECRET` + +Defaults: + +- `environmentName`: `qa-smoke` +- `clientIdEnv`: `DW_CLIENT_ID` +- `clientSecretEnv`: `DW_CLIENT_SECRET` +- `remoteRoot`: `QA/CLI` +- `commandTimeoutMs`: `120000` +- `queries`: `[]` +- `commands`: `[]` +- `install.enabled`: `false` + +If `qa/profile.json` exists, it is loaded automatically. You can also pass `--profile `. + +See [profile.example.json](profile.example.json) for the supported shape. + +## Profile Example + +```json +{ + "environmentName": "qa-smoke", + "baseUrl": "https://your-solution.example.com", + "clientIdEnv": "DW_CLIENT_ID", + "clientSecretEnv": "DW_CLIENT_SECRET", + "remoteRoot": "QA/CLI", + "commandTimeoutMs": 120000, + "queries": [ + { + "name": "YourReadOnlyQuery", + "params": { + "id": "123" + } + } + ], + "commands": [ + { + "name": "YourSafeCommand", + "body": { + "model": { + "id": "123" + } + } + } + ], + "install": { + "enabled": false, + "filePath": "qa/fixtures/addins/YourAddin.nupkg", + "queue": true + } +} +``` + +## CI Example + +The same command works in CI as long as the secret store exposes the required variables: + +```sh +npm ci +npm run qa:smoke -- --mode all --profile qa/profile.json +``` + +To shorten or extend the per-command timeout in CI: + +```sh +npm run qa:smoke -- --timeoutMs 180000 +``` + +## Notes + +- Prefer read-only queries and commands unless you have a dedicated QA tenant and fixture data. +- `dw command --list` is not included because the command is not implemented in the CLI yet. +- `dw install` is optional because it needs a real package and changes the target solution. diff --git a/qa/fixtures/files/source/copied/keep.txt b/qa/fixtures/files/source/copied/keep.txt new file mode 100644 index 0000000..6fc4417 --- /dev/null +++ b/qa/fixtures/files/source/copied/keep.txt @@ -0,0 +1 @@ +This file keeps the copied directory in place for copy tests. diff --git a/qa/fixtures/files/source/moved/keep.txt b/qa/fixtures/files/source/moved/keep.txt new file mode 100644 index 0000000..2c51f74 --- /dev/null +++ b/qa/fixtures/files/source/moved/keep.txt @@ -0,0 +1 @@ +This file keeps the moved directory in place for move tests. diff --git a/qa/fixtures/files/source/nested/nested-upload.txt b/qa/fixtures/files/source/nested/nested-upload.txt new file mode 100644 index 0000000..87d58ac --- /dev/null +++ b/qa/fixtures/files/source/nested/nested-upload.txt @@ -0,0 +1 @@ +Nested fixture for the DynamicWeb CLI QA smoke run. diff --git a/qa/fixtures/files/source/smoke-upload.txt b/qa/fixtures/files/source/smoke-upload.txt new file mode 100644 index 0000000..8dfca8d --- /dev/null +++ b/qa/fixtures/files/source/smoke-upload.txt @@ -0,0 +1 @@ +DynamicWeb CLI QA smoke file. diff --git a/qa/profile.example.json b/qa/profile.example.json new file mode 100644 index 0000000..f3d4633 --- /dev/null +++ b/qa/profile.example.json @@ -0,0 +1,15 @@ +{ + "environmentName": "qa-smoke", + "baseUrl": "https://your-solution.example.com", + "clientIdEnv": "DW_CLIENT_ID", + "clientSecretEnv": "DW_CLIENT_SECRET", + "remoteRoot": "QA/CLI", + "commandTimeoutMs": 120000, + "queries": [], + "commands": [], + "install": { + "enabled": false, + "filePath": "qa/fixtures/addins/YourAddin.nupkg", + "queue": true + } +} diff --git a/qa/run-smoke.mjs b/qa/run-smoke.mjs new file mode 100644 index 0000000..9cd4375 --- /dev/null +++ b/qa/run-smoke.mjs @@ -0,0 +1,1027 @@ +#!/usr/bin/env node + +import fs from 'fs'; +import path from 'path'; +import { spawn } from 'child_process'; +import { fileURLToPath } from 'url'; + +const __filename = fileURLToPath(import.meta.url); +const __dirname = path.dirname(__filename); +const repoRoot = path.resolve(__dirname, '..'); +const defaultProfilePath = path.join(__dirname, 'profile.json'); +const exampleProfilePath = path.join(__dirname, 'profile.example.json'); + +const defaultConfig = { + environmentName: 'qa-smoke', + clientIdEnv: 'DW_CLIENT_ID', + clientSecretEnv: 'DW_CLIENT_SECRET', + remoteRoot: 'QA/CLI', + commandTimeoutMs: 120000, + queries: [], + commands: [], + install: { + enabled: false, + filePath: '', + queue: true + } +}; + +const args = parseArgs(process.argv.slice(2)); + +if (args.help) { + printHelp(); + process.exit(0); +} + +const runId = createRunId(); +const artifactsDir = path.join(__dirname, 'artifacts', runId); +const logsDir = path.join(artifactsDir, 'logs'); +const workspaceDir = path.join(artifactsDir, 'workspace'); +const homeDir = path.join(artifactsDir, 'home'); +const reportPath = path.join(artifactsDir, 'report.json'); + +fs.mkdirSync(logsDir, { recursive: true }); +fs.mkdirSync(workspaceDir, { recursive: true }); +fs.mkdirSync(homeDir, { recursive: true }); + +const profile = loadProfile(args.profile); +const config = buildConfig(profile, args); +const cleanupTargets = []; + +const report = { + runId, + startedAt: new Date().toISOString(), + finishedAt: null, + status: 'running', + mode: config.mode, + artifactsDir, + config: { + baseUrl: config.baseUrl, + protocol: config.protocol, + host: config.host, + environmentName: config.environmentName, + clientIdEnv: config.clientIdEnv, + clientSecretEnv: config.clientSecretEnv, + remoteRoot: config.remoteRoot, + commandTimeoutMs: config.commandTimeoutMs, + profilePath: config.profilePath + }, + steps: [] +}; + +const sharedEnv = { + ...process.env, + HOME: homeDir, + USERPROFILE: homeDir, + HOMEPATH: homeDir, + [config.clientIdEnv]: process.env[config.clientIdEnv], + [config.clientSecretEnv]: process.env[config.clientSecretEnv] +}; + +main().catch(async (error) => { + report.status = 'failed'; + report.error = { + message: error.message, + stack: error.stack + }; + report.finishedAt = new Date().toISOString(); + flushReport(); + await cleanupRemoteTargets(true); + console.error(`QA smoke run failed: ${error.message}`); + console.error(`Report: ${reportPath}`); + process.exitCode = 1; +}); + +async function main() { + console.log(`QA smoke run ${runId}`); + console.log(`Artifacts: ${artifactsDir}`); + + await runStep('validate configuration', async () => { + assert(process.env[config.clientIdEnv], `Missing ${config.clientIdEnv} in the environment.`); + assert(process.env[config.clientSecretEnv], `Missing ${config.clientSecretEnv} in the environment.`); + + return { + baseUrl: config.baseUrl, + host: config.host, + protocol: config.protocol, + environmentName: config.environmentName, + remoteRoot: config.remoteRoot + }; + }); + + const localSourceDir = await runStep('prepare local fixtures', async () => { + const destination = path.join(workspaceDir, 'local-source'); + const templateDir = path.join(__dirname, 'fixtures', 'files', 'source'); + fs.cpSync(templateDir, destination, { recursive: true }); + fs.appendFileSync( + path.join(destination, 'smoke-upload.txt'), + `\nRunId=${runId}\nGeneratedAt=${new Date().toISOString()}\n` + ); + + return { + localSourceDir: destination, + files: listFilesRecursively(destination).map(file => path.relative(destination, file)) + }; + }); + + if (config.mode === 'all' || config.mode === 'saved-env') { + await runSavedEnvironmentFlow(localSourceDir.localSourceDir); + } else { + skipStep('saved environment flow', 'Skipped because --mode is not saved-env or all.'); + } + + if (config.mode === 'all' || config.mode === 'ephemeral') { + await runEphemeralFlow(localSourceDir.localSourceDir); + } else { + skipStep('ephemeral flow', 'Skipped because --mode is not ephemeral or all.'); + } + + await cleanupRemoteTargets(false); + + report.status = 'passed'; + report.finishedAt = new Date().toISOString(); + flushReport(); + + console.log('QA smoke run passed.'); + console.log(`Report: ${reportPath}`); +} + +async function runSavedEnvironmentFlow(localSourceDir) { + console.log('Running saved-environment flow...'); + + await runStep('saved-env: seed config', async () => { + await runCliText('saved-env-config', [ + 'config', + `--env.${config.environmentName}.host`, config.host, + `--env.${config.environmentName}.protocol`, config.protocol, + '--current.env', config.environmentName + ]); + + const configPath = path.join(homeDir, '.dwc'); + const persisted = JSON.parse(fs.readFileSync(configPath, 'utf8')); + + assert(persisted?.env?.[config.environmentName]?.host === config.host, 'Host was not persisted to the isolated config.'); + assert(persisted?.env?.[config.environmentName]?.protocol === config.protocol, 'Protocol was not persisted to the isolated config.'); + assert(persisted?.current?.env === config.environmentName, 'Current environment was not persisted to the isolated config.'); + + return { + configPath, + environmentName: config.environmentName + }; + }); + + await runStep('saved-env: list environments', async () => { + const response = await runCliJson('saved-env-env-list', ['env', '--list', '--output', 'json']); + assertEnvelope(response, 'env', 'list'); + + return response.data[0]; + }); + + await runStep('saved-env: select environment', async () => { + const response = await runCliJson('saved-env-env-select', ['env', config.environmentName, '--output', 'json']); + assertEnvelope(response, 'env', 'select'); + + return response.data[0]; + }); + + await runStep('saved-env: oauth login', async () => { + const response = await runCliJson('saved-env-login', [ + 'login', + '--oauth', + '--clientIdEnv', config.clientIdEnv, + '--clientSecretEnv', config.clientSecretEnv, + '--output', 'json' + ]); + + assertEnvelope(response, 'login', 'oauth-login'); + + return response.data[0]; + }); + + await runStep('saved-env: base command', async () => { + const response = await runCliText('saved-env-base-command', []); + + assert(response.stdout.includes(`Environment: ${config.environmentName}`), 'Base command output is missing the configured environment.'); + assert(response.stdout.includes('Authentication: OAuth client credentials'), 'Base command output is missing the OAuth authentication line.'); + assert(response.stdout.includes(`Host: ${config.host}`), 'Base command output is missing the configured host.'); + + return { + stdout: response.stdout.trim() + }; + }); + + await runConfiguredQueryChecks('saved-env', []); + await runConfiguredCommandChecks('saved-env', []); + + const remoteRoot = path.posix.join(config.remoteRoot, runId, 'saved-env'); + cleanupTargets.push({ + name: 'saved-env', + remoteRoot, + authArgs: [] + }); + + await runFileSuite('saved-env', localSourceDir, remoteRoot, []); + await runInstallCheck('saved-env', []); +} + +async function runEphemeralFlow(localSourceDir) { + console.log('Running ephemeral flow...'); + + const authArgs = getEphemeralAuthArgs(); + + await runStep('ephemeral: root files list', async () => { + const response = await runCliJson('ephemeral-files-root-list', ['files', '--list', '--output', 'json', ...authArgs]); + assertEnvelope(response, 'files', 'list'); + + return { + rootName: response.data[0]?.name ?? '/' + }; + }); + + await runConfiguredQueryChecks('ephemeral', authArgs); + await runConfiguredCommandChecks('ephemeral', authArgs); + + const remoteRoot = path.posix.join(config.remoteRoot, runId, 'ephemeral'); + cleanupTargets.push({ + name: 'ephemeral', + remoteRoot, + authArgs + }); + + await runFileSuite('ephemeral', localSourceDir, remoteRoot, authArgs); +} + +async function runConfiguredQueryChecks(modeLabel, authArgs) { + const enabledQueries = (config.queries || []).filter(query => query.enabled !== false); + + if (enabledQueries.length === 0) { + skipStep(`${modeLabel}: query checks`, 'No queries configured.'); + return; + } + + for (const query of enabledQueries) { + if (query.checkList !== false) { + await runStep(`${modeLabel}: query list ${query.name}`, async () => { + const response = await runCliJson(`${modeLabel}-query-list-${query.name}`, [ + 'query', + query.name, + '--list', + '--output', 'json', + ...authArgs + ]); + + assertEnvelope(response, 'query', 'list'); + + return { + query: query.name, + parameters: response.data[0] + }; + }); + } + + if (query.listOnly) { + skipStep(`${modeLabel}: query run ${query.name}`, 'Skipped because listOnly is true.'); + continue; + } + + await runStep(`${modeLabel}: query run ${query.name}`, async () => { + const response = await runCliJson(`${modeLabel}-query-run-${query.name}`, [ + 'query', + query.name, + ...serializeOptions(query.params), + '--output', 'json', + ...authArgs + ]); + + assertEnvelope(response, 'query', 'run'); + + return { + query: query.name, + resultCount: response.data.length + }; + }); + } +} + +async function runConfiguredCommandChecks(modeLabel, authArgs) { + const enabledCommands = (config.commands || []).filter(command => command.enabled !== false); + + if (enabledCommands.length === 0) { + skipStep(`${modeLabel}: command checks`, 'No commands configured.'); + return; + } + + for (const command of enabledCommands) { + await runStep(`${modeLabel}: command run ${command.name}`, async () => { + const serializedParams = serializeOptions(command.params); + const args = [ + 'command', + command.name, + ...serializedParams, + '--output', 'json', + ...authArgs + ]; + + const body = resolveCommandBody(command); + if (body !== null) { + args.splice(2 + serializedParams.length, 0, '--json', body); + } + + const response = await runCliJson(`${modeLabel}-command-run-${command.name}`, args); + assertEnvelope(response, 'command', 'run'); + + return { + command: command.name, + resultCount: response.data.length + }; + }); + } +} + +async function runFileSuite(modeLabel, localSourceDir, remoteRoot, authArgs) { + const exportRoot = path.join(workspaceDir, `${modeLabel}-export`); + const expectedLocalFile = path.join(localSourceDir, 'smoke-upload.txt'); + const expectedExportedFile = path.join(exportRoot, path.posix.basename(remoteRoot), 'smoke-upload.txt'); + + await runStep(`${modeLabel}: files import`, async () => { + const response = await runCliJson(`${modeLabel}-files-import`, [ + 'files', + localSourceDir, + remoteRoot, + '--import', + '--recursive', + '--overwrite', + '--output', 'json', + ...authArgs + ]); + + assertEnvelope(response, 'files', 'import'); + assert((response.meta?.filesProcessed || 0) >= 4, 'Import did not process the expected number of fixture files.'); + + return { + remoteRoot, + filesProcessed: response.meta.filesProcessed, + chunks: response.meta.chunks + }; + }); + + await runStep(`${modeLabel}: files list imported tree`, async () => { + const response = await runCliJson(`${modeLabel}-files-list-imported`, [ + 'files', + remoteRoot, + '--list', + '--recursive', + '--includeFiles', + '--output', 'json', + ...authArgs + ]); + + assertEnvelope(response, 'files', 'list'); + const listing = flattenListing(response.data[0], remoteRoot); + + assert(listing.files.includes(path.posix.join(remoteRoot, 'smoke-upload.txt')), 'Imported root file is missing from the remote listing.'); + assert(listing.files.includes(path.posix.join(remoteRoot, 'nested', 'nested-upload.txt')), 'Imported nested file is missing from the remote listing.'); + + return { + directories: listing.directories, + files: listing.files + }; + }); + + await runStep(`${modeLabel}: files export`, async () => { + fs.mkdirSync(exportRoot, { recursive: true }); + + const response = await runCliJson(`${modeLabel}-files-export`, [ + 'files', + remoteRoot, + exportRoot, + '--export', + '--output', 'json', + ...authArgs + ]); + + assertEnvelope(response, 'files', 'export'); + assert(fs.existsSync(expectedExportedFile), `Expected exported file was not found at ${expectedExportedFile}.`); + + const expectedContent = fs.readFileSync(expectedLocalFile, 'utf8'); + const exportedContent = fs.readFileSync(expectedExportedFile, 'utf8'); + assert(exportedContent === expectedContent, 'Exported file content does not match the uploaded fixture.'); + + return { + exportedFile: expectedExportedFile + }; + }); + + await runStep(`${modeLabel}: files copy`, async () => { + const sourcePath = path.posix.join(remoteRoot, 'smoke-upload.txt'); + const destination = path.posix.join(remoteRoot, 'copied'); + const response = await runCliJson(`${modeLabel}-files-copy`, [ + 'files', + sourcePath, + '--copy', destination, + '--output', 'json', + ...authArgs + ]); + + assertEnvelope(response, 'files', 'copy'); + + return { + sourcePath, + destination + }; + }); + + await runStep(`${modeLabel}: files verify copied`, async () => { + const response = await runCliJson(`${modeLabel}-files-list-copied`, [ + 'files', + path.posix.join(remoteRoot, 'copied'), + '--list', + '--includeFiles', + '--output', 'json', + ...authArgs + ]); + + assertEnvelope(response, 'files', 'list'); + const listing = flattenListing(response.data[0], path.posix.join(remoteRoot, 'copied')); + assert(listing.files.includes(path.posix.join(remoteRoot, 'copied', 'smoke-upload.txt')), 'Copied file is missing from the destination directory.'); + + return { + files: listing.files + }; + }); + + await runStep(`${modeLabel}: files move`, async () => { + const sourcePath = path.posix.join(remoteRoot, 'copied', 'smoke-upload.txt'); + const destination = path.posix.join(remoteRoot, 'moved'); + const response = await runCliJson(`${modeLabel}-files-move`, [ + 'files', + sourcePath, + '--move', destination, + '--output', 'json', + ...authArgs + ]); + + assertEnvelope(response, 'files', 'move'); + + return { + sourcePath, + destination + }; + }); + + await runStep(`${modeLabel}: files verify moved`, async () => { + const response = await runCliJson(`${modeLabel}-files-list-moved`, [ + 'files', + path.posix.join(remoteRoot, 'moved'), + '--list', + '--includeFiles', + '--output', 'json', + ...authArgs + ]); + + assertEnvelope(response, 'files', 'list'); + const listing = flattenListing(response.data[0], path.posix.join(remoteRoot, 'moved')); + assert(listing.files.includes(path.posix.join(remoteRoot, 'moved', 'smoke-upload.txt')), 'Moved file is missing from the destination directory.'); + + return { + files: listing.files + }; + }); + + await runStep(`${modeLabel}: files delete file`, async () => { + const target = path.posix.join(remoteRoot, 'moved', 'smoke-upload.txt'); + const response = await runCliJson(`${modeLabel}-files-delete-file`, [ + 'files', + target, + '--delete', + '--output', 'json', + ...authArgs + ]); + + assertEnvelope(response, 'files', 'delete'); + + return { + deleted: target + }; + }); + + await runStep(`${modeLabel}: files verify deleted file`, async () => { + const response = await runCliJson(`${modeLabel}-files-list-after-delete`, [ + 'files', + path.posix.join(remoteRoot, 'moved'), + '--list', + '--includeFiles', + '--output', 'json', + ...authArgs + ]); + + assertEnvelope(response, 'files', 'list'); + const listing = flattenListing(response.data[0], path.posix.join(remoteRoot, 'moved')); + assert(!listing.files.includes(path.posix.join(remoteRoot, 'moved', 'smoke-upload.txt')), 'Deleted file is still present in the destination directory.'); + + return { + files: listing.files + }; + }); +} + +async function runInstallCheck(modeLabel, authArgs) { + const installConfig = config.install || {}; + + if (!installConfig.enabled) { + skipStep(`${modeLabel}: install check`, 'Install is disabled in the QA profile.'); + return; + } + + await runStep(`${modeLabel}: install check`, async () => { + const filePath = path.resolve(repoRoot, installConfig.filePath); + assert(fs.existsSync(filePath), `Install fixture was not found: ${filePath}`); + + const response = await runCliJson(`${modeLabel}-install`, [ + 'install', + filePath, + ...(installConfig.queue ? ['--queue'] : []), + '--output', 'json', + ...authArgs + ]); + + assertEnvelope(response, 'install', installConfig.queue ? 'queue' : 'install'); + + return { + filePath + }; + }); +} + +async function cleanupRemoteTargets(alwaysAttempt) { + if (args.keepRemote) { + if (alwaysAttempt) { + console.warn('Remote cleanup skipped because --keep-remote is enabled.'); + } + return; + } + + for (const target of cleanupTargets) { + try { + const response = await runCliJson(`cleanup-${target.name}`, [ + 'files', + target.remoteRoot, + '--delete', + '--output', 'json', + ...target.authArgs + ], { + allowFailure: true + }); + + if (response?.ok) { + console.log(`Cleaned remote folder for ${target.name}: ${target.remoteRoot}`); + } + } catch (error) { + if (alwaysAttempt) { + console.warn(`Best-effort cleanup failed for ${target.remoteRoot}: ${error.message}`); + } else { + throw error; + } + } + } +} + +async function runStep(name, fn) { + const startedAt = new Date(); + const entry = { + name, + status: 'running', + startedAt: startedAt.toISOString() + }; + + report.steps.push(entry); + flushReport(); + + try { + const details = await fn(); + entry.status = 'passed'; + entry.details = details; + entry.finishedAt = new Date().toISOString(); + entry.durationMs = new Date(entry.finishedAt).getTime() - startedAt.getTime(); + flushReport(); + console.log(`PASS ${name}`); + return details; + } catch (error) { + entry.status = 'failed'; + entry.error = { + message: error.message + }; + entry.finishedAt = new Date().toISOString(); + entry.durationMs = new Date(entry.finishedAt).getTime() - startedAt.getTime(); + flushReport(); + console.error(`FAIL ${name}: ${error.message}`); + throw error; + } +} + +function skipStep(name, reason) { + report.steps.push({ + name, + status: 'skipped', + startedAt: new Date().toISOString(), + finishedAt: new Date().toISOString(), + durationMs: 0, + details: { + reason + } + }); + flushReport(); + console.log(`SKIP ${name}: ${reason}`); +} + +async function runCliJson(logName, cliArgs, options = {}) { + const result = await runCli(logName, cliArgs, { ...options, expectJson: true }); + + if (!result.parsed && !options.allowFailure) { + throw new Error(`Expected JSON output from ${logName}, but stdout was empty.`); + } + + return result.parsed; +} + +async function runCliText(logName, cliArgs, options = {}) { + return await runCli(logName, cliArgs, { ...options, expectJson: false }); +} + +async function runCli(logName, cliArgs, options = {}) { + const command = [process.execPath, path.join('bin', 'index.js'), ...cliArgs]; + const timeoutMs = options.timeoutMs ?? config.commandTimeoutMs; + const child = spawn(command[0], command.slice(1), { + cwd: repoRoot, + env: sharedEnv, + stdio: ['ignore', 'pipe', 'pipe'] + }); + + let stdout = ''; + let stderr = ''; + + child.stdout.on('data', chunk => { + stdout += chunk.toString(); + }); + + child.stderr.on('data', chunk => { + stderr += chunk.toString(); + }); + + const timeoutHandle = setTimeout(() => { + stderr += `\nProcess timed out after ${timeoutMs} ms.`; + child.kill('SIGTERM'); + + setTimeout(() => { + if (!child.killed) { + child.kill('SIGKILL'); + } + }, 5000).unref(); + }, timeoutMs); + + const exitCode = await new Promise((resolve, reject) => { + child.on('error', reject); + child.on('close', resolve); + }); + clearTimeout(timeoutHandle); + + let parsed = null; + if (options.expectJson && stdout.trim()) { + try { + parsed = JSON.parse(stdout); + } catch (error) { + throw new Error(`Failed to parse JSON output for ${logName}: ${error.message}\nSTDOUT:\n${stdout}\nSTDERR:\n${stderr}`); + } + } + + const logPath = path.join(logsDir, `${String(report.steps.length).padStart(2, '0')}-${slugify(logName)}.json`); + fs.writeFileSync(logPath, JSON.stringify({ + command, + timeoutMs, + exitCode, + stdout, + stderr, + parsed + }, null, 2)); + + if (exitCode !== 0 && !options.allowFailure) { + const details = parsed?.errors?.map(error => error.message).join('; ') || stderr.trim() || stdout.trim(); + throw new Error(`${logName} exited with code ${exitCode}. ${details}`); + } + + return { + exitCode, + stdout, + stderr, + parsed, + logPath + }; +} + +function buildConfig(profile, cliArgs) { + const merged = { + ...defaultConfig, + ...profile, + install: { + ...defaultConfig.install, + ...(profile.install || {}) + } + }; + + const baseUrl = cliArgs.baseUrl || process.env.DW_BASE_URL || merged.baseUrl; + assert(baseUrl, 'Missing base URL. Set DW_BASE_URL, add it to qa/profile.json, or pass --baseUrl.'); + + const { protocol, host, normalizedBaseUrl } = normalizeBaseUrl(baseUrl); + + return { + ...merged, + protocol, + host, + baseUrl: normalizedBaseUrl, + environmentName: cliArgs.environmentName || merged.environmentName, + clientIdEnv: cliArgs.clientIdEnv || merged.clientIdEnv, + clientSecretEnv: cliArgs.clientSecretEnv || merged.clientSecretEnv, + remoteRoot: normalizeRemoteRoot(cliArgs.remoteRoot || merged.remoteRoot), + commandTimeoutMs: parseTimeout(cliArgs.timeoutMs ?? merged.commandTimeoutMs), + mode: cliArgs.mode, + profilePath: profile.__profilePath || null + }; +} + +function loadProfile(profilePathArg) { + let profilePath = null; + + if (profilePathArg) { + profilePath = path.resolve(repoRoot, profilePathArg); + assert(fs.existsSync(profilePath), `Profile file not found: ${profilePath}`); + } else if (fs.existsSync(defaultProfilePath)) { + profilePath = defaultProfilePath; + } + + if (!profilePath) { + return { __profilePath: null }; + } + + const profile = JSON.parse(fs.readFileSync(profilePath, 'utf8')); + profile.__profilePath = profilePath; + return profile; +} + +function parseArgs(argv) { + const parsed = { + profile: null, + mode: 'all', + baseUrl: null, + remoteRoot: null, + environmentName: null, + clientIdEnv: null, + clientSecretEnv: null, + timeoutMs: null, + keepRemote: false, + help: false + }; + + for (let index = 0; index < argv.length; index += 1) { + const arg = argv[index]; + + if (arg === '--help' || arg === '-h') { + parsed.help = true; + continue; + } + + if (arg === '--keep-remote') { + parsed.keepRemote = true; + continue; + } + + const next = argv[index + 1]; + if (!next) { + throw new Error(`Missing value for ${arg}`); + } + + if (arg === '--profile') { + parsed.profile = next; + index += 1; + continue; + } + + if (arg === '--mode') { + assert(['all', 'saved-env', 'ephemeral'].includes(next), `Invalid mode "${next}". Expected all, saved-env, or ephemeral.`); + parsed.mode = next; + index += 1; + continue; + } + + if (arg === '--baseUrl') { + parsed.baseUrl = next; + index += 1; + continue; + } + + if (arg === '--remoteRoot') { + parsed.remoteRoot = next; + index += 1; + continue; + } + + if (arg === '--environmentName') { + parsed.environmentName = next; + index += 1; + continue; + } + + if (arg === '--clientIdEnv') { + parsed.clientIdEnv = next; + index += 1; + continue; + } + + if (arg === '--clientSecretEnv') { + parsed.clientSecretEnv = next; + index += 1; + continue; + } + + if (arg === '--timeoutMs') { + parsed.timeoutMs = next; + index += 1; + continue; + } + + throw new Error(`Unknown argument: ${arg}`); + } + + return parsed; +} + +function printHelp() { + console.log(` +Usage: + npm run qa:smoke + npm run qa:smoke -- --profile qa/profile.json + +Options: + --profile Load a QA profile JSON file + --mode + --baseUrl Override the solution base URL + --remoteRoot Override the remote QA root directory + --environmentName Override the saved environment name + --clientIdEnv Override the OAuth client ID env var name + --clientSecretEnv Override the OAuth client secret env var name + --timeoutMs Fail a CLI invocation if it runs longer than this + --keep-remote Leave remote QA folders behind for debugging + --help Show this help + +Defaults: + profile: qa/profile.json if present, otherwise no profile + base URL: DW_BASE_URL + client ID env: DW_CLIENT_ID + client secret env: DW_CLIENT_SECRET + command timeout: 120000 ms +`.trim()); + + if (fs.existsSync(exampleProfilePath)) { + console.log(`\nExample profile: ${exampleProfilePath}`); + } +} + +function getEphemeralAuthArgs() { + return [ + '--host', config.host, + '--protocol', config.protocol, + '--auth', 'oauth', + '--clientIdEnv', config.clientIdEnv, + '--clientSecretEnv', config.clientSecretEnv + ]; +} + +function normalizeBaseUrl(rawValue) { + const baseValue = /^https?:\/\//i.test(rawValue) ? rawValue : `https://${rawValue}`; + const parsed = new URL(baseValue); + + assert(!parsed.username && !parsed.password, 'The base URL must not include embedded credentials.'); + assert(!parsed.search && !parsed.hash, 'The base URL must not include a query string or hash fragment.'); + assert(parsed.pathname === '/' || parsed.pathname === '', 'The base URL must not include a path segment.'); + + return { + protocol: parsed.protocol.replace(':', ''), + host: parsed.host, + normalizedBaseUrl: `${parsed.protocol}//${parsed.host}` + }; +} + +function normalizeRemoteRoot(remoteRoot) { + return remoteRoot.replace(/^\/+/, '').replace(/\/+$/, ''); +} + +function parseTimeout(rawValue) { + const timeoutMs = Number(rawValue); + assert(Number.isFinite(timeoutMs) && timeoutMs > 0, `Invalid timeout "${rawValue}". Expected a positive number of milliseconds.`); + return timeoutMs; +} + +function resolveCommandBody(command) { + if (command.bodyFile) { + const profileDir = config.profilePath ? path.dirname(config.profilePath) : repoRoot; + const bodyFile = path.resolve(profileDir, command.bodyFile); + assert(fs.existsSync(bodyFile), `Command body file not found: ${bodyFile}`); + return fs.readFileSync(bodyFile, 'utf8'); + } + + if (command.body !== undefined) { + return JSON.stringify(command.body); + } + + return null; +} + +function serializeOptions(options = {}) { + const args = []; + + for (const [key, value] of Object.entries(options)) { + if (value === undefined || value === null) { + continue; + } + + if (Array.isArray(value)) { + for (const item of value) { + args.push(`--${key}`, String(item)); + } + continue; + } + + if (typeof value === 'boolean') { + args.push(`--${key}`, String(value)); + continue; + } + + args.push(`--${key}`, String(value)); + } + + return args; +} + +function flattenListing(root, rootPath = '') { + const directories = rootPath ? [rootPath] : []; + const files = []; + + walkListing(root, rootPath, directories, files, true); + + return { directories, files }; +} + +function walkListing(node, currentPath, directories, files, isRoot = false) { + if (!isRoot && currentPath) { + directories.push(currentPath); + } + + for (const file of node?.files?.data || []) { + files.push(path.posix.join(currentPath, file.name)); + } + + for (const directory of node?.directories || []) { + walkListing(directory, path.posix.join(currentPath, directory.name), directories, files); + } +} + +function listFilesRecursively(rootDir) { + const entries = fs.readdirSync(rootDir, { withFileTypes: true }); + const files = []; + + for (const entry of entries) { + const resolved = path.join(rootDir, entry.name); + if (entry.isDirectory()) { + files.push(...listFilesRecursively(resolved)); + } else { + files.push(resolved); + } + } + + return files.sort(); +} + +function assertEnvelope(response, command, operation) { + assert(response, `No response received for ${command}.`); + assert(response.command === command, `Expected command "${command}" but got "${response.command}".`); + assert(response.operation === operation, `Expected operation "${operation}" but got "${response.operation}".`); + assert(response.ok === true, `${command} returned a non-ok response.`); +} + +function assert(condition, message) { + if (!condition) { + throw new Error(message); + } +} + +function flushReport() { + fs.writeFileSync(reportPath, JSON.stringify(report, null, 2)); +} + +function createRunId() { + const timestamp = new Date().toISOString().replace(/[:.]/g, '-'); + return `qa-smoke-${timestamp}`; +} + +function slugify(value) { + return value.toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-+|-+$/g, ''); +} diff --git a/test/command.test.js b/test/command.test.js new file mode 100644 index 0000000..dc9e162 --- /dev/null +++ b/test/command.test.js @@ -0,0 +1,52 @@ +import test from 'node:test'; +import assert from 'node:assert/strict'; +import fs from 'node:fs'; +import os from 'node:os'; +import path from 'node:path'; + +import { getQueryParams, parseJsonOrPath } from '../bin/commands/command.js'; + +test('getQueryParams prefixes custom args and excludes framework args', () => { + const params = getQueryParams({ + command: 'DoThing', + host: 'example.com', + protocol: 'https', + verbose: true, + id: 42, + culture: 'en-US' + }); + + assert.deepEqual(params, { + 'Command.id': 42, + 'Command.culture': 'en-US' + }); +}); + +test('parseJsonOrPath returns undefined for empty input', () => { + assert.equal(parseJsonOrPath(), undefined); +}); + +test('parseJsonOrPath parses literal json', () => { + assert.deepEqual(parseJsonOrPath('{"model":{"id":123}}'), { + model: { + id: 123 + } + }); +}); + +test('parseJsonOrPath parses json from a file path', () => { + const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'cli-command-test-')); + const jsonPath = path.join(tempDir, 'body.json'); + + try { + fs.writeFileSync(jsonPath, '{"model":{"id":456}}'); + + assert.deepEqual(parseJsonOrPath(jsonPath), { + model: { + id: 456 + } + }); + } finally { + fs.rmSync(tempDir, { recursive: true, force: true }); + } +}); diff --git a/test/downloader.test.js b/test/downloader.test.js new file mode 100644 index 0000000..fee329c --- /dev/null +++ b/test/downloader.test.js @@ -0,0 +1,62 @@ +import test from 'node:test'; +import assert from 'node:assert/strict'; + +import { getFileNameFromResponse, tryGetFileNameFromResponse } from '../bin/downloader.js'; + +function createResponse(contentDisposition) { + return { + headers: { + get(name) { + return name.toLowerCase() === 'content-disposition' ? contentDisposition : null; + } + } + }; +} + +test('getFileNameFromResponse extracts the filename from content-disposition', () => { + const response = createResponse('attachment; filename=My+Archive.zip'); + + assert.equal(getFileNameFromResponse(response, '/Files'), 'My Archive.zip'); +}); + +test('getFileNameFromResponse throws when no file metadata exists', () => { + const response = createResponse(null); + + assert.throws( + () => getFileNameFromResponse(response, '/Files'), + /No files found in directory '\/Files'/ + ); +}); + +test('tryGetFileNameFromResponse returns null and stays silent by default', () => { + const response = createResponse(null); + const originalLog = console.log; + const calls = []; + console.log = (...args) => { + calls.push(args); + }; + + try { + assert.equal(tryGetFileNameFromResponse(response, '/Files'), null); + assert.deepEqual(calls, []); + } finally { + console.log = originalLog; + } +}); + +test('tryGetFileNameFromResponse logs the error message in verbose mode', () => { + const response = createResponse(null); + const originalLog = console.log; + const calls = []; + console.log = (...args) => { + calls.push(args); + }; + + try { + assert.equal(tryGetFileNameFromResponse(response, '/Files', true), null); + assert.equal(calls.length, 1); + assert.match(String(calls[0][0]), /No files found in directory '\/Files'/); + } finally { + console.log = originalLog; + } +}); diff --git a/test/env.test.js b/test/env.test.js new file mode 100644 index 0000000..c28c4b9 --- /dev/null +++ b/test/env.test.js @@ -0,0 +1,170 @@ +import test from 'node:test'; +import assert from 'node:assert/strict'; + +import { setConfigForTests } from '../bin/commands/config.js'; +import { parseHostInput, setupEnv } from '../bin/commands/env.js'; + +test.afterEach(() => { + setConfigForTests({}); +}); + +test('parseHostInput defaults to https when no protocol is provided', () => { + assert.deepEqual(parseHostInput('example.com'), { + protocol: 'https', + host: 'example.com' + }); +}); + +test('parseHostInput preserves an explicit protocol', () => { + assert.deepEqual(parseHostInput('http://example.com'), { + protocol: 'http', + host: 'example.com' + }); +}); + +test('parseHostInput rejects malformed host values with multiple protocol separators', () => { + assert.throws( + () => parseHostInput('https://example.com://admin'), + /Issues resolving host/ + ); +}); + +test('setupEnv prefers direct host arguments and defaults protocol to https', async () => { + setConfigForTests({ + env: { + saved: { + host: 'saved.example.com', + protocol: 'http' + } + }, + current: { + env: 'saved' + } + }); + + const env = await setupEnv({ + host: 'override.example.com' + }); + + assert.deepEqual(env, { + host: 'override.example.com', + protocol: 'https' + }); +}); + +test('setupEnv uses the explicit protocol when provided with a host override', async () => { + const env = await setupEnv({ + host: 'override.example.com', + protocol: 'http' + }); + + assert.deepEqual(env, { + host: 'override.example.com', + protocol: 'http' + }); +}); + +test('setupEnv resolves the requested environment from config', async () => { + setConfigForTests({ + env: { + dev: { + host: 'dev.example.com', + protocol: 'https' + }, + prod: { + host: 'prod.example.com', + protocol: 'http' + } + }, + current: { + env: 'dev' + } + }); + + const env = await setupEnv({ + env: 'prod' + }); + + assert.deepEqual(env, { + host: 'prod.example.com', + protocol: 'http' + }); +}); + +test('setupEnv falls back to the current environment and defaults a missing protocol', async () => { + const originalLog = console.log; + const logCalls = []; + console.log = (...args) => { + logCalls.push(args); + }; + + setConfigForTests({ + env: { + dev: { + host: 'dev.example.com' + } + }, + current: { + env: 'dev' + } + }); + + try { + const env = await setupEnv({}); + + assert.deepEqual(env, { + host: 'dev.example.com', + protocol: 'https' + }); + assert.equal(logCalls.length, 1); + assert.match(String(logCalls[0][0]), /Protocol for environment not set, defaulting to https/); + } finally { + console.log = originalLog; + } +}); + +test('setupEnv throws in json mode when no environment is configured', async () => { + setConfigForTests({}); + + await assert.rejects( + setupEnv({ + output: 'json' + }), + /Current environment not set, please set it/ + ); +}); + +test('setupEnv uses the interactive fallback when no environment is configured', async () => { + setConfigForTests({}); + const calls = []; + const originalLog = console.log; + console.log = () => {}; + + try { + const env = await setupEnv({}, null, { + interactiveEnvFn: async (argv, options) => { + calls.push({ argv, options }); + setConfigForTests({ + env: { + qa: { + host: 'qa.example.com', + protocol: 'https' + } + }, + current: { + env: 'qa' + } + }); + } + }); + + assert.deepEqual(env, { + host: 'qa.example.com', + protocol: 'https' + }); + assert.equal(calls.length, 1); + assert.deepEqual(Object.keys(calls[0].options), ['environment', 'interactive']); + } finally { + console.log = originalLog; + } +}); diff --git a/test/files.test.js b/test/files.test.js new file mode 100644 index 0000000..cbac51c --- /dev/null +++ b/test/files.test.js @@ -0,0 +1,87 @@ +import test from 'node:test'; +import assert from 'node:assert/strict'; +import fs from 'node:fs'; +import os from 'node:os'; +import path from 'node:path'; + +import { + getFilesOperation, + isFilePath, + prepareDownloadCommandData, + resolveFilePath, + wildcardToRegExp +} from '../bin/commands/files.js'; + +test('prepareDownloadCommandData uses directory download for recursive folder exports', () => { + const result = prepareDownloadCommandData('/Files', 'system/log', [], true, false); + + assert.equal(result.endpoint, 'DirectoryDownload'); + assert.deepEqual(result.data, { + DirectoryPath: '/Files', + ExcludeDirectories: ['system/log'] + }); +}); + +test('prepareDownloadCommandData uses file download for single-file exports', () => { + const result = prepareDownloadCommandData('/Files', '', ['/Files/logo.png'], false, true); + + assert.equal(result.endpoint, 'FileDownload'); + assert.deepEqual(result.data, { + DirectoryPath: '/Files', + ExcludeDirectories: [''], + Ids: ['/Files/logo.png'] + }); +}); + +test('isFilePath respects explicit overrides before extension detection', () => { + assert.equal(isFilePath({ asFile: true }, 'folder.with.dot'), true); + assert.equal(isFilePath({ asDirectory: true }, 'file.txt'), false); + assert.equal(isFilePath({}, 'file.txt'), true); + assert.equal(isFilePath({}, 'folder'), false); +}); + +test('wildcardToRegExp escapes regex characters and expands asterisks', () => { + const regex = wildcardToRegExp('plugin-*.dll'); + + assert.equal(regex.test('plugin-core.dll'), true); + assert.equal(regex.test('plugin-core.dll.bak'), false); + assert.equal(regex.test('plugin-(core).dll'), true); +}); + +test('resolveFilePath resolves wildcard matches from the target directory', () => { + const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'cli-files-test-')); + + try { + fs.writeFileSync(path.join(tempDir, 'plugin-core.dll'), ''); + fs.writeFileSync(path.join(tempDir, 'plugin-extra.nupkg'), ''); + + const resolved = resolveFilePath(path.join(tempDir, 'plugin-*.dll')); + + assert.equal(resolved, path.join(tempDir, 'plugin-core.dll')); + } finally { + fs.rmSync(tempDir, { recursive: true, force: true }); + } +}); + +test('resolveFilePath throws when the wildcard finds no match', () => { + const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'cli-files-test-')); + + try { + assert.throws( + () => resolveFilePath(path.join(tempDir, '*.dll')), + /Could not find any files with the name/ + ); + } finally { + fs.rmSync(tempDir, { recursive: true, force: true }); + } +}); + +test('getFilesOperation reports the selected files subcommand', () => { + assert.equal(getFilesOperation({ list: true }), 'list'); + assert.equal(getFilesOperation({ export: true }), 'export'); + assert.equal(getFilesOperation({ import: true }), 'import'); + assert.equal(getFilesOperation({ delete: true }), 'delete'); + assert.equal(getFilesOperation({ copy: '/dest' }), 'copy'); + assert.equal(getFilesOperation({ move: '/dest' }), 'move'); + assert.equal(getFilesOperation({}), 'unknown'); +}); diff --git a/test/login.test.js b/test/login.test.js new file mode 100644 index 0000000..8b41ed8 --- /dev/null +++ b/test/login.test.js @@ -0,0 +1,88 @@ +import test from 'node:test'; +import assert from 'node:assert/strict'; + +import { parseCookies, resolveOAuthConfig, shouldUseOAuth } from '../bin/commands/login.js'; + +test('parseCookies returns an empty object when the header is missing', () => { + assert.deepEqual(parseCookies(), {}); +}); + +test('parseCookies normalizes the Dynamicweb cookie name and decodes the value', () => { + const cookies = parseCookies('Dynamicweb.Admin=abc%20123; Path=/'); + + assert.equal(cookies.user, 'abc 123'); + assert.equal(cookies.Path, '/'); +}); + +test('shouldUseOAuth honors explicit user auth override', () => { + assert.equal(shouldUseOAuth({ auth: 'user', oauth: true }, { + current: { authType: 'oauth_client_credentials' } + }), false); +}); + +test('shouldUseOAuth enables oauth from arguments or environment configuration', () => { + assert.equal(shouldUseOAuth({ oauth: true }), true); + assert.equal(shouldUseOAuth({}, { current: { authType: 'oauth_client_credentials' } }), true); + assert.equal(shouldUseOAuth({}, { auth: { type: 'oauth_client_credentials' } }), true); + assert.equal(shouldUseOAuth({}), false); +}); + +test('resolveOAuthConfig prefers explicit args over environment variables', () => { + process.env.TEST_CLI_CLIENT_ID = 'env-client-id'; + process.env.TEST_CLI_CLIENT_SECRET = 'env-client-secret'; + + try { + const config = resolveOAuthConfig({ + clientId: 'arg-client-id', + clientSecret: 'arg-client-secret', + clientIdEnv: 'TEST_CLI_CLIENT_ID', + clientSecretEnv: 'TEST_CLI_CLIENT_SECRET' + }); + + assert.deepEqual(config, { + clientId: 'arg-client-id', + clientSecret: 'arg-client-secret', + clientIdEnv: 'TEST_CLI_CLIENT_ID', + clientSecretEnv: 'TEST_CLI_CLIENT_SECRET' + }); + } finally { + delete process.env.TEST_CLI_CLIENT_ID; + delete process.env.TEST_CLI_CLIENT_SECRET; + } +}); + +test('resolveOAuthConfig falls back to configured env var names', () => { + process.env.TEST_CLI_CLIENT_ID = 'env-client-id'; + process.env.TEST_CLI_CLIENT_SECRET = 'env-client-secret'; + + try { + const config = resolveOAuthConfig({}, { + auth: { + clientIdEnv: 'TEST_CLI_CLIENT_ID', + clientSecretEnv: 'TEST_CLI_CLIENT_SECRET' + } + }); + + assert.deepEqual(config, { + clientId: 'env-client-id', + clientSecret: 'env-client-secret', + clientIdEnv: 'TEST_CLI_CLIENT_ID', + clientSecretEnv: 'TEST_CLI_CLIENT_SECRET' + }); + } finally { + delete process.env.TEST_CLI_CLIENT_ID; + delete process.env.TEST_CLI_CLIENT_SECRET; + } +}); + +test('resolveOAuthConfig throws when required credentials are missing', () => { + assert.throws( + () => resolveOAuthConfig({ clientIdEnv: 'MISSING_CLIENT_ID', clientSecret: 'secret' }), + /OAuth client ID not found/ + ); + + assert.throws( + () => resolveOAuthConfig({ clientId: 'client-id', clientSecretEnv: 'MISSING_CLIENT_SECRET' }), + /OAuth client secret not found/ + ); +}); diff --git a/test/query.test.js b/test/query.test.js new file mode 100644 index 0000000..3dc242a --- /dev/null +++ b/test/query.test.js @@ -0,0 +1,140 @@ +import test from 'node:test'; +import assert from 'node:assert/strict'; + +import { + buildInteractiveQueryParams, + buildQueryParamsFromArgv, + extractQueryPropertyPrompts, + getFieldNameFromPropertyPrompt, + getQueryParams +} from '../bin/commands/query.js'; + +test('extractQueryPropertyPrompts maps properties fields to prompt labels', () => { + const prompts = extractQueryPropertyPrompts({ + model: { + properties: { + groups: [ + { + name: 'Properties', + fields: [ + { name: 'id', typeName: 'System.Int32' }, + { name: 'culture', typeName: 'System.String' } + ] + } + ] + } + } + }); + + assert.deepEqual(prompts, [ + 'id (System.Int32)', + 'culture (System.String)' + ]); +}); + +test('extractQueryPropertyPrompts returns an empty list when the Properties group is missing', () => { + const prompts = extractQueryPropertyPrompts({ + model: { + properties: { + groups: [ + { + name: 'Other', + fields: [ + { name: 'ignored', typeName: 'System.String' } + ] + } + ] + } + } + }); + + assert.deepEqual(prompts, []); +}); + +test('getFieldNameFromPropertyPrompt removes only the trailing type suffix', () => { + assert.equal(getFieldNameFromPropertyPrompt('id (System.Int32)'), 'id'); + assert.equal(getFieldNameFromPropertyPrompt('Category (Primary) (System.String)'), 'Category (Primary)'); + assert.equal(getFieldNameFromPropertyPrompt('plainField'), 'plainField'); +}); + +test('buildInteractiveQueryParams maps prompt answers back to field names and skips empty values', async () => { + const prompts = [ + 'id (System.Int32)', + 'Category (Primary) (System.String)', + 'culture (System.String)' + ]; + const answers = ['42', 'news', '']; + const seenMessages = []; + + const params = await buildInteractiveQueryParams(prompts, async ({ message }) => { + seenMessages.push(message); + return answers.shift(); + }); + + assert.deepEqual(seenMessages, prompts); + assert.deepEqual(params, { + id: '42', + 'Category (Primary)': 'news' + }); +}); + +test('buildQueryParamsFromArgv keeps only query-specific arguments', () => { + const params = buildQueryParamsFromArgv({ + query: 'GetItems', + host: 'example.com', + protocol: 'https', + interactive: true, + output: 'json', + id: 123, + culture: 'en-US' + }); + + assert.deepEqual(params, { + id: 123, + culture: 'en-US' + }); +}); + +test('getQueryParams uses filtered argv params in non-interactive mode', async () => { + const params = await getQueryParams(null, null, { + query: 'GetItems', + host: 'example.com', + id: 99, + pageSize: 10 + }, { + log() {} + }); + + assert.deepEqual(params, { + id: 99, + pageSize: 10 + }); +}); + +test('getQueryParams uses property prompts and prompt answers in interactive mode', async () => { + const outputCalls = []; + + const params = await getQueryParams(null, null, { + query: 'GetItems', + interactive: true + }, { + log(value) { + outputCalls.push(value); + } + }, { + getPropertiesFn: async () => [ + 'id (System.Int32)', + 'culture (System.String)' + ], + promptFn: async ({ message }) => message === 'id (System.Int32)' ? '77' : 'da-DK' + }); + + assert.deepEqual(params, { + id: '77', + culture: 'da-DK' + }); + assert.deepEqual(outputCalls, [ + 'The following properties will be requested:', + ['id (System.Int32)', 'culture (System.String)'] + ]); +}); diff --git a/test/utils.test.js b/test/utils.test.js new file mode 100644 index 0000000..72e4d63 --- /dev/null +++ b/test/utils.test.js @@ -0,0 +1,20 @@ +import test from 'node:test'; +import assert from 'node:assert/strict'; + +import { formatBytes, formatElapsed } from '../bin/utils.js'; + +test('formatBytes formats zero bytes', () => { + assert.equal(formatBytes(0), '0 Bytes'); +}); + +test('formatBytes formats kilobytes and megabytes', () => { + assert.equal(formatBytes(1024), '1.00 KB'); + assert.equal(formatBytes(1536), '1.50 KB'); + assert.equal(formatBytes(1024 * 1024), '1.00 MB'); +}); + +test('formatElapsed formats seconds, minutes, and hours', () => { + assert.equal(formatElapsed(999), '0s'); + assert.equal(formatElapsed(61_000), '1m 1s'); + assert.equal(formatElapsed(3_661_000), '1h 1m 1s'); +}); From 56233042020e6c800385e5bcf4e171bb2d281ffc Mon Sep 17 00:00:00 2001 From: nicped Date: Wed, 15 Apr 2026 14:29:31 +0200 Subject: [PATCH 77/86] Refactor command handling and improve error handling across multiple files; add tests for JSON parsing and formatting functions --- bin/commands/command.js | 41 ++++---- bin/commands/config.js | 8 ++ bin/commands/env.js | 20 +++- bin/commands/files.js | 221 ++++++++++++++++++++-------------------- bin/commands/install.js | 60 ++++++----- bin/commands/query.js | 2 +- bin/index.js | 8 +- cliv2-documentation.md | 16 ++- qa/README.md | 8 +- qa/run-smoke.mjs | 5 +- test/command.test.js | 24 +++++ test/downloader.test.js | 34 ++----- test/utils.test.js | 16 +++ 13 files changed, 272 insertions(+), 191 deletions(-) diff --git a/bin/commands/command.js b/bin/commands/command.js index 8a13197..b78f7ae 100644 --- a/bin/commands/command.js +++ b/bin/commands/command.js @@ -8,24 +8,24 @@ const exclude = ['_', '$0', 'command', 'list', 'json', 'verbose', 'v', 'host', ' export function commandCommand() { return { - command: 'command [command]', - describe: 'Runs the given command', + command: 'command [command]', + describe: 'Runs the given command', builder: (yargs) => { return yargs - .positional('command', { - describe: 'The command to execute' - }) - .option('json', { - describe: 'Literal json or location of json file to send' - }) - .option('list', { - alias: 'l', - describe: 'Lists all the properties for the command, currently not working' - }) - .option('output', { - choices: ['json'], - describe: 'Outputs a single JSON response for automation-friendly parsing' - }) + .positional('command', { + describe: 'The command to execute' + }) + .option('json', { + describe: 'Literal json or location of json file to send' + }) + .option('list', { + alias: 'l', + describe: 'Lists all the properties for the command, currently not working' + }) + .option('output', { + choices: ['json'], + describe: 'Outputs a single JSON response for automation-friendly parsing' + }) }, handler: async (argv) => { const output = createCommandOutput(argv); @@ -69,11 +69,14 @@ export function getQueryParams(argv) { export function parseJsonOrPath(json) { if (!json) return + const trimmed = json.trim(); + if (trimmed.startsWith('{') || trimmed.startsWith('[')) { + return JSON.parse(trimmed); + } if (fs.existsSync(json)) { - return JSON.parse(fs.readFileSync(path.resolve(json))) - } else { - return JSON.parse(json) + return JSON.parse(fs.readFileSync(path.resolve(json))); } + return JSON.parse(json); } async function runCommand(env, user, command, queryParams, data) { diff --git a/bin/commands/config.js b/bin/commands/config.js index 7ef298a..c249202 100644 --- a/bin/commands/config.js +++ b/bin/commands/config.js @@ -30,11 +30,19 @@ export function getConfig() { return localConfig || {}; } +/** + * Overrides the in-memory config for testing. + * @param {Object} config - Must be a plain, non-null object; handleConfig writes keys directly onto it. + */ export function setConfigForTests(config) { + if (config === null || typeof config !== 'object') { + throw new Error('setConfigForTests: config must be a plain object'); + } localConfig = config; } export function handleConfig(argv) { + localConfig = localConfig || {}; Object.keys(argv).forEach(a => { if (a != '_' && a != '$0') { localConfig[a] = resolveConfig(a, argv[a], localConfig[a] || {}); diff --git a/bin/commands/env.js b/bin/commands/env.js index 6b79328..dd72e07 100644 --- a/bin/commands/env.js +++ b/bin/commands/env.js @@ -23,6 +23,10 @@ export function getAgent(protocol) { } export function parseHostInput(hostValue) { + if (!hostValue || typeof hostValue !== 'string' || !hostValue.trim()) { + throw createCommandError(`Invalid host value: ${hostValue}`); + } + hostValue = hostValue.trim(); const hostSplit = hostValue.split('://'); if (hostSplit.length === 1) { @@ -142,7 +146,9 @@ async function handleEnv(argv, output) { output.log(`Users in environment ${env}: ${users}`); } else if (argv.env) { const result = await changeEnv(argv, output); - output.addData(result); + if (result !== null) { + output.addData(result); + } } else if (argv.list) { const environments = Object.keys(getConfig().env || {}); output.addData({ environments }); @@ -198,7 +204,7 @@ export async function interactiveEnv(argv, options, output) { const currentEnv = getConfig().env[result.environment]; const data = { environment: result.environment, - protocol: currentEnv.protocol, + protocol: currentEnv.protocol || null, host: currentEnv.host || null, current: getConfig().current.env }; @@ -213,13 +219,13 @@ export async function interactiveEnv(argv, options, output) { async function changeEnv(argv, output) { const environments = getConfig().env || {}; - if (!Object.keys(environments).includes(argv.env)) { + if (!Object.hasOwn(environments, argv.env)) { if (isJsonOutput(argv)) { throw createCommandError(`The specified environment ${argv.env} doesn't exist, please create it`, 404); } logMessage(argv, `The specified environment ${argv.env} doesn't exist, please create it`); - return await interactiveEnv(argv, { + await interactiveEnv(argv, { environment: { type: 'input', default: argv.env, @@ -234,6 +240,7 @@ async function changeEnv(argv, output) { default: true } }, output) + return null; } else { getConfig().current.env = argv.env; updateConfig(); @@ -242,7 +249,10 @@ async function changeEnv(argv, output) { current: getConfig().current.env }; logMessage(argv, `Your current environment is now ${getConfig().current.env}`); - return data; + if (output) { + output.addData(data); + } + return null; } } diff --git a/bin/commands/files.js b/bin/commands/files.js index c0b77cf..8092d43 100644 --- a/bin/commands/files.js +++ b/bin/commands/files.js @@ -10,102 +10,102 @@ import { extractWithProgress } from '../extractor.js'; export function filesCommand() { return { - command: 'files [dirPath] [outPath]', - describe: 'Handles files', + command: 'files [dirPath] [outPath]', + describe: 'Handles files', builder: (yargs) => { return yargs - .positional('dirPath', { - describe: 'The directory to list or export' - }) - .positional('outPath', { - describe: 'The directory to export the specified directory to', - default: '.' - }) - .option('list', { - alias: 'l', - type: 'boolean', - describe: 'Lists all directories and files' - }) - .option('export', { - alias: 'e', - type: 'boolean', - describe: 'Exports the specified directory and all subdirectories at [dirPath] to [outPath]' - }) - .option('import', { - alias: 'i', - type: 'boolean', - describe: 'Imports the file at [dirPath] to [outPath]' - }) - .option('overwrite', { - alias: 'o', - type: 'boolean', - describe: 'Used with import, will overwrite existing files at destination if set to true' - }) - .option('createEmpty', { - type: 'boolean', - describe: 'Used with import, will create a file even if its empty' - }) - .option('includeFiles', { - alias: 'f', - type: 'boolean', - describe: 'Used with export, includes files in list of directories and files' - }) - .option('recursive', { - alias: 'r', - type: 'boolean', - describe: 'Used with list, import and export, handles all directories recursively' - }) - .option('raw', { - type: 'boolean', - describe: 'Used with export, keeps zip file instead of unpacking it' - }) - .option('dangerouslyIncludeLogsAndCache', { - type: 'boolean', - describe: 'Includes log and cache folders during export. Risky and usually not recommended' - }) - .option('iamstupid', { - type: 'boolean', - hidden: true, - describe: 'Deprecated alias for --dangerouslyIncludeLogsAndCache' - }) - .option('delete', { - alias: 'd', - type: 'boolean', - describe: 'Deletes the file or directory at [dirPath]. Detects type from path (use --asFile/--asDirectory to override)' - }) - .option('empty', { - type: 'boolean', - describe: 'Used with --delete, empties a directory instead of deleting it' - }) - .option('copy', { - type: 'string', - describe: 'Copies the file or directory at [dirPath] to the given destination path' - }) - .option('move', { - type: 'string', - describe: 'Moves the file or directory at [dirPath] to the given destination path' - }) - .option('asFile', { - type: 'boolean', - alias: 'af', - describe: 'Forces the command to treat the path as a single file, even if it has no extension.', - conflicts: 'asDirectory' - }) - .option('asDirectory', { - type: 'boolean', - alias: 'ad', - describe: 'Forces the command to treat the path as a directory, even if its name contains a dot.', - conflicts: 'asFile' - }) - .option('output', { - choices: ['json'], - describe: 'Outputs a single JSON response for automation-friendly parsing' - }) - .option('json', { - type: 'boolean', - hidden: true, - describe: 'Deprecated alias for --output json' - }) + .positional('dirPath', { + describe: 'The directory to list or export' + }) + .positional('outPath', { + describe: 'The directory to export the specified directory to', + default: '.' + }) + .option('list', { + alias: 'l', + type: 'boolean', + describe: 'Lists all directories and files' + }) + .option('export', { + alias: 'e', + type: 'boolean', + describe: 'Exports the specified directory and all subdirectories at [dirPath] to [outPath]' + }) + .option('import', { + alias: 'i', + type: 'boolean', + describe: 'Imports the file at [dirPath] to [outPath]' + }) + .option('overwrite', { + alias: 'o', + type: 'boolean', + describe: 'Used with import, will overwrite existing files at destination if set to true' + }) + .option('createEmpty', { + type: 'boolean', + describe: 'Used with import, will create a file even if its empty' + }) + .option('includeFiles', { + alias: 'f', + type: 'boolean', + describe: 'Used with export, includes files in list of directories and files' + }) + .option('recursive', { + alias: 'r', + type: 'boolean', + describe: 'Used with list, import and export, handles all directories recursively' + }) + .option('raw', { + type: 'boolean', + describe: 'Used with export, keeps zip file instead of unpacking it' + }) + .option('dangerouslyIncludeLogsAndCache', { + type: 'boolean', + describe: 'Includes log and cache folders during export. Risky and usually not recommended' + }) + .option('iamstupid', { + type: 'boolean', + hidden: true, + describe: 'Deprecated alias for --dangerouslyIncludeLogsAndCache' + }) + .option('delete', { + alias: 'd', + type: 'boolean', + describe: 'Deletes the file or directory at [dirPath]. Detects type from path (use --asFile/--asDirectory to override)' + }) + .option('empty', { + type: 'boolean', + describe: 'Used with --delete, empties a directory instead of deleting it' + }) + .option('copy', { + type: 'string', + describe: 'Copies the file or directory at [dirPath] to the given destination path' + }) + .option('move', { + type: 'string', + describe: 'Moves the file or directory at [dirPath] to the given destination path' + }) + .option('asFile', { + type: 'boolean', + alias: 'af', + describe: 'Forces the command to treat the path as a single file, even if it has no extension.', + conflicts: 'asDirectory' + }) + .option('asDirectory', { + type: 'boolean', + alias: 'ad', + describe: 'Forces the command to treat the path as a directory, even if its name contains a dot.', + conflicts: 'asFile' + }) + .option('output', { + choices: ['json'], + describe: 'Outputs a single JSON response for automation-friendly parsing' + }) + .option('json', { + type: 'boolean', + hidden: true, + describe: 'Deprecated alias for --output json' + }) }, handler: async (argv) => { if (argv.json && !argv.output) { @@ -149,13 +149,13 @@ async function handleFiles(argv, output) { if (argv.export) { if (argv.dirPath) { - + const isFile = isFilePath(argv, argv.dirPath); if (isFile) { - let parentDirectory = path.dirname(argv.dirPath); + let parentDirectory = path.dirname(argv.dirPath); parentDirectory = parentDirectory === '.' ? '/' : parentDirectory; - + await download(env, user, parentDirectory, argv.outPath, false, null, true, argv.dangerouslyIncludeLogsAndCache, [argv.dirPath], true, output); } else { 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) { } function getFilesInDirectory(dirPath) { - return fs.statSync(dirPath).isFile() ? [ dirPath ] : fs.readdirSync(dirPath) - .map(file => path.join(dirPath, file)) - .filter(file => fs.statSync(file).isFile()); + return fs.statSync(dirPath).isFile() ? [dirPath] : fs.readdirSync(dirPath) + .map(file => path.join(dirPath, file)) + .filter(file => fs.statSync(file).isFile()); } 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 await uploadFiles(env, user, filesInDir, isRoot ? outPath : path.join(outPath, path.basename(dirPath)), createEmpty, overwrite, output); const subDirectories = fs.readdirSync(dirPath) - .map(subDir => path.join(dirPath, subDir)) - .filter(subDir => fs.statSync(subDir).isDirectory()); + .map(subDir => path.join(dirPath, subDir)) + .filter(subDir => fs.statSync(subDir).isDirectory()); for (let subDir of subDirectories) { await processDirectory(env, user, subDir, isRoot ? outPath : path.join(outPath, path.basename(dirPath)), originalDir, createEmpty, false, overwrite, output); } @@ -278,7 +278,7 @@ function resolveTree(dirs, indentLevel, parentHasFiles, output) { resolveTree(dir.files?.data ?? [], indentLevel + '│\t', false, output); } else { resolveTree(dir.directories ?? [], indentLevel + '\t', hasFiles, output); - resolveTree(dir.files?.data ?? [], indentLevel + '\t', false, output); + resolveTree(dir.files?.data ?? [], indentLevel + '\t', false, output); } } } @@ -397,10 +397,8 @@ async function extractArchive(filename, filePath, outPath, raw, output) { } output.log(`Finished extracting ${filename} to ${outPath}\n`); - fs.unlink(filePath, function(err) { - if (err) { - output.verboseLog(`Warning: Failed to delete temporary archive ${filePath}: ${err.message}`); - } + await fs.promises.unlink(filePath).catch(err => { + output.verboseLog(`Warning: Failed to delete temporary archive ${filePath}: ${err.message}`); }); } @@ -577,13 +575,13 @@ async function uploadChunk(env, user, filePathsChunk, destinationPath, createEmp form.append('path', destinationPath); form.append('skipExistingFiles', String(!overwrite)); form.append('allowOverwrite', String(overwrite)); - + filePathsChunk.forEach(fileToUpload => { output.log(`${fileToUpload}`) form.append('files', fs.createReadStream(path.resolve(fileToUpload))); }); - const res = await fetch(`${env.protocol}://${env.host}/Admin/Api/Upload?` + new URLSearchParams({"createEmptyFiles": createEmpty, "createMissingDirectories": true}), { + const res = await fetch(`${env.protocol}://${env.host}/Admin/Api/Upload?` + new URLSearchParams({ "createEmptyFiles": createEmpty, "createMissingDirectories": true }), { method: 'POST', body: form, headers: { @@ -591,7 +589,7 @@ async function uploadChunk(env, user, filePathsChunk, destinationPath, createEmp }, agent: getAgent(env.protocol) }); - + if (res.ok) { return await res.json(); } @@ -604,8 +602,7 @@ export function resolveFilePath(filePath) { let p = path.parse(path.resolve(filePath)) let regex = wildcardToRegExp(p.base); let resolvedPath = fs.readdirSync(p.dir).filter((allFilesPaths) => allFilesPaths.match(regex) !== null)[0] - if (resolvedPath === undefined) - { + if (resolvedPath === undefined) { throw createCommandError(`Could not find any files with the name ${filePath}`, 1); } return path.join(p.dir, resolvedPath); diff --git a/bin/commands/install.js b/bin/commands/install.js index 6bc6a2f..3d1e8fc 100644 --- a/bin/commands/install.js +++ b/bin/commands/install.js @@ -6,22 +6,22 @@ import { uploadFiles, resolveFilePath } from './files.js'; export function installCommand() { return { - command: 'install [filePath]', + command: 'install ', describe: 'Installs the addin on the given path, allowed file extensions are .dll, .nupkg', builder: (yargs) => { return yargs - .positional('filePath', { - describe: 'Path to the file to install' - }) - .option('queue', { - alias: 'q', - type: 'boolean', - describe: 'Queues the install for next Dynamicweb recycle' - }) - .option('output', { - choices: ['json'], - describe: 'Outputs a single JSON response for automation-friendly parsing' - }) + .positional('filePath', { + describe: 'Path to the file to install' + }) + .option('queue', { + alias: 'q', + type: 'boolean', + describe: 'Queues the install for next Dynamicweb recycle' + }) + .option('output', { + choices: ['json'], + describe: 'Outputs a single JSON response for automation-friendly parsing' + }) }, handler: async (argv) => { const output = createInstallOutput(argv); @@ -43,7 +43,7 @@ async function handleInstall(argv, output) { let env = await setupEnv(argv, output); let user = await setupUser(argv, env); let resolvedPath = resolveFilePath(argv.filePath); - await uploadFiles(env, user, [ resolvedPath ], 'System/AddIns/Local', false, true, output); + await uploadFiles(env, user, [resolvedPath], 'System/AddIns/Local', false, true, output); await installAddin(env, user, resolvedPath, argv.queue, output); } @@ -56,15 +56,29 @@ async function installAddin(env, user, resolvedPath, queue, output) { `${filename.substring(0, filename.lastIndexOf('.')) || filename}|${path.extname(resolvedPath)}` ] } - let res = await fetch(`${env.protocol}://${env.host}/Admin/Api/AddinInstall`, { - method: 'POST', - body: JSON.stringify(data), - headers: { - 'Content-Type': 'application/json', - 'Authorization': `Bearer ${user.apiKey}` - }, - agent: getAgent(env.protocol) - }); + const controller = new AbortController(); + const timeoutId = setTimeout(() => controller.abort(), 30_000); + + let res; + try { + res = await fetch(`${env.protocol}://${env.host}/Admin/Api/AddinInstall`, { + method: 'POST', + body: JSON.stringify(data), + headers: { + 'Content-Type': 'application/json', + 'Authorization': `Bearer ${user.apiKey}` + }, + agent: getAgent(env.protocol), + signal: controller.signal + }); + } catch (err) { + if (err.name === 'AbortError') { + throw createCommandError('Addin installation request timed out.', 408); + } + throw err; + } finally { + clearTimeout(timeoutId); + } if (res.ok) { const body = await parseJsonSafe(res); diff --git a/bin/commands/query.js b/bin/commands/query.js index fd3fb11..64d7af5 100644 --- a/bin/commands/query.js +++ b/bin/commands/query.js @@ -124,7 +124,7 @@ export function buildQueryParamsFromArgv(argv) { } async function runQuery(env, user, query, params) { - let res = await fetch(`${env.protocol}://${env.host}/Admin/Api/${query}?` + new URLSearchParams(params), { + let res = await fetch(`${env.protocol}://${env.host}/Admin/Api/${encodeURIComponent(query)}?` + new URLSearchParams(params), { method: 'GET', headers: { 'Authorization': `Bearer ${user.apiKey}` diff --git a/bin/index.js b/bin/index.js index 48cac19..e6e0439 100644 --- a/bin/index.js +++ b/bin/index.js @@ -84,8 +84,12 @@ function baseCommand() { } else if (currentEnv?.current?.user) { console.log(`User: ${currentEnv.current.user}`); } - console.log(`Protocol: ${currentEnv.protocol}`); - console.log(`Host: ${currentEnv.host}`); + if (currentEnv.protocol) { + console.log(`Protocol: ${currentEnv.protocol}`); + } + if (currentEnv.host) { + console.log(`Host: ${currentEnv.host}`); + } } } } diff --git a/cliv2-documentation.md b/cliv2-documentation.md index 6bf21a6..7cea195 100644 --- a/cliv2-documentation.md +++ b/cliv2-documentation.md @@ -25,7 +25,7 @@ Version 2 is an automation-first overhaul. The headline changes: | v1 | v2 | Notes | |----|-----|-------| -| `--json` | `--output json` | `--json` still works but is deprecated | +| `--json` (global output flag) | `--output json` | The global `--json` output flag still works but is deprecated. Note: `dw command --json` is a separate, active flag for passing a JSON payload to a command and is **not** deprecated. | | `--iamstupid` | `--dangerouslyIncludeLogsAndCache` | `--iamstupid` still works but is deprecated | | `--host` (required `--apiKey`) | `--host` (works with `--apiKey` or OAuth) | OAuth credentials are now accepted alongside `--host` | | Errors printed to stdout | Errors in `errors` array (JSON) or thrown (non-JSON) | Scripts should check exit codes and/or `ok` field | @@ -165,7 +165,7 @@ Use `--auth user` to force user authentication even when an environment is confi ## Automation and JSON output -Commands that talk to the Management API -- `env`, `login`, `files`, `query`, `command`, and `install` -- support `--output json`. This returns a structured envelope instead of human-readable console output: +Commands that talk to the Management API -- `env`, `login`, `files`, `query`, `command`, and `install` -- support `--output json`. The `database`, `swift`, and `config` commands do not support `--output json`. This returns a structured envelope instead of human-readable console output: ```json { @@ -695,6 +695,9 @@ dw command --json ./payload.json [options] The `--json` flag accepts either an inline JSON string or a path to a `.json` file. +> [!NOTE] +> The `--json` flag here is specific to the `command` subcommand and passes a payload to the Management API. It is **not** the same as the deprecated global `--json` output flag (replaced by `--output json`). + **Examples:** ```sh @@ -770,6 +773,9 @@ Queued installation ensures all dependencies are in place before any add-in is a Export the current environment's database to a `.bacpac` file. +> [!NOTE] +> `database` does not support `--output json`. + ```sh dw database ./backups --export ``` @@ -787,6 +793,9 @@ GO Download a Swift release from GitHub. +> [!NOTE] +> `swift` does not support `--output json`. + ```sh dw swift [outPath] [options] ``` @@ -810,6 +819,9 @@ dw swift . --nightly --force # download the latest nightly build Write values directly into `~/.dwc` using dot-notation paths. +> [!NOTE] +> `config` does not support `--output json`. + ```sh dw config --env.dev.host localhost:6001 dw config --env.production.protocol https diff --git a/qa/README.md b/qa/README.md index 78e9d38..cb9b7b3 100644 --- a/qa/README.md +++ b/qa/README.md @@ -79,7 +79,13 @@ Defaults: If `qa/profile.json` exists, it is loaded automatically. You can also pass `--profile `. -See [profile.example.json](profile.example.json) for the supported shape. +To get started, copy the template and customize it: + +```sh +cp qa/profile.example.json qa/profile.json +``` + +See [profile.example.json](profile.example.json) for the full list of supported fields. ## Profile Example diff --git a/qa/run-smoke.mjs b/qa/run-smoke.mjs index 9cd4375..15115cb 100644 --- a/qa/run-smoke.mjs +++ b/qa/run-smoke.mjs @@ -668,12 +668,15 @@ async function runCli(logName, cliArgs, options = {}) { stderr += chunk.toString(); }); + let exited = false; + child.on('exit', () => { exited = true; }); + const timeoutHandle = setTimeout(() => { stderr += `\nProcess timed out after ${timeoutMs} ms.`; child.kill('SIGTERM'); setTimeout(() => { - if (!child.killed) { + if (!exited) { child.kill('SIGKILL'); } }, 5000).unref(); diff --git a/test/command.test.js b/test/command.test.js index dc9e162..39ff76b 100644 --- a/test/command.test.js +++ b/test/command.test.js @@ -50,3 +50,27 @@ test('parseJsonOrPath parses json from a file path', () => { fs.rmSync(tempDir, { recursive: true, force: true }); } }); + +test('parseJsonOrPath throws SyntaxError for a file containing invalid JSON', () => { + const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'cli-command-test-')); + const jsonPath = path.join(tempDir, 'bad.json'); + + try { + fs.writeFileSync(jsonPath, '{ not valid json }'); + assert.throws(() => parseJsonOrPath(jsonPath), SyntaxError); + } finally { + fs.rmSync(tempDir, { recursive: true, force: true }); + } +}); + +test('parseJsonOrPath throws SyntaxError for a non-existent file path', () => { + const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'cli-command-test-')); + + try { + const missingPath = path.join(tempDir, 'missing.json'); + // existsSync returns false, so the path string is passed to JSON.parse directly + assert.throws(() => parseJsonOrPath(missingPath), SyntaxError); + } finally { + fs.rmSync(tempDir, { recursive: true, force: true }); + } +}); diff --git a/test/downloader.test.js b/test/downloader.test.js index fee329c..eb5bf38 100644 --- a/test/downloader.test.js +++ b/test/downloader.test.js @@ -28,35 +28,19 @@ test('getFileNameFromResponse throws when no file metadata exists', () => { ); }); -test('tryGetFileNameFromResponse returns null and stays silent by default', () => { +test('tryGetFileNameFromResponse returns null and stays silent by default', (t) => { const response = createResponse(null); - const originalLog = console.log; - const calls = []; - console.log = (...args) => { - calls.push(args); - }; + const mockLog = t.mock.method(console, 'log', () => { }); - try { - assert.equal(tryGetFileNameFromResponse(response, '/Files'), null); - assert.deepEqual(calls, []); - } finally { - console.log = originalLog; - } + assert.equal(tryGetFileNameFromResponse(response, '/Files'), null); + assert.equal(mockLog.mock.calls.length, 0); }); -test('tryGetFileNameFromResponse logs the error message in verbose mode', () => { +test('tryGetFileNameFromResponse logs the error message in verbose mode', (t) => { const response = createResponse(null); - const originalLog = console.log; - const calls = []; - console.log = (...args) => { - calls.push(args); - }; + const mockLog = t.mock.method(console, 'log', () => { }); - try { - assert.equal(tryGetFileNameFromResponse(response, '/Files', true), null); - assert.equal(calls.length, 1); - assert.match(String(calls[0][0]), /No files found in directory '\/Files'/); - } finally { - console.log = originalLog; - } + assert.equal(tryGetFileNameFromResponse(response, '/Files', true), null); + assert.equal(mockLog.mock.calls.length, 1); + assert.match(String(mockLog.mock.calls[0].arguments[0]), /No files found in directory '\/Files'/); }); diff --git a/test/utils.test.js b/test/utils.test.js index 72e4d63..01783fe 100644 --- a/test/utils.test.js +++ b/test/utils.test.js @@ -13,8 +13,24 @@ test('formatBytes formats kilobytes and megabytes', () => { assert.equal(formatBytes(1024 * 1024), '1.00 MB'); }); +test('formatBytes formats sub-kilobyte, gigabyte, and terabyte values', () => { + assert.equal(formatBytes(500), '500.00 Bytes'); + assert.equal(formatBytes(1023), '1023.00 Bytes'); + assert.equal(formatBytes(1024 ** 3), '1.00 GB'); + assert.equal(formatBytes(1024 ** 4), '1.00 TB'); +}); + test('formatElapsed formats seconds, minutes, and hours', () => { assert.equal(formatElapsed(999), '0s'); + assert.equal(formatElapsed(1000), '1s'); + assert.equal(formatElapsed(5000), '5s'); assert.equal(formatElapsed(61_000), '1m 1s'); + assert.equal(formatElapsed(120_000), '2m 0s'); assert.equal(formatElapsed(3_661_000), '1h 1m 1s'); }); + +test('formatElapsed handles invalid inputs', () => { + assert.equal(formatElapsed(-1000), '-1s'); + assert.equal(formatElapsed(null), '0s'); + assert.equal(formatElapsed(undefined), 'NaNs'); +}); From 70173696a952d32730c2d391150163aed38876ed Mon Sep 17 00:00:00 2001 From: nicped Date: Wed, 15 Apr 2026 14:33:42 +0200 Subject: [PATCH 78/86] Bump version --- package-lock.json | 4 ++-- package.json | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/package-lock.json b/package-lock.json index b1bdc7d..60ca06d 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@dynamicweb/cli", - "version": "2.0.0-beta.0", + "version": "2.0.0-beta.2", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "@dynamicweb/cli", - "version": "2.0.0-beta.0", + "version": "2.0.0-beta.2", "license": "MIT", "dependencies": { "@inquirer/prompts": "^8.4.1", diff --git a/package.json b/package.json index 93b116e..d030702 100644 --- a/package.json +++ b/package.json @@ -2,7 +2,7 @@ "name": "@dynamicweb/cli", "type": "module", "description": "CLI for interacting with Dynamicweb 10 solutions from the command line.", - "version": "2.0.0-beta.1", + "version": "2.0.0-beta.2", "main": "bin/index.js", "files": [ "bin/**" @@ -49,4 +49,4 @@ "node-fetch": "^3.2.10", "yargs": "^17.5.1" } -} \ No newline at end of file +} From 48fb464d413c74e091b1141d0aff30e7d43fa318 Mon Sep 17 00:00:00 2001 From: nicped Date: Mon, 20 Apr 2026 14:37:59 +0200 Subject: [PATCH 79/86] Update README to clarify CLI functionality and improve tool comparison --- README.md | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 7787840..8d8b467 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,11 @@ # DynamicWeb CLI -DynamicWeb CLI is the command-line interface for working with DynamicWeb 10 solutions. It helps you manage environments, authenticate against the admin API, run queries and commands, move files in and out of a solution, install add-ins, export databases, and pull Swift solutions. +A CLI for managing DynamicWeb 10 solutions — environments, authentication, queries, commands, files, add-ins, databases, and Swift releases. + +| Tool | Best for | +|------|----------| +| **DW CLI** (this repo) | Automation, CI/CD pipelines, scripting, headless environments | +| **[DW Desktop](https://github.com/dynamicweb/dw-desktop)** | Day-to-day developer workflows with a GUI | This branch now targets `2.0.0-beta.0`. From 6da8cf96127243496dd7cde65cf5b795c099efd9 Mon Sep 17 00:00:00 2001 From: nicped Date: Wed, 22 Apr 2026 16:25:11 +0200 Subject: [PATCH 80/86] Fix duplicate parseJsonSafe in files.js, OAuth login with --host bootstrap, and QA docs - Remove duplicate parseJsonSafe declaration in files.js (caused SyntaxError on startup) - Allow dw login --oauth --host to work from a clean state by deriving env name from hostname - Guard finalizeOAuthLogin against undefined env entry in config - Add PowerShell env var instructions to qa/README.md --- bin/commands/files.js | 9 --------- bin/commands/login.js | 14 +++++++++++--- qa/README.md | 8 ++++++++ 3 files changed, 19 insertions(+), 12 deletions(-) diff --git a/bin/commands/files.js b/bin/commands/files.js index f722416..787ce9b 100644 --- a/bin/commands/files.js +++ b/bin/commands/files.js @@ -741,12 +741,3 @@ export function getFilesOperation(argv) { return 'unknown'; } - - -async function parseJsonSafe(res) { - try { - return await res.json(); - } catch { - return null; - } -} diff --git a/bin/commands/login.js b/bin/commands/login.js index 3254d41..5b4fdd0 100644 --- a/bin/commands/login.js +++ b/bin/commands/login.js @@ -355,9 +355,14 @@ async function interactiveOAuthLogin(argv, output) { async function nonInteractiveOAuthLogin(argv) { verboseLog(argv, 'Configuring OAuth client credentials authentication (non-interactive)'); - const environment = getConfig()?.current?.env; + let environment = getConfig()?.current?.env; + + if (!environment && argv.host) { + environment = new URL(`https://${argv.host.replace(/^https?:\/\//, '')}`).hostname.split('.')[0] || 'default'; + } + if (!environment) { - throw createCommandError('No environment set. Configure one with "dw env" first.'); + throw createCommandError('No environment set. Configure one with "dw env" first, or pass --host.'); } if (argv.host) { @@ -373,7 +378,10 @@ async function nonInteractiveOAuthLogin(argv) { } async function finalizeOAuthLogin(environment, clientIdEnv, clientSecretEnv, argv) { - const env = getConfig().env[environment]; + const env = getConfig().env?.[environment]; + if (!env) { + throw createCommandError(`Environment "${environment}" is not configured. Run "dw env" first or pass --host.`); + } const oauthConfig = resolveOAuthConfig({ ...argv, clientIdEnv, diff --git a/qa/README.md b/qa/README.md index cb9b7b3..1fa9b71 100644 --- a/qa/README.md +++ b/qa/README.md @@ -31,12 +31,20 @@ The harness currently excludes `database` and `swift`. Set the required credentials: +**bash / WSL:** ```sh export DW_BASE_URL=https://your-solution.example.com export DW_CLIENT_ID=your-client-id export DW_CLIENT_SECRET=your-client-secret ``` +**PowerShell:** +```powershell +$env:DW_BASE_URL = "https://your-solution.example.com" +$env:DW_CLIENT_ID = "your-client-id" +$env:DW_CLIENT_SECRET = "your-client-secret" +``` + Run with defaults: ```sh From da460b40dccb7644ad8db794efb5eae95c7680b0 Mon Sep 17 00:00:00 2001 From: nicped Date: Tue, 28 Apr 2026 11:51:07 +0200 Subject: [PATCH 81/86] Bump version to 2.0.0-preview.1 and switch status codes from HTTP to 0/1 --- bin/commands/command.js | 2 +- bin/commands/env.js | 2 +- bin/commands/files.js | 2 +- bin/commands/install.js | 2 +- bin/commands/install.test.js | 2 +- bin/commands/login.js | 2 +- bin/commands/query.js | 2 +- cliv2-documentation.md | 22 +++++++++++----------- package.json | 2 +- 9 files changed, 19 insertions(+), 19 deletions(-) diff --git a/bin/commands/command.js b/bin/commands/command.js index b78f7ae..d93c3f9 100644 --- a/bin/commands/command.js +++ b/bin/commands/command.js @@ -100,7 +100,7 @@ function createCommandOutput(argv) { ok: true, command: 'command', operation: argv.list ? 'list' : 'run', - status: 200, + status: 0, data: [], errors: [], meta: { diff --git a/bin/commands/env.js b/bin/commands/env.js index dd72e07..b9b9feb 100644 --- a/bin/commands/env.js +++ b/bin/commands/env.js @@ -284,7 +284,7 @@ function createEnvOutput(argv) { ok: true, command: 'env', operation: argv.users ? 'users' : argv.list ? 'list' : argv.env ? 'select' : 'setup', - status: 200, + status: 0, data: [], errors: [], meta: {} diff --git a/bin/commands/files.js b/bin/commands/files.js index 787ce9b..608bbc5 100644 --- a/bin/commands/files.js +++ b/bin/commands/files.js @@ -665,7 +665,7 @@ function createFilesOutput(argv) { ok: true, command: 'files', operation: getFilesOperation(argv), - status: 200, + status: 0, data: [], errors: [], meta: {} diff --git a/bin/commands/install.js b/bin/commands/install.js index 3d1e8fc..1816339 100644 --- a/bin/commands/install.js +++ b/bin/commands/install.js @@ -102,7 +102,7 @@ function createInstallOutput(argv) { ok: true, command: 'install', operation: argv.queue ? 'queue' : 'install', - status: 200, + status: 0, data: [], errors: [], meta: { diff --git a/bin/commands/install.test.js b/bin/commands/install.test.js index 0f2e265..74f74cb 100644 --- a/bin/commands/install.test.js +++ b/bin/commands/install.test.js @@ -33,7 +33,7 @@ test('createInstallOutput suppresses regular logs in json mode and emits the fin ok: true, command: 'install', operation: 'install', - status: 200, + status: 0, data: [{ type: 'install', filename: 'addon.nupkg' }], errors: [], meta: { diff --git a/bin/commands/login.js b/bin/commands/login.js index 5b4fdd0..bcd53a0 100644 --- a/bin/commands/login.js +++ b/bin/commands/login.js @@ -540,7 +540,7 @@ function createLoginOutput(argv) { ok: true, command: 'login', operation: shouldUseOAuth(argv, getCurrentEnv(argv)) ? 'oauth-login' : argv.user ? 'select-user' : 'login', - status: 200, + status: 0, data: [], errors: [], meta: {} diff --git a/bin/commands/query.js b/bin/commands/query.js index 64d7af5..5aa7e3c 100644 --- a/bin/commands/query.js +++ b/bin/commands/query.js @@ -142,7 +142,7 @@ function createQueryOutput(argv) { ok: true, command: 'query', operation: argv.list ? 'list' : 'run', - status: 200, + status: 0, data: [], errors: [], meta: { diff --git a/cliv2-documentation.md b/cliv2-documentation.md index 7cea195..194bf42 100644 --- a/cliv2-documentation.md +++ b/cliv2-documentation.md @@ -172,7 +172,7 @@ Commands that talk to the Management API -- `env`, `login`, `files`, `query`, `c "ok": true, "command": "query", "operation": "run", - "status": 200, + "status": 0, "data": [ { "name": "DefaultMail.html", @@ -191,7 +191,7 @@ Commands that talk to the Management API -- `env`, `login`, `files`, `query`, `c | `ok` | `boolean` | `true` if the operation succeeded | | `command` | `string` | Which CLI command ran | | `operation` | `string` | The specific operation within that command | -| `status` | `number` | HTTP-style status code | +| `status` | `number` | `0` on success, `1` on failure | | `data` | `array` | The result payload | | `errors` | `array` | Error objects with `message` and `details` if the operation failed | | `meta` | `object` | Command-specific metadata (query name, file counts, etc.) | @@ -222,7 +222,7 @@ When a command fails, `ok` is `false` and the `errors` array contains structured "ok": false, "command": "query", "operation": "run", - "status": 404, + "status": 1, "data": [], "errors": [ { @@ -351,7 +351,7 @@ dw env --list --output json "ok": true, "command": "env", "operation": "list", - "status": 200, + "status": 0, "data": [ { "environments": ["dev", "staging", "production"] @@ -385,7 +385,7 @@ dw login --oauth --output json "ok": true, "command": "login", "operation": "oauth-login", - "status": 200, + "status": 0, "data": [ { "environment": "dev", @@ -542,7 +542,7 @@ dw files ./Files templates -i -r --output json "ok": true, "command": "files", "operation": "import", - "status": 200, + "status": 0, "data": [ { "type": "upload", @@ -572,7 +572,7 @@ dw files /Templates/Designs/OldDesign --delete --output json "ok": true, "command": "files", "operation": "delete", - "status": 200, + "status": 0, "data": [ { "type": "delete", @@ -594,7 +594,7 @@ dw files /Templates/Designs/MyDesign --copy /Templates/Designs/MyDesign-backup - "ok": true, "command": "files", "operation": "copy", - "status": 200, + "status": 0, "data": [ { "type": "copy", @@ -616,7 +616,7 @@ dw files /Templates/config.json --move /Templates/Backups --output json "ok": true, "command": "files", "operation": "move", - "status": 200, + "status": 0, "data": [ { "type": "move", @@ -670,7 +670,7 @@ dw query FileByName --name DefaultMail.html --output json "ok": true, "command": "query", "operation": "run", - "status": 200, + "status": 0, "data": [ { "name": "DefaultMail.html", @@ -718,7 +718,7 @@ dw command PageDelete --json '{ "id": "1383" }' --output json "ok": true, "command": "command", "operation": "run", - "status": 200, + "status": 0, "data": [ { "success": true, diff --git a/package.json b/package.json index d030702..54bdee4 100644 --- a/package.json +++ b/package.json @@ -2,7 +2,7 @@ "name": "@dynamicweb/cli", "type": "module", "description": "CLI for interacting with Dynamicweb 10 solutions from the command line.", - "version": "2.0.0-beta.2", + "version": "2.0.0-preview.1", "main": "bin/index.js", "files": [ "bin/**" From 5a88a218c30faa27a2837c276f07669cc3b872c6 Mon Sep 17 00:00:00 2001 From: nicped Date: Tue, 28 Apr 2026 11:57:20 +0200 Subject: [PATCH 82/86] Update README to reflect status code changes from HTTP 200 to 0 for command outputs --- README.md | 21 ++++++++++++++------- 1 file changed, 14 insertions(+), 7 deletions(-) diff --git a/README.md b/README.md index 8d8b467..014245d 100644 --- a/README.md +++ b/README.md @@ -15,6 +15,13 @@ This branch now targets `2.0.0-beta.0`. ## Install +Install the preview from npm: + +```sh +npm install -g @dynamicweb/cli@preview +dw --help +``` + Install from npm: ```sh @@ -180,7 +187,7 @@ Representative output: "ok": true, "command": "env", "operation": "list", - "status": 200, + "status": 0, "data": [ { "environments": ["dev", "staging"] @@ -211,7 +218,7 @@ Example JSON output: "ok": true, "command": "env", "operation": "select", - "status": 200, + "status": 0, "data": [ { "environment": "dev", @@ -241,7 +248,7 @@ Example JSON output: "ok": true, "command": "login", "operation": "oauth-login", - "status": 200, + "status": 0, "data": [ { "environment": "dev", @@ -289,7 +296,7 @@ Example JSON output: "ok": true, "command": "files", "operation": "import", - "status": 200, + "status": 0, "data": [ { "type": "upload", @@ -328,7 +335,7 @@ Example JSON output: "ok": true, "command": "query", "operation": "run", - "status": 200, + "status": 0, "data": [ { "name": "DefaultMail.html", @@ -359,7 +366,7 @@ Example JSON output: "ok": true, "command": "command", "operation": "run", - "status": 200, + "status": 0, "data": [ { "success": true, @@ -391,7 +398,7 @@ Example JSON output: "ok": true, "command": "install", "operation": "queue", - "status": 200, + "status": 0, "data": [ { "type": "upload", From 3361360ea26a2893b0bbe5b1ff44adcafe53716b Mon Sep 17 00:00:00 2001 From: nicped Date: Tue, 28 Apr 2026 12:00:02 +0200 Subject: [PATCH 83/86] Update README to reflect branch target change from 2.0.0-beta.0 to 2.0.0-preview.1 --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 014245d..bf80732 100644 --- a/README.md +++ b/README.md @@ -7,7 +7,7 @@ A CLI for managing DynamicWeb 10 solutions — environments, authentication, que | **DW CLI** (this repo) | Automation, CI/CD pipelines, scripting, headless environments | | **[DW Desktop](https://github.com/dynamicweb/dw-desktop)** | Day-to-day developer workflows with a GUI | -This branch now targets `2.0.0-beta.0`. +This branch now targets `2.0.0-preview.1`. ## Requirements @@ -38,7 +38,7 @@ npm install -g . ## What Changed -The `2.0` beta is a substantial overhaul focused on automation and modern authentication. +The `2.0` preview is a substantial overhaul focused on automation and modern authentication. - Automation-first command output: `env`, `login`, `files`, `query`, `command`, and `install` now support `--output json` so scripts and pipelines can consume structured results instead of plain console logs. - OAuth client credentials support: the CLI can now authenticate with OAuth 2.0 `client_credentials`, which makes headless and CI/CD usage much easier. From cd65abbe6c79f047d7b9d642d87e8cb96bb66825 Mon Sep 17 00:00:00 2001 From: nicped Date: Tue, 28 Apr 2026 14:04:52 +0200 Subject: [PATCH 84/86] Add dw folders command and bump version to 2.0.0-preview.2 Introduces `dw folders ` for unambiguous directory management: --create and --rename via DirectorySave, --move via DirectoryMove, --delete/--copy/--export routed through handleFiles (now exported). Updates README and cliv2-documentation with full command reference. --- README.md | 39 +- bin/commands/files.js | 2 +- bin/commands/folders.js | 292 +++++++++ bin/index.js | 2 + cliv2-documentation.md | 123 +++- package-lock.json | 1310 +++++++++------------------------------ package.json | 2 +- 7 files changed, 749 insertions(+), 1021 deletions(-) create mode 100644 bin/commands/folders.js diff --git a/README.md b/README.md index bf80732..2ad01e4 100644 --- a/README.md +++ b/README.md @@ -40,7 +40,7 @@ npm install -g . The `2.0` preview is a substantial overhaul focused on automation and modern authentication. -- Automation-first command output: `env`, `login`, `files`, `query`, `command`, and `install` now support `--output json` so scripts and pipelines can consume structured results instead of plain console logs. +- Automation-first command output: `env`, `login`, `files`, `folders`, `query`, `command`, and `install` now support `--output json` so scripts and pipelines can consume structured results instead of plain console logs. - OAuth client credentials support: the CLI can now authenticate with OAuth 2.0 `client_credentials`, which makes headless and CI/CD usage much easier. - Better environment handling: protocol, host, and auth details are stored more cleanly in `~/.dwc`, while one-off runs can still override host and credentials directly. - Improved file workflows: file import, export, recursive sync, raw archive export, progress reporting, and source-type override flags make file operations more predictable. @@ -265,7 +265,7 @@ Example JSON output: ### `dw files [dirPath] [outPath]` -List, export, and import files from the DynamicWeb file archive. +List, export, import, delete, copy, and move files and directories in the DynamicWeb file archive. Useful flags: @@ -273,6 +273,10 @@ Useful flags: - `-f`, `--includeFiles`: include files in listings - `-e`, `--export`: export from the environment to disk - `-i`, `--import`: import from disk to the environment +- `-d`, `--delete`: delete a file or directory +- `--empty`: used with `--delete`, empties a directory instead of removing it +- `--copy `: copy a file or directory to a destination path +- `--move `: move a file or directory to a destination path - `-r`, `--recursive`: recurse through subdirectories - `--raw`: keep downloaded archives zipped - `--dangerouslyIncludeLogsAndCache`: include log and cache folders during export, which is risky and usually not recommended @@ -287,6 +291,37 @@ dw files system -lr dw files templates/Translations.xml ./templates -e dw files templates/templates.v1 ./templates -e -ad dw files ./Files templates -i -r --output json +dw files /Templates/OldDesign --delete +dw files /Templates/MyDesign --copy /Templates/MyDesign-backup +dw files /Templates/OldName --move /Templates/Archive +``` + +### `dw folders ` + +Create, rename, move, delete, copy, and export directories. Where `dw files` detects file vs. directory from the path extension, `dw folders` always treats the path as a directory — no ambiguity. + +Useful flags: + +- `-c`, `--create`: create the directory at `` +- `--rename `: rename the directory to a new name (keeps it in the same parent) +- `-m`, `--move `: move the directory to the given destination path +- `-d`, `--delete`: delete the directory +- `--empty`: used with `--delete`, empties the directory instead of removing it +- `--copy `: copy the directory to the given destination path +- `-e`, `--export`: export the directory to disk +- `-o`, `--outPath`: local destination for `--export` (defaults to `.`) +- `--raw`: keep exported archives zipped instead of extracting + +Examples: + +```sh +dw folders /Files/NewFolder --create +dw folders /Files/OldName --rename NewName +dw folders /Files/MyFolder --move /Files/Archive +dw folders /Files/MyFolder --delete +dw folders /Files/MyFolder --delete --empty +dw folders /Files/MyFolder --copy /Files/MyFolder-backup +dw folders /Files/MyFolder --export --outPath ./local ``` Example JSON output: diff --git a/bin/commands/files.js b/bin/commands/files.js index 608bbc5..9ce8a20 100644 --- a/bin/commands/files.js +++ b/bin/commands/files.js @@ -130,7 +130,7 @@ export function filesCommand() { } } -async function handleFiles(argv, output) { +export async function handleFiles(argv, output) { let env = await setupEnv(argv, output); let user = await setupUser(argv, env); diff --git a/bin/commands/folders.js b/bin/commands/folders.js new file mode 100644 index 0000000..359ecdc --- /dev/null +++ b/bin/commands/folders.js @@ -0,0 +1,292 @@ +import fetch from 'node-fetch'; +import path from 'path'; +import { setupEnv, getAgent, createCommandError } from './env.js'; +import { setupUser } from './login.js'; +import { interactiveConfirm } from '../utils.js'; +import { handleFiles } from './files.js'; + +export function foldersCommand() { + return { + command: 'folders ', + describe: 'Manages remote directories', + builder: (yargs) => { + return yargs + .positional('folderPath', { + describe: 'The remote directory path to operate on' + }) + .option('create', { + alias: 'c', + type: 'boolean', + describe: 'Creates the directory at [folderPath]' + }) + .option('rename', { + alias: 'rn', + type: 'string', + describe: 'Renames the directory at [folderPath] to the given name' + }) + .option('move', { + alias: 'm', + type: 'string', + describe: 'Moves the directory at [folderPath] to the given destination path' + }) + .option('delete', { + alias: 'd', + type: 'boolean', + describe: 'Deletes the directory at [folderPath]' + }) + .option('empty', { + type: 'boolean', + describe: 'Used with --delete, empties the directory instead of deleting it' + }) + .option('copy', { + type: 'string', + describe: 'Copies the directory at [folderPath] to the given destination path' + }) + .option('export', { + alias: 'e', + type: 'boolean', + describe: 'Exports the directory at [folderPath] to [outPath]' + }) + .option('outPath', { + alias: 'o', + type: 'string', + describe: 'Used with --export, local destination path (defaults to .)', + default: '.' + }) + .option('raw', { + type: 'boolean', + describe: 'Used with --export, keeps zip file instead of unpacking it' + }) + .option('output', { + choices: ['json'], + describe: 'Outputs a single JSON response for automation-friendly parsing' + }) + }, + handler: async (argv) => { + const output = createFoldersOutput(argv); + + try { + await handleFolders(argv, output); + } catch (err) { + output.fail(err); + process.exitCode = 1; + } finally { + output.finish(); + } + } + }; +} + +async function handleFolders(argv, output) { + let env = await setupEnv(argv, output); + let user = await setupUser(argv, env); + + if (argv.create) { + await createFolder(env, user, argv.folderPath, output); + } else if (argv.rename) { + await renameFolder(env, user, argv.folderPath, argv.rename, output); + } else if (argv.move) { + await moveFolder(env, user, argv.folderPath, argv.move, output); + } else if (argv.delete) { + const action = argv.empty + ? `empty directory "${argv.folderPath}"` + : `delete directory "${argv.folderPath}"`; + + if (output.json) { + await deleteFolderViaFiles(env, user, argv, output); + } else { + await interactiveConfirm(`Are you sure you want to ${action}?`, async () => { + await deleteFolderViaFiles(env, user, argv, output); + }); + } + } else if (argv.copy) { + await copyFolderViaFiles(env, user, argv, output); + } else if (argv.export) { + await exportFolderViaFiles(env, user, argv, output); + } +} + +async function createFolder(env, user, folderPath, output) { + const parentPath = path.posix.dirname(folderPath); + const name = path.posix.basename(folderPath); + + output.log(`Creating directory: ${folderPath}`); + + const res = await fetch(`${env.protocol}://${env.host}/Admin/Api/DirectorySave`, { + method: 'POST', + body: JSON.stringify({ + Name: name, + ParentPath: parentPath === '.' ? '/' : parentPath + }), + headers: { + 'Authorization': `Bearer ${user.apiKey}`, + 'Content-Type': 'application/json' + }, + agent: getAgent(env.protocol) + }); + + if (!res.ok) { + throw createCommandError(`Failed to create directory "${folderPath}".`, res.status, await parseJsonSafe(res)); + } + + const body = await parseJsonSafe(res); + + output.setStatus(0); + output.addData({ type: 'create', path: folderPath, response: body }); + output.log(`Successfully created: ${folderPath}`); +} + +async function renameFolder(env, user, folderPath, newName, output) { + const parentPath = path.posix.dirname(folderPath); + const currentName = path.posix.basename(folderPath); + + output.log(`Renaming directory "${currentName}" to "${newName}"`); + + const res = await fetch(`${env.protocol}://${env.host}/Admin/Api/DirectorySave`, { + method: 'POST', + body: JSON.stringify({ + Name: newName, + ParentPath: parentPath === '.' ? '/' : parentPath, + CurrentName: currentName + }), + headers: { + 'Authorization': `Bearer ${user.apiKey}`, + 'Content-Type': 'application/json' + }, + agent: getAgent(env.protocol) + }); + + if (!res.ok) { + throw createCommandError(`Failed to rename "${folderPath}" to "${newName}".`, res.status, await parseJsonSafe(res)); + } + + const body = await parseJsonSafe(res); + + output.setStatus(0); + output.addData({ type: 'rename', path: folderPath, newName, response: body }); + output.log(`Successfully renamed "${currentName}" to "${newName}"`); +} + +async function moveFolder(env, user, sourcePath, destinationPath, output) { + output.log(`Moving directory "${sourcePath}" to "${destinationPath}"`); + + const res = await fetch(`${env.protocol}://${env.host}/Admin/Api/DirectoryMove`, { + method: 'POST', + body: JSON.stringify({ + SourcePath: sourcePath, + DestinationPath: destinationPath + }), + headers: { + 'Authorization': `Bearer ${user.apiKey}`, + 'Content-Type': 'application/json' + }, + agent: getAgent(env.protocol) + }); + + if (!res.ok) { + throw createCommandError(`Failed to move "${sourcePath}" to "${destinationPath}".`, res.status, await parseJsonSafe(res)); + } + + const body = await parseJsonSafe(res); + + output.setStatus(0); + output.addData({ type: 'move', sourcePath, destinationPath, response: body }); + output.log(`Successfully moved "${sourcePath}" to "${destinationPath}"`); +} + +async function deleteFolderViaFiles(env, user, argv, output) { + await handleFiles({ + ...argv, + dirPath: argv.folderPath, + asDirectory: true, + asFile: false + }, output); +} + +async function copyFolderViaFiles(env, user, argv, output) { + await handleFiles({ + ...argv, + dirPath: argv.folderPath, + asDirectory: true, + asFile: false + }, output); +} + +async function exportFolderViaFiles(env, user, argv, output) { + await handleFiles({ + ...argv, + dirPath: argv.folderPath, + outPath: argv.outPath, + asDirectory: true, + asFile: false + }, output); +} + +async function parseJsonSafe(res) { + try { + return await res.json(); + } catch { + return null; + } +} + +function createFoldersOutput(argv) { + const response = { + ok: true, + command: 'folders', + operation: getFoldersOperation(argv), + status: 0, + data: [], + errors: [], + meta: {} + }; + + return { + json: argv.output === 'json', + verbose: Boolean(argv.verbose), + response, + log(...args) { + if (!this.json) { + console.log(...args); + } + }, + verboseLog(...args) { + if (this.verbose && !this.json) { + console.info(...args); + } + }, + addData(entry) { + response.data.push(entry); + }, + mergeMeta(metaOrFn) { + const meta = typeof metaOrFn === 'function' ? metaOrFn(response.meta) : metaOrFn; + response.meta = { ...response.meta, ...meta }; + }, + setStatus(status) { + response.status = status; + }, + fail(err) { + response.ok = false; + response.status = err?.status || 1; + response.errors.push({ + message: err?.message || 'Unknown folders command error.', + details: err?.details ?? null + }); + }, + finish() { + if (this.json) { + console.log(JSON.stringify(response, null, 2)); + } + } + }; +} + +function getFoldersOperation(argv) { + if (argv.create) return 'create'; + if (argv.rename) return 'rename'; + if (argv.move) return 'move'; + if (argv.delete) return 'delete'; + if (argv.copy) return 'copy'; + if (argv.export) return 'export'; + return 'unknown'; +} diff --git a/bin/index.js b/bin/index.js index e6e0439..91711a5 100644 --- a/bin/index.js +++ b/bin/index.js @@ -7,6 +7,7 @@ import { envCommand } from './commands/env.js'; import { configCommand, setupConfig, getConfig } from './commands/config.js'; import { installCommand } from './commands/install.js'; import { filesCommand } from './commands/files.js'; +import { foldersCommand } from './commands/folders.js'; import { swiftCommand } from './commands/swift.js'; import { databaseCommand } from './commands/database.js'; import { queryCommand } from './commands/query.js'; @@ -23,6 +24,7 @@ yargs(hideBin(process.argv)) .command(installCommand()) .command(configCommand()) .command(filesCommand()) + .command(foldersCommand()) .command(swiftCommand()) .command(databaseCommand()) .command(queryCommand()) diff --git a/cliv2-documentation.md b/cliv2-documentation.md index 194bf42..0c79a40 100644 --- a/cliv2-documentation.md +++ b/cliv2-documentation.md @@ -17,8 +17,9 @@ If you need to do something once, interactively, the DynamicWeb backend UI is us Version 2 is an automation-first overhaul. The headline changes: - **OAuth client credentials** -- the CLI can now authenticate with an OAuth 2.0 client ID and secret, removing the need for an interactive login in CI/CD pipelines and service-account scenarios. See [Authentication](#authentication) for setup details. -- **Structured JSON output** -- every API-driven command (`env`, `login`, `files`, `query`, `command`, `install`) supports `--output json`, returning a consistent envelope with `ok`, `status`, `data`, `errors`, and `meta` fields. Interactive prompts are suppressed in JSON mode so output is safe to pipe. See [Automation and JSON output](#automation-and-json-output). +- **Structured JSON output** -- every API-driven command (`env`, `login`, `files`, `folders`, `query`, `command`, `install`) supports `--output json`, returning a consistent envelope with `ok`, `status`, `data`, `errors`, and `meta` fields. Interactive prompts are suppressed in JSON mode so output is safe to pipe. See [Automation and JSON output](#automation-and-json-output). - **File delete, copy, and move** -- the `files` command can now delete, copy, and move files and directories on the environment in addition to listing, exporting, and importing. See the [files command reference](#files). +- **Folder management** -- the new `folders` command creates, renames, moves, deletes, copies, and exports directories. It always treats the path as a directory, removing the file-extension ambiguity that `dw files` has. See the [folders command reference](#folders). - **Consistent error model** -- commands that previously printed a message and exited silently now return structured errors (in JSON mode) or throw with a non-zero exit code. Scripts can rely on exit code `1` and the `errors` array for programmatic error handling. ### Migrating from v1 @@ -165,7 +166,7 @@ Use `--auth user` to force user authentication even when an environment is confi ## Automation and JSON output -Commands that talk to the Management API -- `env`, `login`, `files`, `query`, `command`, and `install` -- support `--output json`. The `database`, `swift`, and `config` commands do not support `--output json`. This returns a structured envelope instead of human-readable console output: +Commands that talk to the Management API -- `env`, `login`, `files`, `folders`, `query`, `command`, and `install` -- support `--output json`. The `database`, `swift`, and `config` commands do not support `--output json`. This returns a structured envelope instead of human-readable console output: ```json { @@ -630,6 +631,124 @@ dw files /Templates/config.json --move /Templates/Backups --output json } ``` +### folders + +Create, rename, move, delete, copy, and export directories in the DynamicWeb file archive. + +```sh +dw folders [options] +``` + +Unlike `dw files`, which infers whether a path refers to a file or a directory based on whether it has a file extension, `dw folders` always treats the path as a directory. This avoids ambiguity for paths like `templates.v1` or `my.theme`. + +**Key options:** + +| Option | Description | +|--------|-------------| +| `-c`, `--create` | Create the directory at `` | +| `--rename ` | Rename the directory to a new name, keeping it in the same parent | +| `-m`, `--move ` | Move the directory to the given destination path | +| `-d`, `--delete` | Delete the directory | +| `--empty` | Used with `--delete`, empties the directory instead of removing it | +| `--copy ` | Copy the directory to the given destination path | +| `-e`, `--export` | Export the directory to disk | +| `-o`, `--outPath` | Local destination for `--export` (defaults to `.`) | +| `--raw` | Keep exported archives zipped instead of extracting | + +**Examples:** + +```sh +# Create a new directory +dw folders /Files/NewFolder --create + +# Rename a directory +dw folders /Files/OldName --rename NewName + +# Move a directory into another location +dw folders /Files/MyFolder --move /Files/Archive + +# Delete a directory +dw folders /Files/MyFolder --delete + +# Empty a directory (remove contents, keep the directory) +dw folders /Files/MyFolder --delete --empty + +# Copy a directory +dw folders /Files/MyFolder --copy /Files/MyFolder-backup + +# Export a directory to disk +dw folders /Files/MyFolder --export --outPath ./local +``` + +**Relationship to `dw files`:** `--delete`, `--copy`, and `--export` are routed through the same implementation as `dw files` and produce identical JSON output shapes. `--create`, `--rename`, and `--move` are directory-only operations with no `dw files` equivalent. + +**JSON output:** + +```sh +dw folders /Files/NewFolder --create --output json +``` + +```json +{ + "ok": true, + "command": "folders", + "operation": "create", + "status": 0, + "data": [ + { + "type": "create", + "path": "/Files/NewFolder" + } + ], + "errors": [], + "meta": {} +} +``` + +```sh +dw folders /Files/OldName --rename NewName --output json +``` + +```json +{ + "ok": true, + "command": "folders", + "operation": "rename", + "status": 0, + "data": [ + { + "type": "rename", + "path": "/Files/OldName", + "newName": "NewName" + } + ], + "errors": [], + "meta": {} +} +``` + +```sh +dw folders /Files/MyFolder --move /Files/Archive --output json +``` + +```json +{ + "ok": true, + "command": "folders", + "operation": "move", + "status": 0, + "data": [ + { + "type": "move", + "sourcePath": "/Files/MyFolder", + "destinationPath": "/Files/Archive" + } + ], + "errors": [], + "meta": {} +} +``` + ### query Run Management API queries, inspect available parameters, or prompt for them interactively. diff --git a/package-lock.json b/package-lock.json index 60ca06d..df3ef3a 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@dynamicweb/cli", - "version": "2.0.0-beta.2", - "lockfileVersion": 2, + "version": "2.0.0-preview.2", + "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@dynamicweb/cli", - "version": "2.0.0-beta.2", + "version": "2.0.0-preview.2", "license": "MIT", "dependencies": { "@inquirer/prompts": "^8.4.1", @@ -34,13 +34,13 @@ } }, "node_modules/@inquirer/checkbox": { - "version": "5.1.3", - "resolved": "https://registry.npmjs.org/@inquirer/checkbox/-/checkbox-5.1.3.tgz", - "integrity": "sha512-+G7I8CT+EHv/hasNfUl3P37DVoMoZfpA+2FXmM54dA8MxYle1YqucxbacxHalw1iAFSdKNEDTGNV7F+j1Ldqcg==", + "version": "5.1.4", + "resolved": "https://registry.npmjs.org/@inquirer/checkbox/-/checkbox-5.1.4.tgz", + "integrity": "sha512-w6KF8ZYRvqHhROkOTHXYC3qIV/KYEu5o12oLqQySvch61vrYtRxNSHTONSdJqWiFJPlCUQAHT5OgOIyuTr+MHQ==", "license": "MIT", "dependencies": { "@inquirer/ansi": "^2.0.5", - "@inquirer/core": "^11.1.8", + "@inquirer/core": "^11.1.9", "@inquirer/figures": "^2.0.5", "@inquirer/type": "^4.0.5" }, @@ -57,12 +57,12 @@ } }, "node_modules/@inquirer/confirm": { - "version": "6.0.11", - "resolved": "https://registry.npmjs.org/@inquirer/confirm/-/confirm-6.0.11.tgz", - "integrity": "sha512-pTpHjg0iEIRMYV/7oCZUMf27/383E6Wyhfc/MY+AVQGEoUobffIYWOK9YLP2XFRGz/9i6WlTQh1CkFVIo2Y7XA==", + "version": "6.0.12", + "resolved": "https://registry.npmjs.org/@inquirer/confirm/-/confirm-6.0.12.tgz", + "integrity": "sha512-h9FgGun3QwVYNj5TWIZZ+slii73bMoBFjPfVIGtnFuL4t8gBiNDV9PcSfIzkuxvgquJKt9nr1QzszpBzTbH8Og==", "license": "MIT", "dependencies": { - "@inquirer/core": "^11.1.8", + "@inquirer/core": "^11.1.9", "@inquirer/type": "^4.0.5" }, "engines": { @@ -78,9 +78,9 @@ } }, "node_modules/@inquirer/core": { - "version": "11.1.8", - "resolved": "https://registry.npmjs.org/@inquirer/core/-/core-11.1.8.tgz", - "integrity": "sha512-/u+yJk2pOKNDOh1ZgdUH2RQaRx6OOH4I0uwL95qPvTFTIL38YBsuSC4r1yXBB3Q6JvNqFFc202gk0Ew79rrcjA==", + "version": "11.1.9", + "resolved": "https://registry.npmjs.org/@inquirer/core/-/core-11.1.9.tgz", + "integrity": "sha512-BDE4fG22uYh1bGSifcj7JSx119TVYNViMhMu85usp4Fswrzh6M0DV3yld64jA98uOAa2GSQ4Bg4bZRm2d2cwSg==", "license": "MIT", "dependencies": { "@inquirer/ansi": "^2.0.5", @@ -103,31 +103,13 @@ } } }, - "node_modules/@inquirer/core/node_modules/cli-width": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/cli-width/-/cli-width-4.1.0.tgz", - "integrity": "sha512-ouuZd4/dm2Sw5Gmqy6bGyNNNe1qt9RpmxveLSO7KcgsTnU7RXfsw+/bukWGo1abgBiMAic068rclZsO4IWmmxQ==", - "license": "ISC", - "engines": { - "node": ">= 12" - } - }, - "node_modules/@inquirer/core/node_modules/mute-stream": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/mute-stream/-/mute-stream-3.0.0.tgz", - "integrity": "sha512-dkEJPVvun4FryqBmZ5KhDo0K9iDXAwn08tMLDinNdRBNPcYEDiWYysLcc6k3mjTMlbP9KyylvRpd4wFtwrT9rw==", - "license": "ISC", - "engines": { - "node": "^20.17.0 || >=22.9.0" - } - }, "node_modules/@inquirer/editor": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/@inquirer/editor/-/editor-5.1.0.tgz", - "integrity": "sha512-6wlkYl65Qfayy48gPCfU4D7li6KCAGN79mLXa/tYHZH99OfZ820yY+HA+DgE88r8YwwgeuY6PQgNqMeK6LuMmw==", + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/@inquirer/editor/-/editor-5.1.1.tgz", + "integrity": "sha512-6y11LgmNpmn5D2aB5FgnCfBUBK8ZstwLCalyJmORcJZ/WrhOjm16mu6eSqIx8DnErxDqSLr+Jkp+GP8/Nwd5tA==", "license": "MIT", "dependencies": { - "@inquirer/core": "^11.1.8", + "@inquirer/core": "^11.1.9", "@inquirer/external-editor": "^3.0.0", "@inquirer/type": "^4.0.5" }, @@ -144,12 +126,12 @@ } }, "node_modules/@inquirer/expand": { - "version": "5.0.12", - "resolved": "https://registry.npmjs.org/@inquirer/expand/-/expand-5.0.12.tgz", - "integrity": "sha512-vOfrB33b7YIZfDauXS8vNNz2Z86FozTZLIt7e+7/dCaPJ1RXZsHCuI9TlcERzEUq57vkM+UdnBgxP0rFd23JYQ==", + "version": "5.0.13", + "resolved": "https://registry.npmjs.org/@inquirer/expand/-/expand-5.0.13.tgz", + "integrity": "sha512-dF2zvrFo9LshkcB23/O1il13kBkBltWIXzut1evfbuBLXMiGIuC45c+ZQ0uukjCDsvI8OWqun4FRYMnzFCQa3g==", "license": "MIT", "dependencies": { - "@inquirer/core": "^11.1.8", + "@inquirer/core": "^11.1.9", "@inquirer/type": "^4.0.5" }, "engines": { @@ -185,28 +167,6 @@ } } }, - "node_modules/@inquirer/external-editor/node_modules/chardet": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/chardet/-/chardet-2.1.1.tgz", - "integrity": "sha512-PsezH1rqdV9VvyNhxxOW32/d75r01NY7TQCmOqomRo15ZSOKbpTFVsfjghxo6JloQUCGnH4k1LGu0R4yCLlWQQ==", - "license": "MIT" - }, - "node_modules/@inquirer/external-editor/node_modules/iconv-lite": { - "version": "0.7.2", - "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.7.2.tgz", - "integrity": "sha512-im9DjEDQ55s9fL4EYzOAv0yMqmMBSZp6G0VvFyTMPKWxiSBHUj9NW/qqLmXUwXrrM7AvqSlTCfvqRb0cM8yYqw==", - "license": "MIT", - "dependencies": { - "safer-buffer": ">= 2.1.2 < 3.0.0" - }, - "engines": { - "node": ">=0.10.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/express" - } - }, "node_modules/@inquirer/figures": { "version": "2.0.5", "resolved": "https://registry.npmjs.org/@inquirer/figures/-/figures-2.0.5.tgz", @@ -217,12 +177,12 @@ } }, "node_modules/@inquirer/input": { - "version": "5.0.11", - "resolved": "https://registry.npmjs.org/@inquirer/input/-/input-5.0.11.tgz", - "integrity": "sha512-twUWidn4ocPO8qi6fRM7tNWt7W1FOnOZqQ+/+PsfLUacMR5rFLDPK9ql0nBPwxi0oELbo8T5NhRs8B2+qQEqFQ==", + "version": "5.0.12", + "resolved": "https://registry.npmjs.org/@inquirer/input/-/input-5.0.12.tgz", + "integrity": "sha512-uiMFBl4LqFzJClh80Q3f9hbOFJ6kgkDWI4LjAeBuyO6EanVVMF69AgOvpi1qdqjDSjDN6578B6nky9ceEpI+1Q==", "license": "MIT", "dependencies": { - "@inquirer/core": "^11.1.8", + "@inquirer/core": "^11.1.9", "@inquirer/type": "^4.0.5" }, "engines": { @@ -238,12 +198,12 @@ } }, "node_modules/@inquirer/number": { - "version": "4.0.11", - "resolved": "https://registry.npmjs.org/@inquirer/number/-/number-4.0.11.tgz", - "integrity": "sha512-Vscmim9TCksQsfjPtka/JwPUcbLhqWYrgfPf1cHrCm24X/F2joFwnageD50yMKsaX14oNGOyKf/RNXAFkNjWpA==", + "version": "4.0.12", + "resolved": "https://registry.npmjs.org/@inquirer/number/-/number-4.0.12.tgz", + "integrity": "sha512-/vrwhEf7Xsuh+YlHF4IjSy3g1cyrQuPaSiHIxCEbLu8qnfvrcvJyCkoktOOF+xV9gSb77/G0n3h04RbMDW2sIg==", "license": "MIT", "dependencies": { - "@inquirer/core": "^11.1.8", + "@inquirer/core": "^11.1.9", "@inquirer/type": "^4.0.5" }, "engines": { @@ -259,13 +219,13 @@ } }, "node_modules/@inquirer/password": { - "version": "5.0.11", - "resolved": "https://registry.npmjs.org/@inquirer/password/-/password-5.0.11.tgz", - "integrity": "sha512-9KZFeRaNHIcejtPb0wN4ddFc7EvobVoAFa049eS3LrDZFxI8O7xUXiITEOinBzkZFAIwY5V4yzQae/QfO9cbbg==", + "version": "5.0.12", + "resolved": "https://registry.npmjs.org/@inquirer/password/-/password-5.0.12.tgz", + "integrity": "sha512-CBh7YHju623lxJRcAOo498ZUwIuMy63bqW/vVq0tQAZVv+lkWlHkP9ealYE1utWSisEShY5VMdzIXRmyEODzcQ==", "license": "MIT", "dependencies": { "@inquirer/ansi": "^2.0.5", - "@inquirer/core": "^11.1.8", + "@inquirer/core": "^11.1.9", "@inquirer/type": "^4.0.5" }, "engines": { @@ -281,21 +241,21 @@ } }, "node_modules/@inquirer/prompts": { - "version": "8.4.1", - "resolved": "https://registry.npmjs.org/@inquirer/prompts/-/prompts-8.4.1.tgz", - "integrity": "sha512-AH5xPQ997K7e0F0vulPlteIHke2awMkFi8F0dBemrDfmvtPmHJo82mdHbONC4F/t8d1NHwrbI5cGVI+RbLWdoQ==", + "version": "8.4.2", + "resolved": "https://registry.npmjs.org/@inquirer/prompts/-/prompts-8.4.2.tgz", + "integrity": "sha512-XJmn/wY4AX56l1BRU+ZjDrFtg9+2uBEi4JvJQj82kwJDQKiPgSn4CEsbfGGygS4Gw6rkL4W18oATjfVfaqub2Q==", "license": "MIT", "dependencies": { - "@inquirer/checkbox": "^5.1.3", - "@inquirer/confirm": "^6.0.11", - "@inquirer/editor": "^5.1.0", - "@inquirer/expand": "^5.0.12", - "@inquirer/input": "^5.0.11", - "@inquirer/number": "^4.0.11", - "@inquirer/password": "^5.0.11", - "@inquirer/rawlist": "^5.2.7", - "@inquirer/search": "^4.1.7", - "@inquirer/select": "^5.1.3" + "@inquirer/checkbox": "^5.1.4", + "@inquirer/confirm": "^6.0.12", + "@inquirer/editor": "^5.1.1", + "@inquirer/expand": "^5.0.13", + "@inquirer/input": "^5.0.12", + "@inquirer/number": "^4.0.12", + "@inquirer/password": "^5.0.12", + "@inquirer/rawlist": "^5.2.8", + "@inquirer/search": "^4.1.8", + "@inquirer/select": "^5.1.4" }, "engines": { "node": ">=23.5.0 || ^22.13.0 || ^21.7.0 || ^20.12.0" @@ -310,12 +270,12 @@ } }, "node_modules/@inquirer/rawlist": { - "version": "5.2.7", - "resolved": "https://registry.npmjs.org/@inquirer/rawlist/-/rawlist-5.2.7.tgz", - "integrity": "sha512-AqRMiD9+uE1lskDPrdqHwrV/EUmxKEBLX44SR7uxK3vD2413AmVfE5EQaPeNzYf5Pq5SitHJDYUFVF0poIr09w==", + "version": "5.2.8", + "resolved": "https://registry.npmjs.org/@inquirer/rawlist/-/rawlist-5.2.8.tgz", + "integrity": "sha512-Su7FQvp5buZmCymN3PPoYv31ZQQX4ve2j02k7piGgKAWgE+AQRB5YoYVveGXcl3TZ9ldgRMSxj56YfDFmmaqLg==", "license": "MIT", "dependencies": { - "@inquirer/core": "^11.1.8", + "@inquirer/core": "^11.1.9", "@inquirer/type": "^4.0.5" }, "engines": { @@ -331,12 +291,12 @@ } }, "node_modules/@inquirer/search": { - "version": "4.1.7", - "resolved": "https://registry.npmjs.org/@inquirer/search/-/search-4.1.7.tgz", - "integrity": "sha512-1y7+0N65AWk5RdlXH/Kn13txf3IjIQ7OEfhCEkDTU+h5wKMLq8DUF3P6z+/kLSxDGDtQT1dRBWEUC3o/VvImsQ==", + "version": "4.1.8", + "resolved": "https://registry.npmjs.org/@inquirer/search/-/search-4.1.8.tgz", + "integrity": "sha512-fGiHKGD6DyPIYUWxoXnQTeXeyYqSOUrasDMABBmMHUalH/LxkuzY0xVRtimXAt1sUeeyYkVuKQx1bebMuN11Kw==", "license": "MIT", "dependencies": { - "@inquirer/core": "^11.1.8", + "@inquirer/core": "^11.1.9", "@inquirer/figures": "^2.0.5", "@inquirer/type": "^4.0.5" }, @@ -353,13 +313,13 @@ } }, "node_modules/@inquirer/select": { - "version": "5.1.3", - "resolved": "https://registry.npmjs.org/@inquirer/select/-/select-5.1.3.tgz", - "integrity": "sha512-zYyqWgGQi3NhBcNq4Isc5rB3oEdQEh1Q/EcAnOW0FK4MpnXWkvSBYgA4cYrTM4A9UB573omouZbnL9JJ74Mq3A==", + "version": "5.1.4", + "resolved": "https://registry.npmjs.org/@inquirer/select/-/select-5.1.4.tgz", + "integrity": "sha512-2kWcGKPMLAXAWRp1AH1SLsQmX+j0QjeljyXMUji9WMZC8nRDO0b7qquIGr6143E7KMLt3VAIGNXzwa/6PXQs4Q==", "license": "MIT", "dependencies": { "@inquirer/ansi": "^2.0.5", - "@inquirer/core": "^11.1.8", + "@inquirer/core": "^11.1.9", "@inquirer/figures": "^2.0.5", "@inquirer/type": "^4.0.5" }, @@ -393,27 +353,45 @@ } }, "node_modules/@types/node": { - "version": "22.7.2", - "resolved": "https://registry.npmjs.org/@types/node/-/node-22.7.2.tgz", - "integrity": "sha512-866lXSrpGpgyHBZUa2m9YNWqHDjjM0aBTJlNtYaGEw4rqY/dcD7deRVTbBBAJelfA7oaGDbNftXF/TL/A6RgoA==", + "version": "25.6.0", + "resolved": "https://registry.npmjs.org/@types/node/-/node-25.6.0.tgz", + "integrity": "sha512-+qIYRKdNYJwY3vRCZMdJbPLJAtGjQBudzZzdzwQYkEPQd+PJGixUL5QfvCLDaULoLv+RhT3LDkwEfKaAkgSmNQ==", + "license": "MIT", "optional": true, "dependencies": { - "undici-types": "~6.19.2" + "undici-types": "~7.19.0" } }, "node_modules/@types/yauzl": { "version": "2.10.3", "resolved": "https://registry.npmjs.org/@types/yauzl/-/yauzl-2.10.3.tgz", "integrity": "sha512-oJoftv0LSuaDZE3Le4DbKX+KS9G36NzOeSap90UIK0yMA/NhKJhqlSGtNDORNRaIbQfzjXDrQa0ytJ6mNRGz/Q==", + "license": "MIT", "optional": true, "dependencies": { "@types/node": "*" } }, + "node_modules/ansi-escapes": { + "version": "7.3.0", + "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-7.3.0.tgz", + "integrity": "sha512-BvU8nYgGQBxcmMuEeUEmNTvrMVjJNSH7RgW24vXexN4Ven6qCvy4TntnvlnwnMLTVlcRQQdbRY8NKnaIoeWDNg==", + "license": "MIT", + "dependencies": { + "environment": "^1.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/ansi-regex": { - "version": "6.1.0", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.1.0.tgz", - "integrity": "sha512-7HSX4QQb4CspciLpVFwyRe79O3xsIZDDLER21kERQ71oaPodF8jL725AgJMFAYbooIqolJoRLuM81SpeUkpkvA==", + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.2.2.tgz", + "integrity": "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==", + "license": "MIT", "engines": { "node": ">=12" }, @@ -422,9 +400,10 @@ } }, "node_modules/ansi-styles": { - "version": "6.2.1", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.1.tgz", - "integrity": "sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==", + "version": "6.2.3", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.3.tgz", + "integrity": "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==", + "license": "MIT", "engines": { "node": ">=12" }, @@ -435,12 +414,14 @@ "node_modules/asynckit": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", - "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==" + "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==", + "license": "MIT" }, "node_modules/buffer-crc32": { "version": "0.2.13", "resolved": "https://registry.npmjs.org/buffer-crc32/-/buffer-crc32-0.2.13.tgz", "integrity": "sha512-VO9Ht/+p3SN7SKWqcrgEzjGbRSJYTx+Q1pTQC0wrWqHx0vpJraQ6GtHx8tvcg1rlK1byhU5gccxgOgj7B0TDkQ==", + "license": "MIT", "engines": { "node": "*" } @@ -458,10 +439,41 @@ "node": ">= 0.4" } }, + "node_modules/chardet": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/chardet/-/chardet-2.1.1.tgz", + "integrity": "sha512-PsezH1rqdV9VvyNhxxOW32/d75r01NY7TQCmOqomRo15ZSOKbpTFVsfjghxo6JloQUCGnH4k1LGu0R4yCLlWQQ==", + "license": "MIT" + }, + "node_modules/cli-cursor": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-5.0.0.tgz", + "integrity": "sha512-aCj4O5wKyszjMmDT4tZj93kxyydN/K5zPWSCe6/0AV/AA1pqe5ZBIw0a2ZfPQV7lL5/yb5HsUreJ6UFAF1tEQw==", + "license": "MIT", + "dependencies": { + "restore-cursor": "^5.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/cli-width": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/cli-width/-/cli-width-4.1.0.tgz", + "integrity": "sha512-ouuZd4/dm2Sw5Gmqy6bGyNNNe1qt9RpmxveLSO7KcgsTnU7RXfsw+/bukWGo1abgBiMAic068rclZsO4IWmmxQ==", + "license": "ISC", + "engines": { + "node": ">= 12" + } + }, "node_modules/cliui": { "version": "8.0.1", "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz", "integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==", + "license": "ISC", "dependencies": { "string-width": "^4.2.0", "strip-ansi": "^6.0.1", @@ -475,6 +487,7 @@ "version": "5.0.1", "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "license": "MIT", "engines": { "node": ">=8" } @@ -483,6 +496,7 @@ "version": "4.3.0", "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "license": "MIT", "dependencies": { "color-convert": "^2.0.1" }, @@ -496,12 +510,23 @@ "node_modules/cliui/node_modules/emoji-regex": { "version": "8.0.0", "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", - "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==" + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "license": "MIT" + }, + "node_modules/cliui/node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "license": "MIT", + "engines": { + "node": ">=8" + } }, "node_modules/cliui/node_modules/string-width": { "version": "4.2.3", "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "license": "MIT", "dependencies": { "emoji-regex": "^8.0.0", "is-fullwidth-code-point": "^3.0.0", @@ -515,6 +540,7 @@ "version": "6.0.1", "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "license": "MIT", "dependencies": { "ansi-regex": "^5.0.1" }, @@ -526,6 +552,7 @@ "version": "7.0.0", "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "license": "MIT", "dependencies": { "ansi-styles": "^4.0.0", "string-width": "^4.1.0", @@ -542,6 +569,7 @@ "version": "2.0.1", "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "license": "MIT", "dependencies": { "color-name": "~1.1.4" }, @@ -552,12 +580,14 @@ "node_modules/color-name": { "version": "1.1.4", "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==" + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "license": "MIT" }, "node_modules/combined-stream": { "version": "1.0.8", "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", + "license": "MIT", "dependencies": { "delayed-stream": "~1.0.0" }, @@ -569,14 +599,16 @@ "version": "4.0.1", "resolved": "https://registry.npmjs.org/data-uri-to-buffer/-/data-uri-to-buffer-4.0.1.tgz", "integrity": "sha512-0R9ikRb668HB7QDxT1vkpuUBtqc53YyAwMwGeUFKRojY/NWKvdZ+9UYtRfGmhqNbRkTSVpMbmyhXipFFv2cb/A==", + "license": "MIT", "engines": { "node": ">= 12" } }, "node_modules/debug": { - "version": "4.3.7", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz", - "integrity": "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==", + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "license": "MIT", "dependencies": { "ms": "^2.1.3" }, @@ -593,6 +625,7 @@ "version": "2.8.4", "resolved": "https://registry.npmjs.org/degit/-/degit-2.8.4.tgz", "integrity": "sha512-vqYuzmSA5I50J882jd+AbAhQtgK6bdKUJIex1JNfEUPENCgYsxugzKVZlFyMwV4i06MmnV47/Iqi5Io86zf3Ng==", + "license": "MIT", "bin": { "degit": "degit" }, @@ -604,6 +637,7 @@ "version": "1.0.0", "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", + "license": "MIT", "engines": { "node": ">=0.4.0" } @@ -623,14 +657,16 @@ } }, "node_modules/emoji-regex": { - "version": "10.4.0", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-10.4.0.tgz", - "integrity": "sha512-EC+0oUMY1Rqm4O6LLrgjtYDvcVYTy7chDnM4Q7030tP4Kwj3u/pR6gP9ygnp2CJMK5Gq+9Q2oqmrFJAz01DXjw==" + "version": "10.6.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-10.6.0.tgz", + "integrity": "sha512-toUI84YS5YmxW219erniWD0CIVOo46xGKColeNQRgOzDorgBi1v4D71/OFzgD9GO2UGKIv1C3Sp8DAn0+j5w7A==", + "license": "MIT" }, "node_modules/end-of-stream": { - "version": "1.4.4", - "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.4.tgz", - "integrity": "sha512-+uw1inIHVPQoaVuHzRyXd21icM+cnt4CzD5rW+NC1wjOUSTOs+Te7FOv7AhN7vS9x/oIyhLP5PR1H+phQAHu5Q==", + "version": "1.4.5", + "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.5.tgz", + "integrity": "sha512-ooEGc6HP26xXq/N+GCGOT0JKCLDGrq2bQUZrQ7gyrJiZANJ/8YDTxTpQBXGMn+WbIQXNVpyWymm7KYVICQnyOg==", + "license": "MIT", "dependencies": { "once": "^1.4.0" } @@ -639,6 +675,7 @@ "version": "1.1.0", "resolved": "https://registry.npmjs.org/environment/-/environment-1.1.0.tgz", "integrity": "sha512-xUtoPkMggbz0MPyPiIWr1Kp4aeWJjDZ6SMvURhimjdZgsRuDplF5/s9hcgGhyXMhs+6vpnuoiZ2kFiu3FMnS8Q==", + "license": "MIT", "engines": { "node": ">=18" }, @@ -695,6 +732,7 @@ "version": "3.2.0", "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", + "license": "MIT", "engines": { "node": ">=6" } @@ -703,6 +741,7 @@ "version": "2.0.1", "resolved": "https://registry.npmjs.org/extract-zip/-/extract-zip-2.0.1.tgz", "integrity": "sha512-GDhU9ntwuKyGXdZBUgTIe+vXnWj0fppUEtMDL0+idd5Sta8TGpHssn/eusA9mrPr9qNDym6SxAYZjNvCn/9RBg==", + "license": "BSD-2-Clause", "dependencies": { "debug": "^4.1.1", "get-stream": "^5.1.0", @@ -746,6 +785,7 @@ "version": "1.1.0", "resolved": "https://registry.npmjs.org/fd-slicer/-/fd-slicer-1.1.0.tgz", "integrity": "sha512-cE1qsB/VwyQozZ+q1dGxR8LBYNZeofhEdUNGSMbQD3Gw2lAzX9Zb3uIU6Ebc/Fmyjo9AWWfnn0AUCHqtevs/8g==", + "license": "MIT", "dependencies": { "pend": "~1.2.0" } @@ -764,6 +804,7 @@ "url": "https://paypal.me/jimmywarting" } ], + "license": "MIT", "dependencies": { "node-domexception": "^1.0.0", "web-streams-polyfill": "^3.0.3" @@ -792,6 +833,7 @@ "version": "4.0.10", "resolved": "https://registry.npmjs.org/formdata-polyfill/-/formdata-polyfill-4.0.10.tgz", "integrity": "sha512-buewHzMvYL29jdeQTVILecSaZKnt/RJWjoZCF5OW60Z67/GmSLBkOFM7qh1PI3zFNtJbaZL5eQu1vLfazOwj4g==", + "license": "MIT", "dependencies": { "fetch-blob": "^3.1.2" }, @@ -812,14 +854,16 @@ "version": "2.0.5", "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", + "license": "ISC", "engines": { "node": "6.* || 8.* || >= 10.*" } }, "node_modules/get-east-asian-width": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/get-east-asian-width/-/get-east-asian-width-1.3.0.tgz", - "integrity": "sha512-vpeMIQKxczTD/0s2CdEWHcb0eeJe6TFjxb+J5xgX7hScxqrGuyjmv4c1D4A/gelKfyox0gJJwIHF+fLjeaM8kQ==", + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/get-east-asian-width/-/get-east-asian-width-1.5.0.tgz", + "integrity": "sha512-CQ+bEO+Tva/qlmw24dCejulK5pMzVnUOFOijVogd3KQs07HnRIgp8TGipvCCRT06xeYEbpbgwaCxglFyiuIcmA==", + "license": "MIT", "engines": { "node": ">=18" }, @@ -868,6 +912,7 @@ "version": "5.2.0", "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-5.2.0.tgz", "integrity": "sha512-nBF+F1rAZVCu/p7rjzgA+Yb4lfYXrpl7a6VmJrU8wF9I1CKvP/QwPNZHnOlwbTkY6dvtFIzFMSyQXbLoTQPRpA==", + "license": "MIT", "dependencies": { "pump": "^3.0.0" }, @@ -918,9 +963,9 @@ } }, "node_modules/hasown": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", - "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.3.tgz", + "integrity": "sha512-ej4AhfhfL2Q2zpMmLo7U1Uv9+PyhIZpgQLGT1F9miIGmiCJIoCgSmczFdrc97mWT4kVY72KA+WnnhJ5pghSvSg==", "license": "MIT", "dependencies": { "function-bind": "^1.1.2" @@ -929,66 +974,29 @@ "node": ">= 0.4" } }, - "node_modules/is-fullwidth-code-point": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", - "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", - "engines": { - "node": ">=8" - } - }, - "node_modules/log-update": { - "version": "6.1.0", - "resolved": "https://registry.npmjs.org/log-update/-/log-update-6.1.0.tgz", - "integrity": "sha512-9ie8ItPR6tjY5uYJh8K/Zrv/RMZ5VOlOWvtZdEHYSTFKZfIBPQa9tOAEeAWhd+AnIneLJ22w5fjOYtoutpWq5w==", - "dependencies": { - "ansi-escapes": "^7.0.0", - "cli-cursor": "^5.0.0", - "slice-ansi": "^7.1.0", - "strip-ansi": "^7.1.0", - "wrap-ansi": "^9.0.0" - }, - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/log-update/node_modules/ansi-escapes": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-7.0.0.tgz", - "integrity": "sha512-GdYO7a61mR0fOlAsvC9/rIHf7L96sBc6dEWzeOu+KAea5bZyQRPIpojrVoI4AXGJS/ycu/fBTdLrUkA4ODrvjw==", - "dependencies": { - "environment": "^1.0.0" - }, - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/log-update/node_modules/cli-cursor": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-5.0.0.tgz", - "integrity": "sha512-aCj4O5wKyszjMmDT4tZj93kxyydN/K5zPWSCe6/0AV/AA1pqe5ZBIw0a2ZfPQV7lL5/yb5HsUreJ6UFAF1tEQw==", + "node_modules/iconv-lite": { + "version": "0.7.2", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.7.2.tgz", + "integrity": "sha512-im9DjEDQ55s9fL4EYzOAv0yMqmMBSZp6G0VvFyTMPKWxiSBHUj9NW/qqLmXUwXrrM7AvqSlTCfvqRb0cM8yYqw==", + "license": "MIT", "dependencies": { - "restore-cursor": "^5.0.0" + "safer-buffer": ">= 2.1.2 < 3.0.0" }, "engines": { - "node": ">=18" + "node": ">=0.10.0" }, "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "type": "opencollective", + "url": "https://opencollective.com/express" } }, - "node_modules/log-update/node_modules/onetime": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/onetime/-/onetime-7.0.0.tgz", - "integrity": "sha512-VXJjc87FScF88uafS3JllDgvAm+c/Slfz06lorj2uAY34rlUu0Nt+v8wreiImcrgAjjIHp1rXpTDlLOGw29WwQ==", + "node_modules/is-fullwidth-code-point": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-5.1.0.tgz", + "integrity": "sha512-5XHYaSyiqADb4RnZ1Bdad6cPp8Toise4TzEjcOYDHZkTCbKgiUl7WTUCpNWHuxmDt91wnsZBc9xinNzopv3JMQ==", + "license": "MIT", "dependencies": { - "mimic-function": "^5.0.0" + "get-east-asian-width": "^1.3.1" }, "engines": { "node": ">=18" @@ -997,13 +1005,17 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/log-update/node_modules/restore-cursor": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-5.1.0.tgz", - "integrity": "sha512-oMA2dcrw6u0YfxJQXm342bFKX/E4sG9rbTzO9ptUcR/e8A33cHuvStiYOwH7fszkZlZ1z/ta9AAoPk2F4qIOHA==", + "node_modules/log-update": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/log-update/-/log-update-6.1.0.tgz", + "integrity": "sha512-9ie8ItPR6tjY5uYJh8K/Zrv/RMZ5VOlOWvtZdEHYSTFKZfIBPQa9tOAEeAWhd+AnIneLJ22w5fjOYtoutpWq5w==", + "license": "MIT", "dependencies": { - "onetime": "^7.0.0", - "signal-exit": "^4.1.0" + "ansi-escapes": "^7.0.0", + "cli-cursor": "^5.0.0", + "slice-ansi": "^7.1.0", + "strip-ansi": "^7.1.0", + "wrap-ansi": "^9.0.0" }, "engines": { "node": ">=18" @@ -1025,6 +1037,7 @@ "version": "1.52.0", "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "license": "MIT", "engines": { "node": ">= 0.6" } @@ -1033,6 +1046,7 @@ "version": "2.1.35", "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "license": "MIT", "dependencies": { "mime-db": "1.52.0" }, @@ -1044,6 +1058,7 @@ "version": "5.0.1", "resolved": "https://registry.npmjs.org/mimic-function/-/mimic-function-5.0.1.tgz", "integrity": "sha512-VP79XUPxV2CigYP3jWwAUFSku2aKqBH7uTAapFWCBqutsbmDo96KY5o8uh6U+/YSIn5OxJnXp73beVkpqMIGhA==", + "license": "MIT", "engines": { "node": ">=18" }, @@ -1054,12 +1069,23 @@ "node_modules/ms": { "version": "2.1.3", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", - "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==" + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT" + }, + "node_modules/mute-stream": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/mute-stream/-/mute-stream-3.0.0.tgz", + "integrity": "sha512-dkEJPVvun4FryqBmZ5KhDo0K9iDXAwn08tMLDinNdRBNPcYEDiWYysLcc6k3mjTMlbP9KyylvRpd4wFtwrT9rw==", + "license": "ISC", + "engines": { + "node": "^20.17.0 || >=22.9.0" + } }, "node_modules/node-domexception": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/node-domexception/-/node-domexception-1.0.0.tgz", "integrity": "sha512-/jKZoMpw0F8GRwl4/eLROPA3cfcXtLApP0QzLmUT/HuPCZWyB7IY9ZrMeKw2O/nFIqPQB3PVM9aYm0F312AXDQ==", + "deprecated": "Use your platform's native DOMException instead", "funding": [ { "type": "github", @@ -1070,6 +1096,7 @@ "url": "https://paypal.me/jimmywarting" } ], + "license": "MIT", "engines": { "node": ">=10.5.0" } @@ -1078,6 +1105,7 @@ "version": "3.3.2", "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-3.3.2.tgz", "integrity": "sha512-dRB78srN/l6gqWulah9SrxeYnxeddIG30+GOqK/9OlLVyLg3HPnr6SqOWTWOXKRwC2eGYCkZ59NNuSgvSrpgOA==", + "license": "MIT", "dependencies": { "data-uri-to-buffer": "^4.0.0", "fetch-blob": "^3.1.4", @@ -1095,19 +1123,37 @@ "version": "1.4.0", "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", + "license": "ISC", "dependencies": { "wrappy": "1" } }, + "node_modules/onetime": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/onetime/-/onetime-7.0.0.tgz", + "integrity": "sha512-VXJjc87FScF88uafS3JllDgvAm+c/Slfz06lorj2uAY34rlUu0Nt+v8wreiImcrgAjjIHp1rXpTDlLOGw29WwQ==", + "license": "MIT", + "dependencies": { + "mimic-function": "^5.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/pend": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/pend/-/pend-1.2.0.tgz", - "integrity": "sha512-F3asv42UuXchdzt+xXqfW1OGlVBe+mxa2mqI0pg5yAHZPvFmY3Y6drSf/GQ1A86WgWEN9Kzh/WrgKa6iGcHXLg==" + "integrity": "sha512-F3asv42UuXchdzt+xXqfW1OGlVBe+mxa2mqI0pg5yAHZPvFmY3Y6drSf/GQ1A86WgWEN9Kzh/WrgKa6iGcHXLg==", + "license": "MIT" }, "node_modules/pump": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.2.tgz", - "integrity": "sha512-tUPXtzlGM8FE3P0ZL6DVs/3P58k9nk8/jZeQCurTJylQA8qFYzHFfhBJkuqyE0FifOsQ0uKWekiZ5g8wtr28cw==", + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.4.tgz", + "integrity": "sha512-VS7sjc6KR7e1ukRFhQSY5LM2uBWAUPiOPa/A3mkKmiMwSmRFUITt0xuj+/lesgnCv+dPIEYlkzrcyXgquIHMcA==", + "license": "MIT", "dependencies": { "end-of-stream": "^1.1.0", "once": "^1.3.1" @@ -1117,19 +1163,38 @@ "version": "2.1.1", "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==", + "license": "MIT", "engines": { "node": ">=0.10.0" } }, + "node_modules/restore-cursor": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-5.1.0.tgz", + "integrity": "sha512-oMA2dcrw6u0YfxJQXm342bFKX/E4sG9rbTzO9ptUcR/e8A33cHuvStiYOwH7fszkZlZ1z/ta9AAoPk2F4qIOHA==", + "license": "MIT", + "dependencies": { + "onetime": "^7.0.0", + "signal-exit": "^4.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/safer-buffer": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", - "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==" + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", + "license": "MIT" }, "node_modules/signal-exit": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", + "license": "ISC", "engines": { "node": ">=14" }, @@ -1138,9 +1203,10 @@ } }, "node_modules/slice-ansi": { - "version": "7.1.0", - "resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-7.1.0.tgz", - "integrity": "sha512-bSiSngZ/jWeX93BqeIAbImyTbEihizcwNjFoRUIY/T1wWQsfsm2Vw1agPKylXvQTU7iASGdHhyqRlqQzfz+Htg==", + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-7.1.2.tgz", + "integrity": "sha512-iOBWFgUX7caIZiuutICxVgX1SdxwAVFFKwt1EvMYYec/NWO5meOJ6K5uQxhrYBdQJne4KxiqZc+KptFOWFSI9w==", + "license": "MIT", "dependencies": { "ansi-styles": "^6.2.1", "is-fullwidth-code-point": "^5.0.0" @@ -1152,24 +1218,11 @@ "url": "https://github.com/chalk/slice-ansi?sponsor=1" } }, - "node_modules/slice-ansi/node_modules/is-fullwidth-code-point": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-5.0.0.tgz", - "integrity": "sha512-OVa3u9kkBbw7b8Xw5F9P+D/T9X+Z4+JruYVNapTjPYZYUznQ5YfWeFkOj606XYYW8yugTfC8Pj0hYqvi4ryAhA==", - "dependencies": { - "get-east-asian-width": "^1.0.0" - }, - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/string-width": { "version": "7.2.0", "resolved": "https://registry.npmjs.org/string-width/-/string-width-7.2.0.tgz", "integrity": "sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ==", + "license": "MIT", "dependencies": { "emoji-regex": "^10.3.0", "get-east-asian-width": "^1.0.0", @@ -1183,11 +1236,12 @@ } }, "node_modules/strip-ansi": { - "version": "7.1.0", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.0.tgz", - "integrity": "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==", + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.2.0.tgz", + "integrity": "sha512-yDPMNjp4WyfYBkHnjIRLfca1i6KMyGCtsVgoKe/z1+6vukgaENdgGBZt+ZmKPc4gavvEZ5OgHfHdrazhgNyG7w==", + "license": "MIT", "dependencies": { - "ansi-regex": "^6.0.1" + "ansi-regex": "^6.2.2" }, "engines": { "node": ">=12" @@ -1197,23 +1251,26 @@ } }, "node_modules/undici-types": { - "version": "6.19.8", - "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.19.8.tgz", - "integrity": "sha512-ve2KP6f/JnbPBFyobGHuerC9g1FYGn/F8n1LWTwNxCEzd6IfqTwUQcNXgEtmmQ6DlRrC1hrSrBnCZPokRrDHjw==", + "version": "7.19.2", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.19.2.tgz", + "integrity": "sha512-qYVnV5OEm2AW8cJMCpdV20CDyaN3g0AjDlOGf1OW4iaDEx8MwdtChUp4zu4H0VP3nDRF/8RKWH+IPp9uW0YGZg==", + "license": "MIT", "optional": true }, "node_modules/web-streams-polyfill": { "version": "3.3.3", "resolved": "https://registry.npmjs.org/web-streams-polyfill/-/web-streams-polyfill-3.3.3.tgz", "integrity": "sha512-d2JWLCivmZYTSIoge9MsgFCZrt571BikcWGYkjC1khllbTeDlGqZ2D8vD8E/lJa8WGWbb7Plm8/XJYV7IJHZZw==", + "license": "MIT", "engines": { "node": ">= 8" } }, "node_modules/wrap-ansi": { - "version": "9.0.0", - "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-9.0.0.tgz", - "integrity": "sha512-G8ura3S+3Z2G+mkgNRq8dqaFZAuxfsxpBB8OCTGRTCtp+l/v9nbFNmCUP1BZMts3G1142MsZfn6eeUKrr4PD1Q==", + "version": "9.0.2", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-9.0.2.tgz", + "integrity": "sha512-42AtmgqjV+X1VpdOfyTGOYRi0/zsoLqtXQckTmqTeybT+BDIbM/Guxo7x3pE2vtpr1ok6xRqM9OpBe+Jyoqyww==", + "license": "MIT", "dependencies": { "ansi-styles": "^6.2.1", "string-width": "^7.0.0", @@ -1229,12 +1286,14 @@ "node_modules/wrappy": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", - "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==" + "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", + "license": "ISC" }, "node_modules/y18n": { "version": "5.0.8", "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", + "license": "ISC", "engines": { "node": ">=10" } @@ -1243,6 +1302,7 @@ "version": "17.7.2", "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz", "integrity": "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==", + "license": "MIT", "dependencies": { "cliui": "^8.0.1", "escalade": "^3.1.1", @@ -1260,6 +1320,7 @@ "version": "21.1.1", "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz", "integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==", + "license": "ISC", "engines": { "node": ">=12" } @@ -1268,6 +1329,7 @@ "version": "5.0.1", "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "license": "MIT", "engines": { "node": ">=8" } @@ -1275,12 +1337,23 @@ "node_modules/yargs/node_modules/emoji-regex": { "version": "8.0.0", "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", - "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==" + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "license": "MIT" + }, + "node_modules/yargs/node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "license": "MIT", + "engines": { + "node": ">=8" + } }, "node_modules/yargs/node_modules/string-width": { "version": "4.2.3", "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "license": "MIT", "dependencies": { "emoji-regex": "^8.0.0", "is-fullwidth-code-point": "^3.0.0", @@ -1294,6 +1367,7 @@ "version": "6.0.1", "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "license": "MIT", "dependencies": { "ansi-regex": "^5.0.1" }, @@ -1305,805 +1379,11 @@ "version": "2.10.0", "resolved": "https://registry.npmjs.org/yauzl/-/yauzl-2.10.0.tgz", "integrity": "sha512-p4a9I6X6nu6IhoGmBqAcbJy1mlC4j27vEPZX9F4L4/vZT3Lyq1VkFHw/V/PUcB9Buo+DG3iHkT0x3Qya58zc3g==", + "license": "MIT", "dependencies": { "buffer-crc32": "~0.2.3", "fd-slicer": "~1.1.0" } } - }, - "dependencies": { - "@inquirer/ansi": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/@inquirer/ansi/-/ansi-2.0.5.tgz", - "integrity": "sha512-doc2sWgJpbFQ64UflSVd17ibMGDuxO1yKgOgLMwavzESnXjFWJqUeG8saYosqKpHp4kWiM5x1nXvEjbpx90gzw==" - }, - "@inquirer/checkbox": { - "version": "5.1.3", - "resolved": "https://registry.npmjs.org/@inquirer/checkbox/-/checkbox-5.1.3.tgz", - "integrity": "sha512-+G7I8CT+EHv/hasNfUl3P37DVoMoZfpA+2FXmM54dA8MxYle1YqucxbacxHalw1iAFSdKNEDTGNV7F+j1Ldqcg==", - "requires": { - "@inquirer/ansi": "^2.0.5", - "@inquirer/core": "^11.1.8", - "@inquirer/figures": "^2.0.5", - "@inquirer/type": "^4.0.5" - } - }, - "@inquirer/confirm": { - "version": "6.0.11", - "resolved": "https://registry.npmjs.org/@inquirer/confirm/-/confirm-6.0.11.tgz", - "integrity": "sha512-pTpHjg0iEIRMYV/7oCZUMf27/383E6Wyhfc/MY+AVQGEoUobffIYWOK9YLP2XFRGz/9i6WlTQh1CkFVIo2Y7XA==", - "requires": { - "@inquirer/core": "^11.1.8", - "@inquirer/type": "^4.0.5" - } - }, - "@inquirer/core": { - "version": "11.1.8", - "resolved": "https://registry.npmjs.org/@inquirer/core/-/core-11.1.8.tgz", - "integrity": "sha512-/u+yJk2pOKNDOh1ZgdUH2RQaRx6OOH4I0uwL95qPvTFTIL38YBsuSC4r1yXBB3Q6JvNqFFc202gk0Ew79rrcjA==", - "requires": { - "@inquirer/ansi": "^2.0.5", - "@inquirer/figures": "^2.0.5", - "@inquirer/type": "^4.0.5", - "cli-width": "^4.1.0", - "fast-wrap-ansi": "^0.2.0", - "mute-stream": "^3.0.0", - "signal-exit": "^4.1.0" - }, - "dependencies": { - "cli-width": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/cli-width/-/cli-width-4.1.0.tgz", - "integrity": "sha512-ouuZd4/dm2Sw5Gmqy6bGyNNNe1qt9RpmxveLSO7KcgsTnU7RXfsw+/bukWGo1abgBiMAic068rclZsO4IWmmxQ==" - }, - "mute-stream": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/mute-stream/-/mute-stream-3.0.0.tgz", - "integrity": "sha512-dkEJPVvun4FryqBmZ5KhDo0K9iDXAwn08tMLDinNdRBNPcYEDiWYysLcc6k3mjTMlbP9KyylvRpd4wFtwrT9rw==" - } - } - }, - "@inquirer/editor": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/@inquirer/editor/-/editor-5.1.0.tgz", - "integrity": "sha512-6wlkYl65Qfayy48gPCfU4D7li6KCAGN79mLXa/tYHZH99OfZ820yY+HA+DgE88r8YwwgeuY6PQgNqMeK6LuMmw==", - "requires": { - "@inquirer/core": "^11.1.8", - "@inquirer/external-editor": "^3.0.0", - "@inquirer/type": "^4.0.5" - } - }, - "@inquirer/expand": { - "version": "5.0.12", - "resolved": "https://registry.npmjs.org/@inquirer/expand/-/expand-5.0.12.tgz", - "integrity": "sha512-vOfrB33b7YIZfDauXS8vNNz2Z86FozTZLIt7e+7/dCaPJ1RXZsHCuI9TlcERzEUq57vkM+UdnBgxP0rFd23JYQ==", - "requires": { - "@inquirer/core": "^11.1.8", - "@inquirer/type": "^4.0.5" - } - }, - "@inquirer/external-editor": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/@inquirer/external-editor/-/external-editor-3.0.0.tgz", - "integrity": "sha512-lDSwMgg+M5rq6JKBYaJwSX6T9e/HK2qqZ1oxmOwn4AQoJE5D+7TumsxLGC02PWS//rkIVqbZv3XA3ejsc9FYvg==", - "requires": { - "chardet": "^2.1.1", - "iconv-lite": "^0.7.2" - }, - "dependencies": { - "chardet": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/chardet/-/chardet-2.1.1.tgz", - "integrity": "sha512-PsezH1rqdV9VvyNhxxOW32/d75r01NY7TQCmOqomRo15ZSOKbpTFVsfjghxo6JloQUCGnH4k1LGu0R4yCLlWQQ==" - }, - "iconv-lite": { - "version": "0.7.2", - "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.7.2.tgz", - "integrity": "sha512-im9DjEDQ55s9fL4EYzOAv0yMqmMBSZp6G0VvFyTMPKWxiSBHUj9NW/qqLmXUwXrrM7AvqSlTCfvqRb0cM8yYqw==", - "requires": { - "safer-buffer": ">= 2.1.2 < 3.0.0" - } - } - } - }, - "@inquirer/figures": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/@inquirer/figures/-/figures-2.0.5.tgz", - "integrity": "sha512-NsSs4kzfm12lNetHwAn3GEuH317IzpwrMCbOuMIVytpjnJ90YYHNwdRgYGuKmVxwuIqSgqk3M5qqQt1cDk0tGQ==" - }, - "@inquirer/input": { - "version": "5.0.11", - "resolved": "https://registry.npmjs.org/@inquirer/input/-/input-5.0.11.tgz", - "integrity": "sha512-twUWidn4ocPO8qi6fRM7tNWt7W1FOnOZqQ+/+PsfLUacMR5rFLDPK9ql0nBPwxi0oELbo8T5NhRs8B2+qQEqFQ==", - "requires": { - "@inquirer/core": "^11.1.8", - "@inquirer/type": "^4.0.5" - } - }, - "@inquirer/number": { - "version": "4.0.11", - "resolved": "https://registry.npmjs.org/@inquirer/number/-/number-4.0.11.tgz", - "integrity": "sha512-Vscmim9TCksQsfjPtka/JwPUcbLhqWYrgfPf1cHrCm24X/F2joFwnageD50yMKsaX14oNGOyKf/RNXAFkNjWpA==", - "requires": { - "@inquirer/core": "^11.1.8", - "@inquirer/type": "^4.0.5" - } - }, - "@inquirer/password": { - "version": "5.0.11", - "resolved": "https://registry.npmjs.org/@inquirer/password/-/password-5.0.11.tgz", - "integrity": "sha512-9KZFeRaNHIcejtPb0wN4ddFc7EvobVoAFa049eS3LrDZFxI8O7xUXiITEOinBzkZFAIwY5V4yzQae/QfO9cbbg==", - "requires": { - "@inquirer/ansi": "^2.0.5", - "@inquirer/core": "^11.1.8", - "@inquirer/type": "^4.0.5" - } - }, - "@inquirer/prompts": { - "version": "8.4.1", - "resolved": "https://registry.npmjs.org/@inquirer/prompts/-/prompts-8.4.1.tgz", - "integrity": "sha512-AH5xPQ997K7e0F0vulPlteIHke2awMkFi8F0dBemrDfmvtPmHJo82mdHbONC4F/t8d1NHwrbI5cGVI+RbLWdoQ==", - "requires": { - "@inquirer/checkbox": "^5.1.3", - "@inquirer/confirm": "^6.0.11", - "@inquirer/editor": "^5.1.0", - "@inquirer/expand": "^5.0.12", - "@inquirer/input": "^5.0.11", - "@inquirer/number": "^4.0.11", - "@inquirer/password": "^5.0.11", - "@inquirer/rawlist": "^5.2.7", - "@inquirer/search": "^4.1.7", - "@inquirer/select": "^5.1.3" - } - }, - "@inquirer/rawlist": { - "version": "5.2.7", - "resolved": "https://registry.npmjs.org/@inquirer/rawlist/-/rawlist-5.2.7.tgz", - "integrity": "sha512-AqRMiD9+uE1lskDPrdqHwrV/EUmxKEBLX44SR7uxK3vD2413AmVfE5EQaPeNzYf5Pq5SitHJDYUFVF0poIr09w==", - "requires": { - "@inquirer/core": "^11.1.8", - "@inquirer/type": "^4.0.5" - } - }, - "@inquirer/search": { - "version": "4.1.7", - "resolved": "https://registry.npmjs.org/@inquirer/search/-/search-4.1.7.tgz", - "integrity": "sha512-1y7+0N65AWk5RdlXH/Kn13txf3IjIQ7OEfhCEkDTU+h5wKMLq8DUF3P6z+/kLSxDGDtQT1dRBWEUC3o/VvImsQ==", - "requires": { - "@inquirer/core": "^11.1.8", - "@inquirer/figures": "^2.0.5", - "@inquirer/type": "^4.0.5" - } - }, - "@inquirer/select": { - "version": "5.1.3", - "resolved": "https://registry.npmjs.org/@inquirer/select/-/select-5.1.3.tgz", - "integrity": "sha512-zYyqWgGQi3NhBcNq4Isc5rB3oEdQEh1Q/EcAnOW0FK4MpnXWkvSBYgA4cYrTM4A9UB573omouZbnL9JJ74Mq3A==", - "requires": { - "@inquirer/ansi": "^2.0.5", - "@inquirer/core": "^11.1.8", - "@inquirer/figures": "^2.0.5", - "@inquirer/type": "^4.0.5" - } - }, - "@inquirer/type": { - "version": "4.0.5", - "resolved": "https://registry.npmjs.org/@inquirer/type/-/type-4.0.5.tgz", - "integrity": "sha512-aetVUNeKNc/VriqXlw1NRSW0zhMBB0W4bNbWRJgzRl/3d0QNDQFfk0GO5SDdtjMZVg6o8ZKEiadd7SCCzoOn5Q==", - "requires": {} - }, - "@types/node": { - "version": "22.7.2", - "resolved": "https://registry.npmjs.org/@types/node/-/node-22.7.2.tgz", - "integrity": "sha512-866lXSrpGpgyHBZUa2m9YNWqHDjjM0aBTJlNtYaGEw4rqY/dcD7deRVTbBBAJelfA7oaGDbNftXF/TL/A6RgoA==", - "optional": true, - "requires": { - "undici-types": "~6.19.2" - } - }, - "@types/yauzl": { - "version": "2.10.3", - "resolved": "https://registry.npmjs.org/@types/yauzl/-/yauzl-2.10.3.tgz", - "integrity": "sha512-oJoftv0LSuaDZE3Le4DbKX+KS9G36NzOeSap90UIK0yMA/NhKJhqlSGtNDORNRaIbQfzjXDrQa0ytJ6mNRGz/Q==", - "optional": true, - "requires": { - "@types/node": "*" - } - }, - "ansi-regex": { - "version": "6.1.0", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.1.0.tgz", - "integrity": "sha512-7HSX4QQb4CspciLpVFwyRe79O3xsIZDDLER21kERQ71oaPodF8jL725AgJMFAYbooIqolJoRLuM81SpeUkpkvA==" - }, - "ansi-styles": { - "version": "6.2.1", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.1.tgz", - "integrity": "sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==" - }, - "asynckit": { - "version": "0.4.0", - "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", - "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==" - }, - "buffer-crc32": { - "version": "0.2.13", - "resolved": "https://registry.npmjs.org/buffer-crc32/-/buffer-crc32-0.2.13.tgz", - "integrity": "sha512-VO9Ht/+p3SN7SKWqcrgEzjGbRSJYTx+Q1pTQC0wrWqHx0vpJraQ6GtHx8tvcg1rlK1byhU5gccxgOgj7B0TDkQ==" - }, - "call-bind-apply-helpers": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", - "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", - "requires": { - "es-errors": "^1.3.0", - "function-bind": "^1.1.2" - } - }, - "cliui": { - "version": "8.0.1", - "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz", - "integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==", - "requires": { - "string-width": "^4.2.0", - "strip-ansi": "^6.0.1", - "wrap-ansi": "^7.0.0" - }, - "dependencies": { - "ansi-regex": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", - "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==" - }, - "ansi-styles": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", - "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", - "requires": { - "color-convert": "^2.0.1" - } - }, - "emoji-regex": { - "version": "8.0.0", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", - "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==" - }, - "string-width": { - "version": "4.2.3", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", - "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", - "requires": { - "emoji-regex": "^8.0.0", - "is-fullwidth-code-point": "^3.0.0", - "strip-ansi": "^6.0.1" - } - }, - "strip-ansi": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", - "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", - "requires": { - "ansi-regex": "^5.0.1" - } - }, - "wrap-ansi": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", - "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", - "requires": { - "ansi-styles": "^4.0.0", - "string-width": "^4.1.0", - "strip-ansi": "^6.0.0" - } - } - } - }, - "color-convert": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", - "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", - "requires": { - "color-name": "~1.1.4" - } - }, - "color-name": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==" - }, - "combined-stream": { - "version": "1.0.8", - "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", - "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", - "requires": { - "delayed-stream": "~1.0.0" - } - }, - "data-uri-to-buffer": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/data-uri-to-buffer/-/data-uri-to-buffer-4.0.1.tgz", - "integrity": "sha512-0R9ikRb668HB7QDxT1vkpuUBtqc53YyAwMwGeUFKRojY/NWKvdZ+9UYtRfGmhqNbRkTSVpMbmyhXipFFv2cb/A==" - }, - "debug": { - "version": "4.3.7", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz", - "integrity": "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==", - "requires": { - "ms": "^2.1.3" - } - }, - "degit": { - "version": "2.8.4", - "resolved": "https://registry.npmjs.org/degit/-/degit-2.8.4.tgz", - "integrity": "sha512-vqYuzmSA5I50J882jd+AbAhQtgK6bdKUJIex1JNfEUPENCgYsxugzKVZlFyMwV4i06MmnV47/Iqi5Io86zf3Ng==" - }, - "delayed-stream": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", - "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==" - }, - "dunder-proto": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", - "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", - "requires": { - "call-bind-apply-helpers": "^1.0.1", - "es-errors": "^1.3.0", - "gopd": "^1.2.0" - } - }, - "emoji-regex": { - "version": "10.4.0", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-10.4.0.tgz", - "integrity": "sha512-EC+0oUMY1Rqm4O6LLrgjtYDvcVYTy7chDnM4Q7030tP4Kwj3u/pR6gP9ygnp2CJMK5Gq+9Q2oqmrFJAz01DXjw==" - }, - "end-of-stream": { - "version": "1.4.4", - "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.4.tgz", - "integrity": "sha512-+uw1inIHVPQoaVuHzRyXd21icM+cnt4CzD5rW+NC1wjOUSTOs+Te7FOv7AhN7vS9x/oIyhLP5PR1H+phQAHu5Q==", - "requires": { - "once": "^1.4.0" - } - }, - "environment": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/environment/-/environment-1.1.0.tgz", - "integrity": "sha512-xUtoPkMggbz0MPyPiIWr1Kp4aeWJjDZ6SMvURhimjdZgsRuDplF5/s9hcgGhyXMhs+6vpnuoiZ2kFiu3FMnS8Q==" - }, - "es-define-property": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", - "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==" - }, - "es-errors": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", - "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==" - }, - "es-object-atoms": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", - "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", - "requires": { - "es-errors": "^1.3.0" - } - }, - "es-set-tostringtag": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz", - "integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==", - "requires": { - "es-errors": "^1.3.0", - "get-intrinsic": "^1.2.6", - "has-tostringtag": "^1.0.2", - "hasown": "^2.0.2" - } - }, - "escalade": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", - "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==" - }, - "extract-zip": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/extract-zip/-/extract-zip-2.0.1.tgz", - "integrity": "sha512-GDhU9ntwuKyGXdZBUgTIe+vXnWj0fppUEtMDL0+idd5Sta8TGpHssn/eusA9mrPr9qNDym6SxAYZjNvCn/9RBg==", - "requires": { - "@types/yauzl": "^2.9.1", - "debug": "^4.1.1", - "get-stream": "^5.1.0", - "yauzl": "^2.10.0" - } - }, - "fast-string-truncated-width": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/fast-string-truncated-width/-/fast-string-truncated-width-3.0.3.tgz", - "integrity": "sha512-0jjjIEL6+0jag3l2XWWizO64/aZVtpiGE3t0Zgqxv0DPuxiMjvB3M24fCyhZUO4KomJQPj3LTSUnDP3GpdwC0g==" - }, - "fast-string-width": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/fast-string-width/-/fast-string-width-3.0.2.tgz", - "integrity": "sha512-gX8LrtNEI5hq8DVUfRQMbr5lpaS4nMIWV+7XEbXk2b8kiQIizgnlr12B4dA3ZEx3308ze0O4Q1R+cHts8kyUJg==", - "requires": { - "fast-string-truncated-width": "^3.0.2" - } - }, - "fast-wrap-ansi": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/fast-wrap-ansi/-/fast-wrap-ansi-0.2.0.tgz", - "integrity": "sha512-rLV8JHxTyhVmFYhBJuMujcrHqOT2cnO5Zxj37qROj23CP39GXubJRBUFF0z8KFK77Uc0SukZUf7JZhsVEQ6n8w==", - "requires": { - "fast-string-width": "^3.0.2" - } - }, - "fd-slicer": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/fd-slicer/-/fd-slicer-1.1.0.tgz", - "integrity": "sha512-cE1qsB/VwyQozZ+q1dGxR8LBYNZeofhEdUNGSMbQD3Gw2lAzX9Zb3uIU6Ebc/Fmyjo9AWWfnn0AUCHqtevs/8g==", - "requires": { - "pend": "~1.2.0" - } - }, - "fetch-blob": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/fetch-blob/-/fetch-blob-3.2.0.tgz", - "integrity": "sha512-7yAQpD2UMJzLi1Dqv7qFYnPbaPx7ZfFK6PiIxQ4PfkGPyNyl2Ugx+a/umUonmKqjhM4DnfbMvdX6otXq83soQQ==", - "requires": { - "node-domexception": "^1.0.0", - "web-streams-polyfill": "^3.0.3" - } - }, - "form-data": { - "version": "4.0.5", - "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.5.tgz", - "integrity": "sha512-8RipRLol37bNs2bhoV67fiTEvdTrbMUYcFTiy3+wuuOnUog2QBHCZWXDRijWQfAkhBj2Uf5UnVaiWwA5vdd82w==", - "requires": { - "asynckit": "^0.4.0", - "combined-stream": "^1.0.8", - "es-set-tostringtag": "^2.1.0", - "hasown": "^2.0.2", - "mime-types": "^2.1.12" - } - }, - "formdata-polyfill": { - "version": "4.0.10", - "resolved": "https://registry.npmjs.org/formdata-polyfill/-/formdata-polyfill-4.0.10.tgz", - "integrity": "sha512-buewHzMvYL29jdeQTVILecSaZKnt/RJWjoZCF5OW60Z67/GmSLBkOFM7qh1PI3zFNtJbaZL5eQu1vLfazOwj4g==", - "requires": { - "fetch-blob": "^3.1.2" - } - }, - "function-bind": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", - "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==" - }, - "get-caller-file": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", - "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==" - }, - "get-east-asian-width": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/get-east-asian-width/-/get-east-asian-width-1.3.0.tgz", - "integrity": "sha512-vpeMIQKxczTD/0s2CdEWHcb0eeJe6TFjxb+J5xgX7hScxqrGuyjmv4c1D4A/gelKfyox0gJJwIHF+fLjeaM8kQ==" - }, - "get-intrinsic": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", - "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", - "requires": { - "call-bind-apply-helpers": "^1.0.2", - "es-define-property": "^1.0.1", - "es-errors": "^1.3.0", - "es-object-atoms": "^1.1.1", - "function-bind": "^1.1.2", - "get-proto": "^1.0.1", - "gopd": "^1.2.0", - "has-symbols": "^1.1.0", - "hasown": "^2.0.2", - "math-intrinsics": "^1.1.0" - } - }, - "get-proto": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", - "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", - "requires": { - "dunder-proto": "^1.0.1", - "es-object-atoms": "^1.0.0" - } - }, - "get-stream": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-5.2.0.tgz", - "integrity": "sha512-nBF+F1rAZVCu/p7rjzgA+Yb4lfYXrpl7a6VmJrU8wF9I1CKvP/QwPNZHnOlwbTkY6dvtFIzFMSyQXbLoTQPRpA==", - "requires": { - "pump": "^3.0.0" - } - }, - "gopd": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", - "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==" - }, - "has-symbols": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", - "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==" - }, - "has-tostringtag": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz", - "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==", - "requires": { - "has-symbols": "^1.0.3" - } - }, - "hasown": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", - "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", - "requires": { - "function-bind": "^1.1.2" - } - }, - "is-fullwidth-code-point": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", - "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==" - }, - "log-update": { - "version": "6.1.0", - "resolved": "https://registry.npmjs.org/log-update/-/log-update-6.1.0.tgz", - "integrity": "sha512-9ie8ItPR6tjY5uYJh8K/Zrv/RMZ5VOlOWvtZdEHYSTFKZfIBPQa9tOAEeAWhd+AnIneLJ22w5fjOYtoutpWq5w==", - "requires": { - "ansi-escapes": "^7.0.0", - "cli-cursor": "^5.0.0", - "slice-ansi": "^7.1.0", - "strip-ansi": "^7.1.0", - "wrap-ansi": "^9.0.0" - }, - "dependencies": { - "ansi-escapes": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-7.0.0.tgz", - "integrity": "sha512-GdYO7a61mR0fOlAsvC9/rIHf7L96sBc6dEWzeOu+KAea5bZyQRPIpojrVoI4AXGJS/ycu/fBTdLrUkA4ODrvjw==", - "requires": { - "environment": "^1.0.0" - } - }, - "cli-cursor": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-5.0.0.tgz", - "integrity": "sha512-aCj4O5wKyszjMmDT4tZj93kxyydN/K5zPWSCe6/0AV/AA1pqe5ZBIw0a2ZfPQV7lL5/yb5HsUreJ6UFAF1tEQw==", - "requires": { - "restore-cursor": "^5.0.0" - } - }, - "onetime": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/onetime/-/onetime-7.0.0.tgz", - "integrity": "sha512-VXJjc87FScF88uafS3JllDgvAm+c/Slfz06lorj2uAY34rlUu0Nt+v8wreiImcrgAjjIHp1rXpTDlLOGw29WwQ==", - "requires": { - "mimic-function": "^5.0.0" - } - }, - "restore-cursor": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-5.1.0.tgz", - "integrity": "sha512-oMA2dcrw6u0YfxJQXm342bFKX/E4sG9rbTzO9ptUcR/e8A33cHuvStiYOwH7fszkZlZ1z/ta9AAoPk2F4qIOHA==", - "requires": { - "onetime": "^7.0.0", - "signal-exit": "^4.1.0" - } - } - } - }, - "math-intrinsics": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", - "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==" - }, - "mime-db": { - "version": "1.52.0", - "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", - "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==" - }, - "mime-types": { - "version": "2.1.35", - "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", - "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", - "requires": { - "mime-db": "1.52.0" - } - }, - "mimic-function": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/mimic-function/-/mimic-function-5.0.1.tgz", - "integrity": "sha512-VP79XUPxV2CigYP3jWwAUFSku2aKqBH7uTAapFWCBqutsbmDo96KY5o8uh6U+/YSIn5OxJnXp73beVkpqMIGhA==" - }, - "ms": { - "version": "2.1.3", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", - "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==" - }, - "node-domexception": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/node-domexception/-/node-domexception-1.0.0.tgz", - "integrity": "sha512-/jKZoMpw0F8GRwl4/eLROPA3cfcXtLApP0QzLmUT/HuPCZWyB7IY9ZrMeKw2O/nFIqPQB3PVM9aYm0F312AXDQ==" - }, - "node-fetch": { - "version": "3.3.2", - "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-3.3.2.tgz", - "integrity": "sha512-dRB78srN/l6gqWulah9SrxeYnxeddIG30+GOqK/9OlLVyLg3HPnr6SqOWTWOXKRwC2eGYCkZ59NNuSgvSrpgOA==", - "requires": { - "data-uri-to-buffer": "^4.0.0", - "fetch-blob": "^3.1.4", - "formdata-polyfill": "^4.0.10" - } - }, - "once": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", - "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", - "requires": { - "wrappy": "1" - } - }, - "pend": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/pend/-/pend-1.2.0.tgz", - "integrity": "sha512-F3asv42UuXchdzt+xXqfW1OGlVBe+mxa2mqI0pg5yAHZPvFmY3Y6drSf/GQ1A86WgWEN9Kzh/WrgKa6iGcHXLg==" - }, - "pump": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.2.tgz", - "integrity": "sha512-tUPXtzlGM8FE3P0ZL6DVs/3P58k9nk8/jZeQCurTJylQA8qFYzHFfhBJkuqyE0FifOsQ0uKWekiZ5g8wtr28cw==", - "requires": { - "end-of-stream": "^1.1.0", - "once": "^1.3.1" - } - }, - "require-directory": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", - "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==" - }, - "safer-buffer": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", - "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==" - }, - "signal-exit": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", - "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==" - }, - "slice-ansi": { - "version": "7.1.0", - "resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-7.1.0.tgz", - "integrity": "sha512-bSiSngZ/jWeX93BqeIAbImyTbEihizcwNjFoRUIY/T1wWQsfsm2Vw1agPKylXvQTU7iASGdHhyqRlqQzfz+Htg==", - "requires": { - "ansi-styles": "^6.2.1", - "is-fullwidth-code-point": "^5.0.0" - }, - "dependencies": { - "is-fullwidth-code-point": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-5.0.0.tgz", - "integrity": "sha512-OVa3u9kkBbw7b8Xw5F9P+D/T9X+Z4+JruYVNapTjPYZYUznQ5YfWeFkOj606XYYW8yugTfC8Pj0hYqvi4ryAhA==", - "requires": { - "get-east-asian-width": "^1.0.0" - } - } - } - }, - "string-width": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-7.2.0.tgz", - "integrity": "sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ==", - "requires": { - "emoji-regex": "^10.3.0", - "get-east-asian-width": "^1.0.0", - "strip-ansi": "^7.1.0" - } - }, - "strip-ansi": { - "version": "7.1.0", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.0.tgz", - "integrity": "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==", - "requires": { - "ansi-regex": "^6.0.1" - } - }, - "undici-types": { - "version": "6.19.8", - "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.19.8.tgz", - "integrity": "sha512-ve2KP6f/JnbPBFyobGHuerC9g1FYGn/F8n1LWTwNxCEzd6IfqTwUQcNXgEtmmQ6DlRrC1hrSrBnCZPokRrDHjw==", - "optional": true - }, - "web-streams-polyfill": { - "version": "3.3.3", - "resolved": "https://registry.npmjs.org/web-streams-polyfill/-/web-streams-polyfill-3.3.3.tgz", - "integrity": "sha512-d2JWLCivmZYTSIoge9MsgFCZrt571BikcWGYkjC1khllbTeDlGqZ2D8vD8E/lJa8WGWbb7Plm8/XJYV7IJHZZw==" - }, - "wrap-ansi": { - "version": "9.0.0", - "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-9.0.0.tgz", - "integrity": "sha512-G8ura3S+3Z2G+mkgNRq8dqaFZAuxfsxpBB8OCTGRTCtp+l/v9nbFNmCUP1BZMts3G1142MsZfn6eeUKrr4PD1Q==", - "requires": { - "ansi-styles": "^6.2.1", - "string-width": "^7.0.0", - "strip-ansi": "^7.1.0" - } - }, - "wrappy": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", - "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==" - }, - "y18n": { - "version": "5.0.8", - "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", - "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==" - }, - "yargs": { - "version": "17.7.2", - "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz", - "integrity": "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==", - "requires": { - "cliui": "^8.0.1", - "escalade": "^3.1.1", - "get-caller-file": "^2.0.5", - "require-directory": "^2.1.1", - "string-width": "^4.2.3", - "y18n": "^5.0.5", - "yargs-parser": "^21.1.1" - }, - "dependencies": { - "ansi-regex": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", - "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==" - }, - "emoji-regex": { - "version": "8.0.0", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", - "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==" - }, - "string-width": { - "version": "4.2.3", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", - "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", - "requires": { - "emoji-regex": "^8.0.0", - "is-fullwidth-code-point": "^3.0.0", - "strip-ansi": "^6.0.1" - } - }, - "strip-ansi": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", - "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", - "requires": { - "ansi-regex": "^5.0.1" - } - } - } - }, - "yargs-parser": { - "version": "21.1.1", - "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz", - "integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==" - }, - "yauzl": { - "version": "2.10.0", - "resolved": "https://registry.npmjs.org/yauzl/-/yauzl-2.10.0.tgz", - "integrity": "sha512-p4a9I6X6nu6IhoGmBqAcbJy1mlC4j27vEPZX9F4L4/vZT3Lyq1VkFHw/V/PUcB9Buo+DG3iHkT0x3Qya58zc3g==", - "requires": { - "buffer-crc32": "~0.2.3", - "fd-slicer": "~1.1.0" - } - } } } diff --git a/package.json b/package.json index 54bdee4..2ba2b7d 100644 --- a/package.json +++ b/package.json @@ -2,7 +2,7 @@ "name": "@dynamicweb/cli", "type": "module", "description": "CLI for interacting with Dynamicweb 10 solutions from the command line.", - "version": "2.0.0-preview.1", + "version": "2.0.0-preview.2", "main": "bin/index.js", "files": [ "bin/**" From 35314f03bc420307a10f3c0d1f283d45eaf89eb4 Mon Sep 17 00:00:00 2001 From: nicped Date: Mon, 4 May 2026 08:48:33 +0200 Subject: [PATCH 85/86] Fix --oauth not recognized on non-login commands and stale env read after interactive login --- bin/commands/login.js | 7 ++++--- bin/index.js | 4 ++++ 2 files changed, 8 insertions(+), 3 deletions(-) diff --git a/bin/commands/login.js b/bin/commands/login.js index bcd53a0..2c737f1 100644 --- a/bin/commands/login.js +++ b/bin/commands/login.js @@ -73,17 +73,18 @@ export async function setupUser(argv, env) { default: getConfig()?.current?.env, prompt: 'never' }, - username: { + username: { type: 'input' }, - password: { + password: { type: 'password' }, interactive: { default: true } }) - user = env.users[env.current.user]; + const currentEnv = getConfig()?.current?.env; + user = getConfig()?.env?.[currentEnv]?.users?.[getConfig()?.env?.[currentEnv]?.current?.user]; } return user; diff --git a/bin/index.js b/bin/index.js index 91711a5..7621dc8 100644 --- a/bin/index.js +++ b/bin/index.js @@ -43,6 +43,10 @@ yargs(hideBin(process.argv)) .option('apiKey', { description: 'Allows setting the apiKey for an environmentless execution of the CLI command' }) + .option('oauth', { + type: 'boolean', + description: 'Use OAuth client credentials authentication (shorthand for --auth oauth)' + }) .option('auth', { choices: ['user', 'oauth'], description: 'Overrides the authentication mode for the command' From 3852e9f4d5df21e2d9005e24cf9caf6c72be0790 Mon Sep 17 00:00:00 2001 From: nicped Date: Mon, 4 May 2026 09:01:38 +0200 Subject: [PATCH 86/86] Add support for --oauth option for OAuth client credentials authentication --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index 2ad01e4..f591de0 100644 --- a/README.md +++ b/README.md @@ -162,6 +162,7 @@ Most API-driven commands support these global options: - `--host`: use a host directly instead of the saved environment - `--protocol`: set the protocol used with `--host` and default to `https` - `--apiKey`: use an API key for environmentless execution +- `--oauth`: use OAuth client credentials authentication (shorthand for `--auth oauth`) - `--auth`: override authentication mode with `user` or `oauth` - `--clientId`: pass an OAuth client ID directly - `--clientSecret`: pass an OAuth client secret directly