d3ployer 0.0.9 → 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,39 +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
- return {
68
+ return defaultsDeep({
72
69
  rootDir: '',
73
70
  ...input,
71
+ servers,
72
+ tasks,
73
+ scenarios,
74
+ }, {
74
75
  packageManager: {
75
76
  manager: 'npm',
76
77
  productionOnly: true,
77
- ...input.packageManager,
78
78
  },
79
- servers,
80
- tasks,
81
- scenarios,
82
- };
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
+ });
83
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,9 +28,34 @@ export interface SymlinkConfig {
27
28
  path: string;
28
29
  target: string;
29
30
  }
31
+ export type ConfigOrDisable<T> = T | false;
30
32
  export interface LogsConfig {
31
33
  time?: number;
34
+ lines?: number;
32
35
  }
36
+ export type Pm2Config = {
37
+ logs: ConfigOrDisable<LogsConfig>;
38
+ };
39
+ export type DockerComposeConfig = {
40
+ configFiles: string[];
41
+ logs: ConfigOrDisable<LogsConfig>;
42
+ };
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>;
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
+ };
33
59
  export interface Placeholders {
34
60
  serverName: string;
35
61
  deployPath: string;
@@ -57,7 +83,7 @@ export interface TaskContext {
57
83
  test: (cmd: string) => Promise<boolean>;
58
84
  taskConfig?: any;
59
85
  }
60
- export type TaskFn = (ctx: TaskContext, ph: Placeholders) => Promise<void>;
86
+ export type TaskFn = (ctx: TaskContext, ph: Placeholders, task: ListrTaskWrapper<any, any, any>) => Promise<void>;
61
87
  export type TaskSkipFn = (ctx: TaskContext, ph: Placeholders) => Promise<boolean | string> | boolean | string;
62
88
  export interface TaskDef {
63
89
  name: string;
@@ -79,20 +105,3 @@ export type ScenarioInput = string[] | {
79
105
  name: string;
80
106
  tasks: string[];
81
107
  };
82
- export interface DeployerConfig {
83
- rootDir: string;
84
- servers: Record<string, ServerConfig>;
85
- files?: FilesConfig;
86
- symlinks?: SymlinkConfig[];
87
- packageManager?: PackageManagerConfig | false;
88
- pm2?: boolean;
89
- dockerCompose?: boolean;
90
- logs?: LogsConfig | false;
91
- tasks?: Record<string, TaskDef>;
92
- scenarios?: Record<string, ScenarioDef>;
93
- }
94
- export type DeployerConfigInput = Omit<DeployerConfig, 'servers' | 'rootDir' | 'tasks' | 'scenarios'> & {
95
- servers: Record<string, ServerConfigInput>;
96
- tasks?: Record<string, TaskInput>;
97
- scenarios?: Record<string, ScenarioInput>;
98
- };
@@ -146,18 +146,38 @@ const pm2SetupTask = async (ctx) => {
146
146
  await ctx.run('pm2 start pm2.config.* --update-env');
147
147
  await ctx.run('pm2 save');
148
148
  };
149
+ function buildDockerComposeTestCmd(dockerComposeConfig) {
150
+ if (dockerComposeConfig === false) {
151
+ return 'false';
152
+ }
153
+ const configFiles = dockerComposeConfig.configFiles ?? [
154
+ 'docker-compose.yml',
155
+ 'docker-compose.yaml',
156
+ 'compose.yml',
157
+ 'compose.yaml',
158
+ ];
159
+ const testCmdPart = configFiles.map(f => `-f ${f}`);
160
+ return `test ${testCmdPart.join(' -o ')}`;
161
+ }
149
162
  const dockerSetupSkip = async (ctx) => {
150
163
  if (ctx.config.dockerCompose === false) {
151
164
  return 'Docker Compose disabled';
152
165
  }
153
- const composeExists = await ctx.test('test -f docker-compose.yml -o -f docker-compose.yaml -o -f compose.yml -o -f compose.yaml');
166
+ const testCmd = buildDockerComposeTestCmd(ctx.config.dockerCompose);
167
+ const composeExists = await ctx.test(testCmd);
154
168
  if (!composeExists) {
155
169
  return 'Docker Compose config not found';
156
170
  }
157
171
  return false;
158
172
  };
159
173
  const dockerSetupTask = async (ctx) => {
160
- await ctx.run('docker compose up -d --build --remove-orphans');
174
+ if (ctx.config.dockerCompose === false) {
175
+ return;
176
+ }
177
+ const configFiles = ctx.config.dockerCompose?.configFiles ?? [];
178
+ const options = configFiles.map(f => `-f ${f}`).join(' ');
179
+ await ctx.run(`docker compose ${options} down --remove-orphans`);
180
+ await ctx.run(`docker compose ${options} up -d --build`);
161
181
  };
162
182
  const clearTargetTask = async (ctx, ph) => {
163
183
  const confirmed = await confirm({
@@ -177,19 +197,22 @@ const printDeploymentTask = async (ctx, ph) => {
177
197
  console.log(chalk.cyan('Directory size'));
178
198
  await ctx.run('du -hd 1 .');
179
199
  };
180
- const streamLogsSkip = async (ctx) => {
200
+ const logsStreamSkip = async (ctx) => {
181
201
  if (ctx.config.logs === false) {
182
202
  return 'Logs streaming disabled';
183
203
  }
184
204
  const hasPm2 = ctx.config.pm2 !== false && await ctx.test('test -f pm2.config.*');
185
- const hasDocker = ctx.config.dockerCompose !== false
186
- && await ctx.test('test -f docker-compose.yml -o -f docker-compose.yaml -o -f compose.yml -o -f compose.yaml');
205
+ let hasDocker = false;
206
+ if (ctx.config.dockerCompose) {
207
+ const testCmd = buildDockerComposeTestCmd(ctx.config.dockerCompose);
208
+ hasDocker = await ctx.test(testCmd);
209
+ }
187
210
  if (!hasPm2 && !hasDocker) {
188
211
  return 'No PM2 or Docker Compose detected';
189
212
  }
190
213
  return false;
191
214
  };
192
- const streamLogsTask = async (ctx) => {
215
+ const logsStreamTask = async (ctx) => {
193
216
  const logsConfig = {
194
217
  time: 3,
195
218
  ...ctx.config.logs,
@@ -197,8 +220,11 @@ const streamLogsTask = async (ctx) => {
197
220
  const time = logsConfig.time;
198
221
  const hasPm2 = ctx.config.pm2 !== false
199
222
  && await ctx.test('test -f pm2.config.*');
200
- const hasDocker = ctx.config.dockerCompose !== false
201
- && await ctx.test('test -f docker-compose.yml -o -f docker-compose.yaml -o -f compose.yml -o -f compose.yaml');
223
+ let hasDocker = false;
224
+ if (ctx.config.dockerCompose !== false) {
225
+ const testCmd = buildDockerComposeTestCmd(ctx.config.dockerCompose);
226
+ hasDocker = await ctx.test(testCmd);
227
+ }
202
228
  if (hasPm2) {
203
229
  const pm2ConfigRaw = await ctx.run('cat pm2.config.*', { printOutput: false });
204
230
  const nameMatch = pm2ConfigRaw.stdout.match(/name: ['"](?<name>.+?)['"]/);
@@ -206,9 +232,11 @@ const streamLogsTask = async (ctx) => {
206
232
  console.log(chalk.cyan(`Streaming PM2 logs for ${time}s...`));
207
233
  await ctx.run(`timeout ${time} pm2 logs "${name}" || true`, { printOutput: true, ignoreError: true });
208
234
  }
209
- else if (hasDocker) {
235
+ else if (hasDocker && ctx.config.dockerCompose) {
236
+ const configFiles = ctx.config.dockerCompose.configFiles ?? [];
237
+ const options = configFiles.map(f => `-f ${f}`).join(' ');
210
238
  console.log(chalk.cyan(`Streaming Docker Compose logs for ${time}s...`));
211
- await ctx.run(`timeout ${time} docker compose logs --tail=50 -f || true`, {
239
+ await ctx.run(`timeout ${time} docker compose ${options} logs --tail=10 -f || true`, {
212
240
  printOutput: true,
213
241
  ignoreError: true,
214
242
  });
@@ -253,10 +281,10 @@ export const defaultTasks = {
253
281
  name: 'Print deployment info',
254
282
  task: printDeploymentTask,
255
283
  },
256
- streamLogs: {
257
- name: 'Stream logs',
258
- skip: streamLogsSkip,
259
- task: streamLogsTask,
284
+ logsStream: {
285
+ name: 'Logs stream',
286
+ skip: logsStreamSkip,
287
+ task: logsStreamTask,
260
288
  },
261
289
  };
262
290
  export const defaultScenarios = {
@@ -269,7 +297,7 @@ export const defaultScenarios = {
269
297
  'pm2:setup',
270
298
  'docker:setup',
271
299
  'print:deployment',
272
- 'stream:logs',
300
+ 'logs:stream',
273
301
  ],
274
302
  },
275
303
  };
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,39 +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
- const listrOptions = {
177
- concurrent: false,
178
- renderer: 'simple',
179
- rendererOptions: {
180
- clearOutput: true,
181
- },
182
- };
183
- function buildServerListr(serverName, server, config, tasks) {
176
+ function buildServerListr(serverName, server, config, tasks, skipTasks = []) {
184
177
  return new Listr([
185
178
  {
186
179
  task: async (ctx) => {
@@ -191,19 +184,22 @@ function buildServerListr(serverName, server, config, tasks) {
191
184
  ctx.ph = buildPlaceholders(serverName, server);
192
185
  },
193
186
  },
194
- ...tasks.map(([_key, taskDef]) => ({
195
- title: chalk.bgCyan.black(` ${taskDef.name} `),
196
- skip: taskDef.skip
197
- ? (ctx) => {
198
- ctx.taskCtx.taskConfig = taskDef.config;
199
- 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';
200
193
  }
201
- : undefined,
202
- task: async (ctx) => {
203
194
  ctx.taskCtx.taskConfig = taskDef.config;
204
- 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);
205
202
  },
206
- options: listrOptions,
207
203
  })),
208
204
  {
209
205
  task: async (ctx) => {
@@ -212,9 +208,12 @@ function buildServerListr(serverName, server, config, tasks) {
212
208
  }
213
209
  },
214
210
  },
215
- ], listrOptions);
211
+ ], {
212
+ concurrent: false,
213
+ renderer: CustomRenderer,
214
+ });
216
215
  }
217
- export async function runScenario(config, scenarioName, serverNames) {
216
+ export async function runScenario(config, scenarioName, serverNames, options = {}) {
218
217
  const scenarioDef = config.scenarios?.[scenarioName];
219
218
  if (!scenarioDef) {
220
219
  const available = Object.keys(config.scenarios ?? {}).join(', ') || 'none';
@@ -223,14 +222,18 @@ export async function runScenario(config, scenarioName, serverNames) {
223
222
  const allTasks = config.tasks ?? {};
224
223
  const tasks = resolveTaskDefs(scenarioDef.tasks, allTasks);
225
224
  const servers = resolveServers(config, serverNames);
226
- const listr = new Listr(servers.map(([name, server]) => ({
227
- title: chalk.bgMagenta.black(` ${name} (${server.host}) `),
228
- task: () => buildServerListr(name, server, config, tasks),
229
- options: listrOptions,
230
- })), listrOptions);
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),
230
+ })), {
231
+ concurrent: false,
232
+ renderer: CustomRenderer,
233
+ });
231
234
  await listr.run();
232
235
  }
233
- export async function runTask(config, taskName, serverNames) {
236
+ export async function runTask(config, taskName, serverNames, options = {}) {
234
237
  const allTasks = config.tasks ?? {};
235
238
  const taskDef = allTasks[taskName];
236
239
  if (!taskDef) {
@@ -238,10 +241,14 @@ export async function runTask(config, taskName, serverNames) {
238
241
  throw new Exception(`Task "${taskName}" not found. Available: ${available}`, 1774742100356);
239
242
  }
240
243
  const servers = resolveServers(config, serverNames);
241
- const listr = new Listr(servers.map(([name, server]) => ({
242
- title: chalk.bgMagenta.black(` ${name} (${server.host}) `),
243
- task: () => buildServerListr(name, server, config, [[taskName, taskDef]]),
244
- options: listrOptions,
245
- })), listrOptions);
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),
249
+ })), {
250
+ concurrent: false,
251
+ renderer: CustomRenderer,
252
+ });
246
253
  await listr.run();
247
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.9",
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",