@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.
Files changed (61) hide show
  1. package/LICENSE +21 -21
  2. package/README.md +95 -403
  3. package/bin/stack +334 -334
  4. package/dist/cli/dev-sync.js +16 -16
  5. package/dist/plugins/addons/auth/index.d.ts.map +1 -1
  6. package/dist/plugins/addons/auth/index.js +31 -12
  7. package/dist/plugins/addons/auth/index.js.map +1 -1
  8. package/dist/plugins/addons/auth/scanfix/secrets.d.ts +3 -0
  9. package/dist/plugins/addons/auth/scanfix/secrets.d.ts.map +1 -1
  10. package/dist/plugins/addons/auth/scanfix/secrets.js +54 -19
  11. package/dist/plugins/addons/auth/scanfix/secrets.js.map +1 -1
  12. package/dist/plugins/addons/auth/scanfix/validate.d.ts +3 -0
  13. package/dist/plugins/addons/auth/scanfix/validate.d.ts.map +1 -1
  14. package/dist/plugins/addons/auth/scanfix/validate.js +37 -18
  15. package/dist/plugins/addons/auth/scanfix/validate.js.map +1 -1
  16. package/dist/plugins/addons/vercel/index.js +9 -9
  17. package/dist/plugins/addons/vercel/scanfix/config.js +10 -10
  18. package/dist/plugins/addons/vercel/scanfix/token.js +15 -15
  19. package/dist/plugins/approved.json +13 -13
  20. package/dist/plugins/pipelines/aws/index.js +12 -12
  21. package/dist/plugins/pipelines/aws/policies/bootstrap-policy.json +135 -135
  22. package/dist/plugins/pipelines/aws/prod.js +1 -1
  23. package/dist/plugins/pipelines/factiii/index.d.ts.map +1 -1
  24. package/dist/plugins/pipelines/factiii/index.js +2 -14
  25. package/dist/plugins/pipelines/factiii/index.js.map +1 -1
  26. package/dist/plugins/pipelines/factiii/prod.js +21 -21
  27. package/dist/plugins/pipelines/factiii/scanfix/port-convention.d.ts.map +1 -1
  28. package/dist/plugins/pipelines/factiii/scanfix/port-convention.js +2 -4
  29. package/dist/plugins/pipelines/factiii/scanfix/port-convention.js.map +1 -1
  30. package/dist/plugins/pipelines/factiii/staging.js +23 -23
  31. package/dist/plugins/pipelines/factiii/workflows/stack-ci.yml +75 -75
  32. package/dist/plugins/pipelines/factiii/workflows/stack-cicd-prod.yml +73 -73
  33. package/dist/plugins/servers/amazon-linux/index.js +16 -16
  34. package/dist/plugins/servers/mac/index.js +12 -12
  35. package/dist/plugins/servers/mac/staging.js +2 -2
  36. package/dist/plugins/servers/ubuntu/index.js +23 -23
  37. package/dist/plugins/servers/windows/index.js +15 -15
  38. package/dist/scanfix/commands/mac.d.ts.map +1 -1
  39. package/dist/scanfix/commands/mac.js +5 -4
  40. package/dist/scanfix/commands/mac.js.map +1 -1
  41. package/dist/scanfix/fixes/certbot.d.ts.map +1 -1
  42. package/dist/scanfix/fixes/certbot.js +4 -18
  43. package/dist/scanfix/fixes/certbot.js.map +1 -1
  44. package/dist/scanfix/fixes/docker.d.ts.map +1 -1
  45. package/dist/scanfix/fixes/docker.js +5 -14
  46. package/dist/scanfix/fixes/docker.js.map +1 -1
  47. package/dist/scanfix/ssl-cert-helper.d.ts.map +1 -1
  48. package/dist/scanfix/ssl-cert-helper.js +18 -4
  49. package/dist/scanfix/ssl-cert-helper.js.map +1 -1
  50. package/dist/scripts/generate-all.js +73 -73
  51. package/dist/utils/deployment-report.js +2 -2
  52. package/dist/utils/secret-prompts.js +34 -34
  53. package/dist/utils/ssh-helper.d.ts.map +1 -1
  54. package/dist/utils/ssh-helper.js +150 -142
  55. package/dist/utils/ssh-helper.js.map +1 -1
  56. package/dist/utils/template-generator.js +74 -74
  57. package/package.json +93 -114
  58. package/dist/plugins/pipelines/factiii/scanfix/docker.d.ts +0 -20
  59. package/dist/plugins/pipelines/factiii/scanfix/docker.d.ts.map +0 -1
  60. package/dist/plugins/pipelines/factiii/scanfix/docker.js +0 -131
  61. 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;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,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"}
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"}
@@ -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: 'inherit', timeout: 15000 });
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 (common issue on Mac servers)
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
- stdio: 'inherit',
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
- // Platform-aware: Mac uses Homebrew, Ubuntu uses apt-get
768
- const serverType = envConfig.server ?? 'ubuntu';
769
- const isMacServer = serverType === 'mac';
770
- const bootstrapCmd = isMacServer
771
- ? (
772
- // ── macOS bootstrap (Homebrew) ──
773
- // Install Homebrew if missing
774
- 'export PATH="/opt/homebrew/bin:/usr/local/bin:$PATH" && ' +
775
- 'if ! command -v brew &>/dev/null; then ' +
776
- 'echo " Installing Homebrew..." && ' +
777
- '/bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)" && ' +
778
- 'eval "$(/opt/homebrew/bin/brew shellenv 2>/dev/null || /usr/local/bin/brew shellenv 2>/dev/null)"; ' +
779
- 'fi && ' +
780
- // Install git if missing (usually pre-installed via Xcode CLI tools)
781
- 'if ! command -v git &>/dev/null; then ' +
782
- 'echo " Installing git..." && ' +
783
- 'brew install git; ' +
784
- 'fi && ' +
785
- // Install Node.js if missing
786
- 'if ! command -v node &>/dev/null; then ' +
787
- 'echo " Installing Node.js..." && ' +
788
- 'brew install node; ' +
789
- 'fi && ' +
790
- // Install Docker (Colima) if missing
791
- 'if ! command -v docker &>/dev/null; then ' +
792
- 'echo " Installing Docker via Colima..." && ' +
793
- 'brew install docker docker-compose colima && ' +
794
- 'colima start --memory 4 --cpu 2; ' +
795
- 'fi && ' +
796
- // Ensure Docker daemon is running (Colima or Docker Desktop)
797
- 'if ! docker info &>/dev/null; then ' +
798
- 'echo " Starting Docker..." && ' +
799
- 'if command -v colima &>/dev/null; then ' +
800
- 'colima start --memory 4 --cpu 2 2>/dev/null; ' +
801
- 'elif [ -d "/Applications/Docker.app" ]; then ' +
802
- 'nohup /Applications/Docker.app/Contents/MacOS/Docker --unattended > /dev/null 2>&1 & ' +
803
- 'for i in $(seq 1 30); do sleep 1; if docker info &>/dev/null; then break; fi; done; ' +
804
- 'fi; ' +
805
- 'fi && ')
806
- : (
807
- // ── Ubuntu/Linux bootstrap (apt-get) ──
808
- // Install git if missing
809
- 'if ! command -v git &>/dev/null; then ' +
810
- 'echo " Installing git..." && ' +
811
- 'sudo apt-get update -qq && sudo DEBIAN_FRONTEND=noninteractive apt-get install -y -qq git; ' +
812
- 'fi && ' +
813
- // Install Node.js if missing
814
- 'if ! command -v node &>/dev/null; then ' +
815
- 'echo " Installing Node.js 20.x..." && ' +
816
- 'curl -fsSL https://deb.nodesource.com/setup_20.x | sudo -E bash - && ' +
817
- 'sudo DEBIAN_FRONTEND=noninteractive apt-get install -y -qq nodejs; ' +
818
- 'fi && ' +
819
- // Install Docker if missing
820
- 'if ! command -v docker &>/dev/null; then ' +
821
- 'echo " Installing Docker..." && ' +
822
- 'sudo apt-get update -qq && sudo DEBIAN_FRONTEND=noninteractive apt-get install -y -qq docker.io docker-compose-v2 && ' +
823
- 'sudo usermod -aG docker $USER && ' +
824
- 'sudo systemctl enable docker && sudo systemctl start docker; ' +
825
- 'fi && ') +
826
- // Clone repo if missing (check .git dir to catch empty/broken clones)
827
- 'if [ ! -d "' + projectDir + '/.git" ]; then ' +
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
- if (!password) {
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: false,
940
+ success: pwResult.status === 0,
925
941
  stdout: '',
926
- stderr: 'No SSH key for ' + stage + '. For EC2: provide the .pem file from AWS Console.',
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);