Skip to content

Commit 47a3873

Browse files
feat: Improve commit message generation & execution
1 parent b92d07f commit 47a3873

4 files changed

Lines changed: 97 additions & 6 deletions

File tree

src/application/services/ProjectAnalyzer.ts

Lines changed: 13 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,10 @@ export interface ServerCommandsConfig {
3131
projectPath?: string;
3232
/** Run on the developer machine before SSH (e.g. npm run build, rsync). */
3333
localCommands?: string[];
34+
/**
35+
* When true, smart deploy never adds `npm run build` on the server (e.g. you build locally and upload assets).
36+
*/
37+
skipRemoteNpmBuild?: boolean;
3438
server?: {
3539
host: string;
3640
user: string;
@@ -253,8 +257,13 @@ export class ProjectAnalyzer {
253257
/**
254258
* Generate smart deployment commands based on analysis
255259
*/
256-
public generateSmartDeployCommands(analysis: SmartDeployAnalysis): string[] {
260+
public generateSmartDeployCommands(
261+
analysis: SmartDeployAnalysis,
262+
options?: { skipRemoteNpmBuild?: boolean }
263+
): string[] {
257264
const commands: string[] = [];
265+
const skipRemoteNpmBuild = options?.skipRemoteNpmBuild === true;
266+
const runRemoteNpmBuild = analysis.needsNpmBuild && !skipRemoteNpmBuild;
258267

259268
// Always pull latest changes
260269
if (analysis.needsGitPull) {
@@ -270,7 +279,7 @@ export class ProjectAnalyzer {
270279
if (analysis.needsNpmInstall) {
271280
commands.push('npm install --production');
272281
}
273-
if (analysis.needsNpmBuild) {
282+
if (runRemoteNpmBuild) {
274283
commands.push('npm run build');
275284
}
276285

@@ -282,8 +291,8 @@ export class ProjectAnalyzer {
282291
commands.push('php artisan migrate --force');
283292
}
284293

285-
// PM2 restart for Node.js projects
286-
if (analysis.needsNpmBuild || analysis.needsNpmInstall) {
294+
// PM2 restart when npm install or when we actually run a remote build
295+
if (analysis.needsNpmInstall || runRemoteNpmBuild) {
287296
commands.push('pm2 restart 0');
288297
}
289298

src/application/services/ServerCommandExecutor.ts

Lines changed: 31 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -54,21 +54,51 @@ export class ServerCommandExecutor {
5454

5555
/**
5656
* If localCommands use rsync, ensure rsync exists in PATH before starting deploy.
57+
* On Windows, commands may use `wsl rsync ...`; in that case only WSL's rsync is checked.
5758
*/
5859
public async validateLocalDeployPrerequisites(localCommands?: string[]): Promise<string[]> {
5960
const cmds = localCommands ?? [];
6061
const needsRsync = cmds.some(c => /\brsync\b/i.test(c));
6162
if (!needsRsync) {
6263
return [];
6364
}
65+
66+
const usesWslRsync = cmds.some(
67+
c => /^\s*wsl\s+/i.test(c) && /\brsync\b/i.test(c)
68+
);
69+
70+
if (usesWslRsync) {
71+
try {
72+
await execAsync('wsl rsync --version', { timeout: 8000 });
73+
return [];
74+
} catch {
75+
return [
76+
'localCommands use `wsl rsync`, but rsync was not found in WSL. In WSL run: sudo apt install rsync (Debian/Ubuntu) or equivalent, then retry.'
77+
];
78+
}
79+
}
80+
6481
try {
6582
await execAsync('rsync --version', { timeout: 8000 });
83+
return [];
6684
} catch {
85+
if (process.platform === 'win32') {
86+
try {
87+
await execAsync('wsl rsync --version', { timeout: 8000 });
88+
return [
89+
'localCommands reference rsync, but `rsync` is not in PATH on Windows. Options: (1) Change the line to `wsl rsync ...` if you use WSL (fix paths if needed, e.g. /mnt/c/...). (2) Install rsync: `choco install rsync` or `scoop install rsync`, then reopen the terminal so PATH updates.'
90+
];
91+
} catch {
92+
/* fall through */
93+
}
94+
return [
95+
'localCommands reference rsync, but rsync was not found in PATH. Install rsync (e.g. `choco install rsync` or `scoop install rsync`), use Git Bash with rsync, or use `wsl rsync ...` after installing rsync inside WSL.'
96+
];
97+
}
6798
return [
6899
'localCommands reference rsync, but rsync was not found in PATH. Install rsync or adjust localCommands.'
69100
];
70101
}
71-
return [];
72102
}
73103

74104
/**

src/cli/SmartCommitCli.ts

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -351,14 +351,19 @@ export class SmartCommitCli {
351351

352352
const analysis = await projectAnalyzer.analyzeChangesForSmartDeploy(projectPath, projectType);
353353

354+
const skipRemoteNpmBuild = serverConfig.skipRemoteNpmBuild === true;
355+
354356
// Show analysis results
355357
console.log(chalk.green('\n📊 Analysis Results:'));
356358
analysis.reasons.forEach(reason => {
357359
console.log(chalk.gray(` • ${reason}`));
358360
});
361+
if (skipRemoteNpmBuild && analysis.needsNpmBuild) {
362+
console.log(chalk.gray(' • Remote npm run build skipped (skipRemoteNpmBuild in config)'));
363+
}
359364

360365
// Generate smart commands
361-
const smartCommands = projectAnalyzer.generateSmartDeployCommands(analysis);
366+
const smartCommands = projectAnalyzer.generateSmartDeployCommands(analysis, { skipRemoteNpmBuild });
362367

363368
if (smartCommands.length === 0) {
364369
console.log(chalk.yellow('✅ No deployment needed - no changes detected!'));

tests/application/services/ProjectAnalyzer.test.ts

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -173,5 +173,52 @@ describe('ProjectAnalyzer.analyzeChangesForSmartDeploy', () => {
173173

174174
expect(commands).not.toContain('npm run build');
175175
});
176+
177+
it('does NOT include npm run build or pm2 when needsNpmBuild but skipRemoteNpmBuild is true', () => {
178+
const { ProjectAnalyzer: PA } = require('../../../src/application/services/ProjectAnalyzer');
179+
const analyzer = new PA({} as IAiAssistant);
180+
181+
const commands = analyzer.generateSmartDeployCommands(
182+
{
183+
needsGitPull: true,
184+
needsComposerInstall: false,
185+
needsComposerUpdate: false,
186+
needsNpmInstall: false,
187+
needsNpmBuild: true,
188+
needsLaravelOptimize: false,
189+
needsLaravelMigrate: false,
190+
needsSystemRestart: false,
191+
reasons: [],
192+
},
193+
{ skipRemoteNpmBuild: true }
194+
);
195+
196+
expect(commands).not.toContain('npm run build');
197+
expect(commands).not.toContain('pm2 restart 0');
198+
});
199+
200+
it('still runs npm install and pm2 when skipRemoteNpmBuild but needsNpmInstall', () => {
201+
const { ProjectAnalyzer: PA } = require('../../../src/application/services/ProjectAnalyzer');
202+
const analyzer = new PA({} as IAiAssistant);
203+
204+
const commands = analyzer.generateSmartDeployCommands(
205+
{
206+
needsGitPull: true,
207+
needsComposerInstall: false,
208+
needsComposerUpdate: false,
209+
needsNpmInstall: true,
210+
needsNpmBuild: true,
211+
needsLaravelOptimize: false,
212+
needsLaravelMigrate: false,
213+
needsSystemRestart: false,
214+
reasons: [],
215+
},
216+
{ skipRemoteNpmBuild: true }
217+
);
218+
219+
expect(commands).not.toContain('npm run build');
220+
expect(commands).toContain('npm install --production');
221+
expect(commands).toContain('pm2 restart 0');
222+
});
176223
});
177224
});

0 commit comments

Comments
 (0)