@swovohq/fuel 0.2.2-alpha.9 → 0.2.2-alpha.91

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 (232) hide show
  1. package/README.md +173 -69
  2. package/dist/bin/fuel.js +77 -31
  3. package/dist/bin/fuel.js.map +1 -1
  4. package/dist/cleanup/finalize.d.ts.map +1 -1
  5. package/dist/cleanup/finalize.js +11 -3
  6. package/dist/cleanup/finalize.js.map +1 -1
  7. package/dist/commands/config-verify.d.ts.map +1 -1
  8. package/dist/commands/config-verify.js +14 -6
  9. package/dist/commands/config-verify.js.map +1 -1
  10. package/dist/commands/create-app.d.ts.map +1 -1
  11. package/dist/commands/create-app.js +41 -7
  12. package/dist/commands/create-app.js.map +1 -1
  13. package/dist/commands/infra-deploy.d.ts +15 -1
  14. package/dist/commands/infra-deploy.d.ts.map +1 -1
  15. package/dist/commands/infra-deploy.js +164 -18
  16. package/dist/commands/infra-deploy.js.map +1 -1
  17. package/dist/commands/infra-destroy.d.ts.map +1 -1
  18. package/dist/commands/infra-destroy.js +3 -3
  19. package/dist/commands/infra-destroy.js.map +1 -1
  20. package/dist/commands/infra-init.d.ts +91 -0
  21. package/dist/commands/infra-init.d.ts.map +1 -1
  22. package/dist/commands/infra-init.js +644 -113
  23. package/dist/commands/infra-init.js.map +1 -1
  24. package/dist/commands/infra-pull.d.ts +8 -0
  25. package/dist/commands/infra-pull.d.ts.map +1 -0
  26. package/dist/commands/infra-pull.js +81 -0
  27. package/dist/commands/infra-pull.js.map +1 -0
  28. package/dist/commands/list-modules.d.ts.map +1 -1
  29. package/dist/commands/list-modules.js +10 -3
  30. package/dist/commands/list-modules.js.map +1 -1
  31. package/dist/engines/convention.d.ts +2 -2
  32. package/dist/engines/convention.d.ts.map +1 -1
  33. package/dist/engines/convention.js +8 -6
  34. package/dist/engines/convention.js.map +1 -1
  35. package/dist/engines/dependencies.d.ts +2 -0
  36. package/dist/engines/dependencies.d.ts.map +1 -1
  37. package/dist/engines/dependencies.js.map +1 -1
  38. package/dist/engines/monorepo.d.ts.map +1 -1
  39. package/dist/engines/monorepo.js.map +1 -1
  40. package/dist/engines/template-source.d.ts.map +1 -1
  41. package/dist/engines/template-source.js +3 -1
  42. package/dist/engines/template-source.js.map +1 -1
  43. package/dist/index.d.ts.map +1 -1
  44. package/dist/index.js.map +1 -1
  45. package/dist/infra/azure-defaults.d.ts +49 -0
  46. package/dist/infra/azure-defaults.d.ts.map +1 -0
  47. package/dist/infra/azure-defaults.js +52 -0
  48. package/dist/infra/azure-defaults.js.map +1 -0
  49. package/dist/infra/azure-keyvault-client.d.ts +4 -0
  50. package/dist/infra/azure-keyvault-client.d.ts.map +1 -0
  51. package/dist/infra/azure-keyvault-client.js +57 -0
  52. package/dist/infra/azure-keyvault-client.js.map +1 -0
  53. package/dist/infra/azure-rbac.d.ts +11 -0
  54. package/dist/infra/azure-rbac.d.ts.map +1 -0
  55. package/dist/infra/azure-rbac.js +49 -0
  56. package/dist/infra/azure-rbac.js.map +1 -0
  57. package/dist/infra/azure-state-storage.d.ts +20 -0
  58. package/dist/infra/azure-state-storage.d.ts.map +1 -0
  59. package/dist/infra/azure-state-storage.js +229 -0
  60. package/dist/infra/azure-state-storage.js.map +1 -0
  61. package/dist/infra/config-loader.d.ts.map +1 -1
  62. package/dist/infra/config-loader.js.map +1 -1
  63. package/dist/infra/config-writer.js +1 -1
  64. package/dist/infra/credentials.d.ts +2 -2
  65. package/dist/infra/credentials.d.ts.map +1 -1
  66. package/dist/infra/credentials.js +8 -4
  67. package/dist/infra/credentials.js.map +1 -1
  68. package/dist/infra/dependency-checker.d.ts.map +1 -1
  69. package/dist/infra/dependency-checker.js.map +1 -1
  70. package/dist/infra/deployment-detect.d.ts +9 -0
  71. package/dist/infra/deployment-detect.d.ts.map +1 -0
  72. package/dist/infra/deployment-detect.js +53 -0
  73. package/dist/infra/deployment-detect.js.map +1 -0
  74. package/dist/infra/git-client.d.ts.map +1 -1
  75. package/dist/infra/git-client.js +6 -10
  76. package/dist/infra/git-client.js.map +1 -1
  77. package/dist/infra/github-client.d.ts +7 -0
  78. package/dist/infra/github-client.d.ts.map +1 -1
  79. package/dist/infra/github-client.js +47 -7
  80. package/dist/infra/github-client.js.map +1 -1
  81. package/dist/infra/github-secrets-client.d.ts.map +1 -1
  82. package/dist/infra/github-secrets-client.js +2 -2
  83. package/dist/infra/github-secrets-client.js.map +1 -1
  84. package/dist/infra/github-token-link.d.ts +4 -0
  85. package/dist/infra/github-token-link.d.ts.map +1 -0
  86. package/dist/infra/github-token-link.js +32 -0
  87. package/dist/infra/github-token-link.js.map +1 -0
  88. package/dist/infra/orchestrator.d.ts +4 -1
  89. package/dist/infra/orchestrator.d.ts.map +1 -1
  90. package/dist/infra/orchestrator.js +218 -63
  91. package/dist/infra/orchestrator.js.map +1 -1
  92. package/dist/infra/port-utils.d.ts.map +1 -1
  93. package/dist/infra/port-utils.js.map +1 -1
  94. package/dist/infra/preflight.d.ts.map +1 -1
  95. package/dist/infra/preflight.js +25 -11
  96. package/dist/infra/preflight.js.map +1 -1
  97. package/dist/infra/remote-secret.d.ts +14 -0
  98. package/dist/infra/remote-secret.d.ts.map +1 -0
  99. package/dist/infra/remote-secret.js +42 -0
  100. package/dist/infra/remote-secret.js.map +1 -0
  101. package/dist/infra/s3-state-bucket.d.ts.map +1 -1
  102. package/dist/infra/s3-state-bucket.js +8 -4
  103. package/dist/infra/s3-state-bucket.js.map +1 -1
  104. package/dist/infra/secrets-manager-client.d.ts.map +1 -1
  105. package/dist/infra/secrets-manager-client.js +8 -5
  106. package/dist/infra/secrets-manager-client.js.map +1 -1
  107. package/dist/infra/subprocess.js +1 -1
  108. package/dist/infra/template-engine.d.ts +14 -5
  109. package/dist/infra/template-engine.d.ts.map +1 -1
  110. package/dist/infra/template-engine.js +157 -39
  111. package/dist/infra/template-engine.js.map +1 -1
  112. package/dist/infra/tofu-runner.d.ts +4 -3
  113. package/dist/infra/tofu-runner.d.ts.map +1 -1
  114. package/dist/infra/tofu-runner.js +40 -34
  115. package/dist/infra/tofu-runner.js.map +1 -1
  116. package/dist/infra/types.d.ts +241 -17
  117. package/dist/infra/types.d.ts.map +1 -1
  118. package/dist/infra/types.js +419 -22
  119. package/dist/infra/types.js.map +1 -1
  120. package/dist/infra-templates/TODO-azure-prod.md +127 -0
  121. package/dist/infra-templates/modules/azure-container-app/main.tf +124 -0
  122. package/dist/infra-templates/modules/azure-container-app/outputs.tf +24 -0
  123. package/dist/infra-templates/modules/azure-container-app/variables.tf +80 -0
  124. package/dist/infra-templates/modules/azure-container-apps-environment/main.tf +18 -0
  125. package/dist/infra-templates/modules/azure-container-apps-environment/outputs.tf +14 -0
  126. package/dist/infra-templates/modules/azure-container-apps-environment/variables.tf +24 -0
  127. package/dist/infra-templates/modules/azure-container-registry/main.tf +10 -0
  128. package/dist/infra-templates/modules/azure-container-registry/outputs.tf +14 -0
  129. package/dist/infra-templates/modules/azure-container-registry/variables.tf +20 -0
  130. package/dist/infra-templates/modules/azure-front-door/main.tf +80 -0
  131. package/dist/infra-templates/modules/azure-front-door/outputs.tf +20 -0
  132. package/dist/infra-templates/modules/azure-front-door/variables.tf +26 -0
  133. package/dist/infra-templates/modules/azure-github-oidc/main.tf +48 -0
  134. package/dist/infra-templates/modules/azure-github-oidc/outputs.tf +9 -0
  135. package/dist/infra-templates/modules/azure-github-oidc/variables.tf +69 -0
  136. package/dist/infra-templates/modules/azure-key-vault/main.tf +21 -0
  137. package/dist/infra-templates/modules/azure-key-vault/outputs.tf +11 -0
  138. package/dist/infra-templates/modules/azure-key-vault/variables.tf +43 -0
  139. package/dist/infra-templates/modules/azure-monitor/main.tf +29 -0
  140. package/dist/infra-templates/modules/azure-monitor/outputs.tf +17 -0
  141. package/dist/infra-templates/modules/azure-monitor/variables.tf +32 -0
  142. package/dist/infra-templates/modules/azure-network/main.tf +134 -0
  143. package/dist/infra-templates/modules/azure-network/outputs.tf +44 -0
  144. package/dist/infra-templates/modules/azure-network/variables.tf +56 -0
  145. package/dist/infra-templates/modules/azure-postgresql-flexible/main.tf +34 -0
  146. package/dist/infra-templates/modules/azure-postgresql-flexible/outputs.tf +20 -0
  147. package/dist/infra-templates/modules/azure-postgresql-flexible/variables.tf +54 -0
  148. package/dist/infra-templates/modules/azure-redis-cache/main.tf +32 -0
  149. package/dist/infra-templates/modules/azure-redis-cache/outputs.tf +15 -0
  150. package/dist/infra-templates/modules/azure-redis-cache/variables.tf +30 -0
  151. package/dist/infra-templates/modules/azure-resource-group/main.tf +5 -0
  152. package/dist/infra-templates/modules/azure-resource-group/outputs.tf +11 -0
  153. package/dist/infra-templates/modules/azure-resource-group/variables.tf +16 -0
  154. package/dist/infra-templates/modules/azure-sql-database/main.tf +63 -0
  155. package/dist/infra-templates/modules/azure-sql-database/outputs.tf +20 -0
  156. package/dist/infra-templates/modules/azure-sql-database/variables.tf +78 -0
  157. package/dist/infra-templates/modules/azure-storage-account/main.tf +13 -0
  158. package/dist/infra-templates/modules/azure-storage-account/outputs.tf +16 -0
  159. package/dist/infra-templates/modules/azure-storage-account/variables.tf +38 -0
  160. package/dist/infra-templates/modules/cloud-watch/event-bridge.tf +15 -0
  161. package/dist/infra-templates/modules/cloud-watch/lambda.tf +84 -0
  162. package/dist/infra-templates/modules/cloud-watch/lambda_function.zip +0 -0
  163. package/dist/infra-templates/modules/cloud-watch/lambda_function_deploy.zip +0 -0
  164. package/dist/infra-templates/modules/cloud-watch/main.tf +156 -0
  165. package/dist/infra-templates/modules/cloud-watch/sns.tf +3 -0
  166. package/dist/infra-templates/modules/cloud-watch/variables.tf +38 -0
  167. package/dist/infra-templates/modules/ecs-core/alb.tf +126 -0
  168. package/dist/infra-templates/modules/ecs-core/cloudwatch.tf +48 -0
  169. package/dist/infra-templates/modules/ecs-core/iam.tf +95 -0
  170. package/dist/infra-templates/modules/ecs-core/main.tf +12 -0
  171. package/dist/infra-templates/modules/ecs-core/outputs.tf +44 -0
  172. package/dist/infra-templates/modules/ecs-core/secrets.tf +9 -0
  173. package/dist/infra-templates/modules/ecs-core/security_groups.tf +73 -0
  174. package/dist/infra-templates/modules/ecs-core/variables.tf +96 -0
  175. package/dist/infra-templates/modules/ecs-service/ecs_autoscaling.tf +51 -0
  176. package/dist/infra-templates/modules/ecs-service/ecs_service.tf +39 -0
  177. package/dist/infra-templates/modules/ecs-service/ecs_task_definition.tf +56 -0
  178. package/dist/infra-templates/modules/ecs-service/variables.tf +170 -0
  179. package/dist/infra-templates/modules/elastic-container-registry/main.tf +32 -0
  180. package/dist/infra-templates/modules/elastic-container-registry/outputs.tf +9 -0
  181. package/dist/infra-templates/modules/elastic-container-registry/variables.tf +25 -0
  182. package/dist/infra-templates/modules/github-oidc-role/main.tf +46 -0
  183. package/dist/infra-templates/modules/github-oidc-role/outputs.tf +4 -0
  184. package/dist/infra-templates/modules/github-oidc-role/variables.tf +11 -0
  185. package/dist/infra-templates/modules/guardduty/main.tf +8 -0
  186. package/dist/infra-templates/modules/iam/main.tf +205 -0
  187. package/dist/infra-templates/modules/iam/outputs.tf +3 -0
  188. package/dist/infra-templates/modules/iam/variables.tf +4 -0
  189. package/dist/infra-templates/modules/random-key/main.tf +11 -0
  190. package/dist/infra-templates/modules/random-key/outputs.tf +4 -0
  191. package/dist/infra-templates/modules/random-key/variables.tf +10 -0
  192. package/dist/infra-templates/modules/redis/main.tf +47 -0
  193. package/dist/infra-templates/modules/redis/outputs.tf +8 -0
  194. package/dist/infra-templates/modules/redis/variables.tf +20 -0
  195. package/dist/infra-templates/modules/relational-database-service/main.tf +44 -0
  196. package/dist/infra-templates/modules/relational-database-service/outputs.tf +13 -0
  197. package/dist/infra-templates/modules/relational-database-service/security-group.tf +15 -0
  198. package/dist/infra-templates/modules/relational-database-service/variables.tf +50 -0
  199. package/dist/infra-templates/modules/secret/main.tf +14 -0
  200. package/dist/infra-templates/modules/secret/outputs.tf +3 -0
  201. package/dist/infra-templates/modules/secret/variables.tf +8 -0
  202. package/dist/infra-templates/modules/simple-storage-service/main.tf +28 -0
  203. package/dist/infra-templates/modules/simple-storage-service/variables.tf +4 -0
  204. package/dist/infra-templates/modules/vpc/data.tf +2 -0
  205. package/dist/infra-templates/modules/vpc/main.tf +76 -0
  206. package/dist/infra-templates/modules/vpc/outputs.tf +23 -0
  207. package/dist/infra-templates/modules/vpc/variables.tf +5 -0
  208. package/dist/infra-templates/package.json +6 -0
  209. package/dist/infra-templates/templates/azure/MANUAL_DEPLOY.md +168 -0
  210. package/dist/infra-templates/templates/azure/backend.tf +8 -0
  211. package/dist/infra-templates/templates/azure/data.tf +21 -0
  212. package/dist/infra-templates/templates/azure/default.tfvars +0 -0
  213. package/dist/infra-templates/templates/azure/locals.tf +116 -0
  214. package/dist/infra-templates/templates/azure/main.tf +421 -0
  215. package/dist/infra-templates/templates/azure/outputs.tf +40 -0
  216. package/dist/infra-templates/templates/azure/providers.tf +65 -0
  217. package/dist/infra-templates/templates/azure/variables.tf +69 -0
  218. package/dist/infra-templates/templates/azure/workflows/build_and_deploy.yml +155 -0
  219. package/dist/infra-templates/templates/azure/workflows/tests.yml +63 -0
  220. package/dist/infra-templates/templates/ecs/README.md +235 -0
  221. package/dist/infra-templates/templates/ecs/backend.tf +7 -0
  222. package/dist/infra-templates/templates/ecs/data.tf +6 -0
  223. package/dist/infra-templates/templates/ecs/default.tfvars +1 -0
  224. package/dist/infra-templates/templates/ecs/locals.tf +97 -0
  225. package/dist/infra-templates/templates/ecs/main.tf +185 -0
  226. package/dist/infra-templates/templates/ecs/outputs.tf +42 -0
  227. package/dist/infra-templates/templates/ecs/providers.tf +21 -0
  228. package/dist/infra-templates/templates/ecs/variables.tf +105 -0
  229. package/dist/infra-templates/templates/ecs/workflows/build_and_deploy.yml +188 -0
  230. package/dist/infra-templates/templates/ecs/workflows/tests.yml +65 -0
  231. package/package.json +10 -2
  232. package/schema/fuel.schema.json +570 -91
