create-zenbu-app 0.0.6 → 0.0.9

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.
Files changed (39) hide show
  1. package/README.md +50 -3
  2. package/dist/index.mjs +349 -40
  3. package/package.json +5 -2
  4. package/templates/plugin/_gitignore +3 -0
  5. package/templates/plugin/package.json +24 -0
  6. package/templates/plugin/src/main/services/{{projectName}}.ts.tmpl +9 -0
  7. package/templates/plugin/tsconfig.json +14 -0
  8. package/templates/plugin/zenbu.plugin.ts.tmpl +6 -0
  9. package/{template → templates/tailwind}/package.json +1 -1
  10. package/templates/tailwind/src/main/schema.ts.tmpl +13 -0
  11. package/{template/src/main/services/app.ts.tmpl → templates/tailwind/src/main/services/init.ts.tmpl} +3 -3
  12. package/{template → templates/tailwind}/src/renderer/App.tsx.tmpl +7 -9
  13. package/{template → templates/tailwind}/src/renderer/main.tsx.tmpl +3 -3
  14. package/{template → templates/tailwind}/zenbu.config.ts.tmpl +7 -6
  15. package/templates/vanilla/_gitignore +8 -0
  16. package/templates/vanilla/electron-builder.json +12 -0
  17. package/templates/vanilla/package.json +45 -0
  18. package/templates/vanilla/src/main/schema.ts.tmpl +13 -0
  19. package/templates/vanilla/src/main/services/init.ts.tmpl +11 -0
  20. package/templates/vanilla/src/main/services/repo.ts.tmpl +11 -0
  21. package/templates/vanilla/src/renderer/App.tsx.tmpl +83 -0
  22. package/templates/vanilla/src/renderer/app.css +143 -0
  23. package/templates/vanilla/src/renderer/index.html +12 -0
  24. package/templates/vanilla/src/renderer/installing.html +118 -0
  25. package/templates/vanilla/src/renderer/main.tsx.tmpl +10 -0
  26. package/templates/vanilla/src/renderer/splash.html +22 -0
  27. package/templates/vanilla/tsconfig.json.tmpl +18 -0
  28. package/templates/vanilla/vite.config.ts.tmpl +16 -0
  29. package/templates/vanilla/zenbu.config.ts.tmpl +56 -0
  30. package/template/src/main/schema.ts.tmpl +0 -16
  31. /package/{template → templates/tailwind}/_gitignore +0 -0
  32. /package/{template → templates/tailwind}/electron-builder.json +0 -0
  33. /package/{template → templates/tailwind}/src/main/services/repo.ts.tmpl +0 -0
  34. /package/{template → templates/tailwind}/src/renderer/app.css +0 -0
  35. /package/{template → templates/tailwind}/src/renderer/index.html +0 -0
  36. /package/{template → templates/tailwind}/src/renderer/installing.html +0 -0
  37. /package/{template → templates/tailwind}/src/renderer/splash.html +0 -0
  38. /package/{template → templates/tailwind}/tsconfig.json.tmpl +0 -0
  39. /package/{template → templates/tailwind}/vite.config.ts.tmpl +0 -0
