@sparkelf/dsh-plus 0.1.0-rc.30 → 0.1.0-rc.32

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
@@ -3,7 +3,7 @@ import { r as runApply } from "./apply-e0aMC4B4.js";
3
3
  import { createRequire } from "node:module";
4
4
  import { execFileSync, spawn, spawnSync } from "node:child_process";
5
5
  import { createWriteStream, existsSync, mkdirSync, readFileSync, readdirSync, rmSync, symlinkSync, writeFileSync } from "node:fs";
6
- import { dirname, join, resolve } from "node:path";
6
+ import { dirname, join, posix, resolve, win32 } from "node:path";
7
7
  import { homedir } from "node:os";
8
8
  import semver from "semver";
9
9
  import { fileURLToPath } from "node:url";
@@ -204,6 +204,31 @@ async function waitForServer(url, timeoutMilliseconds) {
204
204
  }
205
205
  return false;
206
206
  }
207
+ /**
208
+ * Wait until the launcher's log carries the authenticated URL.
209
+ *
210
+ * Readiness and the printed line are separate events: the server answers before the
211
+ * launcher finishes its own startup, so the log is polled here rather than read once.
212
+ *
213
+ * @param logPath - the launcher's log file.
214
+ * @param timeoutMilliseconds - how long to keep polling.
215
+ * @returns the printed URL, or undefined when it never appeared.
216
+ */
217
+ async function waitForAuthenticatedUrl(logPath, timeoutMilliseconds) {
218
+ const deadline = Date.now() + timeoutMilliseconds;
219
+ for (;;) {
220
+ let text = "";
221
+ try {
222
+ text = readFileSync(logPath, "utf8");
223
+ } catch {}
224
+ const found = /dsh web: (http:\/\/\S+)/u.exec(text);
225
+ if (found?.[1] !== void 0) return found[1];
226
+ if (Date.now() >= deadline) return void 0;
227
+ await new Promise((resolveDelay) => {
228
+ setTimeout(resolveDelay, 250);
229
+ });
230
+ }
231
+ }
207
232
  //#endregion
208
233
  //#region lib/types/standalone-profile.js
