Skip to content

Commit 8f0b09b

Browse files
committed
refactor(downstream): consume config.apps via getActiveApp helper (sprint 2c)
1 parent 14fc7e6 commit 8f0b09b

23 files changed

Lines changed: 219 additions & 125 deletions

src/cli/commands/ci.ts

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import { readFile, writeFile } from 'node:fs/promises';
33
import { pathExists, ensureDir } from 'fs-extra';
44
import { execa } from 'execa';
55
import { loadConfig } from '../../config/loader.js';
6+
import { getActiveApp } from '../../domain/workspace.js';
67
import { confirm } from '../prompt.js';
78
import { ui } from '../ui.js';
89

@@ -195,10 +196,11 @@ export async function cmdCiEnvSync(
195196
options: { all?: boolean },
196197
): Promise<void> {
197198
const config = await loadConfig(cwd, undefined);
199+
const app = getActiveApp(config);
198200

199-
const envPath = resolve(cwd, config.envFile);
201+
const envPath = resolve(cwd, app.envFile);
200202
if (!(await pathExists(envPath))) {
201-
ui.error(`Env file not found: ${config.envFile}`);
203+
ui.error(`Env file not found: ${app.envFile}`);
202204
process.exit(1);
203205
}
204206

src/cli/commands/config.ts

Lines changed: 27 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -3,61 +3,63 @@ import { pathExists } from 'fs-extra';
33
import { runLocalCommand } from '../runner.js';
44
import { ui } from '../ui.js';
55
import { loadConfig } from '../../config/loader.js';
6+
import { getActiveApp } from '../../domain/workspace.js';
67

78
export async function cmdConfigShow(cwd: string, options: { config?: string }): Promise<void> {
89
await runLocalCommand(
910
cwd,
1011
async (config) => {
12+
const app = getActiveApp(config);
1113
ui.heading('Shipnode Configuration');
1214

1315
ui.section('App', [
14-
['app', config.app],
16+
['app', app.appType],
1517
['nodeVersion', config.nodeVersion],
16-
['envFile', config.envFile],
17-
['keepReleases', String(config.keepReleases)],
18+
['envFile', app.envFile],
19+
['keepReleases', String(app.keepReleases)],
1820
]);
1921

2022
ui.section('SSH', [
2123
['ssh', `${config.ssh.user}@${config.ssh.host}:${config.ssh.port}`],
2224
['remotePath', config.remotePath],
2325
]);
2426

25-
if (config.pm2) {
26-
for (const app of config.pm2.apps) {
27-
const rows: [string, string][] = [['name', app.name]];
28-
if (app.command) rows.push(['command', app.command]);
29-
if (app.port !== undefined) rows.push(['port', String(app.port)]);
30-
if (app.instances !== undefined) rows.push(['instances', String(app.instances)]);
31-
if (app.maxMemory !== undefined) rows.push(['maxMemory', app.maxMemory]);
32-
if (app.env) {
33-
for (const [k, v] of Object.entries(app.env)) rows.push([`env.${k}`, v]);
27+
if (app.pm2) {
28+
for (const pm2App of app.pm2.apps) {
29+
const rows: [string, string][] = [['name', pm2App.name]];
30+
if (pm2App.command) rows.push(['command', pm2App.command]);
31+
if (pm2App.port !== undefined) rows.push(['port', String(pm2App.port)]);
32+
if (pm2App.instances !== undefined) rows.push(['instances', String(pm2App.instances)]);
33+
if (pm2App.maxMemory !== undefined) rows.push(['maxMemory', pm2App.maxMemory]);
34+
if (pm2App.env) {
35+
for (const [k, v] of Object.entries(pm2App.env)) rows.push([`env.${k}`, v]);
3436
}
35-
ui.section(app.port !== undefined ? `PM2 app: ${app.name} (web)` : `PM2 app: ${app.name}`, rows);
37+
ui.section(pm2App.port !== undefined ? `PM2 app: ${pm2App.name} (web)` : `PM2 app: ${pm2App.name}`, rows);
3638
}
3739
}
3840

39-
if (config.domain) {
41+
if (app.domain) {
4042
ui.section('Domain', [
41-
['domain', config.domain],
43+
['domain', app.domain],
4244
]);
4345
}
4446

45-
if (config.app === 'backend') {
47+
if (app.appType === 'backend') {
4648
ui.section('Health Check', [
47-
['enabled', String(config.healthCheck.enabled)],
48-
['path', config.healthCheck.path],
49-
['timeout', String(config.healthCheck.timeout)],
50-
['retries', String(config.healthCheck.retries)],
51-
['startupDelay', String(config.healthCheck.startupDelay)],
49+
['enabled', String(app.healthCheck.enabled)],
50+
['path', app.healthCheck.path],
51+
['timeout', String(app.healthCheck.timeout)],
52+
['retries', String(app.healthCheck.retries)],
53+
['startupDelay', String(app.healthCheck.startupDelay)],
5254
]);
5355
}
5456

55-
if (config.sharedDirs && config.sharedDirs.length > 0) {
56-
ui.section('Shared Dirs', config.sharedDirs.map((d, i) => [`[${i}]`, d]));
57+
if (app.sharedDirs && app.sharedDirs.length > 0) {
58+
ui.section('Shared Dirs', app.sharedDirs.map((d, i) => [`[${i}]`, d]));
5759
}
5860

59-
if (config.sharedFiles && config.sharedFiles.length > 0) {
60-
ui.section('Shared Files', config.sharedFiles.map((f, i) => [`[${i}]`, f]));
61+
if (app.sharedFiles && app.sharedFiles.length > 0) {
62+
ui.section('Shared Files', app.sharedFiles.map((f, i) => [`[${i}]`, f]));
6163
}
6264

6365
if (config.database) {

src/cli/commands/deploy.ts

Lines changed: 16 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import { LoggingExecutor } from '../../infrastructure/ssh/logging-executor.js';
55
import { runRemoteCommand } from '../runner.js';
66
import { ui } from '../ui.js';
77
import type { ShipnodeConfig } from '../../shared/types.js';
8+
import { getActiveApp } from '../../domain/workspace.js';
89
import { getDeploymentName, getWebApp } from '../../domain/pm2/apps.js';
910

1011
export async function cmdDeploy(cwd: string, options: { dryRun?: boolean; skipBuild?: boolean; config?: string }): Promise<void> {
@@ -19,14 +20,14 @@ export async function cmdDeploy(cwd: string, options: { dryRun?: boolean; skipBu
1920
cwd,
2021
async ({ config, executor }) => {
2122
ui.banner();
22-
ui.step(`Deploying ${chalk.bold(getDeploymentName(config) ?? config.app)}${config.ssh.user}@${config.ssh.host}`);
23+
ui.step(`Deploying ${chalk.bold(getDeploymentName(config) ?? getActiveApp(config).appType)}${config.ssh.user}@${config.ssh.host}`);
2324

2425
const deployer = new DeployService(new LoggingExecutor(executor), config, cwd);
2526
await deployer.execute(options.skipBuild ?? false);
2627

2728
const lines = [
2829
`host ${config.ssh.user}@${config.ssh.host}`,
29-
config.domain ? `url https://${config.domain}` : '',
30+
getActiveApp(config).domain ? `url https://${getActiveApp(config).domain}` : '',
3031
].filter(Boolean).join('\n');
3132

3233
ui.note(lines, 'Done');
@@ -39,15 +40,17 @@ export async function cmdDeploy(cwd: string, options: { dryRun?: boolean; skipBu
3940
function printDryRun(config: ShipnodeConfig, skipBuild: boolean): void {
4041
ui.banner();
4142

43+
const app = getActiveApp(config);
44+
4245
const serverRows: [string, string][] = [
43-
['App type', config.app],
46+
['App type', app.appType],
4447
['Host', `${config.ssh.user}@${config.ssh.host}:${config.ssh.port}`],
4548
['Remote path', config.remotePath],
46-
['Keep releases', String(config.keepReleases)],
49+
['Keep releases', String(app.keepReleases)],
4750
];
4851

49-
if (config.app === 'backend') {
50-
const apps = config.pm2?.apps ?? [];
52+
if (app.appType === 'backend') {
53+
const apps = app.pm2?.apps ?? [];
5154
if (apps.length) {
5255
serverRows.push(['PM2 deployment', getDeploymentName(config) ?? '']);
5356
serverRows.push(['PM2 apps', apps.map((a) => a.port !== undefined ? `${a.name}(web:${a.port})` : a.name).join(', ')]);
@@ -56,14 +59,14 @@ function printDryRun(config: ShipnodeConfig, skipBuild: boolean): void {
5659
if (web) serverRows.push(['Port', String(web.port)]);
5760
}
5861

59-
if (config.domain) serverRows.push(['Domain', config.domain]);
62+
if (app.domain) serverRows.push(['Domain', app.domain]);
6063

6164
const buildRows: [string, string][] = [];
6265
if (skipBuild) {
6366
buildRows.push(['', chalk.dim('skipped (--skip-build)')]);
64-
} else if (config.app === 'frontend') {
67+
} else if (app.appType === 'frontend') {
6568
buildRows.push(['', 'npm run build']);
66-
buildRows.push(['output', config.buildDir ?? 'dist/ (auto-detected)']);
69+
buildRows.push(['output', app.buildDir ?? 'dist/ (auto-detected)']);
6770
} else {
6871
buildRows.push(['', chalk.dim('runs on remote server')]);
6972
}
@@ -73,13 +76,13 @@ function printDryRun(config: ShipnodeConfig, skipBuild: boolean): void {
7376
'Create release directory',
7477
'Rsync files',
7578
'Install dependencies',
76-
config.hooks?.preDeploy ? 'Run preDeploy hook' : '',
79+
app.hooks?.preDeploy ? 'Run preDeploy hook' : '',
7780
'Switch symlink (atomic)',
78-
config.app === 'backend' ? 'Reload PM2' : '',
79-
config.healthCheck.enabled ? `Health check ${config.healthCheck.path}` : '',
81+
app.appType === 'backend' ? 'Reload PM2' : '',
82+
app.healthCheck.enabled ? `Health check ${app.healthCheck.path}` : '',
8083
'Record release',
8184
'Clean old releases',
82-
config.hooks?.postDeploy ? 'Run postDeploy hook' : '',
85+
app.hooks?.postDeploy ? 'Run postDeploy hook' : '',
8386
'Release lock',
8487
].filter(Boolean);
8588

src/cli/commands/doctor.ts

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
import { runRemoteCommand } from '../runner.js';
22
import { ui } from '../ui.js';
3+
import { getActiveApp } from '../../domain/workspace.js';
4+
import type { ShipnodeConfig } from '../../shared/types.js';
35

46
export async function cmdDoctor(cwd: string, options: { config?: string; security?: boolean }): Promise<void> {
57
await runRemoteCommand(
@@ -19,7 +21,8 @@ export async function cmdDoctor(cwd: string, options: { config?: string; securit
1921
);
2022
}
2123

22-
function checkLocal(config: { ssh: { host?: string; user?: string }; remotePath?: string; app: string; pm2?: { apps: unknown[] } }): void {
24+
function checkLocal(config: ShipnodeConfig): void {
25+
const app = getActiveApp(config);
2326
ui.info('Checking local configuration...');
2427

2528
const issues: string[] = [];
@@ -36,7 +39,7 @@ function checkLocal(config: { ssh: { host?: string; user?: string }; remotePath?
3639
issues.push('Remote path is not configured');
3740
}
3841

39-
if (config.app === 'backend' && !config.pm2?.apps.length) {
42+
if (app.appType === 'backend' && !app.pm2?.apps.length) {
4043
issues.push('PM2 apps are not configured for backend app');
4144
}
4245

src/cli/commands/env.ts

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import { pathExists } from 'fs-extra';
33
import { resolve } from 'path';
44
import { runRemoteCommand } from '../runner.js';
55
import { ui } from '../ui.js';
6+
import { getActiveApp } from '../../domain/workspace.js';
67
import { getDeploymentName } from '../../domain/pm2/apps.js';
78

89
export async function cmdEnv(
@@ -12,7 +13,8 @@ export async function cmdEnv(
1213
await runRemoteCommand(
1314
cwd,
1415
async ({ config, executor }) => {
15-
const envFile = options.file ?? config.envFile;
16+
const app = getActiveApp(config);
17+
const envFile = options.file ?? app.envFile;
1618
const localEnvPath = resolve(cwd, envFile);
1719

1820
if (!(await pathExists(localEnvPath))) {
@@ -28,12 +30,12 @@ export async function cmdEnv(
2830
// (`shared/${envFile}`) and the workDir symlink target stay consistent.
2931
// Maintain a `.env` alias too — older configs and any external scripts
3032
// that read `shared/.env` keep working.
31-
const sharedEnv = `${config.remotePath}/shared/${config.envFile}`;
33+
const sharedEnv = `${config.remotePath}/shared/${app.envFile}`;
3234
const sharedEnvAlias = `${config.remotePath}/shared/.env`;
3335
await executor.exec(`mkdir -p "${config.remotePath}/shared"`);
3436
await executor.exec(`echo "${b64}" | base64 -d > "${sharedEnv}"`);
3537
await executor.exec(`chmod 600 "${sharedEnv}"`);
36-
if (config.envFile !== '.env') {
38+
if (app.envFile !== '.env') {
3739
await executor.exec(`ln -sf "${sharedEnv}" "${sharedEnvAlias}"`);
3840
}
3941
ui.success(`Uploaded to ${sharedEnv}`);
@@ -48,7 +50,7 @@ export async function cmdEnv(
4850
}
4951

5052
const namespace = getDeploymentName(config);
51-
if (config.app === 'backend' && namespace) {
53+
if (app.appType === 'backend' && namespace) {
5254
const nodeVersion = config.nodeVersion === 'lts' ? '24' : config.nodeVersion;
5355
const mise = `export PATH="$HOME/.local/bin:$HOME/.local/share/mise/shims:$PATH"`;
5456
const checkResult = await executor.exec(

src/cli/commands/logs.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,13 @@
11
import { runRemoteCommand } from '../runner.js';
2+
import { getActiveApp } from '../../domain/workspace.js';
23
import { getDeploymentName, resolveProcessTarget } from '../../domain/pm2/apps.js';
34

45
export async function cmdLogs(cwd: string, options: { lines?: number; config?: string; process?: string }): Promise<void> {
56
await runRemoteCommand(
67
cwd,
78
async ({ config, executor }) => {
89
const namespace = getDeploymentName(config);
9-
if (config.app !== 'backend' || !namespace) {
10+
if (getActiveApp(config).appType !== 'backend' || !namespace) {
1011
throw new Error('Logs only available for backend apps with PM2');
1112
}
1213
const target = options.process ? resolveProcessTarget(config, options.process) : namespace;

src/cli/commands/metrics.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,11 @@
11
import { execa } from 'execa';
22
import { loadConfig } from '../../config/loader.js';
3+
import { getActiveApp } from '../../domain/workspace.js';
34
import { getDeploymentName } from '../../domain/pm2/apps.js';
45

56
export async function cmdMetrics(cwd: string, options: { config?: string }): Promise<void> {
67
const config = await loadConfig(cwd, options.config);
7-
if (config.app !== 'backend' || !getDeploymentName(config)) {
8+
if (getActiveApp(config).appType !== 'backend' || !getDeploymentName(config)) {
89
throw new Error('Metrics only available for backend apps with PM2');
910
}
1011
const nodeVersion = config.nodeVersion === 'lts' ? '24' : config.nodeVersion;

src/cli/commands/migrate.ts

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,12 +2,14 @@ import { confirm } from '../prompt.js';
22
import { runRemoteCommand } from '../runner.js';
33
import { ReleaseManager } from '../../domain/release/manager.js';
44
import { ui } from '../ui.js';
5+
import { getActiveApp } from '../../domain/workspace.js';
56
import { getDeploymentName } from '../../domain/pm2/apps.js';
67

78
export async function cmdMigrate(cwd: string, options: { config?: string }): Promise<void> {
89
await runRemoteCommand(
910
cwd,
1011
async ({ config, executor }) => {
12+
const app = getActiveApp(config);
1113
const remotePath = config.remotePath;
1214

1315
ui.heading('Zero-Downtime Migration');
@@ -69,7 +71,7 @@ export async function cmdMigrate(cwd: string, options: { config?: string }): Pro
6971

7072
// Reload PM2 if applicable
7173
const namespace = getDeploymentName(config);
72-
if (config.app === 'backend' && namespace) {
74+
if (app.appType === 'backend' && namespace) {
7375
const nodeVersion = config.nodeVersion === 'lts' ? '24' : config.nodeVersion;
7476
const mise = `export PATH="$HOME/.local/bin:$HOME/.local/share/mise/shims:$PATH"`;
7577
ui.info('Reloading PM2 from new path...');
@@ -80,7 +82,7 @@ export async function cmdMigrate(cwd: string, options: { config?: string }): Pro
8082
}
8183

8284
// Record the initial migrated release
83-
const releases = new ReleaseManager(executor, remotePath, config.keepReleases);
85+
const releases = new ReleaseManager(executor, remotePath, app.keepReleases);
8486
await releases.recordRelease({
8587
timestamp,
8688
status: 'success',

src/cli/commands/restart.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,14 @@
11
import { runRemoteCommand } from '../runner.js';
22
import { ui } from '../ui.js';
3+
import { getActiveApp } from '../../domain/workspace.js';
34
import { getDeploymentName, resolveProcessTarget } from '../../domain/pm2/apps.js';
45

56
export async function cmdRestart(cwd: string, options: { config?: string; process?: string }): Promise<void> {
67
await runRemoteCommand(
78
cwd,
89
async ({ config, executor }) => {
910
const namespace = getDeploymentName(config);
10-
if (config.app !== 'backend' || !namespace) {
11+
if (getActiveApp(config).appType !== 'backend' || !namespace) {
1112
throw new Error('Restart only available for backend apps with PM2');
1213
}
1314
const target = options.process ? resolveProcessTarget(config, options.process) : namespace;

src/cli/commands/rollback.ts

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import { ReleaseManager } from '../../domain/release/manager.js';
33
import { HealthCheckService } from '../../services/health.service.js';
44
import { ui } from '../ui.js';
55
import { confirm } from '../prompt.js';
6+
import { getActiveApp } from '../../domain/workspace.js';
67
import { getDeploymentName, getEcosystemPath } from '../../domain/pm2/apps.js';
78

89
export async function cmdRollback(
@@ -12,7 +13,8 @@ export async function cmdRollback(
1213
await runRemoteCommand(
1314
cwd,
1415
async ({ config, executor }) => {
15-
const releases = new ReleaseManager(executor, config.remotePath, config.keepReleases);
16+
const app = getActiveApp(config);
17+
const releases = new ReleaseManager(executor, config.remotePath, app.keepReleases);
1618
const stepsBack = options.steps ?? 1;
1719

1820
ui.info('Fetching release history...');
@@ -46,7 +48,7 @@ export async function cmdRollback(
4648
ui.success('Symlink switched');
4749

4850
const namespace = getDeploymentName(config);
49-
if (config.app === 'backend' && namespace) {
51+
if (app.appType === 'backend' && namespace) {
5052
const nodeVersion = config.nodeVersion === 'lts' ? '24' : config.nodeVersion;
5153
const mise = `export PATH="$HOME/.local/bin:$HOME/.local/share/mise/shims:$PATH"`;
5254
// Prefer reloading from the rolled-back release's ecosystem file (ADR-0001 — it
@@ -60,7 +62,7 @@ export async function cmdRollback(
6062
ui.success('PM2 reloaded');
6163
}
6264

63-
if (config.app === 'backend' && config.healthCheck.enabled) {
65+
if (app.appType === 'backend' && app.healthCheck.enabled) {
6466
ui.info('Running health check...');
6567
const health = new HealthCheckService(executor, config);
6668
await health.perform();

0 commit comments

Comments
 (0)