@@ -36,8 +36,9 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
36
36
  return (mod && mod.__esModule) ? mod : { "default": mod };
37
37
  };
38
38
  Object.defineProperty(exports, "__esModule", { value: true });
39
- exports.CONFIG_TEMPLATE = void 0;
39
+ exports.CONFIG_TEMPLATE_AZURE = exports.CONFIG_TEMPLATE = void 0;
40
40
  exports.applyInfraInitAnswers = applyInfraInitAnswers;
41
+ exports.applyAzureInfraInitAnswers = applyAzureInfraInitAnswers;
41
42
  exports.infraInit = infraInit;
42
43
  const fs = __importStar(require("fs-extra"));
43
44
  const path = __importStar(require("path"));
@@ -47,16 +48,26 @@ const promises_1 = require("readline/promises");
47
48
  const child_process_1 = require("child_process");
48
49
  const util_1 = require("util");
49
50
  const https = __importStar(require("https"));
51
+ const identity_1 = require("@azure/identity");
52
+ const arm_resources_1 = require("@azure/arm-resources");
53
+ const azure_defaults_1 = require("../infra/azure-defaults");
54
+ const github_client_1 = require("../infra/github-client");
55
+ const github_token_link_1 = require("../infra/github-token-link");
50
56
  const execAsync = (0, util_1.promisify)(child_process_1.exec);
