@swovohq/fuel 0.2.2-alpha.87 → 0.2.2-alpha.88

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 (71) hide show
  1. package/dist/bin/fuel.js +10 -31
  2. package/dist/bin/fuel.js.map +1 -1
  3. package/dist/commands/config-verify.d.ts.map +1 -1
  4. package/dist/commands/config-verify.js +14 -6
  5. package/dist/commands/config-verify.js.map +1 -1
  6. package/dist/commands/create-app.d.ts.map +1 -1
  7. package/dist/commands/create-app.js +1 -0
  8. package/dist/commands/create-app.js.map +1 -1
  9. package/dist/commands/infra-deploy.d.ts +4 -0
  10. package/dist/commands/infra-deploy.d.ts.map +1 -1
  11. package/dist/commands/infra-deploy.js +72 -43
  12. package/dist/commands/infra-deploy.js.map +1 -1
  13. package/dist/commands/infra-destroy.js +3 -3
  14. package/dist/commands/infra-destroy.js.map +1 -1
  15. package/dist/commands/infra-init.d.ts +91 -0
  16. package/dist/commands/infra-init.d.ts.map +1 -1
  17. package/dist/commands/infra-init.js +451 -78
  18. package/dist/commands/infra-init.js.map +1 -1
  19. package/dist/commands/infra-pull.d.ts.map +1 -1
  20. package/dist/commands/infra-pull.js +6 -5
  21. package/dist/commands/infra-pull.js.map +1 -1
  22. package/dist/infra/azure-defaults.d.ts +49 -0
  23. package/dist/infra/azure-defaults.d.ts.map +1 -0
  24. package/dist/infra/azure-defaults.js +52 -0
  25. package/dist/infra/azure-defaults.js.map +1 -0
  26. package/dist/infra/azure-keyvault-client.d.ts +4 -0
  27. package/dist/infra/azure-keyvault-client.d.ts.map +1 -0
  28. package/dist/infra/azure-keyvault-client.js +57 -0
  29. package/dist/infra/azure-keyvault-client.js.map +1 -0
  30. package/dist/infra/azure-rbac.d.ts +11 -0
  31. package/dist/infra/azure-rbac.d.ts.map +1 -0
  32. package/dist/infra/azure-rbac.js +47 -0
  33. package/dist/infra/azure-rbac.js.map +1 -0
  34. package/dist/infra/azure-state-storage.d.ts +20 -0
  35. package/dist/infra/azure-state-storage.d.ts.map +1 -0
  36. package/dist/infra/azure-state-storage.js +229 -0
  37. package/dist/infra/azure-state-storage.js.map +1 -0
  38. package/dist/infra/credentials.d.ts +2 -2
  39. package/dist/infra/credentials.d.ts.map +1 -1
  40. package/dist/infra/credentials.js +3 -2
  41. package/dist/infra/credentials.js.map +1 -1
  42. package/dist/infra/deployment-detect.d.ts +9 -0
  43. package/dist/infra/deployment-detect.d.ts.map +1 -0
  44. package/dist/infra/deployment-detect.js +53 -0
  45. package/dist/infra/deployment-detect.js.map +1 -0
  46. package/dist/infra/github-client.d.ts.map +1 -1
  47. package/dist/infra/github-client.js +1 -4
  48. package/dist/infra/github-client.js.map +1 -1
  49. package/dist/infra/orchestrator.d.ts.map +1 -1
  50. package/dist/infra/orchestrator.js +193 -65
  51. package/dist/infra/orchestrator.js.map +1 -1
  52. package/dist/infra/preflight.js +11 -11
  53. package/dist/infra/preflight.js.map +1 -1
  54. package/dist/infra/remote-secret.d.ts +14 -0
  55. package/dist/infra/remote-secret.d.ts.map +1 -0
  56. package/dist/infra/remote-secret.js +42 -0
  57. package/dist/infra/remote-secret.js.map +1 -0
  58. package/dist/infra/template-engine.d.ts +13 -0
  59. package/dist/infra/template-engine.d.ts.map +1 -1
  60. package/dist/infra/template-engine.js +106 -38
  61. package/dist/infra/template-engine.js.map +1 -1
  62. package/dist/infra/tofu-runner.d.ts +4 -3
  63. package/dist/infra/tofu-runner.d.ts.map +1 -1
  64. package/dist/infra/tofu-runner.js +36 -27
  65. package/dist/infra/tofu-runner.js.map +1 -1
  66. package/dist/infra/types.d.ts +239 -16
  67. package/dist/infra/types.d.ts.map +1 -1
  68. package/dist/infra/types.js +153 -17
  69. package/dist/infra/types.js.map +1 -1
  70. package/package.json +10 -2
  71. package/schema/fuel.schema.json +528 -83
