@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/bin.cjs +249 -54
- package/dist/bin.cjs.map +1 -1
- package/dist/{chunk-JJY4RLDJ.js → chunk-NHTDIZZ2.js} +231 -45
- package/dist/chunk-NHTDIZZ2.js.map +1 -0
- package/dist/index.cjs +239 -51
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +93 -88
- package/dist/index.js.map +1 -1
- package/dist/install.cjs +230 -42
- package/dist/install.cjs.map +1 -1
- package/dist/install.d.cts +41 -7
- package/dist/install.d.ts +41 -7
- package/dist/install.js +5 -1
- package/package.json +13 -7
- package/dist/chunk-JJY4RLDJ.js.map +0 -1
package/dist/install.cjs
CHANGED
|
@@ -26,6 +26,8 @@ __export(install_exports, {
|
|
|
26
26
|
engineVersion: () => engineVersion,
|
|
27
27
|
installFailed: () => installFailed,
|
|
28
28
|
installWithPackageManager: () => installWithPackageManager,
|
|
29
|
+
installedPackages: () => installedPackages,
|
|
30
|
+
isPnpmWorkspaceRoot: () => isPnpmWorkspaceRoot,
|
|
29
31
|
planInstall: () => planInstall,
|
|
30
32
|
renderInstallPlan: () => renderInstallPlan,
|
|
31
33
|
schemaPackageVersion: () => schemaPackageVersion
|
|
@@ -171,11 +173,25 @@ var LOCKFILES = [
|
|
|
171
173
|
["npm", "package-lock.json"]
|
|
172
174
|
];
|
|
173
175
|
var ADD = {
|
|
174
|
-
pnpm: ["pnpm", "add"
|
|
175
|
-
npm: ["npm", "install"
|
|
176
|
-
yarn: ["yarn", "add"
|
|
177
|
-
bun: ["bun", "add"
|
|
176
|
+
pnpm: ["pnpm", "add"],
|
|
177
|
+
npm: ["npm", "install"],
|
|
178
|
+
yarn: ["yarn", "add"],
|
|
179
|
+
bun: ["bun", "add"]
|
|
178
180
|
};
|
|
181
|
+
var EXACT = {
|
|
182
|
+
pnpm: "--save-exact",
|
|
183
|
+
npm: "--save-exact",
|
|
184
|
+
yarn: "--exact",
|
|
185
|
+
bun: "--exact"
|
|
186
|
+
};
|
|
187
|
+
var DEV = {
|
|
188
|
+
pnpm: "-D",
|
|
189
|
+
npm: "--save-dev",
|
|
190
|
+
yarn: "--dev",
|
|
191
|
+
bun: "--dev"
|
|
192
|
+
};
|
|
193
|
+
var WORKSPACE_ROOT_FLAG = "-w";
|
|
194
|
+
var PNPM_WORKSPACE = "pnpm-workspace.yaml";
|
|
179
195
|
function engineVersion() {
|
|
180
196
|
const version = ownManifest()?.version;
|
|
181
197
|
if (typeof version === "string" && version.length > 0) {
|
|
@@ -238,69 +254,219 @@ function manifestOf(root) {
|
|
|
238
254
|
return void 0;
|
|
239
255
|
}
|
|
240
256
|
}
|
|
241
|
-
function
|
|
242
|
-
const manifest = manifestOf(
|
|
257
|
+
function declaredIn(dir, name) {
|
|
258
|
+
const manifest = manifestOf(dir);
|
|
243
259
|
for (const field of ["dependencies", "devDependencies"]) {
|
|
244
260
|
const block = manifest?.[field];
|
|
245
261
|
if (block !== null && typeof block === "object" && !Array.isArray(block)) {
|
|
246
262
|
const version = block[name];
|
|
247
263
|
if (typeof version === "string") {
|
|
248
|
-
return version;
|
|
264
|
+
return { version, dev: field === "devDependencies" };
|
|
249
265
|
}
|
|
250
266
|
}
|
|
251
267
|
}
|
|
252
268
|
return void 0;
|
|
253
269
|
}
|
|
270
|
+
function isPnpmWorkspaceRoot(root) {
|
|
271
|
+
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"));
|
|
272
|
+
}
|
|
273
|
+
function workspaceGlobs(root) {
|
|
274
|
+
let text;
|
|
275
|
+
try {
|
|
276
|
+
text = (0, import_node_fs2.readFileSync)((0, import_node_path2.join)(root, PNPM_WORKSPACE), "utf8");
|
|
277
|
+
} catch {
|
|
278
|
+
return [];
|
|
279
|
+
}
|
|
280
|
+
const unquote = (raw) => raw.replace(/^['"]|['"]$/g, "").trim();
|
|
281
|
+
const globs = [];
|
|
282
|
+
let inside = false;
|
|
283
|
+
for (const line of text.split(/\r?\n/)) {
|
|
284
|
+
const flow = /^packages:\s*\[(.*)\]\s*$/.exec(line);
|
|
285
|
+
if (flow?.[1] !== void 0) {
|
|
286
|
+
return flow[1].split(",").map(unquote).filter((glob) => glob !== "");
|
|
287
|
+
}
|
|
288
|
+
if (/^packages:\s*$/.test(line)) {
|
|
289
|
+
inside = true;
|
|
290
|
+
continue;
|
|
291
|
+
}
|
|
292
|
+
if (!inside) {
|
|
293
|
+
continue;
|
|
294
|
+
}
|
|
295
|
+
const item = /^\s+-\s*(.+?)\s*$/.exec(line);
|
|
296
|
+
if (item?.[1] !== void 0) {
|
|
297
|
+
globs.push(unquote(item[1]));
|
|
298
|
+
continue;
|
|
299
|
+
}
|
|
300
|
+
if (line.trim() !== "" && !line.trimStart().startsWith("#")) {
|
|
301
|
+
break;
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
return globs;
|
|
305
|
+
}
|
|
306
|
+
function directoriesIn(dir) {
|
|
307
|
+
try {
|
|
308
|
+
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));
|
|
309
|
+
} catch {
|
|
310
|
+
return [];
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
function isDirectory(path) {
|
|
314
|
+
try {
|
|
315
|
+
return (0, import_node_fs2.statSync)(path).isDirectory();
|
|
316
|
+
} catch {
|
|
317
|
+
return false;
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
function expandGlob(root, glob) {
|
|
321
|
+
let dirs = [root];
|
|
322
|
+
for (const segment of glob.split("/").filter((part) => part !== "" && part !== ".")) {
|
|
323
|
+
const next = [];
|
|
324
|
+
for (const dir of dirs) {
|
|
325
|
+
if (segment === "**") {
|
|
326
|
+
const stack = [dir];
|
|
327
|
+
while (stack.length > 0) {
|
|
328
|
+
const current = stack.pop();
|
|
329
|
+
next.push(current);
|
|
330
|
+
stack.push(...directoriesIn(current));
|
|
331
|
+
}
|
|
332
|
+
continue;
|
|
333
|
+
}
|
|
334
|
+
if (segment.includes("*")) {
|
|
335
|
+
const pattern = new RegExp(
|
|
336
|
+
`^${segment.replace(/[.+^${}()|[\]\\]/g, "\\$&").replace(/\*/g, "[^/]*")}$`
|
|
337
|
+
);
|
|
338
|
+
next.push(
|
|
339
|
+
...directoriesIn(dir).filter((child) => pattern.test(child.slice(dir.length + 1)))
|
|
340
|
+
);
|
|
341
|
+
continue;
|
|
342
|
+
}
|
|
343
|
+
const candidate = (0, import_node_path2.join)(dir, segment);
|
|
344
|
+
if (isDirectory(candidate)) {
|
|
345
|
+
next.push(candidate);
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
dirs = next;
|
|
349
|
+
}
|
|
350
|
+
return dirs;
|
|
351
|
+
}
|
|
352
|
+
function workspaceMembers(root, name) {
|
|
353
|
+
if (!isPnpmWorkspaceRoot(root)) {
|
|
354
|
+
return [];
|
|
355
|
+
}
|
|
356
|
+
const globs = workspaceGlobs(root);
|
|
357
|
+
const excluded = globs.filter((glob) => glob.startsWith("!")).flatMap((glob) => expandGlob(root, glob.slice(1)));
|
|
358
|
+
const found = /* @__PURE__ */ new Set();
|
|
359
|
+
for (const glob of globs.filter((entry) => !entry.startsWith("!"))) {
|
|
360
|
+
for (const dir of expandGlob(root, glob)) {
|
|
361
|
+
if (dir !== root && !excluded.includes(dir) && declaredIn(dir, name) !== void 0) {
|
|
362
|
+
found.add(dir);
|
|
363
|
+
}
|
|
364
|
+
}
|
|
365
|
+
}
|
|
366
|
+
return [...found].sort();
|
|
367
|
+
}
|
|
368
|
+
function manifestPathOf(root, dir) {
|
|
369
|
+
const within = (0, import_node_path2.relative)(root, dir).split(import_node_path2.sep).filter((part) => part !== "");
|
|
370
|
+
return [...within, "package.json"].join("/");
|
|
371
|
+
}
|
|
372
|
+
function addCommand(manager, options, specs) {
|
|
373
|
+
const [bin, verb] = ADD[manager];
|
|
374
|
+
return [
|
|
375
|
+
bin,
|
|
376
|
+
...options.filter === void 0 ? [] : ["--filter", options.filter],
|
|
377
|
+
verb,
|
|
378
|
+
...options.workspaceRoot ? [WORKSPACE_ROOT_FLAG] : [],
|
|
379
|
+
EXACT[manager],
|
|
380
|
+
...options.dev ? [DEV[manager]] : [],
|
|
381
|
+
...specs
|
|
382
|
+
];
|
|
383
|
+
}
|
|
384
|
+
function stepFor(manager, manifest, packages, options) {
|
|
385
|
+
const pending = packages.filter((entry) => !entry.satisfied);
|
|
386
|
+
const specs = (pending.length === 0 ? packages : pending).map(
|
|
387
|
+
(entry) => `${entry.name}@${entry.version}`
|
|
388
|
+
);
|
|
389
|
+
return {
|
|
390
|
+
manifest,
|
|
391
|
+
packages,
|
|
392
|
+
command: addCommand(manager, options, specs),
|
|
393
|
+
satisfied: pending.length === 0
|
|
394
|
+
};
|
|
395
|
+
}
|
|
254
396
|
function planInstall(root, version = engineVersion()) {
|
|
255
397
|
const manager = detectPackageManager(root);
|
|
256
398
|
const lockfile = LOCKFILES.find(
|
|
257
399
|
([name, file]) => name === manager && (0, import_node_fs2.existsSync)((0, import_node_path2.join)(root, file))
|
|
258
400
|
)?.[1];
|
|
259
|
-
const
|
|
260
|
-
const
|
|
401
|
+
const workspaceRoot = manager === "pnpm" && isPnpmWorkspaceRoot(root);
|
|
402
|
+
const runtime = declaredIn(root, RUNTIME_PACKAGE);
|
|
403
|
+
const zod = declaredIn(root, SCHEMA_PACKAGE);
|
|
261
404
|
const packages = [
|
|
262
405
|
{
|
|
263
406
|
name: RUNTIME_PACKAGE,
|
|
264
407
|
version,
|
|
265
|
-
...
|
|
266
|
-
satisfied:
|
|
408
|
+
...runtime === void 0 ? {} : { declared: runtime.version },
|
|
409
|
+
satisfied: runtime?.version === version
|
|
267
410
|
},
|
|
268
411
|
{
|
|
269
412
|
name: SCHEMA_PACKAGE,
|
|
270
413
|
version: schemaPackageVersion(),
|
|
271
|
-
...
|
|
414
|
+
...zod === void 0 ? {} : { declared: zod.version },
|
|
272
415
|
// Any declared zod counts: which zod a project uses is the project's
|
|
273
416
|
// decision, and penv is here to make sure there is one, not to move it.
|
|
274
|
-
satisfied:
|
|
417
|
+
satisfied: zod !== void 0
|
|
275
418
|
}
|
|
276
419
|
];
|
|
277
420
|
const pending = packages.filter((entry) => !entry.satisfied);
|
|
278
|
-
const
|
|
279
|
-
(
|
|
280
|
-
|
|
421
|
+
const steps = [
|
|
422
|
+
stepFor(manager, "package.json", packages, {
|
|
423
|
+
workspaceRoot,
|
|
424
|
+
dev: runtime?.dev === true && pending.every((entry) => entry.name === RUNTIME_PACKAGE) && pending.length > 0
|
|
425
|
+
})
|
|
426
|
+
];
|
|
427
|
+
for (const dir of workspaceMembers(root, RUNTIME_PACKAGE)) {
|
|
428
|
+
const declared = declaredIn(dir, RUNTIME_PACKAGE);
|
|
429
|
+
steps.push(
|
|
430
|
+
stepFor(
|
|
431
|
+
manager,
|
|
432
|
+
manifestPathOf(root, dir),
|
|
433
|
+
[
|
|
434
|
+
{
|
|
435
|
+
name: RUNTIME_PACKAGE,
|
|
436
|
+
version,
|
|
437
|
+
declared: declared.version,
|
|
438
|
+
satisfied: declared.version === version
|
|
439
|
+
}
|
|
440
|
+
],
|
|
441
|
+
{
|
|
442
|
+
filter: `./${(0, import_node_path2.relative)(root, dir).split(import_node_path2.sep).join("/")}`,
|
|
443
|
+
workspaceRoot: false,
|
|
444
|
+
dev: declared.dev
|
|
445
|
+
}
|
|
446
|
+
)
|
|
447
|
+
);
|
|
448
|
+
}
|
|
281
449
|
return {
|
|
282
450
|
root,
|
|
283
451
|
manager,
|
|
284
|
-
|
|
285
|
-
command: [...ADD[manager], ...specs],
|
|
452
|
+
steps,
|
|
286
453
|
...lockfile === void 0 ? {} : { lockfile },
|
|
287
|
-
satisfied:
|
|
454
|
+
satisfied: steps.every((step) => step.satisfied)
|
|
288
455
|
};
|
|
289
456
|
}
|
|
290
457
|
function describe(entry) {
|
|
291
458
|
return `${entry.name} ${entry.version}`;
|
|
292
459
|
}
|
|
293
|
-
function
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
const pending = plan.packages.filter((entry) => !entry.satisfied);
|
|
460
|
+
function installedPackages(plan) {
|
|
461
|
+
const pending = plan.steps.flatMap((step) => step.packages).filter((entry) => !entry.satisfied);
|
|
462
|
+
return [...new Map(pending.map((entry) => [entry.name, entry])).values()];
|
|
463
|
+
}
|
|
464
|
+
function renderStep(step) {
|
|
465
|
+
const pending = step.packages.filter((entry) => !entry.satisfied);
|
|
300
466
|
const added = pending.filter((entry) => entry.declared === void 0);
|
|
301
467
|
const replaced = pending.filter((entry) => entry.declared !== void 0);
|
|
302
468
|
return [
|
|
303
|
-
|
|
469
|
+
step.manifest,
|
|
304
470
|
...added.length === 0 ? [] : [
|
|
305
471
|
' + "dependencies": {',
|
|
306
472
|
...added.map((entry) => ` + "${entry.name}": "${entry.version}"`),
|
|
@@ -309,29 +475,49 @@ function renderInstallPlan(plan) {
|
|
|
309
475
|
...replaced.flatMap((entry) => [
|
|
310
476
|
` - "${entry.name}": "${entry.declared}"`,
|
|
311
477
|
` + "${entry.name}": "${entry.version}"`
|
|
312
|
-
])
|
|
313
|
-
|
|
478
|
+
])
|
|
479
|
+
];
|
|
480
|
+
}
|
|
481
|
+
function renderInstallPlan(plan) {
|
|
482
|
+
if (plan.satisfied) {
|
|
483
|
+
const packages = plan.steps[0]?.packages ?? [];
|
|
484
|
+
return [
|
|
485
|
+
`package.json already has ${packages.map(describe).join(" and ")} \u2014 nothing to install.`
|
|
486
|
+
];
|
|
487
|
+
}
|
|
488
|
+
const pending = plan.steps.filter((step) => !step.satisfied);
|
|
489
|
+
const [first, ...rest] = pending.map((step) => step.command.join(" "));
|
|
490
|
+
const landing = installedPackages(plan).map((entry) => ` + ${entry.name}@${entry.version}`);
|
|
491
|
+
return [
|
|
492
|
+
...pending.flatMap(renderStep),
|
|
493
|
+
...plan.lockfile === void 0 ? [] : [plan.lockfile, ...landing],
|
|
314
494
|
"",
|
|
315
|
-
`Run with: ${
|
|
495
|
+
`Run with: ${first ?? ""}`,
|
|
496
|
+
...rest.map((command) => ` then ${command}`)
|
|
316
497
|
];
|
|
317
498
|
}
|
|
318
499
|
var installWithPackageManager = async (plan) => {
|
|
319
|
-
const
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
500
|
+
for (const step of plan.steps) {
|
|
501
|
+
if (step.satisfied) {
|
|
502
|
+
continue;
|
|
503
|
+
}
|
|
504
|
+
const child = startChild({
|
|
505
|
+
command: step.command,
|
|
506
|
+
env: process.env,
|
|
507
|
+
cwd: plan.root,
|
|
508
|
+
purpose: `install ${step.packages.map(describe).join(" and ")} in ${step.manifest}`
|
|
509
|
+
});
|
|
510
|
+
const ended = await child.ended;
|
|
511
|
+
if (ended.exitCode !== 0 || ended.signal !== null) {
|
|
512
|
+
throw installFailed(plan, step);
|
|
513
|
+
}
|
|
328
514
|
}
|
|
329
515
|
};
|
|
330
|
-
function installFailed(plan) {
|
|
516
|
+
function installFailed(plan, step) {
|
|
331
517
|
return new import_core2.PenvError(
|
|
332
518
|
"INIT_INSTALL_FAILED",
|
|
333
|
-
`${
|
|
334
|
-
`
|
|
519
|
+
`${step.command.join(" ")} did not finish, so penv migrated nothing`,
|
|
520
|
+
`Read what ${plan.manager} printed above \u2014 it names what it refused. Fix that and run this command again; your dotenv files are exactly where they were.`
|
|
335
521
|
);
|
|
336
522
|
}
|
|
337
523
|
// Annotate the CommonJS export names for ESM import in node:
|
|
@@ -342,6 +528,8 @@ function installFailed(plan) {
|
|
|
342
528
|
engineVersion,
|
|
343
529
|
installFailed,
|
|
344
530
|
installWithPackageManager,
|
|
531
|
+
installedPackages,
|
|
532
|
+
isPnpmWorkspaceRoot,
|
|
345
533
|
planInstall,
|
|
346
534
|
renderInstallPlan,
|
|
347
535
|
schemaPackageVersion
|
package/dist/install.cjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/install.ts","../src/child.ts"],"sourcesContent":["/**\n * The runtime dependencies an adopted project takes, and how they get there.\n *\n * PRD §3: an adopted project depends on `@penvhq/penv` at the engine's own\n * version — the typed `@env` surface, not a CLI distribution. It also depends on\n * zod, because the `penv.schema.ts` init scaffolds imports it: zod is a *peer* of\n * `@penvhq/penv`, and a peer is a package the project supplies. Under pnpm's\n * strict layout nothing hoists it to the project root, so an install that named\n * only `@penvhq/penv` left the very schema init had just written unable to\n * resolve `zod` — and adoption could never finish.\n *\n * Both are installed with the package manager the project already uses, and only\n * after showing the exact `package.json` and lockfile change: an install is the\n * one step of adoption that reaches outside the repository, so it is the one step\n * that is shown before it happens rather than reported after.\n *\n * The install itself is a seam. It shells out to a package manager, which the\n * tests must never do — and a fake here is not a weaker test, because what init\n * has to get right is the plan, the consent, and the refusal when the install\n * does not happen.\n *\n * Two commands write that dependency line: `penv init`, which is the engine's,\n * and `penv upgrade`, which is the launcher's. This module is published at\n * `@penvhq/cli/install` so the launcher reaches it without loading the command\n * surface — one answer to \"which package manager, which diff, which spawn\",\n * rather than a second copy on the other side of the launcher/engine split.\n */\n\nimport { existsSync, readFileSync } from \"node:fs\";\nimport { join } from \"node:path\";\nimport { PenvError } from \"@penvhq/core\";\nimport { startChild } from \"./child.js\";\n\n/** The package an adopted project depends on. The CLI engine is not one of its dependencies. */\nexport const RUNTIME_PACKAGE = \"@penvhq/penv\";\n\n/** The peer `penv.schema.ts` imports, which the project supplies because a peer is not hoisted. */\nexport const SCHEMA_PACKAGE = \"zod\";\n\nexport type PackageManager = \"pnpm\" | \"npm\" | \"yarn\" | \"bun\";\n\n/** The lockfile that names each manager, checked in this order. */\nconst LOCKFILES: readonly (readonly [PackageManager, string])[] = [\n [\"pnpm\", \"pnpm-lock.yaml\"],\n [\"yarn\", \"yarn.lock\"],\n [\"bun\", \"bun.lock\"],\n [\"bun\", \"bun.lockb\"],\n [\"npm\", \"package-lock.json\"],\n];\n\n/** How each manager is told to add one exact version. */\nconst ADD: Readonly<Record<PackageManager, readonly string[]>> = {\n pnpm: [\"pnpm\", \"add\", \"--save-exact\"],\n npm: [\"npm\", \"install\", \"--save-exact\"],\n yarn: [\"yarn\", \"add\", \"--exact\"],\n bun: [\"bun\", \"add\", \"--exact\"],\n};\n\n/** One package the adopted project needs, and what its `package.json` says today. */\nexport interface InstallPackage {\n readonly name: string;\n readonly version: string;\n /** What `package.json` already says about it, when it says anything. */\n readonly declared?: string;\n /** True when this project already has it — nothing to install for this one. */\n readonly satisfied: boolean;\n}\n\nexport interface InstallPlan {\n readonly root: string;\n readonly manager: PackageManager;\n /** Everything an adopted project needs, in the order the diff shows them. */\n readonly packages: readonly InstallPackage[];\n /** The command, argv-shaped — what runs, and what a refusal tells the user to run. */\n readonly command: readonly string[];\n /** The lockfile the manager will rewrite, when the project has one. */\n readonly lockfile?: string;\n /** True when every package is already there — nothing to install. */\n readonly satisfied: boolean;\n}\n\n/** Runs an install plan, or throws. Replaced in tests; never spawns there. */\nexport type InstallRuntime = (plan: InstallPlan) => Promise<void>;\n\n/**\n * The engine's own version, read from its manifest rather than restated in the\n * source: `@penvhq/penv` must match the engine exactly, and a constant beside\n * the version a release bumps is a second answer waiting to drift.\n */\nexport function engineVersion(): string {\n const version = ownManifest()?.version;\n if (typeof version === \"string\" && version.length > 0) {\n return version;\n }\n throw new PenvError(\n \"ENGINE_VERSION_UNREADABLE\",\n \"penv could not read its own version, so it cannot say which `@penvhq/penv` this project needs\",\n `Reinstall penv, then run \\`penv init\\` again.`,\n );\n}\n\n/**\n * The zod an adopted project installs: the floor of the peer range the engine\n * and `@penvhq/penv` both declare, which is the version penv is built and tested\n * against.\n *\n * The floor rather than the range, because the diff shown before the install has\n * to be the line that actually lands — `--save-exact` on `^4.4.3` would write\n * whatever the registry resolved that day, which is not something a reader can\n * consent to in advance.\n */\nexport function schemaPackageVersion(): string {\n const peers = ownManifest()?.peerDependencies;\n const declared =\n peers !== null && typeof peers === \"object\" && !Array.isArray(peers)\n ? (peers as Record<string, unknown>)[SCHEMA_PACKAGE]\n : undefined;\n const floor = typeof declared === \"string\" ? declared.replace(/^[\\^~>=\\s]+/, \"\").trim() : \"\";\n if (floor.length > 0) {\n return floor;\n }\n throw new PenvError(\n \"ENGINE_PEER_UNREADABLE\",\n `penv could not read its own \\`${SCHEMA_PACKAGE}\\` peer range, so it cannot say which ${SCHEMA_PACKAGE} this project needs`,\n `Reinstall penv, then run \\`penv init\\` again.`,\n );\n}\n\n/** The engine's own manifest, or `undefined` when it cannot be read. */\nfunction ownManifest(): Record<string, unknown> | undefined {\n try {\n const parsed: unknown = JSON.parse(\n readFileSync(new URL(\"../package.json\", import.meta.url), \"utf8\"),\n );\n return parsed !== null && typeof parsed === \"object\" && !Array.isArray(parsed)\n ? (parsed as Record<string, unknown>)\n : undefined;\n } catch {\n // The callers refuse: a version penv guessed would pin a project's\n // dependency to something nobody chose.\n return undefined;\n }\n}\n\n/** The package manager this project already uses: its lockfile, then what it declares, then npm. */\nexport function detectPackageManager(root: string): PackageManager {\n for (const [manager, lockfile] of LOCKFILES) {\n if (existsSync(join(root, lockfile))) {\n return manager;\n }\n }\n return declaredManager(root) ?? \"npm\";\n}\n\n/** `\"packageManager\": \"pnpm@9.1.0\"` — corepack's field, and a project's own answer. */\nfunction declaredManager(root: string): PackageManager | undefined {\n const declared = manifestOf(root)?.packageManager;\n if (typeof declared !== \"string\") {\n return undefined;\n }\n const name = declared.split(\"@\")[0];\n return name === \"pnpm\" || name === \"npm\" || name === \"yarn\" || name === \"bun\" ? name : undefined;\n}\n\nfunction manifestOf(root: string): Record<string, unknown> | undefined {\n const file = join(root, \"package.json\");\n if (!existsSync(file)) {\n return undefined;\n }\n try {\n const manifest: unknown = JSON.parse(readFileSync(file, \"utf8\"));\n return manifest !== null && typeof manifest === \"object\" && !Array.isArray(manifest)\n ? (manifest as Record<string, unknown>)\n : undefined;\n } catch {\n return undefined;\n }\n}\n\n/** What `package.json` says about one package today, from either dependency block. */\nfunction declaredVersion(root: string, name: string): string | undefined {\n const manifest = manifestOf(root);\n for (const field of [\"dependencies\", \"devDependencies\"] as const) {\n const block: unknown = manifest?.[field];\n if (block !== null && typeof block === \"object\" && !Array.isArray(block)) {\n const version: unknown = (block as Record<string, unknown>)[name];\n if (typeof version === \"string\") {\n return version;\n }\n }\n }\n return undefined;\n}\n\nexport function planInstall(root: string, version: string = engineVersion()): InstallPlan {\n const manager = detectPackageManager(root);\n const lockfile = LOCKFILES.find(\n ([name, file]) => name === manager && existsSync(join(root, file)),\n )?.[1];\n\n const runtimeDeclared = declaredVersion(root, RUNTIME_PACKAGE);\n const zodDeclared = declaredVersion(root, SCHEMA_PACKAGE);\n const packages: InstallPackage[] = [\n {\n name: RUNTIME_PACKAGE,\n version,\n ...(runtimeDeclared === undefined ? {} : { declared: runtimeDeclared }),\n satisfied: runtimeDeclared === version,\n },\n {\n name: SCHEMA_PACKAGE,\n version: schemaPackageVersion(),\n ...(zodDeclared === undefined ? {} : { declared: zodDeclared }),\n // Any declared zod counts: which zod a project uses is the project's\n // decision, and penv is here to make sure there is one, not to move it.\n satisfied: zodDeclared !== undefined,\n },\n ];\n\n const pending = packages.filter((entry) => !entry.satisfied);\n const specs = (pending.length === 0 ? packages : pending).map(\n (entry) => `${entry.name}@${entry.version}`,\n );\n return {\n root,\n manager,\n packages,\n command: [...ADD[manager], ...specs],\n ...(lockfile === undefined ? {} : { lockfile }),\n satisfied: pending.length === 0,\n };\n}\n\nfunction describe(entry: InstallPackage): string {\n return `${entry.name} ${entry.version}`;\n}\n\n/**\n * The change, as it will appear in the diff — the whole point of showing it is\n * that the reader recognises their own file, so these are the `package.json`\n * lines that land and the lockfile that gets rewritten, not a summary of both.\n */\nexport function renderInstallPlan(plan: InstallPlan): string[] {\n if (plan.satisfied) {\n return [\n `package.json already has ${plan.packages.map(describe).join(\" and \")} — nothing to install.`,\n ];\n }\n const pending = plan.packages.filter((entry) => !entry.satisfied);\n const added = pending.filter((entry) => entry.declared === undefined);\n const replaced = pending.filter((entry) => entry.declared !== undefined);\n return [\n \"package.json\",\n ...(added.length === 0\n ? []\n : [\n ' + \"dependencies\": {',\n ...added.map((entry) => ` + \"${entry.name}\": \"${entry.version}\"`),\n \" + }\",\n ]),\n ...replaced.flatMap((entry) => [\n ` - \"${entry.name}\": \"${entry.declared}\"`,\n ` + \"${entry.name}\": \"${entry.version}\"`,\n ]),\n ...(plan.lockfile === undefined\n ? []\n : [plan.lockfile, ...pending.map((entry) => ` + ${entry.name}@${entry.version}`)]),\n \"\",\n `Run with: ${plan.command.join(\" \")}`,\n ];\n}\n\n/**\n * The real install: the project's own package manager, started the way any other\n * child is (`.cmd` shims on Windows included), with its output the user's to see.\n */\nexport const installWithPackageManager: InstallRuntime = async (plan) => {\n const child = startChild({\n command: plan.command,\n env: process.env as Record<string, string>,\n cwd: plan.root,\n purpose: `install ${plan.packages.map(describe).join(\" and \")}`,\n });\n const ended = await child.ended;\n if (ended.exitCode !== 0 || ended.signal !== null) {\n throw installFailed(plan);\n }\n};\n\nexport function installFailed(plan: InstallPlan): PenvError {\n return new PenvError(\n \"INIT_INSTALL_FAILED\",\n `${plan.command.join(\" \")} did not finish, so penv migrated nothing`,\n `Run \\`${plan.command.join(\" \")}\\` yourself, then start this command again. Your dotenv files are exactly where they were.`,\n );\n}\n","/**\n * Starting someone else's command, opaquely.\n *\n * `penv run -- <command>` starts exactly what follows `--`: the argument\n * boundaries the shell already worked out are handed to the operating system\n * untouched, stdio is the parent's, and the child's exit code and terminating\n * signal come back out. penv never parses the command, never rebuilds a command\n * line from it, never wraps it in a shell — a shell would re-split what the user\n * already split, and `penv run -- node -e \"console.log(1 > 2)\"` would redirect to\n * a file called `2`.\n *\n * Windows is the one place where \"hand it to the operating system\" needs help.\n * `pnpm`, `next` and every other node-installed tool are `.cmd` shims there, and\n * Node refuses to execute one without a shell. So a `.cmd`/`.bat` target — and\n * only that — is started through `cmd.exe /d /s /c` with\n * `windowsVerbatimArguments`, building the one command line cmd will accept and\n * escaping every argument so that cmd hands the child the same bytes penv was\n * given. Everything else spawns directly, on every platform.\n */\n\nimport { spawn } from \"node:child_process\";\nimport { existsSync, statSync } from \"node:fs\";\nimport { delimiter, isAbsolute, join, win32 } from \"node:path\";\nimport { PenvError } from \"@penvhq/core\";\n\n/** How a child ended. Exactly one of these is meaningful, and both are forwarded. */\nexport interface ChildResult {\n /** The child's own exit code, or 1 when a signal ended it. */\n readonly exitCode: number;\n /** The signal that ended the child, when one did. */\n readonly signal: NodeJS.Signals | null;\n}\n\nexport interface ChildInvocation {\n /** The command exactly as it followed `--`: the executable, then its arguments. */\n readonly command: readonly string[];\n readonly env: Record<string, string>;\n readonly cwd: string;\n /**\n * What penv is starting this on its own behalf to do — `init`'s dependency\n * install. Absent means the command is the user's, from after `--`, and the\n * two failures have opposite remedies: one is about what they typed, the other\n * about a program penv chose to run.\n */\n readonly purpose?: string;\n}\n\n/** A started child: how it ends, and the one thing a wrapper may do to it. */\nexport interface ChildHandle {\n /** Resolves when the child has ended, however it ended. */\n readonly ended: Promise<ChildResult>;\n /** Asks the child to stop — what `--watch` does before it starts the next one. */\n kill(signal?: NodeJS.Signals): void;\n}\n\n/** The seam `run` starts a child through — replaced in tests that assert what it was given. */\nexport type StartChild = (invocation: ChildInvocation) => ChildHandle;\n\n/** The signals a wrapper must pass through rather than absorb. */\nconst FORWARDED: readonly NodeJS.Signals[] = [\"SIGINT\", \"SIGTERM\", \"SIGHUP\", \"SIGBREAK\"];\n\nexport const startChild: StartChild = (invocation) => {\n const [executable, ...args] = invocation.command;\n if (executable === undefined) {\n throw noCommand();\n }\n\n const target = resolveTarget(executable, args, invocation.env);\n const child = spawn(target.file, target.args, {\n cwd: invocation.cwd,\n env: invocation.env,\n stdio: \"inherit\",\n ...(target.verbatim ? { windowsVerbatimArguments: true } : {}),\n });\n\n // Forwarded rather than handled: penv is a wrapper, and a Ctrl-C belongs to\n // the program the user is looking at. The child decides what to do with it,\n // and its answer comes back as the signal below.\n const forward = new Map<NodeJS.Signals, () => void>();\n for (const signal of FORWARDED) {\n const handler = (): void => {\n child.kill(signal);\n };\n forward.set(signal, handler);\n process.on(signal, handler);\n }\n const release = (): void => {\n for (const [signal, handler] of forward) {\n process.off(signal, handler);\n }\n };\n\n const ended = new Promise<ChildResult>((resolve, reject) => {\n child.on(\"error\", (cause) => {\n release();\n reject(cannotStart(executable, cause, invocation.purpose));\n });\n child.on(\"exit\", (code, signal) => {\n release();\n resolve({ exitCode: code ?? 1, signal });\n });\n });\n\n return {\n ended,\n kill(signal) {\n child.kill(signal);\n },\n };\n};\n\nexport function noCommand(): PenvError {\n return new PenvError(\n \"RUN_NO_COMMAND\",\n \"`penv run` was given no command to start\",\n \"Put the command after `--`, e.g. `penv run -- pnpm dev`.\",\n );\n}\n\nfunction cannotStart(executable: string, cause: unknown, purpose: string | undefined): PenvError {\n const detail = cause instanceof Error ? cause.message : String(cause);\n if (purpose !== undefined) {\n return new PenvError(\n \"PENV_COMMAND_NOT_STARTED\",\n `penv could not start \\`${executable}\\` to ${purpose}: ${detail}`,\n `Check that \\`${executable}\\` runs on its own — penv starts it the way your shell does, so it has to be on PATH. Nothing was changed.`,\n );\n }\n return new PenvError(\n \"RUN_COMMAND_NOT_STARTED\",\n `\\`${executable}\\` could not be started: ${detail}`,\n `Check the command after \\`--\\` runs on its own — \\`${executable}\\` has to be on PATH, exactly as it is spelled here.`,\n );\n}\n\ninterface SpawnTarget {\n readonly file: string;\n readonly args: readonly string[];\n /** True when the args are one pre-built command line rather than a list. */\n readonly verbatim: boolean;\n}\n\nfunction resolveTarget(\n executable: string,\n args: readonly string[],\n env: Readonly<Record<string, string | undefined>>,\n): SpawnTarget {\n if (process.platform !== \"win32\") {\n return { file: executable, args, verbatim: false };\n }\n const resolved = findExecutable(executable, env);\n if (resolved === undefined || !/\\.(cmd|bat)$/i.test(resolved)) {\n return { file: resolved ?? executable, args, verbatim: false };\n }\n return {\n file: env.ComSpec ?? \"cmd.exe\",\n args: [\"/d\", \"/s\", \"/c\", `\"${cmdCommandLine(resolved, args)}\"`],\n verbatim: true,\n };\n}\n\n/** A package-manager shim, which re-invokes cmd on its own way through. */\nconst SHIM = /(?:^|\\\\)node_modules\\\\\\.bin\\\\[^\\\\]+\\.cmd$/i;\n\n/**\n * The one command line cmd.exe is handed, escaped so the child receives the\n * bytes penv was given.\n *\n * The path is normalized first and *then* judged: `./node_modules/.bin/next.cmd`\n * and `.\\node_modules\\.bin\\next.cmd` are the same shim, and deciding on the\n * un-normalized spelling would escape a forward-slash invocation once while cmd\n * expands it twice — so an argument holding `&` would run as a command inside\n * the shim's second round. Windows' own separator, whatever this process runs\n * on, because this line is only ever read by cmd.exe.\n */\nexport function cmdCommandLine(resolved: string, args: readonly string[]): string {\n const command = win32.normalize(resolved);\n const shim = SHIM.test(command);\n return [escapeCommand(command), ...args.map((argument) => escapeArgument(argument, shim))].join(\n \" \",\n );\n}\n\n/**\n * The extensions a name is tried with, in the order the platform's own launcher\n * tries them.\n *\n * On Windows PATHEXT leads and the bare name comes last, because the bare name\n * is almost never what Windows would run: `pnpm`, `npx` and every\n * `node_modules/.bin` tool ship an extensionless POSIX shell script *beside*\n * their `.CMD` shim, in the same directory. Trying the empty extension first\n * matched that script, which is not executable by CreateProcess and is not a\n * `.cmd`, so the wrapper below was skipped and the spawn failed with ENOENT.\n * Everywhere else there are no extensions at all.\n */\nfunction extensions(\n env: Readonly<Record<string, string | undefined>>,\n platform: NodeJS.Platform,\n): string[] {\n if (platform !== \"win32\") {\n return [\"\"];\n }\n const declared = env.PATHEXT ?? \".COM;.EXE;.BAT;.CMD\";\n return [...declared.split(\";\").filter((extension) => extension.length > 0), \"\"];\n}\n\n/**\n * What the shell would have run, found the way the shell finds it: the name as\n * given if it carries a path, else each PATH directory, each with each\n * executable extension.\n *\n * `platform` is a parameter so the ordering above is testable on either kind of\n * machine — it is the whole behavior, and it differs by platform.\n */\nexport function findExecutable(\n executable: string,\n env: Readonly<Record<string, string | undefined>>,\n platform: NodeJS.Platform = process.platform,\n): string | undefined {\n const candidates = extensions(env, platform);\n const isFile = (path: string): boolean => existsSync(path) && statSync(path).isFile();\n\n if (executable.includes(\"/\") || executable.includes(\"\\\\\") || isAbsolute(executable)) {\n return candidates.map((extension) => executable + extension).find(isFile);\n }\n const path = env.PATH ?? env.Path ?? \"\";\n for (const directory of path.split(delimiter).filter((entry) => entry.length > 0)) {\n const hit = candidates.map((extension) => join(directory, executable + extension)).find(isFile);\n if (hit !== undefined) {\n return hit;\n }\n }\n return undefined;\n}\n\n/** The characters cmd.exe expands before the program ever sees them. */\nconst CMD_METACHARACTERS = /([()\\][%!^\"`<>&|;, *?])/g;\n\n/** The command's own path: cmd's metacharacters escaped, and no quotes to confuse it. */\nfunction escapeCommand(command: string): string {\n return command.replace(CMD_METACHARACTERS, \"^$1\");\n}\n\n/**\n * One argument, quoted so the child's runtime splits it exactly where penv was\n * given it, then escaped so cmd.exe passes those quotes through instead of\n * acting on them.\n */\nfunction escapeArgument(argument: string, doubleEscape: boolean): string {\n const quoted = `\"${argument.replace(/(\\\\*)\"/g, '$1$1\\\\\"').replace(/(\\\\*)$/, \"$1$1\")}\"`;\n const escaped = quoted.replace(CMD_METACHARACTERS, \"^$1\");\n return doubleEscape ? escaped.replace(CMD_METACHARACTERS, \"^$1\") : escaped;\n}\n"],"mappings":"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","names":["import_node_fs","import_node_path","import_core","path"]}
|
|
1
|
+
{"version":3,"sources":["../src/install.ts","../src/child.ts"],"sourcesContent":["/**\n * The runtime dependencies an adopted project takes, and how they get there.\n *\n * PRD §3: an adopted project depends on `@penvhq/penv` at the engine's own\n * version — the typed `@env` surface, not a CLI distribution. It also depends on\n * zod, because the `penv.schema.ts` init scaffolds imports it: zod is a *peer* of\n * `@penvhq/penv`, and a peer is a package the project supplies. Under pnpm's\n * strict layout nothing hoists it to the project root, so an install that named\n * only `@penvhq/penv` left the very schema init had just written unable to\n * resolve `zod` — and adoption could never finish.\n *\n * Both are installed with the package manager the project already uses, and only\n * after showing the exact `package.json` and lockfile change: an install is the\n * one step of adoption that reaches outside the repository, so it is the one step\n * that is shown before it happens rather than reported after.\n *\n * The install itself is a seam. It shells out to a package manager, which the\n * tests must never do — and a fake here is not a weaker test, because what init\n * has to get right is the plan, the consent, and the refusal when the install\n * does not happen.\n *\n * A plan is a list of steps, because in a workspace \"the project's dependency\"\n * is plural. pnpm refuses a bare `add` at a workspace root (`-w` is how you say\n * you meant the root), and a workspace package that declares `@penvhq/penv`\n * itself is a second copy of the very version the manifest pins — one repository\n * ran the 0.8 bridge under a 0.11 pin for three releases because nothing looked\n * below the root. So every `package.json` that declares it moves, under one\n * consent, and the commands shown are the ones that run.\n *\n * Two commands write that dependency line: `penv init`, which is the engine's,\n * and `penv upgrade`, which is the launcher's. This module is published at\n * `@penvhq/cli/install` so the launcher reaches it without loading the command\n * surface — one answer to \"which package manager, which diff, which spawn\",\n * rather than a second copy on the other side of the launcher/engine split.\n */\n\nimport { existsSync, readdirSync, readFileSync, statSync } from \"node:fs\";\nimport { join, relative, sep } from \"node:path\";\nimport { PenvError } from \"@penvhq/core\";\nimport { startChild } from \"./child.js\";\n\n/** The package an adopted project depends on. The CLI engine is not one of its dependencies. */\nexport const RUNTIME_PACKAGE = \"@penvhq/penv\";\n\n/** The peer `penv.schema.ts` imports, which the project supplies because a peer is not hoisted. */\nexport const SCHEMA_PACKAGE = \"zod\";\n\nexport type PackageManager = \"pnpm\" | \"npm\" | \"yarn\" | \"bun\";\n\n/** The lockfile that names each manager, checked in this order. */\nconst LOCKFILES: readonly (readonly [PackageManager, string])[] = [\n [\"pnpm\", \"pnpm-lock.yaml\"],\n [\"yarn\", \"yarn.lock\"],\n [\"bun\", \"bun.lock\"],\n [\"bun\", \"bun.lockb\"],\n [\"npm\", \"package-lock.json\"],\n];\n\n/** How each manager is told to add a package. */\nconst ADD: Readonly<Record<PackageManager, readonly [string, string]>> = {\n pnpm: [\"pnpm\", \"add\"],\n npm: [\"npm\", \"install\"],\n yarn: [\"yarn\", \"add\"],\n bun: [\"bun\", \"add\"],\n};\n\n/** How each manager is told to write the version down exactly, with no range. */\nconst EXACT: Readonly<Record<PackageManager, string>> = {\n pnpm: \"--save-exact\",\n npm: \"--save-exact\",\n yarn: \"--exact\",\n bun: \"--exact\",\n};\n\n/** How each manager is told to keep the dependency in the block it is already in. */\nconst DEV: Readonly<Record<PackageManager, string>> = {\n pnpm: \"-D\",\n npm: \"--save-dev\",\n yarn: \"--dev\",\n bun: \"--dev\",\n};\n\n/** pnpm refuses an install at a workspace root without this — `ERR_PNPM_ADDING_TO_ROOT`. */\nconst WORKSPACE_ROOT_FLAG = \"-w\";\n\n/** pnpm's workspace file, which is both what declares the members and what makes the root refuse. */\nconst PNPM_WORKSPACE = \"pnpm-workspace.yaml\";\n\n/** One package the adopted project needs, and what its `package.json` says today. */\nexport interface InstallPackage {\n readonly name: string;\n readonly version: string;\n /** What `package.json` already says about it, when it says anything. */\n readonly declared?: string;\n /** True when this project already has it — nothing to install for this one. */\n readonly satisfied: boolean;\n}\n\n/** One `package.json` the install rewrites, and the command that rewrites it. */\nexport interface InstallStep {\n /** The file the diff names — `package.json`, or a workspace package's path to it. */\n readonly manifest: string;\n /** Everything this file needs, in the order the diff shows them. */\n readonly packages: readonly InstallPackage[];\n /** The command, argv-shaped — run from the project root, whichever file it writes. */\n readonly command: readonly string[];\n /** True when this file already declares every one of them. */\n readonly satisfied: boolean;\n}\n\nexport interface InstallPlan {\n readonly root: string;\n readonly manager: PackageManager;\n /** The root's `package.json` first, then every workspace package that declares the runtime one. */\n readonly steps: readonly InstallStep[];\n /** The lockfile the manager will rewrite, when the project has one. */\n readonly lockfile?: string;\n /** True when every step is already satisfied — nothing to install. */\n readonly satisfied: boolean;\n}\n\n/** Runs an install plan, or throws. Replaced in tests; never spawns there. */\nexport type InstallRuntime = (plan: InstallPlan) => Promise<void>;\n\n/**\n * The engine's own version, read from its manifest rather than restated in the\n * source: `@penvhq/penv` must match the engine exactly, and a constant beside\n * the version a release bumps is a second answer waiting to drift.\n */\nexport function engineVersion(): string {\n const version = ownManifest()?.version;\n if (typeof version === \"string\" && version.length > 0) {\n return version;\n }\n throw new PenvError(\n \"ENGINE_VERSION_UNREADABLE\",\n \"penv could not read its own version, so it cannot say which `@penvhq/penv` this project needs\",\n `Reinstall penv, then run \\`penv init\\` again.`,\n );\n}\n\n/**\n * The zod an adopted project installs: the floor of the peer range the engine\n * and `@penvhq/penv` both declare, which is the version penv is built and tested\n * against.\n *\n * The floor rather than the range, because the diff shown before the install has\n * to be the line that actually lands — `--save-exact` on `^4.4.3` would write\n * whatever the registry resolved that day, which is not something a reader can\n * consent to in advance.\n */\nexport function schemaPackageVersion(): string {\n const peers = ownManifest()?.peerDependencies;\n const declared =\n peers !== null && typeof peers === \"object\" && !Array.isArray(peers)\n ? (peers as Record<string, unknown>)[SCHEMA_PACKAGE]\n : undefined;\n const floor = typeof declared === \"string\" ? declared.replace(/^[\\^~>=\\s]+/, \"\").trim() : \"\";\n if (floor.length > 0) {\n return floor;\n }\n throw new PenvError(\n \"ENGINE_PEER_UNREADABLE\",\n `penv could not read its own \\`${SCHEMA_PACKAGE}\\` peer range, so it cannot say which ${SCHEMA_PACKAGE} this project needs`,\n `Reinstall penv, then run \\`penv init\\` again.`,\n );\n}\n\n/** The engine's own manifest, or `undefined` when it cannot be read. */\nfunction ownManifest(): Record<string, unknown> | undefined {\n try {\n const parsed: unknown = JSON.parse(\n readFileSync(new URL(\"../package.json\", import.meta.url), \"utf8\"),\n );\n return parsed !== null && typeof parsed === \"object\" && !Array.isArray(parsed)\n ? (parsed as Record<string, unknown>)\n : undefined;\n } catch {\n // The callers refuse: a version penv guessed would pin a project's\n // dependency to something nobody chose.\n return undefined;\n }\n}\n\n/** The package manager this project already uses: its lockfile, then what it declares, then npm. */\nexport function detectPackageManager(root: string): PackageManager {\n for (const [manager, lockfile] of LOCKFILES) {\n if (existsSync(join(root, lockfile))) {\n return manager;\n }\n }\n return declaredManager(root) ?? \"npm\";\n}\n\n/** `\"packageManager\": \"pnpm@9.1.0\"` — corepack's field, and a project's own answer. */\nfunction declaredManager(root: string): PackageManager | undefined {\n const declared = manifestOf(root)?.packageManager;\n if (typeof declared !== \"string\") {\n return undefined;\n }\n const name = declared.split(\"@\")[0];\n return name === \"pnpm\" || name === \"npm\" || name === \"yarn\" || name === \"bun\" ? name : undefined;\n}\n\nfunction manifestOf(root: string): Record<string, unknown> | undefined {\n const file = join(root, \"package.json\");\n if (!existsSync(file)) {\n return undefined;\n }\n try {\n const manifest: unknown = JSON.parse(readFileSync(file, \"utf8\"));\n return manifest !== null && typeof manifest === \"object\" && !Array.isArray(manifest)\n ? (manifest as Record<string, unknown>)\n : undefined;\n } catch {\n return undefined;\n }\n}\n\n/** What one `package.json` says about a package today, and which block says it. */\ninterface Declaration {\n readonly version: string;\n /** True when it sits in `devDependencies` — where an install has to leave it. */\n readonly dev: boolean;\n}\n\nfunction declaredIn(dir: string, name: string): Declaration | undefined {\n const manifest = manifestOf(dir);\n for (const field of [\"dependencies\", \"devDependencies\"] as const) {\n const block: unknown = manifest?.[field];\n if (block !== null && typeof block === \"object\" && !Array.isArray(block)) {\n const version: unknown = (block as Record<string, unknown>)[name];\n if (typeof version === \"string\") {\n return { version, dev: field === \"devDependencies\" };\n }\n }\n }\n return undefined;\n}\n\n/** True when `root` is the root of a pnpm workspace, which is what `-w` is for. */\nexport function isPnpmWorkspaceRoot(root: string): boolean {\n return existsSync(join(root, PNPM_WORKSPACE)) && existsSync(join(root, \"package.json\"));\n}\n\n/** The `packages:` list from `pnpm-workspace.yaml`, block or flow form. */\nfunction workspaceGlobs(root: string): string[] {\n let text: string;\n try {\n text = readFileSync(join(root, PNPM_WORKSPACE), \"utf8\");\n } catch {\n return [];\n }\n const unquote = (raw: string): string => raw.replace(/^['\"]|['\"]$/g, \"\").trim();\n const globs: string[] = [];\n let inside = false;\n for (const line of text.split(/\\r?\\n/)) {\n const flow = /^packages:\\s*\\[(.*)\\]\\s*$/.exec(line);\n if (flow?.[1] !== undefined) {\n return flow[1]\n .split(\",\")\n .map(unquote)\n .filter((glob) => glob !== \"\");\n }\n if (/^packages:\\s*$/.test(line)) {\n inside = true;\n continue;\n }\n if (!inside) {\n continue;\n }\n const item = /^\\s+-\\s*(.+?)\\s*$/.exec(line);\n if (item?.[1] !== undefined) {\n globs.push(unquote(item[1]));\n continue;\n }\n if (line.trim() !== \"\" && !line.trimStart().startsWith(\"#\")) {\n break;\n }\n }\n return globs;\n}\n\nfunction directoriesIn(dir: string): string[] {\n try {\n return readdirSync(dir, { withFileTypes: true })\n .filter((entry) => entry.isDirectory() && entry.name !== \"node_modules\")\n .map((entry) => join(dir, entry.name));\n } catch {\n return [];\n }\n}\n\nfunction isDirectory(path: string): boolean {\n try {\n return statSync(path).isDirectory();\n } catch {\n return false;\n }\n}\n\n/** `packages/*` and `apps/**` against the filesystem, one path segment at a time. */\nfunction expandGlob(root: string, glob: string): string[] {\n let dirs = [root];\n for (const segment of glob.split(\"/\").filter((part) => part !== \"\" && part !== \".\")) {\n const next: string[] = [];\n for (const dir of dirs) {\n if (segment === \"**\") {\n const stack = [dir];\n while (stack.length > 0) {\n const current = stack.pop() as string;\n next.push(current);\n stack.push(...directoriesIn(current));\n }\n continue;\n }\n if (segment.includes(\"*\")) {\n const pattern = new RegExp(\n `^${segment.replace(/[.+^${}()|[\\]\\\\]/g, \"\\\\$&\").replace(/\\*/g, \"[^/]*\")}$`,\n );\n next.push(\n ...directoriesIn(dir).filter((child) => pattern.test(child.slice(dir.length + 1))),\n );\n continue;\n }\n const candidate = join(dir, segment);\n if (isDirectory(candidate)) {\n next.push(candidate);\n }\n }\n dirs = next;\n }\n return dirs;\n}\n\n/**\n * Every workspace package that declares `@penvhq/penv` itself, root excluded.\n *\n * Only the ones that already declare it: penv moves a dependency a package\n * chose, and adding one to a package that never asked for it is a different\n * decision than the one being consented to.\n */\nfunction workspaceMembers(root: string, name: string): string[] {\n if (!isPnpmWorkspaceRoot(root)) {\n return [];\n }\n const globs = workspaceGlobs(root);\n const excluded = globs\n .filter((glob) => glob.startsWith(\"!\"))\n .flatMap((glob) => expandGlob(root, glob.slice(1)));\n const found = new Set<string>();\n for (const glob of globs.filter((entry) => !entry.startsWith(\"!\"))) {\n for (const dir of expandGlob(root, glob)) {\n if (dir !== root && !excluded.includes(dir) && declaredIn(dir, name) !== undefined) {\n found.add(dir);\n }\n }\n }\n return [...found].sort();\n}\n\n/** The path a diff shows for one of them, in the spelling every penv path uses. */\nfunction manifestPathOf(root: string, dir: string): string {\n const within = relative(root, dir)\n .split(sep)\n .filter((part) => part !== \"\");\n return [...within, \"package.json\"].join(\"/\");\n}\n\nfunction addCommand(\n manager: PackageManager,\n options: { readonly filter?: string; readonly workspaceRoot: boolean; readonly dev: boolean },\n specs: readonly string[],\n): string[] {\n const [bin, verb] = ADD[manager];\n return [\n bin,\n ...(options.filter === undefined ? [] : [\"--filter\", options.filter]),\n verb,\n ...(options.workspaceRoot ? [WORKSPACE_ROOT_FLAG] : []),\n EXACT[manager],\n ...(options.dev ? [DEV[manager]] : []),\n ...specs,\n ];\n}\n\nfunction stepFor(\n manager: PackageManager,\n manifest: string,\n packages: readonly InstallPackage[],\n options: { readonly filter?: string; readonly workspaceRoot: boolean; readonly dev: boolean },\n): InstallStep {\n const pending = packages.filter((entry) => !entry.satisfied);\n const specs = (pending.length === 0 ? packages : pending).map(\n (entry) => `${entry.name}@${entry.version}`,\n );\n return {\n manifest,\n packages,\n command: addCommand(manager, options, specs),\n satisfied: pending.length === 0,\n };\n}\n\nexport function planInstall(root: string, version: string = engineVersion()): InstallPlan {\n const manager = detectPackageManager(root);\n const lockfile = LOCKFILES.find(\n ([name, file]) => name === manager && existsSync(join(root, file)),\n )?.[1];\n const workspaceRoot = manager === \"pnpm\" && isPnpmWorkspaceRoot(root);\n\n const runtime = declaredIn(root, RUNTIME_PACKAGE);\n const zod = declaredIn(root, SCHEMA_PACKAGE);\n const packages: InstallPackage[] = [\n {\n name: RUNTIME_PACKAGE,\n version,\n ...(runtime === undefined ? {} : { declared: runtime.version }),\n satisfied: runtime?.version === version,\n },\n {\n name: SCHEMA_PACKAGE,\n version: schemaPackageVersion(),\n ...(zod === undefined ? {} : { declared: zod.version }),\n // Any declared zod counts: which zod a project uses is the project's\n // decision, and penv is here to make sure there is one, not to move it.\n satisfied: zod !== undefined,\n },\n ];\n\n // The block a package chose is the block penv writes back to — but only when\n // every package this step installs lives there, since one command names one.\n const pending = packages.filter((entry) => !entry.satisfied);\n const steps: InstallStep[] = [\n stepFor(manager, \"package.json\", packages, {\n workspaceRoot,\n dev:\n runtime?.dev === true &&\n pending.every((entry) => entry.name === RUNTIME_PACKAGE) &&\n pending.length > 0,\n }),\n ];\n for (const dir of workspaceMembers(root, RUNTIME_PACKAGE)) {\n const declared = declaredIn(dir, RUNTIME_PACKAGE) as Declaration;\n steps.push(\n stepFor(\n manager,\n manifestPathOf(root, dir),\n [\n {\n name: RUNTIME_PACKAGE,\n version,\n declared: declared.version,\n satisfied: declared.version === version,\n },\n ],\n {\n filter: `./${relative(root, dir).split(sep).join(\"/\")}`,\n workspaceRoot: false,\n dev: declared.dev,\n },\n ),\n );\n }\n\n return {\n root,\n manager,\n steps,\n ...(lockfile === undefined ? {} : { lockfile }),\n satisfied: steps.every((step) => step.satisfied),\n };\n}\n\nfunction describe(entry: InstallPackage): string {\n return `${entry.name} ${entry.version}`;\n}\n\n/** What this plan actually installs, once per package however many files declare it. */\nexport function installedPackages(plan: InstallPlan): readonly InstallPackage[] {\n const pending = plan.steps.flatMap((step) => step.packages).filter((entry) => !entry.satisfied);\n return [...new Map(pending.map((entry) => [entry.name, entry])).values()];\n}\n\n/** The lines one `package.json` contributes to the diff. */\nfunction renderStep(step: InstallStep): string[] {\n const pending = step.packages.filter((entry) => !entry.satisfied);\n const added = pending.filter((entry) => entry.declared === undefined);\n const replaced = pending.filter((entry) => entry.declared !== undefined);\n return [\n step.manifest,\n ...(added.length === 0\n ? []\n : [\n ' + \"dependencies\": {',\n ...added.map((entry) => ` + \"${entry.name}\": \"${entry.version}\"`),\n \" + }\",\n ]),\n ...replaced.flatMap((entry) => [\n ` - \"${entry.name}\": \"${entry.declared}\"`,\n ` + \"${entry.name}\": \"${entry.version}\"`,\n ]),\n ];\n}\n\n/**\n * The change, as it will appear in the diff — the whole point of showing it is\n * that the reader recognises their own file, so these are the `package.json`\n * lines that land and the lockfile that gets rewritten, not a summary of both.\n *\n * In a workspace that is more than one file, and the commands underneath are the\n * ones that run: a \"Run with:\" line the reader cannot paste is worse than none.\n */\nexport function renderInstallPlan(plan: InstallPlan): string[] {\n if (plan.satisfied) {\n const packages = plan.steps[0]?.packages ?? [];\n return [\n `package.json already has ${packages.map(describe).join(\" and \")} — nothing to install.`,\n ];\n }\n const pending = plan.steps.filter((step) => !step.satisfied);\n const [first, ...rest] = pending.map((step) => step.command.join(\" \"));\n const landing = installedPackages(plan).map((entry) => ` + ${entry.name}@${entry.version}`);\n return [\n ...pending.flatMap(renderStep),\n ...(plan.lockfile === undefined ? [] : [plan.lockfile, ...landing]),\n \"\",\n `Run with: ${first ?? \"\"}`,\n ...rest.map((command) => ` then ${command}`),\n ];\n}\n\n/**\n * The real install: the project's own package manager, started the way any other\n * child is (`.cmd` shims on Windows included), with its output the user's to see.\n *\n * Every step runs from the project root — `-w` and `--filter` are how a workspace\n * says which `package.json` it means, so the directory never changes.\n */\nexport const installWithPackageManager: InstallRuntime = async (plan) => {\n for (const step of plan.steps) {\n if (step.satisfied) {\n continue;\n }\n const child = startChild({\n command: step.command,\n env: process.env as Record<string, string>,\n cwd: plan.root,\n purpose: `install ${step.packages.map(describe).join(\" and \")} in ${step.manifest}`,\n });\n const ended = await child.ended;\n if (ended.exitCode !== 0 || ended.signal !== null) {\n throw installFailed(plan, step);\n }\n }\n};\n\n/**\n * What to do about a package manager that refused.\n *\n * Never the command that just failed: the one remediation guaranteed not to work\n * is the one the reader already ran. The manager said why, on their screen, and\n * nothing was migrated — so the answer is that line and a second `penv init`.\n */\nexport function installFailed(plan: InstallPlan, step: InstallStep): PenvError {\n return new PenvError(\n \"INIT_INSTALL_FAILED\",\n `${step.command.join(\" \")} did not finish, so penv migrated nothing`,\n `Read what ${plan.manager} printed above — it names what it refused. Fix that and run this ` +\n \"command again; your dotenv files are exactly where they were.\",\n );\n}\n","/**\n * Starting someone else's command, opaquely.\n *\n * `penv run -- <command>` starts exactly what follows `--`: the argument\n * boundaries the shell already worked out are handed to the operating system\n * untouched, stdio is the parent's, and the child's exit code and terminating\n * signal come back out. penv never parses the command, never rebuilds a command\n * line from it, never wraps it in a shell — a shell would re-split what the user\n * already split, and `penv run -- node -e \"console.log(1 > 2)\"` would redirect to\n * a file called `2`.\n *\n * Windows is the one place where \"hand it to the operating system\" needs help.\n * `pnpm`, `next` and every other node-installed tool are `.cmd` shims there, and\n * Node refuses to execute one without a shell. So a `.cmd`/`.bat` target — and\n * only that — is started through `cmd.exe /d /s /c` with\n * `windowsVerbatimArguments`, building the one command line cmd will accept and\n * escaping every argument so that cmd hands the child the same bytes penv was\n * given. Everything else spawns directly, on every platform.\n */\n\nimport { spawn } from \"node:child_process\";\nimport { existsSync, statSync } from \"node:fs\";\nimport { delimiter, isAbsolute, join, win32 } from \"node:path\";\nimport { PenvError } from \"@penvhq/core\";\n\n/** How a child ended. Exactly one of these is meaningful, and both are forwarded. */\nexport interface ChildResult {\n /** The child's own exit code, or 1 when a signal ended it. */\n readonly exitCode: number;\n /** The signal that ended the child, when one did. */\n readonly signal: NodeJS.Signals | null;\n}\n\nexport interface ChildInvocation {\n /** The command exactly as it followed `--`: the executable, then its arguments. */\n readonly command: readonly string[];\n readonly env: Record<string, string>;\n readonly cwd: string;\n /**\n * What penv is starting this on its own behalf to do — `init`'s dependency\n * install. Absent means the command is the user's, from after `--`, and the\n * two failures have opposite remedies: one is about what they typed, the other\n * about a program penv chose to run.\n */\n readonly purpose?: string;\n}\n\n/** A started child: how it ends, and the one thing a wrapper may do to it. */\nexport interface ChildHandle {\n /** Resolves when the child has ended, however it ended. */\n readonly ended: Promise<ChildResult>;\n /** Asks the child to stop — what `--watch` does before it starts the next one. */\n kill(signal?: NodeJS.Signals): void;\n}\n\n/** The seam `run` starts a child through — replaced in tests that assert what it was given. */\nexport type StartChild = (invocation: ChildInvocation) => ChildHandle;\n\n/** The signals a wrapper must pass through rather than absorb. */\nconst FORWARDED: readonly NodeJS.Signals[] = [\"SIGINT\", \"SIGTERM\", \"SIGHUP\", \"SIGBREAK\"];\n\nexport const startChild: StartChild = (invocation) => {\n const [executable, ...args] = invocation.command;\n if (executable === undefined) {\n throw noCommand();\n }\n\n const target = resolveTarget(executable, args, invocation.env);\n const child = spawn(target.file, target.args, {\n cwd: invocation.cwd,\n env: invocation.env,\n stdio: \"inherit\",\n ...(target.verbatim ? { windowsVerbatimArguments: true } : {}),\n });\n\n // Forwarded rather than handled: penv is a wrapper, and a Ctrl-C belongs to\n // the program the user is looking at. The child decides what to do with it,\n // and its answer comes back as the signal below.\n const forward = new Map<NodeJS.Signals, () => void>();\n for (const signal of FORWARDED) {\n const handler = (): void => {\n child.kill(signal);\n };\n forward.set(signal, handler);\n process.on(signal, handler);\n }\n const release = (): void => {\n for (const [signal, handler] of forward) {\n process.off(signal, handler);\n }\n };\n\n const ended = new Promise<ChildResult>((resolve, reject) => {\n child.on(\"error\", (cause) => {\n release();\n reject(cannotStart(executable, cause, invocation.purpose));\n });\n child.on(\"exit\", (code, signal) => {\n release();\n resolve({ exitCode: code ?? 1, signal });\n });\n });\n\n return {\n ended,\n kill(signal) {\n child.kill(signal);\n },\n };\n};\n\nexport function noCommand(): PenvError {\n return new PenvError(\n \"RUN_NO_COMMAND\",\n \"`penv run` was given no command to start\",\n \"Put the command after `--`, e.g. `penv run -- pnpm dev`.\",\n );\n}\n\nfunction cannotStart(executable: string, cause: unknown, purpose: string | undefined): PenvError {\n const detail = cause instanceof Error ? cause.message : String(cause);\n if (purpose !== undefined) {\n return new PenvError(\n \"PENV_COMMAND_NOT_STARTED\",\n `penv could not start \\`${executable}\\` to ${purpose}: ${detail}`,\n `Check that \\`${executable}\\` runs on its own — penv starts it the way your shell does, so it has to be on PATH. Nothing was changed.`,\n );\n }\n return new PenvError(\n \"RUN_COMMAND_NOT_STARTED\",\n `\\`${executable}\\` could not be started: ${detail}`,\n `Check the command after \\`--\\` runs on its own — \\`${executable}\\` has to be on PATH, exactly as it is spelled here.`,\n );\n}\n\ninterface SpawnTarget {\n readonly file: string;\n readonly args: readonly string[];\n /** True when the args are one pre-built command line rather than a list. */\n readonly verbatim: boolean;\n}\n\nfunction resolveTarget(\n executable: string,\n args: readonly string[],\n env: Readonly<Record<string, string | undefined>>,\n): SpawnTarget {\n if (process.platform !== \"win32\") {\n return { file: executable, args, verbatim: false };\n }\n const resolved = findExecutable(executable, env);\n if (resolved === undefined || !/\\.(cmd|bat)$/i.test(resolved)) {\n return { file: resolved ?? executable, args, verbatim: false };\n }\n return {\n file: env.ComSpec ?? \"cmd.exe\",\n args: [\"/d\", \"/s\", \"/c\", `\"${cmdCommandLine(resolved, args)}\"`],\n verbatim: true,\n };\n}\n\n/** A package-manager shim, which re-invokes cmd on its own way through. */\nconst SHIM = /(?:^|\\\\)node_modules\\\\\\.bin\\\\[^\\\\]+\\.cmd$/i;\n\n/**\n * The one command line cmd.exe is handed, escaped so the child receives the\n * bytes penv was given.\n *\n * The path is normalized first and *then* judged: `./node_modules/.bin/next.cmd`\n * and `.\\node_modules\\.bin\\next.cmd` are the same shim, and deciding on the\n * un-normalized spelling would escape a forward-slash invocation once while cmd\n * expands it twice — so an argument holding `&` would run as a command inside\n * the shim's second round. Windows' own separator, whatever this process runs\n * on, because this line is only ever read by cmd.exe.\n */\nexport function cmdCommandLine(resolved: string, args: readonly string[]): string {\n const command = win32.normalize(resolved);\n const shim = SHIM.test(command);\n return [escapeCommand(command), ...args.map((argument) => escapeArgument(argument, shim))].join(\n \" \",\n );\n}\n\n/**\n * The extensions a name is tried with, in the order the platform's own launcher\n * tries them.\n *\n * On Windows PATHEXT leads and the bare name comes last, because the bare name\n * is almost never what Windows would run: `pnpm`, `npx` and every\n * `node_modules/.bin` tool ship an extensionless POSIX shell script *beside*\n * their `.CMD` shim, in the same directory. Trying the empty extension first\n * matched that script, which is not executable by CreateProcess and is not a\n * `.cmd`, so the wrapper below was skipped and the spawn failed with ENOENT.\n * Everywhere else there are no extensions at all.\n */\nfunction extensions(\n env: Readonly<Record<string, string | undefined>>,\n platform: NodeJS.Platform,\n): string[] {\n if (platform !== \"win32\") {\n return [\"\"];\n }\n const declared = env.PATHEXT ?? \".COM;.EXE;.BAT;.CMD\";\n return [...declared.split(\";\").filter((extension) => extension.length > 0), \"\"];\n}\n\n/**\n * What the shell would have run, found the way the shell finds it: the name as\n * given if it carries a path, else each PATH directory, each with each\n * executable extension.\n *\n * `platform` is a parameter so the ordering above is testable on either kind of\n * machine — it is the whole behavior, and it differs by platform.\n */\nexport function findExecutable(\n executable: string,\n env: Readonly<Record<string, string | undefined>>,\n platform: NodeJS.Platform = process.platform,\n): string | undefined {\n const candidates = extensions(env, platform);\n const isFile = (path: string): boolean => existsSync(path) && statSync(path).isFile();\n\n if (executable.includes(\"/\") || executable.includes(\"\\\\\") || isAbsolute(executable)) {\n return candidates.map((extension) => executable + extension).find(isFile);\n }\n const path = env.PATH ?? env.Path ?? \"\";\n for (const directory of path.split(delimiter).filter((entry) => entry.length > 0)) {\n const hit = candidates.map((extension) => join(directory, executable + extension)).find(isFile);\n if (hit !== undefined) {\n return hit;\n }\n }\n return undefined;\n}\n\n/** The characters cmd.exe expands before the program ever sees them. */\nconst CMD_METACHARACTERS = /([()\\][%!^\"`<>&|;, *?])/g;\n\n/** The command's own path: cmd's metacharacters escaped, and no quotes to confuse it. */\nfunction escapeCommand(command: string): string {\n return command.replace(CMD_METACHARACTERS, \"^$1\");\n}\n\n/**\n * One argument, quoted so the child's runtime splits it exactly where penv was\n * given it, then escaped so cmd.exe passes those quotes through instead of\n * acting on them.\n */\nfunction escapeArgument(argument: string, doubleEscape: boolean): string {\n const quoted = `\"${argument.replace(/(\\\\*)\"/g, '$1$1\\\\\"').replace(/(\\\\*)$/, \"$1$1\")}\"`;\n const escaped = quoted.replace(CMD_METACHARACTERS, \"^$1\");\n return doubleEscape ? escaped.replace(CMD_METACHARACTERS, \"^$1\") : escaped;\n}\n"],"mappings":"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","names":["import_node_fs","import_node_path","import_core","path"]}
|
package/dist/install.d.cts
CHANGED
|
@@ -21,6 +21,14 @@ import { PenvError } from '@penvhq/core';
|
|
|
21
21
|
* has to get right is the plan, the consent, and the refusal when the install
|
|
22
22
|
* does not happen.
|
|
23
23
|
*
|
|
24
|
+
* A plan is a list of steps, because in a workspace "the project's dependency"
|
|
25
|
+
* is plural. pnpm refuses a bare `add` at a workspace root (`-w` is how you say
|
|
26
|
+
* you meant the root), and a workspace package that declares `@penvhq/penv`
|
|
27
|
+
* itself is a second copy of the very version the manifest pins — one repository
|
|
28
|
+
* ran the 0.8 bridge under a 0.11 pin for three releases because nothing looked
|
|
29
|
+
* below the root. So every `package.json` that declares it moves, under one
|
|
30
|
+
* consent, and the commands shown are the ones that run.
|
|
31
|
+
*
|
|
24
32
|
* Two commands write that dependency line: `penv init`, which is the engine's,
|
|
25
33
|
* and `penv upgrade`, which is the launcher's. This module is published at
|
|
26
34
|
* `@penvhq/cli/install` so the launcher reaches it without loading the command
|
|
@@ -42,16 +50,25 @@ interface InstallPackage {
|
|
|
42
50
|
/** True when this project already has it — nothing to install for this one. */
|
|
43
51
|
readonly satisfied: boolean;
|
|
44
52
|
}
|
|
53
|
+
/** One `package.json` the install rewrites, and the command that rewrites it. */
|
|
54
|
+
interface InstallStep {
|
|
55
|
+
/** The file the diff names — `package.json`, or a workspace package's path to it. */
|
|
56
|
+
readonly manifest: string;
|
|
57
|
+
/** Everything this file needs, in the order the diff shows them. */
|
|
58
|
+
readonly packages: readonly InstallPackage[];
|
|
59
|
+
/** The command, argv-shaped — run from the project root, whichever file it writes. */
|
|
60
|
+
readonly command: readonly string[];
|
|
61
|
+
/** True when this file already declares every one of them. */
|
|
62
|
+
readonly satisfied: boolean;
|
|
63
|
+
}
|
|
45
64
|
interface InstallPlan {
|
|
46
65
|
readonly root: string;
|
|
47
66
|
readonly manager: PackageManager;
|
|
48
|
-
/**
|
|
49
|
-
readonly
|
|
50
|
-
/** The command, argv-shaped — what runs, and what a refusal tells the user to run. */
|
|
51
|
-
readonly command: readonly string[];
|
|
67
|
+
/** The root's `package.json` first, then every workspace package that declares the runtime one. */
|
|
68
|
+
readonly steps: readonly InstallStep[];
|
|
52
69
|
/** The lockfile the manager will rewrite, when the project has one. */
|
|
53
70
|
readonly lockfile?: string;
|
|
54
|
-
/** True when every
|
|
71
|
+
/** True when every step is already satisfied — nothing to install. */
|
|
55
72
|
readonly satisfied: boolean;
|
|
56
73
|
}
|
|
57
74
|
/** Runs an install plan, or throws. Replaced in tests; never spawns there. */
|
|
@@ -75,18 +92,35 @@ declare function engineVersion(): string;
|
|
|
75
92
|
declare function schemaPackageVersion(): string;
|
|
76
93
|
/** The package manager this project already uses: its lockfile, then what it declares, then npm. */
|
|
77
94
|
declare function detectPackageManager(root: string): PackageManager;
|
|
95
|
+
/** True when `root` is the root of a pnpm workspace, which is what `-w` is for. */
|
|
96
|
+
declare function isPnpmWorkspaceRoot(root: string): boolean;
|
|
78
97
|
declare function planInstall(root: string, version?: string): InstallPlan;
|
|
98
|
+
/** What this plan actually installs, once per package however many files declare it. */
|
|
99
|
+
declare function installedPackages(plan: InstallPlan): readonly InstallPackage[];
|
|
79
100
|
/**
|
|
80
101
|
* The change, as it will appear in the diff — the whole point of showing it is
|
|
81
102
|
* that the reader recognises their own file, so these are the `package.json`
|
|
82
103
|
* lines that land and the lockfile that gets rewritten, not a summary of both.
|
|
104
|
+
*
|
|
105
|
+
* In a workspace that is more than one file, and the commands underneath are the
|
|
106
|
+
* ones that run: a "Run with:" line the reader cannot paste is worse than none.
|
|
83
107
|
*/
|
|
84
108
|
declare function renderInstallPlan(plan: InstallPlan): string[];
|
|
85
109
|
/**
|
|
86
110
|
* The real install: the project's own package manager, started the way any other
|
|
87
111
|
* child is (`.cmd` shims on Windows included), with its output the user's to see.
|
|
112
|
+
*
|
|
113
|
+
* Every step runs from the project root — `-w` and `--filter` are how a workspace
|
|
114
|
+
* says which `package.json` it means, so the directory never changes.
|
|
88
115
|
*/
|
|
89
116
|
declare const installWithPackageManager: InstallRuntime;
|
|
90
|
-
|
|
117
|
+
/**
|
|
118
|
+
* What to do about a package manager that refused.
|
|
119
|
+
*
|
|
120
|
+
* Never the command that just failed: the one remediation guaranteed not to work
|
|
121
|
+
* is the one the reader already ran. The manager said why, on their screen, and
|
|
122
|
+
* nothing was migrated — so the answer is that line and a second `penv init`.
|
|
123
|
+
*/
|
|
124
|
+
declare function installFailed(plan: InstallPlan, step: InstallStep): PenvError;
|
|
91
125
|
|
|
92
|
-
export { type InstallPackage, type InstallPlan, type InstallRuntime, type PackageManager, RUNTIME_PACKAGE, SCHEMA_PACKAGE, detectPackageManager, engineVersion, installFailed, installWithPackageManager, planInstall, renderInstallPlan, schemaPackageVersion };
|
|
126
|
+
export { type InstallPackage, type InstallPlan, type InstallRuntime, type InstallStep, type PackageManager, RUNTIME_PACKAGE, SCHEMA_PACKAGE, detectPackageManager, engineVersion, installFailed, installWithPackageManager, installedPackages, isPnpmWorkspaceRoot, planInstall, renderInstallPlan, schemaPackageVersion };
|