create-ampless 1.0.0-alpha.78 → 1.0.0-alpha.79

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -1,9 +1,9 @@
1
1
  #!/usr/bin/env node
2
2
 
3
3
  // src/index.ts
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";
4
+ import { spinner as spinner2, outro as outro4, log as log6 } from "@clack/prompts";
5
+ import { existsSync as existsSync8 } from "fs";
6
+ import { basename as basename4, resolve as resolve5 } from "path";
7
7
 
8
8
  // src/prompts.ts
9
9
  import * as p from "@clack/prompts";
@@ -106,6 +106,9 @@ function sharedTemplateDir() {
106
106
  function templatePath(theme) {
107
107
  return resolve(templatesDir, theme);
108
108
  }
109
+ function pluginTemplateDir(mode) {
110
+ return resolve(templatesDir, mode === "local" ? "plugin-local" : "plugin-standalone");
111
+ }
109
112
 
110
113
  // src/gitignore.ts
111
114
  var DEFAULT_GITIGNORE = `# Dependencies
@@ -269,6 +272,20 @@ async function scaffold(sharedDir, themesRoot, destDir, opts) {
269
272
  // src/args.ts
270
273
  var VALID_THEMES = ["blog", "minimal", "landing", "corporate", "docs", "dads"];
271
274
  var VALID_PLUGINS = ["seo", "rss", "webhook"];
275
+ var VALID_PLUGIN_TRUST_LEVELS = [
276
+ "untrusted",
277
+ "trusted",
278
+ "privileged"
279
+ ];
280
+ var VALID_PLUGIN_CAPABILITIES = [
281
+ "publicHead",
282
+ "publicBody",
283
+ "metadata",
284
+ "eventHooks",
285
+ "adminSettings",
286
+ "writePublicAsset",
287
+ "schema"
288
+ ];
272
289
  var STRING_FLAGS = /* @__PURE__ */ new Set([
273
290
  "--site-name",
274
291
  "--themes",
@@ -279,7 +296,11 @@ var STRING_FLAGS = /* @__PURE__ */ new Set([
279
296
  "--aws-region",
280
297
  "--domain",
281
298
  "--subdomain",
282
- "--iam-service-role"
299
+ "--iam-service-role",
300
+ // Phase 5 `plugin <name>` flags
301
+ "--trust-level",
302
+ "--capabilities",
303
+ "--description"
283
304
  ]);
284
305
  var BOOLEAN_FLAGS = /* @__PURE__ */ new Set([
285
306
  "--deploy",
@@ -291,7 +312,10 @@ var BOOLEAN_FLAGS = /* @__PURE__ */ new Set([
291
312
  "--create-iam-role",
292
313
  "--skip-confirm",
293
314
  "--help",
294
- "-h"
315
+ "-h",
316
+ // Phase 5 `plugin <name>` mode flags
317
+ "--local",
318
+ "--standalone"
295
319
  ]);
296
320
  function parseDeployArgs(argv) {
297
321
  const out = {
@@ -299,6 +323,7 @@ function parseDeployArgs(argv) {
299
323
  mount: false,
300
324
  upgrade: false,
301
325
  copyTheme: false,
326
+ createPlugin: false,
302
327
  dryRun: false,
303
328
  noInstall: false,
304
329
  githubPrivate: false,
@@ -346,6 +371,18 @@ function parseDeployArgs(argv) {
346
371
  case "-h":
347
372
  out.help = true;
348
373
  break;
374
+ case "--local":
375
+ if (out.pluginMode === "standalone") {
376
+ throw new Error("Cannot combine --local and --standalone");
377
+ }
378
+ out.pluginMode = "local";
379
+ break;
380
+ case "--standalone":
381
+ if (out.pluginMode === "local") {
382
+ throw new Error("Cannot combine --local and --standalone");
383
+ }
384
+ out.pluginMode = "standalone";
385
+ break;
349
386
  }
350
387
  continue;
351
388
  }
@@ -401,6 +438,31 @@ function parseDeployArgs(argv) {
401
438
  case "--iam-service-role":
402
439
  out.iamServiceRole = value;
403
440
  break;
441
+ case "--trust-level": {
442
+ if (!VALID_PLUGIN_TRUST_LEVELS.includes(value)) {
443
+ throw new Error(
444
+ `Invalid --trust-level "${value}". Valid values: ${VALID_PLUGIN_TRUST_LEVELS.join(", ")}`
445
+ );
446
+ }
447
+ out.pluginTrustLevel = value;
448
+ break;
449
+ }
450
+ case "--capabilities": {
451
+ const caps = value.split(",").map((c) => c.trim()).filter(Boolean);
452
+ const invalid = caps.filter(
453
+ (c) => !VALID_PLUGIN_CAPABILITIES.includes(c)
454
+ );
455
+ if (invalid.length > 0) {
456
+ throw new Error(
457
+ `Invalid capability(ies): ${invalid.join(", ")}. Valid values: ${VALID_PLUGIN_CAPABILITIES.join(", ")}`
458
+ );
459
+ }
460
+ out.pluginCapabilities = caps;
461
+ break;
462
+ }
463
+ case "--description":
464
+ out.pluginDescription = value;
465
+ break;
404
466
  }
405
467
  continue;
406
468
  }
@@ -424,6 +486,14 @@ function parseDeployArgs(argv) {
424
486
  out.copyThemeTarget = raw;
425
487
  continue;
426
488
  }
489
+ if (raw === "plugin" && out.projectName === void 0 && !out.createPlugin && !out.upgrade && !out.copyTheme) {
490
+ out.createPlugin = true;
491
+ continue;
492
+ }
493
+ if (out.createPlugin && out.pluginName === void 0) {
494
+ out.pluginName = raw;
495
+ continue;
496
+ }
427
497
  if (out.projectName === void 0) {
428
498
  out.projectName = raw;
429
499
  } else {
@@ -436,8 +506,10 @@ var HELP_TEXT = `create-ampless \u2014 scaffold an ampless project
436
506
 
437
507
  Usage:
438
508
  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
509
+ npx create-ampless@latest --mount [options] # in an existing project dir
510
+ npx create-ampless@latest upgrade [options] # in an existing project dir
511
+ npx create-ampless@latest copy-theme <src> <dst> # in an existing project dir
512
+ npx create-ampless@latest plugin <name> [options] # scaffold a plugin (Phase 5)
441
513
 
442
514
  Options:
443
515
  --site-name <name> Site display name (default: "My Blog")
@@ -488,6 +560,32 @@ copy-theme <source> <target>
488
560
  it alone). Run inside an existing ampless project.
