create-smoodly-app 0.0.1 → 0.0.2

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,7 +10,9 @@ 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) or a
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
17
  `supabase link` + `supabase db push`).
16
18
  3. The first editor: pick an existing Auth user or create one (email +
@@ -40,6 +42,7 @@ The CLI has no runtime dependency on `smoodly`.
40
42
 
41
43
  ## Follow-ups (logged 2026-09-04)
42
44
 
45
+ - 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
46
  - Windows: `exec` passes `shell: true` on win32 (needed for the `.cmd` shims)
44
47
  but does not quote arguments, so a project name or path containing a space
45
48
  or `&` breaks. Quote the args when the shell is on, or resolve the `.cmd`
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";
@@ -247,6 +260,32 @@ function plan(a) {
247
260
  return steps;
248
261
  }
249
262
 
263
+ // src/ports.ts
264
+ var DEFAULT_BASE = 54300;
265
+ var BASES = Array.from({ length: 10 }, (_, i) => DEFAULT_BASE + i * 1e3);
266
+ var PORT_LINE = /^([ \t]*#?[ \t]*[a-z0-9_]*port[ \t]*=[ \t]*)543(\d\d)([ \t]*(?:#.*)?)$/gm;
267
+ var LIVE_PORT_LINE = /^[ \t]*[a-z0-9_]*port[ \t]*=[ \t]*(543\d\d)[ \t]*(?:#.*)?$/gm;
268
+ function blockPorts(toml) {
269
+ return [...toml.matchAll(LIVE_PORT_LINE)].map((m) => Number(m[1]));
270
+ }
271
+ function shiftBlock(toml, base) {
272
+ return toml.replace(PORT_LINE, (_m, pre, xx, post) => `${pre}${base + Number(xx)}${post}`);
273
+ }
274
+ async function findFreeBlock(ports, isFree, bases = BASES) {
275
+ for (const base of bases) {
276
+ const shifted = ports.map((p3) => base + (p3 - DEFAULT_BASE));
277
+ let allFree = true;
278
+ for (const port of shifted) {
279
+ if (!await isFree(port)) {
280
+ allFree = false;
281
+ break;
282
+ }
283
+ }
284
+ if (allFree) return base;
285
+ }
286
+ throw new Error(`No free Supabase port block: tried ${bases.join(", ")}. Stop another local Supabase project and run again.`);
287
+ }
288
+
250
289
  // src/supabase-cli.ts
251
290
  function parseStatusEnv(output) {
252
291
  const vars = /* @__PURE__ */ new Map();
@@ -290,12 +329,20 @@ async function run(answers2, deps) {
290
329
  await sh("git", ["add", "-A"], { capture: true });
291
330
  await sh("git", ["commit", "-m", "Smoodly site"], { capture: true });
292
331
  break;
293
- case "supabaseLocal":
332
+ case "supabaseLocal": {
294
333
  deps.log("Starting the local Supabase stack (first run pulls Docker images)\u2026");
295
334
  await sh("supabase", ["init"], { stdin: "ignore" });
335
+ const configPath = join2(cwd, "supabase/config.toml");
336
+ const toml = await deps.fs.readFile(configPath);
337
+ const base = await findFreeBlock(blockPorts(toml), deps.isPortFree);
338
+ if (base !== DEFAULT_BASE) {
339
+ await deps.fs.writeFile(configPath, shiftBlock(toml, base));
340
+ deps.log(`Ports ${DEFAULT_BASE / 100}xx are busy (another Supabase project running?), using ${base / 100}xx.`);
341
+ }
296
342
  await sh("supabase", ["start"]);
297
343
  ctx.keys = parseStatusEnv((await sh("supabase", ["status", "-o", "env"], { capture: true })).stdout);
298
344
  break;
345
+ }
299
346
  case "supabaseInit":
300
347
  await sh("supabase", ["init"], { stdin: "ignore" });
301
348
  break;
@@ -384,7 +431,14 @@ try {
384
431
  editorClient: supabaseEditorClient,
385
432
  askEditor: askEditorWith(flags),
386
433
  log: (line) => p2.log.step(line),
387
- fs: { isEmptyOrMissing, scaffold, writeFile: (path, text2) => writeFile2(path, text2), rm: (dir) => rm(dir, { recursive: true, force: true }) }
434
+ isPortFree,
435
+ fs: {
436
+ isEmptyOrMissing,
437
+ scaffold,
438
+ readFile: (path) => readFile2(path, "utf8"),
439
+ writeFile: (path, text2) => writeFile2(path, text2),
440
+ rm: (dir) => rm(dir, { recursive: true, force: true })
441
+ }
388
442
  });
389
443
  } catch (e) {
390
444
  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.1",
3
+ "version": "0.0.2",
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": { "create-smoodly-app": "./dist/index.js" },
13
- "files": ["dist", "templates"],
14
- "engines": { "node": ">=20" },
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",
@@ -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.1"
16
+ "smoodly": "0.0.2"
17
17
  },
18
18
  "devDependencies": {
19
19
  "@types/node": "^26.4.1",