@swovohq/fuel 0.1.0

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 (109) hide show
  1. package/dist/bin/fuel.d.ts +3 -0
  2. package/dist/bin/fuel.d.ts.map +1 -0
  3. package/dist/bin/fuel.js +353 -0
  4. package/dist/bin/fuel.js.map +1 -0
  5. package/dist/cleanup/finalize.d.ts +11 -0
  6. package/dist/cleanup/finalize.d.ts.map +1 -0
  7. package/dist/cleanup/finalize.js +116 -0
  8. package/dist/cleanup/finalize.js.map +1 -0
  9. package/dist/commands/create-app.d.ts +14 -0
  10. package/dist/commands/create-app.d.ts.map +1 -0
  11. package/dist/commands/create-app.js +354 -0
  12. package/dist/commands/create-app.js.map +1 -0
  13. package/dist/commands/infra-deploy.d.ts +8 -0
  14. package/dist/commands/infra-deploy.d.ts.map +1 -0
  15. package/dist/commands/infra-deploy.js +102 -0
  16. package/dist/commands/infra-deploy.js.map +1 -0
  17. package/dist/commands/infra-destroy.d.ts +5 -0
  18. package/dist/commands/infra-destroy.d.ts.map +1 -0
  19. package/dist/commands/infra-destroy.js +85 -0
  20. package/dist/commands/infra-destroy.js.map +1 -0
  21. package/dist/commands/infra-init.d.ts +75 -0
  22. package/dist/commands/infra-init.d.ts.map +1 -0
  23. package/dist/commands/infra-init.js +577 -0
  24. package/dist/commands/infra-init.js.map +1 -0
  25. package/dist/commands/list-modules.d.ts +2 -0
  26. package/dist/commands/list-modules.d.ts.map +1 -0
  27. package/dist/commands/list-modules.js +71 -0
  28. package/dist/commands/list-modules.js.map +1 -0
  29. package/dist/commands/migrate-init.d.ts +6 -0
  30. package/dist/commands/migrate-init.d.ts.map +1 -0
  31. package/dist/commands/migrate-init.js +174 -0
  32. package/dist/commands/migrate-init.js.map +1 -0
  33. package/dist/engines/convention.d.ts +3 -0
  34. package/dist/engines/convention.d.ts.map +1 -0
  35. package/dist/engines/convention.js +76 -0
  36. package/dist/engines/convention.js.map +1 -0
  37. package/dist/engines/dependencies.d.ts +20 -0
  38. package/dist/engines/dependencies.d.ts.map +1 -0
  39. package/dist/engines/dependencies.js +26 -0
  40. package/dist/engines/dependencies.js.map +1 -0
  41. package/dist/engines/monorepo.d.ts +6 -0
  42. package/dist/engines/monorepo.d.ts.map +1 -0
  43. package/dist/engines/monorepo.js +56 -0
  44. package/dist/engines/monorepo.js.map +1 -0
  45. package/dist/engines/template-source.d.ts +9 -0
  46. package/dist/engines/template-source.d.ts.map +1 -0
  47. package/dist/engines/template-source.js +123 -0
  48. package/dist/engines/template-source.js.map +1 -0
  49. package/dist/index.d.ts +8 -0
  50. package/dist/index.d.ts.map +1 -0
  51. package/dist/index.js +16 -0
  52. package/dist/index.js.map +1 -0
  53. package/dist/infra/config-loader.d.ts +4 -0
  54. package/dist/infra/config-loader.d.ts.map +1 -0
  55. package/dist/infra/config-loader.js +57 -0
  56. package/dist/infra/config-loader.js.map +1 -0
  57. package/dist/infra/config-writer.d.ts +9 -0
  58. package/dist/infra/config-writer.d.ts.map +1 -0
  59. package/dist/infra/config-writer.js +58 -0
  60. package/dist/infra/config-writer.js.map +1 -0
  61. package/dist/infra/credentials.d.ts +3 -0
  62. package/dist/infra/credentials.d.ts.map +1 -0
  63. package/dist/infra/credentials.js +74 -0
  64. package/dist/infra/credentials.js.map +1 -0
  65. package/dist/infra/dependency-checker.d.ts +9 -0
  66. package/dist/infra/dependency-checker.d.ts.map +1 -0
  67. package/dist/infra/dependency-checker.js +41 -0
  68. package/dist/infra/dependency-checker.js.map +1 -0
  69. package/dist/infra/git-client.d.ts +2 -0
  70. package/dist/infra/git-client.d.ts.map +1 -0
  71. package/dist/infra/git-client.js +22 -0
  72. package/dist/infra/git-client.js.map +1 -0
  73. package/dist/infra/github-secrets-client.d.ts +3 -0
  74. package/dist/infra/github-secrets-client.d.ts.map +1 -0
  75. package/dist/infra/github-secrets-client.js +37 -0
  76. package/dist/infra/github-secrets-client.js.map +1 -0
  77. package/dist/infra/orchestrator.d.ts +7 -0
  78. package/dist/infra/orchestrator.d.ts.map +1 -0
  79. package/dist/infra/orchestrator.js +187 -0
  80. package/dist/infra/orchestrator.js.map +1 -0
  81. package/dist/infra/preflight.d.ts +9 -0
  82. package/dist/infra/preflight.d.ts.map +1 -0
  83. package/dist/infra/preflight.js +75 -0
  84. package/dist/infra/preflight.js.map +1 -0
  85. package/dist/infra/s3-state-bucket.d.ts +5 -0
  86. package/dist/infra/s3-state-bucket.d.ts.map +1 -0
  87. package/dist/infra/s3-state-bucket.js +92 -0
  88. package/dist/infra/s3-state-bucket.js.map +1 -0
  89. package/dist/infra/secrets-manager-client.d.ts +3 -0
  90. package/dist/infra/secrets-manager-client.d.ts.map +1 -0
  91. package/dist/infra/secrets-manager-client.js +26 -0
  92. package/dist/infra/secrets-manager-client.js.map +1 -0
  93. package/dist/infra/subprocess.d.ts +5 -0
  94. package/dist/infra/subprocess.d.ts.map +1 -0
  95. package/dist/infra/subprocess.js +26 -0
  96. package/dist/infra/subprocess.js.map +1 -0
  97. package/dist/infra/template-engine.d.ts +43 -0
  98. package/dist/infra/template-engine.d.ts.map +1 -0
  99. package/dist/infra/template-engine.js +233 -0
  100. package/dist/infra/template-engine.js.map +1 -0
  101. package/dist/infra/tofu-runner.d.ts +26 -0
  102. package/dist/infra/tofu-runner.d.ts.map +1 -0
  103. package/dist/infra/tofu-runner.js +210 -0
  104. package/dist/infra/tofu-runner.js.map +1 -0
  105. package/dist/infra/types.d.ts +69 -0
  106. package/dist/infra/types.d.ts.map +1 -0
  107. package/dist/infra/types.js +44 -0
  108. package/dist/infra/types.js.map +1 -0
  109. package/package.json +45 -0
