@factiii/stack 0.1.201 → 0.7.1
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 +95 -403
- package/bin/stack +334 -334
- package/dist/cli/dev-sync.js +16 -16
- package/dist/plugins/addons/auth/index.d.ts.map +1 -1
- package/dist/plugins/addons/auth/index.js +31 -12
- package/dist/plugins/addons/auth/index.js.map +1 -1
- package/dist/plugins/addons/auth/scanfix/secrets.d.ts +3 -0
- package/dist/plugins/addons/auth/scanfix/secrets.d.ts.map +1 -1
- package/dist/plugins/addons/auth/scanfix/secrets.js +54 -19
- package/dist/plugins/addons/auth/scanfix/secrets.js.map +1 -1
- package/dist/plugins/addons/auth/scanfix/validate.d.ts +3 -0
- package/dist/plugins/addons/auth/scanfix/validate.d.ts.map +1 -1
- package/dist/plugins/addons/auth/scanfix/validate.js +37 -18
- package/dist/plugins/addons/auth/scanfix/validate.js.map +1 -1
- 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/index.d.ts.map +1 -1
- package/dist/plugins/pipelines/factiii/index.js +2 -14
- package/dist/plugins/pipelines/factiii/index.js.map +1 -1
- package/dist/plugins/pipelines/factiii/prod.js +21 -21
- package/dist/plugins/pipelines/factiii/scanfix/port-convention.d.ts.map +1 -1
- package/dist/plugins/pipelines/factiii/scanfix/port-convention.js +2 -4
- package/dist/plugins/pipelines/factiii/scanfix/port-convention.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/scanfix/commands/mac.d.ts.map +1 -1
- package/dist/scanfix/commands/mac.js +5 -4
- package/dist/scanfix/commands/mac.js.map +1 -1
- package/dist/scanfix/fixes/certbot.d.ts.map +1 -1
- package/dist/scanfix/fixes/certbot.js +4 -18
- package/dist/scanfix/fixes/certbot.js.map +1 -1
- package/dist/scanfix/fixes/docker.d.ts.map +1 -1
- package/dist/scanfix/fixes/docker.js +5 -14
- package/dist/scanfix/fixes/docker.js.map +1 -1
- package/dist/scanfix/ssl-cert-helper.d.ts.map +1 -1
- package/dist/scanfix/ssl-cert-helper.js +18 -4
- package/dist/scanfix/ssl-cert-helper.js.map +1 -1
- 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 +150 -142
- package/dist/utils/ssh-helper.js.map +1 -1
- package/dist/utils/template-generator.js +74 -74
- package/package.json +93 -114
- package/dist/plugins/pipelines/factiii/scanfix/docker.d.ts +0 -20
- package/dist/plugins/pipelines/factiii/scanfix/docker.d.ts.map +0 -1
- package/dist/plugins/pipelines/factiii/scanfix/docker.js +0 -131
- package/dist/plugins/pipelines/factiii/scanfix/docker.js.map +0 -1
|
@@ -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;AAwbD;;;;;;;;;;;;;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,CA+2B/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
|
@@ -354,14 +354,15 @@ async function autoSetupSshKey(stage, host, user, config, rootDir) {
|
|
|
354
354
|
return null;
|
|
355
355
|
}
|
|
356
356
|
}
|
|
357
|
-
// Fix remote permissions
|
|
357
|
+
// Fix remote permissions using the key we just copied (avoid extra password prompt)
|
|
358
358
|
try {
|
|
359
359
|
(0, child_process_1.spawnSync)('ssh', [
|
|
360
|
+
'-i', keyPath,
|
|
360
361
|
'-o', 'StrictHostKeyChecking=no',
|
|
361
362
|
'-o', 'ConnectTimeout=5',
|
|
362
363
|
user + '@' + host,
|
|
363
364
|
'chmod 700 ~/.ssh && chmod 600 ~/.ssh/authorized_keys',
|
|
364
|
-
], { stdio: '
|
|
365
|
+
], { encoding: 'utf8', stdio: 'pipe', timeout: 15000 });
|
|
365
366
|
}
|
|
366
367
|
catch { /* best effort */ }
|
|
367
368
|
// Verify key auth
|
|
@@ -426,6 +427,11 @@ async function promptAndValidatePassword(stage, host, user, config, rootDir) {
|
|
|
426
427
|
stdio: 'pipe',
|
|
427
428
|
timeout: 15000,
|
|
428
429
|
});
|
|
430
|
+
if (testResult.status === 5) {
|
|
431
|
+
// sshpass exit code 5 = wrong password
|
|
432
|
+
console.log(' [!] Incorrect password.');
|
|
433
|
+
return null;
|
|
434
|
+
}
|
|
429
435
|
if (testResult.status !== 0) {
|
|
430
436
|
// sshpass failed — likely keyboard-interactive auth (Mac servers)
|
|
431
437
|
// Fall back to auto SSH key setup so we never need sshpass again
|
|
@@ -513,16 +519,18 @@ async function promptAndValidatePassword(stage, host, user, config, rootDir) {
|
|
|
513
519
|
// Store password in vault anyway so sshExec can try it
|
|
514
520
|
return await storePasswordAndReturn(password, stage, config, rootDir);
|
|
515
521
|
}
|
|
516
|
-
// Step 2.5: Fix remote permissions (
|
|
522
|
+
// Step 2.5: Fix remote permissions using the key we just copied (avoid extra password prompt)
|
|
517
523
|
console.log(' Fixing remote SSH permissions...');
|
|
518
524
|
try {
|
|
519
525
|
(0, child_process_1.spawnSync)('ssh', [
|
|
526
|
+
'-i', keyPath,
|
|
520
527
|
'-o', 'StrictHostKeyChecking=no',
|
|
521
528
|
'-o', 'ConnectTimeout=5',
|
|
522
529
|
user + '@' + host,
|
|
523
530
|
'chmod 700 ~/.ssh && chmod 600 ~/.ssh/authorized_keys',
|
|
524
531
|
], {
|
|
525
|
-
|
|
532
|
+
encoding: 'utf8',
|
|
533
|
+
stdio: 'pipe',
|
|
526
534
|
timeout: 15000,
|
|
527
535
|
});
|
|
528
536
|
}
|
|
@@ -764,107 +772,67 @@ async function sshRemoteFactiiiCommand(stage, config, command, rootDir) {
|
|
|
764
772
|
const localGithubToken = process.env.GITHUB_TOKEN || '';
|
|
765
773
|
const tokenExport = localGithubToken ? 'export GITHUB_TOKEN="' + localGithubToken + '" && ' : '';
|
|
766
774
|
// Auto-bootstrap: install dependencies and clone repo if missing on server
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
// Install
|
|
774
|
-
'
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
'echo " Cloning project..." && ' +
|
|
829
|
-
'mkdir -p $HOME/.factiii && ' +
|
|
830
|
-
// Remove broken/partial clone directory if it exists without .git
|
|
831
|
-
'if [ -d "' + projectDir + '" ]; then echo " Removing broken clone..." && rm -rf "' + projectDir + '"; fi && ' +
|
|
832
|
-
// Add github.com to known_hosts to avoid interactive prompt
|
|
833
|
-
'mkdir -p ~/.ssh && ssh-keyscan -t ed25519,rsa github.com >> ~/.ssh/known_hosts 2>/dev/null && ' +
|
|
834
|
-
// Generate a GitHub deploy key on the server if none exists
|
|
835
|
-
'if [ ! -f ~/.ssh/github_deploy_key ]; then ' +
|
|
836
|
-
'echo " Generating GitHub deploy key on server..." && ' +
|
|
837
|
-
'ssh-keygen -t ed25519 -f ~/.ssh/github_deploy_key -N "" -C "server-deploy" -q && ' +
|
|
838
|
-
'chmod 600 ~/.ssh/github_deploy_key; ' +
|
|
839
|
-
'fi && ' +
|
|
840
|
-
// Configure SSH to use the deploy key for github.com
|
|
841
|
-
'if ! grep -q "Host github.com" ~/.ssh/config 2>/dev/null; then ' +
|
|
842
|
-
'printf "\\nHost github.com\\n IdentityFile ~/.ssh/github_deploy_key\\n IdentitiesOnly yes\\n" >> ~/.ssh/config && ' +
|
|
843
|
-
'chmod 600 ~/.ssh/config; ' +
|
|
844
|
-
'fi && ' +
|
|
845
|
-
(githubRepo
|
|
846
|
-
? 'cd $HOME/.factiii && ' +
|
|
847
|
-
// Try HTTPS with token first (works for private repos when GITHUB_TOKEN is set)
|
|
848
|
-
'if [ -n "$GITHUB_TOKEN" ]; then ' +
|
|
849
|
-
'GIT_TERMINAL_PROMPT=0 git clone https://x-access-token:$GITHUB_TOKEN@github.com/' + githubRepo + '.git ' + repoName + '; ' +
|
|
850
|
-
// Then try SSH (works if server has a GitHub deploy key in repo settings)
|
|
851
|
-
'elif GIT_TERMINAL_PROMPT=0 git clone git@github.com:' + githubRepo + '.git ' + repoName + ' 2>/dev/null; then ' +
|
|
852
|
-
'true; ' +
|
|
853
|
-
// All clone methods failed — show deploy key and instructions
|
|
854
|
-
'else ' +
|
|
855
|
-
'echo "" && ' +
|
|
856
|
-
'echo " [!] Cannot clone private repo — server needs GitHub access" && ' +
|
|
857
|
-
'echo "" && ' +
|
|
858
|
-
'echo " Add this deploy key to your GitHub repo:" && ' +
|
|
859
|
-
'echo " GitHub → ' + githubRepo + ' → Settings → Deploy keys → Add" && ' +
|
|
860
|
-
'echo "" && ' +
|
|
861
|
-
'cat ~/.ssh/github_deploy_key.pub && ' +
|
|
862
|
-
'echo "" && ' +
|
|
863
|
-
'echo " Then re-run: npx stack fix --prod" && ' +
|
|
864
|
-
'exit 1; ' +
|
|
865
|
-
'fi; '
|
|
866
|
-
: 'echo " [!] No github_repo configured — cannot auto-clone" && exit 1; ') +
|
|
867
|
-
'fi && ';
|
|
775
|
+
const bootstrapCmd =
|
|
776
|
+
// Install git if missing
|
|
777
|
+
'if ! command -v git &>/dev/null; then ' +
|
|
778
|
+
'echo " Installing git..." && ' +
|
|
779
|
+
'sudo apt-get update -qq && sudo DEBIAN_FRONTEND=noninteractive apt-get install -y -qq git; ' +
|
|
780
|
+
'fi && ' +
|
|
781
|
+
// Install Node.js if missing
|
|
782
|
+
'if ! command -v node &>/dev/null; then ' +
|
|
783
|
+
'echo " Installing Node.js 20.x..." && ' +
|
|
784
|
+
'curl -fsSL https://deb.nodesource.com/setup_20.x | sudo -E bash - && ' +
|
|
785
|
+
'sudo DEBIAN_FRONTEND=noninteractive apt-get install -y -qq nodejs; ' +
|
|
786
|
+
'fi && ' +
|
|
787
|
+
// Install Docker if missing
|
|
788
|
+
'if ! command -v docker &>/dev/null; then ' +
|
|
789
|
+
'echo " Installing Docker..." && ' +
|
|
790
|
+
'sudo apt-get update -qq && sudo DEBIAN_FRONTEND=noninteractive apt-get install -y -qq docker.io docker-compose-v2 && ' +
|
|
791
|
+
'sudo usermod -aG docker $USER && ' +
|
|
792
|
+
'sudo systemctl enable docker && sudo systemctl start docker; ' +
|
|
793
|
+
'fi && ' +
|
|
794
|
+
// Clone repo if missing (check .git dir to catch empty/broken clones)
|
|
795
|
+
'if [ ! -d "' + projectDir + '/.git" ]; then ' +
|
|
796
|
+
'echo " Cloning project..." && ' +
|
|
797
|
+
'mkdir -p $HOME/.factiii && ' +
|
|
798
|
+
// Remove broken/partial clone directory if it exists without .git
|
|
799
|
+
'if [ -d "' + projectDir + '" ]; then echo " Removing broken clone..." && rm -rf "' + projectDir + '"; fi && ' +
|
|
800
|
+
// Add github.com to known_hosts to avoid interactive prompt
|
|
801
|
+
'mkdir -p ~/.ssh && ssh-keyscan -t ed25519,rsa github.com >> ~/.ssh/known_hosts 2>/dev/null && ' +
|
|
802
|
+
// Generate a GitHub deploy key on the server if none exists
|
|
803
|
+
'if [ ! -f ~/.ssh/github_deploy_key ]; then ' +
|
|
804
|
+
'echo " Generating GitHub deploy key on server..." && ' +
|
|
805
|
+
'ssh-keygen -t ed25519 -f ~/.ssh/github_deploy_key -N "" -C "server-deploy" -q && ' +
|
|
806
|
+
'chmod 600 ~/.ssh/github_deploy_key; ' +
|
|
807
|
+
'fi && ' +
|
|
808
|
+
// Configure SSH to use the deploy key for github.com
|
|
809
|
+
'if ! grep -q "Host github.com" ~/.ssh/config 2>/dev/null; then ' +
|
|
810
|
+
'printf "\\nHost github.com\\n IdentityFile ~/.ssh/github_deploy_key\\n IdentitiesOnly yes\\n" >> ~/.ssh/config && ' +
|
|
811
|
+
'chmod 600 ~/.ssh/config; ' +
|
|
812
|
+
'fi && ' +
|
|
813
|
+
(githubRepo
|
|
814
|
+
? 'cd $HOME/.factiii && ' +
|
|
815
|
+
// Try HTTPS with token first (works for private repos when GITHUB_TOKEN is set)
|
|
816
|
+
'if [ -n "$GITHUB_TOKEN" ]; then ' +
|
|
817
|
+
'GIT_TERMINAL_PROMPT=0 git clone https://x-access-token:$GITHUB_TOKEN@github.com/' + githubRepo + '.git ' + repoName + '; ' +
|
|
818
|
+
// Then try SSH (works if server has a GitHub deploy key in repo settings)
|
|
819
|
+
'elif GIT_TERMINAL_PROMPT=0 git clone git@github.com:' + githubRepo + '.git ' + repoName + ' 2>/dev/null; then ' +
|
|
820
|
+
'true; ' +
|
|
821
|
+
// All clone methods failed — show deploy key and instructions
|
|
822
|
+
'else ' +
|
|
823
|
+
'echo "" && ' +
|
|
824
|
+
'echo " [!] Cannot clone private repo — server needs GitHub access" && ' +
|
|
825
|
+
'echo "" && ' +
|
|
826
|
+
'echo " Add this deploy key to your GitHub repo:" && ' +
|
|
827
|
+
'echo " GitHub → ' + githubRepo + ' → Settings → Deploy keys → Add" && ' +
|
|
828
|
+
'echo "" && ' +
|
|
829
|
+
'cat ~/.ssh/github_deploy_key.pub && ' +
|
|
830
|
+
'echo "" && ' +
|
|
831
|
+
'echo " Then re-run: npx stack fix --prod" && ' +
|
|
832
|
+
'exit 1; ' +
|
|
833
|
+
'fi; '
|
|
834
|
+
: 'echo " [!] No github_repo configured — cannot auto-clone" && exit 1; ') +
|
|
835
|
+
'fi && ';
|
|
868
836
|
const projectCheckCmd = bootstrapCmd;
|
|
869
837
|
// Step 2: Still no key — prompt for it (EC2: .pem file, others: password)
|
|
870
838
|
if (!resolvedKeyPath) {
|
|
@@ -919,47 +887,61 @@ async function sshRemoteFactiiiCommand(stage, config, command, rootDir) {
|
|
|
919
887
|
if (!password) {
|
|
920
888
|
password = await promptAndValidatePassword(stage, host, user, config, rootDir);
|
|
921
889
|
}
|
|
922
|
-
|
|
890
|
+
// promptAndValidatePassword may have set up an SSH key — check before falling back to sshpass
|
|
891
|
+
const candidateKey1 = path.join(os.homedir(), '.ssh', stage + '_deploy_key');
|
|
892
|
+
if (fs.existsSync(candidateKey1)) {
|
|
893
|
+
const kv = (0, child_process_1.spawnSync)('ssh', [
|
|
894
|
+
'-i', candidateKey1, '-o', 'BatchMode=yes', '-o', 'StrictHostKeyChecking=no',
|
|
895
|
+
'-o', 'ConnectTimeout=5', user + '@' + host, 'echo ok',
|
|
896
|
+
], { encoding: 'utf8', stdio: 'pipe', timeout: 10000 });
|
|
897
|
+
if (kv.status === 0) {
|
|
898
|
+
resolvedKeyPath = candidateKey1;
|
|
899
|
+
}
|
|
900
|
+
}
|
|
901
|
+
// If we now have a key, skip sshpass and fall through to Step 3
|
|
902
|
+
if (!resolvedKeyPath) {
|
|
903
|
+
if (!password) {
|
|
904
|
+
return {
|
|
905
|
+
success: false,
|
|
906
|
+
stdout: '',
|
|
907
|
+
stderr: 'No SSH key for ' + stage + '. For EC2: provide the .pem file from AWS Console.',
|
|
908
|
+
};
|
|
909
|
+
}
|
|
910
|
+
console.log(' SSH (password): ' + user + '@' + host + ' → npx stack ' + command);
|
|
911
|
+
const pwStart = Date.now();
|
|
912
|
+
let pwResult;
|
|
913
|
+
if (process.platform === 'win32') {
|
|
914
|
+
// Windows: no sshpass — use interactive SSH so user types password
|
|
915
|
+
console.log(' You will be prompted for the password by SSH:');
|
|
916
|
+
console.log('');
|
|
917
|
+
pwResult = (0, child_process_1.spawnSync)('ssh', [
|
|
918
|
+
'-tt',
|
|
919
|
+
'-o', 'StrictHostKeyChecking=no',
|
|
920
|
+
'-o', 'ConnectTimeout=10',
|
|
921
|
+
'-o', 'ServerAliveInterval=60',
|
|
922
|
+
'-o', 'ServerAliveCountMax=5',
|
|
923
|
+
user + '@' + host,
|
|
924
|
+
pwRemoteCommand,
|
|
925
|
+
], { encoding: 'utf8', stdio: 'inherit', timeout: 600000 });
|
|
926
|
+
}
|
|
927
|
+
else {
|
|
928
|
+
pwResult = (0, child_process_1.spawnSync)('sshpass', [
|
|
929
|
+
'-p', password, 'ssh', '-tt',
|
|
930
|
+
'-o', 'StrictHostKeyChecking=no',
|
|
931
|
+
'-o', 'ConnectTimeout=10',
|
|
932
|
+
'-o', 'ServerAliveInterval=60',
|
|
933
|
+
'-o', 'ServerAliveCountMax=5',
|
|
934
|
+
user + '@' + host,
|
|
935
|
+
pwRemoteCommand,
|
|
936
|
+
], { encoding: 'utf8', stdio: 'inherit', timeout: 600000 });
|
|
937
|
+
}
|
|
938
|
+
console.log(' SSH completed in ' + Math.floor((Date.now() - pwStart) / 1000) + 's');
|
|
923
939
|
return {
|
|
924
|
-
success:
|
|
940
|
+
success: pwResult.status === 0,
|
|
925
941
|
stdout: '',
|
|
926
|
-
stderr:
|
|
942
|
+
stderr: pwResult.status !== 0 ? 'SSH command exited with code ' + pwResult.status : '',
|
|
927
943
|
};
|
|
928
944
|
}
|
|
929
|
-
console.log(' SSH (password): ' + user + '@' + host + ' → npx stack ' + command);
|
|
930
|
-
const pwStart = Date.now();
|
|
931
|
-
let pwResult;
|
|
932
|
-
if (process.platform === 'win32') {
|
|
933
|
-
// Windows: no sshpass — use interactive SSH so user types password
|
|
934
|
-
console.log(' You will be prompted for the password by SSH:');
|
|
935
|
-
console.log('');
|
|
936
|
-
pwResult = (0, child_process_1.spawnSync)('ssh', [
|
|
937
|
-
'-tt',
|
|
938
|
-
'-o', 'StrictHostKeyChecking=no',
|
|
939
|
-
'-o', 'ConnectTimeout=10',
|
|
940
|
-
'-o', 'ServerAliveInterval=60',
|
|
941
|
-
'-o', 'ServerAliveCountMax=5',
|
|
942
|
-
user + '@' + host,
|
|
943
|
-
pwRemoteCommand,
|
|
944
|
-
], { encoding: 'utf8', stdio: 'inherit', timeout: 600000 });
|
|
945
|
-
}
|
|
946
|
-
else {
|
|
947
|
-
pwResult = (0, child_process_1.spawnSync)('sshpass', [
|
|
948
|
-
'-p', password, 'ssh', '-tt',
|
|
949
|
-
'-o', 'StrictHostKeyChecking=no',
|
|
950
|
-
'-o', 'ConnectTimeout=10',
|
|
951
|
-
'-o', 'ServerAliveInterval=60',
|
|
952
|
-
'-o', 'ServerAliveCountMax=5',
|
|
953
|
-
user + '@' + host,
|
|
954
|
-
pwRemoteCommand,
|
|
955
|
-
], { encoding: 'utf8', stdio: 'inherit', timeout: 600000 });
|
|
956
|
-
}
|
|
957
|
-
console.log(' SSH completed in ' + Math.floor((Date.now() - pwStart) / 1000) + 's');
|
|
958
|
-
return {
|
|
959
|
-
success: pwResult.status === 0,
|
|
960
|
-
stdout: '',
|
|
961
|
-
stderr: pwResult.status !== 0 ? 'SSH command exited with code ' + pwResult.status : '',
|
|
962
|
-
};
|
|
963
945
|
}
|
|
964
946
|
}
|
|
965
947
|
// Step 3: We have a key — build command and run
|
|
@@ -1196,6 +1178,32 @@ async function sshRemoteFactiiiCommand(stage, config, command, rootDir) {
|
|
|
1196
1178
|
if (!password) {
|
|
1197
1179
|
password = await promptAndValidatePassword(stage, host, user, config, rootDir);
|
|
1198
1180
|
}
|
|
1181
|
+
// promptAndValidatePassword may have set up an SSH key — use it directly instead of sshpass
|
|
1182
|
+
const candidateKey2 = path.join(os.homedir(), '.ssh', stage + '_deploy_key');
|
|
1183
|
+
if (fs.existsSync(candidateKey2)) {
|
|
1184
|
+
const kv2 = (0, child_process_1.spawnSync)('ssh', [
|
|
1185
|
+
'-i', candidateKey2, '-o', 'BatchMode=yes', '-o', 'StrictHostKeyChecking=no',
|
|
1186
|
+
'-o', 'ConnectTimeout=5', user + '@' + host, 'echo ok',
|
|
1187
|
+
], { encoding: 'utf8', stdio: 'pipe', timeout: 10000 });
|
|
1188
|
+
if (kv2.status === 0) {
|
|
1189
|
+
console.log(' [OK] Using SSH key set up during password auth');
|
|
1190
|
+
const ksStart = Date.now();
|
|
1191
|
+
const ksResult = (0, child_process_1.spawnSync)('ssh', [
|
|
1192
|
+
'-tt', '-i', candidateKey2,
|
|
1193
|
+
'-o', 'StrictHostKeyChecking=no',
|
|
1194
|
+
'-o', 'ConnectTimeout=10',
|
|
1195
|
+
'-o', 'ServerAliveInterval=60',
|
|
1196
|
+
'-o', 'ServerAliveCountMax=5',
|
|
1197
|
+
user + '@' + host, remoteCommand,
|
|
1198
|
+
], { encoding: 'utf8', stdio: 'inherit', timeout: 600000 });
|
|
1199
|
+
console.log(' SSH completed in ' + Math.floor((Date.now() - ksStart) / 1000) + 's');
|
|
1200
|
+
return {
|
|
1201
|
+
success: ksResult.status === 0,
|
|
1202
|
+
stdout: '',
|
|
1203
|
+
stderr: ksResult.status !== 0 ? 'SSH command exited with code ' + ksResult.status : '',
|
|
1204
|
+
};
|
|
1205
|
+
}
|
|
1206
|
+
}
|
|
1199
1207
|
if (password) {
|
|
1200
1208
|
console.log(' Falling back to SSH password auth...');
|
|
1201
1209
|
console.log(' SSH (password): ' + user + '@' + host + ' → npx stack ' + command);
|