create-ampless 1.0.0-alpha.99 → 1.0.0-beta.148

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.
Files changed (34) hide show
  1. package/README.ja.md +4 -4
  2. package/README.md +4 -4
  3. package/dist/index.js +198 -29
  4. package/dist/templates/_shared/README.ja.md +4 -4
  5. package/dist/templates/_shared/README.md +4 -4
  6. package/dist/templates/_shared/amplify/backend.ts +1 -1
  7. package/dist/templates/_shared/amplify/data/get-published-post.js +13 -1
  8. package/dist/templates/_shared/amplify/data/list-posts-by-tag.js +15 -3
  9. package/dist/templates/_shared/amplify/data/list-published-posts.js +26 -15
  10. package/dist/templates/_shared/amplify/events/dispatcher/handler.ts +11 -1
  11. package/dist/templates/_shared/amplify/secrets/encryption-key.ts +2 -2
  12. package/dist/templates/_shared/app/(admin)/admin/_editor-bootstrap.tsx +13 -0
  13. package/dist/templates/_shared/app/(admin)/admin/layout.tsx +2 -1
  14. package/dist/templates/_shared/app/(admin)/admin/preview/route.tsx +4 -0
  15. package/dist/templates/_shared/cms.config.ts +3 -0
  16. package/dist/templates/_shared/docs/plugin-author-guide.ja.md +841 -31
  17. package/dist/templates/_shared/docs/plugin-author-guide.md +1147 -80
  18. package/dist/templates/_shared/package.json +17 -16
  19. package/dist/templates/_shared/plugins/README.ja.md +1 -1
  20. package/dist/templates/_shared/plugins/README.md +1 -1
  21. package/dist/templates/blog/pages/home.tsx +5 -5
  22. package/dist/templates/blog/pages/post.tsx +5 -3
  23. package/dist/templates/corporate/pages/home.tsx +5 -5
  24. package/dist/templates/corporate/pages/post.tsx +5 -3
  25. package/dist/templates/dads/pages/home.tsx +5 -5
  26. package/dist/templates/dads/pages/post.tsx +5 -3
  27. package/dist/templates/docs/pages/post.tsx +5 -3
  28. package/dist/templates/landing/pages/home.tsx +5 -5
  29. package/dist/templates/landing/pages/post.tsx +5 -3
  30. package/dist/templates/minimal/pages/post.tsx +5 -3
  31. package/dist/templates/plugin-local/README.md +1 -1
  32. package/dist/templates/plugin-standalone/README.ja.md +3 -3
  33. package/dist/templates/plugin-standalone/README.md +3 -3
  34. package/package.json +1 -1
package/README.ja.md CHANGED
@@ -5,10 +5,10 @@
5
5
 
