create-smoodly-app 0.0.36 → 0.0.38

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -4,7 +4,7 @@
4
4
 
5
5
  A fresh Next.js app with the Smoodly admin mounted at `/admin`, a Supabase
6
6
  project holding the schema, `.env.local` written, and a first editor who can
7
- sign in. Spec: `docs/superpowers/specs/2026-09-04-create-smoodly-app-design.md`.
7
+ sign in.
8
8
 
9
9
  ## What it asks
10
10
 
@@ -41,202 +41,3 @@ Every prompt has a flag, so CI can run it: `--supabase local|hosted`, `--locales
41
41
  `SMOODLY_EDITOR_PASSWORD`), `--skip-link`, `--yes`. `--help` lists them.
42
42
  A hidden `--smoodly-dep <spec>` overrides the `smoodly` dependency written into
43
43
  `package.json`; CI points it at the packed tarball.
44
-
45
- ## How it is built
46
-
47
- `templates/lorem`, `templates/lorem-intl`, `templates/blank` and
48
- `templates/blank-intl` are all generated by `scripts/build-template.ts`.
49
- The Lorem Co. pair comes from `examples/site` — the multilingual layout —
50
- plus the five single-language files under `variants/lorem/single/`. The
51
- blank pair (spec 2026-09-11) takes only the content-free glue from the
52
- example (`smoodly/server.ts`, `ops.ts`, `admin.tsx`, `richtext.tsx`, the
53
- admin route files, next-intl's files) and everything with content from
54
- `variants/blank/{common,single,intl}/`. Each template also gets a generated
55
- `package.json` (with `smoodly` pinned to this package's version),
56
- `tsconfig`, `next.config`, README, `AGENTS.md`, a `CLAUDE.md` that imports
57
- it, and the one migration (`coreTablesSQL` + the starter's collection
58
- views). Two exact-line transforms turn the example's glue (locale config,
59
- `href`) into the single-language one; a drifted example fails the build
60
- rather than silently shipping stale copies. Every template gets a check
61
- tsconfig beside it; `npm run typecheck` runs `tsc` over all four
62
- (`typecheck:templates`), so hand-kept files cannot drift silently. All
63
- four templates are git-ignored and rebuilt by `npm run build`.
64
-
65
- `src/plan.ts` turns answers into steps; `src/run.ts` executes them with
66
- injected `exec`, fs and Supabase client so the sequence and every fallback
67
- message are unit-tested. `src/prompts.ts` is the only interactive code.
68
- The CLI has no runtime dependency on `smoodly`.
69
-
70
- npm test # unit tests (template build, planner, runner, editor)
71
- npm run typecheck # tsc, then the four templates
72
- npm run build # templates + esbuild bundle → dist/index.js
73
-
74
- Walked 2026-09-06: both templates scaffolded against the repo's local stack
75
- in hosted mode, `tsc`, `next build` and `/admin` 200 for each; the
76
- multilingual one with `--locales en,de`.
77
-
78
- Walked 2026-09-11: blank multilingual scaffolded in local mode with
79
- `--locales en,fr` from the built CLI, `next build`, `/` and `/fr` seeded
80
- with the Text section and the site footer line, the admin listing Home,
81
- Posts and the Site item, a post with a Button element published and
82
- served at `/posts/<slug>` in both locales; blank single-language
83
- scaffolded in hosted mode against the repo stack and `next build` only.
84
-
85
- ## Follow-ups (logged 2026-09-28, fresh-install proof of 0.0.30)
86
-
87
- Walked 2026-09-28: `npx create-smoodly-app@0.0.30 proof-0030 --supabase
88
- local --yes --editor-email …` from the published package, in a scratch
89
- directory beside three running stacks (the new one took the `563xx`
90
- block). `smoodly` 0.0.30 installed, `tsc` and `next build` clean, `/`,
91
- `/posts`, `/posts/<slug>`, `/company/about-us` and `/admin` 200, the
92
- admin document's `<html lang="en">` present, the editor signed in, an
93
- SEO title edit saved and published and served on `/`. Found:
94
-
95
- - **Local mode left Realtime out (0.0.26–0.0.30).** `LOCAL_START_ARGS`
96
- excluded `realtime` from before the concurrency spec, whose presence
97
- channel and `saved` events ride it; the admin retried the WebSocket
98
- forever (a console error per attempt) and a local install had no
99
- presence or stale-page notice. CI's repo stack already kept Realtime.
100
- Fixed 2026-09-28: the list no longer excludes it; restarted with the new
101
- flags, a script signed in as the editor joined `space:<id>` and saw both
102
- open admin tabs tracked on Home. The live job does not catch this
103
- (it requests `/admin` once, no browser); an existing local install
104
- restarts with `supabase stop && supabase start -x imgproxy,edge-runtime,logflare,vector,supavisor`.
105
- - **The Lorem home's custom-section CTA linked `/articles`**, a 404 —
106
- the articles collection lives at `/posts`. `examples/site/scripts/seed.ts`
107
- hardcoded the href; fixed 2026-09-28 to `paths(locale).articles` like
108
- every other starter link (Finnish gets its own segment too).
109
- - **`--editor-email` without `SMOODLY_EDITOR_PASSWORD` fails late.** The
110
- check lives in `askEditorWith`, which runs after the copy, install,
111
- `git init` and `supabase start`, so the run stops at `ensureEditor`
112
- with a half-made app. It has to be late for hosted mode (the email may
113
- name an existing Auth user, no password needed), but a local stack is
114
- always fresh, so local mode could refuse up front. Fixed 2026-09-28:
115
- `resolveAnswers` refuses local `--editor-email` without the variable
116
- before anything is written (the built CLI exits 1, no directory made);
117
- hosted keeps the late check.
118
-
119
- ## Follow-ups (logged 2026-09-25, docs walk)
120
-
121
- - **Single-language `/admin` had no root `<html>` (0.0.15–0.0.20).** The
122
- variant's `app/(smoodly)/admin/layout.tsx` re-exported `AdminLayout`,
123
- which renders a `<div>`; that only worked while the site had a
124
- top-level `app/layout.tsx`, and 0.0.15 moved the site into `app/(site)`.
125
- `next dev` showed two errors on the first visit to `/admin`: "Missing
126
- <html> tags in the root layout" and, from the client-rendered tree, the
127
- theme boot's "Encountered a script tag while rendering React component".
128
- Found walking the Getting started guide on a fresh `lorem` install of
129
- 0.0.20. Fixed 2026-09-25: the variant layout wraps `AdminLayout` in
130
- `<html><body>` like the example's; both errors gone in the walk. CI's
131
- live job only checks `/admin` for a 200, which this passed; an assertion
132
- on the dev overlay (or on `<html>` in the served document) would have
133
- caught it.
134
-
135
- ## Follow-ups (logged 2026-09-11)
136
-
137
- - **Blank single-language is typechecked, never built by CI.** The live
138
- job scaffolds lorem single (hosted), lorem intl and blank intl (local).
139
- A fourth stack costs ~2 minutes; add it if blank single ever breaks in
140
- a way `tsc` cannot see (a runtime import, a missing file at a path
141
- Next reads).
142
- - **The guide inside the package.** `AGENTS.md` is generated per scaffold
143
- and frozen at install time. Once its text settles, ship it in the
144
- `smoodly` package (`smoodly/AGENTS.md`) and have the scaffold's copy
145
- point there, so it tracks the package version.
146
-
147
- ## Follow-ups (logged 2026-09-06)
148
-
149
- - Local mode on a fresh multilingual install (0.0.5, 2026-09-06) failed at
150
- `supabase start` with `supabase_storage_<app> container is not ready:
151
- unhealthy` — the storage container logged `Started Successfully` and the
152
- CLI still stopped the stack. Five local stacks (57 containers) were
153
- sharing 7.8 GB of Docker memory at the time, the same pressure that
154
- made the repo's live suites flake on 2026-09-05. Smoodly never uses
155
- storage, so the CLI could start the app's stack with
156
- `supabase start -x storage-api,imgproxy,edge-runtime,logflare,vector,realtime`
157
- (the container names `supabase start --help` lists; CI already excludes
158
- most of these for the repo stack), and the "Finish by hand" text could
159
- mention `supabase stop` on other projects. Retried the same day with
160
- the other four stacks stopped (`supabase stop --project-id <name>`):
161
- `npx create-smoodly-app@0.0.5 --supabase local --locales en,fi --yes`
162
- came up with every container healthy, storage included, and the app
163
- built and served `/admin` 200, `/` and `/fi` 404 — so the cause is
164
- memory pressure, not the CLI. Fixed the same day: `run.ts` starts the
165
- app's stack with `-x storage-api,imgproxy,edge-runtime,logflare,vector,realtime,supavisor`
166
- (`LOCAL_START_ARGS`), and the "Finish by hand" text carries the same
167
- flags. Kept: db, kong, auth, PostgREST, Studio and pg-meta, mailpit.
168
- Storage is back in the local stack since the media library
169
- (2026-09-08); the health-check flake is a Docker memory problem, see
170
- the repo's memory note.
171
- - **A non-interactive run without `--locales` needs `--yes`.** The
172
- "Multilingual site?" prompt is asked whenever `--locales` is absent, and
173
- with no TTY (a CI step, stdin at EOF) the CLI exits 13 having created
174
- nothing — "Detected unsettled top-level await". CI's hosted-mode e2e step
175
- hit this; both e2e steps now pass a flag that settles the question
176
- (`--yes` for single-language, `--locales en,de` for the multilingual one).
177
- A prompt that treated EOF as its default value would need no flag.
178
- - **The next-intl template inherits next-intl's `localeDetection`
179
- default**, which redirects an unprefixed URL to the visitor's preferred
180
- locale *keeping the source locale's slugs* — on a site with translated
181
- slugs that redirect lands on a 404 (`/company/about-us` →
182
- `/fi/company/about-us` for a Finnish browser). Recorded in
183
- `examples/site/README.md` under "Multilingual (2026-09-06)". Fixed the
184
- same day: `i18n/routing.ts` in the example — and so in the generated
185
- `templates/next-intl` — sets `localeDetection: false`, so the English
186
- URL stays English for a Finnish browser and the visitor picks a
187
- language through the site's own switcher.
188
-
189
- ## Follow-ups (logged 2026-09-05)
190
-
191
- - Text prompts drop `@` when the terminal sends Option as Meta (macOS,
192
- Finnish/Nordic layouts put `@` on Option+2): the terminal emits `ESC 2` and
193
- Node's readline discards meta-keys, so nothing reaches the prompt.
194
- Reproduced against `@clack/prompts` 1.7.0 with a scripted stdin. Not
195
- fixable from prompt options; documented under "What it asks" with the
196
- paste / `--editor-email` workaround. Revisit if clack exposes a key hook.
197
- - Fresh installs printed a `NOTICE ... does not exist, skipping` for every
198
- `drop trigger/policy if exists` in the migration (fixed 2026-09-05: the
199
- migration opens with `set client_min_messages = warning`).
200
-
201
- ## Follow-ups (logged 2026-09-04)
202
-
203
- - The port probe binds `0.0.0.0` (a loopback-only bind coexists with a Docker-published port on macOS), so macOS may prompt once to allow `node` incoming connections during local mode; either answer is fine.
204
- - Windows: `exec` passes `shell: true` on win32 (needed for the `.cmd` shims)
205
- but does not quote arguments, so a project name or path containing a space
206
- or `&` breaks. Quote the args when the shell is on, or resolve the `.cmd`
207
- path and drop `shell`. Untested on Windows.
208
- - Hosted mode, manual run against a real hosted project with new-format keys
209
- (done 2026-09-05, throwaway project, `sb_secret_` + `sb_publishable_`):
210
- `db push` of the template migration printed no NOTICE, the CLI listed and
211
- created the first editor through the GoTrue admin API with the secret key
212
- as the sole `apikey`, the `members` row landed with role `admin`, `next
213
- build` passed, `/admin` served the shell, and a password sign-in with the
214
- publishable key returned a session. Caveat: the link step ran by hand with
215
- an explicit `--project-ref` (see the next-but-one item); the CLI's own
216
- `supabase link` was not driven interactively.
217
- - Supabase CLI 2.78.1 prints `sb_secret_` keys masked (middle dots) from
218
- `supabase projects api-keys`; 2.116.0 needs `--reveal`. A user who copies
219
- the key from the CLI rather than the dashboard gets "Invalid API key" at
220
- the editor step. Worth a hint in the "Finish by hand" text.
221
- - `listUsers` reads one page of 200 Auth users when offering an existing user
222
- to make the first editor. Fine for a fresh project, wrong for a populated one.
223
- - `supabase link` ran without `--project-ref`, so the CLI opened the
224
- project picker, which preselects the account's first project; `db push`
225
- follows the link, so one Enter would push Smoodly's schema into whichever
226
- project sorts first (seen 2026-09-05 during the hosted run, caught before
227
- the push). Fixed 2026-09-05: `projectRef` (src/env.ts) takes the ref from
228
- the typed URL's first host label and the link step and every fallback
229
- line pass it. Seen again 2026-09-05 from the published 0.0.3: a URL typed
230
- without `https://` gave no ref (the picker opened) and then failed
231
- `ensureEditor` with supabase-js's "Invalid supabaseUrl". Fixed the same
232
- day: `normalizeProjectUrl` (src/env.ts) adds the scheme and refuses
233
- non-URLs at the prompt and for `--url`; a URL with no ref (custom domain)
234
- never offers the link, and `run` throws rather than run a bare
235
- `supabase link`.
236
- - The `--yes` flag defaults to local mode; with the Supabase CLI absent that
237
- stops with the install message rather than falling through to hosted.
238
- - Captured `exec` output is stdout only; stderr streams straight through, so
239
- `git init` hints and the Supabase CLI's update notice appear between the
240
- prompt lines.
241
- - The "Local" option is still offered in the select when the Supabase CLI is
242
- absent, and only bails afterwards; the spec wants the check before the prompt.
package/dist/index.js CHANGED
@@ -2,8 +2,10 @@
2
2
 
