create-kide-app 0.1.6 → 0.1.8
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/index.js +319 -59
- package/package.json +1 -1
package/index.js
CHANGED
|
@@ -1,8 +1,15 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
3
|
import * as p from "@clack/prompts";
|
|
4
|
-
import { execSync, spawn } from "node:child_process";
|
|
5
|
-
import {
|
|
4
|
+
import { execFileSync, execSync, spawn } from "node:child_process";
|
|
5
|
+
import {
|
|
6
|
+
cpSync,
|
|
7
|
+
existsSync,
|
|
8
|
+
mkdirSync,
|
|
9
|
+
readFileSync,
|
|
10
|
+
rmSync,
|
|
11
|
+
writeFileSync,
|
|
12
|
+
} from "node:fs";
|
|
6
13
|
import path from "node:path";
|
|
7
14
|
|
|
8
15
|
// Async spawn wrapper so long-running commands don't block clack spinners
|
|
@@ -26,14 +33,63 @@ const runAsync = (cmd, cwd) =>
|
|
|
26
33
|
|
|
27
34
|
// --- Package manager detection ---
|
|
28
35
|
|
|
29
|
-
const pm = {
|
|
36
|
+
const pm = {
|
|
37
|
+
name: "pnpm",
|
|
38
|
+
exec: "pnpm exec",
|
|
39
|
+
dlx: "pnpm dlx",
|
|
40
|
+
run: "pnpm",
|
|
41
|
+
install: "pnpm install",
|
|
42
|
+
};
|
|
30
43
|
|
|
31
44
|
// --- Template repo ---
|
|
32
45
|
|
|
33
46
|
const REPO = "https://github.com/mhernesniemi/kide-cms.git";
|
|
34
47
|
|
|
35
48
|
// Files from the kide-cms repo that shouldn't leak into scaffolded projects.
|
|
36
|
-
|
|
49
|
+
// NOTE: `.claude/settings.local.json` is removed but `.claude/skills/` is kept,
|
|
50
|
+
// so scaffolds ship the /migrate skill alongside AGENTS.md.
|
|
51
|
+
const CLEANUP = [
|
|
52
|
+
"docs",
|
|
53
|
+
"CLAUDE.md",
|
|
54
|
+
".claude/settings.local.json",
|
|
55
|
+
"data",
|
|
56
|
+
".cms-data",
|
|
57
|
+
"dist",
|
|
58
|
+
".astro",
|
|
59
|
+
".DS_Store",
|
|
60
|
+
];
|
|
61
|
+
|
|
62
|
+
// The project name reaches shell commands (git, wrangler) and path.resolve, so it is
|
|
63
|
+
// validated before either. Restricting it to one path segment of safe characters keeps
|
|
64
|
+
// `$(...)`, backticks, `;` and separators out of those commands and out of the target
|
|
65
|
+
// path. Must start alphanumeric so "." and ".." can never be the whole name.
|
|
66
|
+
const PROJECT_NAME_PATTERN = /^[a-zA-Z0-9][a-zA-Z0-9._-]*$/;
|
|
67
|
+
|
|
68
|
+
const validateProjectName = (value) => {
|
|
69
|
+
if (!PROJECT_NAME_PATTERN.test(value)) {
|
|
70
|
+
return "Project name must start with a letter or number and contain only letters, numbers, dots, dashes and underscores.";
|
|
71
|
+
}
|
|
72
|
+
return undefined;
|
|
73
|
+
};
|
|
74
|
+
|
|
75
|
+
// Resolve the latest release tag (v-prefixed semver) so scaffolds pin to a
|
|
76
|
+
// deliberate release instead of whatever HEAD happens to be. Returns null when
|
|
77
|
+
// the repo has no tags (falls back to the default branch).
|
|
78
|
+
const resolveLatestTag = () => {
|
|
79
|
+
try {
|
|
80
|
+
const output = execSync(
|
|
81
|
+
`git ls-remote --tags --sort=-v:refname ${REPO} "v*"`,
|
|
82
|
+
{ stdio: "pipe" },
|
|
83
|
+
).toString();
|
|
84
|
+
for (const line of output.split("\n")) {
|
|
85
|
+
const match = line.match(/refs\/tags\/(v[0-9][^^\s]*)$/);
|
|
86
|
+
if (match) return match[1];
|
|
87
|
+
}
|
|
88
|
+
} catch {
|
|
89
|
+
// Network/git hiccup — fall back to default branch
|
|
90
|
+
}
|
|
91
|
+
return null;
|
|
92
|
+
};
|
|
37
93
|
|
|
38
94
|
// --- Main ---
|
|
39
95
|
|
|
@@ -48,6 +104,7 @@ async function main() {
|
|
|
48
104
|
placeholder: "my-cms-app",
|
|
49
105
|
validate: (value) => {
|
|
50
106
|
if (!value) return "Project name is required";
|
|
107
|
+
return validateProjectName(value);
|
|
51
108
|
},
|
|
52
109
|
}));
|
|
53
110
|
|
|
@@ -56,6 +113,14 @@ async function main() {
|
|
|
56
113
|
process.exit(0);
|
|
57
114
|
}
|
|
58
115
|
|
|
116
|
+
// Re-check: the interactive path validates above, but a name from argv skips it,
|
|
117
|
+
// and this value reaches shell commands and path.resolve below.
|
|
118
|
+
const nameError = validateProjectName(projectName);
|
|
119
|
+
if (nameError) {
|
|
120
|
+
p.cancel(nameError);
|
|
121
|
+
process.exit(1);
|
|
122
|
+
}
|
|
123
|
+
|
|
59
124
|
const projectDir = path.resolve(process.cwd(), projectName);
|
|
60
125
|
if (existsSync(projectDir)) {
|
|
61
126
|
p.cancel(`Directory "${projectName}" already exists.`);
|
|
@@ -97,8 +162,26 @@ async function main() {
|
|
|
97
162
|
|
|
98
163
|
s.start(`Scaffolding project (using ${pm.name})`);
|
|
99
164
|
|
|
165
|
+
const templateRef = resolveLatestTag();
|
|
166
|
+
let templateCommit = null;
|
|
167
|
+
|
|
100
168
|
try {
|
|
101
|
-
|
|
169
|
+
const branchArgs = templateRef ? ["--branch", templateRef] : [];
|
|
170
|
+
// execFileSync, not execSync: no shell, so projectDir is passed as one argv entry
|
|
171
|
+
// and can never be reinterpreted as a command regardless of what it contains.
|
|
172
|
+
execFileSync("git", ["clone", "--depth", "1", ...branchArgs, REPO, projectDir], {
|
|
173
|
+
stdio: "pipe",
|
|
174
|
+
});
|
|
175
|
+
try {
|
|
176
|
+
templateCommit = execSync("git rev-parse HEAD", {
|
|
177
|
+
cwd: projectDir,
|
|
178
|
+
stdio: "pipe",
|
|
179
|
+
})
|
|
180
|
+
.toString()
|
|
181
|
+
.trim();
|
|
182
|
+
} catch {
|
|
183
|
+
// best-effort — stamp without a commit hash
|
|
184
|
+
}
|
|
102
185
|
rmSync(path.join(projectDir, ".git"), { recursive: true, force: true });
|
|
103
186
|
} catch {
|
|
104
187
|
s.stop("Failed to download template.");
|
|
@@ -111,7 +194,11 @@ async function main() {
|
|
|
111
194
|
rmSync(path.join(projectDir, f), { recursive: true, force: true });
|
|
112
195
|
}
|
|
113
196
|
|
|
114
|
-
s.stop(
|
|
197
|
+
s.stop(
|
|
198
|
+
templateRef
|
|
199
|
+
? `Project scaffolded from ${templateRef}`
|
|
200
|
+
: "Project scaffolded",
|
|
201
|
+
);
|
|
115
202
|
|
|
116
203
|
// --- Apply target-specific files ---
|
|
117
204
|
|
|
@@ -121,13 +208,32 @@ async function main() {
|
|
|
121
208
|
const targetDir = path.join(adaptersDir, target);
|
|
122
209
|
|
|
123
210
|
if (target === "cloudflare") {
|
|
124
|
-
cpSync(
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
211
|
+
cpSync(
|
|
212
|
+
path.join(targetDir, "astro.config.mjs"),
|
|
213
|
+
path.join(projectDir, "astro.config.mjs"),
|
|
214
|
+
);
|
|
215
|
+
cpSync(
|
|
216
|
+
path.join(targetDir, "src/cms/adapters/db.ts"),
|
|
217
|
+
path.join(projectDir, "src/cms/adapters/db.ts"),
|
|
218
|
+
);
|
|
219
|
+
cpSync(
|
|
220
|
+
path.join(targetDir, "drizzle.config.ts"),
|
|
221
|
+
path.join(projectDir, "drizzle.config.ts"),
|
|
222
|
+
);
|
|
223
|
+
cpSync(
|
|
224
|
+
path.join(targetDir, "src/cms/adapters/storage.ts"),
|
|
225
|
+
path.join(projectDir, "src/cms/adapters/storage.ts"),
|
|
226
|
+
);
|
|
227
|
+
cpSync(
|
|
228
|
+
path.join(targetDir, "src/cms/adapters/cf-env.ts"),
|
|
229
|
+
path.join(projectDir, "src/cms/adapters/cf-env.ts"),
|
|
230
|
+
);
|
|
128
231
|
const uploadsRouteDir = path.join(projectDir, "src/pages/uploads");
|
|
129
232
|
mkdirSync(uploadsRouteDir, { recursive: true });
|
|
130
|
-
cpSync(
|
|
233
|
+
cpSync(
|
|
234
|
+
path.join(targetDir, "src/pages/uploads/[...path].ts"),
|
|
235
|
+
path.join(uploadsRouteDir, "[...path].ts"),
|
|
236
|
+
);
|
|
131
237
|
}
|
|
132
238
|
|
|
133
239
|
const pkgPath = path.join(projectDir, "package.json");
|
|
@@ -142,27 +248,101 @@ async function main() {
|
|
|
142
248
|
// Move better-sqlite3 to devDependencies — drizzle-kit needs it to push schema to local D1
|
|
143
249
|
if (pkg.dependencies["better-sqlite3"]) {
|
|
144
250
|
if (!pkg.devDependencies) pkg.devDependencies = {};
|
|
145
|
-
pkg.devDependencies["better-sqlite3"] =
|
|
251
|
+
pkg.devDependencies["better-sqlite3"] =
|
|
252
|
+
pkg.dependencies["better-sqlite3"];
|
|
146
253
|
delete pkg.dependencies["better-sqlite3"];
|
|
147
254
|
}
|
|
148
255
|
delete pkg.dependencies["sharp"];
|
|
149
256
|
|
|
150
|
-
let wranglerContent = readFileSync(
|
|
151
|
-
|
|
257
|
+
let wranglerContent = readFileSync(
|
|
258
|
+
path.join(targetDir, "wrangler.toml"),
|
|
259
|
+
"utf-8",
|
|
260
|
+
);
|
|
261
|
+
wranglerContent = wranglerContent.replaceAll(
|
|
262
|
+
"{{PROJECT_NAME}}",
|
|
263
|
+
projectName,
|
|
264
|
+
);
|
|
265
|
+
// Seed a placeholder database_id so `pnpm dev` works out of the box —
|
|
266
|
+
// miniflare requires a non-empty id even for the local D1. It's overwritten
|
|
267
|
+
// with the real id if a D1 is created below; otherwise paste the real id
|
|
268
|
+
// before deploying (`wrangler deploy` rejects an id it doesn't own).
|
|
269
|
+
wranglerContent = wranglerContent.replace(
|
|
270
|
+
/database_id = ""[^\n]*/,
|
|
271
|
+
`database_id = "${crypto.randomUUID()}" # local placeholder - replace with the id from \`wrangler d1 create ${projectName}-db\` before deploying`,
|
|
272
|
+
);
|
|
152
273
|
writeFileSync(path.join(projectDir, "wrangler.toml"), wranglerContent);
|
|
153
274
|
|
|
154
275
|
pkg.devDependencies.wrangler = "^4.0.0";
|
|
155
276
|
|
|
156
277
|
pkg.scripts.dev = "astro dev";
|
|
157
278
|
pkg.scripts.build = "astro build";
|
|
158
|
-
pkg.scripts.preview =
|
|
159
|
-
|
|
279
|
+
pkg.scripts.preview =
|
|
280
|
+
"astro build && wrangler dev --config dist/server/wrangler.json";
|
|
281
|
+
pkg.scripts.deploy =
|
|
282
|
+
"astro build && wrangler deploy --config dist/server/wrangler.json";
|
|
283
|
+
|
|
284
|
+
// @cloudflare/vite-plugin statically imports module.registerHooks (Node
|
|
285
|
+
// >=22.15), and Node >=23 changes the native ABI (breaking the prebuilt
|
|
286
|
+
// better-sqlite3 used for local D1). So pin Node 22.18–22.x and guard `dev`
|
|
287
|
+
// with a friendly check instead of a cryptic ESM "registerHooks" crash.
|
|
288
|
+
pkg.engines = { ...(pkg.engines ?? {}), node: ">=22.18.0" };
|
|
289
|
+
pkg.scripts.predev =
|
|
290
|
+
`node -e "const v=process.versions.node.split('.').map(Number);` +
|
|
291
|
+
`if(v[0]<22||(v[0]===22&&v[1]<18)||v[0]>=23){` +
|
|
292
|
+
`console.error('\\n[kide] Cloudflare dev needs Node 22.18+ (22.x). You have '+process.version+'.\\n` +
|
|
293
|
+
` Run: nvm install 22 && nvm use (or use fnm/volta)\\n');` +
|
|
294
|
+
`process.exit(1)}"`;
|
|
295
|
+
writeFileSync(path.join(projectDir, ".nvmrc"), "22\n");
|
|
160
296
|
}
|
|
161
297
|
|
|
162
298
|
writeFileSync(pkgPath, `${JSON.stringify(pkg, null, 2)}\n`);
|
|
163
299
|
|
|
164
300
|
rmSync(adaptersDir, { recursive: true, force: true });
|
|
165
301
|
|
|
302
|
+
// Stamp the scaffold provenance. This file is the project's record of which
|
|
303
|
+
// template release it came from — used to diff against upstream and to check
|
|
304
|
+
// whether published security advisories apply to this project.
|
|
305
|
+
let cliVersion = null;
|
|
306
|
+
try {
|
|
307
|
+
cliVersion = JSON.parse(
|
|
308
|
+
readFileSync(new URL("./package.json", import.meta.url), "utf-8"),
|
|
309
|
+
).version;
|
|
310
|
+
} catch {
|
|
311
|
+
// best-effort
|
|
312
|
+
}
|
|
313
|
+
const versionStamp = {
|
|
314
|
+
template: REPO.replace(/\.git$/, ""),
|
|
315
|
+
kideVersion: pkg.version ?? null,
|
|
316
|
+
ref: templateRef ?? "HEAD",
|
|
317
|
+
commit: templateCommit,
|
|
318
|
+
target,
|
|
319
|
+
corePath: "src/cms",
|
|
320
|
+
scaffoldedAt: new Date().toISOString(),
|
|
321
|
+
createKideApp: cliVersion,
|
|
322
|
+
};
|
|
323
|
+
writeFileSync(
|
|
324
|
+
path.join(projectDir, ".kide-version"),
|
|
325
|
+
`${JSON.stringify(versionStamp, null, 2)}\n`,
|
|
326
|
+
);
|
|
327
|
+
|
|
328
|
+
// Wire up the local MCP server so Claude Code (and other MCP clients that read
|
|
329
|
+
// a project-scoped `.mcp.json`) discover it automatically — no manual
|
|
330
|
+
// `claude mcp add` needed. The command runs from the project root, where the
|
|
331
|
+
// `cms:mcp` script lives, so no `cwd` override is required.
|
|
332
|
+
const mcpConfig = {
|
|
333
|
+
mcpServers: {
|
|
334
|
+
kide: {
|
|
335
|
+
type: "stdio",
|
|
336
|
+
command: "pnpm",
|
|
337
|
+
args: ["cms:mcp"],
|
|
338
|
+
},
|
|
339
|
+
},
|
|
340
|
+
};
|
|
341
|
+
writeFileSync(
|
|
342
|
+
path.join(projectDir, ".mcp.json"),
|
|
343
|
+
`${JSON.stringify(mcpConfig, null, 2)}\n`,
|
|
344
|
+
);
|
|
345
|
+
|
|
166
346
|
s.stop("Configuration applied");
|
|
167
347
|
|
|
168
348
|
// --- Install dependencies ---
|
|
@@ -179,10 +359,13 @@ async function main() {
|
|
|
179
359
|
|
|
180
360
|
let gitInitialized = false;
|
|
181
361
|
try {
|
|
182
|
-
execSync(
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
362
|
+
execSync(
|
|
363
|
+
"git init -q && git add . && git commit -q -m 'Initial commit from create-kide-app'",
|
|
364
|
+
{
|
|
365
|
+
cwd: projectDir,
|
|
366
|
+
stdio: "pipe",
|
|
367
|
+
},
|
|
368
|
+
);
|
|
186
369
|
gitInitialized = true;
|
|
187
370
|
} catch {
|
|
188
371
|
// git not available — silently skip
|
|
@@ -209,7 +392,9 @@ async function main() {
|
|
|
209
392
|
// Get the GitHub username so we can check repo availability
|
|
210
393
|
let ghUser = "";
|
|
211
394
|
try {
|
|
212
|
-
ghUser = execSync("gh api user --jq .login", { stdio: "pipe" })
|
|
395
|
+
ghUser = execSync("gh api user --jq .login", { stdio: "pipe" })
|
|
396
|
+
.toString()
|
|
397
|
+
.trim();
|
|
213
398
|
} catch {
|
|
214
399
|
// ignore
|
|
215
400
|
}
|
|
@@ -222,7 +407,8 @@ async function main() {
|
|
|
222
407
|
initialValue: projectName,
|
|
223
408
|
validate: (value) => {
|
|
224
409
|
if (!value) return "Repository name is required";
|
|
225
|
-
if (!/^[a-zA-Z0-9._-]+$/.test(value))
|
|
410
|
+
if (!/^[a-zA-Z0-9._-]+$/.test(value))
|
|
411
|
+
return "Only letters, numbers, dots, hyphens, and underscores";
|
|
226
412
|
},
|
|
227
413
|
});
|
|
228
414
|
if (p.isCancel(input)) break;
|
|
@@ -230,7 +416,10 @@ async function main() {
|
|
|
230
416
|
if (ghUser) {
|
|
231
417
|
try {
|
|
232
418
|
execSync(`gh repo view ${ghUser}/${input}`, { stdio: "pipe" });
|
|
233
|
-
p.note(
|
|
419
|
+
p.note(
|
|
420
|
+
`A repository named "${input}" already exists. Pick a different name.`,
|
|
421
|
+
"Name taken",
|
|
422
|
+
);
|
|
234
423
|
continue;
|
|
235
424
|
} catch {
|
|
236
425
|
// Repo doesn't exist — name is free
|
|
@@ -251,14 +440,20 @@ async function main() {
|
|
|
251
440
|
if (!p.isCancel(visibility)) {
|
|
252
441
|
s.start("Creating GitHub repository");
|
|
253
442
|
try {
|
|
254
|
-
execSync(`gh repo create ${repoName} ${visibility}`, {
|
|
255
|
-
|
|
256
|
-
// Use SSH for the remote (works with the user's existing SSH keys;
|
|
257
|
-
// avoids HTTPS credential prompts when gh's git_protocol defaults to https).
|
|
258
|
-
execSync(`git remote add origin git@github.com:${ghUser}/${repoName}.git`, {
|
|
443
|
+
execSync(`gh repo create ${repoName} ${visibility}`, {
|
|
259
444
|
cwd: projectDir,
|
|
260
445
|
stdio: "pipe",
|
|
261
446
|
});
|
|
447
|
+
|
|
448
|
+
// Use SSH for the remote (works with the user's existing SSH keys;
|
|
449
|
+
// avoids HTTPS credential prompts when gh's git_protocol defaults to https).
|
|
450
|
+
execSync(
|
|
451
|
+
`git remote add origin git@github.com:${ghUser}/${repoName}.git`,
|
|
452
|
+
{
|
|
453
|
+
cwd: projectDir,
|
|
454
|
+
stdio: "pipe",
|
|
455
|
+
},
|
|
456
|
+
);
|
|
262
457
|
execSync("git branch -M main && git push -u origin main", {
|
|
263
458
|
cwd: projectDir,
|
|
264
459
|
stdio: "pipe",
|
|
@@ -297,7 +492,11 @@ async function main() {
|
|
|
297
492
|
stdio: "pipe",
|
|
298
493
|
}).toString();
|
|
299
494
|
pushOk = !out.includes("Error:") && out.includes("Changes applied");
|
|
300
|
-
s.stop(
|
|
495
|
+
s.stop(
|
|
496
|
+
pushOk
|
|
497
|
+
? "Schema pushed"
|
|
498
|
+
: "Schema push failed — run `pnpm exec drizzle-kit push` manually",
|
|
499
|
+
);
|
|
301
500
|
} catch {
|
|
302
501
|
s.stop("Schema will be set up on first dev start");
|
|
303
502
|
}
|
|
@@ -316,7 +515,7 @@ async function main() {
|
|
|
316
515
|
"Seeding for Cloudflare requires a D1 database.",
|
|
317
516
|
"",
|
|
318
517
|
` ${pm.dlx} wrangler d1 create ${projectName}-db`,
|
|
319
|
-
" #
|
|
518
|
+
" # then replace the placeholder database_id in wrangler.toml",
|
|
320
519
|
` ${pm.dlx} wrangler d1 migrations apply ${projectName}-db --local`,
|
|
321
520
|
` ${pm.run} cms:seed`,
|
|
322
521
|
].join("\n"),
|
|
@@ -326,10 +525,17 @@ async function main() {
|
|
|
326
525
|
|
|
327
526
|
// --- Cloudflare resource setup ---
|
|
328
527
|
|
|
329
|
-
const cf = {
|
|
528
|
+
const cf = {
|
|
529
|
+
d1Created: false,
|
|
530
|
+
r2Created: false,
|
|
531
|
+
migrationsApplied: false,
|
|
532
|
+
deployed: false,
|
|
533
|
+
url: null,
|
|
534
|
+
};
|
|
330
535
|
if (target === "cloudflare") {
|
|
331
536
|
const setupNow = await p.confirm({
|
|
332
|
-
message:
|
|
537
|
+
message:
|
|
538
|
+
"Set up Cloudflare resources now? (creates D1 database and R2 bucket)",
|
|
333
539
|
initialValue: true,
|
|
334
540
|
});
|
|
335
541
|
|
|
@@ -337,14 +543,26 @@ async function main() {
|
|
|
337
543
|
// Check wrangler authentication
|
|
338
544
|
let authenticated = false;
|
|
339
545
|
try {
|
|
340
|
-
execSync(`${pm.exec} wrangler whoami`, {
|
|
546
|
+
execSync(`${pm.exec} wrangler whoami`, {
|
|
547
|
+
cwd: projectDir,
|
|
548
|
+
stdio: "pipe",
|
|
549
|
+
});
|
|
341
550
|
authenticated = true;
|
|
342
551
|
} catch {
|
|
343
|
-
p.note(
|
|
344
|
-
|
|
552
|
+
p.note(
|
|
553
|
+
"You need to log in to Cloudflare first.",
|
|
554
|
+
"Wrangler login required",
|
|
555
|
+
);
|
|
556
|
+
const doLogin = await p.confirm({
|
|
557
|
+
message: "Open browser to log in?",
|
|
558
|
+
initialValue: true,
|
|
559
|
+
});
|
|
345
560
|
if (!p.isCancel(doLogin) && doLogin) {
|
|
346
561
|
try {
|
|
347
|
-
execSync(`${pm.exec} wrangler login`, {
|
|
562
|
+
execSync(`${pm.exec} wrangler login`, {
|
|
563
|
+
cwd: projectDir,
|
|
564
|
+
stdio: "inherit",
|
|
565
|
+
});
|
|
348
566
|
authenticated = true;
|
|
349
567
|
} catch {
|
|
350
568
|
s.stop("Login failed");
|
|
@@ -357,22 +575,38 @@ async function main() {
|
|
|
357
575
|
let databaseId = null;
|
|
358
576
|
s.start("Creating D1 database");
|
|
359
577
|
try {
|
|
360
|
-
const output = execSync(
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
578
|
+
const output = execSync(
|
|
579
|
+
`${pm.exec} wrangler d1 create ${projectName}-db`,
|
|
580
|
+
{
|
|
581
|
+
cwd: projectDir,
|
|
582
|
+
stdio: "pipe",
|
|
583
|
+
},
|
|
584
|
+
).toString();
|
|
364
585
|
const match = output.match(/database_id\s*=\s*"([^"]+)"/);
|
|
365
|
-
if (match)
|
|
366
|
-
|
|
367
|
-
|
|
586
|
+
if (match) {
|
|
587
|
+
databaseId = match[1];
|
|
588
|
+
cf.d1Created = true;
|
|
589
|
+
s.stop("D1 database created");
|
|
590
|
+
} else {
|
|
591
|
+
// Created but the id couldn't be parsed from wrangler's output —
|
|
592
|
+
// don't mark as done, so the summary tells the user to wire it up.
|
|
593
|
+
s.stop(
|
|
594
|
+
"D1 database created, but its id could not be read — copy the database_id to wrangler.toml manually",
|
|
595
|
+
);
|
|
596
|
+
}
|
|
368
597
|
} catch (err) {
|
|
369
598
|
// Already exists — look it up
|
|
370
599
|
try {
|
|
371
|
-
const listOutput = execSync(`${pm.exec} wrangler d1 list`, {
|
|
600
|
+
const listOutput = execSync(`${pm.exec} wrangler d1 list`, {
|
|
601
|
+
cwd: projectDir,
|
|
602
|
+
stdio: "pipe",
|
|
603
|
+
}).toString();
|
|
372
604
|
const lines = listOutput.split("\n");
|
|
373
605
|
const dbLine = lines.find((l) => l.includes(`${projectName}-db`));
|
|
374
606
|
if (dbLine) {
|
|
375
|
-
const idMatch = dbLine.match(
|
|
607
|
+
const idMatch = dbLine.match(
|
|
608
|
+
/[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}/,
|
|
609
|
+
);
|
|
376
610
|
if (idMatch) databaseId = idMatch[0];
|
|
377
611
|
}
|
|
378
612
|
if (databaseId) {
|
|
@@ -391,14 +625,20 @@ async function main() {
|
|
|
391
625
|
if (databaseId) {
|
|
392
626
|
const wranglerPath = path.join(projectDir, "wrangler.toml");
|
|
393
627
|
let wranglerContent = readFileSync(wranglerPath, "utf-8");
|
|
394
|
-
wranglerContent = wranglerContent.replace(
|
|
628
|
+
wranglerContent = wranglerContent.replace(
|
|
629
|
+
/database_id = "[^"]*"[^\n]*/,
|
|
630
|
+
`database_id = "${databaseId}"`,
|
|
631
|
+
);
|
|
395
632
|
writeFileSync(wranglerPath, wranglerContent);
|
|
396
633
|
}
|
|
397
634
|
|
|
398
635
|
// Create R2 bucket
|
|
399
636
|
s.start("Creating R2 bucket");
|
|
400
637
|
try {
|
|
401
|
-
execSync(
|
|
638
|
+
execSync(
|
|
639
|
+
`${pm.exec} wrangler r2 bucket create ${projectName}-assets`,
|
|
640
|
+
{ cwd: projectDir, stdio: "pipe" },
|
|
641
|
+
);
|
|
402
642
|
cf.r2Created = true;
|
|
403
643
|
s.stop("R2 bucket created");
|
|
404
644
|
} catch {
|
|
@@ -410,7 +650,10 @@ async function main() {
|
|
|
410
650
|
if (databaseId) {
|
|
411
651
|
s.start("Generating database migrations");
|
|
412
652
|
try {
|
|
413
|
-
execSync(`${pm.exec} drizzle-kit generate`, {
|
|
653
|
+
execSync(`${pm.exec} drizzle-kit generate`, {
|
|
654
|
+
cwd: projectDir,
|
|
655
|
+
stdio: "pipe",
|
|
656
|
+
});
|
|
414
657
|
s.stop("Migrations generated");
|
|
415
658
|
} catch (err) {
|
|
416
659
|
s.stop("Migration generation failed");
|
|
@@ -420,15 +663,20 @@ async function main() {
|
|
|
420
663
|
|
|
421
664
|
s.start("Applying migrations to remote D1");
|
|
422
665
|
try {
|
|
423
|
-
execSync(
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
666
|
+
execSync(
|
|
667
|
+
`${pm.exec} wrangler d1 migrations apply ${projectName}-db --remote`,
|
|
668
|
+
{
|
|
669
|
+
cwd: projectDir,
|
|
670
|
+
stdio: "pipe",
|
|
671
|
+
input: "y\n",
|
|
672
|
+
},
|
|
673
|
+
);
|
|
428
674
|
cf.migrationsApplied = true;
|
|
429
675
|
s.stop("Migrations applied");
|
|
430
676
|
} catch {
|
|
431
|
-
s.stop(
|
|
677
|
+
s.stop(
|
|
678
|
+
"Migration apply failed — run manually with: wrangler d1 migrations apply --remote",
|
|
679
|
+
);
|
|
432
680
|
}
|
|
433
681
|
}
|
|
434
682
|
|
|
@@ -441,8 +689,13 @@ async function main() {
|
|
|
441
689
|
if (!p.isCancel(doDeploy) && doDeploy) {
|
|
442
690
|
s.start("Building and deploying to Cloudflare");
|
|
443
691
|
try {
|
|
444
|
-
const deployOutput = await runAsync(
|
|
445
|
-
|
|
692
|
+
const deployOutput = await runAsync(
|
|
693
|
+
`${pm.run} run deploy`,
|
|
694
|
+
projectDir,
|
|
695
|
+
);
|
|
696
|
+
const urlMatch = deployOutput.match(
|
|
697
|
+
/https:\/\/[^\s]+\.workers\.dev/,
|
|
698
|
+
);
|
|
446
699
|
if (urlMatch) cf.url = urlMatch[0];
|
|
447
700
|
cf.deployed = true;
|
|
448
701
|
s.stop("Deployed to Cloudflare");
|
|
@@ -489,13 +742,20 @@ async function main() {
|
|
|
489
742
|
const lines = [`cd ${projectName}`];
|
|
490
743
|
const remaining = [];
|
|
491
744
|
if (!cf.d1Created) {
|
|
492
|
-
remaining.push(
|
|
745
|
+
remaining.push(
|
|
746
|
+
` ${pm.dlx} wrangler d1 create ${projectName}-db`,
|
|
747
|
+
" # then replace the placeholder database_id in wrangler.toml",
|
|
748
|
+
);
|
|
493
749
|
}
|
|
494
750
|
if (!cf.r2Created) {
|
|
495
|
-
remaining.push(
|
|
751
|
+
remaining.push(
|
|
752
|
+
` ${pm.dlx} wrangler r2 bucket create ${projectName}-assets`,
|
|
753
|
+
);
|
|
496
754
|
}
|
|
497
755
|
if (!cf.migrationsApplied) {
|
|
498
|
-
remaining.push(
|
|
756
|
+
remaining.push(
|
|
757
|
+
` ${pm.dlx} wrangler d1 migrations apply ${projectName}-db --remote`,
|
|
758
|
+
);
|
|
499
759
|
}
|
|
500
760
|
if (!cf.deployed) {
|
|
501
761
|
remaining.push(` ${pm.run} run deploy`);
|