6
6
  [ampless](https://github.com/heavymoons/ampless) プロジェクト向け CLI スキャフォールディングツール。
7
7
 
8
- > **プレリリース / アルファ版。** v1.0 まではマイナーバージョンでも破壊的変更が入る可能性があります。`@alpha` タグを使用してください(`@latest` タグは v1.0 まで存在しません)。
8
+ > **プレリリース / ベータ版。** v1.0 まではマイナーバージョンでも破壊的変更が入る可能性があります。`@beta` タグを使用してください(`@latest` タグは v1.0 まで存在しません)。
9
9
 
10
10
  ```bash
11
- npx create-ampless@alpha
11
+ npx create-ampless@beta
12
12
  ```
13
13
 
14
14
  ウィザードが以下を順に案内します:
@@ -36,7 +36,7 @@ npm run dev # http://localhost:3000
36
36
  ウィザードは `npx` から Amplify Hosting の URL まで一気に進めることもできます:
37
37
 
38
38
  ```bash
39
- npx create-ampless@alpha my-site --deploy
39
+ npx create-ampless@beta my-site --deploy
40
40
  ```
41
41
 
42
42
  このフラグを追加すると、スキャフォールディング後に以下が実行されます:
@@ -51,7 +51,7 @@ npx create-ampless@alpha my-site --deploy
51
51
  コマンドラインに指定が不足している場合はインタラクティブに確認します。CI 向けのフル指定の例:
52
52
 
53
53
  ```bash
54
- npx create-ampless@alpha my-site --deploy \
54
+ npx create-ampless@beta my-site --deploy \
55
55
  --github-owner my-org \
56
56
  --github-private \
57
57
  --aws-region us-east-1 \
package/README.md CHANGED
@@ -5,10 +5,10 @@
5
5
 
6
6
  CLI scaffolding tool for [ampless](https://github.com/heavymoons/ampless) projects.
7
7
 
8
- > **Pre-release / alpha.** Breaking changes possible in any minor version until v1.0. Use the `@alpha` tag (the `@latest` tag won't exist until v1.0).
8
+ > **Pre-release / beta.** Breaking changes possible in any minor version until v1.0. Use the `@beta` tag (the `@latest` tag won't exist until v1.0).
9
9
 
10
10
  ```bash
11
- npx create-ampless@alpha
11
+ npx create-ampless@beta
12
12
  ```
13
13
 
14
14
  The wizard walks you through:
@@ -36,7 +36,7 @@ Sign up at `/login` — the first registered user is automatically promoted to t
36
36
  The wizard can also take you all the way from `npx` to a live Amplify Hosting URL:
37
37
 
38
38
  ```bash
39
- npx create-ampless@alpha my-site --deploy
39
+ npx create-ampless@beta my-site --deploy
40
40
  ```
41
41
 
42
42
  That extra flag, after scaffolding, runs:
@@ -51,7 +51,7 @@ That extra flag, after scaffolding, runs:
51
51
  Anything missing on the command line gets asked interactively. For CI-friendly fully-flagged usage:
52
52
 
53
53
  ```bash
54
- npx create-ampless@alpha my-site --deploy \
54
+ npx create-ampless@beta my-site --deploy \
55
55
  --github-owner my-org \
56
56
  --github-private \
57
57
  --aws-region us-east-1 \
package/dist/index.js CHANGED
@@ -516,12 +516,12 @@ function parseDeployArgs(argv) {
516
516
  var HELP_TEXT = `create-ampless \u2014 scaffold an ampless project
517
517
 
518
518
  Usage:
519
- npx create-ampless@latest <project-name> [options]
520
- npx create-ampless@latest --mount [options] # in an existing project dir
521
- npx create-ampless@latest upgrade [options] # in an existing project dir
522
- npx create-ampless@latest copy-theme <src> <dst> # in an existing project dir
523
- npx create-ampless@latest plugin <name> [options] # scaffold a plugin (Phase 5)
524
- npx create-ampless@latest setup-encryption-key [--gitignore] # generate encryption key file
519
+ npx create-ampless@beta <project-name> [options]
520
+ npx create-ampless@beta --mount [options] # in an existing project dir
521
+ npx create-ampless@beta upgrade [options] # in an existing project dir
522
+ npx create-ampless@beta copy-theme <src> <dst> # in an existing project dir
523
+ npx create-ampless@beta plugin <name> [options] # scaffold a plugin (Phase 5)
524
+ npx create-ampless@beta setup-encryption-key [--gitignore] # generate encryption key file
525
525
 
526
526
  Options:
527
527
  --site-name <name> Site display name (default: "My Blog")
@@ -558,7 +558,7 @@ Options:
558
558
  flag values (for CI / automation)
559
559
  -h, --help Show this message
560
560
 
561
- upgrade Sync ampless package files / deps to latest alpha.
561
+ upgrade Sync ampless package files / deps to latest beta.
562
562
  Run inside an existing ampless project.
563
563
 
564
564
  --dry-run Show what would change without writing any files
@@ -571,7 +571,7 @@ copy-theme <source> <target>
571
571
  (the convention that flags it as user-owned, so upgrade leaves
572
572
  it alone). Run inside an existing ampless project.
573
573
 
574
- Example: npx create-ampless@latest copy-theme blog my-blog
574
+ Example: npx create-ampless@beta copy-theme blog my-blog
575
575
 
576
576
  plugin <name>
577
577
  Scaffold an ampless plugin. Two modes:
@@ -596,8 +596,8 @@ plugin <name>
596
596
  --description "<text>" Optional one-line description
597
597
 
598
598
  Examples:
599
- npx create-ampless@latest plugin site-verification
600
- npx create-ampless@latest plugin @ishinao/ampless-plugin-foo --standalone
599
+ npx create-ampless@beta plugin site-verification
600
+ npx create-ampless@beta plugin @ishinao/ampless-plugin-foo --standalone
601
601
 
602
602
  setup-encryption-key
603
603
  Generate the AES-256-GCM encryption key for plugin secret storage
@@ -624,8 +624,8 @@ setup-encryption-key
624
624
  Rotation: re-run with confirm overwrite. Existing ciphertext
625
625
  becomes unreadable; re-save each secret via /admin/plugins.
626
626
 
627
- Example: npx create-ampless@latest setup-encryption-key
628
- Example: npx create-ampless@latest setup-encryption-key --gitignore
627
+ Example: npx create-ampless@beta setup-encryption-key
628
+ Example: npx create-ampless@beta setup-encryption-key --gitignore
629
629
  `;
630
630
 
631
631
  // src/deploy-prompts.ts
@@ -1885,23 +1885,41 @@ var AMPLESS_MANAGED_APP_PATHS = [
1885
1885
  "app/login",
1886
1886
  "app/site"
1887
1887
  ];
1888
- var AMPLESS_RETIRED_PATHS = [];
1888
+ var AMPLESS_RETIRED_PATHS = [
1889
+ // Phase 7 preview pipeline migrated from a `'use server'` action to a
1890
+ // Route Handler at `app/(admin)/admin/preview/route.tsx` (default
1891
+ // endpoint: `/admin/preview`). The old action made Next.js 15+ refuse
1892
+ // to compile the edit-post page
1893
+ // because the import graph traced `react-dom/server` from Client
1894
+ // Components through Server Action modules. Sites scaffolded before
1895
+ // this change pick the new endpoint up on their next `update-ampless`
1896
+ // — the new route file is seeded as part of the normal template
1897
+ // sync, and the old action file is retired by this entry.
1898
+ "app/(admin)/admin/_actions/render-preview.tsx"
1899
+ ];
1889
1900
  var AMPLESS_PACKAGES = /* @__PURE__ */ new Set([
1890
1901
  "ampless",
1891
1902
  "@ampless/admin",
1892
1903
  "@ampless/backend",
1893
- "@ampless/runtime",
1894
- "@ampless/plugin-seo",
1895
- "@ampless/plugin-rss",
1896
- "@ampless/plugin-schema-jsonld",
1897
- "@ampless/plugin-webhook",
1898
- "@ampless/plugin-og-image",
1899
1904
  "@ampless/plugin-analytics-ga4",
1905
+ "@ampless/plugin-cookie-consent",
1900
1906
  "@ampless/plugin-gtm",
1907
+ "@ampless/plugin-og-image",
1901
1908
  "@ampless/plugin-plausible",
1902
- "@ampless/plugin-cookie-consent",
1903
- "@ampless/plugin-reading-time"
1909
+ "@ampless/plugin-reading-time",
1910
+ "@ampless/plugin-rss",
1911
+ "@ampless/plugin-schema-jsonld",
1912
+ "@ampless/plugin-seo",
1913
+ "@ampless/plugin-webhook",
1914
+ "@ampless/plugin-x-embed",
1915
+ "@ampless/plugin-youtube",
1916
+ "@ampless/runtime"
1904
1917
  ]);
1918
+ var AMPLESS_MANAGED_TRANSITIVE_PREFIXES = ["@tiptap/"];
1919
+ function isManagedDep(name) {
1920
+ if (AMPLESS_PACKAGES.has(name)) return true;
1921
+ return AMPLESS_MANAGED_TRANSITIVE_PREFIXES.some((p3) => name.startsWith(p3));
1922
+ }
1905
1923
  var AMPLESS_MANAGED_SCRIPTS = /* @__PURE__ */ new Set([
1906
1924
  "sandbox",
1907
1925
  "sandbox:dev",
@@ -2138,6 +2156,120 @@ function detectIndent(jsonStr) {
2138
2156
  if (!m) return 2;
2139
2157
  return m[1].length;
2140
2158
  }
2159
+ async function bumpUserAmplessPlugins(destDir, projectPkg, templatePkg) {
2160
+ const result = { bumped: [], warnings: [] };
2161
+ const deps = projectPkg.dependencies ?? {};
2162
+ const templateDeps = templatePkg.dependencies ?? {};
2163
+ for (const pkgName of Object.keys(deps)) {
2164
+ if (!pkgName.startsWith("@ampless/")) continue;
2165
+ if (pkgName in templateDeps) continue;
2166
+ let latest;
2167
+ try {
2168
+ const { stdout } = await execa4("npm", ["view", `${pkgName}@beta`, "version"], {
2169
+ cwd: destDir,
2170
+ stdio: ["ignore", "pipe", "pipe"]
2171
+ });
2172
+ latest = stdout.trim();
2173
+ } catch (e) {
2174
+ result.warnings.push(
2175
+ `${pkgName}: failed to resolve @beta version (${e instanceof Error ? e.message : String(e)}). Leaving the existing pin in package.json.`
2176
+ );
2177
+ continue;
2178
+ }
2179
+ if (!latest) {
2180
+ result.warnings.push(`${pkgName}: npm view returned empty version. Skipping bump.`);
2181
+ continue;
2182
+ }
2183
+ const target = `^${latest}`;
2184
+ const current = deps[pkgName];
2185
+ if (current === target) continue;
2186
+ deps[pkgName] = target;
2187
+ result.bumped.push({ name: pkgName, from: current, to: target });
2188
+ }
2189
+ return result;
2190
+ }
2191
+ function djb2Hash(s) {
2192
+ let h = 5381;
2193
+ for (let i = 0; i < s.length; i++) {
2194
+ h = h * 33 ^ s.charCodeAt(i) | 0;
2195
+ }
2196
+ return (h >>> 0).toString(16);
2197
+ }
2198
+ async function regenerateEditorBootstrap(destDir, projectPkg) {
2199
+ const targetPath = join3(destDir, "app", "(admin)", "admin", "_editor-bootstrap.tsx");
2200
+ const installedPlugins = [];
2201
+ const warnings = [];
2202
+ const deps = projectPkg.dependencies ?? {};
2203
+ for (const pkgName of Object.keys(deps)) {
2204
+ const pkgJsonPath = join3(destDir, "node_modules", pkgName, "package.json");
2205
+ if (!existsSync5(pkgJsonPath)) continue;
2206
+ let mf;
2207
+ try {
2208
+ mf = JSON.parse(await readFile3(pkgJsonPath, "utf-8"));
2209
+ } catch (e) {
2210
+ warnings.push(`${pkgName}: failed to read package.json (${e instanceof Error ? e.message : String(e)}). Skipping editor wiring.`);
2211
+ continue;
2212
+ }
2213
+ const ep = mf?.amplessPlugin?.editorExports;
2214
+ if (typeof ep !== "string") continue;
2215
+ if (!ep.startsWith("./") || ep.includes("..") || ep.startsWith("/") || /[?#]/.test(ep)) {
2216
+ warnings.push(`${pkgName}: amplessPlugin.editorExports="${ep}" is invalid (must be a ./-prefixed relative subpath without .. or query/fragment). Skipping editor wiring.`);
2217
+ continue;
2218
+ }
2219
+ if (!mf?.exports || typeof mf.exports[ep] === "undefined") {
2220
+ warnings.push(`${pkgName}: amplessPlugin.editorExports="${ep}" is not declared in package.json#exports. Skipping editor wiring.`);
2221
+ continue;
2222
+ }
2223
+ installedPlugins.push({ packageName: pkgName, editorExports: ep });
2224
+ }
2225
+ installedPlugins.sort((a, b) => a.packageName.localeCompare(b.packageName));
2226
+ const lines = [];
2227
+ lines.push("'use client'", "");
2228
+ lines.push("// AUTO-GENERATED by `npm run update-ampless`. Do not edit \u2014 your changes");
2229
+ lines.push("// will be overwritten on the next run. To enable / disable a plugin's");
2230
+ lines.push("// editor extension, add / remove the corresponding @ampless/plugin-...");
2231
+ lines.push("// entry in your project's package.json (and register it in cms.config.ts");
2232
+ lines.push("// for runtime).", "");
2233
+ lines.push("import { installAdminEditorExtensions, installAdminTiptapNodeMarkdown, installAdminTiptapNodeHtml } from '@ampless/admin/editor'");
2234
+ const identifiers = [];
2235
+ const seen = /* @__PURE__ */ new Map();
2236
+ for (const p3 of installedPlugins) {
2237
+ let ident = "__" + p3.packageName.replace(/^@/, "").replace(/[^a-zA-Z0-9]+/g, "_") + "_editor";
2238
+ if (seen.has(ident) && seen.get(ident) !== p3.packageName) {
2239
+ const suffix = djb2Hash(p3.packageName).slice(0, 8);
2240
+ const orig = ident;
2241
+ ident = `${ident}_${suffix}`;
2242
+ warnings.push(`editor-bootstrap codegen: sanitised identifier "${orig}" collides between "${seen.get(orig)}" and "${p3.packageName}". Using "${ident}" for the latter to disambiguate.`);
2243
+ }
2244
+ seen.set(ident, p3.packageName);
2245
+ const importSubpath = p3.editorExports.startsWith("./") ? `${p3.packageName}/${p3.editorExports.slice(2)}` : `${p3.packageName}/${p3.editorExports}`;
2246
+ lines.push(`import * as ${ident} from '${importSubpath}'`);
2247
+ identifiers.push(ident);
2248
+ }
2249
+ lines.push("");
2250
+ lines.push("export function EditorBootstrap({ children }: { children: React.ReactNode }) {");
2251
+ if (identifiers.length === 0) {
2252
+ lines.push(" installAdminEditorExtensions([])");
2253
+ lines.push(" installAdminTiptapNodeMarkdown([])");
2254
+ lines.push(" installAdminTiptapNodeHtml([])");
2255
+ } else {
2256
+ lines.push(" installAdminEditorExtensions([");
2257
+ for (const id of identifiers) lines.push(` ${id}.editorExtension,`);
2258
+ lines.push(" ])");
2259
+ lines.push(" installAdminTiptapNodeMarkdown([");
2260
+ for (const id of identifiers) lines.push(` ${id}.tiptapNodeToMarkdown ?? {},`);
2261
+ lines.push(" ])");
2262
+ lines.push(" installAdminTiptapNodeHtml([");
2263
+ for (const id of identifiers) lines.push(` ${id}.tiptapNodeToHtml ?? {},`);
2264
+ lines.push(" ])");
2265
+ }
2266
+ lines.push(" return <>{children}</>");
2267
+ lines.push("}");
2268
+ lines.push("");
2269
+ await mkdir2(dirname(targetPath), { recursive: true });
2270
+ await writeFile3(targetPath, lines.join("\n"), "utf-8");
2271
+ return { path: targetPath, pluginCount: installedPlugins.length, warnings };
2272
+ }
2141
2273
  async function runUpgradeIn(destDir, sharedDir, opts = {}) {
2142
2274
  const derivedRoot = opts.templatesRoot ?? join3(sharedDir, "..");
2143
2275
  const themeSyncEnabled = existsSync5(join3(derivedRoot, "_shared"));
@@ -2231,7 +2363,7 @@ async function runUpgradeIn(destDir, sharedDir, opts = {}) {
2231
2363
  const templateDeps = templatePkg[section] ?? {};
2232
2364
  const projectDeps = projectPkg[section] ?? {};
2233
2365
  for (const [name, version] of Object.entries(templateDeps)) {
2234
- if (!AMPLESS_PACKAGES.has(name)) continue;
2366
+ if (!isManagedDep(name)) continue;
2235
2367
  projectDeps[name] = version;
2236
2368
  }
2237
2369
  if (Object.keys(projectDeps).length > 0) {
@@ -2251,11 +2383,46 @@ async function runUpgradeIn(destDir, sharedDir, opts = {}) {
2251
2383
  projectPkg["scripts"] = projectScripts;
2252
2384
  }
2253
2385
  await writeFile3(projectPkgPath, JSON.stringify(projectPkg, null, indent) + "\n", "utf-8");
2386
+ const templatePkgForBump = templatePkg;
2387
+ const projectPkgForBump = projectPkg;
2388
+ const userPluginBump = await bumpUserAmplessPlugins(destDir, projectPkgForBump, templatePkgForBump);
2389
+ if (userPluginBump.bumped.length > 0) {
2390
+ await writeFile3(projectPkgPath, JSON.stringify(projectPkg, null, indent) + "\n", "utf-8");
2391
+ }
2254
2392
  if (!opts.noInstall) {
2255
2393
  const usePnpm = existsSync5(join3(destDir, "pnpm-lock.yaml"));
2256
2394
  const pm = usePnpm ? "pnpm" : "npm";
2257
2395
  await execa4(pm, ["install"], { cwd: destDir, stdio: "inherit" });
2258
2396
  }
2397
+ const codegenProjectPkg = JSON.parse(
2398
+ await readFile3(join3(destDir, "package.json"), "utf-8")
2399
+ );
2400
+ const bootstrapResult = await regenerateEditorBootstrap(destDir, codegenProjectPkg);
2401
+ if (opts.noInstall) {
2402
+ bootstrapResult.warnings.push(
2403
+ "_editor-bootstrap.tsx was generated without running npm install \u2014 node_modules may be stale. Run `npm run update-ampless` (without --no-install) to pick up newly added plugin editor extensions."
2404
+ );
2405
+ if (userPluginBump.bumped.length > 0) {
2406
+ userPluginBump.warnings.push(
2407
+ `package.json was updated with ${userPluginBump.bumped.length} bumped @ampless/* plugin version(s) but npm install was skipped \u2014 run \`npm install\` (or \`npm run update-ampless\` without --no-install) to apply the new versions.`
2408
+ );
2409
+ }
2410
+ }
2411
+ for (const w of bootstrapResult.warnings) {
2412
+ log3.warn(w);
2413
+ }
2414
+ log3.info(
2415
+ `editor: ${pc3.cyan(`auto-wired ${bootstrapResult.pluginCount} editor extension(s) in _editor-bootstrap.tsx`)}`
2416
+ );
2417
+ if (userPluginBump.bumped.length > 0) {
2418
+ log3.info(`plugins: ${pc3.cyan(`bumped ${userPluginBump.bumped.length} user-installed @ampless/* plugin(s) to latest @beta:`)}`);
2419
+ for (const { name, from, to } of userPluginBump.bumped) {
2420
+ log3.info(` ${name}: ${from} \u2192 ${to}`);
2421
+ }
2422
+ }
2423
+ for (const w of userPluginBump.warnings) {
2424
+ log3.warn(w);
2425
+ }
2259
2426
  return {
2260
2427
  added: replaceNew,
2261
2428
  updated: replaceUpdate,
@@ -2265,7 +2432,9 @@ async function runUpgradeIn(destDir, sharedDir, opts = {}) {
2265
2432
  themesPreserved: themeResult.preserved,
2266
2433
  themesQuarantined: themeResult.quarantined,
2267
2434
  packageJsonMerged: true,
2268
- obsoleteRemoved: obsoleteFiles
2435
+ obsoleteRemoved: obsoleteFiles,
2436
+ editorExtensionsWired: bootstrapResult.pluginCount,
2437
+ userPluginsBumped: userPluginBump.bumped.length
2269
2438
  };
2270
2439
  }
2271
2440
  async function runUpgrade(args) {
@@ -2361,8 +2530,8 @@ async function runCopyTheme(args) {
2361
2530
  const source = args.copyThemeSource;
2362
2531
  const target = args.copyThemeTarget;
2363
2532
  if (!source || !target) {
2364
- log4.error("Usage: npx create-ampless@latest copy-theme <source> <target>");
2365
- log4.info("Example: npx create-ampless@latest copy-theme blog my-blog");
2533
+ log4.error("Usage: npx create-ampless@beta copy-theme <source> <target>");
2534
+ log4.info("Example: npx create-ampless@beta copy-theme blog my-blog");
2366
2535
  process.exit(1);
2367
2536
  }
2368
2537
  try {
@@ -2397,7 +2566,7 @@ import {
2397
2566
  cancel as cancel3
2398
2567
  } from "@clack/prompts";
2399
2568
  import pc5 from "picocolors";
2400
- var SCAFFOLD_AMPLESS_VERSION = "1.0.0-alpha.22";
2569
+ var SCAFFOLD_AMPLESS_VERSION = "1.0.0-beta.0";
2401
2570
  var TEXT_EXTENSIONS3 = /* @__PURE__ */ new Set([
2402
2571
  ".json",
2403
2572
  ".md",
@@ -2648,7 +2817,7 @@ async function runCreatePlugin(args) {
2648
2817
  ${pc5.cyan("pnpm install")}
2649
2818
  ${pc5.cyan("pnpm test")}
2650
2819
  ${pc5.cyan("pnpm build")}
2651
- ${pc5.cyan("pnpm publish --access public --tag alpha")}
2820
+ ${pc5.cyan("pnpm publish --access public --tag beta")}
2652
2821
 
2653
2822
  Then add the published package to your ampless site:
2654
2823
  ${pc5.cyan(`pnpm add ${result.packageName ?? result.pluginName}`)}`
@@ -2671,7 +2840,7 @@ var SECRETS_DIR = "amplify/secrets";
2671
2840
  var KEY_FILE = "encryption-key.ts";
2672
2841
  var KEY_FILE_PATH = `${SECRETS_DIR}/${KEY_FILE}`;
2673
2842
  function buildFileContent(keyB64) {
2674
- return `// Generated by \`npx create-ampless setup-encryption-key\`.
2843
+ return `// Generated by \`npx create-ampless@beta setup-encryption-key\`.
2675
2844
  // AES-256-GCM key (32 bytes, base64) for plugin secret storage.
2676
2845
  // Rotate via the same CLI command. Existing ciphertext becomes unreadable on rotation.
2677
2846
 
@@ -2815,7 +2984,7 @@ async function runMount(args) {
2815
2984
  if (problem) {
2816
2985
  log7.error(problem);
2817
2986
  log7.info(
2818
- "Run `npx create-ampless@latest <name>` first to scaffold, or `cd` into a scaffolded project before passing --mount."
2987
+ "Run `npx create-ampless@beta <name>` first to scaffold, or `cd` into a scaffolded project before passing --mount."
2819
2988
  );
2820
2989
  process.exit(1);
2821
2990
  }
@@ -133,7 +133,7 @@ export default defineConfig({
133
133
  このプロジェクトディレクトリ内で:
134
134
 
135
135
  ```bash
136
- npx create-ampless@latest --mount \
136
+ npx create-ampless@beta --mount \
137
137
  --github-owner <your-user-or-org> \
138
138
  --aws-region <region> \
139
139
  --create-iam-role # 初回のみ。次回以降は `--iam-service-role <arn>` で使い回し
@@ -153,7 +153,7 @@ CLI が以下を一気に実行します:
153
153
  - `--skip-confirm` — 非対話モード(CI / 再実行向け)
154
154
  - `--aws-profile <name>` — 複数 AWS profile がある場合に明示
155
155
 
156
- 全フラグは `npx create-ampless@latest --help` を参照。
156
+ 全フラグは `npx create-ampless@beta --help` を参照。
157
157
 
158
158
  **このフロー固有の事前準備([トップの必要なもの](#必要なもの) に加えて):**
159
159
 
@@ -216,13 +216,13 @@ MCP のセットアップとトークン管理の詳細については [docs/arc
216
216
 
217
217
  ## ampless の更新
218
218
 
219
- ampless は `alpha` dist-tag でリリースしています。新機能を取り込むには:
219
+ public beta 中の ampless は `beta` dist-tag でリリースしています。新機能を取り込むには:
220
220
 
221
221
  ```bash
222
222
  npm run update-ampless
223
223
  ```
224
224
 
225
- これは `npx create-ampless@latest upgrade` を実行し、以下を行います:
225
+ これは `npx create-ampless@beta upgrade` を実行し、以下を行います:
226
226
 
227
227
  - `package.json` の `@ampless/*` / `ampless` 依存をバージョンアップ
228
228
  - 共有テンプレートファイル(admin アプリの土台、amplify バックエンド、lib/、middleware、テーマ)を再同期 — `cms.config.ts`、`theme.*` 管理画面設定、投稿、テーマ manifest 値などのユーザーカスタマイズは保持されます
@@ -133,7 +133,7 @@ The shipped [`amplify.yml`](./amplify.yml) runs `npx ampx pipeline-deploy` (Ampl
133
133
  From inside this project directory:
134
134
 
135
135
  ```bash
136
- npx create-ampless@latest --mount \
136
+ npx create-ampless@beta --mount \
137
137
  --github-owner <your-user-or-org> \
138
138
  --aws-region <region> \
139
139
  --create-iam-role # first time only; reuse `--iam-service-role <arn>` on later mounts
@@ -153,7 +153,7 @@ Useful extra flags:
153
153
  - `--skip-confirm` — non-interactive (good for CI / re-runs)
154
154
  - `--aws-profile <name>` — explicit AWS profile when you have multiple
155
155
 
156
- See `npx create-ampless@latest --help` for the full list.
156
+ See `npx create-ampless@beta --help` for the full list.
157
157
 
158
158
  **Prerequisites for this flow (in addition to the [top-level Requirements](#requirements)):**
159
159
 
@@ -216,13 +216,13 @@ See [docs/architecture/04-access-layer-mcp.md](./docs/architecture/04-access-lay
216
216
 
217
217
  ## Updating ampless
218
218
 
219
- ampless releases on the `alpha` dist-tag. To pick up new features:
219
+ ampless releases on the `beta` dist-tag during the public beta. To pick up new features:
220
220
 
221
221
  ```bash
222
222
  npm run update-ampless
223
223
  ```
224
224
 
225
- This runs `npx create-ampless@latest upgrade`, which:
225
+ This runs `npx create-ampless@beta upgrade`, which:
226
226
 
227
227
  - Bumps the `@ampless/*` and `ampless` dependencies in `package.json`
228
228
  - Re-syncs the shared template files (admin app shell, amplify backend, lib/, middleware, themes) — your customizations to `cms.config.ts`, `theme.*` admin settings, posts, and theme manifest values are preserved.
@@ -13,7 +13,7 @@ import { userAdmin } from './functions/user-admin/resource.js'
13
13
  import { pluginSecretHandler } from './functions/plugin-secret-handler/resource.js'
14
14
  import { customizeBackend } from './backend.custom.js'
15
15
  // Plugin secret encryption key (Phase 6a v2.2).
16
- // Generate with: npx create-ampless setup-encryption-key
16
+ // Generate with: npx create-ampless@beta setup-encryption-key
17
17
  // The key lives in amplify/secrets/encryption-key.ts (gitignore it for
18
18
  // public repos; safe to commit for private repos).
19
19
  import { PLUGIN_SECRET_ENCRYPTION_KEY } from './secrets/encryption-key.js'
@@ -8,6 +8,13 @@ import { util } from '@aws-appsync/utils'
8
8
  //
9
9
  // Drafts are dropped in the response handler. If draft + published
10
10
  // somehow share a slug we prefer the published row.
11
+ //
12
+ // Scheduled-publish: a `published` post is visible only when its
13
+ // publishedAt is absent (= immediate) OR <= now (= already live).
14
+ // When publishedAt is present and in the future the post is treated as
15
+ // "not yet live" and is hidden from the public site. This check is
16
+ // server-authoritative: the AppSync resolver enforces it on every read
17
+ // without relying on any client-side filtering.
11
18
  export function request(ctx) {
12
19
  const slug = ctx.args.slug
13
20
  return {
@@ -25,6 +32,11 @@ export function request(ctx) {
25
32
  export function response(ctx) {
26
33
  if (ctx.error) util.error(ctx.error.message, ctx.error.type)
27
34
  const items = ctx.result.items ?? []
28
- const published = items.find((i) => i.status === 'published')
35
+ const now = util.time.nowISO8601()
36
+ // Hide a published post only when publishedAt exists AND is in the future
37
+ // (scheduled). Missing publishedAt = immediate publish.
38
+ const published = items.find(
39
+ (i) => i.status === 'published' && (!i.publishedAt || i.publishedAt <= now)
40
+ )
29
41
  return published ?? null
30
42
  }
@@ -9,14 +9,26 @@ import { util } from '@aws-appsync/utils'
9
9
  // the tag partition condition. Drafts never appear here because the
10
10
  // admin client only writes PostTag rows for posts whose status is
11
11
  // 'published'.
12
+ //
13
+ // Scheduled-publish: the SK is a composite of `${publishedAt}#${postId}`.
14
+ // ISO 8601 timestamps are fixed-width to millisecond precision, so lexical
15
+ // order == chronological order. We add an upper-bound SK condition
16
+ // `<= now + '#' + '￿'` to exclude PostTag rows whose publishedAt is in
17
+ // the future. U+FFFF sorts above every character that can appear in a real
18
+ // postId, so `now#￿` is strictly greater than `now#<any-postId>`:
19
+ // every postId suffix sharing the current-millisecond ISO prefix is
20
+ // included, while any SK whose timestamp prefix is later is excluded.
21
+ // (Written as an escape so this source file stays ASCII.)
12
22
  export function request(ctx) {
13
23
  const { tag, limit, nextToken } = ctx.args
24
+ const now = util.time.nowISO8601()
25
+ const upper = now + '#￿'
14
26
  return {
15
27
  operation: 'Query',
16
28
  query: {
17
- expression: '#tag = :tag',
18
- expressionNames: { '#tag': 'tag' },
19
- expressionValues: util.dynamodb.toMapValues({ ':tag': tag }),
29
+ expression: '#tag = :tag AND #pap <= :upper',
30
+ expressionNames: { '#tag': 'tag', '#pap': 'publishedAtPostId' },
31
+ expressionValues: util.dynamodb.toMapValues({ ':tag': tag, ':upper': upper }),
20
32
  },
21
33
  scanIndexForward: false, // newest first (SK descends)
22
34
  limit: limit ?? 20,
@@ -1,4 +1,4 @@
1
- import { util } from '@aws-appsync/utils'
1
+ import { util, runtime } from '@aws-appsync/utils'
2
2
 
3
3
  // AppSync JS resolver: list published posts, newest first.
4
4
  //
@@ -8,29 +8,40 @@ import { util } from '@aws-appsync/utils'
8
8
  // so a single Query reads only the `published` partition. Drafts never
9
9
  // appear because the PK condition pins status='published'.
10
10
  //
11
+ // Scheduled-publish: the upper bound of the SK range is always clamped
12
+ // to `now`. This ensures future-dated published posts (scheduled but not
13
+ // yet live) never appear in public listings. The upper bound is the lesser
14
+ // of `now` and any caller-supplied `to` argument.
15
+ //
16
+ // Reverse-range guard: if `from` is supplied and ends up > upper (e.g.
17
+ // the caller asks for a range entirely in the future), we return an empty
18
+ // result early via `runtime.earlyReturn` instead of issuing an inverted
19
+ // BETWEEN to DynamoDB (which DynamoDB rejects with a ValidationException).
20
+ //
11
21
  // Date-range filtering (`from`, `to`) is pushed into the SK condition,
12
22
  // so DynamoDB only reads the matching range. `nextToken` paginates
13
23
  // without re-issuing a fresh query.
14
24
  export function request(ctx) {
15
25
  const { from, to, limit, nextToken } = ctx.args
26
+ const now = util.time.nowISO8601()
27
+ // Always clamp the upper bound to now so future-dated posts are excluded.
28
+ const upper = to && to < now ? to : now
29
+
30
+ // If `from` is later than `upper` the range is empty — skip the query
31
+ // rather than letting DynamoDB see an inverted BETWEEN / >= condition.
32
+ if (from && from > upper) {
33
+ return runtime.earlyReturn({ items: [], nextToken: null })
34
+ }
16
35
 
17
36
  let keyExpression = '#status = :status'
18
- const expressionNames = { '#status': 'status' }
19
- const expressionValueMap = { ':status': 'published' }
37
+ const expressionNames = { '#status': 'status', '#publishedAt': 'publishedAt' }
38
+ const expressionValueMap = { ':status': 'published', ':upper': upper }
20
39
 
21
- if (from && to) {
22
- keyExpression += ' AND #publishedAt BETWEEN :from AND :to'
23
- expressionNames['#publishedAt'] = 'publishedAt'
24
- expressionValueMap[':from'] = from
25
- expressionValueMap[':to'] = to
26
- } else if (from) {
27
- keyExpression += ' AND #publishedAt >= :from'
28
- expressionNames['#publishedAt'] = 'publishedAt'
40
+ if (from) {
41
+ keyExpression += ' AND #publishedAt BETWEEN :from AND :upper'
29
42
  expressionValueMap[':from'] = from
30
- } else if (to) {
31
- keyExpression += ' AND #publishedAt <= :to'
32
- expressionNames['#publishedAt'] = 'publishedAt'
33
- expressionValueMap[':to'] = to
43
+ } else {
44
+ keyExpression += ' AND #publishedAt <= :upper'
34
45
  }
35
46
 
36
47
  return {
@@ -1,4 +1,14 @@
1
1
  // Re-exported from @ampless/backend so the package can ship Lambda
2
2
  // handler updates via `npm update`. Amplify's esbuild follows this
3
3
  // import and bundles the real handler into the Lambda artifact.
4
- export { handler } from '@ampless/backend/events/dispatcher'
4
+ //
5
+ // The dispatcher now also captures per-post revision history. The
6
+ // retention window comes from the user-side `cms.config` — the package
7
+ // handler never imports `cms.config` directly (it can't know the project
8
+ // layout), so this thin shell wires the value in via the factory.
9
+ import { createDispatcherHandler } from '@ampless/backend/events/dispatcher'
10
+ import config from '../../../cms.config'
11
+
12
+ export const handler = createDispatcherHandler({
13
+ historyRetentionDays: config.history?.retentionDays ?? 0,
14
+ })
@@ -1,9 +1,9 @@
1
- // Generated by `npx create-ampless setup-encryption-key`.
1
+ // Generated by `npx create-ampless@beta setup-encryption-key`.
2
2
  // AES-256-GCM key (32 bytes, base64) for plugin secret storage.
3
3
  // Rotate via the same CLI command. Existing ciphertext becomes unreadable on rotation.
4
4
  //
5
5
  // IMPORTANT: This is a placeholder. Replace the value below with a real key
6
- // by running: npx create-ampless setup-encryption-key
6
+ // by running: npx create-ampless@beta setup-encryption-key
7
7
  // (from your project root, after scaffolding).
8
8
 
9
9
  export const PLUGIN_SECRET_ENCRYPTION_KEY = ''
@@ -0,0 +1,13 @@
1
+ 'use client'
2
+
3
+ // Placeholder for fresh scaffolds before `update-ampless` runs.
4
+ // `npm run update-ampless` rewrites this file with auto-wired plugin
5
+ // editor extensions based on the project's installed `@ampless/plugin-*`
6
+ // packages.
7
+
8
+ import { installAdminEditorExtensions } from '@ampless/admin/editor'
9
+
10
+ export function EditorBootstrap({ children }: { children: React.ReactNode }) {
11
+ installAdminEditorExtensions([])
12
+ return <>{children}</>
13
+ }
@@ -1,4 +1,5 @@
1
1
  import { admin } from '@/lib/admin'
2
2
  import { createAdminLayout } from '@ampless/admin/pages'
3
+ import { EditorBootstrap } from './_editor-bootstrap'
3
4
 
4
- export default createAdminLayout(admin)
5
+ export default createAdminLayout(admin, { editorBootstrap: EditorBootstrap })
@@ -0,0 +1,4 @@
1
+ import { admin } from '@/lib/admin'
2
+ import { createPreviewRouteHandler } from '@ampless/admin/api'
3
+
4
+ export const POST = createPreviewRouteHandler(admin)