@pylonsync/create-pylon 0.3.277 → 0.3.279

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 (2) hide show
  1. package/bin/create-pylon.js +89 -39
  2. package/package.json +4 -1
@@ -42,7 +42,12 @@ import { stdin, stdout, exit, argv, cwd } from "node:process";
42
42
  // ---------------------------------------------------------------------------
43
43
 
44
44
  const HERE = dirname(fileURLToPath(import.meta.url));
45
- const TEMPLATES = resolve(HERE, "..", "templates");
45
+ // PYLON_CREATE_TEMPLATES_DIR overrides the template source dir. Only used by
46
+ // the scaffolder's own tests (to point at a fixture / empty dir and exercise
47
+ // the missing-template guard); undocumented + irrelevant in normal use.
48
+ const TEMPLATES = process.env.PYLON_CREATE_TEMPLATES_DIR
49
+ ? resolve(process.env.PYLON_CREATE_TEMPLATES_DIR)
50
+ : resolve(HERE, "..", "templates");
46
51
 
47
52
  // ---------------------------------------------------------------------------
48
53
  // Version pin — every generated dep references this version of @pylonsync/*.
@@ -257,22 +262,35 @@ Examples:
257
262
  }
258
263
 
259
264
  const rl = createInterface({ input: stdin, output: stdout });
265
+ // Non-interactive stdin (CI, piped, `| npm create …`): never block on a prompt.
266
+ // Fall back to the same defaults the interactive path uses. The skill-install
267
+ // below already gates on `stdin.isTTY`; the INPUT prompts must too — without
268
+ // this, `npm create @pylonsync/pylon my-app` (or any partial-flag invocation)
269
+ // HANGS forever in CI waiting on input that never comes.
270
+ const isInteractive = !!stdin.isTTY;
260
271
  if (!projectName) {
261
- projectName = (await rl.question("Project name: ")).trim() || "my-pylon-app";
272
+ projectName =
273
+ (isInteractive ? (await rl.question("Project name: ")).trim() : "") ||
274
+ "my-pylon-app";
262
275
  }