@@ -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,9 @@ 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");
50
54
  const execAsync = (0, util_1.promisify)(child_process_1.exec);
51
55
  function httpsGetJson(url, headers) {
52
56
  return new Promise((resolve, reject) => {
@@ -98,6 +102,44 @@ exports.CONFIG_TEMPLATE = {
98
102
  },
99
103
  ],
100
104
  };
105
+ exports.CONFIG_TEMPLATE_AZURE = {
106
+ name: 'my-app',
107
+ deployment: 'azure',
108
+ slack_webhook_url: '',
109
+ location: 'eastus2',
110
+ monitor: { daily_data_cap_gb: 1, retention_days: 90, dashboard_count: 0 },
111
+ front_door: { sku_name: 'Standard_AzureFrontDoor' },
112
+ storage_account: { account_kind: 'BlockBlobStorage', account_tier: 'Premium', account_replication_type: 'LRS', access_tier: 'Hot' },
113
+ key_vault: { sku_name: 'standard' },
114
+ apps: [
115
+ {
116
+ name: 'my-app-api',
117
+ technology: 'nest',
118
+ dockerfile: 'apps/api/Dockerfile',
119
+ health_check_path: '/health',
120
+ domain: 'api.example.com',
121
+ port: 3000,
122
+ branch: 'main',
123
+ database: { enabled: true, engine: 'mssql', sku_name: 'GP_S_Gen5_4' },
124
+ redis: { enabled: false, sku_name: 'Balanced_B0' },
125
+ container_app: { cpu: 1, memory: '2Gi', concurrent_requests: 20, min_replicas: 0, max_replicas: 3 },
126
+ variables: {},
127
+ workers: [],
128
+ },
129
+ {
130
+ name: 'my-app-web',
131
+ technology: 'react',
132
+ dockerfile: 'apps/web/Dockerfile',
133
+ health_check_path: '/health',
134
+ domain: 'app.example.com',
135
+ port: 3000,
136
+ branch: 'main',
137
+ build_env: {
138
+ NEXT_PUBLIC_API_HOST: 'https://api.example.com',
139
+ },
140
+ },
141
+ ],
142
+ };
101
143
  // --- Private prompt helpers ---
102
144
  async function ask(rl, label, defaultVal) {
103
145
  const hint = defaultVal ? ` [${defaultVal}]` : '';
@@ -134,6 +176,14 @@ async function askSelect(rl, label, options, defaultIdx = 1) {
134
176
  console.log(` Please enter a number between 1 and ${options.length}.`);
135
177
  }
136
178
  }
179
+ /** Prompts which cloud provider to provision infrastructure for. */
180
+ async function askCloudChoice(rl) {
181
+ console.log(' Cloud provider');
182
+ console.log(' ────────────────────────────────────────────────────');
183
+ const choice = await askSelect(rl, 'Deploy to', ['AWS (ECS)', 'Azure (Container Apps)'], 1);
184
+ console.log('');
185
+ return choice === 'AWS (ECS)' ? 'aws' : 'azure';
186
+ }
137
187
  /** Prompts for KEY=VALUE pairs until the user submits an empty line. */
138
188
  async function askKeyValues(rl, label) {
139
189
  console.log(`\n ${label} (KEY=VALUE format, empty line to finish):`);
@@ -236,6 +286,19 @@ async function tryGetAcmCertificate(arn, credentials) {
236
286
  return { error: firstLine };
237
287
  }
238
288
  }
289
+ /** Verifies Azure credentials by acquiring a token and listing resource groups (cheap, works with just Reader access). */
290
+ async function tryGetAzureAccountInfo(credentials) {
291
+ try {
292
+ const credential = new identity_1.ClientSecretCredential(credentials.AZURE_TENANT_ID, credentials.AZURE_CLIENT_ID, credentials.AZURE_CLIENT_SECRET);
293
+ const client = new arm_resources_1.ResourceManagementClient(credential, credentials.AZURE_SUBSCRIPTION_ID);
294
+ const iterator = client.resourceGroups.list();
295
+ await iterator.next();
296
+ return { subscriptionId: credentials.AZURE_SUBSCRIPTION_ID };
297
+ }
298
+ catch (err) {
299
+ return { error: err instanceof Error ? err.message : String(err) };
300
+ }
301
+ }
239
302
  /** Displays credential verification results and throws if any check failed. */
240
303
  function assertCredentialResults(stsResult, ghUserResult, ghOrgResult) {
241
304
  const failures = [];
@@ -250,6 +313,20 @@ function assertCredentialResults(stsResult, ghUserResult, ghOrgResult) {
250
313
  failures.map((f) => ` • ${f}`).join('\n'));
251
314
  }
252
315
  }
