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

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 (201) hide show
  1. package/README.md +108 -4
  2. package/dist/bin/fuel.js +76 -30
  3. package/dist/bin/fuel.js.map +1 -1
  4. package/dist/commands/config-verify.d.ts.map +1 -1
  5. package/dist/commands/config-verify.js +14 -6
  6. package/dist/commands/config-verify.js.map +1 -1
  7. package/dist/commands/create-app.d.ts.map +1 -1
  8. package/dist/commands/create-app.js +36 -4
  9. package/dist/commands/create-app.js.map +1 -1
  10. package/dist/commands/infra-deploy.d.ts +15 -1
  11. package/dist/commands/infra-deploy.d.ts.map +1 -1
  12. package/dist/commands/infra-deploy.js +144 -15
  13. package/dist/commands/infra-deploy.js.map +1 -1
  14. package/dist/commands/infra-destroy.js +3 -3
  15. package/dist/commands/infra-destroy.js.map +1 -1
  16. package/dist/commands/infra-init.d.ts +91 -0
  17. package/dist/commands/infra-init.d.ts.map +1 -1
  18. package/dist/commands/infra-init.js +510 -80
  19. package/dist/commands/infra-init.js.map +1 -1
  20. package/dist/commands/infra-pull.d.ts +8 -0
  21. package/dist/commands/infra-pull.d.ts.map +1 -0
  22. package/dist/commands/infra-pull.js +80 -0
  23. package/dist/commands/infra-pull.js.map +1 -0
  24. package/dist/engines/convention.d.ts +2 -2
  25. package/dist/engines/convention.d.ts.map +1 -1
  26. package/dist/engines/convention.js +6 -4
  27. package/dist/engines/convention.js.map +1 -1
  28. package/dist/engines/dependencies.d.ts +2 -0
  29. package/dist/engines/dependencies.d.ts.map +1 -1
  30. package/dist/engines/dependencies.js.map +1 -1
  31. package/dist/infra/azure-defaults.d.ts +49 -0
  32. package/dist/infra/azure-defaults.d.ts.map +1 -0
  33. package/dist/infra/azure-defaults.js +52 -0
  34. package/dist/infra/azure-defaults.js.map +1 -0
  35. package/dist/infra/azure-keyvault-client.d.ts +4 -0
  36. package/dist/infra/azure-keyvault-client.d.ts.map +1 -0
  37. package/dist/infra/azure-keyvault-client.js +57 -0
  38. package/dist/infra/azure-keyvault-client.js.map +1 -0
  39. package/dist/infra/azure-rbac.d.ts +11 -0
  40. package/dist/infra/azure-rbac.d.ts.map +1 -0
  41. package/dist/infra/azure-rbac.js +47 -0
  42. package/dist/infra/azure-rbac.js.map +1 -0
  43. package/dist/infra/azure-state-storage.d.ts +20 -0
  44. package/dist/infra/azure-state-storage.d.ts.map +1 -0
  45. package/dist/infra/azure-state-storage.js +229 -0
  46. package/dist/infra/azure-state-storage.js.map +1 -0
  47. package/dist/infra/credentials.d.ts +2 -2
  48. package/dist/infra/credentials.d.ts.map +1 -1
  49. package/dist/infra/credentials.js +8 -4
  50. package/dist/infra/credentials.js.map +1 -1
  51. package/dist/infra/deployment-detect.d.ts +9 -0
  52. package/dist/infra/deployment-detect.d.ts.map +1 -0
  53. package/dist/infra/deployment-detect.js +53 -0
  54. package/dist/infra/deployment-detect.js.map +1 -0
  55. package/dist/infra/git-client.d.ts.map +1 -1
  56. package/dist/infra/git-client.js +6 -10
  57. package/dist/infra/git-client.js.map +1 -1
  58. package/dist/infra/github-client.d.ts +7 -0
  59. package/dist/infra/github-client.d.ts.map +1 -1
  60. package/dist/infra/github-client.js +44 -4
  61. package/dist/infra/github-client.js.map +1 -1
  62. package/dist/infra/github-token-link.d.ts +4 -0
  63. package/dist/infra/github-token-link.d.ts.map +1 -0
  64. package/dist/infra/github-token-link.js +28 -0
  65. package/dist/infra/github-token-link.js.map +1 -0
  66. package/dist/infra/orchestrator.d.ts +4 -1
  67. package/dist/infra/orchestrator.d.ts.map +1 -1
  68. package/dist/infra/orchestrator.js +209 -59
  69. package/dist/infra/orchestrator.js.map +1 -1
  70. package/dist/infra/preflight.d.ts.map +1 -1
  71. package/dist/infra/preflight.js +17 -9
  72. package/dist/infra/preflight.js.map +1 -1
  73. package/dist/infra/remote-secret.d.ts +14 -0
  74. package/dist/infra/remote-secret.d.ts.map +1 -0
  75. package/dist/infra/remote-secret.js +42 -0
  76. package/dist/infra/remote-secret.js.map +1 -0
  77. package/dist/infra/template-engine.d.ts +14 -5
  78. package/dist/infra/template-engine.d.ts.map +1 -1
  79. package/dist/infra/template-engine.js +145 -39
  80. package/dist/infra/template-engine.js.map +1 -1
  81. package/dist/infra/tofu-runner.d.ts +4 -3
  82. package/dist/infra/tofu-runner.d.ts.map +1 -1
  83. package/dist/infra/tofu-runner.js +36 -27
  84. package/dist/infra/tofu-runner.js.map +1 -1
  85. package/dist/infra/types.d.ts +240 -16
  86. package/dist/infra/types.d.ts.map +1 -1
  87. package/dist/infra/types.js +207 -18
  88. package/dist/infra/types.js.map +1 -1
  89. package/dist/infra-templates/TODO-azure-prod.md +119 -0
  90. package/dist/infra-templates/modules/azure-container-app/main.tf +124 -0
  91. package/dist/infra-templates/modules/azure-container-app/outputs.tf +24 -0
  92. package/dist/infra-templates/modules/azure-container-app/variables.tf +80 -0
  93. package/dist/infra-templates/modules/azure-container-apps-environment/main.tf +18 -0
  94. package/dist/infra-templates/modules/azure-container-apps-environment/outputs.tf +14 -0
  95. package/dist/infra-templates/modules/azure-container-apps-environment/variables.tf +24 -0
  96. package/dist/infra-templates/modules/azure-container-registry/main.tf +10 -0
  97. package/dist/infra-templates/modules/azure-container-registry/outputs.tf +14 -0
  98. package/dist/infra-templates/modules/azure-container-registry/variables.tf +20 -0
  99. package/dist/infra-templates/modules/azure-front-door/main.tf +80 -0
  100. package/dist/infra-templates/modules/azure-front-door/outputs.tf +20 -0
  101. package/dist/infra-templates/modules/azure-front-door/variables.tf +26 -0
  102. package/dist/infra-templates/modules/azure-github-oidc/main.tf +48 -0
  103. package/dist/infra-templates/modules/azure-github-oidc/outputs.tf +9 -0
  104. package/dist/infra-templates/modules/azure-github-oidc/variables.tf +69 -0
  105. package/dist/infra-templates/modules/azure-key-vault/main.tf +21 -0
  106. package/dist/infra-templates/modules/azure-key-vault/outputs.tf +11 -0
  107. package/dist/infra-templates/modules/azure-key-vault/variables.tf +43 -0
  108. package/dist/infra-templates/modules/azure-monitor/main.tf +29 -0
  109. package/dist/infra-templates/modules/azure-monitor/outputs.tf +17 -0
  110. package/dist/infra-templates/modules/azure-monitor/variables.tf +32 -0
  111. package/dist/infra-templates/modules/azure-network/main.tf +134 -0
  112. package/dist/infra-templates/modules/azure-network/outputs.tf +44 -0
  113. package/dist/infra-templates/modules/azure-network/variables.tf +56 -0
  114. package/dist/infra-templates/modules/azure-postgresql-flexible/main.tf +34 -0
  115. package/dist/infra-templates/modules/azure-postgresql-flexible/outputs.tf +20 -0
  116. package/dist/infra-templates/modules/azure-postgresql-flexible/variables.tf +54 -0
  117. package/dist/infra-templates/modules/azure-redis-cache/main.tf +32 -0
  118. package/dist/infra-templates/modules/azure-redis-cache/outputs.tf +15 -0
  119. package/dist/infra-templates/modules/azure-redis-cache/variables.tf +30 -0
  120. package/dist/infra-templates/modules/azure-resource-group/main.tf +5 -0
  121. package/dist/infra-templates/modules/azure-resource-group/outputs.tf +11 -0
  122. package/dist/infra-templates/modules/azure-resource-group/variables.tf +16 -0
  123. package/dist/infra-templates/modules/azure-sql-database/main.tf +63 -0
  124. package/dist/infra-templates/modules/azure-sql-database/outputs.tf +20 -0
  125. package/dist/infra-templates/modules/azure-sql-database/variables.tf +78 -0
  126. package/dist/infra-templates/modules/azure-storage-account/main.tf +13 -0
  127. package/dist/infra-templates/modules/azure-storage-account/outputs.tf +16 -0
  128. package/dist/infra-templates/modules/azure-storage-account/variables.tf +38 -0
  129. package/dist/infra-templates/modules/cloud-watch/event-bridge.tf +15 -0
  130. package/dist/infra-templates/modules/cloud-watch/lambda.tf +84 -0
  131. package/dist/infra-templates/modules/cloud-watch/lambda_function.zip +0 -0
  132. package/dist/infra-templates/modules/cloud-watch/lambda_function_deploy.zip +0 -0
  133. package/dist/infra-templates/modules/cloud-watch/main.tf +156 -0
  134. package/dist/infra-templates/modules/cloud-watch/sns.tf +3 -0
  135. package/dist/infra-templates/modules/cloud-watch/variables.tf +38 -0
  136. package/dist/infra-templates/modules/ecs-core/alb.tf +126 -0
  137. package/dist/infra-templates/modules/ecs-core/cloudwatch.tf +48 -0
  138. package/dist/infra-templates/modules/ecs-core/iam.tf +95 -0
  139. package/dist/infra-templates/modules/ecs-core/main.tf +12 -0
  140. package/dist/infra-templates/modules/ecs-core/outputs.tf +44 -0
  141. package/dist/infra-templates/modules/ecs-core/secrets.tf +9 -0
  142. package/dist/infra-templates/modules/ecs-core/security_groups.tf +73 -0
  143. package/dist/infra-templates/modules/ecs-core/variables.tf +96 -0
  144. package/dist/infra-templates/modules/ecs-service/ecs_autoscaling.tf +51 -0
  145. package/dist/infra-templates/modules/ecs-service/ecs_service.tf +39 -0
  146. package/dist/infra-templates/modules/ecs-service/ecs_task_definition.tf +56 -0
  147. package/dist/infra-templates/modules/ecs-service/variables.tf +170 -0
  148. package/dist/infra-templates/modules/elastic-container-registry/main.tf +32 -0
  149. package/dist/infra-templates/modules/elastic-container-registry/outputs.tf +9 -0
  150. package/dist/infra-templates/modules/elastic-container-registry/variables.tf +25 -0
  151. package/dist/infra-templates/modules/github-oidc-role/main.tf +46 -0
  152. package/dist/infra-templates/modules/github-oidc-role/outputs.tf +4 -0
  153. package/dist/infra-templates/modules/github-oidc-role/variables.tf +11 -0
  154. package/dist/infra-templates/modules/guardduty/main.tf +8 -0
  155. package/dist/infra-templates/modules/iam/main.tf +205 -0
  156. package/dist/infra-templates/modules/iam/outputs.tf +3 -0
  157. package/dist/infra-templates/modules/iam/variables.tf +4 -0
  158. package/dist/infra-templates/modules/random-key/main.tf +11 -0
  159. package/dist/infra-templates/modules/random-key/outputs.tf +4 -0
  160. package/dist/infra-templates/modules/random-key/variables.tf +10 -0
  161. package/dist/infra-templates/modules/redis/main.tf +47 -0
  162. package/dist/infra-templates/modules/redis/outputs.tf +8 -0
  163. package/dist/infra-templates/modules/redis/variables.tf +20 -0
  164. package/dist/infra-templates/modules/relational-database-service/main.tf +44 -0
  165. package/dist/infra-templates/modules/relational-database-service/outputs.tf +13 -0
  166. package/dist/infra-templates/modules/relational-database-service/security-group.tf +15 -0
  167. package/dist/infra-templates/modules/relational-database-service/variables.tf +50 -0
  168. package/dist/infra-templates/modules/secret/main.tf +14 -0
  169. package/dist/infra-templates/modules/secret/outputs.tf +3 -0
  170. package/dist/infra-templates/modules/secret/variables.tf +8 -0
  171. package/dist/infra-templates/modules/simple-storage-service/main.tf +28 -0
  172. package/dist/infra-templates/modules/simple-storage-service/variables.tf +4 -0
  173. package/dist/infra-templates/modules/vpc/data.tf +2 -0
  174. package/dist/infra-templates/modules/vpc/main.tf +76 -0
  175. package/dist/infra-templates/modules/vpc/outputs.tf +23 -0
  176. package/dist/infra-templates/modules/vpc/variables.tf +5 -0
  177. package/dist/infra-templates/package.json +6 -0
  178. package/dist/infra-templates/templates/azure/MANUAL_DEPLOY.md +168 -0
  179. package/dist/infra-templates/templates/azure/backend.tf +8 -0
  180. package/dist/infra-templates/templates/azure/data.tf +21 -0
  181. package/dist/infra-templates/templates/azure/default.tfvars +0 -0
  182. package/dist/infra-templates/templates/azure/locals.tf +116 -0
  183. package/dist/infra-templates/templates/azure/main.tf +421 -0
  184. package/dist/infra-templates/templates/azure/outputs.tf +40 -0
  185. package/dist/infra-templates/templates/azure/providers.tf +65 -0
  186. package/dist/infra-templates/templates/azure/variables.tf +69 -0
  187. package/dist/infra-templates/templates/azure/workflows/build_and_deploy.yml +155 -0
  188. package/dist/infra-templates/templates/azure/workflows/tests.yml +63 -0
  189. package/dist/infra-templates/templates/ecs/README.md +225 -0
  190. package/dist/infra-templates/templates/ecs/backend.tf +7 -0
  191. package/dist/infra-templates/templates/ecs/data.tf +6 -0
  192. package/dist/infra-templates/templates/ecs/default.tfvars +1 -0
  193. package/dist/infra-templates/templates/ecs/locals.tf +97 -0
  194. package/dist/infra-templates/templates/ecs/main.tf +185 -0
  195. package/dist/infra-templates/templates/ecs/outputs.tf +42 -0
  196. package/dist/infra-templates/templates/ecs/providers.tf +21 -0
  197. package/dist/infra-templates/templates/ecs/variables.tf +105 -0
  198. package/dist/infra-templates/templates/ecs/workflows/build_and_deploy.yml +188 -0
  199. package/dist/infra-templates/templates/ecs/workflows/tests.yml +65 -0
  200. package/package.json +10 -2
  201. 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,6 +48,11 @@ 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) => {
@@ -98,6 +104,44 @@ exports.CONFIG_TEMPLATE = {
98
104
  },
99
105
  ],
