@penvhq/cli 0.11.0 → 0.12.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
@@ -216,11 +216,25 @@ var LOCKFILES = [
216
216
  ["npm", "package-lock.json"]
217
217
  ];
218
218
  var ADD = {
219
- pnpm: ["pnpm", "add", "--save-exact"],
220
- npm: ["npm", "install", "--save-exact"],
221
- yarn: ["yarn", "add", "--exact"],
222
- bun: ["bun", "add", "--exact"]
219
+ pnpm: ["pnpm", "add"],
220
+ npm: ["npm", "install"],
221
+ yarn: ["yarn", "add"],
222
+ bun: ["bun", "add"]
223
223
  };
224
+ var EXACT = {
225
+ pnpm: "--save-exact",
226
+ npm: "--save-exact",
227
+ yarn: "--exact",
228
+ bun: "--exact"
229
+ };
230
+ var DEV = {
231
+ pnpm: "-D",
232
+ npm: "--save-dev",
233
+ yarn: "--dev",
234
+ bun: "--dev"
235
+ };
236
+ var WORKSPACE_ROOT_FLAG = "-w";
237
+ var PNPM_WORKSPACE = "pnpm-workspace.yaml";
224
238
  function engineVersion() {
225
239
  const version = ownManifest()?.version;
226
240
  if (typeof version === "string" && version.length > 0) {
@@ -283,69 +297,219 @@ function manifestOf(root) {
283
297
  return void 0;
284
298
  }
285
299
  }
286
- function declaredVersion(root, name) {
287
- const manifest = manifestOf(root);
300
+ function declaredIn(dir, name) {
301
+ const manifest = manifestOf(dir);
288
302
  for (const field of ["dependencies", "devDependencies"]) {
289
303
  const block = manifest?.[field];
290
304
  if (block !== null && typeof block === "object" && !Array.isArray(block)) {
291
305
  const version = block[name];
292
306
  if (typeof version === "string") {
293
- return version;
307
+ return { version, dev: field === "devDependencies" };
294
308
  }
295
309
  }
296
310
  }
297
311
  return void 0;
298
312
  }
313
+ function isPnpmWorkspaceRoot(root) {
314
+ 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"));
315
+ }
316
+ function workspaceGlobs(root) {
317
+ let text;
318
+ try {
319
+ text = (0, import_node_fs2.readFileSync)((0, import_node_path2.join)(root, PNPM_WORKSPACE), "utf8");
320
+ } catch {
321
+ return [];
322
+ }
323
+ const unquote = (raw) => raw.replace(/^['"]|['"]$/g, "").trim();
324
+ const globs = [];
325
+ let inside = false;
326
+ for (const line of text.split(/\r?\n/)) {
327
+ const flow = /^packages:\s*\[(.*)\]\s*$/.exec(line);
328
+ if (flow?.[1] !== void 0) {
329
+ return flow[1].split(",").map(unquote).filter((glob) => glob !== "");
330
+ }
331
+ if (/^packages:\s*$/.test(line)) {
332
+ inside = true;
333
+ continue;
334
+ }
335
+ if (!inside) {
336
+ continue;
337
+ }
338
+ const item = /^\s+-\s*(.+?)\s*$/.exec(line);
339
+ if (item?.[1] !== void 0) {
340
+ globs.push(unquote(item[1]));
341
+ continue;
342
+ }
343
+ if (line.trim() !== "" && !line.trimStart().startsWith("#")) {
344
+ break;
345
+ }
346
+ }
347
+ return globs;
348
+ }
349
+ function directoriesIn(dir) {
350
+ try {
351
+ 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));
352
+ } catch {
353
+ return [];
354
+ }
355
+ }
356
+ function isDirectory(path) {
357
+ try {
358
+ return (0, import_node_fs2.statSync)(path).isDirectory();
359
+ } catch {
360
+ return false;
361
+ }
362
+ }
363
+ function expandGlob(root, glob) {
364
+ let dirs = [root];
365
+ for (const segment of glob.split("/").filter((part) => part !== "" && part !== ".")) {
366
+ const next = [];
367
+ for (const dir of dirs) {
368
+ if (segment === "**") {
369
+ const stack = [dir];
370
+ while (stack.length > 0) {
371
+ const current = stack.pop();
372
+ next.push(current);
373
+ stack.push(...directoriesIn(current));
374
+ }
375
+ continue;
376
+ }
377
+ if (segment.includes("*")) {
378
+ const pattern = new RegExp(
379
+ `^${segment.replace(/[.+^${}()|[\]\\]/g, "\\$&").replace(/\*/g, "[^/]*")}$`
380
+ );
381
+ next.push(
382
+ ...directoriesIn(dir).filter((child) => pattern.test(child.slice(dir.length + 1)))
383
+ );
384
+ continue;
385
+ }
386
+ const candidate = (0, import_node_path2.join)(dir, segment);
387
+ if (isDirectory(candidate)) {
388
+ next.push(candidate);
389
+ }
390
+ }
391
+ dirs = next;
392
+ }
393
+ return dirs;
394
+ }
395
+ function workspaceMembers(root, name) {
396
+ if (!isPnpmWorkspaceRoot(root)) {
397
+ return [];
398
+ }
399
+ const globs = workspaceGlobs(root);
400
+ const excluded = globs.filter((glob) => glob.startsWith("!")).flatMap((glob) => expandGlob(root, glob.slice(1)));
401
+ const found = /* @__PURE__ */ new Set();
402
+ for (const glob of globs.filter((entry) => !entry.startsWith("!"))) {
403
+ for (const dir of expandGlob(root, glob)) {
404
+ if (dir !== root && !excluded.includes(dir) && declaredIn(dir, name) !== void 0) {
405
+ found.add(dir);
406
+ }
407
+ }
408
+ }
409
+ return [...found].sort();
410
+ }
411
+ function manifestPathOf(root, dir) {
412
+ const within = (0, import_node_path2.relative)(root, dir).split(import_node_path2.sep).filter((part) => part !== "");
413
+ return [...within, "package.json"].join("/");
414
+ }
415
+ function addCommand(manager, options, specs) {
416
+ const [bin, verb] = ADD[manager];
417
+ return [
418
+ bin,
419
+ ...options.filter === void 0 ? [] : ["--filter", options.filter],
420
+ verb,
421
+ ...options.workspaceRoot ? [WORKSPACE_ROOT_FLAG] : [],
422
+ EXACT[manager],
423
+ ...options.dev ? [DEV[manager]] : [],
424
+ ...specs
425
+ ];
426
+ }
427
+ function stepFor(manager, manifest, packages, options) {
428
+ const pending = packages.filter((entry) => !entry.satisfied);
429
+ const specs = (pending.length === 0 ? packages : pending).map(
430
+ (entry) => `${entry.name}@${entry.version}`
431
+ );
432
+ return {
433
+ manifest,
434
+ packages,
435
+ command: addCommand(manager, options, specs),
436
+ satisfied: pending.length === 0
437
+ };
438
+ }
299
439
  function planInstall(root, version = engineVersion()) {
300
440
  const manager = detectPackageManager(root);
301
441
  const lockfile = LOCKFILES.find(
302
442
  ([name, file]) => name === manager && (0, import_node_fs2.existsSync)((0, import_node_path2.join)(root, file))
303
443
  )?.[1];
304
- const runtimeDeclared = declaredVersion(root, RUNTIME_PACKAGE);
305
- const zodDeclared = declaredVersion(root, SCHEMA_PACKAGE);
444
+ const workspaceRoot = manager === "pnpm" && isPnpmWorkspaceRoot(root);
445
+ const runtime = declaredIn(root, RUNTIME_PACKAGE);
446
+ const zod = declaredIn(root, SCHEMA_PACKAGE);
306
447
  const packages = [
307
448
  {
308
449
  name: RUNTIME_PACKAGE,
309
450
  version,
310
- ...runtimeDeclared === void 0 ? {} : { declared: runtimeDeclared },
311
- satisfied: runtimeDeclared === version
451
+ ...runtime === void 0 ? {} : { declared: runtime.version },
452
+ satisfied: runtime?.version === version
312
453
  },
313
454
  {
314
455
  name: SCHEMA_PACKAGE,
315
456
  version: schemaPackageVersion(),
316
- ...zodDeclared === void 0 ? {} : { declared: zodDeclared },
457
+ ...zod === void 0 ? {} : { declared: zod.version },
317
458
  // Any declared zod counts: which zod a project uses is the project's
318
459
  // decision, and penv is here to make sure there is one, not to move it.
319
- satisfied: zodDeclared !== void 0
460
+ satisfied: zod !== void 0
320
461
  }
321
462
  ];
322
463
  const pending = packages.filter((entry) => !entry.satisfied);
323
- const specs = (pending.length === 0 ? packages : pending).map(
324
- (entry) => `${entry.name}@${entry.version}`
325
- );
464
+ const steps = [
465
+ stepFor(manager, "package.json", packages, {
466
+ workspaceRoot,
467
+ dev: runtime?.dev === true && pending.every((entry) => entry.name === RUNTIME_PACKAGE) && pending.length > 0
468
+ })
469
+ ];
470
+ for (const dir of workspaceMembers(root, RUNTIME_PACKAGE)) {
471
+ const declared = declaredIn(dir, RUNTIME_PACKAGE);
472
+ steps.push(
473
+ stepFor(
474
+ manager,
475
+ manifestPathOf(root, dir),
476
+ [
477
+ {
478
+ name: RUNTIME_PACKAGE,
479
+ version,
480
+ declared: declared.version,
481
+ satisfied: declared.version === version
482
+ }
483
+ ],
484
+ {
485
+ filter: `./${(0, import_node_path2.relative)(root, dir).split(import_node_path2.sep).join("/")}`,
486
+ workspaceRoot: false,
487
+ dev: declared.dev
488
+ }
489
+ )
490
+ );
491
+ }
326
492
  return {
327
493
  root,
328
494
  manager,
329
- packages,
330
- command: [...ADD[manager], ...specs],
495
+ steps,
331
496
  ...lockfile === void 0 ? {} : { lockfile },
332
- satisfied: pending.length === 0
497
+ satisfied: steps.every((step) => step.satisfied)
333
498
  };
334
499
  }
335
500
  function describe(entry) {
336
501
  return `${entry.name} ${entry.version}`;
337
502
  }
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);
503
+ function installedPackages(plan2) {
504
+ const pending = plan2.steps.flatMap((step) => step.packages).filter((entry) => !entry.satisfied);
505
+ return [...new Map(pending.map((entry) => [entry.name, entry])).values()];
506
+ }
507
+ function renderStep(step) {
508
+ const pending = step.packages.filter((entry) => !entry.satisfied);
345
509
  const added = pending.filter((entry) => entry.declared === void 0);
346
510
  const replaced = pending.filter((entry) => entry.declared !== void 0);
347
511
  return [
348
- "package.json",
512
+ step.manifest,
349
513
  ...added.length === 0 ? [] : [
350
514
  ' + "dependencies": {',
351
515
  ...added.map((entry) => ` + "${entry.name}": "${entry.version}"`),
@@ -354,29 +518,49 @@ function renderInstallPlan(plan2) {
354
518
  ...replaced.flatMap((entry) => [
355
519
  ` - "${entry.name}": "${entry.declared}"`,
356
520
  ` + "${entry.name}": "${entry.version}"`
357
- ]),
358
- ...plan2.lockfile === void 0 ? [] : [plan2.lockfile, ...pending.map((entry) => ` + ${entry.name}@${entry.version}`)],
521
+ ])
522
+ ];
523
+ }
524
+ function renderInstallPlan(plan2) {
525
+ if (plan2.satisfied) {
526
+ const packages = plan2.steps[0]?.packages ?? [];
527
+ return [
528
+ `package.json already has ${packages.map(describe).join(" and ")} \u2014 nothing to install.`
529
+ ];
530
+ }
531
+ const pending = plan2.steps.filter((step) => !step.satisfied);
532
+ const [first, ...rest] = pending.map((step) => step.command.join(" "));
533
+ const landing = installedPackages(plan2).map((entry) => ` + ${entry.name}@${entry.version}`);
534
+ return [
535
+ ...pending.flatMap(renderStep),
536
+ ...plan2.lockfile === void 0 ? [] : [plan2.lockfile, ...landing],
359
537
  "",
360
- `Run with: ${plan2.command.join(" ")}`
538
+ `Run with: ${first ?? ""}`,
539
+ ...rest.map((command) => ` then ${command}`)
361
540
  ];
362
541
  }
363
542
  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);