3
3
  // src/index.ts
4
4
  import * as p2 from "@clack/prompts";
5
- import { readFile as readFile2, rm, writeFile as writeFile2 } from "node:fs/promises";
6
- import { fileURLToPath } from "node:url";
5
+ import { existsSync } from "node:fs";
6
+ import { readdir as readdir2, readFile as readFile2, rm, writeFile as writeFile2 } from "node:fs/promises";
7
+ import { join as join3, relative } from "node:path";
8
+ import { fileURLToPath, pathToFileURL } from "node:url";
7
9
 
8
10
  // src/args.ts
9
11
  import { parseArgs } from "node:util";
@@ -61,7 +63,10 @@ function parseFlags(argv) {
61
63
  }
62
64
  var HELP = `create-smoodly-app \u2014 a fresh Next.js site with Smoodly installed
63
65
 
64
- Usage: npx create-smoodly-app [name] [options]
66
+ Usage: npx create-smoodly-app [folder] [options]
67
+
68
+ folder: a name, a path or "." (the current empty folder). Its last part becomes
69
+ the package name: lowercase letters, digits, dots, dashes and underscores.
65
70
 
66
71
  Options:
67
72
  --supabase local|hosted Where content lives (asked when omitted; local needs the Supabase CLI)
@@ -115,7 +120,7 @@ function supabaseEditorClient(keys) {
115
120
 
116
121
  // src/exec.ts
117
122
  import { spawn } from "node:child_process";
118
- var exec = (cmd, args, opts) => new Promise((resolve2, reject) => {
123
+ var exec = (cmd, args, opts) => new Promise((resolve3, reject) => {
119
124
  const child = spawn(cmd, args, {
120
125
  cwd: opts.cwd,
121
126
  stdio: [opts.stdin ?? "inherit", opts.capture ? "pipe" : "inherit", "inherit"],
@@ -126,7 +131,7 @@ var exec = (cmd, args, opts) => new Promise((resolve2, reject) => {
126
131
  child.on("error", reject);
127
132
  child.on(
128
133
  "close",
129
- (code) => code === 0 ? resolve2({ stdout }) : reject(new Error(`\`${cmd} ${args.join(" ")}\` exited with code ${code}`))
134
+ (code) => code === 0 ? resolve3({ stdout }) : reject(new Error(`\`${cmd} ${args.join(" ")}\` exited with code ${code}`))
130
135
  );