100
106
  };
107
+ exports.CONFIG_TEMPLATE_AZURE = {
108
+ name: 'my-app',
109
+ deployment: 'azure',
110
+ slack_webhook_url: '',
111
+ location: 'eastus2',
112
+ monitor: { daily_data_cap_gb: 1, retention_days: 90, dashboard_count: 0 },
113
+ front_door: { sku_name: 'Standard_AzureFrontDoor' },
114
+ storage_account: { account_kind: 'BlockBlobStorage', account_tier: 'Premium', account_replication_type: 'LRS', access_tier: 'Hot' },
115
+ key_vault: { sku_name: 'standard' },
116
+ apps: [
117
+ {
118
+ name: 'my-app-api',
119
+ technology: 'nest',
120
+ dockerfile: 'apps/api/Dockerfile',
121
+ health_check_path: '/health',
122
+ domain: 'api.example.com',
123
+ port: 3000,
124
+ branch: 'main',
125
+ database: { enabled: true, engine: 'mssql', sku_name: 'GP_S_Gen5_4' },
126
+ redis: { enabled: false, sku_name: 'Balanced_B0' },
127
+ container_app: { cpu: 1, memory: '2Gi', concurrent_requests: 20, min_replicas: 0, max_replicas: 3 },
128
+ variables: {},
129
+ workers: [],
130
+ },
131
+ {
132
+ name: 'my-app-web',
133
+ technology: 'react',
134
+ dockerfile: 'apps/web/Dockerfile',
135
+ health_check_path: '/health',
136
+ domain: 'app.example.com',
137
+ port: 3000,
138
+ branch: 'main',
139
+ build_env: {
140
+ NEXT_PUBLIC_API_HOST: 'https://api.example.com',
141
+ },
142
+ },
143
+ ],
144
+ };
101
145
  // --- Private prompt helpers ---
