@sparkelf/dsh-plus 0.1.0-rc.38 → 0.1.0-rc.39

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/lib/bin.js CHANGED
@@ -422,6 +422,42 @@ function runPnpm(cwd, args, label) {
422
422
  if (result.status !== 0) throw new Error(label + " failed with exit code " + String(result.status));
423
423
  }
424
424
  /**
425
+ * Report whether pnpm can run.
426
+ *
427
+ * The profile installs its own tree so its \`overrides\` apply, and only pnpm reads those
428
+ * from a workspace, so pnpm is a prerequisite the distribution cannot supply. Probing
429
+ * first turns a cryptic failure from the install into a message naming what is missing.
430
+ *
431
+ * @returns \`true\` when pnpm answers with a version.
432
+ */
433
+ function pnpmAvailable() {
434
+ return spawnSync("pnpm", ["--version"], {
435
+ stdio: "pipe",
436
+ shell: process.platform === "win32",
437
+ encoding: "utf8"
438
+ }).status === 0;
439
+ }
440
+ /**
441
+ * The commands that install pnpm, in the order worth trying.
442
+ *
443
+ * Corepack ships with Node and needs no download, so it comes first; npm is the fallback
444
+ * for an installation whose Corepack is absent or disabled. Both are the consumer's own
445
+ * toolchain, which is what lets the first start offer to install rather than only report.
446
+ *
447
+ * @returns commands to try in order, stopping at the first that works.
448
+ */
449
+ function pnpmInstallCommands() {
450
+ return ["corepack enable pnpm", "npm install -g pnpm"];
451
+ }
452
+ /**
453
+ * The command to show a consumer who declines the automatic install.
454
+ *
455
+ * @returns the preferred command, which is the one the offer runs first.
456
+ */
457
+ function pnpmInstallCommand() {
458
+ return pnpmInstallCommands()[0] ?? "npm install -g pnpm";
459
+ }
460
+ /**
425
461
  * Expose the distribution's nested packages at the top level the profile searches.
426
462
  *
427
463
  * The profile resolves a bundle from one directory, so a package npm nested under the
@@ -698,6 +734,30 @@ async function start(argv) {
698
734
  const options = parseStartOptions(argv);
699
735
  const anchor = installationAnchor();
700
736
  const paths = resolvePaths(anchor);
737
+ if (!pnpmAvailable()) {
738
+ const command = pnpmInstallCommand();
739
+ console.log("pnpm is required to install the plus profile, and was not found.");
740
+ console.log("Install it with: " + command);
741
+ if (!await confirm("Install pnpm now?")) {
742
+ console.log("Install pnpm and run dsh-plus start again.");
743
+ return 1;
744
+ }
745
+ let installed = false;
746
+ for (const candidate of pnpmInstallCommands()) if (spawnSync(candidate, {
747
+ stdio: "inherit",
748
+ shell: true
749
+ }).status === 0 && pnpmAvailable()) {
750
+ installed = true;
751
+ break;
752
+ }
753
+ if (!installed) {
754
+ console.log("Could not install pnpm automatically.");
755
+ console.log("Run one of these, then run dsh-plus start again:");
756
+ for (const candidate of pnpmInstallCommands()) console.log(" " + candidate);
757
+ return 1;
758
+ }
759
+ console.log("pnpm installed.");
760
+ }
701
761
  const created = ensureProfile(paths, installationRoot());
702
762
  console.log(created ? "Created the plus profile at " + paths.profileDirectory : "Using the existing plus profile");
703
763
  for (const label of applyProfileNpmPatches(paths.distributionDirectory, paths.profileDirectory)) console.log("Applied the reviewed patch " + label);
@@ -14,7 +14,7 @@ import { dirname, join } from 'node:path';
14
14
  import { fileURLToPath } from 'node:url';
15
15
  import { newerVersion } from "./registry-versions.js";
16
16
  import { DEFAULT_PORT, STOP_GRACE_MILLISECONDS, choosePort, clearState, isRunning, portAvailable, readState, spawnServer, stateDirectory, waitForAuthenticatedUrl, waitForServer, writeState, } from "./standalone-server.js";
17
- import { STANDALONE_PROFILE, applyProfileNpmPatches, ensureProfile, readDistributionProfile, resolvePaths, } from "./standalone-profile.js";
17
+ import { STANDALONE_PROFILE, applyProfileNpmPatches, ensureProfile, pnpmAvailable, pnpmInstallCommand, pnpmInstallCommands, readDistributionProfile, resolvePaths, } from "./standalone-profile.js";
18
18
  /** Milliseconds a start waits for the server to answer before reporting failure. */
19
19
  const READY_TIMEOUT_MILLISECONDS = 90_000;
