d3ployer 0.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 100k
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,159 @@
1
+ # d3ployer
2
+
3
+ A TypeScript-based SSH deployment CLI tool. Run tasks and scenarios against remote servers over SSH, with rsync for file transfer.
4
+
5
+ ## Install
6
+
7
+ ```bash
8
+ npm install d3ployer
9
+ ```
10
+
11
+ This exposes the `dpl` CLI command.
12
+
13
+ ## Quick start
14
+
15
+ Create a `deployer.config.ts` in your project root:
16
+
17
+ ```ts
18
+ import { defineConfig } from 'd3ployer';
19
+
20
+ export default defineConfig({
21
+ servers: {
22
+ prod: {
23
+ host: '192.168.1.10',
24
+ deployPath: '/opt/myapp',
25
+ },
26
+ },
27
+ files: {
28
+ basePath: './dist',
29
+ exclude: ['node_modules', '.git'],
30
+ },
31
+ symlinks: [
32
+ { path: 'config.json', target: '/etc/myapp/config.json' },
33
+ ],
34
+ tasks: {
35
+ restart: async (ctx) => {
36
+ await ctx.runRemote('systemctl restart myapp');
37
+ },
38
+ },
39
+ scenarios: {
40
+ deploy: ['upload', 'symlinks', 'depInstall', 'restart'],
41
+ },
42
+ });
43
+ ```
44
+
45
+ Then deploy:
46
+
47
+ ```bash
48
+ dpl deploy # run "deploy" scenario on all servers
49
+ dpl deploy prod # run on specific server(s)
50
+ dpl upload # run a single task
51
+ dpl list # list available scenarios, tasks, and servers
52
+ ```
53
+
54
+ ## CLI
55
+
56
+ ```
57
+ dpl <name> [servers...] Run a scenario or task
58
+ dpl list List scenarios, tasks, and servers
59
+
60
+ Options:
61
+ -c, --config <path> Path to deployer.config.ts
62
+ ```
63
+
64
+ If `<name>` matches a scenario, it runs all tasks in that scenario sequentially. Otherwise it runs the matching task directly.
65
+
66
+ ## Config
67
+
68
+ ### `servers`
69
+
70
+ Define target servers. Only `host` and `deployPath` are required.
71
+
72
+ | Field | Default | Description |
73
+ | --------------- | -------------------- | ------------------------------------ |
74
+ | `host` | (required) | Server hostname or IP |
75
+ | `deployPath` | (required) | Remote path to deploy to |
76
+ | `port` | `22` | SSH port |
77
+ | `username` | Current OS user | SSH username |
78
+ | `authMethod` | `'agent'` | `'agent'`, `'key'`, or `'password'` |
79
+ | `privateKey` | - | Path to private key (for `'key'`) |
80
+ | `password` | - | SSH password (for `'password'`) |
81
+ | `agent` | `SSH_AUTH_SOCK` | SSH agent socket path |
82
+ | `packageManager`| - | Override package manager per server |
83
+ | `initCmd` | - | Command to run on connect |
84
+
85
+ ### `files`
86
+
87
+ Configure rsync file upload.
88
+
89
+ ```ts
90
+ files: {
91
+ basePath: './dist', // local directory to sync (default: '.')
92
+ include: ['src/**'], // rsync include patterns
93
+ exclude: ['node_modules'],// rsync exclude patterns
94
+ }
95
+ ```
96
+
97
+ ### `symlinks`
98
+
99
+ Create symlinks on the remote server.
100
+
101
+ ```ts
102
+ symlinks: [
103
+ { path: 'config.json', target: '/etc/myapp/config.json' },
104
+ ]
105
+ ```
106
+
107
+ Relative paths are resolved against `deployPath`.
108
+
109
+ ### `tasks`
110
+
111
+ Custom task functions receive a `TaskContext` and `Placeholders`:
112
+
113
+ ```ts
114
+ tasks: {
115
+ migrate: async (ctx, ph) => {
116
+ await ctx.runRemote(`cd ${ph.deployPath} && npm run migrate`);
117
+ },
118
+ }
119
+ ```
120
+
121
+ **TaskContext** provides:
122
+ - `runRemote(cmd)` - execute a command on the remote server
123
+ - `runLocal(cmd)` - execute a command locally
124
+ - `server` - current server config
125
+ - `ssh` - SSH2Promise connection
126
+ - `config` - full deployer config
127
+
128
+ **Placeholders** provide:
129
+ - `serverName` - name of the current server
130
+ - `deployPath` - remote deploy path
131
+ - `timestamp` - ISO timestamp (safe for filenames)
132
+
133
+ ### `scenarios`
134
+
135
+ Named sequences of tasks:
136
+
137
+ ```ts
138
+ scenarios: {
139
+ deploy: ['upload', 'symlinks', 'depInstall', 'restart'],
140
+ }
141
+ ```
142
+
143
+ ## Built-in tasks
144
+
145
+ | Task | Description |
146
+ | ------------ | ---------------------------------------------- |
147
+ | `upload` | Rsync files to the remote server |
148
+ | `symlinks` | Create configured symlinks on the remote server |
149
+ | `depInstall` | Run package manager install on the remote server|
150
+
151
+ ## Requirements
152
+
153
+ - Node.js (ESM)
154
+ - `rsync` installed locally (for the `upload` task)
155
+ - SSH access to target servers
156
+
157
+ ## License
158
+
159
+ MIT
package/dist/bin.d.ts ADDED
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env node
2
+ import './cli.js';
package/dist/bin.js ADDED
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env node
2
+ import './cli.js';
package/dist/cli.d.ts ADDED
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env node
2
+ export {};
package/dist/cli.js ADDED
@@ -0,0 +1,67 @@
1
+ #!/usr/bin/env node
2
+ import chalk from 'chalk';
3
+ import { Command } from 'commander';
4
+ import { loadConfig } from './configLoader.js';
5
+ import { runScenario, runTask } from './runner.js';
6
+ const program = new Command()
7
+ .name('deployer')
8
+ .description('TypeScript deployment tool')
9
+ .option('-c, --config <path>', 'path to deployer.config.ts');
10
+ program
11
+ .argument('<name>', 'scenario or task name')
12
+ .argument('[servers...]', 'target server(s)')
13
+ .action(async (name, servers) => {
14
+ try {
15
+ const config = await loadConfig(program.opts().config);
16
+ const serverList = servers.length > 0 ? servers : undefined;
17
+ if (config.scenarios?.[name]) {
18
+ await runScenario(config, name, serverList);
19
+ }
20
+ else {
21
+ await runTask(config, name, serverList);
22
+ }
23
+ }
24
+ catch (err) {
25
+ console.error(err.message);
26
+ process.exit(1);
27
+ }
28
+ });
29
+ program
30
+ .command('list')
31
+ .description('list available scenarios, tasks and servers')
32
+ .action(async () => {
33
+ try {
34
+ const config = await loadConfig(program.opts().config);
35
+ console.log(chalk.bold('\nScenarios:'));
36
+ const scenarios = config.scenarios ?? {};
37
+ const scenarioKeys = Object.keys(scenarios);
38
+ if (scenarioKeys.length) {
39
+ for (const name of scenarioKeys) {
40
+ console.log(` ${chalk.cyan(name)} → [${scenarios[name].join(', ')}]`);
41
+ }
42
+ }
43
+ else {
44
+ console.log(' (none)');
45
+ }
46
+ console.log(chalk.bold('\nTasks:'));
47
+ const taskKeys = Object.keys(config.tasks ?? {});
48
+ if (taskKeys.length) {
49
+ for (const name of taskKeys) {
50
+ console.log(` ${chalk.cyan(name)}`);
51
+ }
52
+ }
53
+ else {
54
+ console.log(' (none)');
55
+ }
56
+ console.log(chalk.bold('\nServers:'));
57
+ for (const [name, s] of Object.entries(config.servers)) {
58
+ console.log(` ${chalk.cyan(name)} → ${s.username}@${s.host}:${s.port ?? 22} (${s.deployPath})`);
59
+ }
60
+ console.log();
61
+ }
62
+ catch (err) {
63
+ console.error(err.message);
64
+ process.exit(1);
65
+ }
66
+ });
67
+ program.parse();
@@ -0,0 +1,2 @@
1
+ import type { DeployerConfig, DeployerConfigInput } from './def.js';
2
+ export declare function defineConfig(input: DeployerConfigInput): DeployerConfig;
package/dist/config.js ADDED
@@ -0,0 +1,26 @@
1
+ import { defaultsDeep } from 'lodash-es';
2
+ import os from 'node:os';
3
+ import { defaultTasks } from './defaultTasks.js';
4
+ const SERVER_DEFAULTS = {
5
+ port: 22,
6
+ username: os.userInfo().username,
7
+ authMethod: 'agent',
8
+ };
9
+ function resolveServer(input) {
10
+ return defaultsDeep({}, input, SERVER_DEFAULTS);
11
+ }
12
+ export function defineConfig(input) {
13
+ const servers = {};
14
+ for (const [name, serverInput] of Object.entries(input.servers)) {
15
+ servers[name] = resolveServer(serverInput);
16
+ }
17
+ return {
18
+ ...input,
19
+ rootDir: '',
20
+ servers,
21
+ tasks: {
22
+ ...defaultTasks,
23
+ ...input.tasks,
24
+ },
25
+ };
26
+ }
@@ -0,0 +1,3 @@
1
+ import type { DeployerConfig } from './def.js';
2
+ export declare function findConfigFile(startDir?: string): string;
3
+ export declare function loadConfig(configPath?: string): Promise<DeployerConfig>;
@@ -0,0 +1,33 @@
1
+ import fs from 'node:fs';
2
+ import path from 'node:path';
3
+ import { defineConfig } from './config.js';
4
+ import { Exception } from './utils/Exception.js';
5
+ const CONFIG_FILENAME = 'deployer.config.ts';
6
+ export function findConfigFile(startDir = process.cwd()) {
7
+ let dir = path.resolve(startDir);
8
+ let parent = dir;
9
+ do {
10
+ dir = parent;
11
+ const candidate = path.join(dir, CONFIG_FILENAME);
12
+ if (fs.existsSync(candidate)) {
13
+ return candidate;
14
+ }
15
+ parent = path.dirname(dir);
16
+ } while (parent !== dir);
17
+ throw new Exception(`Could not find ${CONFIG_FILENAME} in ${startDir} or any parent directory`, 1774741892462);
18
+ }
19
+ export async function loadConfig(configPath) {
20
+ const resolvedPath = configPath ?? findConfigFile();
21
+ const absolutePath = path.resolve(resolvedPath);
22
+ if (!fs.existsSync(absolutePath)) {
23
+ throw new Exception(`Config file not found: ${absolutePath}`, 1774741902017);
24
+ }
25
+ const module = await import(absolutePath);
26
+ const raw = module.default ?? module;
27
+ const config = defineConfig(raw);
28
+ config.rootDir = path.dirname(absolutePath);
29
+ if (!config.servers || Object.keys(config.servers).length === 0) {
30
+ throw new Exception('Config must define at least one server', 1774741913430);
31
+ }
32
+ return config;
33
+ }
@@ -0,0 +1,3 @@
1
+ import SSH2Promise from 'ssh2-promise';
2
+ import type { ServerConfig } from './def.js';
3
+ export declare function createSSHConnection(server: ServerConfig): SSH2Promise;
@@ -0,0 +1,30 @@
1
+ import fs from 'node:fs';
2
+ import SSH2Promise from 'ssh2-promise';
3
+ import { Exception } from './utils/Exception.js';
4
+ export function createSSHConnection(server) {
5
+ const sshConfig = {
6
+ host: server.host,
7
+ port: server.port ?? 22,
8
+ username: server.username,
9
+ reconnect: false,
10
+ };
11
+ switch (server.authMethod ?? 'agent') {
12
+ case 'key':
13
+ if (!server.privateKey) {
14
+ throw new Exception(`Server "${server.host}": privateKey is required for key auth`, 1774741923779);
15
+ }
16
+ sshConfig.privateKey = fs.readFileSync(server.privateKey);
17
+ break;
18
+ case 'password':
19
+ if (!server.password) {
20
+ throw new Exception(`Server "${server.host}": password is required for password auth`, 1774741926213);
21
+ }
22
+ sshConfig.password = server.password;
23
+ break;
24
+ case 'agent':
25
+ sshConfig.agent = server.agent ?? process.env.SSH_AUTH_SOCK;
26
+ break;
27
+ }
28
+ console.log(`Connecting to ${server.username}@${server.host}:${sshConfig.port}`);
29
+ return new SSH2Promise(sshConfig);
30
+ }
package/dist/def.d.ts ADDED
@@ -0,0 +1,48 @@
1
+ import type SSH2Promise from 'ssh2-promise';
2
+ export type AuthMethod = 'key' | 'password' | 'agent';
3
+ export interface ServerConfig {
4
+ host: string;
5
+ port: number;
6
+ username: string;
7
+ authMethod: AuthMethod;
8
+ privateKey?: string;
9
+ password?: string;
10
+ agent?: string;
11
+ deployPath: string;
12
+ }
13
+ export type ServerConfigInput = Partial<ServerConfig> & Pick<ServerConfig, 'host' | 'deployPath'>;
14
+ export interface FilesConfig {
15
+ basePath?: string;
16
+ include?: string[];
17
+ exclude?: string[];
18
+ }
19
+ export interface SymlinkConfig {
20
+ path: string;
21
+ target: string;
22
+ }
23
+ export interface Placeholders {
24
+ serverName: string;
25
+ deployPath: string;
26
+ timestamp: string;
27
+ }
28
+ export interface TaskContext {
29
+ server: ServerConfig & {
30
+ name: string;
31
+ };
32
+ ssh: SSH2Promise;
33
+ config: DeployerConfig;
34
+ runRemote: (cmd: string) => Promise<string>;
35
+ runLocal: (cmd: string) => Promise<string>;
36
+ }
37
+ export type TaskFn = (ctx: TaskContext, ph: Placeholders) => Promise<void>;
38
+ export interface DeployerConfig {
39
+ rootDir: string;
40
+ servers: Record<string, ServerConfig>;
41
+ files?: FilesConfig;
42
+ symlinks?: SymlinkConfig[];
43
+ tasks?: Record<string, TaskFn>;
44
+ scenarios?: Record<string, string[]>;
45
+ }
46
+ export type DeployerConfigInput = Omit<DeployerConfig, 'servers' | 'rootDir'> & {
47
+ servers: Record<string, ServerConfigInput>;
48
+ };
package/dist/def.js ADDED
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,2 @@
1
+ import type { TaskFn } from './def.js';
2
+ export declare const defaultTasks: Record<string, TaskFn>;
@@ -0,0 +1,92 @@
1
+ import path from 'node:path';
2
+ import { spawn } from 'node:child_process';
3
+ import { Exception } from './utils/Exception.js';
4
+ function buildRsyncCommand(server, source, dest, files) {
5
+ const args = ['rsync', '-avz', '--delete', '--progress=info2'];
6
+ // ssh shell
7
+ const sshParts = ['ssh'];
8
+ if (server.port && server.port !== 22) {
9
+ sshParts.push(`-p ${server.port}`);
10
+ }
11
+ if (server.authMethod === 'key' && server.privateKey) {
12
+ sshParts.push(`-i ${server.privateKey}`);
13
+ }
14
+ sshParts.push('-o StrictHostKeyChecking=no');
15
+ args.push('-e', `"${sshParts.join(' ')}"`);
16
+ // include/exclude
17
+ if (files.include) {
18
+ for (const pattern of files.include) {
19
+ args.push(`--include=${pattern}`);
20
+ }
21
+ args.push('--exclude=*');
22
+ }
23
+ if (files.exclude) {
24
+ for (const pattern of files.exclude) {
25
+ args.push(`--exclude=${pattern}`);
26
+ }
27
+ }
28
+ args.push(source, dest);
29
+ return args.join(' ');
30
+ }
31
+ function execRsync(command) {
32
+ return new Promise((resolve, reject) => {
33
+ const child = spawn('sh', ['-c', command], {
34
+ stdio: ['inherit', 'pipe', 'pipe'],
35
+ });
36
+ const stderrChunks = [];
37
+ child.stdout.on('data', (data) => {
38
+ process.stdout.write(data);
39
+ });
40
+ child.stderr.on('data', (data) => {
41
+ stderrChunks.push(data.toString());
42
+ process.stderr.write(data);
43
+ });
44
+ child.on('close', (code) => {
45
+ if (code !== 0) {
46
+ const details = stderrChunks.length
47
+ ? `\n${stderrChunks.join('')}`
48
+ : '';
49
+ reject(new Exception(`rsync exited with code ${code} (cmd: ${command})${details}`, 1774741947570));
50
+ return;
51
+ }
52
+ resolve();
53
+ });
54
+ child.on('error', (err) => {
55
+ reject(new Exception(`rsync failed: ${command}\n${err.message}`, 1774741947571));
56
+ });
57
+ });
58
+ }
59
+ const uploadTask = async (ctx, ph) => {
60
+ const files = ctx.config.files;
61
+ if (!files) {
62
+ return;
63
+ }
64
+ const localBase = files.basePath?.startsWith('/')
65
+ ? files.basePath
66
+ : path.resolve(ctx.config.rootDir, files.basePath ?? '.');
67
+ const remotePath = ph.deployPath;
68
+ const dest = `${ctx.server.username}@${ctx.server.host}:${remotePath}`;
69
+ const source = localBase.endsWith('/') ? localBase : localBase + '/';
70
+ await ctx.runRemote(`mkdir -p ${remotePath}`);
71
+ const command = buildRsyncCommand(ctx.server, source, dest, files);
72
+ await execRsync(command);
73
+ };
74
+ const symlinksTask = async (ctx, ph) => {
75
+ const symlinks = ctx.config.symlinks;
76
+ if (!symlinks || symlinks.length === 0) {
77
+ return;
78
+ }
79
+ for (const link of symlinks) {
80
+ const target = link.target.startsWith('/')
81
+ ? link.target
82
+ : `${ph.deployPath}/${link.target}`;
83
+ const path = link.path.startsWith('/')
84
+ ? link.path
85
+ : `${ph.deployPath}/${link.path}`;
86
+ await ctx.runRemote(`ln -sfn ${target} ${path}`);
87
+ }
88
+ };
89
+ export const defaultTasks = {
90
+ upload: uploadTask,
91
+ symlinks: symlinksTask,
92
+ };
@@ -0,0 +1,4 @@
1
+ export type { AuthMethod, DeployerConfig, DeployerConfigInput, FilesConfig, Placeholders, ServerConfig, ServerConfigInput, SymlinkConfig, TaskContext, TaskFn, } from './def.js';
2
+ export { defineConfig } from './config.js';
3
+ export { runScenario, runTask } from './runner.js';
4
+ export { loadConfig, findConfigFile } from './configLoader.js';
package/dist/index.js ADDED
@@ -0,0 +1,3 @@
1
+ export { defineConfig } from './config.js';
2
+ export { runScenario, runTask } from './runner.js';
3
+ export { loadConfig, findConfigFile } from './configLoader.js';
@@ -0,0 +1,5 @@
1
+ import type { DeployerConfig, ServerConfig, TaskFn } from './def.js';
2
+ export declare function resolveServers(config: DeployerConfig, serverNames?: string[]): Array<[string, ServerConfig]>;
3
+ export declare function resolveTaskFns(taskNames: string[], allTasks: Record<string, TaskFn>): Array<[string, TaskFn]>;
4
+ export declare function runScenario(config: DeployerConfig, scenarioName: string, serverNames?: string[]): Promise<void>;
5
+ export declare function runTask(config: DeployerConfig, taskName: string, serverNames?: string[]): Promise<void>;
package/dist/runner.js ADDED
@@ -0,0 +1,168 @@
1
+ import chalk from 'chalk';
2
+ import { Listr } from 'listr2';
3
+ import { spawn } from 'node:child_process';
4
+ import { createSSHConnection } from './connection.js';
5
+ import { Exception } from './utils/Exception.js';
6
+ function execLocal(command) {
7
+ return new Promise((resolve, reject) => {
8
+ const child = spawn('sh', ['-c', command], {
9
+ stdio: ['inherit', 'pipe', 'pipe'],
10
+ });
11
+ const chunks = [];
12
+ child.stdout.on('data', (data) => {
13
+ const text = data.toString();
14
+ chunks.push(text);
15
+ process.stdout.write(text);
16
+ });
17
+ child.stderr.on('data', (data) => {
18
+ const text = data.toString();
19
+ chunks.push(text);
20
+ process.stderr.write(text);
21
+ });
22
+ child.on('close', (code) => {
23
+ const output = chunks.join('');
24
+ if (code !== 0) {
25
+ reject(new Exception(`Local command failed (exit ${code}): ${command}\n${output}`, 1774742010146));
26
+ return;
27
+ }
28
+ resolve(output);
29
+ });
30
+ child.on('error', (err) => {
31
+ reject(new Exception(`Local command failed: ${command}\n${err.message}`, 1774742013175));
32
+ });
33
+ });
34
+ }
35
+ function execRemote(ssh, command) {
36
+ return new Promise((resolve, reject) => {
37
+ ssh.spawn(command)
38
+ .then((stream) => {
39
+ const chunks = [];
40
+ stream.on('data', (data) => {
41
+ const text = data.toString();
42
+ chunks.push(text);
43
+ process.stdout.write(text);
44
+ });
45
+ stream.stderr.on('data', (data) => {
46
+ const text = data.toString();
47
+ chunks.push(text);
48
+ process.stderr.write(text);
49
+ });
50
+ stream.on('close', (code) => {
51
+ const output = chunks.join('');
52
+ if (code !== 0) {
53
+ reject(new Exception(`Remote command failed (exit ${code}): ${command}\n${output}`, 1774742047909));
54
+ return;
55
+ }
56
+ resolve(output);
57
+ });
58
+ })
59
+ .catch((err) => {
60
+ reject(new Exception(`Remote command failed: ${command}\n${err.message}`, 1774742062700));
61
+ });
62
+ });
63
+ }
64
+ function buildPlaceholders(serverName, server) {
65
+ return {
66
+ serverName,
67
+ deployPath: server.deployPath,
68
+ timestamp: new Date().toISOString().replace(/[:.]/g, '-'),
69
+ };
70
+ }
71
+ function buildTaskContext(serverName, server, ssh, config) {
72
+ return {
73
+ server: { ...server, name: serverName },
74
+ ssh,
75
+ config,
76
+ runRemote: (cmd) => execRemote(ssh, cmd),
77
+ runLocal: (cmd) => execLocal(cmd),
78
+ };
79
+ }
80
+ export function resolveServers(config, serverNames) {
81
+ const allEntries = Object.entries(config.servers);
82
+ if (!serverNames || serverNames.length === 0) {
83
+ return allEntries;
84
+ }
85
+ const result = [];
86
+ for (const name of serverNames) {
87
+ const server = config.servers[name];
88
+ if (!server) {
89
+ const available = Object.keys(config.servers).join(', ');
90
+ throw new Exception(`Server "${name}" not found. Available: ${available}`, 1774742073310);
91
+ }
92
+ result.push([name, server]);
93
+ }
94
+ return result;
95
+ }
96
+ export function resolveTaskFns(taskNames, allTasks) {
97
+ return taskNames.map(name => {
98
+ const fn = allTasks[name];
99
+ if (!fn) {
100
+ const available = Object.keys(allTasks).join(', ');
101
+ throw new Exception(`Task "${name}" not found. Available: ${available}`, 1774742082083);
102
+ }
103
+ return [name, fn];
104
+ });
105
+ }
106
+ const listrOptions = {
107
+ concurrent: false,
108
+ renderer: 'simple',
109
+ rendererOptions: {
110
+ clearOutput: true,
111
+ },
112
+ };
113
+ function buildServerListr(serverName, server, config, tasks) {
114
+ return new Listr([
115
+ {
116
+ task: async (ctx) => {
117
+ const ssh = createSSHConnection(server);
118
+ await ssh.connect();
119
+ ctx.ssh = ssh;
120
+ ctx.taskCtx = buildTaskContext(serverName, server, ssh, config);
121
+ ctx.ph = buildPlaceholders(serverName, server);
122
+ },
123
+ },
124
+ ...tasks.map(([taskName, taskFn]) => ({
125
+ title: chalk.bgCyan.black(` ${taskName} `),
126
+ task: async (ctx, task) => taskFn(ctx.taskCtx, ctx.ph),
127
+ options: listrOptions,
128
+ })),
129
+ {
130
+ task: async (ctx) => {
131
+ if (ctx.ssh) {
132
+ await ctx.ssh.close();
133
+ }
134
+ },
135
+ },
136
+ ], listrOptions);
137
+ }
138
+ export async function runScenario(config, scenarioName, serverNames) {
139
+ const scenarioTasks = config.scenarios?.[scenarioName];
140
+ if (!scenarioTasks) {
141
+ const available = Object.keys(config.scenarios ?? {}).join(', ') || 'none';
142
+ throw new Exception(`Scenario "${scenarioName}" not found. Available: ${available}`, 1774742090385);
143
+ }
144
+ const allTasks = config.tasks ?? {};
145
+ const tasks = resolveTaskFns(scenarioTasks, allTasks);
146
+ const servers = resolveServers(config, serverNames);
147
+ const listr = new Listr(servers.map(([name, server]) => ({
148
+ title: chalk.bgMagenta.black(` ${name} (${server.host}) `),
149
+ task: () => buildServerListr(name, server, config, tasks),
150
+ options: listrOptions,
151
+ })), listrOptions);
152
+ await listr.run();
153
+ }
154
+ export async function runTask(config, taskName, serverNames) {
155
+ const allTasks = config.tasks ?? {};
156
+ const taskFn = allTasks[taskName];
157
+ if (!taskFn) {
158
+ const available = Object.keys(allTasks).join(', ') || 'none';
159
+ throw new Exception(`Task "${taskName}" not found. Available: ${available}`, 1774742100356);
160
+ }
161
+ const servers = resolveServers(config, serverNames);
162
+ const listr = new Listr(servers.map(([name, server]) => ({
163
+ title: chalk.bgMagenta.black(` ${name} (${server.host}) `),
164
+ task: () => buildServerListr(name, server, config, [[taskName, taskFn]]),
165
+ options: listrOptions,
166
+ })), listrOptions);
167
+ await listr.run();
168
+ }
@@ -0,0 +1,10 @@
1
+ type Reason = Error | string;
2
+ export declare class Exception extends Error {
3
+ code: number;
4
+ protected _reasons: Reason[];
5
+ get reasons(): Reason[];
6
+ constructor(message: string, code?: number, error?: Reason);
7
+ toString(): string;
8
+ protected _initErrorMessage(message: any, error: any): void;
9
+ }
10
+ export {};
@@ -0,0 +1,33 @@
1
+ export class Exception extends Error {
2
+ code;
3
+ _reasons = [];
4
+ get reasons() { return this._reasons; }
5
+ constructor(message, code = -1, error) {
6
+ super(message);
7
+ this.code = code;
8
+ if (error) {
9
+ this._reasons = error instanceof Exception
10
+ ? error.reasons.concat([error])
11
+ : [error];
12
+ this._initErrorMessage(this.message, error);
13
+ }
14
+ }
15
+ toString() {
16
+ return `Exception #${this.code}: ${this.message}`;
17
+ }
18
+ _initErrorMessage(message, error) {
19
+ // @ts-ignore - it depends on the environment
20
+ const captureStackTrace = Error.captureStackTrace;
21
+ if (typeof captureStackTrace === 'function') {
22
+ captureStackTrace(this, this.constructor);
23
+ }
24
+ else {
25
+ this.stack = (new Error(message)).stack;
26
+ }
27
+ const messageLines = (this.message.match(/\n/g) || []).length + 1;
28
+ this.stack = this.constructor.name + ': [' + this.code + '] ' + message + '\n' +
29
+ this.stack.split('\n').slice(1, messageLines + 1).join('\n')
30
+ + '\n'
31
+ + error.stack;
32
+ }
33
+ }
@@ -0,0 +1 @@
1
+ export * from './Exception.js';
@@ -0,0 +1 @@
1
+ export * from './Exception.js';
package/package.json ADDED
@@ -0,0 +1,66 @@
1
+ {
2
+ "name": "d3ployer",
3
+ "version": "0.0.1",
4
+ "license": "MIT",
5
+ "type": "module",
6
+ "bin": {
7
+ "dpl": "dist/bin.js"
8
+ },
9
+ "exports": {
10
+ ".": {
11
+ "types": "./dist/index.d.ts",
12
+ "import": "./dist/index.js"
13
+ }
14
+ },
15
+ "types": "./dist/index.d.ts",
16
+ "files": [
17
+ "dist"
18
+ ],
19
+ "scripts": {
20
+ "build": "tsc -p tsconfig.build.json",
21
+ "dev": "tsx src/main.ts",
22
+ "dev:watch": "tsx --watch src/main.ts",
23
+ "dev:inspect": "tsx --inspect-brk src/main.ts",
24
+ "script": "tsx src/script.ts",
25
+ "deploy": "tsx src/cli.ts",
26
+ "test": "mocha --import=tsx",
27
+ "test:inspect": "mocha --import=tsx --inspect-brk",
28
+ "coverage": "c8 mocha --import=tsx",
29
+ "prepare": "husky"
30
+ },
31
+ "dependencies": {
32
+ "chalk": "^5.6.2",
33
+ "commander": "^14.0.3",
34
+ "listr2": "^10.2.1",
35
+ "lodash-es": "^4.17.23",
36
+ "ssh2-promise": "^1.0.3"
37
+ },
38
+ "devDependencies": {
39
+ "@eslint/js": "^9.39.3",
40
+ "@stylistic/eslint-plugin": "^2.13.0",
41
+ "@tsconfig/node24": "^24.0.4",
42
+ "@types/chai": "^5.2.3",
43
+ "@types/chai-as-promised": "^8.0.2",
44
+ "@types/lodash-es": "^4.17.12",
45
+ "@types/mocha": "^10.0.10",
46
+ "@types/node": "^24.10.13",
47
+ "c8": "^10.1.3",
48
+ "chai": "^6.2.2",
49
+ "chai-as-promised": "^8.0.2",
50
+ "eslint": "^8.57.1",
51
+ "eslint-plugin-import": "^2.32.0",
52
+ "husky": "^9.1.7",
53
+ "lint-staged": "^15.5.2",
54
+ "mocha": "^11.3.0",
55
+ "patch-package": "^8.0.1",
56
+ "tsx": "^4.21.0",
57
+ "typescript": "^5.9.3",
58
+ "typescript-eslint": "^8.56.0"
59
+ },
60
+ "lint-staged": {
61
+ "*.{js,jsx,ts,tsx}": [
62
+ "eslint --fix",
63
+ "git add"
64
+ ]
65
+ }
66
+ }