102
146
  async function ask(rl, label, defaultVal) {
103
147
  const hint = defaultVal ? ` [${defaultVal}]` : '';
@@ -134,6 +178,14 @@ async function askSelect(rl, label, options, defaultIdx = 1) {
134
178
  console.log(` Please enter a number between 1 and ${options.length}.`);
135
179
  }
136
180
  }
181
+ /** Prompts which cloud provider to provision infrastructure for. */
182
+ async function askCloudChoice(rl) {
183
+ console.log(' Cloud provider');
184
+ console.log(' ────────────────────────────────────────────────────');
185
+ const choice = await askSelect(rl, 'Deploy to', ['AWS (ECS)', 'Azure (Container Apps)'], 1);
186
+ console.log('');
187
+ return choice === 'AWS (ECS)' ? 'aws' : 'azure';
188
+ }
137
189
  /** Prompts for KEY=VALUE pairs until the user submits an empty line. */
138
190
  async function askKeyValues(rl, label) {
139
191
  console.log(`\n ${label} (KEY=VALUE format, empty line to finish):`);
@@ -152,6 +204,58 @@ async function askKeyValues(rl, label) {
152
204
  }
153
205
  return result;
154
206
  }
207
+ /**
208
+ * Opens a GitHub token-creation page with the required scope already checked — the user
209
+ * never has to know or look up which permissions the CLI needs — then collects the pasted
210
+ * token and validates it immediately so a wrong/incomplete token surfaces right away instead
211
+ * of deep into a later step (repo creation, push, branch protection).
212
+ */
213
+ async function askGitHubToken(rl) {
214
+ const url = (0, github_token_link_1.buildGitHubTokenUrl)();
215
+ console.log(chalk_1.default.cyan('\n Create a GitHub Personal Access Token'));
216
+ console.log(` ${url}`);
217
+ console.log(' The "repo" scope is already selected — just click "Generate token" at the bottom of the page.\n');
218
+ (0, github_token_link_1.openInBrowser)(url);
219
+ while (true) {
220
+ const token = await ask(rl, 'Paste the generated token (GITHUB_TOKEN)');
221
+ try {
222
+ await (0, github_client_1.validateGitHubToken)(token);
223
+ console.log(chalk_1.default.green(' ✓') + ' Token has the required permissions');
224
+ return { token };
225
+ }
226
+ catch (err) {
227
+ console.log(chalk_1.default.yellow(` ⚠ ${err instanceof Error ? err.message : String(err)}`));
228
+ const retry = await askYesNo(rl, ' Try pasting the token again?', true);
229
+ if (!retry) {
230
+ throw new Error('GitHub token setup aborted — a validated token is required to continue.');
231
+ }
232
+ }
233
+ }
234
+ }
235
+ /** Lists the orgs available to the given token and lets the user pick one, falling back to manual entry. */
236
+ async function askGitHubOrganizationFor(rl, token) {
237
+ try {
238
+ const { status, body } = await httpsGetJson('https://api.github.com/user/orgs', {
239
+ Authorization: `Bearer ${token}`,
240
+ 'User-Agent': 'fuel-cli',
241
+ Accept: 'application/vnd.github+json',
242
+ });
243
+ if (status === 200) {
244
+ const orgs = JSON.parse(body);
245
+ if (orgs.length === 1) {
246
+ console.log(` GitHub Org: ${chalk_1.default.green(orgs[0].login)} (only org available to this account)`);
247
+ return orgs[0].login;
248
+ }
249
+ if (orgs.length > 1) {
250
+ return askSelect(rl, 'GitHub organization', orgs.map((o) => o.login), 1);
251
+ }
252
+ }
253
+ }
254
+ catch {
255
+ // fall through to manual entry
256
+ }
257
+ return ask(rl, 'GITHUB_ORGANIZATION');
258
+ }
155
259
  /** Calls GET /user on the GitHub API to verify the token. Returns login + email on success or `{ error: string }` on failure. */
