@penvhq/cli 0.5.0 → 0.6.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
@@ -250,14 +250,18 @@ declare function runGet(options: GetOptions): Promise<string>;
250
250
  declare function runExplain(options: GetOptions): Promise<GetExplanation>;
251
251
 
252
252
  /** What init touched, so a caller can report it and a test can assert it. */
253
- type InitTarget = "penv-dir" | "schema" | "config" | "tsconfig" | "gitignore";
253
+ type InitTarget = "penv-dir" | "schema" | "config" | "tsconfig" | "gitignore" | "seam";
254
254
  /**
255
255
  * `conflicted` is the one that is not a success. penv wanted to write something,
256
256
  * found the user's file already saying something else about the same thing, and
257
257
  * left it alone — so the step is reported with a warning rather than a ✓, and the
258
258
  * text says what will not work until the user decides.
259
+ *
260
+ * `info` is a step penv did not perform automatically — a manual instruction (the
261
+ * injection seam for a framework penv cannot scaffold), reported so the user
262
+ * knows the one thing left to do.
259
263
  */
260
- type InitAction = "created" | "kept" | "updated" | "conflicted";
264
+ type InitAction = "created" | "kept" | "updated" | "conflicted" | "info";
261
265
  interface InitStep {
262
266
  readonly target: InitTarget;
263
267
  readonly action: InitAction;
@@ -289,6 +293,13 @@ interface InitDecisions {
289
293
  * offers that.
290
294
  */
291
295
  readonly alias: string;
296
+ /**
297
+ * Whether to inject the validated config into `process.env` for libraries that
298
+ * read it directly, so `env.ts` loads with `{ inject: true }` and penv places
299
+ * the framework's pre-app seam. Off by default and only ever turned on by an
300
+ * explicit yes — a project that reads config only through `@env` gets none.
301
+ */
302
+ readonly inject: boolean;
292
303
  }
293
304
  interface InitResult {
294
305
  readonly root: string;
package/dist/index.d.ts CHANGED
@@ -250,14 +250,18 @@ declare function runGet(options: GetOptions): Promise<string>;
250
250
  declare function runExplain(options: GetOptions): Promise<GetExplanation>;
251
251
 
252
252
  /** What init touched, so a caller can report it and a test can assert it. */
253
- type InitTarget = "penv-dir" | "schema" | "config" | "tsconfig" | "gitignore";
253
+ type InitTarget = "penv-dir" | "schema" | "config" | "tsconfig" | "gitignore" | "seam";
254
254
  /**
255
255
  * `conflicted` is the one that is not a success. penv wanted to write something,
256
256
  * found the user's file already saying something else about the same thing, and
257
257
  * left it alone — so the step is reported with a warning rather than a ✓, and the
258
258
  * text says what will not work until the user decides.
259
+ *
260
+ * `info` is a step penv did not perform automatically — a manual instruction (the
261
+ * injection seam for a framework penv cannot scaffold), reported so the user
262
+ * knows the one thing left to do.
259
263
  */
260
- type InitAction = "created" | "kept" | "updated" | "conflicted";
264
+ type InitAction = "created" | "kept" | "updated" | "conflicted" | "info";
261
265
  interface InitStep {
262
266
  readonly target: InitTarget;
263
267
  readonly action: InitAction;
@@ -289,6 +293,13 @@ interface InitDecisions {
289
293
  * offers that.
290
294
  */
291
295
  readonly alias: string;
296
+ /**
297
+ * Whether to inject the validated config into `process.env` for libraries that
298
+ * read it directly, so `env.ts` loads with `{ inject: true }` and penv places
299
+ * the framework's pre-app seam. Off by default and only ever turned on by an
300
+ * explicit yes — a project that reads config only through `@env` gets none.
301
+ */
302
+ readonly inject: boolean;
292
303
  }
293
304
  interface InitResult {
294
305
  readonly root: string;
package/dist/index.js CHANGED
@@ -350,19 +350,19 @@ import {
350
350
  refFromAccessPath as refFromAccessPath2,
351
351
  variableName
352
352
  } from "@penvhq/core";
353
- function defOf(node) {
354
- if (typeof node !== "object" || node === null) {
353
+ function defOf(node2) {
354
+ if (typeof node2 !== "object" || node2 === null) {
355
355
  return void 0;
356
356
  }
357
- const def = node.def;
357
+ const def = node2.def;
358
358
  return typeof def === "object" && def !== null ? def : void 0;
359
359
  }
360
- function typeOf(node) {
361
- const type = defOf(node)?.type;
360
+ function typeOf(node2) {
361
+ const type = defOf(node2)?.type;
362
362
  return typeof type === "string" ? type : void 0;
363
363
  }
364
- function unwrap(node) {
365
- let current = node;
364
+ function unwrap(node2) {
365
+ let current = node2;
366
366
  for (let depth = 0; depth < 8; depth += 1) {
367
367
  const inner = defOf(current)?.innerType;
368
368
  if (inner === void 0) {
@@ -372,38 +372,38 @@ function unwrap(node) {
372
372
  }
373
373
  return current;
374
374
  }
375
- function shapeOf(node) {
376
- if (typeOf(node) !== "object") {
375
+ function shapeOf(node2) {
376
+ if (typeOf(node2) !== "object") {
377
377
  return void 0;
378
378
  }
379
- const shape = node.shape;
379
+ const shape = node2.shape;
380
380
  return typeof shape === "object" && shape !== null ? shape : void 0;
381
381
  }
382
382
  function lookup(root, path) {
383
- let node = unwrap(root);
383
+ let node2 = unwrap(root);
384
384
  for (const key of path) {
385
- const shape = shapeOf(node);
385
+ const shape = shapeOf(node2);
386
386
  if (shape === void 0) {
387
387
  return { kind: "unknown" };
388
388
  }
389
389
  if (!Object.hasOwn(shape, key)) {
390
390
  return { kind: "absent" };
391
391
  }
392
- node = unwrap(shape[key]);
392
+ node2 = unwrap(shape[key]);
393
393
  }
394
- return { kind: "found", node };
394
+ return { kind: "found", node: node2 };
395
395
  }
396
- function minLengthOf(node) {
397
- if (typeOf(node) !== "string") {
396
+ function minLengthOf(node2) {
397
+ if (typeOf(node2) !== "string") {
398
398
  return void 0;
399
399
  }
400
- const min = node.minLength;
400
+ const min = node2.minLength;
401
401
  return typeof min === "number" ? min : void 0;
402
402
  }
403
403
  var ABSENCE_PERMITTED = /* @__PURE__ */ new Set(["optional", "default", "catch", "prefault"]);
404
404
  var ABSENCE_REFUSED = /* @__PURE__ */ new Set(["nullable", "nonoptional", "readonly"]);
405
- function permitsAbsence(node) {
406
- let current = node;
405
+ function permitsAbsence(node2) {
406
+ let current = node2;
407
407
  for (let depth = 0; depth < 8; depth += 1) {
408
408
  const type = typeOf(current);
409
409
  if (type !== void 0 && ABSENCE_PERMITTED.has(type)) {
@@ -420,13 +420,13 @@ function permitsAbsence(node) {
420
420
  }
421
421
  return void 0;
422
422
  }
423
- function leaves(node, path, inherited, out2) {
424
- const own = permitsAbsence(node);
423
+ function leaves(node2, path, inherited, out2) {
424
+ const own = permitsAbsence(node2);
425
425
  const absencePermitted = inherited === true || own === true ? true : combine(inherited, own);
426
- const shape = shapeOf(unwrap(node));
426
+ const shape = shapeOf(unwrap(node2));
427
427
  if (shape === void 0) {
428
428
  if (path.length > 0) {
429
- out2.push({ path, absencePermitted, node });
429
+ out2.push({ path, absencePermitted, node: node2 });
430
430
  }
431
431
  return;
432
432
  }
@@ -442,8 +442,8 @@ function declaredLeaves(schema) {
442
442
  leaves(schema, [], false, out2);
443
443
  return out2;
444
444
  }
445
- function defaultValueOf(node) {
446
- let current = node;
445
+ function defaultValueOf(node2) {
446
+ let current = node2;
447
447
  for (let depth = 0; depth < 8; depth += 1) {
448
448
  const def = defOf(current);
449
449
  if (def === void 0) {
@@ -1110,18 +1110,18 @@ function place(root, path, value) {
1110
1110
  if (leaf === void 0) {
1111
1111
  return;
1112
1112
  }
1113
- let node = root;
1113
+ let node2 = root;
1114
1114
  for (const key of path.slice(0, -1)) {
1115
- const existing = node[key];
1115
+ const existing = node2[key];
1116
1116
  if (typeof existing === "object" && existing !== null) {
1117
- node = existing;
1117
+ node2 = existing;
1118
1118
  continue;
1119
1119
  }
1120
1120
  const child = {};
1121
- node[key] = child;
1122
- node = child;
1121
+ node2[key] = child;
1122
+ node2 = child;
1123
1123
  }
1124
- node[leaf] = value;
1124
+ node2[leaf] = value;
1125
1125
  }
1126
1126
  function isZodType(value) {
1127
1127
  return typeof value === "object" && value !== null && "safeParse" in value && typeof value.safeParse === "function";
@@ -2805,6 +2805,8 @@ import { join } from "path";
2805
2805
  import { DEFAULT_SCHEMA_FILE } from "@penvhq/core";
2806
2806
  var SIGNATURES = [
2807
2807
  { name: "Next.js", packages: ["next"], publicPrefixes: ["NEXT_PUBLIC_"] },
2808
+ { name: "SvelteKit", packages: ["@sveltejs/kit"], publicPrefixes: ["PUBLIC_"] },
2809
+ { name: "Nuxt", packages: ["nuxt"], publicPrefixes: ["NUXT_PUBLIC_"] },
2808
2810
  {
2809
2811
  name: "TanStack Start",
2810
2812
  packages: ["@tanstack/react-start", "@tanstack/start"],
@@ -2878,17 +2880,23 @@ function detectFramework(cwd) {
2878
2880
  }
2879
2881
  for (const signature of SIGNATURES) {
2880
2882
  if (signature.packages.some((name) => dependencies.has(name))) {
2881
- const schema = schemaFileFor(cwd);
2882
- return {
2883
- name: signature.name,
2884
- schemaFile: schema.file,
2885
- ...schema.displaced === void 0 ? {} : { displacedFrom: schema.displaced },
2886
- publicPrefixes: signature.publicPrefixes
2887
- };
2883
+ return detectedFrom(cwd, signature.name, signature.publicPrefixes);
2888
2884
  }
2889
2885
  }
2886
+ if (existsSync(join(cwd, "bunfig.toml")) || dependencies.has("bun-types")) {
2887
+ return detectedFrom(cwd, "Bun", []);
2888
+ }
2890
2889
  return void 0;
2891
2890
  }
2891
+ function detectedFrom(cwd, name, publicPrefixes) {
2892
+ const schema = schemaFileFor(cwd);
2893
+ return {
2894
+ name,
2895
+ schemaFile: schema.file,
2896
+ ...schema.displaced === void 0 ? {} : { displacedFrom: schema.displaced },
2897
+ publicPrefixes
2898
+ };
2899
+ }
2892
2900
  var DEFAULT_ALIAS = "@env";
2893
2901
  var IMPORTS_ALIAS = "#env";
2894
2902
  function detectAlias(cwd) {
@@ -2915,6 +2923,132 @@ import {
2915
2923
  validateSchemaFile
2916
2924
  } from "@penvhq/core";
2917
2925
  import { defineCommand as defineCommand9 } from "citty";
2926
+
2927
+ // src/seams.ts
2928
+ function nextjs({ alias, srcDir }) {
2929
+ return {
2930
+ kind: "scaffold",
2931
+ file: `${srcDir}instrumentation.ts`,
2932
+ content: `// This is instrumentation.ts, Next.js's own startup hook \u2014 Next runs it once,
2933
+ // before any of your app code, on every server boot. penv put one line here so
2934
+ // your config is in process.env before the first library reads it. The filename
2935
+ // is Next's requirement (it only finds \`instrumentation.ts\`), so keep it as-is.
2936
+ export async function register() {
2937
+ // Next also calls register on the Edge runtime, where penv cannot read the
2938
+ // filesystem \u2014 so inject runs on the Node runtime only.
2939
+ if (process.env.NEXT_RUNTIME === "nodejs") {
2940
+ await import("${alias}");
2941
+ }
2942
+ }
2943
+ `,
2944
+ ifPresent: `Add to your existing \`${srcDir}instrumentation.ts\`, inside \`register()\` and first:
2945
+ if (process.env.NEXT_RUNTIME === "nodejs") { await import("${alias}"); }`,
2946
+ notes: []
2947
+ };
2948
+ }
2949
+ function sveltekit({ alias }) {
2950
+ return {
2951
+ kind: "scaffold",
2952
+ // SvelteKit's hooks module is always under `src/`, whatever the schema path.
2953
+ file: "src/hooks.server.ts",
2954
+ content: `// This is hooks.server.ts, SvelteKit's server-startup module \u2014 it loads before
2955
+ // your routes handle a request. penv put this import first so your config is in
2956
+ // process.env before any library reads it. Keep it the first line; the name and
2957
+ // location are SvelteKit's, not penv's.
2958
+ import "${alias}";
2959
+ `,
2960
+ ifPresent: `Add \`import "${alias}";\` as the FIRST line of src/hooks.server.ts, above every other import.`,
2961
+ notes: [
2962
+ `SvelteKit resolves app aliases through \`kit.alias\` \u2014 make sure \`${alias}\` is registered in svelte.config.js, not only tsconfig.`
2963
+ ]
2964
+ };
2965
+ }
2966
+ function nuxt({ alias }) {
2967
+ return {
2968
+ kind: "scaffold",
2969
+ // The `0.` prefix is load-bearing: Nitro sorts plugin filenames as strings,
2970
+ // so it must sort first to run before any other plugin reads process.env.
2971
+ file: "server/plugins/0.penv.ts",
2972
+ content: `// This is a Nitro server plugin (Nuxt's server layer runs it once at startup,
2973
+ // before any middleware or request handler). penv put the import here so your
2974
+ // config is in process.env before a library reads it. The \`0.\` prefix keeps this
2975
+ // plugin first \u2014 Nitro sorts plugin filenames as text \u2014 so don't rename it.
2976
+ import "${alias}";
2977
+
2978
+ // Nitro requires every plugin file to default-export a function; the injection
2979
+ // above already ran at import, so this body is intentionally empty.
2980
+ export default defineNitroPlugin(() => {});
2981
+ `,
2982
+ ifPresent: `Create server/plugins/0.penv.ts with \`import "${alias}";\` and \`export default defineNitroPlugin(() => {});\`.`,
2983
+ notes: [
2984
+ `Wire \`${alias}\` for the Nitro (server) build so it never leaks into the client bundle.`
2985
+ ]
2986
+ };
2987
+ }
2988
+ function bun({ alias }) {
2989
+ return {
2990
+ kind: "scaffold",
2991
+ file: ".penv/preload.ts",
2992
+ 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.
2995
+ import "${alias}";
2996
+ `,
2997
+ 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
+ ]
3002
+ };
3003
+ }
3004
+ function node({ alias, srcDir }) {
3005
+ const runtimeSpecifier = alias.startsWith("#") ? alias : `./${srcDir}.penv/env.js`;
3006
+ return {
3007
+ kind: "instruct",
3008
+ instruction: `Plain Node / Express / Fastify have no framework hook that runs before your
3009
+ code, so place the injection yourself, one of two ways (both run first):
3010
+ \u2022 Preload at launch \u2014 add to your start script: node --import "${runtimeSpecifier}" your-entry.js
3011
+ \u2022 Or make it the FIRST line of your entry file: import "${runtimeSpecifier}";
3012
+ ` + (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.
3013
+ `)
3014
+ };
3015
+ }
3016
+ function tanstack({ alias, srcDir }) {
3017
+ return {
3018
+ kind: "instruct",
3019
+ instruction: `Add \`import "${alias}";\` as the FIRST line of ${srcDir}server.ts (TanStack Start's
3020
+ server entry), above the \`@tanstack/react-start/server-entry\` import \u2014 nothing may
3021
+ import before it. If you have no server.ts yet, create it from TanStack's server-entry
3022
+ template and put that import first.`
3023
+ };
3024
+ }
3025
+ function astro(_context) {
3026
+ return {
3027
+ 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.'
3029
+ };
3030
+ }
3031
+ function viteSpa(_context) {
3032
+ return {
3033
+ kind: "none",
3034
+ 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."
3035
+ };
3036
+ }
3037
+ var SEAMS = {
3038
+ "Next.js": nextjs,
3039
+ SvelteKit: sveltekit,
3040
+ Nuxt: nuxt,
3041
+ Bun: bun,
3042
+ "TanStack Start": tanstack,
3043
+ Astro: astro,
3044
+ Vite: viteSpa
3045
+ };
3046
+ function seamFor(framework, context) {
3047
+ const build = framework === void 0 ? node : SEAMS[framework] ?? node;
3048
+ return build(context);
3049
+ }
3050
+
3051
+ // src/commands/init.ts
2918
3052
  var CONFIG_FILE = "penv.config.ts";
2919
3053
  var TSCONFIG_FILE = "tsconfig.json";
2920
3054
  var GITIGNORE_FILE = ".gitignore";
@@ -2924,6 +3058,7 @@ var IMPORTS_PREFIX = "#";
2924
3058
  var PACKAGE_FILE = "package.json";
2925
3059
  var DEFAULT_DECISIONS = {
2926
3060
  environments: [],
3061
+ inject: false,
2927
3062
  schemaFile: DEFAULT_SCHEMA_FILE2,
2928
3063
  publicPrefixes: [],
2929
3064
  alias: DEFAULT_ALIAS
@@ -3042,6 +3177,9 @@ function planInit(root, flags = {}) {
3042
3177
  detected,
3043
3178
  decisions: {
3044
3179
  environments,
3180
+ // Injection is a choice about the app's needs, not something on disk — so
3181
+ // the plan defaults it off, and only the interactive prompt turns it on.
3182
+ inject: false,
3045
3183
  schemaFile,
3046
3184
  publicPrefixes: declared?.publicPrefixes ?? detected?.publicPrefixes ?? [],
3047
3185
  alias
@@ -3088,16 +3226,33 @@ async function promptForDecisions(plan2, io) {
3088
3226
  );
3089
3227
  io.write("");
3090
3228
  }
3229
+ const inject = seamKindFor(plan2) === "none" ? false : await askInject(io);
3091
3230
  const proceed = (await io.ask(prompt("Proceed?", "Y/n"))).trim().toLowerCase();
3092
3231
  if (proceed.length > 0 && proceed !== "y" && proceed !== "yes") {
3093
3232
  return void 0;
3094
3233
  }
3095
- return { ...plan2.decisions, environments };
3234
+ return { ...plan2.decisions, environments, inject };
3235
+ }
3236
+ function seamKindFor(plan2) {
3237
+ return seamFor(plan2.detected?.name, {
3238
+ alias: plan2.decisions.alias,
3239
+ srcDir: plan2.decisions.schemaFile.startsWith("src/") ? "src/" : ""
3240
+ }).kind;
3241
+ }
3242
+ async function askInject(io) {
3243
+ io.write("");
3244
+ io.write("Also inject your validated config into process.env?");
3245
+ io.write(" y libraries that read process.env directly (WorkOS, Prisma\u2026) just work");
3246
+ io.write(' n config stays available only through import { env } from "@env"');
3247
+ const answer = (await io.ask(prompt("inject", "y/N"))).trim().toLowerCase();
3248
+ return answer === "y" || answer === "yes";
3096
3249
  }
3097
3250
  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.";
3098
3251
  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";
3099
- function renderSchemaModule(fields, draft) {
3252
+ function renderSchemaModule(fields, draft, inject = false) {
3100
3253
  const body = fields.length === 0 ? EMPTY_SCHEMA_BODY : fields.map((field) => ` ${field.key}: ${field.type},`).join("\n");
3254
+ 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";
3255
+ const loadCall = inject ? "export const env = load(schema, { inject: true });\n" : "export const env = load(schema);\n";
3101
3256
  return `${draft ? DRAFT_HEADER : ""}import { z } from "zod";
3102
3257
  import { load } from "@penvhq/penv";
3103
3258
 
@@ -3107,11 +3262,7 @@ export const schema = z.object({
3107
3262
  ${body}
3108
3263
  });
3109
3264
 
3110
- // The loaded, validated values for the current environment. Import this in app
3111
- // code. Importing it loads configuration and throws (naming the parameter and
3112
- // environment) if anything required is missing or invalid.
3113
- export const env = load(schema);
3114
-
3265
+ ` + loadComment + loadCall + `
3115
3266
  // Registers the schema's shape with penv's types (erased at runtime, so
3116
3267
  // nothing cycles). This is what makes \`override\` keys in penv.config.ts
3117
3268
  // autocomplete from this schema \u2014 a typo'd parameter id is a compile error.
@@ -3403,7 +3554,7 @@ function writeSchemaFile(root, fields, draft, decisions = DEFAULT_DECISIONS) {
3403
3554
  };
3404
3555
  }
3405
3556
  mkdirSync(dirname3(file), { recursive: true });
3406
- writeFileSync2(file, renderSchemaModule(fields, draft), "utf8");
3557
+ writeFileSync2(file, renderSchemaModule(fields, draft, decisions.inject), "utf8");
3407
3558
  return {
3408
3559
  target: "schema",
3409
3560
  action: "created",
@@ -3480,14 +3631,56 @@ function writeGitignore(root, decisions = DEFAULT_DECISIONS) {
3480
3631
  text: `${existing === void 0 ? "Created" : "Updated"} ${relative3}`
3481
3632
  };
3482
3633
  }
3634
+ function srcDirOf(root) {
3635
+ return existsSync2(join2(root, "src")) ? "src/" : "";
3636
+ }
3637
+ function writeSeam(root, decisions = DEFAULT_DECISIONS) {
3638
+ if (!decisions.inject) {
3639
+ return void 0;
3640
+ }
3641
+ const framework = detectFramework(root)?.name;
3642
+ const seam = seamFor(framework, { alias: decisions.alias, srcDir: srcDirOf(root) });
3643
+ if (seam.kind === "none") {
3644
+ return { target: "seam", action: "info", text: "No injection seam needed", note: seam.reason };
3645
+ }
3646
+ if (seam.kind === "instruct") {
3647
+ return {
3648
+ target: "seam",
3649
+ action: "info",
3650
+ text: "Place the injection seam",
3651
+ note: seam.instruction
3652
+ };
3653
+ }
3654
+ const file = join2(root, ...seam.file.split("/"));
3655
+ const notes = seam.notes.length === 0 ? "" : `
3656
+ ${seam.notes.map((n) => ` ${n}`).join("\n")}`;
3657
+ if (existsSync2(file)) {
3658
+ return {
3659
+ target: "seam",
3660
+ action: "info",
3661
+ text: `Add the injection seam to ${seam.file}`,
3662
+ note: `${seam.ifPresent}${notes}`
3663
+ };
3664
+ }
3665
+ mkdirSync(dirname3(file), { recursive: true });
3666
+ writeFileSync2(file, seam.content, "utf8");
3667
+ return {
3668
+ target: "seam",
3669
+ action: "created",
3670
+ text: `Wrote ${seam.file} (runs the injection before your app)`,
3671
+ ...notes === "" ? {} : { note: notes.trimStart() }
3672
+ };
3673
+ }
3483
3674
  function scaffold(root, fields, draft, decisions = DEFAULT_DECISIONS) {
3484
- return [
3675
+ const steps = [
3485
3676
  ensurePenvDir(root),
3486
3677
  writeSchemaFile(root, fields, draft, decisions),
3487
3678
  writeConfigFile(root, decisions),
3488
3679
  writeTsconfigAlias(root, decisions),
3489
3680
  writeGitignore(root, decisions)
3490
3681
  ];
3682
+ const seam = writeSeam(root, decisions);
3683
+ return seam === void 0 ? steps : [...steps, seam];
3491
3684
  }
3492
3685
  function runInit(options) {
3493
3686
  const root = resolve4(options.cwd);
@@ -3496,7 +3689,7 @@ function runInit(options) {
3496
3689
  }
3497
3690
  function renderInit(result2) {
3498
3691
  const steps = result2.steps.map((step) => {
3499
- const glyph = step.action === "conflicted" ? WARN : CHECK;
3692
+ const glyph = step.action === "conflicted" ? WARN : step.action === "info" ? "\u2192" : CHECK;
3500
3693
  return step.note === void 0 ? { glyph, text: step.text } : { glyph, text: step.text, note: step.note };
3501
3694
  });
3502
3695
  return [
@@ -3729,6 +3922,9 @@ function environmentNamed(file, explicit) {
3729
3922
  function decisionsOf(config, cwd) {
3730
3923
  return {
3731
3924
  environments: config.environments,
3925
+ // `import` re-scaffolds env.ts for an existing project; injection is an init
3926
+ // choice, so it is not turned on here.
3927
+ inject: false,
3732
3928
  schemaFile: schemaFileOf3(config),
3733
3929
  publicPrefixes: config.publicPrefixes ?? [],
3734
3930
  alias: detectAlias(cwd)