create-quadrokit 0.2.0 → 0.2.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/README.md CHANGED
@@ -16,16 +16,20 @@ bun create-quadrokit/src/index.ts --template dashboard --dir /path/to/my-app
16
16
  | `--template <name>` | `dashboard` \| `website` \| `ecommerce` \| `admin-shell` |
17
17
  | `--dir <path>` | Target directory (created; must be empty) |
18
18
  | `--keep-workspace` | Keep `workspace:*` in `package.json` (for development inside the QuadroKit monorepo only) |
19
+ | `--git` / `--no-git` | Initialize a git repo after scaffolding (skip the prompt) |
20
+ | `--install` / `--no-install` | Run `bun install` or `npm install` after scaffolding (skip the prompt) |
19
21
 
20
- Interactive mode: run without `--template` / `--dir` to be prompted.
22
+ On start, the CLI prints a short **prerequisites** check (Node.js, Bun, Git). Node 18+ is required; Bun is optional (falls back to npm for installs). Git is only needed if you choose to init a repository.
23
+
24
+ Interactive mode: run without `--template` / `--dir` to be prompted for those; you are then asked about **git** and **install** unless overridden with the flags above.
21
25
 
22
26
  ## What it does
23
27
 
24
- 1. Copies the chosen template from `create-quadrokit/templates/<name>` when installed from npm, or `packages/templates/<name>` when you run the CLI from the monorepo (skips `node_modules`, `dist`).
25
- 2. Copies `packages/sample-client/generated` `<dir>/.quadrokit/generated`.
26
- 3. Rewrites `src/lib/quadro-client.ts` to import from `../../.quadrokit/generated/client.gen.js`.
27
- 4. Removes `@quadrokit/sample-client` and rewrites other `workspace:*` deps to `^0.2.0` (unless `--keep-workspace`).
28
- 5. Writes `QUADROKIT.md` with proxy and `quadrokit-client generate` instructions.
28
+ 1. Copies the chosen template from `create-quadrokit/templates/<name>` when installed from npm, or `packages/templates/<name>` when you run the CLI from the monorepo (skips `node_modules`, `dist`, `.quadrokit`, lockfiles).
29
+ 2. Rewrites `src/lib/quadro-client.ts` to import from `../../.quadrokit/generated/client.gen.js`.
30
+ 3. Removes `@quadrokit/sample-client` and rewrites other `workspace:*` deps to published semver (`@quadrokit/client` → `^0.2.1`, other `@quadrokit/*` → `^0.2.0`) unless `--keep-workspace`.
31
+ 4. Writes `QUADROKIT.md` with proxy and `quadrokit:generate` instructions. Does **not** copy `.quadrokit/generated` run `bun run quadrokit:generate` after install.
32
+ 5. Optionally runs `git init` and installs dependencies (`bun` if available, otherwise `npm`).
29
33
 
30
34
  ## Published usage (future)
31
35
 
@@ -1,8 +1,57 @@
1
1
  #!/usr/bin/env node
2
+ import { spawnSync } from 'node:child_process';
2
3
  import { cp, mkdir, readdir, readFile, stat, writeFile } from 'node:fs/promises';
3
4
  import path from 'node:path';
4
5
  import { fileURLToPath } from 'node:url';
5
6
  import prompts from 'prompts';
