@penvhq/cli 0.11.0 → 0.13.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.cjs CHANGED
@@ -208,6 +208,7 @@ function escapeArgument(argument, doubleEscape) {
208
208
  var import_meta = {};
209
209
  var RUNTIME_PACKAGE = "@penvhq/penv";
210
210
  var SCHEMA_PACKAGE = "zod";
211
+ var TYPES_PACKAGE = "@penvhq/core";
211
212
  var LOCKFILES = [
212
213
  ["pnpm", "pnpm-lock.yaml"],
213
214
  ["yarn", "yarn.lock"],
@@ -216,11 +217,25 @@ var LOCKFILES = [
216
217
  ["npm", "package-lock.json"]
217
218
  ];
218
219
  var ADD = {
219
- pnpm: ["pnpm", "add", "--save-exact"],
220
- npm: ["npm", "install", "--save-exact"],
221
- yarn: ["yarn", "add", "--exact"],
222
- bun: ["bun", "add", "--exact"]
220
+ pnpm: ["pnpm", "add"],
221
+ npm: ["npm", "install"],
222
+ yarn: ["yarn", "add"],
223
+ bun: ["bun", "add"]
223
224
  };
225
+ var EXACT = {
226
+ pnpm: "--save-exact",
227
+ npm: "--save-exact",
228
+ yarn: "--exact",
229
+ bun: "--exact"
230
+ };
231
+ var DEV = {
232
+ pnpm: "-D",
233
+ npm: "--save-dev",
234
+ yarn: "--dev",
235
+ bun: "--dev"
236
+ };
237
+ var WORKSPACE_ROOT_FLAG = "-w";
238
+ var PNPM_WORKSPACE = "pnpm-workspace.yaml";
224
239
  function engineVersion() {
225
240
  const version = ownManifest()?.version;
226
241
  if (typeof version === "string" && version.length > 0) {
@@ -283,100 +298,288 @@ function manifestOf(root) {
283
298
  return void 0;
284
299
  }
285
300
  }
286
- function declaredVersion(root, name) {
287
- const manifest = manifestOf(root);
301
+ function declaredIn(dir, name) {
302
+ const manifest = manifestOf(dir);
288
303
  for (const field of ["dependencies", "devDependencies"]) {
289
304
  const block = manifest?.[field];
290
305
  if (block !== null && typeof block === "object" && !Array.isArray(block)) {
291
306
  const version = block[name];
292
307
  if (typeof version === "string") {
293
- return version;
308
+ return { version, dev: field === "devDependencies" };
294
309
  }
295
310
  }
296
311
  }
297
312
  return void 0;
298
313
  }
314
+ function isPnpmWorkspaceRoot(root) {
315
+ return (0, import_node_fs2.existsSync)((0, import_node_path2.join)(root, PNPM_WORKSPACE)) && (0, import_node_fs2.existsSync)((0, import_node_path2.join)(root, "package.json"));
316
+ }
317
+ function workspaceGlobs(root) {
318
+ let text;
319
+ try {
320
+ text = (0, import_node_fs2.readFileSync)((0, import_node_path2.join)(root, PNPM_WORKSPACE), "utf8");
321
+ } catch {
322
+ return [];
323
+ }
324
+ const unquote = (raw) => raw.replace(/^['"]|['"]$/g, "").trim();
325
+ const globs = [];
326
+ let inside = false;
327
+ for (const line of text.split(/\r?\n/)) {
328
+ const flow = /^packages:\s*\[(.*)\]\s*$/.exec(line);
329
+ if (flow?.[1] !== void 0) {
330
+ return flow[1].split(",").map(unquote).filter((glob) => glob !== "");
331
+ }
332
+ if (/^packages:\s*$/.test(line)) {
333
+ inside = true;
334
+ continue;
335
+ }
336
+ if (!inside) {
337
+ continue;
338
+ }
339
+ const item = /^\s+-\s*(.+?)\s*$/.exec(line);
340
+ if (item?.[1] !== void 0) {
341
+ globs.push(unquote(item[1]));
342
+ continue;
343
+ }
344
+ if (line.trim() !== "" && !line.trimStart().startsWith("#")) {
345
+ break;
346
+ }
347
+ }
348
+ return globs;
349
+ }
350
+ function directoriesIn(dir) {
351
+ try {
352
+ return (0, import_node_fs2.readdirSync)(dir, { withFileTypes: true }).filter((entry) => entry.isDirectory() && entry.name !== "node_modules").map((entry) => (0, import_node_path2.join)(dir, entry.name));
353
+ } catch {
354
+ return [];
355
+ }
356
+ }
357
+ function isDirectory(path) {
358
+ try {
359
+ return (0, import_node_fs2.statSync)(path).isDirectory();
360
+ } catch {
361
+ return false;
362
+ }
363
+ }
364
+ function expandGlob(root, glob) {
365
+ let dirs = [root];
366
+ for (const segment of glob.split("/").filter((part) => part !== "" && part !== ".")) {
367
+ const next = [];
368
+ for (const dir of dirs) {
369
+ if (segment === "**") {
370
+ const stack = [dir];
371
+ while (stack.length > 0) {
372
+ const current = stack.pop();
373
+ next.push(current);
374
+ stack.push(...directoriesIn(current));
375
+ }
376
+ continue;
377
+ }
378
+ if (segment.includes("*")) {
379
+ const pattern = new RegExp(
380
+ `^${segment.replace(/[.+^${}()|[\]\\]/g, "\\$&").replace(/\*/g, "[^/]*")}$`
381
+ );
382
+ next.push(
383
+ ...directoriesIn(dir).filter((child) => pattern.test(child.slice(dir.length + 1)))
384
+ );
385
+ continue;
386
+ }
387
+ const candidate = (0, import_node_path2.join)(dir, segment);
388
+ if (isDirectory(candidate)) {
389
+ next.push(candidate);
390
+ }
391
+ }
392
+ dirs = next;
393
+ }
394
+ return dirs;
395
+ }
396
+ function workspaceMembers(root, name) {
397
+ if (!isPnpmWorkspaceRoot(root)) {
398
+ return [];
399
+ }
400
+ const globs = workspaceGlobs(root);
401
+ const excluded = globs.filter((glob) => glob.startsWith("!")).flatMap((glob) => expandGlob(root, glob.slice(1)));
402
+ const found = /* @__PURE__ */ new Set();
403
+ for (const glob of globs.filter((entry) => !entry.startsWith("!"))) {
404
+ for (const dir of expandGlob(root, glob)) {
405
+ if (dir !== root && !excluded.includes(dir) && declaredIn(dir, name) !== void 0) {
406
+ found.add(dir);
407
+ }
408
+ }
409
+ }
410
+ return [...found].sort();
411
+ }
412
+ function manifestPathOf(root, dir) {
413
+ const within = (0, import_node_path2.relative)(root, dir).split(import_node_path2.sep).filter((part) => part !== "");
414
+ return [...within, "package.json"].join("/");
415
+ }
416
+ function addCommand(manager, options, specs) {
417
+ const [bin, verb] = ADD[manager];
418
+ return [
419
+ bin,
420
+ ...options.filter === void 0 ? [] : ["--filter", options.filter],
421
+ verb,
422
+ ...options.workspaceRoot ? [WORKSPACE_ROOT_FLAG] : [],
423
+ EXACT[manager],
424
+ ...options.dev ? [DEV[manager]] : [],
425
+ ...specs
426
+ ];
427
+ }
428
+ function stepFor(manager, manifest, packages, options) {
429
+ const pending = packages.filter((entry) => !entry.satisfied);
430
+ const specs = (pending.length === 0 ? packages : pending).map(
431
+ (entry) => `${entry.name}@${entry.version}`
432
+ );
433
+ return {
434
+ manifest,
435
+ packages,
436
+ command: addCommand(manager, options, specs),
437
+ dev: options.dev,
438
+ satisfied: pending.length === 0
439
+ };
440
+ }
299
441
  function planInstall(root, version = engineVersion()) {
300
442
  const manager = detectPackageManager(root);
301
443
  const lockfile = LOCKFILES.find(
302
444
  ([name, file]) => name === manager && (0, import_node_fs2.existsSync)((0, import_node_path2.join)(root, file))
303
445
  )?.[1];
304
- const runtimeDeclared = declaredVersion(root, RUNTIME_PACKAGE);
305
- const zodDeclared = declaredVersion(root, SCHEMA_PACKAGE);
446
+ const workspaceRoot = manager === "pnpm" && isPnpmWorkspaceRoot(root);
447
+ const runtime = declaredIn(root, RUNTIME_PACKAGE);
448
+ const zod = declaredIn(root, SCHEMA_PACKAGE);
449
+ const types = declaredIn(root, TYPES_PACKAGE);
306
450
  const packages = [
307
451
  {
308
452
  name: RUNTIME_PACKAGE,
309
453
  version,
310
- ...runtimeDeclared === void 0 ? {} : { declared: runtimeDeclared },
311
- satisfied: runtimeDeclared === version
454
+ ...runtime === void 0 ? {} : { declared: runtime.version },
455
+ satisfied: runtime?.version === version
312
456
  },
313
457
  {
314
458
  name: SCHEMA_PACKAGE,
315
459
  version: schemaPackageVersion(),
316
- ...zodDeclared === void 0 ? {} : { declared: zodDeclared },
460
+ ...zod === void 0 ? {} : { declared: zod.version },
317
461
  // Any declared zod counts: which zod a project uses is the project's
318
462
  // decision, and penv is here to make sure there is one, not to move it.
319
- satisfied: zodDeclared !== void 0
463
+ satisfied: zod !== void 0
320
464
  }
321
465
  ];
466
+ const typesPackage = {
467
+ name: TYPES_PACKAGE,
468
+ version,
469
+ ...types === void 0 ? {} : { declared: types.version },
470
+ // Any declared version counts, for zod's reason: the augmentation binds on
471
+ // the module resolving, not on which release of it a project pinned.
472
+ satisfied: types !== void 0
473
+ };
322
474
  const pending = packages.filter((entry) => !entry.satisfied);
323
- const specs = (pending.length === 0 ? packages : pending).map(
324
- (entry) => `${entry.name}@${entry.version}`
325
- );
475
+ const steps = [
476
+ stepFor(manager, "package.json", packages, {
477
+ workspaceRoot,
478
+ dev: runtime?.dev === true && pending.every((entry) => entry.name === RUNTIME_PACKAGE) && pending.length > 0
479
+ }),
480
+ stepFor(manager, "package.json", [typesPackage], { workspaceRoot, dev: true })
481
+ ];
482
+ for (const dir of workspaceMembers(root, RUNTIME_PACKAGE)) {
483
+ const declared = declaredIn(dir, RUNTIME_PACKAGE);
484
+ steps.push(
485
+ stepFor(
486
+ manager,
487
+ manifestPathOf(root, dir),
488
+ [
489
+ {
490
+ name: RUNTIME_PACKAGE,
491
+ version,
492
+ declared: declared.version,
493
+ satisfied: declared.version === version
494
+ }
495
+ ],
496
+ {
497
+ filter: `./${(0, import_node_path2.relative)(root, dir).split(import_node_path2.sep).join("/")}`,
498
+ workspaceRoot: false,
499
+ dev: declared.dev
500
+ }
501
+ )
502
+ );
503
+ }
326
504
  return {
327
505
  root,
328
506
  manager,
329
- packages,
330
- command: [...ADD[manager], ...specs],
507
+ steps,
331
508
  ...lockfile === void 0 ? {} : { lockfile },
332
- satisfied: pending.length === 0
509
+ satisfied: steps.every((step) => step.satisfied)
333
510
  };
334
511
  }
335
512
  function describe(entry) {
336
513
  return `${entry.name} ${entry.version}`;
337
514
  }
338
- function renderInstallPlan(plan2) {
339
- if (plan2.satisfied) {
340
- return [
341
- `package.json already has ${plan2.packages.map(describe).join(" and ")} \u2014 nothing to install.`
342
- ];
343
- }
344
- const pending = plan2.packages.filter((entry) => !entry.satisfied);
515
+ function installedPackages(plan2) {
516
+ const pending = plan2.steps.flatMap((step) => step.packages).filter((entry) => !entry.satisfied);
517
+ return [...new Map(pending.map((entry) => [entry.name, entry])).values()];
518
+ }
519
+ function plannedPackages(plan2) {
520
+ const all = plan2.steps.flatMap((step) => step.packages);
521
+ return [...new Map(all.map((entry) => [entry.name, entry])).values()];
522
+ }
523
+ function series(values) {
524
+ return values.length < 2 ? values[0] ?? "" : `${values.slice(0, -1).join(", ")} and ${values.at(-1)}`;
525
+ }
526
+ function renderStep(step) {
527
+ const pending = step.packages.filter((entry) => !entry.satisfied);
345
528
  const added = pending.filter((entry) => entry.declared === void 0);
346
529
  const replaced = pending.filter((entry) => entry.declared !== void 0);
530
+ const block = step.dev ? "devDependencies" : "dependencies";
347
531
  return [
348
- "package.json",
532
+ step.manifest,
349
533
  ...added.length === 0 ? [] : [
350
- ' + "dependencies": {',
534
+ ` + "${block}": {`,
351
535
  ...added.map((entry) => ` + "${entry.name}": "${entry.version}"`),
352
536
  " + }"
353
537
  ],
354
538
  ...replaced.flatMap((entry) => [
355
539
  ` - "${entry.name}": "${entry.declared}"`,
356
540
  ` + "${entry.name}": "${entry.version}"`
357
- ]),
358
- ...plan2.lockfile === void 0 ? [] : [plan2.lockfile, ...pending.map((entry) => ` + ${entry.name}@${entry.version}`)],
541
+ ])
542
+ ];
543
+ }
544
+ function renderInstallPlan(plan2) {
545
+ if (plan2.satisfied) {
546
+ return [
547
+ `package.json already has ${series(plannedPackages(plan2).map(describe))} \u2014 nothing to install.`
548
+ ];
549
+ }
550
+ const pending = plan2.steps.filter((step) => !step.satisfied);
551
+ const [first, ...rest] = pending.map((step) => step.command.join(" "));
552
+ const landing = installedPackages(plan2).map((entry) => ` + ${entry.name}@${entry.version}`);
553
+ return [
554
+ ...pending.flatMap(renderStep),
555
+ ...plan2.lockfile === void 0 ? [] : [plan2.lockfile, ...landing],
359
556
  "",
360
- `Run with: ${plan2.command.join(" ")}`
557
+ `Run with: ${first ?? ""}`,
558
+ ...rest.map((command) => ` then ${command}`)
361
559
  ];
362
560
  }
363
561
  var installWithPackageManager = async (plan2) => {
364
- const child = startChild({
365
- command: plan2.command,
366
- env: process.env,
367
- cwd: plan2.root,
368
- purpose: `install ${plan2.packages.map(describe).join(" and ")}`
369
- });
370
- const ended = await child.ended;
371
- if (ended.exitCode !== 0 || ended.signal !== null) {
372
- throw installFailed(plan2);
562
+ for (const step of plan2.steps) {
563
+ if (step.satisfied) {
564
+ continue;
565
+ }
566
+ const child = startChild({
567
+ command: step.command,
568
+ env: process.env,
569
+ cwd: plan2.root,
570
+ purpose: `install ${step.packages.map(describe).join(" and ")} in ${step.manifest}`
571
+ });
572
+ const ended = await child.ended;
573
+ if (ended.exitCode !== 0 || ended.signal !== null) {
574
+ throw installFailed(plan2, step);
575
+ }
373
576
  }
374
577
  };
375
- function installFailed(plan2) {
578
+ function installFailed(plan2, step) {
376
579
  return new import_core2.PenvError(
377
580
  "INIT_INSTALL_FAILED",
378
- `${plan2.command.join(" ")} did not finish, so penv migrated nothing`,
379
- `Run \`${plan2.command.join(" ")}\` yourself, then start this command again. Your dotenv files are exactly where they were.`
581
+ `${step.command.join(" ")} did not finish, so penv migrated nothing`,
582
+ `Read what ${plan2.manager} printed above \u2014 it names what it refused. Fix that and run this command again; your dotenv files are exactly where they were.`
380
583
  );
381
584
  }
382
585
 
@@ -906,7 +1109,7 @@ function writeError(lines) {
906
1109
  }
907
1110
  }
908
1111
  function reportError(error) {
909
- if (error instanceof import_core6.PenvError) {
1112
+ if ((0, import_core6.isPenvErrorLike)(error)) {
910
1113
  process.stderr.write(`${err.red(CROSS)} ${error.summary}
911
1114
  `);
912
1115
  if (error.remedy !== void 0) {
@@ -4441,8 +4644,8 @@ function exportsSchema(file) {
4441
4644
  /export\s*\{[^}]*\bschema\b[^}]*\}/.test(source)
4442
4645
  );
4443
4646
  }
4444
- function occupied(cwd, relative5) {
4445
- const file = (0, import_node_path11.join)(cwd, ...relative5.split("/"));
4647
+ function occupied(cwd, relative6) {
4648
+ const file = (0, import_node_path11.join)(cwd, ...relative6.split("/"));
4446
4649
  return (0, import_node_fs11.existsSync)(file) && !exportsSchema(file);
4447
4650
  }
4448
4651
  function schemaFileFor(cwd) {
@@ -4864,7 +5067,7 @@ function environmentsFromFlag(flag) {
4864
5067
  function configOf(decisions) {
4865
5068
  return { environments: decisions.environments, providers: {}, schemaFile: decisions.schemaFile };
4866
5069
  }
4867
- function declaredIn(root) {
5070
+ function declaredIn2(root) {
4868
5071
  const file = (0, import_node_path13.join)(root, CONFIG_FILE);
4869
5072
  if (!(0, import_node_fs13.existsSync)(file)) {
4870
5073
  return void 0;
@@ -4872,7 +5075,7 @@ function declaredIn(root) {
4872
5075
  return (0, import_core25.loadConfigFrom)(file);
4873
5076
  }
4874
5077
  function planInit(root, flags = {}) {
4875
- const declared = declaredIn(root);
5078
+ const declared = declaredIn2(root);
4876
5079
  const detected = detectFramework(root);
4877
5080
  const notes = [];
4878
5081
  if (declared !== void 0) {
@@ -5539,7 +5742,7 @@ function scaffold(root, fields, draft, decisions = DEFAULT_DECISIONS, framework
5539
5742
  return seam === void 0 ? steps : [...steps, seam];
5540
5743
  }
5541
5744
  function scaffoldPaths(root, decisions = DEFAULT_DECISIONS, framework = detectFramework(root)?.name) {
5542
- const fileAt = (relative5) => (0, import_node_path13.join)(root, ...relative5.split("/"));
5745
+ const fileAt = (relative6) => (0, import_node_path13.join)(root, ...relative6.split("/"));
5543
5746
  const seam = decisions.inject ? seamFor(framework, {
5544
5747
  alias: decisions.alias,
5545
5748
  srcDir: srcPrefix(root),
@@ -5586,7 +5789,7 @@ function planCutover(input) {
5586
5789
  "Run `penv init` again and choose the files penv should adopt."
5587
5790
  );
5588
5791
  }
5589
- const declared = declaredIn(root);
5792
+ const declared = declaredIn2(root);
5590
5793
  const named = environmentsDeclaredBy(selected);
5591
5794
  const chosen = named.length > 0 ? named : [requireEnvironment(input)];
5592
5795
  const environments = declared === void 0 ? chosen : declared.environments;
@@ -5837,7 +6040,11 @@ function renderCutover(result2) {
5837
6040
  const glyph = step.action === "conflicted" ? WARN : step.action === "info" ? "\u2192" : CHECK;
5838
6041
  return step.note === void 0 ? { glyph, text: step.text } : { glyph, text: step.text, note: step.note };
5839
6042
  }),
5840
- ...plan2.install.packages.filter((entry) => !entry.satisfied).map((entry) => ({ glyph: CHECK, text: `Installed ${entry.name}`, note: entry.version })),
6043
+ ...installedPackages(plan2.install).map((entry) => ({
6044
+ glyph: CHECK,
6045
+ text: `Installed ${entry.name}`,
6046
+ note: entry.version
6047
+ })),
5841
6048
  {
5842
6049
  glyph: CHECK,
5843
6050
  text: `Imported ${plan2.fields.length} parameters`,
@@ -5997,7 +6204,7 @@ async function cutoverInteractively(root, base, adoption) {
5997
6204
  write(renderCutover(await applyCutover({ ...plan2, decisions: { ...plan2.decisions, inject } })));
5998
6205
  }
5999
6206
  function offeredEnvironment(root) {
6000
- const declared = declaredIn(root);
6207
+ const declared = declaredIn2(root);
6001
6208
  if (declared === void 0) {
6002
6209
  return DEVELOPMENT;
6003
6210
  }