d3ployer 0.0.10 → 0.0.11

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
@@ -6,19 +6,24 @@ import { runScenario, runTask } from './runner.js';
6
6
  const program = new Command()
7
7
  .name('deployer')
8
8
  .description('TypeScript deployment tool')
9
- .option('-c, --config <path>', 'path to deployer.config.ts');
9
+ .option('-c, --config <path>', 'path to deployer.config.ts')
10
+ .option('--skip <tasks>', 'comma-separated list of tasks to skip');
10
11
  program
11
12
  .argument('<name>', 'scenario or task name')
12
13
  .argument('[servers...]', 'target server(s)')
13
14
  .action(async (name, servers) => {
14
15
  try {
15
- const config = await loadConfig(program.opts().config);
16
+ const opts = program.opts();
17
+ const config = await loadConfig(opts.config);
16
18
  const serverList = servers.length > 0 ? servers : undefined;
19
+ const skipTasks = opts.skip
20
+ ? opts.skip.split(',').map((s) => s.trim()).filter(Boolean)
21
+ : [];
17
22
  if (config.scenarios?.[name]) {
18
- await runScenario(config, name, serverList);
23
+ await runScenario(config, name, serverList, { skip: skipTasks });
19
24
  }
20
25
  else {
21
- await runTask(config, name, serverList);
26
+ await runTask(config, name, serverList, { skip: skipTasks });
22
27
  }
23
28
  }
24
29
  catch (err) {
package/dist/config.d.ts CHANGED
@@ -1,3 +1,2 @@
1
1
  import type { DeployerConfig, DeployerConfigInput } from './def.js';
2
- export declare function camelToColonCase(str: string): string;
3
2
  export declare function defineConfig(input: DeployerConfigInput): DeployerConfig;
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 { defaultScenarios, defaultTasks } from './defaultTasks.js';
3
+ import { defaultScenarios, defaultTasks } from './tasks/index.js';
4
4
  const SERVER_DEFAULTS = {
5
5
  port: 22,
6
6
  username: os.userInfo().username,
@@ -35,9 +35,6 @@ function normalizeScenario(key, input) {
35
35
  tasks: input.tasks,
36
36
  };
37
37
  }
38
- export function camelToColonCase(str) {
39
- return str.replace(/([a-z0-9])([A-Z])/g, '$1:$2').toLowerCase();
40
- }
41
38
  export function defineConfig(input) {
42
39
  const servers = {};
43
40
  for (const [name, serverInput] of Object.entries(input.servers)) {
@@ -45,57 +42,52 @@ export function defineConfig(input) {
45
42
  }
46
43
  const tasks = {};
47
44
  for (const [key, taskDef] of Object.entries(defaultTasks)) {
48
- tasks[camelToColonCase(key)] = taskDef;
45
+ tasks[key] = taskDef;
49
46
  }
50
47
  if (input.tasks) {
51
48
  for (const [key, taskInput] of Object.entries(input.tasks)) {
52
- tasks[camelToColonCase(key)] = normalizeTask(key, taskInput);
49
+ tasks[key] = normalizeTask(key, taskInput);
53
50
  }
54
51
  }
55
52
  const scenarios = {};
56
53
  for (const [key, scenarioDef] of Object.entries(defaultScenarios)) {
57
- scenarios[camelToColonCase(key)] = {
54
+ scenarios[key] = {
58
55
  ...scenarioDef,
59
- tasks: scenarioDef.tasks.map(camelToColonCase),
56
+ tasks: scenarioDef.tasks,
60
57
  };
61
58
  }
62
59
  if (input.scenarios) {
63
60
  for (const [key, scenarioInput] of Object.entries(input.scenarios)) {
64
61
  const normalized = normalizeScenario(key, scenarioInput);
65
- scenarios[camelToColonCase(key)] = {
62
+ scenarios[key] = {
66
63
  ...normalized,
67
- tasks: normalized.tasks.map(camelToColonCase),
64
+ tasks: normalized.tasks,
68
65
  };
69
66
  }
70
67
  }
71
- let packageManager;
72
- if (input.packageManager === false) {
73
- packageManager = false;
74
- }
75
- else {
76
- packageManager = {
77
- manager: 'npm',
78
- productionOnly: true,
79
- ...input.packageManager,
80
- };
81
- }
82
- let dockerCompose;
83
- if (input.dockerCompose === false) {
84
- dockerCompose = false;
85
- }
86
- else {
87
- dockerCompose = {
88
- configFiles: undefined,
89
- ...input.dockerCompose,
90
- };
91
- }
92
- return {
68
+ return defaultsDeep({
93
69
  rootDir: '',
94
70
  ...input,
95
- packageManager,
96
- dockerCompose,
97
71
  servers,
98
72
  tasks,
99
73
  scenarios,
100
- };
74
+ }, {
75
+ packageManager: {
76
+ manager: 'npm',
77
+ productionOnly: true,
78
+ },
79
+ pm2: {
80
+ logs: {
81
+ lines: 25,
82
+ time: 3,
83
+ },
84
+ },
85
+ dockerCompose: {
86
+ configFiles: undefined,
87
+ logs: {
88
+ lines: 25,
89
+ time: 3,
90
+ },
91
+ },
92
+ });
101
93
  }
package/dist/def.d.ts CHANGED
@@ -1,3 +1,4 @@
1
+ import type { ListrTaskWrapper } from 'listr2';
1
2
  import type SSH2Promise from 'ssh2-promise';
2
3
  export type AuthMethod = 'key' | 'password' | 'agent';
3
4
  export type PackageManager = 'npm' | 'yarn' | 'pnpm';
@@ -27,12 +28,34 @@ export interface SymlinkConfig {
27
28
  path: string;
28
29
  target: string;
29
30
  }
31
+ export type ConfigOrDisable<T> = T | false;
32
+ export interface LogsConfig {
33
+ time?: number;
34
+ lines?: number;
35
+ }
36
+ export type Pm2Config = {
37
+ logs: ConfigOrDisable<LogsConfig>;
38
+ };
30
39
  export type DockerComposeConfig = {
31
40
  configFiles: string[];
41
+ logs: ConfigOrDisable<LogsConfig>;
32
42
  };
33
- export interface LogsConfig {
34
- time?: number;
43
+ export interface DeployerConfig {
44
+ rootDir: string;
45
+ servers: Record<string, ServerConfig>;
46
+ files?: FilesConfig;
47
+ symlinks?: SymlinkConfig[];
48
+ packageManager?: ConfigOrDisable<PackageManagerConfig>;
49
+ pm2?: ConfigOrDisable<Pm2Config>;
50
+ dockerCompose?: ConfigOrDisable<DockerComposeConfig>;
51
+ tasks?: Record<string, TaskDef>;
52
+ scenarios?: Record<string, ScenarioDef>;
35
53
  }
54
+ export type DeployerConfigInput = Omit<DeployerConfig, 'servers' | 'rootDir' | 'tasks' | 'scenarios'> & {
55
+ servers: Record<string, ServerConfigInput>;
56
+ tasks?: Record<string, TaskInput>;
57
+ scenarios?: Record<string, ScenarioInput>;
58
+ };
36
59
  export interface Placeholders {
37
60
  serverName: string;
38
61
  deployPath: string;
@@ -60,7 +83,7 @@ export interface TaskContext {
60
83
  test: (cmd: string) => Promise<boolean>;
61
84
  taskConfig?: any;
62
85
  }
63
- export type TaskFn = (ctx: TaskContext, ph: Placeholders) => Promise<void>;
86
+ export type TaskFn = (ctx: TaskContext, ph: Placeholders, task: ListrTaskWrapper<any, any, any>) => Promise<void>;
64
87
  export type TaskSkipFn = (ctx: TaskContext, ph: Placeholders) => Promise<boolean | string> | boolean | string;
65
88
  export interface TaskDef {
66
89
  name: string;
@@ -82,20 +105,3 @@ export type ScenarioInput = string[] | {
82
105
  name: string;
83
106
  tasks: string[];
84
107
  };
85
- export interface DeployerConfig {
86
- rootDir: string;
87
- servers: Record<string, ServerConfig>;
88
- files?: FilesConfig;
89
- symlinks?: SymlinkConfig[];
90
- packageManager?: PackageManagerConfig | false;
91
- pm2?: boolean;
92
- dockerCompose?: DockerComposeConfig | false;
93
- logs?: LogsConfig | false;
94
- tasks?: Record<string, TaskDef>;
95
- scenarios?: Record<string, ScenarioDef>;
96
- }
97
- export type DeployerConfigInput = Omit<DeployerConfig, 'servers' | 'rootDir' | 'tasks' | 'scenarios'> & {
98
- servers: Record<string, ServerConfigInput>;
99
- tasks?: Record<string, TaskInput>;
100
- scenarios?: Record<string, ScenarioInput>;
101
- };
package/dist/index.d.ts CHANGED
@@ -2,5 +2,5 @@ export type { AuthMethod, DeployerConfig, DeployerConfigInput, FilesConfig, Logs
2
2
  export { defineConfig } from './config.js';
3
3
  export { runScenario, runTask } from './runner.js';
4
4
  export { loadConfig, findConfigFile } from './configLoader.js';
5
- export { buildRsyncCommand, downloadSkip, downloadTask } from './defaultTasks.js';
6
- export type { RsyncOptions } from './defaultTasks.js';
5
+ export { buildRsyncCommand, downloadSkip, downloadTask } from './tasks/index.js';
6
+ export type { RsyncOptions } from './tasks/upload.js';
package/dist/index.js CHANGED
@@ -1,4 +1,4 @@
1
1
  export { defineConfig } from './config.js';
2
2
  export { runScenario, runTask } from './runner.js';
3
3
  export { loadConfig, findConfigFile } from './configLoader.js';
4
- export { buildRsyncCommand, downloadSkip, downloadTask } from './defaultTasks.js';
4
+ export { buildRsyncCommand, downloadSkip, downloadTask } from './tasks/index.js';
@@ -0,0 +1,23 @@
1
+ export declare class CustomRenderer {
2
+ static nonTTY: boolean;
3
+ static rendererOptions: {
4
+ pausedTimer: {
5
+ field: (time: any) => string;
6
+ format: () => (text: string | number) => string;
7
+ condition?: boolean | ((args_0: number) => boolean);
8
+ args?: [number];
9
+ };
10
+ };
11
+ static rendererTaskOptions: {};
12
+ private tasks;
13
+ private options;
14
+ private logger;
15
+ private cache;
16
+ constructor(tasks: any, options: any);
17
+ end(): void;
18
+ render(): void;
19
+ private formatTitle;
20
+ renderer(tasks: any[]): void;
21
+ calculate(task: any): void;
22
+ reset(task: any): void;
23
+ }
@@ -0,0 +1,153 @@
1
+ import chalk from 'chalk';
2
+ import { color, LISTR_LOGGER_STDERR_LEVELS, LISTR_LOGGER_STYLE, ListrLogger, ListrLogLevels, ListrTaskEventType, ListrTaskState, PRESET_TIMER, } from 'listr2';
3
+ export class CustomRenderer {
4
+ static nonTTY = true;
5
+ static rendererOptions = {
6
+ pausedTimer: {
7
+ ...PRESET_TIMER,
8
+ field: (time) => `${ListrLogLevels.PAUSED}:${time}`,
9
+ format: () => color.yellowBright,
10
+ },
11
+ };
12
+ static rendererTaskOptions = {};
13
+ tasks;
14
+ options;
15
+ logger;
16
+ cache = {
17
+ rendererOptions: new Map(),
18
+ rendererTaskOptions: new Map(),
19
+ };
20
+ constructor(tasks, options) {
21
+ this.tasks = tasks;
22
+ this.options = {
23
+ ...CustomRenderer.rendererOptions,
24
+ ...options,
25
+ icon: {
26
+ ...LISTR_LOGGER_STYLE.icon,
27
+ ...options?.icon ?? {},
28
+ },
29
+ color: {
30
+ ...LISTR_LOGGER_STYLE.color,
31
+ ...options?.color ?? {},
32
+ },
33
+ };
34
+ this.logger = this.options.logger ?? new ListrLogger({
35
+ useIcons: true,
36
+ toStderr: LISTR_LOGGER_STDERR_LEVELS,
37
+ });
38
+ this.logger.options.icon = this.options.icon;
39
+ this.logger.options.color = this.options.color;
40
+ if (this.options.timestamp) {
41
+ this.logger.options.fields.prefix.unshift(this.options.timestamp);
42
+ }
43
+ }
44
+ end() { }
45
+ render() {
46
+ this.renderer(this.tasks);
47
+ }
48
+ formatTitle(title) {
49
+ return chalk.bgCyan.black(` ${title} `);
50
+ }
51
+ renderer(tasks) {
52
+ tasks.forEach((task) => {
53
+ this.calculate(task);
54
+ task.once(ListrTaskEventType.CLOSED, () => {
55
+ this.reset(task);
56
+ });
57
+ const rendererTaskOptions = this.cache.rendererTaskOptions.get(task.id);
58
+ task.on(ListrTaskEventType.SUBTASK, (subtasks) => {
59
+ this.renderer(subtasks);
60
+ });
61
+ task.on(ListrTaskEventType.STATE, (state) => {
62
+ if (!task.hasTitle())
63
+ return;
64
+ const title = this.formatTitle(task.title);
65
+ if (state === ListrTaskState.STARTED) {
66
+ this.logger.log(ListrLogLevels.STARTED, title);
67
+ }
68
+ // else if (state === ListrTaskState.COMPLETED) {
69
+ // const timer = rendererTaskOptions?.timer;
70
+ // this.logger.log(ListrLogLevels.COMPLETED, title, timer && {
71
+ // suffix: {
72
+ // ...timer,
73
+ // condition: !!task.message?.duration && timer.condition,
74
+ // args: [ task.message.duration ],
75
+ // },
76
+ // });
77
+ // }
78
+ else if (state === ListrTaskState.PROMPT) {
79
+ this.logger.process.hijack();
80
+ task.on(ListrTaskEventType.PROMPT, (prompt) => {
81
+ this.logger.process.toStderr(prompt, false);
82
+ });
83
+ }
84
+ else if (state === ListrTaskState.PROMPT_COMPLETED) {
85
+ task.off(ListrTaskEventType.PROMPT);
86
+ this.logger.process.release();
87
+ }
88
+ });
89
+ task.on(ListrTaskEventType.OUTPUT, (output) => {
90
+ this.logger.log(ListrLogLevels.OUTPUT, output);
91
+ });
92
+ task.on(ListrTaskEventType.MESSAGE, (message) => {
93
+ const title = this.formatTitle(task.title);
94
+ if (message.error) {
95
+ this.logger.log(ListrLogLevels.FAILED, title, {
96
+ suffix: {
97
+ field: `${ListrLogLevels.FAILED}: ${message.error}`,
98
+ format: () => color.red,
99
+ },
100
+ });
101
+ }
102
+ else if (message.skip) {
103
+ process.stdout.write(chalk.gray(`Skipped: ${message.skip}\n`));
104
+ }
105
+ else if (message.rollback) {
106
+ this.logger.log(ListrLogLevels.ROLLBACK, title, {
107
+ suffix: {
108
+ field: `${ListrLogLevels.ROLLBACK}: ${message.rollback}`,
109
+ format: () => color.red,
110
+ },
111
+ });
112
+ }
113
+ else if (message.retry) {
114
+ this.logger.log(ListrLogLevels.RETRY, title, {
115
+ suffix: {
116
+ field: `${ListrLogLevels.RETRY}:${message.retry.count}`,
117
+ format: () => color.red,
118
+ },
119
+ });
120
+ }
121
+ else if (message.paused) {
122
+ const rendererOptions = this.cache.rendererOptions.get(task.id);
123
+ const timer = rendererOptions?.pausedTimer;
124
+ this.logger.log(ListrLogLevels.PAUSED, title, timer && {
125
+ suffix: {
126
+ ...timer,
127
+ condition: !!message?.paused && timer.condition,
128
+ args: [message.paused - Date.now()],
129
+ },
130
+ });
131
+ }
132
+ });
133
+ });
134
+ }
135
+ calculate(task) {
136
+ if (this.cache.rendererOptions.has(task.id) && this.cache.rendererTaskOptions.has(task.id))
137
+ return;
138
+ const rendererOptions = {
139
+ ...this.options,
140
+ ...task.rendererOptions,
141
+ };
142
+ this.cache.rendererOptions.set(task.id, rendererOptions);
143
+ this.cache.rendererTaskOptions.set(task.id, {
144
+ ...CustomRenderer.rendererTaskOptions,
145
+ timer: rendererOptions.timer,
146
+ ...task.rendererTaskOptions,
147
+ });
148
+ }
149
+ reset(task) {
150
+ this.cache.rendererOptions.delete(task.id);
151
+ this.cache.rendererTaskOptions.delete(task.id);
152
+ }
153
+ }
package/dist/runner.d.ts CHANGED
@@ -1,5 +1,9 @@
1
1
  import type { DeployerConfig, ServerConfig, TaskDef } from './def.js';
2
- export declare function resolveServers(config: DeployerConfig, serverNames?: string[]): Array<[string, ServerConfig]>;
3
- export declare function resolveTaskDefs(taskNames: string[], allTasks: Record<string, TaskDef>): Array<[string, TaskDef]>;
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>;
2
+ export declare function resolveServers(config: DeployerConfig, serverNames?: string[]): Record<string, ServerConfig>;
3
+ export declare function resolveTaskDefs(taskNames: string[], allTasks: Record<string, TaskDef>): Record<string, TaskDef>;
4
+ export declare function runScenario(config: DeployerConfig, scenarioName: string, serverNames?: string[], options?: {
5
+ skip?: string[];
6
+ }): Promise<void>;
7
+ export declare function runTask(config: DeployerConfig, taskName: string, serverNames?: string[], options?: {
8
+ skip?: string[];
9
+ }): Promise<void>;
package/dist/runner.js CHANGED
@@ -1,5 +1,5 @@
1
- import chalk from 'chalk';
2
1
  import { Listr } from 'listr2';
2
+ import { CustomRenderer } from './renderer.js';
3
3
  import { spawn } from 'node:child_process';
4
4
  import { createSSHConnection } from './connection.js';
5
5
  import { Exception } from './utils/Exception.js';
@@ -148,32 +148,32 @@ function buildTaskContext(serverName, server, ssh, config) {
148
148
  };
149
149
  }
150
150
  export function resolveServers(config, serverNames) {
151
- const allEntries = Object.entries(config.servers);
152
151
  if (!serverNames || serverNames.length === 0) {
153
- return allEntries;
152
+ return config.servers;
154
153
  }
155
- const result = [];
154
+ const result = {};
156
155
  for (const name of serverNames) {
157
156
  const server = config.servers[name];
158
157
  if (!server) {
159
158
  const available = Object.keys(config.servers).join(', ');
160
159
  throw new Exception(`Server "${name}" not found. Available: ${available}`, 1774742073310);
161
160
  }
162
- result.push([name, server]);
161
+ result[name] = server;
163
162
  }
164
163
  return result;
165
164
  }
166
165
  export function resolveTaskDefs(taskNames, allTasks) {
167
- return taskNames.map(name => {
166
+ return Object.fromEntries(taskNames
167
+ .map(name => {
168
168
  const def = allTasks[name];
169
169
  if (!def) {
170
170
  const available = Object.keys(allTasks).join(', ');
171
171
  throw new Exception(`Task "${name}" not found. Available: ${available}`, 1774742082083);
172
172
  }
173
173
  return [name, def];
174
- });
174
+ }));
175
175
  }
176
- function buildServerListr(serverName, server, config, tasks) {
176
+ function buildServerListr(serverName, server, config, tasks, skipTasks = []) {
177
177
  return new Listr([
178
178
  {
179
179
  task: async (ctx) => {
@@ -184,17 +184,21 @@ function buildServerListr(serverName, server, config, tasks) {
184
184
  ctx.ph = buildPlaceholders(serverName, server);
185
185
  },
186
186
  },
187
- ...tasks.map(([_key, taskDef]) => ({
188
- title: chalk.bgCyan.black(` ${taskDef.name} `),
189
- skip: taskDef.skip
190
- ? (ctx) => {
191
- ctx.taskCtx.taskConfig = taskDef.config;
192
- return taskDef.skip(ctx.taskCtx, ctx.ph);
187
+ ...Object.entries(tasks)
188
+ .map(([key, taskDef]) => ({
189
+ title: taskDef.name,
190
+ skip: (ctx) => {
191
+ if (skipTasks.includes(key)) {
192
+ return 'Skipped via --skip';
193
193
  }
194
- : undefined,
195
- task: async (ctx) => {
196
194
  ctx.taskCtx.taskConfig = taskDef.config;
197
- return taskDef.task(ctx.taskCtx, ctx.ph);
195
+ return taskDef.skip
196
+ ? taskDef.skip(ctx.taskCtx, ctx.ph)
197
+ : false;
198
+ },
199
+ task: async (ctx, task) => {
200
+ ctx.taskCtx.taskConfig = taskDef.config;
201
+ return taskDef.task(ctx.taskCtx, ctx.ph, task);
198
202
  },
199
203
  })),
200
204
  {
@@ -206,10 +210,10 @@ function buildServerListr(serverName, server, config, tasks) {
206
210
  },
207
211
  ], {
208
212
  concurrent: false,
209
- renderer: 'simple',
213
+ renderer: CustomRenderer,
210
214
  });
211
215
  }
212
- export async function runScenario(config, scenarioName, serverNames) {
216
+ export async function runScenario(config, scenarioName, serverNames, options = {}) {
213
217
  const scenarioDef = config.scenarios?.[scenarioName];
214
218
  if (!scenarioDef) {
215
219
  const available = Object.keys(config.scenarios ?? {}).join(', ') || 'none';
@@ -218,16 +222,18 @@ export async function runScenario(config, scenarioName, serverNames) {
218
222
  const allTasks = config.tasks ?? {};
219
223
  const tasks = resolveTaskDefs(scenarioDef.tasks, allTasks);
220
224
  const servers = resolveServers(config, serverNames);
221
- const listr = new Listr(servers.map(([name, server]) => ({
222
- title: chalk.bgMagenta.black(` ${name} (${server.host}) `),
223
- task: () => buildServerListr(name, server, config, tasks),
225
+ const skipTasks = options.skip ?? [];
226
+ const listr = new Listr(Object.entries(servers)
227
+ .map(([name, server]) => ({
228
+ title: `${scenarioDef.name} to ${name} (${server.host})`,
229
+ task: () => buildServerListr(name, server, config, tasks, skipTasks),
224
230
  })), {
225
231
  concurrent: false,
226
- renderer: 'simple',
232
+ renderer: CustomRenderer,
227
233
  });
228
234
  await listr.run();
229
235
  }
230
- export async function runTask(config, taskName, serverNames) {
236
+ export async function runTask(config, taskName, serverNames, options = {}) {
231
237
  const allTasks = config.tasks ?? {};
232
238
  const taskDef = allTasks[taskName];
233
239
  if (!taskDef) {
@@ -235,12 +241,14 @@ export async function runTask(config, taskName, serverNames) {
235
241
  throw new Exception(`Task "${taskName}" not found. Available: ${available}`, 1774742100356);
236
242
  }
237
243
  const servers = resolveServers(config, serverNames);
238
- const listr = new Listr(servers.map(([name, server]) => ({
239
- title: chalk.bgMagenta.black(` ${name} (${server.host}) `),
240
- task: () => buildServerListr(name, server, config, [[taskName, taskDef]]),
244
+ const skipTasks = options.skip ?? [];
245
+ const listr = new Listr(Object.entries(servers)
246
+ .map(([name, server]) => ({
247
+ title: `${taskDef.name} to ${name} (${server.host})`,
248
+ task: () => buildServerListr(name, server, config, { [taskName]: taskDef }, skipTasks),
241
249
  })), {
242
250
  concurrent: false,
243
- renderer: 'simple',
251
+ renderer: CustomRenderer,
244
252
  });
245
253
  await listr.run();
246
254
  }
@@ -0,0 +1,2 @@
1
+ import type { TaskFn } from '../def.js';
2
+ export declare const clearTargetTask: TaskFn;
@@ -0,0 +1,15 @@
1
+ import { ListrEnquirerPromptAdapter } from '@listr2/prompt-adapter-enquirer';
2
+ import chalk from 'chalk';
3
+ export const clearTargetTask = async (ctx, ph, task) => {
4
+ const confirmed = await task.prompt(ListrEnquirerPromptAdapter).run({
5
+ type: 'Confirm',
6
+ message: chalk.red(`Remove entire deploy path ${ph.deployPath} on ${ctx.server.host}?`),
7
+ initial: false,
8
+ });
9
+ console.log();
10
+ if (!confirmed) {
11
+ task.skip('User cancelled');
12
+ return;
13
+ }
14
+ await ctx.run(`rm -rf ${ph.deployPath}`);
15
+ };
@@ -0,0 +1,3 @@
1
+ import type { TaskFn, TaskSkipFn } from '../def.js';
2
+ export declare const downloadSkip: TaskSkipFn;
3
+ export declare const downloadTask: TaskFn;
@@ -0,0 +1,27 @@
1
+ import chalk from 'chalk';
2
+ import path from 'node:path';
3
+ import { Exception } from '../utils/index.js';
4
+ import { buildRsyncCommand } from './upload.js';
5
+ export const downloadSkip = (ctx) => {
6
+ const files = ctx.taskConfig;
7
+ return !files
8
+ ? 'No files configuration provided in task config'
9
+ : false;
10
+ };
11
+ export const downloadTask = async (ctx, ph) => {
12
+ const files = ctx.taskConfig;
13
+ if (!files) {
14
+ throw new Exception('No files configuration provided in task config', 1784523741234);
15
+ }
16
+ const localBase = files.basePath?.startsWith('/')
17
+ ? files.basePath
18
+ : path.resolve(ctx.config.rootDir, files.basePath ?? '.');
19
+ const remotePath = ph.deployPath;
20
+ const source = `${ctx.server.username}@${ctx.server.host}:${remotePath}/`;
21
+ const dest = localBase.endsWith('/') ? localBase : localBase + '/';
22
+ const command = buildRsyncCommand(ctx.server, source, dest, files, {
23
+ delete: false,
24
+ });
25
+ console.log(chalk.grey(command));
26
+ await ctx.runLocal(command);
27
+ };
@@ -0,0 +1,6 @@
1
+ import type { ScenarioDef, TaskDef } from '../def.js';
2
+ export { buildRsyncCommand } from './upload.js';
3
+ export type { RsyncOptions } from './upload.js';
4
+ export { downloadSkip, downloadTask } from './download.js';
5
+ export declare const defaultTasks: Record<string, TaskDef>;
6
+ export declare const defaultScenarios: Record<string, ScenarioDef>;
@@ -0,0 +1,76 @@
1
+ import { clearTargetTask } from './clearTarget.js';
2
+ import { downloadSkip, downloadTask } from './download.js';
3
+ import { installPackagesSkip, installPackagesTask } from './installPackages.js';
4
+ import { printDeploymentTask } from './printDeployment.js';
5
+ import { printLogsDockerSkip, printLogsDockerTask, printLogsPm2Skip, printLogsPm2Task } from './printLogs.js';
6
+ import { setupDockerSkip, setupDockerTask } from './setupDocker.js';
7
+ import { setupPm2Skip, setupPm2Task } from './setupPm2.js';
8
+ import { symlinksSkip, symlinksTask } from './symlinks.js';
9
+ import { uploadSkip, uploadTask } from './upload.js';
10
+ export { buildRsyncCommand } from './upload.js';
11
+ export { downloadSkip, downloadTask } from './download.js';
12
+ export const defaultTasks = {
13
+ 'clear:target': {
14
+ name: 'Clear target',
15
+ task: clearTargetTask,
16
+ },
17
+ upload: {
18
+ name: 'Upload files',
19
+ skip: uploadSkip,
20
+ task: uploadTask,
21
+ },
22
+ download: {
23
+ name: 'Download files',
24
+ skip: downloadSkip,
25
+ task: downloadTask,
26
+ },
27
+ symlinks: {
28
+ name: 'Create symlinks',
29
+ skip: symlinksSkip,
30
+ task: symlinksTask,
31
+ },
32
+ 'install:packages': {
33
+ name: 'Install packages',
34
+ skip: installPackagesSkip,
35
+ task: installPackagesTask,
36
+ },
37
+ 'setup:pm2': {
38
+ name: 'PM2 setup',
39
+ skip: setupPm2Skip,
40
+ task: setupPm2Task,
41
+ },
42
+ 'setup:docker': {
43
+ name: 'Docker Compose setup',
44
+ skip: setupDockerSkip,
45
+ task: setupDockerTask,
46
+ },
47
+ 'print:deployment': {
48
+ name: 'Print deployment info',
49
+ task: printDeploymentTask,
50
+ },
51
+ 'print:logs:pm2': {
52
+ name: 'Print PM2 logs',
53
+ skip: printLogsPm2Skip,
54
+ task: printLogsPm2Task,
55
+ },
56
+ 'print:logs:docker': {
57
+ name: 'Print docker logs',
58
+ skip: printLogsDockerSkip,
59
+ task: printLogsDockerTask,
60
+ },
61
+ };
62
+ export const defaultScenarios = {
63
+ deploy: {
64
+ name: 'Deploy',
65
+ tasks: [
66
+ 'upload',
67
+ 'symlinks',
68
+ 'install:packages',
69
+ 'setup:pm2',
70
+ 'setup:docker',
71
+ 'print:deployment',
72
+ 'print:logs:pm2',
73
+ 'print:logs:docker',
74
+ ],
75
+ },
76
+ };
@@ -0,0 +1,3 @@
1
+ import type { TaskFn, TaskSkipFn } from '../def.js';
2
+ export declare const installPackagesSkip: TaskSkipFn;
3
+ export declare const installPackagesTask: TaskFn;
@@ -0,0 +1,38 @@
1
+ import { Exception } from '../utils/index.js';
2
+ export const installPackagesSkip = (ctx) => {
3
+ if (ctx.server.packageManager !== undefined) {
4
+ return ctx.server.packageManager === false
5
+ ? 'Package manager disabled for server'
6
+ : false;
7
+ }
8
+ if (ctx.config.packageManager !== undefined) {
9
+ return ctx.config.packageManager === false
10
+ ? 'Package manager disabled in config'
11
+ : false;
12
+ }
13
+ return false;
14
+ };
15
+ export const installPackagesTask = async (ctx) => {
16
+ const config = {
17
+ manager: 'npm',
18
+ productionOnly: true,
19
+ ...ctx.config.packageManager,
20
+ ...ctx.server.packageManager,
21
+ };
22
+ let cmd = `${config.manager} install`;
23
+ if (config.productionOnly) {
24
+ if (config.manager === 'npm') {
25
+ cmd += ' --omit=dev';
26
+ }
27
+ else if (config.manager === 'yarn') {
28
+ cmd += ' --production';
29
+ }
30
+ else if (config.manager === 'pnpm') {
31
+ cmd += ' --prod';
32
+ }
33
+ else {
34
+ throw new Exception(`Unsupported package manager "${config.manager}"`, 1774823752134);
35
+ }
36
+ }
37
+ await ctx.run(cmd);
38
+ };
@@ -0,0 +1,5 @@
1
+ import type { TaskFn, TaskSkipFn } from '../def.js';
2
+ export declare const printLogsPm2Skip: TaskSkipFn;
3
+ export declare const printLogsDockerSkip: TaskSkipFn;
4
+ export declare const printLogsPm2Task: TaskFn;
5
+ export declare const printLogsDockerComposeTask: TaskFn;
@@ -0,0 +1,69 @@
1
+ import chalk from 'chalk';
2
+ import { buildDockerComposeTestCmd } from './setupDocker.js';
3
+ export const printLogsPm2Skip = async (ctx) => {
4
+ if (ctx.config.pm2 === false
5
+ || ctx.config.pm2.logs === false) {
6
+ return 'Logs disabled';
7
+ }
8
+ const hasPm2 = await ctx.test('test -f pm2.config.*');
9
+ if (!hasPm2) {
10
+ return 'No PM2 detected';
11
+ }
12
+ return false;
13
+ };
14
+ export const printLogsDockerSkip = async (ctx) => {
15
+ if (ctx.config.dockerCompose === false
16
+ || ctx.config.dockerCompose.logs === false) {
17
+ return 'Logs disabled';
18
+ }
19
+ const testCmd = buildDockerComposeTestCmd(ctx.config.dockerCompose);
20
+ const hasDocker = await ctx.test(testCmd);
21
+ if (!hasDocker) {
22
+ return 'No Docker Compose detected';
23
+ }
24
+ return false;
25
+ };
26
+ export const printLogsPm2Task = async (ctx) => {
27
+ if (ctx.config.pm2 === false
28
+ || ctx.config.pm2.logs === false) {
29
+ return;
30
+ }
31
+ const logsConfig = {
32
+ time: 3,
33
+ lines: 25,
34
+ ...ctx.config.pm2.logs,
35
+ };
36
+ const hasPm2 = await ctx.test('test -f pm2.config.*');
37
+ if (hasPm2) {
38
+ const pm2ConfigRaw = await ctx.run('cat pm2.config.*', { printOutput: false });
39
+ const nameMatch = pm2ConfigRaw.stdout.match(/name: ['"](?<name>.+?)['"]/);
40
+ const name = nameMatch.groups?.name ?? 'all';
41
+ console.log(chalk.cyan(`Streaming PM2 logs for ${logsConfig.time}s...`));
42
+ await ctx.run(`timeout ${logsConfig.time} pm2 logs --lines=${logsConfig.lines} "${name}" || true`, {
43
+ printOutput: true,
44
+ ignoreError: true,
45
+ });
46
+ }
47
+ };
48
+ export const printLogsDockerComposeTask = async (ctx) => {
49
+ if (ctx.config.dockerCompose === false
50
+ || ctx.config.dockerCompose.logs === false) {
51
+ return;
52
+ }
53
+ const logsConfig = {
54
+ time: 3,
55
+ lines: 25,
56
+ ...ctx.config.dockerCompose.logs,
57
+ };
58
+ const testCmd = buildDockerComposeTestCmd(ctx.config.dockerCompose);
59
+ const hasDocker = await ctx.test(testCmd);
60
+ if (hasDocker) {
61
+ const configFiles = ctx.config.dockerCompose.configFiles ?? [];
62
+ const options = configFiles.map(f => `-f ${f}`).join(' ');
63
+ console.log(chalk.cyan(`Streaming Docker Compose logs for ${logsConfig.time}s...`));
64
+ await ctx.run(`timeout ${logsConfig.time} docker compose ${options} logs --tail=${logsConfig.lines} -f || true`, {
65
+ printOutput: true,
66
+ ignoreError: true,
67
+ });
68
+ }
69
+ };
@@ -0,0 +1,2 @@
1
+ import type { TaskFn } from '../def.js';
2
+ export declare const printDeploymentTask: TaskFn;
@@ -0,0 +1,8 @@
1
+ import chalk from 'chalk';
2
+ export const printDeploymentTask = async (ctx, ph) => {
3
+ await ctx.run('date');
4
+ console.log(chalk.cyan('Deployment directory'), ph.deployPath);
5
+ await ctx.run('ls -la .');
6
+ console.log(chalk.cyan('Directory size'));
7
+ await ctx.run('du -hd 1 .');
8
+ };
@@ -0,0 +1,5 @@
1
+ import type { TaskFn, TaskSkipFn } from '../def.js';
2
+ export declare const printLogsPm2Skip: TaskSkipFn;
3
+ export declare const printLogsDockerSkip: TaskSkipFn;
4
+ export declare const printLogsPm2Task: TaskFn;
5
+ export declare const printLogsDockerTask: TaskFn;
@@ -0,0 +1,69 @@
1
+ import chalk from 'chalk';
2
+ import { buildDockerComposeTestCmd } from './setupDocker.js';
3
+ export const printLogsPm2Skip = async (ctx) => {
4
+ if (ctx.config.pm2 === false
5
+ || ctx.config.pm2.logs === false) {
6
+ return 'Logs disabled';
7
+ }
8
+ const hasPm2 = await ctx.test('test -f pm2.config.*');
9
+ if (!hasPm2) {
10
+ return 'No PM2 detected';
11
+ }
12
+ return false;
13
+ };
14
+ export const printLogsDockerSkip = async (ctx) => {
15
+ if (ctx.config.dockerCompose === false
16
+ || ctx.config.dockerCompose.logs === false) {
17
+ return 'Logs disabled';
18
+ }
19
+ const testCmd = buildDockerComposeTestCmd(ctx.config.dockerCompose);
20
+ const hasDocker = await ctx.test(testCmd);
21
+ if (!hasDocker) {
22
+ return 'No Docker Compose detected';
23
+ }
24
+ return false;
25
+ };
26
+ export const printLogsPm2Task = async (ctx) => {
27
+ if (ctx.config.pm2 === false
28
+ || ctx.config.pm2.logs === false) {
29
+ return;
30
+ }
31
+ const logsConfig = {
32
+ time: 3,
33
+ lines: 25,
34
+ ...ctx.config.pm2.logs,
35
+ };
36
+ const hasPm2 = await ctx.test('test -f pm2.config.*');
37
+ if (hasPm2) {
38
+ const pm2ConfigRaw = await ctx.run('cat pm2.config.*', { printOutput: false });
39
+ const nameMatch = pm2ConfigRaw.stdout.match(/name: ['"](?<name>.+?)['"]/);
40
+ const name = nameMatch.groups?.name ?? 'all';
41
+ console.log(chalk.cyan(`Streaming PM2 logs for ${logsConfig.time}s...`));
42
+ await ctx.run(`timeout ${logsConfig.time} pm2 logs --lines=${logsConfig.lines} "${name}" || true`, {
43
+ printOutput: true,
44
+ ignoreError: true,
45
+ });
46
+ }
47
+ };
48
+ export const printLogsDockerTask = async (ctx) => {
49
+ if (ctx.config.dockerCompose === false
50
+ || ctx.config.dockerCompose.logs === false) {
51
+ return;
52
+ }
53
+ const logsConfig = {
54
+ time: 3,
55
+ lines: 25,
56
+ ...ctx.config.dockerCompose.logs,
57
+ };
58
+ const testCmd = buildDockerComposeTestCmd(ctx.config.dockerCompose);
59
+ const hasDocker = await ctx.test(testCmd);
60
+ if (hasDocker) {
61
+ const configFiles = ctx.config.dockerCompose.configFiles ?? [];
62
+ const options = configFiles.map(f => `-f ${f}`).join(' ');
63
+ console.log(chalk.cyan(`Streaming Docker Compose logs for ${logsConfig.time}s...`));
64
+ await ctx.run(`timeout ${logsConfig.time} docker compose ${options} logs --tail=${logsConfig.lines} -f || true`, {
65
+ printOutput: true,
66
+ ignoreError: true,
67
+ });
68
+ }
69
+ };
@@ -0,0 +1,4 @@
1
+ import type { DockerComposeConfig, TaskFn, TaskSkipFn } from '../def.js';
2
+ export declare function buildDockerComposeTestCmd(dockerComposeConfig: DockerComposeConfig | false): string;
3
+ export declare const setupDockerSkip: TaskSkipFn;
4
+ export declare const setupDockerTask: TaskFn;
@@ -0,0 +1,33 @@
1
+ export function buildDockerComposeTestCmd(dockerComposeConfig) {
2
+ if (dockerComposeConfig === false) {
3
+ return 'false';
4
+ }
5
+ const configFiles = dockerComposeConfig.configFiles ?? [
6
+ 'docker-compose.yml',
7
+ 'docker-compose.yaml',
8
+ 'compose.yml',
9
+ 'compose.yaml',
10
+ ];
11
+ const testCmdPart = configFiles.map(f => `-f ${f}`);
12
+ return `test ${testCmdPart.join(' -o ')}`;
13
+ }
14
+ export const setupDockerSkip = async (ctx) => {
15
+ if (ctx.config.dockerCompose === false) {
16
+ return 'Docker Compose disabled';
17
+ }
18
+ const testCmd = buildDockerComposeTestCmd(ctx.config.dockerCompose);
19
+ const composeExists = await ctx.test(testCmd);
20
+ if (!composeExists) {
21
+ return 'Docker Compose config not found';
22
+ }
23
+ return false;
24
+ };
25
+ export const setupDockerTask = async (ctx) => {
26
+ if (ctx.config.dockerCompose === false) {
27
+ return;
28
+ }
29
+ const configFiles = ctx.config.dockerCompose?.configFiles ?? [];
30
+ const options = configFiles.map(f => `-f ${f}`).join(' ');
31
+ await ctx.run(`docker compose ${options} down --remove-orphans`);
32
+ await ctx.run(`docker compose ${options} up -d --build`);
33
+ };
@@ -0,0 +1,3 @@
1
+ import type { TaskFn, TaskSkipFn } from '../def.js';
2
+ export declare const setupPm2Skip: TaskSkipFn;
3
+ export declare const setupPm2Task: TaskFn;
@@ -0,0 +1,14 @@
1
+ export const setupPm2Skip = async (ctx) => {
2
+ if (ctx.config.pm2 === false) {
3
+ return 'PM2 disabled';
4
+ }
5
+ const pm2ConfigExists = await ctx.test('test -f pm2.config.*');
6
+ if (!pm2ConfigExists) {
7
+ return 'PM2 config not found';
8
+ }
9
+ return false;
10
+ };
11
+ export const setupPm2Task = async (ctx) => {
12
+ await ctx.run('pm2 start pm2.config.* --update-env');
13
+ await ctx.run('pm2 save');
14
+ };
@@ -0,0 +1,3 @@
1
+ import type { TaskFn, TaskSkipFn } from '../def.js';
2
+ export declare const symlinksSkip: TaskSkipFn;
3
+ export declare const symlinksTask: TaskFn;
@@ -0,0 +1,18 @@
1
+ export const symlinksSkip = (ctx) => {
2
+ const symlinks = ctx.config.symlinks;
3
+ return !symlinks || symlinks.length === 0
4
+ ? 'No symlinks defined in config'
5
+ : false;
6
+ };
7
+ export const symlinksTask = async (ctx, ph) => {
8
+ const symlinks = ctx.config.symlinks;
9
+ for (const link of symlinks) {
10
+ const target = link.target.startsWith('/')
11
+ ? link.target
12
+ : `${ph.deployPath}/${link.target}`;
13
+ const path = link.path.startsWith('/')
14
+ ? link.path
15
+ : `${ph.deployPath}/${link.path}`;
16
+ await ctx.run(`ln -sfn ${target} ${path}`);
17
+ }
18
+ };
@@ -0,0 +1,7 @@
1
+ import type { FilesConfig, ServerConfig, TaskFn, TaskSkipFn } from '../def.js';
2
+ export type RsyncOptions = {
3
+ delete?: boolean;
4
+ };
5
+ export declare function buildRsyncCommand(server: ServerConfig, source: string, dest: string, files: FilesConfig, options?: RsyncOptions): string;
6
+ export declare const uploadSkip: TaskSkipFn;
7
+ export declare const uploadTask: TaskFn;
@@ -0,0 +1,54 @@
1
+ import chalk from 'chalk';
2
+ import path from 'node:path';
3
+ export function buildRsyncCommand(server, source, dest, files, options = {}) {
4
+ const { delete: useDelete = true, } = options;
5
+ const args = ['rsync', '-avz', '--progress=info2'];
6
+ if (useDelete) {
7
+ args.push('--delete');
8
+ }
9
+ // ssh shell
10
+ const sshParts = ['ssh'];
11
+ if (server.port && server.port !== 22) {
12
+ sshParts.push(`-p ${server.port}`);
13
+ }
14
+ if (server.authMethod === 'key' && server.privateKey) {
15
+ sshParts.push(`-i ${server.privateKey}`);
16
+ }
17
+ sshParts.push('-o StrictHostKeyChecking=no');
18
+ args.push('-e', `"${sshParts.join(' ')}"`);
19
+ // include/exclude
20
+ if (files.exclude) {
21
+ for (const pattern of files.exclude) {
22
+ args.push(`--exclude="${pattern}"`);
23
+ }
24
+ }
25
+ if (files.include) {
26
+ for (const pattern of files.include) {
27
+ args.push(`--include="${pattern}"`);
28
+ }
29
+ args.push('--exclude="*"');
30
+ }
31
+ args.push(source, dest);
32
+ return args.join(' ');
33
+ }
34
+ export const uploadSkip = (ctx) => {
35
+ const files = ctx.config.files;
36
+ return !files
37
+ ? 'No files configuration defined'
38
+ : false;
39
+ };
40
+ export const uploadTask = async (ctx, ph) => {
41
+ const files = ctx.config.files;
42
+ const localBase = files.basePath?.startsWith('/')
43
+ ? files.basePath
44
+ : path.resolve(ctx.config.rootDir, files.basePath ?? '.');
45
+ const remotePath = ph.deployPath;
46
+ const dest = `${ctx.server.username}@${ctx.server.host}:${remotePath}`;
47
+ const source = localBase.endsWith('/') ? localBase : localBase + '/';
48
+ await ctx.run(`mkdir -p ${remotePath}`);
49
+ const command = buildRsyncCommand(ctx.server, source, dest, files, {
50
+ delete: true,
51
+ });
52
+ console.log(chalk.grey(command));
53
+ await ctx.runLocal(command);
54
+ };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "d3ployer",
3
- "version": "0.0.10",
3
+ "version": "0.0.11",
4
4
  "license": "MIT",
5
5
  "type": "module",
6
6
  "bin": {
@@ -25,11 +25,12 @@
25
25
  "prepare": "husky"
26
26
  },
27
27
  "dependencies": {
28
- "@inquirer/confirm": "^6.0.10",
28
+ "@listr2/prompt-adapter-enquirer": "^4.2.1",
29
29
  "chalk": "^5.6.2",
30
30
  "commander": "^14.0.3",
31
+ "enquirer": "^2.4.1",
31
32
  "listr2": "^10.2.1",
32
- "lodash-es": "^4.17.23",
33
+ "lodash-es": "^4.18.1",
33
34
  "ssh2-promise": "^1.0.3"
34
35
  },
35
36
  "devDependencies": {
@@ -38,7 +39,6 @@
38
39
  "@tsconfig/node24": "^24.0.4",
39
40
  "@types/chai": "^5.2.3",
40
41
  "@types/chai-as-promised": "^8.0.2",
41
- "@types/lodash-es": "^4.17.12",
42
42
  "@types/mocha": "^10.0.10",
43
43
  "@types/node": "^24.10.13",
44
44
  "c8": "^10.1.3",