209
234
  /**
@@ -232,6 +257,27 @@ function resolveHome(env = process.env) {
232
257
  return configured === void 0 || configured === "" ? join(homedir(), ".dsh") : resolve(configured);
233
258
  }
234
259
  /**
260
+ * Whether one absolute path is the other or sits beneath it.
261
+ *
262
+ * A path outside the parent has a relative form starting with `..`, which every
263
+ * platform answers the same way. Comparing the text against a literal separator is
264
+ * not: Windows separates with a backslash, so a nested path never matched its own
265
+ * ancestor and every Windows installation reported the distribution as sitting
266
+ * outside its own tree.
267
+ *
268
+ * @param candidate - absolute path to test.
269
+ * @param parent - absolute path that may contain it.
270
+ * @param platform - separating convention, injectable so the Windows answer is
271
+ * testable where the suite runs on a POSIX host.
272
+ * @returns true when the candidate is the parent or inside it.
273
+ */
274
+ function isWithin(candidate, parent, platform = process.platform) {
275
+ if (candidate === parent) return true;
276
+ const path = platform === "win32" ? win32 : posix;
277
+ const inside = path.relative(parent, candidate);
278
+ return inside !== "" && !inside.startsWith("..") && !inside.startsWith(path.sep + "..");
279
+ }
280
+ /**
235
281
  * Resolve the installed distribution directory from one requiring anchor.
236
282
  *
237
283
  * The anchor must be a path inside the consumer's own tree. Resolving from this
@@ -250,7 +296,7 @@ function resolveDistributionDirectory(anchor) {
250
296
  }
251
297
  let current = resolve(dirname(anchor));
252
298
  for (;;) {
253
- if (resolved === current || resolved.startsWith(current + "/")) return resolved;
299
+ if (isWithin(resolved, current)) return resolved;
254
300
  const parent = dirname(current);
255
301
  if (parent === current) break;
256
302
  current = parent;
@@ -529,12 +575,13 @@ async function startDetached(home, entry, options) {
529
575
  if (isRunning(pid)) process.kill(pid, "SIGTERM");
530
576
  return 1;
531
577
  }
578
+ const opened = await waitForAuthenticatedUrl(logPath, READY_TIMEOUT_MILLISECONDS) ?? url;
532
579
  writeState(home, {
533
580
  pid,
534
581
  port,
535
- url
582
+ url: opened
536
583
  });
537
- console.log("Plus is running at " + url);
584
+ console.log("Plus is running at " + opened);
538
585
  console.log("Logs: " + logPath);
539
586
  console.log("Stop it with: dsh-plus stop");
540
587
  return 0;
@@ -13,7 +13,7 @@ import { createRequire } from 'node:module';
13
13
  import { dirname, join } from 'node:path';
14
14
  import { fileURLToPath } from 'node:url';
15
15
  import { newerVersion } from "./registry-versions.js";
16
- import { DEFAULT_PORT, STOP_GRACE_MILLISECONDS, choosePort, clearState, isRunning, portAvailable, readState, spawnServer, stateDirectory, waitForServer, writeState, } from "./standalone-server.js";
16
+ import { DEFAULT_PORT, STOP_GRACE_MILLISECONDS, choosePort, clearState, isRunning, portAvailable, readState, spawnServer, stateDirectory, waitForAuthenticatedUrl, waitForServer, writeState, } from "./standalone-server.js";
17
17
  import { STANDALONE_PROFILE, ensureProfile, 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;
@@ -137,8 +137,13 @@ async function startDetached(home, entry, options) {
137
137
  process.kill(pid, 'SIGTERM');
138
138
  return 1;
139
139
  }
140
- writeState(home, { pid, port, url });
141
- console.log('Plus is running at ' + url);
140
+ // The launcher prints the URL carrying the process launch token, and the server
141
+ // refuses every request without the cookie that token mints. Reporting the bare
142
+ // address sends the user to a 401 that tells them to reopen the launcher's URL,
143
+ // which lives only in the log this command points at.
144
+ const opened = await waitForAuthenticatedUrl(logPath, READY_TIMEOUT_MILLISECONDS) ?? url;
145
+ writeState(home, { pid, port, url: opened });
146
+ console.log('Plus is running at ' + opened);
142
147
  console.log('Logs: ' + logPath);
143
148
  console.log('Stop it with: dsh-plus stop');
144
149
  return 0;
@@ -21,6 +21,22 @@ export interface StandalonePaths {
21
21
  }
22
22
  /** Resolve the DSH home, honouring the environment override the launcher itself reads. */
23
23
  export declare function resolveHome(env?: NodeJS.ProcessEnv): string;
24
+ /**
25
+ * Whether one absolute path is the other or sits beneath it.
26
+ *
27
+ * A path outside the parent has a relative form starting with `..`, which every
28
+ * platform answers the same way. Comparing the text against a literal separator is
29
+ * not: Windows separates with a backslash, so a nested path never matched its own
30
+ * ancestor and every Windows installation reported the distribution as sitting
31
+ * outside its own tree.
32
+ *
33
+ * @param candidate - absolute path to test.
34
+ * @param parent - absolute path that may contain it.
35
+ * @param platform - separating convention, injectable so the Windows answer is
36
+ * testable where the suite runs on a POSIX host.
37
+ * @returns true when the candidate is the parent or inside it.
38
+ */
39
+ export declare function isWithin(candidate: string, parent: string, platform?: NodeJS.Platform): boolean;
24
40
  /**
25
41
  * Resolve the installed distribution directory from one requiring anchor.
26
42
  *
@@ -11,7 +11,7 @@
11
11
  import { existsSync, mkdirSync, readdirSync, readFileSync, symlinkSync, writeFileSync } from 'node:fs';
12
12
  import { createRequire } from 'node:module';
13
13
  import { homedir } from 'node:os';
14
- import { dirname, join, resolve } from 'node:path';
14
+ import { dirname, join, posix, resolve, win32 } from 'node:path';
15
15
  /** Profile name a standalone installation owns. */
16
16
  export const STANDALONE_PROFILE = 'plus';