@@ -0,0 +1,75 @@
1
+ export declare const CONFIG_TEMPLATE: {
2
+ name: string;
3
+ deployment: string;
4
+ ssl: string;
5
+ slack_webhook_url: string;
6
+ apps: ({
7
+ name: string;
8
+ technology: string;
9
+ dockerfile: string;
10
+ health_check_path: string;
11
+ domain: string;
12
+ port: number;
13
+ branch: string;
14
+ postgres: boolean;
15
+ redis: boolean;
16
+ cpu_units: number;
17
+ memory_units: number;
18
+ db_instance_type: string;
19
+ db_allocated_storage: number;
20
+ variables: {};
21
+ workers: never[];
22
+ build_env?: undefined;
23
+ } | {
24
+ name: string;
25
+ technology: string;
26
+ dockerfile: string;
27
+ health_check_path: string;
28
+ domain: string;
29
+ port: number;
30
+ branch: string;
31
+ cpu_units: number;
32
+ memory_units: number;
33
+ build_env: {
34
+ NEXT_PUBLIC_API_HOST: string;
35
+ };
36
+ postgres?: undefined;
37
+ redis?: undefined;
38
+ db_instance_type?: undefined;
39
+ db_allocated_storage?: undefined;
40
+ variables?: undefined;
41
+ workers?: undefined;
42
+ })[];
43
+ };
44
+ export interface InfraInitAnswers {
45
+ credentials: Record<string, string>;
46
+ projectName: string;
47
+ ssl: string;
48
+ slackWebhookUrl: string;
49
+ /** api is always NestJS; web is always React/Next.js */
50
+ appTypes: Array<{
51
+ suffix: 'api' | 'web';
52
+ dockerfile: string;
53
+ port: number;
54
+ healthCheckPath: string;
55
+ postgres: boolean;
56
+ redis: boolean;
57
+ cpuUnits: number;
58
+ memoryUnits: number;
59
+ dbInstanceType: string;
60
+ dbAllocatedStorage: number;
61
+ redisInstanceType: string;
62
+ variables: Record<string, Record<string, string>>;
63
+ }>;
64
+ environments: string[];
65
+ baseDomain: string;
66
+ domains: Record<string, Record<string, string>>;
67
+ }
68
+ export declare function applyInfraInitAnswers(answers: InfraInitAnswers, opts: {
69
+ output?: string;
70
+ }): Promise<void>;
71
+ export declare function infraInit(opts: {
72
+ output?: string;
73
+ template?: boolean;
74
+ }): Promise<void>;
75
+ //# sourceMappingURL=infra-init.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"infra-init.d.ts","sourceRoot":"","sources":["../../src/commands/infra-init.ts"],"names":[],"mappings":"AAsBA,eAAO,MAAM,eAAe;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAsC3B,CAAA;AAED,MAAM,WAAW,gBAAgB;IAC/B,WAAW,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAA;IACnC,WAAW,EAAE,MAAM,CAAA;IACnB,GAAG,EAAE,MAAM,CAAA;IACX,eAAe,EAAE,MAAM,CAAA;IACvB,wDAAwD;IACxD,QAAQ,EAAE,KAAK,CAAC;QACd,MAAM,EAAE,KAAK,GAAG,KAAK,CAAA;QACrB,UAAU,EAAE,MAAM,CAAA;QAClB,IAAI,EAAE,MAAM,CAAA;QACZ,eAAe,EAAE,MAAM,CAAA;QAEvB,QAAQ,EAAE,OAAO,CAAA;QACjB,KAAK,EAAE,OAAO,CAAA;QACd,QAAQ,EAAE,MAAM,CAAA;QAChB,WAAW,EAAE,MAAM,CAAA;QACnB,cAAc,EAAE,MAAM,CAAA;QACtB,kBAAkB,EAAE,MAAM,CAAA;QAC1B,iBAAiB,EAAE,MAAM,CAAA;QACzB,SAAS,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC,CAAA;KAClD,CAAC,CAAA;IACF,YAAY,EAAE,MAAM,EAAE,CAAA;IACtB,UAAU,EAAE,MAAM,CAAA;IAClB,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC,CAAA;CAChD;AA0ZD,wBAAsB,qBAAqB,CACzC,OAAO,EAAE,gBAAgB,EACzB,IAAI,EAAE;IAAE,MAAM,CAAC,EAAE,MAAM,CAAA;CAAE,GACxB,OAAO,CAAC,IAAI,CAAC,CA6Ff;AAID,wBAAsB,SAAS,CAAC,IAAI,EAAE;IAAE,MAAM,CAAC,EAAE,MAAM,CAAC;IAAC,QAAQ,CAAC,EAAE,OAAO,CAAA;CAAE,GAAG,OAAO,CAAC,IAAI,CAAC,CAsB5F"}
@@ -0,0 +1,577 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || (function () {
19
+ var ownKeys = function(o) {
20
+ ownKeys = Object.getOwnPropertyNames || function (o) {
21
+ var ar = [];
22
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
+ return ar;
24
+ };
25
+ return ownKeys(o);
26
+ };
27
+ return function (mod) {
28
+ if (mod && mod.__esModule) return mod;
29
+ var result = {};
30
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
+ __setModuleDefault(result, mod);
32
+ return result;
33
+ };
34
+ })();
35
+ var __importDefault = (this && this.__importDefault) || function (mod) {
36
+ return (mod && mod.__esModule) ? mod : { "default": mod };
37
+ };
38
+ Object.defineProperty(exports, "__esModule", { value: true });
39
+ exports.CONFIG_TEMPLATE = void 0;
40
+ exports.applyInfraInitAnswers = applyInfraInitAnswers;
41
+ exports.infraInit = infraInit;
42
+ const fs = __importStar(require("fs-extra"));
43
+ const path = __importStar(require("path"));
44
+ const chalk_1 = __importDefault(require("chalk"));
45
+ const promises_1 = require("readline/promises");
46
+ const child_process_1 = require("child_process");
47
+ const util_1 = require("util");
48
+ const https = __importStar(require("https"));
49
+ const execAsync = (0, util_1.promisify)(child_process_1.exec);
50
+ function httpsGetJson(url, headers) {
51
+ return new Promise((resolve, reject) => {
52
+ const req = https.get(url, { headers }, (res) => {
53
+ let body = '';
54
+ res.on('data', (chunk) => { body += chunk.toString(); });
55
+ res.on('end', () => resolve({ status: res.statusCode ?? 0, body }));
56
+ });
57
+ req.on('error', reject);
58
+ req.setTimeout(10000, () => { req.destroy(); reject(new Error('Request timed out')); });
59
+ });
60
+ }
61
+ exports.CONFIG_TEMPLATE = {
62
+ name: 'my-app',
63
+ deployment: 'ecs',
64
+ ssl: 'arn:aws:acm:us-east-1:ACCOUNT_ID:certificate/CERTIFICATE_ID',
65
+ slack_webhook_url: '',
66
+ apps: [
67
+ {
68
+ name: 'my-app-api',
69
+ technology: 'nest',
70
+ dockerfile: 'apps/api/Dockerfile',
71
+ health_check_path: '/health',
72
+ domain: 'api.example.com',
73
+ port: 3000,
74
+ branch: 'main',
75
+ postgres: true,
76
+ redis: false,
77
+ cpu_units: 1024,
78
+ memory_units: 2048,
79
+ db_instance_type: 'db.t3.small',
80
+ db_allocated_storage: 30,
81
+ variables: {},
82
+ workers: [],
83
+ },
84
+ {
85
+ name: 'my-app-web',
86
+ technology: 'react',
87
+ dockerfile: 'apps/web/Dockerfile',
88
+ health_check_path: '/health',
89
+ domain: 'app.example.com',
90
+ port: 3000,
91
+ branch: 'main',
92
+ cpu_units: 512,
93
+ memory_units: 1024,
94
+ build_env: {
95
+ NEXT_PUBLIC_API_HOST: 'https://api.example.com',
96
+ },
97
+ },
98
+ ],
99
+ };
100
+ // --- Private prompt helpers ---
101
+ async function ask(rl, label, defaultVal) {
102
+ const hint = defaultVal ? ` [${defaultVal}]` : '';
103
+ const answer = await rl.question(` ${label}${hint}: `);
104
+ return answer.trim() || defaultVal || '';
105
+ }
106
+ async function askInt(rl, label, defaultVal) {
107
+ while (true) {
108
+ const raw = await ask(rl, label, defaultVal !== undefined ? String(defaultVal) : undefined);
109
+ const n = parseInt(raw, 10);
110
+ if (!isNaN(n) && n > 0)
111
+ return n;
112
+ console.log(' Please enter a positive integer.');
113
+ }
114
+ }
115
+ async function askYesNo(rl, label, defaultYes = false) {
116
+ const hint = defaultYes ? '[Y/n]' : '[y/N]';
117
+ const answer = await rl.question(` ${label} ${hint}: `);
118
+ const trimmed = answer.trim().toLowerCase();
119
+ if (!trimmed)
120
+ return defaultYes;
121
+ return trimmed === 'y' || trimmed === 'yes';
122
+ }
123
+ /** Prints a numbered list and returns the selected option. `defaultIdx` is 1-based. */
124
+ async function askSelect(rl, label, options, defaultIdx = 1) {
125
+ const formatted = options.map((o, i) => `(${i + 1}) ${o}`).join(' ');
126
+ console.log(` ${label}: ${formatted}`);
127
+ while (true) {
128
+ const raw = await rl.question(` Choice [${defaultIdx}]: `);
129
+ const trimmed = raw.trim();
130
+ const n = trimmed === '' ? defaultIdx : parseInt(trimmed, 10);
131
+ if (n >= 1 && n <= options.length)
132
+ return options[n - 1];
133
+ console.log(` Please enter a number between 1 and ${options.length}.`);
134
+ }
135
+ }
136
+ /** Prompts for KEY=VALUE pairs until the user submits an empty line. */
137
+ async function askKeyValues(rl, label) {
138
+ console.log(`\n ${label} (KEY=VALUE format, empty line to finish):`);
139
+ const result = {};
140
+ while (true) {
141
+ const input = await rl.question(' > ');
142
+ const trimmed = input.trim();
143
+ if (!trimmed)
144
+ break;
145
+ const eqIdx = trimmed.indexOf('=');
146
+ if (eqIdx < 1) {
147
+ console.log(' Invalid format. Use KEY=VALUE (e.g. DATABASE_URL=postgres://...).');
148
+ continue;
149
+ }
150
+ result[trimmed.slice(0, eqIdx).trim()] = trimmed.slice(eqIdx + 1).trim();
151
+ }
152
+ return result;
153
+ }
154
+ /** Calls GET /user on the GitHub API to verify the token. Returns login + email on success or `{ error: string }` on failure. */
155
+ async function tryGetGithubUser(credentials) {
156
+ try {
157
+ const { status, body } = await httpsGetJson('https://api.github.com/user', {
158
+ Authorization: `Bearer ${credentials.GITHUB_TOKEN}`,
159
+ 'User-Agent': 'fuel-cli',
160
+ Accept: 'application/vnd.github+json',
161
+ });
162
+ const data = JSON.parse(body);
163
+ if (status !== 200)
164
+ return { error: data.message ?? `HTTP ${status}` };
165
+ return { login: data.login ?? '(unknown)', email: data.email ?? '(private)' };
166
+ }
167
+ catch (err) {
168
+ return { error: err instanceof Error ? err.message : String(err) };
169
+ }
170
+ }
171
+ /** Calls GET /orgs/{org} to retrieve the GitHub org ID. Returns id + login on success or `{ error: string }` on failure. */
172
+ async function tryGetGithubOrg(credentials) {
173
+ const org = credentials.GITHUB_ORGANIZATION;
174
+ if (!org)
175
+ return { error: 'GITHUB_ORGANIZATION not set' };
176
+ try {
177
+ const { status, body } = await httpsGetJson(`https://api.github.com/orgs/${encodeURIComponent(org)}`, {
178
+ Authorization: `Bearer ${credentials.GITHUB_TOKEN}`,
179
+ 'User-Agent': 'fuel-cli',
180
+ Accept: 'application/vnd.github+json',
181
+ });
182
+ const data = JSON.parse(body);
183
+ if (status !== 200)
184
+ return { error: data.message ?? `HTTP ${status}` };
185
+ return { id: data.id ?? 0, login: data.login ?? org };
186
+ }
187
+ catch (err) {
188
+ return { error: err instanceof Error ? err.message : String(err) };
189
+ }
190
+ }
191
+ /** Calls `aws sts get-caller-identity` using the provided credentials. Returns Account ID on success, or `{ error: string }` on failure. */
192
+ async function tryGetAwsAccountId(credentials) {
193
+ try {
194
+ const { stdout } = await execAsync('aws sts get-caller-identity --output json', {
195
+ env: {
196
+ ...process.env,
197
+ AWS_ACCESS_KEY_ID: credentials.AWS_ACCESS_KEY_ID,
198
+ AWS_SECRET_ACCESS_KEY: credentials.AWS_SECRET_ACCESS_KEY,
199
+ AWS_REGION: credentials.AWS_REGION,
200
+ },
201
+ timeout: 10000,
202
+ });
203
+ const accountId = JSON.parse(stdout).Account ?? '';
204
+ return { accountId };
205
+ }
206
+ catch (err) {
207
+ const e = err;
208
+ const raw = e.stderr ?? e.message ?? String(err);
209
+ const firstLine = raw.split('\n').map((l) => l.trim()).find((l) => l.length > 0) ?? raw;
210
+ return { error: firstLine };
211
+ }
212
+ }
213
+ /** Calls `aws acm describe-certificate` to verify the SSL cert ARN exists and is ISSUED. */
214
+ async function tryGetAcmCertificate(arn, credentials) {
215
+ try {
216
+ const { stdout } = await execAsync(`aws acm describe-certificate --certificate-arn "${arn}" --output json`, {
217
+ env: {
218
+ ...process.env,
219
+ AWS_ACCESS_KEY_ID: credentials.AWS_ACCESS_KEY_ID,
220
+ AWS_SECRET_ACCESS_KEY: credentials.AWS_SECRET_ACCESS_KEY,
221
+ AWS_REGION: credentials.AWS_REGION,
222
+ },
223
+ timeout: 10000,
224
+ });
225
+ const cert = JSON.parse(stdout).Certificate;
226
+ return {
227
+ domainName: cert?.DomainName ?? '(unknown)',
228
+ status: cert?.Status ?? '(unknown)',
229
+ };
230
+ }
231
+ catch (err) {
232
+ const e = err;
233
+ const raw = e.stderr ?? e.message ?? String(err);
234
+ const firstLine = raw.split('\n').map((l) => l.trim()).find((l) => l.length > 0) ?? raw;
235
+ return { error: firstLine };
236
+ }
237
+ }
238
+ /** Displays credential verification results and throws if any check failed. */
239
+ function assertCredentialResults(stsResult, ghUserResult, ghOrgResult) {
240
+ const failures = [];
241
+ if ('error' in stsResult)
242
+ failures.push(`AWS credentials: ${stsResult.error}`);
243
+ if ('error' in ghUserResult)
244
+ failures.push(`GitHub token: ${ghUserResult.error}`);
245
+ if ('error' in ghOrgResult)
246
+ failures.push(`GitHub org: ${ghOrgResult.error}`);
247
+ if (failures.length > 0) {
248
+ throw new Error('Credential verification failed:\n' +
249
+ failures.map((f) => ` • ${f}`).join('\n'));
250
+ }
251
+ }
252
+ /** Derives a sensible domain default: cliswovotest-api.swovo.com (production), cliswovotest-api-dev.swovo.com (others) */
253
+ function defaultDomain(projectName, suffix, env, baseDomain) {
254
+ const label = suffix === 'web' ? 'app' : suffix;
255
+ if (env === 'production')
256
+ return `${projectName}-${label}.${baseDomain}`;
257
+ return `${projectName}-${label}-${env}.${baseDomain}`;
258
+ }
259
+ // --- collectAnswers ---
260
+ async function collectAnswers(rl) {
261
+ console.log('\n━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━');
262
+ console.log(' FUEL ► INFRASTRUCTURE SETUP');
263
+ console.log('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n');
264
+ // --- Credentials ---
265
+ console.log(' Credentials');
266
+ console.log(' ────────────────────────────────────────────────────');
267
+ let credentials = {};
268
+ let useExistingCreds = false;
269
+ const credFilePath = path.resolve('.fuel-credentials');
270
+ if (await fs.pathExists(credFilePath)) {
271
+ try {
272
+ const existing = await fs.readJson(credFilePath);
273
+ console.log('\n Found existing .fuel-credentials');
274
+ const [stsResult, ghUserResult, ghOrgResult] = await Promise.all([
275
+ tryGetAwsAccountId(existing),
276
+ tryGetGithubUser(existing),
277
+ tryGetGithubOrg(existing),
278
+ ]);
279
+ if ('accountId' in stsResult) {
280
+ console.log(` AWS Account: ${chalk_1.default.green(stsResult.accountId)}`);
281
+ }
282
+ else {
283
+ console.log(' ' + chalk_1.default.yellow('⚠') + ' AWS credentials invalid:');
284
+ console.log(` ${stsResult.error}`);
285
+ }
286
+ console.log(` AWS Region: ${existing.AWS_REGION ?? '(not set)'}`);
287
+ if ('login' in ghUserResult) {
288
+ console.log(` GitHub User: ${chalk_1.default.green(ghUserResult.login)}`);
289
+ console.log(` GitHub Email: ${ghUserResult.email}`);
290
+ }
291
+ else {
292
+ console.log(' ' + chalk_1.default.yellow('⚠') + ' GitHub token invalid:');
293
+ console.log(` ${ghUserResult.error}`);
294
+ }
295
+ if ('id' in ghOrgResult) {
296
+ console.log(` GitHub Org: ${chalk_1.default.green(ghOrgResult.login)} (ID: ${ghOrgResult.id})`);
297
+ }
298
+ else {
299
+ console.log(` GitHub Org: ${existing.GITHUB_ORGANIZATION ?? '(not set)'}`);
300
+ console.log(' ' + chalk_1.default.yellow('⚠') + ' Could not verify org:');
301
+ console.log(` ${ghOrgResult.error}`);
302
+ }
303
+ assertCredentialResults(stsResult, ghUserResult, ghOrgResult);
304
+ console.log('');
305
+ useExistingCreds = await askYesNo(rl, 'Use these credentials?', true);
306
+ if (useExistingCreds)
307
+ credentials = existing;
308
+ }
309
+ catch (e) {
310
+ if (e instanceof Error && e.message.startsWith('Credential verification failed'))
311
+ throw e;
312
+ console.log(' Could not read .fuel-credentials — prompting for credentials.');
313
+ }
314
+ }
315
+ if (!useExistingCreds) {
316
+ credentials.AWS_ACCESS_KEY_ID = await ask(rl, 'AWS_ACCESS_KEY_ID');
317
+ credentials.AWS_SECRET_ACCESS_KEY = await ask(rl, 'AWS_SECRET_ACCESS_KEY');
318
+ credentials.AWS_REGION = await ask(rl, 'AWS_REGION', 'us-east-1');
319
+ credentials.GITHUB_TOKEN = await ask(rl, 'GITHUB_TOKEN');
320
+ credentials.GITHUB_USERNAME = await ask(rl, 'GITHUB_USERNAME');
321
+ credentials.GITHUB_ORGANIZATION = await ask(rl, 'GITHUB_ORGANIZATION');
322
+ console.log('\n Verifying credentials...');
323
+ const [stsResult, ghUserResult, ghOrgResult] = await Promise.all([
324
+ tryGetAwsAccountId(credentials),
325
+ tryGetGithubUser(credentials),
326
+ tryGetGithubOrg(credentials),
327
+ ]);
328
+ if ('accountId' in stsResult) {
329
+ console.log(` AWS Account: ${chalk_1.default.green(stsResult.accountId)}`);
330
+ }
331
+ else {
332
+ console.log(' ' + chalk_1.default.yellow('⚠') + ' AWS credentials invalid:');
333
+ console.log(` ${stsResult.error}`);
334
+ }
335
+ console.log(` AWS Region: ${credentials.AWS_REGION}`);
336
+ if ('login' in ghUserResult) {
337
+ console.log(` GitHub User: ${chalk_1.default.green(ghUserResult.login)}`);
338
+ console.log(` GitHub Email: ${ghUserResult.email}`);
339
+ }
340
+ else {
341
+ console.log(' ' + chalk_1.default.yellow('⚠') + ' GitHub token invalid:');
342
+ console.log(` ${ghUserResult.error}`);
343
+ }
344
+ if ('id' in ghOrgResult) {
345
+ console.log(` GitHub Org: ${chalk_1.default.green(ghOrgResult.login)} (ID: ${ghOrgResult.id})`);
346
+ }
347
+ else {
348
+ console.log(` GitHub Org: ${credentials.GITHUB_ORGANIZATION}`);
349
+ console.log(' ' + chalk_1.default.yellow('⚠') + ' Could not verify org:');
350
+ console.log(` ${ghOrgResult.error}`);
351
+ }
352
+ assertCredentialResults(stsResult, ghUserResult, ghOrgResult);
353
+ }
354
+ console.log('');
355
+ // --- Project ---
356
+ console.log(' Project');
357
+ console.log(' ────────────────────────────────────────────────────');
358
+ const projectName = await ask(rl, 'Project name', 'my-app');
359
+ const ssl = await ask(rl, 'SSL certificate ARN (arn:aws:acm:...)');
360
+ const slackWebhookUrl = await ask(rl, 'Slack webhook URL for deploy/alarm notifications (leave empty to skip)', '');
361
+ const certResult = await tryGetAcmCertificate(ssl, credentials);
362
+ if ('domainName' in certResult) {
363
+ const statusColored = certResult.status === 'ISSUED'
364
+ ? chalk_1.default.green(certResult.status)
365
+ : chalk_1.default.yellow(certResult.status);
366
+ console.log(` ${chalk_1.default.green('✓')} Certificate found: ${certResult.domainName} [${statusColored}]`);
367
+ if (certResult.status !== 'ISSUED') {
368
+ throw new Error(`SSL certificate is not ISSUED (status: ${certResult.status}). Provision or validate the certificate first.`);
369
+ }
370
+ }
371
+ else {
372
+ throw new Error(`SSL certificate not found or inaccessible: ${certResult.error}`);
373
+ }
374
+ console.log('');
375
+ // --- App configuration ---
376
+ console.log(' App configuration');
377
+ console.log(' ────────────────────────────────────────────────────');
378
+ const appChoice = await askSelect(rl, 'Include which apps?', ['API only (NestJS)', 'Web only (React/Next.js)', 'API + Web'], 3);
379
+ const includeApi = appChoice !== 'Web only (React/Next.js)';
380
+ const includeWeb = appChoice !== 'API only (NestJS)';
381
+ const appTypes = [];
382
+ if (includeApi) {
383
+ console.log('\n API (NestJS):');
384
+ const dockerfile = await ask(rl, 'Dockerfile', 'apps/api/Dockerfile');
385
+ const port = await askInt(rl, 'Port', 3000);
386
+ const healthCheckPath = await ask(rl, 'Health check path', '/health');
387
+ const postgres = await askYesNo(rl, 'Include PostgreSQL?', true);
388
+ let dbInstanceType = 'db.t3.small';
389
+ let dbAllocatedStorage = 30;
390
+ if (postgres) {
391
+ dbInstanceType = await ask(rl, ' DB instance type', 'db.t3.small');
392
+ dbAllocatedStorage = await askInt(rl, ' DB allocated storage (GB)', 30);
393
+ }
394
+ const redis = await askYesNo(rl, 'Include Redis?', false);
395
+ let redisInstanceType = 'cache.t3.small';
396
+ if (redis) {
397
+ redisInstanceType = await ask(rl, ' Redis instance type', 'cache.t3.small');
398
+ }
399
+ const cpuUnits = await askInt(rl, 'CPU units', 1024);
400
+ const memoryUnits = await askInt(rl, 'Memory (MB)', 2048);
401
+ appTypes.push({
402
+ suffix: 'api', dockerfile, port, healthCheckPath,
403
+ postgres, redis, cpuUnits, memoryUnits,
404
+ dbInstanceType, dbAllocatedStorage, redisInstanceType, variables: {},
405
+ });
406
+ }
407
+ if (includeWeb) {
408
+ console.log('\n Web (React/Next.js):');
409
+ const dockerfile = await ask(rl, 'Dockerfile', 'apps/web/Dockerfile');
410
+ const port = await askInt(rl, 'Port', 3000);
411
+ const healthCheckPath = await ask(rl, 'Health check path', '/health');
412
+ const cpuUnits = await askInt(rl, 'CPU units', 512);
413
+ const memoryUnits = await askInt(rl, 'Memory (MB)', 1024);
414
+ appTypes.push({
415
+ suffix: 'web', dockerfile, port, healthCheckPath,
416
+ postgres: false, redis: false, cpuUnits, memoryUnits,
417
+ dbInstanceType: 'db.t3.small', dbAllocatedStorage: 30,
418
+ redisInstanceType: 'cache.t3.small', variables: {},
419
+ });
420
+ }
421
+ console.log('');
422
+ // --- Environments ---
423
+ console.log(' Environments');
424
+ console.log(' ────────────────────────────────────────────────────');
425
+ const envPresets = {
426
+ 'production only': ['production'],
427
+ 'dev + production': ['dev', 'production'],
428
+ 'dev + staging + production': ['dev', 'staging', 'production'],
429
+ };
430
+ const envChoice = await askSelect(rl, 'Environments', Object.keys(envPresets), 3);
431
+ const environments = envPresets[envChoice];
432
+ console.log('');
433
+ // --- Base domain + per-app domains ---
434
+ console.log(' Domains');
435
+ console.log(' ────────────────────────────────────────────────────');
436
+ const baseDomain = await ask(rl, 'Base domain (e.g. swovo.com)');
437
+ console.log('');
438
+ const domains = {};
439
+ for (const appType of appTypes) {
440
+ domains[appType.suffix] = {};
441
+ for (const env of environments) {
442
+ const suggestion = baseDomain ? defaultDomain(projectName, appType.suffix, env, baseDomain) : '';
443
+ const appFullName = `${projectName}-${appType.suffix}-${env}`;
444
+ domains[appType.suffix][env] = await ask(rl, appFullName, suggestion || undefined);
445
+ }
446
+ }
447
+ console.log('');
448
+ // --- Environment variables (per app × per environment) ---
449
+ console.log(' Environment variables');
450
+ console.log(' ────────────────────────────────────────────────────');
451
+ for (const appType of appTypes) {
452
+ const isApi = appType.suffix === 'api';
453
+ for (const env of environments) {
454
+ const label = isApi
455
+ ? `${projectName}-api-${env} runtime variables (e.g. TWILIO_TOKEN=xxx)`
456
+ : `${projectName}-web-${env} build variables (NEXT_PUBLIC_API_HOST and NEXT_PUBLIC_APP_HOST are already included)`;
457
+ appType.variables[env] = await askKeyValues(rl, label);
458
+ }
459
+ }
460
+ console.log('');
461
+ return { credentials, projectName, ssl, slackWebhookUrl, appTypes, environments, baseDomain, domains };
462
+ }
463
+ // --- applyInfraInitAnswers ---
464
+ async function applyInfraInitAnswers(answers, opts) {
465
+ // 1. Write .fuel-credentials
466
+ const credPath = path.resolve('.fuel-credentials');
467
+ await fs.writeFile(credPath, JSON.stringify(answers.credentials, null, 2));
468
+ console.log(chalk_1.default.green('✓') + ' Credentials written to .fuel-credentials');
469
+ // 2. Ensure .gitignore contains .fuel-credentials
470
+ const gitignorePath = path.resolve('.gitignore');
471
+ const CRED_LINE = '.fuel-credentials';
472
+ if (await fs.pathExists(gitignorePath)) {
473
+ const content = await fs.readFile(gitignorePath, 'utf-8');
474
+ if (!content.split('\n').some((l) => l.trim() === CRED_LINE)) {
475
+ await fs.appendFile(gitignorePath, `\n${CRED_LINE}\n`);
476
+ console.log(chalk_1.default.green('✓') + ' .fuel-credentials added to .gitignore');
477
+ }
478
+ else {
479
+ console.log(' .fuel-credentials already in .gitignore');
480
+ }
481
+ }
482
+ else {
483
+ await fs.writeFile(gitignorePath, `${CRED_LINE}\n`);
484
+ console.log(chalk_1.default.green('✓') + ' .fuel-credentials added to .gitignore');
485
+ }
486
+ // 3. Build config (cartesian product: appTypes × environments)
487
+ const apps = [];
488
+ for (const appType of answers.appTypes) {
489
+ const isApi = appType.suffix === 'api';
490
+ const technology = isApi ? 'nest' : 'react';
491
+ for (const env of answers.environments) {
492
+ const branch = env === 'production' ? 'main' : env;
493
+ const app = {
494
+ name: `${answers.projectName}-${appType.suffix}-${env}`,
495
+ technology,
496
+ dockerfile: appType.dockerfile,
497
+ domain: answers.domains[appType.suffix][env],
498
+ port: appType.port,
499
+ health_check_path: appType.healthCheckPath,
500
+ branch,
501
+ cpu_units: appType.cpuUnits,
502
+ memory_units: appType.memoryUnits,
503
+ variables: {},
504
+ build_env: {},
505
+ workers: [],
506
+ };
507
+ if (isApi) {
508
+ app.postgres = appType.postgres;
509
+ app.redis = appType.redis;
510
+ app.variables = appType.variables[env] ?? {};
511
+ if (appType.postgres) {
512
+ app.db_instance_type = appType.dbInstanceType;
513
+ app.db_allocated_storage = appType.dbAllocatedStorage;
514
+ }
515
+ if (appType.redis) {
516
+ app.redis_instance_type = appType.redisInstanceType;
517
+ }
518
+ }
519
+ else {
520
+ // Auto-populate build_env from domains; user-provided vars override
521
+ const autoBuildEnv = {};
522
+ const webDomain = answers.domains['web']?.[env];
523
+ if (webDomain)
524
+ autoBuildEnv['NEXT_PUBLIC_APP_HOST'] = `https://${webDomain}`;
525
+ const apiDomain = answers.domains['api']?.[env];
526
+ if (apiDomain)
527
+ autoBuildEnv['NEXT_PUBLIC_API_HOST'] = `https://${apiDomain}`;
528
+ app.build_env = { ...autoBuildEnv, ...(appType.variables[env] ?? {}) };
529
+ }
530
+ apps.push(app);
531
+ }
532
+ }
533
+ const config = {
534
+ name: answers.projectName,
535
+ deployment: 'ecs',
536
+ ssl: answers.ssl,
537
+ slack_webhook_url: answers.slackWebhookUrl || '',
538
+ apps,
539
+ };
540
+ // 4. Guard against overwriting existing config
541
+ const outputPath = opts.output ? path.resolve(opts.output) : path.resolve('config.json');
542
+ if (await fs.pathExists(outputPath)) {
543
+ throw new Error(`${outputPath} already exists. Remove it first or use --output <path>.`);
544
+ }
545
+ // 5. Write config.json
546
+ await fs.writeJson(outputPath, config, { spaces: 2 });
547
+ const totalApps = answers.appTypes.length * answers.environments.length;
548
+ console.log(chalk_1.default.green('✓') +
549
+ ` config.json written — ${totalApps} apps (${answers.appTypes.length} types × ${answers.environments.length} environments)`);
550
+ // 6. Next steps
551
+ console.log('\n Next steps:');
552
+ console.log(' Review and edit config.json to add workers/variables, then:');
553
+ console.log(` fuel create:app ${answers.projectName} --deployment ecs --config config.json\n`);
554
+ }
555
+ // --- infraInit ---
556
+ async function infraInit(opts) {
557
+ if (opts.template) {
558
+ const outputPath = opts.output ? path.resolve(opts.output) : path.resolve('config.json');
559
+ if (await fs.pathExists(outputPath)) {
560
+ throw new Error(`${outputPath} already exists. Remove it first or use --output <path>.`);
561
+ }
562
+ await fs.writeJson(outputPath, exports.CONFIG_TEMPLATE, { spaces: 2 });
563
+ console.log(chalk_1.default.green('✓') + ` Config template written to ${outputPath}`);
564
+ console.log('\nEdit the file with your project details, then run:\n' +
565
+ ' fuel create:app my-app --deployment ecs --config config.json\n');
566
+ return;
567
+ }
568
+ const rl = (0, promises_1.createInterface)({ input: process.stdin, output: process.stdout });
569
+ try {
570
+ const answers = await collectAnswers(rl);
571
+ await applyInfraInitAnswers(answers, opts);
572
+ }
573
+ finally {
574
+ rl.close();
575
+ }
576
+ }
577
+ //# sourceMappingURL=infra-init.js.map