51
57
  function httpsGetJson(url, headers) {
52
58
  return new Promise((resolve, reject) => {
53
59
  const req = https.get(url, { headers }, (res) => {
54
60
  let body = '';
55
- res.on('data', (chunk) => { body += chunk.toString(); });
61
+ res.on('data', (chunk) => {
62
+ body += chunk.toString();
63
+ });
56
64
  res.on('end', () => resolve({ status: res.statusCode ?? 0, body }));
57
65
  });
58
66
  req.on('error', reject);
59
- req.setTimeout(10000, () => { req.destroy(); reject(new Error('Request timed out')); });
67
+ req.setTimeout(10000, () => {
68
+ req.destroy();
69
+ reject(new Error('Request timed out'));
70
+ });
60
71
  });
61
72
  }
62
73
  exports.CONFIG_TEMPLATE = {
@@ -80,7 +91,7 @@ exports.CONFIG_TEMPLATE = {
80
91
  db_instance_type: 'db.t3.small',
81
92
  db_allocated_storage: 30,
82
93
  variables: {},
83
- workers: [],
94
+ workers: []
84
95
  },
85
96
  {
86
97
  name: 'my-app-web',
@@ -93,10 +104,59 @@ exports.CONFIG_TEMPLATE = {
93
104
  cpu_units: 1024,
94
105
  memory_units: 2048,
95
106
  build_env: {
96
- NEXT_PUBLIC_API_HOST: 'https://api.example.com',
107
+ NEXT_PUBLIC_API_HOST: 'https://api.example.com'
108
+ }
109
+ }
110
+ ]
111
+ };
112
+ exports.CONFIG_TEMPLATE_AZURE = {
113
+ name: 'my-app',
114
+ deployment: 'azure',
115
+ slack_webhook_url: '',
116
+ location: 'eastus2',
117
+ monitor: { daily_data_cap_gb: 1, retention_days: 90, dashboard_count: 0 },
118
+ front_door: { sku_name: 'Standard_AzureFrontDoor' },
119
+ storage_account: {
120
+ account_kind: 'BlockBlobStorage',
121
+ account_tier: 'Premium',
122
+ account_replication_type: 'LRS',
123
+ access_tier: 'Hot'
124
+ },
125
+ key_vault: { sku_name: 'standard' },
126
+ apps: [
127
+ {
128
+ name: 'my-app-api',
129
+ technology: 'nest',
130
+ dockerfile: 'apps/api/Dockerfile',
131
+ health_check_path: '/health',
132
+ domain: 'api.example.com',
133
+ port: 3000,
134
+ branch: 'main',
135
+ database: { enabled: true, engine: 'mssql', sku_name: 'GP_S_Gen5_4' },
136
+ redis: { enabled: false, sku_name: 'Balanced_B0' },
137
+ container_app: {
138
+ cpu: 1,
139
+ memory: '2Gi',
140
+ concurrent_requests: 20,
141
+ min_replicas: 0,
142
+ max_replicas: 3
97
143
  },
144
+ variables: {},
145
+ workers: []
98
146
  },
99
- ],
147
+ {
148
+ name: 'my-app-web',
149
+ technology: 'react',
150
+ dockerfile: 'apps/web/Dockerfile',
151
+ health_check_path: '/health',
152
+ domain: 'app.example.com',
153
+ port: 3000,
154
+ branch: 'main',
155
+ build_env: {
156
+ NEXT_PUBLIC_API_HOST: 'https://api.example.com'
157
+ }
158
+ }
159
+ ]
100
160
  };
101
161
  // --- Private prompt helpers ---
102
162
  async function ask(rl, label, defaultVal) {
@@ -134,6 +194,14 @@ async function askSelect(rl, label, options, defaultIdx = 1) {
134
194
  console.log(` Please enter a number between 1 and ${options.length}.`);
135
195
  }
136
196
  }
197
+ /** Prompts which cloud provider to provision infrastructure for. */
198
+ async function askCloudChoice(rl) {
199
+ console.log(' Cloud provider');
200
+ console.log(' ────────────────────────────────────────────────────');
201
+ const choice = await askSelect(rl, 'Deploy to', ['AWS (ECS)', 'Azure (Container Apps)'], 1);
202
+ console.log('');
203
+ return choice === 'AWS (ECS)' ? 'aws' : 'azure';
204
+ }
137
205
  /** Prompts for KEY=VALUE pairs until the user submits an empty line. */
138
206
  async function askKeyValues(rl, label) {
139
207
  console.log(`\n ${label} (KEY=VALUE format, empty line to finish):`);
@@ -152,18 +220,73 @@ async function askKeyValues(rl, label) {
152
220
  }
153
221
  return result;
154
222
  }
223
+ /**
224
+ * Opens a GitHub token-creation page with the required scope already checked — the user
225
+ * never has to know or look up which permissions the CLI needs — then collects the pasted
226
+ * token and validates it immediately so a wrong/incomplete token surfaces right away instead
227
+ * of deep into a later step (repo creation, push, branch protection).
228
+ */
229
+ async function askGitHubToken(rl) {
230
+ const url = (0, github_token_link_1.buildGitHubTokenUrl)();
231
+ console.log(chalk_1.default.cyan('\n Create a GitHub Personal Access Token'));
232
+ console.log(` ${url}`);
233
+ console.log(' The "repo" scope is already selected — just click "Generate token" at the bottom of the page.\n');
234
+ (0, github_token_link_1.openInBrowser)(url);
235
+ while (true) {
236
+ const token = await ask(rl, 'Paste the generated token (GITHUB_TOKEN)');
237
+ try {
238
+ await (0, github_client_1.validateGitHubToken)(token);
239
+ console.log(chalk_1.default.green(' ✓') + ' Token has the required permissions');
240
+ return { token };
241
+ }
242
+ catch (err) {
243
+ console.log(chalk_1.default.yellow(` ⚠ ${err instanceof Error ? err.message : String(err)}`));
244
+ const retry = await askYesNo(rl, ' Try pasting the token again?', true);
245
+ if (!retry) {
246
+ throw new Error('GitHub token setup aborted — a validated token is required to continue.');
247
+ }
248
+ }
249
+ }
250
+ }
251
+ /** Lists the orgs available to the given token and lets the user pick one, falling back to manual entry. */
252
+ async function askGitHubOrganizationFor(rl, token) {
253
+ try {
254
+ const { status, body } = await httpsGetJson('https://api.github.com/user/orgs', {
255
+ Authorization: `Bearer ${token}`,
256
+ 'User-Agent': 'fuel-cli',
257
+ Accept: 'application/vnd.github+json'
258
+ });
259
+ if (status === 200) {
260
+ const orgs = JSON.parse(body);
261
+ if (orgs.length === 1) {
262
+ console.log(` GitHub Org: ${chalk_1.default.green(orgs[0].login)} (only org available to this account)`);
263
+ return orgs[0].login;
264
+ }
265
+ if (orgs.length > 1) {
266
+ return askSelect(rl, 'GitHub organization', orgs.map((o) => o.login), 1);
267
+ }
268
+ }
269
+ }
270
+ catch {
271
+ // fall through to manual entry
272
+ }
273
+ return ask(rl, 'GITHUB_ORGANIZATION');
274
+ }
155
275
  /** Calls GET /user on the GitHub API to verify the token. Returns login + email on success or `{ error: string }` on failure. */