156
260
  async function tryGetGithubUser(credentials) {
157
261
  try {
@@ -236,6 +340,19 @@ async function tryGetAcmCertificate(arn, credentials) {
236
340
  return { error: firstLine };
237
341
  }
238
342
  }
343
+ /** Verifies Azure credentials by acquiring a token and listing resource groups (cheap, works with just Reader access). */
344
+ async function tryGetAzureAccountInfo(credentials) {
345
+ try {
346
+ const credential = new identity_1.ClientSecretCredential(credentials.AZURE_TENANT_ID, credentials.AZURE_CLIENT_ID, credentials.AZURE_CLIENT_SECRET);
347
+ const client = new arm_resources_1.ResourceManagementClient(credential, credentials.AZURE_SUBSCRIPTION_ID);
348
+ const iterator = client.resourceGroups.list();
349
+ await iterator.next();
350
+ return { subscriptionId: credentials.AZURE_SUBSCRIPTION_ID };
351
+ }
352
+ catch (err) {
353
+ return { error: err instanceof Error ? err.message : String(err) };
354
+ }
355
+ }
239
356
  /** Displays credential verification results and throws if any check failed. */
240
357
  function assertCredentialResults(stsResult, ghUserResult, ghOrgResult) {
241
358
  const failures = [];
@@ -250,6 +367,20 @@ function assertCredentialResults(stsResult, ghUserResult, ghOrgResult) {
250
367
  failures.map((f) => ` • ${f}`).join('\n'));
251
368
  }
252
369
  }
370
+ /** Displays Azure credential verification results and throws if any check failed. */
371
+ function assertAzureCredentialResults(azResult, ghUserResult, ghOrgResult) {
372
+ const failures = [];
373
+ if ('error' in azResult)
374
+ failures.push(`Azure credentials: ${azResult.error}`);
375
+ if ('error' in ghUserResult)
376
+ failures.push(`GitHub token: ${ghUserResult.error}`);
377
+ if ('error' in ghOrgResult)
378
+ failures.push(`GitHub org: ${ghOrgResult.error}`);
379
+ if (failures.length > 0) {
380
+ throw new Error('Credential verification failed:\n' +
381
+ failures.map((f) => ` • ${f}`).join('\n'));
382
+ }
383
+ }
253
384
  /** Derives a sensible domain default: cliswovotest-api.swovo.com (production), cliswovotest-api-dev.swovo.com (others) */
