create-smoodly-app 0.0.1 → 0.0.3
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
|
@@ -10,11 +10,17 @@ sign in. Spec: `docs/superpowers/specs/2026-09-04-create-smoodly-app-design.md`.
|
|
|
10
10
|
|
|
11
11
|
1. Project name (also the positional argument).
|
|
12
12
|
2. Where content lives: a **local Supabase stack** (`supabase init` +
|
|
13
|
-
`supabase start` inside the app; needs the Supabase CLI and Docker
|
|
13
|
+
`supabase start` inside the app; needs the Supabase CLI and Docker; when
|
|
14
|
+
another project holds the default `543xx` ports the new one moves to the
|
|
15
|
+
next free block, `553xx` and so on, and says so) or a
|
|
14
16
|
**hosted project** (URL, publishable key, secret key; then an offer to
|
|
15
|
-
`supabase link
|
|
17
|
+
`supabase link --project-ref <ref>` + `supabase db push`, the ref taken
|
|
18
|
+
from the URL so the CLI never opens its project picker).
|
|
16
19
|
3. The first editor: pick an existing Auth user or create one (email +
|
|
17
20
|
password). The `editors` row is written with role `owner`.
|
|
21
|
+
If `@` does not appear when you type it (macOS, `@` on Option+2 with the
|
|
22
|
+
terminal's "Option as Meta" setting on), paste the address or pass
|
|
23
|
+
`--editor-email`.
|
|
18
24
|
|
|
19
25
|
Every prompt has a flag, so CI can run it: `--supabase local|hosted`, `--url`,
|
|
20
26
|
`--publishable-key`, `--secret-key`, `--editor-email` (password from
|
|
@@ -38,28 +44,51 @@ The CLI has no runtime dependency on `smoodly`.
|
|
|
38
44
|
npm test # unit tests (template build, planner, runner, editor)
|
|
39
45
|
npm run build # template + esbuild bundle → dist/index.js
|
|
40
46
|
|
|
47
|
+
## Follow-ups (logged 2026-09-05)
|
|
48
|
+
|
|
49
|
+
- Text prompts drop `@` when the terminal sends Option as Meta (macOS,
|
|
50
|
+
Finnish/Nordic layouts put `@` on Option+2): the terminal emits `ESC 2` and
|
|
51
|
+
Node's readline discards meta-keys, so nothing reaches the prompt.
|
|
52
|
+
Reproduced against `@clack/prompts` 1.7.0 with a scripted stdin. Not
|
|
53
|
+
fixable from prompt options; documented under "What it asks" with the
|
|
54
|
+
paste / `--editor-email` workaround. Revisit if clack exposes a key hook.
|
|
55
|
+
- Fresh installs printed a `NOTICE ... does not exist, skipping` for every
|
|
56
|
+
`drop trigger/policy if exists` in the migration (fixed 2026-09-05: the
|
|
57
|
+
migration opens with `set client_min_messages = warning`).
|
|
58
|
+
|
|
41
59
|
## Follow-ups (logged 2026-09-04)
|
|
42
60
|
|
|
61
|
+
- 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.
|
|
43
62
|
- Windows: `exec` passes `shell: true` on win32 (needed for the `.cmd` shims)
|
|
44
63
|
but does not quote arguments, so a project name or path containing a space
|
|
45
64
|
or `&` breaks. Quote the args when the shell is on, or resolve the `.cmd`
|
|
46
65
|
path and drop `shell`. Untested on Windows.
|
|
47
|
-
- Hosted mode
|
|
48
|
-
|
|
49
|
-
|
|
66
|
+
- Hosted mode, manual run against a real hosted project with new-format keys
|
|
67
|
+
(done 2026-09-05, throwaway project, `sb_secret_` + `sb_publishable_`):
|
|
68
|
+
`db push` of the template migration printed no NOTICE, the CLI listed and
|
|
69
|
+
created the first editor through the GoTrue admin API with the secret key
|
|
70
|
+
as the sole `apikey`, the `editors` row landed with role `owner`, `next
|
|
71
|
+
build` passed, `/admin` served the shell, and a password sign-in with the
|
|
72
|
+
publishable key returned a session. Caveat: the link step ran by hand with
|
|
73
|
+
an explicit `--project-ref` (see the next-but-one item); the CLI's own
|
|
74
|
+
`supabase link` was not driven interactively.
|
|
75
|
+
- Supabase CLI 2.78.1 prints `sb_secret_` keys masked (middle dots) from
|
|
76
|
+
`supabase projects api-keys`; 2.116.0 needs `--reveal`. A user who copies
|
|
77
|
+
the key from the CLI rather than the dashboard gets "Invalid API key" at
|
|
78
|
+
the editor step. Worth a hint in the "Finish by hand" text.
|
|
50
79
|
- `listUsers` reads one page of 200 Auth users when offering an existing user
|
|
51
80
|
to make the first editor. Fine for a fresh project, wrong for a populated one.
|
|
52
|
-
- `supabase link`
|
|
53
|
-
project
|
|
81
|
+
- `supabase link` ran without `--project-ref`, so the CLI opened the
|
|
82
|
+
project picker, which preselects the account's first project; `db push`
|
|
83
|
+
follows the link, so one Enter would push Smoodly's schema into whichever
|
|
84
|
+
project sorts first (seen 2026-09-05 during the hosted run, caught before
|
|
85
|
+
the push). Fixed 2026-09-05: `projectRef` (src/env.ts) takes the ref from
|
|
86
|
+
the typed URL's first host label and the link step and every fallback
|
|
87
|
+
line pass it; a custom domain yields no ref and falls back to the picker.
|
|
54
88
|
- The `--yes` flag defaults to local mode; with the Supabase CLI absent that
|
|
55
89
|
stops with the install message rather than falling through to hosted.
|
|
56
90
|
- Captured `exec` output is stdout only; stderr streams straight through, so
|
|
57
91
|
`git init` hints and the Supabase CLI's update notice appear between the
|
|
58
92
|
prompt lines.
|
|
59
|
-
- CI never runs local mode, and never applies the template's migration to a
|
|
60
|
-
fresh database: the e2e uses hosted mode against a stack whose schema came
|
|
61
|
-
from the repo's own migration. Add a local-mode e2e step — the runner's stack
|
|
62
|
-
uses ports 643xx and a fresh `supabase init` defaults to 543xx, so both can
|
|
63
|
-
run at once.
|
|
64
93
|
- The "Local" option is still offered in the select when the Supabase CLI is
|
|
65
94
|
absent, and only bails afterwards; the spec wants the check before the prompt.
|
package/dist/index.js
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
// src/index.ts
|
|
4
4
|
import * as p2 from "@clack/prompts";
|
|
5
|
-
import { rm, writeFile as writeFile2 } from "node:fs/promises";
|
|
5
|
+
import { readFile as readFile2, rm, writeFile as writeFile2 } from "node:fs/promises";
|
|
6
6
|
import { fileURLToPath } from "node:url";
|
|
7
7
|
|
|
8
8
|
// src/args.ts
|
|
@@ -118,6 +118,19 @@ async function hasCommand(cmd, run2, cwd) {
|
|
|
118
118
|
}
|
|
119
119
|
}
|
|
120
120
|
|
|
121
|
+
// src/ports-net.ts
|
|
122
|
+
import { createServer } from "node:net";
|
|
123
|
+
function canListen(port, host) {
|
|
124
|
+
return new Promise((resolve2) => {
|
|
125
|
+
const server = createServer();
|
|
126
|
+
server.once("error", () => resolve2(false));
|
|
127
|
+
server.listen(port, host, () => server.close(() => resolve2(true)));
|
|
128
|
+
});
|
|
129
|
+
}
|
|
130
|
+
async function isPortFree(port) {
|
|
131
|
+
return await canListen(port, "127.0.0.1") && await canListen(port, "0.0.0.0");
|
|
132
|
+
}
|
|
133
|
+
|
|
121
134
|
// src/prompts.ts
|
|
122
135
|
import * as p from "@clack/prompts";
|
|
123
136
|
import { resolve } from "node:path";
|
|
@@ -231,6 +244,16 @@ function renderEnv(keys) {
|
|
|
231
244
|
""
|
|
232
245
|
].join("\n");
|
|
233
246
|
}
|
|
247
|
+
function projectRef(url) {
|
|
248
|
+
let host;
|
|
249
|
+
try {
|
|
250
|
+
host = new URL(url).hostname;
|
|
251
|
+
} catch {
|
|
252
|
+
return null;
|
|
253
|
+
}
|
|
254
|
+
const m = /^([a-z0-9]{20})\.supabase\.(co|in|red)$/.exec(host);
|
|
255
|
+
return m ? m[1] : null;
|
|
256
|
+
}
|
|
234
257
|
|
|
235
258
|
// src/plan.ts
|
|
236
259
|
function plan(a) {
|
|
@@ -247,6 +270,32 @@ function plan(a) {
|
|
|
247
270
|
return steps;
|
|
248
271
|
}
|
|
249
272
|
|
|
273
|
+
// src/ports.ts
|
|
274
|
+
var DEFAULT_BASE = 54300;
|
|
275
|
+
var BASES = Array.from({ length: 10 }, (_, i) => DEFAULT_BASE + i * 1e3);
|
|
276
|
+
var PORT_LINE = /^([ \t]*#?[ \t]*[a-z0-9_]*port[ \t]*=[ \t]*)543(\d\d)([ \t]*(?:#.*)?)$/gm;
|
|
277
|
+
var LIVE_PORT_LINE = /^[ \t]*[a-z0-9_]*port[ \t]*=[ \t]*(543\d\d)[ \t]*(?:#.*)?$/gm;
|
|
278
|
+
function blockPorts(toml) {
|
|
279
|
+
return [...toml.matchAll(LIVE_PORT_LINE)].map((m) => Number(m[1]));
|
|
280
|
+
}
|
|
281
|
+
function shiftBlock(toml, base) {
|
|
282
|
+
return toml.replace(PORT_LINE, (_m, pre, xx, post) => `${pre}${base + Number(xx)}${post}`);
|
|
283
|
+
}
|
|
284
|
+
async function findFreeBlock(ports, isFree, bases = BASES) {
|
|
285
|
+
for (const base of bases) {
|
|
286
|
+
const shifted = ports.map((p3) => base + (p3 - DEFAULT_BASE));
|
|
287
|
+
let allFree = true;
|
|
288
|
+
for (const port of shifted) {
|
|
289
|
+
if (!await isFree(port)) {
|
|
290
|
+
allFree = false;
|
|
291
|
+
break;
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
if (allFree) return base;
|
|
295
|
+
}
|
|
296
|
+
throw new Error(`No free Supabase port block: tried ${bases.join(", ")}. Stop another local Supabase project and run again.`);
|
|
297
|
+
}
|
|
298
|
+
|
|
250
299
|
// src/supabase-cli.ts
|
|
251
300
|
function parseStatusEnv(output) {
|
|
252
301
|
const vars = /* @__PURE__ */ new Map();
|
|
@@ -290,17 +339,25 @@ async function run(answers2, deps) {
|
|
|
290
339
|
await sh("git", ["add", "-A"], { capture: true });
|
|
291
340
|
await sh("git", ["commit", "-m", "Smoodly site"], { capture: true });
|
|
292
341
|
break;
|
|
293
|
-
case "supabaseLocal":
|
|
342
|
+
case "supabaseLocal": {
|
|
294
343
|
deps.log("Starting the local Supabase stack (first run pulls Docker images)\u2026");
|
|
295
344
|
await sh("supabase", ["init"], { stdin: "ignore" });
|
|
345
|
+
const configPath = join2(cwd, "supabase/config.toml");
|
|
346
|
+
const toml = await deps.fs.readFile(configPath);
|
|
347
|
+
const base = await findFreeBlock(blockPorts(toml), deps.isPortFree);
|
|
348
|
+
if (base !== DEFAULT_BASE) {
|
|
349
|
+
await deps.fs.writeFile(configPath, shiftBlock(toml, base));
|
|
350
|
+
deps.log(`Ports ${DEFAULT_BASE / 100}xx are busy (another Supabase project running?), using ${base / 100}xx.`);
|
|
351
|
+
}
|
|
296
352
|
await sh("supabase", ["start"]);
|
|
297
353
|
ctx.keys = parseStatusEnv((await sh("supabase", ["status", "-o", "env"], { capture: true })).stdout);
|
|
298
354
|
break;
|
|
355
|
+
}
|
|
299
356
|
case "supabaseInit":
|
|
300
357
|
await sh("supabase", ["init"], { stdin: "ignore" });
|
|
301
358
|
break;
|
|
302
359
|
case "linkAndPush":
|
|
303
|
-
await sh("supabase",
|
|
360
|
+
await sh("supabase", linkArgs(answers2));
|
|
304
361
|
await sh("supabase", ["db", "push"]);
|
|
305
362
|
break;
|
|
306
363
|
case "writeEnv":
|
|
@@ -340,11 +397,11 @@ ${renderEnv(keys).split("\n").filter((l) => !l.startsWith("#")).join("\n").trimE
|
|
|
340
397
|
case "supabaseLocal":
|
|
341
398
|
return [`cd ${answers2.name} && supabase init && supabase start`, "", envLines, "", editorLines].join("\n");
|
|
342
399
|
case "supabaseInit": {
|
|
343
|
-
const commands = answers2.hosted?.linkAndPush ? [`cd ${answers2.name} && supabase init &&
|
|
400
|
+
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`];
|
|
344
401
|
return [...commands, "", envLines, "", editorLines].join("\n");
|
|
345
402
|
}
|
|
346
403
|
case "linkAndPush":
|
|
347
|
-
return [`cd ${answers2.name} &&
|
|
404
|
+
return [`cd ${answers2.name} && ${linkCommand(answers2)} && supabase db push`, "", envLines, "", editorLines].join("\n");
|
|
348
405
|
case "writeEnv":
|
|
349
406
|
return [envLines, "", editorLines].join("\n");
|
|
350
407
|
case "ensureEditor":
|
|
@@ -357,6 +414,13 @@ ${renderEnv(keys).split("\n").filter((l) => !l.startsWith("#")).join("\n").trimE
|
|
|
357
414
|
return "";
|
|
358
415
|
}
|
|
359
416
|
}
|
|
417
|
+
function linkArgs(answers2) {
|
|
418
|
+
const ref = answers2.hosted ? projectRef(answers2.hosted.keys.url) : null;
|
|
419
|
+
return ref ? ["link", "--project-ref", ref] : ["link"];
|
|
420
|
+
}
|
|
421
|
+
function linkCommand(answers2) {
|
|
422
|
+
return `supabase ${linkArgs(answers2).join(" ")}`;
|
|
423
|
+
}
|
|
360
424
|
|
|
361
425
|
// src/index.ts
|
|
362
426
|
var TEMPLATE_DIR = fileURLToPath(new URL("../templates/next/", import.meta.url));
|
|
@@ -384,7 +448,14 @@ try {
|
|
|
384
448
|
editorClient: supabaseEditorClient,
|
|
385
449
|
askEditor: askEditorWith(flags),
|
|
386
450
|
log: (line) => p2.log.step(line),
|
|
387
|
-
|
|
451
|
+
isPortFree,
|
|
452
|
+
fs: {
|
|
453
|
+
isEmptyOrMissing,
|
|
454
|
+
scaffold,
|
|
455
|
+
readFile: (path) => readFile2(path, "utf8"),
|
|
456
|
+
writeFile: (path, text2) => writeFile2(path, text2),
|
|
457
|
+
rm: (dir) => rm(dir, { recursive: true, force: true })
|
|
458
|
+
}
|
|
388
459
|
});
|
|
389
460
|
} catch (e) {
|
|
390
461
|
p2.cancel(e instanceof Error ? e.message : String(e));
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "create-smoodly-app",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.3",
|
|
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": {
|
|
@@ -9,9 +9,16 @@
|
|
|
9
9
|
"directory": "packages/create-smoodly-app"
|
|
10
10
|
},
|
|
11
11
|
"type": "module",
|
|
12
|
-
"bin": {
|
|
13
|
-
|
|
14
|
-
|
|
12
|
+
"bin": {
|
|
13
|
+
"create-smoodly-app": "./dist/index.js"
|
|
14
|
+
},
|
|
15
|
+
"files": [
|
|
16
|
+
"dist",
|
|
17
|
+
"templates"
|
|
18
|
+
],
|
|
19
|
+
"engines": {
|
|
20
|
+
"node": ">=20"
|
|
21
|
+
},
|
|
15
22
|
"scripts": {
|
|
16
23
|
"build": "npm run build:template && npm run build:cli",
|
|
17
24
|
"build:template": "tsx scripts/build-template.ts",
|