package/README.md CHANGED
@@ -5,9 +5,56 @@ Scaffold a new [Zenbu](https://github.com/zenbu-labs/zenbu.js) app.
5
5
  ```bash
6
6
  pnpm create zenbu-app my-app
7
7
  cd my-app
8
- pnpm install
9
8
  pnpm dev
10
9
  ```
11
10
 
12
- Zenbu currently requires pnpm 10+. The bundled .app re-installs from the
13
- pnpm lockfile at first launch, so the project's lockfile must be a pnpm one.
11
+ ## Interactive mode
12
+
13
+ Run with no arguments to be prompted for a project name and config options:
14
+
15
+ ```bash
16
+ pnpm create zenbu-app
17
+ ```
18
+
19
+ You'll be asked for:
20
+
21
+ - **Project name** — defaults to `my-zenbu-app` (just press enter to accept).
22
+ - **Use Tailwind CSS?** — defaults to `yes`.
23
+
24
+ Each prompt's default is selected when you press enter, so a default scaffold
25
+ is just `enter, enter, enter`.
26
+
27
+ ## Flags
28
+
29
+ | Flag | Description |
30
+ |---|---|
31
+ | `--yes`, `-y` | Skip every prompt and take each option's default. With no project name, scaffolds into the current directory. |
32
+ | `--no-install` | Skip the post-copy `<pm> install` step. |
33
+
34
+ A few common invocations:
35
+
36
+ ```bash
37
+ pnpm create zenbu-app # interactive, then scaffolds ./my-zenbu-app
38
+ pnpm create zenbu-app my-app # interactive options, scaffolds ./my-app
39
+ pnpm create zenbu-app . # interactive options, scaffolds into cwd
40
+ pnpm create zenbu-app --yes # all defaults, scaffolds into cwd
41
+ pnpm create zenbu-app my-app --yes # all defaults, scaffolds ./my-app
42
+ ```
43
+
44
+ ## Templates
45
+
46
+ The CLI ships full per-config copies of the project under `templates/<slug>/`
47
+ — there are no in-template conditionals. Today:
48
+
49
+ - `templates/tailwind/` — Tailwind CSS v4 wired up via `@tailwindcss/vite`.
50
+ - `templates/vanilla/` — plain CSS, no utility framework.
51
+
52
+ The selected slug is computed from the answered config options (Tailwind
53
+ contributes `tailwind`; the empty set falls back to `vanilla`).
54
+
55
+ ## Package manager support
56
+
57
+ The detected invoking package manager is recorded in `zenbu.config.ts` and
58
+ used for the post-copy install. pnpm, npm, yarn, and bun are all supported,
59
+ but the bundled `.app` re-installs from the project's lockfile at first
60
+ launch — so currently the lockfile must be a pnpm one.
package/dist/index.mjs CHANGED
@@ -3,15 +3,85 @@ import path from "node:path";
3
3
  import fs from "node:fs";
4
4
  import { fileURLToPath } from "node:url";
5
5
  import { spawnSync } from "node:child_process";
6
+ import * as p from "@clack/prompts";
6
7
  //#region src/index.ts
7
8
  const __dirname = path.dirname(fileURLToPath(import.meta.url));
8
- const TEMPLATE_DIR = path.resolve(__dirname, "..", "template");
9
- const argv = process.argv.slice(2);
10
- const flagsSet = new Set(argv.filter((a) => a.startsWith("-")));
11
- const positional = argv.filter((a) => !a.startsWith("-"));
9
+ const TEMPLATES_DIR = path.resolve(__dirname, "..", "templates");
10
+ const rawArgv = process.argv.slice(2);
11
+ const flagsSet = /* @__PURE__ */ new Set();
12
+ const positional = [];
13
+ const dependsOn = [];
14
+ /**
15
+ * Parse flags. `--depends-on NAME=PATH` is consumed positionally because it
16
+ * carries a value; everything else is a boolean flag or a positional arg.
17
+ */
18
+ for (let i = 0; i < rawArgv.length; i++) {
19
+ const arg = rawArgv[i];
20
+ if (arg === "--depends-on" || arg === "--dependsOn") {
21
+ const value = rawArgv[++i];
22
+ if (!value) {
23
+ console.error("create-zenbu-app: --depends-on requires NAME=PATH");
24
+ process.exit(1);
25
+ }
26
+ dependsOn.push(parseDependsOn(value));
27
+ } else if (arg.startsWith("--depends-on=") || arg.startsWith("--dependsOn=")) dependsOn.push(parseDependsOn(arg.slice(arg.indexOf("=") + 1)));
28
+ else if (arg.startsWith("-")) flagsSet.add(arg);
29
+ else positional.push(arg);
30
+ }
12
31
  const yes = flagsSet.has("--yes") || flagsSet.has("-y");
13
32
  const noInstall = flagsSet.has("--no-install");
33
+ const noGit = flagsSet.has("--no-git");
34
+ const pluginMode = flagsSet.has("--plugin");
35
+ const noAddToHost = flagsSet.has("--no-add-to-host");
36
+ if (dependsOn.length > 0 && !pluginMode) {
37
+ console.error("create-zenbu-app: --depends-on is only valid with --plugin");
38
+ process.exit(1);
39
+ }
40
+ function parseDependsOn(raw) {
41
+ const eq = raw.indexOf("=");
42
+ if (eq < 0) {
43
+ console.error(`create-zenbu-app: --depends-on must be of the form NAME=PATH (got "${raw}")`);
44
+ process.exit(1);
45
+ }
46
+ const name = raw.slice(0, eq).trim();
47
+ const rel = raw.slice(eq + 1).trim();
48
+ if (!name) {
49
+ console.error("create-zenbu-app: --depends-on NAME may not be empty");
50
+ process.exit(1);
51
+ }
52
+ if (!rel) {
53
+ console.error("create-zenbu-app: --depends-on PATH may not be empty");
54
+ process.exit(1);
55
+ }
56
+ const abs = path.isAbsolute(rel) ? rel : path.resolve(process.cwd(), rel);
57
+ if (!fs.existsSync(abs)) {
58
+ console.error(`create-zenbu-app: --depends-on path does not exist: ${abs}`);
59
+ process.exit(1);
60
+ }
61
+ return {
62
+ name,
63
+ from: abs
64
+ };
65
+ }
14
66
  const ZENBU_LOCAL_CORE = process.env.ZENBU_LOCAL_CORE;
67
+ const CONFIG_OPTIONS = [{
68
+ id: "tailwind",
69
+ default: true,
70
+ ask: () => p.confirm({
71
+ message: "Use Tailwind CSS?",
72
+ initialValue: true
73
+ }),
74
+ slug: (v) => v ? "tailwind" : null
75
+ }];
76
+ function resolveSlug(answers) {
77
+ const parts = [];
78
+ for (const opt of CONFIG_OPTIONS) {
79
+ const value = answers[opt.id];
80
+ const fragment = opt.slug(value);
81
+ if (fragment) parts.push(fragment);
82
+ }
83
+ return parts.length > 0 ? parts.join("-") : "vanilla";
84
+ }
15
85
  /**
16
86
  * Detect the PM that invoked `create-zenbu-app`. Order:
17
87
  * 1. `process.versions.bun` → bun (covers `bunx create-zenbu-app`).
@@ -68,22 +138,61 @@ function probeVersion(pm) {
68
138
  const match = (res.stdout ?? "").trim().match(/\d+\.\d+\.\d+(?:[-+][\w.]+)?/);
69
139
  return match ? match[0] : null;
70
140
  }
71
- function renderTemplate(value, projectName) {
72
- return value.replace(/\{\{projectName\}\}/g, projectName);
141
+ function renderTemplate(value, ctx) {
142
+ return value.replace(/\{\{(\w+)\}\}/g, (full, key) => {
143
+ return Object.prototype.hasOwnProperty.call(ctx, key) ? ctx[key] : full;
144
+ });
73
145
  }
74
- function copyDirSync(src, dest, projectName) {
146
+ /**
147
+ * Copy a template directory tree to `dest`. Both file *contents* and
148
+ * file/directory *names* go through `renderTemplate`, so a template can
149
+ * place a file at e.g. `src/main/services/{{projectName}}.ts.tmpl` and have
150
+ * it land at `src/main/services/<projectName>.ts`. `.tmpl` is stripped.
151
+ */
152
+ function copyDirSync(src, dest, ctx) {
75
153
  fs.mkdirSync(dest, { recursive: true });
76
154
  for (const entry of fs.readdirSync(src, { withFileTypes: true })) {
77
155
  const srcPath = path.join(src, entry.name);
78
- const destName = entry.name.endsWith(".tmpl") ? entry.name.slice(0, -5) : entry.name;
156
+ let destName = entry.name.endsWith(".tmpl") ? entry.name.slice(0, -5) : entry.name;
157
+ destName = renderTemplate(destName, ctx);
79
158
  const destPath = path.join(dest, destName);
80
- if (entry.isDirectory()) copyDirSync(srcPath, destPath, projectName);
159
+ if (entry.isDirectory()) copyDirSync(srcPath, destPath, ctx);
81
160
  else {
82
161
  const content = fs.readFileSync(srcPath, "utf8");
83
- fs.writeFileSync(destPath, renderTemplate(content, projectName));
162
+ fs.writeFileSync(destPath, renderTemplate(content, ctx));
84
163
  }
85
164
  }
86
165
  }
166
+ function toPascalCase(s) {
167
+ return s.split(/[^a-zA-Z0-9]+/).filter(Boolean).map((part) => part.charAt(0).toUpperCase() + part.slice(1)).join("");
168
+ }
169
+ function relPosix(fromDir, toFile) {
170
+ let r = path.relative(fromDir, toFile).split(path.sep).join("/");
171
+ if (!r.startsWith(".")) r = "./" + r;
172
+ return r;
173
+ }
174
+ /**
175
+ * Render a plugin's `dependsOn` literal for the scaffolded `zenbu.plugin.ts`.
176
+ * Returns either an empty string (no deps → field omitted) or a leading-`\n`
177
+ * fragment that slots in after the `services:` line, e.g.
178
+ *
179
+ * \n dependsOn: [\n { name: "app", from: "../../zenbu.config.ts" },\n ],
180
+ *
181
+ * Each `from` is rewritten relative to `pluginDir` so the generated file is
182
+ * stable across moves of the surrounding workspace.
183
+ */
184
+ function renderDependsOn(pluginDir, deps) {
185
+ if (deps.length === 0) return "";
186
+ const lines = [];
187
+ lines.push("");
188
+ lines.push(" dependsOn: [");
189
+ for (const d of deps) {
190
+ const fromRel = relPosix(pluginDir, d.from);
191
+ lines.push(` { name: ${JSON.stringify(d.name)}, from: ${JSON.stringify(fromRel)} },`);
192
+ }
193
+ lines.push(" ],");
194
+ return lines.join("\n");
195
+ }
87
196
  /**
88
197
  * Replace the `// {{packageManager}}` marker in the scaffolded
89
198
  * `zenbu.config.ts` with a real `packageManager: { ... }` line. Idempotent
@@ -97,13 +206,29 @@ function seedPackageManager(projectDir, pm) {
97
206
  const replaced = original.replace(/\/\/\s*\{\{packageManager\}\}/, literal);
98
207
  if (replaced !== original) fs.writeFileSync(configPath, replaced);
99
208
  }
100
- function rewireToLocalCore(projectDir, corePath) {
209
+ /**
210
+ * Pin `@zenbujs/core` to a local checkout. For apps the dep lives in
211
+ * `dependencies`; for plugins it's in `devDependencies` (plugins peer on
212
+ * core).
213
+ */
214
+ function rewireToLocalCore(projectDir, corePath, isPlugin) {
101
215
  const pkgPath = path.join(projectDir, "package.json");
102
216
  const pkg = JSON.parse(fs.readFileSync(pkgPath, "utf8"));
103
- pkg.dependencies = pkg.dependencies ?? {};
104
- pkg.dependencies["@zenbujs/core"] = `link:${corePath}`;
217
+ const bucket = isPlugin ? "devDependencies" : "dependencies";
218
+ pkg[bucket] = pkg[bucket] ?? {};
219
+ pkg[bucket]["@zenbujs/core"] = `link:${corePath}`;
105
220
  fs.writeFileSync(pkgPath, JSON.stringify(pkg, null, 2) + "\n");
106
221
  }
222
+ /** Walk upward from `fromDir` looking for an ancestor with a `.git` dir. */
223
+ function findGitRoot(fromDir) {
224
+ let dir = path.resolve(fromDir);
225
+ while (true) {
226
+ if (fs.existsSync(path.join(dir, ".git"))) return dir;
227
+ const parent = path.dirname(dir);
228
+ if (parent === dir) return null;
229
+ dir = parent;
230
+ }
231
+ }
107
232
  function gitInitWithInitialCommit(projectDir) {
108
233
  if (spawnSync("git", [
109
234
  "init",
@@ -133,6 +258,66 @@ function gitInitWithInitialCommit(projectDir) {
133
258
  }
134
259
  });
135
260
  }
261
+ function appendPluginToHostConfig(hostConfigPath, entry) {
262
+ if (!fs.existsSync(hostConfigPath)) return "missing-file";
263
+ const raw = fs.readFileSync(hostConfigPath, "utf8");
264
+ const pluginsMatch = raw.match(/\bplugins\s*:\s*\[/);
265
+ if (!pluginsMatch) return "unsafe-shape";
266
+ const openIdx = pluginsMatch.index + pluginsMatch[0].length - 1;
267
+ let depth = 1;
268
+ let i = openIdx + 1;
269
+ while (i < raw.length && depth > 0) {
270
+ const ch = raw[i];
271
+ if (ch === "\"" || ch === "'" || ch === "`") {
272
+ const quote = ch;
273
+ i++;
274
+ while (i < raw.length && raw[i] !== quote) {
275
+ if (raw[i] === "\\") i++;
276
+ i++;
277
+ }
278
+ i++;
279
+ continue;
280
+ }
281
+ if (ch === "/" && raw[i + 1] === "/") {
282
+ while (i < raw.length && raw[i] !== "\n") i++;
283
+ continue;
284
+ }
285
+ if (ch === "/" && raw[i + 1] === "*") {
286
+ i += 2;
287
+ while (i < raw.length - 1 && !(raw[i] === "*" && raw[i + 1] === "/")) i++;
288
+ i += 2;
289
+ continue;
290
+ }
291
+ if (ch === "[") depth++;
292
+ else if (ch === "]") depth--;
293
+ i++;
294
+ }
295
+ if (depth !== 0) return "unsafe-shape";
296
+ const closeIdx = i - 1;
297
+ const arrayBody = raw.slice(openIdx + 1, closeIdx);
298
+ if (/\.\.\./.test(arrayBody)) return "unsafe-shape";
299
+ if (arrayBody.includes(JSON.stringify(entry))) return "already-present";
300
+ const lines = arrayBody.split("\n");
301
+ let indent = " ";
302
+ for (const line of lines) {
303
+ if (line.trim().length === 0) continue;
304
+ const m = line.match(/^[ \t]+/);
305
+ if (m) {
306
+ indent = m[0];
307
+ break;
308
+ }
309
+ }
310
+ const beforeClose = raw.slice(0, closeIdx);
311
+ const lastLineStart = beforeClose.lastIndexOf("\n") + 1;
312
+ const closeLineIndent = beforeClose.slice(lastLineStart).match(/^[ \t]*/)[0];
313
+ let head = raw.slice(0, closeIdx).replace(/\s*$/, "");
314
+ if (head.length > 0 && !head.endsWith(",") && !head.endsWith("[")) head += ",";
315
+ const insertion = `\n${indent}${JSON.stringify(entry)},\n${closeLineIndent}`;
316
+ const next = head + insertion + raw.slice(closeIdx);
317
+ if (next === raw) return "already-present";
318
+ fs.writeFileSync(hostConfigPath, next);
319
+ return "added";
320
+ }
136
321
  /**
137
322
  * Run `<pm> install` in the freshly-scaffolded project so the user can go
138
323
  * straight to `pnpm dev`/`bun dev`/etc. without the extra step. We run with
@@ -146,52 +331,176 @@ function runInstall(projectDir, pm) {
146
331
  stdio: "inherit"
147
332
  }).status === 0;
148
333
  }
149
- function main() {
150
- const projectName = positional[0] ?? ".";
151
- if (projectName === "." && !yes) {
152
- console.error("Usage: npm create zenbu-app <project-name>\n (use --yes to scaffold into the current directory)");
153
- process.exit(1);
334
+ /** Run `<pm> exec zen link [extraArgs...]` in `cwd`. */
335
+ function runZenLink(cwd, pm, extraArgs = []) {
336
+ const args = [
337
+ "exec",
338
+ "zen",
339
+ "link",
340
+ ...extraArgs
341
+ ];
342
+ return spawnSync(pm.type === "yarn" && pm.version.startsWith("1.") ? pm.type : pm.type, pm.type === "yarn" && pm.version.startsWith("1.") ? [
343
+ "zen",
344
+ "link",
345
+ ...extraArgs
346
+ ] : args, {
347
+ cwd,
348
+ stdio: "inherit"
349
+ }).status === 0;
350
+ }
351
+ function bail(reason) {
352
+ p.cancel(reason);
353
+ process.exit(1);
354
+ }
355
+ function validateProjectName(value) {
356
+ const trimmed = (value ?? "").trim();
357
+ if (!trimmed) return "Project name is required.";
358
+ if (trimmed === ".") return void 0;
359
+ if (/[\\/]/.test(trimmed)) return "Project name cannot contain slashes.";
360
+ if (/\s/.test(trimmed)) return "Project name cannot contain whitespace.";
361
+ if (/^[._]/.test(trimmed)) return "Project name cannot start with '.' or '_'.";
362
+ }
363
+ async function promptProjectName() {
364
+ const result = await p.text({
365
+ message: "Project name?",
366
+ placeholder: "my-zenbu-app",
367
+ defaultValue: "my-zenbu-app",
368
+ validate: validateProjectName
369
+ });
370
+ if (p.isCancel(result)) bail("Scaffolding cancelled.");
371
+ return result;
372
+ }
373
+ async function promptOptions() {
374
+ const answers = {};
375
+ for (const opt of CONFIG_OPTIONS) {
376
+ const value = await opt.ask();
377
+ if (p.isCancel(value)) bail("Scaffolding cancelled.");
378
+ answers[opt.id] = value;
154
379
  }
380
+ return answers;
381
+ }
382
+ function defaultAnswers() {
383
+ const answers = {};
384
+ for (const opt of CONFIG_OPTIONS) answers[opt.id] = opt.default;
385
+ return answers;
386
+ }
387
+ async function main() {
388
+ p.intro(pluginMode ? "create-zenbu-app (plugin)" : "create-zenbu-app");
389
+ let projectName;
390
+ if (positional[0]) projectName = positional[0];
391
+ else if (yes) projectName = ".";
392
+ else projectName = await promptProjectName();
155
393
  const projectDir = path.resolve(process.cwd(), projectName);
156
394
  const displayName = path.basename(projectDir);
157
395
  if (fs.existsSync(projectDir)) {
158
396
  const entries = fs.readdirSync(projectDir).filter((e) => e !== ".git");
159
- const allowedExisting = projectName === "." && fs.existsSync(path.join(projectDir, "package.json"));
160
- if (entries.length > 0 && !allowedExisting) {
161
- console.error(`Error: directory "${projectName}" already exists and is not empty.`);
162
- process.exit(1);
163
- }
397
+ const allowedExisting = (projectName === "." || projectDir === process.cwd()) && fs.existsSync(path.join(projectDir, "package.json"));
398
+ if (entries.length > 0 && !allowedExisting) bail(`Directory "${projectName}" already exists and is not empty.`);
164
399
  }
400
+ let templateDir;
401
+ let slug;
402
+ if (pluginMode) {
403
+ slug = "plugin";
404
+ templateDir = path.join(TEMPLATES_DIR, "plugin");
405
+ } else {
406
+ slug = resolveSlug(yes ? defaultAnswers() : await promptOptions());
407
+ templateDir = path.join(TEMPLATES_DIR, slug);
408
+ }
409
+ if (!fs.existsSync(templateDir)) bail(`No template found for configuration "${slug}".`);
165
410
  const pm = detectPackageManager();
166
- console.log(`\nScaffolding Zenbu app in "${displayName}"...`);
167
- if (pm.fallback) console.log(`couldn't detect invoking package manager; defaulting to ${pm.type}@${pm.version}.`);
168
- else console.log(`detected ${pm.type}@${pm.version} as the invoking package manager`);
169
- console.log("");
170
- copyDirSync(TEMPLATE_DIR, projectDir, displayName);
411
+ p.log.step(`Scaffolding Zenbu ${pluginMode ? "plugin" : "app"} in "${displayName}" (template: ${slug})`);
412
+ if (pm.fallback) p.log.info(`couldn't detect invoking package manager; defaulting to ${pm.type}@${pm.version}.`);
413
+ else p.log.info(`detected ${pm.type}@${pm.version} as the invoking package manager`);
414
+ const ctx = pluginMode ? {
415
+ projectName: displayName,
416
+ className: toPascalCase(displayName),
417
+ dependsOn: renderDependsOn(projectDir, dependsOn)
418
+ } : { projectName: displayName };
419
+ copyDirSync(templateDir, projectDir, ctx);
171
420
  const gi = path.join(projectDir, "_gitignore");
172
421
  if (fs.existsSync(gi)) fs.renameSync(gi, path.join(projectDir, ".gitignore"));
173
- seedPackageManager(projectDir, pm);
422
+ if (!pluginMode) seedPackageManager(projectDir, pm);
174
423
  if (ZENBU_LOCAL_CORE) {
175
424
  const corePath = path.resolve(ZENBU_LOCAL_CORE);
176
- rewireToLocalCore(projectDir, corePath);
177
- console.log(`linked @zenbujs/core -> ${corePath}`);
425
+ rewireToLocalCore(projectDir, corePath, pluginMode);
426
+ p.log.info(`linked @zenbujs/core -> ${corePath}`);
178
427
  }
179
428
  let installed = false;
180
429
  if (!noInstall) {
181
- console.log(`running ${pm.type} install\n`);
430
+ p.log.step(`running ${pm.type} install`);
182
431
  installed = runInstall(projectDir, pm);
183
- if (!installed) console.warn(` → ${pm.type} install failed; you can retry manually after the scaffold completes.\n`);
432
+ if (!installed) p.log.warn(`${pm.type} install failed; you can retry manually after the scaffold completes.`);
433
+ }
434
+ const hostsToLink = /* @__PURE__ */ new Set();
435
+ if (pluginMode && !noAddToHost) for (const dep of dependsOn) {
436
+ if (!path.basename(dep.from).startsWith("zenbu.config.")) continue;
437
+ const hostDir = path.dirname(dep.from);
438
+ const pluginManifestRel = relPosix(hostDir, path.join(projectDir, "zenbu.plugin.ts"));
439
+ let accept = yes;
440
+ if (!yes) {
441
+ const result = await p.confirm({
442
+ message: `Add "${pluginManifestRel}" to ${path.relative(process.cwd(), dep.from) || dep.from} plugins:?`,
443
+ initialValue: true
444
+ });
445
+ if (p.isCancel(result)) bail("Scaffolding cancelled.");
446
+ accept = !!result;
447
+ }
448
+ if (!accept) continue;
449
+ switch (appendPluginToHostConfig(dep.from, pluginManifestRel)) {
450
+ case "added":
451
+ p.log.success(`wired into ${dep.from}`);
452
+ hostsToLink.add(hostDir);
453
+ break;
454
+ case "already-present":
455
+ p.log.info(`already listed in ${dep.from}`);
456
+ hostsToLink.add(hostDir);
457
+ break;
458
+ case "missing-file":
459
+ p.log.warn(`host config not found: ${dep.from}`);
460
+ break;
461
+ case "unsafe-shape":
462
+ p.log.warn(`${dep.from}: couldn't safely edit plugins:[]. Add this manually:\n ${JSON.stringify(pluginManifestRel)},`);
463
+ break;
464
+ }
465
+ }
466
+ if (installed) if (pluginMode) if (hostsToLink.size > 0) for (const hostDir of hostsToLink) {
467
+ p.log.step(`running zen link in ${hostDir}`);
468
+ if (!runZenLink(hostDir, pm)) p.log.warn(`zen link failed in ${hostDir}`);
469
+ }
470
+ else {
471
+ p.log.step(`running zen link --plugin .`);
472
+ if (!runZenLink(projectDir, pm, ["--plugin", "."])) p.log.warn(`zen link --plugin . failed`);
473
+ }
474
+ else {
475
+ p.log.step(`running ${pm.type} run link`);
476
+ if (spawnSync(pm.type, ["run", "link"], {
477
+ cwd: projectDir,
478
+ stdio: "inherit"
479
+ }).status !== 0) p.log.warn(`${pm.type} run link failed; you can retry manually after the scaffold completes.`);
480
+ }
481
+ if (!noGit) {
482
+ const ancestorRepo = findGitRoot(projectDir);
483
+ if (ancestorRepo) p.log.info(`inside existing repo at ${ancestorRepo} — skipping git init`);
484
+ else {
485
+ let doInit = yes;
486
+ if (!yes) {
487
+ const result = await p.confirm({
488
+ message: "Initialize a git repo here?",
489
+ initialValue: true
490
+ });
491
+ if (p.isCancel(result)) bail("Scaffolding cancelled.");
492
+ doInit = !!result;
493
+ }
494
+ if (doInit) gitInitWithInitialCommit(projectDir);
495
+ }
184
496
  }
185
- if (!fs.existsSync(path.join(projectDir, ".git"))) gitInitWithInitialCommit(projectDir);
186
497
  const cdHint = projectName === "." ? "" : `cd ${displayName} && `;
187
- if (installed) console.log(`Done. Next:\n\n ${cdHint}${pm.type} dev\n`);
188
- else console.log(`Done. Next:\n\n ${cdHint}${pm.type} install\n ${pm.type} dev\n`);
498
+ const next = pluginMode ? installed ? `${cdHint}${pm.type} run typecheck` : `${cdHint}${pm.type} install` : installed ? `${cdHint}${pm.type} dev` : `${cdHint}${pm.type} install\n ${cdHint}${pm.type} dev`;
499
+ p.outro(`Done. Next:\n\n ${next}\n`);
189
500
  }
190
- try {
191
- main();
192
- } catch (err) {
501
+ main().catch((err) => {
193
502
  console.error("\nError:", err?.message ?? err);
194
503
  process.exit(1);
195
- }
504
+ });
196
505
  //#endregion
197
506
  export {};
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "create-zenbu-app",
3
- "version": "0.0.6",
3
+ "version": "0.0.9",
4
4
  "description": "Scaffold a new Zenbu app",
5
5
  "type": "module",
6
6
  "publishConfig": {
@@ -11,7 +11,7 @@
11
11
  },
12
12
  "files": [
13
13
  "dist/",
14
- "template/"
14
+ "templates/"
15
15
  ],
16
16
  "keywords": [
17
17
  "zenbu",
@@ -29,6 +29,9 @@
29
29
  "tsdown": "^0.21.10",
30
30
  "typescript": "^5.0.0"
31
31
  },
32
+ "dependencies": {
33
+ "@clack/prompts": "^1.3.0"
34
+ },
32
35
  "scripts": {
33
36
  "build": "tsdown",
34
37
  "typecheck": "tsc --noEmit -p tsconfig.json"
@@ -0,0 +1,3 @@
1
+ node_modules/
2
+ dist/
3
+ .zenbu/
@@ -0,0 +1,24 @@
1
+ {
2
+ "name": "{{projectName}}",
3
+ "version": "0.0.0",
4
+ "private": true,
5
+ "type": "module",
6
+ "scripts": {
7
+ "link": "zen link --plugin .",
8
+ "typecheck": "tsc --noEmit -p tsconfig.json"
9
+ },
10
+ "peerDependencies": {
11
+ "@zenbujs/core": "*",
12
+ "react": ">=18",
13
+ "react-dom": ">=18"
14
+ },
15
+ "devDependencies": {
16
+ "@zenbujs/core": "^0.0.13",
17
+ "@types/node": "^22.0.0",
18
+ "@types/react": "^19.0.0",
19
+ "@types/react-dom": "^19.0.0",
20
+ "react": "^19.0.0",
21
+ "react-dom": "^19.0.0",
22
+ "typescript": "^5.4.5"
23
+ }
24
+ }
@@ -0,0 +1,9 @@
1
+ import { Service } from "@zenbujs/core/runtime"
2
+
3
+ export class {{className}}Service extends Service.create({
4
+ key: "{{projectName}}",
5
+ }) {
6
+ evaluate() {
7
+ // Plugin starts here.
8
+ }
9
+ }
@@ -0,0 +1,14 @@
1
+ {
2
+ "compilerOptions": {
3
+ "target": "ESNext",
4
+ "module": "ESNext",
5
+ "moduleResolution": "bundler",
6
+ "jsx": "react-jsx",
7
+ "strict": true,
8
+ "esModuleInterop": true,
9
+ "skipLibCheck": true,
10
+ "noEmit": true,
11
+ "types": ["node"]
12
+ },
13
+ "include": ["src"]
14
+ }
@@ -0,0 +1,6 @@
1
+ import { definePlugin } from "@zenbujs/core/config"
2
+
3
+ export default definePlugin({
4
+ name: "{{projectName}}",
5
+ services: ["./src/main/services/*.ts"],{{dependsOn}}
6
+ })
@@ -12,7 +12,7 @@
12
12
  "db:generate": "zen db generate"
13
13
  },
14
14
  "dependencies": {
15
- "@zenbujs/core": "^0.0.9",
15
+ "@zenbujs/core": "^0.0.13",
16
16
  "@tailwindcss/vite": "^4.2.0",
17
17
  "@vitejs/plugin-react": "^5.0.0",
18
18
  "react": "^19.0.0",
@@ -0,0 +1,13 @@
1
+ import { createSchema, z } from "@zenbujs/core/db"
2
+
3
+ export default createSchema({
4
+ issues: z
5
+ .array(
6
+ z.object({
7
+ id: z.string(),
8
+ title: z.string(),
9
+ createdAt: z.number(),
10
+ }),
11
+ )
12
+ .default([]),
13
+ })
@@ -1,11 +1,11 @@
1
1
  import { Service } from "@zenbujs/core/runtime"
2
2
  import { WindowService } from "@zenbujs/core/services"
3
3
 
4
- export class AppService extends Service.create({
5
- key: "app",
4
+ export class InitService extends Service.create({
5
+ key: "init",
6
6
  deps: { window: WindowService },
7
7
  }) {
8
8
  async evaluate() {
9
- await this.ctx.window.openView({ type: "app" })
9
+ await this.ctx.window.openView({ type: "entrypoint" })
10
10
  }
11
11
  }