From fff7299e3980a879c95ff13518c489c7bbcbb3fd Mon Sep 17 00:00:00 2001 From: Antoine Leclair Date: Sun, 2 Mar 2025 11:38:11 -0500 Subject: [PATCH 1/7] Add commands to add/remove nodes --- src/commands/init.ts | 45 +++++-- src/commands/nodes/add.ts | 160 ++++++++++++++++--------- src/commands/nodes/list.ts | 43 +++++++ src/commands/nodes/remove.ts | 35 ++++++ src/commands/registry/addon/install.ts | 133 ++++++++++++++++++++ src/commands/registry/addon/remove.ts | 29 +++++ src/commands/registry/addon/update.ts | 31 +++++ 7 files changed, 407 insertions(+), 69 deletions(-) create mode 100644 src/commands/nodes/list.ts create mode 100644 src/commands/nodes/remove.ts create mode 100644 src/commands/registry/addon/install.ts create mode 100644 src/commands/registry/addon/remove.ts create mode 100644 src/commands/registry/addon/update.ts diff --git a/src/commands/init.ts b/src/commands/init.ts index 87d2c04..b3ba1d4 100644 --- a/src/commands/init.ts +++ b/src/commands/init.ts @@ -191,24 +191,30 @@ async function uploadLocalImage({image, ssh, verbose}: {image: string; ssh: Node } } -async function installDockerIfNeeded({ +export async function installDockerIfNeeded({ dockerAlreadyInstalled, + dockerVersion, verbose, ssh, progressBar, }: { dockerAlreadyInstalled: boolean + dockerVersion?: string, verbose: boolean ssh: NodeSSH progressBar: SingleBar | undefined }): Promise { if (!dockerAlreadyInstalled) { if (verbose) { - process.stdout.write('Installing Docker\n') + if (dockerVersion === undefined) { + process.stdout.write('Installing Docker\n') + } else { + process.stdout.write(`Installing Docker ${dockerVersion}\n`) + } } try { - await installDocker({ssh, verbose, progressBar}) + await installDocker({ssh, dockerVersion, verbose, progressBar}) } catch (error) { if ((error as Error).toString().includes('Could not get lock')) { throw new Error(`Package manager already busy. Try again in a few minutes.`) @@ -247,7 +253,7 @@ async function getSshPrivateKeyPaths(): Promise { return privKeyPaths } -async function connectSsh({ +export async function connectSsh({ host, username, password, @@ -291,17 +297,19 @@ async function connectSsh({ throw new Error('Failed to connect with SSH') } -async function checkDockerInstalled(ssh: NodeSSH): Promise { +export async function checkDockerInstalled(ssh: NodeSSH): Promise { const {code} = await ssh.execCommand('command -v docker >/dev/null 2>&1') return code === 0 } async function installDocker({ ssh, + dockerVersion, verbose, progressBar, }: { ssh: NodeSSH + dockerVersion?: string verbose: boolean progressBar: SingleBar | undefined }): Promise { @@ -316,9 +324,26 @@ async function installDocker({ '$(. /etc/os-release && echo "$VERSION_CODENAME") stable" | ' + 'sudo tee /etc/apt/sources.list.d/docker.list > /dev/null', 'sudo apt-get update', - 'DEBIAN_FRONTEND=noninteractive sudo apt-get install -y docker-ce docker-ce-cli ' + - 'containerd.io docker-buildx-plugin docker-compose-plugin', ] + if (dockerVersion === undefined) { + commands.push( + 'DEBIAN_FRONTEND=noninteractive sudo apt-get install -y docker-ce docker-ce-cli ' + + 'containerd.io docker-buildx-plugin docker-compose-plugin', + ) + } else { + process.stdout.write( `DEBIAN_FRONTEND=noninteractive sudo apt-get install -y \ + docker-ce=$(apt-cache madison docker-ce | grep --fixed-strings ${dockerVersion} | head -1 | awk '{print $3}') \ + docker-ce-cli=$(apt-cache madison docker-ce-cli | grep --fixed-strings ${dockerVersion} | head -1 | awk '{print $3}') \ + containerd.io docker-buildx-plugin docker-compose-plugin\n`, +) + commands.push( + `DEBIAN_FRONTEND=noninteractive sudo apt-get install -y \ + docker-ce=$(apt-cache madison docker-ce | grep --fixed-strings ${dockerVersion} | head -1 | awk '{print $3}') \ + docker-ce-cli=$(apt-cache madison docker-ce-cli | grep --fixed-strings ${dockerVersion} | head -1 | awk '{print $3}') \ + containerd.io docker-buildx-plugin docker-compose-plugin`, + ) + } + for await (const command of commands) { await runSshCommand({ssh, command, verbose, progressBar}) } @@ -371,7 +396,7 @@ function extractApiKey(output: string): string { return match[1] } -async function runSshCommand({ +export async function runSshCommand({ ssh, command, stdin, @@ -422,7 +447,7 @@ async function runSshCommand({ return stdout } -async function userCanSudoWitoutPassword({ssh, verbose}: {ssh: NodeSSH; verbose: boolean}): Promise { +export async function userCanSudoWitoutPassword({ssh, verbose}: {ssh: NodeSSH; verbose: boolean}): Promise { try { await runSshCommand({ssh, command: 'sudo -n true', verbose, progressBar: undefined}) if (verbose) { @@ -439,7 +464,7 @@ async function userCanSudoWitoutPassword({ssh, verbose}: {ssh: NodeSSH; verbose: } } -async function setupRootSshAccess({ +export async function setupRootSshAccess({ ssh, verbose, password, diff --git a/src/commands/nodes/add.ts b/src/commands/nodes/add.ts index 7afedeb..6af7160 100644 --- a/src/commands/nodes/add.ts +++ b/src/commands/nodes/add.ts @@ -1,34 +1,37 @@ import {Args, Command, Flags} from '@oclif/core' - +import {NodeSSH} from 'node-ssh' +import inquirerPassword from '@inquirer/password' +import {SingleBar} from 'cli-progress' import {getDisco} from '../../config.js' import {request} from '../../auth-request.js' - -import * as fs from 'node:fs' -import * as os from 'node:os' -import * as path from 'node:path' -import {NodeSSH} from 'node-ssh' - -const GET_NODE_SCRIPT_URL = (version: string) => `https://downloads.letsdisco.dev/${version}/node` +import { checkDockerInstalled, connectSsh, installDockerIfNeeded, runSshCommand, setupRootSshAccess, userCanSudoWitoutPassword } from '../init.js' export default class NodesAdd extends Command { - static override args = { - sshString: Args.string({description: 'ssh user@IP to connect to new machine', required: true}), + static args = { + sshString: Args.string({required: true}), } - static override description = 'add a new server to your deployment' + static description = 'initializes a new server' - static override examples = ['<%= config.bin %> <%= command.id %> root@12.34.56.78'] + static examples = [ + '<%= config.bin %> <%= command.id %> root@disco.example.com', + '<%= config.bin %> <%= command.id %> root@disco.example.com --version 0.4.0', + ] - static override flags = { + static flags = { + verbose: Flags.boolean({default: false, description: 'show extra output'}), + 'identity-file': Flags.string({ + char: 'i', + description: 'SSH key to use for authentication', + }), disco: Flags.string({required: false}), - version: Flags.string({required: false, default: 'latest'}), } public async run(): Promise { const {args, flags} = await this.parse(NodesAdd) - const discoConfig = getDisco(flags.disco || null) - // eslint-disable-next-line new-cap - const nodeScriptUrl = GET_NODE_SCRIPT_URL(flags.version) + const {verbose, 'identity-file': identityFile, disco} = flags + + const discoConfig = getDisco(disco || null) const url = `https://${discoConfig.host}/api/disco/swarm/join-token` const res = await request({ @@ -36,65 +39,104 @@ export default class NodesAdd extends Command { url, discoConfig, }) - const data = (await res.json()) as any - - const token = data.joinToken - const {ip} = data - const command = `curl ${nodeScriptUrl} | sudo IP=${ip} TOKEN=${token} sh` + const {joinToken, ip: leaderIp, dockerVersion, registryHost} = (await res.json()) as { + joinToken: string + ip: string + dockerVersion: string + registryHost: null | string + } - // TODO centralize this code which is identical to code in init.ts + if (registryHost === null) { + this.log("Image registry not configured") + return; + } - const [username, host] = args.sshString.split('@') + const [argUsername, host] = args.sshString.split('@') - const sshKeyPaths = [ - path.join(os.homedir(), '.ssh', 'id_ed25519'), - path.join(os.homedir(), '.ssh', 'id_rsa'), - ].filter((p) => { - try { - return fs.statSync(p).isFile() - } catch { - return false - } - }) + let username = argUsername - if (sshKeyPaths.length === 0) { - this.error('could not find an SSH key in ~/.ssh') + let ssh + let password + try { + ;({ssh, password} = await connectSsh({host, username, identityFile})) + } catch { + this.error('could not connect to SSH') } - const ssh = new NodeSSH() + if (username !== 'root') { + const canSudoWithoutPassword = await userCanSudoWitoutPassword({ssh, verbose}) + // use password if provided, or ask for one if needed + const passwordToUse = + password === undefined + ? canSudoWithoutPassword + ? undefined + : await inquirerPassword({message: `${username}@${host}'s password:`}) + : password + if (verbose) { + if (passwordToUse === undefined) { + process.stdout.write('Will not use password\n') + } else { + process.stdout.write('Will use password\n') + } + } - let connected = false - for await (const sshKeyPath of sshKeyPaths) { + await setupRootSshAccess({ssh, password: passwordToUse, verbose}) + username = 'root' try { - await ssh.connect({ - host, - privateKeyPath: sshKeyPath, - username, - }) - connected = true - break + ;({ssh, password} = await connectSsh({host, username, identityFile})) } catch { - // skip error + this.error('could not connect to SSH as root') } } - if (!connected) { - this.error('could not connect to server') + const dockerAlreadyInstalled = await checkDockerInstalled(ssh) + let progressBar + if (!verbose) { + const dockerInstallOutputCount = 309 + const count = dockerAlreadyInstalled ? 0 : dockerInstallOutputCount; + progressBar = new SingleBar({format: '[{bar}] {percentage}%', clearOnComplete: true}) + progressBar.start(count, 0) } - this.log('connected') + await installDockerIfNeeded({dockerAlreadyInstalled, verbose, ssh, dockerVersion, progressBar}) - // do something with stderr output? - const {code} = await ssh.execCommand(command, { - onStdout(chunk) { - const str = chunk.toString('utf8') - process.stdout.write(str) - }, - }) - if (code !== 0) { - this.error('failed to run ssh script') + + if (verbose) { + this.log('Joining Swarm') } + await joinSwarm({ + ssh, + joinToken, + leaderIp, + verbose, + progressBar, + }) + ssh.dispose() + if (progressBar !== undefined) { + progressBar.stop() + } + + this.log('Done') } } + + +async function joinSwarm({ + ssh, + joinToken, + leaderIp, + verbose, + progressBar, +}: { + ssh: NodeSSH + joinToken: string, + leaderIp: string, + verbose: boolean + progressBar: SingleBar | undefined +}): Promise { + const command = `docker swarm join --token ${joinToken} ${leaderIp}:2377`; + await runSshCommand({ssh, command, verbose, progressBar}) +} + diff --git a/src/commands/nodes/list.ts b/src/commands/nodes/list.ts new file mode 100644 index 0000000..dfcea42 --- /dev/null +++ b/src/commands/nodes/list.ts @@ -0,0 +1,43 @@ +import {Command, Flags} from '@oclif/core' +import {getDisco} from '../../config.js' +import {request} from '../../auth-request.js' + +export default class NodesList extends Command { + static description = 'initializes a new server' + + static examples = [ + '<%= config.bin %> <%= command.id %> root@disco.example.com', + '<%= config.bin %> <%= command.id %> root@disco.example.com --version 0.4.0', + ] + + static flags = { + disco: Flags.string({required: false}), + } + + public async run(): Promise { + const {flags} = await this.parse(NodesList) + const {disco} = flags + + const discoConfig = getDisco(disco || null) + + const url = `https://${discoConfig.host}/api/disco/swarm/nodes` + const res = await request({ + method: 'GET', + url, + discoConfig, + }) + const {nodes} = (await res.json()) as { + nodes: { + created: string, + name: string, + state: string, + address: string, + isLeader: boolean, + }[] + } + + for (const node of nodes) { + this.log(`${node.name}${node.isLeader ? ' (main)' : ''}, state: ${node.state}`); + } + } +} diff --git a/src/commands/nodes/remove.ts b/src/commands/nodes/remove.ts new file mode 100644 index 0000000..ca221a2 --- /dev/null +++ b/src/commands/nodes/remove.ts @@ -0,0 +1,35 @@ +import {Args, Command, Flags} from '@oclif/core' +import {getDisco} from '../../config.js' +import {request} from '../../auth-request.js' + +export default class NodesList extends Command { + static description = 'initializes a new server' + + static examples = [ + '<%= config.bin %> <%= command.id %> root@disco.example.com', + '<%= config.bin %> <%= command.id %> root@disco.example.com --version 0.4.0', + ] + + static flags = { + disco: Flags.string({required: false}), + } + + static args = { + name: Args.string({required: true}), + } + + public async run(): Promise { + const {flags, args} = await this.parse(NodesList) + const {disco} = flags + const {name} = args; + + const discoConfig = getDisco(disco || null) + const url = `https://${discoConfig.host}/api/disco/swarm/nodes/${name}` + await request({ + method: 'DELETE', + url, + discoConfig, + }) + this.log('Node removed') + } +} diff --git a/src/commands/registry/addon/install.ts b/src/commands/registry/addon/install.ts new file mode 100644 index 0000000..5ce5f4e --- /dev/null +++ b/src/commands/registry/addon/install.ts @@ -0,0 +1,133 @@ +import {Command, Flags} from '@oclif/core' +import {DiscoConfig, getDisco} from '../../../config.js' +import {request, readEventSource} from '../../../auth-request.js' + +const addonProjectName = 'addon-registry'; +const addonRepo = 'antoineleclair/disco-addon-docker-registry'; // TODO update +const branch = 'rework'; // TODO update + +export default class RegistryAddonInstall extends Command { + static description = 'install Registry addon' + + static examples = ['<%= config.bin %> <%= command.id %>'] + + static flags = { + domain: Flags.string({ + required: true, + description: 'domain name where the registry will be served, e.g. registry.example.com', + }), + disco: Flags.string({required: false}), + } + + public async run(): Promise { + const {flags} = await this.parse(RegistryAddonInstall) + const discoConfig = getDisco(flags.disco || null) + this.log('addProject') + await addProject({discoConfig, domain: flags.domain}) + this.log('setProjectEnvVariables') + await setProjectEnvVariables({discoConfig, domain: flags.domain}) + this.log('addUser') + const {username, password} = await addUser({discoConfig}) + this.log('setupRegistry') + await setupRegistry({discoConfig, username, password, domain: flags.domain}) + this.log('done') + } +} + +async function addProject({discoConfig, domain}: {discoConfig: DiscoConfig, domain: string}) { + const url = `https://${discoConfig.host}/api/projects` + const body = { + name: addonProjectName, + githubRepo: addonRepo, + branch, + domain + } + + const res = await request({method: 'POST', url, discoConfig, body, expectedStatuses: [201]}) + const {deployment} = (await res.json()) as {deployment: {number: number}} + if (deployment) { + const url = `https://${discoConfig.host}/api/projects/${addonProjectName}/deployments/${deployment.number}/output` + + await readEventSource(url, discoConfig, { + onMessage(event: MessageEvent) { + process.stdout.write(JSON.parse(event.data).text) + }, + }) + } +} + +async function setProjectEnvVariables({discoConfig, domain}: {discoConfig: DiscoConfig, domain: string}) { + const url = `https://${discoConfig.host}/api/projects/${addonProjectName}/env` + const body = { + envVariables: [ + { + "name": "REGISTRY_HTTP_HOST", + "value": `https://${domain}`, + }, + { + "name": "REGISTRY_AUTH", + "value": "htpasswd", + }, + { + "name": "REGISTRY_AUTH_HTPASSWD_REALM", + "value": "Registry Realm", + }, + { + "name": "REGISTRY_AUTH_HTPASSWD_PATH", + "value": "/auth/htpasswd", + }, + ], + } + + const res = await request({method: 'POST', url, discoConfig, body}) + const data = (await res.json()) as any + const deploymentUrl = `https://${discoConfig.host}/api/projects/${addonProjectName}/deployments/${data.deployment.number}/output` + await readEventSource(deploymentUrl, discoConfig, { + onMessage(event: MessageEvent) { + const output = JSON.parse(event.data) + process.stdout.write(output.text) + }, + }) +} + +async function addUser({discoConfig}: {discoConfig: DiscoConfig}): Promise<{username: string; password: string}> { + const url = `https://${discoConfig.host}/api/projects/${addonProjectName}/cgi/endpoints/users` + const res = await request({ + method: 'POST', + url, + discoConfig, + expectedStatuses: [200], + }) + const {user: {username, password}} = (await res.json()) as {user:{ + username: string + password: string + }} + return {username, password} +} + +async function setupRegistry({ + discoConfig, + username, + password, + domain, +}: { + discoConfig: DiscoConfig + username: string + password: string + domain: string +}) { + const reqBody = { + host: domain, + authType: 'basic', + username, + password, + } + const url = `https://${discoConfig.host}/api/disco/registry` + await request({ + method: 'POST', + body: reqBody, + url, + discoConfig, + expectedStatuses: [200], + }) +} diff --git a/src/commands/registry/addon/remove.ts b/src/commands/registry/addon/remove.ts new file mode 100644 index 0000000..43b87b8 --- /dev/null +++ b/src/commands/registry/addon/remove.ts @@ -0,0 +1,29 @@ +import {Command, Flags} from '@oclif/core' +import {getDisco} from '../../../config.js' +import {request} from '../../../auth-request.js' + +const addonProjectName = 'addon-registry'; + +export default class RegistryAddonRemove extends Command { + static description = 'remove Registry addon' + + static examples = ['<%= config.bin %> <%= command.id %>'] + + static flags = { + disco: Flags.string({required: false}), + } + + public async run(): Promise { + const {flags} = await this.parse(RegistryAddonRemove) + const discoConfig = getDisco(flags.disco || null) + { + const url = `https://${discoConfig.host}/api/disco/registry` + await request({method: 'DELETE', url, discoConfig, expectedStatuses: [200]}) + } + + { + const url = `https://${discoConfig.host}/api/projects/${addonProjectName}` + await request({method: 'DELETE', url, discoConfig, expectedStatuses: [200, 204]}) + } + } +} diff --git a/src/commands/registry/addon/update.ts b/src/commands/registry/addon/update.ts new file mode 100644 index 0000000..98f0056 --- /dev/null +++ b/src/commands/registry/addon/update.ts @@ -0,0 +1,31 @@ +import {Command, Flags} from '@oclif/core' +import {getDisco} from '../../../config.js' +import {request, readEventSource} from '../../../auth-request.js' + +const addonProjectName = 'addon-registry'; + +export default class RegistryAddonUpdate extends Command { + static description = 'update Registry addon' + + static examples = ['<%= config.bin %> <%= command.id %>'] + + static flags = { + disco: Flags.string({required: false}), + } + + public async run(): Promise { + const {flags} = await this.parse(RegistryAddonUpdate) + const discoConfig = getDisco(flags.disco || null) + const url = `https://${discoConfig.host}/api/projects/${addonProjectName}/deployments` + const res = await request({method: 'POST', url, body: {}, discoConfig, expectedStatuses: [201]}) + const data = (await res.json()) as any + + const deploymentUrl = `https://${discoConfig.host}/api/projects/${addonProjectName}/deployments/${data.deployment.number}/output` + await readEventSource(deploymentUrl, discoConfig, { + onMessage(event: MessageEvent) { + const message = JSON.parse(event.data) + process.stdout.write(message.text) + }, + }) + } +} From bd0094992b747bb5061aff940247f186a916e07a Mon Sep 17 00:00:00 2001 From: Antoine Leclair Date: Sun, 2 Mar 2025 11:56:24 -0500 Subject: [PATCH 2/7] Better logging --- src/commands/registry/addon/install.ts | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/src/commands/registry/addon/install.ts b/src/commands/registry/addon/install.ts index 5ce5f4e..b682fc2 100644 --- a/src/commands/registry/addon/install.ts +++ b/src/commands/registry/addon/install.ts @@ -3,8 +3,8 @@ import {DiscoConfig, getDisco} from '../../../config.js' import {request, readEventSource} from '../../../auth-request.js' const addonProjectName = 'addon-registry'; -const addonRepo = 'antoineleclair/disco-addon-docker-registry'; // TODO update -const branch = 'rework'; // TODO update +const addonRepo = 'letsdiscodev/disco-addon-docker-registry'; +const branch = 'main'; export default class RegistryAddonInstall extends Command { static description = 'install Registry addon' @@ -22,15 +22,15 @@ export default class RegistryAddonInstall extends Command { public async run(): Promise { const {flags} = await this.parse(RegistryAddonInstall) const discoConfig = getDisco(flags.disco || null) - this.log('addProject') + this.log(`Adding ${addonProjectName} project`) await addProject({discoConfig, domain: flags.domain}) - this.log('setProjectEnvVariables') + this.log(`Setting env variables for ${addonProjectName}`) await setProjectEnvVariables({discoConfig, domain: flags.domain}) - this.log('addUser') + this.log('Adding user to registry') const {username, password} = await addUser({discoConfig}) - this.log('setupRegistry') + this.log('Setting up Disco to use Registry') await setupRegistry({discoConfig, username, password, domain: flags.domain}) - this.log('done') + this.log('Done') } } From f34503665b7fcf962a1e20e69704f4be6b96857f Mon Sep 17 00:00:00 2001 From: Antoine Leclair Date: Sun, 2 Mar 2025 11:58:11 -0500 Subject: [PATCH 3/7] Remove some loggin --- src/commands/init.ts | 5 ----- 1 file changed, 5 deletions(-) diff --git a/src/commands/init.ts b/src/commands/init.ts index b3ba1d4..545b591 100644 --- a/src/commands/init.ts +++ b/src/commands/init.ts @@ -331,11 +331,6 @@ async function installDocker({ 'containerd.io docker-buildx-plugin docker-compose-plugin', ) } else { - process.stdout.write( `DEBIAN_FRONTEND=noninteractive sudo apt-get install -y \ - docker-ce=$(apt-cache madison docker-ce | grep --fixed-strings ${dockerVersion} | head -1 | awk '{print $3}') \ - docker-ce-cli=$(apt-cache madison docker-ce-cli | grep --fixed-strings ${dockerVersion} | head -1 | awk '{print $3}') \ - containerd.io docker-buildx-plugin docker-compose-plugin\n`, -) commands.push( `DEBIAN_FRONTEND=noninteractive sudo apt-get install -y \ docker-ce=$(apt-cache madison docker-ce | grep --fixed-strings ${dockerVersion} | head -1 | awk '{print $3}') \ From 35e0261f7c9824a2111c87e251438dfe901b96e8 Mon Sep 17 00:00:00 2001 From: Antoine Leclair Date: Sun, 2 Mar 2025 12:08:15 -0500 Subject: [PATCH 4/7] Docs when no registry --- src/commands/nodes/add.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/commands/nodes/add.ts b/src/commands/nodes/add.ts index 6af7160..eff7e1d 100644 --- a/src/commands/nodes/add.ts +++ b/src/commands/nodes/add.ts @@ -48,6 +48,8 @@ export default class NodesAdd extends Command { if (registryHost === null) { this.log("Image registry not configured") + this.log("You can install the addon by using the command disco registry:addon:install. For example:") + this.log(`disco registry:addon:install --domain registry.example.com --disco ${discoConfig.name}`) return; } From 1dd7bf943c5233e03d680a2aa64b824c4c414059 Mon Sep 17 00:00:00 2001 From: Antoine Leclair Date: Sun, 2 Mar 2025 12:13:20 -0500 Subject: [PATCH 5/7] Better docs --- src/commands/nodes/list.ts | 7 +++---- src/commands/nodes/remove.ts | 9 ++++----- 2 files changed, 7 insertions(+), 9 deletions(-) diff --git a/src/commands/nodes/list.ts b/src/commands/nodes/list.ts index dfcea42..cc744fc 100644 --- a/src/commands/nodes/list.ts +++ b/src/commands/nodes/list.ts @@ -3,11 +3,10 @@ import {getDisco} from '../../config.js' import {request} from '../../auth-request.js' export default class NodesList extends Command { - static description = 'initializes a new server' + static description = 'show node list' static examples = [ - '<%= config.bin %> <%= command.id %> root@disco.example.com', - '<%= config.bin %> <%= command.id %> root@disco.example.com --version 0.4.0', + '<%= config.bin %> <%= command.id %>', ] static flags = { @@ -37,7 +36,7 @@ export default class NodesList extends Command { } for (const node of nodes) { - this.log(`${node.name}${node.isLeader ? ' (main)' : ''}, state: ${node.state}`); + this.log(`${node.name}${node.isLeader ? ' (main)' : ''}`); } } } diff --git a/src/commands/nodes/remove.ts b/src/commands/nodes/remove.ts index ca221a2..e5ca6a5 100644 --- a/src/commands/nodes/remove.ts +++ b/src/commands/nodes/remove.ts @@ -2,12 +2,11 @@ import {Args, Command, Flags} from '@oclif/core' import {getDisco} from '../../config.js' import {request} from '../../auth-request.js' -export default class NodesList extends Command { - static description = 'initializes a new server' +export default class NodesRemove extends Command { + static description = 'remove node' static examples = [ - '<%= config.bin %> <%= command.id %> root@disco.example.com', - '<%= config.bin %> <%= command.id %> root@disco.example.com --version 0.4.0', + '<%= config.bin %> <%= command.id %> brilliant-fleet', ] static flags = { @@ -19,7 +18,7 @@ export default class NodesList extends Command { } public async run(): Promise { - const {flags, args} = await this.parse(NodesList) + const {flags, args} = await this.parse(NodesRemove) const {disco} = flags const {name} = args; From 2d09ef7e5fd6d42d7e38a8568946dee5949d3b41 Mon Sep 17 00:00:00 2001 From: Antoine Leclair Date: Sun, 2 Mar 2025 13:46:11 -0500 Subject: [PATCH 6/7] Fix import? --- src/commands/init.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/commands/init.ts b/src/commands/init.ts index 545b591..8c80c2d 100644 --- a/src/commands/init.ts +++ b/src/commands/init.ts @@ -382,7 +382,7 @@ async function initDisco({ return apiKey } -function extractApiKey(output: string): string { +export function extractApiKey(output: string): string { const match = output.match(/Created API key: ([a-z0-9]{32})/) if (!match) { throw new Error('could not extract API key') From 05047c7c7b17933255f5ca138de08451da25ae82 Mon Sep 17 00:00:00 2001 From: Antoine Leclair Date: Sun, 2 Mar 2025 13:48:00 -0500 Subject: [PATCH 7/7] Fix import? --- test/commands/init.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/commands/init.test.ts b/test/commands/init.test.ts index d99fdd3..eff0516 100644 --- a/test/commands/init.test.ts +++ b/test/commands/init.test.ts @@ -1,6 +1,6 @@ import {expect} from '@oclif/test' -import {extractApiKey} from '../../src/commands/init' +import {extractApiKey} from '../../src/commands/init.js' describe('init utils', () => { describe('extractApiKey', () => {