@penvhq/cli 0.9.2 → 0.9.4

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) {
@@ -2152,20 +2217,20 @@ function readCutover(root) {
2152
2217
  if (parsed === null || typeof parsed !== "object" || Array.isArray(parsed)) {
2153
2218
  throw unreadable("it is not an object");
2154
2219
  }
2155
- const record = parsed;
2156
- if (record.format !== CUTOVER_FORMAT) {
2220
+ const record2 = parsed;
2221
+ if (record2.format !== CUTOVER_FORMAT) {
2157
2222
  throw unreadable(
2158
- `it is format ${JSON.stringify(record.format)}, and penv reads format ${CUTOVER_FORMAT}`
2223
+ `it is format ${JSON.stringify(record2.format)}, and penv reads format ${CUTOVER_FORMAT}`
2159
2224
  );
2160
2225
  }
2161
- const files = record.files;
2226
+ const files = record2.files;
2162
2227
  if (!Array.isArray(files) || files.some((name) => typeof name !== "string")) {
2163
2228
  throw unreadable("it lists no filenames");
2164
2229
  }
2165
- const environments = Array.isArray(record.environments) ? record.environments.filter((name) => typeof name === "string") : [];
2230
+ const environments = Array.isArray(record2.environments) ? record2.environments.filter((name) => typeof name === "string") : [];
2166
2231
  return {
2167
2232
  format: CUTOVER_FORMAT,
2168
- movedAt: typeof record.movedAt === "string" ? record.movedAt : "",
2233
+ movedAt: typeof record2.movedAt === "string" ? record2.movedAt : "",
2169
2234
  files,
2170
2235
  environments
2171
2236
  };
@@ -4216,7 +4281,7 @@ var getCommand = defineCommand12({
4216
4281
  });
4217
4282
 
4218
4283
  // src/commands/import.ts
4219
- import { copyFileSync, existsSync as existsSync8, readFileSync as readFileSync8 } from "fs";
4284
+ import { copyFileSync, existsSync as existsSync9, readFileSync as readFileSync9 } from "fs";
4220
4285
  import { basename as basename2, isAbsolute as isAbsolute4, relative as relative4, resolve as resolve7 } from "path";
4221
4286
  import {
4222
4287
  assertNever as assertNever2,
@@ -4470,8 +4535,8 @@ function draftFieldsAcross(sources, environments) {
4470
4535
  }
4471
4536
 
4472
4537
  // 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";
4538
+ import { existsSync as existsSync8, mkdirSync as mkdirSync4, readdirSync as readdirSync5, readFileSync as readFileSync8, writeFileSync as writeFileSync5 } from "fs";
4539
+ import { dirname as dirname6, join as join9, resolve as resolve6 } from "path";
4475
4540
  import { createInterface as createInterface2 } from "readline/promises";
4476
4541
  import {
4477
4542
  CUTOVER_PATH as CUTOVER_PATH3,
@@ -4494,6 +4559,97 @@ import {
4494
4559
  } from "@penvhq/core";
4495
4560
  import { defineCommand as defineCommand13 } from "citty";
4496
4561
 
4562
+ // src/scaffold-undo.ts
4563
+ import {
4564
+ existsSync as existsSync7,
4565
+ mkdirSync as mkdirSync3,
4566
+ readdirSync as readdirSync4,
4567
+ readFileSync as readFileSync7,
4568
+ rmdirSync,
4569
+ statSync as statSync3,
4570
+ unlinkSync,
4571
+ writeFileSync as writeFileSync4
4572
+ } from "fs";
4573
+ import { dirname as dirname5, join as join8 } from "path";
4574
+ function record(path, files, dirs) {
4575
+ let stats;
4576
+ try {
4577
+ stats = statSync3(path);
4578
+ } catch {
4579
+ return;
4580
+ }
4581
+ if (stats.isDirectory()) {
4582
+ dirs.add(path);
4583
+ for (const entry of readdirSync4(path)) {
4584
+ record(join8(path, entry), files, dirs);
4585
+ }
4586
+ return;
4587
+ }
4588
+ if (stats.isFile()) {
4589
+ files.set(path, readFileSync7(path));
4590
+ }
4591
+ }
4592
+ function captureScaffold(root, paths) {
4593
+ const files = /* @__PURE__ */ new Map();
4594
+ const dirs = /* @__PURE__ */ new Set();
4595
+ for (const path of paths) {
4596
+ for (let dir = dirname5(path); dir.startsWith(root) && dir !== root; dir = dirname5(dir)) {
4597
+ if (existsSync7(dir)) {
4598
+ dirs.add(dir);
4599
+ }
4600
+ }
4601
+ record(path, files, dirs);
4602
+ }
4603
+ return { root, paths: [...paths], files, dirs };
4604
+ }
4605
+ function restoreScaffold(undo) {
4606
+ for (const path of undo.paths) {
4607
+ removeAdded(path, undo);
4608
+ }
4609
+ for (const path of undo.paths) {
4610
+ pruneAncestors(path, undo);
4611
+ }
4612
+ for (const [file, contents] of undo.files) {
4613
+ mkdirSync3(dirname5(file), { recursive: true });
4614
+ writeFileSync4(file, contents);
4615
+ }
4616
+ }
4617
+ function removeAdded(path, undo) {
4618
+ let stats;
4619
+ try {
4620
+ stats = statSync3(path);
4621
+ } catch {
4622
+ return;
4623
+ }
4624
+ if (stats.isFile()) {
4625
+ if (!undo.files.has(path)) {
4626
+ unlinkSync(path);
4627
+ }
4628
+ return;
4629
+ }
4630
+ if (!stats.isDirectory()) {
4631
+ return;
4632
+ }
4633
+ for (const entry of readdirSync4(path)) {
4634
+ removeAdded(join8(path, entry), undo);
4635
+ }
4636
+ if (!undo.dirs.has(path) && readdirSync4(path).length === 0) {
4637
+ rmdirSync(path);
4638
+ }
4639
+ }
4640
+ function pruneAncestors(path, undo) {
4641
+ for (let dir = dirname5(path); dir.startsWith(undo.root) && dir !== undo.root; dir = dirname5(dir)) {
4642
+ if (undo.dirs.has(dir)) {
4643
+ return;
4644
+ }
4645
+ try {
4646
+ rmdirSync(dir);
4647
+ } catch {
4648
+ return;
4649
+ }
4650
+ }
4651
+ }
4652
+
4497
4653
  // src/seams.ts
4498
4654
  function nextjs({ alias, srcDir }) {
4499
4655
  return {
@@ -4649,7 +4805,7 @@ var NOT_ENVIRONMENTS2 = [...RESERVED_TOKENS2, "example", "sample", "template"];
4649
4805
  function suggestEnvironments(root) {
4650
4806
  let entries;
4651
4807
  try {
4652
- entries = readdirSync4(root);
4808
+ entries = readdirSync5(root);
4653
4809
  } catch {
4654
4810
  return [];
4655
4811
  }
@@ -4693,8 +4849,8 @@ function configOf(decisions) {
4693
4849
  return { environments: decisions.environments, providers: {}, schemaFile: decisions.schemaFile };
4694
4850
  }
4695
4851
  function declaredIn(root) {
4696
- const file = join8(root, CONFIG_FILE);
4697
- if (!existsSync7(file)) {
4852
+ const file = join9(root, CONFIG_FILE);
4853
+ if (!existsSync8(file)) {
4698
4854
  return void 0;
4699
4855
  }
4700
4856
  return loadConfigFrom(file);
@@ -5137,15 +5293,15 @@ function renderTsconfig(target, alias) {
5137
5293
  }
5138
5294
  function ensurePenvDir(root) {
5139
5295
  const dir = resolve6(root, PENV_DIR2);
5140
- if (existsSync7(dir)) {
5296
+ if (existsSync8(dir)) {
5141
5297
  return { target: "penv-dir", action: "kept", text: `Found ${PENV_DIR2}/` };
5142
5298
  }
5143
- mkdirSync3(dir, { recursive: true });
5299
+ mkdirSync4(dir, { recursive: true });
5144
5300
  return { target: "penv-dir", action: "created", text: `Created ${PENV_DIR2}/` };
5145
5301
  }
5146
5302
  function writeSchemaShapeFile(root, fields, draft, decisions = DEFAULT_DECISIONS) {
5147
- const file = join8(root, SCHEMA_SHAPE_FILE3);
5148
- if (existsSync7(file)) {
5303
+ const file = join9(root, SCHEMA_SHAPE_FILE3);
5304
+ if (existsSync8(file)) {
5149
5305
  return {
5150
5306
  target: "schema",
5151
5307
  action: "kept",
@@ -5162,7 +5318,7 @@ function writeSchemaShapeFile(root, fields, draft, decisions = DEFAULT_DECISIONS
5162
5318
  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
5319
  };
5164
5320
  }
5165
- writeFileSync4(file, renderSchemaShapeModule(fields, draft), "utf8");
5321
+ writeFileSync5(file, renderSchemaShapeModule(fields, draft), "utf8");
5166
5322
  return {
5167
5323
  target: "schema",
5168
5324
  action: "created",
@@ -5171,8 +5327,8 @@ function writeSchemaShapeFile(root, fields, draft, decisions = DEFAULT_DECISIONS
5171
5327
  };
5172
5328
  }
5173
5329
  function writeEnvFile(root, decisions = DEFAULT_DECISIONS) {
5174
- const file = join8(root, ...decisions.schemaFile.split("/"));
5175
- if (existsSync7(file)) {
5330
+ const file = join9(root, ...decisions.schemaFile.split("/"));
5331
+ if (existsSync8(file)) {
5176
5332
  return {
5177
5333
  target: "env",
5178
5334
  action: "kept",
@@ -5180,8 +5336,8 @@ function writeEnvFile(root, decisions = DEFAULT_DECISIONS) {
5180
5336
  note: "(yours \u2014 penv never regenerates it)"
5181
5337
  };
5182
5338
  }
5183
- mkdirSync3(dirname5(file), { recursive: true });
5184
- writeFileSync4(file, renderEnvModule(decisions.schemaFile, decisions.inject), "utf8");
5339
+ mkdirSync4(dirname6(file), { recursive: true });
5340
+ writeFileSync5(file, renderEnvModule(decisions.schemaFile, decisions.inject), "utf8");
5185
5341
  return {
5186
5342
  target: "env",
5187
5343
  action: "created",
@@ -5190,19 +5346,19 @@ function writeEnvFile(root, decisions = DEFAULT_DECISIONS) {
5190
5346
  };
5191
5347
  }
5192
5348
  function writeConfigFile(root, decisions = DEFAULT_DECISIONS) {
5193
- const file = join8(root, CONFIG_FILE);
5194
- if (existsSync7(file)) {
5349
+ const file = join9(root, CONFIG_FILE);
5350
+ if (existsSync8(file)) {
5195
5351
  return { target: "config", action: "kept", text: `Kept ${CONFIG_FILE}` };
5196
5352
  }
5197
- writeFileSync4(file, renderConfigModule(decisions), "utf8");
5353
+ writeFileSync5(file, renderConfigModule(decisions), "utf8");
5198
5354
  return { target: "config", action: "created", text: `Generated ${CONFIG_FILE}` };
5199
5355
  }
5200
5356
  function writeTsconfigAlias(root, decisions = DEFAULT_DECISIONS) {
5201
5357
  const alias = decisions.alias;
5202
5358
  const imports = alias.startsWith(IMPORTS_PREFIX);
5203
- const file = join8(root, imports ? PACKAGE_FILE : TSCONFIG_FILE);
5359
+ const file = join9(root, imports ? PACKAGE_FILE : TSCONFIG_FILE);
5204
5360
  const where = imports ? PACKAGE_FILE : TSCONFIG_FILE;
5205
- if (!existsSync7(file)) {
5361
+ if (!existsSync8(file)) {
5206
5362
  if (imports) {
5207
5363
  return {
5208
5364
  target: "tsconfig",
@@ -5211,14 +5367,14 @@ function writeTsconfigAlias(root, decisions = DEFAULT_DECISIONS) {
5211
5367
  note: `(run \`npm init\` first, or use \`--alias @env\` to alias through ${TSCONFIG_FILE})`
5212
5368
  };
5213
5369
  }
5214
- writeFileSync4(file, renderTsconfig(decisions.schemaFile, alias), "utf8");
5370
+ writeFileSync5(file, renderTsconfig(decisions.schemaFile, alias), "utf8");
5215
5371
  return {
5216
5372
  target: "tsconfig",
5217
5373
  action: "created",
5218
5374
  text: `Created ${TSCONFIG_FILE} with the ${alias} path alias`
5219
5375
  };
5220
5376
  }
5221
- const source = readFileSync7(file, "utf8");
5377
+ const source = readFileSync8(file, "utf8");
5222
5378
  const edit = imports ? insertImportsAlias(source, decisions.schemaFile, alias) : insertEnvAlias(source, decisions.schemaFile, alias);
5223
5379
  if (edit.conflict !== void 0) {
5224
5380
  return {
@@ -5235,7 +5391,7 @@ function writeTsconfigAlias(root, decisions = DEFAULT_DECISIONS) {
5235
5391
  text: `Kept the ${alias} alias in ${where}`
5236
5392
  };
5237
5393
  }
5238
- writeFileSync4(file, edit.source, "utf8");
5394
+ writeFileSync5(file, edit.source, "utf8");
5239
5395
  return {
5240
5396
  target: "tsconfig",
5241
5397
  action: "updated",
@@ -5243,14 +5399,14 @@ function writeTsconfigAlias(root, decisions = DEFAULT_DECISIONS) {
5243
5399
  };
5244
5400
  }
5245
5401
  function writeGitignore(root, decisions = DEFAULT_DECISIONS) {
5246
- const file = join8(root, ...STATE_GITIGNORE_PATH.split("/"));
5402
+ const file = join9(root, ...STATE_GITIGNORE_PATH.split("/"));
5247
5403
  const wanted = renderStateGitignore(configOf(decisions));
5248
- const existing = existsSync7(file) ? readFileSync7(file, "utf8") : void 0;
5404
+ const existing = existsSync8(file) ? readFileSync8(file, "utf8") : void 0;
5249
5405
  if (existing === wanted) {
5250
5406
  return { target: "gitignore", action: "kept", text: `Kept ${STATE_GITIGNORE_PATH}` };
5251
5407
  }
5252
- mkdirSync3(dirname5(file), { recursive: true });
5253
- writeFileSync4(file, wanted, "utf8");
5408
+ mkdirSync4(dirname6(file), { recursive: true });
5409
+ writeFileSync5(file, wanted, "utf8");
5254
5410
  return {
5255
5411
  target: "gitignore",
5256
5412
  action: existing === void 0 ? "created" : "updated",
@@ -5266,12 +5422,12 @@ function outdatedRuntimeWarning(root) {
5266
5422
  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
5423
  }
5268
5424
  function installedPenvVersion(root) {
5269
- const file = join8(root, "node_modules", "@penvhq", "penv", "package.json");
5270
- if (!existsSync7(file)) {
5425
+ const file = join9(root, "node_modules", "@penvhq", "penv", "package.json");
5426
+ if (!existsSync8(file)) {
5271
5427
  return void 0;
5272
5428
  }
5273
5429
  try {
5274
- const version = JSON.parse(readFileSync7(file, "utf8")).version;
5430
+ const version = JSON.parse(readFileSync8(file, "utf8")).version;
5275
5431
  return typeof version === "string" ? version : void 0;
5276
5432
  } catch {
5277
5433
  return void 0;
@@ -5311,11 +5467,11 @@ function writeSeam(root, decisions = DEFAULT_DECISIONS, framework = detectFramew
5311
5467
  };
5312
5468
  }
5313
5469
  const alsoNote = writeAlso(root, seam.also);
5314
- const file = join8(root, ...seam.file.split("/"));
5470
+ const file = join9(root, ...seam.file.split("/"));
5315
5471
  const baseNotes = [...seam.notes, ...alsoNote === void 0 ? [] : [alsoNote]];
5316
5472
  const notes = baseNotes.length === 0 ? "" : `
5317
5473
  ${baseNotes.map((n) => ` ${n}`).join("\n")}`;
5318
- if (existsSync7(file)) {
5474
+ if (existsSync8(file)) {
5319
5475
  return {
5320
5476
  target: "seam",
5321
5477
  action: "info",
@@ -5323,8 +5479,8 @@ ${baseNotes.map((n) => ` ${n}`).join("\n")}`;
5323
5479
  note: withWarning(`${seam.ifPresent}${notes}`, outdated)
5324
5480
  };
5325
5481
  }
5326
- mkdirSync3(dirname5(file), { recursive: true });
5327
- writeFileSync4(file, seam.content, "utf8");
5482
+ mkdirSync4(dirname6(file), { recursive: true });
5483
+ writeFileSync5(file, seam.content, "utf8");
5328
5484
  return {
5329
5485
  target: "seam",
5330
5486
  action: outdated === void 0 ? "created" : "info",
@@ -5336,12 +5492,12 @@ function writeAlso(root, also) {
5336
5492
  if (also === void 0) {
5337
5493
  return void 0;
5338
5494
  }
5339
- const file = join8(root, ...also.file.split("/"));
5340
- if (existsSync7(file)) {
5495
+ const file = join9(root, ...also.file.split("/"));
5496
+ if (existsSync8(file)) {
5341
5497
  return also.ifPresent;
5342
5498
  }
5343
- mkdirSync3(dirname5(file), { recursive: true });
5344
- writeFileSync4(file, also.content, "utf8");
5499
+ mkdirSync4(dirname6(file), { recursive: true });
5500
+ writeFileSync5(file, also.content, "utf8");
5345
5501
  return `Wrote ${also.file} to register it.`;
5346
5502
  }
5347
5503
  function withWarning(note, warning) {
@@ -5366,6 +5522,23 @@ function scaffold(root, fields, draft, decisions = DEFAULT_DECISIONS, framework
5366
5522
  const seam = writeSeam(root, decisions, framework);
5367
5523
  return seam === void 0 ? steps : [...steps, seam];
5368
5524
  }
5525
+ function scaffoldPaths(root, decisions = DEFAULT_DECISIONS, framework = detectFramework(root)?.name) {
5526
+ const fileAt = (relative5) => join9(root, ...relative5.split("/"));
5527
+ const seam = decisions.inject ? seamFor(framework, {
5528
+ alias: decisions.alias,
5529
+ srcDir: srcPrefix(root),
5530
+ schemaFile: decisions.schemaFile
5531
+ }) : void 0;
5532
+ return [
5533
+ fileAt(PENV_DIR2),
5534
+ fileAt(SCHEMA_SHAPE_FILE3),
5535
+ fileAt(decisions.schemaFile),
5536
+ fileAt(CONFIG_FILE),
5537
+ fileAt(TSCONFIG_FILE),
5538
+ fileAt(PACKAGE_FILE),
5539
+ ...seam?.kind === "scaffold" ? [fileAt(seam.file), ...seam.also === void 0 ? [] : [fileAt(seam.also.file)]] : []
5540
+ ];
5541
+ }
5369
5542
  var DEVELOPMENT = "development";
5370
5543
  var LOCAL_PROVIDER = "@penvhq/provider-filesystem";
5371
5544
  function planAdoption(root) {
@@ -5428,7 +5601,7 @@ function planCutover(input) {
5428
5601
  const diagnostics = [];
5429
5602
  let variables = 0;
5430
5603
  for (const file of selected) {
5431
- const parsed = parseDotenv(readFileSync7(join8(root, file.name), "utf8"));
5604
+ const parsed = parseDotenv(readFileSync8(join9(root, file.name), "utf8"));
5432
5605
  const fileRefs = refsForEntries(parsed.entries, file.name, config);
5433
5606
  adopted.push({ file, entries: parsed.entries, refs: fileRefs, scope: scopeOf(file) });
5434
5607
  refs.push(...fileRefs);
@@ -5539,17 +5712,27 @@ async function applyCutover(plan2, options = {}) {
5539
5712
  if (!plan2.install.satisfied) {
5540
5713
  await (options.install ?? installWithPackageManager)(plan2.install);
5541
5714
  }
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);
5715
+ const undo = captureScaffold(plan2.root, scaffoldPaths(plan2.root, plan2.decisions, plan2.framework));
5716
+ let steps;
5717
+ try {
5718
+ steps = scaffold(plan2.root, plan2.fields, true, plan2.decisions, plan2.framework);
5719
+ const project = openProject(plan2.root);
5720
+ const tree = localTree(project);
5721
+ for (const adopted of plan2.adopted) {
5722
+ writeEntries(tree, adopted.entries, adopted.refs, adopted.scope);
5723
+ }
5724
+ for (const environment of plan2.adopting) {
5725
+ const check = await checkEnvironment(project, environment);
5726
+ if (check.schema === void 0) {
5727
+ throw draftNotLoaded(check.result);
5728
+ }
5729
+ if (!check.result.ok) {
5730
+ throw invalidAfterImport(check.result);
5731
+ }
5552
5732
  }
5733
+ } catch (error) {
5734
+ restoreScaffold(undo);
5735
+ throw error;
5553
5736
  }
5554
5737
  const cutover = bundleDotenvFiles(
5555
5738
  plan2.root,
@@ -5558,13 +5741,24 @@ async function applyCutover(plan2, options = {}) {
5558
5741
  );
5559
5742
  return { plan: plan2, steps, moved: cutover.files, validated: plan2.adopting };
5560
5743
  }
5744
+ function issueLines(result2) {
5745
+ return result2.issues.map((issue) => ` ${issue.subject}: ${issue.message}`).join("\n");
5746
+ }
5747
+ 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
5748
  function invalidAfterImport(result2) {
5562
- const lines = result2.issues.map((issue) => ` ${issue.subject}: ${issue.message}`).join("\n");
5563
5749
  return new PenvError18(
5564
5750
  "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.`
5751
+ `The imported values do not satisfy the draft schema for ${result2.environment}:
5752
+ ${issueLines(result2)}`,
5753
+ `Correct the values above, then run \`penv init\` again. ${SCAFFOLD_ROLLED_BACK}`
5754
+ );
5755
+ }
5756
+ function draftNotLoaded(result2) {
5757
+ return new PenvError18(
5758
+ "INIT_DRAFT_NOT_LOADED",
5759
+ `penv could not load the schema it drafted for ${result2.environment}, so nothing was checked against it:
5760
+ ${issueLines(result2)}`,
5761
+ `Fix the error above, then run \`penv init\` again. ${SCAFFOLD_ROLLED_BACK}`
5568
5762
  );
5569
5763
  }
5570
5764
  function renderSelection(plan2, selected = plan2.preselected) {
@@ -5627,7 +5821,7 @@ function renderCutover(result2) {
5627
5821
  const glyph = step.action === "conflicted" ? WARN : step.action === "info" ? "\u2192" : CHECK;
5628
5822
  return step.note === void 0 ? { glyph, text: step.text } : { glyph, text: step.text, note: step.note };
5629
5823
  }),
5630
- ...plan2.install.satisfied ? [] : [{ glyph: CHECK, text: `Installed ${plan2.install.package}`, note: plan2.install.version }],
5824
+ ...plan2.install.packages.filter((entry) => !entry.satisfied).map((entry) => ({ glyph: CHECK, text: `Installed ${entry.name}`, note: entry.version })),
5631
5825
  {
5632
5826
  glyph: CHECK,
5633
5827
  text: `Imported ${plan2.fields.length} parameters`,
@@ -5651,12 +5845,12 @@ function dailyCommand(root) {
5651
5845
  return `penv run -- ${detectPackageManager(root)} ${devScript(root)}`;
5652
5846
  }
5653
5847
  function devScript(root) {
5654
- const file = join8(root, PACKAGE_FILE);
5655
- if (!existsSync7(file)) {
5848
+ const file = join9(root, PACKAGE_FILE);
5849
+ if (!existsSync8(file)) {
5656
5850
  return "dev";
5657
5851
  }
5658
5852
  try {
5659
- const scripts = JSON.parse(readFileSync7(file, "utf8")).scripts;
5853
+ const scripts = JSON.parse(readFileSync8(file, "utf8")).scripts;
5660
5854
  if (scripts !== null && typeof scripts === "object" && !Array.isArray(scripts)) {
5661
5855
  const named = Object.keys(scripts);
5662
5856
  return ["dev", "start"].find((script) => named.includes(script)) ?? named[0] ?? "dev";
@@ -6025,14 +6219,14 @@ function configInEffect(cwd, environment) {
6025
6219
  function importDotenv(options) {
6026
6220
  const cwd = resolve7(options.cwd);
6027
6221
  const file = isAbsolute4(options.file) ? options.file : resolve7(cwd, options.file);
6028
- if (!existsSync8(file)) {
6222
+ if (!existsSync9(file)) {
6029
6223
  throw new PenvError19(
6030
6224
  "IMPORT_FILE_MISSING",
6031
6225
  `There is no file at ${file} to import`,
6032
6226
  "Point `penv import` at an existing dotenv file, e.g. `penv import .env`."
6033
6227
  );
6034
6228
  }
6035
- const parsed = parseDotenv2(readFileSync8(file, "utf8"));
6229
+ const parsed = parseDotenv2(readFileSync9(file, "utf8"));
6036
6230
  const { config, decisions } = configInEffect(cwd, environmentNamed(file, options.environment));
6037
6231
  const source = displayPath3(cwd, file);
6038
6232
  const named = scopeFromFilename(file, config);
@@ -6383,15 +6577,15 @@ var listCommand = defineCommand16({
6383
6577
 
6384
6578
  // src/commands/migrate.ts
6385
6579
  import {
6386
- existsSync as existsSync9,
6387
- mkdirSync as mkdirSync4,
6388
- readdirSync as readdirSync5,
6389
- readFileSync as readFileSync9,
6580
+ existsSync as existsSync10,
6581
+ mkdirSync as mkdirSync5,
6582
+ readdirSync as readdirSync6,
6583
+ readFileSync as readFileSync10,
6390
6584
  renameSync as renameSync2,
6391
6585
  rmSync as rmSync2,
6392
- writeFileSync as writeFileSync5
6586
+ writeFileSync as writeFileSync6
6393
6587
  } from "fs";
6394
- import { dirname as dirname6, join as join9 } from "path";
6588
+ import { dirname as dirname7, join as join10 } from "path";
6395
6589
  import { createInterface as createInterface3 } from "readline/promises";
6396
6590
  import {
6397
6591
  loadConfig as loadConfig2,
@@ -6407,22 +6601,22 @@ import { defineCommand as defineCommand17 } from "citty";
6407
6601
  var OLD_GITIGNORE = `${PENV_DIR3}/.gitignore`;
6408
6602
  function planMigrate(cwd) {
6409
6603
  const { config, file } = loadConfig2(cwd);
6410
- const root = dirname6(file);
6604
+ const root = dirname7(file);
6411
6605
  const entries = oldLayoutEntries(root, config);
6412
6606
  const tree = recordsDir3(root);
6413
6607
  const collisions = collidingEntries(entries, tree);
6414
6608
  if (collisions.length > 0) {
6415
6609
  throw new PenvError22(
6416
6610
  "HALF_MIGRATED",
6417
- `${describe(collisions)} in both \`${PENV_DIR3}/\` and \`${RECORDS_PATH4}/\`, and penv cannot tell which copy is current`,
6611
+ `${describe2(collisions)} in both \`${PENV_DIR3}/\` and \`${RECORDS_PATH4}/\`, and penv cannot tell which copy is current`,
6418
6612
  `Move what is left under \`${PENV_DIR3}/\` into \`${RECORDS_PATH4}/\` yourself, keeping the copy you want, then run \`penv validate\`.`
6419
6613
  );
6420
6614
  }
6421
6615
  const creates = [];
6422
- if (entries.length > 0 && !existsSync9(tree)) {
6616
+ if (entries.length > 0 && !existsSync10(tree)) {
6423
6617
  creates.push(`${RECORDS_PATH4}/`);
6424
6618
  }
6425
- if (readIfPresent(join9(root, ...STATE_GITIGNORE_PATH2.split("/"))) !== renderStateGitignore2(config)) {
6619
+ if (readIfPresent(join10(root, ...STATE_GITIGNORE_PATH2.split("/"))) !== renderStateGitignore2(config)) {
6426
6620
  creates.push(STATE_GITIGNORE_PATH2);
6427
6621
  }
6428
6622
  return {
@@ -6432,23 +6626,23 @@ function planMigrate(cwd) {
6432
6626
  to: `${RECORDS_PATH4}/${entry}`
6433
6627
  })),
6434
6628
  creates,
6435
- removes: existsSync9(join9(root, ...OLD_GITIGNORE.split("/"))) ? [OLD_GITIGNORE] : []
6629
+ removes: existsSync10(join10(root, ...OLD_GITIGNORE.split("/"))) ? [OLD_GITIGNORE] : []
6436
6630
  };
6437
6631
  }
6438
6632
  function readIfPresent(file) {
6439
- return existsSync9(file) ? readFileSync9(file, "utf8") : void 0;
6633
+ return existsSync10(file) ? readFileSync10(file, "utf8") : void 0;
6440
6634
  }
6441
6635
  function collidingEntries(entries, tree) {
6442
6636
  let held;
6443
6637
  try {
6444
- held = readdirSync5(tree);
6638
+ held = readdirSync6(tree);
6445
6639
  } catch {
6446
6640
  return [];
6447
6641
  }
6448
6642
  const taken = new Set(held.map((name) => name.toLowerCase()));
6449
6643
  return entries.filter((entry) => taken.has(entry.toLowerCase())).sort();
6450
6644
  }
6451
- function describe(names) {
6645
+ function describe2(names) {
6452
6646
  return names.length === 1 ? `\`${names[0]}\` is` : `${names.map((name) => `\`${name}\``).join(", ")} are`;
6453
6647
  }
6454
6648
  function isNoop(plan2) {
@@ -6460,18 +6654,18 @@ function applyMigrate(plan2) {
6460
6654
  }
6461
6655
  const { config } = loadConfig2(plan2.root);
6462
6656
  if (plan2.moves.length > 0) {
6463
- mkdirSync4(recordsDir3(plan2.root), { recursive: true });
6657
+ mkdirSync5(recordsDir3(plan2.root), { recursive: true });
6464
6658
  for (const move of plan2.moves) {
6465
- renameSync2(join9(plan2.root, ...move.from.split("/")), join9(plan2.root, ...move.to.split("/")));
6659
+ renameSync2(join10(plan2.root, ...move.from.split("/")), join10(plan2.root, ...move.to.split("/")));
6466
6660
  }
6467
6661
  }
6468
6662
  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");
6663
+ const ignore = join10(plan2.root, ...STATE_GITIGNORE_PATH2.split("/"));
6664
+ mkdirSync5(dirname7(ignore), { recursive: true });
6665
+ writeFileSync6(ignore, renderStateGitignore2(config), "utf8");
6472
6666
  }
6473
6667
  for (const removed of plan2.removes) {
6474
- rmSync2(join9(plan2.root, ...removed.split("/")), { force: true });
6668
+ rmSync2(join10(plan2.root, ...removed.split("/")), { force: true });
6475
6669
  }
6476
6670
  return { ...plan2, status: "migrated" };
6477
6671
  }
@@ -6973,8 +7167,8 @@ var rotateCommand = defineCommand20({
6973
7167
  });
6974
7168
 
6975
7169
  // 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";
7170
+ import { existsSync as existsSync11, watch as watch2 } from "fs";
7171
+ import { basename as basename3, dirname as dirname8, resolve as resolve8 } from "path";
6978
7172
  import { SCHEMA_SHAPE_FILE as SCHEMA_SHAPE_FILE4, schemaFileOf as schemaFileOf5, schemaInsideTree } from "@penvhq/core";
6979
7173
  import { defineCommand as defineCommand21 } from "citty";
6980
7174
  var DEBOUNCE_MS2 = 100;
@@ -7033,7 +7227,7 @@ function runWatch(options) {
7033
7227
  watcher.close();
7034
7228
  }
7035
7229
  function armRecovery(target, recursive, only) {
7036
- const parent = dirname7(target);
7230
+ const parent = dirname8(target);
7037
7231
  const name = basename3(target);
7038
7232
  let recovery;
7039
7233
  try {
@@ -7041,14 +7235,14 @@ function runWatch(options) {
7041
7235
  if (closed || recovery === void 0) {
7042
7236
  return;
7043
7237
  }
7044
- if (!existsSync10(parent)) {
7238
+ if (!existsSync11(parent)) {
7045
7239
  stop2(recovery);
7046
7240
  return;
7047
7241
  }
7048
7242
  if (filename !== null && basename3(filename) !== name) {
7049
7243
  return;
7050
7244
  }
7051
- if (!existsSync10(target)) {
7245
+ if (!existsSync11(target)) {
7052
7246
  return;
7053
7247
  }
7054
7248
  stop2(recovery);
@@ -7072,7 +7266,7 @@ function runWatch(options) {
7072
7266
  if (closed) {
7073
7267
  return;
7074
7268
  }
7075
- if (!existsSync10(target)) {
7269
+ if (!existsSync11(target)) {
7076
7270
  if (watcher !== void 0) {
7077
7271
  stop2(watcher);
7078
7272
  }
@@ -7107,11 +7301,11 @@ function runWatch(options) {
7107
7301
  watchers.add(watcher);
7108
7302
  }
7109
7303
  addWatcher(project.recordsDir, true);
7110
- addWatcher(dirname7(project.configFile), false, configFile);
7304
+ addWatcher(dirname8(project.configFile), false, configFile);
7111
7305
  addWatcher(project.root, false, SCHEMA_SHAPE_FILE4);
7112
7306
  if (schemaInsideTree(project.config) === void 0) {
7113
7307
  const schemaFile = resolve8(project.root, schemaFileOf5(project.config));
7114
- addWatcher(dirname7(schemaFile), false, basename3(schemaFile));
7308
+ addWatcher(dirname8(schemaFile), false, basename3(schemaFile));
7115
7309
  }
7116
7310
  void validate();
7117
7311
  return {