263
276
  if (!flags.template) {
264
- const lines = Object.entries(TEMPLATE_REGISTRY)
265
- .map(([k, v]) => ` ${k.padEnd(10)} ${v.blurb}`)
266
- .join("\n");
267
- process.stdout.write(`\n${lines}\n`);
268
- const ans = (
269
- await rl.question(
270
- `Template (${TEMPLATES_AVAILABLE.join(", ")}) [default]: `,
277
+ if (isInteractive) {
278
+ const lines = Object.entries(TEMPLATE_REGISTRY)
279
+ .map(([k, v]) => ` ${k.padEnd(10)} ${v.blurb}`)
280
+ .join("\n");
281
+ process.stdout.write(`\n${lines}\n`);
282
+ const ans = (
283
+ await rl.question(
284
+ `Template (${TEMPLATES_AVAILABLE.join(", ")}) [default]: `,
285
+ )
271
286
  )
272
- )
273
- .trim()
274
- .toLowerCase();
275
- flags.template = TEMPLATES_AVAILABLE.includes(ans) ? ans : "default";
287
+ .trim()
288
+ .toLowerCase();
289
+ flags.template = TEMPLATES_AVAILABLE.includes(ans) ? ans : "default";
290
+ } else {
291
+ console.log("Non-interactive stdin — using --template default.");
292
+ flags.template = "default";
293
+ }
276
294
  }
277
295
  // `ssr` was the original name of the default template; keep it working as a
278
296
  // quiet alias so older `--template ssr` invocations don't break.
@@ -286,32 +304,44 @@ if (flags.template === "saas") flags.template = "default";
286
304
  const isUnified = TEMPLATE_REGISTRY[flags.template]?.unified === true;
287
305
  if (!isUnified && !flags.platforms) {
288
306
  const supported = TEMPLATE_REGISTRY[flags.template].platforms.join(", ");
289
- const ans = (
290
- await rl.question(
291
- `Platforms for ${flags.template} (${supported}, comma-separated) [web]: `,
292
- )
293
- ).trim();
307
+ const ans = isInteractive
308
+ ? (
309
+ await rl.question(
310
+ `Platforms for ${flags.template} (${supported}, comma-separated) [web]: `,
311
+ )
312
+ ).trim()
313
+ : "";
294
314
  flags.platforms = ans || "web";
295
315
  }
296
316
  if (!flags.pm) {
297
317
  const detected = detectPackageManager();
298
318
  const def = detected ?? "bun";
299
- const choice = (
300
- await rl.question(`Package manager (bun, pnpm, yarn, npm) [${def}]: `)
301
- )
302
- .trim()
303
- .toLowerCase();
304
- flags.pm = ["bun", "pnpm", "yarn", "npm"].includes(choice) ? choice : def;
319
+ if (isInteractive) {
320
+ const choice = (
321
+ await rl.question(`Package manager (bun, pnpm, yarn, npm) [${def}]: `)
322
+ )
323
+ .trim()
324
+ .toLowerCase();
325
+ flags.pm = ["bun", "pnpm", "yarn", "npm"].includes(choice) ? choice : def;
326
+ } else {
327
+ flags.pm = def;
328
+ }
305
329
  }
306
330
  if (flags.skill === undefined) {
307
- const ans = (
308
- await rl.question(
309
- "Add the Pylon skill to your coding agent (Claude Code / Codex / Cursor)? [Y/n]: ",
331
+ if (isInteractive) {
332
+ const ans = (
333
+ await rl.question(
334
+ "Add the Pylon skill to your coding agent (Claude Code / Codex / Cursor)? [Y/n]: ",
335
+ )
310
336
  )
311
- )
312
- .trim()
313
- .toLowerCase();
314
- flags.skill = ans !== "n" && ans !== "no";
337
+ .trim()
338
+ .toLowerCase();
339
+ flags.skill = ans !== "n" && ans !== "no";
340
+ } else {
341
+ // Non-interactive: can't prompt. The actual install is TTY-gated below,
342
+ // so this only controls whether the footer prints the one-liner hint.
343
+ flags.skill = true;
344
+ }
315
345
  }
316
346
  rl.close();
317
347
 
@@ -462,6 +492,26 @@ function copyTemplate(srcSubpath, destSubpath = "") {
462
492
  return true;
463
493
  }
464
494
 
495
+ // Like copyTemplate but FATAL when the source is missing. A required
496
+ // template dir that isn't on disk means a corrupt/partial create-pylon
497
+ // install (or a published tarball that dropped files) — historically this
498
+ // scaffolded an EMPTY project and STILL printed "✓ Created", sending the
499
+ // user to a dead `pylon dev` with no clue why. Fail loud + actionable
500
+ // instead. (The @pylonsync/client publish-drop that broke the default
501
+ // scaffold is exactly this failure mode.)
502
+ function mustCopy(srcSubpath, destSubpath = "", label = srcSubpath) {
503
+ if (!copyTemplate(srcSubpath, destSubpath)) {
504
+ console.error(
505
+ `\nError: template files for "${label}" are missing from this install\n` +
506
+ ` (expected ${join(TEMPLATES, srcSubpath)}).\n` +
507
+ ` This usually means a corrupt or partial create-pylon install.\n` +
508
+ ` Re-run with @latest: npm create @pylonsync/pylon@latest\n` +
509
+ ` or report it: https://github.com/pylonsync/pylon/issues\n`,
510
+ );
511
+ exit(1);
512
+ }
513
+ }
514
+
465
515
  // ---------------------------------------------------------------------------
466
516
  // Apply templates in order:
467
517
  // 1. _root — gitignore, env.example, README
@@ -475,24 +525,24 @@ if (isUnified) {
475
525
  // Single unified app: app.ts + app/ routes + functions/, served by
476
526
  // `pylon dev` (frontend + API, one port). The template ships its own
477
527
  // package.json — no monorepo root, no turbo, no workspaces.
478
- copyTemplate(flags.template);
528
+ mustCopy(flags.template);
479
529
  } else {
480
- copyTemplate("_root");
481
- copyTemplate(`backend/${flags.template}`);
530
+ mustCopy("_root");
531
+ mustCopy(`backend/${flags.template}`);
482
532
 
483
533
  // `web` (Next.js) and `vite` are alternative web-frontend toolchains;
484
534
  // the mutex check above guarantees at most one of them is set. Either
485
535
  // way we also pull in packages/ui so the shared primitives are present.
486
536
  if (platforms.includes("web")) {
487
- copyTemplate("ui");
488
- copyTemplate(`web/${flags.template}`);
537
+ mustCopy("ui");
538
+ mustCopy(`web/${flags.template}`);
489
539
  }
490
540
  if (platforms.includes("vite")) {
491
- copyTemplate("ui");
492
- copyTemplate(`vite/${flags.template}`);
541
+ mustCopy("ui");
542
+ mustCopy(`vite/${flags.template}`);
493
543
  }
494
544
  for (const p of ["ios", "mac", "expo"]) {
495
- if (platforms.includes(p)) copyTemplate(`${p}/${flags.template}`);
545
+ if (platforms.includes(p)) mustCopy(`${p}/${flags.template}`);
496
546
  }
497
547
  }
498
548
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pylonsync/create-pylon",
3
- "version": "0.3.277",
3
+ "version": "0.3.279",
4
4
  "description": "Scaffold a new Pylon app — realtime backend + web/mobile/expo frontends in one command. Run via `npm create @pylonsync/pylon@latest`.",
5
5
  "publishConfig": {
6
6
  "access": "public"
@@ -9,6 +9,9 @@
9
9
  "bin": {
10
10
  "create-pylon": "./bin/create-pylon.js"
11
11
  },
12
+ "scripts": {
13
+ "test": "node --test"
14
+ },
12
15
  "files": [
13
16
  "bin",
14
17
  "templates"