@swovohq/fuel 0.2.2-alpha.87 → 0.2.2-alpha.89
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.
- package/README.md +65 -2
- package/dist/bin/fuel.js +10 -31
- package/dist/bin/fuel.js.map +1 -1
- package/dist/commands/config-verify.d.ts.map +1 -1
- package/dist/commands/config-verify.js +14 -6
- package/dist/commands/config-verify.js.map +1 -1
- package/dist/commands/create-app.d.ts.map +1 -1
- package/dist/commands/create-app.js +10 -3
- package/dist/commands/create-app.js.map +1 -1
- package/dist/commands/infra-deploy.d.ts +4 -0
- package/dist/commands/infra-deploy.d.ts.map +1 -1
- package/dist/commands/infra-deploy.js +75 -43
- package/dist/commands/infra-deploy.js.map +1 -1
- package/dist/commands/infra-destroy.js +3 -3
- package/dist/commands/infra-destroy.js.map +1 -1
- package/dist/commands/infra-init.d.ts +91 -0
- package/dist/commands/infra-init.d.ts.map +1 -1
- package/dist/commands/infra-init.js +510 -80
- package/dist/commands/infra-init.js.map +1 -1
- package/dist/commands/infra-pull.d.ts.map +1 -1
- package/dist/commands/infra-pull.js +6 -5
- package/dist/commands/infra-pull.js.map +1 -1
- package/dist/infra/azure-defaults.d.ts +49 -0
- package/dist/infra/azure-defaults.d.ts.map +1 -0
- package/dist/infra/azure-defaults.js +52 -0
- package/dist/infra/azure-defaults.js.map +1 -0
- package/dist/infra/azure-keyvault-client.d.ts +4 -0
- package/dist/infra/azure-keyvault-client.d.ts.map +1 -0
- package/dist/infra/azure-keyvault-client.js +57 -0
- package/dist/infra/azure-keyvault-client.js.map +1 -0
- package/dist/infra/azure-rbac.d.ts +11 -0
- package/dist/infra/azure-rbac.d.ts.map +1 -0
- package/dist/infra/azure-rbac.js +47 -0
- package/dist/infra/azure-rbac.js.map +1 -0
- package/dist/infra/azure-state-storage.d.ts +20 -0
- package/dist/infra/azure-state-storage.d.ts.map +1 -0
- package/dist/infra/azure-state-storage.js +229 -0
- package/dist/infra/azure-state-storage.js.map +1 -0
- package/dist/infra/credentials.d.ts +2 -2
- package/dist/infra/credentials.d.ts.map +1 -1
- package/dist/infra/credentials.js +8 -4
- package/dist/infra/credentials.js.map +1 -1
- package/dist/infra/deployment-detect.d.ts +9 -0
- package/dist/infra/deployment-detect.d.ts.map +1 -0
- package/dist/infra/deployment-detect.js +53 -0
- package/dist/infra/deployment-detect.js.map +1 -0
- package/dist/infra/github-client.d.ts +6 -0
- package/dist/infra/github-client.d.ts.map +1 -1
- package/dist/infra/github-client.js +31 -4
- package/dist/infra/github-client.js.map +1 -1
- package/dist/infra/github-token-link.d.ts +4 -0
- package/dist/infra/github-token-link.d.ts.map +1 -0
- package/dist/infra/github-token-link.js +28 -0
- package/dist/infra/github-token-link.js.map +1 -0
- package/dist/infra/orchestrator.d.ts +4 -1
- package/dist/infra/orchestrator.d.ts.map +1 -1
- package/dist/infra/orchestrator.js +203 -66
- package/dist/infra/orchestrator.js.map +1 -1
- package/dist/infra/preflight.d.ts.map +1 -1
- package/dist/infra/preflight.js +17 -9
- package/dist/infra/preflight.js.map +1 -1
- package/dist/infra/remote-secret.d.ts +14 -0
- package/dist/infra/remote-secret.d.ts.map +1 -0
- package/dist/infra/remote-secret.js +42 -0
- package/dist/infra/remote-secret.js.map +1 -0
- package/dist/infra/template-engine.d.ts +13 -4
- package/dist/infra/template-engine.d.ts.map +1 -1
- package/dist/infra/template-engine.js +141 -49
- package/dist/infra/template-engine.js.map +1 -1
- package/dist/infra/tofu-runner.d.ts +4 -3
- package/dist/infra/tofu-runner.d.ts.map +1 -1
- package/dist/infra/tofu-runner.js +36 -27
- package/dist/infra/tofu-runner.js.map +1 -1
- package/dist/infra/types.d.ts +239 -16
- package/dist/infra/types.d.ts.map +1 -1
- package/dist/infra/types.js +206 -18
- package/dist/infra/types.js.map +1 -1
- package/dist/infra-templates/TODO-azure-prod.md +119 -0
- package/dist/infra-templates/modules/azure-container-app/main.tf +124 -0
- package/dist/infra-templates/modules/azure-container-app/outputs.tf +24 -0
- package/dist/infra-templates/modules/azure-container-app/variables.tf +80 -0
- package/dist/infra-templates/modules/azure-container-apps-environment/main.tf +18 -0
- package/dist/infra-templates/modules/azure-container-apps-environment/outputs.tf +14 -0
- package/dist/infra-templates/modules/azure-container-apps-environment/variables.tf +24 -0
- package/dist/infra-templates/modules/azure-container-registry/main.tf +10 -0
- package/dist/infra-templates/modules/azure-container-registry/outputs.tf +14 -0
- package/dist/infra-templates/modules/azure-container-registry/variables.tf +20 -0
- package/dist/infra-templates/modules/azure-front-door/main.tf +80 -0
- package/dist/infra-templates/modules/azure-front-door/outputs.tf +20 -0
- package/dist/infra-templates/modules/azure-front-door/variables.tf +26 -0
- package/dist/infra-templates/modules/azure-github-oidc/main.tf +48 -0
- package/dist/infra-templates/modules/azure-github-oidc/outputs.tf +9 -0
- package/dist/infra-templates/modules/azure-github-oidc/variables.tf +69 -0
- package/dist/infra-templates/modules/azure-key-vault/main.tf +21 -0
- package/dist/infra-templates/modules/azure-key-vault/outputs.tf +11 -0
- package/dist/infra-templates/modules/azure-key-vault/variables.tf +43 -0
- package/dist/infra-templates/modules/azure-monitor/main.tf +29 -0
- package/dist/infra-templates/modules/azure-monitor/outputs.tf +17 -0
- package/dist/infra-templates/modules/azure-monitor/variables.tf +32 -0
- package/dist/infra-templates/modules/azure-network/main.tf +134 -0
- package/dist/infra-templates/modules/azure-network/outputs.tf +44 -0
- package/dist/infra-templates/modules/azure-network/variables.tf +56 -0
- package/dist/infra-templates/modules/azure-postgresql-flexible/main.tf +34 -0
- package/dist/infra-templates/modules/azure-postgresql-flexible/outputs.tf +20 -0
- package/dist/infra-templates/modules/azure-postgresql-flexible/variables.tf +54 -0
- package/dist/infra-templates/modules/azure-redis-cache/main.tf +32 -0
- package/dist/infra-templates/modules/azure-redis-cache/outputs.tf +15 -0
- package/dist/infra-templates/modules/azure-redis-cache/variables.tf +30 -0
- package/dist/infra-templates/modules/azure-resource-group/main.tf +5 -0
- package/dist/infra-templates/modules/azure-resource-group/outputs.tf +11 -0
- package/dist/infra-templates/modules/azure-resource-group/variables.tf +16 -0
- package/dist/infra-templates/modules/azure-sql-database/main.tf +63 -0
- package/dist/infra-templates/modules/azure-sql-database/outputs.tf +20 -0
- package/dist/infra-templates/modules/azure-sql-database/variables.tf +78 -0
- package/dist/infra-templates/modules/azure-storage-account/main.tf +13 -0
- package/dist/infra-templates/modules/azure-storage-account/outputs.tf +16 -0
- package/dist/infra-templates/modules/azure-storage-account/variables.tf +38 -0
- package/dist/infra-templates/modules/cloud-watch/event-bridge.tf +15 -0
- package/dist/infra-templates/modules/cloud-watch/lambda.tf +84 -0
- package/dist/infra-templates/modules/cloud-watch/lambda_function.zip +0 -0
- package/dist/infra-templates/modules/cloud-watch/lambda_function_deploy.zip +0 -0
- package/dist/infra-templates/modules/cloud-watch/main.tf +156 -0
- package/dist/infra-templates/modules/cloud-watch/sns.tf +3 -0
- package/dist/infra-templates/modules/cloud-watch/variables.tf +38 -0
- package/dist/infra-templates/modules/ecs-core/alb.tf +126 -0
- package/dist/infra-templates/modules/ecs-core/cloudwatch.tf +48 -0
- package/dist/infra-templates/modules/ecs-core/iam.tf +95 -0
- package/dist/infra-templates/modules/ecs-core/main.tf +12 -0
- package/dist/infra-templates/modules/ecs-core/outputs.tf +44 -0
- package/dist/infra-templates/modules/ecs-core/secrets.tf +9 -0
- package/dist/infra-templates/modules/ecs-core/security_groups.tf +73 -0
- package/dist/infra-templates/modules/ecs-core/variables.tf +96 -0
- package/dist/infra-templates/modules/ecs-service/ecs_autoscaling.tf +51 -0
- package/dist/infra-templates/modules/ecs-service/ecs_service.tf +39 -0
- package/dist/infra-templates/modules/ecs-service/ecs_task_definition.tf +56 -0
- package/dist/infra-templates/modules/ecs-service/variables.tf +170 -0
- package/dist/infra-templates/modules/elastic-container-registry/main.tf +32 -0
- package/dist/infra-templates/modules/elastic-container-registry/outputs.tf +9 -0
- package/dist/infra-templates/modules/elastic-container-registry/variables.tf +25 -0
- package/dist/infra-templates/modules/github-oidc-role/main.tf +46 -0
- package/dist/infra-templates/modules/github-oidc-role/outputs.tf +4 -0
- package/dist/infra-templates/modules/github-oidc-role/variables.tf +11 -0
- package/dist/infra-templates/modules/guardduty/main.tf +8 -0
- package/dist/infra-templates/modules/iam/main.tf +205 -0
- package/dist/infra-templates/modules/iam/outputs.tf +3 -0
- package/dist/infra-templates/modules/iam/variables.tf +4 -0
- package/dist/infra-templates/modules/random-key/main.tf +11 -0
- package/dist/infra-templates/modules/random-key/outputs.tf +4 -0
- package/dist/infra-templates/modules/random-key/variables.tf +10 -0
- package/dist/infra-templates/modules/redis/main.tf +47 -0
- package/dist/infra-templates/modules/redis/outputs.tf +8 -0
- package/dist/infra-templates/modules/redis/variables.tf +20 -0
- package/dist/infra-templates/modules/relational-database-service/main.tf +44 -0
- package/dist/infra-templates/modules/relational-database-service/outputs.tf +13 -0
- package/dist/infra-templates/modules/relational-database-service/security-group.tf +15 -0
- package/dist/infra-templates/modules/relational-database-service/variables.tf +50 -0
- package/dist/infra-templates/modules/secret/main.tf +14 -0
- package/dist/infra-templates/modules/secret/outputs.tf +3 -0
- package/dist/infra-templates/modules/secret/variables.tf +8 -0
- package/dist/infra-templates/modules/simple-storage-service/main.tf +28 -0
- package/dist/infra-templates/modules/simple-storage-service/variables.tf +4 -0
- package/dist/infra-templates/modules/vpc/data.tf +2 -0
- package/dist/infra-templates/modules/vpc/main.tf +76 -0
- package/dist/infra-templates/modules/vpc/outputs.tf +23 -0
- package/dist/infra-templates/modules/vpc/variables.tf +5 -0
- package/dist/infra-templates/package.json +6 -0
- package/dist/infra-templates/templates/azure/MANUAL_DEPLOY.md +168 -0
- package/dist/infra-templates/templates/azure/backend.tf +8 -0
- package/dist/infra-templates/templates/azure/data.tf +21 -0
- package/dist/infra-templates/templates/azure/default.tfvars +0 -0
- package/dist/infra-templates/templates/azure/locals.tf +116 -0
- package/dist/infra-templates/templates/azure/main.tf +421 -0
- package/dist/infra-templates/templates/azure/outputs.tf +40 -0
- package/dist/infra-templates/templates/azure/providers.tf +65 -0
- package/dist/infra-templates/templates/azure/variables.tf +69 -0
- package/dist/infra-templates/templates/azure/workflows/build_and_deploy.yml +155 -0
- package/dist/infra-templates/templates/azure/workflows/tests.yml +63 -0
- package/dist/infra-templates/templates/ecs/README.md +225 -0
- package/dist/infra-templates/templates/ecs/backend.tf +7 -0
- package/dist/infra-templates/templates/ecs/data.tf +6 -0
- package/dist/infra-templates/templates/ecs/default.tfvars +1 -0
- package/dist/infra-templates/templates/ecs/locals.tf +97 -0
- package/dist/infra-templates/templates/ecs/main.tf +185 -0
- package/dist/infra-templates/templates/ecs/outputs.tf +42 -0
- package/dist/infra-templates/templates/ecs/providers.tf +21 -0
- package/dist/infra-templates/templates/ecs/variables.tf +105 -0
- package/dist/infra-templates/templates/ecs/workflows/build_and_deploy.yml +188 -0
- package/dist/infra-templates/templates/ecs/workflows/tests.yml +65 -0
- package/package.json +10 -2
- 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
|
-
|
|
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
|
|
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
|
-
// ---
|
|
559
|
-
async function
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
}
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
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
|
-
|
|
618
|
-
|
|
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
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
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
|
|
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
|
-
|
|
637
|
-
|
|
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
|
-
|
|
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
|
|
656
|
-
|
|
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();
|