7
+ const MIN_NODE_MAJOR = 18;
8
+ function cmdVersion(cmd, args) {
9
+ const r = spawnSync(cmd, args, { encoding: 'utf8', shell: false });
10
+ if (r.error || r.status !== 0)
11
+ return null;
12
+ const line = r.stdout.trim().split('\n')[0];
13
+ return line || null;
14
+ }
15
+ function nodeMajor() {
16
+ const m = /^v?(\d+)/.exec(process.version);
17
+ return m ? Number(m[1]) : 0;
18
+ }
19
+ function checkPrereqs() {
20
+ const nodeVersion = process.version;
21
+ const nodeOk = nodeMajor() >= MIN_NODE_MAJOR;
22
+ const bunVersion = cmdVersion('bun', ['--version']);
23
+ const gitVersion = cmdVersion('git', ['--version']);
24
+ return {
25
+ nodeOk,
26
+ nodeVersion,
27
+ bunOk: bunVersion !== null,
28
+ bunVersion,
29
+ gitOk: gitVersion !== null,
30
+ gitVersion,
31
+ };
32
+ }
33
+ function printWelcome() {
34
+ console.log(`
35
+ ${'✨'.repeat(2)} ${'\u001b[1mcreate-quadrokit\u001b[0m'} ${'✨'.repeat(2)}
36
+ ${'🚀'.repeat(2)} Scaffold a QuadroKit Vite + React app for 4D REST
37
+ `);
38
+ }
39
+ function printPrereqReport(p) {
40
+ console.log(' Prerequisites');
41
+ const line = (ok, emoji, label, detail) => {
42
+ const mark = ok ? '✅' : '⚠️ ';
43
+ console.log(` ${mark} ${emoji} ${label}: ${detail}`);
44
+ };
45
+ line(p.nodeOk, '📗', 'Node.js', p.nodeOk ? p.nodeVersion : `${p.nodeVersion} (need >= ${MIN_NODE_MAJOR})`);
46
+ line(p.bunOk, '🥟', 'Bun', p.bunVersion ?? 'not found (you can use npm install instead)');
47
+ line(p.gitOk, '📌', 'Git', p.gitVersion ?? 'not found (needed only to init a repository)');
48
+ console.log('');
49
+ }
50
+ async function runInDir(cwd, cmd, args) {
51
+ const shell = process.platform === 'win32';
52
+ const r = spawnSync(cmd, args, { cwd, stdio: 'inherit', shell });
53
+ return r.status === 0;
54
+ }
6
55
  const TEMPLATES = ['dashboard', 'website', 'ecommerce', 'admin-shell'];
7
56
  /** Directory of the create-quadrokit package (works when installed under node_modules). */
8
57
  function packageRoot() {
@@ -32,22 +81,13 @@ async function resolveBiomeJsonPath() {
32
81
  }
33
82
  return null;
34
83
  }
