create-ampless 0.2.0-alpha.3 → 0.2.0-alpha.30
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.ja.md +77 -0
- package/README.md +40 -1
- package/dist/index.js +2171 -47
- package/dist/templates/_shared/AGENTS.ja.md +104 -0
- package/dist/templates/_shared/AGENTS.md +104 -0
- package/dist/templates/_shared/README.ja.md +181 -0
- package/dist/templates/_shared/README.md +181 -0
- package/dist/templates/_shared/RUNBOOK.ja.md +150 -0
- package/dist/templates/_shared/RUNBOOK.md +19 -1
- package/dist/templates/_shared/amplify/auth/post-confirmation/resource.ts +6 -0
- package/dist/templates/_shared/amplify/backend.custom.ts +41 -0
- package/dist/templates/_shared/amplify/backend.ts +12 -2
- package/dist/templates/_shared/amplify/data/resource.custom.ts +33 -0
- package/dist/templates/_shared/amplify/data/resource.ts +43 -18
- package/dist/templates/_shared/amplify/events/processor-untrusted/resource.ts +4 -0
- package/dist/templates/_shared/amplify/functions/mcp-handler/handler.ts +1 -0
- package/dist/templates/_shared/amplify/functions/mcp-handler/resource.ts +11 -0
- package/dist/templates/_shared/amplify/functions/user-admin/handler.ts +4 -0
- package/dist/templates/_shared/amplify/functions/user-admin/resource.ts +12 -0
- package/dist/templates/_shared/amplify.yml +18 -0
- package/dist/templates/_shared/app/(admin)/admin/mcp-tokens/page.tsx +5 -0
- package/dist/templates/_shared/app/(admin)/admin/users/page.tsx +5 -0
- package/dist/templates/_shared/app/globals.css +55 -39
- package/dist/templates/_shared/app/layout.tsx +22 -1
- package/dist/templates/_shared/app/site/[siteId]/[...path]/route.ts +17 -0
- package/dist/templates/_shared/lib/admin.ts +9 -7
- package/dist/templates/_shared/lib/amplify.ts +17 -6
- package/dist/templates/_shared/package.json +11 -9
- package/dist/templates/blog/README.ja.md +22 -0
- package/dist/templates/blog/README.md +17 -47
- package/dist/templates/blog/tokens.css +26 -40
- package/dist/templates/corporate/README.ja.md +18 -0
- package/dist/templates/corporate/README.md +12 -14
- package/dist/templates/corporate/tokens.css +17 -39
- package/dist/templates/dads/README.ja.md +31 -0
- package/dist/templates/dads/README.md +13 -17
- package/dist/templates/dads/tokens.css +22 -42
- package/dist/templates/docs/README.ja.md +24 -0
- package/dist/templates/docs/README.md +10 -13
- package/dist/templates/docs/tokens.css +17 -39
- package/dist/templates/landing/README.ja.md +20 -0
- package/dist/templates/landing/README.md +14 -19
- package/dist/templates/landing/tokens.css +17 -39
- package/dist/templates/minimal/README.ja.md +14 -0
- package/dist/templates/minimal/README.md +9 -47
- package/dist/templates/minimal/tokens.css +17 -39
- package/package.json +2 -1
package/dist/index.js
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
3
|
// src/index.ts
|
|
4
|
-
import { spinner, outro, log } from "@clack/prompts";
|
|
5
|
-
import { existsSync as
|
|
6
|
-
import { resolve as
|
|
4
|
+
import { spinner as spinner2, outro as outro3, log as log5 } from "@clack/prompts";
|
|
5
|
+
import { existsSync as existsSync7 } from "fs";
|
|
6
|
+
import { basename as basename3, resolve as resolve5 } from "path";
|
|
7
7
|
|
|
8
8
|
// src/prompts.ts
|
|
9
9
|
import * as p from "@clack/prompts";
|
|
@@ -28,7 +28,10 @@ async function runPrompts(argProjectName) {
|
|
|
28
28
|
// Multiple themes can ship side-by-side. The first selected is the
|
|
29
29
|
// default active theme; admins can switch per-site at runtime. Add
|
|
30
30
|
// / remove themes later by editing themes-registry.ts and
|
|
31
|
-
// themes/<name>/.
|
|
31
|
+
// themes/<name>/. Initial values pick every shipped theme so the
|
|
32
|
+
// shared `themes-registry.ts` placeholder (which imports all of
|
|
33
|
+
// them) compiles out of the box; deselect any the project won't
|
|
34
|
+
// need before confirming.
|
|
32
35
|
themes: () => p.multiselect({
|
|
33
36
|
message: "Themes to install (space to toggle)",
|
|
34
37
|
options: [
|
|
@@ -39,7 +42,7 @@ async function runPrompts(argProjectName) {
|
|
|
39
42
|
{ value: "docs", label: "Docs \u2014 sidebar-led docs (tag-driven sections)" },
|
|
40
43
|
{ value: "dads", label: "DADS \u2014 Digital Agency Design System (Japanese government style)" }
|
|
41
44
|
],
|
|
42
|
-
initialValues: ["blog"],
|
|
45
|
+
initialValues: ["blog", "minimal", "landing", "corporate", "docs", "dads"],
|
|
43
46
|
required: true
|
|
44
47
|
}),
|
|
45
48
|
plugins: () => p.multiselect({
|
|
@@ -83,8 +86,8 @@ async function runPrompts(argProjectName) {
|
|
|
83
86
|
}
|
|
84
87
|
|
|
85
88
|
// src/scaffold.ts
|
|
86
|
-
import { cp, readFile, writeFile, readdir, mkdir, rm } from "fs/promises";
|
|
87
|
-
import { join, extname, resolve as resolve2 } from "path";
|
|
89
|
+
import { cp, readFile, writeFile, readdir as readdir2, mkdir, rm } from "fs/promises";
|
|
90
|
+
import { join as join2, extname, resolve as resolve2 } from "path";
|
|
88
91
|
|
|
89
92
|
// src/templates.ts
|
|
90
93
|
import { existsSync } from "fs";
|
|
@@ -104,6 +107,100 @@ function templatePath(theme) {
|
|
|
104
107
|
return resolve(templatesDir, theme);
|
|
105
108
|
}
|
|
106
109
|
|
|
110
|
+
// src/gitignore.ts
|
|
111
|
+
var DEFAULT_GITIGNORE = `# Dependencies
|
|
112
|
+
node_modules/
|
|
113
|
+
|
|
114
|
+
# Next.js
|
|
115
|
+
.next/
|
|
116
|
+
next-env.d.ts
|
|
117
|
+
|
|
118
|
+
# Amplify
|
|
119
|
+
.amplify/
|
|
120
|
+
amplify_outputs.json
|
|
121
|
+
|
|
122
|
+
# TypeScript build artifacts
|
|
123
|
+
*.tsbuildinfo
|
|
124
|
+
|
|
125
|
+
# Env / OS noise
|
|
126
|
+
.env
|
|
127
|
+
.env.local
|
|
128
|
+
.env.*.local
|
|
129
|
+
.DS_Store
|
|
130
|
+
|
|
131
|
+
# Logs
|
|
132
|
+
npm-debug.log*
|
|
133
|
+
yarn-debug.log*
|
|
134
|
+
yarn-error.log*
|
|
135
|
+
|
|
136
|
+
# Editor
|
|
137
|
+
.vscode/
|
|
138
|
+
.idea/
|
|
139
|
+
`;
|
|
140
|
+
|
|
141
|
+
// src/themes-registry.ts
|
|
142
|
+
import { readdir } from "fs/promises";
|
|
143
|
+
import { existsSync as existsSync2 } from "fs";
|
|
144
|
+
import { join } from "path";
|
|
145
|
+
var CUSTOM_THEME_PREFIX = "my-";
|
|
146
|
+
function toIdentifier(themeName) {
|
|
147
|
+
if (!themeName.includes("-")) return themeName;
|
|
148
|
+
return themeName.replace(/-([a-z0-9])/g, (_, c) => c.toUpperCase());
|
|
149
|
+
}
|
|
150
|
+
function isCustomTheme(themeName) {
|
|
151
|
+
return themeName.startsWith(CUSTOM_THEME_PREFIX);
|
|
152
|
+
}
|
|
153
|
+
async function discoverInstalledThemes(destDir) {
|
|
154
|
+
const themesDir = join(destDir, "themes");
|
|
155
|
+
if (!existsSync2(themesDir)) return [];
|
|
156
|
+
const entries = await readdir(themesDir, { withFileTypes: true });
|
|
157
|
+
return entries.filter((e) => e.isDirectory()).map((e) => e.name);
|
|
158
|
+
}
|
|
159
|
+
function buildRegistry(themes) {
|
|
160
|
+
const sorted = [...themes].sort((a, b) => {
|
|
161
|
+
const ca = isCustomTheme(a);
|
|
162
|
+
const cb = isCustomTheme(b);
|
|
163
|
+
if (ca !== cb) return ca ? 1 : -1;
|
|
164
|
+
return a.localeCompare(b);
|
|
165
|
+
});
|
|
166
|
+
const imports = sorted.map((t) => `import ${toIdentifier(t)} from '@/themes/${t}'`).join("\n");
|
|
167
|
+
const map = sorted.map((t) => {
|
|
168
|
+
const id = toIdentifier(t);
|
|
169
|
+
return id === t ? ` ${t},` : ` '${t}': ${id},`;
|
|
170
|
+
}).join("\n");
|
|
171
|
+
const fallback = sorted[0] ?? "blog";
|
|
172
|
+
return `// Generated by create-ampless. Lists every theme installed under
|
|
173
|
+
// \`themes/<name>/\`. Auto-managed: \`create-ampless upgrade\` and
|
|
174
|
+
// \`copy-theme\` regenerate this file from the directories present
|
|
175
|
+
// under \`themes/\`. Edits made by hand will survive only until the
|
|
176
|
+
// next upgrade.
|
|
177
|
+
//
|
|
178
|
+
// \`theme.active\` (per-site, in KvStore) picks which theme renders for
|
|
179
|
+
// each request; everything listed here gets bundled so switching is
|
|
180
|
+
// instant. Custom themes use the \`${CUSTOM_THEME_PREFIX}\` prefix and
|
|
181
|
+
// are never touched by upgrade \u2014 clone an ampless default theme into
|
|
182
|
+
// a \`${CUSTOM_THEME_PREFIX}<name>\` directory via \`copy-theme\` before
|
|
183
|
+
// modifying it.
|
|
184
|
+
|
|
185
|
+
${imports}
|
|
186
|
+
|
|
187
|
+
export const themes = {
|
|
188
|
+
${map}
|
|
189
|
+
} as const
|
|
190
|
+
|
|
191
|
+
export type ThemeName = keyof typeof themes
|
|
192
|
+
|
|
193
|
+
export const themeList = Object.values(themes)
|
|
194
|
+
|
|
195
|
+
/**
|
|
196
|
+
* Default theme used when a site has no \`theme.active\` override.
|
|
197
|
+
* Falls back to the first registered theme so an empty registry would
|
|
198
|
+
* still surface a clear runtime error rather than silent breakage.
|
|
199
|
+
*/
|
|
200
|
+
export const DEFAULT_THEME: ThemeName = (themeList[0]?.name as ThemeName) ?? '${fallback}'
|
|
201
|
+
`;
|
|
202
|
+
}
|
|
203
|
+
|
|
107
204
|
// src/scaffold.ts
|
|
108
205
|
var TEXT_EXTENSIONS = /* @__PURE__ */ new Set([
|
|
109
206
|
".json",
|
|
@@ -131,10 +228,10 @@ async function substituteFile(filePath, vars) {
|
|
|
131
228
|
if (replaced !== content) await writeFile(filePath, replaced, "utf-8");
|
|
132
229
|
}
|
|
133
230
|
async function substituteDir(dirPath, vars) {
|
|
134
|
-
const entries = await
|
|
231
|
+
const entries = await readdir2(dirPath, { withFileTypes: true });
|
|
135
232
|
await Promise.all(
|
|
136
233
|
entries.map(async (entry) => {
|
|
137
|
-
const fullPath =
|
|
234
|
+
const fullPath = join2(dirPath, entry.name);
|
|
138
235
|
if (entry.isDirectory()) {
|
|
139
236
|
await substituteDir(fullPath, vars);
|
|
140
237
|
} else {
|
|
@@ -143,30 +240,6 @@ async function substituteDir(dirPath, vars) {
|
|
|
143
240
|
})
|
|
144
241
|
);
|
|
145
242
|
}
|
|
146
|
-
function buildRegistry(themes) {
|
|
147
|
-
const imports = themes.map((t) => `import ${t} from '@/themes/${t}'`).join("\n");
|
|
148
|
-
const map = themes.map((t) => ` ${t},`).join("\n");
|
|
149
|
-
return `// Generated by create-ampless. Lists every theme installed under
|
|
150
|
-
// \`themes/<name>/\`. Adding a theme = drop a directory under themes/,
|
|
151
|
-
// add an import + map entry below, and redeploy.
|
|
152
|
-
//
|
|
153
|
-
// \`theme.active\` (per-site, in KvStore) picks which theme renders for
|
|
154
|
-
// each request; everything listed here gets bundled so switching is
|
|
155
|
-
// instant.
|
|
156
|
-
|
|
157
|
-
${imports}
|
|
158
|
-
|
|
159
|
-
export const themes = {
|
|
160
|
-
${map}
|
|
161
|
-
} as const
|
|
162
|
-
|
|
163
|
-
export type ThemeName = keyof typeof themes
|
|
164
|
-
|
|
165
|
-
export const themeList = Object.values(themes)
|
|
166
|
-
|
|
167
|
-
export const DEFAULT_THEME: ThemeName = (themeList[0]?.name as ThemeName) ?? '${themes[0]}'
|
|
168
|
-
`;
|
|
169
|
-
}
|
|
170
243
|
async function scaffold(sharedDir, themesRoot, destDir, opts) {
|
|
171
244
|
await cp(sharedDir, destDir, { recursive: true });
|
|
172
245
|
const destThemesDir = resolve2(destDir, "themes");
|
|
@@ -180,6 +253,8 @@ async function scaffold(sharedDir, themesRoot, destDir, opts) {
|
|
|
180
253
|
void themesRoot;
|
|
181
254
|
const registryPath = resolve2(destDir, "themes-registry.ts");
|
|
182
255
|
await writeFile(registryPath, buildRegistry(opts.themes), "utf-8");
|
|
256
|
+
const gitignorePath = resolve2(destDir, ".gitignore");
|
|
257
|
+
await writeFile(gitignorePath, DEFAULT_GITIGNORE, "utf-8");
|
|
183
258
|
const vars = {
|
|
184
259
|
projectName: opts.projectName,
|
|
185
260
|
siteName: opts.siteName,
|
|
@@ -191,36 +266,2085 @@ async function scaffold(sharedDir, themesRoot, destDir, opts) {
|
|
|
191
266
|
await substituteDir(destDir, vars);
|
|
192
267
|
}
|
|
193
268
|
|
|
194
|
-
// src/
|
|
269
|
+
// src/args.ts
|
|
270
|
+
var VALID_THEMES = ["blog", "minimal", "landing", "corporate", "docs", "dads"];
|
|
271
|
+
var VALID_PLUGINS = ["seo", "rss", "webhook"];
|
|
272
|
+
var STRING_FLAGS = /* @__PURE__ */ new Set([
|
|
273
|
+
"--site-name",
|
|
274
|
+
"--themes",
|
|
275
|
+
"--plugins",
|
|
276
|
+
"--github-owner",
|
|
277
|
+
"--github-token",
|
|
278
|
+
"--aws-profile",
|
|
279
|
+
"--aws-region",
|
|
280
|
+
"--domain",
|
|
281
|
+
"--subdomain",
|
|
282
|
+
"--iam-service-role"
|
|
283
|
+
]);
|
|
284
|
+
var BOOLEAN_FLAGS = /* @__PURE__ */ new Set([
|
|
285
|
+
"--deploy",
|
|
286
|
+
"--mount",
|
|
287
|
+
"--upgrade",
|
|
288
|
+
"--dry-run",
|
|
289
|
+
"--no-install",
|
|
290
|
+
"--github-private",
|
|
291
|
+
"--create-iam-role",
|
|
292
|
+
"--skip-confirm",
|
|
293
|
+
"--help",
|
|
294
|
+
"-h"
|
|
295
|
+
]);
|
|
296
|
+
function parseDeployArgs(argv) {
|
|
297
|
+
const out = {
|
|
298
|
+
deploy: false,
|
|
299
|
+
mount: false,
|
|
300
|
+
upgrade: false,
|
|
301
|
+
copyTheme: false,
|
|
302
|
+
dryRun: false,
|
|
303
|
+
noInstall: false,
|
|
304
|
+
githubPrivate: false,
|
|
305
|
+
createIamRole: false,
|
|
306
|
+
skipConfirm: false,
|
|
307
|
+
help: false,
|
|
308
|
+
unknown: []
|
|
309
|
+
};
|
|
310
|
+
for (let i = 0; i < argv.length; i++) {
|
|
311
|
+
const raw = argv[i];
|
|
312
|
+
let token = raw;
|
|
313
|
+
let inlineValue;
|
|
314
|
+
const eq = raw.indexOf("=");
|
|
315
|
+
if (raw.startsWith("--") && eq > 2) {
|
|
316
|
+
token = raw.slice(0, eq);
|
|
317
|
+
inlineValue = raw.slice(eq + 1);
|
|
318
|
+
}
|
|
319
|
+
if (BOOLEAN_FLAGS.has(token)) {
|
|
320
|
+
switch (token) {
|
|
321
|
+
case "--deploy":
|
|
322
|
+
out.deploy = true;
|
|
323
|
+
break;
|
|
324
|
+
case "--mount":
|
|
325
|
+
out.mount = true;
|
|
326
|
+
break;
|
|
327
|
+
case "--upgrade":
|
|
328
|
+
out.upgrade = true;
|
|
329
|
+
break;
|
|
330
|
+
case "--dry-run":
|
|
331
|
+
out.dryRun = true;
|
|
332
|
+
break;
|
|
333
|
+
case "--no-install":
|
|
334
|
+
out.noInstall = true;
|
|
335
|
+
break;
|
|
336
|
+
case "--github-private":
|
|
337
|
+
out.githubPrivate = true;
|
|
338
|
+
break;
|
|
339
|
+
case "--create-iam-role":
|
|
340
|
+
out.createIamRole = true;
|
|
341
|
+
break;
|
|
342
|
+
case "--skip-confirm":
|
|
343
|
+
out.skipConfirm = true;
|
|
344
|
+
break;
|
|
345
|
+
case "--help":
|
|
346
|
+
case "-h":
|
|
347
|
+
out.help = true;
|
|
348
|
+
break;
|
|
349
|
+
}
|
|
350
|
+
continue;
|
|
351
|
+
}
|
|
352
|
+
if (STRING_FLAGS.has(token)) {
|
|
353
|
+
const value = inlineValue ?? argv[++i];
|
|
354
|
+
if (value === void 0) {
|
|
355
|
+
throw new Error(`Missing value for ${token}`);
|
|
356
|
+
}
|
|
357
|
+
switch (token) {
|
|
358
|
+
case "--site-name":
|
|
359
|
+
out.siteName = value;
|
|
360
|
+
break;
|
|
361
|
+
case "--themes": {
|
|
362
|
+
const themes = value.split(",").map((t) => t.trim()).filter(Boolean);
|
|
363
|
+
const invalid = themes.filter((t) => !VALID_THEMES.includes(t));
|
|
364
|
+
if (invalid.length > 0) {
|
|
365
|
+
throw new Error(
|
|
366
|
+
`Invalid theme(s): ${invalid.join(", ")}. Valid values: ${VALID_THEMES.join(", ")}`
|
|
367
|
+
);
|
|
368
|
+
}
|
|
369
|
+
out.themes = themes;
|
|
370
|
+
break;
|
|
371
|
+
}
|
|
372
|
+
case "--plugins": {
|
|
373
|
+
const plugins = value.split(",").map((p3) => p3.trim()).filter(Boolean);
|
|
374
|
+
const invalid = plugins.filter((p3) => !VALID_PLUGINS.includes(p3));
|
|
375
|
+
if (invalid.length > 0) {
|
|
376
|
+
throw new Error(
|
|
377
|
+
`Invalid plugin(s): ${invalid.join(", ")}. Valid values: ${VALID_PLUGINS.join(", ")}`
|
|
378
|
+
);
|
|
379
|
+
}
|
|
380
|
+
out.plugins = plugins;
|
|
381
|
+
break;
|
|
382
|
+
}
|
|
383
|
+
case "--github-owner":
|
|
384
|
+
out.githubOwner = value;
|
|
385
|
+
break;
|
|
386
|
+
case "--github-token":
|
|
387
|
+
out.githubToken = value;
|
|
388
|
+
break;
|
|
389
|
+
case "--aws-profile":
|
|
390
|
+
out.awsProfile = value;
|
|
391
|
+
break;
|
|
392
|
+
case "--aws-region":
|
|
393
|
+
out.awsRegion = value;
|
|
394
|
+
break;
|
|
395
|
+
case "--domain":
|
|
396
|
+
out.domain = value;
|
|
397
|
+
break;
|
|
398
|
+
case "--subdomain":
|
|
399
|
+
out.subdomain = value;
|
|
400
|
+
break;
|
|
401
|
+
case "--iam-service-role":
|
|
402
|
+
out.iamServiceRole = value;
|
|
403
|
+
break;
|
|
404
|
+
}
|
|
405
|
+
continue;
|
|
406
|
+
}
|
|
407
|
+
if (raw.startsWith("-")) {
|
|
408
|
+
out.unknown.push(raw);
|
|
409
|
+
continue;
|
|
410
|
+
}
|
|
411
|
+
if (raw === "upgrade" && out.projectName === void 0) {
|
|
412
|
+
out.upgrade = true;
|
|
413
|
+
continue;
|
|
414
|
+
}
|
|
415
|
+
if (raw === "copy-theme" && out.projectName === void 0 && !out.copyTheme) {
|
|
416
|
+
out.copyTheme = true;
|
|
417
|
+
continue;
|
|
418
|
+
}
|
|
419
|
+
if (out.copyTheme && out.copyThemeSource === void 0) {
|
|
420
|
+
out.copyThemeSource = raw;
|
|
421
|
+
continue;
|
|
422
|
+
}
|
|
423
|
+
if (out.copyTheme && out.copyThemeTarget === void 0) {
|
|
424
|
+
out.copyThemeTarget = raw;
|
|
425
|
+
continue;
|
|
426
|
+
}
|
|
427
|
+
if (out.projectName === void 0) {
|
|
428
|
+
out.projectName = raw;
|
|
429
|
+
} else {
|
|
430
|
+
out.unknown.push(raw);
|
|
431
|
+
}
|
|
432
|
+
}
|
|
433
|
+
return out;
|
|
434
|
+
}
|
|
435
|
+
var HELP_TEXT = `create-ampless \u2014 scaffold an ampless project
|
|
436
|
+
|
|
437
|
+
Usage:
|
|
438
|
+
npx create-ampless@latest <project-name> [options]
|
|
439
|
+
npx create-ampless@latest --mount [options] # in an existing project dir
|
|
440
|
+
npx create-ampless@latest upgrade [options] # in an existing project dir
|
|
441
|
+
|
|
442
|
+
Options:
|
|
443
|
+
--site-name <name> Site display name (default: "My Blog")
|
|
444
|
+
--themes <list> Comma-separated theme names to install
|
|
445
|
+
Valid: blog, minimal, landing, corporate, docs, dads
|
|
446
|
+
(default: blog)
|
|
447
|
+
--plugins <list> Comma-separated plugin names to install
|
|
448
|
+
Valid: seo, rss, webhook
|
|
449
|
+
(default: seo)
|
|
450
|
+
--deploy Also create GitHub repo + Amplify Hosting app and
|
|
451
|
+
kick off the first deploy after scaffolding
|
|
452
|
+
--mount Skip scaffolding and mount the CURRENT directory
|
|
453
|
+
onto a new GitHub repo + Amplify Hosting app.
|
|
454
|
+
Use after you've scaffolded and tested locally
|
|
455
|
+
with 'npx ampx sandbox' and now want to publish.
|
|
456
|
+
Implies --deploy. Scaffold flags (--site-name,
|
|
457
|
+
--themes, --plugins) are ignored.
|
|
458
|
+
--github-owner <login> GitHub owner (user or org). Defaults to the
|
|
459
|
+
authenticated 'gh' user
|
|
460
|
+
--github-private Create a private repo (default: public)
|
|
461
|
+
--github-token <token> GitHub token. Falls back to GITHUB_TOKEN env,
|
|
462
|
+
then 'gh auth token', then an interactive prompt
|
|
463
|
+
--aws-profile <profile> AWS profile name to pass to the aws CLI
|
|
464
|
+
--aws-region <region> AWS region (defaults to aws config / env)
|
|
465
|
+
--domain <name> Custom domain (apex or subdomain) to attach
|
|
466
|
+
--subdomain <prefix> Subdomain prefix for the domain (default: apex)
|
|
467
|
+
--iam-service-role <arn> Existing IAM role for Amplify Hosting (must trust
|
|
468
|
+
amplify.amazonaws.com and have
|
|
469
|
+
AdministratorAccess-Amplify attached)
|
|
470
|
+
--create-iam-role Let create-ampless provision the Amplify Hosting
|
|
471
|
+
service role (idempotent; defaults to role name
|
|
472
|
+
AmplifyDeployBackend)
|
|
473
|
+
--skip-confirm Skip all interactive prompts and use defaults /
|
|
474
|
+
flag values (for CI / automation)
|
|
475
|
+
-h, --help Show this message
|
|
476
|
+
|
|
477
|
+
upgrade Sync ampless package files / deps to latest alpha.
|
|
478
|
+
Run inside an existing ampless project.
|
|
479
|
+
|
|
480
|
+
--dry-run Show what would change without writing any files
|
|
481
|
+
--no-install Skip running pnpm/npm install after updating
|
|
482
|
+
package.json
|
|
483
|
+
|
|
484
|
+
copy-theme <source> <target>
|
|
485
|
+
Copy a theme so the original stays managed by ampless and
|
|
486
|
+
customisations live in the copy. Target must start with "my-"
|
|
487
|
+
(the convention that flags it as user-owned, so upgrade leaves
|
|
488
|
+
it alone). Run inside an existing ampless project.
|
|
489
|
+
|
|
490
|
+
Example: npx create-ampless@latest copy-theme blog my-blog
|
|
491
|
+
`;
|
|
492
|
+
|
|
493
|
+
// src/deploy-prompts.ts
|
|
494
|
+
import * as p2 from "@clack/prompts";
|
|
495
|
+
import { execa as execa3 } from "execa";
|
|
496
|
+
|
|
497
|
+
// src/deploy.ts
|
|
498
|
+
import { execa as execa2 } from "execa";
|
|
499
|
+
import { existsSync as existsSync4 } from "fs";
|
|
500
|
+
import { readFile as readFile2, writeFile as writeFile2 } from "fs/promises";
|
|
501
|
+
import { basename as basename2, resolve as resolve4 } from "path";
|
|
502
|
+
import { spinner, log } from "@clack/prompts";
|
|
503
|
+
import pc2 from "picocolors";
|
|
504
|
+
|
|
505
|
+
// src/preflight.ts
|
|
506
|
+
import { execa } from "execa";
|
|
507
|
+
import { basename } from "path";
|
|
195
508
|
import pc from "picocolors";
|
|
509
|
+
var DEFAULT_AMPLIFY_ROLE_NAME = "AmplifyDeployBackend";
|
|
510
|
+
var AMPLIFY_BACKEND_POLICY_ARN = "arn:aws:iam::aws:policy/AdministratorAccess-Amplify";
|
|
511
|
+
var TRUST_POLICY_JSON = JSON.stringify({
|
|
512
|
+
Version: "2012-10-17",
|
|
513
|
+
Statement: [
|
|
514
|
+
{
|
|
515
|
+
Effect: "Allow",
|
|
516
|
+
Principal: { Service: "amplify.amazonaws.com" },
|
|
517
|
+
Action: "sts:AssumeRole"
|
|
518
|
+
}
|
|
519
|
+
]
|
|
520
|
+
});
|
|
521
|
+
async function tryExec(cmd, args) {
|
|
522
|
+
try {
|
|
523
|
+
const r = await execa(cmd, args, { reject: false, stdio: "pipe" });
|
|
524
|
+
return {
|
|
525
|
+
exitCode: r.exitCode ?? 0,
|
|
526
|
+
stdout: r.stdout?.toString() ?? "",
|
|
527
|
+
stderr: r.stderr?.toString() ?? ""
|
|
528
|
+
};
|
|
529
|
+
} catch (err) {
|
|
530
|
+
const e = err;
|
|
531
|
+
return {
|
|
532
|
+
exitCode: 1,
|
|
533
|
+
stdout: e.stdout ?? "",
|
|
534
|
+
stderr: e.stderr ?? e.message ?? String(err)
|
|
535
|
+
};
|
|
536
|
+
}
|
|
537
|
+
}
|
|
538
|
+
function awsBaseArgs(opts) {
|
|
539
|
+
const out = [];
|
|
540
|
+
if (opts.awsProfile) out.push("--profile", opts.awsProfile);
|
|
541
|
+
if (opts.awsRegion) out.push("--region", opts.awsRegion);
|
|
542
|
+
return out;
|
|
543
|
+
}
|
|
544
|
+
function awsCmd(opts, extra) {
|
|
545
|
+
return [...awsBaseArgs(opts), ...extra, "--output", "json"];
|
|
546
|
+
}
|
|
547
|
+
function shortName(opts) {
|
|
548
|
+
return basename(opts.projectDir);
|
|
549
|
+
}
|
|
550
|
+
async function checkGhInstalled(problems) {
|
|
551
|
+
const r = await tryExec("gh", ["--version"]);
|
|
552
|
+
if (r.exitCode !== 0) {
|
|
553
|
+
problems.push({
|
|
554
|
+
id: "gh-not-installed",
|
|
555
|
+
message: "GitHub CLI (`gh`) is not installed.",
|
|
556
|
+
remediation: [
|
|
557
|
+
"Install it (macOS: `brew install gh`; other OS: https://cli.github.com/),",
|
|
558
|
+
"then run `gh auth login` to sign in."
|
|
559
|
+
]
|
|
560
|
+
});
|
|
561
|
+
return false;
|
|
562
|
+
}
|
|
563
|
+
return true;
|
|
564
|
+
}
|
|
565
|
+
async function checkGhAuth(problems) {
|
|
566
|
+
const r = await tryExec("gh", ["auth", "status"]);
|
|
567
|
+
const combined = `${r.stdout}
|
|
568
|
+
${r.stderr}`;
|
|
569
|
+
if (r.exitCode !== 0 || /not logged in/i.test(combined)) {
|
|
570
|
+
problems.push({
|
|
571
|
+
id: "gh-not-authenticated",
|
|
572
|
+
message: "GitHub CLI is not authenticated.",
|
|
573
|
+
remediation: ["Run `gh auth login` and choose a method that includes the `repo` scope."]
|
|
574
|
+
});
|
|
575
|
+
return false;
|
|
576
|
+
}
|
|
577
|
+
const scopesMatch = combined.match(/Token scopes?:\s*([^\n]+)/i);
|
|
578
|
+
if (!scopesMatch) {
|
|
579
|
+
problems.push({
|
|
580
|
+
id: "gh-scopes-unknown",
|
|
581
|
+
message: "Could not determine GitHub token scopes from `gh auth status`.",
|
|
582
|
+
remediation: ["Run `gh auth refresh -s repo` to ensure the `repo` scope is granted."]
|
|
583
|
+
});
|
|
584
|
+
return false;
|
|
585
|
+
}
|
|
586
|
+
const hasRepo = /(^|[^a-z])repo([^a-z]|$)/i.test(scopesMatch[1]);
|
|
587
|
+
if (!hasRepo) {
|
|
588
|
+
problems.push({
|
|
589
|
+
id: "gh-missing-repo-scope",
|
|
590
|
+
message: "GitHub token is missing the `repo` scope.",
|
|
591
|
+
remediation: ["Run `gh auth refresh -s repo` to add the `repo` scope to your token."]
|
|
592
|
+
});
|
|
593
|
+
return false;
|
|
594
|
+
}
|
|
595
|
+
return true;
|
|
596
|
+
}
|
|
597
|
+
async function checkAwsInstalled(problems) {
|
|
598
|
+
const r = await tryExec("aws", ["--version"]);
|
|
599
|
+
if (r.exitCode !== 0) {
|
|
600
|
+
problems.push({
|
|
601
|
+
id: "aws-not-installed",
|
|
602
|
+
message: "AWS CLI (`aws`) is not installed.",
|
|
603
|
+
remediation: [
|
|
604
|
+
"Install it (macOS: `brew install awscli`; other OS: https://aws.amazon.com/cli/),",
|
|
605
|
+
"then run `aws configure` to set credentials."
|
|
606
|
+
]
|
|
607
|
+
});
|
|
608
|
+
return false;
|
|
609
|
+
}
|
|
610
|
+
return true;
|
|
611
|
+
}
|
|
612
|
+
async function checkAwsCreds(opts, problems) {
|
|
613
|
+
const r = await tryExec("aws", [...awsBaseArgs(opts), "sts", "get-caller-identity", "--output", "json"]);
|
|
614
|
+
if (r.exitCode !== 0) {
|
|
615
|
+
problems.push({
|
|
616
|
+
id: "aws-credentials-missing",
|
|
617
|
+
message: "AWS credentials are not configured (sts get-caller-identity failed).",
|
|
618
|
+
remediation: [
|
|
619
|
+
"Run `aws configure` (or set AWS_ACCESS_KEY_ID / AWS_SECRET_ACCESS_KEY / AWS_SESSION_TOKEN),",
|
|
620
|
+
opts.awsProfile ? `or check that profile \`${opts.awsProfile}\` exists in ~/.aws/config.` : "or pass --aws-profile <name> if you use named profiles."
|
|
621
|
+
]
|
|
622
|
+
});
|
|
623
|
+
return void 0;
|
|
624
|
+
}
|
|
625
|
+
try {
|
|
626
|
+
const parsed = JSON.parse(r.stdout);
|
|
627
|
+
const account = parsed.Account;
|
|
628
|
+
if (!account) {
|
|
629
|
+
problems.push({
|
|
630
|
+
id: "aws-identity-malformed",
|
|
631
|
+
message: "aws sts get-caller-identity did not return an Account field.",
|
|
632
|
+
remediation: ["Re-run `aws sts get-caller-identity` manually to inspect the response."]
|
|
633
|
+
});
|
|
634
|
+
return void 0;
|
|
635
|
+
}
|
|
636
|
+
let region = opts.awsRegion;
|
|
637
|
+
if (!region) {
|
|
638
|
+
const fromEnv = process.env.AWS_REGION?.trim() || process.env.AWS_DEFAULT_REGION?.trim();
|
|
639
|
+
if (fromEnv) region = fromEnv;
|
|
640
|
+
}
|
|
641
|
+
if (!region) {
|
|
642
|
+
const cfg = await tryExec("aws", [...awsBaseArgs(opts), "configure", "get", "region"]);
|
|
643
|
+
const trimmed = cfg.stdout.trim();
|
|
644
|
+
if (trimmed) region = trimmed;
|
|
645
|
+
}
|
|
646
|
+
if (!region) {
|
|
647
|
+
problems.push({
|
|
648
|
+
id: "aws-region-missing",
|
|
649
|
+
message: "AWS region is not set.",
|
|
650
|
+
remediation: [
|
|
651
|
+
"Pass --aws-region <name>, set AWS_REGION env var, or run `aws configure set region <name>`."
|
|
652
|
+
]
|
|
653
|
+
});
|
|
654
|
+
return void 0;
|
|
655
|
+
}
|
|
656
|
+
return { account, region };
|
|
657
|
+
} catch (err) {
|
|
658
|
+
problems.push({
|
|
659
|
+
id: "aws-identity-parse-failed",
|
|
660
|
+
message: `Could not parse aws sts get-caller-identity response: ${err instanceof Error ? err.message : String(err)}`,
|
|
661
|
+
remediation: ["Re-run `aws sts get-caller-identity` manually to inspect the response."]
|
|
662
|
+
});
|
|
663
|
+
return void 0;
|
|
664
|
+
}
|
|
665
|
+
}
|
|
666
|
+
async function checkGithubRepoFree(opts, owner, problems) {
|
|
667
|
+
if (!owner) return;
|
|
668
|
+
const name = `${owner}/${shortName(opts)}`;
|
|
669
|
+
const r = await tryExec("gh", ["repo", "view", name]);
|
|
670
|
+
if (r.exitCode === 0) {
|
|
671
|
+
problems.push({
|
|
672
|
+
id: "github-repo-exists",
|
|
673
|
+
message: `GitHub repo ${name} already exists.`,
|
|
674
|
+
remediation: [
|
|
675
|
+
"Pick a different project name, or delete the existing repo first:",
|
|
676
|
+
` gh repo delete ${name} --yes`
|
|
677
|
+
]
|
|
678
|
+
});
|
|
679
|
+
}
|
|
680
|
+
}
|
|
681
|
+
async function checkAmplifyAppNameFree(opts, problems) {
|
|
682
|
+
const name = shortName(opts);
|
|
683
|
+
const r = await tryExec(
|
|
684
|
+
"aws",
|
|
685
|
+
awsCmd(opts, [
|
|
686
|
+
"amplify",
|
|
687
|
+
"list-apps",
|
|
688
|
+
"--query",
|
|
689
|
+
`apps[?name=='${name}']`
|
|
690
|
+
])
|
|
691
|
+
);
|
|
692
|
+
if (r.exitCode !== 0) {
|
|
693
|
+
problems.push({
|
|
694
|
+
id: "amplify-list-apps-failed",
|
|
695
|
+
message: "Could not list Amplify Hosting apps to check for a name collision.",
|
|
696
|
+
remediation: [
|
|
697
|
+
"Verify your IAM identity has `amplify:ListApps` permission, then re-run.",
|
|
698
|
+
`Raw error: ${r.stderr.trim() || r.stdout.trim()}`
|
|
699
|
+
]
|
|
700
|
+
});
|
|
701
|
+
return;
|
|
702
|
+
}
|
|
703
|
+
try {
|
|
704
|
+
const arr = JSON.parse(r.stdout);
|
|
705
|
+
if (Array.isArray(arr) && arr.length > 0) {
|
|
706
|
+
problems.push({
|
|
707
|
+
id: "amplify-app-name-taken",
|
|
708
|
+
message: `Amplify Hosting already has an app named "${name}" in this region.`,
|
|
709
|
+
remediation: [
|
|
710
|
+
"Pick a different project name, or delete the existing app first:",
|
|
711
|
+
` aws amplify list-apps --query "apps[?name=='${name}'].appId" --output text${opts.awsProfile ? ` --profile ${opts.awsProfile}` : ""}${opts.awsRegion ? ` --region ${opts.awsRegion}` : ""}`,
|
|
712
|
+
" aws amplify delete-app --app-id <appId>"
|
|
713
|
+
]
|
|
714
|
+
});
|
|
715
|
+
}
|
|
716
|
+
} catch {
|
|
717
|
+
}
|
|
718
|
+
}
|
|
719
|
+
async function checkCdkBootstrap(opts, identity, problems) {
|
|
720
|
+
const r = await tryExec(
|
|
721
|
+
"aws",
|
|
722
|
+
[
|
|
723
|
+
...awsBaseArgs(opts),
|
|
724
|
+
"ssm",
|
|
725
|
+
"get-parameter",
|
|
726
|
+
"--name",
|
|
727
|
+
"/cdk-bootstrap/hnb659fds/version",
|
|
728
|
+
"--output",
|
|
729
|
+
"json"
|
|
730
|
+
]
|
|
731
|
+
);
|
|
732
|
+
if (r.exitCode !== 0) {
|
|
733
|
+
problems.push({
|
|
734
|
+
id: "cdk-not-bootstrapped",
|
|
735
|
+
message: `CDK is not bootstrapped in region ${identity.region}.`,
|
|
736
|
+
remediation: [
|
|
737
|
+
`Run: npx cdk bootstrap aws://${identity.account}/${identity.region}`
|
|
738
|
+
]
|
|
739
|
+
});
|
|
740
|
+
}
|
|
741
|
+
}
|
|
742
|
+
async function checkRoute53Zone(opts, problems) {
|
|
743
|
+
if (!opts.domain) return;
|
|
744
|
+
const registrable = extractRegistrableDomain(opts.domain);
|
|
745
|
+
const r = await tryExec(
|
|
746
|
+
"aws",
|
|
747
|
+
[
|
|
748
|
+
...awsBaseArgs(opts),
|
|
749
|
+
"route53",
|
|
750
|
+
"list-hosted-zones-by-name",
|
|
751
|
+
"--dns-name",
|
|
752
|
+
registrable,
|
|
753
|
+
"--max-items",
|
|
754
|
+
"1",
|
|
755
|
+
"--output",
|
|
756
|
+
"json"
|
|
757
|
+
]
|
|
758
|
+
);
|
|
759
|
+
if (r.exitCode !== 0) {
|
|
760
|
+
problems.push({
|
|
761
|
+
id: "route53-list-failed",
|
|
762
|
+
message: `Could not query Route 53 for zone ${registrable}.`,
|
|
763
|
+
remediation: [
|
|
764
|
+
"Either grant `route53:ListHostedZonesByName` to your IAM identity,",
|
|
765
|
+
"or be prepared to add the CNAME records Amplify prints manually after deploy.",
|
|
766
|
+
`Raw error: ${r.stderr.trim() || r.stdout.trim()}`
|
|
767
|
+
]
|
|
768
|
+
});
|
|
769
|
+
return;
|
|
770
|
+
}
|
|
771
|
+
try {
|
|
772
|
+
const parsed = JSON.parse(r.stdout);
|
|
773
|
+
const zones = parsed.HostedZones ?? [];
|
|
774
|
+
const found = zones.some(
|
|
775
|
+
(z) => z.Name && z.Name.replace(/\.$/, "") === registrable
|
|
776
|
+
);
|
|
777
|
+
if (!found) {
|
|
778
|
+
problems.push({
|
|
779
|
+
id: "route53-zone-missing",
|
|
780
|
+
message: `No Route 53 hosted zone for ${registrable} found in this AWS account.`,
|
|
781
|
+
remediation: [
|
|
782
|
+
"Amplify will still issue an ACM certificate, but you will need to add",
|
|
783
|
+
"the verification CNAMEs at your DNS provider manually. If you want",
|
|
784
|
+
"auto-validation, create a hosted zone first:",
|
|
785
|
+
` aws route53 create-hosted-zone --name ${registrable} --caller-reference $(date +%s)${opts.awsProfile ? ` --profile ${opts.awsProfile}` : ""}`
|
|
786
|
+
]
|
|
787
|
+
});
|
|
788
|
+
}
|
|
789
|
+
} catch {
|
|
790
|
+
}
|
|
791
|
+
}
|
|
792
|
+
async function resolveIamServiceRole(opts, args, problems) {
|
|
793
|
+
if (args.explicitArn) {
|
|
794
|
+
const roleName = args.explicitArn.split("/").pop() ?? args.explicitArn;
|
|
795
|
+
const r = await tryExec(
|
|
796
|
+
"aws",
|
|
797
|
+
[
|
|
798
|
+
...awsBaseArgs(opts),
|
|
799
|
+
"iam",
|
|
800
|
+
"get-role",
|
|
801
|
+
"--role-name",
|
|
802
|
+
roleName,
|
|
803
|
+
"--output",
|
|
804
|
+
"json"
|
|
805
|
+
]
|
|
806
|
+
);
|
|
807
|
+
if (r.exitCode !== 0) {
|
|
808
|
+
problems.push({
|
|
809
|
+
id: "iam-service-role-not-found",
|
|
810
|
+
message: `IAM role ${args.explicitArn} not found or inaccessible.`,
|
|
811
|
+
remediation: [
|
|
812
|
+
"Verify the ARN is correct and the role exists:",
|
|
813
|
+
` aws iam get-role --role-name ${roleName}${opts.awsProfile ? ` --profile ${opts.awsProfile}` : ""}`
|
|
814
|
+
]
|
|
815
|
+
});
|
|
816
|
+
return { willCreate: false };
|
|
817
|
+
}
|
|
818
|
+
return { arn: args.explicitArn, willCreate: false };
|
|
819
|
+
}
|
|
820
|
+
if (args.willCreate) {
|
|
821
|
+
return { willCreate: true };
|
|
822
|
+
}
|
|
823
|
+
const found = await findExistingAmplifyServiceRole(opts);
|
|
824
|
+
if (found) {
|
|
825
|
+
return { arn: found, willCreate: false };
|
|
826
|
+
}
|
|
827
|
+
problems.push({
|
|
828
|
+
id: "iam-service-role-missing",
|
|
829
|
+
message: "No Amplify Hosting service role found.",
|
|
830
|
+
remediation: [
|
|
831
|
+
"Either pass --iam-service-role <arn> pointing at an existing role,",
|
|
832
|
+
"or let create-ampless build one for you with --create-iam-role.",
|
|
833
|
+
"",
|
|
834
|
+
"Alternatively, create one yourself:",
|
|
835
|
+
` aws iam create-role --role-name ${DEFAULT_AMPLIFY_ROLE_NAME} \\`,
|
|
836
|
+
` --assume-role-policy-document '${TRUST_POLICY_JSON}'`,
|
|
837
|
+
` aws iam attach-role-policy --role-name ${DEFAULT_AMPLIFY_ROLE_NAME} \\`,
|
|
838
|
+
` --policy-arn ${AMPLIFY_BACKEND_POLICY_ARN}`,
|
|
839
|
+
` # then re-run with: --iam-service-role $(aws iam get-role --role-name ${DEFAULT_AMPLIFY_ROLE_NAME} --query Role.Arn --output text)`
|
|
840
|
+
]
|
|
841
|
+
});
|
|
842
|
+
return { willCreate: false };
|
|
843
|
+
}
|
|
844
|
+
function trustPolicyAllowsAmplify(doc) {
|
|
845
|
+
let parsed = doc;
|
|
846
|
+
if (typeof doc === "string") {
|
|
847
|
+
try {
|
|
848
|
+
parsed = JSON.parse(decodeURIComponent(doc));
|
|
849
|
+
} catch {
|
|
850
|
+
try {
|
|
851
|
+
parsed = JSON.parse(doc);
|
|
852
|
+
} catch {
|
|
853
|
+
return false;
|
|
854
|
+
}
|
|
855
|
+
}
|
|
856
|
+
}
|
|
857
|
+
const stmts = parsed?.Statement;
|
|
858
|
+
const arr = Array.isArray(stmts) ? stmts : stmts ? [stmts] : [];
|
|
859
|
+
for (const stmt of arr) {
|
|
860
|
+
const s = stmt;
|
|
861
|
+
const svc = s.Principal?.Service;
|
|
862
|
+
if (typeof svc === "string" && svc === "amplify.amazonaws.com") return true;
|
|
863
|
+
if (Array.isArray(svc) && svc.includes("amplify.amazonaws.com")) return true;
|
|
864
|
+
}
|
|
865
|
+
return false;
|
|
866
|
+
}
|
|
867
|
+
async function findExistingAmplifyServiceRole(opts) {
|
|
868
|
+
let marker;
|
|
869
|
+
for (let page = 0; page < 20; page++) {
|
|
870
|
+
const args = [
|
|
871
|
+
...awsBaseArgs(opts),
|
|
872
|
+
"iam",
|
|
873
|
+
"list-roles",
|
|
874
|
+
"--max-items",
|
|
875
|
+
"200",
|
|
876
|
+
"--output",
|
|
877
|
+
"json"
|
|
878
|
+
];
|
|
879
|
+
if (marker) args.push("--starting-token", marker);
|
|
880
|
+
const r = await tryExec("aws", args);
|
|
881
|
+
if (r.exitCode !== 0) return void 0;
|
|
882
|
+
let parsed;
|
|
883
|
+
try {
|
|
884
|
+
parsed = JSON.parse(r.stdout);
|
|
885
|
+
} catch {
|
|
886
|
+
return void 0;
|
|
887
|
+
}
|
|
888
|
+
for (const role of parsed.Roles ?? []) {
|
|
889
|
+
if (!role.RoleName || !role.Arn) continue;
|
|
890
|
+
if (!trustPolicyAllowsAmplify(role.AssumeRolePolicyDocument)) continue;
|
|
891
|
+
const pr = await tryExec(
|
|
892
|
+
"aws",
|
|
893
|
+
[
|
|
894
|
+
...awsBaseArgs(opts),
|
|
895
|
+
"iam",
|
|
896
|
+
"list-attached-role-policies",
|
|
897
|
+
"--role-name",
|
|
898
|
+
role.RoleName,
|
|
899
|
+
"--output",
|
|
900
|
+
"json"
|
|
901
|
+
]
|
|
902
|
+
);
|
|
903
|
+
if (pr.exitCode !== 0) continue;
|
|
904
|
+
try {
|
|
905
|
+
const policies = JSON.parse(pr.stdout);
|
|
906
|
+
const attached = (policies.AttachedPolicies ?? []).some(
|
|
907
|
+
(p3) => p3.PolicyArn === AMPLIFY_BACKEND_POLICY_ARN
|
|
908
|
+
);
|
|
909
|
+
if (attached) return role.Arn;
|
|
910
|
+
} catch {
|
|
911
|
+
continue;
|
|
912
|
+
}
|
|
913
|
+
}
|
|
914
|
+
if (!parsed.IsTruncated) break;
|
|
915
|
+
marker = parsed.Marker;
|
|
916
|
+
if (!marker) break;
|
|
917
|
+
}
|
|
918
|
+
return void 0;
|
|
919
|
+
}
|
|
920
|
+
async function runPreflight(opts, extra = {}) {
|
|
921
|
+
const problems = [];
|
|
922
|
+
const hasGh = await checkGhInstalled(problems);
|
|
923
|
+
if (hasGh) await checkGhAuth(problems);
|
|
924
|
+
const hasAws = await checkAwsInstalled(problems);
|
|
925
|
+
const identity = hasAws ? await checkAwsCreds(opts, problems) : void 0;
|
|
926
|
+
if (hasGh && opts.githubOwner && !extra.mountMode) {
|
|
927
|
+
await checkGithubRepoFree(opts, opts.githubOwner, problems);
|
|
928
|
+
}
|
|
929
|
+
if (identity) {
|
|
930
|
+
await checkAmplifyAppNameFree(opts, problems);
|
|
931
|
+
await checkCdkBootstrap(opts, identity, problems);
|
|
932
|
+
await checkRoute53Zone(opts, problems);
|
|
933
|
+
}
|
|
934
|
+
let willCreateIamRole = false;
|
|
935
|
+
let iamServiceRoleArn;
|
|
936
|
+
if (identity) {
|
|
937
|
+
const resolved = await resolveIamServiceRole(
|
|
938
|
+
opts,
|
|
939
|
+
{ explicitArn: extra.iamServiceRoleArn, willCreate: extra.createIamRole === true },
|
|
940
|
+
problems
|
|
941
|
+
);
|
|
942
|
+
willCreateIamRole = resolved.willCreate;
|
|
943
|
+
iamServiceRoleArn = resolved.arn;
|
|
944
|
+
}
|
|
945
|
+
if (problems.length > 0) {
|
|
946
|
+
return { problems };
|
|
947
|
+
}
|
|
948
|
+
return {
|
|
949
|
+
problems,
|
|
950
|
+
resolution: {
|
|
951
|
+
iamServiceRoleArn,
|
|
952
|
+
willCreateIamRole,
|
|
953
|
+
awsAccount: identity.account,
|
|
954
|
+
awsRegion: identity.region
|
|
955
|
+
}
|
|
956
|
+
};
|
|
957
|
+
}
|
|
958
|
+
function formatPreflightReport(problems) {
|
|
959
|
+
const lines = [];
|
|
960
|
+
lines.push(`${pc.red("\u2717")} create-ampless --deploy: prerequisites missing`);
|
|
961
|
+
lines.push("");
|
|
962
|
+
for (const p3 of problems) {
|
|
963
|
+
lines.push(` ${pc.red("\u2717")} ${p3.message}`);
|
|
964
|
+
for (const r of p3.remediation) {
|
|
965
|
+
lines.push(r ? ` ${r}` : "");
|
|
966
|
+
}
|
|
967
|
+
lines.push("");
|
|
968
|
+
}
|
|
969
|
+
lines.push("Fix the items above and re-run.");
|
|
970
|
+
return lines.join("\n");
|
|
971
|
+
}
|
|
972
|
+
function printPreflightReport(problems) {
|
|
973
|
+
process.stderr.write(`${formatPreflightReport(problems)}
|
|
974
|
+
`);
|
|
975
|
+
}
|
|
976
|
+
|
|
977
|
+
// src/mount.ts
|
|
978
|
+
import { existsSync as existsSync3 } from "fs";
|
|
979
|
+
import { resolve as resolve3 } from "path";
|
|
980
|
+
function validateMountableProject(destDir) {
|
|
981
|
+
const required = ["package.json", "cms.config.ts"];
|
|
982
|
+
for (const f of required) {
|
|
983
|
+
if (!existsSync3(resolve3(destDir, f))) {
|
|
984
|
+
return `Not an ampless project (missing ${f} in ${destDir})`;
|
|
985
|
+
}
|
|
986
|
+
}
|
|
987
|
+
const amplifyFiles = ["amplify/backend.ts", "amplify/data/resource.ts"];
|
|
988
|
+
if (!amplifyFiles.some((f) => existsSync3(resolve3(destDir, f)))) {
|
|
989
|
+
return `Not an ampless project (missing amplify/ in ${destDir})`;
|
|
990
|
+
}
|
|
991
|
+
return null;
|
|
992
|
+
}
|
|
993
|
+
function originPointsAt(origin, owner, name) {
|
|
994
|
+
const candidates = [
|
|
995
|
+
`https://github.com/${owner}/${name}`,
|
|
996
|
+
`https://github.com/${owner}/${name}.git`,
|
|
997
|
+
`git@github.com:${owner}/${name}`,
|
|
998
|
+
`git@github.com:${owner}/${name}.git`,
|
|
999
|
+
`ssh://git@github.com/${owner}/${name}`,
|
|
1000
|
+
`ssh://git@github.com/${owner}/${name}.git`
|
|
1001
|
+
];
|
|
1002
|
+
return candidates.includes(origin);
|
|
1003
|
+
}
|
|
1004
|
+
|
|
1005
|
+
// src/deploy.ts
|
|
1006
|
+
var AMPLIFY_BUILD_SPEC = `version: 1
|
|
1007
|
+
frontend:
|
|
1008
|
+
phases:
|
|
1009
|
+
preBuild:
|
|
1010
|
+
commands:
|
|
1011
|
+
- npm install
|
|
1012
|
+
build:
|
|
1013
|
+
commands:
|
|
1014
|
+
- npx ampx pipeline-deploy --branch $AWS_BRANCH --app-id $AWS_APP_ID
|
|
1015
|
+
- npm run build
|
|
1016
|
+
artifacts:
|
|
1017
|
+
baseDirectory: .next
|
|
1018
|
+
files:
|
|
1019
|
+
- '**/*'
|
|
1020
|
+
cache:
|
|
1021
|
+
paths:
|
|
1022
|
+
- node_modules/**/*
|
|
1023
|
+
- .next/cache/**/*
|
|
1024
|
+
`;
|
|
1025
|
+
async function resolveGithubToken(explicit, env = process.env) {
|
|
1026
|
+
if (explicit && explicit.trim()) return explicit.trim();
|
|
1027
|
+
const envToken = env.GITHUB_TOKEN;
|
|
1028
|
+
if (envToken && envToken.trim()) return envToken.trim();
|
|
1029
|
+
try {
|
|
1030
|
+
const { stdout } = await execa2("gh", ["auth", "token"], { reject: false });
|
|
1031
|
+
const trimmed = stdout?.trim();
|
|
1032
|
+
if (trimmed) return trimmed;
|
|
1033
|
+
} catch {
|
|
1034
|
+
}
|
|
1035
|
+
return void 0;
|
|
1036
|
+
}
|
|
1037
|
+
function extractRegistrableDomain(domain) {
|
|
1038
|
+
const labels = domain.replace(/\.$/, "").split(".");
|
|
1039
|
+
if (labels.length <= 2) return labels.join(".");
|
|
1040
|
+
const MULTI_PART_SUFFIXES = /* @__PURE__ */ new Set([
|
|
1041
|
+
"co.uk",
|
|
1042
|
+
"org.uk",
|
|
1043
|
+
"me.uk",
|
|
1044
|
+
"gov.uk",
|
|
1045
|
+
"ac.uk",
|
|
1046
|
+
"co.jp",
|
|
1047
|
+
"ne.jp",
|
|
1048
|
+
"or.jp",
|
|
1049
|
+
"ac.jp",
|
|
1050
|
+
"go.jp",
|
|
1051
|
+
"ad.jp",
|
|
1052
|
+
"gr.jp",
|
|
1053
|
+
"lg.jp",
|
|
1054
|
+
"com.au",
|
|
1055
|
+
"net.au",
|
|
1056
|
+
"org.au",
|
|
1057
|
+
"edu.au",
|
|
1058
|
+
"gov.au",
|
|
1059
|
+
"co.nz",
|
|
1060
|
+
"net.nz",
|
|
1061
|
+
"org.nz",
|
|
1062
|
+
"com.br",
|
|
1063
|
+
"com.mx",
|
|
1064
|
+
"com.cn",
|
|
1065
|
+
"com.sg",
|
|
1066
|
+
"com.hk",
|
|
1067
|
+
"com.tw",
|
|
1068
|
+
"co.kr",
|
|
1069
|
+
"or.kr",
|
|
1070
|
+
"co.in",
|
|
1071
|
+
"co.id",
|
|
1072
|
+
"co.za"
|
|
1073
|
+
]);
|
|
1074
|
+
const lastTwo = labels.slice(-2).join(".");
|
|
1075
|
+
if (MULTI_PART_SUFFIXES.has(lastTwo) && labels.length >= 3) {
|
|
1076
|
+
return labels.slice(-3).join(".");
|
|
1077
|
+
}
|
|
1078
|
+
return lastTwo;
|
|
1079
|
+
}
|
|
1080
|
+
function splitDomain(domain, subdomain) {
|
|
1081
|
+
const registrable = extractRegistrableDomain(domain);
|
|
1082
|
+
if (subdomain !== void 0) {
|
|
1083
|
+
return { registrable, subdomain };
|
|
1084
|
+
}
|
|
1085
|
+
if (domain === registrable) {
|
|
1086
|
+
return { registrable, subdomain: "" };
|
|
1087
|
+
}
|
|
1088
|
+
const prefix = domain.slice(0, domain.length - registrable.length - 1);
|
|
1089
|
+
return { registrable, subdomain: prefix };
|
|
1090
|
+
}
|
|
1091
|
+
async function rewriteCmsConfigForDomain(projectDir, fullDomain) {
|
|
1092
|
+
const path = resolve4(projectDir, "cms.config.ts");
|
|
1093
|
+
if (!existsSync4(path)) {
|
|
1094
|
+
return { urlRewritten: false, sitesInjected: false };
|
|
1095
|
+
}
|
|
1096
|
+
let content = await readFile2(path, "utf-8");
|
|
1097
|
+
let urlRewritten = false;
|
|
1098
|
+
let sitesInjected = false;
|
|
1099
|
+
const urlRe = /url:\s*['"]http:\/\/localhost:3000['"]/;
|
|
1100
|
+
if (urlRe.test(content)) {
|
|
1101
|
+
content = content.replace(urlRe, `url: 'https://${fullDomain}'`);
|
|
1102
|
+
urlRewritten = true;
|
|
1103
|
+
}
|
|
1104
|
+
const sitesActiveRe = /^\s*sites:\s*\{/m;
|
|
1105
|
+
if (!sitesActiveRe.test(content)) {
|
|
1106
|
+
const siteCloseRe = /(\n(\s*)site:\s*\{[\s\S]*?\n\2\},)/;
|
|
1107
|
+
const m = siteCloseRe.exec(content);
|
|
1108
|
+
if (m) {
|
|
1109
|
+
const indent = m[2] ?? " ";
|
|
1110
|
+
const inner = indent + " ";
|
|
1111
|
+
const innermost = inner + " ";
|
|
1112
|
+
const inject = `
|
|
1113
|
+
${indent}sites: {
|
|
1114
|
+
${inner}default: {
|
|
1115
|
+
${innermost}domains: ['${fullDomain}'],
|
|
1116
|
+
${inner}},
|
|
1117
|
+
${indent}},`;
|
|
1118
|
+
content = content.replace(siteCloseRe, `$1${inject}`);
|
|
1119
|
+
sitesInjected = true;
|
|
1120
|
+
}
|
|
1121
|
+
}
|
|
1122
|
+
if (urlRewritten || sitesInjected) {
|
|
1123
|
+
await writeFile2(path, content, "utf-8");
|
|
1124
|
+
}
|
|
1125
|
+
return { urlRewritten, sitesInjected };
|
|
1126
|
+
}
|
|
1127
|
+
async function run(cmd, args, opts = {}) {
|
|
1128
|
+
const { step, ...execaOpts } = opts;
|
|
1129
|
+
try {
|
|
1130
|
+
const result = await execa2(cmd, args, { stdio: "pipe", ...execaOpts });
|
|
1131
|
+
return result.stdout?.toString() ?? "";
|
|
1132
|
+
} catch (err) {
|
|
1133
|
+
const e = err;
|
|
1134
|
+
const detail = (e.stderr || e.stdout || e.shortMessage || e.message || String(err)).trim();
|
|
1135
|
+
const prefix = step ? `${step}: ` : "";
|
|
1136
|
+
throw new Error(`${prefix}${cmd} ${args.join(" ")}
|
|
1137
|
+
${detail}`);
|
|
1138
|
+
}
|
|
1139
|
+
}
|
|
1140
|
+
function awsArgs(opts, extra) {
|
|
1141
|
+
const base = [];
|
|
1142
|
+
if (opts.awsProfile) base.push("--profile", opts.awsProfile);
|
|
1143
|
+
if (opts.awsRegion) base.push("--region", opts.awsRegion);
|
|
1144
|
+
base.push(...extra);
|
|
1145
|
+
base.push("--output", "json");
|
|
1146
|
+
return base;
|
|
1147
|
+
}
|
|
1148
|
+
async function isGitRepo(dir) {
|
|
1149
|
+
const r = await execa2("git", ["rev-parse", "--git-dir"], { cwd: dir, reject: false });
|
|
1150
|
+
return r.exitCode === 0;
|
|
1151
|
+
}
|
|
1152
|
+
async function isDirty(dir) {
|
|
1153
|
+
const r = await execa2("git", ["status", "--porcelain"], { cwd: dir, reject: false });
|
|
1154
|
+
return (r.stdout?.toString() ?? "").trim().length > 0;
|
|
1155
|
+
}
|
|
1156
|
+
async function currentBranch(dir) {
|
|
1157
|
+
const r = await execa2("git", ["rev-parse", "--abbrev-ref", "HEAD"], {
|
|
1158
|
+
cwd: dir,
|
|
1159
|
+
reject: false
|
|
1160
|
+
});
|
|
1161
|
+
if (r.exitCode !== 0) return null;
|
|
1162
|
+
const name = r.stdout?.toString().trim();
|
|
1163
|
+
return name && name !== "HEAD" ? name : null;
|
|
1164
|
+
}
|
|
1165
|
+
async function ensureGitignore(dir) {
|
|
1166
|
+
const target = resolve4(dir, ".gitignore");
|
|
1167
|
+
if (existsSync4(target)) return;
|
|
1168
|
+
await writeFile2(target, DEFAULT_GITIGNORE, "utf-8");
|
|
1169
|
+
}
|
|
1170
|
+
async function gitInitOrReuse(dir, mount) {
|
|
1171
|
+
const repo = await isGitRepo(dir);
|
|
1172
|
+
if (!repo) {
|
|
1173
|
+
await run("git", ["init", "-b", "main"], { cwd: dir, step: "git init" });
|
|
1174
|
+
}
|
|
1175
|
+
const dirty = await isDirty(dir);
|
|
1176
|
+
if (dirty) {
|
|
1177
|
+
await run("git", ["add", "."], { cwd: dir, step: "git add" });
|
|
1178
|
+
const message = mount && repo ? "Prepare for create-ampless --mount" : "Initial scaffold (create-ampless)";
|
|
1179
|
+
await run("git", ["commit", "-m", message], { cwd: dir, step: "git commit" });
|
|
1180
|
+
}
|
|
1181
|
+
if (mount) {
|
|
1182
|
+
const branch = await currentBranch(dir);
|
|
1183
|
+
if (branch && branch !== "main") {
|
|
1184
|
+
log.warn(
|
|
1185
|
+
`Current branch is "${branch}" \u2014 Amplify Hosting will be wired to the "main" branch. Push from "main" later, or rename your branch with \`git branch -m main\`.`
|
|
1186
|
+
);
|
|
1187
|
+
}
|
|
1188
|
+
}
|
|
1189
|
+
}
|
|
1190
|
+
function shortName2(opts) {
|
|
1191
|
+
return basename2(opts.projectDir);
|
|
1192
|
+
}
|
|
1193
|
+
async function ghRepoExists(name, token) {
|
|
1194
|
+
const r = await execa2("gh", ["repo", "view", name, "--json", "nameWithOwner"], {
|
|
1195
|
+
reject: false,
|
|
1196
|
+
env: { ...process.env, GH_TOKEN: token }
|
|
1197
|
+
});
|
|
1198
|
+
if (r.exitCode !== 0) return false;
|
|
1199
|
+
try {
|
|
1200
|
+
const parsed = JSON.parse(r.stdout?.toString() ?? "{}");
|
|
1201
|
+
const resolved = parsed.nameWithOwner?.toLowerCase();
|
|
1202
|
+
return resolved === name.toLowerCase();
|
|
1203
|
+
} catch {
|
|
1204
|
+
return true;
|
|
1205
|
+
}
|
|
1206
|
+
}
|
|
1207
|
+
async function getOriginUrl(dir) {
|
|
1208
|
+
const r = await execa2("git", ["remote", "get-url", "origin"], {
|
|
1209
|
+
cwd: dir,
|
|
1210
|
+
reject: false
|
|
1211
|
+
});
|
|
1212
|
+
if (r.exitCode !== 0) return null;
|
|
1213
|
+
const url = r.stdout?.toString().trim();
|
|
1214
|
+
return url ? url : null;
|
|
1215
|
+
}
|
|
1216
|
+
function repoHttpsUrl(owner, name) {
|
|
1217
|
+
return `https://github.com/${owner}/${name}`;
|
|
1218
|
+
}
|
|
1219
|
+
async function ghRepoCreate(opts) {
|
|
1220
|
+
const owner = opts.githubOwner;
|
|
1221
|
+
const name = shortName2(opts);
|
|
1222
|
+
const fullName = `${owner}/${name}`;
|
|
1223
|
+
const visibility = opts.githubPrivate ? "--private" : "--public";
|
|
1224
|
+
const exists = await ghRepoExists(fullName, opts.githubToken);
|
|
1225
|
+
if (exists) {
|
|
1226
|
+
const origin = await getOriginUrl(opts.projectDir);
|
|
1227
|
+
if (origin && !originPointsAt(origin, owner, name)) {
|
|
1228
|
+
throw new Error(
|
|
1229
|
+
`Existing 'origin' remote points at ${origin}, but --mount expects ${fullName}.
|
|
1230
|
+
Remove or rename the existing remote (\`git remote remove origin\`) and re-run.`
|
|
1231
|
+
);
|
|
1232
|
+
}
|
|
1233
|
+
if (!origin) {
|
|
1234
|
+
await run(
|
|
1235
|
+
"git",
|
|
1236
|
+
["remote", "add", "origin", `https://github.com/${fullName}.git`],
|
|
1237
|
+
{ cwd: opts.projectDir, step: "git remote add origin" }
|
|
1238
|
+
);
|
|
1239
|
+
}
|
|
1240
|
+
await run(
|
|
1241
|
+
"git",
|
|
1242
|
+
["push", "-u", "origin", "HEAD:main"],
|
|
1243
|
+
{
|
|
1244
|
+
cwd: opts.projectDir,
|
|
1245
|
+
step: "git push origin main",
|
|
1246
|
+
env: { ...process.env, GH_TOKEN: opts.githubToken }
|
|
1247
|
+
}
|
|
1248
|
+
);
|
|
1249
|
+
return repoHttpsUrl(owner, name);
|
|
1250
|
+
}
|
|
1251
|
+
const out = await run(
|
|
1252
|
+
"gh",
|
|
1253
|
+
[
|
|
1254
|
+
"repo",
|
|
1255
|
+
"create",
|
|
1256
|
+
fullName,
|
|
1257
|
+
"--source",
|
|
1258
|
+
opts.projectDir,
|
|
1259
|
+
"--push",
|
|
1260
|
+
visibility,
|
|
1261
|
+
"--description",
|
|
1262
|
+
"Created by create-ampless"
|
|
1263
|
+
],
|
|
1264
|
+
{
|
|
1265
|
+
cwd: opts.projectDir,
|
|
1266
|
+
step: "gh repo create",
|
|
1267
|
+
env: { ...process.env, GH_TOKEN: opts.githubToken }
|
|
1268
|
+
}
|
|
1269
|
+
);
|
|
1270
|
+
const match = out.match(/https:\/\/github\.com\/[^\s]+/g);
|
|
1271
|
+
if (match && match.length > 0) return match[match.length - 1].replace(/[.,]$/, "");
|
|
1272
|
+
return repoHttpsUrl(owner, name);
|
|
1273
|
+
}
|
|
1274
|
+
async function amplifyCreateApp(opts, repoUrl, iamServiceRoleArn) {
|
|
1275
|
+
const cmd = [
|
|
1276
|
+
"amplify",
|
|
1277
|
+
"create-app",
|
|
1278
|
+
"--name",
|
|
1279
|
+
shortName2(opts),
|
|
1280
|
+
"--repository",
|
|
1281
|
+
repoUrl,
|
|
1282
|
+
"--access-token",
|
|
1283
|
+
opts.githubToken,
|
|
1284
|
+
"--platform",
|
|
1285
|
+
"WEB_COMPUTE",
|
|
1286
|
+
"--build-spec",
|
|
1287
|
+
AMPLIFY_BUILD_SPEC
|
|
1288
|
+
];
|
|
1289
|
+
if (iamServiceRoleArn) {
|
|
1290
|
+
cmd.push("--iam-service-role-arn", iamServiceRoleArn);
|
|
1291
|
+
}
|
|
1292
|
+
const out = await run(
|
|
1293
|
+
"aws",
|
|
1294
|
+
awsArgs(opts, cmd),
|
|
1295
|
+
{ step: "aws amplify create-app" }
|
|
1296
|
+
);
|
|
1297
|
+
const parsed = JSON.parse(out);
|
|
1298
|
+
const appId = parsed.app?.appId;
|
|
1299
|
+
if (!appId) throw new Error("aws amplify create-app: missing app.appId in response");
|
|
1300
|
+
return {
|
|
1301
|
+
appId,
|
|
1302
|
+
defaultDomain: parsed.app?.defaultDomain ?? "amplifyapp.com"
|
|
1303
|
+
};
|
|
1304
|
+
}
|
|
1305
|
+
async function amplifyCreateBranch(opts, appId) {
|
|
1306
|
+
await run(
|
|
1307
|
+
"aws",
|
|
1308
|
+
awsArgs(opts, [
|
|
1309
|
+
"amplify",
|
|
1310
|
+
"create-branch",
|
|
1311
|
+
"--app-id",
|
|
1312
|
+
appId,
|
|
1313
|
+
"--branch-name",
|
|
1314
|
+
"main",
|
|
1315
|
+
"--stage",
|
|
1316
|
+
"PRODUCTION"
|
|
1317
|
+
]),
|
|
1318
|
+
{ step: "aws amplify create-branch" }
|
|
1319
|
+
);
|
|
1320
|
+
}
|
|
1321
|
+
async function amplifyStartJob(opts, appId) {
|
|
1322
|
+
await run(
|
|
1323
|
+
"aws",
|
|
1324
|
+
awsArgs(opts, [
|
|
1325
|
+
"amplify",
|
|
1326
|
+
"start-job",
|
|
1327
|
+
"--app-id",
|
|
1328
|
+
appId,
|
|
1329
|
+
"--branch-name",
|
|
1330
|
+
"main",
|
|
1331
|
+
"--job-type",
|
|
1332
|
+
"RELEASE"
|
|
1333
|
+
]),
|
|
1334
|
+
{ step: "aws amplify start-job" }
|
|
1335
|
+
);
|
|
1336
|
+
}
|
|
1337
|
+
async function findRoute53Zone(opts, registrable) {
|
|
1338
|
+
try {
|
|
1339
|
+
const out = await run(
|
|
1340
|
+
"aws",
|
|
1341
|
+
awsArgs(opts, [
|
|
1342
|
+
"route53",
|
|
1343
|
+
"list-hosted-zones-by-name",
|
|
1344
|
+
"--dns-name",
|
|
1345
|
+
registrable,
|
|
1346
|
+
"--max-items",
|
|
1347
|
+
"1"
|
|
1348
|
+
]),
|
|
1349
|
+
{ step: "aws route53 list-hosted-zones-by-name" }
|
|
1350
|
+
);
|
|
1351
|
+
const parsed = JSON.parse(out);
|
|
1352
|
+
const zones = parsed.HostedZones ?? [];
|
|
1353
|
+
for (const z of zones) {
|
|
1354
|
+
if (z.Name && z.Name.replace(/\.$/, "") === registrable && z.Id) {
|
|
1355
|
+
return z.Id.replace("/hostedzone/", "");
|
|
1356
|
+
}
|
|
1357
|
+
}
|
|
1358
|
+
} catch {
|
|
1359
|
+
}
|
|
1360
|
+
return void 0;
|
|
1361
|
+
}
|
|
1362
|
+
async function amplifyCreateDomain(opts, appId) {
|
|
1363
|
+
const { registrable, subdomain } = splitDomain(opts.domain, opts.subdomain);
|
|
1364
|
+
const fullName = subdomain ? `${subdomain}.${registrable}` : registrable;
|
|
1365
|
+
const route53Zone = await findRoute53Zone(opts, registrable);
|
|
1366
|
+
if (route53Zone) {
|
|
1367
|
+
log.info(
|
|
1368
|
+
`Route 53 hosted zone detected for ${pc2.cyan(registrable)} \u2014 Amplify will auto-create DNS records once ACM validates.`
|
|
1369
|
+
);
|
|
1370
|
+
}
|
|
1371
|
+
const out = await run(
|
|
1372
|
+
"aws",
|
|
1373
|
+
awsArgs(opts, [
|
|
1374
|
+
"amplify",
|
|
1375
|
+
"create-domain-association",
|
|
1376
|
+
"--app-id",
|
|
1377
|
+
appId,
|
|
1378
|
+
"--domain-name",
|
|
1379
|
+
registrable,
|
|
1380
|
+
"--sub-domain-settings",
|
|
1381
|
+
`prefix=${subdomain},branchName=main`
|
|
1382
|
+
]),
|
|
1383
|
+
{ step: "aws amplify create-domain-association" }
|
|
1384
|
+
);
|
|
1385
|
+
const parsed = JSON.parse(out);
|
|
1386
|
+
const verification = [];
|
|
1387
|
+
const certRecord = parsed.domainAssociation?.certificateVerificationDNSRecord;
|
|
1388
|
+
if (certRecord) {
|
|
1389
|
+
const parts = certRecord.trim().split(/\s+/);
|
|
1390
|
+
if (parts.length >= 3) {
|
|
1391
|
+
verification.push({
|
|
1392
|
+
cname: parts[0].replace(/\.$/, ""),
|
|
1393
|
+
target: parts.slice(2).join(" ").replace(/\.$/, "")
|
|
1394
|
+
});
|
|
1395
|
+
}
|
|
1396
|
+
}
|
|
1397
|
+
for (const sd of parsed.domainAssociation?.subDomains ?? []) {
|
|
1398
|
+
if (!sd.dnsRecord) continue;
|
|
1399
|
+
const parts = sd.dnsRecord.trim().split(/\s+/);
|
|
1400
|
+
if (parts.length >= 3) {
|
|
1401
|
+
const prefix = sd.subDomainSetting?.prefix ?? "";
|
|
1402
|
+
const cname = prefix ? `${prefix}.${registrable}` : registrable;
|
|
1403
|
+
verification.push({
|
|
1404
|
+
cname,
|
|
1405
|
+
target: parts.slice(2).join(" ").replace(/\.$/, "")
|
|
1406
|
+
});
|
|
1407
|
+
}
|
|
1408
|
+
}
|
|
1409
|
+
return {
|
|
1410
|
+
domainUrl: `https://${fullName}`,
|
|
1411
|
+
verification: route53Zone ? void 0 : verification.length > 0 ? verification : void 0
|
|
1412
|
+
};
|
|
1413
|
+
}
|
|
1414
|
+
async function provisionIamServiceRole(opts) {
|
|
1415
|
+
const roleName = DEFAULT_AMPLIFY_ROLE_NAME;
|
|
1416
|
+
const trustPolicy = JSON.stringify({
|
|
1417
|
+
Version: "2012-10-17",
|
|
1418
|
+
Statement: [
|
|
1419
|
+
{
|
|
1420
|
+
Effect: "Allow",
|
|
1421
|
+
Principal: { Service: "amplify.amazonaws.com" },
|
|
1422
|
+
Action: "sts:AssumeRole"
|
|
1423
|
+
}
|
|
1424
|
+
]
|
|
1425
|
+
});
|
|
1426
|
+
const createArgs = [
|
|
1427
|
+
"iam",
|
|
1428
|
+
"create-role",
|
|
1429
|
+
"--role-name",
|
|
1430
|
+
roleName,
|
|
1431
|
+
"--assume-role-policy-document",
|
|
1432
|
+
trustPolicy,
|
|
1433
|
+
"--description",
|
|
1434
|
+
"Amplify Hosting service role created by create-ampless"
|
|
1435
|
+
];
|
|
1436
|
+
try {
|
|
1437
|
+
await run("aws", awsArgs(opts, createArgs), { step: "aws iam create-role" });
|
|
1438
|
+
} catch (err) {
|
|
1439
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
1440
|
+
if (!/EntityAlreadyExists/i.test(msg) && !/already exists/i.test(msg)) {
|
|
1441
|
+
throw err;
|
|
1442
|
+
}
|
|
1443
|
+
}
|
|
1444
|
+
await run(
|
|
1445
|
+
"aws",
|
|
1446
|
+
awsArgs(opts, [
|
|
1447
|
+
"iam",
|
|
1448
|
+
"attach-role-policy",
|
|
1449
|
+
"--role-name",
|
|
1450
|
+
roleName,
|
|
1451
|
+
"--policy-arn",
|
|
1452
|
+
AMPLIFY_BACKEND_POLICY_ARN
|
|
1453
|
+
]),
|
|
1454
|
+
{ step: "aws iam attach-role-policy" }
|
|
1455
|
+
);
|
|
1456
|
+
const out = await run(
|
|
1457
|
+
"aws",
|
|
1458
|
+
awsArgs(opts, ["iam", "get-role", "--role-name", roleName]),
|
|
1459
|
+
{ step: "aws iam get-role" }
|
|
1460
|
+
);
|
|
1461
|
+
const parsed = JSON.parse(out);
|
|
1462
|
+
const arn = parsed.Role?.Arn;
|
|
1463
|
+
if (!arn) {
|
|
1464
|
+
throw new Error(`aws iam get-role for ${roleName}: missing Role.Arn in response`);
|
|
1465
|
+
}
|
|
1466
|
+
return arn;
|
|
1467
|
+
}
|
|
1468
|
+
var PreflightError = class extends Error {
|
|
1469
|
+
result;
|
|
1470
|
+
constructor(result) {
|
|
1471
|
+
super("create-ampless --deploy: pre-flight failed");
|
|
1472
|
+
this.name = "PreflightError";
|
|
1473
|
+
this.result = result;
|
|
1474
|
+
}
|
|
1475
|
+
};
|
|
1476
|
+
async function runDeploy(opts) {
|
|
1477
|
+
const pre = await runPreflight(opts, {
|
|
1478
|
+
iamServiceRoleArn: opts.iamServiceRoleArn,
|
|
1479
|
+
createIamRole: opts.createIamRole === true,
|
|
1480
|
+
mountMode: opts.mount === true
|
|
1481
|
+
});
|
|
1482
|
+
if (pre.problems.length > 0) {
|
|
1483
|
+
printPreflightReport(pre.problems);
|
|
1484
|
+
throw new PreflightError(pre);
|
|
1485
|
+
}
|
|
1486
|
+
const resolution = pre.resolution;
|
|
1487
|
+
let serviceRoleArn = resolution.iamServiceRoleArn;
|
|
1488
|
+
if (resolution.willCreateIamRole && !serviceRoleArn) {
|
|
1489
|
+
const s0 = spinner();
|
|
1490
|
+
s0.start(`Provisioning IAM service role ${DEFAULT_AMPLIFY_ROLE_NAME}`);
|
|
1491
|
+
try {
|
|
1492
|
+
serviceRoleArn = await provisionIamServiceRole(opts);
|
|
1493
|
+
s0.stop(`IAM role: ${serviceRoleArn}`);
|
|
1494
|
+
} catch (err) {
|
|
1495
|
+
s0.stop("IAM role provisioning: failed");
|
|
1496
|
+
throw new Error(
|
|
1497
|
+
`Failed to provision IAM service role: ${err instanceof Error ? err.message : String(err)}`
|
|
1498
|
+
);
|
|
1499
|
+
}
|
|
1500
|
+
}
|
|
1501
|
+
await writeFile2(resolve4(opts.projectDir, "amplify.yml"), AMPLIFY_BUILD_SPEC, "utf-8");
|
|
1502
|
+
if (opts.domain) {
|
|
1503
|
+
const { registrable, subdomain } = splitDomain(opts.domain, opts.subdomain);
|
|
1504
|
+
const fullDomain = subdomain ? `${subdomain}.${registrable}` : registrable;
|
|
1505
|
+
const mutations = await rewriteCmsConfigForDomain(opts.projectDir, fullDomain);
|
|
1506
|
+
if (mutations.urlRewritten || mutations.sitesInjected) {
|
|
1507
|
+
const parts = [];
|
|
1508
|
+
if (mutations.urlRewritten) parts.push(`site.url \u2192 https://${fullDomain}`);
|
|
1509
|
+
if (mutations.sitesInjected) parts.push(`sites.default.domains += ${fullDomain}`);
|
|
1510
|
+
log.info(`cms.config.ts updated: ${parts.join(", ")}`);
|
|
1511
|
+
}
|
|
1512
|
+
}
|
|
1513
|
+
const created = {};
|
|
1514
|
+
const fail = (step, cause) => {
|
|
1515
|
+
const msg = cause instanceof Error ? cause.message : String(cause);
|
|
1516
|
+
const cleanup = [];
|
|
1517
|
+
if (created.appId) {
|
|
1518
|
+
cleanup.push(
|
|
1519
|
+
` - Amplify app: ${created.appId} (delete: aws amplify delete-app --app-id ${created.appId}${opts.awsProfile ? ` --profile ${opts.awsProfile}` : ""}${opts.awsRegion ? ` --region ${opts.awsRegion}` : ""})`
|
|
1520
|
+
);
|
|
1521
|
+
}
|
|
1522
|
+
if (created.repoUrl) {
|
|
1523
|
+
cleanup.push(
|
|
1524
|
+
` - GitHub repo: ${created.repoUrl} (delete: gh repo delete ${opts.githubOwner}/${shortName2(opts)} --yes)`
|
|
1525
|
+
);
|
|
1526
|
+
}
|
|
1527
|
+
const cleanupBlock = cleanup.length > 0 ? `
|
|
1528
|
+
Created so far:
|
|
1529
|
+
${cleanup.join("\n")}
|
|
1530
|
+
|
|
1531
|
+
Re-run after cleaning up.` : "";
|
|
1532
|
+
throw new Error(`Deploy failed at: ${step}
|
|
1533
|
+
${msg}${cleanupBlock}`);
|
|
1534
|
+
};
|
|
1535
|
+
let s = spinner();
|
|
1536
|
+
const gitMsg = opts.mount ? "git: preparing repo for mount" : "git init + initial commit";
|
|
1537
|
+
s.start(gitMsg);
|
|
1538
|
+
try {
|
|
1539
|
+
if (opts.mount) {
|
|
1540
|
+
await ensureGitignore(opts.projectDir);
|
|
1541
|
+
}
|
|
1542
|
+
await gitInitOrReuse(opts.projectDir, opts.mount === true);
|
|
1543
|
+
s.stop(opts.mount ? "git: ready" : "git: committed initial scaffold");
|
|
1544
|
+
} catch (err) {
|
|
1545
|
+
s.stop("git: failed");
|
|
1546
|
+
fail("git init / commit", err);
|
|
1547
|
+
}
|
|
1548
|
+
s = spinner();
|
|
1549
|
+
s.start("Creating GitHub repo + pushing");
|
|
1550
|
+
try {
|
|
1551
|
+
created.repoUrl = await ghRepoCreate(opts);
|
|
1552
|
+
s.stop(`GitHub: ${created.repoUrl}`);
|
|
1553
|
+
} catch (err) {
|
|
1554
|
+
s.stop("GitHub: failed");
|
|
1555
|
+
fail("gh repo create", err);
|
|
1556
|
+
}
|
|
1557
|
+
let app;
|
|
1558
|
+
s = spinner();
|
|
1559
|
+
s.start("Creating Amplify Hosting app");
|
|
1560
|
+
try {
|
|
1561
|
+
app = await amplifyCreateApp(opts, created.repoUrl, serviceRoleArn);
|
|
1562
|
+
created.appId = app.appId;
|
|
1563
|
+
s.stop(`Amplify app: ${app.appId}`);
|
|
1564
|
+
} catch (err) {
|
|
1565
|
+
s.stop("Amplify create-app: failed");
|
|
1566
|
+
return fail("aws amplify create-app", err);
|
|
1567
|
+
}
|
|
1568
|
+
s = spinner();
|
|
1569
|
+
s.start("Creating main branch");
|
|
1570
|
+
try {
|
|
1571
|
+
await amplifyCreateBranch(opts, app.appId);
|
|
1572
|
+
s.stop("Amplify branch: main");
|
|
1573
|
+
} catch (err) {
|
|
1574
|
+
s.stop("Amplify create-branch: failed");
|
|
1575
|
+
fail("aws amplify create-branch", err);
|
|
1576
|
+
}
|
|
1577
|
+
s = spinner();
|
|
1578
|
+
s.start("Starting first deployment");
|
|
1579
|
+
try {
|
|
1580
|
+
await amplifyStartJob(opts, app.appId);
|
|
1581
|
+
s.stop("Amplify: first job started");
|
|
1582
|
+
} catch (err) {
|
|
1583
|
+
s.stop("Amplify start-job: failed");
|
|
1584
|
+
fail("aws amplify start-job", err);
|
|
1585
|
+
}
|
|
1586
|
+
const amplifyAppUrl = `https://main.${app.defaultDomain}`;
|
|
1587
|
+
const result = {
|
|
1588
|
+
githubRepoUrl: created.repoUrl,
|
|
1589
|
+
amplifyAppId: app.appId,
|
|
1590
|
+
amplifyAppUrl
|
|
1591
|
+
};
|
|
1592
|
+
if (opts.domain) {
|
|
1593
|
+
s = spinner();
|
|
1594
|
+
s.start(`Associating custom domain ${opts.domain}`);
|
|
1595
|
+
try {
|
|
1596
|
+
const dom = await amplifyCreateDomain(opts, app.appId);
|
|
1597
|
+
result.domainUrl = dom.domainUrl;
|
|
1598
|
+
result.domainVerification = dom.verification;
|
|
1599
|
+
s.stop(`Custom domain queued: ${opts.domain}`);
|
|
1600
|
+
} catch (err) {
|
|
1601
|
+
s.stop("Custom domain: failed");
|
|
1602
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
1603
|
+
log.warn(`Custom domain step failed:
|
|
1604
|
+
${msg}`);
|
|
1605
|
+
}
|
|
1606
|
+
}
|
|
1607
|
+
return result;
|
|
1608
|
+
}
|
|
1609
|
+
|
|
1610
|
+
// src/deploy-prompts.ts
|
|
1611
|
+
async function detectGithubLogin() {
|
|
1612
|
+
try {
|
|
1613
|
+
const { stdout } = await execa3("gh", ["api", "user", "--jq", ".login"], { reject: false });
|
|
1614
|
+
const trimmed = stdout?.trim();
|
|
1615
|
+
return trimmed || void 0;
|
|
1616
|
+
} catch {
|
|
1617
|
+
return void 0;
|
|
1618
|
+
}
|
|
1619
|
+
}
|
|
1620
|
+
async function detectAwsRegion() {
|
|
1621
|
+
if (process.env.AWS_REGION?.trim()) return process.env.AWS_REGION.trim();
|
|
1622
|
+
if (process.env.AWS_DEFAULT_REGION?.trim()) return process.env.AWS_DEFAULT_REGION.trim();
|
|
1623
|
+
try {
|
|
1624
|
+
const { stdout } = await execa3("aws", ["configure", "get", "region"], { reject: false });
|
|
1625
|
+
const trimmed = stdout?.trim();
|
|
1626
|
+
return trimmed || void 0;
|
|
1627
|
+
} catch {
|
|
1628
|
+
return void 0;
|
|
1629
|
+
}
|
|
1630
|
+
}
|
|
1631
|
+
async function gatherDeployOptions(args, projectDir, projectName) {
|
|
1632
|
+
p2.log.info("Configuring deploy (GitHub + Amplify Hosting)");
|
|
1633
|
+
let githubOwner = args.githubOwner;
|
|
1634
|
+
if (!githubOwner) {
|
|
1635
|
+
const detected = await detectGithubLogin();
|
|
1636
|
+
const answer = await p2.text({
|
|
1637
|
+
message: "GitHub owner (user or org)",
|
|
1638
|
+
placeholder: detected ?? "your-github-username",
|
|
1639
|
+
defaultValue: detected ?? "",
|
|
1640
|
+
validate: (v) => !v?.trim() ? "GitHub owner is required" : void 0
|
|
1641
|
+
});
|
|
1642
|
+
if (p2.isCancel(answer)) {
|
|
1643
|
+
p2.cancel("Cancelled.");
|
|
1644
|
+
return null;
|
|
1645
|
+
}
|
|
1646
|
+
githubOwner = answer.trim();
|
|
1647
|
+
}
|
|
1648
|
+
let githubPrivate = args.githubPrivate;
|
|
1649
|
+
if (!args.githubPrivate && !args.skipConfirm) {
|
|
1650
|
+
const choice = await p2.select({
|
|
1651
|
+
message: "Repository visibility",
|
|
1652
|
+
options: [
|
|
1653
|
+
{ value: "public", label: "Public" },
|
|
1654
|
+
{ value: "private", label: "Private" }
|
|
1655
|
+
],
|
|
1656
|
+
initialValue: "public"
|
|
1657
|
+
});
|
|
1658
|
+
if (p2.isCancel(choice)) {
|
|
1659
|
+
p2.cancel("Cancelled.");
|
|
1660
|
+
return null;
|
|
1661
|
+
}
|
|
1662
|
+
githubPrivate = choice === "private";
|
|
1663
|
+
}
|
|
1664
|
+
let githubToken = await resolveGithubToken(args.githubToken);
|
|
1665
|
+
if (!githubToken) {
|
|
1666
|
+
const answer = await p2.password({
|
|
1667
|
+
message: "GitHub token (needs `repo` scope) \u2014 or run `gh auth login` first",
|
|
1668
|
+
validate: (v) => !v?.trim() ? "Token is required" : void 0
|
|
1669
|
+
});
|
|
1670
|
+
if (p2.isCancel(answer)) {
|
|
1671
|
+
p2.cancel("Cancelled.");
|
|
1672
|
+
return null;
|
|
1673
|
+
}
|
|
1674
|
+
githubToken = answer.trim();
|
|
1675
|
+
}
|
|
1676
|
+
let awsRegion = args.awsRegion;
|
|
1677
|
+
if (!awsRegion) {
|
|
1678
|
+
const detected = await detectAwsRegion();
|
|
1679
|
+
const answer = await p2.text({
|
|
1680
|
+
message: "AWS region",
|
|
1681
|
+
placeholder: detected ?? "us-east-1",
|
|
1682
|
+
defaultValue: detected ?? "us-east-1"
|
|
1683
|
+
});
|
|
1684
|
+
if (p2.isCancel(answer)) {
|
|
1685
|
+
p2.cancel("Cancelled.");
|
|
1686
|
+
return null;
|
|
1687
|
+
}
|
|
1688
|
+
awsRegion = answer.trim() || detected || "us-east-1";
|
|
1689
|
+
}
|
|
1690
|
+
let domain = args.domain;
|
|
1691
|
+
let subdomain = args.subdomain;
|
|
1692
|
+
if (!domain) {
|
|
1693
|
+
const wantDomain = await p2.confirm({
|
|
1694
|
+
message: "Attach a custom domain?",
|
|
1695
|
+
initialValue: false
|
|
1696
|
+
});
|
|
1697
|
+
if (p2.isCancel(wantDomain)) {
|
|
1698
|
+
p2.cancel("Cancelled.");
|
|
1699
|
+
return null;
|
|
1700
|
+
}
|
|
1701
|
+
if (wantDomain) {
|
|
1702
|
+
const dom = await p2.text({
|
|
1703
|
+
message: "Domain (apex or subdomain, e.g. example.com or blog.example.com)",
|
|
1704
|
+
validate: (v) => {
|
|
1705
|
+
if (!v?.trim()) return "Domain is required";
|
|
1706
|
+
if (!/^[a-z0-9.-]+\.[a-z]{2,}$/i.test(v.trim())) return "Looks invalid";
|
|
1707
|
+
}
|
|
1708
|
+
});
|
|
1709
|
+
if (p2.isCancel(dom)) {
|
|
1710
|
+
p2.cancel("Cancelled.");
|
|
1711
|
+
return null;
|
|
1712
|
+
}
|
|
1713
|
+
domain = dom.trim();
|
|
1714
|
+
if (!subdomain) {
|
|
1715
|
+
const sub = await p2.text({
|
|
1716
|
+
message: "Subdomain prefix (leave blank for apex)",
|
|
1717
|
+
placeholder: "",
|
|
1718
|
+
defaultValue: ""
|
|
1719
|
+
});
|
|
1720
|
+
if (p2.isCancel(sub)) {
|
|
1721
|
+
p2.cancel("Cancelled.");
|
|
1722
|
+
return null;
|
|
1723
|
+
}
|
|
1724
|
+
subdomain = sub.trim() || void 0;
|
|
1725
|
+
}
|
|
1726
|
+
}
|
|
1727
|
+
}
|
|
1728
|
+
if (!args.skipConfirm) {
|
|
1729
|
+
const proceed = await p2.confirm({
|
|
1730
|
+
message: `Proceed?
|
|
1731
|
+
GitHub: ${githubOwner}/${projectName} (${githubPrivate ? "private" : "public"})
|
|
1732
|
+
Region: ${awsRegion}` + (domain ? `
|
|
1733
|
+
Domain: ${subdomain ? `${subdomain}.` : ""}${domain}` : ""),
|
|
1734
|
+
initialValue: true
|
|
1735
|
+
});
|
|
1736
|
+
if (p2.isCancel(proceed) || !proceed) {
|
|
1737
|
+
p2.cancel("Cancelled.");
|
|
1738
|
+
return null;
|
|
1739
|
+
}
|
|
1740
|
+
}
|
|
1741
|
+
return {
|
|
1742
|
+
projectDir,
|
|
1743
|
+
projectName,
|
|
1744
|
+
githubOwner,
|
|
1745
|
+
githubPrivate,
|
|
1746
|
+
githubToken,
|
|
1747
|
+
awsProfile: args.awsProfile,
|
|
1748
|
+
awsRegion,
|
|
1749
|
+
domain,
|
|
1750
|
+
subdomain,
|
|
1751
|
+
iamServiceRoleArn: args.iamServiceRole,
|
|
1752
|
+
createIamRole: args.createIamRole,
|
|
1753
|
+
skipConfirm: args.skipConfirm
|
|
1754
|
+
};
|
|
1755
|
+
}
|
|
1756
|
+
|
|
1757
|
+
// src/upgrade.ts
|
|
1758
|
+
import { cp as cp2, readFile as readFile3, writeFile as writeFile3, readdir as readdir3, mkdir as mkdir2, rm as rm2 } from "fs/promises";
|
|
1759
|
+
import { existsSync as existsSync5 } from "fs";
|
|
1760
|
+
import { join as join3, relative, extname as extname2, dirname } from "path";
|
|
1761
|
+
import { log as log3, outro } from "@clack/prompts";
|
|
1762
|
+
import pc3 from "picocolors";
|
|
1763
|
+
import { execa as execa4 } from "execa";
|
|
1764
|
+
var AMPLESS_MANAGED_APP_PATHS = [
|
|
1765
|
+
"app/(admin)/admin",
|
|
1766
|
+
"app/api/admin",
|
|
1767
|
+
"app/api/media",
|
|
1768
|
+
"app/api/mcp",
|
|
1769
|
+
"app/login",
|
|
1770
|
+
"app/site"
|
|
1771
|
+
];
|
|
1772
|
+
var AMPLESS_PACKAGES = /* @__PURE__ */ new Set([
|
|
1773
|
+
"ampless",
|
|
1774
|
+
"@ampless/admin",
|
|
1775
|
+
"@ampless/backend",
|
|
1776
|
+
"@ampless/runtime",
|
|
1777
|
+
"@ampless/plugin-seo",
|
|
1778
|
+
"@ampless/plugin-rss",
|
|
1779
|
+
"@ampless/plugin-webhook",
|
|
1780
|
+
"@ampless/plugin-og-image"
|
|
1781
|
+
]);
|
|
1782
|
+
var AMPLESS_MANAGED_SCRIPTS = /* @__PURE__ */ new Set([
|
|
1783
|
+
"sandbox",
|
|
1784
|
+
"sandbox:dev",
|
|
1785
|
+
"update-ampless",
|
|
1786
|
+
"copy-theme"
|
|
1787
|
+
]);
|
|
1788
|
+
var PROTECTED_PATTERNS = [
|
|
1789
|
+
/^cms\.config\.ts$/,
|
|
1790
|
+
// `themes/` and `themes-registry.ts` are handled separately — see
|
|
1791
|
+
// syncThemes() below. The themes directory is owned per-entry by
|
|
1792
|
+
// either ampless (default themes, resynced on upgrade) or the user
|
|
1793
|
+
// (`my-*` themes, preserved). The registry is regenerated to match
|
|
1794
|
+
// whatever is on disk afterward.
|
|
1795
|
+
/^\.env/,
|
|
1796
|
+
/^node_modules(\/|$)/,
|
|
1797
|
+
/^\.next(\/|$)/,
|
|
1798
|
+
/^\.turbo(\/|$)/,
|
|
1799
|
+
/^\.amplify(\/|$)/,
|
|
1800
|
+
/^amplify_outputs\.json$/,
|
|
1801
|
+
/^next-env\.d\.ts$/,
|
|
1802
|
+
/^tsconfig\.tsbuildinfo$/,
|
|
1803
|
+
/^pnpm-lock\.yaml$/,
|
|
1804
|
+
/^package-lock\.json$/,
|
|
1805
|
+
// Anything under themes/ is handled by syncThemes; the file-walk
|
|
1806
|
+
// classifier must not double-process it.
|
|
1807
|
+
/^themes(\/|$)/,
|
|
1808
|
+
/^themes-registry\.ts$/
|
|
1809
|
+
];
|
|
1810
|
+
var SEED_IF_MISSING_PATTERN = /\.custom\.ts$/;
|
|
1811
|
+
var TEXT_EXTENSIONS2 = /* @__PURE__ */ new Set([
|
|
1812
|
+
".json",
|
|
1813
|
+
".md",
|
|
1814
|
+
".ts",
|
|
1815
|
+
".tsx",
|
|
1816
|
+
".js",
|
|
1817
|
+
".jsx",
|
|
1818
|
+
".mjs",
|
|
1819
|
+
".cjs",
|
|
1820
|
+
".html",
|
|
1821
|
+
".css",
|
|
1822
|
+
".env",
|
|
1823
|
+
".txt",
|
|
1824
|
+
".yaml",
|
|
1825
|
+
".yml",
|
|
1826
|
+
".toml",
|
|
1827
|
+
".gitignore"
|
|
1828
|
+
]);
|
|
1829
|
+
function isProtected(relPath) {
|
|
1830
|
+
return PROTECTED_PATTERNS.some((re) => re.test(relPath));
|
|
1831
|
+
}
|
|
1832
|
+
async function listShippedThemes(templatesRoot) {
|
|
1833
|
+
if (!existsSync5(templatesRoot)) return [];
|
|
1834
|
+
const entries = await readdir3(templatesRoot, { withFileTypes: true });
|
|
1835
|
+
return entries.filter((e) => e.isDirectory() && e.name !== "_shared").map((e) => e.name).sort();
|
|
1836
|
+
}
|
|
1837
|
+
var USER_OWNED_THEME_FILES = /* @__PURE__ */ new Set(["README.md", ".gitignore"]);
|
|
1838
|
+
function isUserOwnedThemeFile(path) {
|
|
1839
|
+
const name = path.split(/[/\\]/).pop() ?? "";
|
|
1840
|
+
return USER_OWNED_THEME_FILES.has(name);
|
|
1841
|
+
}
|
|
1842
|
+
async function captureUserOwnedFiles(dir) {
|
|
1843
|
+
const out = /* @__PURE__ */ new Map();
|
|
1844
|
+
if (!existsSync5(dir)) return out;
|
|
1845
|
+
for (const name of USER_OWNED_THEME_FILES) {
|
|
1846
|
+
const p3 = join3(dir, name);
|
|
1847
|
+
if (existsSync5(p3)) {
|
|
1848
|
+
out.set(name, await readFile3(p3));
|
|
1849
|
+
}
|
|
1850
|
+
}
|
|
1851
|
+
return out;
|
|
1852
|
+
}
|
|
1853
|
+
async function restorePreservedFiles(dir, files) {
|
|
1854
|
+
for (const [name, content] of files) {
|
|
1855
|
+
await mkdir2(dir, { recursive: true });
|
|
1856
|
+
await writeFile3(join3(dir, name), content);
|
|
1857
|
+
}
|
|
1858
|
+
}
|
|
1859
|
+
async function syncThemes(destDir, templatesRoot) {
|
|
1860
|
+
const shipped = await listShippedThemes(templatesRoot);
|
|
1861
|
+
const themesDir = join3(destDir, "themes");
|
|
1862
|
+
await mkdir2(themesDir, { recursive: true });
|
|
1863
|
+
for (const name of shipped) {
|
|
1864
|
+
const src = join3(templatesRoot, name);
|
|
1865
|
+
const dst = join3(themesDir, name);
|
|
1866
|
+
const preservedFiles = await captureUserOwnedFiles(dst);
|
|
1867
|
+
await rm2(dst, { recursive: true, force: true });
|
|
1868
|
+
await cp2(src, dst, {
|
|
1869
|
+
recursive: true,
|
|
1870
|
+
filter: (sourcePath) => !isUserOwnedThemeFile(sourcePath)
|
|
1871
|
+
});
|
|
1872
|
+
await restorePreservedFiles(dst, preservedFiles);
|
|
1873
|
+
}
|
|
1874
|
+
const installed = await discoverInstalledThemes(destDir);
|
|
1875
|
+
const shippedSet = new Set(shipped);
|
|
1876
|
+
const preserved = installed.filter((t) => !shippedSet.has(t));
|
|
1877
|
+
const all = [...shipped, ...preserved];
|
|
1878
|
+
await writeFile3(join3(destDir, "themes-registry.ts"), buildRegistry(all), "utf-8");
|
|
1879
|
+
return { synced: shipped, preserved };
|
|
1880
|
+
}
|
|
1881
|
+
async function walkDir(dir, base, out) {
|
|
1882
|
+
const entries = await readdir3(dir, { withFileTypes: true });
|
|
1883
|
+
for (const entry of entries) {
|
|
1884
|
+
const full = join3(dir, entry.name);
|
|
1885
|
+
const rel = relative(base, full);
|
|
1886
|
+
if (entry.isDirectory()) {
|
|
1887
|
+
await walkDir(full, base, out);
|
|
1888
|
+
} else {
|
|
1889
|
+
out.push(rel);
|
|
1890
|
+
}
|
|
1891
|
+
}
|
|
1892
|
+
}
|
|
1893
|
+
async function listFilesRecursive(root) {
|
|
1894
|
+
const out = [];
|
|
1895
|
+
async function walk(current, relPrefix) {
|
|
1896
|
+
const entries = await readdir3(current, { withFileTypes: true });
|
|
1897
|
+
for (const entry of entries) {
|
|
1898
|
+
const rel = relPrefix ? `${relPrefix}/${entry.name}` : entry.name;
|
|
1899
|
+
if (entry.isDirectory()) {
|
|
1900
|
+
await walk(join3(current, entry.name), rel);
|
|
1901
|
+
} else if (entry.isFile()) {
|
|
1902
|
+
out.push(rel);
|
|
1903
|
+
}
|
|
1904
|
+
}
|
|
1905
|
+
}
|
|
1906
|
+
if (!existsSync5(root)) return out;
|
|
1907
|
+
await walk(root, "");
|
|
1908
|
+
return out;
|
|
1909
|
+
}
|
|
1910
|
+
async function pruneEmptyDirs(root) {
|
|
1911
|
+
if (!existsSync5(root)) return;
|
|
1912
|
+
const entries = await readdir3(root, { withFileTypes: true });
|
|
1913
|
+
for (const entry of entries) {
|
|
1914
|
+
if (entry.isDirectory()) {
|
|
1915
|
+
await pruneEmptyDirs(join3(root, entry.name));
|
|
1916
|
+
}
|
|
1917
|
+
}
|
|
1918
|
+
const remaining = await readdir3(root);
|
|
1919
|
+
if (remaining.length === 0) {
|
|
1920
|
+
await rm2(root, { recursive: true, force: true });
|
|
1921
|
+
}
|
|
1922
|
+
}
|
|
1923
|
+
async function findObsoleteAppFiles(destDir, sharedDir) {
|
|
1924
|
+
const obsolete = [];
|
|
1925
|
+
for (const managedPath of AMPLESS_MANAGED_APP_PATHS) {
|
|
1926
|
+
const userPath = join3(destDir, managedPath);
|
|
1927
|
+
if (!existsSync5(userPath)) continue;
|
|
1928
|
+
const templatePath2 = join3(sharedDir, managedPath);
|
|
1929
|
+
const templateFiles = new Set(await listFilesRecursive(templatePath2));
|
|
1930
|
+
const userFiles = await listFilesRecursive(userPath);
|
|
1931
|
+
for (const f of userFiles) {
|
|
1932
|
+
if (!templateFiles.has(f)) {
|
|
1933
|
+
obsolete.push(`${managedPath}/${f}`);
|
|
1934
|
+
}
|
|
1935
|
+
}
|
|
1936
|
+
}
|
|
1937
|
+
return obsolete;
|
|
1938
|
+
}
|
|
1939
|
+
async function removeObsoleteAppFiles(destDir, paths) {
|
|
1940
|
+
for (const rel of paths) {
|
|
1941
|
+
const abs = join3(destDir, rel);
|
|
1942
|
+
if (existsSync5(abs)) {
|
|
1943
|
+
await rm2(abs, { force: true });
|
|
1944
|
+
}
|
|
1945
|
+
}
|
|
1946
|
+
for (const managedPath of AMPLESS_MANAGED_APP_PATHS) {
|
|
1947
|
+
await pruneEmptyDirs(join3(destDir, managedPath));
|
|
1948
|
+
}
|
|
1949
|
+
}
|
|
1950
|
+
function substituteVars(content, vars) {
|
|
1951
|
+
return content.replace(/\{\{(\w+)\}\}/g, (_, key) => vars[key] ?? `{{${key}}}`);
|
|
1952
|
+
}
|
|
1953
|
+
async function copyWithSubstitution(src, dst, vars) {
|
|
1954
|
+
const ext = extname2(src) || (src.endsWith(".gitignore") ? ".gitignore" : "");
|
|
1955
|
+
await mkdir2(dirname(dst), { recursive: true });
|
|
1956
|
+
if (TEXT_EXTENSIONS2.has(ext)) {
|
|
1957
|
+
const content = await readFile3(src, "utf-8");
|
|
1958
|
+
await writeFile3(dst, substituteVars(content, vars), "utf-8");
|
|
1959
|
+
} else {
|
|
1960
|
+
const buf = await readFile3(src);
|
|
1961
|
+
await writeFile3(dst, buf);
|
|
1962
|
+
}
|
|
1963
|
+
}
|
|
1964
|
+
function detectIndent(jsonStr) {
|
|
1965
|
+
const m = jsonStr.match(/^{\n(\s+)/);
|
|
1966
|
+
if (!m) return 2;
|
|
1967
|
+
return m[1].length;
|
|
1968
|
+
}
|
|
1969
|
+
async function runUpgradeIn(destDir, sharedDir, opts = {}) {
|
|
1970
|
+
const derivedRoot = opts.templatesRoot ?? join3(sharedDir, "..");
|
|
1971
|
+
const themeSyncEnabled = existsSync5(join3(derivedRoot, "_shared"));
|
|
1972
|
+
const templatesRoot = themeSyncEnabled ? derivedRoot : "";
|
|
1973
|
+
const problem = validateMountableProject(destDir);
|
|
1974
|
+
if (problem) {
|
|
1975
|
+
throw new Error(problem);
|
|
1976
|
+
}
|
|
1977
|
+
const projectPkgPath = join3(destDir, "package.json");
|
|
1978
|
+
const projectPkgRaw = await readFile3(projectPkgPath, "utf-8");
|
|
1979
|
+
const projectPkg = JSON.parse(projectPkgRaw);
|
|
1980
|
+
const projectName = typeof projectPkg.name === "string" && projectPkg.name ? projectPkg.name : "my-ampless-site";
|
|
1981
|
+
const vars = { projectName };
|
|
1982
|
+
const allRelPaths = [];
|
|
1983
|
+
await walkDir(sharedDir, sharedDir, allRelPaths);
|
|
1984
|
+
const classification = { replace: [], merge: [], seed: [], protected: [] };
|
|
1985
|
+
for (const rel of allRelPaths) {
|
|
1986
|
+
if (SEED_IF_MISSING_PATTERN.test(rel)) {
|
|
1987
|
+
classification.seed.push(rel);
|
|
1988
|
+
} else if (isProtected(rel)) {
|
|
1989
|
+
classification.protected.push(rel);
|
|
1990
|
+
} else if (rel === "package.json") {
|
|
1991
|
+
classification.merge.push(rel);
|
|
1992
|
+
} else {
|
|
1993
|
+
classification.replace.push(rel);
|
|
1994
|
+
}
|
|
1995
|
+
}
|
|
1996
|
+
const replaceNew = classification.replace.filter((r) => !existsSync5(join3(destDir, r)));
|
|
1997
|
+
const replaceUpdate = classification.replace.filter((r) => existsSync5(join3(destDir, r)));
|
|
1998
|
+
const seedNew = classification.seed.filter((r) => !existsSync5(join3(destDir, r)));
|
|
1999
|
+
const seedSkipped = classification.seed.filter((r) => existsSync5(join3(destDir, r)));
|
|
2000
|
+
const shippedThemes = themeSyncEnabled ? await listShippedThemes(templatesRoot) : [];
|
|
2001
|
+
const existingThemes = themeSyncEnabled ? await discoverInstalledThemes(destDir) : [];
|
|
2002
|
+
const preservedThemes = existingThemes.filter((t) => !shippedThemes.includes(t));
|
|
2003
|
+
const obsoleteFiles = await findObsoleteAppFiles(destDir, sharedDir);
|
|
2004
|
+
log3.info(
|
|
2005
|
+
`replace: ${pc3.green(`${replaceNew.length} added`)} / ${pc3.yellow(`${replaceUpdate.length} updated`)}`
|
|
2006
|
+
);
|
|
2007
|
+
log3.info(`merge: ${pc3.cyan("package.json: sync ampless deps and managed scripts with the template")}`);
|
|
2008
|
+
if (classification.seed.length > 0) {
|
|
2009
|
+
log3.info(
|
|
2010
|
+
`seed: ${pc3.green(`${seedNew.length} added`)} / ${pc3.dim(`${seedSkipped.length} kept (existing *.custom.ts left untouched)`)}`
|
|
2011
|
+
);
|
|
2012
|
+
}
|
|
2013
|
+
if (themeSyncEnabled) {
|
|
2014
|
+
log3.info(
|
|
2015
|
+
`themes: ${pc3.cyan(`${shippedThemes.length} default themes synced`)} / ${pc3.dim(`${preservedThemes.length} custom (my-*) themes preserved`)}`
|
|
2016
|
+
);
|
|
2017
|
+
}
|
|
2018
|
+
if (obsoleteFiles.length > 0) {
|
|
2019
|
+
log3.info(`cleanup: ${pc3.yellow(`${obsoleteFiles.length} removed`)} (files under ampless-managed app/ paths that no longer exist in the template)`);
|
|
2020
|
+
}
|
|
2021
|
+
log3.info(`protected: ${pc3.dim(`${classification.protected.length} template files left untouched`)}`);
|
|
2022
|
+
if (opts.dryRun) {
|
|
2023
|
+
return {
|
|
2024
|
+
added: replaceNew,
|
|
2025
|
+
updated: replaceUpdate,
|
|
2026
|
+
seeded: seedNew,
|
|
2027
|
+
protected: classification.protected,
|
|
2028
|
+
themesSynced: shippedThemes,
|
|
2029
|
+
themesPreserved: preservedThemes,
|
|
2030
|
+
packageJsonMerged: false,
|
|
2031
|
+
obsoleteRemoved: obsoleteFiles
|
|
2032
|
+
};
|
|
2033
|
+
}
|
|
2034
|
+
for (const rel of classification.replace) {
|
|
2035
|
+
const src = join3(sharedDir, rel);
|
|
2036
|
+
const dst = join3(destDir, rel);
|
|
2037
|
+
await copyWithSubstitution(src, dst, vars);
|
|
2038
|
+
}
|
|
2039
|
+
for (const rel of seedNew) {
|
|
2040
|
+
const src = join3(sharedDir, rel);
|
|
2041
|
+
const dst = join3(destDir, rel);
|
|
2042
|
+
await copyWithSubstitution(src, dst, vars);
|
|
2043
|
+
}
|
|
2044
|
+
const themeResult = themeSyncEnabled ? await syncThemes(destDir, templatesRoot) : { synced: [], preserved: [] };
|
|
2045
|
+
await removeObsoleteAppFiles(destDir, obsoleteFiles);
|
|
2046
|
+
const templatePkgRaw = await readFile3(join3(sharedDir, "package.json"), "utf-8");
|
|
2047
|
+
const templatePkg = JSON.parse(templatePkgRaw);
|
|
2048
|
+
const indent = detectIndent(projectPkgRaw);
|
|
2049
|
+
for (const section of ["dependencies", "devDependencies"]) {
|
|
2050
|
+
const templateDeps = templatePkg[section] ?? {};
|
|
2051
|
+
const projectDeps = projectPkg[section] ?? {};
|
|
2052
|
+
for (const [name, version] of Object.entries(templateDeps)) {
|
|
2053
|
+
if (!AMPLESS_PACKAGES.has(name)) continue;
|
|
2054
|
+
projectDeps[name] = version;
|
|
2055
|
+
}
|
|
2056
|
+
if (Object.keys(projectDeps).length > 0) {
|
|
2057
|
+
projectPkg[section] = projectDeps;
|
|
2058
|
+
}
|
|
2059
|
+
}
|
|
2060
|
+
const templateScripts = templatePkg["scripts"] ?? {};
|
|
2061
|
+
const projectScripts = projectPkg["scripts"] ?? {};
|
|
2062
|
+
for (const name of AMPLESS_MANAGED_SCRIPTS) {
|
|
2063
|
+
if (name in templateScripts) {
|
|
2064
|
+
projectScripts[name] = templateScripts[name];
|
|
2065
|
+
} else {
|
|
2066
|
+
delete projectScripts[name];
|
|
2067
|
+
}
|
|
2068
|
+
}
|
|
2069
|
+
if (Object.keys(projectScripts).length > 0) {
|
|
2070
|
+
projectPkg["scripts"] = projectScripts;
|
|
2071
|
+
}
|
|
2072
|
+
await writeFile3(projectPkgPath, JSON.stringify(projectPkg, null, indent) + "\n", "utf-8");
|
|
2073
|
+
if (!opts.noInstall) {
|
|
2074
|
+
const usePnpm = existsSync5(join3(destDir, "pnpm-lock.yaml"));
|
|
2075
|
+
const pm = usePnpm ? "pnpm" : "npm";
|
|
2076
|
+
await execa4(pm, ["install"], { cwd: destDir, stdio: "inherit" });
|
|
2077
|
+
}
|
|
2078
|
+
return {
|
|
2079
|
+
added: replaceNew,
|
|
2080
|
+
updated: replaceUpdate,
|
|
2081
|
+
seeded: seedNew,
|
|
2082
|
+
protected: classification.protected,
|
|
2083
|
+
themesSynced: themeResult.synced,
|
|
2084
|
+
themesPreserved: themeResult.preserved,
|
|
2085
|
+
packageJsonMerged: true,
|
|
2086
|
+
obsoleteRemoved: obsoleteFiles
|
|
2087
|
+
};
|
|
2088
|
+
}
|
|
2089
|
+
async function runUpgrade(args) {
|
|
2090
|
+
const destDir = process.cwd();
|
|
2091
|
+
try {
|
|
2092
|
+
await runUpgradeIn(destDir, sharedTemplateDir(), {
|
|
2093
|
+
dryRun: args.dryRun,
|
|
2094
|
+
noInstall: args.noInstall
|
|
2095
|
+
});
|
|
2096
|
+
} catch (err) {
|
|
2097
|
+
log3.error(err instanceof Error ? err.message : String(err));
|
|
2098
|
+
process.exit(1);
|
|
2099
|
+
}
|
|
2100
|
+
if (args.dryRun) {
|
|
2101
|
+
outro(`${pc3.dim("(dry-run) No files were changed.")}`);
|
|
2102
|
+
return;
|
|
2103
|
+
}
|
|
2104
|
+
outro(
|
|
2105
|
+
`${pc3.green("\u2714")} Upgrade complete
|
|
2106
|
+
|
|
2107
|
+
Next steps:
|
|
2108
|
+
${pc3.cyan("git diff")} ${pc3.dim("# review changes")}
|
|
2109
|
+
${pc3.cyan("git commit && git push")} ${pc3.dim("# deploy via Amplify Hosting")}`
|
|
2110
|
+
);
|
|
2111
|
+
}
|
|
2112
|
+
|
|
2113
|
+
// src/copy-theme.ts
|
|
2114
|
+
import { cp as cp3, readFile as readFile4, writeFile as writeFile4 } from "fs/promises";
|
|
2115
|
+
import { existsSync as existsSync6 } from "fs";
|
|
2116
|
+
import { join as join4 } from "path";
|
|
2117
|
+
import { log as log4, outro as outro2 } from "@clack/prompts";
|
|
2118
|
+
import pc4 from "picocolors";
|
|
2119
|
+
async function runCopyThemeIn(destDir, source, target) {
|
|
2120
|
+
const problem = validateMountableProject(destDir);
|
|
2121
|
+
if (problem) {
|
|
2122
|
+
throw new Error(problem);
|
|
2123
|
+
}
|
|
2124
|
+
if (!isCustomTheme(target)) {
|
|
2125
|
+
throw new Error(
|
|
2126
|
+
`Target theme name must start with "${CUSTOM_THEME_PREFIX}" (got "${target}"). This prefix marks the theme as user-owned so create-ampless upgrade leaves it alone.`
|
|
2127
|
+
);
|
|
2128
|
+
}
|
|
2129
|
+
if (source === target) {
|
|
2130
|
+
throw new Error(`Source and target are identical (${source}).`);
|
|
2131
|
+
}
|
|
2132
|
+
const themesDir = join4(destDir, "themes");
|
|
2133
|
+
const sourceDir = join4(themesDir, source);
|
|
2134
|
+
const targetDir = join4(themesDir, target);
|
|
2135
|
+
if (!existsSync6(sourceDir)) {
|
|
2136
|
+
throw new Error(`Source theme not found: themes/${source}/`);
|
|
2137
|
+
}
|
|
2138
|
+
if (existsSync6(targetDir)) {
|
|
2139
|
+
throw new Error(`Target theme already exists: themes/${target}/`);
|
|
2140
|
+
}
|
|
2141
|
+
await cp3(sourceDir, targetDir, { recursive: true });
|
|
2142
|
+
const filesRewritten = await rewriteThemeName(targetDir, source, target);
|
|
2143
|
+
const installed = await discoverInstalledThemes(destDir);
|
|
2144
|
+
await writeFile4(join4(destDir, "themes-registry.ts"), buildRegistry(installed), "utf-8");
|
|
2145
|
+
return { source, target, filesRewritten };
|
|
2146
|
+
}
|
|
2147
|
+
async function rewriteThemeName(targetDir, source, target) {
|
|
2148
|
+
const touched = [];
|
|
2149
|
+
await rewriteFile(
|
|
2150
|
+
join4(targetDir, "index.ts"),
|
|
2151
|
+
(s) => s.replace(new RegExp(`name:\\s*'${escapeRegex(source)}'`), `name: '${target}'`).replace(new RegExp(`name:\\s*"${escapeRegex(source)}"`), `name: "${target}"`),
|
|
2152
|
+
touched
|
|
2153
|
+
);
|
|
2154
|
+
await rewriteFile(
|
|
2155
|
+
join4(targetDir, "manifest.ts"),
|
|
2156
|
+
(s) => s.replace(new RegExp(`name:\\s*'${escapeRegex(source)}'`), `name: '${target}'`).replace(new RegExp(`name:\\s*"${escapeRegex(source)}"`), `name: "${target}"`),
|
|
2157
|
+
touched
|
|
2158
|
+
);
|
|
2159
|
+
await rewriteFile(
|
|
2160
|
+
join4(targetDir, "tokens.css"),
|
|
2161
|
+
(s) => s.replace(new RegExp(`\\[data-theme='${escapeRegex(source)}'\\]`, "g"), `[data-theme='${target}']`).replace(new RegExp(`\\[data-theme="${escapeRegex(source)}"\\]`, "g"), `[data-theme="${target}"]`),
|
|
2162
|
+
touched
|
|
2163
|
+
);
|
|
2164
|
+
return touched;
|
|
2165
|
+
}
|
|
2166
|
+
async function rewriteFile(path, transform, touched) {
|
|
2167
|
+
if (!existsSync6(path)) return;
|
|
2168
|
+
const before = await readFile4(path, "utf-8");
|
|
2169
|
+
const after = transform(before);
|
|
2170
|
+
if (after === before) return;
|
|
2171
|
+
await writeFile4(path, after, "utf-8");
|
|
2172
|
+
touched.push(path);
|
|
2173
|
+
}
|
|
2174
|
+
function escapeRegex(s) {
|
|
2175
|
+
return s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
2176
|
+
}
|
|
2177
|
+
async function runCopyTheme(args) {
|
|
2178
|
+
const destDir = process.cwd();
|
|
2179
|
+
const source = args.copyThemeSource;
|
|
2180
|
+
const target = args.copyThemeTarget;
|
|
2181
|
+
if (!source || !target) {
|
|
2182
|
+
log4.error("Usage: npx create-ampless@latest copy-theme <source> <target>");
|
|
2183
|
+
log4.info("Example: npx create-ampless@latest copy-theme blog my-blog");
|
|
2184
|
+
process.exit(1);
|
|
2185
|
+
}
|
|
2186
|
+
try {
|
|
2187
|
+
const result = await runCopyThemeIn(destDir, source, target);
|
|
2188
|
+
outro2(
|
|
2189
|
+
`${pc4.green("\u2714")} Copied themes/${result.source}/ \u2192 themes/${result.target}/
|
|
2190
|
+
|
|
2191
|
+
Files rewritten:
|
|
2192
|
+
` + result.filesRewritten.map((f) => ` ${pc4.dim(f)}`).join("\n") + `
|
|
2193
|
+
|
|
2194
|
+
themes-registry.ts updated. Next:
|
|
2195
|
+
${pc4.cyan(`Open themes/${result.target}/ and start customising`)}
|
|
2196
|
+
${pc4.cyan(`Activate via /admin/sites/<siteId>/theme`)}`
|
|
2197
|
+
);
|
|
2198
|
+
} catch (err) {
|
|
2199
|
+
log4.error(err instanceof Error ? err.message : String(err));
|
|
2200
|
+
process.exit(1);
|
|
2201
|
+
}
|
|
2202
|
+
}
|
|
2203
|
+
|
|
2204
|
+
// src/index.ts
|
|
2205
|
+
import pc5 from "picocolors";
|
|
2206
|
+
var DEFAULT_THEMES = VALID_THEMES;
|
|
2207
|
+
function buildNonInteractiveOpts(args) {
|
|
2208
|
+
const projectName = args.projectName ?? (() => {
|
|
2209
|
+
const b = basename3(process.cwd());
|
|
2210
|
+
return b && b !== "/" ? b : "my-ampless-site";
|
|
2211
|
+
})();
|
|
2212
|
+
const siteName = args.siteName ?? "My Blog";
|
|
2213
|
+
const themes = args.themes ?? [...DEFAULT_THEMES];
|
|
2214
|
+
const plugins = args.plugins ?? ["seo"];
|
|
2215
|
+
return {
|
|
2216
|
+
projectName,
|
|
2217
|
+
siteName,
|
|
2218
|
+
themes,
|
|
2219
|
+
defaultTheme: themes[0],
|
|
2220
|
+
plugins
|
|
2221
|
+
};
|
|
2222
|
+
}
|
|
2223
|
+
function warnIgnoredScaffoldFlags(args) {
|
|
2224
|
+
const ignored = [];
|
|
2225
|
+
if (args.siteName) ignored.push("--site-name");
|
|
2226
|
+
if (args.themes) ignored.push("--themes");
|
|
2227
|
+
if (args.plugins) ignored.push("--plugins");
|
|
2228
|
+
if (args.projectName) ignored.push("<project-name> positional");
|
|
2229
|
+
if (ignored.length > 0) {
|
|
2230
|
+
log5.warn(`--mount mode ignores: ${ignored.join(", ")}`);
|
|
2231
|
+
}
|
|
2232
|
+
}
|
|
2233
|
+
async function runMount(args) {
|
|
2234
|
+
const destDir = process.cwd();
|
|
2235
|
+
const projectName = basename3(destDir);
|
|
2236
|
+
warnIgnoredScaffoldFlags(args);
|
|
2237
|
+
const problem = validateMountableProject(destDir);
|
|
2238
|
+
if (problem) {
|
|
2239
|
+
log5.error(problem);
|
|
2240
|
+
log5.info(
|
|
2241
|
+
"Run `npx create-ampless@latest <name>` first to scaffold, or `cd` into a scaffolded project before passing --mount."
|
|
2242
|
+
);
|
|
2243
|
+
process.exit(1);
|
|
2244
|
+
}
|
|
2245
|
+
const deployOpts = await gatherDeployOptions(args, destDir, projectName);
|
|
2246
|
+
if (!deployOpts) return;
|
|
2247
|
+
try {
|
|
2248
|
+
const result = await runDeploy({ ...deployOpts, mount: true });
|
|
2249
|
+
printDeployResult(result);
|
|
2250
|
+
} catch (err) {
|
|
2251
|
+
if (err instanceof PreflightError) {
|
|
2252
|
+
process.exit(1);
|
|
2253
|
+
}
|
|
2254
|
+
log5.error(err instanceof Error ? err.message : String(err));
|
|
2255
|
+
process.exit(1);
|
|
2256
|
+
}
|
|
2257
|
+
}
|
|
2258
|
+
function printDeployResult(result) {
|
|
2259
|
+
const lines = [
|
|
2260
|
+
`${pc5.green("\u2714")} Project deployed`,
|
|
2261
|
+
``,
|
|
2262
|
+
` GitHub: ${pc5.cyan(result.githubRepoUrl)}`,
|
|
2263
|
+
` Amplify app: ${pc5.cyan(result.amplifyAppId)}`,
|
|
2264
|
+
` Amplify URL: ${pc5.cyan(result.amplifyAppUrl)}`
|
|
2265
|
+
];
|
|
2266
|
+
if (result.domainUrl) {
|
|
2267
|
+
lines.push(` Custom domain: ${pc5.cyan(result.domainUrl)}`);
|
|
2268
|
+
}
|
|
2269
|
+
if (result.domainVerification && result.domainVerification.length > 0) {
|
|
2270
|
+
lines.push("", ` ${pc5.bold("Add these DNS records to verify the domain:")}`);
|
|
2271
|
+
for (const v of result.domainVerification) {
|
|
2272
|
+
lines.push(` ${v.cname} CNAME ${v.target}`);
|
|
2273
|
+
}
|
|
2274
|
+
}
|
|
2275
|
+
lines.push(
|
|
2276
|
+
"",
|
|
2277
|
+
` First build is now running in Amplify Hosting.`,
|
|
2278
|
+
` Watch it at ${pc5.cyan(`https://console.aws.amazon.com/amplify/home#/${result.amplifyAppId}`)}`
|
|
2279
|
+
);
|
|
2280
|
+
outro3(lines.join("\n"));
|
|
2281
|
+
}
|
|
196
2282
|
async function main() {
|
|
197
|
-
const
|
|
198
|
-
|
|
2283
|
+
const args = parseDeployArgs(process.argv.slice(2));
|
|
2284
|
+
if (args.help) {
|
|
2285
|
+
process.stdout.write(HELP_TEXT);
|
|
2286
|
+
return;
|
|
2287
|
+
}
|
|
2288
|
+
for (const flag of args.unknown) {
|
|
2289
|
+
log5.warn(`Unknown argument ignored: ${flag}`);
|
|
2290
|
+
}
|
|
2291
|
+
if (args.upgrade) {
|
|
2292
|
+
await runUpgrade(args);
|
|
2293
|
+
return;
|
|
2294
|
+
}
|
|
2295
|
+
if (args.copyTheme) {
|
|
2296
|
+
await runCopyTheme(args);
|
|
2297
|
+
return;
|
|
2298
|
+
}
|
|
2299
|
+
if (args.mount) {
|
|
2300
|
+
await runMount(args);
|
|
2301
|
+
return;
|
|
2302
|
+
}
|
|
2303
|
+
let opts;
|
|
2304
|
+
if (args.skipConfirm) {
|
|
2305
|
+
opts = buildNonInteractiveOpts(args);
|
|
2306
|
+
} else {
|
|
2307
|
+
opts = await runPrompts(args.projectName);
|
|
2308
|
+
}
|
|
199
2309
|
if (!opts) return;
|
|
200
|
-
const destDir =
|
|
201
|
-
if (
|
|
202
|
-
|
|
2310
|
+
const destDir = resolve5(process.cwd(), opts.projectName);
|
|
2311
|
+
if (existsSync7(destDir)) {
|
|
2312
|
+
log5.error(`Directory already exists: ${destDir}`);
|
|
203
2313
|
process.exit(1);
|
|
204
2314
|
}
|
|
205
2315
|
const sharedDir = sharedTemplateDir();
|
|
206
|
-
const s =
|
|
2316
|
+
const s = spinner2();
|
|
207
2317
|
s.start("Scaffolding project...");
|
|
208
2318
|
try {
|
|
209
2319
|
await scaffold(sharedDir, templatesDir, destDir, opts);
|
|
210
2320
|
s.stop("Done!");
|
|
211
2321
|
} catch (err) {
|
|
212
2322
|
s.stop("Failed.");
|
|
213
|
-
|
|
2323
|
+
log5.error(String(err));
|
|
214
2324
|
process.exit(1);
|
|
215
2325
|
}
|
|
216
|
-
|
|
217
|
-
|
|
2326
|
+
if (args.deploy) {
|
|
2327
|
+
const deployOpts = await gatherDeployOptions(args, destDir, opts.projectName);
|
|
2328
|
+
if (!deployOpts) return;
|
|
2329
|
+
try {
|
|
2330
|
+
const result = await runDeploy(deployOpts);
|
|
2331
|
+
printDeployResult(result);
|
|
2332
|
+
} catch (err) {
|
|
2333
|
+
if (err instanceof PreflightError) {
|
|
2334
|
+
process.exit(1);
|
|
2335
|
+
}
|
|
2336
|
+
log5.error(err instanceof Error ? err.message : String(err));
|
|
2337
|
+
process.exit(1);
|
|
2338
|
+
}
|
|
2339
|
+
return;
|
|
2340
|
+
}
|
|
2341
|
+
outro3(
|
|
2342
|
+
`${pc5.green("\u2714")} Project created at ${pc5.bold(opts.projectName)}
|
|
218
2343
|
|
|
219
2344
|
Next steps:
|
|
220
|
-
${
|
|
221
|
-
${
|
|
222
|
-
${
|
|
223
|
-
${pc.cyan("npm run dev")} ${pc.dim("# start Next.js")}`
|
|
2345
|
+
${pc5.cyan("cd")} ${opts.projectName}
|
|
2346
|
+
${pc5.cyan("npm install")}
|
|
2347
|
+
${pc5.cyan("npm run sandbox")} ${pc5.dim("# deploy sandbox + start Next.js")}`
|
|
224
2348
|
);
|
|
225
2349
|
}
|
|
226
2350
|
main().catch((err) => {
|