create-better-schemic 0.1.0-alpha.1
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/LICENSE +21 -0
- package/README.md +19 -0
- package/lib/index.js +1592 -0
- package/package.json +39 -0
- package/src/index.ts +357 -0
package/package.json
ADDED
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "create-better-schemic",
|
|
3
|
+
"version": "0.1.0-alpha.1",
|
|
4
|
+
"description": "Scaffold a new Better-schemic project — package.json, tsconfig, a driver, and the database/ schema, in one command.",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"author": "Vertio Solutions",
|
|
7
|
+
"homepage": "https://github.com/NONSTANDARDCODE/better-schemic",
|
|
8
|
+
"repository": {
|
|
9
|
+
"type": "git",
|
|
10
|
+
"url": "git+https://github.com/NONSTANDARDCODE/better-schemic.git",
|
|
11
|
+
"directory": "packages/create-better-schemic"
|
|
12
|
+
},
|
|
13
|
+
"type": "module",
|
|
14
|
+
"bin": {
|
|
15
|
+
"create-better-schemic": "lib/index.js"
|
|
16
|
+
},
|
|
17
|
+
"sideEffects": false,
|
|
18
|
+
"publishConfig": {
|
|
19
|
+
"access": "public"
|
|
20
|
+
},
|
|
21
|
+
"files": [
|
|
22
|
+
"lib",
|
|
23
|
+
"src",
|
|
24
|
+
"LICENSE"
|
|
25
|
+
],
|
|
26
|
+
"scripts": {
|
|
27
|
+
"build": "tsup",
|
|
28
|
+
"prepack": "bun run build",
|
|
29
|
+
"typecheck": "tsc --noEmit",
|
|
30
|
+
"lint": "biome check ."
|
|
31
|
+
},
|
|
32
|
+
"devDependencies": {
|
|
33
|
+
"@biomejs/biome": "^2.3.11",
|
|
34
|
+
"@clack/prompts": "^1.6.0",
|
|
35
|
+
"@types/bun": "latest",
|
|
36
|
+
"tsup": "^8",
|
|
37
|
+
"typescript": "^5"
|
|
38
|
+
}
|
|
39
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,357 @@
|
|
|
1
|
+
import { spawnSync } from "node:child_process";
|
|
2
|
+
import {
|
|
3
|
+
existsSync,
|
|
4
|
+
mkdirSync,
|
|
5
|
+
readdirSync,
|
|
6
|
+
readFileSync,
|
|
7
|
+
writeFileSync,
|
|
8
|
+
} from "node:fs";
|
|
9
|
+
import { basename, join, resolve } from "node:path";
|
|
10
|
+
import * as p from "@clack/prompts";
|
|
11
|
+
// The @better-schemic versions this scaffolder pins are its OWN version (the packages release lockstep), so a
|
|
12
|
+
// fresh project always gets a matching set. Inlined at build by tsup.
|
|
13
|
+
import { version as BETTER_SCHEMIC_VERSION } from "../package.json";
|
|
14
|
+
|
|
15
|
+
const RANGE = `^${BETTER_SCHEMIC_VERSION}`;
|
|
16
|
+
const bold = (s: string) => `\x1b[1m${s}\x1b[0m`;
|
|
17
|
+
const dim = (s: string) => `\x1b[2m${s}\x1b[0m`;
|
|
18
|
+
|
|
19
|
+
/** Supported drivers: the package + the runtime deps a project authoring/connecting with it needs. */
|
|
20
|
+
const DRIVERS: Record<
|
|
21
|
+
string,
|
|
22
|
+
{ label: string; pkg: string; deps: Record<string, string> }
|
|
23
|
+
> = {
|
|
24
|
+
surrealdb: {
|
|
25
|
+
label: "SurrealDB",
|
|
26
|
+
pkg: "@better-schemic/surrealdb",
|
|
27
|
+
deps: { surrealdb: "^2.0.3", zod: "^4.3.5" },
|
|
28
|
+
},
|
|
29
|
+
};
|
|
30
|
+
const DRIVER_NAMES = Object.keys(DRIVERS);
|
|
31
|
+
const PMS = ["bun", "npm", "pnpm", "yarn"] as const;
|
|
32
|
+
type Pm = (typeof PMS)[number];
|
|
33
|
+
|
|
34
|
+
interface Options {
|
|
35
|
+
dir?: string;
|
|
36
|
+
driver?: string;
|
|
37
|
+
pm?: string;
|
|
38
|
+
install?: boolean; // undefined = ask
|
|
39
|
+
git: boolean;
|
|
40
|
+
yes: boolean;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function parseArgs(argv: string[]): Options {
|
|
44
|
+
const o: Options = { git: true, yes: false };
|
|
45
|
+
for (let i = 0; i < argv.length; i++) {
|
|
46
|
+
const a = argv[i];
|
|
47
|
+
if (a === "-y" || a === "--yes") o.yes = true;
|
|
48
|
+
else if (a === "--no-install") o.install = false;
|
|
49
|
+
else if (a === "--no-git") o.git = false;
|
|
50
|
+
else if (a === "--driver") o.driver = argv[++i];
|
|
51
|
+
else if (a === "--pm") o.pm = argv[++i];
|
|
52
|
+
else if (a === "-h" || a === "--help") {
|
|
53
|
+
printHelp();
|
|
54
|
+
process.exit(0);
|
|
55
|
+
} else if (!a.startsWith("-") && o.dir === undefined) o.dir = a;
|
|
56
|
+
}
|
|
57
|
+
return o;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function printHelp(): void {
|
|
61
|
+
console.log(`create-better-schemic — scaffold a new Better-schemic project
|
|
62
|
+
|
|
63
|
+
Usage: create-better-schemic [directory] [options]
|
|
64
|
+
|
|
65
|
+
Options:
|
|
66
|
+
--driver <name> ${DRIVER_NAMES.join(" | ")}
|
|
67
|
+
--pm <name> ${PMS.join(" | ")} (the package manager to install with)
|
|
68
|
+
--no-install scaffold only; don't install or run \`better-schemic init\`
|
|
69
|
+
--no-git don't run \`git init\`
|
|
70
|
+
-y, --yes accept defaults, no prompts
|
|
71
|
+
-h, --help show this help`);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/** Which package manager invoked us (from npm_config_user_agent), for a sensible default. */
|
|
75
|
+
function detectPm(): Pm {
|
|
76
|
+
const ua = process.env.npm_config_user_agent ?? "";
|
|
77
|
+
for (const pm of PMS) if (ua.startsWith(pm)) return pm;
|
|
78
|
+
return "npm";
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/** Abort cleanly on Ctrl-C / cancel from any prompt. */
|
|
82
|
+
function abortIfCancel<T>(value: T | symbol): T {
|
|
83
|
+
if (p.isCancel(value)) {
|
|
84
|
+
p.cancel("Cancelled.");
|
|
85
|
+
process.exit(1);
|
|
86
|
+
}
|
|
87
|
+
return value as T;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// --- templates ---------------------------------------------------------------------------------
|
|
91
|
+
|
|
92
|
+
// One `db` script aliases the `better-schemic` CLI — `bun run db gen`, `bun run db migrate`, `bun run db seed`,
|
|
93
|
+
// etc. (bun/pnpm/yarn forward the subcommand directly; npm needs `npm run db -- gen`).
|
|
94
|
+
const SCRIPTS: Record<string, string> = {
|
|
95
|
+
db: "better-schemic",
|
|
96
|
+
};
|
|
97
|
+
|
|
98
|
+
/** Runtime deps for `driver` under `pm` (pnpm needs @better-schemic/core directly — strict node_modules). */
|
|
99
|
+
function depMap(driver: string, pm: Pm): Record<string, string> {
|
|
100
|
+
const d = DRIVERS[driver];
|
|
101
|
+
const deps: Record<string, string> = {
|
|
102
|
+
"@better-schemic/cli": RANGE,
|
|
103
|
+
[d.pkg]: RANGE,
|
|
104
|
+
...d.deps,
|
|
105
|
+
};
|
|
106
|
+
if (pm === "pnpm") deps["@better-schemic/core"] = RANGE;
|
|
107
|
+
return deps;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
// Kept conservative so it works across TypeScript 5, 6, and 7: `module: "ESNext"` (5.0+) rather than
|
|
111
|
+
// "Preserve" (5.4+); `moduleResolution: "bundler"` (5.0+). Seeds load supporting files via `ctx.file()`
|
|
112
|
+
// (no import attributes), so nothing here is version-gated.
|
|
113
|
+
const TSCONFIG = `${JSON.stringify(
|
|
114
|
+
{
|
|
115
|
+
compilerOptions: {
|
|
116
|
+
target: "ESNext",
|
|
117
|
+
module: "ESNext",
|
|
118
|
+
moduleResolution: "bundler",
|
|
119
|
+
moduleDetection: "force",
|
|
120
|
+
strict: true,
|
|
121
|
+
skipLibCheck: true,
|
|
122
|
+
resolveJsonModule: true,
|
|
123
|
+
noEmit: true,
|
|
124
|
+
lib: ["ESNext"],
|
|
125
|
+
types: ["node"],
|
|
126
|
+
},
|
|
127
|
+
exclude: ["node_modules"],
|
|
128
|
+
},
|
|
129
|
+
null,
|
|
130
|
+
2,
|
|
131
|
+
)}\n`;
|
|
132
|
+
|
|
133
|
+
/** NEW project → write a fresh package.json. EXISTING → MERGE deps + scripts (never clobber). */
|
|
134
|
+
function applyPackageJson(
|
|
135
|
+
target: string,
|
|
136
|
+
name: string,
|
|
137
|
+
driver: string,
|
|
138
|
+
pm: Pm,
|
|
139
|
+
): string {
|
|
140
|
+
const path = join(target, "package.json");
|
|
141
|
+
const deps = depMap(driver, pm);
|
|
142
|
+
if (!existsSync(path)) {
|
|
143
|
+
const fresh = {
|
|
144
|
+
name,
|
|
145
|
+
version: "0.0.0",
|
|
146
|
+
private: true,
|
|
147
|
+
type: "module",
|
|
148
|
+
scripts: SCRIPTS,
|
|
149
|
+
dependencies: Object.fromEntries(Object.entries(deps).sort()),
|
|
150
|
+
devDependencies: { "@types/node": "^20", typescript: "^5" },
|
|
151
|
+
};
|
|
152
|
+
writeFileSync(path, `${JSON.stringify(fresh, null, 2)}\n`);
|
|
153
|
+
return "+ package.json";
|
|
154
|
+
}
|
|
155
|
+
const pkg = JSON.parse(readFileSync(path, "utf8")) as {
|
|
156
|
+
dependencies?: Record<string, string>;
|
|
157
|
+
scripts?: Record<string, string>;
|
|
158
|
+
};
|
|
159
|
+
pkg.dependencies ??= {};
|
|
160
|
+
const added: string[] = [];
|
|
161
|
+
for (const [k, v] of Object.entries(deps))
|
|
162
|
+
if (!(k in pkg.dependencies)) {
|
|
163
|
+
pkg.dependencies[k] = v;
|
|
164
|
+
added.push(k);
|
|
165
|
+
}
|
|
166
|
+
pkg.dependencies = Object.fromEntries(
|
|
167
|
+
Object.entries(pkg.dependencies).sort(),
|
|
168
|
+
);
|
|
169
|
+
pkg.scripts ??= {};
|
|
170
|
+
for (const [k, v] of Object.entries(SCRIPTS))
|
|
171
|
+
if (!(k in pkg.scripts)) pkg.scripts[k] = v;
|
|
172
|
+
writeFileSync(path, `${JSON.stringify(pkg, null, 2)}\n`);
|
|
173
|
+
return added.length
|
|
174
|
+
? `~ package.json (added ${added.join(", ")})`
|
|
175
|
+
: dim("· package.json (deps already present)");
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
/** NEW project → write a fresh tsconfig. EXISTING → keep theirs, with a compatibility note. */
|
|
179
|
+
function applyTsconfig(target: string): string {
|
|
180
|
+
const path = join(target, "tsconfig.json");
|
|
181
|
+
if (!existsSync(path)) {
|
|
182
|
+
writeFileSync(path, TSCONFIG);
|
|
183
|
+
return "+ tsconfig.json";
|
|
184
|
+
}
|
|
185
|
+
return dim(
|
|
186
|
+
"· tsconfig.json (kept — needs module esnext/preserve/nodenext + resolveJsonModule for seed imports)",
|
|
187
|
+
);
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
const GITIGNORE = `node_modules/
|
|
191
|
+
.env
|
|
192
|
+
*.log
|
|
193
|
+
.DS_Store
|
|
194
|
+
`;
|
|
195
|
+
|
|
196
|
+
// --- write + run -------------------------------------------------------------------------------
|
|
197
|
+
|
|
198
|
+
function writeIfAbsent(dir: string, file: string, content: string): string {
|
|
199
|
+
const path = join(dir, file);
|
|
200
|
+
if (existsSync(path)) return dim(`· ${file} (exists, skipped)`);
|
|
201
|
+
writeFileSync(path, content);
|
|
202
|
+
return `+ ${file}`;
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
function run(
|
|
206
|
+
cmd: string,
|
|
207
|
+
args: string[],
|
|
208
|
+
cwd: string,
|
|
209
|
+
capture = false,
|
|
210
|
+
): { ok: boolean; out: string } {
|
|
211
|
+
const r = spawnSync(cmd, args, {
|
|
212
|
+
cwd,
|
|
213
|
+
stdio: capture ? "pipe" : "inherit",
|
|
214
|
+
encoding: "utf8",
|
|
215
|
+
});
|
|
216
|
+
return { ok: r.status === 0, out: `${r.stdout ?? ""}${r.stderr ?? ""}` };
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
async function main(): Promise<void> {
|
|
220
|
+
const opts = parseArgs(process.argv.slice(2));
|
|
221
|
+
const tty = !!process.stdin.isTTY && !opts.yes;
|
|
222
|
+
|
|
223
|
+
p.intro(bold("create-better-schemic"));
|
|
224
|
+
|
|
225
|
+
// 1. directory / name
|
|
226
|
+
let dir = opts.dir;
|
|
227
|
+
if (!dir && tty) {
|
|
228
|
+
dir = abortIfCancel(
|
|
229
|
+
await p.text({
|
|
230
|
+
message: "Project directory",
|
|
231
|
+
placeholder: "better-schemic-app",
|
|
232
|
+
defaultValue: "better-schemic-app",
|
|
233
|
+
}),
|
|
234
|
+
);
|
|
235
|
+
}
|
|
236
|
+
dir ||= "better-schemic-app";
|
|
237
|
+
const target = resolve(process.cwd(), dir);
|
|
238
|
+
const name = basename(target);
|
|
239
|
+
mkdirSync(target, { recursive: true });
|
|
240
|
+
const isExisting = existsSync(join(target, "package.json"));
|
|
241
|
+
// Warn only for a non-empty NON-project dir (random files); a package.json = a deliberate adapt.
|
|
242
|
+
if (readdirSync(target).length && !isExisting && tty) {
|
|
243
|
+
const ok = abortIfCancel(
|
|
244
|
+
await p.confirm({
|
|
245
|
+
message: `${bold(dir)} is not empty — continue?`,
|
|
246
|
+
initialValue: false,
|
|
247
|
+
}),
|
|
248
|
+
);
|
|
249
|
+
if (!ok) {
|
|
250
|
+
p.cancel("Aborted.");
|
|
251
|
+
process.exit(1);
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
// 2. driver
|
|
256
|
+
let driver = opts.driver;
|
|
257
|
+
if (!driver && tty) {
|
|
258
|
+
driver = abortIfCancel(
|
|
259
|
+
await p.select({
|
|
260
|
+
message: "Database driver",
|
|
261
|
+
options: DRIVER_NAMES.map((n) => ({
|
|
262
|
+
value: n,
|
|
263
|
+
label: DRIVERS[n].label,
|
|
264
|
+
})),
|
|
265
|
+
initialValue: "surrealdb",
|
|
266
|
+
}),
|
|
267
|
+
);
|
|
268
|
+
}
|
|
269
|
+
driver ||= "surrealdb";
|
|
270
|
+
if (!DRIVERS[driver]) {
|
|
271
|
+
p.cancel(`Unknown driver "${driver}". Known: ${DRIVER_NAMES.join(", ")}.`);
|
|
272
|
+
process.exit(1);
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
// 3. install? + which package manager (ask, default = detected)
|
|
276
|
+
const detected = detectPm();
|
|
277
|
+
let install = opts.install;
|
|
278
|
+
if (install === undefined)
|
|
279
|
+
install = tty
|
|
280
|
+
? abortIfCancel(
|
|
281
|
+
await p.confirm({
|
|
282
|
+
message: "Install dependencies now?",
|
|
283
|
+
initialValue: true,
|
|
284
|
+
}),
|
|
285
|
+
)
|
|
286
|
+
: true;
|
|
287
|
+
let pm: Pm = (opts.pm as Pm) ?? detected;
|
|
288
|
+
if (install && !opts.pm && tty) {
|
|
289
|
+
pm = abortIfCancel(
|
|
290
|
+
await p.select({
|
|
291
|
+
message: "Install with which package manager?",
|
|
292
|
+
options: [detected, ...PMS.filter((x) => x !== detected)].map((m) => ({
|
|
293
|
+
value: m,
|
|
294
|
+
label: m === detected ? `${m} (detected)` : m,
|
|
295
|
+
})),
|
|
296
|
+
initialValue: detected,
|
|
297
|
+
}),
|
|
298
|
+
) as Pm;
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
// 4. write/merge the project envelope (fresh for a new project, merge into an existing one)
|
|
302
|
+
const written = [
|
|
303
|
+
applyPackageJson(target, name, driver, pm),
|
|
304
|
+
applyTsconfig(target),
|
|
305
|
+
writeIfAbsent(target, ".gitignore", GITIGNORE),
|
|
306
|
+
];
|
|
307
|
+
const header = isExisting ? `Adding Better-schemic to ${name}` : name;
|
|
308
|
+
p.log.step(
|
|
309
|
+
`${bold(header)} ${dim(`(${DRIVERS[driver].label})`)}\n${written.join("\n")}`,
|
|
310
|
+
);
|
|
311
|
+
if (opts.git && !existsSync(join(target, ".git")))
|
|
312
|
+
run("git", ["init", "-q"], target);
|
|
313
|
+
|
|
314
|
+
// 5. install + compose on `better-schemic init` (which scaffolds config + database/ via the driver)
|
|
315
|
+
if (install) {
|
|
316
|
+
const s = p.spinner();
|
|
317
|
+
s.start(`Installing dependencies with ${pm}`);
|
|
318
|
+
const r = run(pm, ["install"], target, true);
|
|
319
|
+
if (!r.ok) {
|
|
320
|
+
s.stop(`${pm} install failed`);
|
|
321
|
+
p.log.error(r.out.trim().split("\n").slice(-8).join("\n"));
|
|
322
|
+
p.outro(`Fix the install, then run ${bold("better-schemic init")}.`);
|
|
323
|
+
process.exit(1);
|
|
324
|
+
}
|
|
325
|
+
s.stop("Dependencies installed");
|
|
326
|
+
p.log.step("Scaffolding the schema");
|
|
327
|
+
const runtime = pm === "bun" ? "bun" : "node";
|
|
328
|
+
const cliJs = join(
|
|
329
|
+
target,
|
|
330
|
+
"node_modules",
|
|
331
|
+
"@better-schemic",
|
|
332
|
+
"cli",
|
|
333
|
+
"lib",
|
|
334
|
+
"cli.js",
|
|
335
|
+
);
|
|
336
|
+
// Mark the inner init as already-bootstrapped so it scaffolds directly (never forwards back here).
|
|
337
|
+
process.env.BETTER_SCHEMIC_NO_BOOTSTRAP = "1";
|
|
338
|
+
process.env.SCHEMIC_NO_BOOTSTRAP = "1"; // legacy alias
|
|
339
|
+
run(runtime, [cliJs, "init", "--driver", driver], target);
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
// 6. next steps
|
|
343
|
+
const cd = dir === "." ? "" : `cd ${dir}\n`;
|
|
344
|
+
// npm needs `npm run db -- <sub>` to forward args; bun/pnpm/yarn forward `<pm> run db <sub>` directly.
|
|
345
|
+
const dbCmd = (sub: string) =>
|
|
346
|
+
pm === "npm" ? `npm run db -- ${sub}` : `${pm} run db ${sub}`;
|
|
347
|
+
const steps = install
|
|
348
|
+
? `${cd}cp .env.example .env ${dim("# set your connection")}\n${dbCmd("gen")}\n${dbCmd("migrate")}`
|
|
349
|
+
: `${cd}${pm} install\n${pm} exec better-schemic init --driver ${driver}\ncp .env.example .env\n${dbCmd("gen")}`;
|
|
350
|
+
p.note(steps, "Next steps");
|
|
351
|
+
p.outro(`${bold(name)} is ready.`);
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
main().catch((e: unknown) => {
|
|
355
|
+
p.log.error(e instanceof Error ? e.message : String(e));
|
|
356
|
+
process.exit(1);
|
|
357
|
+
});
|