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,391 @@
1
+ import ora from 'ora';
2
+ import inquirer from 'inquirer';
3
+ import { readFileSync, writeFileSync } from 'fs';
4
+ import { Logger } from '../utils/logger.js';
5
+ import { ConfigLoader } from '../config/loader.js';
6
+ import { createSSHConnection } from '../core/ssh.js';
7
+
8
+ /**
9
+ * List environment variables on server
10
+ */
11
+ export async function envListCommand(environment: string): Promise<void> {
12
+ Logger.header(`Environment Variables - ${environment}`);
13
+
14
+ const config = ConfigLoader.load();
15
+ const envConfig = config.environments[environment];
16
+
17
+ if (!envConfig) {
18
+ throw new Error(`Environment '${environment}' not found`);
19
+ }
20
+
21
+ const spinner = ora('Connecting to server...').start();
22
+
23
+ try {
24
+ const ssh = await createSSHConnection(envConfig.server);
25
+ spinner.succeed('Connected');
26
+
27
+ const remoteDir = `/var/www/${config.name}-${environment}`;
28
+ const envFile = `${remoteDir}/.env`;
29
+
30
+ // Check if .env exists
31
+ const exists = await ssh.exists(envFile);
32
+
33
+ if (!exists) {
34
+ spinner.warn('No .env file found on server');
35
+ Logger.info(`File would be at: ${envFile}`);
36
+ ssh.disconnect();
37
+ return;
38
+ }
39
+
40
+ // Read .env file (masked)
41
+ const result = await ssh.exec(`cat ${envFile}`);
42
+
43
+ Logger.br();
44
+ Logger.section('Environment Variables');
45
+
46
+ const lines = result.stdout.trim().split('\n');
47
+ const masked: string[] = [];
48
+
49
+ for (const line of lines) {
50
+ if (line.trim() && !line.startsWith('#')) {
51
+ const [key] = line.split('=');
52
+ masked.push(`${key}=***`);
53
+ }
54
+ }
55
+
56
+ if (masked.length === 0) {
57
+ Logger.info('No environment variables set');
58
+ } else {
59
+ masked.forEach(line => Logger.log(` ${line}`));
60
+ }
61
+
62
+ Logger.br();
63
+ Logger.info(`Total: ${masked.length} variable(s)`);
64
+ Logger.br();
65
+
66
+ ssh.disconnect();
67
+ } catch (error) {
68
+ spinner.fail('Failed to list environment variables');
69
+ Logger.error(error instanceof Error ? error.message : String(error));
70
+ process.exit(1);
71
+ }
72
+ }
73
+
74
+ /**
75
+ * Set environment variable on server
76
+ */
77
+ export async function envSetCommand(
78
+ environment: string,
79
+ key: string,
80
+ value: string
81
+ ): Promise<void> {
82
+ Logger.header(`Set Environment Variable - ${environment}`);
83
+
84
+ const config = ConfigLoader.load();
85
+ const envConfig = config.environments[environment];
86
+
87
+ if (!envConfig) {
88
+ throw new Error(`Environment '${environment}' not found`);
89
+ }
90
+
91
+ Logger.kv('Key', key);
92
+ Logger.kv('Value', '***');
93
+ Logger.br();
94
+
95
+ const spinner = ora('Connecting to server...').start();
96
+
97
+ try {
98
+ const ssh = await createSSHConnection(envConfig.server);
99
+ spinner.succeed('Connected');
100
+
101
+ const remoteDir = `/var/www/${config.name}-${environment}`;
102
+ const envFile = `${remoteDir}/.env`;
103
+
104
+ // Ensure directory exists
105
+ await ssh.mkdir(remoteDir, true);
106
+
107
+ // Check if .env file exists
108
+ const fileExists = await ssh.exists(envFile);
109
+
110
+ if (!fileExists) {
111
+ spinner.fail('.env file not found on server');
112
+ Logger.error('No .env file exists on the server');
113
+ Logger.info('Push environment file first:');
114
+ Logger.command(`hostfn env push ${environment} .env`);
115
+ ssh.disconnect();
116
+ process.exit(1);
117
+ }
118
+
119
+ // Check if key already exists
120
+ const checkResult = await ssh.exec(
121
+ `grep -q "^${key}=" ${envFile} 2>/dev/null && echo "exists" || echo "new"`
122
+ );
123
+
124
+ const isNew = checkResult.stdout.trim() === 'new';
125
+
126
+ const updateSpinner = ora(
127
+ isNew ? 'Adding variable...' : 'Updating variable...'
128
+ ).start();
129
+
130
+ if (isNew) {
131
+ // Append new variable
132
+ await ssh.exec(`echo "${key}=${value}" >> ${envFile}`);
133
+ } else {
134
+ // Update existing variable (use sed for safety)
135
+ await ssh.exec(
136
+ `sed -i.bak "s|^${key}=.*|${key}=${value}|" ${envFile}`
137
+ );
138
+ }
139
+
140
+ updateSpinner.succeed(isNew ? 'Variable added' : 'Variable updated');
141
+
142
+ Logger.br();
143
+ Logger.success('Environment variable set successfully!');
144
+ Logger.br();
145
+ Logger.warn('Restart service for changes to take effect:');
146
+ Logger.command(`hostfn deploy ${environment}`);
147
+ Logger.br();
148
+
149
+ ssh.disconnect();
150
+ } catch (error) {
151
+ spinner.fail('Failed to set environment variable');
152
+ Logger.error(error instanceof Error ? error.message : String(error));
153
+ process.exit(1);
154
+ }
155
+ }
156
+
157
+ /**
158
+ * Push .env file to server
159
+ */
160
+ export async function envPushCommand(
161
+ environment: string,
162
+ localFile: string
163
+ ): Promise<void> {
164
+ Logger.header(`Push Environment File - ${environment}`);
165
+
166
+ const config = ConfigLoader.load();
167
+ const envConfig = config.environments[environment];
168
+
169
+ if (!envConfig) {
170
+ throw new Error(`Environment '${environment}' not found`);
171
+ }
172
+
173
+ // Read local file
174
+ try {
175
+ const content = readFileSync(localFile, 'utf-8');
176
+ const lines = content.split('\n').filter(l => l.trim() && !l.startsWith('#'));
177
+
178
+ Logger.kv('Local file', localFile);
179
+ Logger.kv('Variables', lines.length.toString());
180
+ Logger.br();
181
+
182
+ // Confirm
183
+ const { confirm } = await inquirer.prompt([
184
+ {
185
+ type: 'confirm',
186
+ name: 'confirm',
187
+ message: `Push ${lines.length} variables to ${environment}?`,
188
+ default: false,
189
+ },
190
+ ]);
191
+
192
+ if (!confirm) {
193
+ Logger.info('Push cancelled');
194
+ return;
195
+ }
196
+
197
+ const spinner = ora('Connecting to server...').start();
198
+
199
+ const ssh = await createSSHConnection(envConfig.server);
200
+ spinner.succeed('Connected');
201
+
202
+ const remoteDir = `/var/www/${config.name}-${environment}`;
203
+ const envFile = `${remoteDir}/.env`;
204
+
205
+ // Ensure directory exists
206
+ const dirSpinner = ora('Ensuring remote directory exists...').start();
207
+ await ssh.mkdir(remoteDir, true);
208
+ dirSpinner.succeed('Remote directory ready');
209
+
210
+ // Create backup
211
+ const backupSpinner = ora('Creating backup...').start();
212
+ await ssh.exec(`cp ${envFile} ${envFile}.backup 2>/dev/null || true`);
213
+ backupSpinner.succeed('Backup created');
214
+
215
+ // Upload file
216
+ const uploadSpinner = ora('Uploading .env file...').start();
217
+
218
+ // Write content via SSH (safer than SFTP for small files)
219
+ await ssh.exec(`cat > ${envFile} << 'ENVEOF'
220
+ ${content}
221
+ ENVEOF`);
222
+
223
+ uploadSpinner.succeed('.env file uploaded');
224
+
225
+ Logger.br();
226
+ Logger.success('Environment file pushed successfully!');
227
+ Logger.br();
228
+ Logger.info('Backup saved at: ' + envFile + '.backup');
229
+ Logger.br();
230
+ Logger.warn('Restart service for changes to take effect:');
231
+ Logger.command(`hostfn deploy ${environment}`);
232
+ Logger.br();
233
+
234
+ ssh.disconnect();
235
+ } catch (error) {
236
+ Logger.error(error instanceof Error ? error.message : String(error));
237
+ process.exit(1);
238
+ }
239
+ }
240
+
241
+ /**
242
+ * Pull .env file from server
243
+ */
244
+ export async function envPullCommand(
245
+ environment: string,
246
+ localFile: string
247
+ ): Promise<void> {
248
+ Logger.header(`Pull Environment File - ${environment}`);
249
+
250
+ const config = ConfigLoader.load();
251
+ const envConfig = config.environments[environment];
252
+
253
+ if (!envConfig) {
254
+ throw new Error(`Environment '${environment}' not found`);
255
+ }
256
+
257
+ const spinner = ora('Connecting to server...').start();
258
+
259
+ try {
260
+ const ssh = await createSSHConnection(envConfig.server);
261
+ spinner.succeed('Connected');
262
+
263
+ const remoteDir = `/var/www/${config.name}-${environment}`;
264
+ const envFile = `${remoteDir}/.env`;
265
+
266
+ // Check if exists
267
+ const exists = await ssh.exists(envFile);
268
+
269
+ if (!exists) {
270
+ spinner.warn('No .env file found on server');
271
+ ssh.disconnect();
272
+ return;
273
+ }
274
+
275
+ // Download file
276
+ const downloadSpinner = ora('Downloading .env file...').start();
277
+ const result = await ssh.exec(`cat ${envFile}`);
278
+
279
+ writeFileSync(localFile, result.stdout);
280
+ downloadSpinner.succeed('.env file downloaded');
281
+
282
+ Logger.br();
283
+ Logger.success('Environment file pulled successfully!');
284
+ Logger.br();
285
+ Logger.kv('Saved to', localFile);
286
+ Logger.br();
287
+ Logger.warn('⚠️ This file contains sensitive data - do not commit to git!');
288
+ Logger.br();
289
+
290
+ ssh.disconnect();
291
+ } catch (error) {
292
+ spinner.fail('Failed to pull environment file');
293
+ Logger.error(error instanceof Error ? error.message : String(error));
294
+ process.exit(1);
295
+ }
296
+ }
297
+
298
+ /**
299
+ * Validate required environment variables
300
+ */
301
+ export async function envValidateCommand(environment: string): Promise<void> {
302
+ Logger.header(`Validate Environment Variables - ${environment}`);
303
+
304
+ const config = ConfigLoader.load();
305
+ const envConfig = config.environments[environment];
306
+
307
+ if (!envConfig) {
308
+ throw new Error(`Environment '${environment}' not found`);
309
+ }
310
+
311
+ const required = config.env.required || [];
312
+
313
+ if (required.length === 0) {
314
+ Logger.warn('No required environment variables defined in config');
315
+ Logger.info('Add them to hostfn.config.json under env.required');
316
+ return;
317
+ }
318
+
319
+ Logger.info(`Checking ${required.length} required variable(s)...`);
320
+ Logger.br();
321
+
322
+ const spinner = ora('Connecting to server...').start();
323
+
324
+ try {
325
+ const ssh = await createSSHConnection(envConfig.server);
326
+ spinner.succeed('Connected');
327
+
328
+ const remoteDir = `/var/www/${config.name}-${environment}`;
329
+ const envFile = `${remoteDir}/.env`;
330
+
331
+ // Check if exists
332
+ const exists = await ssh.exists(envFile);
333
+
334
+ if (!exists) {
335
+ Logger.error('No .env file found on server');
336
+ Logger.info('Push environment variables first:');
337
+ Logger.command(`hostfn env push ${environment} .env`);
338
+ ssh.disconnect();
339
+ process.exit(1);
340
+ }
341
+
342
+ // Read env file
343
+ const result = await ssh.exec(`cat ${envFile}`);
344
+ const content = result.stdout;
345
+
346
+ // Parse variables
347
+ const envVars = new Set<string>();
348
+ content.split('\n').forEach(line => {
349
+ if (line.trim() && !line.startsWith('#')) {
350
+ const [key] = line.split('=');
351
+ if (key) envVars.add(key.trim());
352
+ }
353
+ });
354
+
355
+ // Check each required variable
356
+ const missing: string[] = [];
357
+
358
+ Logger.section('Validation Results');
359
+
360
+ for (const varName of required) {
361
+ if (envVars.has(varName)) {
362
+ Logger.log(` ✓ ${varName}`);
363
+ } else {
364
+ Logger.log(` ✗ ${varName} (missing)`);
365
+ missing.push(varName);
366
+ }
367
+ }
368
+
369
+ Logger.br();
370
+
371
+ if (missing.length === 0) {
372
+ Logger.success('All required environment variables are set!');
373
+ } else {
374
+ Logger.error(`Missing ${missing.length} required variable(s)`);
375
+ Logger.br();
376
+ Logger.info('Set missing variables:');
377
+ missing.forEach(v => {
378
+ Logger.command(`hostfn env set ${environment} ${v} "value"`);
379
+ });
380
+ process.exit(1);
381
+ }
382
+
383
+ Logger.br();
384
+
385
+ ssh.disconnect();
386
+ } catch (error) {
387
+ spinner.fail('Failed to validate environment variables');
388
+ Logger.error(error instanceof Error ? error.message : String(error));
389
+ process.exit(1);
390
+ }
391
+ }