35
- async function resolveGeneratedClientDir() {
36
- const pkg = packageRoot();
37
- const bundled = path.join(pkg, 'vendor', 'generated');
38
- if (await pathExists(bundled)) {
39
- return bundled;
40
- }
41
- const monorepo = path.join(pkg, '..', 'packages', 'sample-client', 'generated');
42
- if (await pathExists(monorepo)) {
43
- return monorepo;
44
- }
45
- return null;
46
- }
47
84
  function parseArgs(argv) {
48
85
  let template;
49
86
  let dir;
50
87
  let keepWorkspace = false;
88
+ /** `undefined` = prompt later */
89
+ let initGit;
90
+ let installDeps;
51
91
  for (let i = 0; i < argv.length; i++) {
52
92
  const a = argv[i];
53
93
  if (a === '--template' && argv[i + 1]) {
@@ -59,8 +99,20 @@ function parseArgs(argv) {
59
99
  else if (a === '--keep-workspace') {
60
100
  keepWorkspace = true;
61
101
  }
102
+ else if (a === '--git') {
103
+ initGit = true;
104
+ }
105
+ else if (a === '--no-git') {
106
+ initGit = false;
107
+ }
108
+ else if (a === '--install') {
109
+ installDeps = true;
110
+ }
111
+ else if (a === '--no-install') {
112
+ installDeps = false;
113
+ }
62
114
  }
63
- return { template, dir, keepWorkspace };
115
+ return { template, dir, keepWorkspace, initGit, installDeps };
64
116
  }
65
117
  async function pathExists(p) {
66
118
  try {
@@ -71,11 +123,22 @@ async function pathExists(p) {
71
123
  return false;
72
124
  }
73
125
  }
126
+ const SKIP_TEMPLATE_NAMES = new Set([
127
+ 'node_modules',
128
+ 'dist',
129
+ '.turbo',
130
+ '.quadrokit',
131
+ 'bun.lock',
132
+ 'bun.lockb',
133
+ 'package-lock.json',
134
+ 'pnpm-lock.yaml',
135
+ 'yarn.lock',
136
+ ]);
74
137
  async function copyTemplate(src, dest) {
75
138
  await mkdir(dest, { recursive: true });
76
139
  const entries = await readdir(src, { withFileTypes: true });
77
140
  for (const e of entries) {
78
- if (e.name === 'node_modules' || e.name === 'dist' || e.name === '.turbo') {
141
+ if (SKIP_TEMPLATE_NAMES.has(e.name)) {
79
142
  continue;
80
143
  }
81
144
  const from = path.join(src, e.name);
@@ -96,7 +159,7 @@ function rewriteWorkspaceDeps(pkg) {
96
159
  }
97
160
  for (const name of Object.keys(deps)) {
98
161
  if (deps[name] === 'workspace:*') {
99
- deps[name] = '^0.2.0';
162
+ deps[name] = name === '@quadrokit/client' ? '^0.2.1' : '^0.2.0';
100
163
  }
101
164
  }
102
165
  }
@@ -141,23 +204,13 @@ async function writeStandaloneBiome(dest, keepWorkspace) {
141
204
  }
142
205
  async function writeQuadroClientImport(dest) {
143
206
  const p = path.join(dest, 'src', 'lib', 'quadro-client.ts');
144
- const body = `import { createClient } from '../../.quadrokit/generated/client.gen.js';
207
+ const body = `import { createClient } from '../../.quadrokit/generated/client.gen.mjs';
145
208
 
146
209
  /** Same-origin \`/rest\` in dev (Vite proxy) and production (reverse proxy). */
147
210
  export const quadro = createClient({ baseURL: '/rest' });
148
211
  `;
149
212
  await writeFile(p, body, 'utf8');
150
213
  }
151
- async function seedGenerated(dest) {
152
- const genSrc = await resolveGeneratedClientDir();
153
- const genDest = path.join(dest, '.quadrokit', 'generated');
154
- if (!genSrc) {
155
- console.warn('Warning: sample generated client not found — run `quadrokit-client generate` after install.');
156
- return;
157
- }
158
- await mkdir(path.join(dest, '.quadrokit'), { recursive: true });
159
- await cp(genSrc, genDest, { recursive: true });
160
- }
161
214
  async function writeReadme(dest, template) {
162
215
  const text = `# ${path.basename(dest)}
163
216
 
@@ -165,14 +218,20 @@ Created with **create-quadrokit** (template: \`${template}\`).
165
218
 
166
219
  ## Next steps
167
220
 
168
- 1. \`cd ${path.basename(dest)}\` and \`bun install\`
169
- 2. Copy \`.env.example\` to \`.env\` and set \`VITE_4D_ORIGIN\` to your 4D web server.
170
- 3. Regenerate the typed client when your REST catalog changes:
221
+ 1. \`cd ${path.basename(dest)}\` and install dependencies (\`bun install\` or \`npm install\`).
222
+ 2. Generate the typed REST client (required before \`dev\` / \`build\`):
223
+
224
+ \`\`\`bash
225
+ bun run quadrokit:generate
226
+ \`\`\`
227
+
228
+ This uses \`assets/catalog.placeholder.json\` by default (demo catalog from QuadroKit). Point at your live 4D catalog:
171
229
 
172
230
  \`\`\`bash
173
- bunx quadrokit-client generate --url "http://localhost:7080/rest/\\$catalog" --token YOUR_TOKEN --out .quadrokit/generated
231
+ QUADROKIT_CATALOG_URL='http://localhost:7080/rest/\\$catalog' QUADROKIT_CATALOG_TOKEN='…' bun run quadrokit:generate
174
232
  \`\`\`
175
233
 
234
+ 3. Copy \`.env.example\` to \`.env\` and set \`VITE_4D_ORIGIN\` to your 4D web server.
176
235
  4. \`bun run dev\` — the dev server proxies \`/rest\` to \`VITE_4D_ORIGIN\` so **4DSID_** cookies stay same-origin.
177
236
 
178
237
  Production: serve the SPA and reverse-proxy \`/rest\` to 4D on the **same host** as the UI.
@@ -181,7 +240,14 @@ Production: serve the SPA and reverse-proxy \`/rest\` to 4D on the **same host**
181
240
  }
182
241
  async function main() {
183
242
  const argv = process.argv.slice(2);
184
- const { template: tArg, dir: dirArg, keepWorkspace } = parseArgs(argv);
243
+ const { template: tArg, dir: dirArg, keepWorkspace, initGit: gitFlag, installDeps: installFlag, } = parseArgs(argv);
244
+ printWelcome();
245
+ const prereqs = checkPrereqs();
246
+ printPrereqReport(prereqs);
247
+ if (!prereqs.nodeOk) {
248
+ console.error(` ❌ Node.js ${MIN_NODE_MAJOR}+ is required.`);
249
+ process.exit(1);
250
+ }
185
251
  const template = tArg && TEMPLATES.includes(tArg)
186
252
  ? tArg
187
253
  : (await prompts({
@@ -218,19 +284,75 @@ async function main() {
218
284
  }
219
285
  await copyTemplate(src, dest);
220
286
  await writeStandaloneBiome(dest, keepWorkspace);
221
- await seedGenerated(dest);
222
287
  await patchPackageJson(dest, path
223
288
  .basename(dest)
224
289
  .replace(/[^a-z0-9-]/gi, '-')
225
290
  .toLowerCase() || 'quadro-app', keepWorkspace);
226
291
  await writeQuadroClientImport(dest);
227
292
  await writeReadme(dest, template);
228
- console.log(`
229
- Created QuadroKit project at ${dest}
293
+ console.log(`\n 🎉 Project files are ready at ${'\u001b[1m'}${dest}${'\u001b[0m'}\n`);
294
+ let initGit = gitFlag;
295
+ if (initGit === undefined) {
296
+ const g = await prompts({
297
+ type: 'confirm',
298
+ name: 'v',
299
+ message: 'Initialize a git repository here?',
300
+ initial: true,
301
+ });
302
+ if (typeof g.v !== 'boolean') {
303
+ process.exit(1);
304
+ }
305
+ initGit = g.v;
306
+ }
307
+ if (initGit) {
308
+ if (!prereqs.gitOk) {
309
+ console.log(' ⚠️ Skipping git init — Git is not available on your PATH.\n');
310
+ }
311
+ else {
312
+ const ok = await runInDir(dest, 'git', ['init']);
313
+ if (ok) {
314
+ console.log(' ✅ Git repository initialized.\n');
315
+ }
316
+ else {
317
+ console.log(' ⚠️ `git init` failed — you can run it manually later.\n');
318
+ }
319
+ }
320
+ }
321
+ let installDeps = installFlag;
322
+ if (installDeps === undefined) {
323
+ const ins = await prompts({
324
+ type: 'confirm',
325
+ name: 'v',
326
+ message: 'Install dependencies now?',
327
+ initial: true,
328
+ });
329
+ if (typeof ins.v !== 'boolean') {
330
+ process.exit(1);
331
+ }
332
+ installDeps = ins.v;
333
+ }
334
+ if (installDeps) {
335
+ const useBun = prereqs.bunOk;
336
+ const cmd = useBun ? 'bun' : 'npm';
337
+ const args = useBun ? ['install'] : ['install'];
338
+ console.log(` 📦 Running ${cmd} ${args.join(' ')} …\n`);
339
+ const ok = await runInDir(dest, cmd, args);
340
+ if (ok) {
341
+ console.log(` ✅ Dependencies installed with ${cmd}.\n`);
342
+ }
343
+ else {
344
+ console.log(` ⚠️ Install failed — run \`${useBun ? 'bun install' : 'npm install'}\` inside the project.\n`);
345
+ }
346
+ }
347
+ const pm = prereqs.bunOk ? 'bun' : 'npm';
348
+ const runDev = prereqs.bunOk ? 'bun run dev' : 'npm run dev';
349
+ const installLine = installDeps ? ` → ${runDev}` : ` → ${pm} install\n → ${runDev}`;
350
+ console.log(` 🚀🚀 Next steps
351
+ → cd ${path.basename(dest)}
352
+ ${installLine}
353
+ → Copy .env.example → .env and set VITE_4D_ORIGIN
230
354
 
231
- cd ${path.basename(dest)}
232
- bun install
233
- bun run dev
355
+ Happy building! ✨
234
356
  `);
235
357
  }
236
358
  main().catch((e) => {
package/package.json CHANGED
@@ -1,17 +1,16 @@
1
1
  {
2
2
  "name": "create-quadrokit",
3
- "version": "0.2.0",
3
+ "version": "0.2.1",
4
4
  "description": "Scaffold a QuadroKit Vite + React app from a template",
5
5
  "type": "module",
6
- "bin": "./dist/index.js",
6
+ "bin": "./dist/index.mjs",
7
7
  "files": [
8
8
  "dist",
9
9
  "templates",
10
- "vendor",
11
10
  "biome.monorepo.json"
12
11
  ],
13
12
  "scripts": {
14
- "build": "tsc -p tsconfig.build.json",
13
+ "build": "tsc -p tsconfig.build.json && bun ../scripts/rename-dist-js-to-mjs.ts dist",
15
14
  "typecheck": "tsc -p tsconfig.json --noEmit",
16
15
  "prepublishOnly": "bun run scripts/sync-for-publish.ts && bun run build"
17
16
  },
@@ -34,4 +34,6 @@ Each template has a [`biome.json`](dashboard/biome.json) with `"root": false` an
34
34
 
35
35
  ## Scaffolding outside the monorepo
36
36
 
37
- Use [`create-quadrokit`](../../create-quadrokit/README.md); it copies a template, seeds `.quadrokit/generated`, and rewrites `workspace:*` to published semver.
37
+ Use [`create-quadrokit`](../../create-quadrokit/README.md); it copies a template, rewrites `workspace:*` to published semver, and skips lockfiles and `.quadrokit/` (so new projects run `bun run quadrokit:generate` after install).
38
+
39
+ For **monorepo** template packages, `.quadrokit/generated` is kept (regenerate with `bun run quadrokit:generate` in a template after changing `assets/catalog.placeholder.json`) so `tsc` can typecheck against the demo catalog.
@@ -4,6 +4,7 @@
4
4
  "private": true,
5
5
  "type": "module",
6
6
  "scripts": {
7
+ "quadrokit:generate": "node scripts/quadrokit-generate.mjs",
7
8
  "dev": "vite",
8
9
  "build": "tsc -b && vite build",
9
10
  "preview": "vite preview",
@@ -4,6 +4,7 @@
4
4
  "private": true,
5
5
  "type": "module",
6
6
  "scripts": {
7
+ "quadrokit:generate": "node scripts/quadrokit-generate.mjs",
7
8
  "dev": "vite",
8
9
  "build": "tsc -b && vite build",
9
10
  "preview": "vite preview",
@@ -4,6 +4,7 @@
4
4
  "private": true,
5
5
  "type": "module",
6
6
  "scripts": {
7
+ "quadrokit:generate": "node scripts/quadrokit-generate.mjs",
7
8
  "dev": "vite",
8
9
  "build": "tsc -b && vite build",
9
10
  "preview": "vite preview",
@@ -4,6 +4,7 @@
4
4
  "private": true,
5
5
  "type": "module",
6
6
  "scripts": {
7
+ "quadrokit:generate": "node scripts/quadrokit-generate.mjs",
7
8
  "dev": "vite",
8
9
  "build": "tsc -b && vite build",
9
10
  "preview": "vite preview",