@penvhq/cli 0.5.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.cjs CHANGED
@@ -368,19 +368,19 @@ function refsFrom(files) {
368
368
 
369
369
  // src/schema.ts
370
370
  var import_core4 = require("@penvhq/core");
371
- function defOf(node) {
372
- if (typeof node !== "object" || node === null) {
371
+ function defOf(node2) {
372
+ if (typeof node2 !== "object" || node2 === null) {
373
373
  return void 0;
374
374
  }
375
- const def = node.def;
375
+ const def = node2.def;
376
376
  return typeof def === "object" && def !== null ? def : void 0;
377
377
  }
378
- function typeOf(node) {
379
- const type = defOf(node)?.type;
378
+ function typeOf(node2) {
379
+ const type = defOf(node2)?.type;
380
380
  return typeof type === "string" ? type : void 0;
381
381
  }
382
- function unwrap(node) {
383
- let current = node;
382
+ function unwrap(node2) {
383
+ let current = node2;
384
384
  for (let depth = 0; depth < 8; depth += 1) {
385
385
  const inner = defOf(current)?.innerType;
386
386
  if (inner === void 0) {
@@ -390,38 +390,38 @@ function unwrap(node) {
390
390
  }
391
391
  return current;
392
392
  }
393
- function shapeOf(node) {
394
- if (typeOf(node) !== "object") {
393
+ function shapeOf(node2) {
394
+ if (typeOf(node2) !== "object") {
395
395
  return void 0;
396
396
  }
397
- const shape = node.shape;
397
+ const shape = node2.shape;
398
398
  return typeof shape === "object" && shape !== null ? shape : void 0;
399
399
  }
400
400
  function lookup(root, path) {
401
- let node = unwrap(root);
401
+ let node2 = unwrap(root);
402
402
  for (const key of path) {
403
- const shape = shapeOf(node);
403
+ const shape = shapeOf(node2);
404
404
  if (shape === void 0) {
405
405
  return { kind: "unknown" };
406
406
  }
407
407
  if (!Object.hasOwn(shape, key)) {
408
408
  return { kind: "absent" };
409
409
  }
410
- node = unwrap(shape[key]);
410
+ node2 = unwrap(shape[key]);
411
411
  }
412
- return { kind: "found", node };
412
+ return { kind: "found", node: node2 };
413
413
  }
414
- function minLengthOf(node) {
415
- if (typeOf(node) !== "string") {
414
+ function minLengthOf(node2) {
415
+ if (typeOf(node2) !== "string") {
416
416
  return void 0;
417
417
  }
418
- const min = node.minLength;
418
+ const min = node2.minLength;
419
419
  return typeof min === "number" ? min : void 0;
420
420
  }
421
421
  var ABSENCE_PERMITTED = /* @__PURE__ */ new Set(["optional", "default", "catch", "prefault"]);
422
422
  var ABSENCE_REFUSED = /* @__PURE__ */ new Set(["nullable", "nonoptional", "readonly"]);
423
- function permitsAbsence(node) {
424
- let current = node;
423
+ function permitsAbsence(node2) {
424
+ let current = node2;
425
425
  for (let depth = 0; depth < 8; depth += 1) {
426
426
  const type = typeOf(current);
427
427
  if (type !== void 0 && ABSENCE_PERMITTED.has(type)) {
@@ -438,13 +438,13 @@ function permitsAbsence(node) {
438
438
  }
439
439
  return void 0;
440
440
  }
441
- function leaves(node, path, inherited, out2) {
442
- const own = permitsAbsence(node);
441
+ function leaves(node2, path, inherited, out2) {
442
+ const own = permitsAbsence(node2);
443
443
  const absencePermitted = inherited === true || own === true ? true : combine(inherited, own);
444
- const shape = shapeOf(unwrap(node));
444
+ const shape = shapeOf(unwrap(node2));
445
445
  if (shape === void 0) {
446
446
  if (path.length > 0) {
447
- out2.push({ path, absencePermitted, node });
447
+ out2.push({ path, absencePermitted, node: node2 });
448
448
  }
449
449
  return;
450
450
  }
@@ -460,8 +460,8 @@ function declaredLeaves(schema) {
460
460
  leaves(schema, [], false, out2);
461
461
  return out2;
462
462
  }
463
- function defaultValueOf(node) {
464
- let current = node;
463
+ function defaultValueOf(node2) {
464
+ let current = node2;
465
465
  for (let depth = 0; depth < 8; depth += 1) {
466
466
  const def = defOf(current);
467
467
  if (def === void 0) {
@@ -1111,18 +1111,18 @@ function place(root, path, value) {
1111
1111
  if (leaf === void 0) {
1112
1112
  return;
1113
1113
  }
1114
- let node = root;
1114
+ let node2 = root;
1115
1115
  for (const key of path.slice(0, -1)) {
1116
- const existing = node[key];
1116
+ const existing = node2[key];
1117
1117
  if (typeof existing === "object" && existing !== null) {
1118
- node = existing;
1118
+ node2 = existing;
1119
1119
  continue;
1120
1120
  }
1121
1121
  const child = {};
1122
- node[key] = child;
1123
- node = child;
1122
+ node2[key] = child;
1123
+ node2 = child;
1124
1124
  }
1125
- node[leaf] = value;
1125
+ node2[leaf] = value;
1126
1126
  }
1127
1127
  function isZodType(value) {
1128
1128
  return typeof value === "object" && value !== null && "safeParse" in value && typeof value.safeParse === "function";
@@ -2775,6 +2775,8 @@ var import_node_path5 = require("path");
2775
2775
  var import_core14 = require("@penvhq/core");
2776
2776
  var SIGNATURES = [
2777
2777
  { name: "Next.js", packages: ["next"], publicPrefixes: ["NEXT_PUBLIC_"] },
2778
+ { name: "SvelteKit", packages: ["@sveltejs/kit"], publicPrefixes: ["PUBLIC_"] },
2779
+ { name: "Nuxt", packages: ["nuxt"], publicPrefixes: ["NUXT_PUBLIC_"] },
2778
2780
  {
2779
2781
  name: "TanStack Start",
2780
2782
  packages: ["@tanstack/react-start", "@tanstack/start"],
@@ -2801,7 +2803,7 @@ function occupied(cwd, relative3) {
2801
2803
  return (0, import_node_fs2.existsSync)(file) && !exportsSchema(file);
2802
2804
  }
2803
2805
  function schemaFileFor(cwd) {
2804
- const dir = (0, import_node_fs2.existsSync)((0, import_node_path5.join)(cwd, "src")) ? "src/" : "";
2806
+ const dir = srcPrefix(cwd);
2805
2807
  const preferred = `${dir}env.ts`;
2806
2808
  if (!occupied(cwd, preferred)) {
2807
2809
  return { file: preferred };
@@ -2812,6 +2814,9 @@ function schemaFileFor(cwd) {
2812
2814
  }
2813
2815
  return { file: import_core14.DEFAULT_SCHEMA_FILE, displaced: preferred };
2814
2816
  }
2817
+ function srcPrefix(cwd) {
2818
+ return (0, import_node_fs2.existsSync)((0, import_node_path5.join)(cwd, "src")) ? "src/" : "";
2819
+ }
2815
2820
  function dependenciesOf(cwd) {
2816
2821
  const manifest = manifestOf(cwd);
2817
2822
  if (manifest === void 0) {
@@ -2848,17 +2853,23 @@ function detectFramework(cwd) {
2848
2853
  }
2849
2854
  for (const signature of SIGNATURES) {
2850
2855
  if (signature.packages.some((name) => dependencies.has(name))) {
2851
- const schema = schemaFileFor(cwd);
2852
- return {
2853
- name: signature.name,
2854
- schemaFile: schema.file,
2855
- ...schema.displaced === void 0 ? {} : { displacedFrom: schema.displaced },
2856
- publicPrefixes: signature.publicPrefixes
2857
- };
2856
+ return detectedFrom(cwd, signature.name, signature.publicPrefixes);
2858
2857
  }
2859
2858
  }
2859
+ if ((0, import_node_fs2.existsSync)((0, import_node_path5.join)(cwd, "bunfig.toml")) || dependencies.has("bun-types")) {
2860
+ return detectedFrom(cwd, "Bun", []);
2861
+ }
2860
2862
  return void 0;
2861
2863
  }
2864
+ function detectedFrom(cwd, name, publicPrefixes) {
2865
+ const schema = schemaFileFor(cwd);
2866
+ return {
2867
+ name,
2868
+ schemaFile: schema.file,
2869
+ ...schema.displaced === void 0 ? {} : { displacedFrom: schema.displaced },
2870
+ publicPrefixes
2871
+ };
2872
+ }
2862
2873
  var DEFAULT_ALIAS = "@env";
2863
2874
  var IMPORTS_ALIAS = "#env";
2864
2875
  function detectAlias(cwd) {
@@ -2876,6 +2887,145 @@ var import_node_path6 = require("path");
2876
2887
  var import_promises = require("readline/promises");
2877
2888
  var import_core15 = require("@penvhq/core");
2878
2889
  var import_citty9 = require("citty");
2890
+
2891
+ // src/seams.ts
2892
+ function nextjs({ alias, srcDir }) {
2893
+ return {
2894
+ kind: "scaffold",
2895
+ file: `${srcDir}instrumentation.ts`,
2896
+ content: `// This is instrumentation.ts, Next.js's own startup hook \u2014 Next runs it once,
2897
+ // before any of your app code, on every server boot. penv put one line here so
2898
+ // your config is in process.env before the first library reads it. The filename
2899
+ // is Next's requirement (it only finds \`instrumentation.ts\`), so keep it as-is.
2900
+ export async function register() {
2901
+ // Next also calls register on the Edge runtime, where penv cannot read the
2902
+ // filesystem \u2014 so inject runs on the Node runtime only.
2903
+ if (process.env.NEXT_RUNTIME === "nodejs") {
2904
+ await import("${alias}");
2905
+ }
2906
+ }
2907
+ `,
2908
+ ifPresent: `Add to your existing \`${srcDir}instrumentation.ts\`, inside \`register()\` and first:
2909
+ if (process.env.NEXT_RUNTIME === "nodejs") { await import("${alias}"); }`,
2910
+ notes: []
2911
+ };
2912
+ }
2913
+ function sveltekit({ alias }) {
2914
+ return {
2915
+ kind: "scaffold",
2916
+ // SvelteKit's hooks module is always under `src/`, whatever the schema path.
2917
+ file: "src/hooks.server.ts",
2918
+ content: `// This is hooks.server.ts, SvelteKit's server-startup module \u2014 it loads before
2919
+ // your routes handle a request. penv put this import first so your config is in
2920
+ // process.env before any library reads it. Keep it the first line; the name and
2921
+ // location are SvelteKit's, not penv's.
2922
+ import "${alias}";
2923
+ `,
2924
+ ifPresent: `Add \`import "${alias}";\` as the FIRST line of src/hooks.server.ts, above every other import.`,
2925
+ notes: [
2926
+ `SvelteKit resolves app aliases through \`kit.alias\` \u2014 make sure \`${alias}\` is registered in svelte.config.js, not only tsconfig.`
2927
+ ]
2928
+ };
2929
+ }
2930
+ function nuxt({ alias }) {
2931
+ return {
2932
+ kind: "scaffold",
2933
+ // The `0.` prefix is load-bearing: Nitro sorts plugin filenames as strings,
2934
+ // so it must sort first to run before any other plugin reads process.env.
2935
+ file: "server/plugins/0.penv.ts",
2936
+ content: `// This is a Nitro server plugin (Nuxt's server layer runs it once at startup,
2937
+ // before any middleware or request handler). penv put the import here so your
2938
+ // config is in process.env before a library reads it. The \`0.\` prefix keeps this
2939
+ // plugin first \u2014 Nitro sorts plugin filenames as text \u2014 so don't rename it.
2940
+ import "${alias}";
2941
+
2942
+ // Nitro requires every plugin file to default-export a function; the injection
2943
+ // above already ran at import, so this body is intentionally empty.
2944
+ export default defineNitroPlugin(() => {});
2945
+ `,
2946
+ ifPresent: `Create server/plugins/0.penv.ts with \`import "${alias}";\` and \`export default defineNitroPlugin(() => {});\`.`,
2947
+ notes: [
2948
+ `Wire \`${alias}\` for the Nitro (server) build so it never leaks into the client bundle.`
2949
+ ]
2950
+ };
2951
+ }
2952
+ function bun({ alias }) {
2953
+ return {
2954
+ kind: "scaffold",
2955
+ file: ".penv/preload.ts",
2956
+ content: `// Bun evaluates this file before your entry point, because it is listed under
2957
+ // \`preload\` in bunfig.toml. Running it injects your config into process.env
2958
+ // before any library reads it.
2959
+ import "${alias}";
2960
+ `,
2961
+ ifPresent: `Keep .penv/preload.ts as \`import "${alias}";\`.`,
2962
+ notes: [`\`${alias}\` must be a tsconfig \`paths\` alias for Bun to resolve it.`],
2963
+ // The preload file is inert until bunfig.toml points at it — so penv writes
2964
+ // that too, both for the runtime and for `bun test`. Only when there is no
2965
+ // bunfig to overwrite; an existing one is the user's, and penv prints instead.
2966
+ also: {
2967
+ file: "bunfig.toml",
2968
+ content: 'preload = ["./.penv/preload.ts"]\n\n[test]\npreload = ["./.penv/preload.ts"]\n',
2969
+ ifPresent: 'Add "./.penv/preload.ts" to the `preload` array in bunfig.toml \u2014 and mirror it under `[test]` so `bun test` injects too.'
2970
+ }
2971
+ };
2972
+ }
2973
+ function node({ alias, schemaFile }) {
2974
+ const builtSchema = `./${schemaFile.replace(/\.ts$/, ".js")}`;
2975
+ const runtimeSpecifier = alias.startsWith("#") ? alias : builtSchema;
2976
+ return {
2977
+ kind: "instruct",
2978
+ instruction: `Plain Node / Express / Fastify have no framework hook that runs before your
2979
+ code, so place the injection yourself, one of two ways (both run first):
2980
+ \u2022 Preload at launch \u2014 add to your start script: node --import "${runtimeSpecifier}" your-entry.js
2981
+ \u2022 Or make it the FIRST line of your entry file: import "${runtimeSpecifier}";
2982
+ ` + (alias.startsWith("#") ? "" : `Note: \`${alias}\` is a tsconfig alias that Node does not resolve at runtime \u2014 the paths above point at the built schema module instead.
2983
+ `)
2984
+ };
2985
+ }
2986
+ function tanstack({ alias, srcDir }) {
2987
+ return {
2988
+ kind: "instruct",
2989
+ instruction: `Add \`import "${alias}";\` as the FIRST line of ${srcDir}server.ts (TanStack Start's
2990
+ server entry), above the \`@tanstack/react-start/server-entry\` import \u2014 nothing may
2991
+ import before it. If you have no server.ts yet, create it from TanStack's server-entry
2992
+ template and put that import first.`
2993
+ };
2994
+ }
2995
+ function astro({ alias }) {
2996
+ return {
2997
+ kind: "instruct",
2998
+ instruction: `Astro has no single pre-app hook. Add a small integration to astro.config that
2999
+ injects the import into every SSR page, then cover the routes it cannot reach:
3000
+ \u2022 In astro.config integrations, add:
3001
+ { name: "penv", hooks: { "astro:config:setup": ({ injectScript }) =>
3002
+ injectScript("page-ssr", 'import "${alias}";') } }
3003
+ \u2022 Also add \`import "${alias}";\` as the first line of src/middleware.ts and of any
3004
+ endpoint route (src/pages/*.ts) that imports a library reading process.env.
3005
+ Injection only applies under a server (on-demand) adapter, not a static build.`
3006
+ };
3007
+ }
3008
+ function viteSpa(_context) {
3009
+ return {
3010
+ kind: "none",
3011
+ reason: "This is a client-only app \u2014 nothing reads process.env at runtime, so injection does not apply. Read client config through import.meta.env (only VITE_-prefixed vars, which ship to the browser and must not hold secrets). If you add a server later, place the injection there."
3012
+ };
3013
+ }
3014
+ var SEAMS = {
3015
+ "Next.js": nextjs,
3016
+ SvelteKit: sveltekit,
3017
+ Nuxt: nuxt,
3018
+ Bun: bun,
3019
+ "TanStack Start": tanstack,
3020
+ Astro: astro,
3021
+ Vite: viteSpa
3022
+ };
3023
+ function seamFor(framework, context) {
3024
+ const build = framework === void 0 ? node : SEAMS[framework] ?? node;
3025
+ return build(context);
3026
+ }
3027
+
3028
+ // src/commands/init.ts
2879
3029
  var CONFIG_FILE = "penv.config.ts";
2880
3030
  var TSCONFIG_FILE = "tsconfig.json";
2881
3031
  var GITIGNORE_FILE = ".gitignore";
@@ -2885,6 +3035,7 @@ var IMPORTS_PREFIX = "#";
2885
3035
  var PACKAGE_FILE = "package.json";
2886
3036
  var DEFAULT_DECISIONS = {
2887
3037
  environments: [],
3038
+ inject: false,
2888
3039
  schemaFile: import_core15.DEFAULT_SCHEMA_FILE,
2889
3040
  publicPrefixes: [],
2890
3041
  alias: DEFAULT_ALIAS
@@ -3003,6 +3154,9 @@ function planInit(root, flags = {}) {
3003
3154
  detected,
3004
3155
  decisions: {
3005
3156
  environments,
3157
+ // Injection is a choice about the app's needs, not something on disk — so
3158
+ // the plan defaults it off, and only the interactive prompt turns it on.
3159
+ inject: false,
3006
3160
  schemaFile,
3007
3161
  publicPrefixes: declared?.publicPrefixes ?? detected?.publicPrefixes ?? [],
3008
3162
  alias
@@ -3053,12 +3207,30 @@ async function promptForDecisions(plan2, io) {
3053
3207
  if (proceed.length > 0 && proceed !== "y" && proceed !== "yes") {
3054
3208
  return void 0;
3055
3209
  }
3056
- return { ...plan2.decisions, environments };
3210
+ const inject = seamKindFor(plan2) === "none" ? false : await askInject(io);
3211
+ return { ...plan2.decisions, environments, inject };
3212
+ }
3213
+ function seamKindFor(plan2) {
3214
+ return seamFor(plan2.detected?.name, {
3215
+ alias: plan2.decisions.alias,
3216
+ srcDir: plan2.decisions.schemaFile.startsWith("src/") ? "src/" : "",
3217
+ schemaFile: plan2.decisions.schemaFile
3218
+ }).kind;
3219
+ }
3220
+ async function askInject(io) {
3221
+ io.write("");
3222
+ io.write("Also inject your validated config into process.env?");
3223
+ io.write(" y libraries that read process.env directly (WorkOS, Prisma\u2026) just work");
3224
+ io.write(' n config stays available only through import { env } from "@env"');
3225
+ const answer = (await io.ask(prompt("inject", "y/N"))).trim().toLowerCase();
3226
+ return answer === "y" || answer === "yes";
3057
3227
  }
3058
3228
  var EMPTY_SCHEMA_BODY = " // One key per parameter, e.g. `databaseUrl: z.url(),`. Nesting a key nests\n // the parameter: `redis: z.object({ password: z.string() })` is redis/password.";
3059
3229
  var DRAFT_HEADER = "// DRAFT \u2014 generated by `penv import` from one sample of each value, and yours\n// to correct. Single-sample inference cannot know that a boolean seen as `true`\n// must also accept `1`/`0`, or that a string is really a URL. penv scaffolds\n// this file once and never regenerates it, so edits here are safe.\n";
3060
- function renderSchemaModule(fields, draft) {
3230
+ function renderSchemaModule(fields, draft, inject = false) {
3061
3231
  const body = fields.length === 0 ? EMPTY_SCHEMA_BODY : fields.map((field) => ` ${field.key}: ${field.type},`).join("\n");
3232
+ const loadComment = inject ? "// The loaded, validated values for the current environment. Import this in app\n// code. `inject: true` also copies the values into process.env, for libraries\n// that read process.env directly (WorkOS, Prisma) instead of importing @env.\n" : "// The loaded, validated values for the current environment. Import this in app\n// code. Importing it loads configuration and throws (naming the parameter and\n// environment) if anything required is missing or invalid.\n";
3233
+ const loadCall = inject ? "export const env = load(schema, { inject: true });\n" : "export const env = load(schema);\n";
3062
3234
  return `${draft ? DRAFT_HEADER : ""}import { z } from "zod";
3063
3235
  import { load } from "@penvhq/penv";
3064
3236
 
@@ -3068,11 +3240,7 @@ export const schema = z.object({
3068
3240
  ${body}
3069
3241
  });
3070
3242
 
3071
- // The loaded, validated values for the current environment. Import this in app
3072
- // code. Importing it loads configuration and throws (naming the parameter and
3073
- // environment) if anything required is missing or invalid.
3074
- export const env = load(schema);
3075
-
3243
+ ` + loadComment + loadCall + `
3076
3244
  // Registers the schema's shape with penv's types (erased at runtime, so
3077
3245
  // nothing cycles). This is what makes \`override\` keys in penv.config.ts
3078
3246
  // autocomplete from this schema \u2014 a typo'd parameter id is a compile error.
@@ -3364,7 +3532,7 @@ function writeSchemaFile(root, fields, draft, decisions = DEFAULT_DECISIONS) {
3364
3532
  };
3365
3533
  }
3366
3534
  (0, import_node_fs3.mkdirSync)((0, import_node_path6.dirname)(file), { recursive: true });
3367
- (0, import_node_fs3.writeFileSync)(file, renderSchemaModule(fields, draft), "utf8");
3535
+ (0, import_node_fs3.writeFileSync)(file, renderSchemaModule(fields, draft, decisions.inject), "utf8");
3368
3536
  return {
3369
3537
  target: "schema",
3370
3538
  action: "created",
@@ -3441,23 +3609,119 @@ function writeGitignore(root, decisions = DEFAULT_DECISIONS) {
3441
3609
  text: `${existing === void 0 ? "Created" : "Updated"} ${relative3}`
3442
3610
  };
3443
3611
  }
3444
- function scaffold(root, fields, draft, decisions = DEFAULT_DECISIONS) {
3445
- return [
3612
+ var INJECT_MIN_VERSION = "0.6.0";
3613
+ function outdatedRuntimeWarning(root) {
3614
+ const version = installedPenvVersion(root);
3615
+ if (version === void 0 || !isBelow(version, INJECT_MIN_VERSION)) {
3616
+ return void 0;
3617
+ }
3618
+ return `Injection needs @penvhq/penv ${INJECT_MIN_VERSION}+ \u2014 this project has ${version}, whose \`load\` ignores \`{ inject: true }\`. Upgrade, or process.env stays empty.`;
3619
+ }
3620
+ function installedPenvVersion(root) {
3621
+ const file = (0, import_node_path6.join)(root, "node_modules", "@penvhq", "penv", "package.json");
3622
+ if (!(0, import_node_fs3.existsSync)(file)) {
3623
+ return void 0;
3624
+ }
3625
+ try {
3626
+ const version = JSON.parse((0, import_node_fs3.readFileSync)(file, "utf8")).version;
3627
+ return typeof version === "string" ? version : void 0;
3628
+ } catch {
3629
+ return void 0;
3630
+ }
3631
+ }
3632
+ function releaseTriple(version) {
3633
+ const parts = (version.split("-")[0] ?? "").split(".");
3634
+ const at = (i) => Number.parseInt(parts[i] ?? "0", 10) || 0;
3635
+ return [at(0), at(1), at(2)];
3636
+ }
3637
+ function isBelow(a, b) {
3638
+ const [am, an, ap] = releaseTriple(a);
3639
+ const [bm, bn, bp] = releaseTriple(b);
3640
+ if (am !== bm) return am < bm;
3641
+ if (an !== bn) return an < bn;
3642
+ return ap < bp;
3643
+ }
3644
+ function writeSeam(root, decisions = DEFAULT_DECISIONS, framework = detectFramework(root)?.name) {
3645
+ if (!decisions.inject) {
3646
+ return void 0;
3647
+ }
3648
+ const seam = seamFor(framework, {
3649
+ alias: decisions.alias,
3650
+ srcDir: srcPrefix(root),
3651
+ schemaFile: decisions.schemaFile
3652
+ });
3653
+ const outdated = outdatedRuntimeWarning(root);
3654
+ if (seam.kind === "none") {
3655
+ return { target: "seam", action: "info", text: "No injection seam needed", note: seam.reason };
3656
+ }
3657
+ if (seam.kind === "instruct") {
3658
+ return {
3659
+ target: "seam",
3660
+ action: "info",
3661
+ text: "Place the injection seam",
3662
+ note: withWarning(seam.instruction, outdated)
3663
+ };
3664
+ }
3665
+ const alsoNote = writeAlso(root, seam.also);
3666
+ const file = (0, import_node_path6.join)(root, ...seam.file.split("/"));
3667
+ const baseNotes = [...seam.notes, ...alsoNote === void 0 ? [] : [alsoNote]];
3668
+ const notes = baseNotes.length === 0 ? "" : `
3669
+ ${baseNotes.map((n) => ` ${n}`).join("\n")}`;
3670
+ if ((0, import_node_fs3.existsSync)(file)) {
3671
+ return {
3672
+ target: "seam",
3673
+ action: "info",
3674
+ text: `Add the injection seam to ${seam.file}`,
3675
+ note: withWarning(`${seam.ifPresent}${notes}`, outdated)
3676
+ };
3677
+ }
3678
+ (0, import_node_fs3.mkdirSync)((0, import_node_path6.dirname)(file), { recursive: true });
3679
+ (0, import_node_fs3.writeFileSync)(file, seam.content, "utf8");
3680
+ return {
3681
+ target: "seam",
3682
+ action: outdated === void 0 ? "created" : "info",
3683
+ text: `Wrote ${seam.file} (runs the injection before your app)`,
3684
+ ...notes === "" && outdated === void 0 ? {} : { note: withWarning(notes.trimStart(), outdated) }
3685
+ };
3686
+ }
3687
+ function writeAlso(root, also) {
3688
+ if (also === void 0) {
3689
+ return void 0;
3690
+ }
3691
+ const file = (0, import_node_path6.join)(root, ...also.file.split("/"));
3692
+ if ((0, import_node_fs3.existsSync)(file)) {
3693
+ return also.ifPresent;
3694
+ }
3695
+ (0, import_node_fs3.mkdirSync)((0, import_node_path6.dirname)(file), { recursive: true });
3696
+ (0, import_node_fs3.writeFileSync)(file, also.content, "utf8");
3697
+ return `Wrote ${also.file} to register it.`;
3698
+ }
3699
+ function withWarning(note, warning) {
3700
+ if (warning === void 0) {
3701
+ return note;
3702
+ }
3703
+ return note === "" ? warning : `${note}
3704
+ \u26A0 ${warning}`;
3705
+ }
3706
+ function scaffold(root, fields, draft, decisions = DEFAULT_DECISIONS, framework = detectFramework(root)?.name) {
3707
+ const steps = [
3446
3708
  ensurePenvDir(root),
3447
3709
  writeSchemaFile(root, fields, draft, decisions),
3448
3710
  writeConfigFile(root, decisions),
3449
3711
  writeTsconfigAlias(root, decisions),
3450
3712
  writeGitignore(root, decisions)
3451
3713
  ];
3714
+ const seam = writeSeam(root, decisions, framework);
3715
+ return seam === void 0 ? steps : [...steps, seam];
3452
3716
  }
3453
3717
  function runInit(options) {
3454
3718
  const root = (0, import_node_path6.resolve)(options.cwd);
3455
3719
  const decisions = options.decisions ?? planInit(root).decisions;
3456
- return { root, decisions, steps: scaffold(root, [], false, decisions) };
3720
+ return { root, decisions, steps: scaffold(root, [], false, decisions, options.framework) };
3457
3721
  }
3458
3722
  function renderInit(result2) {
3459
3723
  const steps = result2.steps.map((step) => {
3460
- const glyph = step.action === "conflicted" ? WARN : CHECK;
3724
+ const glyph = step.action === "conflicted" ? WARN : step.action === "info" ? "\u2192" : CHECK;
3461
3725
  return step.note === void 0 ? { glyph, text: step.text } : { glyph, text: step.text, note: step.note };
3462
3726
  });
3463
3727
  return [
@@ -3520,7 +3784,15 @@ var initCommand = (0, import_citty9.defineCommand)({
3520
3784
  if (!asked) {
3521
3785
  write([...plan2.notes, ""]);
3522
3786
  }
3523
- write(renderInit(runInit({ cwd: root, decisions })));
3787
+ write(
3788
+ renderInit(
3789
+ runInit({
3790
+ cwd: root,
3791
+ decisions,
3792
+ ...plan2.detected && { framework: plan2.detected.name }
3793
+ })
3794
+ )
3795
+ );
3524
3796
  });
3525
3797
  }
3526
3798
  });
@@ -3690,6 +3962,9 @@ function environmentNamed(file, explicit) {
3690
3962
  function decisionsOf(config, cwd) {
3691
3963
  return {
3692
3964
  environments: config.environments,
3965
+ // `import` re-scaffolds env.ts for an existing project; injection is an init
3966
+ // choice, so it is not turned on here.
3967
+ inject: false,
3693
3968
  schemaFile: (0, import_core16.schemaFileOf)(config),
3694
3969
  publicPrefixes: config.publicPrefixes ?? [],
3695
3970
  alias: detectAlias(cwd)