@penvhq/cli 0.9.3 → 0.9.5

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.js CHANGED
@@ -59,7 +59,7 @@ var startChild = (invocation) => {
59
59
  const ended = new Promise((resolve9, reject) => {
60
60
  child.on("error", (cause) => {
61
61
  release();
62
- reject(cannotStart(executable, cause));
62
+ reject(cannotStart(executable, cause, invocation.purpose));
63
63
  });
64
64
  child.on("exit", (code, signal) => {
65
65
  release();
@@ -80,8 +80,15 @@ function noCommand() {
80
80
  "Put the command after `--`, e.g. `penv run -- pnpm dev`."
81
81
  );
82
82
  }
83
- function cannotStart(executable, cause) {
83
+ function cannotStart(executable, cause, purpose) {
84
84
  const detail = cause instanceof Error ? cause.message : String(cause);
85
+ if (purpose !== void 0) {
86
+ return new PenvError(
87
+ "PENV_COMMAND_NOT_STARTED",
88
+ `penv could not start \`${executable}\` to ${purpose}: ${detail}`,
89
+ `Check that \`${executable}\` runs on its own \u2014 penv starts it the way your shell does, so it has to be on PATH. Nothing was changed.`
90
+ );
91
+ }
85
92
  return new PenvError(
86
93
  "RUN_COMMAND_NOT_STARTED",
87
94
  `\`${executable}\` could not be started: ${detail}`,
@@ -110,12 +117,15 @@ function cmdCommandLine(resolved, args) {
110
117
  " "
111
118
  );
112
119
  }
113
- function extensions(env) {
120
+ function extensions(env, platform) {
121
+ if (platform !== "win32") {
122
+ return [""];
123
+ }
114
124
  const declared = env.PATHEXT ?? ".COM;.EXE;.BAT;.CMD";
115
- return ["", ...declared.split(";").filter((extension) => extension.length > 0)];
125
+ return [...declared.split(";").filter((extension) => extension.length > 0), ""];
116
126
  }
117
- function findExecutable(executable, env) {
118
- const candidates = extensions(env);
127
+ function findExecutable(executable, env, platform = process.platform) {
128
+ const candidates = extensions(env, platform);
119
129
  const isFile = (path2) => existsSync(path2) && statSync(path2).isFile();
120
130
  if (executable.includes("/") || executable.includes("\\") || isAbsolute(executable)) {
121
131
  return candidates.map((extension) => executable + extension).find(isFile);
@@ -141,6 +151,7 @@ function escapeArgument(argument, doubleEscape) {
141
151
 
142
152
  // src/install.ts
143
153
  var RUNTIME_PACKAGE = "@penvhq/penv";
154
+ var SCHEMA_PACKAGE = "zod";
144
155
  var LOCKFILES = [
145
156
  ["pnpm", "pnpm-lock.yaml"],
146
157
  ["yarn", "yarn.lock"],
@@ -155,13 +166,9 @@ var ADD = {
155
166
  bun: ["bun", "add", "--exact"]
156
167
  };
157
168
  function engineVersion() {
158
- const manifest = new URL("../package.json", import.meta.url);
159
- try {
160
- const version = JSON.parse(readFileSync(manifest, "utf8")).version;
161
- if (typeof version === "string" && version.length > 0) {
162
- return version;
163
- }
164
- } catch {
169
+ const version = ownManifest()?.version;
170
+ if (typeof version === "string" && version.length > 0) {
171
+ return version;
165
172
  }
166
173
  throw new PenvError2(
167
174
  "ENGINE_VERSION_UNREADABLE",
@@ -169,6 +176,29 @@ function engineVersion() {
169
176
  `Reinstall penv, then run \`penv init\` again.`
170
177
  );
171
178
  }
179
+ function schemaPackageVersion() {
180
+ const peers = ownManifest()?.peerDependencies;
181
+ const declared = peers !== null && typeof peers === "object" && !Array.isArray(peers) ? peers[SCHEMA_PACKAGE] : void 0;
182
+ const floor = typeof declared === "string" ? declared.replace(/^[\^~>=\s]+/, "").trim() : "";
183
+ if (floor.length > 0) {
184
+ return floor;
185
+ }
186
+ throw new PenvError2(
187
+ "ENGINE_PEER_UNREADABLE",
188
+ `penv could not read its own \`${SCHEMA_PACKAGE}\` peer range, so it cannot say which ${SCHEMA_PACKAGE} this project needs`,
189
+ `Reinstall penv, then run \`penv init\` again.`
190
+ );
191
+ }
192
+ function ownManifest() {
193
+ try {
194
+ const parsed = JSON.parse(
195
+ readFileSync(new URL("../package.json", import.meta.url), "utf8")
196
+ );
197
+ return parsed !== null && typeof parsed === "object" && !Array.isArray(parsed) ? parsed : void 0;
198
+ } catch {
199
+ return void 0;
200
+ }
201
+ }
172
202
  function detectPackageManager(root) {
173
203
  for (const [manager, lockfile] of LOCKFILES) {
174
204
  if (existsSync2(join2(root, lockfile))) {
@@ -197,12 +227,12 @@ function manifestOf(root) {
197
227
  return void 0;
198
228
  }
199
229
  }
200
- function declaredVersion(root) {
230
+ function declaredVersion(root, name) {
201
231
  const manifest = manifestOf(root);
202
232
  for (const field of ["dependencies", "devDependencies"]) {
203
233
  const block = manifest?.[field];
204
234
  if (block !== null && typeof block === "object" && !Array.isArray(block)) {
205
- const version = block[RUNTIME_PACKAGE];
235
+ const version = block[name];
206
236
  if (typeof version === "string") {
207
237
  return version;
208
238
  }
@@ -215,27 +245,61 @@ function planInstall(root, version = engineVersion()) {
215
245
  const lockfile = LOCKFILES.find(
216
246
  ([name, file]) => name === manager && existsSync2(join2(root, file))
217
247
  )?.[1];
218
- const declared = declaredVersion(root);
248
+ const runtimeDeclared = declaredVersion(root, RUNTIME_PACKAGE);
249
+ const zodDeclared = declaredVersion(root, SCHEMA_PACKAGE);
250
+ const packages = [
251
+ {
252
+ name: RUNTIME_PACKAGE,
253
+ version,
254
+ ...runtimeDeclared === void 0 ? {} : { declared: runtimeDeclared },
255
+ satisfied: runtimeDeclared === version
256
+ },
257
+ {
258
+ name: SCHEMA_PACKAGE,
259
+ version: schemaPackageVersion(),
260
+ ...zodDeclared === void 0 ? {} : { declared: zodDeclared },
261
+ // Any declared zod counts: which zod a project uses is the project's
262
+ // decision, and penv is here to make sure there is one, not to move it.
263
+ satisfied: zodDeclared !== void 0
264
+ }
265
+ ];
266
+ const pending = packages.filter((entry) => !entry.satisfied);
267
+ const specs = (pending.length === 0 ? packages : pending).map(
268
+ (entry) => `${entry.name}@${entry.version}`
269
+ );
219
270
  return {
220
271
  root,
221
272
  manager,
222
- package: RUNTIME_PACKAGE,
223
- version,
224
- command: [...ADD[manager], `${RUNTIME_PACKAGE}@${version}`],
273
+ packages,
274
+ command: [...ADD[manager], ...specs],
225
275
  ...lockfile === void 0 ? {} : { lockfile },
226
- ...declared === void 0 ? {} : { declared },
227
- satisfied: declared === version
276
+ satisfied: pending.length === 0
228
277
  };
229
278
  }
279
+ function describe(entry) {
280
+ return `${entry.name} ${entry.version}`;
281
+ }
230
282
  function renderInstallPlan(plan2) {
231
283
  if (plan2.satisfied) {
232
- return [`package.json already pins ${plan2.package} ${plan2.version} \u2014 nothing to install.`];
284
+ return [
285
+ `package.json already has ${plan2.packages.map(describe).join(" and ")} \u2014 nothing to install.`
286
+ ];
233
287
  }
234
- const line = `"${plan2.package}": "${plan2.version}"`;
288
+ const pending = plan2.packages.filter((entry) => !entry.satisfied);
289
+ const added = pending.filter((entry) => entry.declared === void 0);
290
+ const replaced = pending.filter((entry) => entry.declared !== void 0);
235
291
  return [
236
292
  "package.json",
237
- ...plan2.declared === void 0 ? [' + "dependencies": {', ` + ${line}`, " + }"] : [` - "${plan2.package}": "${plan2.declared}"`, ` + ${line}`],
238
- ...plan2.lockfile === void 0 ? [] : [plan2.lockfile, ` + ${plan2.package}@${plan2.version}`],
293
+ ...added.length === 0 ? [] : [
294
+ ' + "dependencies": {',
295
+ ...added.map((entry) => ` + "${entry.name}": "${entry.version}"`),
296
+ " + }"
297
+ ],
298
+ ...replaced.flatMap((entry) => [
299
+ ` - "${entry.name}": "${entry.declared}"`,
300
+ ` + "${entry.name}": "${entry.version}"`
301
+ ]),
302
+ ...plan2.lockfile === void 0 ? [] : [plan2.lockfile, ...pending.map((entry) => ` + ${entry.name}@${entry.version}`)],
239
303
  "",
240
304
  `Run with: ${plan2.command.join(" ")}`
241
305
  ];
@@ -244,7 +308,8 @@ var installWithPackageManager = async (plan2) => {
244
308
  const child = startChild({
245
309
  command: plan2.command,
246
310
  env: process.env,
247
- cwd: plan2.root
311
+ cwd: plan2.root,
312
+ purpose: `install ${plan2.packages.map(describe).join(" and ")}`
248
313
  });
249
314
  const ended = await child.ended;
250
315
  if (ended.exitCode !== 0 || ended.signal !== null) {
@@ -260,7 +325,7 @@ function installFailed(plan2) {
260
325
  }
261
326
 
262
327
  // src/project.ts
263
- import { existsSync as existsSync3, readFileSync as readFileSync2 } from "fs";
328
+ import { existsSync as existsSync3, readFileSync as readFileSync3 } from "fs";
264
329
  import { dirname, join as join3 } from "path";
265
330
  import {
266
331
  assertMigrated,
@@ -341,10 +406,19 @@ function environmentFromShorthand(config, candidates, explicit) {
341
406
  }
342
407
 
343
408
  // src/registry.ts
409
+ import { readFileSync as readFileSync2 } from "fs";
344
410
  import { createRequire } from "module";
345
411
  import { resolve } from "path";
346
412
  import { pathToFileURL } from "url";
347
- import { holdsProjection, PENV_DIR, PenvError as PenvError4, recordsDir } from "@penvhq/core";
413
+ import {
414
+ holdsProjection,
415
+ LOCAL_EXTENSIONS_PATH,
416
+ localExtensionsFile,
417
+ PENV_DIR,
418
+ PenvError as PenvError4,
419
+ parseLocalExtensions,
420
+ recordsDir
421
+ } from "@penvhq/core";
348
422
  import { createFilesystemProvider } from "@penvhq/provider-filesystem";
349
423
  import { createMockProvider } from "@penvhq/provider-mock";
350
424
  var PLUGIN_FACTORY_EXPORT = "penvProviderFactory";
@@ -413,16 +487,40 @@ async function loadPluginProvider(type, context) {
413
487
  assertSatisfiesContract(provider, type);
414
488
  return provider;
415
489
  }
416
- function assertProvidersRegistered(config, projectRoot) {
490
+ function assertProvidersRegistered(config, projectRoot, options) {
491
+ const local = localExtensions(projectRoot);
492
+ const ci = options?.ci ?? isCi(process.env.CI);
417
493
  for (const [environment, provider] of Object.entries(config.providers)) {
418
494
  if (isProviderRegistered(provider.type)) {
419
495
  continue;
420
496
  }
497
+ if (ci && local.includes(provider.type)) {
498
+ throw localExtensionInCi(provider.type, environment);
499
+ }
421
500
  if (resolvePlugin(provider.type, projectRoot) === void 0) {
422
501
  throw unknownProvider(provider.type, environment);
423
502
  }
424
503
  }
425
504
  }
505
+ function localExtensions(projectRoot) {
506
+ let text;
507
+ try {
508
+ text = readFileSync2(localExtensionsFile(projectRoot), "utf8");
509
+ } catch {
510
+ return [];
511
+ }
512
+ return parseLocalExtensions(text);
513
+ }
514
+ function isCi(value) {
515
+ return value !== void 0 && value !== "" && value !== "0" && value.toLowerCase() !== "false";
516
+ }
517
+ function localExtensionInCi(type, environment) {
518
+ return new PenvError4(
519
+ "LOCAL_EXTENSION_IN_CI",
520
+ `The provider \`${type}\` for environment ${environment} is a local extension, and this is CI`,
521
+ `${LOCAL_EXTENSIONS_PATH} records it as a package this project develops, so nothing pins the bytes CI would run. Publish it and run \`penv add ${type}\` to pin a release.`
522
+ );
523
+ }
426
524
  function resolvePlugin(specifier, fromDir) {
427
525
  try {
428
526
  const require2 = createRequire(resolve(fromDir, "noop.js"));
@@ -460,7 +558,7 @@ function unknownProvider(type, environment) {
460
558
  return new PenvError4(
461
559
  "UNKNOWN_PROVIDER",
462
560
  `The provider \`${type}\`${where} in penv.config.ts is not installed in this project`,
463
- `Install it with \`npm i ${type}\` \u2014 a provider's \`type\` is the package penv imports. The CLI ships ${preinstalled} pre-installed.`
561
+ `A provider's \`type\` is the package penv imports, so it has to resolve from this project: install it with \`npm i ${type}\`, or \u2014 if this repository is the one that builds it \u2014 run \`penv add --local ${type}\` once it is a dependency of the root. The CLI ships ${preinstalled} pre-installed.`
464
562
  );
465
563
  }
466
564
 
@@ -493,7 +591,7 @@ function selfContainedSchemaModule(root, schemaFile) {
493
591
  if (!existsSync3(file)) {
494
592
  return void 0;
495
593
  }
496
- const source = readFileSync2(file, "utf8");
594
+ const source = readFileSync3(file, "utf8");
497
595
  return source.includes("PenvSchemaShape") || source.includes("z.object") ? schemaFile : void 0;
498
596
  }
499
597
  function schemaShapeFileOf(project) {
@@ -729,10 +827,7 @@ function writeError(lines) {
729
827
  }
730
828
  function reportError(error) {
731
829
  if (error instanceof PenvError6) {
732
- const suffix = error.remedy === void 0 ? void 0 : `
733
- ${error.remedy}`;
734
- const message = suffix !== void 0 && error.message.endsWith(suffix) ? error.message.slice(0, -suffix.length) : error.message;
735
- process.stderr.write(`${err.red(CROSS)} ${message}
830
+ process.stderr.write(`${err.red(CROSS)} ${error.summary}
736
831
  `);
737
832
  if (error.remedy !== void 0) {
738
833
  process.stderr.write(` ${err.cyan("\u2192")} ${error.remedy}
@@ -762,7 +857,7 @@ async function guard(run) {
762
857
  }
763
858
 
764
859
  // src/commands/run.ts
765
- import { existsSync as existsSync4, readFileSync as readFileSync3, watch } from "fs";
860
+ import { existsSync as existsSync4, readFileSync as readFileSync4, watch } from "fs";
766
861
  import { createRequire as createRequire2 } from "module";
767
862
  import { basename, dirname as dirname2, join as join4, resolve as resolve2 } from "path";
768
863
  import {
@@ -1607,7 +1702,7 @@ function packageManifest(specifier, root) {
1607
1702
  const file = join4(directory, "package.json");
1608
1703
  if (existsSync4(file)) {
1609
1704
  try {
1610
- const parsed = JSON.parse(readFileSync3(file, "utf8"));
1705
+ const parsed = JSON.parse(readFileSync4(file, "utf8"));
1611
1706
  if (isPlainObject(parsed) && parsed.name === specifier) {
1612
1707
  return parsed;
1613
1708
  }
@@ -1680,7 +1775,7 @@ function snapshotPath(host) {
1680
1775
  }
1681
1776
  function readSnapshot(path) {
1682
1777
  try {
1683
- return readFileSync3(path, "utf8");
1778
+ return readFileSync4(path, "utf8");
1684
1779
  } catch {
1685
1780
  throw new PenvError8(
1686
1781
  "RUN_SNAPSHOT_MISSING",
@@ -2101,7 +2196,7 @@ import {
2101
2196
  existsSync as existsSync5,
2102
2197
  mkdirSync as mkdirSync2,
2103
2198
  readdirSync as readdirSync2,
2104
- readFileSync as readFileSync4,
2199
+ readFileSync as readFileSync5,
2105
2200
  renameSync,
2106
2201
  rmSync,
2107
2202
  writeFileSync as writeFileSync2
@@ -2145,27 +2240,27 @@ function readCutover(root) {
2145
2240
  }
2146
2241
  let parsed;
2147
2242
  try {
2148
- parsed = JSON.parse(readFileSync4(file, "utf8"));
2243
+ parsed = JSON.parse(readFileSync5(file, "utf8"));
2149
2244
  } catch {
2150
2245
  throw unreadable("it is not JSON");
2151
2246
  }
2152
2247
  if (parsed === null || typeof parsed !== "object" || Array.isArray(parsed)) {
2153
2248
  throw unreadable("it is not an object");
2154
2249
  }
2155
- const record = parsed;
2156
- if (record.format !== CUTOVER_FORMAT) {
2250
+ const record2 = parsed;
2251
+ if (record2.format !== CUTOVER_FORMAT) {
2157
2252
  throw unreadable(
2158
- `it is format ${JSON.stringify(record.format)}, and penv reads format ${CUTOVER_FORMAT}`
2253
+ `it is format ${JSON.stringify(record2.format)}, and penv reads format ${CUTOVER_FORMAT}`
2159
2254
  );
2160
2255
  }
2161
- const files = record.files;
2256
+ const files = record2.files;
2162
2257
  if (!Array.isArray(files) || files.some((name) => typeof name !== "string")) {
2163
2258
  throw unreadable("it lists no filenames");
2164
2259
  }
2165
- const environments = Array.isArray(record.environments) ? record.environments.filter((name) => typeof name === "string") : [];
2260
+ const environments = Array.isArray(record2.environments) ? record2.environments.filter((name) => typeof name === "string") : [];
2166
2261
  return {
2167
2262
  format: CUTOVER_FORMAT,
2168
- movedAt: typeof record.movedAt === "string" ? record.movedAt : "",
2263
+ movedAt: typeof record2.movedAt === "string" ? record2.movedAt : "",
2169
2264
  files,
2170
2265
  environments
2171
2266
  };
@@ -2292,7 +2387,7 @@ var cleanupCommand = defineCommand5({
2292
2387
  });
2293
2388
 
2294
2389
  // src/commands/doctor.ts
2295
- import { readdirSync as readdirSync3, readFileSync as readFileSync5, statSync as statSync2 } from "fs";
2390
+ import { readdirSync as readdirSync3, readFileSync as readFileSync6, statSync as statSync2 } from "fs";
2296
2391
  import { join as join6, relative as relative2 } from "path";
2297
2392
  import {
2298
2393
  ARTIFACT_BUILD_COMMAND as ARTIFACT_BUILD_COMMAND2,
@@ -2307,6 +2402,7 @@ import {
2307
2402
  isSecret as isSecret3,
2308
2403
  isStuck,
2309
2404
  openValue as openValue2,
2405
+ RECORDS_PATH as RECORDS_PATH2,
2310
2406
  resolveAll as resolveAll2,
2311
2407
  rotationOf,
2312
2408
  tryParseDuration,
@@ -2762,6 +2858,7 @@ async function runDoctor(options) {
2762
2858
  findings.push(...unusedFindings(drift));
2763
2859
  }
2764
2860
  findings.push(...fallbackFindings(subjects, environment));
2861
+ findings.push(...secrecyFindings(subjects, environment));
2765
2862
  findings.push(...plaintextSecretFindings(subjects, environment));
2766
2863
  findings.push(...publicSecretFindings(subjects, environment, project.config));
2767
2864
  findings.push(...encryptionFindings(subjects, environment));
@@ -2774,6 +2871,7 @@ async function runDoctor(options) {
2774
2871
  }
2775
2872
  findings.push(...await providerDriftFindings(project, environment, options.source));
2776
2873
  findings.push(...await projectionFindings(project, environment, options.projection));
2874
+ findings.push(...localExtensionFindings(project));
2777
2875
  findings.push(...artifactFindings(project));
2778
2876
  findings.push({
2779
2877
  check: "provider",
@@ -2911,6 +3009,31 @@ function fallbackFindings(subjects, environment) {
2911
3009
  }
2912
3010
  ];
2913
3011
  }
3012
+ function secrecyFindings(subjects, environment) {
3013
+ const undeclared = subjects.filter(
3014
+ ({ meta }) => effectiveMeta(meta, environment).secret === void 0
3015
+ );
3016
+ if (undeclared.length === 0) {
3017
+ return [
3018
+ {
3019
+ check: "secrecy-undeclared",
3020
+ severity: "pass",
3021
+ label: "Secrecy policy",
3022
+ subject: subjects.length === 0 ? `no parameter resolves for ${environment}` : `every parameter declares whether it is secret for ${environment}`
3023
+ }
3024
+ ];
3025
+ }
3026
+ return [
3027
+ {
3028
+ check: "secrecy-undeclared",
3029
+ severity: "unknown",
3030
+ label: "Secrecy policy",
3031
+ subject: `${undeclared.length} of ${subjects.length} declare neither way for ${environment}`,
3032
+ detail: "penv was never told which of these values must be encrypted",
3033
+ remedy: `declare \`"secret": true\` or \`"secret": false\` in a parameter's meta file, ${RECORDS_PATH2}/<name>.json`
3034
+ }
3035
+ ];
3036
+ }
2914
3037
  function plaintextSecretFindings(subjects, environment) {
2915
3038
  const secrets = subjects.filter(({ meta }) => isSecret3(meta, environment));
2916
3039
  const findings = [];
@@ -3231,6 +3354,29 @@ function scannableFiles(directory, out2) {
3231
3354
  }
3232
3355
  }
3233
3356
  }
3357
+ function localExtensionFindings(project) {
3358
+ const names = localExtensions(project.root);
3359
+ if (names.length === 0) {
3360
+ return [
3361
+ {
3362
+ check: "local-extension",
3363
+ severity: "pass",
3364
+ label: "Extensions",
3365
+ subject: "every extension this project names is pinned"
3366
+ }
3367
+ ];
3368
+ }
3369
+ return [
3370
+ {
3371
+ check: "local-extension",
3372
+ severity: "unknown",
3373
+ label: "Extensions",
3374
+ subject: names.join(", "),
3375
+ detail: "resolved from this project's node_modules \u2014 no release, no pinned bytes",
3376
+ remedy: `publish it and run \`penv add ${names[0]}\` when this stops being a development path`
3377
+ }
3378
+ ];
3379
+ }
3234
3380
  function artifactFindings(project) {
3235
3381
  const files = [];
3236
3382
  scannableFiles(project.root, files);
@@ -3241,7 +3387,7 @@ function artifactFindings(project) {
3241
3387
  if (statSync2(file).size > ARTIFACT_SCAN_LIMIT) {
3242
3388
  continue;
3243
3389
  }
3244
- text = readFileSync5(file, "utf8");
3390
+ text = readFileSync6(file, "utf8");
3245
3391
  } catch {
3246
3392
  continue;
3247
3393
  }
@@ -4216,7 +4362,7 @@ var getCommand = defineCommand12({
4216
4362
  });
4217
4363
 
4218
4364
  // src/commands/import.ts
4219
- import { copyFileSync, existsSync as existsSync8, readFileSync as readFileSync8 } from "fs";
4365
+ import { copyFileSync, existsSync as existsSync9, readFileSync as readFileSync10 } from "fs";
4220
4366
  import { basename as basename2, isAbsolute as isAbsolute4, relative as relative4, resolve as resolve7 } from "path";
4221
4367
  import {
4222
4368
  assertNever as assertNever2,
@@ -4305,7 +4451,7 @@ function writeEntries(tree, entries, refs, scope) {
4305
4451
  }
4306
4452
 
4307
4453
  // src/detect.ts
4308
- import { existsSync as existsSync6, readFileSync as readFileSync6 } from "fs";
4454
+ import { existsSync as existsSync6, readFileSync as readFileSync7 } from "fs";
4309
4455
  import { join as join7 } from "path";
4310
4456
  import { DEFAULT_SCHEMA_FILE } from "@penvhq/core";
4311
4457
  var SIGNATURES = [
@@ -4323,7 +4469,7 @@ var SIGNATURES = [
4323
4469
  function exportsSchema(file) {
4324
4470
  let source;
4325
4471
  try {
4326
- source = readFileSync6(file, "utf8");
4472
+ source = readFileSync7(file, "utf8");
4327
4473
  } catch {
4328
4474
  return false;
4329
4475
  }
@@ -4375,7 +4521,7 @@ function manifestOf2(cwd) {
4375
4521
  }
4376
4522
  let manifest;
4377
4523
  try {
4378
- manifest = JSON.parse(readFileSync6(file, "utf8"));
4524
+ manifest = JSON.parse(readFileSync7(file, "utf8"));
4379
4525
  } catch {
4380
4526
  return void 0;
4381
4527
  }
@@ -4470,8 +4616,8 @@ function draftFieldsAcross(sources, environments) {
4470
4616
  }
4471
4617
 
4472
4618
  // src/commands/init.ts
4473
- import { existsSync as existsSync7, mkdirSync as mkdirSync3, readdirSync as readdirSync4, readFileSync as readFileSync7, writeFileSync as writeFileSync4 } from "fs";
4474
- import { dirname as dirname5, join as join8, resolve as resolve6 } from "path";
4619
+ import { existsSync as existsSync8, mkdirSync as mkdirSync4, readdirSync as readdirSync5, readFileSync as readFileSync9, writeFileSync as writeFileSync5 } from "fs";
4620
+ import { dirname as dirname6, join as join9, resolve as resolve6 } from "path";
4475
4621
  import { createInterface as createInterface2 } from "readline/promises";
4476
4622
  import {
4477
4623
  CUTOVER_PATH as CUTOVER_PATH3,
@@ -4482,7 +4628,7 @@ import {
4482
4628
  PenvError as PenvError18,
4483
4629
  parameterId as parameterId6,
4484
4630
  parseDotenv,
4485
- RECORDS_PATH as RECORDS_PATH2,
4631
+ RECORDS_PATH as RECORDS_PATH3,
4486
4632
  RESERVED_TOKENS as RESERVED_TOKENS2,
4487
4633
  ROLLBACK_DOTENV_PATH as ROLLBACK_DOTENV_PATH3,
4488
4634
  renderStateGitignore,
@@ -4494,6 +4640,97 @@ import {
4494
4640
  } from "@penvhq/core";
4495
4641
  import { defineCommand as defineCommand13 } from "citty";
4496
4642
 
4643
+ // src/scaffold-undo.ts
4644
+ import {
4645
+ existsSync as existsSync7,
4646
+ mkdirSync as mkdirSync3,
4647
+ readdirSync as readdirSync4,
4648
+ readFileSync as readFileSync8,
4649
+ rmdirSync,
4650
+ statSync as statSync3,
4651
+ unlinkSync,
4652
+ writeFileSync as writeFileSync4
4653
+ } from "fs";
4654
+ import { dirname as dirname5, join as join8 } from "path";
4655
+ function record(path, files, dirs) {
4656
+ let stats;
4657
+ try {
4658
+ stats = statSync3(path);
4659
+ } catch {
4660
+ return;
4661
+ }
4662
+ if (stats.isDirectory()) {
4663
+ dirs.add(path);
4664
+ for (const entry of readdirSync4(path)) {
4665
+ record(join8(path, entry), files, dirs);
4666
+ }
4667
+ return;
4668
+ }
4669
+ if (stats.isFile()) {
4670
+ files.set(path, readFileSync8(path));
4671
+ }
4672
+ }
4673
+ function captureScaffold(root, paths) {
4674
+ const files = /* @__PURE__ */ new Map();
4675
+ const dirs = /* @__PURE__ */ new Set();
4676
+ for (const path of paths) {
4677
+ for (let dir = dirname5(path); dir.startsWith(root) && dir !== root; dir = dirname5(dir)) {
4678
+ if (existsSync7(dir)) {
4679
+ dirs.add(dir);
4680
+ }
4681
+ }
4682
+ record(path, files, dirs);
4683
+ }
4684
+ return { root, paths: [...paths], files, dirs };
4685
+ }
4686
+ function restoreScaffold(undo) {
4687
+ for (const path of undo.paths) {
4688
+ removeAdded(path, undo);
4689
+ }
4690
+ for (const path of undo.paths) {
4691
+ pruneAncestors(path, undo);
4692
+ }
4693
+ for (const [file, contents] of undo.files) {
4694
+ mkdirSync3(dirname5(file), { recursive: true });
4695
+ writeFileSync4(file, contents);
4696
+ }
4697
+ }
4698
+ function removeAdded(path, undo) {
4699
+ let stats;
4700
+ try {
4701
+ stats = statSync3(path);
4702
+ } catch {
4703
+ return;
4704
+ }
4705
+ if (stats.isFile()) {
4706
+ if (!undo.files.has(path)) {
4707
+ unlinkSync(path);
4708
+ }
4709
+ return;
4710
+ }
4711
+ if (!stats.isDirectory()) {
4712
+ return;
4713
+ }
4714
+ for (const entry of readdirSync4(path)) {
4715
+ removeAdded(join8(path, entry), undo);
4716
+ }
4717
+ if (!undo.dirs.has(path) && readdirSync4(path).length === 0) {
4718
+ rmdirSync(path);
4719
+ }
4720
+ }
4721
+ function pruneAncestors(path, undo) {
4722
+ for (let dir = dirname5(path); dir.startsWith(undo.root) && dir !== undo.root; dir = dirname5(dir)) {
4723
+ if (undo.dirs.has(dir)) {
4724
+ return;
4725
+ }
4726
+ try {
4727
+ rmdirSync(dir);
4728
+ } catch {
4729
+ return;
4730
+ }
4731
+ }
4732
+ }
4733
+
4497
4734
  // src/seams.ts
4498
4735
  function nextjs({ alias, srcDir }) {
4499
4736
  return {
@@ -4649,7 +4886,7 @@ var NOT_ENVIRONMENTS2 = [...RESERVED_TOKENS2, "example", "sample", "template"];
4649
4886
  function suggestEnvironments(root) {
4650
4887
  let entries;
4651
4888
  try {
4652
- entries = readdirSync4(root);
4889
+ entries = readdirSync5(root);
4653
4890
  } catch {
4654
4891
  return [];
4655
4892
  }
@@ -4693,8 +4930,8 @@ function configOf(decisions) {
4693
4930
  return { environments: decisions.environments, providers: {}, schemaFile: decisions.schemaFile };
4694
4931
  }
4695
4932
  function declaredIn(root) {
4696
- const file = join8(root, CONFIG_FILE);
4697
- if (!existsSync7(file)) {
4933
+ const file = join9(root, CONFIG_FILE);
4934
+ if (!existsSync8(file)) {
4698
4935
  return void 0;
4699
4936
  }
4700
4937
  return loadConfigFrom(file);
@@ -5137,15 +5374,15 @@ function renderTsconfig(target, alias) {
5137
5374
  }
5138
5375
  function ensurePenvDir(root) {
5139
5376
  const dir = resolve6(root, PENV_DIR2);
5140
- if (existsSync7(dir)) {
5377
+ if (existsSync8(dir)) {
5141
5378
  return { target: "penv-dir", action: "kept", text: `Found ${PENV_DIR2}/` };
5142
5379
  }
5143
- mkdirSync3(dir, { recursive: true });
5380
+ mkdirSync4(dir, { recursive: true });
5144
5381
  return { target: "penv-dir", action: "created", text: `Created ${PENV_DIR2}/` };
5145
5382
  }
5146
5383
  function writeSchemaShapeFile(root, fields, draft, decisions = DEFAULT_DECISIONS) {
5147
- const file = join8(root, SCHEMA_SHAPE_FILE3);
5148
- if (existsSync7(file)) {
5384
+ const file = join9(root, SCHEMA_SHAPE_FILE3);
5385
+ if (existsSync8(file)) {
5149
5386
  return {
5150
5387
  target: "schema",
5151
5388
  action: "kept",
@@ -5162,7 +5399,7 @@ function writeSchemaShapeFile(root, fields, draft, decisions = DEFAULT_DECISIONS
5162
5399
  note: `(the shape lives there, from before the ${SCHEMA_SHAPE_FILE3} split \u2014 penv did not add a second one. To adopt the split: move the \`z.object\` (and any \`declare module\` block) into ${SCHEMA_SHAPE_FILE3}, leaving ${oldLayout} importing \`schema\` from it and calling \`load\`.)`
5163
5400
  };
5164
5401
  }
5165
- writeFileSync4(file, renderSchemaShapeModule(fields, draft), "utf8");
5402
+ writeFileSync5(file, renderSchemaShapeModule(fields, draft), "utf8");
5166
5403
  return {
5167
5404
  target: "schema",
5168
5405
  action: "created",
@@ -5171,8 +5408,8 @@ function writeSchemaShapeFile(root, fields, draft, decisions = DEFAULT_DECISIONS
5171
5408
  };
5172
5409
  }
5173
5410
  function writeEnvFile(root, decisions = DEFAULT_DECISIONS) {
5174
- const file = join8(root, ...decisions.schemaFile.split("/"));
5175
- if (existsSync7(file)) {
5411
+ const file = join9(root, ...decisions.schemaFile.split("/"));
5412
+ if (existsSync8(file)) {
5176
5413
  return {
5177
5414
  target: "env",
5178
5415
  action: "kept",
@@ -5180,8 +5417,8 @@ function writeEnvFile(root, decisions = DEFAULT_DECISIONS) {
5180
5417
  note: "(yours \u2014 penv never regenerates it)"
5181
5418
  };
5182
5419
  }
5183
- mkdirSync3(dirname5(file), { recursive: true });
5184
- writeFileSync4(file, renderEnvModule(decisions.schemaFile, decisions.inject), "utf8");
5420
+ mkdirSync4(dirname6(file), { recursive: true });
5421
+ writeFileSync5(file, renderEnvModule(decisions.schemaFile, decisions.inject), "utf8");
5185
5422
  return {
5186
5423
  target: "env",
5187
5424
  action: "created",
@@ -5190,19 +5427,19 @@ function writeEnvFile(root, decisions = DEFAULT_DECISIONS) {
5190
5427
  };
5191
5428
  }
5192
5429
  function writeConfigFile(root, decisions = DEFAULT_DECISIONS) {
5193
- const file = join8(root, CONFIG_FILE);
5194
- if (existsSync7(file)) {
5430
+ const file = join9(root, CONFIG_FILE);
5431
+ if (existsSync8(file)) {
5195
5432
  return { target: "config", action: "kept", text: `Kept ${CONFIG_FILE}` };
5196
5433
  }
5197
- writeFileSync4(file, renderConfigModule(decisions), "utf8");
5434
+ writeFileSync5(file, renderConfigModule(decisions), "utf8");
5198
5435
  return { target: "config", action: "created", text: `Generated ${CONFIG_FILE}` };
5199
5436
  }
5200
5437
  function writeTsconfigAlias(root, decisions = DEFAULT_DECISIONS) {
5201
5438
  const alias = decisions.alias;
5202
5439
  const imports = alias.startsWith(IMPORTS_PREFIX);
5203
- const file = join8(root, imports ? PACKAGE_FILE : TSCONFIG_FILE);
5440
+ const file = join9(root, imports ? PACKAGE_FILE : TSCONFIG_FILE);
5204
5441
  const where = imports ? PACKAGE_FILE : TSCONFIG_FILE;
5205
- if (!existsSync7(file)) {
5442
+ if (!existsSync8(file)) {
5206
5443
  if (imports) {
5207
5444
  return {
5208
5445
  target: "tsconfig",
@@ -5211,14 +5448,14 @@ function writeTsconfigAlias(root, decisions = DEFAULT_DECISIONS) {
5211
5448
  note: `(run \`npm init\` first, or use \`--alias @env\` to alias through ${TSCONFIG_FILE})`
5212
5449
  };
5213
5450
  }
5214
- writeFileSync4(file, renderTsconfig(decisions.schemaFile, alias), "utf8");
5451
+ writeFileSync5(file, renderTsconfig(decisions.schemaFile, alias), "utf8");
5215
5452
  return {
5216
5453
  target: "tsconfig",
5217
5454
  action: "created",
5218
5455
  text: `Created ${TSCONFIG_FILE} with the ${alias} path alias`
5219
5456
  };
5220
5457
  }
5221
- const source = readFileSync7(file, "utf8");
5458
+ const source = readFileSync9(file, "utf8");
5222
5459
  const edit = imports ? insertImportsAlias(source, decisions.schemaFile, alias) : insertEnvAlias(source, decisions.schemaFile, alias);
5223
5460
  if (edit.conflict !== void 0) {
5224
5461
  return {
@@ -5235,7 +5472,7 @@ function writeTsconfigAlias(root, decisions = DEFAULT_DECISIONS) {
5235
5472
  text: `Kept the ${alias} alias in ${where}`
5236
5473
  };
5237
5474
  }
5238
- writeFileSync4(file, edit.source, "utf8");
5475
+ writeFileSync5(file, edit.source, "utf8");
5239
5476
  return {
5240
5477
  target: "tsconfig",
5241
5478
  action: "updated",
@@ -5243,14 +5480,14 @@ function writeTsconfigAlias(root, decisions = DEFAULT_DECISIONS) {
5243
5480
  };
5244
5481
  }
5245
5482
  function writeGitignore(root, decisions = DEFAULT_DECISIONS) {
5246
- const file = join8(root, ...STATE_GITIGNORE_PATH.split("/"));
5483
+ const file = join9(root, ...STATE_GITIGNORE_PATH.split("/"));
5247
5484
  const wanted = renderStateGitignore(configOf(decisions));
5248
- const existing = existsSync7(file) ? readFileSync7(file, "utf8") : void 0;
5485
+ const existing = existsSync8(file) ? readFileSync9(file, "utf8") : void 0;
5249
5486
  if (existing === wanted) {
5250
5487
  return { target: "gitignore", action: "kept", text: `Kept ${STATE_GITIGNORE_PATH}` };
5251
5488
  }
5252
- mkdirSync3(dirname5(file), { recursive: true });
5253
- writeFileSync4(file, wanted, "utf8");
5489
+ mkdirSync4(dirname6(file), { recursive: true });
5490
+ writeFileSync5(file, wanted, "utf8");
5254
5491
  return {
5255
5492
  target: "gitignore",
5256
5493
  action: existing === void 0 ? "created" : "updated",
@@ -5266,12 +5503,12 @@ function outdatedRuntimeWarning(root) {
5266
5503
  return `Injection needs @penvhq/penv ${INJECT_MIN_VERSION}+ \u2014 this project has ${version}, whose \`load\` ignores \`{ inject: true }\`. Upgrade, or process.env stays empty.`;
5267
5504
  }
5268
5505
  function installedPenvVersion(root) {
5269
- const file = join8(root, "node_modules", "@penvhq", "penv", "package.json");
5270
- if (!existsSync7(file)) {
5506
+ const file = join9(root, "node_modules", "@penvhq", "penv", "package.json");
5507
+ if (!existsSync8(file)) {
5271
5508
  return void 0;
5272
5509
  }
5273
5510
  try {
5274
- const version = JSON.parse(readFileSync7(file, "utf8")).version;
5511
+ const version = JSON.parse(readFileSync9(file, "utf8")).version;
5275
5512
  return typeof version === "string" ? version : void 0;
5276
5513
  } catch {
5277
5514
  return void 0;
@@ -5311,11 +5548,11 @@ function writeSeam(root, decisions = DEFAULT_DECISIONS, framework = detectFramew
5311
5548
  };
5312
5549
  }
5313
5550
  const alsoNote = writeAlso(root, seam.also);
5314
- const file = join8(root, ...seam.file.split("/"));
5551
+ const file = join9(root, ...seam.file.split("/"));
5315
5552
  const baseNotes = [...seam.notes, ...alsoNote === void 0 ? [] : [alsoNote]];
5316
5553
  const notes = baseNotes.length === 0 ? "" : `
5317
5554
  ${baseNotes.map((n) => ` ${n}`).join("\n")}`;
5318
- if (existsSync7(file)) {
5555
+ if (existsSync8(file)) {
5319
5556
  return {
5320
5557
  target: "seam",
5321
5558
  action: "info",
@@ -5323,8 +5560,8 @@ ${baseNotes.map((n) => ` ${n}`).join("\n")}`;
5323
5560
  note: withWarning(`${seam.ifPresent}${notes}`, outdated)
5324
5561
  };
5325
5562
  }
5326
- mkdirSync3(dirname5(file), { recursive: true });
5327
- writeFileSync4(file, seam.content, "utf8");
5563
+ mkdirSync4(dirname6(file), { recursive: true });
5564
+ writeFileSync5(file, seam.content, "utf8");
5328
5565
  return {
5329
5566
  target: "seam",
5330
5567
  action: outdated === void 0 ? "created" : "info",
@@ -5336,12 +5573,12 @@ function writeAlso(root, also) {
5336
5573
  if (also === void 0) {
5337
5574
  return void 0;
5338
5575
  }
5339
- const file = join8(root, ...also.file.split("/"));
5340
- if (existsSync7(file)) {
5576
+ const file = join9(root, ...also.file.split("/"));
5577
+ if (existsSync8(file)) {
5341
5578
  return also.ifPresent;
5342
5579
  }
5343
- mkdirSync3(dirname5(file), { recursive: true });
5344
- writeFileSync4(file, also.content, "utf8");
5580
+ mkdirSync4(dirname6(file), { recursive: true });
5581
+ writeFileSync5(file, also.content, "utf8");
5345
5582
  return `Wrote ${also.file} to register it.`;
5346
5583
  }
5347
5584
  function withWarning(note, warning) {
@@ -5366,6 +5603,23 @@ function scaffold(root, fields, draft, decisions = DEFAULT_DECISIONS, framework
5366
5603
  const seam = writeSeam(root, decisions, framework);
5367
5604
  return seam === void 0 ? steps : [...steps, seam];
5368
5605
  }
5606
+ function scaffoldPaths(root, decisions = DEFAULT_DECISIONS, framework = detectFramework(root)?.name) {
5607
+ const fileAt = (relative5) => join9(root, ...relative5.split("/"));
5608
+ const seam = decisions.inject ? seamFor(framework, {
5609
+ alias: decisions.alias,
5610
+ srcDir: srcPrefix(root),
5611
+ schemaFile: decisions.schemaFile
5612
+ }) : void 0;
5613
+ return [
5614
+ fileAt(PENV_DIR2),
5615
+ fileAt(SCHEMA_SHAPE_FILE3),
5616
+ fileAt(decisions.schemaFile),
5617
+ fileAt(CONFIG_FILE),
5618
+ fileAt(TSCONFIG_FILE),
5619
+ fileAt(PACKAGE_FILE),
5620
+ ...seam?.kind === "scaffold" ? [fileAt(seam.file), ...seam.also === void 0 ? [] : [fileAt(seam.also.file)]] : []
5621
+ ];
5622
+ }
5369
5623
  var DEVELOPMENT = "development";
5370
5624
  var LOCAL_PROVIDER = "@penvhq/provider-filesystem";
5371
5625
  function planAdoption(root) {
@@ -5428,7 +5682,7 @@ function planCutover(input) {
5428
5682
  const diagnostics = [];
5429
5683
  let variables = 0;
5430
5684
  for (const file of selected) {
5431
- const parsed = parseDotenv(readFileSync7(join8(root, file.name), "utf8"));
5685
+ const parsed = parseDotenv(readFileSync9(join9(root, file.name), "utf8"));
5432
5686
  const fileRefs = refsForEntries(parsed.entries, file.name, config);
5433
5687
  adopted.push({ file, entries: parsed.entries, refs: fileRefs, scope: scopeOf(file) });
5434
5688
  refs.push(...fileRefs);
@@ -5539,17 +5793,27 @@ async function applyCutover(plan2, options = {}) {
5539
5793
  if (!plan2.install.satisfied) {
5540
5794
  await (options.install ?? installWithPackageManager)(plan2.install);
5541
5795
  }
5542
- const steps = scaffold(plan2.root, plan2.fields, true, plan2.decisions, plan2.framework);
5543
- const project = openProject(plan2.root);
5544
- const tree = localTree(project);
5545
- for (const adopted of plan2.adopted) {
5546
- writeEntries(tree, adopted.entries, adopted.refs, adopted.scope);
5547
- }
5548
- for (const environment of plan2.adopting) {
5549
- const check = await checkEnvironment(project, environment);
5550
- if (!check.result.ok) {
5551
- throw invalidAfterImport(check.result);
5796
+ const undo = captureScaffold(plan2.root, scaffoldPaths(plan2.root, plan2.decisions, plan2.framework));
5797
+ let steps;
5798
+ try {
5799
+ steps = scaffold(plan2.root, plan2.fields, true, plan2.decisions, plan2.framework);
5800
+ const project = openProject(plan2.root);
5801
+ const tree = localTree(project);
5802
+ for (const adopted of plan2.adopted) {
5803
+ writeEntries(tree, adopted.entries, adopted.refs, adopted.scope);
5804
+ }
5805
+ for (const environment of plan2.adopting) {
5806
+ const check = await checkEnvironment(project, environment);
5807
+ if (check.schema === void 0) {
5808
+ throw draftNotLoaded(check.result);
5809
+ }
5810
+ if (!check.result.ok) {
5811
+ throw invalidAfterImport(check.result);
5812
+ }
5552
5813
  }
5814
+ } catch (error) {
5815
+ restoreScaffold(undo);
5816
+ throw error;
5553
5817
  }
5554
5818
  const cutover = bundleDotenvFiles(
5555
5819
  plan2.root,
@@ -5558,13 +5822,24 @@ async function applyCutover(plan2, options = {}) {
5558
5822
  );
5559
5823
  return { plan: plan2, steps, moved: cutover.files, validated: plan2.adopting };
5560
5824
  }
5825
+ function issueLines(result2) {
5826
+ return result2.issues.map((issue) => ` ${issue.subject}: ${issue.message}`).join("\n");
5827
+ }
5828
+ var SCAFFOLD_ROLLED_BACK = "penv put the project back as it found it \u2014 your dotenv files and everything else are exactly where they were.";
5561
5829
  function invalidAfterImport(result2) {
5562
- const lines = result2.issues.map((issue) => ` ${issue.subject}: ${issue.message}`).join("\n");
5563
5830
  return new PenvError18(
5564
5831
  "INIT_CUTOVER_INVALID",
5565
- `The imported values do not satisfy the draft schema for ${result2.environment}, so your dotenv files were left where they are:
5566
- ${lines}`,
5567
- `Correct ${SCHEMA_SHAPE_FILE3} or the values above, then run \`penv init\` again.`
5832
+ `The imported values do not satisfy the draft schema for ${result2.environment}:
5833
+ ${issueLines(result2)}`,
5834
+ `Correct the values above, then run \`penv init\` again. ${SCAFFOLD_ROLLED_BACK}`
5835
+ );
5836
+ }
5837
+ function draftNotLoaded(result2) {
5838
+ return new PenvError18(
5839
+ "INIT_DRAFT_NOT_LOADED",
5840
+ `penv could not load the schema it drafted for ${result2.environment}, so nothing was checked against it:
5841
+ ${issueLines(result2)}`,
5842
+ `Fix the error above, then run \`penv init\` again. ${SCAFFOLD_ROLLED_BACK}`
5568
5843
  );
5569
5844
  }
5570
5845
  function renderSelection(plan2, selected = plan2.preselected) {
@@ -5595,7 +5870,7 @@ function renderCutoverPlan(plan2) {
5595
5870
  ]);
5596
5871
  rows.push([
5597
5872
  ` ${out.dim("values")}`,
5598
- `${RECORDS_PATH2}/`,
5873
+ `${RECORDS_PATH3}/`,
5599
5874
  out.dim(`\u2190 from ${plan2.variables} variables`)
5600
5875
  ]);
5601
5876
  rows.push([
@@ -5627,11 +5902,11 @@ function renderCutover(result2) {
5627
5902
  const glyph = step.action === "conflicted" ? WARN : step.action === "info" ? "\u2192" : CHECK;
5628
5903
  return step.note === void 0 ? { glyph, text: step.text } : { glyph, text: step.text, note: step.note };
5629
5904
  }),
5630
- ...plan2.install.satisfied ? [] : [{ glyph: CHECK, text: `Installed ${plan2.install.package}`, note: plan2.install.version }],
5905
+ ...plan2.install.packages.filter((entry) => !entry.satisfied).map((entry) => ({ glyph: CHECK, text: `Installed ${entry.name}`, note: entry.version })),
5631
5906
  {
5632
5907
  glyph: CHECK,
5633
5908
  text: `Imported ${plan2.fields.length} parameters`,
5634
- note: `into ${RECORDS_PATH2}/`
5909
+ note: `into ${RECORDS_PATH3}/`
5635
5910
  },
5636
5911
  { glyph: CHECK, text: `Validated ${result2.validated.join(", ")}` },
5637
5912
  {
@@ -5651,12 +5926,12 @@ function dailyCommand(root) {
5651
5926
  return `penv run -- ${detectPackageManager(root)} ${devScript(root)}`;
5652
5927
  }
5653
5928
  function devScript(root) {
5654
- const file = join8(root, PACKAGE_FILE);
5655
- if (!existsSync7(file)) {
5929
+ const file = join9(root, PACKAGE_FILE);
5930
+ if (!existsSync8(file)) {
5656
5931
  return "dev";
5657
5932
  }
5658
5933
  try {
5659
- const scripts = JSON.parse(readFileSync7(file, "utf8")).scripts;
5934
+ const scripts = JSON.parse(readFileSync9(file, "utf8")).scripts;
5660
5935
  if (scripts !== null && typeof scripts === "object" && !Array.isArray(scripts)) {
5661
5936
  const named = Object.keys(scripts);
5662
5937
  return ["dev", "start"].find((script) => named.includes(script)) ?? named[0] ?? "dev";
@@ -5890,7 +6165,7 @@ function runUndoAction(root, action) {
5890
6165
  ]),
5891
6166
  "",
5892
6167
  result2.missing.length === 0 ? `${out.green(CHECK)} ${out.bold("Undone.")} Your dotenv files are back exactly as they were, and ${CUTOVER_PATH3} is gone.` : `${out.yellow(WARN)} ${out.bold("Undone as far as penv could.")} Everything still in the bundle is back, and ${CUTOVER_PATH3} is gone.`,
5893
- `penv's records are still in ${RECORDS_PATH2}/ \u2014 nothing penv scaffolded is yours to lose.`
6168
+ `penv's records are still in ${RECORDS_PATH3}/ \u2014 nothing penv scaffolded is yours to lose.`
5894
6169
  ]);
5895
6170
  }
5896
6171
 
@@ -6025,14 +6300,14 @@ function configInEffect(cwd, environment) {
6025
6300
  function importDotenv(options) {
6026
6301
  const cwd = resolve7(options.cwd);
6027
6302
  const file = isAbsolute4(options.file) ? options.file : resolve7(cwd, options.file);
6028
- if (!existsSync8(file)) {
6303
+ if (!existsSync9(file)) {
6029
6304
  throw new PenvError19(
6030
6305
  "IMPORT_FILE_MISSING",
6031
6306
  `There is no file at ${file} to import`,
6032
6307
  "Point `penv import` at an existing dotenv file, e.g. `penv import .env`."
6033
6308
  );
6034
6309
  }
6035
- const parsed = parseDotenv2(readFileSync8(file, "utf8"));
6310
+ const parsed = parseDotenv2(readFileSync10(file, "utf8"));
6036
6311
  const { config, decisions } = configInEffect(cwd, environmentNamed(file, options.environment));
6037
6312
  const source = displayPath3(cwd, file);
6038
6313
  const named = scopeFromFilename(file, config);
@@ -6303,7 +6578,7 @@ var keyCommand = defineCommand15({
6303
6578
  });
6304
6579
 
6305
6580
  // src/commands/list.ts
6306
- import { assertNever as assertNever3, RECORDS_PATH as RECORDS_PATH3, resolveAll as resolveAll3, variableName as variableName8 } from "@penvhq/core";
6581
+ import { assertNever as assertNever3, RECORDS_PATH as RECORDS_PATH4, resolveAll as resolveAll3, variableName as variableName8 } from "@penvhq/core";
6307
6582
  import { defineCommand as defineCommand16 } from "citty";
6308
6583
  function scopeLabel2(scope) {
6309
6584
  switch (scope.kind) {
@@ -6347,7 +6622,7 @@ function paintScope(entry) {
6347
6622
  function renderList(result2) {
6348
6623
  if (result2.parameters.length === 0) {
6349
6624
  return [
6350
- `No parameters in ${RECORDS_PATH3}/ for environment ${result2.environment}.`,
6625
+ `No parameters in ${RECORDS_PATH4}/ for environment ${result2.environment}.`,
6351
6626
  tip(`penv set <key> --env ${result2.environment}`)
6352
6627
  ];
6353
6628
  }
@@ -6383,22 +6658,22 @@ var listCommand = defineCommand16({
6383
6658
 
6384
6659
  // src/commands/migrate.ts
6385
6660
  import {
6386
- existsSync as existsSync9,
6387
- mkdirSync as mkdirSync4,
6388
- readdirSync as readdirSync5,
6389
- readFileSync as readFileSync9,
6661
+ existsSync as existsSync10,
6662
+ mkdirSync as mkdirSync5,
6663
+ readdirSync as readdirSync6,
6664
+ readFileSync as readFileSync11,
6390
6665
  renameSync as renameSync2,
6391
6666
  rmSync as rmSync2,
6392
- writeFileSync as writeFileSync5
6667
+ writeFileSync as writeFileSync6
6393
6668
  } from "fs";
6394
- import { dirname as dirname6, join as join9 } from "path";
6669
+ import { dirname as dirname7, join as join10 } from "path";
6395
6670
  import { createInterface as createInterface3 } from "readline/promises";
6396
6671
  import {
6397
6672
  loadConfig as loadConfig2,
6398
6673
  oldLayoutEntries,
6399
6674
  PENV_DIR as PENV_DIR3,
6400
6675
  PenvError as PenvError22,
6401
- RECORDS_PATH as RECORDS_PATH4,
6676
+ RECORDS_PATH as RECORDS_PATH5,
6402
6677
  recordsDir as recordsDir3,
6403
6678
  renderStateGitignore as renderStateGitignore2,
6404
6679
  STATE_GITIGNORE_PATH as STATE_GITIGNORE_PATH2
@@ -6407,48 +6682,48 @@ import { defineCommand as defineCommand17 } from "citty";
6407
6682
  var OLD_GITIGNORE = `${PENV_DIR3}/.gitignore`;
6408
6683
  function planMigrate(cwd) {
6409
6684
  const { config, file } = loadConfig2(cwd);
6410
- const root = dirname6(file);
6685
+ const root = dirname7(file);
6411
6686
  const entries = oldLayoutEntries(root, config);
6412
6687
  const tree = recordsDir3(root);
6413
6688
  const collisions = collidingEntries(entries, tree);
6414
6689
  if (collisions.length > 0) {
6415
6690
  throw new PenvError22(
6416
6691
  "HALF_MIGRATED",
6417
- `${describe(collisions)} in both \`${PENV_DIR3}/\` and \`${RECORDS_PATH4}/\`, and penv cannot tell which copy is current`,
6418
- `Move what is left under \`${PENV_DIR3}/\` into \`${RECORDS_PATH4}/\` yourself, keeping the copy you want, then run \`penv validate\`.`
6692
+ `${describe2(collisions)} in both \`${PENV_DIR3}/\` and \`${RECORDS_PATH5}/\`, and penv cannot tell which copy is current`,
6693
+ `Move what is left under \`${PENV_DIR3}/\` into \`${RECORDS_PATH5}/\` yourself, keeping the copy you want, then run \`penv validate\`.`
6419
6694
  );
6420
6695
  }
6421
6696
  const creates = [];
6422
- if (entries.length > 0 && !existsSync9(tree)) {
6423
- creates.push(`${RECORDS_PATH4}/`);
6697
+ if (entries.length > 0 && !existsSync10(tree)) {
6698
+ creates.push(`${RECORDS_PATH5}/`);
6424
6699
  }
6425
- if (readIfPresent(join9(root, ...STATE_GITIGNORE_PATH2.split("/"))) !== renderStateGitignore2(config)) {
6700
+ if (readIfPresent(join10(root, ...STATE_GITIGNORE_PATH2.split("/"))) !== renderStateGitignore2(config)) {
6426
6701
  creates.push(STATE_GITIGNORE_PATH2);
6427
6702
  }
6428
6703
  return {
6429
6704
  root,
6430
6705
  moves: entries.map((entry) => ({
6431
6706
  from: `${PENV_DIR3}/${entry}`,
6432
- to: `${RECORDS_PATH4}/${entry}`
6707
+ to: `${RECORDS_PATH5}/${entry}`
6433
6708
  })),
6434
6709
  creates,
6435
- removes: existsSync9(join9(root, ...OLD_GITIGNORE.split("/"))) ? [OLD_GITIGNORE] : []
6710
+ removes: existsSync10(join10(root, ...OLD_GITIGNORE.split("/"))) ? [OLD_GITIGNORE] : []
6436
6711
  };
6437
6712
  }
6438
6713
  function readIfPresent(file) {
6439
- return existsSync9(file) ? readFileSync9(file, "utf8") : void 0;
6714
+ return existsSync10(file) ? readFileSync11(file, "utf8") : void 0;
6440
6715
  }
6441
6716
  function collidingEntries(entries, tree) {
6442
6717
  let held;
6443
6718
  try {
6444
- held = readdirSync5(tree);
6719
+ held = readdirSync6(tree);
6445
6720
  } catch {
6446
6721
  return [];
6447
6722
  }
6448
6723
  const taken = new Set(held.map((name) => name.toLowerCase()));
6449
6724
  return entries.filter((entry) => taken.has(entry.toLowerCase())).sort();
6450
6725
  }
6451
- function describe(names) {
6726
+ function describe2(names) {
6452
6727
  return names.length === 1 ? `\`${names[0]}\` is` : `${names.map((name) => `\`${name}\``).join(", ")} are`;
6453
6728
  }
6454
6729
  function isNoop(plan2) {
@@ -6460,18 +6735,18 @@ function applyMigrate(plan2) {
6460
6735
  }
6461
6736
  const { config } = loadConfig2(plan2.root);
6462
6737
  if (plan2.moves.length > 0) {
6463
- mkdirSync4(recordsDir3(plan2.root), { recursive: true });
6738
+ mkdirSync5(recordsDir3(plan2.root), { recursive: true });
6464
6739
  for (const move of plan2.moves) {
6465
- renameSync2(join9(plan2.root, ...move.from.split("/")), join9(plan2.root, ...move.to.split("/")));
6740
+ renameSync2(join10(plan2.root, ...move.from.split("/")), join10(plan2.root, ...move.to.split("/")));
6466
6741
  }
6467
6742
  }
6468
6743
  if (plan2.creates.includes(STATE_GITIGNORE_PATH2)) {
6469
- const ignore = join9(plan2.root, ...STATE_GITIGNORE_PATH2.split("/"));
6470
- mkdirSync4(dirname6(ignore), { recursive: true });
6471
- writeFileSync5(ignore, renderStateGitignore2(config), "utf8");
6744
+ const ignore = join10(plan2.root, ...STATE_GITIGNORE_PATH2.split("/"));
6745
+ mkdirSync5(dirname7(ignore), { recursive: true });
6746
+ writeFileSync6(ignore, renderStateGitignore2(config), "utf8");
6472
6747
  }
6473
6748
  for (const removed of plan2.removes) {
6474
- rmSync2(join9(plan2.root, ...removed.split("/")), { force: true });
6749
+ rmSync2(join10(plan2.root, ...removed.split("/")), { force: true });
6475
6750
  }
6476
6751
  return { ...plan2, status: "migrated" };
6477
6752
  }
@@ -6484,7 +6759,7 @@ function runMigrate(options) {
6484
6759
  }
6485
6760
  function renderMigrate(result2) {
6486
6761
  if (result2.status === "current") {
6487
- return [`${out.green(CHECK)} Already on ${RECORDS_PATH4}/ \u2014 nothing to migrate.`];
6762
+ return [`${out.green(CHECK)} Already on ${RECORDS_PATH5}/ \u2014 nothing to migrate.`];
6488
6763
  }
6489
6764
  const done = result2.status === "migrated";
6490
6765
  const glyph = done ? CHECK : "\u2192";
@@ -6525,7 +6800,7 @@ async function approveOnTty() {
6525
6800
  var migrateCommand = defineCommand17({
6526
6801
  meta: {
6527
6802
  name: "migrate",
6528
- description: `Move a project written under an earlier layout to ${RECORDS_PATH4}/`
6803
+ description: `Move a project written under an earlier layout to ${RECORDS_PATH5}/`
6529
6804
  },
6530
6805
  args: {
6531
6806
  yes: { type: "boolean", description: "Apply the previewed move without asking" }
@@ -6973,8 +7248,8 @@ var rotateCommand = defineCommand20({
6973
7248
  });
6974
7249
 
6975
7250
  // src/commands/watch.ts
6976
- import { existsSync as existsSync10, watch as watch2 } from "fs";
6977
- import { basename as basename3, dirname as dirname7, resolve as resolve8 } from "path";
7251
+ import { existsSync as existsSync11, watch as watch2 } from "fs";
7252
+ import { basename as basename3, dirname as dirname8, resolve as resolve8 } from "path";
6978
7253
  import { SCHEMA_SHAPE_FILE as SCHEMA_SHAPE_FILE4, schemaFileOf as schemaFileOf5, schemaInsideTree } from "@penvhq/core";
6979
7254
  import { defineCommand as defineCommand21 } from "citty";
6980
7255
  var DEBOUNCE_MS2 = 100;
@@ -7033,7 +7308,7 @@ function runWatch(options) {
7033
7308
  watcher.close();
7034
7309
  }
7035
7310
  function armRecovery(target, recursive, only) {
7036
- const parent = dirname7(target);
7311
+ const parent = dirname8(target);
7037
7312
  const name = basename3(target);
7038
7313
  let recovery;
7039
7314
  try {
@@ -7041,14 +7316,14 @@ function runWatch(options) {
7041
7316
  if (closed || recovery === void 0) {
7042
7317
  return;
7043
7318
  }
7044
- if (!existsSync10(parent)) {
7319
+ if (!existsSync11(parent)) {
7045
7320
  stop2(recovery);
7046
7321
  return;
7047
7322
  }
7048
7323
  if (filename !== null && basename3(filename) !== name) {
7049
7324
  return;
7050
7325
  }
7051
- if (!existsSync10(target)) {
7326
+ if (!existsSync11(target)) {
7052
7327
  return;
7053
7328
  }
7054
7329
  stop2(recovery);
@@ -7072,7 +7347,7 @@ function runWatch(options) {
7072
7347
  if (closed) {
7073
7348
  return;
7074
7349
  }
7075
- if (!existsSync10(target)) {
7350
+ if (!existsSync11(target)) {
7076
7351
  if (watcher !== void 0) {
7077
7352
  stop2(watcher);
7078
7353
  }
@@ -7107,11 +7382,11 @@ function runWatch(options) {
7107
7382
  watchers.add(watcher);
7108
7383
  }
7109
7384
  addWatcher(project.recordsDir, true);
7110
- addWatcher(dirname7(project.configFile), false, configFile);
7385
+ addWatcher(dirname8(project.configFile), false, configFile);
7111
7386
  addWatcher(project.root, false, SCHEMA_SHAPE_FILE4);
7112
7387
  if (schemaInsideTree(project.config) === void 0) {
7113
7388
  const schemaFile = resolve8(project.root, schemaFileOf5(project.config));
7114
- addWatcher(dirname7(schemaFile), false, basename3(schemaFile));
7389
+ addWatcher(dirname8(schemaFile), false, basename3(schemaFile));
7115
7390
  }
7116
7391
  void validate();
7117
7392
  return {