@penvhq/cli 0.6.0 → 0.7.0

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/dist/index.d.cts CHANGED
@@ -310,6 +310,8 @@ interface InitOptions {
310
310
  readonly cwd: string;
311
311
  /** What to write. Omitted means the plan's defaults, as `--yes` takes them. */
312
312
  readonly decisions?: InitDecisions;
313
+ /** The detected framework name, passed by the command so the seam step need not re-detect it. */
314
+ readonly framework?: string;
313
315
  }
314
316
  interface AliasEdit {
315
317
  readonly source: string;
package/dist/index.d.ts CHANGED
@@ -310,6 +310,8 @@ interface InitOptions {
310
310
  readonly cwd: string;
311
311
  /** What to write. Omitted means the plan's defaults, as `--yes` takes them. */
312
312
  readonly decisions?: InitDecisions;
313
+ /** The detected framework name, passed by the command so the seam step need not re-detect it. */
314
+ readonly framework?: string;
313
315
  }
314
316
  interface AliasEdit {
315
317
  readonly source: string;
package/dist/index.js CHANGED
@@ -2833,7 +2833,7 @@ function occupied(cwd, relative3) {
2833
2833
  return existsSync(file) && !exportsSchema(file);
2834
2834
  }
2835
2835
  function schemaFileFor(cwd) {
2836
- const dir = existsSync(join(cwd, "src")) ? "src/" : "";
2836
+ const dir = srcPrefix(cwd);
2837
2837
  const preferred = `${dir}env.ts`;
2838
2838
  if (!occupied(cwd, preferred)) {
2839
2839
  return { file: preferred };
@@ -2844,6 +2844,9 @@ function schemaFileFor(cwd) {
2844
2844
  }
2845
2845
  return { file: DEFAULT_SCHEMA_FILE, displaced: preferred };
2846
2846
  }
2847
+ function srcPrefix(cwd) {
2848
+ return existsSync(join(cwd, "src")) ? "src/" : "";
2849
+ }
2847
2850
  function dependenciesOf(cwd) {
2848
2851
  const manifest = manifestOf(cwd);
2849
2852
  if (manifest === void 0) {
@@ -2990,19 +2993,25 @@ function bun({ alias }) {
2990
2993
  kind: "scaffold",
2991
2994
  file: ".penv/preload.ts",
2992
2995
  content: `// Bun evaluates this file before your entry point, because it is listed under
2993
- // \`preload\` in bunfig.toml (see the note penv printed). Running it injects your
2994
- // config into process.env before any library reads it.
2996
+ // \`preload\` in bunfig.toml. Running it injects your config into process.env
2997
+ // before any library reads it.
2995
2998
  import "${alias}";
2996
2999
  `,
2997
3000
  ifPresent: `Keep .penv/preload.ts as \`import "${alias}";\`.`,
2998
- notes: [
2999
- 'Register it in bunfig.toml so Bun runs it: `preload = ["./.penv/preload.ts"]` (and the same under `[test]`).',
3000
- `\`${alias}\` must be a tsconfig \`paths\` alias for Bun to resolve it.`
3001
- ]
3001
+ notes: [`\`${alias}\` must be a tsconfig \`paths\` alias for Bun to resolve it.`],
3002
+ // The preload file is inert until bunfig.toml points at it — so penv writes
3003
+ // that too, both for the runtime and for `bun test`. Only when there is no
3004
+ // bunfig to overwrite; an existing one is the user's, and penv prints instead.
3005
+ also: {
3006
+ file: "bunfig.toml",
3007
+ content: 'preload = ["./.penv/preload.ts"]\n\n[test]\npreload = ["./.penv/preload.ts"]\n',
3008
+ ifPresent: 'Add "./.penv/preload.ts" to the `preload` array in bunfig.toml \u2014 and mirror it under `[test]` so `bun test` injects too.'
3009
+ }
3002
3010
  };
3003
3011
  }
3004
- function node({ alias, srcDir }) {
3005
- const runtimeSpecifier = alias.startsWith("#") ? alias : `./${srcDir}.penv/env.js`;
3012
+ function node({ alias, schemaFile }) {
3013
+ const builtSchema = `./${schemaFile.replace(/\.ts$/, ".js")}`;
3014
+ const runtimeSpecifier = alias.startsWith("#") ? alias : builtSchema;
3006
3015
  return {
3007
3016
  kind: "instruct",
3008
3017
  instruction: `Plain Node / Express / Fastify have no framework hook that runs before your
@@ -3022,10 +3031,17 @@ import before it. If you have no server.ts yet, create it from TanStack's server
3022
3031
  template and put that import first.`
3023
3032
  };
3024
3033
  }
3025
- function astro(_context) {
3034
+ function astro({ alias }) {
3026
3035
  return {
3027
3036
  kind: "instruct",
3028
- instruction: 'Astro has no single pre-app hook. Add a small integration to astro.config that\ninjects the import into every SSR page, then cover the routes it cannot reach:\n \u2022 In astro.config integrations, add:\n { name: "penv", hooks: { "astro:config:setup": ({ injectScript }) =>\n injectScript("page-ssr", \'import "@env";\') } }\n \u2022 Also add `import "@env";` as the first line of src/middleware.ts and of any\n endpoint route (src/pages/*.ts) that imports a library reading process.env.\nInjection only applies under a server (on-demand) adapter, not a static build.'
3037
+ instruction: `Astro has no single pre-app hook. Add a small integration to astro.config that
3038
+ injects the import into every SSR page, then cover the routes it cannot reach:
3039
+ \u2022 In astro.config integrations, add:
3040
+ { name: "penv", hooks: { "astro:config:setup": ({ injectScript }) =>
3041
+ injectScript("page-ssr", 'import "${alias}";') } }
3042
+ \u2022 Also add \`import "${alias}";\` as the first line of src/middleware.ts and of any
3043
+ endpoint route (src/pages/*.ts) that imports a library reading process.env.
3044
+ Injection only applies under a server (on-demand) adapter, not a static build.`
3029
3045
  };
3030
3046
  }
3031
3047
  function viteSpa(_context) {
@@ -3226,17 +3242,18 @@ async function promptForDecisions(plan2, io) {
3226
3242
  );
3227
3243
  io.write("");
3228
3244
  }
3229
- const inject = seamKindFor(plan2) === "none" ? false : await askInject(io);
3230
3245
  const proceed = (await io.ask(prompt("Proceed?", "Y/n"))).trim().toLowerCase();
3231
3246
  if (proceed.length > 0 && proceed !== "y" && proceed !== "yes") {
3232
3247
  return void 0;
3233
3248
  }
3249
+ const inject = seamKindFor(plan2) === "none" ? false : await askInject(io);
3234
3250
  return { ...plan2.decisions, environments, inject };
3235
3251
  }
3236
3252
  function seamKindFor(plan2) {
3237
3253
  return seamFor(plan2.detected?.name, {
3238
3254
  alias: plan2.decisions.alias,
3239
- srcDir: plan2.decisions.schemaFile.startsWith("src/") ? "src/" : ""
3255
+ srcDir: plan2.decisions.schemaFile.startsWith("src/") ? "src/" : "",
3256
+ schemaFile: plan2.decisions.schemaFile
3240
3257
  }).kind;
3241
3258
  }
3242
3259
  async function askInject(io) {
@@ -3631,15 +3648,48 @@ function writeGitignore(root, decisions = DEFAULT_DECISIONS) {
3631
3648
  text: `${existing === void 0 ? "Created" : "Updated"} ${relative3}`
3632
3649
  };
3633
3650
  }
3634
- function srcDirOf(root) {
3635
- return existsSync2(join2(root, "src")) ? "src/" : "";
3651
+ var INJECT_MIN_VERSION = "0.6.0";
3652
+ function outdatedRuntimeWarning(root) {
3653
+ const version = installedPenvVersion(root);
3654
+ if (version === void 0 || !isBelow(version, INJECT_MIN_VERSION)) {
3655
+ return void 0;
3656
+ }
3657
+ return `Injection needs @penvhq/penv ${INJECT_MIN_VERSION}+ \u2014 this project has ${version}, whose \`load\` ignores \`{ inject: true }\`. Upgrade, or process.env stays empty.`;
3658
+ }
3659
+ function installedPenvVersion(root) {
3660
+ const file = join2(root, "node_modules", "@penvhq", "penv", "package.json");
3661
+ if (!existsSync2(file)) {
3662
+ return void 0;
3663
+ }
3664
+ try {
3665
+ const version = JSON.parse(readFileSync2(file, "utf8")).version;
3666
+ return typeof version === "string" ? version : void 0;
3667
+ } catch {
3668
+ return void 0;
3669
+ }
3670
+ }
3671
+ function releaseTriple(version) {
3672
+ const parts = (version.split("-")[0] ?? "").split(".");
3673
+ const at = (i) => Number.parseInt(parts[i] ?? "0", 10) || 0;
3674
+ return [at(0), at(1), at(2)];
3675
+ }
3676
+ function isBelow(a, b) {
3677
+ const [am, an, ap] = releaseTriple(a);
3678
+ const [bm, bn, bp] = releaseTriple(b);
3679
+ if (am !== bm) return am < bm;
3680
+ if (an !== bn) return an < bn;
3681
+ return ap < bp;
3636
3682
  }
3637
- function writeSeam(root, decisions = DEFAULT_DECISIONS) {
3683
+ function writeSeam(root, decisions = DEFAULT_DECISIONS, framework = detectFramework(root)?.name) {
3638
3684
  if (!decisions.inject) {
3639
3685
  return void 0;
3640
3686
  }
3641
- const framework = detectFramework(root)?.name;
3642
- const seam = seamFor(framework, { alias: decisions.alias, srcDir: srcDirOf(root) });
3687
+ const seam = seamFor(framework, {
3688
+ alias: decisions.alias,
3689
+ srcDir: srcPrefix(root),
3690
+ schemaFile: decisions.schemaFile
3691
+ });
3692
+ const outdated = outdatedRuntimeWarning(root);
3643
3693
  if (seam.kind === "none") {
3644
3694
  return { target: "seam", action: "info", text: "No injection seam needed", note: seam.reason };
3645
3695
  }
@@ -3648,30 +3698,51 @@ function writeSeam(root, decisions = DEFAULT_DECISIONS) {
3648
3698
  target: "seam",
3649
3699
  action: "info",
3650
3700
  text: "Place the injection seam",
3651
- note: seam.instruction
3701
+ note: withWarning(seam.instruction, outdated)
3652
3702
  };
3653
3703
  }
3704
+ const alsoNote = writeAlso(root, seam.also);
3654
3705
  const file = join2(root, ...seam.file.split("/"));
3655
- const notes = seam.notes.length === 0 ? "" : `
3656
- ${seam.notes.map((n) => ` ${n}`).join("\n")}`;
3706
+ const baseNotes = [...seam.notes, ...alsoNote === void 0 ? [] : [alsoNote]];
3707
+ const notes = baseNotes.length === 0 ? "" : `
3708
+ ${baseNotes.map((n) => ` ${n}`).join("\n")}`;
3657
3709
  if (existsSync2(file)) {
3658
3710
  return {
3659
3711
  target: "seam",
3660
3712
  action: "info",
3661
3713
  text: `Add the injection seam to ${seam.file}`,
3662
- note: `${seam.ifPresent}${notes}`
3714
+ note: withWarning(`${seam.ifPresent}${notes}`, outdated)
3663
3715
  };
3664
3716
  }
3665
3717
  mkdirSync(dirname3(file), { recursive: true });
3666
3718
  writeFileSync2(file, seam.content, "utf8");
3667
3719
  return {
3668
3720
  target: "seam",
3669
- action: "created",
3721
+ action: outdated === void 0 ? "created" : "info",
3670
3722
  text: `Wrote ${seam.file} (runs the injection before your app)`,
3671
- ...notes === "" ? {} : { note: notes.trimStart() }
3723
+ ...notes === "" && outdated === void 0 ? {} : { note: withWarning(notes.trimStart(), outdated) }
3672
3724
  };
3673
3725
  }
3674
- function scaffold(root, fields, draft, decisions = DEFAULT_DECISIONS) {
3726
+ function writeAlso(root, also) {
3727
+ if (also === void 0) {
3728
+ return void 0;
3729
+ }
3730
+ const file = join2(root, ...also.file.split("/"));
3731
+ if (existsSync2(file)) {
3732
+ return also.ifPresent;
3733
+ }
3734
+ mkdirSync(dirname3(file), { recursive: true });
3735
+ writeFileSync2(file, also.content, "utf8");
3736
+ return `Wrote ${also.file} to register it.`;
3737
+ }
3738
+ function withWarning(note, warning) {
3739
+ if (warning === void 0) {
3740
+ return note;
3741
+ }
3742
+ return note === "" ? warning : `${note}
3743
+ \u26A0 ${warning}`;
3744
+ }
3745
+ function scaffold(root, fields, draft, decisions = DEFAULT_DECISIONS, framework = detectFramework(root)?.name) {
3675
3746
  const steps = [
3676
3747
  ensurePenvDir(root),
3677
3748
  writeSchemaFile(root, fields, draft, decisions),
@@ -3679,13 +3750,13 @@ function scaffold(root, fields, draft, decisions = DEFAULT_DECISIONS) {
3679
3750
  writeTsconfigAlias(root, decisions),
3680
3751
  writeGitignore(root, decisions)
3681
3752
  ];
3682
- const seam = writeSeam(root, decisions);
3753
+ const seam = writeSeam(root, decisions, framework);
3683
3754
  return seam === void 0 ? steps : [...steps, seam];
3684
3755
  }
3685
3756
  function runInit(options) {
3686
3757
  const root = resolve4(options.cwd);
3687
3758
  const decisions = options.decisions ?? planInit(root).decisions;
3688
- return { root, decisions, steps: scaffold(root, [], false, decisions) };
3759
+ return { root, decisions, steps: scaffold(root, [], false, decisions, options.framework) };
3689
3760
  }
3690
3761
  function renderInit(result2) {
3691
3762
  const steps = result2.steps.map((step) => {
@@ -3752,7 +3823,15 @@ var initCommand = defineCommand9({
3752
3823
  if (!asked) {
3753
3824
  write([...plan2.notes, ""]);
3754
3825
  }
3755
- write(renderInit(runInit({ cwd: root, decisions })));
3826
+ write(
3827
+ renderInit(
3828
+ runInit({
3829
+ cwd: root,
3830
+ decisions,
3831
+ ...plan2.detected && { framework: plan2.detected.name }
3832
+ })
3833
+ )
3834
+ );
3756
3835
  });
3757
3836
  }
3758
3837
  });