hostfn 0.1.0 → 0.1.2

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 (179) hide show
  1. package/dist/__tests__/core/backup.test.d.ts +2 -0
  2. package/dist/__tests__/core/backup.test.d.ts.map +1 -0
  3. package/dist/__tests__/core/backup.test.js +108 -0
  4. package/dist/__tests__/core/backup.test.js.map +1 -0
  5. package/dist/__tests__/core/health.test.d.ts +2 -0
  6. package/dist/__tests__/core/health.test.d.ts.map +1 -0
  7. package/dist/__tests__/core/health.test.js +97 -0
  8. package/dist/__tests__/core/health.test.js.map +1 -0
  9. package/dist/__tests__/core/lock.test.d.ts +2 -0
  10. package/dist/__tests__/core/lock.test.d.ts.map +1 -0
  11. package/dist/__tests__/core/lock.test.js +136 -0
  12. package/dist/__tests__/core/lock.test.js.map +1 -0
  13. package/dist/__tests__/core/nginx-multi-domain.test.d.ts +2 -0
  14. package/dist/__tests__/core/nginx-multi-domain.test.d.ts.map +1 -0
  15. package/dist/__tests__/core/nginx-multi-domain.test.js +158 -0
  16. package/dist/__tests__/core/nginx-multi-domain.test.js.map +1 -0
  17. package/dist/__tests__/runtimes/pm2.test.d.ts +2 -0
  18. package/dist/__tests__/runtimes/pm2.test.d.ts.map +1 -0
  19. package/dist/__tests__/runtimes/pm2.test.js +111 -0
  20. package/dist/__tests__/runtimes/pm2.test.js.map +1 -0
  21. package/dist/__tests__/utils/validation.test.d.ts +2 -0
  22. package/dist/__tests__/utils/validation.test.d.ts.map +1 -0
  23. package/dist/__tests__/utils/validation.test.js +136 -0
  24. package/dist/__tests__/utils/validation.test.js.map +1 -0
  25. package/dist/commands/deploy.d.ts +11 -0
  26. package/dist/commands/deploy.d.ts.map +1 -0
  27. package/dist/commands/deploy.js +636 -0
  28. package/dist/commands/deploy.js.map +1 -0
  29. package/dist/commands/env.d.ts +21 -0
  30. package/dist/commands/env.d.ts.map +1 -0
  31. package/dist/commands/env.js +317 -0
  32. package/dist/commands/env.js.map +1 -0
  33. package/dist/commands/expose.d.ts +6 -0
  34. package/dist/commands/expose.d.ts.map +1 -0
  35. package/dist/commands/expose.js +379 -0
  36. package/dist/commands/expose.js.map +1 -0
  37. package/dist/commands/init.d.ts +2 -0
  38. package/dist/commands/init.d.ts.map +1 -0
  39. package/dist/commands/init.js +175 -0
  40. package/dist/commands/init.js.map +1 -0
  41. package/dist/commands/logs.d.ts +10 -0
  42. package/dist/commands/logs.d.ts.map +1 -0
  43. package/dist/commands/logs.js +75 -0
  44. package/dist/commands/logs.js.map +1 -0
  45. package/dist/commands/rollback.d.ts +6 -0
  46. package/dist/commands/rollback.d.ts.map +1 -0
  47. package/dist/commands/rollback.js +113 -0
  48. package/dist/commands/rollback.js.map +1 -0
  49. package/dist/commands/server/info.d.ts +2 -0
  50. package/dist/commands/server/info.d.ts.map +1 -0
  51. package/dist/commands/server/info.js +104 -0
  52. package/dist/commands/server/info.js.map +1 -0
  53. package/dist/commands/server/setup.d.ts +11 -0
  54. package/dist/commands/server/setup.d.ts.map +1 -0
  55. package/dist/commands/server/setup.js +161 -0
  56. package/dist/commands/server/setup.js.map +1 -0
  57. package/dist/commands/status.d.ts +6 -0
  58. package/dist/commands/status.d.ts.map +1 -0
  59. package/dist/commands/status.js +120 -0
  60. package/dist/commands/status.js.map +1 -0
  61. package/dist/config/loader.d.ts +21 -0
  62. package/dist/config/loader.d.ts.map +1 -0
  63. package/dist/config/loader.js +54 -0
  64. package/dist/config/loader.js.map +1 -0
  65. package/dist/config/schema.d.ts +323 -0
  66. package/dist/config/schema.d.ts.map +1 -0
  67. package/dist/config/schema.js +108 -0
  68. package/dist/config/schema.js.map +1 -0
  69. package/dist/core/backup.d.ts +34 -0
  70. package/dist/core/backup.d.ts.map +1 -0
  71. package/dist/core/backup.js +95 -0
  72. package/dist/core/backup.js.map +1 -0
  73. package/dist/core/health.d.ts +31 -0
  74. package/dist/core/health.d.ts.map +1 -0
  75. package/dist/core/health.js +78 -0
  76. package/dist/core/health.js.map +1 -0
  77. package/dist/core/local.d.ts +19 -0
  78. package/dist/core/local.d.ts.map +1 -0
  79. package/dist/core/local.js +50 -0
  80. package/dist/core/local.js.map +1 -0
  81. package/dist/core/lock.d.ts +28 -0
  82. package/dist/core/lock.d.ts.map +1 -0
  83. package/dist/core/lock.js +89 -0
  84. package/dist/core/lock.js.map +1 -0
  85. package/dist/core/nginx.d.ts +43 -0
  86. package/dist/core/nginx.d.ts.map +1 -0
  87. package/dist/core/nginx.js +131 -0
  88. package/dist/core/nginx.js.map +1 -0
  89. package/dist/core/ssh.d.ts +79 -0
  90. package/dist/core/ssh.d.ts.map +1 -0
  91. package/dist/core/ssh.js +264 -0
  92. package/dist/core/ssh.js.map +1 -0
  93. package/dist/core/sync.d.ts +25 -0
  94. package/dist/core/sync.d.ts.map +1 -0
  95. package/dist/core/sync.js +117 -0
  96. package/dist/core/sync.js.map +1 -0
  97. package/dist/core/workspace.d.ts +13 -0
  98. package/dist/core/workspace.d.ts.map +1 -0
  99. package/dist/core/workspace.js +141 -0
  100. package/dist/core/workspace.js.map +1 -0
  101. package/dist/index.d.ts +3 -0
  102. package/dist/index.d.ts.map +1 -0
  103. package/dist/index.js +232 -0
  104. package/dist/index.js.map +1 -0
  105. package/dist/runtimes/base.d.ts +115 -0
  106. package/dist/runtimes/base.d.ts.map +1 -0
  107. package/dist/runtimes/base.js +16 -0
  108. package/dist/runtimes/base.js.map +1 -0
  109. package/dist/runtimes/nodejs/detector.d.ts +47 -0
  110. package/dist/runtimes/nodejs/detector.d.ts.map +1 -0
  111. package/dist/runtimes/nodejs/detector.js +143 -0
  112. package/dist/runtimes/nodejs/detector.js.map +1 -0
  113. package/dist/runtimes/nodejs/index.d.ts +14 -0
  114. package/dist/runtimes/nodejs/index.d.ts.map +1 -0
  115. package/dist/runtimes/nodejs/index.js +213 -0
  116. package/dist/runtimes/nodejs/index.js.map +1 -0
  117. package/dist/runtimes/nodejs/pm2.d.ts +17 -0
  118. package/dist/runtimes/nodejs/pm2.d.ts.map +1 -0
  119. package/dist/runtimes/nodejs/pm2.js +60 -0
  120. package/dist/runtimes/nodejs/pm2.js.map +1 -0
  121. package/dist/runtimes/registry.d.ts +34 -0
  122. package/dist/runtimes/registry.d.ts.map +1 -0
  123. package/dist/runtimes/registry.js +58 -0
  124. package/dist/runtimes/registry.js.map +1 -0
  125. package/dist/utils/logger.d.ts +47 -0
  126. package/dist/utils/logger.d.ts.map +1 -0
  127. package/dist/utils/logger.js +76 -0
  128. package/dist/utils/logger.js.map +1 -0
  129. package/dist/utils/validation.d.ts +32 -0
  130. package/dist/utils/validation.d.ts.map +1 -0
  131. package/dist/utils/validation.js +125 -0
  132. package/dist/utils/validation.js.map +1 -0
  133. package/package.json +34 -20
  134. package/{packages/cli/src → src}/__tests__/runtimes/pm2.test.ts +2 -2
  135. package/{packages/cli/src → src}/commands/deploy.ts +39 -23
  136. package/{packages/cli/src → src}/commands/env.ts +19 -6
  137. package/{packages/cli/src → src}/commands/expose.ts +20 -6
  138. package/{packages/cli/src → src}/commands/logs.ts +8 -4
  139. package/{packages/cli/src → src}/commands/rollback.ts +8 -6
  140. package/{packages/cli/src → src}/commands/status.ts +7 -3
  141. package/LICENSE +0 -21
  142. package/README.md +0 -1136
  143. package/_conduct/specs/1.v0.spec.md +0 -1041
  144. package/examples/express-api/package.json +0 -22
  145. package/examples/express-api/src/index.ts +0 -16
  146. package/examples/express-api/tsconfig.json +0 -11
  147. package/examples/github-actions-deploy.yml +0 -40
  148. package/examples/monorepo-config.json +0 -76
  149. package/examples/monorepo-multi-server-config.json +0 -74
  150. package/packages/cli/package.json +0 -40
  151. package/turbo.json +0 -24
  152. /package/{packages/cli/src → src}/__tests__/core/backup.test.ts +0 -0
  153. /package/{packages/cli/src → src}/__tests__/core/health.test.ts +0 -0
  154. /package/{packages/cli/src → src}/__tests__/core/lock.test.ts +0 -0
  155. /package/{packages/cli/src → src}/__tests__/core/nginx-multi-domain.test.ts +0 -0
  156. /package/{packages/cli/src → src}/__tests__/utils/validation.test.ts +0 -0
  157. /package/{packages/cli/src → src}/commands/init.ts +0 -0
  158. /package/{packages/cli/src → src}/commands/server/info.ts +0 -0
  159. /package/{packages/cli/src → src}/commands/server/setup.ts +0 -0
  160. /package/{packages/cli/src → src}/config/loader.ts +0 -0
  161. /package/{packages/cli/src → src}/config/schema.ts +0 -0
  162. /package/{packages/cli/src → src}/core/backup.ts +0 -0
  163. /package/{packages/cli/src → src}/core/health.ts +0 -0
  164. /package/{packages/cli/src → src}/core/local.ts +0 -0
  165. /package/{packages/cli/src → src}/core/lock.ts +0 -0
  166. /package/{packages/cli/src → src}/core/nginx.ts +0 -0
  167. /package/{packages/cli/src → src}/core/ssh.ts +0 -0
  168. /package/{packages/cli/src → src}/core/sync.ts +0 -0
  169. /package/{packages/cli/src → src}/core/workspace.ts +0 -0
  170. /package/{packages/cli/src → src}/index.ts +0 -0
  171. /package/{packages/cli/src → src}/runtimes/base.ts +0 -0
  172. /package/{packages/cli/src → src}/runtimes/nodejs/detector.ts +0 -0
  173. /package/{packages/cli/src → src}/runtimes/nodejs/index.ts +0 -0
  174. /package/{packages/cli/src → src}/runtimes/nodejs/pm2.ts +0 -0
  175. /package/{packages/cli/src → src}/runtimes/registry.ts +0 -0
  176. /package/{packages/cli/src → src}/utils/logger.ts +0 -0
  177. /package/{packages/cli/src → src}/utils/validation.ts +0 -0
  178. /package/{packages/cli/tsconfig.json → tsconfig.json} +0 -0
  179. /package/{packages/cli/vitest.config.ts → vitest.config.ts} +0 -0