316
+ /** Displays Azure credential verification results and throws if any check failed. */
317
+ function assertAzureCredentialResults(azResult, ghUserResult, ghOrgResult) {
318
+ const failures = [];
319
+ if ('error' in azResult)
320
+ failures.push(`Azure credentials: ${azResult.error}`);
321
+ if ('error' in ghUserResult)
322
+ failures.push(`GitHub token: ${ghUserResult.error}`);
323
+ if ('error' in ghOrgResult)
324
+ failures.push(`GitHub org: ${ghOrgResult.error}`);
325
+ if (failures.length > 0) {
326
+ throw new Error('Credential verification failed:\n' +
327
+ failures.map((f) => ` • ${f}`).join('\n'));
328
+ }
329
+ }
253
330
  /** Derives a sensible domain default: cliswovotest-api.swovo.com (production), cliswovotest-api-dev.swovo.com (others) */
254
331
  function defaultDomain(projectName, suffix, env, baseDomain) {
255
332
  const label = suffix === 'web' ? 'app' : suffix;
@@ -462,6 +539,177 @@ async function collectAnswers(rl) {
462
539
  console.log('');
463
540
  return { credentials, projectName, ssl, slackWebhookUrl, appTypes, environments, baseDomain, domains };
464
541
  }
542
+ // --- collectAzureAnswers ---
543
+ async function collectAzureAnswers(rl) {
544
+ // --- Credentials ---
545
+ console.log(' Credentials');
546
+ console.log(' ────────────────────────────────────────────────────');
547
+ let credentials = {};
548
+ let useExistingCreds = false;
549
+ const credFilePath = path.resolve('.fuel-credentials');
550
+ if (await fs.pathExists(credFilePath)) {
551
+ try {
552
+ const existing = await fs.readJson(credFilePath);
553
+ console.log('\n Found existing .fuel-credentials');
554
+ const [azResult, ghUserResult, ghOrgResult] = await Promise.all([
555
+ tryGetAzureAccountInfo(existing),
556
+ tryGetGithubUser(existing),
557
+ tryGetGithubOrg(existing),
558
+ ]);
559
+ if ('subscriptionId' in azResult) {
560
+ console.log(` Azure Subscription: ${chalk_1.default.green(azResult.subscriptionId)}`);
561
+ }
562
+ else {
563
+ console.log(' ' + chalk_1.default.yellow('⚠') + ' Azure credentials invalid:');
564
+ console.log(` ${azResult.error}`);
565
+ }
566
+ if ('login' in ghUserResult) {
567
+ console.log(` GitHub User: ${chalk_1.default.green(ghUserResult.login)}`);
568
+ }
569
+ else {
570
+ console.log(' ' + chalk_1.default.yellow('⚠') + ' GitHub token invalid:');
571
+ console.log(` ${ghUserResult.error}`);
572
+ }
573
+ if ('id' in ghOrgResult) {
574
+ console.log(` GitHub Org: ${chalk_1.default.green(ghOrgResult.login)} (ID: ${ghOrgResult.id})`);
575
+ }
576
+ else {
577
+ console.log(' ' + chalk_1.default.yellow('⚠') + ' Could not verify org:');
578
+ console.log(` ${ghOrgResult.error}`);
579
+ }
580
+ assertAzureCredentialResults(azResult, ghUserResult, ghOrgResult);
581
+ console.log('');
582
+ useExistingCreds = await askYesNo(rl, 'Use these credentials?', true);
583
+ if (useExistingCreds)
584
+ credentials = existing;
585
+ }
586
+ catch (e) {
587
+ if (e instanceof Error && e.message.startsWith('Credential verification failed'))
588
+ throw e;
589
+ console.log(' Could not read .fuel-credentials — prompting for credentials.');
590
+ }
591
+ }
592
+ if (!useExistingCreds) {
593
+ credentials.AZURE_SUBSCRIPTION_ID = await ask(rl, 'AZURE_SUBSCRIPTION_ID');
594
+ credentials.AZURE_TENANT_ID = await ask(rl, 'AZURE_TENANT_ID');
595
+ credentials.AZURE_CLIENT_ID = await ask(rl, 'AZURE_CLIENT_ID');
596
+ credentials.AZURE_CLIENT_SECRET = await ask(rl, 'AZURE_CLIENT_SECRET');
597
+ credentials.GITHUB_TOKEN = await ask(rl, 'GITHUB_TOKEN');
598
+ credentials.GITHUB_USERNAME = await ask(rl, 'GITHUB_USERNAME');
599
+ credentials.GITHUB_ORGANIZATION = await ask(rl, 'GITHUB_ORGANIZATION');
600
+ console.log('\n Verifying credentials...');
601
+ const [azResult, ghUserResult, ghOrgResult] = await Promise.all([
602
+ tryGetAzureAccountInfo(credentials),
603
+ tryGetGithubUser(credentials),
604
+ tryGetGithubOrg(credentials),
605
+ ]);
606
+ if ('subscriptionId' in azResult) {
607
+ console.log(` Azure Subscription: ${chalk_1.default.green(azResult.subscriptionId)}`);
608
+ }
609
+ else {
610
+ console.log(' ' + chalk_1.default.yellow('⚠') + ' Azure credentials invalid:');
611
+ console.log(` ${azResult.error}`);
612
+ }
613
+ if ('login' in ghUserResult) {
614
+ console.log(` GitHub User: ${chalk_1.default.green(ghUserResult.login)}`);
615
+ }
616
+ else {
617
+ console.log(' ' + chalk_1.default.yellow('⚠') + ' GitHub token invalid:');
618
+ console.log(` ${ghUserResult.error}`);
619
+ }
620
+ if ('id' in ghOrgResult) {
621
+ console.log(` GitHub Org: ${chalk_1.default.green(ghOrgResult.login)} (ID: ${ghOrgResult.id})`);
622
+ }
623
+ else {
624
+ console.log(' ' + chalk_1.default.yellow('⚠') + ' Could not verify org:');
625
+ console.log(` ${ghOrgResult.error}`);
626
+ }
627
+ assertAzureCredentialResults(azResult, ghUserResult, ghOrgResult);
628
+ }
629
+ console.log('');
630
+ // --- Project ---
631
+ console.log(' Project');
632
+ console.log(' ────────────────────────────────────────────────────');
633
+ const projectName = await ask(rl, 'Project name', 'my-app');
634
+ const location = await ask(rl, 'Azure region', 'eastus2');
635
+ const slackWebhookUrl = await ask(rl, 'Slack webhook URL for deploy/alarm notifications (leave empty to skip)', '');
636
+ console.log('');
637
+ // --- App configuration ---
638
+ console.log(' App configuration');
639
+ console.log(' ────────────────────────────────────────────────────');
640
+ const appChoice = await askSelect(rl, 'Include which apps?', ['API only (NestJS)', 'Web only (React/Next.js)', 'API + Web'], 3);
641
+ const includeApi = appChoice !== 'Web only (React/Next.js)';
642
+ const includeWeb = appChoice !== 'API only (NestJS)';
643
+ const appTypes = [];
644
+ if (includeApi) {
645
+ console.log('\n API (NestJS):');
646
+ const dockerfile = await ask(rl, 'Dockerfile', 'apps/api/Dockerfile');
647
+ const port = await askInt(rl, 'Port', 3000);
648
+ const healthCheckPath = await ask(rl, 'Health check path', '/health');
649
+ const databaseEnabled = await askYesNo(rl, 'Include a database?', true);
650
+ let databaseEngine = 'mssql';
651
+ if (databaseEnabled) {
652
+ const engineChoice = await askSelect(rl, ' Database engine', ['Azure SQL Database (mssql)', 'PostgreSQL Flexible Server (postgres)'], 1);
653
+ databaseEngine = engineChoice.includes('mssql') ? 'mssql' : 'postgres';
654
+ }
655
+ const redisEnabled = await askYesNo(rl, 'Include Redis?', false);
656
+ appTypes.push({
657
+ suffix: 'api', dockerfile, port, healthCheckPath,
658
+ databaseEnabled, databaseEngine, redisEnabled, variables: {},
659
+ });
660
+ }
661
+ if (includeWeb) {
662
+ console.log('\n Web (React/Next.js):');
663
+ const dockerfile = await ask(rl, 'Dockerfile', 'apps/web/Dockerfile');
664
+ const port = await askInt(rl, 'Port', 3000);
665
+ const healthCheckPath = await ask(rl, 'Health check path', '/health');
666
+ appTypes.push({
667
+ suffix: 'web', dockerfile, port, healthCheckPath,
668
+ databaseEnabled: false, databaseEngine: 'mssql', redisEnabled: false, variables: {},
669
+ });
670
+ }
671
+ console.log('');
672
+ // --- Environments ---
673
+ console.log(' Environments');
674
+ console.log(' ────────────────────────────────────────────────────');
675
+ const envPresets = {
676
+ 'production only': ['production'],
677
+ 'dev + production': ['dev', 'production'],
678
+ 'dev + staging + production': ['dev', 'staging', 'production'],
679
+ };
680
+ const envChoice = await askSelect(rl, 'Environments', Object.keys(envPresets), 3);
681
+ const environments = envPresets[envChoice];
682
+ console.log('');
683
+ // --- Base domain + per-app domains ---
684
+ console.log(' Domains');
685
+ console.log(' ────────────────────────────────────────────────────');
686
+ const baseDomain = await ask(rl, 'Base domain');
687
+ console.log('');
688
+ const domains = {};
689
+ for (const appType of appTypes) {
690
+ domains[appType.suffix] = {};
691
+ for (const env of environments) {
692
+ const suggestion = baseDomain ? defaultDomain(projectName, appType.suffix, env, baseDomain) : '';
693
+ const appFullName = `${projectName}-${appType.suffix}-${env}`;
694
+ domains[appType.suffix][env] = await ask(rl, appFullName, suggestion || undefined);
695
+ }
696
+ }
697
+ console.log('');
698
+ // --- Environment variables (per app × per environment) ---
699
+ console.log(' Environment variables');
700
+ console.log(' ────────────────────────────────────────────────────');
701
+ for (const appType of appTypes) {
702
+ const isApi = appType.suffix === 'api';
703
+ for (const env of environments) {
704
+ const label = isApi
705
+ ? `${projectName}-api-${env} runtime variables (e.g. TWILIO_TOKEN=xxx)`
706
+ : `${projectName}-web-${env} build variables (NEXT_PUBLIC_API_HOST and NEXT_PUBLIC_APP_HOST are already included)`;
707
+ appType.variables[env] = await askKeyValues(rl, label);
708
+ }
709
+ }
710
+ console.log('');
711
+ return { credentials, projectName, slackWebhookUrl, location, appTypes, environments, baseDomain, domains };
712
+ }
465
713
  // --- applyInfraInitAnswers ---
466
714
  async function applyInfraInitAnswers(answers, opts) {
467
715
  // 1. Write .fuel-credentials
@@ -555,96 +803,210 @@ async function applyInfraInitAnswers(answers, opts) {
555
803
  console.log(' Review and edit fuel.yml to add workers/variables, then:');
556
804
  console.log(` fuel create:app ${answers.projectName} --config fuel.yml\n`);
557
805
  }
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)}`);
806
+ // --- applyAzureInfraInitAnswers ---
807
+ async function applyAzureInfraInitAnswers(answers, opts) {
808
+ // 1. Write .fuel-credentials
809
+ const credPath = path.resolve('.fuel-credentials');
810
+ await fs.writeFile(credPath, JSON.stringify(answers.credentials, null, 2));
811
+ console.log(chalk_1.default.green('✓') + ' Credentials written to .fuel-credentials');
812
+ // 2. Ensure .gitignore contains .fuel-credentials
813
+ const gitignorePath = path.resolve('.gitignore');
814
+ const CRED_LINE = '.fuel-credentials';
815
+ if (await fs.pathExists(gitignorePath)) {
816
+ const content = await fs.readFile(gitignorePath, 'utf-8');
817
+ if (!content.split('\n').some((l) => l.trim() === CRED_LINE)) {
818
+ await fs.appendFile(gitignorePath, `\n${CRED_LINE}\n`);
819
+ console.log(chalk_1.default.green('✓') + ' .fuel-credentials added to .gitignore');
820
+ }
821
+ else {
822
+ console.log(' .fuel-credentials already in .gitignore');
823
+ }
824
+ }
825
+ else {
826
+ await fs.writeFile(gitignorePath, `${CRED_LINE}\n`);
827
+ console.log(chalk_1.default.green('') + ' .fuel-credentials added to .gitignore');
828
+ }
829
+ // 3. Build config (cartesian product: appTypes × environments)
830
+ const apps = [];
831
+ for (const appType of answers.appTypes) {
832
+ const isApi = appType.suffix === 'api';
833
+ const technology = isApi ? 'nest' : 'react';
834
+ for (const env of answers.environments) {
835
+ const branch = env === 'production' ? 'main' : env;
836
+ const app = {
837
+ name: `${answers.projectName}-${appType.suffix}-${env}`,
838
+ technology,
839
+ dockerfile: appType.dockerfile,
840
+ domain: answers.domains[appType.suffix][env],
841
+ port: appType.port,
842
+ health_check_path: appType.healthCheckPath,
843
+ branch,
844
+ variables: {},
845
+ build_env: {},
846
+ workers: [],
847
+ };
848
+ if (isApi) {
849
+ app.variables = appType.variables[env] ?? {};
850
+ app.database = { enabled: appType.databaseEnabled, engine: appType.databaseEngine };
851
+ app.redis = { enabled: appType.redisEnabled };
616
852
  }
617
- if ('login' in ghOrgResult) {
618
- console.log(` GitHub Org: ${chalk_1.default.green(ghOrgResult.login)}`);
853
+ else {
854
+ // Auto-populate build_env from domains; user-provided vars override
855
+ const autoBuildEnv = {};
856
+ const webDomain = answers.domains['web']?.[env];
857
+ if (webDomain)
858
+ autoBuildEnv['NEXT_PUBLIC_APP_HOST'] = `https://${webDomain}`;
859
+ const apiDomain = answers.domains['api']?.[env];
860
+ if (apiDomain)
861
+ autoBuildEnv['NEXT_PUBLIC_API_HOST'] = `https://${apiDomain}`;
862
+ app.build_env = { ...autoBuildEnv, ...(appType.variables[env] ?? {}) };
863
+ app.database = { enabled: false, engine: azure_defaults_1.AZURE_DEFAULTS.database.engine };
864
+ app.redis = { enabled: false };
619
865
  }
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
- }
866
+ app.container_app = {
867
+ cpu: azure_defaults_1.AZURE_DEFAULTS.containerApp.cpu,
868
+ memory: azure_defaults_1.AZURE_DEFAULTS.containerApp.memory,
869
+ concurrent_requests: azure_defaults_1.AZURE_DEFAULTS.containerApp.concurrentRequests,
870
+ min_replicas: azure_defaults_1.AZURE_DEFAULTS.containerApp.minReplicas,
871
+ max_replicas: azure_defaults_1.AZURE_DEFAULTS.containerApp.maxReplicas,
872
+ };
873
+ apps.push(app);
874
+ }
875
+ }
876
+ const config = {
877
+ name: answers.projectName,
878
+ deployment: 'azure',
879
+ location: answers.location,
880
+ slack_webhook_url: answers.slackWebhookUrl || '',
881
+ apps,
882
+ };
883
+ // 4. Guard against overwriting existing config
884
+ const outputPath = opts.output ? path.resolve(opts.output) : path.resolve('fuel.yml');
885
+ if (await fs.pathExists(outputPath)) {
886
+ throw new Error(`${outputPath} already exists. Remove it first or use --output <path>.`);
887
+ }
888
+ // 5. Write fuel.yml
889
+ const schemaComment = '# yaml-language-server: $schema=node_modules/@swovohq/fuel/schema/fuel.schema.json\n';
890
+ await fs.writeFile(outputPath, schemaComment + YAML.stringify(config));
891
+ const totalApps = answers.appTypes.length * answers.environments.length;
892
+ console.log(chalk_1.default.green('✓') +
893
+ ` fuel.yml written — ${totalApps} apps (${answers.appTypes.length} types × ${answers.environments.length} environments)`);
894
+ // 6. Next steps
895
+ console.log('\n Next steps:');
896
+ console.log(' Review and edit fuel.yml to add workers/variables, then:');
897
+ console.log(` fuel create:app ${answers.projectName} --config fuel.yml\n`);
898
+ }
899
+ // --- infraInit ---
900
+ /** Interactive credentials-only flow (used by `auth:init` and `infra:init --credentials-only`) for the given cloud. */
901
+ async function collectCredentialsOnly(rl, cloud) {
902
+ console.log('\n Credentials');
903
+ console.log(' ────────────────────────────────────────────────────');
904
+ const credentials = {};
905
+ const credFilePath = path.resolve('.fuel-credentials');
906
+ if (await fs.pathExists(credFilePath)) {
907
+ try {
908
+ const existing = await fs.readJson(credFilePath);
909
+ console.log('\n Found existing .fuel-credentials');
910
+ if (cloud === 'aws') {
911
+ const [stsResult, ghUserResult, ghOrgResult] = await Promise.all([
912
+ tryGetAwsAccountId(existing), tryGetGithubUser(existing), tryGetGithubOrg(existing),
913
+ ]);
914
+ if ('accountId' in stsResult)
915
+ console.log(` AWS Account: ${chalk_1.default.green(stsResult.accountId)}`);
916
+ if ('login' in ghUserResult)
917
+ console.log(` GitHub User: ${chalk_1.default.green(ghUserResult.login)}`);
918
+ if ('login' in ghOrgResult)
919
+ console.log(` GitHub Org: ${chalk_1.default.green(ghOrgResult.login)}`);
920
+ assertCredentialResults(stsResult, ghUserResult, ghOrgResult);
630
921
  }
631
922
  else {
632
- await fs.writeFile(gitignorePath, `${CRED_LINE}\n`);
923
+ const [azResult, ghUserResult, ghOrgResult] = await Promise.all([
924
+ tryGetAzureAccountInfo(existing), tryGetGithubUser(existing), tryGetGithubOrg(existing),
925
+ ]);
926
+ if ('subscriptionId' in azResult)
927
+ console.log(` Azure Subscription: ${chalk_1.default.green(azResult.subscriptionId)}`);
928
+ if ('login' in ghUserResult)
929
+ console.log(` GitHub User: ${chalk_1.default.green(ghUserResult.login)}`);
930
+ if ('login' in ghOrgResult)
931
+ console.log(` GitHub Org: ${chalk_1.default.green(ghOrgResult.login)}`);
932
+ assertAzureCredentialResults(azResult, ghUserResult, ghOrgResult);
633
933
  }
634
934
  console.log('');
935
+ const useExisting = await askYesNo(rl, 'Use these credentials?', true);
936
+ if (useExisting) {
937
+ console.log(chalk_1.default.green('\n ✓ Existing credentials verified.\n'));
938
+ return;
939
+ }
635
940
  }
636
- finally {
637
- rl.close();
941
+ catch (e) {
942
+ if (e instanceof Error && e.message.startsWith('Credential verification failed'))
943
+ throw e;
944
+ console.log(' Could not read .fuel-credentials — prompting for credentials.');
945
+ }
946
+ }
947
+ if (cloud === 'aws') {
948
+ credentials.AWS_ACCESS_KEY_ID = await ask(rl, 'AWS_ACCESS_KEY_ID');
949
+ credentials.AWS_SECRET_ACCESS_KEY = await ask(rl, 'AWS_SECRET_ACCESS_KEY');
950
+ credentials.AWS_REGION = await ask(rl, 'AWS_REGION', 'us-east-1');
951
+ }
952
+ else {
953
+ credentials.AZURE_SUBSCRIPTION_ID = await ask(rl, 'AZURE_SUBSCRIPTION_ID');
954
+ credentials.AZURE_TENANT_ID = await ask(rl, 'AZURE_TENANT_ID');
955
+ credentials.AZURE_CLIENT_ID = await ask(rl, 'AZURE_CLIENT_ID');
956
+ credentials.AZURE_CLIENT_SECRET = await ask(rl, 'AZURE_CLIENT_SECRET');
957
+ }
958
+ credentials.GITHUB_TOKEN = await ask(rl, 'GITHUB_TOKEN');
959
+ credentials.GITHUB_USERNAME = await ask(rl, 'GITHUB_USERNAME');
960
+ credentials.GITHUB_ORGANIZATION = await ask(rl, 'GITHUB_ORGANIZATION');
961
+ console.log('\n Verifying credentials...');
962
+ if (cloud === 'aws') {
963
+ const [stsResult, ghUserResult, ghOrgResult] = await Promise.all([
964
+ tryGetAwsAccountId(credentials), tryGetGithubUser(credentials), tryGetGithubOrg(credentials),
965
+ ]);
966
+ if ('accountId' in stsResult)
967
+ console.log(` AWS Account: ${chalk_1.default.green(stsResult.accountId)}`);
968
+ if ('login' in ghUserResult)
969
+ console.log(` GitHub User: ${chalk_1.default.green(ghUserResult.login)}`);
970
+ if ('login' in ghOrgResult)
971
+ console.log(` GitHub Org: ${chalk_1.default.green(ghOrgResult.login)}`);
972
+ assertCredentialResults(stsResult, ghUserResult, ghOrgResult);
973
+ }
974
+ else {
975
+ const [azResult, ghUserResult, ghOrgResult] = await Promise.all([
976
+ tryGetAzureAccountInfo(credentials), tryGetGithubUser(credentials), tryGetGithubOrg(credentials),
977
+ ]);
978
+ if ('subscriptionId' in azResult)
979
+ console.log(` Azure Subscription: ${chalk_1.default.green(azResult.subscriptionId)}`);
980
+ if ('login' in ghUserResult)
981
+ console.log(` GitHub User: ${chalk_1.default.green(ghUserResult.login)}`);
982
+ if ('login' in ghOrgResult)
983
+ console.log(` GitHub Org: ${chalk_1.default.green(ghOrgResult.login)}`);
984
+ assertAzureCredentialResults(azResult, ghUserResult, ghOrgResult);
985
+ }
986
+ await fs.writeFile(credFilePath, JSON.stringify(credentials, null, 2));
987
+ console.log(chalk_1.default.green('\n ✓') + ' Credentials written to .fuel-credentials');
988
+ const gitignorePath = path.resolve('.gitignore');
989
+ const CRED_LINE = '.fuel-credentials';
990
+ if (await fs.pathExists(gitignorePath)) {
991
+ const content = await fs.readFile(gitignorePath, 'utf-8');
992
+ if (!content.split('\n').some((l) => l.trim() === CRED_LINE)) {
993
+ await fs.appendFile(gitignorePath, `\n${CRED_LINE}\n`);
638
994
  }
639
- return;
640
995
  }
996
+ else {
997
+ await fs.writeFile(gitignorePath, `${CRED_LINE}\n`);
998
+ }
999
+ console.log('');
1000
+ }
1001
+ async function infraInit(opts) {
641
1002
  if (opts.template) {
642
1003
  const outputPath = opts.output ? path.resolve(opts.output) : path.resolve('fuel.yml');
643
1004
  if (await fs.pathExists(outputPath)) {
644
1005
  throw new Error(`${outputPath} already exists. Remove it first or use --output <path>.`);
645
1006
  }
646
1007
  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));
1008
+ const template = opts.azure ? exports.CONFIG_TEMPLATE_AZURE : exports.CONFIG_TEMPLATE;
1009
+ await fs.writeFile(outputPath, schemaComment + YAML.stringify(template));
648
1010
  console.log(chalk_1.default.green('✓') + ` Config template written to ${outputPath}`);
649
1011
  console.log('\nEdit the file with your project details, then run:\n' +
650
1012
  ' fuel create:app my-app --config fuel.yml\n');
@@ -652,8 +1014,19 @@ async function infraInit(opts) {
652
1014
  }
653
1015
  const rl = (0, promises_1.createInterface)({ input: process.stdin, output: process.stdout });
654
1016
  try {
655
- const answers = await collectAnswers(rl);
656
- await applyInfraInitAnswers(answers, opts);
1017
+ const cloud = opts.azure ? 'azure' : await askCloudChoice(rl);
1018
+ if (opts.credentialsOnly) {
1019
+ await collectCredentialsOnly(rl, cloud);
1020
+ return;
1021
+ }
1022
+ if (cloud === 'azure') {
1023
+ const answers = await collectAzureAnswers(rl);
1024
+ await applyAzureInfraInitAnswers(answers, opts);
1025
+ }
1026
+ else {
1027
+ const answers = await collectAnswers(rl);
1028
+ await applyInfraInitAnswers(answers, opts);
1029
+ }
657
1030
  }
658
1031
  finally {
659
1032
  rl.close();