489
561
 
490
562
  Example: npx create-ampless@latest copy-theme blog my-blog
563
+
564
+ plugin <name>
565
+ Scaffold an ampless plugin. Two modes:
566
+
567
+ --local Default. Writes plugins/<name>/index.ts inside
568
+ the current ampless site. The site itself is the
569
+ build / publish unit; the plugin file is just
570
+ code that sits there. Plugin name is a kebab-case
571
+ identifier (e.g. "site-verification").
572
+
573
+ --standalone Writes a complete npm package at ./<name>/ ready
574
+ for 'npm publish'. Plugin name should be the npm
575
+ package name (e.g. "@scope/ampless-plugin-foo"
576
+ or "ampless-plugin-foo"); the kebab segment
577
+ becomes the AmplessPlugin.name.
578
+
579
+ --trust-level <value> 'untrusted' (default), 'trusted', or 'privileged'
580
+ --capabilities <list> Comma-separated. Valid: publicHead, publicBody,
581
+ metadata, eventHooks, adminSettings,
582
+ writePublicAsset, schema
583
+ (default: publicHead,adminSettings)
584
+ --description "<text>" Optional one-line description
585
+
586
+ Examples:
587
+ npx create-ampless@latest plugin site-verification
588
+ npx create-ampless@latest plugin @ishinao/ampless-plugin-foo --standalone
491
589
  `;
492
590
 
493
591
  // src/deploy-prompts.ts
@@ -2210,12 +2308,289 @@ async function runCopyTheme(args) {
2210
2308
  }
2211
2309
  }
2212
2310
 
2213
- // src/index.ts
2311
+ // src/plugin.ts
2312
+ import { cp as cp4, mkdir as mkdir3, readFile as readFile5, writeFile as writeFile5, readdir as readdir5 } from "fs/promises";
2313
+ import { existsSync as existsSync7 } from "fs";
2314
+ import { join as join5, extname as extname3, basename as basename3 } from "path";
2315
+ import {
2316
+ log as log5,
2317
+ outro as outro3,
2318
+ text as text3,
2319
+ select as select2,
2320
+ multiselect as multiselect2,
2321
+ isCancel as isCancel3,
2322
+ cancel as cancel3
2323
+ } from "@clack/prompts";
2214
2324
  import pc5 from "picocolors";
2325
+ var SCAFFOLD_AMPLESS_VERSION = "1.0.0-alpha.22";
2326
+ var TEXT_EXTENSIONS3 = /* @__PURE__ */ new Set([
2327
+ ".json",
2328
+ ".md",
2329
+ ".ts",
2330
+ ".tsx",
2331
+ ".js",
2332
+ ".jsx",
2333
+ ".mjs",
2334
+ ".cjs",
2335
+ ".html",
2336
+ ".css",
2337
+ ".env",
2338
+ ".txt",
2339
+ ".yaml",
2340
+ ".yml",
2341
+ ".toml",
2342
+ ".gitignore"
2343
+ ]);
2344
+ function toKebab(s) {
2345
+ return s.replace(/[A-Z]/g, (c) => "-" + c.toLowerCase()).replace(/^-/, "");
2346
+ }
2347
+ function toCamelCase(kebab) {
2348
+ return kebab.replace(/-([a-z])/g, (_, c) => c.toUpperCase());
2349
+ }
2350
+ function toPascalCase(kebab) {
2351
+ const c = toCamelCase(kebab);
2352
+ return c.charAt(0).toUpperCase() + c.slice(1);
2353
+ }
2354
+ function toDisplayName(kebab) {
2355
+ const words = kebab.split("-");
2356
+ return words.map((w, i) => i === 0 ? w.charAt(0).toUpperCase() + w.slice(1) : w).join(" ");
2357
+ }
2358
+ function sanitizeForJsComment(s) {
2359
+ return s.replace(/\*\//g, "*\\/").replace(/[\r\n]+/g, " ").replace(/\s{2,}/g, " ").trim();
2360
+ }
2361
+ function lastSegment(packageName) {
2362
+ return packageName.replace(/^@[^/]+\//, "");
2363
+ }
2364
+ function pluginNameFromPackage(packageName) {
2365
+ const segment = lastSegment(packageName);
2366
+ if (segment.startsWith("ampless-plugin-")) {
2367
+ return segment.slice("ampless-plugin-".length);
2368
+ }
2369
+ if (segment.startsWith("plugin-")) {
2370
+ return segment.slice("plugin-".length);
2371
+ }
2372
+ return segment;
2373
+ }
2374
+ async function substituteFile2(filePath, vars) {
2375
+ const ext = extname3(filePath) || (basename3(filePath) === ".gitignore" ? ".gitignore" : "");
2376
+ if (!TEXT_EXTENSIONS3.has(ext)) return;
2377
+ const content = await readFile5(filePath, "utf-8");
2378
+ const replaced = content.replace(/\{\{(\w+)\}\}/g, (_, key) => vars[key] ?? `{{${key}}}`);
2379
+ if (replaced !== content) await writeFile5(filePath, replaced, "utf-8");
2380
+ }
2381
+ async function substituteDir2(dir, vars) {
2382
+ const entries = await readdir5(dir, { withFileTypes: true });
2383
+ await Promise.all(
2384
+ entries.map(async (entry) => {
2385
+ const fullPath = join5(dir, entry.name);
2386
+ if (entry.isDirectory()) {
2387
+ await substituteDir2(fullPath, vars);
2388
+ } else {
2389
+ await substituteFile2(fullPath, vars);
2390
+ }
2391
+ })
2392
+ );
2393
+ }
2394
+ var KEBAB_RE = /^[a-z][a-z0-9]*(?:-[a-z][a-z0-9]*)*$/;
2395
+ var PACKAGE_NAME_RE = /^(@[a-z][a-z0-9-]*\/)?[a-z][a-z0-9]*(?:-[a-z0-9]+)*$/;
2396
+ async function runCreatePluginIn(destDir, args) {
2397
+ const mode = args.pluginMode ?? "local";
2398
+ let rawName = args.pluginName;
2399
+ if (!rawName) {
2400
+ if (args.skipConfirm === true) {
2401
+ throw new Error(
2402
+ mode === "local" ? "Plugin name is required when --skip-confirm is set. Pass it as the positional argument: `create-ampless plugin <name> --skip-confirm`." : "Plugin package name is required when --skip-confirm is set. Pass it as the positional argument: `create-ampless plugin <package-name> --standalone --skip-confirm`."
2403
+ );
2404
+ }
2405
+ const answer = await text3({
2406
+ message: mode === "local" ? 'Plugin name (kebab-case, e.g. "site-verification"):' : 'Plugin package name (e.g. "@scope/ampless-plugin-foo" or "ampless-plugin-foo"):',
2407
+ validate(v) {
2408
+ if (!v) return "Plugin name is required";
2409
+ const re = mode === "local" ? KEBAB_RE : PACKAGE_NAME_RE;
2410
+ if (!re.test(v)) {
2411
+ return mode === "local" ? 'Must be kebab-case starting with a lowercase letter (e.g. "site-verification")' : 'Must be a valid npm package name (e.g. "ampless-plugin-foo" or "@scope/ampless-plugin-foo")';
2412
+ }
2413
+ }
2414
+ });
2415
+ if (isCancel3(answer)) {
2416
+ cancel3("Cancelled.");
2417
+ process.exit(0);
2418
+ }
2419
+ rawName = answer;
2420
+ } else {
2421
+ const re = mode === "local" ? KEBAB_RE : PACKAGE_NAME_RE;
2422
+ if (!re.test(rawName)) {
2423
+ throw new Error(
2424
+ mode === "local" ? `Invalid plugin name "${rawName}". Must be kebab-case (e.g. "site-verification").` : `Invalid plugin name "${rawName}". Must be a valid npm package name (e.g. "ampless-plugin-foo" or "@scope/ampless-plugin-foo").`
2425
+ );
2426
+ }
2427
+ }
2428
+ const skipConfirm = args.skipConfirm === true;
2429
+ let trustLevel = args.pluginTrustLevel;
2430
+ if (!trustLevel) {
2431
+ if (skipConfirm) {
2432
+ trustLevel = "untrusted";
2433
+ } else {
2434
+ const answer = await select2({
2435
+ message: "Trust level:",
2436
+ options: VALID_PLUGIN_TRUST_LEVELS.map((t) => ({
2437
+ value: t,
2438
+ label: t,
2439
+ hint: t === "untrusted" ? "No special permissions (default)" : t === "trusted" ? "Can read site config" : "Full access to internal APIs"
2440
+ }))
2441
+ });
2442
+ if (isCancel3(answer)) {
2443
+ cancel3("Cancelled.");
2444
+ process.exit(0);
2445
+ }
2446
+ trustLevel = answer;
2447
+ }
2448
+ }
2449
+ let capabilities = args.pluginCapabilities;
2450
+ if (!capabilities) {
2451
+ if (skipConfirm) {
2452
+ capabilities = ["publicHead", "adminSettings"];
2453
+ } else {
2454
+ const answer = await multiselect2({
2455
+ message: "Capabilities (space to toggle, enter to confirm):",
2456
+ options: VALID_PLUGIN_CAPABILITIES.map((c) => ({
2457
+ value: c,
2458
+ label: c
2459
+ })),
2460
+ initialValues: ["publicHead", "adminSettings"],
2461
+ required: false
2462
+ });
2463
+ if (isCancel3(answer)) {
2464
+ cancel3("Cancelled.");
2465
+ process.exit(0);
2466
+ }
2467
+ capabilities = answer;
2468
+ }
2469
+ }
2470
+ const rawDescription = args.pluginDescription ?? "";
2471
+ const description = sanitizeForJsComment(rawDescription);
2472
+ const descriptionJson = JSON.stringify(rawDescription);
2473
+ const nameKebab = mode === "local" ? rawName : pluginNameFromPackage(rawName);
2474
+ const packageName = mode === "standalone" ? rawName : nameKebab;
2475
+ if (!KEBAB_RE.test(nameKebab)) {
2476
+ throw new Error(
2477
+ `Invalid plugin name: "${nameKebab}" (derived from "${rawName}" by stripping the npm scope and the conventional ampless-plugin- prefix) is not valid kebab-case. Each segment must start with a letter and contain only letters and digits. Examples: "site-verification", "image-zoom".`
2478
+ );
2479
+ }
2480
+ const nameCamelCase = toCamelCase(nameKebab);
2481
+ const NamePascalCase = toPascalCase(nameKebab);
2482
+ const DisplayName = toDisplayName(nameKebab);
2483
+ void toKebab;
2484
+ let outputDir;
2485
+ if (mode === "local") {
2486
+ const problem = validateMountableProject(destDir);
2487
+ if (problem) {
2488
+ throw new Error(problem);
2489
+ }
2490
+ outputDir = join5(destDir, "plugins", nameKebab);
2491
+ if (existsSync7(outputDir)) {
2492
+ throw new Error(
2493
+ `Plugin directory already exists: plugins/${nameKebab}/
2494
+ Remove it first or choose a different name.`
2495
+ );
2496
+ }
2497
+ } else {
2498
+ const nameSegment = lastSegment(rawName);
2499
+ outputDir = join5(destDir, nameSegment);
2500
+ if (existsSync7(outputDir)) {
2501
+ throw new Error(
2502
+ `Directory already exists: ${nameSegment}/
2503
+ Remove it first or choose a different name.`
2504
+ );
2505
+ }
2506
+ }
2507
+ const templateDir = pluginTemplateDir(mode);
2508
+ if (!existsSync7(templateDir)) {
2509
+ throw new Error(
2510
+ `Plugin template not found: ${templateDir}
2511
+ The ${mode === "local" ? "plugin-local" : "plugin-standalone"} template directory has not been added to the repo yet. Run \`git pull\` to pick up the latest templates.`
2512
+ );
2513
+ }
2514
+ await mkdir3(outputDir, { recursive: true });
2515
+ await cp4(templateDir, outputDir, { recursive: true });
2516
+ const capabilitiesList = capabilities && capabilities.length > 0 ? capabilities.map((c) => `'${c}'`).join(", ") : "";
2517
+ const capabilitiesJsonArray = capabilities && capabilities.length > 0 ? capabilities.map((c) => `"${c}"`).join(", ") : "";
2518
+ const vars = {
2519
+ // Use camelCase keys so they survive substituteVars' `\w+` regex.
2520
+ // Templates reference them as `{{nameKebab}}` (not `{{name-kebab}}`).
2521
+ nameKebab,
2522
+ nameCamelCase,
2523
+ NameCamelCase: NamePascalCase,
2524
+ packageName,
2525
+ description,
2526
+ descriptionJson,
2527
+ trustLevel: trustLevel ?? "untrusted",
2528
+ capabilitiesList,
2529
+ capabilitiesJsonArray,
2530
+ DisplayName,
2531
+ displayNameJa: DisplayName,
2532
+ // same as English for now; maintainers can localise
2533
+ amplessVersion: SCAFFOLD_AMPLESS_VERSION
2534
+ };
2535
+ await substituteDir2(outputDir, vars);
2536
+ return {
2537
+ mode,
2538
+ outputDir,
2539
+ pluginName: nameKebab,
2540
+ packageName: mode === "standalone" ? packageName : void 0
2541
+ };
2542
+ }
2543
+ async function runCreatePlugin(args) {
2544
+ const destDir = process.cwd();
2545
+ try {
2546
+ const result = await runCreatePluginIn(destDir, args);
2547
+ if (result.mode === "local") {
2548
+ const rel = `plugins/${result.pluginName}/`;
2549
+ const factoryName = `${toCamelCase(result.pluginName)}Plugin`;
2550
+ outro3(
2551
+ `${pc5.green("\u2714")} Plugin scaffolded at ${pc5.bold(rel)}
2552
+
2553
+ Register it in ${pc5.cyan("cms.config.ts")}:
2554
+
2555
+ import ${factoryName} from './plugins/${result.pluginName}'
2556
+
2557
+ export default defineConfig({
2558
+ // ...
2559
+ plugins: [
2560
+ ${factoryName}(),
2561
+ ],
2562
+ })
2563
+
2564
+ Then restart the dev server to pick up the new plugin.`
2565
+ );
2566
+ } else {
2567
+ const dirName = result.packageName ? lastSegment(result.packageName) : result.pluginName;
2568
+ outro3(
2569
+ `${pc5.green("\u2714")} Standalone plugin scaffolded at ${pc5.bold(dirName + "/")}
2570
+
2571
+ Next steps:
2572
+ ${pc5.cyan(`cd ${dirName}`)}
2573
+ ${pc5.cyan("pnpm install")}
2574
+ ${pc5.cyan("pnpm test")}
2575
+ ${pc5.cyan("pnpm build")}
2576
+ ${pc5.cyan("pnpm publish --access public --tag alpha")}
2577
+
2578
+ Then add the published package to your ampless site:
2579
+ ${pc5.cyan(`pnpm add ${result.packageName ?? result.pluginName}`)}`
2580
+ );
2581
+ }
2582
+ } catch (err) {
2583
+ log5.error(err instanceof Error ? err.message : String(err));
2584
+ process.exit(1);
2585
+ }
2586
+ }
2587
+
2588
+ // src/index.ts
2589
+ import pc6 from "picocolors";
2215
2590
  var DEFAULT_THEMES = VALID_THEMES;