20
20
  function parseStartOptions(argv) {
@@ -100,6 +100,36 @@ async function start(argv) {
100
100
  const options = parseStartOptions(argv);
101
101
  const anchor = installationAnchor();
102
102
  const paths = resolvePaths(anchor);
103
+ // The profile installs its own dependency tree, which needs pnpm. Asking here rather
104
+ // than failing inside the install turns a missing prerequisite into a decision the
105
+ // consumer makes, and the install it can run is the one command that provides it.
106
+ if (!pnpmAvailable()) {
107
+ const command = pnpmInstallCommand();
108
+ console.log('pnpm is required to install the ' + STANDALONE_PROFILE + ' profile, and was not found.');
109
+ console.log('Install it with: ' + command);
110
+ if (!await confirm('Install pnpm now?')) {
111
+ console.log('Install pnpm and run dsh-plus start again.');
112
+ return 1;
113
+ }
114
+ // Try each installer in turn: Corepack is absent on an installation that disabled it,
115
+ // and npm succeeds there. A failure reports the commands rather than a stack trace.
116
+ let installed = false;
117
+ for (const candidate of pnpmInstallCommands()) {
118
+ const attempt = spawnSync(candidate, { stdio: 'inherit', shell: true });
119
+ if (attempt.status === 0 && pnpmAvailable()) {
120
+ installed = true;
121
+ break;
122
+ }
123
+ }
124
+ if (!installed) {
125
+ console.log('Could not install pnpm automatically.');
126
+ console.log('Run one of these, then run dsh-plus start again:');
127
+ for (const candidate of pnpmInstallCommands())
128
+ console.log(' ' + candidate);
129
+ return 1;
130
+ }
131
+ console.log('pnpm installed.');
132
+ }
103
133
  const created = ensureProfile(paths, installationRoot());
104
134
  console.log(created
105
135
  ? 'Created the ' + STANDALONE_PROFILE + ' profile at ' + paths.profileDirectory
@@ -94,6 +94,32 @@ export declare function installProfilePackages(paths: StandalonePaths, consumerD
94
94
  * @param profileModules - the profile's \`node_modules\` directory.
95
95
  */
96
96
  export declare function alignReplacedPackageNames(profileModules: string): void;
97
+ /**
98
+ * Report whether pnpm can run.
99
+ *
100
+ * The profile installs its own tree so its \`overrides\` apply, and only pnpm reads those
101
+ * from a workspace, so pnpm is a prerequisite the distribution cannot supply. Probing
102
+ * first turns a cryptic failure from the install into a message naming what is missing.
103
+ *
104
+ * @returns \`true\` when pnpm answers with a version.
105
+ */
106
+ export declare function pnpmAvailable(): boolean;
107
+ /**
108
+ * The commands that install pnpm, in the order worth trying.
109
+ *
110
+ * Corepack ships with Node and needs no download, so it comes first; npm is the fallback
111
+ * for an installation whose Corepack is absent or disabled. Both are the consumer's own
112
+ * toolchain, which is what lets the first start offer to install rather than only report.
113
+ *
114
+ * @returns commands to try in order, stopping at the first that works.
115
+ */
116
+ export declare function pnpmInstallCommands(): readonly string[];
117
+ /**
118
+ * The command to show a consumer who declines the automatic install.
119
+ *
120
+ * @returns the preferred command, which is the one the offer runs first.
121
+ */
122
+ export declare function pnpmInstallCommand(): string;
97
123
  /** Resolve every path a command needs, without creating anything. */
98
124
  export declare function resolvePaths(anchor: string, env?: NodeJS.ProcessEnv): StandalonePaths;
99
125
  /**
@@ -219,6 +219,43 @@ function runPnpm(cwd, args, label) {
219
219
  if (result.status !== 0)
220
220
  throw new Error(label + ' failed with exit code ' + String(result.status));
221
221
  }
222
+ /**
223
+ * Report whether pnpm can run.
224
+ *
225
+ * The profile installs its own tree so its \`overrides\` apply, and only pnpm reads those
226
+ * from a workspace, so pnpm is a prerequisite the distribution cannot supply. Probing
227
+ * first turns a cryptic failure from the install into a message naming what is missing.
228
+ *
229
+ * @returns \`true\` when pnpm answers with a version.
230
+ */
231
+ export function pnpmAvailable() {
232
+ const probe = spawnSync('pnpm', ['--version'], {
233
+ stdio: 'pipe',
234
+ shell: process.platform === 'win32',
235
+ encoding: 'utf8',
236
+ });
237
+ return probe.status === 0;
238
+ }
239
+ /**
240
+ * The commands that install pnpm, in the order worth trying.
241
+ *
242
+ * Corepack ships with Node and needs no download, so it comes first; npm is the fallback
243
+ * for an installation whose Corepack is absent or disabled. Both are the consumer's own
244
+ * toolchain, which is what lets the first start offer to install rather than only report.
245
+ *
246
+ * @returns commands to try in order, stopping at the first that works.
247
+ */
248
+ export function pnpmInstallCommands() {
249
+ return ['corepack enable pnpm', 'npm install -g pnpm'];
250
+ }
251
+ /**
252
+ * The command to show a consumer who declines the automatic install.
253
+ *
254
+ * @returns the preferred command, which is the one the offer runs first.
255
+ */
256
+ export function pnpmInstallCommand() {
257
+ return pnpmInstallCommands()[0] ?? 'npm install -g pnpm';
258
+ }
222
259
  /**
223
260
  * Expose the distribution's nested packages at the top level the profile searches.
224
261
  *
package/package.json CHANGED
@@ -166,5 +166,5 @@
166
166
  },
167
167
  "type": "module",
168
168
  "types": "lib/types/index.d.ts",
169
- "version": "0.1.0-rc.38"
169
+ "version": "0.1.0-rc.39"
170
170
  }