17
17
  function requireRecord(value, label) {
@@ -29,6 +29,28 @@ export function resolveHome(env = process.env) {
29
29
  const configured = env.DSH_HOME;
30
30
  return configured === undefined || configured === '' ? join(homedir(), '.dsh') : resolve(configured);
31
31
  }
32
+ /**
33
+ * Whether one absolute path is the other or sits beneath it.
34
+ *
35
+ * A path outside the parent has a relative form starting with `..`, which every
36
+ * platform answers the same way. Comparing the text against a literal separator is
37
+ * not: Windows separates with a backslash, so a nested path never matched its own
38
+ * ancestor and every Windows installation reported the distribution as sitting
39
+ * outside its own tree.
40
+ *
41
+ * @param candidate - absolute path to test.
42
+ * @param parent - absolute path that may contain it.
43
+ * @param platform - separating convention, injectable so the Windows answer is
44
+ * testable where the suite runs on a POSIX host.
45
+ * @returns true when the candidate is the parent or inside it.
46
+ */
47
+ export function isWithin(candidate, parent, platform = process.platform) {
48
+ if (candidate === parent)
49
+ return true;
50
+ const path = platform === 'win32' ? win32 : posix;
51
+ const inside = path.relative(parent, candidate);
52
+ return inside !== '' && !inside.startsWith('..') && !inside.startsWith(path.sep + '..');
53
+ }
32
54
  /**
33
55
  * Resolve the installed distribution directory from one requiring anchor.
34
56
  *
@@ -54,7 +76,7 @@ export function resolveDistributionDirectory(anchor) {
54
76
  // reports \`<root>/node_modules/@sparkelf/dsh-plus\`.
55
77
  let current = resolve(dirname(anchor));
56
78
  for (;;) {
57
- if (resolved === current || resolved.startsWith(current + '/'))
79
+ if (isWithin(resolved, current))
58
80
  return resolved;
59
81
  const parent = dirname(current);
60
82
  if (parent === current)
@@ -56,4 +56,15 @@ export declare function spawnServer(options: {
56
56
  * @returns whether the server answered within the budget.
57
57
  */
58
58
  export declare function waitForServer(url: string, timeoutMilliseconds: number): Promise<boolean>;
59
+ /**
60
+ * Wait until the launcher's log carries the authenticated URL.
61
+ *
62
+ * Readiness and the printed line are separate events: the server answers before the
63
+ * launcher finishes its own startup, so the log is polled here rather than read once.
64
+ *
65
+ * @param logPath - the launcher's log file.
66
+ * @param timeoutMilliseconds - how long to keep polling.
67
+ * @returns the printed URL, or undefined when it never appeared.
68
+ */
69
+ export declare function waitForAuthenticatedUrl(logPath: string, timeoutMilliseconds: number): Promise<string | undefined>;
59
70
  //# sourceMappingURL=standalone-server.d.ts.map
@@ -126,4 +126,33 @@ export async function waitForServer(url, timeoutMilliseconds) {
126
126
  }
127
127
  return false;
128
128
  }
129
+ /**
130
+ * Wait until the launcher's log carries the authenticated URL.
131
+ *
132
+ * Readiness and the printed line are separate events: the server answers before the
133
+ * launcher finishes its own startup, so the log is polled here rather than read once.
134
+ *
135
+ * @param logPath - the launcher's log file.
136
+ * @param timeoutMilliseconds - how long to keep polling.
137
+ * @returns the printed URL, or undefined when it never appeared.
138
+ */
139
+ export async function waitForAuthenticatedUrl(logPath, timeoutMilliseconds) {
140
+ const deadline = Date.now() + timeoutMilliseconds;
141
+ for (;;) {
142
+ let text = '';
143
+ try {
144
+ text = readFileSync(logPath, 'utf8');
145
+ }
146
+ catch {
147
+ // The launcher creates the log before it prints, so an absent file is a race
148
+ // with its first write rather than an error to report.
149
+ }
150
+ const found = /dsh web: (http:\/\/\S+)/u.exec(text);
151
+ if (found?.[1] !== undefined)
152
+ return found[1];
153
+ if (Date.now() >= deadline)
154
+ return undefined;
155
+ await new Promise((resolveDelay) => { setTimeout(resolveDelay, 250); });
156
+ }
157
+ }
129
158
  //# sourceMappingURL=standalone-server.js.map
package/package.json CHANGED
@@ -138,5 +138,5 @@
138
138
  },
139
139
  "type": "module",
140
140
  "types": "lib/types/index.d.ts",
141
- "version": "0.1.0-rc.30"
141
+ "version": "0.1.0-rc.32"
142
142
  }