hostfn 0.1.0

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.
Files changed (47) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +1136 -0
  3. package/_conduct/specs/1.v0.spec.md +1041 -0
  4. package/examples/express-api/package.json +22 -0
  5. package/examples/express-api/src/index.ts +16 -0
  6. package/examples/express-api/tsconfig.json +11 -0
  7. package/examples/github-actions-deploy.yml +40 -0
  8. package/examples/monorepo-config.json +76 -0
  9. package/examples/monorepo-multi-server-config.json +74 -0
  10. package/package.json +39 -0
  11. package/packages/cli/package.json +40 -0
  12. package/packages/cli/src/__tests__/core/backup.test.ts +137 -0
  13. package/packages/cli/src/__tests__/core/health.test.ts +125 -0
  14. package/packages/cli/src/__tests__/core/lock.test.ts +173 -0
  15. package/packages/cli/src/__tests__/core/nginx-multi-domain.test.ts +176 -0
  16. package/packages/cli/src/__tests__/runtimes/pm2.test.ts +130 -0
  17. package/packages/cli/src/__tests__/utils/validation.test.ts +164 -0
  18. package/packages/cli/src/commands/deploy.ts +817 -0
  19. package/packages/cli/src/commands/env.ts +391 -0
  20. package/packages/cli/src/commands/expose.ts +438 -0
  21. package/packages/cli/src/commands/init.ts +192 -0
  22. package/packages/cli/src/commands/logs.ts +106 -0
  23. package/packages/cli/src/commands/rollback.ts +142 -0
  24. package/packages/cli/src/commands/server/info.ts +131 -0
  25. package/packages/cli/src/commands/server/setup.ts +200 -0
  26. package/packages/cli/src/commands/status.ts +149 -0
  27. package/packages/cli/src/config/loader.ts +66 -0
  28. package/packages/cli/src/config/schema.ts +140 -0
  29. package/packages/cli/src/core/backup.ts +128 -0
  30. package/packages/cli/src/core/health.ts +116 -0
  31. package/packages/cli/src/core/local.ts +67 -0
  32. package/packages/cli/src/core/lock.ts +108 -0
  33. package/packages/cli/src/core/nginx.ts +170 -0
  34. package/packages/cli/src/core/ssh.ts +335 -0
  35. package/packages/cli/src/core/sync.ts +138 -0
  36. package/packages/cli/src/core/workspace.ts +180 -0
  37. package/packages/cli/src/index.ts +240 -0
  38. package/packages/cli/src/runtimes/base.ts +144 -0
  39. package/packages/cli/src/runtimes/nodejs/detector.ts +157 -0
  40. package/packages/cli/src/runtimes/nodejs/index.ts +228 -0
  41. package/packages/cli/src/runtimes/nodejs/pm2.ts +71 -0
  42. package/packages/cli/src/runtimes/registry.ts +76 -0
  43. package/packages/cli/src/utils/logger.ts +86 -0
  44. package/packages/cli/src/utils/validation.ts +147 -0
  45. package/packages/cli/tsconfig.json +25 -0
  46. package/packages/cli/vitest.config.ts +19 -0
  47. package/turbo.json +24 -0