156
276
  async function tryGetGithubUser(credentials) {
157
277
  try {
158
278
  const { status, body } = await httpsGetJson('https://api.github.com/user', {
159
279
  Authorization: `Bearer ${credentials.GITHUB_TOKEN}`,
160
280
  'User-Agent': 'fuel-cli',
161
- Accept: 'application/vnd.github+json',
281
+ Accept: 'application/vnd.github+json'
162
282
  });
163
283
  const data = JSON.parse(body);
164
284
  if (status !== 200)
165
285
  return { error: data.message ?? `HTTP ${status}` };
166
- return { login: data.login ?? '(unknown)', email: data.email ?? '(private)' };
286
+ return {
287
+ login: data.login ?? '(unknown)',
288
+ email: data.email ?? '(private)'
289
+ };
167
290
  }
168
291
  catch (err) {
169
292
  return { error: err instanceof Error ? err.message : String(err) };
@@ -178,7 +301,7 @@ async function tryGetGithubOrg(credentials) {
178
301
  const { status, body } = await httpsGetJson(`https://api.github.com/orgs/${encodeURIComponent(org)}`, {
179
302
  Authorization: `Bearer ${credentials.GITHUB_TOKEN}`,
180
303
  'User-Agent': 'fuel-cli',
181
- Accept: 'application/vnd.github+json',
304
+ Accept: 'application/vnd.github+json'
182
305
  });
183
306
  const data = JSON.parse(body);
184
307
  if (status !== 200)
@@ -197,9 +320,9 @@ async function tryGetAwsAccountId(credentials) {
197
320
  ...process.env,
198
321
  AWS_ACCESS_KEY_ID: credentials.AWS_ACCESS_KEY_ID,
199
322
  AWS_SECRET_ACCESS_KEY: credentials.AWS_SECRET_ACCESS_KEY,
200
- AWS_REGION: credentials.AWS_REGION,
323
+ AWS_REGION: credentials.AWS_REGION
201
324
  },
202
- timeout: 10000,
325
+ timeout: 10000
203
326
  });
204
327
  const accountId = JSON.parse(stdout).Account ?? '';
205
328
  return { accountId };
@@ -207,7 +330,10 @@ async function tryGetAwsAccountId(credentials) {
207
330
  catch (err) {
208
331
  const e = err;
209
332
  const raw = e.stderr ?? e.message ?? String(err);
210
- const firstLine = raw.split('\n').map((l) => l.trim()).find((l) => l.length > 0) ?? raw;
333
+ const firstLine = raw
334
+ .split('\n')
335
+ .map((l) => l.trim())
336
+ .find((l) => l.length > 0) ?? raw;
211
337
  return { error: firstLine };
212
338
  }
213
339
  }
@@ -219,23 +345,39 @@ async function tryGetAcmCertificate(arn, credentials) {
219
345
  ...process.env,
220
346
  AWS_ACCESS_KEY_ID: credentials.AWS_ACCESS_KEY_ID,
221
347
  AWS_SECRET_ACCESS_KEY: credentials.AWS_SECRET_ACCESS_KEY,
222
- AWS_REGION: credentials.AWS_REGION,
348
+ AWS_REGION: credentials.AWS_REGION
223
349
  },
224
- timeout: 10000,
350
+ timeout: 10000
225
351
  });
226
352
  const cert = JSON.parse(stdout).Certificate;
227
353
  return {
228
354
  domainName: cert?.DomainName ?? '(unknown)',
229
- status: cert?.Status ?? '(unknown)',
355
+ status: cert?.Status ?? '(unknown)'
230
356
  };
231
357
  }
232
358
  catch (err) {
233
359
  const e = err;
234
360
  const raw = e.stderr ?? e.message ?? String(err);
235
- const firstLine = raw.split('\n').map((l) => l.trim()).find((l) => l.length > 0) ?? raw;
361
+ const firstLine = raw
362
+ .split('\n')
363
+ .map((l) => l.trim())
364
+ .find((l) => l.length > 0) ?? raw;
236
365
  return { error: firstLine };
237
366
  }
238
367
  }
368
+ /** Verifies Azure credentials by acquiring a token and listing resource groups (cheap, works with just Reader access). */
369
+ async function tryGetAzureAccountInfo(credentials) {
370
+ try {
371
+ const credential = new identity_1.ClientSecretCredential(credentials.AZURE_TENANT_ID, credentials.AZURE_CLIENT_ID, credentials.AZURE_CLIENT_SECRET);
372
+ const client = new arm_resources_1.ResourceManagementClient(credential, credentials.AZURE_SUBSCRIPTION_ID);
373
+ const iterator = client.resourceGroups.list();
374
+ await iterator.next();
375
+ return { subscriptionId: credentials.AZURE_SUBSCRIPTION_ID };
376
+ }
377
+ catch (err) {
378
+ return { error: err instanceof Error ? err.message : String(err) };
379
+ }
380
+ }
239
381
  /** Displays credential verification results and throws if any check failed. */
240
382
  function assertCredentialResults(stsResult, ghUserResult, ghOrgResult) {
241
383
  const failures = [];
@@ -250,6 +392,20 @@ function assertCredentialResults(stsResult, ghUserResult, ghOrgResult) {
250
392
  failures.map((f) => ` • ${f}`).join('\n'));
251
393
  }
252
394
  }
395
+ /** Displays Azure credential verification results and throws if any check failed. */
396
+ function assertAzureCredentialResults(azResult, ghUserResult, ghOrgResult) {
397
+ const failures = [];
398
+ if ('error' in azResult)
399
+ failures.push(`Azure credentials: ${azResult.error}`);
400
+ if ('error' in ghUserResult)
401
+ failures.push(`GitHub token: ${ghUserResult.error}`);
402
+ if ('error' in ghOrgResult)
403
+ failures.push(`GitHub org: ${ghOrgResult.error}`);
404
+ if (failures.length > 0) {
405
+ throw new Error('Credential verification failed:\n' +
406
+ failures.map((f) => ` • ${f}`).join('\n'));
407
+ }
408
+ }
253
409
  /** Derives a sensible domain default: cliswovotest-api.swovo.com (production), cliswovotest-api-dev.swovo.com (others) */