131
136
  });
132
137
  async function hasCommand(cmd, run2, cwd) {
@@ -141,10 +146,10 @@ async function hasCommand(cmd, run2, cwd) {
141
146
  // src/ports-net.ts
142
147
  import { createServer } from "node:net";
143
148
  function canListen(port, host) {
144
- return new Promise((resolve2) => {
149
+ return new Promise((resolve3) => {
145
150
  const server = createServer();
146
- server.once("error", () => resolve2(false));
147
- server.listen(port, host, () => server.close(() => resolve2(true)));
151
+ server.once("error", () => resolve3(false));
152
+ server.listen(port, host, () => server.close(() => resolve3(true)));
148
153
  });
149
154
  }
150
155
  async function isPortFree(port) {
@@ -153,13 +158,15 @@ async function isPortFree(port) {
153
158
 
154
159
  // src/prompts.ts
155
160
  import * as p from "@clack/prompts";
156
- import { resolve } from "node:path";
161
+ import { resolve as resolve2 } from "node:path";
157
162
 
158
163
  // src/env.ts
159
164
  function renderEnv(keys) {
160
165
  return [
161
- "# Smoodly, self-host: the content project's values. SMOODLY_AUTH_URL and",
162
- "# SMOODLY_AUTH_KEY default to these two \u2014 leave them unset.",
166
+ "# Smoodly, self-host: the content project's values. The live site reads",
167
+ "# with SMOODLY_URL and SMOODLY_SECRET_KEY; the admin also signs in with",
168
+ "# SMOODLY_PUBLISHABLE_KEY. SMOODLY_AUTH_URL and SMOODLY_AUTH_KEY default",
169
+ "# to these \u2014 leave them unset.",
163
170
  `SMOODLY_URL=${keys.url}`,
164
171
  `SMOODLY_SECRET_KEY=${keys.secretKey}`,
165
172
  `SMOODLY_PUBLISHABLE_KEY=${keys.publishableKey}`,
@@ -196,10 +203,13 @@ function projectRef(url) {
196
203
 
197
204
  // src/scaffold.ts
198
205
  import { cp, readdir, readFile, rename, writeFile } from "node:fs/promises";
199
- import { join } from "node:path";
206
+ import { basename, join, resolve } from "node:path";
207
+ var nameFromDir = (dir) => basename(resolve(dir));
200
208
  function validName(name) {
201
209
  if (!name) return "The name is empty.";
202
- if (!/^[a-z0-9][a-z0-9._-]*$/.test(name)) return "Use lowercase letters, digits, dots, dashes and underscores (it becomes the package name).";
210
+ if (!/^[a-z0-9][a-z0-9._-]*$/.test(name)) {
211
+ return `The folder name "${name}" becomes the package name: use lowercase letters, digits, dots, dashes and underscores.`;
212
+ }
203
213
  return null;
204
214
  }
205
215
  async function isEmptyOrMissing(dir) {
@@ -244,6 +254,13 @@ async function scaffold(opts) {
244
254
  routingPath,
245
255
  routing.replace(ROUTING_LOCALES, ` locales: [${opts.locales.map((l) => JSON.stringify(l)).join(", ")}],`).replace(ROUTING_DEFAULT, ` defaultLocale: ${JSON.stringify(opts.locales[0])},`)
246
256
  );
257
+ const source = await readFile(join(opts.dir, "messages", "en.json"), "utf8").catch(() => null);
258
+ if (source !== null) {
259
+ for (const locale of opts.locales) {
260
+ const file = join(opts.dir, "messages", `${locale}.json`);
261
+ if (await readFile(file, "utf8").catch(() => null) === null) await writeFile(file, source);
262
+ }
263
+ }
247
264
  if (opts.starter !== "blank") {
248
265
  const rewrites = [
249
266
  { map: articlesSegments(opts.locales), from: '{ en: "posts", fi: "artikkelit" }', lines: [["smoodly/collections.ts", ARTICLES_PATH_LINE], ["components/pages/Articles.tsx", ARTICLES_SLUG_LINE]] },
@@ -252,10 +269,10 @@ async function scaffold(opts) {
252
269
  for (const { map, from, lines } of rewrites) {
253
270
  for (const [rel, line] of lines) {
254
271
  const path = join(opts.dir, rel);
255
- const source = await readFile(path, "utf8");
256
- if (!source.includes(line)) throw new Error(`${rel} in the template has drifted; expected the line
272
+ const source2 = await readFile(path, "utf8");
273
+ if (!source2.includes(line)) throw new Error(`${rel} in the template has drifted; expected the line
257
274
  ${line}`);
258
- await writeFile(path, source.replace(line, line.replace(from, map)));
275
+ await writeFile(path, source2.replace(line, line.replace(from, map)));
259
276
  }
260
277
  }
261
278
  }
@@ -278,10 +295,12 @@ async function answerOrThrow(value) {
278
295
  return v;
279
296
  }
280
297
  async function resolveAnswers(flags2, ctx, env = process.env) {
281
- const name = flags2.name ?? await answer(p.text({ message: "Project name", placeholder: "my-site", validate: (v) => validName(v ?? "") ?? void 0 }));
298
+ const folderName = (typed2) => nameFromDir(resolve2(ctx.cwd, typed2));
299
+ const typed = flags2.name ?? await answer(p.text({ message: "Project folder", placeholder: "my-site", validate: (v) => v ? validName(folderName(v)) ?? void 0 : "A folder name or path." }));
300
+ const dir = resolve2(ctx.cwd, typed);
301
+ const name = nameFromDir(dir);
282
302
  const invalid = validName(name);
283
303
  if (invalid) bail(invalid);
284
- const dir = resolve(ctx.cwd, name);
285
304
  if (!await isEmptyOrMissing(dir)) bail(`${dir} exists and is not empty.`);
286
305
  const locales = flags2.locales ?? (flags2.yes ? void 0 : await answer(p.confirm({ message: "Multilingual site? (next-intl, translated URLs)", initialValue: false })) ? await answer(
287
306
  p.text({
@@ -376,13 +395,13 @@ function plan(a) {
376
395
  if (a.supabase === "local" && !a.hasSupabaseCli) {
377
396
  throw new Error("Local mode needs the Supabase CLI: https://supabase.com/docs/guides/cli (brew install supabase/tap/supabase)");
378
397
  }
379
- const steps = ["copyTemplate", "install", "gitInit"];
398
+ const steps = ["copyTemplate", "install"];
380
399
  if (a.supabase === "local") steps.push("supabaseLocal");
381
400
  else if (a.hasSupabaseCli) {
382
401
  steps.push("supabaseInit");
383
402
  if (a.hosted?.linkAndPush) steps.push("linkAndPush");
384
403
  }
385
- steps.push("writeEnv", "ensureEditor", "seed", "finish");
404
+ steps.push("writeEnv", "ensureEditor", "seed", "gitInit", "finish");
386
405
  return steps;
387
406
  }
388
407
 
@@ -459,6 +478,8 @@ async function run(answers2, deps) {
459
478
  await sh("npm", ["install"]);
460
479
  break;
461
480
  case "gitInit":
481
+ await deps.agentFiles?.(cwd).catch(() => {
482
+ });
462
483
  await sh("git", ["init"], { capture: true });
463
484
  await sh("git", ["add", "-A"], { capture: true });
464
485
  await sh("git", ["commit", "-m", "Smoodly site"], { capture: true });
@@ -467,19 +488,22 @@ async function run(answers2, deps) {
467
488
  deps.log("Starting the local Supabase stack (first run pulls Docker images)\u2026");
468
489
  await sh("supabase", ["init"], { stdin: "ignore" });
469
490
  const configPath = join2(cwd, "supabase/config.toml");
470
- const toml = await deps.fs.readFile(configPath);
491
+ const toml = withoutSeedSql(await deps.fs.readFile(configPath));
471
492
  const base = await findFreeBlock(blockPorts(toml), deps.isPortFree);
493
+ await deps.fs.writeFile(configPath, base !== DEFAULT_BASE ? shiftBlock(toml, base) : toml);
472
494
  if (base !== DEFAULT_BASE) {
473
- await deps.fs.writeFile(configPath, shiftBlock(toml, base));
474
495
  deps.log(`Ports ${DEFAULT_BASE / 100}xx are busy (another Supabase project running?), using ${base / 100}xx.`);
475
496
  }
476
497
  await sh("supabase", LOCAL_START_ARGS);
477
498
  ctx.keys = parseStatusEnv((await sh("supabase", ["status", "-o", "env"], { capture: true })).stdout);
478
499
  break;
479
500
  }
480
- case "supabaseInit":
501
+ case "supabaseInit": {
481
502
  await sh("supabase", ["init"], { stdin: "ignore" });
503
+ const configPath = join2(cwd, "supabase/config.toml");
504
+ await deps.fs.writeFile(configPath, withoutSeedSql(await deps.fs.readFile(configPath)));
482
505
  break;
506
+ }
483
507
  case "linkAndPush":
484
508
  await sh("supabase", linkArgs(answers2));
485
509
  await sh("supabase", ["db", "push"]);
@@ -512,10 +536,11 @@ async function run(answers2, deps) {
512
536
  }
513
537
  return { ok: true, keys: ctx.keys, editor: ctx.editor };
514
538
  }
539
+ var withoutSeedSql = (toml) => toml.replace(/(\[db\.seed\]\n(?:#.*\n)*enabled = )true/, "$1false");
515
540
  function fallbackFor(step, answers2, keys) {
516
- const envLines = keys ? `Write ${answers2.name}/.env.local:
541
+ const envLines = keys ? `Write ${answers2.dir}/.env.local:
517
542
 
518
- ${renderEnv(keys).split("\n").filter((l) => !l.startsWith("#")).join("\n").trimEnd()}` : `Write ${answers2.name}/.env.local with SMOODLY_URL, SMOODLY_SECRET_KEY and SMOODLY_PUBLISHABLE_KEY (from \`supabase status\`).`;
543
+ ${renderEnv(keys).split("\n").filter((l) => !l.startsWith("#")).join("\n").trimEnd()}` : `Write ${answers2.dir}/.env.local with SMOODLY_URL, SMOODLY_SECRET_KEY and SMOODLY_PUBLISHABLE_KEY (from \`supabase status\`).`;
519
544
  const editorLines = [
520
545
  "Create the first editor: add a user in Supabase Studio (Authentication \u2192 Users), then in the SQL editor:",
521
546
  "",
@@ -523,13 +548,13 @@ ${renderEnv(keys).split("\n").filter((l) => !l.startsWith("#")).join("\n").trimE
523
548
  ].join("\n");
524
549
  switch (step) {
525
550
  case "supabaseLocal":
526
- return [`cd ${answers2.name} && supabase init && supabase ${LOCAL_START_ARGS.join(" ")}`, "", envLines, "", editorLines].join("\n");
551
+ return [`cd ${answers2.dir} && supabase init && supabase ${LOCAL_START_ARGS.join(" ")}`, "", envLines, "", editorLines].join("\n");
527
552
  case "supabaseInit": {
528
- const commands = answers2.hosted?.linkAndPush ? [`cd ${answers2.name} && supabase init && ${linkCommand(answers2)} && supabase db push`] : [`cd ${answers2.name} && supabase init`, "", `When you are ready: ${linkCommand(answers2)} && supabase db push`];
553
+ const commands = answers2.hosted?.linkAndPush ? [`cd ${answers2.dir} && supabase init && ${linkCommand(answers2)} && supabase db push`] : [`cd ${answers2.dir} && supabase init`, "", `When you are ready: ${linkCommand(answers2)} && supabase db push`];
529
554
  return [...commands, "", envLines, "", editorLines].join("\n");
530
555
  }
531
556
  case "linkAndPush":
532
- return [`cd ${answers2.name} && ${linkCommand(answers2)} && supabase db push`, "", envLines, "", editorLines].join("\n");
557
+ return [`cd ${answers2.dir} && ${linkCommand(answers2)} && supabase db push`, "", envLines, "", editorLines].join("\n");
533
558
  case "writeEnv":
534
559
  return [envLines, "", editorLines].join("\n");
535
560
  case "ensureEditor":
@@ -539,7 +564,7 @@ ${renderEnv(keys).split("\n").filter((l) => !l.startsWith("#")).join("\n").trimE
539
564
  editorLines
540
565
  ].join("\n");
541
566
  case "seed":
542
- return `cd ${answers2.name} && npm run seed`;
567
+ return `cd ${answers2.dir} && npm run seed`;
543
568
  default:
544
569
  return "";
545
570
  }
@@ -580,6 +605,7 @@ var outcome;
580
605
  try {
581
606
  const hasSupabaseCli = await hasCommand("supabase", exec, process.cwd());
582
607
  answers = await resolveAnswers(flags, { hasSupabaseCli, cwd: process.cwd() });
608
+ const existed = existsSync(answers.dir);
583
609
  outcome = await run(answers, {
584
610
  exec,
585
611
  templateDirs: TEMPLATE_DIRS,
@@ -592,7 +618,14 @@ try {
592
618
  scaffold,
593
619
  readFile: (path) => readFile2(path, "utf8"),
594
620
  writeFile: (path, text2) => writeFile2(path, text2),
595
- rm: (dir) => rm(dir, { recursive: true, force: true })
621
+ rm: async (dir) => {
622
+ if (!existed) return rm(dir, { recursive: true, force: true });
623
+ for (const name of await readdir2(dir)) await rm(join3(dir, name), { recursive: true, force: true });
624
+ }
625
+ },
626
+ agentFiles: async (dir) => {
627
+ const gen = await import(pathToFileURL(join3(dir, "node_modules/next/dist/server/lib/generate-agent-files.js")).href);
628
+ (gen.writeAgentFiles ?? gen.default?.writeAgentFiles)(dir);
596
629
  }
597
630
  });
598
631
  } catch (e) {
@@ -606,7 +639,7 @@ if (!outcome.ok) {
606
639
  }
607
640
  p2.note(
608
641
  [
609
- `cd ${answers.name}`,
642
+ ...relative(process.cwd(), answers.dir) ? [`cd ${relative(process.cwd(), answers.dir)}`] : [],
610
643
  "npm run dev",
611
644
  "",
612
645
  answers.starter === "blank" ? "Site: http://localhost:3000 (a blank site \u2014 build it in components/ and at /admin)" : "Site: http://localhost:3000 (the Lorem Co. starter \u2014 edit it at /admin)",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "create-smoodly-app",
3
- "version": "0.0.36",
3
+ "version": "0.0.38",
4
4
  "description": "Create a Smoodly site: a fresh Next.js app with the admin mounted, a Supabase schema, an env file and a first editor.",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -13,14 +13,16 @@ where things are.
13
13
  is the one registry. A component that is not registered does not exist
14
14
  for the admin.
15
15
  - **After a new collection or a `smoodly` upgrade, run `npx smoodly migrate`**
16
- and apply what it writes (`supabase db push` hosted, `supabase db reset`
16
+ and apply what it writes (`supabase db push` hosted, `supabase migration up`
17
17
  local). It copies the core schema files the package ships and writes a
18
18
  read view and order index per collection. Never edit a file it wrote.
19
19
  The admin reads the base tables, so it lists a collection either way;
20
20
  the view is for SQL and PostgREST readers, the index for `order`.
21
- - **Seed through the stores.** `scripts/seed.ts` is starter content,
22
- written through the page and entry stores, never raw SQL. It is
23
- idempotent; run it with `npm run seed`.
21
+ - **Content from code goes through `content()`.** `scripts/seed.ts` is
22
+ starter content written with `content()` from `smoodly/server.ts`:
23
+ the admin's own operations, so validation and publish checks apply and
24
+ a failure throws. Never write rows or call the stores directly. The
25
+ seed is idempotent; run it with `npm run seed`.
24
26
  - **Never import the admin into the site.** Nothing under `app/(site)`,
25
27
  `app/[locale]` or `components/`, and nothing they import
26
28
  (`smoodly/server.ts` included), may import `@/smoodly/admin` or
@@ -50,6 +52,6 @@ where things are.
50
52
  ## Commands
51
53
 
52
54
  npm run dev # http://localhost:3000, /admin is the editor
53
- npm run build # next build
55
+ npm run build # checks the schema files (smoodly migrate --check), then next build
54
56
  npm run seed # starter content, idempotent
55
57
  npm run typecheck # tsc --noEmit
@@ -15,8 +15,8 @@ for an agent (or a person) building the site.
15
15
  - `components/sections/Text.tsx` — the one section: a title and a richtext body. Listed in `smoodly/sections.ts`.
16
16
  - `components/elements/Button.tsx` — the one element: a link styled as a button, embedded in richtext through `smoodly/toolsets.ts`.
17
17
  - `components/LocaleNav.tsx` — the language links every page view renders from its `locales` map: the same page in the other language, or its home when there is no translation; nothing with one locale.
18
- - `smoodly/` — the wiring: `config.ts` (the registry and settings), `collections.ts` (posts), `globals.ts` (the site item), `sections.ts`, `elements.ts`, `toolsets.ts`, `server.ts` (stores, site layer, renderer, the read API `q`), `ops.ts` (the admin's one server action), `admin.tsx` (the admin mount), `richtext.tsx` (the bound renderer).
19
- - `scripts/seed.ts` — the home page and the site item, written through the stores. `npm run seed` is idempotent.
18
+ - `smoodly/` — the wiring: `config.ts` (the registry and settings), `collections.ts` (posts), `globals.ts` (the site item), `sections.ts`, `elements.ts`, `toolsets.ts`, `server.ts` (site layer, renderer, the read API `q`, the content API `content()`), `ops.ts` (the admin's one server action), `admin.tsx` (the admin mount), `richtext.tsx` (the bound renderer).
19
+ - `scripts/seed.ts` — the home page and the site item, written through the content API (`content()`). `npm run seed` is idempotent.
20
20
  - `app/globals.css` — a reset, a container, the two tones. Replace it.
21
21
  - `.env.local` — `SMOODLY_URL`, `SMOODLY_SECRET_KEY`, `SMOODLY_PUBLISHABLE_KEY`.
22
22
 
@@ -43,7 +43,7 @@ collection to `collections`, and write its entry page by copying
43
43
  Then write its read view and order index and apply them:
44
44
 
45
45
  npx smoodly migrate
46
- supabase db push # hosted; `supabase db reset` for the local stack
46
+ supabase db push # hosted; `supabase migration up` for the local stack (keeps the content)
47
47
 
48
48
  The same command brings the core schema forward after a `smoodly` upgrade.
49
49
 
@@ -3,3 +3,4 @@ node_modules/
3
3
  .env.local
4
4
  next-env.d.ts
5
5
  *.tsbuildinfo
6
+ smoodly-export-*
@@ -4,7 +4,7 @@
4
4
  "type": "module",
5
5
  "scripts": {
6
6
  "dev": "next dev",
7
- "build": "next build",
7
+ "build": "smoodly migrate --check && next build",
8
8
  "start": "next start",
9
9
  "seed": "tsx scripts/seed.ts",
10
10
  "typecheck": "tsc --noEmit"
@@ -14,7 +14,7 @@
14
14
  "next": "^16.3.4",
15
15
  "react": "^19",
16
16
  "react-dom": "^19",
17
- "smoodly": "0.0.36"
17
+ "smoodly": "0.0.38"
18
18
  },
19
19
  "devDependencies": {
20
20
  "@next/env": "^16.3.4",