@@ -0,0 +1,138 @@
1
+ import { execa } from 'execa';
2
+ import { Logger } from '../utils/logger.js';
3
+
4
+ export interface SyncOptions {
5
+ exclude?: string[];
6
+ include?: string[];
7
+ dryRun?: boolean;
8
+ verbose?: boolean;
9
+ }
10
+
11
+ /**
12
+ * Sync files to remote server using rsync
13
+ */
14
+ export class FileSync {
15
+ /**
16
+ * Sync local directory to remote server
17
+ */
18
+ static async sync(
19
+ localPath: string,
20
+ remotePath: string,
21
+ sshConnection: string, // user@host
22
+ options: SyncOptions = {}
23
+ ): Promise<void> {
24
+ const args = [
25
+ '-avz', // archive, verbose, compress
26
+ '--delete', // delete files on remote that don't exist locally
27
+ ];
28
+
29
+ // Handle SSH authentication for CI/CD mode
30
+ let tempKeyPath: string | undefined;
31
+ if (process.env.HOSTFN_SSH_KEY) {
32
+ // CI/CD mode: create temporary key file from base64-encoded env var
33
+ const { writeFileSync, mkdtempSync } = await import('fs');
34
+ const { join } = await import('path');
35
+ const { tmpdir } = await import('os');
36
+
37
+ const tempDir = mkdtempSync(join(tmpdir(), 'hostfn-ssh-'));
38
+ tempKeyPath = join(tempDir, 'id_rsa');
39
+
40
+ // Decode and write the SSH key
41
+ const keyBuffer = Buffer.from(process.env.HOSTFN_SSH_KEY, 'base64');
42
+ writeFileSync(tempKeyPath, keyBuffer, { mode: 0o600 });
43
+
44
+ // Build SSH command with the temporary key
45
+ const sshCmd = `ssh -i ${tempKeyPath} -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -o BatchMode=yes`;
46
+ args.push('-e', sshCmd);
47
+ } else {
48
+ // Local mode: use default SSH config (respects ~/.ssh/config and ssh-agent)
49
+ args.push('-e', 'ssh -o StrictHostKeyChecking=no -o BatchMode=yes');
50
+ }
51
+
52
+ // Add dry-run flag
53
+ if (options.dryRun) {
54
+ args.push('--dry-run');
55
+ }
56
+
57
+ // Add exclude patterns
58
+ if (options.exclude && options.exclude.length > 0) {
59
+ for (const pattern of options.exclude) {
60
+ args.push('--exclude', pattern);
61
+ }
62
+ }
63
+
64
+ // Add include patterns
65
+ if (options.include && options.include.length > 0) {
66
+ for (const pattern of options.include) {
67
+ args.push('--include', pattern);
68
+ }
69
+ }
70
+
71
+ // Ensure trailing slash on source (rsync behavior)
72
+ const source = localPath.endsWith('/') ? localPath : `${localPath}/`;
73
+ const destination = `${sshConnection}:${remotePath}`;
74
+
75
+ args.push(source, destination);
76
+
77
+ try {
78
+ const result = await execa('rsync', args, {
79
+ stdio: options.verbose ? 'inherit' : 'pipe',
80
+ });
81
+
82
+ if (result.exitCode !== 0) {
83
+ throw new Error(`rsync failed with exit code ${result.exitCode}`);
84
+ }
85
+ } catch (error) {
86
+ if (error instanceof Error) {
87
+ // Check if rsync is not installed
88
+ if (error.message.includes('ENOENT') || error.message.includes('not found')) {
89
+ throw new Error(
90
+ 'rsync is not installed on your system.\n' +
91
+ 'Please install it:\n' +
92
+ ' - macOS: brew install rsync\n' +
93
+ ' - Ubuntu/Debian: apt-get install rsync\n' +
94
+ ' - Windows: Install via WSL or use Git Bash'
95
+ );
96
+ }
97
+ }
98
+ throw error;
99
+ } finally {
100
+ // Cleanup temporary key file if it was created
101
+ if (tempKeyPath) {
102
+ try {
103
+ const { unlinkSync, rmdirSync } = await import('fs');
104
+ const { dirname } = await import('path');
105
+ unlinkSync(tempKeyPath);
106
+ rmdirSync(dirname(tempKeyPath));
107
+ } catch {
108
+ // Ignore cleanup errors
109
+ }
110
+ }
111
+ }
112
+ }
113
+
114
+ /**
115
+ * Check if rsync is available
116
+ */
117
+ static async isRsyncAvailable(): Promise<boolean> {
118
+ try {
119
+ await execa('rsync', ['--version']);
120
+ return true;
121
+ } catch {
122
+ return false;
123
+ }
124
+ }
125
+
126
+ /**
127
+ * Get rsync version
128
+ */
129
+ static async getRsyncVersion(): Promise<string> {
130
+ try {
131
+ const result = await execa('rsync', ['--version']);
132
+ const match = result.stdout.match(/rsync\s+version\s+([\d.]+)/);
133
+ return match ? match[1] : 'unknown';
134
+ } catch {
135
+ return 'not installed';
136
+ }
137
+ }
138
+ }
@@ -0,0 +1,180 @@
1
+ import { readFileSync, writeFileSync, existsSync, cpSync, mkdirSync } from 'fs';
2
+ import { join, dirname, resolve } from 'path';
3
+ import { Logger } from '../utils/logger.js';
4
+
5
+ interface PackageJson {
6
+ name?: string;
7
+ version?: string;
8
+ dependencies?: Record<string, string>;
9
+ devDependencies?: Record<string, string>;
10
+ workspaces?: string[] | { packages: string[] };
11
+ scripts?: Record<string, string>;
12
+ }
13
+
14
+ export class WorkspaceManager {
15
+ private workspaceRoot: string | null = null;
16
+ private workspacePackages: Map<string, string> = new Map();
17
+
18
+ async detectWorkspace(cwd: string): Promise<boolean> {
19
+ let currentDir = cwd;
20
+ const root = resolve('/');
21
+
22
+ while (currentDir !== root) {
23
+ const pkgPath = join(currentDir, 'package.json');
24
+
25
+ if (existsSync(pkgPath)) {
26
+ const pkg = JSON.parse(readFileSync(pkgPath, 'utf-8')) as PackageJson;
27
+
28
+ if (pkg.workspaces) {
29
+ this.workspaceRoot = currentDir;
30
+ await this.indexWorkspacePackages();
31
+ return true;
32
+ }
33
+ }
34
+
35
+ currentDir = dirname(currentDir);
36
+ }
37
+
38
+ return false;
39
+ }
40
+
41
+ private async indexWorkspacePackages(): Promise<void> {
42
+ if (!this.workspaceRoot) return;
43
+
44
+ const rootPkg = this.readPackageJson(this.workspaceRoot);
45
+ if (!rootPkg?.workspaces) return;
46
+
47
+ const workspaces = Array.isArray(rootPkg.workspaces)
48
+ ? rootPkg.workspaces
49
+ : rootPkg.workspaces.packages;
50
+
51
+ const glob = await import('fast-glob');
52
+
53
+ for (const pattern of workspaces) {
54
+ const matches = await glob.default(join(this.workspaceRoot, pattern, 'package.json'), {
55
+ absolute: true,
56
+ });
57
+
58
+ for (const pkgPath of matches) {
59
+ const pkg = JSON.parse(readFileSync(pkgPath, 'utf-8')) as PackageJson;
60
+ if (pkg.name) {
61
+ this.workspacePackages.set(pkg.name, dirname(pkgPath));
62
+ }
63
+ }
64
+ }
65
+ }
66
+
67
+ getWorkspaceDependencies(servicePath: string): string[] {
68
+ const pkg = this.readPackageJson(servicePath);
69
+ if (!pkg) return [];
70
+
71
+ const allDeps = {
72
+ ...pkg.dependencies,
73
+ ...pkg.devDependencies,
74
+ };
75
+
76
+ const workspaceDeps: string[] = [];
77
+
78
+ for (const [depName, depVersion] of Object.entries(allDeps)) {
79
+ if (depVersion === '*' || depVersion === 'workspace:*' || depVersion.startsWith('workspace:')) {
80
+ if (this.workspacePackages.has(depName)) {
81
+ workspaceDeps.push(depName);
82
+ }
83
+ }
84
+ }
85
+
86
+ return workspaceDeps;
87
+ }
88
+
89
+ async bundleWorkspaceDependencies(servicePath: string, targetDir: string): Promise<void> {
90
+ const workspaceDeps = this.getWorkspaceDependencies(servicePath);
91
+
92
+ if (workspaceDeps.length === 0) {
93
+ return;
94
+ }
95
+
96
+ Logger.info(`Bundling ${workspaceDeps.length} workspace dependencies...`);
97
+
98
+ const nodeModulesDir = join(targetDir, 'node_modules');
99
+ mkdirSync(nodeModulesDir, { recursive: true });
100
+
101
+ for (const depName of workspaceDeps) {
102
+ const depPath = this.workspacePackages.get(depName);
103
+ if (!depPath) continue;
104
+
105
+ const depPkg = this.readPackageJson(depPath);
106
+ if (depPkg?.scripts?.build) {
107
+ Logger.log(` → Building ${depName}...`);
108
+ try {
109
+ const { execSync } = await import('child_process');
110
+ execSync('npm run build', { cwd: depPath, stdio: 'ignore' });
111
+ } catch (error) {
112
+ Logger.warn(` ⚠ Failed to build ${depName}, bundling as-is`);
113
+ }
114
+ }
115
+
116
+ const targetDepDir = join(nodeModulesDir, depName);
117
+
118
+ mkdirSync(dirname(targetDepDir), { recursive: true });
119
+
120
+ cpSync(depPath, targetDepDir, {
121
+ recursive: true,
122
+ filter: (src) => {
123
+ const relativePath = src.replace(depPath, '');
124
+ return !relativePath.includes('node_modules') &&
125
+ !relativePath.includes('.git');
126
+ },
127
+ });
128
+
129
+ Logger.log(` ✓ Bundled ${depName}`);
130
+ }
131
+ }
132
+
133
+ rewritePackageJson(servicePath: string, targetDir: string): void {
134
+ const workspaceDeps = this.getWorkspaceDependencies(servicePath);
135
+
136
+ if (workspaceDeps.length === 0) {
137
+ return;
138
+ }
139
+
140
+ const pkgPath = join(targetDir, 'package.json');
141
+ const pkg = JSON.parse(readFileSync(pkgPath, 'utf-8')) as PackageJson;
142
+
143
+ if (pkg.dependencies) {
144
+ for (const depName of workspaceDeps) {
145
+ if (pkg.dependencies[depName]) {
146
+ pkg.dependencies[depName] = `file:./node_modules/${depName}`;
147
+ }
148
+ }
149
+ }
150
+
151
+ if (pkg.devDependencies) {
152
+ for (const depName of workspaceDeps) {
153
+ if (pkg.devDependencies[depName]) {
154
+ delete pkg.devDependencies[depName];
155
+ }
156
+ }
157
+ }
158
+
159
+ writeFileSync(pkgPath, JSON.stringify(pkg, null, 2));
160
+ }
161
+
162
+ private readPackageJson(dir: string): PackageJson | null {
163
+ const pkgPath = join(dir, 'package.json');
164
+ if (!existsSync(pkgPath)) return null;
165
+
166
+ try {
167
+ return JSON.parse(readFileSync(pkgPath, 'utf-8'));
168
+ } catch {
169
+ return null;
170
+ }
171
+ }
172
+
173
+ getWorkspaceRoot(): string | null {
174
+ return this.workspaceRoot;
175
+ }
176
+
177
+ isInWorkspace(): boolean {
178
+ return this.workspaceRoot !== null;
179
+ }
180
+ }
@@ -0,0 +1,240 @@
1
+ #!/usr/bin/env node
2
+
3
+ import { Command } from 'commander';
4
+ import { Logger } from './utils/logger.js';
5
+ import { RuntimeRegistry } from './runtimes/registry.js';
6
+ import { NodeJSAdapter } from './runtimes/nodejs/index.js';
7
+
8
+ // Register runtime adapters
9
+ RuntimeRegistry.register(new NodeJSAdapter());
10
+
11
+ const program = new Command();
12
+
13
+ program
14
+ .name('hostfn')
15
+ .description('Universal application deployment CLI')
16
+ .version('0.1.0');
17
+
18
+ // Init command
19
+ program
20
+ .command('init')
21
+ .description('Initialize hostfn configuration in current project')
22
+ .action(async () => {
23
+ try {
24
+ const { initCommand } = await import('./commands/init.js');
25
+ await initCommand();
26
+ } catch (error) {
27
+ Logger.error(error instanceof Error ? error.message : String(error));
28
+ process.exit(1);
29
+ }
30
+ });
31
+
32
+ // Server setup command
33
+ program
34
+ .command('server')
35
+ .description('Server management commands')
36
+ .addCommand(
37
+ new Command('setup')
38
+ .description('Setup a new server for deployments')
39
+ .argument('<host>', 'SSH connection string (user@host)')
40
+ .option('--env <environment>', 'Environment name', 'production')
41
+ .option('--node-version <version>', 'Node.js version', '20')
42
+ .option('--port <port>', 'Service port', '3000')
43
+ .option('--redis', 'Install Redis', false)
44
+ .option('--password <password>', 'SSH password (if not using key auth)')
45
+ .action(async (host, options) => {
46
+ try {
47
+ const { serverSetupCommand } = await import('./commands/server/setup.js');
48
+ await serverSetupCommand(host, options);
49
+ } catch (error) {
50
+ Logger.error(error instanceof Error ? error.message : String(error));
51
+ process.exit(1);
52
+ }
53
+ })
54
+ )
55
+ .addCommand(
56
+ new Command('info')
57
+ .description('Show server information')
58
+ .argument('<host>', 'SSH connection string (user@host)')
59
+ .action(async (host) => {
60
+ try {
61
+ const { serverInfoCommand } = await import('./commands/server/info.js');
62
+ await serverInfoCommand(host);
63
+ } catch (error) {
64
+ Logger.error(error instanceof Error ? error.message : String(error));
65
+ process.exit(1);
66
+ }
67
+ })
68
+ );
69
+
70
+ // Deploy command
71
+ program
72
+ .command('deploy')
73
+ .description('Deploy application to server')
74
+ .argument('[environment]', 'Environment to deploy to', 'production')
75
+ .option('--host <host>', 'Override server host')
76
+ .option('--ci', 'CI/CD mode (non-interactive)', false)
77
+ .option('--local', 'Local deployment mode (skip SSH, for self-hosted runners)', false)
78
+ .option('--dry-run', 'Show what would be deployed without executing', false)
79
+ .option('--service <name>', 'Deploy specific service in monorepo')
80
+ .option('--all', 'Deploy all services in monorepo (default behavior)', false)
81
+ .action(async (environment, options) => {
82
+ try {
83
+ const { deployCommand } = await import('./commands/deploy.js');
84
+ await deployCommand(environment, options);
85
+ } catch (error) {
86
+ Logger.error(error instanceof Error ? error.message : String(error));
87
+ process.exit(1);
88
+ }
89
+ });
90
+
91
+ // Logs command
92
+ program
93
+ .command('logs')
94
+ .description('View application logs')
95
+ .argument('[environment]', 'Environment', 'production')
96
+ .option('--lines <n>', 'Number of lines to show', '100')
97
+ .option('--errors', 'Show only errors', false)
98
+ .option('--output <file>', 'Save logs to file')
99
+ .option('--service <name>', 'View logs for specific service in monorepo')
100
+ .action(async (environment, options) => {
101
+ try {
102
+ const { logsCommand } = await import('./commands/logs.js');
103
+ await logsCommand(environment, options);
104
+ } catch (error) {
105
+ Logger.error(error instanceof Error ? error.message : String(error));
106
+ process.exit(1);
107
+ }
108
+ });
109
+
110
+ // Status command
111
+ program
112
+ .command('status')
113
+ .description('Show application status')
114
+ .argument('[environment]', 'Environment', 'production')
115
+ .option('--service <name>', 'Show status for specific service in monorepo')
116
+ .action(async (environment, options) => {
117
+ try {
118
+ const { statusCommand } = await import('./commands/status.js');
119
+ await statusCommand(environment, options);
120
+ } catch (error) {
121
+ Logger.error(error instanceof Error ? error.message : String(error));
122
+ process.exit(1);
123
+ }
124
+ });
125
+
126
+ // Rollback command
127
+ program
128
+ .command('rollback')
129
+ .description('Rollback to previous deployment')
130
+ .argument('[environment]', 'Environment', 'production')
131
+ .option('--to <timestamp>', 'Rollback to specific backup')
132
+ .action(async (environment, options) => {
133
+ try {
134
+ const { rollbackCommand } = await import('./commands/rollback.js');
135
+ await rollbackCommand(environment, options);
136
+ } catch (error) {
137
+ Logger.error(error instanceof Error ? error.message : String(error));
138
+ process.exit(1);
139
+ }
140
+ });
141
+
142
+ // Expose command
143
+ program
144
+ .command('expose')
145
+ .description('Configure Nginx and SSL for deployed services')
146
+ .argument('[environment]', 'Environment', 'production')
147
+ .option('--host <host>', 'Override server host')
148
+ .option('--skip-ssl', 'Skip SSL certificate setup', false)
149
+ .option('--force', 'Overwrite existing configuration', false)
150
+ .action(async (environment, options) => {
151
+ try {
152
+ const { exposeCommand } = await import('./commands/expose.js');
153
+ await exposeCommand(environment, options);
154
+ } catch (error) {
155
+ Logger.error(error instanceof Error ? error.message : String(error));
156
+ process.exit(1);
157
+ }
158
+ });
159
+
160
+ // Environment variable commands
161
+ const envCommand = program
162
+ .command('env')
163
+ .description('Manage environment variables');
164
+
165
+ envCommand
166
+ .command('list')
167
+ .description('List environment variables on server (masked)')
168
+ .argument('<environment>', 'Environment')
169
+ .action(async (environment) => {
170
+ try {
171
+ const { envListCommand } = await import('./commands/env.js');
172
+ await envListCommand(environment);
173
+ } catch (error) {
174
+ Logger.error(error instanceof Error ? error.message : String(error));
175
+ process.exit(1);
176
+ }
177
+ });
178
+
179
+ envCommand
180
+ .command('set')
181
+ .description('Set an environment variable')
182
+ .argument('<environment>', 'Environment')
183
+ .argument('<key>', 'Variable name')
184
+ .argument('<value>', 'Variable value')
185
+ .action(async (environment, key, value) => {
186
+ try {
187
+ const { envSetCommand } = await import('./commands/env.js');
188
+ await envSetCommand(environment, key, value);
189
+ } catch (error) {
190
+ Logger.error(error instanceof Error ? error.message : String(error));
191
+ process.exit(1);
192
+ }
193
+ });
194
+
195
+ envCommand
196
+ .command('push')
197
+ .description('Upload .env file to server')
198
+ .argument('<environment>', 'Environment')
199
+ .argument('<file>', 'Local .env file path')
200
+ .action(async (environment, file) => {
201
+ try {
202
+ const { envPushCommand } = await import('./commands/env.js');
203
+ await envPushCommand(environment, file);
204
+ } catch (error) {
205
+ Logger.error(error instanceof Error ? error.message : String(error));
206
+ process.exit(1);
207
+ }
208
+ });
209
+
210
+ envCommand
211
+ .command('pull')
212
+ .description('Download .env file from server')
213
+ .argument('<environment>', 'Environment')
214
+ .argument('<file>', 'Local file path to save to')
215
+ .action(async (environment, file) => {
216
+ try {
217
+ const { envPullCommand } = await import('./commands/env.js');
218
+ await envPullCommand(environment, file);
219
+ } catch (error) {
220
+ Logger.error(error instanceof Error ? error.message : String(error));
221
+ process.exit(1);
222
+ }
223
+ });
224
+
225
+ envCommand
226
+ .command('validate')
227
+ .description('Validate required environment variables exist')
228
+ .argument('<environment>', 'Environment')
229
+ .action(async (environment) => {
230
+ try {
231
+ const { envValidateCommand } = await import('./commands/env.js');
232
+ await envValidateCommand(environment);
233
+ } catch (error) {
234
+ Logger.error(error instanceof Error ? error.message : String(error));
235
+ process.exit(1);
236
+ }
237
+ });
238
+
239
+ // Parse CLI arguments
240
+ program.parse(process.argv);
@@ -0,0 +1,144 @@
1
+ import { Runtime } from '../config/schema.js';
2
+
3
+ /**
4
+ * Base interface for runtime adapters
5
+ * Each runtime (Node.js, Python, Go, etc.) implements this interface
6
+ */
7
+ export interface RuntimeAdapter {
8
+ /**
9
+ * Runtime identifier
10
+ */
11
+ readonly name: Runtime;
12
+
13
+ /**
14
+ * Detect if this runtime is used in the project
15
+ * @param cwd Project directory
16
+ * @returns Detection result with confidence score
17
+ */
18
+ detect(cwd: string): Promise<RuntimeDetectionResult>;
19
+
20
+ /**
21
+ * Get default configuration for this runtime
22
+ * @param cwd Project directory
23
+ * @returns Partial configuration with smart defaults
24
+ */
25
+ getDefaultConfig(cwd: string): Promise<Partial<RuntimeConfig>>;
26
+
27
+ /**
28
+ * Generate server setup script for this runtime
29
+ * @param version Runtime version
30
+ * @param options Additional setup options
31
+ * @returns Shell script content
32
+ */
33
+ generateSetupScript(version: string, options?: SetupOptions): string;
34
+
35
+ /**
36
+ * Get process manager command (PM2, systemd, etc.)
37
+ */
38
+ getProcessManager(): ProcessManager;
39
+
40
+ /**
41
+ * Validate runtime-specific configuration
42
+ * @param config Runtime configuration
43
+ * @returns Validation errors (empty if valid)
44
+ */
45
+ validateConfig(config: RuntimeConfig): string[];
46
+ }
47
+
48
+ /**
49
+ * Runtime detection result
50
+ */
51
+ export interface RuntimeDetectionResult {
52
+ detected: boolean;
53
+ confidence: number; // 0-100
54
+ version?: string;
55
+ framework?: string;
56
+ packageManager?: string;
57
+ metadata?: Record<string, unknown>;
58
+ }
59
+
60
+ /**
61
+ * Runtime configuration
62
+ */
63
+ export interface RuntimeConfig {
64
+ name: string;
65
+ runtime: Runtime;
66
+ version: string;
67
+ build?: {
68
+ command: string;
69
+ directory: string;
70
+ };
71
+ start: {
72
+ command: string;
73
+ entry?: string;
74
+ };
75
+ port?: number;
76
+ }
77
+
78
+ /**
79
+ * Server setup options
80
+ */
81
+ export interface SetupOptions {
82
+ port?: number;
83
+ environment?: string;
84
+ installRedis?: boolean;
85
+ nodeModulesMode?: 'all' | 'production' | 'none';
86
+ }
87
+
88
+ /**
89
+ * Process manager interface
90
+ */
91
+ export interface ProcessManager {
92
+ name: string;
93
+
94
+ /**
95
+ * Generate start command
96
+ */
97
+ generateStartCommand(config: RuntimeConfig, environment: string): string;
98
+
99
+ /**
100
+ * Generate reload command (zero-downtime)
101
+ */
102
+ generateReloadCommand(serviceName: string): string;
103
+
104
+ /**
105
+ * Generate stop command
106
+ */
107
+ generateStopCommand(serviceName: string): string;
108
+
109
+ /**
110
+ * Generate status command
111
+ */
112
+ generateStatusCommand(serviceName: string): string;
113
+
114
+ /**
115
+ * Generate logs command
116
+ */
117
+ generateLogsCommand(serviceName: string, lines?: number): string;
118
+ }
119
+
120
+ /**
121
+ * Abstract base class for runtime adapters
122
+ */
123
+ export abstract class BaseRuntimeAdapter implements RuntimeAdapter {
124
+ abstract readonly name: Runtime;
125
+
126
+ abstract detect(cwd: string): Promise<RuntimeDetectionResult>;
127
+ abstract getDefaultConfig(cwd: string): Promise<Partial<RuntimeConfig>>;
128
+ abstract generateSetupScript(version: string, options?: SetupOptions): string;
129
+ abstract getProcessManager(): ProcessManager;
130
+
131
+ validateConfig(config: RuntimeConfig): string[] {
132
+ const errors: string[] = [];
133
+
134
+ if (!config.name) {
135
+ errors.push('Application name is required');
136
+ }
137
+
138
+ if (!config.start?.command) {
139
+ errors.push('Start command is required');
140
+ }
141
+
142
+ return errors;
143
+ }
144
+ }