@penvhq/launcher 0.10.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.js CHANGED
@@ -5,7 +5,7 @@ import {
5
5
  httpFetcher,
6
6
  nodeSpawner,
7
7
  runLauncher
8
- } from "./chunk-BNUZ52VN.js";
8
+ } from "./chunk-A5LJQPYS.js";
9
9
 
10
10
  // src/bin.ts
11
11
  function readLine() {
@@ -170,6 +170,8 @@ import {
170
170
  PenvError
171
171
  } from "@penvhq/core";
172
172
  var INSTALL_COMMAND = "penv install";
173
+ var UPGRADE_COMMAND = "penv upgrade";
174
+ var YES_FLAG = "--yes";
173
175
  var MIN_PACKAGE_AGE_DAYS = 7;
174
176
  var TRUST_YOUNG_FLAG = "--trust-young";
175
177
  function addCommand(name, version, extra) {
@@ -275,7 +277,7 @@ var AddFlagError = class extends PenvError {
275
277
  super(
276
278
  "PENV_ADD_FLAG",
277
279
  `\`penv add\` does not understand \`${flag}\``,
278
- `Run \`penv add <package>\` with \`${TRUST_YOUNG_FLAG}\`, \`--registry <url>\`, or \`${LOCAL_FLAG}\` \u2014 those are the three it takes.`
280
+ `Run \`penv add <package>\` with \`${TRUST_YOUNG_FLAG}\`, \`--registry <url>\`, \`${LOCAL_FLAG}\`, or \`${YES_FLAG}\` \u2014 those are the four it takes.`
279
281
  );
280
282
  }
281
283
  };
@@ -352,11 +354,11 @@ var OfficialRegistryError = class extends PenvError {
352
354
  };
353
355
  var RegistryUnreadableError = class extends PenvError {
354
356
  name = "RegistryUnreadableError";
355
- constructor(name, url, detail) {
357
+ constructor(name, url, detail, retry = addCommand(name)) {
356
358
  super(
357
359
  "PENV_REGISTRY_UNREADABLE",
358
360
  `Reading ${name} from ${url} failed: ${detail}`,
359
- `Run \`${addCommand(name)}\` again when the registry is reachable.`
361
+ `Run \`${retry}\` again when the registry is reachable.`
360
362
  );
361
363
  }
362
364
  };
@@ -372,11 +374,11 @@ var PackageUnknownError = class extends PenvError {
372
374
  };
373
375
  var VersionUnknownError = class extends PenvError {
374
376
  name = "VersionUnknownError";
375
- constructor(name, version, url) {
377
+ constructor(name, version, url, retry = addCommand(name)) {
376
378
  super(
377
379
  "PENV_VERSION_UNKNOWN",
378
380
  `${url} publishes no ${version} of ${name}`,
379
- `Run \`${addCommand(name)}\` to take the version \`latest\` points at.`
381
+ `Run \`${retry}\` to take the version \`latest\` points at.`
380
382
  );
381
383
  }
382
384
  };
@@ -421,13 +423,13 @@ var AddNoDownloadError = class extends PenvError {
421
423
  );
422
424
  }
423
425
  };
424
- var AddNotInteractiveError = class extends PenvError {
425
- name = "AddNotInteractiveError";
426
- constructor(name) {
426
+ var AddTrustUnattendedError = class extends PenvError {
427
+ name = "AddTrustUnattendedError";
428
+ constructor(name, ceremony) {
427
429
  super(
428
- "PENV_ADD_NOT_INTERACTIVE",
429
- `Adding ${name} rewrites ${MANIFEST_PATH}, and this run has nobody to decide that`,
430
- `Run \`${addCommand(name)}\` from a terminal and commit what it writes. In CI, run \`${INSTALL_COMMAND}\` \u2014 it installs the versions the committed manifest already pins.`
430
+ "PENV_ADD_TRUST_UNATTENDED",
431
+ `Adding ${name} records ${ceremony}, and this run has nobody to write it`,
432
+ `Run \`${addCommand(name)}\` from a terminal, without \`${YES_FLAG}\` \u2014 the trust block is a line only a person can write. In CI, run \`${INSTALL_COMMAND}\`: it installs the versions the committed manifest already pins.`
431
433
  );
432
434
  }
433
435
  };
@@ -501,6 +503,66 @@ var EnginePinMismatchError = class extends PenvError {
501
503
  );
502
504
  }
503
505
  };
506
+ var UpgradeSubjectError = class extends PenvError {
507
+ name = "UpgradeSubjectError";
508
+ constructor() {
509
+ super(
510
+ "PENV_UPGRADE_SUBJECT",
511
+ "`penv upgrade` takes one version, or none",
512
+ `Run \`${UPGRADE_COMMAND}\` to move to whatever \`latest\` points at, or \`${UPGRADE_COMMAND} 0.9.6\` to move to an exact release.`
513
+ );
514
+ }
515
+ };
516
+ var UpgradeFlagError = class extends PenvError {
517
+ name = "UpgradeFlagError";
518
+ constructor(flag) {
519
+ super(
520
+ "PENV_UPGRADE_FLAG",
521
+ `\`${UPGRADE_COMMAND}\` does not understand \`${flag}\``,
522
+ `Run \`${UPGRADE_COMMAND} [version]\` with \`${YES_FLAG}\` \u2014 that is the one flag it takes.`
523
+ );
524
+ }
525
+ };
526
+ var UpgradeNoDownloadError = class extends PenvError {
527
+ name = "UpgradeNoDownloadError";
528
+ constructor() {
529
+ super(
530
+ "PENV_UPGRADE_NO_DOWNLOAD",
531
+ `Upgrading means reading the registry for the version and integrity to pin, and \`--no-download\` says this run does not`,
532
+ `Run \`${UPGRADE_COMMAND}\` without \`--no-download\`. Nothing was fetched or written.`
533
+ );
534
+ }
535
+ };
536
+ var UpgradeUnattendedError = class extends PenvError {
537
+ name = "UpgradeUnattendedError";
538
+ constructor() {
539
+ super(
540
+ "PENV_UPGRADE_UNATTENDED",
541
+ `Upgrading rewrites ${MANIFEST_PATH} and package.json, and this run has nobody to decide that`,
542
+ `Run \`${UPGRADE_COMMAND} <version> ${YES_FLAG}\` \u2014 unattended, penv moves the pin only to a version you named. In CI, run \`${INSTALL_COMMAND}\`: it installs what the committed manifest already pins.`
543
+ );
544
+ }
545
+ };
546
+ var UpgradeDeclinedError = class extends PenvError {
547
+ name = "UpgradeDeclinedError";
548
+ constructor(version) {
549
+ super(
550
+ "PENV_UPGRADE_DECLINED",
551
+ `${ENGINE_PACKAGE} ${version} was not installed, so ${MANIFEST_PATH} and package.json are unchanged`,
552
+ `Run \`${UPGRADE_COMMAND} ${version}\` again when you want both of them to move.`
553
+ );
554
+ }
555
+ };
556
+ var UpgradeInstallFailedError = class extends PenvError {
557
+ name = "UpgradeInstallFailedError";
558
+ constructor(manager, version) {
559
+ super(
560
+ "PENV_UPGRADE_INSTALL_FAILED",
561
+ `${manager} did not finish moving this project to ${ENGINE_PACKAGE} ${version}, so ${MANIFEST_PATH} still pins the engine it pinned before`,
562
+ `Read what ${manager} printed above \u2014 it names what it refused. Fix that and run \`${UPGRADE_COMMAND} ${version}\` again; the pin and the dependency move together or not at all.`
563
+ );
564
+ }
565
+ };
504
566
  var EngineEntryError = class extends PenvError {
505
567
  name = "EngineEntryError";
506
568
  constructor(name, version, dir) {
@@ -811,7 +873,8 @@ async function fetchRelease(query) {
811
873
  throw new RegistryUnreadableError(
812
874
  query.name,
813
875
  url,
814
- cause instanceof Error ? cause.message : String(cause)
876
+ cause instanceof Error ? cause.message : String(cause),
877
+ query.retry
815
878
  );
816
879
  }
817
880
  let parsed;
@@ -821,7 +884,8 @@ async function fetchRelease(query) {
821
884
  throw new RegistryUnreadableError(
822
885
  query.name,
823
886
  url,
824
- cause instanceof Error ? cause.message : String(cause)
887
+ cause instanceof Error ? cause.message : String(cause),
888
+ query.retry
825
889
  );
826
890
  }
827
891
  const packument = record(parsed);
@@ -837,7 +901,7 @@ async function fetchRelease(query) {
837
901
  const version = record(at(versions, asked)) === void 0 ? text3(at(tags, asked)) ?? asked : asked;
838
902
  const release = record(at(versions, version));
839
903
  if (release === void 0) {
840
- throw new VersionUnknownError(query.name, asked, url);
904
+ throw new VersionUnknownError(query.name, asked, url, query.retry);
841
905
  }
842
906
  const dist = record(at(release, "dist"));
843
907
  const integrity = text3(at(dist, "integrity"));
@@ -939,12 +1003,17 @@ function parseRequest(argv) {
939
1003
  let registry;
940
1004
  let trustYoung = false;
941
1005
  let local = false;
1006
+ let yes = false;
942
1007
  for (let index = 0; index < argv.length; index += 1) {
943
1008
  const token = argv[index] ?? "";
944
1009
  if (token === LOCAL_FLAG) {
945
1010
  local = true;
946
1011
  continue;
947
1012
  }
1013
+ if (token === YES_FLAG) {
1014
+ yes = true;
1015
+ continue;
1016
+ }
948
1017
  if (token === TRUST_YOUNG_FLAG) {
949
1018
  trustYoung = true;
950
1019
  continue;
@@ -984,7 +1053,10 @@ function parseRequest(argv) {
984
1053
  throw new AddLocalFlagError(`\`${TRUST_YOUNG_FLAG}\``);
985
1054
  }
986
1055
  }
987
- return { name, version, registry, trustYoung, local };
1056
+ return { name, version, registry, trustYoung, local, yes };
1057
+ }
1058
+ function ceremonyFor(tier) {
1059
+ return tier === "official" ? void 0 : "who publishes it and why you trust it";
988
1060
  }
989
1061
  function tierOf(request) {
990
1062
  if (request.name.startsWith(OFFICIAL_SCOPE2)) {
@@ -1063,7 +1135,29 @@ function recordExtension(manifestFile, name, entry) {
1063
1135
  };
1064
1136
  return serializeManifest(next);
1065
1137
  }
1066
- async function offerConfigEdit(options, name) {
1138
+ var NONE = /* @__PURE__ */ new Set(["", "none", "no", "n"]);
1139
+ var ALL = /* @__PURE__ */ new Set(["all", "a", "yes", "y"]);
1140
+ function chooseEnvironments(answer, offered) {
1141
+ const normalized = answer.trim().toLowerCase();
1142
+ if (NONE.has(normalized)) {
1143
+ return { chosen: [], unknown: [] };
1144
+ }
1145
+ if (ALL.has(normalized)) {
1146
+ return { chosen: offered, unknown: [] };
1147
+ }
1148
+ const chosen = [];
1149
+ const unknown = [];
1150
+ for (const token of normalized.split(/[\s,]+/).filter((part) => part !== "")) {
1151
+ const match = offered.find((environment) => environment.toLowerCase() === token);
1152
+ if (match === void 0) {
1153
+ unknown.push(token);
1154
+ } else {
1155
+ chosen.push(match);
1156
+ }
1157
+ }
1158
+ return { chosen, unknown };
1159
+ }
1160
+ async function offerConfigEdit(options, name, ask) {
1067
1161
  const { io, root } = options;
1068
1162
  const configFile = findConfigFile(root);
1069
1163
  const line = `Add \`type: ${JSON.stringify(name)}\` to an environment in penv.config.ts.`;
@@ -1078,31 +1172,35 @@ async function offerConfigEdit(options, name) {
1078
1172
  io.out(line);
1079
1173
  return;
1080
1174
  }
1081
- if (entries.every((entry) => entry.type === name)) {
1175
+ const offered = entries.filter((entry) => entry.type !== name).map((entry) => entry.environment);
1176
+ if (offered.length === 0) {
1082
1177
  return;
1083
1178
  }
1084
- if (!io.interactive) {
1179
+ if (!ask) {
1085
1180
  io.out(line);
1086
1181
  return;
1087
1182
  }
1088
- for (const entry of entries) {
1089
- if (entry.type === name) {
1090
- continue;
1091
- }
1092
- if (!await io.confirm(`Point \`${entry.environment}\` at ${name} in ${shown}?`)) {
1093
- continue;
1094
- }
1095
- const next = setProviderType(current, entry.environment, name);
1183
+ const { chosen, unknown } = chooseEnvironments(
1184
+ await io.ask(`Point which of ${offered.join(", ")} at ${name} in ${shown}? all / none / names`),
1185
+ offered
1186
+ );
1187
+ if (unknown.length > 0) {
1188
+ io.out(`${shown} declares no ${unknown.join(", ")}, so nothing was repointed.`);
1189
+ io.out(line);
1190
+ return;
1191
+ }
1192
+ for (const environment of chosen) {
1193
+ const next = setProviderType(current, environment, name);
1096
1194
  if (next === void 0) {
1097
- io.out(`Add \`type: ${JSON.stringify(name)}\` to \`${entry.environment}\` in ${shown}.`);
1195
+ io.out(`Add \`type: ${JSON.stringify(name)}\` to \`${environment}\` in ${shown}.`);
1098
1196
  continue;
1099
1197
  }
1100
1198
  current = next;
1101
1199
  writeFileSync3(configFile, current);
1102
- io.out(`\u2713 ${shown} points \`${entry.environment}\` at ${name}`);
1200
+ io.out(`\u2713 ${shown} points \`${environment}\` at ${name}`);
1103
1201
  }
1104
1202
  }
1105
- async function offerOnboarding(io, name, onboard) {
1203
+ async function offerOnboarding(io, name, onboard, ask) {
1106
1204
  if (onboard === void 0) {
1107
1205
  return void 0;
1108
1206
  }
@@ -1111,7 +1209,7 @@ async function offerOnboarding(io, name, onboard) {
1111
1209
  return void 0;
1112
1210
  }
1113
1211
  const command = `penv ${args.join(" ")}`;
1114
- if (io.interactive && await io.confirm(`Run \`${command}\` now?`)) {
1212
+ if (ask && await io.confirm(`Run \`${command}\` now?`)) {
1115
1213
  return args;
1116
1214
  }
1117
1215
  io.out(`Run \`${command}\` to finish setting ${name} up.`);
@@ -1192,8 +1290,9 @@ async function addLocal(options, request) {
1192
1290
  io.out(`\u2713 ${request.name} resolves from this project and imports \u2014 nothing is pinned`);
1193
1291
  io.out(`\u2713 ${LOCAL_EXTENSIONS_PATH2} records it`);
1194
1292
  io.out(`\u2713 ${declaration} declares its config type`);
1195
- await offerConfigEdit(options, request.name);
1196
- return { onboard: await offerOnboarding(io, request.name, installed.onboard) };
1293
+ const ask = io.interactive && !request.yes;
1294
+ await offerConfigEdit(options, request.name, ask);
1295
+ return { onboard: await offerOnboarding(io, request.name, installed.onboard, ask) };
1197
1296
  }
1198
1297
  async function add(options) {
1199
1298
  const { io, fetcher, home, root, manifestFile } = options;
@@ -1203,11 +1302,13 @@ async function add(options) {
1203
1302
  }
1204
1303
  const tier = tierOf(request);
1205
1304
  const now = (options.now ?? (() => /* @__PURE__ */ new Date()))();
1305
+ const ask = io.interactive && !request.yes;
1206
1306
  if (options.noDownload === true) {
1207
1307
  throw new AddNoDownloadError(request.name);
1208
1308
  }
1209
- if (options.ci === true || !io.interactive) {
1210
- throw new AddNotInteractiveError(request.name);
1309
+ const ceremony = ceremonyFor(tier);
1310
+ if (ceremony !== void 0 && (options.ci === true || !ask)) {
1311
+ throw new AddTrustUnattendedError(request.name, ceremony);
1211
1312
  }
1212
1313
  const release = await fetchRelease({
1213
1314
  name: request.name,
@@ -1249,8 +1350,8 @@ async function add(options) {
1249
1350
  io.out(`\u2713 ${release.name} ${release.version} installed \u2014 ${provenance}`);
1250
1351
  io.out(`\u2713 ${MANIFEST_PATH2} pins it`);
1251
1352
  io.out(`\u2713 ${declaration} declares its config type`);
1252
- await offerConfigEdit(options, release.name);
1253
- return { onboard: await offerOnboarding(io, release.name, installed.onboard) };
1353
+ await offerConfigEdit(options, release.name, ask);
1354
+ return { onboard: await offerOnboarding(io, release.name, installed.onboard, ask) };
1254
1355
  }
1255
1356
 
1256
1357
  // src/delegate.ts
@@ -1299,12 +1400,12 @@ function nodeSpawner() {
1299
1400
  // src/engine.ts
1300
1401
  import { existsSync as existsSync2, readFileSync as readFileSync5 } from "fs";
1301
1402
  import { createRequire as createRequire2 } from "module";
1302
- import { dirname as dirname4, join as join5, resolve as resolve3, sep as sep3 } from "path";
1403
+ import { dirname as dirname4, join as join4, resolve as resolve3, sep as sep3 } from "path";
1303
1404
  import { ENGINE_PACKAGE as ENGINE_PACKAGE2, PenvError as PenvError2 } from "@penvhq/core";
1304
1405
  var ENGINE_BIN = "penv-engine";
1305
1406
  function readPackageManifest(dir) {
1306
1407
  try {
1307
- return JSON.parse(readFileSync5(join5(dir, "package.json"), "utf8"));
1408
+ return JSON.parse(readFileSync5(join4(dir, "package.json"), "utf8"));
1308
1409
  } catch {
1309
1410
  return void 0;
1310
1411
  }
@@ -1369,8 +1470,8 @@ var DEV_PIN_INTEGRITY = "sha512-development-build-not-a-published-release";
1369
1470
  var DEV_PIN_VERSION = "0.0.0-dev";
1370
1471
  var BUNDLED_ENGINE_PIN = {
1371
1472
  package: ENGINE_PACKAGE3,
1372
- version: "0.10.0",
1373
- integrity: "sha512-b+U0c3xWbk1ILtMfPjJW8x5XBxSLAaw/IAOLMBmGvT2uuYv41JPED8PrenEj4s3ZJY5BxpWU9mQcJHEBouaw/Q=="
1473
+ version: "0.12.0",
1474
+ integrity: "sha512-9HUylEIbFq0BGTOW54Vxhkzv8AvuB7H3bImTFP5J+dfglkR7udP2KUsr8b0I2mf45YMTQO5xsYpI5+nhLERGtA=="
1374
1475
  };
1375
1476
  function assertReleasePin(pin) {
1376
1477
  if (pin.integrity === DEV_PIN_INTEGRITY || pin.version === DEV_PIN_VERSION) {
@@ -1387,7 +1488,7 @@ function releaseEnginePin(pin, ranVersion) {
1387
1488
 
1388
1489
  // src/project.ts
1389
1490
  import { existsSync as existsSync3 } from "fs";
1390
- import { dirname as dirname5, join as join6, resolve as resolve4 } from "path";
1491
+ import { dirname as dirname5, join as join5, resolve as resolve4 } from "path";
1391
1492
  import { MANIFEST_PATH as MANIFEST_PATH3, stateDir } from "@penvhq/core";
1392
1493
  var MANIFEST_SEGMENTS = MANIFEST_PATH3.split("/");
1393
1494
  function findUp(cwd, matches) {
@@ -1404,42 +1505,152 @@ function findUp(cwd, matches) {
1404
1505
  }
1405
1506
  }
1406
1507
  function findProject(cwd) {
1407
- const root = findUp(cwd, (dir) => existsSync3(join6(dir, ...MANIFEST_SEGMENTS)));
1408
- return root === void 0 ? void 0 : { root, manifestFile: join6(root, ...MANIFEST_SEGMENTS) };
1508
+ const root = findUp(cwd, (dir) => existsSync3(join5(dir, ...MANIFEST_SEGMENTS)));
1509
+ return root === void 0 ? void 0 : { root, manifestFile: join5(root, ...MANIFEST_SEGMENTS) };
1409
1510
  }
1410
1511
  function findAdoptedRoot(cwd) {
1411
1512
  return findUp(cwd, (dir) => existsSync3(stateDir(dir)));
1412
1513
  }
1413
1514
 
1515
+ // src/upgrade.ts
1516
+ import { writeFileSync as writeFileSync4 } from "fs";
1517
+ import {
1518
+ installWithPackageManager,
1519
+ planInstall,
1520
+ RUNTIME_PACKAGE,
1521
+ renderInstallPlan
1522
+ } from "@penvhq/cli/install";
1523
+ import { ENGINE_PACKAGE as ENGINE_PACKAGE4, MANIFEST_PATH as MANIFEST_PATH4, serializeManifest as serializeManifest2 } from "@penvhq/core";
1524
+ function parseRequest2(argv) {
1525
+ let version;
1526
+ let yes = false;
1527
+ for (const token of argv) {
1528
+ if (token === YES_FLAG) {
1529
+ yes = true;
1530
+ continue;
1531
+ }
1532
+ if (token.startsWith("-")) {
1533
+ throw new UpgradeFlagError(token);
1534
+ }
1535
+ if (version !== void 0 || token === "") {
1536
+ throw new UpgradeSubjectError();
1537
+ }
1538
+ version = token;
1539
+ }
1540
+ return { version, yes };
1541
+ }
1542
+ function renderPinChange(engine, release) {
1543
+ return [
1544
+ MANIFEST_PATH4,
1545
+ ` - "integrity": "${engine.integrity}"`,
1546
+ ` - "version": "${engine.version}"`,
1547
+ ` + "integrity": "${release.integrity}"`,
1548
+ ` + "version": "${release.version}"`
1549
+ ];
1550
+ }
1551
+ function pinnedElsewhere(manifest, version) {
1552
+ return Object.entries(manifest.extensions).filter(([, entry]) => entry.version !== version).map(([name]) => name);
1553
+ }
1554
+ async function consent(options, release, plan, yes) {
1555
+ const { io, manifest } = options;
1556
+ io.out(`${ENGINE_PACKAGE4} ${manifest.engine.version} \u2192 ${release.version}`);
1557
+ io.out("");
1558
+ for (const line of renderPinChange(manifest.engine, release)) {
1559
+ io.out(line);
1560
+ }
1561
+ for (const line of renderInstallPlan(plan)) {
1562
+ io.out(line);
1563
+ }
1564
+ if (yes) {
1565
+ return;
1566
+ }
1567
+ if (!await io.confirm(`Move this project to ${release.version}?`)) {
1568
+ throw new UpgradeDeclinedError(release.version);
1569
+ }
1570
+ }
1571
+ async function upgrade(options) {
1572
+ const { io, fetcher, home, root, manifest, manifestFile } = options;
1573
+ const request = parseRequest2(options.argv);
1574
+ if (options.noDownload === true) {
1575
+ throw new UpgradeNoDownloadError();
1576
+ }
1577
+ if ((options.ci === true || !io.interactive) && !(request.yes && request.version !== void 0)) {
1578
+ throw new UpgradeUnattendedError();
1579
+ }
1580
+ const release = await fetchRelease({
1581
+ name: ENGINE_PACKAGE4,
1582
+ ...request.version === void 0 ? {} : { version: request.version },
1583
+ retry: UPGRADE_COMMAND,
1584
+ fetcher
1585
+ });
1586
+ const plan = planInstall(root, release.version);
1587
+ if (manifest.engine.version === release.version && plan.satisfied) {
1588
+ io.out(`\u2713 Already on ${ENGINE_PACKAGE4} ${release.version} \u2014 nothing to move`);
1589
+ return;
1590
+ }
1591
+ const manifestText = serializeManifest2({
1592
+ ...manifest,
1593
+ engine: { package: ENGINE_PACKAGE4, version: release.version, integrity: release.integrity }
1594
+ });
1595
+ await consent(options, release, plan, request.yes);
1596
+ await installPin({
1597
+ home,
1598
+ kind: "engines",
1599
+ pin: { name: ENGINE_PACKAGE4, version: release.version, integrity: release.integrity },
1600
+ fetcher
1601
+ });
1602
+ io.out(`\u2713 ${ENGINE_PACKAGE4} ${release.version} installed`);
1603
+ if (!plan.satisfied) {
1604
+ try {
1605
+ await (options.install ?? installWithPackageManager)(plan);
1606
+ } catch {
1607
+ throw new UpgradeInstallFailedError(plan.manager, release.version);
1608
+ }
1609
+ for (const step of plan.steps.filter((entry) => !entry.satisfied)) {
1610
+ io.out(`\u2713 ${step.manifest} depends on ${RUNTIME_PACKAGE} ${release.version}`);
1611
+ }
1612
+ }
1613
+ writeFileSync4(manifestFile, manifestText);
1614
+ io.out(`\u2713 ${MANIFEST_PATH4} pins ${ENGINE_PACKAGE4} ${release.version}`);
1615
+ const kept = pinnedElsewhere(manifest, release.version);
1616
+ if (kept.length > 0) {
1617
+ const subject = kept.length === 1 ? "keeps its own pin" : "keep their own pins";
1618
+ io.out(`\u2192 ${kept.join(", ")} ${subject} \u2014 \`penv add <package>@<version>\` moves one.`);
1619
+ }
1620
+ }
1621
+
1414
1622
  // src/launcher.ts
1415
- import { existsSync as existsSync4, readFileSync as readFileSync6, writeFileSync as writeFileSync4 } from "fs";
1416
- import { join as join7 } from "path";
1623
+ import { existsSync as existsSync4, readFileSync as readFileSync6, writeFileSync as writeFileSync5 } from "fs";
1624
+ import { join as join6 } from "path";
1417
1625
  import {
1418
1626
  MANIFEST_FORMAT,
1419
- MANIFEST_PATH as MANIFEST_PATH5,
1627
+ MANIFEST_PATH as MANIFEST_PATH6,
1420
1628
  PENV_HOME_VAR,
1421
1629
  PenvError as PenvError3,
1630
+ packageEntry as packageEntry2,
1422
1631
  parseManifest as parseManifest2,
1423
1632
  penvHome,
1424
- serializeManifest as serializeManifest2,
1633
+ serializeManifest as serializeManifest3,
1425
1634
  UnsupportedManifestFormatError
1426
1635
  } from "@penvhq/core";
1427
1636
 
1428
1637
  // src/help.ts
1429
- import { EXTENSIONS_PATH as EXTENSIONS_PATH3, MANIFEST_PATH as MANIFEST_PATH4 } from "@penvhq/core";
1638
+ import { RUNTIME_PACKAGE as RUNTIME_PACKAGE2 } from "@penvhq/cli/install";
1639
+ import { ENGINE_PACKAGE as ENGINE_PACKAGE5, EXTENSIONS_PATH as EXTENSIONS_PATH3, MANIFEST_PATH as MANIFEST_PATH5, OFFICIAL_SCOPE as OFFICIAL_SCOPE3 } from "@penvhq/core";
1430
1640
  function printLauncherCommands(io) {
1431
1641
  io.out("");
1432
1642
  io.out("LAUNCHER COMMANDS (penv itself, whatever engine a project pins)");
1433
1643
  io.out("");
1434
- io.out(` install Installs everything ${MANIFEST_PATH4} pins`);
1644
+ io.out(` install Installs everything ${MANIFEST_PATH5} pins`);
1435
1645
  io.out(" add <package> Pins a provider extension, and commits the decision");
1436
1646
  io.out(` add ${LOCAL_FLAG} <package> Adds the one this repository builds \u2014 nothing is pinned`);
1647
+ io.out(` upgrade [version] Moves the engine pin and ${RUNTIME_PACKAGE2} together`);
1437
1648
  io.out("");
1438
1649
  }
1439
1650
  function printInstallHelp(io) {
1440
1651
  io.out(`${INSTALL_COMMAND}`);
1441
1652
  io.out("");
1442
- io.out(`Downloads, verifies and installs every version ${MANIFEST_PATH4} pins \u2014 the engine and`);
1653
+ io.out(`Downloads, verifies and installs every version ${MANIFEST_PATH5} pins \u2014 the engine and`);
1443
1654
  io.out("every extension \u2014 into $PENV_HOME. The one penv command CI and production run that");
1444
1655
  io.out("may reach the registry.");
1445
1656
  }
@@ -1451,15 +1662,32 @@ function printAddHelp(io) {
1451
1662
  io.out(
1452
1663
  ` ${TRUST_YOUNG_FLAG} Add a release published less than ${MIN_PACKAGE_AGE_DAYS} days ago`
1453
1664
  );
1665
+ io.out(` ${YES_FLAG} Ask nothing \u2014 the config and onboarding offers print instead`);
1454
1666
  io.out("");
1455
- io.out(`Pins the release in ${MANIFEST_PATH4} and writes its type declaration to`);
1667
+ io.out(`Pins the release in ${MANIFEST_PATH5} and writes its type declaration to`);
1456
1668
  io.out(`${EXTENSIONS_PATH3}/. Both are committed, so \`${INSTALL_COMMAND}\` gives every machine`);
1457
1669
  io.out(`the bytes you reviewed. \`${LOCAL_FLAG}\` pins nothing and touches no manifest.`);
1670
+ io.out("");
1671
+ io.out(`An \`${OFFICIAL_SCOPE3}*\` package asks nothing, so it adds unattended. Anything else`);
1672
+ io.out("records who publishes it and why you trust it, which needs a person at a terminal \u2014");
1673
+ io.out(`\`${YES_FLAG}\` cannot write that line for you.`);
1674
+ }
1675
+ function printUpgradeHelp(io) {
1676
+ io.out("penv upgrade [version]");
1677
+ io.out("");
1678
+ io.out(` ${YES_FLAG} Skip the question \u2014 unattended runs need it and a version`);
1679
+ io.out("");
1680
+ io.out(`Moves the ${ENGINE_PACKAGE5} pin in ${MANIFEST_PATH5} and this project's`);
1681
+ io.out(`${RUNTIME_PACKAGE2} dependency to the same exact version. No version takes whatever`);
1682
+ io.out("`latest` points at; the integrity is the one the registry states, never one penv");
1683
+ io.out("computed. Both files are shown before either moves, and they move together or not");
1684
+ io.out("at all. Extensions keep their own pins.");
1458
1685
  }
1459
1686
 
1460
1687
  // src/launcher.ts
1461
1688
  var INSTALL = "install";
1462
1689
  var ADD = "add";
1690
+ var UPGRADE = "upgrade";
1463
1691
  var VERSION_FLAGS = /* @__PURE__ */ new Set(["--version", "-v"]);
1464
1692
  var HELP_FLAGS = /* @__PURE__ */ new Set(["--help", "-h"]);
1465
1693
  var NO_DOWNLOAD = "--no-download";
@@ -1555,6 +1783,10 @@ async function launch(options) {
1555
1783
  printAddHelp(io);
1556
1784
  return 0;
1557
1785
  }
1786
+ if (first === UPGRADE) {
1787
+ printUpgradeHelp(io);
1788
+ return 0;
1789
+ }
1558
1790
  }
1559
1791
  const project = findProject(cwd);
1560
1792
  if (project === void 0) {
@@ -1582,6 +1814,20 @@ async function launch(options) {
1582
1814
  io.out(`penv ${manifest.engine.version}`);
1583
1815
  return 0;
1584
1816
  }
1817
+ if (first === UPGRADE) {
1818
+ await upgrade({
1819
+ argv: forwarded.slice(1),
1820
+ root: project.root,
1821
+ manifestFile: project.manifestFile,
1822
+ manifest,
1823
+ home,
1824
+ io,
1825
+ fetcher: options.fetcher,
1826
+ noDownload,
1827
+ ci: isCi(env)
1828
+ });
1829
+ return 0;
1830
+ }
1585
1831
  const enginePin = enginePinOf(manifest);
1586
1832
  const engineDir = await ensure(options, "engines", enginePin, home, noDownload);
1587
1833
  for (const pin of extensionPinsOf(manifest)) {
@@ -1600,16 +1846,16 @@ async function adopt(options, forwarded, home, cwd, noDownload) {
1600
1846
  if (root === void 0) {
1601
1847
  return 0;
1602
1848
  }
1603
- const manifestFile = join7(root, ...MANIFEST_PATH5.split("/"));
1849
+ const manifestFile = join6(root, ...MANIFEST_PATH6.split("/"));
1604
1850
  if (existsSync4(manifestFile)) {
1605
1851
  return 0;
1606
1852
  }
1607
1853
  const pin = releaseEnginePin(options.bundledPin, engine.version);
1608
- writeFileSync4(
1854
+ writeFileSync5(
1609
1855
  manifestFile,
1610
- serializeManifest2({ format: MANIFEST_FORMAT, engine: pin, extensions: {} })
1856
+ serializeManifest3({ format: MANIFEST_FORMAT, engine: pin, extensions: {} })
1611
1857
  );
1612
- options.io.out(`\u2713 ${MANIFEST_PATH5} pins ${pin.package} ${pin.version}`);
1858
+ options.io.out(`\u2713 ${MANIFEST_PATH6} pins ${pin.package} ${pin.version}`);
1613
1859
  await ensureAdoptedEngine(
1614
1860
  options,
1615
1861
  { name: pin.package, version: pin.version, integrity: pin.integrity },
@@ -1656,12 +1902,14 @@ async function install(options, pins, home, broken) {
1656
1902
  if (state === "corrupt") {
1657
1903
  throw new PackageCorruptError(pin.name, pin.version, dir);
1658
1904
  }
1659
- if (state === "installed") {
1660
- options.io.out(`\u2713 ${pin.name} ${pin.version} already installed`);
1661
- continue;
1905
+ if (state === "absent") {
1906
+ await installPin({ home, kind, pin, fetcher: options.fetcher });
1907
+ }
1908
+ if (kind === "extensions") {
1909
+ await assertLoadable(pin.name, packageEntry2(dir), false);
1662
1910
  }
1663
- await installPin({ home, kind, pin, fetcher: options.fetcher });
1664
- options.io.out(`\u2713 ${pin.name} ${pin.version} installed`);
1911
+ const verb = state === "absent" ? "installed" : "already installed";
1912
+ options.io.out(`\u2713 ${pin.name} ${pin.version} ${verb}`);
1665
1913
  }
1666
1914
  if (broken.length > 0) {
1667
1915
  report(new ManifestEntriesUnreadableError(broken), options.io);
@@ -1709,6 +1957,8 @@ export {
1709
1957
  readProviderEntries,
1710
1958
  setProviderType,
1711
1959
  INSTALL_COMMAND,
1960
+ UPGRADE_COMMAND,
1961
+ YES_FLAG,
1712
1962
  MIN_PACKAGE_AGE_DAYS,
1713
1963
  TRUST_YOUNG_FLAG,
1714
1964
  NoProjectError,
@@ -1735,7 +1985,7 @@ export {
1735
1985
  ReleaseIncompleteError,
1736
1986
  PackageTooYoungError,
1737
1987
  AddNoDownloadError,
1738
- AddNotInteractiveError,
1988
+ AddTrustUnattendedError,
1739
1989
  TrustDeclinedError,
1740
1990
  TrustPublisherMissingError,
1741
1991
  TrustReasonMissingError,
@@ -1743,6 +1993,12 @@ export {
1743
1993
  DeclarationNotSelfContainedError,
1744
1994
  EnginePinUnreleasedError,
1745
1995
  EnginePinMismatchError,
1996
+ UpgradeSubjectError,
1997
+ UpgradeFlagError,
1998
+ UpgradeNoDownloadError,
1999
+ UpgradeUnattendedError,
2000
+ UpgradeDeclinedError,
2001
+ UpgradeInstallFailedError,
1746
2002
  EngineEntryError,
1747
2003
  readExtensionPackage,
1748
2004
  renderDeclaration,
@@ -1773,6 +2029,7 @@ export {
1773
2029
  releaseEnginePin,
1774
2030
  findProject,
1775
2031
  findAdoptedRoot,
2032
+ upgrade,
1776
2033
  runLauncher
1777
2034
  };
1778
- //# sourceMappingURL=chunk-BNUZ52VN.js.map
2035
+ //# sourceMappingURL=chunk-A5LJQPYS.js.map