@@ -0,0 +1,125 @@
1
+ import { Logger } from './logger.js';
2
+ /**
3
+ * Validation utilities
4
+ */
5
+ /**
6
+ * Validate SSH connection string format
7
+ */
8
+ export function validateSSHConnection(connectionString) {
9
+ const regex = /^([^@]+)@([^:]+)(?::(\d+))?$/;
10
+ if (!regex.test(connectionString)) {
11
+ Logger.error(`Invalid SSH connection string: ${connectionString}`);
12
+ Logger.info('Expected format: user@host or user@host:port');
13
+ Logger.info('Examples:');
14
+ Logger.log(' ubuntu@123.45.67.89');
15
+ Logger.log(' ubuntu@myserver.com:2222');
16
+ return false;
17
+ }
18
+ return true;
19
+ }
20
+ /**
21
+ * Validate HTTP URL format
22
+ */
23
+ export function validateHttpUrl(url) {
24
+ try {
25
+ const parsed = new URL(url);
26
+ if (!['http:', 'https:'].includes(parsed.protocol)) {
27
+ Logger.error(`Invalid URL protocol: ${parsed.protocol}`);
28
+ Logger.info('Expected: http:// or https://');
29
+ return false;
30
+ }
31
+ return true;
32
+ }
33
+ catch (error) {
34
+ Logger.error(`Invalid URL format: ${url}`);
35
+ Logger.info('Expected format: http://host:port/path');
36
+ return false;
37
+ }
38
+ }
39
+ /**
40
+ * Validate environment name
41
+ */
42
+ export function validateEnvironmentName(name) {
43
+ const regex = /^[a-zA-Z0-9_-]+$/;
44
+ if (!regex.test(name)) {
45
+ Logger.error(`Invalid environment name: ${name}`);
46
+ Logger.info('Environment names can only contain: a-z, A-Z, 0-9, _, -');
47
+ Logger.info('Examples: production, staging, dev, prod-eu');
48
+ return false;
49
+ }
50
+ return true;
51
+ }
52
+ /**
53
+ * Validate port number
54
+ */
55
+ export function validatePort(port) {
56
+ if (port < 1 || port > 65535) {
57
+ Logger.error(`Invalid port number: ${port}`);
58
+ Logger.info('Port must be between 1 and 65535');
59
+ return false;
60
+ }
61
+ if (port < 1024) {
62
+ Logger.warn(`Port ${port} is privileged (< 1024) - may require sudo`);
63
+ }
64
+ return true;
65
+ }
66
+ /**
67
+ * Validate Node.js version format
68
+ */
69
+ export function validateNodeVersion(version) {
70
+ const regex = /^\d+(\.\d+)?(\.\d+)?$/;
71
+ if (!regex.test(version)) {
72
+ Logger.error(`Invalid Node.js version: ${version}`);
73
+ Logger.info('Expected format: 18 or 18.19 or 18.19.0');
74
+ return false;
75
+ }
76
+ const major = parseInt(version.split('.')[0]);
77
+ if (major < 14) {
78
+ Logger.warn(`Node.js ${version} is very old - consider using 18 or 20`);
79
+ }
80
+ return true;
81
+ }
82
+ /**
83
+ * Validate application name
84
+ */
85
+ export function validateAppName(name) {
86
+ const regex = /^[a-zA-Z0-9_-]+$/;
87
+ if (!regex.test(name)) {
88
+ Logger.error(`Invalid application name: ${name}`);
89
+ Logger.info('Names can only contain: a-z, A-Z, 0-9, _, -');
90
+ Logger.info('Examples: my-app, api_server, webapp-v2');
91
+ return false;
92
+ }
93
+ if (name.length < 2) {
94
+ Logger.error('Application name must be at least 2 characters');
95
+ return false;
96
+ }
97
+ if (name.length > 50) {
98
+ Logger.error('Application name must be less than 50 characters');
99
+ return false;
100
+ }
101
+ return true;
102
+ }
103
+ /**
104
+ * Validate remote directory path
105
+ */
106
+ export function validateRemotePath(path) {
107
+ if (!path.startsWith('/')) {
108
+ Logger.error(`Remote path must be absolute: ${path}`);
109
+ Logger.info('Example: /var/www/my-app');
110
+ return false;
111
+ }
112
+ if (path.includes('..')) {
113
+ Logger.error('Remote path cannot contain ..');
114
+ return false;
115
+ }
116
+ // Warn about dangerous paths
117
+ const dangerousPaths = ['/', '/etc', '/bin', '/usr', '/var/log', '/root', '/home'];
118
+ if (dangerousPaths.includes(path)) {
119
+ Logger.error(`Refusing to deploy to system directory: ${path}`);
120
+ Logger.info('Use a subdirectory like /var/www/my-app');
121
+ return false;
122
+ }
123
+ return true;
124
+ }
125
+ //# sourceMappingURL=validation.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"validation.js","sourceRoot":"","sources":["../../src/utils/validation.ts"],"names":[],"mappings":"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"}
package/package.json CHANGED
@@ -1,26 +1,40 @@
1
1
  {
2
2
  "name": "hostfn",
3
- "version": "0.1.0",
4
- "description": "Universal application deployment CLI",
3
+ "version": "0.1.2",
5
4
  "license": "Apache-2.0",
6
- "workspaces": [
7
- "packages/*"
8
- ],
5
+ "description": "Universal application deployment CLI",
6
+ "type": "module",
7
+ "bin": {
8
+ "hostfn": "./dist/index.js"
9
+ },
9
10
  "scripts": {
10
- "build": "turbo run build",
11
- "dev": "turbo run dev",
12
- "lint": "turbo run lint",
13
- "test": "turbo run test",
14
- "clean": "turbo run clean && rm -rf node_modules"
11
+ "dev": "tsx watch src/index.ts",
12
+ "build": "tsc",
13
+ "clean": "rm -rf dist",
14
+ "lint": "tsc --noEmit",
15
+ "test": "vitest",
16
+ "test:watch": "vitest --watch",
17
+ "test:coverage": "vitest --coverage"
18
+ },
19
+ "dependencies": {
20
+ "commander": "^11.1.0",
21
+ "chalk": "^5.3.0",
22
+ "ora": "^7.0.1",
23
+ "inquirer": "^9.2.12",
24
+ "zod": "^3.22.4",
25
+ "ssh2": "^1.15.0",
26
+ "dotenv": "^16.3.1",
27
+ "execa": "^8.0.1"
15
28
  },
16
29
  "devDependencies": {
17
- "turbo": "^1.11.0",
18
- "typescript": "^5.3.0"
30
+ "@types/node": "^20.10.0",
31
+ "@types/ssh2": "^1.15.0",
32
+ "@types/inquirer": "^9.0.7",
33
+ "@vitest/coverage-v8": "^1.0.0",
34
+ "typescript": "^5.3.0",
35
+ "tsx": "^4.7.0",
36
+ "vitest": "^1.0.0"
19
37
  },
20
- "keywords": [
21
- "deployment",
22
- "vps"
23
- ],
24
38
  "author": "21n",
25
39
  "repository": {
26
40
  "type": "git",
@@ -29,11 +43,11 @@
29
43
  "bugs": {
30
44
  "url": "https://github.com/21nCo/hostfn/issues"
31
45
  },
46
+ "keywords": [
47
+ "deployment",
48
+ "vps"
49
+ ],
32
50
  "engines": {
33
51
  "node": ">=18.0.0"
34
- },
35
- "packageManager": "npm@10.2.0",
36
- "dependencies": {
37
- "fast-glob": "^3.3.3"
38
52
  }
39
53
  }
@@ -22,8 +22,8 @@ describe('PM2Manager', () => {
22
22
 
23
23
  expect(result).toContain("name: 'test-app-production'");
24
24
  expect(result).toContain("script: 'dist/index.js'");
25
- expect(result).toContain("env_file: '.env'");
26
- expect(result).toContain("NODE_ENV: 'production'");
25
+ // Implementation uses inline env with double quotes (JSON.stringify)
26
+ expect(result).toContain('NODE_ENV: "production"');
27
27
  expect(result).toContain('PORT: 3000');
28
28
  expect(result).toContain("instances: 'max'");
29
29
  expect(result).toContain("exec_mode: 'cluster'");
@@ -181,24 +181,32 @@ async function deployMultiService(
181
181
  const servicePath = serviceConfig.path;
182
182
  const originalCwd = process.cwd();
183
183
 
184
- // Change to service directory
185
- process.chdir(servicePath);
186
-
187
- // Deploy single service
188
- await deploySingleService(
189
- serviceSpecificConfig,
190
- serviceEnvConfig,
191
- environment,
192
- options,
193
- servicePath
194
- );
195
-
196
- // Restore original directory
197
- process.chdir(originalCwd);
198
-
199
- results.push({ service: serviceName, success: true });
200
- Logger.success(`Service '${serviceName}' deployed successfully`);
201
- Logger.br();
184
+ try {
185
+ // Change to service directory
186
+ process.chdir(servicePath);
187
+
188
+ // Deploy single service
189
+ await deploySingleService(
190
+ serviceSpecificConfig,
191
+ serviceEnvConfig,
192
+ environment,
193
+ options,
194
+ servicePath
195
+ );
196
+
197
+ results.push({ service: serviceName, success: true });
198
+ Logger.success(`Service '${serviceName}' deployed successfully`);
199
+ Logger.br();
200
+ } catch (error) {
201
+ // Restore original directory in case of error
202
+ process.chdir(originalCwd);
203
+ throw error;
204
+ } finally {
205
+ // Restore original directory
206
+ if (process.cwd() !== originalCwd) {
207
+ process.chdir(originalCwd);
208
+ }
209
+ }
202
210
  } catch (error) {
203
211
  const errorMsg = error instanceof Error ? error.message : String(error);
204
212
  results.push({ service: serviceName, success: false, error: errorMsg });
@@ -546,11 +554,19 @@ async function deploySingleService(
546
554
  const backupManager = new BackupManager(ssh as any, remoteDir);
547
555
  const backupSpinner = ora('Creating backup of current deployment...').start();
548
556
 
549
- try {
550
- backupPath = await backupManager.create();
551
- backupSpinner.succeed(`Backup created: ${backupPath.split('/').pop()}`);
552
- } catch (error) {
553
- backupSpinner.warn('No existing deployment to backup');
557
+ // Check if there's anything to backup
558
+ const hasExistingDeployment = await ssh.exists(`${remoteDir}/dist`);
559
+
560
+ if (!hasExistingDeployment) {
561
+ backupSpinner.info('No existing deployment to backup');
562
+ } else {
563
+ try {
564
+ backupPath = await backupManager.create();
565
+ backupSpinner.succeed(`Backup created: ${backupPath.split('/').pop()}`);
566
+ } catch (error) {
567
+ backupSpinner.fail('Failed to create backup');
568
+ throw error;
569
+ }
554
570
  }
555
571
 
556
572
  Logger.br();
@@ -116,6 +116,11 @@ export async function envSetCommand(
116
116
  process.exit(1);
117
117
  }
118
118
 
119
+ // Validate key format
120
+ if (!/^[A-Z_][A-Z0-9_]*$/.test(key)) {
121
+ throw new Error('Invalid key format. Must be uppercase, alphanumeric, and underscores only.');
122
+ }
123
+
119
124
  // Check if key already exists
120
125
  const checkResult = await ssh.exec(
121
126
  `grep -q "^${key}=" ${envFile} 2>/dev/null && echo "exists" || echo "new"`
@@ -127,15 +132,23 @@ export async function envSetCommand(
127
132
  isNew ? 'Adding variable...' : 'Updating variable...'
128
133
  ).start();
129
134
 
135
+ // Use a temporary file strategy to avoid shell injection issues with value
136
+ const tempFile = `/tmp/env_var_${Date.now()}`;
137
+ await ssh.exec(`echo "${value.replace(/"/g, '\\"')}" > ${tempFile}`);
138
+
130
139
  if (isNew) {
131
- // Append new variable
132
- await ssh.exec(`echo "${key}=${value}" >> ${envFile}`);
140
+ // Append new variable safely
141
+ await ssh.exec(`echo "${key}=\\"$(cat ${tempFile})\\"" >> ${envFile}`);
133
142
  } else {
134
- // Update existing variable (use sed for safety)
135
- await ssh.exec(
136
- `sed -i.bak "s|^${key}=.*|${key}=${value}|" ${envFile}`
137
- );
143
+ // Update existing variable safely using temp file content
144
+ // We use a more complex sed or awk command, or just overwrite the line
145
+ // Constructing safe sed is hard, so let's use grep -v to remove old and append new
146
+ await ssh.exec(`grep -v "^${key}=" ${envFile} > ${envFile}.tmp && mv ${envFile}.tmp ${envFile}`);
147
+ await ssh.exec(`echo "${key}=\\"$(cat ${tempFile})\\"" >> ${envFile}`);
138
148
  }
149
+
150
+ // Cleanup temp file
151
+ await ssh.exec(`rm ${tempFile}`);
139
152
 
140
153
  updateSpinner.succeed(isNew ? 'Variable added' : 'Variable updated');
141
154
 
@@ -94,7 +94,8 @@ export async function exposeCommand(environment: string, options: {
94
94
  if (shouldSetupSsl && domains.length > 0) {
95
95
  // Check if certificate exists for the primary domain (first in the list)
96
96
  const primaryDomain = domains[0];
97
- const certCheck = await ssh.exec(`sudo test -d /etc/letsencrypt/live/${primaryDomain} && echo "exists"`);
97
+ const safePrimaryDomain = `'${primaryDomain.replace(/'/g, "'\\''")}'`;
98
+ const certCheck = await ssh.exec(`sudo test -d /etc/letsencrypt/live/${safePrimaryDomain} && echo "exists"`);
98
99
  certsExist = certCheck.stdout.trim() === 'exists';
99
100
  }
100
101
 
@@ -114,10 +115,16 @@ export async function exposeCommand(environment: string, options: {
114
115
 
115
116
  // Only include services on this server
116
117
  if (serviceEnvConfig.server === host) {
118
+ const port = serviceConfig.port;
119
+ // Validate port
120
+ if (!port || isNaN(Number(port)) || Number(port) < 1 || Number(port) > 65535) {
121
+ throw new Error(`Invalid port for service '${serviceName}': ${port}. Must be between 1 and 65535.`);
122
+ }
123
+
117
124
  const isDefault = !serviceConfig.exposePath;
118
125
  services.push({
119
126
  name: `${config.name}-${serviceName}`,
120
- port: serviceConfig.port,
127
+ port: Number(port),
121
128
  exposePath: serviceConfig.exposePath,
122
129
  isDefault,
123
130
  });
@@ -136,9 +143,15 @@ export async function exposeCommand(environment: string, options: {
136
143
  Logger.section('Configuring Single Service');
137
144
  Logger.br();
138
145
 
146
+ const port = envConfig.port;
147
+ // Validate port
148
+ if (!port || isNaN(Number(port)) || Number(port) < 1 || Number(port) > 65535) {
149
+ throw new Error(`Invalid port for environment '${environment}': ${port}. Must be between 1 and 65535.`);
150
+ }
151
+
139
152
  services.push({
140
153
  name: `${config.name}-${environment}`,
141
- port: envConfig.port,
154
+ port: Number(port),
142
155
  isDefault: true,
143
156
  });
144
157
 
@@ -247,7 +260,7 @@ EOF`);
247
260
 
248
261
  // Setup SSL if domains are configured
249
262
  if (shouldSetupSsl && domains.length > 0 && sslEmail) {
250
- await setupSSL(ssh, domains, sslEmail, environment);
263
+ await setupSSL(ssh, host, domains, sslEmail, environment);
251
264
  }
252
265
 
253
266
  // Success summary
@@ -293,6 +306,7 @@ EOF`);
293
306
  */
294
307
  async function setupSSL(
295
308
  ssh: SSHConnection,
309
+ host: string,
296
310
  domains: string[],
297
311
  email: string,
298
312
  environment: string
@@ -371,7 +385,7 @@ async function setupSSL(
371
385
  Logger.warn('SSL expansion failed');
372
386
  Logger.info('You can manually run certbot with --expand:');
373
387
  const manualDomainFlags = domains.map(d => `-d ${d}`).join(' ');
374
- Logger.command(`ssh ${ssh} "sudo certbot --nginx ${manualDomainFlags} --expand"`);
388
+ Logger.command(`ssh ${host} "sudo certbot --nginx ${manualDomainFlags} --expand"`);
375
389
  return;
376
390
  }
377
391
  } else {
@@ -416,7 +430,7 @@ async function setupSSL(
416
430
  Logger.warn('SSL setup failed, but nginx is still configured for HTTP');
417
431
  Logger.info('You can manually run certbot later:');
418
432
  const manualDomainFlags = domains.map(d => `-d ${d}`).join(' ');
419
- Logger.command(`ssh ${ssh} "sudo certbot --nginx ${manualDomainFlags}"`);
433
+ Logger.command(`ssh ${host} "sudo certbot --nginx ${manualDomainFlags}"`);
420
434
  return;
421
435
  }
422
436
  }
@@ -66,9 +66,10 @@ async function fetchLogs(
66
66
  const pm2 = adapter.getProcessManager();
67
67
 
68
68
  const spinner = ora('Connecting to server...').start();
69
+ let ssh: { disconnect: () => void } | null = null;
69
70
 
70
71
  try {
71
- const ssh = await createSSHConnection(server);
72
+ ssh = await createSSHConnection(server);
72
73
  spinner.succeed('Connected');
73
74
 
74
75
  Logger.br();
@@ -82,10 +83,10 @@ async function fetchLogs(
82
83
  }
83
84
 
84
85
  // Execute logs command
85
- const result = await ssh.exec(logsCmd, { streaming: !options.output });
86
+ const result = await (ssh as any).exec(logsCmd, { streaming: !options.output });
86
87
 
87
88
  if (result.exitCode !== 0) {
88
- Logger.error('Failed to fetch logs');
89
+ throw new Error(result.stderr || 'Failed to fetch logs');
89
90
  }
90
91
 
91
92
  // Save to file if requested
@@ -97,10 +98,13 @@ async function fetchLogs(
97
98
  Logger.kv('Size', `${(result.stdout.length / 1024).toFixed(2)} KB`);
98
99
  }
99
100
 
100
- ssh.disconnect();
101
101
  } catch (error) {
102
102
  spinner.fail('Failed to get logs');
103
103
  Logger.error(error instanceof Error ? error.message : String(error));
104
104
  process.exit(1);
105
+ } finally {
106
+ if (ssh) {
107
+ ssh.disconnect();
108
+ }
105
109
  }
106
110
  }
@@ -28,14 +28,15 @@ export async function rollbackCommand(
28
28
  const serviceName = `${config.name}-${environment}`;
29
29
 
30
30
  const spinner = ora('Connecting to server...').start();
31
+ let ssh: { disconnect: () => void } | null = null;
31
32
 
32
33
  try {
33
- const ssh = await createSSHConnection(envConfig.server);
34
+ ssh = await createSSHConnection(envConfig.server);
34
35
  spinner.succeed('Connected');
35
36
 
36
37
  Logger.br();
37
38
 
38
- const backupManager = new BackupManager(ssh, remoteDir);
39
+ const backupManager = new BackupManager(ssh as any, remoteDir);
39
40
 
40
41
  // List available backups
41
42
  const listSpinner = ora('Fetching available backups...').start();
@@ -44,7 +45,6 @@ export async function rollbackCommand(
44
45
 
45
46
  if (backups.length === 0) {
46
47
  Logger.warn('No backups available');
47
- ssh.disconnect();
48
48
  return;
49
49
  }
50
50
 
@@ -96,7 +96,6 @@ export async function rollbackCommand(
96
96
 
97
97
  if (!confirm) {
98
98
  Logger.info('Rollback cancelled');
99
- ssh.disconnect();
100
99
  return;
101
100
  }
102
101
 
@@ -112,7 +111,7 @@ export async function rollbackCommand(
112
111
  const pm2 = adapter.getProcessManager();
113
112
 
114
113
  const reloadSpinner = ora('Reloading service...').start();
115
- const reloadResult = await ssh.exec(
114
+ const reloadResult = await (ssh as any).exec(
116
115
  pm2.generateReloadCommand(serviceName),
117
116
  { cwd: remoteDir }
118
117
  );
@@ -133,10 +132,13 @@ export async function rollbackCommand(
133
132
  Logger.command(`hostfn status ${environment}`);
134
133
  Logger.br();
135
134
 
136
- ssh.disconnect();
137
135
  } catch (error) {
138
136
  spinner.fail('Rollback failed');
139
137
  Logger.error(error instanceof Error ? error.message : String(error));
140
138
  process.exit(1);
139
+ } finally {
140
+ if (ssh) {
141
+ ssh.disconnect();
142
+ }
141
143
  }
142
144
  }
@@ -75,15 +75,16 @@ async function showServiceStatus(
75
75
  displayName?: string
76
76
  ): Promise<void> {
77
77
  const spinner = ora(`Fetching status for ${displayName || serviceName}...`).start();
78
+ let ssh: { disconnect: () => void } | null = null;
78
79
 
79
80
  try {
80
- const ssh = await createSSHConnection(server);
81
+ ssh = await createSSHConnection(server);
81
82
  spinner.succeed('Connected');
82
83
 
83
84
  Logger.br();
84
85
 
85
86
  // Get PM2 service details
86
- const result = await ssh.exec(`pm2 jlist | jq '.[] | select(.name=="${serviceName}")'`);
87
+ const result = await (ssh as any).exec(`pm2 jlist | jq '.[] | select(.name=="${serviceName}")'`);
87
88
 
88
89
  if (result.stdout.trim()) {
89
90
  const service = JSON.parse(result.stdout);
@@ -129,10 +130,13 @@ async function showServiceStatus(
129
130
  Logger.info('Has the service been deployed?');
130
131
  }
131
132
 
132
- ssh.disconnect();
133
133
  } catch (error) {
134
134
  spinner.fail('Failed to get status');
135
135
  throw error;
136
+ } finally {
137
+ if (ssh) {
138
+ ssh.disconnect();
139
+ }
136
140
  }
137
141
  }
138
142
 
package/LICENSE DELETED
@@ -1,21 +0,0 @@
1
- MIT License
2
-
3
- Copyright (c) 2025 21n
4
-
5
- Permission is hereby granted, free of charge, to any person obtaining a copy
6
- of this software and associated documentation files (the "Software"), to deal
7
- in the Software without restriction, including without limitation the rights
8
- to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
- copies of the Software, and to permit persons to whom the Software is
10
- furnished to do so, subject to the following conditions:
11
-
12
- The above copyright notice and this permission notice shall be included in all
13
- copies or substantial portions of the Software.
14
-
15
- THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
- IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
- FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
- AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
- LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
- OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
- SOFTWARE.