d3ployer 0.0.1 → 0.0.3

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/dist/cli.js CHANGED
@@ -36,18 +36,23 @@ program
36
36
  const scenarios = config.scenarios ?? {};
37
37
  const scenarioKeys = Object.keys(scenarios);
38
38
  if (scenarioKeys.length) {
39
- for (const name of scenarioKeys) {
40
- console.log(` ${chalk.cyan(name)} [${scenarios[name].join(', ')}]`);
39
+ for (const key of scenarioKeys) {
40
+ const s = scenarios[key];
41
+ const label = s.name !== key ? `${chalk.cyan(key)} (${s.name})` : chalk.cyan(key);
42
+ console.log(` ${label} → [${s.tasks.join(', ')}]`);
41
43
  }
42
44
  }
43
45
  else {
44
46
  console.log(' (none)');
45
47
  }
46
48
  console.log(chalk.bold('\nTasks:'));
47
- const taskKeys = Object.keys(config.tasks ?? {});
49
+ const tasks = config.tasks ?? {};
50
+ const taskKeys = Object.keys(tasks);
48
51
  if (taskKeys.length) {
49
- for (const name of taskKeys) {
50
- console.log(` ${chalk.cyan(name)}`);
52
+ for (const key of taskKeys) {
53
+ const t = tasks[key];
54
+ const label = t.name !== key ? `${chalk.cyan(key)} (${t.name})` : chalk.cyan(key);
55
+ console.log(` ${label}`);
51
56
  }
52
57
  }
53
58
  else {
package/dist/config.js CHANGED
@@ -1,6 +1,6 @@
1
1
  import { defaultsDeep } from 'lodash-es';
2
2
  import os from 'node:os';
3
- import { defaultTasks } from './defaultTasks.js';
3
+ import { defaultScenarios, defaultTasks } from './defaultTasks.js';
4
4
  const SERVER_DEFAULTS = {
5
5
  port: 22,
6
6
  username: os.userInfo().username,
@@ -9,18 +9,41 @@ const SERVER_DEFAULTS = {
9
9
  function resolveServer(input) {
10
10
  return defaultsDeep({}, input, SERVER_DEFAULTS);
11
11
  }
12
+ function normalizeTask(key, input) {
13
+ if (typeof input === 'function') {
14
+ return { name: key, fn: input };
15
+ }
16
+ return { name: input.name, fn: input.task };
17
+ }
18
+ function normalizeScenario(key, input) {
19
+ if (Array.isArray(input)) {
20
+ return { name: key, tasks: input };
21
+ }
22
+ return { name: input.name, tasks: input.tasks };
23
+ }
12
24
  export function defineConfig(input) {
13
25
  const servers = {};
14
26
  for (const [name, serverInput] of Object.entries(input.servers)) {
15
27
  servers[name] = resolveServer(serverInput);
16
28
  }
29
+ const tasks = { ...defaultTasks };
30
+ if (input.tasks) {
31
+ for (const [key, taskInput] of Object.entries(input.tasks)) {
32
+ tasks[key] = normalizeTask(key, taskInput);
33
+ }
34
+ }
35
+ const scenarios = { ...defaultScenarios };
36
+ if (input.scenarios) {
37
+ for (const [key, scenarioInput] of Object.entries(input.scenarios)) {
38
+ scenarios[key] = normalizeScenario(key, scenarioInput);
39
+ }
40
+ }
17
41
  return {
18
- ...input,
42
+ packageManager: 'npm',
19
43
  rootDir: '',
44
+ ...input,
20
45
  servers,
21
- tasks: {
22
- ...defaultTasks,
23
- ...input.tasks,
24
- },
46
+ tasks,
47
+ scenarios,
25
48
  };
26
49
  }
@@ -1,6 +1,5 @@
1
1
  import fs from 'node:fs';
2
2
  import path from 'node:path';
3
- import { defineConfig } from './config.js';
4
3
  import { Exception } from './utils/Exception.js';
5
4
  const CONFIG_FILENAME = 'deployer.config.ts';
6
5
  export function findConfigFile(startDir = process.cwd()) {
@@ -23,8 +22,7 @@ export async function loadConfig(configPath) {
23
22
  throw new Exception(`Config file not found: ${absolutePath}`, 1774741902017);
24
23
  }
25
24
  const module = await import(absolutePath);
26
- const raw = module.default ?? module;
27
- const config = defineConfig(raw);
25
+ const config = module.default ?? module;
28
26
  config.rootDir = path.dirname(absolutePath);
29
27
  if (!config.servers || Object.keys(config.servers).length === 0) {
30
28
  throw new Exception('Config must define at least one server', 1774741913430);
package/dist/def.d.ts CHANGED
@@ -9,6 +9,8 @@ export interface ServerConfig {
9
9
  password?: string;
10
10
  agent?: string;
11
11
  deployPath: string;
12
+ packageManager?: string;
13
+ initCmd?: string;
12
14
  }
13
15
  export type ServerConfigInput = Partial<ServerConfig> & Pick<ServerConfig, 'host' | 'deployPath'>;
14
16
  export interface FilesConfig {
@@ -25,24 +27,55 @@ export interface Placeholders {
25
27
  deployPath: string;
26
28
  timestamp: string;
27
29
  }
30
+ export type ExecResult = {
31
+ success: boolean;
32
+ code: number;
33
+ stdout: string;
34
+ stderr: string;
35
+ };
36
+ export type RunOptions = {
37
+ printOutput?: boolean;
38
+ ignoreError?: boolean;
39
+ };
28
40
  export interface TaskContext {
29
41
  server: ServerConfig & {
30
42
  name: string;
31
43
  };
32
44
  ssh: SSH2Promise;
33
45
  config: DeployerConfig;
34
- runRemote: (cmd: string) => Promise<string>;
35
- runLocal: (cmd: string) => Promise<string>;
46
+ runLocal: (cmd: string, options?: RunOptions) => Promise<ExecResult>;
47
+ testLocal: (cmd: string) => Promise<boolean>;
48
+ run: (cmd: string, options?: RunOptions) => Promise<ExecResult>;
49
+ test: (cmd: string) => Promise<boolean>;
36
50
  }
37
51
  export type TaskFn = (ctx: TaskContext, ph: Placeholders) => Promise<void>;
52
+ export interface TaskDef {
53
+ name: string;
54
+ fn: TaskFn;
55
+ }
56
+ export type TaskInput = TaskFn | {
57
+ name: string;
58
+ task: TaskFn;
59
+ };
60
+ export interface ScenarioDef {
61
+ name: string;
62
+ tasks: string[];
63
+ }
64
+ export type ScenarioInput = string[] | {
65
+ name: string;
66
+ tasks: string[];
67
+ };
38
68
  export interface DeployerConfig {
39
69
  rootDir: string;
40
70
  servers: Record<string, ServerConfig>;
71
+ packageManager?: string;
41
72
  files?: FilesConfig;
42
73
  symlinks?: SymlinkConfig[];
43
- tasks?: Record<string, TaskFn>;
44
- scenarios?: Record<string, string[]>;
74
+ tasks?: Record<string, TaskDef>;
75
+ scenarios?: Record<string, ScenarioDef>;
45
76
  }
46
- export type DeployerConfigInput = Omit<DeployerConfig, 'servers' | 'rootDir'> & {
77
+ export type DeployerConfigInput = Omit<DeployerConfig, 'servers' | 'rootDir' | 'tasks' | 'scenarios'> & {
47
78
  servers: Record<string, ServerConfigInput>;
79
+ tasks?: Record<string, TaskInput>;
80
+ scenarios?: Record<string, ScenarioInput>;
48
81
  };
@@ -1,2 +1,3 @@
1
- import type { TaskFn } from './def.js';
2
- export declare const defaultTasks: Record<string, TaskFn>;
1
+ import type { ScenarioDef, TaskDef } from './def.js';
2
+ export declare const defaultTasks: Record<string, TaskDef>;
3
+ export declare const defaultScenarios: Record<string, ScenarioDef>;
@@ -1,5 +1,6 @@
1
- import path from 'node:path';
1
+ import chalk from 'chalk';
2
2
  import { spawn } from 'node:child_process';
3
+ import path from 'node:path';
3
4
  import { Exception } from './utils/Exception.js';
4
5
  function buildRsyncCommand(server, source, dest, files) {
5
6
  const args = ['rsync', '-avz', '--delete', '--progress=info2'];
@@ -67,7 +68,7 @@ const uploadTask = async (ctx, ph) => {
67
68
  const remotePath = ph.deployPath;
68
69
  const dest = `${ctx.server.username}@${ctx.server.host}:${remotePath}`;
69
70
  const source = localBase.endsWith('/') ? localBase : localBase + '/';
70
- await ctx.runRemote(`mkdir -p ${remotePath}`);
71
+ await ctx.run(`mkdir -p ${remotePath}`);
71
72
  const command = buildRsyncCommand(ctx.server, source, dest, files);
72
73
  await execRsync(command);
73
74
  };
@@ -83,10 +84,59 @@ const symlinksTask = async (ctx, ph) => {
83
84
  const path = link.path.startsWith('/')
84
85
  ? link.path
85
86
  : `${ph.deployPath}/${link.path}`;
86
- await ctx.runRemote(`ln -sfn ${target} ${path}`);
87
+ await ctx.run(`ln -sfn ${target} ${path}`);
88
+ }
89
+ };
90
+ const depInstallTask = async (ctx, ph) => {
91
+ const pm = ctx.server.packageManager ?? ctx.config.packageManager ?? 'npm';
92
+ const cmd = `${pm} install`;
93
+ await ctx.run(cmd);
94
+ };
95
+ const printDeploymentTask = async (ctx, ph) => {
96
+ console.log(chalk.cyan('Deployment directory'), ph.deployPath);
97
+ await ctx.run('ls -la .');
98
+ console.log(chalk.cyan('Directory size'));
99
+ await ctx.run('du -hd 1 .');
100
+ };
101
+ const pm2SetupTask = async (ctx, ph) => {
102
+ const pm2ConfigExists = await ctx.test('test -f pm2.config.js');
103
+ if (!pm2ConfigExists) {
104
+ console.log(chalk.yellow('pm2.config.js not found, skipping PM2 setup'));
105
+ return;
87
106
  }
107
+ await ctx.run('pm2 start pm2.config.js --update-env');
108
+ await ctx.run('pm2 save');
88
109
  };
89
110
  export const defaultTasks = {
90
- upload: uploadTask,
91
- symlinks: symlinksTask,
111
+ upload: {
112
+ name: 'Upload files',
113
+ fn: uploadTask,
114
+ },
115
+ symlinks: {
116
+ name: 'Create symlinks',
117
+ fn: symlinksTask,
118
+ },
119
+ depInstall: {
120
+ name: 'Install dependencies',
121
+ fn: depInstallTask,
122
+ },
123
+ printDeployment: {
124
+ name: 'Print deployment info',
125
+ fn: printDeploymentTask,
126
+ },
127
+ pm2Setup: {
128
+ name: 'PM2 setup',
129
+ fn: pm2SetupTask,
130
+ },
131
+ };
132
+ export const defaultScenarios = {
133
+ deploy: {
134
+ name: 'Deploy',
135
+ tasks: [
136
+ 'upload',
137
+ 'symlinks',
138
+ 'depInstall',
139
+ 'pm2Setup',
140
+ ],
141
+ },
92
142
  };
package/dist/index.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- export type { AuthMethod, DeployerConfig, DeployerConfigInput, FilesConfig, Placeholders, ServerConfig, ServerConfigInput, SymlinkConfig, TaskContext, TaskFn, } from './def.js';
1
+ export type { AuthMethod, DeployerConfig, DeployerConfigInput, FilesConfig, Placeholders, ScenarioDef, ScenarioInput, ServerConfig, ServerConfigInput, SymlinkConfig, TaskContext, TaskDef, TaskFn, TaskInput, } from './def.js';
2
2
  export { defineConfig } from './config.js';
3
3
  export { runScenario, runTask } from './runner.js';
4
4
  export { loadConfig, findConfigFile } from './configLoader.js';
package/dist/runner.d.ts CHANGED
@@ -1,5 +1,5 @@
1
- import type { DeployerConfig, ServerConfig, TaskFn } from './def.js';
1
+ import type { DeployerConfig, ServerConfig, TaskDef } from './def.js';
2
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]>;
3
+ export declare function resolveTaskDefs(taskNames: string[], allTasks: Record<string, TaskDef>): Array<[string, TaskDef]>;
4
4
  export declare function runScenario(config: DeployerConfig, scenarioName: string, serverNames?: string[]): Promise<void>;
5
5
  export declare function runTask(config: DeployerConfig, taskName: string, serverNames?: string[]): Promise<void>;
package/dist/runner.js CHANGED
@@ -3,57 +3,89 @@ import { Listr } from 'listr2';
3
3
  import { spawn } from 'node:child_process';
4
4
  import { createSSHConnection } from './connection.js';
5
5
  import { Exception } from './utils/Exception.js';
6
- function execLocal(command) {
6
+ function execLocal(command, options) {
7
+ const result = {
8
+ success: undefined,
9
+ code: undefined,
10
+ stdout: '',
11
+ stderr: '',
12
+ };
7
13
  return new Promise((resolve, reject) => {
8
14
  const child = spawn('sh', ['-c', command], {
15
+ cwd: options.cwd,
9
16
  stdio: ['inherit', 'pipe', 'pipe'],
10
17
  });
11
- const chunks = [];
12
18
  child.stdout.on('data', (data) => {
13
19
  const text = data.toString();
14
- chunks.push(text);
15
- process.stdout.write(text);
20
+ result.stdout += text;
21
+ if (options.printOutput) {
22
+ process.stdout.write(text);
23
+ }
16
24
  });
17
25
  child.stderr.on('data', (data) => {
18
26
  const text = data.toString();
19
- chunks.push(text);
20
- process.stderr.write(text);
27
+ result.stderr += text;
28
+ if (options.printOutput) {
29
+ process.stderr.write(text);
30
+ }
21
31
  });
22
32
  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));
33
+ result.code = code;
34
+ result.success = code === 0;
35
+ if (code !== 0
36
+ && !options.ignoreError) {
37
+ reject(new Exception(`Local command failed (exit ${code}): ${command}\n${result.stderr}`, 1774742010146));
26
38
  return;
27
39
  }
28
- resolve(output);
40
+ resolve(result);
29
41
  });
30
42
  child.on('error', (err) => {
31
43
  reject(new Exception(`Local command failed: ${command}\n${err.message}`, 1774742013175));
32
44
  });
33
45
  });
34
46
  }
35
- function execRemote(ssh, command) {
47
+ function execRemote(ssh, command, options) {
48
+ const result = {
49
+ success: undefined,
50
+ code: undefined,
51
+ stdout: '',
52
+ stderr: '',
53
+ };
54
+ const parts = [];
55
+ if (options.cwd) {
56
+ parts.push(`cd ${options.cwd}`);
57
+ }
58
+ if (options.initCmd) {
59
+ parts.push(options.initCmd);
60
+ }
61
+ parts.push(command);
62
+ const wrappedCommand = parts.join('; \\\n');
36
63
  return new Promise((resolve, reject) => {
37
- ssh.spawn(command)
64
+ ssh.spawn(wrappedCommand)
38
65
  .then((stream) => {
39
- const chunks = [];
40
66
  stream.on('data', (data) => {
41
67
  const text = data.toString();
42
- chunks.push(text);
43
- process.stdout.write(text);
68
+ result.stdout += text;
69
+ if (options.printOutput) {
70
+ process.stdout.write(text);
71
+ }
44
72
  });
45
73
  stream.stderr.on('data', (data) => {
46
74
  const text = data.toString();
47
- chunks.push(text);
48
- process.stderr.write(text);
75
+ result.stderr += text;
76
+ if (options.printOutput) {
77
+ process.stderr.write(text);
78
+ }
49
79
  });
50
80
  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));
81
+ result.code = code;
82
+ result.success = code === 0;
83
+ if (code !== 0
84
+ && !options.ignoreError) {
85
+ reject(new Exception(`Remote command failed (exit ${code}): ${command}\n${result.stderr}`, 1774742047909));
54
86
  return;
55
87
  }
56
- resolve(output);
88
+ resolve(result);
57
89
  });
58
90
  })
59
91
  .catch((err) => {
@@ -73,8 +105,46 @@ function buildTaskContext(serverName, server, ssh, config) {
73
105
  server: { ...server, name: serverName },
74
106
  ssh,
75
107
  config,
76
- runRemote: (cmd) => execRemote(ssh, cmd),
77
- runLocal: (cmd) => execLocal(cmd),
108
+ runLocal: (cmd, options = {}) => {
109
+ options = {
110
+ ignoreError: false,
111
+ printOutput: true,
112
+ ...options,
113
+ };
114
+ return execLocal(cmd, {
115
+ cwd: config.rootDir,
116
+ ...options,
117
+ });
118
+ },
119
+ testLocal: async (cmd) => {
120
+ const result = await execLocal(cmd, {
121
+ ignoreError: true,
122
+ printOutput: false,
123
+ cwd: config.rootDir,
124
+ });
125
+ return result.success;
126
+ },
127
+ run: (cmd, options = {}) => {
128
+ options = {
129
+ ignoreError: false,
130
+ printOutput: true,
131
+ ...options,
132
+ };
133
+ return execRemote(ssh, cmd, {
134
+ cwd: server.deployPath,
135
+ initCmd: server.initCmd,
136
+ ...options,
137
+ });
138
+ },
139
+ test: async (cmd) => {
140
+ const result = await execRemote(ssh, cmd, {
141
+ ignoreError: true,
142
+ printOutput: false,
143
+ cwd: config.rootDir,
144
+ initCmd: server.initCmd,
145
+ });
146
+ return result.success;
147
+ },
78
148
  };
79
149
  }
80
150
  export function resolveServers(config, serverNames) {
@@ -93,14 +163,14 @@ export function resolveServers(config, serverNames) {
93
163
  }
94
164
  return result;
95
165
  }
96
- export function resolveTaskFns(taskNames, allTasks) {
166
+ export function resolveTaskDefs(taskNames, allTasks) {
97
167
  return taskNames.map(name => {
98
- const fn = allTasks[name];
99
- if (!fn) {
168
+ const def = allTasks[name];
169
+ if (!def) {
100
170
  const available = Object.keys(allTasks).join(', ');
101
171
  throw new Exception(`Task "${name}" not found. Available: ${available}`, 1774742082083);
102
172
  }
103
- return [name, fn];
173
+ return [name, def];
104
174
  });
105
175
  }
106
176
  const listrOptions = {
@@ -121,9 +191,9 @@ function buildServerListr(serverName, server, config, tasks) {
121
191
  ctx.ph = buildPlaceholders(serverName, server);
122
192
  },
123
193
  },
124
- ...tasks.map(([taskName, taskFn]) => ({
125
- title: chalk.bgCyan.black(` ${taskName} `),
126
- task: async (ctx, task) => taskFn(ctx.taskCtx, ctx.ph),
194
+ ...tasks.map(([_key, taskDef]) => ({
195
+ title: chalk.bgCyan.black(` ${taskDef.name} `),
196
+ task: async (ctx, task) => taskDef.fn(ctx.taskCtx, ctx.ph),
127
197
  options: listrOptions,
128
198
  })),
129
199
  {
@@ -136,13 +206,13 @@ function buildServerListr(serverName, server, config, tasks) {
136
206
  ], listrOptions);
137
207
  }
138
208
  export async function runScenario(config, scenarioName, serverNames) {
139
- const scenarioTasks = config.scenarios?.[scenarioName];
140
- if (!scenarioTasks) {
209
+ const scenarioDef = config.scenarios?.[scenarioName];
210
+ if (!scenarioDef) {
141
211
  const available = Object.keys(config.scenarios ?? {}).join(', ') || 'none';
142
212
  throw new Exception(`Scenario "${scenarioName}" not found. Available: ${available}`, 1774742090385);
143
213
  }
144
214
  const allTasks = config.tasks ?? {};
145
- const tasks = resolveTaskFns(scenarioTasks, allTasks);
215
+ const tasks = resolveTaskDefs(scenarioDef.tasks, allTasks);
146
216
  const servers = resolveServers(config, serverNames);
147
217
  const listr = new Listr(servers.map(([name, server]) => ({
148
218
  title: chalk.bgMagenta.black(` ${name} (${server.host}) `),
@@ -153,15 +223,15 @@ export async function runScenario(config, scenarioName, serverNames) {
153
223
  }
154
224
  export async function runTask(config, taskName, serverNames) {
155
225
  const allTasks = config.tasks ?? {};
156
- const taskFn = allTasks[taskName];
157
- if (!taskFn) {
226
+ const taskDef = allTasks[taskName];
227
+ if (!taskDef) {
158
228
  const available = Object.keys(allTasks).join(', ') || 'none';
159
229
  throw new Exception(`Task "${taskName}" not found. Available: ${available}`, 1774742100356);
160
230
  }
161
231
  const servers = resolveServers(config, serverNames);
162
232
  const listr = new Listr(servers.map(([name, server]) => ({
163
233
  title: chalk.bgMagenta.black(` ${name} (${server.host}) `),
164
- task: () => buildServerListr(name, server, config, [[taskName, taskFn]]),
234
+ task: () => buildServerListr(name, server, config, [[taskName, taskDef]]),
165
235
  options: listrOptions,
166
236
  })), listrOptions);
167
237
  await listr.run();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "d3ployer",
3
- "version": "0.0.1",
3
+ "version": "0.0.3",
4
4
  "license": "MIT",
5
5
  "type": "module",
6
6
  "bin": {