import { NodeSSH } from 'node-ssh'; import { ParsedArgs, runLocally } from '../mod.ts'; const commands = { DOKKU_SET_ADMIN_PUBKEY(identity: string) { return `bash -c "echo \\"${identity}\\" | dokku ssh-keys:add admin"`; }, DOKKU_INSTALL_PLUGIN(plugin: string, opts: { source: 'dokku' | 'custom' } = { source: 'dokku' }) { if (opts.source === 'dokku') { return `dokku plugin:install https://github.com/dokku/dokku-${plugin}.git`; } return `dokku plugin:install ${plugin}`; }, DOKKU_CREATE_POSTGRES_SERVICE(name: string) { return `dokku postgres:create ${name}`; }, DOKKU_SET_GLOBAL_DOMAIN(domain: string) { return `dokku domains:set-global "${domain}"`; }, DOKKU_LETSENCRYPT_SETUP_EMAIL(email: string) { return `dokku letsencrypt:set --global email "${email}"`; }, DOKKU_LETSENCRYPT_CRON() { return 'dokku letsencrypt:cron-job --add'; }, DOKKU_CREATE_APP(name: string) { return `dokku apps:create ${name}`; }, DOKKU_LINK_PG(app: string, service: string) { return `dokku postgres:link ${service} ${app}`; }, DOKKU_SET_CONFIG_VAR(app: string, key: string, value: string, restart = false) { return `dokku config:set ${restart ? '' : '--no-restart'} ${app} ${key}="${value}"`; }, DOKKU_LOAD_GIT(app: string, repo: string, pathspec?: string) { return `dokku git:sync ${app} ${repo} ${pathspec || ''}`; }, DOKKU_LE_ENABLE(app: string) { return `dokku letsencrypt:enable ${app}`; }, DOKKU_REBUILD(app: string) { return `dokku ps:rebuild ${app}`; }, }; class TribesClient { ssh: NodeSSH; constructor(ssh: NodeSSH) { this.ssh = ssh; } /** * Execute a command on the Tribes remote, storing the output for use. Throws an error with stderr if the exit code was nonzero. * @param name The name of the command to execute. * @param args The args for the command. * @returns The standard output of the command. */ async do(name: K, ...args: Parameters) { // @ts-ignore this is okay because typescript enforces the correct args at the callsite const cmd = commands[name](...args); console.log(`=> ${cmd}`); const res = await this.ssh.execCommand(cmd); if (res.code) throw new Error(`Error executing ${name}: ${res.stderr}`); return res.stdout; } /** * Execute a command on the remote, printing the output after execution. * @param name The name of the command to execute. * @param args The args for the command. * @returns The standard output of the command. */ async loud(name: K, ...args: Parameters) { const stdout = await this.do(name, ...args); console.log(stdout); } async disconnect() { this.ssh.dispose(); } } interface HandlerOptions { args: ParsedArgs; arg: (name: string) => string; tribes: TribesClient; domain: string; } export const makeSshAction = (handler: (opts: HandlerOptions) => Promise | void) => { return async (args: ParsedArgs) => { const arg = (name: string) => args[name] as string; const domain = args._[0] as string; const tribes = await connect(domain, arg('identity-file')); await handler({ args, arg, tribes, domain }); console.log('Done.'); tribes.disconnect(); }; }; const ssh = new NodeSSH(); const connect = async (host: string, identityFile: string) => { if (ssh.isConnected() && ssh.connection?.host === host) return new TribesClient(ssh); await ssh.connect({ host, username: 'root', privateKeyPath: identityFile, }); return new TribesClient(ssh); }; export const privkeyToPubkey = async (path: string) => { const result = await runLocally(`ssh-keygen -y -f ${path}`); const decoder = new TextDecoder(); if (result.code) { throw new Error( `tribes-cli: error generating public key from private key ${path}:` + ` ${decoder.decode(result.stderr)}`, ); } return decoder.decode(result.stdout); };