254
385
  function defaultDomain(projectName, suffix, env, baseDomain) {
255
386
  const label = suffix === 'web' ? 'app' : suffix;
@@ -317,9 +448,10 @@ async function collectAnswers(rl) {
317
448
  credentials.AWS_ACCESS_KEY_ID = await ask(rl, 'AWS_ACCESS_KEY_ID');
318
449
  credentials.AWS_SECRET_ACCESS_KEY = await ask(rl, 'AWS_SECRET_ACCESS_KEY');
319
450
  credentials.AWS_REGION = await ask(rl, 'AWS_REGION', 'us-east-1');
320
- credentials.GITHUB_TOKEN = await ask(rl, 'GITHUB_TOKEN');
451
+ const gh = await askGitHubToken(rl);
452
+ credentials.GITHUB_TOKEN = gh.token;
321
453
  credentials.GITHUB_USERNAME = await ask(rl, 'GITHUB_USERNAME');
322
- credentials.GITHUB_ORGANIZATION = await ask(rl, 'GITHUB_ORGANIZATION');
454
+ credentials.GITHUB_ORGANIZATION = await askGitHubOrganizationFor(rl, gh.token);
323
455
  console.log('\n Verifying credentials...');
324
456
  const [stsResult, ghUserResult, ghOrgResult] = await Promise.all([
325
457
  tryGetAwsAccountId(credentials),
@@ -462,6 +594,178 @@ async function collectAnswers(rl) {
462
594
  console.log('');
463
595
  return { credentials, projectName, ssl, slackWebhookUrl, appTypes, environments, baseDomain, domains };
464
596
  }
597
+ // --- collectAzureAnswers ---
598
+ async function collectAzureAnswers(rl) {
599
+ // --- Credentials ---
600
+ console.log(' Credentials');
601
+ console.log(' ────────────────────────────────────────────────────');
602
+ let credentials = {};
603
+ let useExistingCreds = false;
604
+ const credFilePath = path.resolve('.fuel-credentials');
605
+ if (await fs.pathExists(credFilePath)) {
606
+ try {
607
+ const existing = await fs.readJson(credFilePath);
608
+ console.log('\n Found existing .fuel-credentials');
609
+ const [azResult, ghUserResult, ghOrgResult] = await Promise.all([
610
+ tryGetAzureAccountInfo(existing),
611
+ tryGetGithubUser(existing),
612
+ tryGetGithubOrg(existing),
613
+ ]);
614
+ if ('subscriptionId' in azResult) {
615
+ console.log(` Azure Subscription: ${chalk_1.default.green(azResult.subscriptionId)}`);
616
+ }
617
+ else {
618
+ console.log(' ' + chalk_1.default.yellow('⚠') + ' Azure credentials invalid:');
619
+ console.log(` ${azResult.error}`);
620
+ }
621
+ if ('login' in ghUserResult) {
622
+ console.log(` GitHub User: ${chalk_1.default.green(ghUserResult.login)}`);
623
+ }
624
+ else {
625
+ console.log(' ' + chalk_1.default.yellow('⚠') + ' GitHub token invalid:');
626
+ console.log(` ${ghUserResult.error}`);
627
+ }
628
+ if ('id' in ghOrgResult) {
629
+ console.log(` GitHub Org: ${chalk_1.default.green(ghOrgResult.login)} (ID: ${ghOrgResult.id})`);
630
+ }
631
+ else {
632
+ console.log(' ' + chalk_1.default.yellow('⚠') + ' Could not verify org:');
633
+ console.log(` ${ghOrgResult.error}`);
634
+ }
635
+ assertAzureCredentialResults(azResult, ghUserResult, ghOrgResult);
636
+ console.log('');
637
+ useExistingCreds = await askYesNo(rl, 'Use these credentials?', true);
638
+ if (useExistingCreds)
639
+ credentials = existing;
640
+ }
641
+ catch (e) {
642
+ if (e instanceof Error && e.message.startsWith('Credential verification failed'))
643
+ throw e;
644
+ console.log(' Could not read .fuel-credentials — prompting for credentials.');
645
+ }
646
+ }
647
+ if (!useExistingCreds) {
648
+ credentials.AZURE_SUBSCRIPTION_ID = await ask(rl, 'AZURE_SUBSCRIPTION_ID');
649
+ credentials.AZURE_TENANT_ID = await ask(rl, 'AZURE_TENANT_ID');
650
+ credentials.AZURE_CLIENT_ID = await ask(rl, 'AZURE_CLIENT_ID');
651
+ credentials.AZURE_CLIENT_SECRET = await ask(rl, 'AZURE_CLIENT_SECRET');
652
+ const gh = await askGitHubToken(rl);
653
+ credentials.GITHUB_TOKEN = gh.token;
654
+ credentials.GITHUB_USERNAME = await ask(rl, 'GITHUB_USERNAME');
655
+ credentials.GITHUB_ORGANIZATION = await askGitHubOrganizationFor(rl, gh.token);
656
+ console.log('\n Verifying credentials...');
657
+ const [azResult, ghUserResult, ghOrgResult] = await Promise.all([
658
+ tryGetAzureAccountInfo(credentials),
659
+ tryGetGithubUser(credentials),
660
+ tryGetGithubOrg(credentials),
661
+ ]);
662
+ if ('subscriptionId' in azResult) {
663
+ console.log(` Azure Subscription: ${chalk_1.default.green(azResult.subscriptionId)}`);
664
+ }
665
+ else {
666
+ console.log(' ' + chalk_1.default.yellow('⚠') + ' Azure credentials invalid:');
667
+ console.log(` ${azResult.error}`);
668
+ }
669
+ if ('login' in ghUserResult) {
670
+ console.log(` GitHub User: ${chalk_1.default.green(ghUserResult.login)}`);
671
+ }
672
+ else {
673
+ console.log(' ' + chalk_1.default.yellow('⚠') + ' GitHub token invalid:');
674
+ console.log(` ${ghUserResult.error}`);
675
+ }
676
+ if ('id' in ghOrgResult) {
677
+ console.log(` GitHub Org: ${chalk_1.default.green(ghOrgResult.login)} (ID: ${ghOrgResult.id})`);
678
+ }
679
+ else {
680
+ console.log(' ' + chalk_1.default.yellow('⚠') + ' Could not verify org:');
681
+ console.log(` ${ghOrgResult.error}`);
682
+ }
683
+ assertAzureCredentialResults(azResult, ghUserResult, ghOrgResult);
684
+ }
685
+ console.log('');
686
+ // --- Project ---
687
+ console.log(' Project');
688
+ console.log(' ────────────────────────────────────────────────────');
689
+ const projectName = await ask(rl, 'Project name', 'my-app');
690
+ const location = await ask(rl, 'Azure region', 'eastus2');
691
+ const slackWebhookUrl = await ask(rl, 'Slack webhook URL for deploy/alarm notifications (leave empty to skip)', '');
692
+ console.log('');
693
+ // --- App configuration ---
694
+ console.log(' App configuration');
695
+ console.log(' ────────────────────────────────────────────────────');
696
+ const appChoice = await askSelect(rl, 'Include which apps?', ['API only (NestJS)', 'Web only (React/Next.js)', 'API + Web'], 3);
697
+ const includeApi = appChoice !== 'Web only (React/Next.js)';
698
+ const includeWeb = appChoice !== 'API only (NestJS)';
699
+ const appTypes = [];
700
+ if (includeApi) {
701
+ console.log('\n API (NestJS):');
702
+ const dockerfile = await ask(rl, 'Dockerfile', 'apps/api/Dockerfile');
703
+ const port = await askInt(rl, 'Port', 3000);
704
+ const healthCheckPath = await ask(rl, 'Health check path', '/health');
705
+ const databaseEnabled = await askYesNo(rl, 'Include a database?', true);
706
+ let databaseEngine = 'mssql';
707
+ if (databaseEnabled) {
708
+ const engineChoice = await askSelect(rl, ' Database engine', ['Azure SQL Database (mssql)', 'PostgreSQL Flexible Server (postgres)'], 1);
709
+ databaseEngine = engineChoice.includes('mssql') ? 'mssql' : 'postgres';
710
+ }
711
+ const redisEnabled = await askYesNo(rl, 'Include Redis?', false);
712
+ appTypes.push({
713
+ suffix: 'api', dockerfile, port, healthCheckPath,
714
+ databaseEnabled, databaseEngine, redisEnabled, variables: {},
715
+ });
716
+ }
717
+ if (includeWeb) {
718
+ console.log('\n Web (React/Next.js):');
719
+ const dockerfile = await ask(rl, 'Dockerfile', 'apps/web/Dockerfile');
720
+ const port = await askInt(rl, 'Port', 3000);
721
+ const healthCheckPath = await ask(rl, 'Health check path', '/health');
722
+ appTypes.push({
723
+ suffix: 'web', dockerfile, port, healthCheckPath,
724
+ databaseEnabled: false, databaseEngine: 'mssql', redisEnabled: false, variables: {},
725
+ });
726
+ }
727
+ console.log('');
728
+ // --- Environments ---
729
+ console.log(' Environments');
730
+ console.log(' ────────────────────────────────────────────────────');
731
+ const envPresets = {
732
+ 'production only': ['production'],
733
+ 'dev + production': ['dev', 'production'],
734
+ 'dev + staging + production': ['dev', 'staging', 'production'],
735
+ };
736
+ const envChoice = await askSelect(rl, 'Environments', Object.keys(envPresets), 3);
737
+ const environments = envPresets[envChoice];
738
+ console.log('');
739
+ // --- Base domain + per-app domains ---
740
+ console.log(' Domains');
741
+ console.log(' ────────────────────────────────────────────────────');
742
+ const baseDomain = await ask(rl, 'Base domain');
743
+ console.log('');
744
+ const domains = {};
745
+ for (const appType of appTypes) {
746
+ domains[appType.suffix] = {};
747
+ for (const env of environments) {
748
+ const suggestion = baseDomain ? defaultDomain(projectName, appType.suffix, env, baseDomain) : '';
749
+ const appFullName = `${projectName}-${appType.suffix}-${env}`;
750
+ domains[appType.suffix][env] = await ask(rl, appFullName, suggestion || undefined);
751
+ }
752
+ }
753
+ console.log('');
754
+ // --- Environment variables (per app × per environment) ---
755
+ console.log(' Environment variables');
756
+ console.log(' ────────────────────────────────────────────────────');
757
+ for (const appType of appTypes) {
758
+ const isApi = appType.suffix === 'api';
759
+ for (const env of environments) {
760
+ const label = isApi
761
+ ? `${projectName}-api-${env} runtime variables (e.g. TWILIO_TOKEN=xxx)`
762
+ : `${projectName}-web-${env} build variables (NEXT_PUBLIC_API_HOST and NEXT_PUBLIC_APP_HOST are already included)`;
763
+ appType.variables[env] = await askKeyValues(rl, label);
764
+ }
765
+ }
766
+ console.log('');
767
+ return { credentials, projectName, slackWebhookUrl, location, appTypes, environments, baseDomain, domains };
768
+ }
465
769
  // --- applyInfraInitAnswers ---
466
770
  async function applyInfraInitAnswers(answers, opts) {
467
771
  // 1. Write .fuel-credentials
@@ -555,96 +859,211 @@ async function applyInfraInitAnswers(answers, opts) {
555
859
  console.log(' Review and edit fuel.yml to add workers/variables, then:');
556
860
  console.log(` fuel create:app ${answers.projectName} --config fuel.yml\n`);
557
861
  }
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)}`);
862
+ // --- applyAzureInfraInitAnswers ---
863
+ async function applyAzureInfraInitAnswers(answers, opts) {
864
+ // 1. Write .fuel-credentials
865
+ const credPath = path.resolve('.fuel-credentials');
866
+ await fs.writeFile(credPath, JSON.stringify(answers.credentials, null, 2));
867
+ console.log(chalk_1.default.green('✓') + ' Credentials written to .fuel-credentials');
868
+ // 2. Ensure .gitignore contains .fuel-credentials
869
+ const gitignorePath = path.resolve('.gitignore');
870
+ const CRED_LINE = '.fuel-credentials';
871
+ if (await fs.pathExists(gitignorePath)) {
872
+ const content = await fs.readFile(gitignorePath, 'utf-8');
873
+ if (!content.split('\n').some((l) => l.trim() === CRED_LINE)) {
874
+ await fs.appendFile(gitignorePath, `\n${CRED_LINE}\n`);
875
+ console.log(chalk_1.default.green('✓') + ' .fuel-credentials added to .gitignore');
876
+ }
877
+ else {
878
+ console.log(' .fuel-credentials already in .gitignore');
879
+ }
880
+ }
881
+ else {
882
+ await fs.writeFile(gitignorePath, `${CRED_LINE}\n`);
883
+ console.log(chalk_1.default.green('✓') + ' .fuel-credentials added to .gitignore');
884
+ }
885
+ // 3. Build config (cartesian product: appTypes × environments)
886
+ const apps = [];
887
+ for (const appType of answers.appTypes) {
888
+ const isApi = appType.suffix === 'api';
889
+ const technology = isApi ? 'nest' : 'react';
890
+ for (const env of answers.environments) {
891
+ const branch = env === 'production' ? 'main' : env;
892
+ const app = {
893
+ name: `${answers.projectName}-${appType.suffix}-${env}`,
894
+ technology,
895
+ dockerfile: appType.dockerfile,
896
+ domain: answers.domains[appType.suffix][env],
897
+ port: appType.port,
898
+ health_check_path: appType.healthCheckPath,
899
+ branch,
900
+ variables: {},
901
+ build_env: {},
902
+ workers: [],
903
+ };
904
+ if (isApi) {
905
+ app.variables = appType.variables[env] ?? {};
906
+ app.database = { enabled: appType.databaseEnabled, engine: appType.databaseEngine };
907
+ app.redis = { enabled: appType.redisEnabled };
616
908
  }
617
- if ('login' in ghOrgResult) {
618
- console.log(` GitHub Org: ${chalk_1.default.green(ghOrgResult.login)}`);
909
+ else {
910
+ // Auto-populate build_env from domains; user-provided vars override
911
+ const autoBuildEnv = {};
912
+ const webDomain = answers.domains['web']?.[env];
913
+ if (webDomain)
914
+ autoBuildEnv['NEXT_PUBLIC_APP_HOST'] = `https://${webDomain}`;
915
+ const apiDomain = answers.domains['api']?.[env];
916
+ if (apiDomain)
917
+ autoBuildEnv['NEXT_PUBLIC_API_HOST'] = `https://${apiDomain}`;
918
+ app.build_env = { ...autoBuildEnv, ...(appType.variables[env] ?? {}) };
919
+ app.database = { enabled: false, engine: azure_defaults_1.AZURE_DEFAULTS.database.engine };
920
+ app.redis = { enabled: false };
619
921
  }
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
- }
922
+ app.container_app = {
923
+ cpu: azure_defaults_1.AZURE_DEFAULTS.containerApp.cpu,
924
+ memory: azure_defaults_1.AZURE_DEFAULTS.containerApp.memory,
925
+ concurrent_requests: azure_defaults_1.AZURE_DEFAULTS.containerApp.concurrentRequests,
926
+ min_replicas: azure_defaults_1.AZURE_DEFAULTS.containerApp.minReplicas,
927
+ max_replicas: azure_defaults_1.AZURE_DEFAULTS.containerApp.maxReplicas,
928
+ };
929
+ apps.push(app);
930
+ }
931
+ }
932
+ const config = {
933
+ name: answers.projectName,
934
+ deployment: 'azure',
935
+ location: answers.location,
936
+ slack_webhook_url: answers.slackWebhookUrl || '',
937
+ apps,
938
+ };
939
+ // 4. Guard against overwriting existing config
940
+ const outputPath = opts.output ? path.resolve(opts.output) : path.resolve('fuel.yml');
941
+ if (await fs.pathExists(outputPath)) {
942
+ throw new Error(`${outputPath} already exists. Remove it first or use --output <path>.`);
943
+ }
944
+ // 5. Write fuel.yml
945
+ const schemaComment = '# yaml-language-server: $schema=node_modules/@swovohq/fuel/schema/fuel.schema.json\n';
946
+ await fs.writeFile(outputPath, schemaComment + YAML.stringify(config));
947
+ const totalApps = answers.appTypes.length * answers.environments.length;
948
+ console.log(chalk_1.default.green('✓') +
949
+ ` fuel.yml written — ${totalApps} apps (${answers.appTypes.length} types × ${answers.environments.length} environments)`);
950
+ // 6. Next steps
951
+ console.log('\n Next steps:');
952
+ console.log(' Review and edit fuel.yml to add workers/variables, then:');
953
+ console.log(` fuel create:app ${answers.projectName} --config fuel.yml\n`);
954
+ }
955
+ // --- infraInit ---
956
+ /** Interactive credentials-only flow (used by `auth:init` and `infra:init --credentials-only`) for the given cloud. */
957
+ async function collectCredentialsOnly(rl, cloud) {
958
+ console.log('\n Credentials');
959
+ console.log(' ────────────────────────────────────────────────────');
960
+ const credentials = {};
961
+ const credFilePath = path.resolve('.fuel-credentials');
962
+ if (await fs.pathExists(credFilePath)) {
963
+ try {
964
+ const existing = await fs.readJson(credFilePath);
965
+ console.log('\n Found existing .fuel-credentials');
966
+ if (cloud === 'aws') {
967
+ const [stsResult, ghUserResult, ghOrgResult] = await Promise.all([
968
+ tryGetAwsAccountId(existing), tryGetGithubUser(existing), tryGetGithubOrg(existing),
969
+ ]);
970
+ if ('accountId' in stsResult)
971
+ console.log(` AWS Account: ${chalk_1.default.green(stsResult.accountId)}`);
972
+ if ('login' in ghUserResult)
973
+ console.log(` GitHub User: ${chalk_1.default.green(ghUserResult.login)}`);
974
+ if ('login' in ghOrgResult)
975
+ console.log(` GitHub Org: ${chalk_1.default.green(ghOrgResult.login)}`);
976
+ assertCredentialResults(stsResult, ghUserResult, ghOrgResult);
630
977
  }
631
978
  else {
632
- await fs.writeFile(gitignorePath, `${CRED_LINE}\n`);
979
+ const [azResult, ghUserResult, ghOrgResult] = await Promise.all([
980
+ tryGetAzureAccountInfo(existing), tryGetGithubUser(existing), tryGetGithubOrg(existing),
981
+ ]);
982
+ if ('subscriptionId' in azResult)
983
+ console.log(` Azure Subscription: ${chalk_1.default.green(azResult.subscriptionId)}`);
984
+ if ('login' in ghUserResult)
985
+ console.log(` GitHub User: ${chalk_1.default.green(ghUserResult.login)}`);
986
+ if ('login' in ghOrgResult)
987
+ console.log(` GitHub Org: ${chalk_1.default.green(ghOrgResult.login)}`);
988
+ assertAzureCredentialResults(azResult, ghUserResult, ghOrgResult);
633
989
  }
634
990
  console.log('');
991
+ const useExisting = await askYesNo(rl, 'Use these credentials?', true);
992
+ if (useExisting) {
993
+ console.log(chalk_1.default.green('\n ✓ Existing credentials verified.\n'));
994
+ return;
995
+ }
635
996
  }
636
- finally {
637
- rl.close();
997
+ catch (e) {
998
+ if (e instanceof Error && e.message.startsWith('Credential verification failed'))
999
+ throw e;
1000
+ console.log(' Could not read .fuel-credentials — prompting for credentials.');
638
1001
  }
639
- return;
640
1002
  }
1003
+ if (cloud === 'aws') {
1004
+ credentials.AWS_ACCESS_KEY_ID = await ask(rl, 'AWS_ACCESS_KEY_ID');
1005
+ credentials.AWS_SECRET_ACCESS_KEY = await ask(rl, 'AWS_SECRET_ACCESS_KEY');
1006
+ credentials.AWS_REGION = await ask(rl, 'AWS_REGION', 'us-east-1');
1007
+ }
1008
+ else {
1009
+ credentials.AZURE_SUBSCRIPTION_ID = await ask(rl, 'AZURE_SUBSCRIPTION_ID');
1010
+ credentials.AZURE_TENANT_ID = await ask(rl, 'AZURE_TENANT_ID');
1011
+ credentials.AZURE_CLIENT_ID = await ask(rl, 'AZURE_CLIENT_ID');
1012
+ credentials.AZURE_CLIENT_SECRET = await ask(rl, 'AZURE_CLIENT_SECRET');
1013
+ }
1014
+ const gh = await askGitHubToken(rl);
1015
+ credentials.GITHUB_TOKEN = gh.token;
1016
+ credentials.GITHUB_USERNAME = await ask(rl, 'GITHUB_USERNAME');
1017
+ credentials.GITHUB_ORGANIZATION = await askGitHubOrganizationFor(rl, gh.token);
1018
+ console.log('\n Verifying credentials...');
1019
+ if (cloud === 'aws') {
1020
+ const [stsResult, ghUserResult, ghOrgResult] = await Promise.all([
1021
+ tryGetAwsAccountId(credentials), tryGetGithubUser(credentials), tryGetGithubOrg(credentials),
1022
+ ]);
1023
+ if ('accountId' in stsResult)
1024
+ console.log(` AWS Account: ${chalk_1.default.green(stsResult.accountId)}`);
1025
+ if ('login' in ghUserResult)
1026
+ console.log(` GitHub User: ${chalk_1.default.green(ghUserResult.login)}`);
1027
+ if ('login' in ghOrgResult)
1028
+ console.log(` GitHub Org: ${chalk_1.default.green(ghOrgResult.login)}`);
1029
+ assertCredentialResults(stsResult, ghUserResult, ghOrgResult);
1030
+ }
1031
+ else {
1032
+ const [azResult, ghUserResult, ghOrgResult] = await Promise.all([
1033
+ tryGetAzureAccountInfo(credentials), tryGetGithubUser(credentials), tryGetGithubOrg(credentials),
1034
+ ]);
1035
+ if ('subscriptionId' in azResult)
1036
+ console.log(` Azure Subscription: ${chalk_1.default.green(azResult.subscriptionId)}`);
1037
+ if ('login' in ghUserResult)
1038
+ console.log(` GitHub User: ${chalk_1.default.green(ghUserResult.login)}`);
1039
+ if ('login' in ghOrgResult)
1040
+ console.log(` GitHub Org: ${chalk_1.default.green(ghOrgResult.login)}`);
1041
+ assertAzureCredentialResults(azResult, ghUserResult, ghOrgResult);
1042
+ }
1043
+ await fs.writeFile(credFilePath, JSON.stringify(credentials, null, 2));
1044
+ console.log(chalk_1.default.green('\n ✓') + ' Credentials written to .fuel-credentials');
1045
+ const gitignorePath = path.resolve('.gitignore');
1046
+ const CRED_LINE = '.fuel-credentials';
1047
+ if (await fs.pathExists(gitignorePath)) {
1048
+ const content = await fs.readFile(gitignorePath, 'utf-8');
1049
+ if (!content.split('\n').some((l) => l.trim() === CRED_LINE)) {
1050
+ await fs.appendFile(gitignorePath, `\n${CRED_LINE}\n`);
1051
+ }
1052
+ }
1053
+ else {
1054
+ await fs.writeFile(gitignorePath, `${CRED_LINE}\n`);
1055
+ }
1056
+ console.log('');
1057
+ }
1058
+ async function infraInit(opts) {
641
1059
  if (opts.template) {
642
1060
  const outputPath = opts.output ? path.resolve(opts.output) : path.resolve('fuel.yml');
643
1061
  if (await fs.pathExists(outputPath)) {
644
1062
  throw new Error(`${outputPath} already exists. Remove it first or use --output <path>.`);
645
1063
  }
646
1064
  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));
1065
+ const template = opts.azure ? exports.CONFIG_TEMPLATE_AZURE : exports.CONFIG_TEMPLATE;
1066
+ await fs.writeFile(outputPath, schemaComment + YAML.stringify(template));
648
1067
  console.log(chalk_1.default.green('✓') + ` Config template written to ${outputPath}`);
649
1068
  console.log('\nEdit the file with your project details, then run:\n' +
650
1069
  ' fuel create:app my-app --config fuel.yml\n');
@@ -652,8 +1071,19 @@ async function infraInit(opts) {
652
1071
  }
653
1072
  const rl = (0, promises_1.createInterface)({ input: process.stdin, output: process.stdout });
654
1073
  try {
655
- const answers = await collectAnswers(rl);
656
- await applyInfraInitAnswers(answers, opts);
1074
+ const cloud = opts.azure ? 'azure' : await askCloudChoice(rl);
1075
+ if (opts.credentialsOnly) {
1076
+ await collectCredentialsOnly(rl, cloud);
1077
+ return;
1078
+ }
1079
+ if (cloud === 'azure') {
1080
+ const answers = await collectAzureAnswers(rl);
1081
+ await applyAzureInfraInitAnswers(answers, opts);
1082
+ }
1083
+ else {
1084
+ const answers = await collectAnswers(rl);
1085
+ await applyInfraInitAnswers(answers, opts);
1086
+ }
657
1087
  }
658
1088
  finally {
659
1089
  rl.close();