2216
2591
  function buildNonInteractiveOpts(args) {
2217
2592
  const projectName = args.projectName ?? (() => {
2218
- const b = basename3(process.cwd());
2593
+ const b = basename4(process.cwd());
2219
2594
  return b && b !== "/" ? b : "my-ampless-site";
2220
2595
  })();
2221
2596
  const siteName = args.siteName ?? "My Blog";
@@ -2236,17 +2611,17 @@ function warnIgnoredScaffoldFlags(args) {
2236
2611
  if (args.plugins) ignored.push("--plugins");
2237
2612
  if (args.projectName) ignored.push("<project-name> positional");
2238
2613
  if (ignored.length > 0) {
2239
- log5.warn(`--mount mode ignores: ${ignored.join(", ")}`);
2614
+ log6.warn(`--mount mode ignores: ${ignored.join(", ")}`);
2240
2615
  }
2241
2616
  }
2242
2617
  async function runMount(args) {
2243
2618
  const destDir = process.cwd();
2244
- const projectName = basename3(destDir);
2619
+ const projectName = basename4(destDir);
2245
2620
  warnIgnoredScaffoldFlags(args);
2246
2621
  const problem = validateMountableProject(destDir);
2247
2622
  if (problem) {
2248
- log5.error(problem);
2249
- log5.info(
2623
+ log6.error(problem);
2624
+ log6.info(
2250
2625
  "Run `npx create-ampless@latest <name>` first to scaffold, or `cd` into a scaffolded project before passing --mount."
2251
2626
  );
2252
2627
  process.exit(1);
@@ -2260,23 +2635,23 @@ async function runMount(args) {
2260
2635
  if (err instanceof PreflightError) {
2261
2636
  process.exit(1);
2262
2637
  }
2263
- log5.error(err instanceof Error ? err.message : String(err));
2638
+ log6.error(err instanceof Error ? err.message : String(err));
2264
2639
  process.exit(1);
2265
2640
  }
2266
2641
  }
2267
2642
  function printDeployResult(result) {
2268
2643
  const lines = [
2269
- `${pc5.green("\u2714")} Project deployed`,
2644
+ `${pc6.green("\u2714")} Project deployed`,
2270
2645
  ``,
2271
- ` GitHub: ${pc5.cyan(result.githubRepoUrl)}`,
2272
- ` Amplify app: ${pc5.cyan(result.amplifyAppId)}`,
2273
- ` Amplify URL: ${pc5.cyan(result.amplifyAppUrl)}`
2646
+ ` GitHub: ${pc6.cyan(result.githubRepoUrl)}`,
2647
+ ` Amplify app: ${pc6.cyan(result.amplifyAppId)}`,
2648
+ ` Amplify URL: ${pc6.cyan(result.amplifyAppUrl)}`
2274
2649
  ];
2275
2650
  if (result.domainUrl) {
2276
- lines.push(` Custom domain: ${pc5.cyan(result.domainUrl)}`);
2651
+ lines.push(` Custom domain: ${pc6.cyan(result.domainUrl)}`);
2277
2652
  }
2278
2653
  if (result.domainVerification && result.domainVerification.length > 0) {
2279
- lines.push("", ` ${pc5.bold("Add these DNS records to verify the domain:")}`);
2654
+ lines.push("", ` ${pc6.bold("Add these DNS records to verify the domain:")}`);
2280
2655
  for (const v of result.domainVerification) {
2281
2656
  lines.push(` ${v.cname} CNAME ${v.target}`);
2282
2657
  }
@@ -2284,9 +2659,9 @@ function printDeployResult(result) {
2284
2659
  lines.push(
2285
2660
  "",
2286
2661
  ` First build is now running in Amplify Hosting.`,
2287
- ` Watch it at ${pc5.cyan(`https://console.aws.amazon.com/amplify/home#/${result.amplifyAppId}`)}`
2662
+ ` Watch it at ${pc6.cyan(`https://console.aws.amazon.com/amplify/home#/${result.amplifyAppId}`)}`
2288
2663
  );
2289
- outro3(lines.join("\n"));
2664
+ outro4(lines.join("\n"));
2290
2665
  }
2291
2666
  async function main() {
2292
2667
  const args = parseDeployArgs(process.argv.slice(2));
@@ -2295,7 +2670,7 @@ async function main() {
2295
2670
  return;
2296
2671
  }
2297
2672
  for (const flag of args.unknown) {
2298
- log5.warn(`Unknown argument ignored: ${flag}`);
2673
+ log6.warn(`Unknown argument ignored: ${flag}`);
2299
2674
  }
2300
2675
  if (args.upgrade) {
2301
2676
  await runUpgrade(args);
@@ -2305,6 +2680,10 @@ async function main() {
2305
2680
  await runCopyTheme(args);
2306
2681
  return;
2307
2682
  }
2683
+ if (args.createPlugin) {
2684
+ await runCreatePlugin(args);
2685
+ return;
2686
+ }
2308
2687
  if (args.mount) {
2309
2688
  await runMount(args);
2310
2689
  return;
@@ -2317,8 +2696,8 @@ async function main() {
2317
2696
  }
2318
2697
  if (!opts) return;
2319
2698
  const destDir = resolve5(process.cwd(), opts.projectName);
2320
- if (existsSync7(destDir)) {
2321
- log5.error(`Directory already exists: ${destDir}`);
2699
+ if (existsSync8(destDir)) {
2700
+ log6.error(`Directory already exists: ${destDir}`);
2322
2701
  process.exit(1);
2323
2702
  }
2324
2703
  const sharedDir = sharedTemplateDir();
@@ -2329,7 +2708,7 @@ async function main() {
2329
2708
  s.stop("Done!");
2330
2709
  } catch (err) {
2331
2710
  s.stop("Failed.");
2332
- log5.error(String(err));
2711
+ log6.error(String(err));
2333
2712
  process.exit(1);
2334
2713
  }
2335
2714
  if (args.deploy) {
@@ -2342,18 +2721,18 @@ async function main() {
2342
2721
  if (err instanceof PreflightError) {
2343
2722
  process.exit(1);
2344
2723
  }
2345
- log5.error(err instanceof Error ? err.message : String(err));
2724
+ log6.error(err instanceof Error ? err.message : String(err));
2346
2725
  process.exit(1);
2347
2726
  }
2348
2727
  return;
2349
2728
  }
2350
- outro3(
2351
- `${pc5.green("\u2714")} Project created at ${pc5.bold(opts.projectName)}
2729
+ outro4(
2730
+ `${pc6.green("\u2714")} Project created at ${pc6.bold(opts.projectName)}
2352
2731
 
2353
2732
  Next steps:
2354
- ${pc5.cyan("cd")} ${opts.projectName}
2355
- ${pc5.cyan("npm install")}
2356
- ${pc5.cyan("npm run sandbox")} ${pc5.dim("# deploy sandbox + start Next.js")}`
2733
+ ${pc6.cyan("cd")} ${opts.projectName}
2734
+ ${pc6.cyan("npm install")}
2735
+ ${pc6.cyan("npm run sandbox")} ${pc6.dim("# deploy sandbox + start Next.js")}`
2357
2736
  );
2358
2737
  }
2359
2738
  main().catch((err) => {
@@ -0,0 +1,34 @@
1
+ # {{nameKebab}}
2
+
3
+ {{description}}
4
+
5
+ Site-local plugin in `plugins/{{nameKebab}}/`. This directory is
6
+ user-owned: `update-ampless` never touches it.
7
+
8
+ ## Register
9
+
10
+ Add to `cms.config.ts`:
11
+
12
+ ```typescript
13
+ import {{nameCamelCase}}Plugin from './plugins/{{nameKebab}}'
14
+
15
+ export default defineConfig({
16
+ // ...
17
+ plugins: [
18
+ {{nameCamelCase}}Plugin(),
19
+ ],
20
+ })
21
+ ```
22
+
23
+ Restart `next dev` and visit `/admin/plugins` to configure.
24
+
25
+ ## Settings
26
+
27
+ (none yet — declare via `settings.public` in `index.ts`)
28
+
29
+ ## Notes
30
+
31
+ - Trust level: `{{trustLevel}}`
32
+ - Capabilities: `{{capabilitiesList}}`
33
+ - When this plugin grows useful for more than one site, lift it into
34
+ its own npm package via `npx create-ampless plugin <name> --standalone`.
@@ -0,0 +1,39 @@
1
+ import { definePlugin, type AmplessPlugin } from 'ampless'
2
+
3
+ export interface {{NameCamelCase}}Options {
4
+ /** Optional namespace when registering multiple instances. */
5
+ instanceId?: string
6
+ // TODO: add the constructor options your plugin needs.
7
+ }
8
+
9
+ /**
10
+ * {{description}}
11
+ *
12
+ * Site-local plugin. Add to your `cms.config.ts`:
13
+ *
14
+ * import {{nameCamelCase}}Plugin from './plugins/{{nameKebab}}'
15
+ *
16
+ * export default defineConfig({
17
+ * // ...
18
+ * plugins: [
19
+ * {{nameCamelCase}}Plugin(),
20
+ * ],
21
+ * })
22
+ */
23
+ export default function {{nameCamelCase}}Plugin(
24
+ options: {{NameCamelCase}}Options = {}
25
+ ): AmplessPlugin {
26
+ const instanceId = options.instanceId ?? '{{nameKebab}}'
27
+ return definePlugin({
28
+ name: '{{nameKebab}}',
29
+ instanceId,
30
+ apiVersion: 1,
31
+ trust_level: '{{trustLevel}}',
32
+ displayName: { en: '{{DisplayName}}', ja: '{{displayNameJa}}' },
33
+ capabilities: [{{capabilitiesList}}],
34
+ // TODO: implement the surfaces declared in `capabilities`. The
35
+ // most common starting point is `publicHead(ctx)` returning a
36
+ // descriptor array. See the plugin author guide:
37
+ // https://github.com/heavymoons/ampless/blob/main/packages/ampless/docs/plugin-author-guide.md
38
+ })
39
+ }
@@ -0,0 +1 @@
1
+ # {{packageName}}
@@ -0,0 +1,43 @@
1
+ > English: [README.md](./README.md)
2
+
3
+ # {{packageName}}
4
+
5
+ > [Pre-release / alpha] {{description}}
6
+
7
+ ## インストール
8
+
9
+ ```bash
10
+ npm install {{packageName}}@alpha
11
+ ```
12
+
13
+ ## 設定
14
+
15
+ ```typescript
16
+ // cms.config.ts
17
+ import { defineConfig } from 'ampless'
18
+ import {{nameCamelCase}}Plugin from '{{packageName}}'
19
+
20
+ export default defineConfig({
21
+ // ...
22
+ plugins: [
23
+ {{nameCamelCase}}Plugin(),
24
+ ],
25
+ })
26
+ ```
27
+
28
+ `/admin/plugins` で設定。
29
+
30
+ ## Trust level
31
+
32
+ `{{trustLevel}}`。Capabilities: `{{capabilitiesList}}`。
33
+
34
+ ## Publish
35
+
36
+ ```bash
37
+ pnpm install
38
+ pnpm test
39
+ pnpm build
40
+ pnpm publish --access public --tag alpha
41
+ ```
42
+
43
+ API 詳細は [ampless plugin author guide](https://github.com/heavymoons/ampless/blob/main/packages/ampless/docs/plugin-author-guide.md) を参照。
@@ -0,0 +1,43 @@
1
+ > 日本語版: [README.ja.md](./README.ja.md)
2
+
3
+ # {{packageName}}
4
+
5
+ > [Pre-release / alpha] {{description}}
6
+
7
+ ## Install
8
+
9
+ ```bash
10
+ npm install {{packageName}}@alpha
11
+ ```
12
+
13
+ ## Configure
14
+
15
+ ```typescript
16
+ // cms.config.ts
17
+ import { defineConfig } from 'ampless'
18
+ import {{nameCamelCase}}Plugin from '{{packageName}}'
19
+
20
+ export default defineConfig({
21
+ // ...
22
+ plugins: [
23
+ {{nameCamelCase}}Plugin(),
24
+ ],
25
+ })
26
+ ```
27
+
28
+ Then go to `/admin/plugins` to configure.
29
+
30
+ ## Trust level
31
+
32
+ `{{trustLevel}}`. Capabilities: `{{capabilitiesList}}`.
33
+
34
+ ## Publishing
35
+
36
+ ```bash
37
+ pnpm install
38
+ pnpm test
39
+ pnpm build
40
+ pnpm publish --access public --tag alpha
41
+ ```
42
+
43
+ See the [ampless plugin author guide](https://github.com/heavymoons/ampless/blob/main/packages/ampless/docs/plugin-author-guide.md) for the full API.
@@ -0,0 +1,47 @@
1
+ {
2
+ "name": "{{packageName}}",
3
+ "version": "0.1.0",
4
+ "description": {{descriptionJson}},
5
+ "license": "MIT",
6
+ "type": "module",
7
+ "exports": {
8
+ ".": {
9
+ "import": "./dist/index.js",
10
+ "types": "./dist/index.d.ts"
11
+ },
12
+ "./package.json": "./package.json"
13
+ },
14
+ "files": [
15
+ "dist",
16
+ "README.md"
17
+ ],
18
+ "publishConfig": {
19
+ "access": "public"
20
+ },
21
+ "scripts": {
22
+ "build": "tsup",
23
+ "dev": "tsup --watch",
24
+ "lint": "tsc --noEmit",
25
+ "test": "vitest run --passWithNoTests"
26
+ },
27
+ "dependencies": {
28
+ "ampless": "^{{amplessVersion}}"
29
+ },
30
+ "devDependencies": {
31
+ "tsup": "^8.5.1",
32
+ "typescript": "^5.7.0",
33
+ "vitest": "^4.1.7"
34
+ },
35
+ "keywords": [
36
+ "ampless",
37
+ "plugin",
38
+ "ampless-plugin"
39
+ ],
40
+ "amplessPlugin": {
41
+ "apiVersion": 1,
42
+ "name": "{{nameKebab}}",
43
+ "trustLevel": "{{trustLevel}}",
44
+ "capabilities": [{{capabilitiesJsonArray}}],
45
+ "displayName": { "en": "{{DisplayName}}", "ja": "{{displayNameJa}}" }
46
+ }
47
+ }
@@ -0,0 +1,16 @@
1
+ import { describe, it, expect } from 'vitest'
2
+ import {{nameCamelCase}}Plugin from './index.js'
3
+
4
+ describe('{{nameCamelCase}}Plugin', () => {
5
+ it('returns a plugin with the declared name + apiVersion', () => {
6
+ const plugin = {{nameCamelCase}}Plugin()
7
+ expect(plugin.name).toBe('{{nameKebab}}')
8
+ expect(plugin.apiVersion).toBe(1)
9
+ expect(plugin.trust_level).toBe('{{trustLevel}}')
10
+ })
11
+
12
+ it('honors an explicit instanceId', () => {
13
+ const plugin = {{nameCamelCase}}Plugin({ instanceId: 'custom-instance' })
14
+ expect(plugin.instanceId).toBe('custom-instance')
15
+ })
16
+ })
@@ -0,0 +1,29 @@
1
+ import { definePlugin, type AmplessPlugin } from 'ampless'
2
+
3
+ export interface {{NameCamelCase}}Options {
4
+ /** Optional namespace when registering multiple instances. */
5
+ instanceId?: string
6
+ // TODO: add the constructor options your plugin needs.
7
+ }
8
+
9
+ /**
10
+ * {{description}}
11
+ */
12
+ export default function {{nameCamelCase}}Plugin(
13
+ options: {{NameCamelCase}}Options = {}
14
+ ): AmplessPlugin {
15
+ const instanceId = options.instanceId ?? '{{nameKebab}}'
16
+ return definePlugin({
17
+ name: '{{nameKebab}}',
18
+ packageName: '{{packageName}}',
19
+ instanceId,
20
+ apiVersion: 1,
21
+ trust_level: '{{trustLevel}}',
22
+ displayName: { en: '{{DisplayName}}', ja: '{{displayNameJa}}' },
23
+ capabilities: [{{capabilitiesList}}],
24
+ // TODO: implement the surfaces declared in `capabilities`. The
25
+ // most common starting point is `publicHead(ctx)` returning a
26
+ // descriptor array. See:
27
+ // https://github.com/heavymoons/ampless/blob/main/packages/ampless/docs/plugin-author-guide.md
28
+ })
29
+ }
@@ -0,0 +1,16 @@
1
+ {
2
+ "compilerOptions": {
3
+ "target": "ES2022",
4
+ "module": "ESNext",
5
+ "moduleResolution": "Bundler",
6
+ "lib": ["ES2022", "DOM"],
7
+ "strict": true,
8
+ "esModuleInterop": true,
9
+ "skipLibCheck": true,
10
+ "declaration": true,
11
+ "outDir": "dist",
12
+ "rootDir": "src",
13
+ "verbatimModuleSyntax": true
14
+ },
15
+ "include": ["src"]
16
+ }
@@ -0,0 +1,8 @@
1
+ import { defineConfig } from 'tsup'
2
+
3
+ export default defineConfig({
4
+ entry: ['src/index.ts'],
5
+ format: ['esm'],
6
+ dts: true,
7
+ clean: true,
8
+ })
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "create-ampless",
3
- "version": "1.0.0-alpha.78",
3
+ "version": "1.0.0-alpha.79",
4
4
  "description": "Create a new ampless project",
5
5
  "license": "MIT",
6
6
  "type": "module",