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.
- package/LICENSE +21 -0
- package/README.md +1136 -0
- package/_conduct/specs/1.v0.spec.md +1041 -0
- package/examples/express-api/package.json +22 -0
- package/examples/express-api/src/index.ts +16 -0
- package/examples/express-api/tsconfig.json +11 -0
- package/examples/github-actions-deploy.yml +40 -0
- package/examples/monorepo-config.json +76 -0
- package/examples/monorepo-multi-server-config.json +74 -0
- package/package.json +39 -0
- package/packages/cli/package.json +40 -0
- package/packages/cli/src/__tests__/core/backup.test.ts +137 -0
- package/packages/cli/src/__tests__/core/health.test.ts +125 -0
- package/packages/cli/src/__tests__/core/lock.test.ts +173 -0
- package/packages/cli/src/__tests__/core/nginx-multi-domain.test.ts +176 -0
- package/packages/cli/src/__tests__/runtimes/pm2.test.ts +130 -0
- package/packages/cli/src/__tests__/utils/validation.test.ts +164 -0
- package/packages/cli/src/commands/deploy.ts +817 -0
- package/packages/cli/src/commands/env.ts +391 -0
- package/packages/cli/src/commands/expose.ts +438 -0
- package/packages/cli/src/commands/init.ts +192 -0
- package/packages/cli/src/commands/logs.ts +106 -0
- package/packages/cli/src/commands/rollback.ts +142 -0
- package/packages/cli/src/commands/server/info.ts +131 -0
- package/packages/cli/src/commands/server/setup.ts +200 -0
- package/packages/cli/src/commands/status.ts +149 -0
- package/packages/cli/src/config/loader.ts +66 -0
- package/packages/cli/src/config/schema.ts +140 -0
- package/packages/cli/src/core/backup.ts +128 -0
- package/packages/cli/src/core/health.ts +116 -0
- package/packages/cli/src/core/local.ts +67 -0
- package/packages/cli/src/core/lock.ts +108 -0
- package/packages/cli/src/core/nginx.ts +170 -0
- package/packages/cli/src/core/ssh.ts +335 -0
- package/packages/cli/src/core/sync.ts +138 -0
- package/packages/cli/src/core/workspace.ts +180 -0
- package/packages/cli/src/index.ts +240 -0
- package/packages/cli/src/runtimes/base.ts +144 -0
- package/packages/cli/src/runtimes/nodejs/detector.ts +157 -0
- package/packages/cli/src/runtimes/nodejs/index.ts +228 -0
- package/packages/cli/src/runtimes/nodejs/pm2.ts +71 -0
- package/packages/cli/src/runtimes/registry.ts +76 -0
- package/packages/cli/src/utils/logger.ts +86 -0
- package/packages/cli/src/utils/validation.ts +147 -0
- package/packages/cli/tsconfig.json +25 -0
- package/packages/cli/vitest.config.ts +19 -0
- 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
|
+
}
|