543
+ for (const step of plan2.steps) {
544
+ if (step.satisfied) {
545
+ continue;
546
+ }
547
+ const child = startChild({
548
+ command: step.command,
549
+ env: process.env,
550
+ cwd: plan2.root,
551
+ purpose: `install ${step.packages.map(describe).join(" and ")} in ${step.manifest}`
552
+ });
553
+ const ended = await child.ended;
554
+ if (ended.exitCode !== 0 || ended.signal !== null) {
555
+ throw installFailed(plan2, step);
556
+ }
373
557
  }
374
558
  };
375
- function installFailed(plan2) {
559
+ function installFailed(plan2, step) {
376
560
  return new import_core2.PenvError(
377
561
  "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.`
562
+ `${step.command.join(" ")} did not finish, so penv migrated nothing`,
563
+ `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
564
  );
381
565
  }
382
566
 
@@ -906,7 +1090,7 @@ function writeError(lines) {
906
1090
  }
907
1091
  }
908
1092
  function reportError(error) {
909
- if (error instanceof import_core6.PenvError) {
1093
+ if ((0, import_core6.isPenvErrorLike)(error)) {
910
1094
  process.stderr.write(`${err.red(CROSS)} ${error.summary}
911
1095
  `);
912
1096
  if (error.remedy !== void 0) {
@@ -4441,8 +4625,8 @@ function exportsSchema(file) {
4441
4625
  /export\s*\{[^}]*\bschema\b[^}]*\}/.test(source)
4442
4626
  );
4443
4627
  }
4444
- function occupied(cwd, relative5) {
4445
- const file = (0, import_node_path11.join)(cwd, ...relative5.split("/"));
4628
+ function occupied(cwd, relative6) {
4629
+ const file = (0, import_node_path11.join)(cwd, ...relative6.split("/"));
4446
4630
  return (0, import_node_fs11.existsSync)(file) && !exportsSchema(file);
4447
4631
  }
4448
4632
  function schemaFileFor(cwd) {
@@ -4864,7 +5048,7 @@ function environmentsFromFlag(flag) {
4864
5048
  function configOf(decisions) {
4865
5049
  return { environments: decisions.environments, providers: {}, schemaFile: decisions.schemaFile };
4866
5050
  }
4867
- function declaredIn(root) {
5051
+ function declaredIn2(root) {
4868
5052
  const file = (0, import_node_path13.join)(root, CONFIG_FILE);
4869
5053
  if (!(0, import_node_fs13.existsSync)(file)) {
4870
5054
  return void 0;
@@ -4872,7 +5056,7 @@ function declaredIn(root) {
4872
5056
  return (0, import_core25.loadConfigFrom)(file);
4873
5057
  }
4874
5058
  function planInit(root, flags = {}) {
4875
- const declared = declaredIn(root);
5059
+ const declared = declaredIn2(root);
4876
5060
  const detected = detectFramework(root);
4877
5061
  const notes = [];
4878
5062
  if (declared !== void 0) {
@@ -5539,7 +5723,7 @@ function scaffold(root, fields, draft, decisions = DEFAULT_DECISIONS, framework
5539
5723
  return seam === void 0 ? steps : [...steps, seam];
5540
5724
  }
5541
5725
  function scaffoldPaths(root, decisions = DEFAULT_DECISIONS, framework = detectFramework(root)?.name) {
5542
- const fileAt = (relative5) => (0, import_node_path13.join)(root, ...relative5.split("/"));
5726
+ const fileAt = (relative6) => (0, import_node_path13.join)(root, ...relative6.split("/"));
5543
5727
  const seam = decisions.inject ? seamFor(framework, {
5544
5728
  alias: decisions.alias,
5545
5729
  srcDir: srcPrefix(root),
@@ -5586,7 +5770,7 @@ function planCutover(input) {
5586
5770
  "Run `penv init` again and choose the files penv should adopt."
5587
5771
  );
5588
5772
  }
5589
- const declared = declaredIn(root);
5773
+ const declared = declaredIn2(root);
5590
5774
  const named = environmentsDeclaredBy(selected);
5591
5775
  const chosen = named.length > 0 ? named : [requireEnvironment(input)];
5592
5776
  const environments = declared === void 0 ? chosen : declared.environments;
@@ -5837,7 +6021,11 @@ function renderCutover(result2) {
5837
6021
  const glyph = step.action === "conflicted" ? WARN : step.action === "info" ? "\u2192" : CHECK;
5838
6022
  return step.note === void 0 ? { glyph, text: step.text } : { glyph, text: step.text, note: step.note };
5839
6023
  }),
5840
- ...plan2.install.packages.filter((entry) => !entry.satisfied).map((entry) => ({ glyph: CHECK, text: `Installed ${entry.name}`, note: entry.version })),
6024
+ ...installedPackages(plan2.install).map((entry) => ({
6025
+ glyph: CHECK,
6026
+ text: `Installed ${entry.name}`,
6027
+ note: entry.version
6028
+ })),
5841
6029
  {
5842
6030
  glyph: CHECK,
5843
6031
  text: `Imported ${plan2.fields.length} parameters`,
@@ -5997,7 +6185,7 @@ async function cutoverInteractively(root, base, adoption) {
5997
6185
  write(renderCutover(await applyCutover({ ...plan2, decisions: { ...plan2.decisions, inject } })));
5998
6186
  }
5999
6187
  function offeredEnvironment(root) {
6000
- const declared = declaredIn(root);
6188
+ const declared = declaredIn2(root);
6001
6189
  if (declared === void 0) {
6002
6190
  return DEVELOPMENT;
6003
6191
  }