create-smoodly-app 0.0.2 → 0.0.4

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
@@ -14,9 +14,16 @@ sign in. Spec: `docs/superpowers/specs/2026-09-04-create-smoodly-app-design.md`.
14
14
  another project holds the default `543xx` ports the new one moves to the
15
15
  next free block, `553xx` and so on, and says so) or a
16
16
  **hosted project** (URL, publishable key, secret key; then an offer to
17
- `supabase link` + `supabase db push`).
17
+ `supabase link --project-ref <ref>` + `supabase db push`, the ref taken
18
+ from the URL so the CLI never opens its project picker). The URL may be
19
+ pasted without `https://`; anything that is not an http(s) URL is refused
20
+ before any step runs. A custom domain has no ref, so the link is not
21
+ offered and the two commands are printed instead.
18
22
  3. The first editor: pick an existing Auth user or create one (email +
19
23
  password). The `editors` row is written with role `owner`.
24
+ If `@` does not appear when you type it (macOS, `@` on Option+2 with the
25
+ terminal's "Option as Meta" setting on), paste the address or pass
26
+ `--editor-email`.
20
27
 
21
28
  Every prompt has a flag, so CI can run it: `--supabase local|hosted`, `--url`,
22
29
  `--publishable-key`, `--secret-key`, `--editor-email` (password from
@@ -40,6 +47,18 @@ The CLI has no runtime dependency on `smoodly`.
40
47
  npm test # unit tests (template build, planner, runner, editor)
41
48
  npm run build # template + esbuild bundle → dist/index.js
42
49
 
50
+ ## Follow-ups (logged 2026-09-05)
51
+
52
+ - Text prompts drop `@` when the terminal sends Option as Meta (macOS,
53
+ Finnish/Nordic layouts put `@` on Option+2): the terminal emits `ESC 2` and
54
+ Node's readline discards meta-keys, so nothing reaches the prompt.
55
+ Reproduced against `@clack/prompts` 1.7.0 with a scripted stdin. Not
56
+ fixable from prompt options; documented under "What it asks" with the
57
+ paste / `--editor-email` workaround. Revisit if clack exposes a key hook.
58
+ - Fresh installs printed a `NOTICE ... does not exist, skipping` for every
59
+ `drop trigger/policy if exists` in the migration (fixed 2026-09-05: the
60
+ migration opens with `set client_min_messages = warning`).
61
+
43
62
  ## Follow-ups (logged 2026-09-04)
44
63
 
45
64
  - 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.
@@ -47,22 +66,38 @@ The CLI has no runtime dependency on `smoodly`.
47
66
  but does not quote arguments, so a project name or path containing a space
48
67
  or `&` breaks. Quote the args when the shell is on, or resolve the `.cmd`
49
68
  path and drop `shell`. Untested on Windows.
50
- - Hosted mode has not been run against a real hosted Supabase project with a
51
- new-format `sb_secret_` key; CI and the local smoke run both use legacy JWT
52
- keys. One manual run before the first customer install.
69
+ - Hosted mode, manual run against a real hosted project with new-format keys
70
+ (done 2026-09-05, throwaway project, `sb_secret_` + `sb_publishable_`):
71
+ `db push` of the template migration printed no NOTICE, the CLI listed and
72
+ created the first editor through the GoTrue admin API with the secret key
73
+ as the sole `apikey`, the `editors` row landed with role `owner`, `next
74
+ build` passed, `/admin` served the shell, and a password sign-in with the
75
+ publishable key returned a session. Caveat: the link step ran by hand with
76
+ an explicit `--project-ref` (see the next-but-one item); the CLI's own
77
+ `supabase link` was not driven interactively.
78
+ - Supabase CLI 2.78.1 prints `sb_secret_` keys masked (middle dots) from
79
+ `supabase projects api-keys`; 2.116.0 needs `--reveal`. A user who copies
80
+ the key from the CLI rather than the dashboard gets "Invalid API key" at
81
+ the editor step. Worth a hint in the "Finish by hand" text.
53
82
  - `listUsers` reads one page of 200 Auth users when offering an existing user
54
83
  to make the first editor. Fine for a fresh project, wrong for a populated one.
55
- - `supabase link` runs without `--project-ref`, so the CLI re-asks for the
56
- project even though it is derivable from the URL already typed.
84
+ - `supabase link` ran without `--project-ref`, so the CLI opened the
85
+ project picker, which preselects the account's first project; `db push`
86
+ follows the link, so one Enter would push Smoodly's schema into whichever
87
+ project sorts first (seen 2026-09-05 during the hosted run, caught before
88
+ the push). Fixed 2026-09-05: `projectRef` (src/env.ts) takes the ref from
89
+ the typed URL's first host label and the link step and every fallback
90
+ line pass it. Seen again 2026-09-05 from the published 0.0.3: a URL typed
91
+ without `https://` gave no ref (the picker opened) and then failed
92
+ `ensureEditor` with supabase-js's "Invalid supabaseUrl". Fixed the same
93
+ day: `normalizeProjectUrl` (src/env.ts) adds the scheme and refuses
94
+ non-URLs at the prompt and for `--url`; a URL with no ref (custom domain)
95
+ never offers the link, and `run` throws rather than run a bare
96
+ `supabase link`.
57
97
  - The `--yes` flag defaults to local mode; with the Supabase CLI absent that
58
98
  stops with the install message rather than falling through to hosted.
59
99
  - Captured `exec` output is stdout only; stderr streams straight through, so
60
100
  `git init` hints and the Supabase CLI's update notice appear between the
61
101
  prompt lines.
62
- - CI never runs local mode, and never applies the template's migration to a
63
- fresh database: the e2e uses hosted mode against a stack whose schema came
64
- from the repo's own migration. Add a local-mode e2e step — the runner's stack
65
- uses ports 643xx and a fresh `supabase init` defaults to 543xx, so both can
66
- run at once.
67
102
  - The "Local" option is still offered in the select when the Supabase CLI is
68
103
  absent, and only bails afterwards; the spec wants the check before the prompt.
package/dist/index.js CHANGED
@@ -135,6 +135,41 @@ async function isPortFree(port) {
135
135
  import * as p from "@clack/prompts";
136
136
  import { resolve } from "node:path";
137
137
 
138
+ // src/env.ts
139
+ function renderEnv(keys) {
140
+ return [
141
+ "# Smoodly, self-host: the content project's values. SMOODLY_AUTH_URL and",
142
+ "# SMOODLY_AUTH_KEY default to these two \u2014 leave them unset.",
143
+ `SMOODLY_URL=${keys.url}`,
144
+ `SMOODLY_SECRET_KEY=${keys.secretKey}`,
145
+ `SMOODLY_PUBLISHABLE_KEY=${keys.publishableKey}`,
146
+ ""
147
+ ].join("\n");
148
+ }
149
+ function normalizeProjectUrl(input) {
150
+ const raw = input.trim();
151
+ if (!raw) return null;
152
+ const withScheme = /^[a-z][a-z0-9+.-]*:\/\//i.test(raw) ? raw : `https://${raw}`;
153
+ try {
154
+ const url = new URL(withScheme);
155
+ if (url.protocol !== "http:" && url.protocol !== "https:") return null;
156
+ if (!url.hostname) return null;
157
+ return url.origin;
158
+ } catch {
159
+ return null;
160
+ }
161
+ }
162
+ function projectRef(url) {
163
+ let host;
164
+ try {
165
+ host = new URL(url).hostname;
166
+ } catch {
167
+ return null;
168
+ }
169
+ const m = /^([a-z0-9]{20})\.supabase\.(co|in|red)$/.exec(host);
170
+ return m ? m[1] : null;
171
+ }
172
+
138
173
  // src/scaffold.ts
139
174
  import { cp, readdir, readFile, rename, writeFile } from "node:fs/promises";
140
175
  import { join } from "node:path";
@@ -197,10 +232,23 @@ async function resolveAnswers(flags2, ctx) {
197
232
  if (!ctx.hasSupabaseCli) bail("Local mode needs the Supabase CLI: https://supabase.com/docs/guides/cli (brew install supabase/tap/supabase)");
198
233
  return { name, dir, supabase, hasSupabaseCli: true, smoodlyDep: flags2.smoodlyDep };
199
234
  }
200
- const url = flags2.url ?? await answer(p.text({ message: "Supabase project URL", placeholder: "https://abcd.supabase.co" }));
235
+ const badUrl = "Supabase project URL must be an http(s) URL, like https://abcd.supabase.co";
236
+ const typedUrl = flags2.url ?? await answer(
237
+ p.text({
238
+ message: "Supabase project URL",
239
+ placeholder: "https://abcd.supabase.co",
240
+ validate: (v) => normalizeProjectUrl(v ?? "") ? void 0 : badUrl
241
+ })
242
+ );
243
+ const url = normalizeProjectUrl(typedUrl);
244
+ if (!url) throw new Error(`${badUrl} (got "${typedUrl}")`);
201
245
  const publishableKey = flags2.publishableKey ?? await answer(p.text({ message: "Publishable (anon) key" }));
202
246
  const secretKey = flags2.secretKey ?? await answer(p.password({ message: "Secret (service role) key" }));
203
- const linkAndPush = !ctx.hasSupabaseCli ? false : flags2.skipLink ? false : flags2.yes ? true : await answer(p.confirm({ message: "Link this app to the project and push the migration now? (supabase link, supabase db push)" }));
247
+ const ref = projectRef(url);
248
+ if (ctx.hasSupabaseCli && !flags2.skipLink && !ref) {
249
+ p.log.warn(`No project ref in ${url}; skipping supabase link. Run \`supabase link --project-ref <project-ref>\` and \`supabase db push\` yourself.`);
250
+ }
251
+ const linkAndPush = !ctx.hasSupabaseCli ? false : flags2.skipLink || !ref ? false : flags2.yes ? true : await answer(p.confirm({ message: `Link this app to the project and push the migration now? (supabase link --project-ref ${ref}, supabase db push)` }));
204
252
  return { name, dir, supabase, hasSupabaseCli: ctx.hasSupabaseCli, hosted: { keys: { url, publishableKey, secretKey }, linkAndPush }, smoodlyDep: flags2.smoodlyDep };
205
253
  }
206
254
  function askEditorWith(flags2, env = process.env) {
@@ -233,18 +281,6 @@ function askEditorWith(flags2, env = process.env) {
233
281
  // src/run.ts
234
282
  import { join as join2 } from "node:path";
235
283
 
236
- // src/env.ts
237
- function renderEnv(keys) {
238
- return [
239
- "# Smoodly, self-host: the content project's values. SMOODLY_AUTH_URL and",
240
- "# SMOODLY_AUTH_KEY default to these two \u2014 leave them unset.",
241
- `SMOODLY_URL=${keys.url}`,
242
- `SMOODLY_SECRET_KEY=${keys.secretKey}`,
243
- `SMOODLY_PUBLISHABLE_KEY=${keys.publishableKey}`,
244
- ""
245
- ].join("\n");
246
- }
247
-
248
284
  // src/plan.ts
249
285
  function plan(a) {
250
286
  if (a.supabase === "local" && !a.hasSupabaseCli) {
@@ -347,7 +383,7 @@ async function run(answers2, deps) {
347
383
  await sh("supabase", ["init"], { stdin: "ignore" });
348
384
  break;
349
385
  case "linkAndPush":
350
- await sh("supabase", ["link"]);
386
+ await sh("supabase", linkArgs(answers2));
351
387
  await sh("supabase", ["db", "push"]);
352
388
  break;
353
389
  case "writeEnv":
@@ -387,11 +423,11 @@ ${renderEnv(keys).split("\n").filter((l) => !l.startsWith("#")).join("\n").trimE
387
423
  case "supabaseLocal":
388
424
  return [`cd ${answers2.name} && supabase init && supabase start`, "", envLines, "", editorLines].join("\n");
389
425
  case "supabaseInit": {
390
- const commands = answers2.hosted?.linkAndPush ? [`cd ${answers2.name} && supabase init && supabase link && supabase db push`] : [`cd ${answers2.name} && supabase init`, "", "When you are ready: supabase link && supabase db push"];
426
+ 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`];
391
427
  return [...commands, "", envLines, "", editorLines].join("\n");
392
428
  }
393
429
  case "linkAndPush":
394
- return [`cd ${answers2.name} && supabase link && supabase db push`, "", envLines, "", editorLines].join("\n");
430
+ return [`cd ${answers2.name} && ${linkCommand(answers2)} && supabase db push`, "", envLines, "", editorLines].join("\n");
395
431
  case "writeEnv":
396
432
  return [envLines, "", editorLines].join("\n");
397
433
  case "ensureEditor":
@@ -404,6 +440,16 @@ ${renderEnv(keys).split("\n").filter((l) => !l.startsWith("#")).join("\n").trimE
404
440
  return "";
405
441
  }
406
442
  }
443
+ function linkArgs(answers2) {
444
+ const url = answers2.hosted?.keys.url ?? "";
445
+ const ref = projectRef(url);
446
+ if (!ref) throw new Error(`no project ref in ${url}; run \`supabase link --project-ref <project-ref>\` yourself`);
447
+ return ["link", "--project-ref", ref];
448
+ }
449
+ function linkCommand(answers2) {
450
+ const ref = answers2.hosted ? projectRef(answers2.hosted.keys.url) : null;
451
+ return `supabase link --project-ref ${ref ?? "<project-ref>"}`;
452
+ }
407
453
 
408
454
  // src/index.ts
409
455
  var TEMPLATE_DIR = fileURLToPath(new URL("../templates/next/", import.meta.url));
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "create-smoodly-app",
3
- "version": "0.0.2",
3
+ "version": "0.0.4",
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,7 +13,7 @@
13
13
  "react": "^19",
14
14
  "react-dom": "^19",
15
15
  "@supabase/supabase-js": "^2.113.0",
16
- "smoodly": "0.0.2"
16
+ "smoodly": "0.0.4"
17
17
  },
18
18
  "devDependencies": {
19
19
  "@types/node": "^26.4.1",
@@ -1,3 +1,5 @@
1
+ set client_min_messages = warning;
2
+
1
3
  -- The space a request acts in: the key's claim in Smoodly Cloud, the
2
4
  -- default space in self-host (service role, anon key). Policies, the
3
5
  -- insert trigger and the reorder RPC all read this ONE function.