@factiii/stack 0.1.185 → 0.1.187
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 -21
- package/README.md +441 -441
- package/bin/stack +300 -300
- package/dist/cli/dev-sync.js +16 -16
- package/dist/cli/init.d.ts.map +1 -1
- package/dist/cli/init.js +9 -1
- package/dist/cli/init.js.map +1 -1
- package/dist/plugins/addons/auth/index.js +7 -7
- package/dist/plugins/addons/vercel/index.js +9 -9
- package/dist/plugins/addons/vercel/scanfix/config.js +10 -10
- package/dist/plugins/addons/vercel/scanfix/token.js +15 -15
- package/dist/plugins/approved.json +13 -13
- package/dist/plugins/pipelines/aws/index.js +12 -12
- package/dist/plugins/pipelines/aws/policies/bootstrap-policy.json +135 -135
- package/dist/plugins/pipelines/aws/prod.js +1 -1
- package/dist/plugins/pipelines/factiii/prod.js +21 -21
- package/dist/plugins/pipelines/factiii/scanfix/bootstrap.d.ts.map +1 -1
- package/dist/plugins/pipelines/factiii/scanfix/bootstrap.js +10 -2
- package/dist/plugins/pipelines/factiii/scanfix/bootstrap.js.map +1 -1
- package/dist/plugins/pipelines/factiii/staging.js +23 -23
- package/dist/plugins/pipelines/factiii/workflows/stack-ci.yml +75 -75
- package/dist/plugins/pipelines/factiii/workflows/stack-cicd-prod.yml +73 -73
- package/dist/plugins/servers/amazon-linux/index.js +16 -16
- package/dist/plugins/servers/mac/index.js +12 -12
- package/dist/plugins/servers/mac/staging.js +2 -2
- package/dist/plugins/servers/ubuntu/index.js +23 -23
- package/dist/plugins/servers/windows/index.js +15 -15
- package/dist/scripts/generate-all.js +73 -73
- package/dist/utils/deployment-report.js +2 -2
- package/dist/utils/secret-prompts.js +34 -34
- package/dist/utils/ssh-helper.d.ts.map +1 -1
- package/dist/utils/ssh-helper.js +178 -25
- package/dist/utils/ssh-helper.js.map +1 -1
- package/dist/utils/template-generator.js +74 -74
- package/package.json +100 -93
|
@@ -271,31 +271,31 @@ function generateNginx(allConfigs) {
|
|
|
271
271
|
return 0;
|
|
272
272
|
}
|
|
273
273
|
// Generate nginx config
|
|
274
|
-
let nginxConf = `# Auto-generated nginx configuration
|
|
275
|
-
# Generated by: npx stack (generate-all)
|
|
276
|
-
# Do not edit directly - modify stack.yml files and run: npx stack deploy
|
|
277
|
-
|
|
278
|
-
events {
|
|
279
|
-
worker_connections 1024;
|
|
280
|
-
}
|
|
281
|
-
|
|
282
|
-
http {
|
|
283
|
-
include /etc/nginx/mime.types;
|
|
284
|
-
default_type application/octet-stream;
|
|
285
|
-
|
|
286
|
-
sendfile on;
|
|
287
|
-
keepalive_timeout 65;
|
|
288
|
-
client_max_body_size 100M;
|
|
289
|
-
|
|
290
|
-
# Logging
|
|
291
|
-
access_log /var/log/nginx/access.log;
|
|
292
|
-
error_log /var/log/nginx/error.log;
|
|
293
|
-
|
|
294
|
-
# Gzip
|
|
295
|
-
gzip on;
|
|
296
|
-
gzip_vary on;
|
|
297
|
-
gzip_types text/plain text/css application/json application/javascript text/xml application/xml application/xml+rss text/javascript;
|
|
298
|
-
|
|
274
|
+
let nginxConf = `# Auto-generated nginx configuration
|
|
275
|
+
# Generated by: npx stack (generate-all)
|
|
276
|
+
# Do not edit directly - modify stack.yml files and run: npx stack deploy
|
|
277
|
+
|
|
278
|
+
events {
|
|
279
|
+
worker_connections 1024;
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
http {
|
|
283
|
+
include /etc/nginx/mime.types;
|
|
284
|
+
default_type application/octet-stream;
|
|
285
|
+
|
|
286
|
+
sendfile on;
|
|
287
|
+
keepalive_timeout 65;
|
|
288
|
+
client_max_body_size 100M;
|
|
289
|
+
|
|
290
|
+
# Logging
|
|
291
|
+
access_log /var/log/nginx/access.log;
|
|
292
|
+
error_log /var/log/nginx/error.log;
|
|
293
|
+
|
|
294
|
+
# Gzip
|
|
295
|
+
gzip on;
|
|
296
|
+
gzip_vary on;
|
|
297
|
+
gzip_types text/plain text/css application/json application/javascript text/xml application/xml application/xml+rss text/javascript;
|
|
298
|
+
|
|
299
299
|
`;
|
|
300
300
|
// ============================================================
|
|
301
301
|
// CRITICAL: HTTPS Certificate Paths
|
|
@@ -308,54 +308,54 @@ http {
|
|
|
308
308
|
for (const { domain, service, port } of routes) {
|
|
309
309
|
// Always generate HTTPS-capable config
|
|
310
310
|
// Certificates must exist before nginx can start (obtained via: npx stack fix --staging/--prod)
|
|
311
|
-
nginxConf += `
|
|
312
|
-
# ${service} - ${domain}
|
|
313
|
-
|
|
314
|
-
# HTTP - ACME challenge + redirect to HTTPS
|
|
315
|
-
server {
|
|
316
|
-
listen 80;
|
|
317
|
-
server_name ${domain};
|
|
318
|
-
|
|
319
|
-
# Allow certbot ACME challenge (for renewals)
|
|
320
|
-
location /.well-known/acme-challenge/ {
|
|
321
|
-
root /var/www/certbot;
|
|
322
|
-
}
|
|
323
|
-
|
|
324
|
-
# Redirect all other traffic to HTTPS
|
|
325
|
-
location / {
|
|
326
|
-
return 301 https://$server_name$request_uri;
|
|
327
|
-
}
|
|
328
|
-
}
|
|
329
|
-
|
|
330
|
-
# HTTPS - main server block
|
|
331
|
-
server {
|
|
332
|
-
listen 443 ssl;
|
|
333
|
-
http2 on;
|
|
334
|
-
server_name ${domain};
|
|
335
|
-
|
|
336
|
-
# SSL certificate paths (Let's Encrypt)
|
|
337
|
-
ssl_certificate /etc/letsencrypt/live/${domain}/fullchain.pem;
|
|
338
|
-
ssl_certificate_key /etc/letsencrypt/live/${domain}/privkey.pem;
|
|
339
|
-
|
|
340
|
-
# SSL security settings
|
|
341
|
-
ssl_protocols TLSv1.2 TLSv1.3;
|
|
342
|
-
ssl_prefer_server_ciphers on;
|
|
343
|
-
ssl_ciphers ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256;
|
|
344
|
-
ssl_session_cache shared:SSL:10m;
|
|
345
|
-
ssl_session_timeout 10m;
|
|
346
|
-
|
|
347
|
-
location / {
|
|
348
|
-
proxy_pass http://${service}:${port};
|
|
349
|
-
proxy_http_version 1.1;
|
|
350
|
-
proxy_set_header Upgrade $http_upgrade;
|
|
351
|
-
proxy_set_header Connection 'upgrade';
|
|
352
|
-
proxy_set_header Host $host;
|
|
353
|
-
proxy_cache_bypass $http_upgrade;
|
|
354
|
-
proxy_set_header X-Real-IP $remote_addr;
|
|
355
|
-
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
|
356
|
-
proxy_set_header X-Forwarded-Proto $scheme;
|
|
357
|
-
}
|
|
358
|
-
}
|
|
311
|
+
nginxConf += `
|
|
312
|
+
# ${service} - ${domain}
|
|
313
|
+
|
|
314
|
+
# HTTP - ACME challenge + redirect to HTTPS
|
|
315
|
+
server {
|
|
316
|
+
listen 80;
|
|
317
|
+
server_name ${domain};
|
|
318
|
+
|
|
319
|
+
# Allow certbot ACME challenge (for renewals)
|
|
320
|
+
location /.well-known/acme-challenge/ {
|
|
321
|
+
root /var/www/certbot;
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
# Redirect all other traffic to HTTPS
|
|
325
|
+
location / {
|
|
326
|
+
return 301 https://$server_name$request_uri;
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
# HTTPS - main server block
|
|
331
|
+
server {
|
|
332
|
+
listen 443 ssl;
|
|
333
|
+
http2 on;
|
|
334
|
+
server_name ${domain};
|
|
335
|
+
|
|
336
|
+
# SSL certificate paths (Let's Encrypt)
|
|
337
|
+
ssl_certificate /etc/letsencrypt/live/${domain}/fullchain.pem;
|
|
338
|
+
ssl_certificate_key /etc/letsencrypt/live/${domain}/privkey.pem;
|
|
339
|
+
|
|
340
|
+
# SSL security settings
|
|
341
|
+
ssl_protocols TLSv1.2 TLSv1.3;
|
|
342
|
+
ssl_prefer_server_ciphers on;
|
|
343
|
+
ssl_ciphers ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256;
|
|
344
|
+
ssl_session_cache shared:SSL:10m;
|
|
345
|
+
ssl_session_timeout 10m;
|
|
346
|
+
|
|
347
|
+
location / {
|
|
348
|
+
proxy_pass http://${service}:${port};
|
|
349
|
+
proxy_http_version 1.1;
|
|
350
|
+
proxy_set_header Upgrade $http_upgrade;
|
|
351
|
+
proxy_set_header Connection 'upgrade';
|
|
352
|
+
proxy_set_header Host $host;
|
|
353
|
+
proxy_cache_bypass $http_upgrade;
|
|
354
|
+
proxy_set_header X-Real-IP $remote_addr;
|
|
355
|
+
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
|
356
|
+
proxy_set_header X-Forwarded-Proto $scheme;
|
|
357
|
+
}
|
|
358
|
+
}
|
|
359
359
|
`;
|
|
360
360
|
}
|
|
361
361
|
nginxConf += `}\n`;
|
|
@@ -154,8 +154,8 @@ function formatDeploymentReport(data) {
|
|
|
154
154
|
function formatWorkflowSummary(data) {
|
|
155
155
|
const report = formatDeploymentReport(data);
|
|
156
156
|
// Workflow summaries support markdown, so we can enhance it
|
|
157
|
-
return `\`\`\`
|
|
158
|
-
${report}
|
|
157
|
+
return `\`\`\`
|
|
158
|
+
${report}
|
|
159
159
|
\`\`\``;
|
|
160
160
|
}
|
|
161
161
|
/**
|
|
@@ -64,16 +64,16 @@ const SECRET_METADATA = {
|
|
|
64
64
|
STAGING_SSH: {
|
|
65
65
|
type: 'ssh_key',
|
|
66
66
|
description: 'SSH private key for accessing staging server',
|
|
67
|
-
helpText: `
|
|
68
|
-
Step 1: Generate a new SSH key pair:
|
|
69
|
-
ssh-keygen -t ed25519 -C "staging-deploy" -f ~/.ssh/staging_deploy
|
|
70
|
-
|
|
71
|
-
Step 2: Add PUBLIC key to your staging server:
|
|
72
|
-
ssh-copy-id -i ~/.ssh/staging_deploy.pub ubuntu@YOUR_HOST
|
|
73
|
-
|
|
74
|
-
(HOST is configured in stack.yml → environments.staging.host)
|
|
75
|
-
|
|
76
|
-
Step 3: Paste the PRIVATE key below (multi-line, end with blank line):
|
|
67
|
+
helpText: `
|
|
68
|
+
Step 1: Generate a new SSH key pair:
|
|
69
|
+
ssh-keygen -t ed25519 -C "staging-deploy" -f ~/.ssh/staging_deploy
|
|
70
|
+
|
|
71
|
+
Step 2: Add PUBLIC key to your staging server:
|
|
72
|
+
ssh-copy-id -i ~/.ssh/staging_deploy.pub ubuntu@YOUR_HOST
|
|
73
|
+
|
|
74
|
+
(HOST is configured in stack.yml → environments.staging.host)
|
|
75
|
+
|
|
76
|
+
Step 3: Paste the PRIVATE key below (multi-line, end with blank line):
|
|
77
77
|
cat ~/.ssh/staging_deploy`,
|
|
78
78
|
validation: (value) => {
|
|
79
79
|
if (!value || value.trim().length === 0) {
|
|
@@ -91,16 +91,16 @@ const SECRET_METADATA = {
|
|
|
91
91
|
PROD_SSH: {
|
|
92
92
|
type: 'ssh_key',
|
|
93
93
|
description: 'SSH private key for accessing production server',
|
|
94
|
-
helpText: `
|
|
95
|
-
Step 1: Generate a new SSH key pair:
|
|
96
|
-
ssh-keygen -t ed25519 -C "production-deploy" -f ~/.ssh/prod_deploy
|
|
97
|
-
|
|
98
|
-
Step 2: Add PUBLIC key to your production server:
|
|
99
|
-
ssh-copy-id -i ~/.ssh/prod_deploy.pub ubuntu@YOUR_HOST
|
|
100
|
-
|
|
101
|
-
(HOST is configured in stack.yml → environments.production.host)
|
|
102
|
-
|
|
103
|
-
Step 3: Paste the PRIVATE key below (multi-line, end with blank line):
|
|
94
|
+
helpText: `
|
|
95
|
+
Step 1: Generate a new SSH key pair:
|
|
96
|
+
ssh-keygen -t ed25519 -C "production-deploy" -f ~/.ssh/prod_deploy
|
|
97
|
+
|
|
98
|
+
Step 2: Add PUBLIC key to your production server:
|
|
99
|
+
ssh-copy-id -i ~/.ssh/prod_deploy.pub ubuntu@YOUR_HOST
|
|
100
|
+
|
|
101
|
+
(HOST is configured in stack.yml → environments.production.host)
|
|
102
|
+
|
|
103
|
+
Step 3: Paste the PRIVATE key below (multi-line, end with blank line):
|
|
104
104
|
cat ~/.ssh/prod_deploy`,
|
|
105
105
|
validation: (value) => {
|
|
106
106
|
if (!value || value.trim().length === 0) {
|
|
@@ -118,14 +118,14 @@ const SECRET_METADATA = {
|
|
|
118
118
|
AWS_SECRET_ACCESS_KEY: {
|
|
119
119
|
type: 'aws_secret',
|
|
120
120
|
description: 'AWS Secret Access Key (the only secret AWS value)',
|
|
121
|
-
helpText: `
|
|
122
|
-
Get from AWS Console: IAM → Users → Security credentials
|
|
123
|
-
|
|
124
|
-
This is shown only once when you create the key.
|
|
125
|
-
If lost, you must create a new key pair.
|
|
126
|
-
|
|
127
|
-
Note: AWS_ACCESS_KEY_ID and AWS_REGION go in stack.yml (not secrets)
|
|
128
|
-
|
|
121
|
+
helpText: `
|
|
122
|
+
Get from AWS Console: IAM → Users → Security credentials
|
|
123
|
+
|
|
124
|
+
This is shown only once when you create the key.
|
|
125
|
+
If lost, you must create a new key pair.
|
|
126
|
+
|
|
127
|
+
Note: AWS_ACCESS_KEY_ID and AWS_REGION go in stack.yml (not secrets)
|
|
128
|
+
|
|
129
129
|
Enter AWS Secret Access Key:`,
|
|
130
130
|
validation: (value) => {
|
|
131
131
|
if (!value || value.trim().length === 0) {
|
|
@@ -143,12 +143,12 @@ const SECRET_METADATA = {
|
|
|
143
143
|
VERCEL_TOKEN: {
|
|
144
144
|
type: 'api_token',
|
|
145
145
|
description: 'Vercel API Token for deployments',
|
|
146
|
-
helpText: `
|
|
147
|
-
Get your token from: https://vercel.com/account/tokens
|
|
148
|
-
Create a new token with:
|
|
149
|
-
- Scope: Full Account (or specific team)
|
|
150
|
-
- Expiration: No Expiration (or custom)
|
|
151
|
-
|
|
146
|
+
helpText: `
|
|
147
|
+
Get your token from: https://vercel.com/account/tokens
|
|
148
|
+
Create a new token with:
|
|
149
|
+
- Scope: Full Account (or specific team)
|
|
150
|
+
- Expiration: No Expiration (or custom)
|
|
151
|
+
|
|
152
152
|
Enter Vercel API Token:`,
|
|
153
153
|
validation: (value) => {
|
|
154
154
|
if (!value || value.trim().length === 0) {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"ssh-helper.d.ts","sourceRoot":"","sources":["../../src/utils/ssh-helper.ts"],"names":[],"mappings":"AAgBA,OAAO,KAAK,EAAE,iBAAiB,EAAE,aAAa,EAAE,KAAK,EAAE,MAAM,mBAAmB,CAAC;AAKjF;;;GAGG;AACH,wBAAgB,kBAAkB,CAAC,OAAO,EAAE,MAAM,EAAE,UAAU,EAAE,MAAM,GAAG,IAAI,CAU5E;AA+BD;;;;;;;;GAQG;AACH,wBAAgB,mBAAmB,CAAC,KAAK,EAAE,MAAM,EAAE,QAAQ,CAAC,EAAE,MAAM,GAAG,MAAM,EAAE,CAS9E;AAED;;;;;;;;GAQG;AACH,wBAAgB,kBAAkB,CAAC,KAAK,EAAE,MAAM,EAAE,QAAQ,CAAC,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CA4BlF;AAED;;;;;;;GAOG;AACH,wBAAgB,oBAAoB,CAClC,KAAK,EAAE,KAAK,EACZ,MAAM,EAAE,aAAa,GACpB,iBAAiB,GAAG,IAAI,CAc1B;
|
|
1
|
+
{"version":3,"file":"ssh-helper.d.ts","sourceRoot":"","sources":["../../src/utils/ssh-helper.ts"],"names":[],"mappings":"AAgBA,OAAO,KAAK,EAAE,iBAAiB,EAAE,aAAa,EAAE,KAAK,EAAE,MAAM,mBAAmB,CAAC;AAKjF;;;GAGG;AACH,wBAAgB,kBAAkB,CAAC,OAAO,EAAE,MAAM,EAAE,UAAU,EAAE,MAAM,GAAG,IAAI,CAU5E;AA+BD;;;;;;;;GAQG;AACH,wBAAgB,mBAAmB,CAAC,KAAK,EAAE,MAAM,EAAE,QAAQ,CAAC,EAAE,MAAM,GAAG,MAAM,EAAE,CAS9E;AAED;;;;;;;;GAQG;AACH,wBAAgB,kBAAkB,CAAC,KAAK,EAAE,MAAM,EAAE,QAAQ,CAAC,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CA4BlF;AAED;;;;;;;GAOG;AACH,wBAAgB,oBAAoB,CAClC,KAAK,EAAE,KAAK,EACZ,MAAM,EAAE,aAAa,GACpB,iBAAiB,GAAG,IAAI,CAc1B;AA+aD;;;;;;;;;;;;;GAaG;AACH,wBAAsB,uBAAuB,CAC3C,KAAK,EAAE,KAAK,EACZ,MAAM,EAAE,aAAa,EACrB,OAAO,EAAE,MAAM,EACf,OAAO,CAAC,EAAE,MAAM,GACf,OAAO,CAAC;IAAE,OAAO,EAAE,OAAO,CAAC;IAAC,MAAM,EAAE,MAAM,CAAC;IAAC,MAAM,EAAE,MAAM,CAAA;CAAE,CAAC,CAiyB/D;AAED;;;;;;;;;;GAUG;AACH,wBAAsB,OAAO,CAC3B,SAAS,EAAE,iBAAiB,EAC5B,OAAO,EAAE,MAAM,EACf,KAAK,CAAC,EAAE,KAAK,EACb,MAAM,CAAC,EAAE,aAAa,EACtB,OAAO,CAAC,EAAE,MAAM,GACf,OAAO,CAAC,MAAM,CAAC,CAqMjB"}
|
package/dist/utils/ssh-helper.js
CHANGED
|
@@ -305,25 +305,54 @@ async function autoSetupSshKey(stage, host, user, config, rootDir) {
|
|
|
305
305
|
}
|
|
306
306
|
// Copy public key to server
|
|
307
307
|
console.log(' Copying public key to ' + user + '@' + host + '...');
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
308
|
+
if (process.platform === 'win32') {
|
|
309
|
+
// Windows: ssh-copy-id is not available — use SSH to pipe the public key
|
|
310
|
+
console.log(' Enter password when prompted by SSH:');
|
|
311
|
+
console.log('');
|
|
312
|
+
try {
|
|
313
|
+
const pubKeyContent = fs.readFileSync(pubKeyPath, 'utf8').trim();
|
|
314
|
+
const addKeyCmd = 'mkdir -p ~/.ssh && chmod 700 ~/.ssh && echo "' + pubKeyContent + '" >> ~/.ssh/authorized_keys && chmod 600 ~/.ssh/authorized_keys && sort -u -o ~/.ssh/authorized_keys ~/.ssh/authorized_keys';
|
|
315
|
+
const copyResult = (0, child_process_1.spawnSync)('ssh', [
|
|
316
|
+
'-o', 'StrictHostKeyChecking=no',
|
|
317
|
+
'-o', 'ConnectTimeout=10',
|
|
318
|
+
user + '@' + host,
|
|
319
|
+
addKeyCmd,
|
|
320
|
+
], {
|
|
321
|
+
stdio: 'inherit',
|
|
322
|
+
timeout: 60000,
|
|
323
|
+
});
|
|
324
|
+
if (copyResult.status !== 0) {
|
|
325
|
+
console.log(' [!] Failed to copy public key to server');
|
|
326
|
+
return null;
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
catch (e) {
|
|
330
|
+
console.log(' [!] Failed to copy public key: ' + (e instanceof Error ? e.message : String(e)));
|
|
321
331
|
return null;
|
|
322
332
|
}
|
|
323
333
|
}
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
334
|
+
else {
|
|
335
|
+
// Linux/Mac: use ssh-copy-id
|
|
336
|
+
console.log(' Enter password when prompted:');
|
|
337
|
+
console.log('');
|
|
338
|
+
try {
|
|
339
|
+
const copyResult = (0, child_process_1.spawnSync)('ssh-copy-id', [
|
|
340
|
+
'-i', pubKeyPath,
|
|
341
|
+
'-o', 'StrictHostKeyChecking=no',
|
|
342
|
+
user + '@' + host,
|
|
343
|
+
], {
|
|
344
|
+
stdio: 'inherit',
|
|
345
|
+
timeout: 60000,
|
|
346
|
+
});
|
|
347
|
+
if (copyResult.status !== 0) {
|
|
348
|
+
console.log(' [!] ssh-copy-id failed');
|
|
349
|
+
return null;
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
catch (e) {
|
|
353
|
+
console.log(' [!] ssh-copy-id failed: ' + (e instanceof Error ? e.message : String(e)));
|
|
354
|
+
return null;
|
|
355
|
+
}
|
|
327
356
|
}
|
|
328
357
|
// Fix remote permissions
|
|
329
358
|
try {
|
|
@@ -755,6 +784,8 @@ async function sshRemoteFactiiiCommand(stage, config, command, rootDir) {
|
|
|
755
784
|
'if [ ! -d "' + projectDir + '/.git" ]; then ' +
|
|
756
785
|
'echo " Cloning project..." && ' +
|
|
757
786
|
'mkdir -p $HOME/.factiii && ' +
|
|
787
|
+
// Remove broken/partial clone directory if it exists without .git
|
|
788
|
+
'if [ -d "' + projectDir + '" ]; then echo " Removing broken clone..." && rm -rf "' + projectDir + '"; fi && ' +
|
|
758
789
|
(githubRepo
|
|
759
790
|
? 'cd $HOME/.factiii && git clone https://github.com/' + githubRepo + '.git ' + repoName + '; '
|
|
760
791
|
: 'echo " [!] No github_repo configured — cannot auto-clone" && exit 1; ') +
|
|
@@ -822,15 +853,32 @@ async function sshRemoteFactiiiCommand(stage, config, command, rootDir) {
|
|
|
822
853
|
}
|
|
823
854
|
console.log(' SSH (password): ' + user + '@' + host + ' → npx stack ' + command);
|
|
824
855
|
const pwStart = Date.now();
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
'
|
|
829
|
-
'
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
856
|
+
let pwResult;
|
|
857
|
+
if (process.platform === 'win32') {
|
|
858
|
+
// Windows: no sshpass — use interactive SSH so user types password
|
|
859
|
+
console.log(' You will be prompted for the password by SSH:');
|
|
860
|
+
console.log('');
|
|
861
|
+
pwResult = (0, child_process_1.spawnSync)('ssh', [
|
|
862
|
+
'-tt',
|
|
863
|
+
'-o', 'StrictHostKeyChecking=no',
|
|
864
|
+
'-o', 'ConnectTimeout=10',
|
|
865
|
+
'-o', 'ServerAliveInterval=60',
|
|
866
|
+
'-o', 'ServerAliveCountMax=5',
|
|
867
|
+
user + '@' + host,
|
|
868
|
+
pwRemoteCommand,
|
|
869
|
+
], { encoding: 'utf8', stdio: 'inherit', timeout: 600000 });
|
|
870
|
+
}
|
|
871
|
+
else {
|
|
872
|
+
pwResult = (0, child_process_1.spawnSync)('sshpass', [
|
|
873
|
+
'-p', password, 'ssh', '-tt',
|
|
874
|
+
'-o', 'StrictHostKeyChecking=no',
|
|
875
|
+
'-o', 'ConnectTimeout=10',
|
|
876
|
+
'-o', 'ServerAliveInterval=60',
|
|
877
|
+
'-o', 'ServerAliveCountMax=5',
|
|
878
|
+
user + '@' + host,
|
|
879
|
+
pwRemoteCommand,
|
|
880
|
+
], { encoding: 'utf8', stdio: 'inherit', timeout: 600000 });
|
|
881
|
+
}
|
|
834
882
|
console.log(' SSH completed in ' + Math.floor((Date.now() - pwStart) / 1000) + 's');
|
|
835
883
|
return {
|
|
836
884
|
success: pwResult.status === 0,
|
|
@@ -859,6 +907,29 @@ async function sshRemoteFactiiiCommand(stage, config, command, rootDir) {
|
|
|
859
907
|
timeout: 10000,
|
|
860
908
|
});
|
|
861
909
|
if (keyTest.status !== 0) {
|
|
910
|
+
const keyTestStderr = (keyTest.stderr ?? '').toLowerCase();
|
|
911
|
+
// Detect DNS/connection failures — don't delete key or try password, the server is unreachable
|
|
912
|
+
const isConnectionFailure = keyTestStderr.includes('could not resolve') ||
|
|
913
|
+
keyTestStderr.includes('connection refused') ||
|
|
914
|
+
keyTestStderr.includes('connection timed out') ||
|
|
915
|
+
keyTestStderr.includes('no route to host') ||
|
|
916
|
+
keyTestStderr.includes('network is unreachable') ||
|
|
917
|
+
keyTestStderr.includes('unknown port') ||
|
|
918
|
+
keyTestStderr.includes('name or service not known');
|
|
919
|
+
if (isConnectionFailure) {
|
|
920
|
+
console.log(' [!] Cannot connect to ' + host + ' — server may be down or domain not resolving');
|
|
921
|
+
console.log(' SSH error: ' + (keyTest.stderr ?? '').trim());
|
|
922
|
+
console.log('');
|
|
923
|
+
console.log(' Check:');
|
|
924
|
+
console.log(' - DNS: does ' + host + ' resolve? (nslookup ' + host + ')');
|
|
925
|
+
console.log(' - Server: is the server running and accepting SSH on port 22?');
|
|
926
|
+
console.log(' - Firewall: is port 22 open?');
|
|
927
|
+
return {
|
|
928
|
+
success: false,
|
|
929
|
+
stdout: '',
|
|
930
|
+
stderr: 'Cannot connect to ' + host + ': ' + (keyTest.stderr ?? '').trim(),
|
|
931
|
+
};
|
|
932
|
+
}
|
|
862
933
|
// Categorize key type — only remove repo-specific deploy keys, not system/.pem keys
|
|
863
934
|
const isRepoSpecificKey = activeKeyPath.includes('_deploy_key') || activeKeyPath.endsWith('_factiii');
|
|
864
935
|
const isPemKey = activeKeyPath.endsWith('.pem');
|
|
@@ -1054,6 +1125,68 @@ async function sshRemoteFactiiiCommand(stage, config, command, rootDir) {
|
|
|
1054
1125
|
console.log(' Falling back to SSH password auth...');
|
|
1055
1126
|
console.log(' SSH (password): ' + user + '@' + host + ' → npx stack ' + command);
|
|
1056
1127
|
const startTime = Date.now();
|
|
1128
|
+
// On Windows, sshpass is not available — use interactive SSH so user types password
|
|
1129
|
+
if (process.platform === 'win32') {
|
|
1130
|
+
console.log(' You will be prompted for the password by SSH:');
|
|
1131
|
+
console.log('');
|
|
1132
|
+
const result = (0, child_process_1.spawnSync)('ssh', [
|
|
1133
|
+
'-tt',
|
|
1134
|
+
'-o', 'StrictHostKeyChecking=no',
|
|
1135
|
+
'-o', 'ConnectTimeout=10',
|
|
1136
|
+
'-o', 'ServerAliveInterval=60',
|
|
1137
|
+
'-o', 'ServerAliveCountMax=5',
|
|
1138
|
+
user + '@' + host,
|
|
1139
|
+
remoteCommand,
|
|
1140
|
+
], {
|
|
1141
|
+
encoding: 'utf8',
|
|
1142
|
+
stdio: 'inherit',
|
|
1143
|
+
timeout: 600000,
|
|
1144
|
+
});
|
|
1145
|
+
const elapsed = Math.floor((Date.now() - startTime) / 1000);
|
|
1146
|
+
console.log(' SSH completed in ' + elapsed + 's');
|
|
1147
|
+
if (result.status === 0) {
|
|
1148
|
+
// Connection worked — set up SSH key for future so password isn't needed again
|
|
1149
|
+
console.log(' Setting up SSH key for future connections...');
|
|
1150
|
+
await autoSetupSshKey(stage, host, user, config, rootDir);
|
|
1151
|
+
return { success: true, stdout: '', stderr: '' };
|
|
1152
|
+
}
|
|
1153
|
+
// Interactive SSH failed — try auto key setup as last resort
|
|
1154
|
+
console.log(' [!] SSH connection failed');
|
|
1155
|
+
console.log(' Setting up SSH key auth for future connections...');
|
|
1156
|
+
const autoKeyResult = await autoSetupSshKey(stage, host, user, config, rootDir);
|
|
1157
|
+
if (autoKeyResult) {
|
|
1158
|
+
console.log(' Retrying command with SSH key...');
|
|
1159
|
+
console.log(' SSH (key): ' + user + '@' + host + ' → npx stack ' + command);
|
|
1160
|
+
const retryStart = Date.now();
|
|
1161
|
+
const retryResult = (0, child_process_1.spawnSync)('ssh', [
|
|
1162
|
+
'-tt',
|
|
1163
|
+
'-i', autoKeyResult,
|
|
1164
|
+
'-o', 'StrictHostKeyChecking=no',
|
|
1165
|
+
'-o', 'ConnectTimeout=10',
|
|
1166
|
+
'-o', 'ServerAliveInterval=60',
|
|
1167
|
+
'-o', 'ServerAliveCountMax=5',
|
|
1168
|
+
user + '@' + host,
|
|
1169
|
+
remoteCommand,
|
|
1170
|
+
], {
|
|
1171
|
+
encoding: 'utf8',
|
|
1172
|
+
stdio: 'inherit',
|
|
1173
|
+
timeout: 600000,
|
|
1174
|
+
});
|
|
1175
|
+
const retryElapsed = Math.floor((Date.now() - retryStart) / 1000);
|
|
1176
|
+
console.log(' SSH completed in ' + retryElapsed + 's');
|
|
1177
|
+
return {
|
|
1178
|
+
success: retryResult.status === 0,
|
|
1179
|
+
stdout: '',
|
|
1180
|
+
stderr: retryResult.status !== 0 ? 'SSH command exited with code ' + retryResult.status : '',
|
|
1181
|
+
};
|
|
1182
|
+
}
|
|
1183
|
+
return {
|
|
1184
|
+
success: false,
|
|
1185
|
+
stdout: '',
|
|
1186
|
+
stderr: 'SSH connection failed. Check password and server accessibility.',
|
|
1187
|
+
};
|
|
1188
|
+
}
|
|
1189
|
+
// Linux/Mac: use sshpass for non-interactive password auth
|
|
1057
1190
|
const result = (0, child_process_1.spawnSync)('sshpass', [
|
|
1058
1191
|
'-p', password,
|
|
1059
1192
|
'ssh',
|
|
@@ -1298,6 +1431,26 @@ async function sshExec(envConfig, command, stage, config, rootDir) {
|
|
|
1298
1431
|
}
|
|
1299
1432
|
}
|
|
1300
1433
|
if (password) {
|
|
1434
|
+
if (process.platform === 'win32') {
|
|
1435
|
+
// Windows: no sshpass — use interactive SSH (stdio: 'inherit' so user can type password)
|
|
1436
|
+
const result = (0, child_process_1.spawnSync)('ssh', [
|
|
1437
|
+
'-tt',
|
|
1438
|
+
'-o', 'StrictHostKeyChecking=no',
|
|
1439
|
+
'-o', 'ConnectTimeout=10',
|
|
1440
|
+
'-o', 'ServerAliveInterval=60',
|
|
1441
|
+
'-o', 'ServerAliveCountMax=5',
|
|
1442
|
+
user + '@' + host,
|
|
1443
|
+
command,
|
|
1444
|
+
], {
|
|
1445
|
+
encoding: 'utf8',
|
|
1446
|
+
stdio: 'inherit',
|
|
1447
|
+
});
|
|
1448
|
+
if (result.status !== 0) {
|
|
1449
|
+
throw new Error('SSH command failed with exit code ' + result.status);
|
|
1450
|
+
}
|
|
1451
|
+
return '';
|
|
1452
|
+
}
|
|
1453
|
+
// Linux/Mac: use sshpass
|
|
1301
1454
|
const result = (0, child_process_1.spawnSync)('sshpass', [
|
|
1302
1455
|
'-p', password,
|
|
1303
1456
|
'ssh',
|