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 +4 -1
- package/dist/index.js +57 -3
- package/package.json +11 -4
- package/templates/next/package.json +1 -1
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
|
|
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
|
-
|
|
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.
|
|
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": {
|
|
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",
|