@lsts_tech/infra 1.0.0 → 1.0.2
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 +58 -70
- package/dist/bin/init.d.ts +4 -3
- package/dist/bin/init.d.ts.map +1 -1
- package/dist/bin/init.js +619 -117
- package/dist/bin/init.js.map +1 -1
- package/dist/src/auth/index.d.ts +17 -0
- package/dist/src/auth/index.d.ts.map +1 -0
- package/dist/src/auth/index.js +18 -0
- package/dist/src/auth/index.js.map +1 -0
- package/dist/stacks/Dns.d.ts +24 -14
- package/dist/stacks/Dns.d.ts.map +1 -1
- package/dist/stacks/Dns.js +69 -18
- package/dist/stacks/Dns.js.map +1 -1
- package/dist/stacks/Pipeline.d.ts +7 -0
- package/dist/stacks/Pipeline.d.ts.map +1 -1
- package/dist/stacks/Pipeline.js +60 -7
- package/dist/stacks/Pipeline.js.map +1 -1
- package/docs/CLI.md +58 -15
- package/docs/CONFIGURATION.md +73 -30
- package/docs/EXAMPLES.md +5 -1
- package/examples/delegated-subdomain/infra.config.ts +102 -0
- package/examples/next-and-expo/infra.config.ts +33 -28
- package/examples/next-only/infra.config.ts +35 -22
- package/package.json +10 -4
- package/scripts/ensure-pipelines.sh +151 -43
- package/scripts/postdeploy-update-dns.sh +42 -11
- package/scripts/predeploy-checks.sh +38 -5
- package/templates/buildspec.yml +23 -0
- package/templates/ensure-pipelines.sh +157 -22
- package/templates/env.example +15 -0
- package/templates/infra.config.expo-web.ts +153 -0
- package/templates/infra.config.next-only.ts +159 -0
- package/templates/infra.config.ts +21 -4
- package/templates/pipelines.example.json +19 -0
- package/templates/private.example.json +13 -0
- package/templates/scaffold.gitignore +29 -0
- package/templates/scaffold.package.json +25 -0
- package/templates/scaffold.tsconfig.json +22 -0
- package/templates/secrets.schema.expo-web.json +8 -0
package/dist/bin/init.js
CHANGED
|
@@ -1,82 +1,77 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
/**
|
|
3
|
-
* @lsts_tech/infra — CLI
|
|
3
|
+
* @lsts_tech/infra — CLI
|
|
4
4
|
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
5
|
+
* Commands:
|
|
6
|
+
* - init: scaffold white-label infra files
|
|
7
|
+
* - doctor: validate AWS/domain/pipeline readiness before deploy
|
|
7
8
|
*/
|
|
8
|
-
import {
|
|
9
|
-
import {
|
|
9
|
+
import { spawnSync } from "node:child_process";
|
|
10
|
+
import { chmodSync, existsSync, mkdirSync, readFileSync, writeFileSync, } from "node:fs";
|
|
11
|
+
import { dirname, join, resolve } from "node:path";
|
|
10
12
|
import { fileURLToPath } from "node:url";
|
|
11
13
|
const __filename = fileURLToPath(import.meta.url);
|
|
12
14
|
const __dirname = dirname(__filename);
|
|
13
|
-
const
|
|
15
|
+
const PACKAGE_ROOT = resolve(__dirname, "..", "..");
|
|
16
|
+
const TEMPLATES_DIR = resolve(PACKAGE_ROOT, "templates");
|
|
17
|
+
const SCRIPTS_DIR = resolve(PACKAGE_ROOT, "scripts");
|
|
14
18
|
const PIPELINE_DEFS = {
|
|
15
19
|
production: { suffix: "prod", defaultBranch: "main" },
|
|
16
20
|
dev: { suffix: "dev", defaultBranch: "develop" },
|
|
17
21
|
mobile: { suffix: "mobile", defaultBranch: "mobile" },
|
|
18
22
|
};
|
|
19
|
-
const
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
{
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
target: ".env.example",
|
|
38
|
-
description: "Infrastructure environment template",
|
|
39
|
-
},
|
|
40
|
-
{
|
|
41
|
-
source: "secrets.schema.json",
|
|
42
|
-
target: "schemas/secrets.schema.json",
|
|
43
|
-
description: "Secrets schema definition",
|
|
44
|
-
},
|
|
45
|
-
{
|
|
46
|
-
source: "ensure-pipelines.sh",
|
|
47
|
-
target: "scripts/ensure-pipelines.sh",
|
|
48
|
-
description: "Pipeline management script",
|
|
49
|
-
},
|
|
50
|
-
{
|
|
51
|
-
source: "buildspec.yml",
|
|
52
|
-
target: "buildspec.yml",
|
|
53
|
-
description: "CodeBuild build specification",
|
|
54
|
-
},
|
|
55
|
-
];
|
|
23
|
+
const PROFILE_TEMPLATES = {
|
|
24
|
+
"next-only": "infra.config.next-only.ts",
|
|
25
|
+
"next-expo": "infra.config.ts",
|
|
26
|
+
"expo-web": "infra.config.expo-web.ts",
|
|
27
|
+
};
|
|
28
|
+
function readPackageVersion() {
|
|
29
|
+
try {
|
|
30
|
+
const raw = readFileSync(join(PACKAGE_ROOT, "package.json"), "utf-8");
|
|
31
|
+
const parsed = JSON.parse(raw);
|
|
32
|
+
if (parsed.version && parsed.version.trim().length > 0) {
|
|
33
|
+
return parsed.version.trim();
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
catch {
|
|
37
|
+
// best-effort fallback
|
|
38
|
+
}
|
|
39
|
+
return "1.0.1";
|
|
40
|
+
}
|
|
56
41
|
function printHelp() {
|
|
57
42
|
console.log(`
|
|
58
|
-
@lsts_tech/infra —
|
|
43
|
+
@lsts_tech/infra — CLI
|
|
59
44
|
|
|
60
45
|
Usage:
|
|
61
|
-
npx @lsts_tech/infra
|
|
46
|
+
npx @lsts_tech/infra <command> [options]
|
|
47
|
+
|
|
48
|
+
Commands:
|
|
49
|
+
init Scaffold environment-driven infra files
|
|
50
|
+
doctor Validate AWS/domain/pipeline readiness
|
|
62
51
|
|
|
63
|
-
|
|
64
|
-
--provider <name>
|
|
65
|
-
--project <slug>
|
|
66
|
-
--app-name <name>
|
|
67
|
-
--domain <domain>
|
|
68
|
-
--repo <owner/repo>
|
|
69
|
-
--pipelines <list>
|
|
70
|
-
--
|
|
71
|
-
--
|
|
72
|
-
--
|
|
73
|
-
--
|
|
74
|
-
--
|
|
52
|
+
Init options:
|
|
53
|
+
--provider <name> Cloud provider (v1 supports: aws)
|
|
54
|
+
--project <slug> Project/app prefix (default: myapp)
|
|
55
|
+
--app-name <name> SST app name (default: --project)
|
|
56
|
+
--domain <domain> Root domain (default: example.com)
|
|
57
|
+
--repo <owner/repo> GitHub repo for pipelines (default: myorg/myrepo)
|
|
58
|
+
--pipelines <list> CSV: production,dev,mobile or 'none' (default: production,dev)
|
|
59
|
+
--profile <name> next-only | next-expo | expo-web (default: next-only)
|
|
60
|
+
--with-expo Legacy shorthand for --profile next-expo
|
|
61
|
+
--pipeline-permissions <mode> admin | least-privilege (default: admin)
|
|
62
|
+
--infra-path <path> Infra path from monorepo root (default: packages/infra)
|
|
63
|
+
--target <path> Directory to scaffold into (default: current directory)
|
|
64
|
+
--force Overwrite existing files
|
|
65
|
+
|
|
66
|
+
Doctor options:
|
|
67
|
+
--target <path> Infra directory to inspect (default: current directory)
|
|
68
|
+
--region <aws-region> AWS region hint for checks (default: AWS_REGION or us-east-1)
|
|
69
|
+
--strict Fail if warnings exist
|
|
75
70
|
|
|
76
71
|
Examples:
|
|
77
72
|
npx @lsts_tech/infra init --project acme --domain acme.com --repo acme/web
|
|
78
|
-
npx @lsts_tech/infra init --project acme --pipelines production,
|
|
79
|
-
npx @lsts_tech/infra
|
|
73
|
+
npx @lsts_tech/infra init --project acme --profile expo-web --pipelines production,mobile
|
|
74
|
+
npx @lsts_tech/infra doctor --target packages/infra --strict
|
|
80
75
|
`);
|
|
81
76
|
}
|
|
82
77
|
function parseArgs(args) {
|
|
@@ -163,28 +158,23 @@ function parsePipelines(raw) {
|
|
|
163
158
|
}
|
|
164
159
|
return deduped;
|
|
165
160
|
}
|
|
166
|
-
function
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
? "branch-dev"
|
|
184
|
-
: "branch-mobile";
|
|
185
|
-
return ` ["${name}"]="__${branchFlag.toUpperCase().replace(/-/g, "_")}__"`;
|
|
186
|
-
})
|
|
187
|
-
.join("\n");
|
|
161
|
+
function parseProfile(raw) {
|
|
162
|
+
const normalized = raw.trim().toLowerCase();
|
|
163
|
+
if (normalized === "next" || normalized === "next-only")
|
|
164
|
+
return "next-only";
|
|
165
|
+
if (normalized === "next-expo" || normalized === "next+expo")
|
|
166
|
+
return "next-expo";
|
|
167
|
+
if (normalized === "expo" || normalized === "expo-web")
|
|
168
|
+
return "expo-web";
|
|
169
|
+
throw new Error(`Invalid profile: ${raw}. Allowed: next-only,next-expo,expo-web`);
|
|
170
|
+
}
|
|
171
|
+
function parsePipelinePermissionsMode(raw) {
|
|
172
|
+
const normalized = raw.trim().toLowerCase();
|
|
173
|
+
if (normalized === "admin")
|
|
174
|
+
return "admin";
|
|
175
|
+
if (normalized === "least-privilege" || normalized === "least_privilege")
|
|
176
|
+
return "least-privilege";
|
|
177
|
+
throw new Error(`Invalid pipeline permissions mode: ${raw}. Allowed: admin,least-privilege`);
|
|
188
178
|
}
|
|
189
179
|
function applyTemplate(content, replacements) {
|
|
190
180
|
let output = content;
|
|
@@ -193,7 +183,130 @@ function applyTemplate(content, replacements) {
|
|
|
193
183
|
}
|
|
194
184
|
return output;
|
|
195
185
|
}
|
|
186
|
+
function getScaffoldFiles(profile) {
|
|
187
|
+
const files = [
|
|
188
|
+
{
|
|
189
|
+
sourceDir: "templates",
|
|
190
|
+
source: "sst.config.ts",
|
|
191
|
+
target: "sst.config.ts",
|
|
192
|
+
description: "SST app entrypoint",
|
|
193
|
+
templated: true,
|
|
194
|
+
},
|
|
195
|
+
{
|
|
196
|
+
sourceDir: "templates",
|
|
197
|
+
source: "sst-env.d.ts",
|
|
198
|
+
target: "sst-env.d.ts",
|
|
199
|
+
description: "SST type stubs",
|
|
200
|
+
templated: false,
|
|
201
|
+
},
|
|
202
|
+
{
|
|
203
|
+
sourceDir: "templates",
|
|
204
|
+
source: PROFILE_TEMPLATES[profile],
|
|
205
|
+
target: "infra.config.ts",
|
|
206
|
+
description: "Infrastructure configuration",
|
|
207
|
+
templated: true,
|
|
208
|
+
},
|
|
209
|
+
{
|
|
210
|
+
sourceDir: "templates",
|
|
211
|
+
source: "env.example",
|
|
212
|
+
target: ".env.example",
|
|
213
|
+
description: "Infrastructure environment template",
|
|
214
|
+
templated: true,
|
|
215
|
+
},
|
|
216
|
+
{
|
|
217
|
+
sourceDir: "templates",
|
|
218
|
+
source: profile === "expo-web" ? "secrets.schema.expo-web.json" : "secrets.schema.json",
|
|
219
|
+
target: "schemas/secrets.schema.json",
|
|
220
|
+
description: "Secrets schema definition",
|
|
221
|
+
templated: false,
|
|
222
|
+
},
|
|
223
|
+
{
|
|
224
|
+
sourceDir: "templates",
|
|
225
|
+
source: "ensure-pipelines.sh",
|
|
226
|
+
target: "scripts/ensure-pipelines.sh",
|
|
227
|
+
description: "Pipeline management script",
|
|
228
|
+
executable: true,
|
|
229
|
+
templated: true,
|
|
230
|
+
},
|
|
231
|
+
{
|
|
232
|
+
sourceDir: "scripts",
|
|
233
|
+
source: "predeploy-checks.sh",
|
|
234
|
+
target: "scripts/predeploy-checks.sh",
|
|
235
|
+
description: "Pre-deploy checks",
|
|
236
|
+
executable: true,
|
|
237
|
+
templated: false,
|
|
238
|
+
},
|
|
239
|
+
{
|
|
240
|
+
sourceDir: "scripts",
|
|
241
|
+
source: "postdeploy-update-dns.sh",
|
|
242
|
+
target: "scripts/postdeploy-update-dns.sh",
|
|
243
|
+
description: "Post-deploy DNS sync script",
|
|
244
|
+
executable: true,
|
|
245
|
+
templated: false,
|
|
246
|
+
},
|
|
247
|
+
{
|
|
248
|
+
sourceDir: "scripts",
|
|
249
|
+
source: "sst-deploy.sh",
|
|
250
|
+
target: "scripts/sst-deploy.sh",
|
|
251
|
+
description: "CI-safe SST deploy wrapper",
|
|
252
|
+
executable: true,
|
|
253
|
+
templated: false,
|
|
254
|
+
},
|
|
255
|
+
{
|
|
256
|
+
sourceDir: "scripts",
|
|
257
|
+
source: "ensure-secrets.sh",
|
|
258
|
+
target: "scripts/ensure-secrets.sh",
|
|
259
|
+
description: "Secret bootstrap script",
|
|
260
|
+
executable: true,
|
|
261
|
+
templated: false,
|
|
262
|
+
},
|
|
263
|
+
{
|
|
264
|
+
sourceDir: "templates",
|
|
265
|
+
source: "buildspec.yml",
|
|
266
|
+
target: "buildspec.yml",
|
|
267
|
+
description: "CodeBuild build specification",
|
|
268
|
+
templated: true,
|
|
269
|
+
},
|
|
270
|
+
{
|
|
271
|
+
sourceDir: "templates",
|
|
272
|
+
source: "scaffold.package.json",
|
|
273
|
+
target: "package.json",
|
|
274
|
+
description: "Infra package manifest",
|
|
275
|
+
templated: true,
|
|
276
|
+
},
|
|
277
|
+
{
|
|
278
|
+
sourceDir: "templates",
|
|
279
|
+
source: "scaffold.tsconfig.json",
|
|
280
|
+
target: "tsconfig.json",
|
|
281
|
+
description: "Infra TypeScript configuration",
|
|
282
|
+
templated: true,
|
|
283
|
+
},
|
|
284
|
+
{
|
|
285
|
+
sourceDir: "templates",
|
|
286
|
+
source: "scaffold.gitignore",
|
|
287
|
+
target: ".gitignore",
|
|
288
|
+
description: "Infra gitignore template",
|
|
289
|
+
templated: false,
|
|
290
|
+
},
|
|
291
|
+
{
|
|
292
|
+
sourceDir: "templates",
|
|
293
|
+
source: "pipelines.example.json",
|
|
294
|
+
target: "config/pipelines.example.json",
|
|
295
|
+
description: "Runtime pipeline config example",
|
|
296
|
+
templated: true,
|
|
297
|
+
},
|
|
298
|
+
{
|
|
299
|
+
sourceDir: "templates",
|
|
300
|
+
source: "private.example.json",
|
|
301
|
+
target: "config/private.example.json",
|
|
302
|
+
description: "Private local config example",
|
|
303
|
+
templated: true,
|
|
304
|
+
},
|
|
305
|
+
];
|
|
306
|
+
return files;
|
|
307
|
+
}
|
|
196
308
|
function buildReplacements(options) {
|
|
309
|
+
const infraVersion = readPackageVersion();
|
|
197
310
|
return {
|
|
198
311
|
"__PROVIDER__": options.provider,
|
|
199
312
|
"__PROJECT_PREFIX__": options.project,
|
|
@@ -201,40 +314,390 @@ function buildReplacements(options) {
|
|
|
201
314
|
"__ROOT_DOMAIN__": options.domain,
|
|
202
315
|
"__PIPELINE_REPO__": options.repo,
|
|
203
316
|
"__PIPELINES_DEFAULT__": options.pipelines.join(","),
|
|
204
|
-
"__ENABLE_EXPO_SITE__": options.
|
|
317
|
+
"__ENABLE_EXPO_SITE__": options.profile === "next-expo" || options.profile === "expo-web" ? "true" : "false",
|
|
318
|
+
"__PROFILE__": options.profile,
|
|
205
319
|
"__INFRA_PATH__": options.infraPath,
|
|
206
|
-
"
|
|
207
|
-
"
|
|
208
|
-
"
|
|
320
|
+
"__CREATE_PIPELINES_DEFAULT__": "false",
|
|
321
|
+
"__PIPELINE_PERMISSIONS_MODE__": options.pipelinePermissionsMode,
|
|
322
|
+
"__INFRA_VERSION__": `^${infraVersion}`,
|
|
209
323
|
};
|
|
210
324
|
}
|
|
211
|
-
function
|
|
212
|
-
const
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
325
|
+
function commandExists(name) {
|
|
326
|
+
const check = spawnSync("bash", ["-lc", `command -v ${name}`], {
|
|
327
|
+
stdio: "ignore",
|
|
328
|
+
});
|
|
329
|
+
return check.status === 0;
|
|
330
|
+
}
|
|
331
|
+
function parseEnvFile(envPath) {
|
|
332
|
+
if (!existsSync(envPath)) {
|
|
333
|
+
return {};
|
|
216
334
|
}
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
335
|
+
const raw = readFileSync(envPath, "utf-8");
|
|
336
|
+
const output = {};
|
|
337
|
+
for (const line of raw.split(/\r?\n/)) {
|
|
338
|
+
const trimmed = line.trim();
|
|
339
|
+
if (!trimmed || trimmed.startsWith("#")) {
|
|
340
|
+
continue;
|
|
341
|
+
}
|
|
342
|
+
const match = trimmed.match(/^([A-Za-z_][A-Za-z0-9_]*)=(.*)$/);
|
|
343
|
+
if (!match) {
|
|
344
|
+
continue;
|
|
345
|
+
}
|
|
346
|
+
const key = match[1];
|
|
347
|
+
let value = match[2].trim();
|
|
348
|
+
if ((value.startsWith("\"") && value.endsWith("\"")) ||
|
|
349
|
+
(value.startsWith("'") && value.endsWith("'"))) {
|
|
350
|
+
value = value.slice(1, -1);
|
|
351
|
+
}
|
|
352
|
+
output[key] = value;
|
|
353
|
+
}
|
|
354
|
+
return output;
|
|
355
|
+
}
|
|
356
|
+
function runCommand(binary, args, cwd) {
|
|
357
|
+
return spawnSync(binary, args, {
|
|
358
|
+
cwd,
|
|
359
|
+
encoding: "utf-8",
|
|
360
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
361
|
+
});
|
|
362
|
+
}
|
|
363
|
+
function runAwsJson(args, cwd) {
|
|
364
|
+
const result = runCommand("aws", [...args, "--output", "json"], cwd);
|
|
365
|
+
if (result.status !== 0) {
|
|
366
|
+
return null;
|
|
367
|
+
}
|
|
368
|
+
try {
|
|
369
|
+
return JSON.parse(result.stdout || "{}");
|
|
370
|
+
}
|
|
371
|
+
catch {
|
|
372
|
+
return null;
|
|
373
|
+
}
|
|
374
|
+
}
|
|
375
|
+
function domainCandidates(domain) {
|
|
376
|
+
const labels = domain
|
|
377
|
+
.toLowerCase()
|
|
378
|
+
.replace(/\.$/, "")
|
|
379
|
+
.split(".")
|
|
380
|
+
.filter(Boolean);
|
|
381
|
+
if (labels.length < 2) {
|
|
382
|
+
return [domain.replace(/\.$/, "")];
|
|
383
|
+
}
|
|
384
|
+
const candidates = [];
|
|
385
|
+
for (let index = 0; index <= labels.length - 2; index++) {
|
|
386
|
+
candidates.push(labels.slice(index).join("."));
|
|
387
|
+
}
|
|
388
|
+
return Array.from(new Set(candidates));
|
|
389
|
+
}
|
|
390
|
+
function findHostedZone(domain, cwd) {
|
|
391
|
+
for (const candidate of domainCandidates(domain)) {
|
|
392
|
+
const zones = runAwsJson(["route53", "list-hosted-zones-by-name", "--dns-name", candidate], cwd);
|
|
393
|
+
const hostedZones = Array.isArray(zones?.HostedZones) ? zones.HostedZones : [];
|
|
394
|
+
for (const zone of hostedZones) {
|
|
395
|
+
const zoneName = String(zone?.Name ?? "").replace(/\.$/, "").toLowerCase();
|
|
396
|
+
if (zoneName !== candidate.toLowerCase()) {
|
|
397
|
+
continue;
|
|
398
|
+
}
|
|
399
|
+
const zoneIdRaw = String(zone?.Id ?? "");
|
|
400
|
+
const zoneId = zoneIdRaw.replace("/hostedzone/", "");
|
|
401
|
+
return {
|
|
402
|
+
requested: domain,
|
|
403
|
+
matchedCandidate: candidate,
|
|
404
|
+
zoneName,
|
|
405
|
+
zoneId,
|
|
406
|
+
};
|
|
407
|
+
}
|
|
408
|
+
}
|
|
409
|
+
return null;
|
|
410
|
+
}
|
|
411
|
+
function isLikelyRepo(repo) {
|
|
412
|
+
return /^[A-Za-z0-9_.-]+\/[A-Za-z0-9_.-]+$/.test(repo);
|
|
413
|
+
}
|
|
414
|
+
function isBoolTrue(value, fallback = false) {
|
|
415
|
+
if (!value)
|
|
416
|
+
return fallback;
|
|
417
|
+
const normalized = value.trim().toLowerCase();
|
|
418
|
+
if (["1", "true", "yes", "y"].includes(normalized))
|
|
419
|
+
return true;
|
|
420
|
+
if (["0", "false", "no", "n"].includes(normalized))
|
|
421
|
+
return false;
|
|
422
|
+
return fallback;
|
|
423
|
+
}
|
|
424
|
+
function wildcardMatch(certDomain, domain) {
|
|
425
|
+
const normalizedCert = certDomain.toLowerCase();
|
|
426
|
+
const normalizedDomain = domain.toLowerCase();
|
|
427
|
+
if (normalizedCert === normalizedDomain) {
|
|
428
|
+
return true;
|
|
429
|
+
}
|
|
430
|
+
if (!normalizedCert.startsWith("*.")) {
|
|
431
|
+
return false;
|
|
432
|
+
}
|
|
433
|
+
const certSuffix = normalizedCert.slice(1); // ".example.com"
|
|
434
|
+
if (!normalizedDomain.endsWith(certSuffix)) {
|
|
435
|
+
return false;
|
|
436
|
+
}
|
|
437
|
+
// Wildcard should match one additional label at minimum.
|
|
438
|
+
const certLabelCount = normalizedCert.split(".").length;
|
|
439
|
+
const domainLabelCount = normalizedDomain.split(".").length;
|
|
440
|
+
return domainLabelCount >= certLabelCount;
|
|
441
|
+
}
|
|
442
|
+
function addDoctorResult(results, status, label, detail) {
|
|
443
|
+
results.push({ status, label, detail });
|
|
444
|
+
const icon = status === "PASS" ? "✅" : status === "WARN" ? "⚠️" : "❌";
|
|
445
|
+
console.log(`${icon} ${label}: ${detail}`);
|
|
446
|
+
}
|
|
447
|
+
function runDoctor(flags) {
|
|
448
|
+
const targetRaw = readFlag(flags, "target", ".");
|
|
449
|
+
const targetDir = resolve(process.cwd(), targetRaw);
|
|
450
|
+
const strict = readBool(flags, "strict", false);
|
|
451
|
+
const region = readFlag(flags, "region", process.env.AWS_REGION ?? "us-east-1");
|
|
452
|
+
const envFromFile = parseEnvFile(join(targetDir, ".env"));
|
|
453
|
+
const env = {
|
|
454
|
+
...envFromFile,
|
|
455
|
+
...Object.fromEntries(Object.entries(process.env)
|
|
456
|
+
.filter((entry) => typeof entry[1] === "string")
|
|
457
|
+
.map(([key, value]) => [key, value])),
|
|
458
|
+
};
|
|
459
|
+
const rootDomain = env.INFRA_ROOT_DOMAIN ?? env.DOMAIN_ROOT ?? "";
|
|
460
|
+
const pipelineRepo = env.INFRA_PIPELINE_REPO ?? "";
|
|
461
|
+
const pipelinePrefix = env.INFRA_PIPELINE_PREFIX ?? "myapp";
|
|
462
|
+
let selectedPipelines = [];
|
|
463
|
+
try {
|
|
464
|
+
selectedPipelines = parsePipelines(env.INFRA_PIPELINES ?? "production,dev");
|
|
465
|
+
}
|
|
466
|
+
catch (error) {
|
|
467
|
+
console.error(error instanceof Error ? error.message : String(error));
|
|
220
468
|
process.exit(1);
|
|
469
|
+
return;
|
|
470
|
+
}
|
|
471
|
+
const enableExpo = isBoolTrue(env.INFRA_ENABLE_EXPO_SITE, false) || env.INFRA_PROFILE === "expo-web";
|
|
472
|
+
const webStageMap = {
|
|
473
|
+
production: env.INFRA_WEB_DOMAIN_PRODUCTION ?? rootDomain,
|
|
474
|
+
dev: env.INFRA_WEB_DOMAIN_DEV ?? (rootDomain ? `dev.${rootDomain}` : ""),
|
|
475
|
+
mobile: env.INFRA_WEB_DOMAIN_MOBILE ?? (rootDomain ? `api.${rootDomain}` : ""),
|
|
476
|
+
};
|
|
477
|
+
const expoStageMap = {
|
|
478
|
+
production: env.INFRA_EXPO_DOMAIN_PRODUCTION ?? (rootDomain ? `mobile.${rootDomain}` : ""),
|
|
479
|
+
dev: env.INFRA_EXPO_DOMAIN_DEV ?? (rootDomain ? `dev.mobile.${rootDomain}` : ""),
|
|
480
|
+
mobile: env.INFRA_EXPO_DOMAIN_MOBILE ?? (rootDomain ? `preview.mobile.${rootDomain}` : ""),
|
|
481
|
+
};
|
|
482
|
+
const results = [];
|
|
483
|
+
console.log("\n🩺 @lsts_tech/infra doctor\n");
|
|
484
|
+
console.log(`target : ${targetDir}`);
|
|
485
|
+
console.log(`region : ${region}`);
|
|
486
|
+
if (!existsSync(targetDir)) {
|
|
487
|
+
console.error(`Target directory does not exist: ${targetDir}`);
|
|
488
|
+
process.exit(1);
|
|
489
|
+
}
|
|
490
|
+
if (!rootDomain) {
|
|
491
|
+
addDoctorResult(results, "FAIL", "Root domain", "INFRA_ROOT_DOMAIN (or DOMAIN_ROOT) is required.");
|
|
492
|
+
}
|
|
493
|
+
else {
|
|
494
|
+
addDoctorResult(results, "PASS", "Root domain", rootDomain);
|
|
495
|
+
}
|
|
496
|
+
if (!pipelineRepo) {
|
|
497
|
+
addDoctorResult(results, "WARN", "Pipeline repo", "INFRA_PIPELINE_REPO is not set.");
|
|
498
|
+
}
|
|
499
|
+
else if (!isLikelyRepo(pipelineRepo)) {
|
|
500
|
+
addDoctorResult(results, "WARN", "Pipeline repo", `Unexpected format: ${pipelineRepo} (expected owner/repo).`);
|
|
501
|
+
}
|
|
502
|
+
else {
|
|
503
|
+
addDoctorResult(results, "PASS", "Pipeline repo", pipelineRepo);
|
|
504
|
+
}
|
|
505
|
+
const permissionsMode = env.INFRA_PIPELINE_PERMISSIONS_MODE ?? "admin";
|
|
506
|
+
if (permissionsMode !== "admin" && permissionsMode !== "least-privilege") {
|
|
507
|
+
addDoctorResult(results, "FAIL", "Pipeline permissions mode", `Invalid value '${permissionsMode}'. Allowed: admin, least-privilege.`);
|
|
508
|
+
}
|
|
509
|
+
else {
|
|
510
|
+
addDoctorResult(results, "PASS", "Pipeline permissions mode", permissionsMode);
|
|
221
511
|
}
|
|
222
|
-
|
|
512
|
+
if (!commandExists("aws")) {
|
|
513
|
+
addDoctorResult(results, "FAIL", "AWS CLI", "aws CLI is not installed or not in PATH.");
|
|
514
|
+
}
|
|
515
|
+
else {
|
|
516
|
+
const sts = runAwsJson(["sts", "get-caller-identity"], targetDir);
|
|
517
|
+
if (!sts?.Account) {
|
|
518
|
+
addDoctorResult(results, "FAIL", "AWS auth", "Unable to call sts:get-caller-identity with current credentials.");
|
|
519
|
+
}
|
|
520
|
+
else {
|
|
521
|
+
addDoctorResult(results, "PASS", "AWS auth", `Authenticated as account ${String(sts.Account)} (${String(sts.Arn ?? "unknown")})`);
|
|
522
|
+
}
|
|
523
|
+
}
|
|
524
|
+
const stageDomainPairs = [];
|
|
525
|
+
for (const stage of ["production", "dev", "mobile"]) {
|
|
526
|
+
if (webStageMap[stage]) {
|
|
527
|
+
stageDomainPairs.push({ stage, kind: "web", domain: webStageMap[stage] });
|
|
528
|
+
}
|
|
529
|
+
if (enableExpo && expoStageMap[stage]) {
|
|
530
|
+
stageDomainPairs.push({ stage, kind: "expo", domain: expoStageMap[stage] });
|
|
531
|
+
}
|
|
532
|
+
}
|
|
533
|
+
const duplicates = new Map();
|
|
534
|
+
for (const entry of stageDomainPairs) {
|
|
535
|
+
duplicates.set(entry.domain, (duplicates.get(entry.domain) ?? 0) + 1);
|
|
536
|
+
if (!rootDomain || entry.domain.endsWith(rootDomain)) {
|
|
537
|
+
addDoctorResult(results, "PASS", `Stage domain (${entry.kind}:${entry.stage})`, entry.domain);
|
|
538
|
+
}
|
|
539
|
+
else {
|
|
540
|
+
addDoctorResult(results, "WARN", `Stage domain (${entry.kind}:${entry.stage})`, `${entry.domain} does not end with root domain ${rootDomain}.`);
|
|
541
|
+
}
|
|
542
|
+
}
|
|
543
|
+
for (const [domain, count] of duplicates.entries()) {
|
|
544
|
+
if (count > 1) {
|
|
545
|
+
addDoctorResult(results, "WARN", "Domain overlap", `${domain} is used by multiple stage mappings. Confirm this is intentional.`);
|
|
546
|
+
}
|
|
547
|
+
}
|
|
548
|
+
if (commandExists("aws") && rootDomain) {
|
|
549
|
+
const checkedDomains = Array.from(new Set(stageDomainPairs.map((entry) => entry.domain))).filter(Boolean);
|
|
550
|
+
for (const domain of checkedDomains) {
|
|
551
|
+
const zone = findHostedZone(domain, targetDir);
|
|
552
|
+
if (!zone) {
|
|
553
|
+
addDoctorResult(results, "FAIL", `Hosted zone (${domain})`, "No matching Route53 hosted zone found for domain or parent domains.");
|
|
554
|
+
continue;
|
|
555
|
+
}
|
|
556
|
+
if (zone.matchedCandidate === domain.toLowerCase()) {
|
|
557
|
+
addDoctorResult(results, "PASS", `Hosted zone (${domain})`, `Resolved exact zone ${zone.zoneName} (${zone.zoneId}).`);
|
|
558
|
+
}
|
|
559
|
+
else {
|
|
560
|
+
addDoctorResult(results, "WARN", `Hosted zone (${domain})`, `Falling back to parent zone ${zone.zoneName} (${zone.zoneId}) via candidate ${zone.matchedCandidate}.`);
|
|
561
|
+
}
|
|
562
|
+
}
|
|
563
|
+
}
|
|
564
|
+
const certMap = {
|
|
565
|
+
[webStageMap.production]: env.INFRA_WEB_CERT_ARN_PRODUCTION,
|
|
566
|
+
[webStageMap.dev]: env.INFRA_WEB_CERT_ARN_DEV,
|
|
567
|
+
[webStageMap.mobile]: env.INFRA_WEB_CERT_ARN_MOBILE,
|
|
568
|
+
};
|
|
569
|
+
if (enableExpo) {
|
|
570
|
+
certMap[expoStageMap.production] = env.INFRA_EXPO_CERT_ARN_PRODUCTION;
|
|
571
|
+
certMap[expoStageMap.dev] = env.INFRA_EXPO_CERT_ARN_DEV;
|
|
572
|
+
certMap[expoStageMap.mobile] = env.INFRA_EXPO_CERT_ARN_MOBILE;
|
|
573
|
+
}
|
|
574
|
+
const acmList = commandExists("aws")
|
|
575
|
+
? runAwsJson([
|
|
576
|
+
"acm",
|
|
577
|
+
"list-certificates",
|
|
578
|
+
"--region",
|
|
579
|
+
"us-east-1",
|
|
580
|
+
"--certificate-statuses",
|
|
581
|
+
"ISSUED",
|
|
582
|
+
"PENDING_VALIDATION",
|
|
583
|
+
], targetDir)
|
|
584
|
+
: null;
|
|
585
|
+
const acmDomains = Array.isArray(acmList?.CertificateSummaryList)
|
|
586
|
+
? acmList.CertificateSummaryList
|
|
587
|
+
.map((item) => item?.DomainName)
|
|
588
|
+
.filter((value) => typeof value === "string" && value.length > 0)
|
|
589
|
+
: [];
|
|
590
|
+
for (const domain of Array.from(new Set(Object.keys(certMap))).filter(Boolean)) {
|
|
591
|
+
const explicitArn = certMap[domain];
|
|
592
|
+
if (explicitArn && explicitArn.trim().length > 0) {
|
|
593
|
+
addDoctorResult(results, "PASS", `ACM (${domain})`, `Using explicit cert ARN (${explicitArn}).`);
|
|
594
|
+
continue;
|
|
595
|
+
}
|
|
596
|
+
const hasMatch = acmDomains.some((certDomain) => wildcardMatch(certDomain, domain));
|
|
597
|
+
if (hasMatch) {
|
|
598
|
+
addDoctorResult(results, "PASS", `ACM (${domain})`, "Found existing ISSUED/PENDING_VALIDATION certificate in us-east-1.");
|
|
599
|
+
}
|
|
600
|
+
else {
|
|
601
|
+
addDoctorResult(results, "WARN", `ACM (${domain})`, "No existing certificate found; SST will request one during deploy.");
|
|
602
|
+
}
|
|
603
|
+
}
|
|
604
|
+
if (commandExists("aws")) {
|
|
605
|
+
const explicitConnectionArn = env.INFRA_CODESTAR_CONNECTION_ARN ?? env.CODESTAR_CONNECTION_ARN;
|
|
606
|
+
if (explicitConnectionArn) {
|
|
607
|
+
const connection = runAwsJson(["codestar-connections", "get-connection", "--connection-arn", explicitConnectionArn], targetDir);
|
|
608
|
+
const status = connection?.Connection?.ConnectionStatus;
|
|
609
|
+
if (status === "AVAILABLE") {
|
|
610
|
+
addDoctorResult(results, "PASS", "CodeStar connection", `${explicitConnectionArn} is AVAILABLE.`);
|
|
611
|
+
}
|
|
612
|
+
else if (status) {
|
|
613
|
+
addDoctorResult(results, "WARN", "CodeStar connection", `${explicitConnectionArn} status is ${status}.`);
|
|
614
|
+
}
|
|
615
|
+
else {
|
|
616
|
+
addDoctorResult(results, "FAIL", "CodeStar connection", `Unable to fetch ${explicitConnectionArn}.`);
|
|
617
|
+
}
|
|
618
|
+
}
|
|
619
|
+
else {
|
|
620
|
+
const list = runAwsJson(["codestar-connections", "list-connections", "--provider-type-filter", "GitHub"], targetDir);
|
|
621
|
+
const connections = Array.isArray(list?.Connections) ? list.Connections : [];
|
|
622
|
+
const available = connections.filter((connection) => connection.ConnectionStatus === "AVAILABLE");
|
|
623
|
+
if (available.length > 0) {
|
|
624
|
+
addDoctorResult(results, "PASS", "CodeStar connection", `Found ${available.length} AVAILABLE GitHub connection(s).`);
|
|
625
|
+
}
|
|
626
|
+
else if (connections.length > 0) {
|
|
627
|
+
addDoctorResult(results, "WARN", "CodeStar connection", "Connections exist but none are AVAILABLE.");
|
|
628
|
+
}
|
|
629
|
+
else {
|
|
630
|
+
addDoctorResult(results, "WARN", "CodeStar connection", "No GitHub CodeStar connection found.");
|
|
631
|
+
}
|
|
632
|
+
}
|
|
633
|
+
}
|
|
634
|
+
if (selectedPipelines.length === 0) {
|
|
635
|
+
addDoctorResult(results, "PASS", "Pipeline selection", "No pipelines selected (INFRA_PIPELINES=none).");
|
|
636
|
+
}
|
|
637
|
+
else {
|
|
638
|
+
addDoctorResult(results, "PASS", "Pipeline selection", `Selected: ${selectedPipelines.join(",")}`);
|
|
639
|
+
}
|
|
640
|
+
const branchMap = {
|
|
641
|
+
production: env.INFRA_PIPELINE_BRANCH_PROD ?? PIPELINE_DEFS.production.defaultBranch,
|
|
642
|
+
dev: env.INFRA_PIPELINE_BRANCH_DEV ?? PIPELINE_DEFS.dev.defaultBranch,
|
|
643
|
+
mobile: env.INFRA_PIPELINE_BRANCH_MOBILE ?? PIPELINE_DEFS.mobile.defaultBranch,
|
|
644
|
+
};
|
|
645
|
+
for (const stage of selectedPipelines) {
|
|
646
|
+
const branch = branchMap[stage];
|
|
647
|
+
if (!branch || branch.trim().length === 0) {
|
|
648
|
+
addDoctorResult(results, "FAIL", `Pipeline branch (${stage})`, "Branch is empty.");
|
|
649
|
+
continue;
|
|
650
|
+
}
|
|
651
|
+
addDoctorResult(results, "PASS", `Pipeline branch (${stage})`, branch);
|
|
652
|
+
}
|
|
653
|
+
const createPipelines = isBoolTrue(env.INFRA_CREATE_PIPELINES, false);
|
|
654
|
+
if (createPipelines) {
|
|
655
|
+
addDoctorResult(results, "WARN", "Pipeline mutation mode", "INFRA_CREATE_PIPELINES=true. Production deploys will create/update pipelines.");
|
|
656
|
+
}
|
|
657
|
+
else {
|
|
658
|
+
addDoctorResult(results, "PASS", "Pipeline mutation mode", "INFRA_CREATE_PIPELINES=false (safe default).");
|
|
659
|
+
}
|
|
660
|
+
const failures = results.filter((result) => result.status === "FAIL").length;
|
|
661
|
+
const warnings = results.filter((result) => result.status === "WARN").length;
|
|
662
|
+
console.log("\nSummary");
|
|
663
|
+
console.log(` Failures : ${failures}`);
|
|
664
|
+
console.log(` Warnings : ${warnings}`);
|
|
665
|
+
console.log(` Result : ${failures > 0 || (strict && warnings > 0) ? "FAILED" : "PASSED"}`);
|
|
666
|
+
if (failures > 0 || (strict && warnings > 0)) {
|
|
667
|
+
process.exit(1);
|
|
668
|
+
}
|
|
669
|
+
}
|
|
670
|
+
function runInit(flags) {
|
|
671
|
+
const providerRaw = readFlag(flags, "provider", "aws").toLowerCase();
|
|
223
672
|
if (providerRaw !== "aws") {
|
|
224
|
-
console.error(`Unsupported provider: ${providerRaw}. v1
|
|
673
|
+
console.error(`Unsupported provider: ${providerRaw}. v1 currently supports only aws.`);
|
|
225
674
|
process.exit(1);
|
|
226
675
|
}
|
|
227
|
-
const project = toSlug(readFlag(
|
|
228
|
-
const appName = readFlag(
|
|
229
|
-
const domain = readFlag(
|
|
230
|
-
const repo = readFlag(
|
|
231
|
-
const infraPath = readFlag(
|
|
232
|
-
const withExpo = readBool(
|
|
233
|
-
const force = readBool(
|
|
234
|
-
const
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
676
|
+
const project = toSlug(readFlag(flags, "project", "myapp"));
|
|
677
|
+
const appName = readFlag(flags, "app-name", project);
|
|
678
|
+
const domain = readFlag(flags, "domain", "example.com");
|
|
679
|
+
const repo = readFlag(flags, "repo", "myorg/myrepo");
|
|
680
|
+
const infraPath = readFlag(flags, "infra-path", "packages/infra");
|
|
681
|
+
const withExpo = readBool(flags, "with-expo", false);
|
|
682
|
+
const force = readBool(flags, "force", false);
|
|
683
|
+
const profileFlag = readFlag(flags, "profile", "");
|
|
684
|
+
let profile;
|
|
685
|
+
try {
|
|
686
|
+
profile = profileFlag
|
|
687
|
+
? parseProfile(profileFlag)
|
|
688
|
+
: withExpo
|
|
689
|
+
? "next-expo"
|
|
690
|
+
: "next-only";
|
|
691
|
+
}
|
|
692
|
+
catch (error) {
|
|
693
|
+
console.error(error instanceof Error ? error.message : String(error));
|
|
694
|
+
process.exit(1);
|
|
695
|
+
return;
|
|
696
|
+
}
|
|
697
|
+
const branchProd = readFlag(flags, "branch-prod", PIPELINE_DEFS.production.defaultBranch);
|
|
698
|
+
const branchDev = readFlag(flags, "branch-dev", PIPELINE_DEFS.dev.defaultBranch);
|
|
699
|
+
const branchMobile = readFlag(flags, "branch-mobile", PIPELINE_DEFS.mobile.defaultBranch);
|
|
700
|
+
const pipelinesRaw = readFlag(flags, "pipelines", profile === "expo-web" ? "production,dev,mobile" : "production,dev");
|
|
238
701
|
let pipelines;
|
|
239
702
|
try {
|
|
240
703
|
pipelines = parsePipelines(pipelinesRaw);
|
|
@@ -244,7 +707,17 @@ function main() {
|
|
|
244
707
|
process.exit(1);
|
|
245
708
|
return;
|
|
246
709
|
}
|
|
247
|
-
const
|
|
710
|
+
const pipelinePermissionsModeRaw = readFlag(flags, "pipeline-permissions", "admin");
|
|
711
|
+
let pipelinePermissionsMode;
|
|
712
|
+
try {
|
|
713
|
+
pipelinePermissionsMode = parsePipelinePermissionsMode(pipelinePermissionsModeRaw);
|
|
714
|
+
}
|
|
715
|
+
catch (error) {
|
|
716
|
+
console.error(error instanceof Error ? error.message : String(error));
|
|
717
|
+
process.exit(1);
|
|
718
|
+
return;
|
|
719
|
+
}
|
|
720
|
+
const targetRaw = readFlag(flags, "target", ".");
|
|
248
721
|
const targetDir = resolve(process.cwd(), targetRaw);
|
|
249
722
|
const options = {
|
|
250
723
|
provider: "aws",
|
|
@@ -252,11 +725,12 @@ function main() {
|
|
|
252
725
|
domain,
|
|
253
726
|
repo,
|
|
254
727
|
pipelines,
|
|
255
|
-
|
|
728
|
+
profile,
|
|
256
729
|
appName,
|
|
257
730
|
infraPath,
|
|
258
731
|
targetDir,
|
|
259
732
|
force,
|
|
733
|
+
pipelinePermissionsMode,
|
|
260
734
|
};
|
|
261
735
|
const replacements = buildReplacements(options);
|
|
262
736
|
replacements["__BRANCH_PROD__"] = branchProd;
|
|
@@ -267,17 +741,25 @@ function main() {
|
|
|
267
741
|
console.log(` targetDir : ${options.targetDir}`);
|
|
268
742
|
console.log(` project : ${options.project}`);
|
|
269
743
|
console.log(` appName : ${options.appName}`);
|
|
744
|
+
console.log(` profile : ${options.profile}`);
|
|
270
745
|
console.log(` domain : ${options.domain}`);
|
|
271
746
|
console.log(` repo : ${options.repo}`);
|
|
272
747
|
console.log(` pipelines : ${options.pipelines.join(",") || "none"}`);
|
|
273
|
-
console.log(`
|
|
748
|
+
console.log(` permissions: ${options.pipelinePermissionsMode}`);
|
|
274
749
|
console.log("");
|
|
275
750
|
let created = 0;
|
|
276
751
|
let overwritten = 0;
|
|
277
752
|
let skipped = 0;
|
|
278
|
-
|
|
279
|
-
|
|
753
|
+
const files = getScaffoldFiles(profile);
|
|
754
|
+
for (const file of files) {
|
|
755
|
+
const root = file.sourceDir === "templates" ? TEMPLATES_DIR : SCRIPTS_DIR;
|
|
756
|
+
const sourcePath = join(root, file.source);
|
|
280
757
|
const targetPath = join(options.targetDir, file.target);
|
|
758
|
+
if (!existsSync(sourcePath)) {
|
|
759
|
+
console.warn(` ⚠️ ${file.target} — source missing (${sourcePath}), skipped`);
|
|
760
|
+
skipped++;
|
|
761
|
+
continue;
|
|
762
|
+
}
|
|
281
763
|
const exists = existsSync(targetPath);
|
|
282
764
|
if (exists && !options.force) {
|
|
283
765
|
console.log(` ⏭ ${file.target} — already exists (skipped)`);
|
|
@@ -289,9 +771,9 @@ function main() {
|
|
|
289
771
|
mkdirSync(targetParent, { recursive: true });
|
|
290
772
|
}
|
|
291
773
|
const raw = readFileSync(sourcePath, "utf-8");
|
|
292
|
-
const content = applyTemplate(raw, replacements);
|
|
774
|
+
const content = file.templated ? applyTemplate(raw, replacements) : raw;
|
|
293
775
|
writeFileSync(targetPath, content, "utf-8");
|
|
294
|
-
if (file.target.endsWith(".sh")) {
|
|
776
|
+
if (file.executable || file.target.endsWith(".sh")) {
|
|
295
777
|
chmodSync(targetPath, 0o755);
|
|
296
778
|
}
|
|
297
779
|
if (exists) {
|
|
@@ -305,11 +787,31 @@ function main() {
|
|
|
305
787
|
}
|
|
306
788
|
console.log(`\n📦 Done! Created ${created}, overwritten ${overwritten}, skipped ${skipped}.\n`);
|
|
307
789
|
console.log("Next steps:");
|
|
308
|
-
console.log(" 1.
|
|
309
|
-
console.log(" 2.
|
|
310
|
-
console.log(" 3.
|
|
311
|
-
console.log(" 4.
|
|
790
|
+
console.log(" 1. Copy .env.example to .env and set project values");
|
|
791
|
+
console.log(" 2. Review config/pipelines.example.json and create config/pipelines.json if needed");
|
|
792
|
+
console.log(" 3. Set SST secrets (npx sst secret set <Name> <value> --stage <stage>)");
|
|
793
|
+
console.log(" 4. Validate setup (npx @lsts_tech/infra doctor --target .)");
|
|
794
|
+
console.log(" 5. Deploy app infra (npx sst deploy --stage dev)");
|
|
795
|
+
console.log(" 6. Create pipelines explicitly (APPROVE=true bash scripts/ensure-pipelines.sh)");
|
|
312
796
|
console.log("");
|
|
313
797
|
}
|
|
798
|
+
function main() {
|
|
799
|
+
const parsed = parseArgs(process.argv.slice(2));
|
|
800
|
+
if (!parsed.command || parsed.command === "help" || parsed.flags.help) {
|
|
801
|
+
printHelp();
|
|
802
|
+
process.exit(0);
|
|
803
|
+
}
|
|
804
|
+
if (parsed.command === "init") {
|
|
805
|
+
runInit(parsed.flags);
|
|
806
|
+
return;
|
|
807
|
+
}
|
|
808
|
+
if (parsed.command === "doctor") {
|
|
809
|
+
runDoctor(parsed.flags);
|
|
810
|
+
return;
|
|
811
|
+
}
|
|
812
|
+
console.error(`Unknown command: ${parsed.command}`);
|
|
813
|
+
printHelp();
|
|
814
|
+
process.exit(1);
|
|
815
|
+
}
|
|
314
816
|
main();
|
|
315
817
|
//# sourceMappingURL=init.js.map
|