create-ampless 0.2.0-alpha.5 → 0.2.0-alpha.6
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/dist/index.js
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
// src/index.ts
|
|
4
4
|
import { spinner as spinner2, outro, log as log3 } from "@clack/prompts";
|
|
5
5
|
import { existsSync as existsSync2 } from "fs";
|
|
6
|
-
import { resolve as resolve4 } from "path";
|
|
6
|
+
import { basename as basename3, resolve as resolve4 } from "path";
|
|
7
7
|
|
|
8
8
|
// src/prompts.ts
|
|
9
9
|
import * as p from "@clack/prompts";
|
|
@@ -192,17 +192,24 @@ async function scaffold(sharedDir, themesRoot, destDir, opts) {
|
|
|
192
192
|
}
|
|
193
193
|
|
|
194
194
|
// src/args.ts
|
|
195
|
+
var VALID_THEMES = ["blog", "minimal", "landing", "corporate", "docs", "dads"];
|
|
196
|
+
var VALID_PLUGINS = ["seo", "rss", "webhook"];
|
|
195
197
|
var STRING_FLAGS = /* @__PURE__ */ new Set([
|
|
198
|
+
"--site-name",
|
|
199
|
+
"--themes",
|
|
200
|
+
"--plugins",
|
|
196
201
|
"--github-owner",
|
|
197
202
|
"--github-token",
|
|
198
203
|
"--aws-profile",
|
|
199
204
|
"--aws-region",
|
|
200
205
|
"--domain",
|
|
201
|
-
"--subdomain"
|
|
206
|
+
"--subdomain",
|
|
207
|
+
"--iam-service-role"
|
|
202
208
|
]);
|
|
203
209
|
var BOOLEAN_FLAGS = /* @__PURE__ */ new Set([
|
|
204
210
|
"--deploy",
|
|
205
211
|
"--github-private",
|
|
212
|
+
"--create-iam-role",
|
|
206
213
|
"--skip-confirm",
|
|
207
214
|
"--help",
|
|
208
215
|
"-h"
|
|
@@ -211,6 +218,7 @@ function parseDeployArgs(argv) {
|
|
|
211
218
|
const out = {
|
|
212
219
|
deploy: false,
|
|
213
220
|
githubPrivate: false,
|
|
221
|
+
createIamRole: false,
|
|
214
222
|
skipConfirm: false,
|
|
215
223
|
help: false,
|
|
216
224
|
unknown: []
|
|
@@ -232,6 +240,9 @@ function parseDeployArgs(argv) {
|
|
|
232
240
|
case "--github-private":
|
|
233
241
|
out.githubPrivate = true;
|
|
234
242
|
break;
|
|
243
|
+
case "--create-iam-role":
|
|
244
|
+
out.createIamRole = true;
|
|
245
|
+
break;
|
|
235
246
|
case "--skip-confirm":
|
|
236
247
|
out.skipConfirm = true;
|
|
237
248
|
break;
|
|
@@ -248,6 +259,31 @@ function parseDeployArgs(argv) {
|
|
|
248
259
|
throw new Error(`Missing value for ${token}`);
|
|
249
260
|
}
|
|
250
261
|
switch (token) {
|
|
262
|
+
case "--site-name":
|
|
263
|
+
out.siteName = value;
|
|
264
|
+
break;
|
|
265
|
+
case "--themes": {
|
|
266
|
+
const themes = value.split(",").map((t) => t.trim()).filter(Boolean);
|
|
267
|
+
const invalid = themes.filter((t) => !VALID_THEMES.includes(t));
|
|
268
|
+
if (invalid.length > 0) {
|
|
269
|
+
throw new Error(
|
|
270
|
+
`Invalid theme(s): ${invalid.join(", ")}. Valid values: ${VALID_THEMES.join(", ")}`
|
|
271
|
+
);
|
|
272
|
+
}
|
|
273
|
+
out.themes = themes;
|
|
274
|
+
break;
|
|
275
|
+
}
|
|
276
|
+
case "--plugins": {
|
|
277
|
+
const plugins = value.split(",").map((p3) => p3.trim()).filter(Boolean);
|
|
278
|
+
const invalid = plugins.filter((p3) => !VALID_PLUGINS.includes(p3));
|
|
279
|
+
if (invalid.length > 0) {
|
|
280
|
+
throw new Error(
|
|
281
|
+
`Invalid plugin(s): ${invalid.join(", ")}. Valid values: ${VALID_PLUGINS.join(", ")}`
|
|
282
|
+
);
|
|
283
|
+
}
|
|
284
|
+
out.plugins = plugins;
|
|
285
|
+
break;
|
|
286
|
+
}
|
|
251
287
|
case "--github-owner":
|
|
252
288
|
out.githubOwner = value;
|
|
253
289
|
break;
|
|
@@ -266,6 +302,9 @@ function parseDeployArgs(argv) {
|
|
|
266
302
|
case "--subdomain":
|
|
267
303
|
out.subdomain = value;
|
|
268
304
|
break;
|
|
305
|
+
case "--iam-service-role":
|
|
306
|
+
out.iamServiceRole = value;
|
|
307
|
+
break;
|
|
269
308
|
}
|
|
270
309
|
continue;
|
|
271
310
|
}
|
|
@@ -287,6 +326,13 @@ Usage:
|
|
|
287
326
|
npx create-ampless@alpha <project-name> [options]
|
|
288
327
|
|
|
289
328
|
Options:
|
|
329
|
+
--site-name <name> Site display name (default: "My Blog")
|
|
330
|
+
--themes <list> Comma-separated theme names to install
|
|
331
|
+
Valid: blog, minimal, landing, corporate, docs, dads
|
|
332
|
+
(default: blog)
|
|
333
|
+
--plugins <list> Comma-separated plugin names to install
|
|
334
|
+
Valid: seo, rss, webhook
|
|
335
|
+
(default: seo)
|
|
290
336
|
--deploy Also create GitHub repo + Amplify Hosting app and
|
|
291
337
|
kick off the first deploy after scaffolding
|
|
292
338
|
--github-owner <login> GitHub owner (user or org). Defaults to the
|
|
@@ -298,26 +344,507 @@ Options:
|
|
|
298
344
|
--aws-region <region> AWS region (defaults to aws config / env)
|
|
299
345
|
--domain <name> Custom domain (apex or subdomain) to attach
|
|
300
346
|
--subdomain <prefix> Subdomain prefix for the domain (default: apex)
|
|
301
|
-
--
|
|
347
|
+
--iam-service-role <arn> Existing IAM role for Amplify Hosting (must trust
|
|
348
|
+
amplify.amazonaws.com and have
|
|
349
|
+
AdministratorAccess-Amplify attached)
|
|
350
|
+
--create-iam-role Let create-ampless provision the Amplify Hosting
|
|
351
|
+
service role (idempotent; defaults to role name
|
|
352
|
+
AmplifyDeployBackend)
|
|
353
|
+
--skip-confirm Skip all interactive prompts and use defaults /
|
|
354
|
+
flag values (for CI / automation)
|
|
302
355
|
-h, --help Show this message
|
|
303
356
|
`;
|
|
304
357
|
|
|
305
358
|
// src/deploy-prompts.ts
|
|
306
359
|
import * as p2 from "@clack/prompts";
|
|
307
|
-
import { execa as
|
|
360
|
+
import { execa as execa3 } from "execa";
|
|
308
361
|
|
|
309
362
|
// src/deploy.ts
|
|
310
|
-
import { execa } from "execa";
|
|
363
|
+
import { execa as execa2 } from "execa";
|
|
311
364
|
import { writeFile as writeFile2 } from "fs/promises";
|
|
312
|
-
import { resolve as resolve3 } from "path";
|
|
365
|
+
import { basename as basename2, resolve as resolve3 } from "path";
|
|
313
366
|
import { spinner, log } from "@clack/prompts";
|
|
367
|
+
import pc2 from "picocolors";
|
|
368
|
+
|
|
369
|
+
// src/preflight.ts
|
|
370
|
+
import { execa } from "execa";
|
|
371
|
+
import { basename } from "path";
|
|
314
372
|
import pc from "picocolors";
|
|
373
|
+
var DEFAULT_AMPLIFY_ROLE_NAME = "AmplifyDeployBackend";
|
|
374
|
+
var AMPLIFY_BACKEND_POLICY_ARN = "arn:aws:iam::aws:policy/AdministratorAccess-Amplify";
|
|
375
|
+
var TRUST_POLICY_JSON = JSON.stringify({
|
|
376
|
+
Version: "2012-10-17",
|
|
377
|
+
Statement: [
|
|
378
|
+
{
|
|
379
|
+
Effect: "Allow",
|
|
380
|
+
Principal: { Service: "amplify.amazonaws.com" },
|
|
381
|
+
Action: "sts:AssumeRole"
|
|
382
|
+
}
|
|
383
|
+
]
|
|
384
|
+
});
|
|
385
|
+
async function tryExec(cmd, args) {
|
|
386
|
+
try {
|
|
387
|
+
const r = await execa(cmd, args, { reject: false, stdio: "pipe" });
|
|
388
|
+
return {
|
|
389
|
+
exitCode: r.exitCode ?? 0,
|
|
390
|
+
stdout: r.stdout?.toString() ?? "",
|
|
391
|
+
stderr: r.stderr?.toString() ?? ""
|
|
392
|
+
};
|
|
393
|
+
} catch (err) {
|
|
394
|
+
const e = err;
|
|
395
|
+
return {
|
|
396
|
+
exitCode: 1,
|
|
397
|
+
stdout: e.stdout ?? "",
|
|
398
|
+
stderr: e.stderr ?? e.message ?? String(err)
|
|
399
|
+
};
|
|
400
|
+
}
|
|
401
|
+
}
|
|
402
|
+
function awsBaseArgs(opts) {
|
|
403
|
+
const out = [];
|
|
404
|
+
if (opts.awsProfile) out.push("--profile", opts.awsProfile);
|
|
405
|
+
if (opts.awsRegion) out.push("--region", opts.awsRegion);
|
|
406
|
+
return out;
|
|
407
|
+
}
|
|
408
|
+
function awsCmd(opts, extra) {
|
|
409
|
+
return [...awsBaseArgs(opts), ...extra, "--output", "json"];
|
|
410
|
+
}
|
|
411
|
+
function shortName(opts) {
|
|
412
|
+
return basename(opts.projectDir);
|
|
413
|
+
}
|
|
414
|
+
async function checkGhInstalled(problems) {
|
|
415
|
+
const r = await tryExec("gh", ["--version"]);
|
|
416
|
+
if (r.exitCode !== 0) {
|
|
417
|
+
problems.push({
|
|
418
|
+
id: "gh-not-installed",
|
|
419
|
+
message: "GitHub CLI (`gh`) is not installed.",
|
|
420
|
+
remediation: [
|
|
421
|
+
"Install it (macOS: `brew install gh`; other OS: https://cli.github.com/),",
|
|
422
|
+
"then run `gh auth login` to sign in."
|
|
423
|
+
]
|
|
424
|
+
});
|
|
425
|
+
return false;
|
|
426
|
+
}
|
|
427
|
+
return true;
|
|
428
|
+
}
|
|
429
|
+
async function checkGhAuth(problems) {
|
|
430
|
+
const r = await tryExec("gh", ["auth", "status"]);
|
|
431
|
+
const combined = `${r.stdout}
|
|
432
|
+
${r.stderr}`;
|
|
433
|
+
if (r.exitCode !== 0 || /not logged in/i.test(combined)) {
|
|
434
|
+
problems.push({
|
|
435
|
+
id: "gh-not-authenticated",
|
|
436
|
+
message: "GitHub CLI is not authenticated.",
|
|
437
|
+
remediation: ["Run `gh auth login` and choose a method that includes the `repo` scope."]
|
|
438
|
+
});
|
|
439
|
+
return false;
|
|
440
|
+
}
|
|
441
|
+
const scopesMatch = combined.match(/Token scopes?:\s*([^\n]+)/i);
|
|
442
|
+
if (!scopesMatch) {
|
|
443
|
+
problems.push({
|
|
444
|
+
id: "gh-scopes-unknown",
|
|
445
|
+
message: "Could not determine GitHub token scopes from `gh auth status`.",
|
|
446
|
+
remediation: ["Run `gh auth refresh -s repo` to ensure the `repo` scope is granted."]
|
|
447
|
+
});
|
|
448
|
+
return false;
|
|
449
|
+
}
|
|
450
|
+
const hasRepo = /(^|[^a-z])repo([^a-z]|$)/i.test(scopesMatch[1]);
|
|
451
|
+
if (!hasRepo) {
|
|
452
|
+
problems.push({
|
|
453
|
+
id: "gh-missing-repo-scope",
|
|
454
|
+
message: "GitHub token is missing the `repo` scope.",
|
|
455
|
+
remediation: ["Run `gh auth refresh -s repo` to add the `repo` scope to your token."]
|
|
456
|
+
});
|
|
457
|
+
return false;
|
|
458
|
+
}
|
|
459
|
+
return true;
|
|
460
|
+
}
|
|
461
|
+
async function checkAwsInstalled(problems) {
|
|
462
|
+
const r = await tryExec("aws", ["--version"]);
|
|
463
|
+
if (r.exitCode !== 0) {
|
|
464
|
+
problems.push({
|
|
465
|
+
id: "aws-not-installed",
|
|
466
|
+
message: "AWS CLI (`aws`) is not installed.",
|
|
467
|
+
remediation: [
|
|
468
|
+
"Install it (macOS: `brew install awscli`; other OS: https://aws.amazon.com/cli/),",
|
|
469
|
+
"then run `aws configure` to set credentials."
|
|
470
|
+
]
|
|
471
|
+
});
|
|
472
|
+
return false;
|
|
473
|
+
}
|
|
474
|
+
return true;
|
|
475
|
+
}
|
|
476
|
+
async function checkAwsCreds(opts, problems) {
|
|
477
|
+
const r = await tryExec("aws", [...awsBaseArgs(opts), "sts", "get-caller-identity", "--output", "json"]);
|
|
478
|
+
if (r.exitCode !== 0) {
|
|
479
|
+
problems.push({
|
|
480
|
+
id: "aws-credentials-missing",
|
|
481
|
+
message: "AWS credentials are not configured (sts get-caller-identity failed).",
|
|
482
|
+
remediation: [
|
|
483
|
+
"Run `aws configure` (or set AWS_ACCESS_KEY_ID / AWS_SECRET_ACCESS_KEY / AWS_SESSION_TOKEN),",
|
|
484
|
+
opts.awsProfile ? `or check that profile \`${opts.awsProfile}\` exists in ~/.aws/config.` : "or pass --aws-profile <name> if you use named profiles."
|
|
485
|
+
]
|
|
486
|
+
});
|
|
487
|
+
return void 0;
|
|
488
|
+
}
|
|
489
|
+
try {
|
|
490
|
+
const parsed = JSON.parse(r.stdout);
|
|
491
|
+
const account = parsed.Account;
|
|
492
|
+
if (!account) {
|
|
493
|
+
problems.push({
|
|
494
|
+
id: "aws-identity-malformed",
|
|
495
|
+
message: "aws sts get-caller-identity did not return an Account field.",
|
|
496
|
+
remediation: ["Re-run `aws sts get-caller-identity` manually to inspect the response."]
|
|
497
|
+
});
|
|
498
|
+
return void 0;
|
|
499
|
+
}
|
|
500
|
+
let region = opts.awsRegion;
|
|
501
|
+
if (!region) {
|
|
502
|
+
const fromEnv = process.env.AWS_REGION?.trim() || process.env.AWS_DEFAULT_REGION?.trim();
|
|
503
|
+
if (fromEnv) region = fromEnv;
|
|
504
|
+
}
|
|
505
|
+
if (!region) {
|
|
506
|
+
const cfg = await tryExec("aws", [...awsBaseArgs(opts), "configure", "get", "region"]);
|
|
507
|
+
const trimmed = cfg.stdout.trim();
|
|
508
|
+
if (trimmed) region = trimmed;
|
|
509
|
+
}
|
|
510
|
+
if (!region) {
|
|
511
|
+
problems.push({
|
|
512
|
+
id: "aws-region-missing",
|
|
513
|
+
message: "AWS region is not set.",
|
|
514
|
+
remediation: [
|
|
515
|
+
"Pass --aws-region <name>, set AWS_REGION env var, or run `aws configure set region <name>`."
|
|
516
|
+
]
|
|
517
|
+
});
|
|
518
|
+
return void 0;
|
|
519
|
+
}
|
|
520
|
+
return { account, region };
|
|
521
|
+
} catch (err) {
|
|
522
|
+
problems.push({
|
|
523
|
+
id: "aws-identity-parse-failed",
|
|
524
|
+
message: `Could not parse aws sts get-caller-identity response: ${err instanceof Error ? err.message : String(err)}`,
|
|
525
|
+
remediation: ["Re-run `aws sts get-caller-identity` manually to inspect the response."]
|
|
526
|
+
});
|
|
527
|
+
return void 0;
|
|
528
|
+
}
|
|
529
|
+
}
|
|
530
|
+
async function checkGithubRepoFree(opts, owner, problems) {
|
|
531
|
+
if (!owner) return;
|
|
532
|
+
const name = `${owner}/${shortName(opts)}`;
|
|
533
|
+
const r = await tryExec("gh", ["repo", "view", name]);
|
|
534
|
+
if (r.exitCode === 0) {
|
|
535
|
+
problems.push({
|
|
536
|
+
id: "github-repo-exists",
|
|
537
|
+
message: `GitHub repo ${name} already exists.`,
|
|
538
|
+
remediation: [
|
|
539
|
+
"Pick a different project name, or delete the existing repo first:",
|
|
540
|
+
` gh repo delete ${name} --yes`
|
|
541
|
+
]
|
|
542
|
+
});
|
|
543
|
+
}
|
|
544
|
+
}
|
|
545
|
+
async function checkAmplifyAppNameFree(opts, problems) {
|
|
546
|
+
const name = shortName(opts);
|
|
547
|
+
const r = await tryExec(
|
|
548
|
+
"aws",
|
|
549
|
+
awsCmd(opts, [
|
|
550
|
+
"amplify",
|
|
551
|
+
"list-apps",
|
|
552
|
+
"--query",
|
|
553
|
+
`apps[?name=='${name}']`
|
|
554
|
+
])
|
|
555
|
+
);
|
|
556
|
+
if (r.exitCode !== 0) {
|
|
557
|
+
problems.push({
|
|
558
|
+
id: "amplify-list-apps-failed",
|
|
559
|
+
message: "Could not list Amplify Hosting apps to check for a name collision.",
|
|
560
|
+
remediation: [
|
|
561
|
+
"Verify your IAM identity has `amplify:ListApps` permission, then re-run.",
|
|
562
|
+
`Raw error: ${r.stderr.trim() || r.stdout.trim()}`
|
|
563
|
+
]
|
|
564
|
+
});
|
|
565
|
+
return;
|
|
566
|
+
}
|
|
567
|
+
try {
|
|
568
|
+
const arr = JSON.parse(r.stdout);
|
|
569
|
+
if (Array.isArray(arr) && arr.length > 0) {
|
|
570
|
+
problems.push({
|
|
571
|
+
id: "amplify-app-name-taken",
|
|
572
|
+
message: `Amplify Hosting already has an app named "${name}" in this region.`,
|
|
573
|
+
remediation: [
|
|
574
|
+
"Pick a different project name, or delete the existing app first:",
|
|
575
|
+
` aws amplify list-apps --query "apps[?name=='${name}'].appId" --output text${opts.awsProfile ? ` --profile ${opts.awsProfile}` : ""}${opts.awsRegion ? ` --region ${opts.awsRegion}` : ""}`,
|
|
576
|
+
" aws amplify delete-app --app-id <appId>"
|
|
577
|
+
]
|
|
578
|
+
});
|
|
579
|
+
}
|
|
580
|
+
} catch {
|
|
581
|
+
}
|
|
582
|
+
}
|
|
583
|
+
async function checkCdkBootstrap(opts, identity, problems) {
|
|
584
|
+
const r = await tryExec(
|
|
585
|
+
"aws",
|
|
586
|
+
[
|
|
587
|
+
...awsBaseArgs(opts),
|
|
588
|
+
"ssm",
|
|
589
|
+
"get-parameter",
|
|
590
|
+
"--name",
|
|
591
|
+
"/cdk-bootstrap/hnb659fds/version",
|
|
592
|
+
"--output",
|
|
593
|
+
"json"
|
|
594
|
+
]
|
|
595
|
+
);
|
|
596
|
+
if (r.exitCode !== 0) {
|
|
597
|
+
problems.push({
|
|
598
|
+
id: "cdk-not-bootstrapped",
|
|
599
|
+
message: `CDK is not bootstrapped in region ${identity.region}.`,
|
|
600
|
+
remediation: [
|
|
601
|
+
`Run: npx cdk bootstrap aws://${identity.account}/${identity.region}`
|
|
602
|
+
]
|
|
603
|
+
});
|
|
604
|
+
}
|
|
605
|
+
}
|
|
606
|
+
async function checkRoute53Zone(opts, problems) {
|
|
607
|
+
if (!opts.domain) return;
|
|
608
|
+
const registrable = extractRegistrableDomain(opts.domain);
|
|
609
|
+
const r = await tryExec(
|
|
610
|
+
"aws",
|
|
611
|
+
[
|
|
612
|
+
...awsBaseArgs(opts),
|
|
613
|
+
"route53",
|
|
614
|
+
"list-hosted-zones-by-name",
|
|
615
|
+
"--dns-name",
|
|
616
|
+
registrable,
|
|
617
|
+
"--max-items",
|
|
618
|
+
"1",
|
|
619
|
+
"--output",
|
|
620
|
+
"json"
|
|
621
|
+
]
|
|
622
|
+
);
|
|
623
|
+
if (r.exitCode !== 0) {
|
|
624
|
+
problems.push({
|
|
625
|
+
id: "route53-list-failed",
|
|
626
|
+
message: `Could not query Route 53 for zone ${registrable}.`,
|
|
627
|
+
remediation: [
|
|
628
|
+
"Either grant `route53:ListHostedZonesByName` to your IAM identity,",
|
|
629
|
+
"or be prepared to add the CNAME records Amplify prints manually after deploy.",
|
|
630
|
+
`Raw error: ${r.stderr.trim() || r.stdout.trim()}`
|
|
631
|
+
]
|
|
632
|
+
});
|
|
633
|
+
return;
|
|
634
|
+
}
|
|
635
|
+
try {
|
|
636
|
+
const parsed = JSON.parse(r.stdout);
|
|
637
|
+
const zones = parsed.HostedZones ?? [];
|
|
638
|
+
const found = zones.some(
|
|
639
|
+
(z) => z.Name && z.Name.replace(/\.$/, "") === registrable
|
|
640
|
+
);
|
|
641
|
+
if (!found) {
|
|
642
|
+
problems.push({
|
|
643
|
+
id: "route53-zone-missing",
|
|
644
|
+
message: `No Route 53 hosted zone for ${registrable} found in this AWS account.`,
|
|
645
|
+
remediation: [
|
|
646
|
+
"Amplify will still issue an ACM certificate, but you will need to add",
|
|
647
|
+
"the verification CNAMEs at your DNS provider manually. If you want",
|
|
648
|
+
"auto-validation, create a hosted zone first:",
|
|
649
|
+
` aws route53 create-hosted-zone --name ${registrable} --caller-reference $(date +%s)${opts.awsProfile ? ` --profile ${opts.awsProfile}` : ""}`
|
|
650
|
+
]
|
|
651
|
+
});
|
|
652
|
+
}
|
|
653
|
+
} catch {
|
|
654
|
+
}
|
|
655
|
+
}
|
|
656
|
+
async function resolveIamServiceRole(opts, args, problems) {
|
|
657
|
+
if (args.explicitArn) {
|
|
658
|
+
const roleName = args.explicitArn.split("/").pop() ?? args.explicitArn;
|
|
659
|
+
const r = await tryExec(
|
|
660
|
+
"aws",
|
|
661
|
+
[
|
|
662
|
+
...awsBaseArgs(opts),
|
|
663
|
+
"iam",
|
|
664
|
+
"get-role",
|
|
665
|
+
"--role-name",
|
|
666
|
+
roleName,
|
|
667
|
+
"--output",
|
|
668
|
+
"json"
|
|
669
|
+
]
|
|
670
|
+
);
|
|
671
|
+
if (r.exitCode !== 0) {
|
|
672
|
+
problems.push({
|
|
673
|
+
id: "iam-service-role-not-found",
|
|
674
|
+
message: `IAM role ${args.explicitArn} not found or inaccessible.`,
|
|
675
|
+
remediation: [
|
|
676
|
+
"Verify the ARN is correct and the role exists:",
|
|
677
|
+
` aws iam get-role --role-name ${roleName}${opts.awsProfile ? ` --profile ${opts.awsProfile}` : ""}`
|
|
678
|
+
]
|
|
679
|
+
});
|
|
680
|
+
return { willCreate: false };
|
|
681
|
+
}
|
|
682
|
+
return { arn: args.explicitArn, willCreate: false };
|
|
683
|
+
}
|
|
684
|
+
if (args.willCreate) {
|
|
685
|
+
return { willCreate: true };
|
|
686
|
+
}
|
|
687
|
+
const found = await findExistingAmplifyServiceRole(opts);
|
|
688
|
+
if (found) {
|
|
689
|
+
return { arn: found, willCreate: false };
|
|
690
|
+
}
|
|
691
|
+
problems.push({
|
|
692
|
+
id: "iam-service-role-missing",
|
|
693
|
+
message: "No Amplify Hosting service role found.",
|
|
694
|
+
remediation: [
|
|
695
|
+
"Either pass --iam-service-role <arn> pointing at an existing role,",
|
|
696
|
+
"or let create-ampless build one for you with --create-iam-role.",
|
|
697
|
+
"",
|
|
698
|
+
"Alternatively, create one yourself:",
|
|
699
|
+
` aws iam create-role --role-name ${DEFAULT_AMPLIFY_ROLE_NAME} \\`,
|
|
700
|
+
` --assume-role-policy-document '${TRUST_POLICY_JSON}'`,
|
|
701
|
+
` aws iam attach-role-policy --role-name ${DEFAULT_AMPLIFY_ROLE_NAME} \\`,
|
|
702
|
+
` --policy-arn ${AMPLIFY_BACKEND_POLICY_ARN}`,
|
|
703
|
+
` # then re-run with: --iam-service-role $(aws iam get-role --role-name ${DEFAULT_AMPLIFY_ROLE_NAME} --query Role.Arn --output text)`
|
|
704
|
+
]
|
|
705
|
+
});
|
|
706
|
+
return { willCreate: false };
|
|
707
|
+
}
|
|
708
|
+
function trustPolicyAllowsAmplify(doc) {
|
|
709
|
+
let parsed = doc;
|
|
710
|
+
if (typeof doc === "string") {
|
|
711
|
+
try {
|
|
712
|
+
parsed = JSON.parse(decodeURIComponent(doc));
|
|
713
|
+
} catch {
|
|
714
|
+
try {
|
|
715
|
+
parsed = JSON.parse(doc);
|
|
716
|
+
} catch {
|
|
717
|
+
return false;
|
|
718
|
+
}
|
|
719
|
+
}
|
|
720
|
+
}
|
|
721
|
+
const stmts = parsed?.Statement;
|
|
722
|
+
const arr = Array.isArray(stmts) ? stmts : stmts ? [stmts] : [];
|
|
723
|
+
for (const stmt of arr) {
|
|
724
|
+
const s = stmt;
|
|
725
|
+
const svc = s.Principal?.Service;
|
|
726
|
+
if (typeof svc === "string" && svc === "amplify.amazonaws.com") return true;
|
|
727
|
+
if (Array.isArray(svc) && svc.includes("amplify.amazonaws.com")) return true;
|
|
728
|
+
}
|
|
729
|
+
return false;
|
|
730
|
+
}
|
|
731
|
+
async function findExistingAmplifyServiceRole(opts) {
|
|
732
|
+
let marker;
|
|
733
|
+
for (let page = 0; page < 20; page++) {
|
|
734
|
+
const args = [
|
|
735
|
+
...awsBaseArgs(opts),
|
|
736
|
+
"iam",
|
|
737
|
+
"list-roles",
|
|
738
|
+
"--max-items",
|
|
739
|
+
"200",
|
|
740
|
+
"--output",
|
|
741
|
+
"json"
|
|
742
|
+
];
|
|
743
|
+
if (marker) args.push("--starting-token", marker);
|
|
744
|
+
const r = await tryExec("aws", args);
|
|
745
|
+
if (r.exitCode !== 0) return void 0;
|
|
746
|
+
let parsed;
|
|
747
|
+
try {
|
|
748
|
+
parsed = JSON.parse(r.stdout);
|
|
749
|
+
} catch {
|
|
750
|
+
return void 0;
|
|
751
|
+
}
|
|
752
|
+
for (const role of parsed.Roles ?? []) {
|
|
753
|
+
if (!role.RoleName || !role.Arn) continue;
|
|
754
|
+
if (!trustPolicyAllowsAmplify(role.AssumeRolePolicyDocument)) continue;
|
|
755
|
+
const pr = await tryExec(
|
|
756
|
+
"aws",
|
|
757
|
+
[
|
|
758
|
+
...awsBaseArgs(opts),
|
|
759
|
+
"iam",
|
|
760
|
+
"list-attached-role-policies",
|
|
761
|
+
"--role-name",
|
|
762
|
+
role.RoleName,
|
|
763
|
+
"--output",
|
|
764
|
+
"json"
|
|
765
|
+
]
|
|
766
|
+
);
|
|
767
|
+
if (pr.exitCode !== 0) continue;
|
|
768
|
+
try {
|
|
769
|
+
const policies = JSON.parse(pr.stdout);
|
|
770
|
+
const attached = (policies.AttachedPolicies ?? []).some(
|
|
771
|
+
(p3) => p3.PolicyArn === AMPLIFY_BACKEND_POLICY_ARN
|
|
772
|
+
);
|
|
773
|
+
if (attached) return role.Arn;
|
|
774
|
+
} catch {
|
|
775
|
+
continue;
|
|
776
|
+
}
|
|
777
|
+
}
|
|
778
|
+
if (!parsed.IsTruncated) break;
|
|
779
|
+
marker = parsed.Marker;
|
|
780
|
+
if (!marker) break;
|
|
781
|
+
}
|
|
782
|
+
return void 0;
|
|
783
|
+
}
|
|
784
|
+
async function runPreflight(opts, extra = {}) {
|
|
785
|
+
const problems = [];
|
|
786
|
+
const hasGh = await checkGhInstalled(problems);
|
|
787
|
+
if (hasGh) await checkGhAuth(problems);
|
|
788
|
+
const hasAws = await checkAwsInstalled(problems);
|
|
789
|
+
const identity = hasAws ? await checkAwsCreds(opts, problems) : void 0;
|
|
790
|
+
if (hasGh && opts.githubOwner) {
|
|
791
|
+
await checkGithubRepoFree(opts, opts.githubOwner, problems);
|
|
792
|
+
}
|
|
793
|
+
if (identity) {
|
|
794
|
+
await checkAmplifyAppNameFree(opts, problems);
|
|
795
|
+
await checkCdkBootstrap(opts, identity, problems);
|
|
796
|
+
await checkRoute53Zone(opts, problems);
|
|
797
|
+
}
|
|
798
|
+
let willCreateIamRole = false;
|
|
799
|
+
let iamServiceRoleArn;
|
|
800
|
+
if (identity) {
|
|
801
|
+
const resolved = await resolveIamServiceRole(
|
|
802
|
+
opts,
|
|
803
|
+
{ explicitArn: extra.iamServiceRoleArn, willCreate: extra.createIamRole === true },
|
|
804
|
+
problems
|
|
805
|
+
);
|
|
806
|
+
willCreateIamRole = resolved.willCreate;
|
|
807
|
+
iamServiceRoleArn = resolved.arn;
|
|
808
|
+
}
|
|
809
|
+
if (problems.length > 0) {
|
|
810
|
+
return { problems };
|
|
811
|
+
}
|
|
812
|
+
return {
|
|
813
|
+
problems,
|
|
814
|
+
resolution: {
|
|
815
|
+
iamServiceRoleArn,
|
|
816
|
+
willCreateIamRole,
|
|
817
|
+
awsAccount: identity.account,
|
|
818
|
+
awsRegion: identity.region
|
|
819
|
+
}
|
|
820
|
+
};
|
|
821
|
+
}
|
|
822
|
+
function formatPreflightReport(problems) {
|
|
823
|
+
const lines = [];
|
|
824
|
+
lines.push(`${pc.red("\u2717")} create-ampless --deploy: prerequisites missing`);
|
|
825
|
+
lines.push("");
|
|
826
|
+
for (const p3 of problems) {
|
|
827
|
+
lines.push(` ${pc.red("\u2717")} ${p3.message}`);
|
|
828
|
+
for (const r of p3.remediation) {
|
|
829
|
+
lines.push(r ? ` ${r}` : "");
|
|
830
|
+
}
|
|
831
|
+
lines.push("");
|
|
832
|
+
}
|
|
833
|
+
lines.push("Fix the items above and re-run.");
|
|
834
|
+
return lines.join("\n");
|
|
835
|
+
}
|
|
836
|
+
function printPreflightReport(problems) {
|
|
837
|
+
process.stderr.write(`${formatPreflightReport(problems)}
|
|
838
|
+
`);
|
|
839
|
+
}
|
|
840
|
+
|
|
841
|
+
// src/deploy.ts
|
|
315
842
|
var AMPLIFY_BUILD_SPEC = `version: 1
|
|
316
843
|
frontend:
|
|
317
844
|
phases:
|
|
318
845
|
preBuild:
|
|
319
846
|
commands:
|
|
320
|
-
- npm
|
|
847
|
+
- npm install
|
|
321
848
|
build:
|
|
322
849
|
commands:
|
|
323
850
|
- npx ampx pipeline-deploy --branch $AWS_BRANCH --app-id $AWS_APP_ID
|
|
@@ -336,7 +863,7 @@ async function resolveGithubToken(explicit, env = process.env) {
|
|
|
336
863
|
const envToken = env.GITHUB_TOKEN;
|
|
337
864
|
if (envToken && envToken.trim()) return envToken.trim();
|
|
338
865
|
try {
|
|
339
|
-
const { stdout } = await
|
|
866
|
+
const { stdout } = await execa2("gh", ["auth", "token"], { reject: false });
|
|
340
867
|
const trimmed = stdout?.trim();
|
|
341
868
|
if (trimmed) return trimmed;
|
|
342
869
|
} catch {
|
|
@@ -400,7 +927,7 @@ function splitDomain(domain, subdomain) {
|
|
|
400
927
|
async function run(cmd, args, opts = {}) {
|
|
401
928
|
const { step, ...execaOpts } = opts;
|
|
402
929
|
try {
|
|
403
|
-
const result = await
|
|
930
|
+
const result = await execa2(cmd, args, { stdio: "pipe", ...execaOpts });
|
|
404
931
|
return result.stdout?.toString() ?? "";
|
|
405
932
|
} catch (err) {
|
|
406
933
|
const e = err;
|
|
@@ -410,15 +937,6 @@ async function run(cmd, args, opts = {}) {
|
|
|
410
937
|
${detail}`);
|
|
411
938
|
}
|
|
412
939
|
}
|
|
413
|
-
async function ensureCommandExists(cmd) {
|
|
414
|
-
try {
|
|
415
|
-
await execa(cmd, ["--version"], { stdio: "ignore" });
|
|
416
|
-
} catch {
|
|
417
|
-
const hint = cmd === "gh" ? "Install with `brew install gh` (or see https://cli.github.com/) then run `gh auth login`." : "Install with `brew install awscli` (or see https://aws.amazon.com/cli/) then run `aws configure`.";
|
|
418
|
-
throw new Error(`Required command not found: ${cmd}
|
|
419
|
-
${hint}`);
|
|
420
|
-
}
|
|
421
|
-
}
|
|
422
940
|
function awsArgs(opts, extra) {
|
|
423
941
|
const base = [];
|
|
424
942
|
if (opts.awsProfile) base.push("--profile", opts.awsProfile);
|
|
@@ -436,9 +954,12 @@ async function gitInitCommit(dir) {
|
|
|
436
954
|
{ cwd: dir, step: "git commit" }
|
|
437
955
|
);
|
|
438
956
|
}
|
|
957
|
+
function shortName2(opts) {
|
|
958
|
+
return basename2(opts.projectDir);
|
|
959
|
+
}
|
|
439
960
|
async function ghRepoCreate(opts) {
|
|
440
961
|
const visibility = opts.githubPrivate ? "--private" : "--public";
|
|
441
|
-
const name = `${opts.githubOwner}/${opts
|
|
962
|
+
const name = `${opts.githubOwner}/${shortName2(opts)}`;
|
|
442
963
|
const out = await run(
|
|
443
964
|
"gh",
|
|
444
965
|
[
|
|
@@ -462,23 +983,27 @@ async function ghRepoCreate(opts) {
|
|
|
462
983
|
if (match && match.length > 0) return match[match.length - 1].replace(/[.,]$/, "");
|
|
463
984
|
return `https://github.com/${name}`;
|
|
464
985
|
}
|
|
465
|
-
async function amplifyCreateApp(opts, repoUrl) {
|
|
986
|
+
async function amplifyCreateApp(opts, repoUrl, iamServiceRoleArn) {
|
|
987
|
+
const cmd = [
|
|
988
|
+
"amplify",
|
|
989
|
+
"create-app",
|
|
990
|
+
"--name",
|
|
991
|
+
shortName2(opts),
|
|
992
|
+
"--repository",
|
|
993
|
+
repoUrl,
|
|
994
|
+
"--access-token",
|
|
995
|
+
opts.githubToken,
|
|
996
|
+
"--platform",
|
|
997
|
+
"WEB_COMPUTE",
|
|
998
|
+
"--build-spec",
|
|
999
|
+
AMPLIFY_BUILD_SPEC
|
|
1000
|
+
];
|
|
1001
|
+
if (iamServiceRoleArn) {
|
|
1002
|
+
cmd.push("--iam-service-role-arn", iamServiceRoleArn);
|
|
1003
|
+
}
|
|
466
1004
|
const out = await run(
|
|
467
1005
|
"aws",
|
|
468
|
-
awsArgs(opts,
|
|
469
|
-
"amplify",
|
|
470
|
-
"create-app",
|
|
471
|
-
"--name",
|
|
472
|
-
opts.projectName,
|
|
473
|
-
"--repository",
|
|
474
|
-
repoUrl,
|
|
475
|
-
"--access-token",
|
|
476
|
-
opts.githubToken,
|
|
477
|
-
"--platform",
|
|
478
|
-
"WEB_COMPUTE",
|
|
479
|
-
"--build-spec",
|
|
480
|
-
AMPLIFY_BUILD_SPEC
|
|
481
|
-
]),
|
|
1006
|
+
awsArgs(opts, cmd),
|
|
482
1007
|
{ step: "aws amplify create-app" }
|
|
483
1008
|
);
|
|
484
1009
|
const parsed = JSON.parse(out);
|
|
@@ -552,7 +1077,7 @@ async function amplifyCreateDomain(opts, appId) {
|
|
|
552
1077
|
const route53Zone = await findRoute53Zone(opts, registrable);
|
|
553
1078
|
if (route53Zone) {
|
|
554
1079
|
log.info(
|
|
555
|
-
`Route 53 hosted zone detected for ${
|
|
1080
|
+
`Route 53 hosted zone detected for ${pc2.cyan(registrable)} \u2014 Amplify will auto-create DNS records once ACM validates.`
|
|
556
1081
|
);
|
|
557
1082
|
}
|
|
558
1083
|
const out = await run(
|
|
@@ -598,9 +1123,92 @@ async function amplifyCreateDomain(opts, appId) {
|
|
|
598
1123
|
verification: route53Zone ? void 0 : verification.length > 0 ? verification : void 0
|
|
599
1124
|
};
|
|
600
1125
|
}
|
|
1126
|
+
async function provisionIamServiceRole(opts) {
|
|
1127
|
+
const roleName = DEFAULT_AMPLIFY_ROLE_NAME;
|
|
1128
|
+
const trustPolicy = JSON.stringify({
|
|
1129
|
+
Version: "2012-10-17",
|
|
1130
|
+
Statement: [
|
|
1131
|
+
{
|
|
1132
|
+
Effect: "Allow",
|
|
1133
|
+
Principal: { Service: "amplify.amazonaws.com" },
|
|
1134
|
+
Action: "sts:AssumeRole"
|
|
1135
|
+
}
|
|
1136
|
+
]
|
|
1137
|
+
});
|
|
1138
|
+
const createArgs = [
|
|
1139
|
+
"iam",
|
|
1140
|
+
"create-role",
|
|
1141
|
+
"--role-name",
|
|
1142
|
+
roleName,
|
|
1143
|
+
"--assume-role-policy-document",
|
|
1144
|
+
trustPolicy,
|
|
1145
|
+
"--description",
|
|
1146
|
+
"Amplify Hosting service role created by create-ampless"
|
|
1147
|
+
];
|
|
1148
|
+
try {
|
|
1149
|
+
await run("aws", awsArgs(opts, createArgs), { step: "aws iam create-role" });
|
|
1150
|
+
} catch (err) {
|
|
1151
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
1152
|
+
if (!/EntityAlreadyExists/i.test(msg) && !/already exists/i.test(msg)) {
|
|
1153
|
+
throw err;
|
|
1154
|
+
}
|
|
1155
|
+
}
|
|
1156
|
+
await run(
|
|
1157
|
+
"aws",
|
|
1158
|
+
awsArgs(opts, [
|
|
1159
|
+
"iam",
|
|
1160
|
+
"attach-role-policy",
|
|
1161
|
+
"--role-name",
|
|
1162
|
+
roleName,
|
|
1163
|
+
"--policy-arn",
|
|
1164
|
+
AMPLIFY_BACKEND_POLICY_ARN
|
|
1165
|
+
]),
|
|
1166
|
+
{ step: "aws iam attach-role-policy" }
|
|
1167
|
+
);
|
|
1168
|
+
const out = await run(
|
|
1169
|
+
"aws",
|
|
1170
|
+
awsArgs(opts, ["iam", "get-role", "--role-name", roleName]),
|
|
1171
|
+
{ step: "aws iam get-role" }
|
|
1172
|
+
);
|
|
1173
|
+
const parsed = JSON.parse(out);
|
|
1174
|
+
const arn = parsed.Role?.Arn;
|
|
1175
|
+
if (!arn) {
|
|
1176
|
+
throw new Error(`aws iam get-role for ${roleName}: missing Role.Arn in response`);
|
|
1177
|
+
}
|
|
1178
|
+
return arn;
|
|
1179
|
+
}
|
|
1180
|
+
var PreflightError = class extends Error {
|
|
1181
|
+
result;
|
|
1182
|
+
constructor(result) {
|
|
1183
|
+
super("create-ampless --deploy: pre-flight failed");
|
|
1184
|
+
this.name = "PreflightError";
|
|
1185
|
+
this.result = result;
|
|
1186
|
+
}
|
|
1187
|
+
};
|
|
601
1188
|
async function runDeploy(opts) {
|
|
602
|
-
await
|
|
603
|
-
|
|
1189
|
+
const pre = await runPreflight(opts, {
|
|
1190
|
+
iamServiceRoleArn: opts.iamServiceRoleArn,
|
|
1191
|
+
createIamRole: opts.createIamRole === true
|
|
1192
|
+
});
|
|
1193
|
+
if (pre.problems.length > 0) {
|
|
1194
|
+
printPreflightReport(pre.problems);
|
|
1195
|
+
throw new PreflightError(pre);
|
|
1196
|
+
}
|
|
1197
|
+
const resolution = pre.resolution;
|
|
1198
|
+
let serviceRoleArn = resolution.iamServiceRoleArn;
|
|
1199
|
+
if (resolution.willCreateIamRole && !serviceRoleArn) {
|
|
1200
|
+
const s0 = spinner();
|
|
1201
|
+
s0.start(`Provisioning IAM service role ${DEFAULT_AMPLIFY_ROLE_NAME}`);
|
|
1202
|
+
try {
|
|
1203
|
+
serviceRoleArn = await provisionIamServiceRole(opts);
|
|
1204
|
+
s0.stop(`IAM role: ${serviceRoleArn}`);
|
|
1205
|
+
} catch (err) {
|
|
1206
|
+
s0.stop("IAM role provisioning: failed");
|
|
1207
|
+
throw new Error(
|
|
1208
|
+
`Failed to provision IAM service role: ${err instanceof Error ? err.message : String(err)}`
|
|
1209
|
+
);
|
|
1210
|
+
}
|
|
1211
|
+
}
|
|
604
1212
|
await writeFile2(resolve3(opts.projectDir, "amplify.yml"), AMPLIFY_BUILD_SPEC, "utf-8");
|
|
605
1213
|
const created = {};
|
|
606
1214
|
const fail = (step, cause) => {
|
|
@@ -613,7 +1221,7 @@ async function runDeploy(opts) {
|
|
|
613
1221
|
}
|
|
614
1222
|
if (created.repoUrl) {
|
|
615
1223
|
cleanup.push(
|
|
616
|
-
` - GitHub repo: ${created.repoUrl} (delete: gh repo delete ${opts.githubOwner}/${opts
|
|
1224
|
+
` - GitHub repo: ${created.repoUrl} (delete: gh repo delete ${opts.githubOwner}/${shortName2(opts)} --yes)`
|
|
617
1225
|
);
|
|
618
1226
|
}
|
|
619
1227
|
const cleanupBlock = cleanup.length > 0 ? `
|
|
@@ -646,7 +1254,7 @@ ${msg}${cleanupBlock}`);
|
|
|
646
1254
|
s = spinner();
|
|
647
1255
|
s.start("Creating Amplify Hosting app");
|
|
648
1256
|
try {
|
|
649
|
-
app = await amplifyCreateApp(opts, created.repoUrl);
|
|
1257
|
+
app = await amplifyCreateApp(opts, created.repoUrl, serviceRoleArn);
|
|
650
1258
|
created.appId = app.appId;
|
|
651
1259
|
s.stop(`Amplify app: ${app.appId}`);
|
|
652
1260
|
} catch (err) {
|
|
@@ -671,7 +1279,7 @@ ${msg}${cleanupBlock}`);
|
|
|
671
1279
|
s.stop("Amplify start-job: failed");
|
|
672
1280
|
fail("aws amplify start-job", err);
|
|
673
1281
|
}
|
|
674
|
-
const amplifyAppUrl = `https://main.${app.
|
|
1282
|
+
const amplifyAppUrl = `https://main.${app.defaultDomain}`;
|
|
675
1283
|
const result = {
|
|
676
1284
|
githubRepoUrl: created.repoUrl,
|
|
677
1285
|
amplifyAppId: app.appId,
|
|
@@ -698,7 +1306,7 @@ ${msg}`);
|
|
|
698
1306
|
// src/deploy-prompts.ts
|
|
699
1307
|
async function detectGithubLogin() {
|
|
700
1308
|
try {
|
|
701
|
-
const { stdout } = await
|
|
1309
|
+
const { stdout } = await execa3("gh", ["api", "user", "--jq", ".login"], { reject: false });
|
|
702
1310
|
const trimmed = stdout?.trim();
|
|
703
1311
|
return trimmed || void 0;
|
|
704
1312
|
} catch {
|
|
@@ -709,7 +1317,7 @@ async function detectAwsRegion() {
|
|
|
709
1317
|
if (process.env.AWS_REGION?.trim()) return process.env.AWS_REGION.trim();
|
|
710
1318
|
if (process.env.AWS_DEFAULT_REGION?.trim()) return process.env.AWS_DEFAULT_REGION.trim();
|
|
711
1319
|
try {
|
|
712
|
-
const { stdout } = await
|
|
1320
|
+
const { stdout } = await execa3("aws", ["configure", "get", "region"], { reject: false });
|
|
713
1321
|
const trimmed = stdout?.trim();
|
|
714
1322
|
return trimmed || void 0;
|
|
715
1323
|
} catch {
|
|
@@ -836,12 +1444,30 @@ async function gatherDeployOptions(args, projectDir, projectName) {
|
|
|
836
1444
|
awsRegion,
|
|
837
1445
|
domain,
|
|
838
1446
|
subdomain,
|
|
1447
|
+
iamServiceRoleArn: args.iamServiceRole,
|
|
1448
|
+
createIamRole: args.createIamRole,
|
|
839
1449
|
skipConfirm: args.skipConfirm
|
|
840
1450
|
};
|
|
841
1451
|
}
|
|
842
1452
|
|
|
843
1453
|
// src/index.ts
|
|
844
|
-
import
|
|
1454
|
+
import pc3 from "picocolors";
|
|
1455
|
+
function buildNonInteractiveOpts(args) {
|
|
1456
|
+
const projectName = args.projectName ?? (() => {
|
|
1457
|
+
const b = basename3(process.cwd());
|
|
1458
|
+
return b && b !== "/" ? b : "my-ampless-site";
|
|
1459
|
+
})();
|
|
1460
|
+
const siteName = args.siteName ?? "My Blog";
|
|
1461
|
+
const themes = args.themes ?? ["blog"];
|
|
1462
|
+
const plugins = args.plugins ?? ["seo"];
|
|
1463
|
+
return {
|
|
1464
|
+
projectName,
|
|
1465
|
+
siteName,
|
|
1466
|
+
themes,
|
|
1467
|
+
defaultTheme: themes[0],
|
|
1468
|
+
plugins
|
|
1469
|
+
};
|
|
1470
|
+
}
|
|
845
1471
|
async function main() {
|
|
846
1472
|
const args = parseDeployArgs(process.argv.slice(2));
|
|
847
1473
|
if (args.help) {
|
|
@@ -851,7 +1477,12 @@ async function main() {
|
|
|
851
1477
|
for (const flag of args.unknown) {
|
|
852
1478
|
log3.warn(`Unknown argument ignored: ${flag}`);
|
|
853
1479
|
}
|
|
854
|
-
|
|
1480
|
+
let opts;
|
|
1481
|
+
if (args.skipConfirm) {
|
|
1482
|
+
opts = buildNonInteractiveOpts(args);
|
|
1483
|
+
} else {
|
|
1484
|
+
opts = await runPrompts(args.projectName);
|
|
1485
|
+
}
|
|
855
1486
|
if (!opts) return;
|
|
856
1487
|
const destDir = resolve4(process.cwd(), opts.projectName);
|
|
857
1488
|
if (existsSync2(destDir)) {
|
|
@@ -875,17 +1506,17 @@ async function main() {
|
|
|
875
1506
|
try {
|
|
876
1507
|
const result = await runDeploy(deployOpts);
|
|
877
1508
|
const lines = [
|
|
878
|
-
`${
|
|
1509
|
+
`${pc3.green("\u2714")} Project deployed`,
|
|
879
1510
|
``,
|
|
880
|
-
` GitHub: ${
|
|
881
|
-
` Amplify app: ${
|
|
882
|
-
` Amplify URL: ${
|
|
1511
|
+
` GitHub: ${pc3.cyan(result.githubRepoUrl)}`,
|
|
1512
|
+
` Amplify app: ${pc3.cyan(result.amplifyAppId)}`,
|
|
1513
|
+
` Amplify URL: ${pc3.cyan(result.amplifyAppUrl)}`
|
|
883
1514
|
];
|
|
884
1515
|
if (result.domainUrl) {
|
|
885
|
-
lines.push(` Custom domain: ${
|
|
1516
|
+
lines.push(` Custom domain: ${pc3.cyan(result.domainUrl)}`);
|
|
886
1517
|
}
|
|
887
1518
|
if (result.domainVerification && result.domainVerification.length > 0) {
|
|
888
|
-
lines.push("", ` ${
|
|
1519
|
+
lines.push("", ` ${pc3.bold("Add these DNS records to verify the domain:")}`);
|
|
889
1520
|
for (const v of result.domainVerification) {
|
|
890
1521
|
lines.push(` ${v.cname} CNAME ${v.target}`);
|
|
891
1522
|
}
|
|
@@ -893,23 +1524,26 @@ async function main() {
|
|
|
893
1524
|
lines.push(
|
|
894
1525
|
"",
|
|
895
1526
|
` First build is now running in Amplify Hosting.`,
|
|
896
|
-
` Watch it at ${
|
|
1527
|
+
` Watch it at ${pc3.cyan(`https://console.aws.amazon.com/amplify/home#/${result.amplifyAppId}`)}`
|
|
897
1528
|
);
|
|
898
1529
|
outro(lines.join("\n"));
|
|
899
1530
|
} catch (err) {
|
|
1531
|
+
if (err instanceof PreflightError) {
|
|
1532
|
+
process.exit(1);
|
|
1533
|
+
}
|
|
900
1534
|
log3.error(err instanceof Error ? err.message : String(err));
|
|
901
1535
|
process.exit(1);
|
|
902
1536
|
}
|
|
903
1537
|
return;
|
|
904
1538
|
}
|
|
905
1539
|
outro(
|
|
906
|
-
`${
|
|
1540
|
+
`${pc3.green("\u2714")} Project created at ${pc3.bold(opts.projectName)}
|
|
907
1541
|
|
|
908
1542
|
Next steps:
|
|
909
|
-
${
|
|
910
|
-
${
|
|
911
|
-
${
|
|
912
|
-
${
|
|
1543
|
+
${pc3.cyan("cd")} ${opts.projectName}
|
|
1544
|
+
${pc3.cyan("npm install")}
|
|
1545
|
+
${pc3.cyan("npx ampx sandbox")} ${pc3.dim("# start Amplify backend")}
|
|
1546
|
+
${pc3.cyan("npm run dev")} ${pc3.dim("# start Next.js")}`
|
|
913
1547
|
);
|
|
914
1548
|
}
|
|
915
1549
|
main().catch((err) => {
|
|
@@ -1,6 +1,17 @@
|
|
|
1
|
+
import { fileURLToPath } from 'node:url'
|
|
2
|
+
import { dirname, resolve } from 'node:path'
|
|
1
3
|
import { a, defineData, type ClientSchema } from '@aws-amplify/backend'
|
|
2
4
|
import { amplessSchemaModels, defaultAuthorizationModes } from '@ampless/backend'
|
|
3
5
|
|
|
6
|
+
// AppSync's `a.handler.custom({ entry })` paths are resolved by CDK
|
|
7
|
+
// relative to the file that called `a.handler.custom`. When the call
|
|
8
|
+
// originates inside `@ampless/backend` (via amplessSchemaModels), the
|
|
9
|
+
// CDK synth tries to resolve `./list-published-posts.js` relative to
|
|
10
|
+
// `node_modules/@ampless/backend/dist/index.js` and fails with
|
|
11
|
+
// `UnresolvedEntryPathError`. Anchor the resolver paths at THIS file's
|
|
12
|
+
// directory so the result is unambiguous.
|
|
13
|
+
const __dirname = dirname(fileURLToPath(import.meta.url))
|
|
14
|
+
|
|
4
15
|
// Ampless's built-in models (Post / Page / Media / Taxonomy / PostTag /
|
|
5
16
|
// KvStore) plus the three public-read custom queries
|
|
6
17
|
// (listPublishedPosts / getPublishedPost / listPostsByTag).
|
|
@@ -8,19 +19,20 @@ import { amplessSchemaModels, defaultAuthorizationModes } from '@ampless/backend
|
|
|
8
19
|
// Add project-specific models alongside the spread:
|
|
9
20
|
//
|
|
10
21
|
// const schema = a.schema({
|
|
11
|
-
// ...amplessSchemaModels(a),
|
|
22
|
+
// ...amplessSchemaModels(a, { resolverPaths }),
|
|
12
23
|
// MyCustomModel: a
|
|
13
24
|
// .model({ siteId: a.string().required(), foo: a.string() })
|
|
14
25
|
// .identifier(['siteId', 'foo'])
|
|
15
26
|
// .authorization((allow) => [allow.groups(['ampless-admin'])]),
|
|
16
27
|
// })
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
28
|
+
const resolverPaths = {
|
|
29
|
+
listPublishedPosts: resolve(__dirname, 'list-published-posts.js'),
|
|
30
|
+
getPublishedPost: resolve(__dirname, 'get-published-post.js'),
|
|
31
|
+
listPostsByTag: resolve(__dirname, 'list-posts-by-tag.js'),
|
|
32
|
+
}
|
|
33
|
+
|
|
22
34
|
const schema = a.schema({
|
|
23
|
-
...amplessSchemaModels(a),
|
|
35
|
+
...amplessSchemaModels(a, { resolverPaths }),
|
|
24
36
|
})
|
|
25
37
|
|
|
26
38
|
export type Schema = ClientSchema<typeof schema>
|