254
410
  function defaultDomain(projectName, suffix, env, baseDomain) {
255
411
  const label = suffix === 'web' ? 'app' : suffix;
@@ -270,12 +426,12 @@ async function collectAnswers(rl) {
270
426
  const credFilePath = path.resolve('.fuel-credentials');
271
427
  if (await fs.pathExists(credFilePath)) {
272
428
  try {
273
- const existing = await fs.readJson(credFilePath);
429
+ const existing = (await fs.readJson(credFilePath));
274
430
  console.log('\n Found existing .fuel-credentials');
275
431
  const [stsResult, ghUserResult, ghOrgResult] = await Promise.all([
276
432
  tryGetAwsAccountId(existing),
277
433
  tryGetGithubUser(existing),
278
- tryGetGithubOrg(existing),
434
+ tryGetGithubOrg(existing)
279
435
  ]);
280
436
  if ('accountId' in stsResult) {
281
437
  console.log(` AWS Account: ${chalk_1.default.green(stsResult.accountId)}`);
@@ -308,7 +464,8 @@ async function collectAnswers(rl) {
308
464
  credentials = existing;
309
465
  }
310
466
  catch (e) {
311
- if (e instanceof Error && e.message.startsWith('Credential verification failed'))
467
+ if (e instanceof Error &&
468
+ e.message.startsWith('Credential verification failed'))
312
469
  throw e;
313
470
  console.log(' Could not read .fuel-credentials — prompting for credentials.');
314
471
  }
@@ -317,14 +474,15 @@ async function collectAnswers(rl) {
317
474
  credentials.AWS_ACCESS_KEY_ID = await ask(rl, 'AWS_ACCESS_KEY_ID');
318
475
  credentials.AWS_SECRET_ACCESS_KEY = await ask(rl, 'AWS_SECRET_ACCESS_KEY');
319
476
  credentials.AWS_REGION = await ask(rl, 'AWS_REGION', 'us-east-1');
320
- credentials.GITHUB_TOKEN = await ask(rl, 'GITHUB_TOKEN');
477
+ const gh = await askGitHubToken(rl);
478
+ credentials.GITHUB_TOKEN = gh.token;
321
479
  credentials.GITHUB_USERNAME = await ask(rl, 'GITHUB_USERNAME');
322
- credentials.GITHUB_ORGANIZATION = await ask(rl, 'GITHUB_ORGANIZATION');
480
+ credentials.GITHUB_ORGANIZATION = await askGitHubOrganizationFor(rl, gh.token);
323
481
  console.log('\n Verifying credentials...');
324
482
  const [stsResult, ghUserResult, ghOrgResult] = await Promise.all([
325
483
  tryGetAwsAccountId(credentials),
326
484
  tryGetGithubUser(credentials),
327
- tryGetGithubOrg(credentials),
485
+ tryGetGithubOrg(credentials)
328
486
  ]);
329
487
  if ('accountId' in stsResult) {
330
488
  console.log(` AWS Account: ${chalk_1.default.green(stsResult.accountId)}`);
@@ -401,9 +559,18 @@ async function collectAnswers(rl) {
401
559
  const cpuUnits = await askInt(rl, 'CPU units', 1024);
402
560
  const memoryUnits = await askInt(rl, 'Memory (MB)', 2048);
403
561
  appTypes.push({
404
- suffix: 'api', dockerfile, port, healthCheckPath,
405
- postgres, redis, cpuUnits, memoryUnits,
406
- dbInstanceType, dbAllocatedStorage, redisInstanceType, variables: {},
562
+ suffix: 'api',
563
+ dockerfile,
564
+ port,
565
+ healthCheckPath,
566
+ postgres,
567
+ redis,
568
+ cpuUnits,
569
+ memoryUnits,
570
+ dbInstanceType,
571
+ dbAllocatedStorage,
572
+ redisInstanceType,
573
+ variables: {}
407
574
  });
408
575
  }
409
576
  if (includeWeb) {
@@ -414,10 +581,18 @@ async function collectAnswers(rl) {
414
581
  const cpuUnits = await askInt(rl, 'CPU units', 1024);
415
582
  const memoryUnits = await askInt(rl, 'Memory (MB)', 2048);
416
583
  appTypes.push({
417
- suffix: 'web', dockerfile, port, healthCheckPath,
418
- postgres: false, redis: false, cpuUnits, memoryUnits,
419
- dbInstanceType: 'db.t3.small', dbAllocatedStorage: 30,
420
- redisInstanceType: 'cache.t3.small', variables: {},
584
+ suffix: 'web',
585
+ dockerfile,
586
+ port,
587
+ healthCheckPath,
588
+ postgres: false,
589
+ redis: false,
590
+ cpuUnits,
591
+ memoryUnits,
592
+ dbInstanceType: 'db.t3.small',
593
+ dbAllocatedStorage: 30,
594
+ redisInstanceType: 'cache.t3.small',
595
+ variables: {}
421
596
  });
422
597
  }
423
598
  console.log('');
@@ -427,7 +602,7 @@ async function collectAnswers(rl) {
427
602
  const envPresets = {
428
603
  'production only': ['production'],
429
604
  'dev + production': ['dev', 'production'],
430
- 'dev + staging + production': ['dev', 'staging', 'production'],
605
+ 'dev + staging + production': ['dev', 'staging', 'production']
431
606
  };
432
607
  const envChoice = await askSelect(rl, 'Environments', Object.keys(envPresets), 3);
433
608
  const environments = envPresets[envChoice];
@@ -441,7 +616,9 @@ async function collectAnswers(rl) {
441
616
  for (const appType of appTypes) {
442
617
  domains[appType.suffix] = {};
443
618
  for (const env of environments) {
444
- const suggestion = baseDomain ? defaultDomain(projectName, appType.suffix, env, baseDomain) : '';
619
+ const suggestion = baseDomain
620
+ ? defaultDomain(projectName, appType.suffix, env, baseDomain)
621
+ : '';
445
622
  const appFullName = `${projectName}-${appType.suffix}-${env}`;
446
623
  domains[appType.suffix][env] = await ask(rl, appFullName, suggestion || undefined);
447
624
  }
@@ -460,7 +637,212 @@ async function collectAnswers(rl) {
460
637
  }
461
638
  }
462
639
  console.log('');
463
- return { credentials, projectName, ssl, slackWebhookUrl, appTypes, environments, baseDomain, domains };
640
+ return {
641
+ credentials,
642
+ projectName,
643
+ ssl,
644
+ slackWebhookUrl,
645
+ appTypes,
646
+ environments,
647
+ baseDomain,
648
+ domains
649
+ };
650
+ }
651
+ // --- collectAzureAnswers ---
652
+ async function collectAzureAnswers(rl) {
653
+ // --- Credentials ---
654
+ console.log(' Credentials');
655
+ console.log(' ────────────────────────────────────────────────────');
656
+ let credentials = {};
657
+ let useExistingCreds = false;
658
+ const credFilePath = path.resolve('.fuel-credentials');
659
+ if (await fs.pathExists(credFilePath)) {
660
+ try {
661
+ const existing = (await fs.readJson(credFilePath));
662
+ console.log('\n Found existing .fuel-credentials');
663
+ const [azResult, ghUserResult, ghOrgResult] = await Promise.all([
664
+ tryGetAzureAccountInfo(existing),
665
+ tryGetGithubUser(existing),
666
+ tryGetGithubOrg(existing)
667
+ ]);
668
+ if ('subscriptionId' in azResult) {
669
+ console.log(` Azure Subscription: ${chalk_1.default.green(azResult.subscriptionId)}`);
670
+ }
671
+ else {
672
+ console.log(' ' + chalk_1.default.yellow('⚠') + ' Azure credentials invalid:');
673
+ console.log(` ${azResult.error}`);
674
+ }
675
+ if ('login' in ghUserResult) {
676
+ console.log(` GitHub User: ${chalk_1.default.green(ghUserResult.login)}`);
677
+ }
678
+ else {
679
+ console.log(' ' + chalk_1.default.yellow('⚠') + ' GitHub token invalid:');
680
+ console.log(` ${ghUserResult.error}`);
681
+ }
682
+ if ('id' in ghOrgResult) {
683
+ console.log(` GitHub Org: ${chalk_1.default.green(ghOrgResult.login)} (ID: ${ghOrgResult.id})`);
684
+ }
685
+ else {
686
+ console.log(' ' + chalk_1.default.yellow('⚠') + ' Could not verify org:');
687
+ console.log(` ${ghOrgResult.error}`);
688
+ }
689
+ assertAzureCredentialResults(azResult, ghUserResult, ghOrgResult);
690
+ console.log('');
691
+ useExistingCreds = await askYesNo(rl, 'Use these credentials?', true);
692
+ if (useExistingCreds)
693
+ credentials = existing;
694
+ }
695
+ catch (e) {
696
+ if (e instanceof Error &&
697
+ e.message.startsWith('Credential verification failed'))
698
+ throw e;
699
+ console.log(' Could not read .fuel-credentials — prompting for credentials.');
700
+ }
701
+ }
702
+ if (!useExistingCreds) {
703
+ credentials.AZURE_SUBSCRIPTION_ID = await ask(rl, 'AZURE_SUBSCRIPTION_ID');
704
+ credentials.AZURE_TENANT_ID = await ask(rl, 'AZURE_TENANT_ID');
705
+ credentials.AZURE_CLIENT_ID = await ask(rl, 'AZURE_CLIENT_ID');
706
+ credentials.AZURE_CLIENT_SECRET = await ask(rl, 'AZURE_CLIENT_SECRET');
707
+ const gh = await askGitHubToken(rl);
708
+ credentials.GITHUB_TOKEN = gh.token;
709
+ credentials.GITHUB_USERNAME = await ask(rl, 'GITHUB_USERNAME');
710
+ credentials.GITHUB_ORGANIZATION = await askGitHubOrganizationFor(rl, gh.token);
711
+ console.log('\n Verifying credentials...');
712
+ const [azResult, ghUserResult, ghOrgResult] = await Promise.all([
713
+ tryGetAzureAccountInfo(credentials),
714
+ tryGetGithubUser(credentials),
715
+ tryGetGithubOrg(credentials)
716
+ ]);
717
+ if ('subscriptionId' in azResult) {
718
+ console.log(` Azure Subscription: ${chalk_1.default.green(azResult.subscriptionId)}`);
719
+ }
720
+ else {
721
+ console.log(' ' + chalk_1.default.yellow('⚠') + ' Azure credentials invalid:');
722
+ console.log(` ${azResult.error}`);
723
+ }
724
+ if ('login' in ghUserResult) {
725
+ console.log(` GitHub User: ${chalk_1.default.green(ghUserResult.login)}`);
726
+ }
727
+ else {
728
+ console.log(' ' + chalk_1.default.yellow('⚠') + ' GitHub token invalid:');
729
+ console.log(` ${ghUserResult.error}`);
730
+ }
731
+ if ('id' in ghOrgResult) {
732
+ console.log(` GitHub Org: ${chalk_1.default.green(ghOrgResult.login)} (ID: ${ghOrgResult.id})`);
733
+ }
734
+ else {
735
+ console.log(' ' + chalk_1.default.yellow('⚠') + ' Could not verify org:');
736
+ console.log(` ${ghOrgResult.error}`);
737
+ }
738
+ assertAzureCredentialResults(azResult, ghUserResult, ghOrgResult);
739
+ }
740
+ console.log('');
741
+ // --- Project ---
742
+ console.log(' Project');
743
+ console.log(' ────────────────────────────────────────────────────');
744
+ const projectName = await ask(rl, 'Project name', 'my-app');
745
+ const location = await ask(rl, 'Azure region', 'eastus2');
746
+ const slackWebhookUrl = await ask(rl, 'Slack webhook URL for deploy/alarm notifications (leave empty to skip)', '');
747
+ console.log('');
748
+ // --- App configuration ---
749
+ console.log(' App configuration');
750
+ console.log(' ────────────────────────────────────────────────────');
751
+ const appChoice = await askSelect(rl, 'Include which apps?', ['API only (NestJS)', 'Web only (React/Next.js)', 'API + Web'], 3);
752
+ const includeApi = appChoice !== 'Web only (React/Next.js)';
753
+ const includeWeb = appChoice !== 'API only (NestJS)';
754
+ const appTypes = [];
755
+ if (includeApi) {
756
+ console.log('\n API (NestJS):');
757
+ const dockerfile = await ask(rl, 'Dockerfile', 'apps/api/Dockerfile');
758
+ const port = await askInt(rl, 'Port', 3000);
759
+ const healthCheckPath = await ask(rl, 'Health check path', '/health');
760
+ const databaseEnabled = await askYesNo(rl, 'Include a database?', true);
761
+ let databaseEngine = 'mssql';
762
+ if (databaseEnabled) {
763
+ const engineChoice = await askSelect(rl, ' Database engine', ['Azure SQL Database (mssql)', 'PostgreSQL Flexible Server (postgres)'], 1);
764
+ databaseEngine = engineChoice.includes('mssql') ? 'mssql' : 'postgres';
765
+ }
766
+ const redisEnabled = await askYesNo(rl, 'Include Redis?', false);
767
+ appTypes.push({
768
+ suffix: 'api',
769
+ dockerfile,
770
+ port,
771
+ healthCheckPath,
772
+ databaseEnabled,
773
+ databaseEngine,
774
+ redisEnabled,
775
+ variables: {}
776
+ });
777
+ }
778
+ if (includeWeb) {
779
+ console.log('\n Web (React/Next.js):');
780
+ const dockerfile = await ask(rl, 'Dockerfile', 'apps/web/Dockerfile');
781
+ const port = await askInt(rl, 'Port', 3000);
782
+ const healthCheckPath = await ask(rl, 'Health check path', '/health');
783
+ appTypes.push({
784
+ suffix: 'web',
785
+ dockerfile,
786
+ port,
787
+ healthCheckPath,
788
+ databaseEnabled: false,
789
+ databaseEngine: 'mssql',
790
+ redisEnabled: false,
791
+ variables: {}
792
+ });
793
+ }
794
+ console.log('');
795
+ // --- Environments ---
796
+ console.log(' Environments');
797
+ console.log(' ────────────────────────────────────────────────────');
798
+ const envPresets = {
799
+ 'production only': ['production'],
800
+ 'dev + production': ['dev', 'production'],
801
+ 'dev + staging + production': ['dev', 'staging', 'production']
802
+ };
803
+ const envChoice = await askSelect(rl, 'Environments', Object.keys(envPresets), 3);
804
+ const environments = envPresets[envChoice];
805
+ console.log('');
806
+ // --- Base domain + per-app domains ---
807
+ console.log(' Domains');
808
+ console.log(' ────────────────────────────────────────────────────');
809
+ const baseDomain = await ask(rl, 'Base domain');
810
+ console.log('');
811
+ const domains = {};
812
+ for (const appType of appTypes) {
813
+ domains[appType.suffix] = {};
814
+ for (const env of environments) {
815
+ const suggestion = baseDomain
816
+ ? defaultDomain(projectName, appType.suffix, env, baseDomain)
817
+ : '';
818
+ const appFullName = `${projectName}-${appType.suffix}-${env}`;
819
+ domains[appType.suffix][env] = await ask(rl, appFullName, suggestion || undefined);
820
+ }
821
+ }
822
+ console.log('');
823
+ // --- Environment variables (per app × per environment) ---
824
+ console.log(' Environment variables');
825
+ console.log(' ────────────────────────────────────────────────────');
826
+ for (const appType of appTypes) {
827
+ const isApi = appType.suffix === 'api';
828
+ for (const env of environments) {
829
+ const label = isApi
830
+ ? `${projectName}-api-${env} runtime variables (e.g. TWILIO_TOKEN=xxx)`
831
+ : `${projectName}-web-${env} build variables (NEXT_PUBLIC_API_HOST and NEXT_PUBLIC_APP_HOST are already included)`;
832
+ appType.variables[env] = await askKeyValues(rl, label);
833
+ }
834
+ }
835
+ console.log('');
836
+ return {
837
+ credentials,
838
+ projectName,
839
+ slackWebhookUrl,
840
+ location,
841
+ appTypes,
842
+ environments,
843
+ baseDomain,
844
+ domains
845
+ };
464
846
  }
465
847
  // --- applyInfraInitAnswers ---
466
848
  async function applyInfraInitAnswers(answers, opts) {
@@ -504,7 +886,7 @@ async function applyInfraInitAnswers(answers, opts) {
504
886
  memory_units: appType.memoryUnits,
505
887
  variables: {},
506
888
  build_env: {},
507
- workers: [],
889
+ workers: []
508
890
  };
509
891
  if (isApi) {
510
892
  app.postgres = appType.postgres;
@@ -537,10 +919,12 @@ async function applyInfraInitAnswers(answers, opts) {
537
919
  deployment: 'ecs',
538
920
  ssl: answers.ssl,
539
921
  slack_webhook_url: answers.slackWebhookUrl || '',
540
- apps,
922
+ apps
541
923
  };
542
924
  // 4. Guard against overwriting existing config
543
- const outputPath = opts.output ? path.resolve(opts.output) : path.resolve('fuel.yml');
925
+ const outputPath = opts.output
926
+ ? path.resolve(opts.output)
927
+ : path.resolve('fuel.yml');
544
928
  if (await fs.pathExists(outputPath)) {
545
929
  throw new Error(`${outputPath} already exists. Remove it first or use --output <path>.`);
546
930
  }
@@ -555,96 +939,230 @@ async function applyInfraInitAnswers(answers, opts) {
555
939
  console.log(' Review and edit fuel.yml to add workers/variables, then:');
556
940
  console.log(` fuel create:app ${answers.projectName} --config fuel.yml\n`);
557
941
  }
558
- // --- infraInit ---
559
- async function infraInit(opts) {
560
- if (opts.credentialsOnly) {
561
- const rl = (0, promises_1.createInterface)({ input: process.stdin, output: process.stdout });
562
- try {
563
- console.log('\n Credentials');
564
- console.log(' ────────────────────────────────────────────────────');
565
- const credentials = {};
566
- const credFilePath = path.resolve('.fuel-credentials');
567
- if (await fs.pathExists(credFilePath)) {
568
- try {
569
- const existing = await fs.readJson(credFilePath);
570
- console.log('\n Found existing .fuel-credentials');
571
- const [stsResult, ghUserResult, ghOrgResult] = await Promise.all([
572
- tryGetAwsAccountId(existing),
573
- tryGetGithubUser(existing),
574
- tryGetGithubOrg(existing),
575
- ]);
576
- if ('accountId' in stsResult) {
577
- console.log(` AWS Account: ${chalk_1.default.green(stsResult.accountId)}`);
578
- }
579
- if ('login' in ghUserResult) {
580
- console.log(` GitHub User: ${chalk_1.default.green(ghUserResult.login)}`);
581
- }
582
- if ('login' in ghOrgResult) {
583
- console.log(` GitHub Org: ${chalk_1.default.green(ghOrgResult.login)}`);
584
- }
585
- assertCredentialResults(stsResult, ghUserResult, ghOrgResult);
586
- console.log('');
587
- const useExisting = await askYesNo(rl, 'Use these credentials?', true);
588
- if (useExisting) {
589
- console.log(chalk_1.default.green('\n ✓ Existing credentials verified.\n'));
590
- return;
591
- }
592
- }
593
- catch (e) {
594
- if (e instanceof Error && e.message.startsWith('Credential verification failed'))
595
- throw e;
596
- console.log(' Could not read .fuel-credentials — prompting for credentials.');
597
- }
598
- }
599
- credentials.AWS_ACCESS_KEY_ID = await ask(rl, 'AWS_ACCESS_KEY_ID');
600
- credentials.AWS_SECRET_ACCESS_KEY = await ask(rl, 'AWS_SECRET_ACCESS_KEY');
601
- credentials.AWS_REGION = await ask(rl, 'AWS_REGION', 'us-east-1');
602
- credentials.GITHUB_TOKEN = await ask(rl, 'GITHUB_TOKEN');
603
- credentials.GITHUB_USERNAME = await ask(rl, 'GITHUB_USERNAME');
604
- credentials.GITHUB_ORGANIZATION = await ask(rl, 'GITHUB_ORGANIZATION');
605
- console.log('\n Verifying credentials...');
606
- const [stsResult, ghUserResult, ghOrgResult] = await Promise.all([
607
- tryGetAwsAccountId(credentials),
608
- tryGetGithubUser(credentials),
609
- tryGetGithubOrg(credentials),
610
- ]);
611
- if ('accountId' in stsResult) {
612
- console.log(` AWS Account: ${chalk_1.default.green(stsResult.accountId)}`);
613
- }
614
- if ('login' in ghUserResult) {
615
- console.log(` GitHub User: ${chalk_1.default.green(ghUserResult.login)}`);
942
+ // --- applyAzureInfraInitAnswers ---
943
+ async function applyAzureInfraInitAnswers(answers, opts) {
944
+ // 1. Write .fuel-credentials
945
+ const credPath = path.resolve('.fuel-credentials');
946
+ await fs.writeFile(credPath, JSON.stringify(answers.credentials, null, 2));
947
+ console.log(chalk_1.default.green('✓') + ' Credentials written to .fuel-credentials');
948
+ // 2. Ensure .gitignore contains .fuel-credentials
949
+ const gitignorePath = path.resolve('.gitignore');
950
+ const CRED_LINE = '.fuel-credentials';
951
+ if (await fs.pathExists(gitignorePath)) {
952
+ const content = await fs.readFile(gitignorePath, 'utf-8');
953
+ if (!content.split('\n').some((l) => l.trim() === CRED_LINE)) {
954
+ await fs.appendFile(gitignorePath, `\n${CRED_LINE}\n`);
955
+ console.log(chalk_1.default.green('✓') + ' .fuel-credentials added to .gitignore');
956
+ }
957
+ else {
958
+ console.log(' .fuel-credentials already in .gitignore');
959
+ }
960
+ }
961
+ else {
962
+ await fs.writeFile(gitignorePath, `${CRED_LINE}\n`);
963
+ console.log(chalk_1.default.green('✓') + ' .fuel-credentials added to .gitignore');
964
+ }
965
+ // 3. Build config (cartesian product: appTypes × environments)
966
+ const apps = [];
967
+ for (const appType of answers.appTypes) {
968
+ const isApi = appType.suffix === 'api';
969
+ const technology = isApi ? 'nest' : 'react';
970
+ for (const env of answers.environments) {
971
+ const branch = env === 'production' ? 'main' : env;
972
+ const app = {
973
+ name: `${answers.projectName}-${appType.suffix}-${env}`,
974
+ technology,
975
+ dockerfile: appType.dockerfile,
976
+ domain: answers.domains[appType.suffix][env],
977
+ port: appType.port,
978
+ health_check_path: appType.healthCheckPath,
979
+ branch,
980
+ variables: {},
981
+ build_env: {},
982
+ workers: []
983
+ };
984
+ if (isApi) {
985
+ app.variables = appType.variables[env] ?? {};
986
+ app.database = {
987
+ enabled: appType.databaseEnabled,
988
+ engine: appType.databaseEngine
989
+ };
990
+ app.redis = { enabled: appType.redisEnabled };
616
991
  }
617
- if ('login' in ghOrgResult) {
618
- console.log(` GitHub Org: ${chalk_1.default.green(ghOrgResult.login)}`);
992
+ else {
993
+ // Auto-populate build_env from domains; user-provided vars override
994
+ const autoBuildEnv = {};
995
+ const webDomain = answers.domains['web']?.[env];
996
+ if (webDomain)
997
+ autoBuildEnv['NEXT_PUBLIC_APP_HOST'] = `https://${webDomain}`;
998
+ const apiDomain = answers.domains['api']?.[env];
999
+ if (apiDomain)
1000
+ autoBuildEnv['NEXT_PUBLIC_API_HOST'] = `https://${apiDomain}`;
1001
+ app.build_env = { ...autoBuildEnv, ...(appType.variables[env] ?? {}) };
1002
+ app.database = {
1003
+ enabled: false,
1004
+ engine: azure_defaults_1.AZURE_DEFAULTS.database.engine
1005
+ };
1006
+ app.redis = { enabled: false };
619
1007
  }
620
- assertCredentialResults(stsResult, ghUserResult, ghOrgResult);
621
- await fs.writeFile(credFilePath, JSON.stringify(credentials, null, 2));
622
- console.log(chalk_1.default.green('\n ✓') + ' Credentials written to .fuel-credentials');
623
- const gitignorePath = path.resolve('.gitignore');
624
- const CRED_LINE = '.fuel-credentials';
625
- if (await fs.pathExists(gitignorePath)) {
626
- const content = await fs.readFile(gitignorePath, 'utf-8');
627
- if (!content.split('\n').some((l) => l.trim() === CRED_LINE)) {
628
- await fs.appendFile(gitignorePath, `\n${CRED_LINE}\n`);
629
- }
1008
+ app.container_app = {
1009
+ cpu: azure_defaults_1.AZURE_DEFAULTS.containerApp.cpu,
1010
+ memory: azure_defaults_1.AZURE_DEFAULTS.containerApp.memory,
1011
+ concurrent_requests: azure_defaults_1.AZURE_DEFAULTS.containerApp.concurrentRequests,
1012
+ min_replicas: azure_defaults_1.AZURE_DEFAULTS.containerApp.minReplicas,
1013
+ max_replicas: azure_defaults_1.AZURE_DEFAULTS.containerApp.maxReplicas
1014
+ };
1015
+ apps.push(app);
1016
+ }
1017
+ }
1018
+ const config = {
1019
+ name: answers.projectName,
1020
+ deployment: 'azure',
1021
+ location: answers.location,
1022
+ slack_webhook_url: answers.slackWebhookUrl || '',
1023
+ apps
1024
+ };
1025
+ // 4. Guard against overwriting existing config
1026
+ const outputPath = opts.output
1027
+ ? path.resolve(opts.output)
1028
+ : path.resolve('fuel.yml');
1029
+ if (await fs.pathExists(outputPath)) {
1030
+ throw new Error(`${outputPath} already exists. Remove it first or use --output <path>.`);
1031
+ }
1032
+ // 5. Write fuel.yml
1033
+ const schemaComment = '# yaml-language-server: $schema=node_modules/@swovohq/fuel/schema/fuel.schema.json\n';
1034
+ await fs.writeFile(outputPath, schemaComment + YAML.stringify(config));
1035
+ const totalApps = answers.appTypes.length * answers.environments.length;
1036
+ console.log(chalk_1.default.green('✓') +
1037
+ ` fuel.yml written — ${totalApps} apps (${answers.appTypes.length} types × ${answers.environments.length} environments)`);
1038
+ // 6. Next steps
1039
+ console.log('\n Next steps:');
1040
+ console.log(' Review and edit fuel.yml to add workers/variables, then:');
1041
+ console.log(` fuel create:app ${answers.projectName} --config fuel.yml\n`);
1042
+ }
1043
+ // --- infraInit ---
1044
+ /** Interactive credentials-only flow (used by `auth:init` and `infra:init --credentials-only`) for the given cloud. */
1045
+ async function collectCredentialsOnly(rl, cloud) {
1046
+ console.log('\n Credentials');
1047
+ console.log(' ────────────────────────────────────────────────────');
1048
+ const credentials = {};
1049
+ const credFilePath = path.resolve('.fuel-credentials');
1050
+ if (await fs.pathExists(credFilePath)) {
1051
+ try {
1052
+ const existing = (await fs.readJson(credFilePath));
1053
+ console.log('\n Found existing .fuel-credentials');
1054
+ if (cloud === 'aws') {
1055
+ const [stsResult, ghUserResult, ghOrgResult] = await Promise.all([
1056
+ tryGetAwsAccountId(existing),
1057
+ tryGetGithubUser(existing),
1058
+ tryGetGithubOrg(existing)
1059
+ ]);
1060
+ if ('accountId' in stsResult)
1061
+ console.log(` AWS Account: ${chalk_1.default.green(stsResult.accountId)}`);
1062
+ if ('login' in ghUserResult)
1063
+ console.log(` GitHub User: ${chalk_1.default.green(ghUserResult.login)}`);
1064
+ if ('login' in ghOrgResult)
1065
+ console.log(` GitHub Org: ${chalk_1.default.green(ghOrgResult.login)}`);
1066
+ assertCredentialResults(stsResult, ghUserResult, ghOrgResult);
630
1067
  }
631
1068
  else {
632
- await fs.writeFile(gitignorePath, `${CRED_LINE}\n`);
1069
+ const [azResult, ghUserResult, ghOrgResult] = await Promise.all([
1070
+ tryGetAzureAccountInfo(existing),
1071
+ tryGetGithubUser(existing),
1072
+ tryGetGithubOrg(existing)
1073
+ ]);
1074
+ if ('subscriptionId' in azResult)
1075
+ console.log(` Azure Subscription: ${chalk_1.default.green(azResult.subscriptionId)}`);
1076
+ if ('login' in ghUserResult)
1077
+ console.log(` GitHub User: ${chalk_1.default.green(ghUserResult.login)}`);
1078
+ if ('login' in ghOrgResult)
1079
+ console.log(` GitHub Org: ${chalk_1.default.green(ghOrgResult.login)}`);
1080
+ assertAzureCredentialResults(azResult, ghUserResult, ghOrgResult);
633
1081
  }
634
1082
  console.log('');
1083
+ const useExisting = await askYesNo(rl, 'Use these credentials?', true);
1084
+ if (useExisting) {
1085
+ console.log(chalk_1.default.green('\n ✓ Existing credentials verified.\n'));
1086
+ return;
1087
+ }
635
1088
  }
636
- finally {
637
- rl.close();
1089
+ catch (e) {
1090
+ if (e instanceof Error &&
1091
+ e.message.startsWith('Credential verification failed'))
1092
+ throw e;
1093
+ console.log(' Could not read .fuel-credentials — prompting for credentials.');
638
1094
  }
639
- return;
640
1095
  }
1096
+ if (cloud === 'aws') {
1097
+ credentials.AWS_ACCESS_KEY_ID = await ask(rl, 'AWS_ACCESS_KEY_ID');
1098
+ credentials.AWS_SECRET_ACCESS_KEY = await ask(rl, 'AWS_SECRET_ACCESS_KEY');
1099
+ credentials.AWS_REGION = await ask(rl, 'AWS_REGION', 'us-east-1');
1100
+ }
1101
+ else {
1102
+ credentials.AZURE_SUBSCRIPTION_ID = await ask(rl, 'AZURE_SUBSCRIPTION_ID');
1103
+ credentials.AZURE_TENANT_ID = await ask(rl, 'AZURE_TENANT_ID');
1104
+ credentials.AZURE_CLIENT_ID = await ask(rl, 'AZURE_CLIENT_ID');
1105
+ credentials.AZURE_CLIENT_SECRET = await ask(rl, 'AZURE_CLIENT_SECRET');
1106
+ }
1107
+ const gh = await askGitHubToken(rl);
1108
+ credentials.GITHUB_TOKEN = gh.token;
1109
+ credentials.GITHUB_USERNAME = await ask(rl, 'GITHUB_USERNAME');
1110
+ credentials.GITHUB_ORGANIZATION = await askGitHubOrganizationFor(rl, gh.token);
1111
+ console.log('\n Verifying credentials...');
1112
+ if (cloud === 'aws') {
1113
+ const [stsResult, ghUserResult, ghOrgResult] = await Promise.all([
1114
+ tryGetAwsAccountId(credentials),
1115
+ tryGetGithubUser(credentials),
1116
+ tryGetGithubOrg(credentials)
1117
+ ]);
1118
+ if ('accountId' in stsResult)
1119
+ console.log(` AWS Account: ${chalk_1.default.green(stsResult.accountId)}`);
1120
+ if ('login' in ghUserResult)
1121
+ console.log(` GitHub User: ${chalk_1.default.green(ghUserResult.login)}`);
1122
+ if ('login' in ghOrgResult)
1123
+ console.log(` GitHub Org: ${chalk_1.default.green(ghOrgResult.login)}`);
1124
+ assertCredentialResults(stsResult, ghUserResult, ghOrgResult);
1125
+ }
1126
+ else {
1127
+ const [azResult, ghUserResult, ghOrgResult] = await Promise.all([
1128
+ tryGetAzureAccountInfo(credentials),
1129
+ tryGetGithubUser(credentials),
1130
+ tryGetGithubOrg(credentials)
1131
+ ]);
1132
+ if ('subscriptionId' in azResult)
1133
+ console.log(` Azure Subscription: ${chalk_1.default.green(azResult.subscriptionId)}`);
1134
+ if ('login' in ghUserResult)
1135
+ console.log(` GitHub User: ${chalk_1.default.green(ghUserResult.login)}`);
1136
+ if ('login' in ghOrgResult)
1137
+ console.log(` GitHub Org: ${chalk_1.default.green(ghOrgResult.login)}`);
1138
+ assertAzureCredentialResults(azResult, ghUserResult, ghOrgResult);
1139
+ }
1140
+ await fs.writeFile(credFilePath, JSON.stringify(credentials, null, 2));
1141
+ console.log(chalk_1.default.green('\n ✓') + ' Credentials written to .fuel-credentials');
1142
+ const gitignorePath = path.resolve('.gitignore');
1143
+ const CRED_LINE = '.fuel-credentials';
1144
+ if (await fs.pathExists(gitignorePath)) {
1145
+ const content = await fs.readFile(gitignorePath, 'utf-8');
1146
+ if (!content.split('\n').some((l) => l.trim() === CRED_LINE)) {
1147
+ await fs.appendFile(gitignorePath, `\n${CRED_LINE}\n`);
1148
+ }
1149
+ }
1150
+ else {
1151
+ await fs.writeFile(gitignorePath, `${CRED_LINE}\n`);
1152
+ }
1153
+ console.log('');
1154
+ }
1155
+ async function infraInit(opts) {
641
1156
  if (opts.template) {
642
- const outputPath = opts.output ? path.resolve(opts.output) : path.resolve('fuel.yml');
1157
+ const outputPath = opts.output
1158
+ ? path.resolve(opts.output)
1159
+ : path.resolve('fuel.yml');
643
1160
  if (await fs.pathExists(outputPath)) {
644
1161
  throw new Error(`${outputPath} already exists. Remove it first or use --output <path>.`);
645
1162
  }
646
1163
  const schemaComment = '# yaml-language-server: $schema=node_modules/@swovohq/fuel/schema/fuel.schema.json\n';
647
- await fs.writeFile(outputPath, schemaComment + YAML.stringify(exports.CONFIG_TEMPLATE));
1164
+ const template = opts.azure ? exports.CONFIG_TEMPLATE_AZURE : exports.CONFIG_TEMPLATE;
1165
+ await fs.writeFile(outputPath, schemaComment + YAML.stringify(template));
648
1166
  console.log(chalk_1.default.green('✓') + ` Config template written to ${outputPath}`);
649
1167
  console.log('\nEdit the file with your project details, then run:\n' +
650
1168
  ' fuel create:app my-app --config fuel.yml\n');
@@ -652,8 +1170,21 @@ async function infraInit(opts) {
652
1170
  }
653
1171
  const rl = (0, promises_1.createInterface)({ input: process.stdin, output: process.stdout });
654
1172
  try {
655
- const answers = await collectAnswers(rl);
656
- await applyInfraInitAnswers(answers, opts);
1173
+ const cloud = opts.azure
1174
+ ? 'azure'
1175
+ : await askCloudChoice(rl);
1176
+ if (opts.credentialsOnly) {
1177
+ await collectCredentialsOnly(rl, cloud);
1178
+ return;
1179
+ }
1180
+ if (cloud === 'azure') {
1181
+ const answers = await collectAzureAnswers(rl);
1182
+ await applyAzureInfraInitAnswers(answers, opts);
1183
+ }
1184
+ else {
1185
+ const answers = await collectAnswers(rl);
1186
+ await applyInfraInitAnswers(answers, opts);
1187
+ }
657
1188
  }
658
1189
  finally {
659
1190
  rl.close();