@catheadowl/ukp 0.2.1 → 0.2.2

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@catheadowl/ukp",
3
- "version": "0.2.1",
3
+ "version": "0.2.2",
4
4
  "type": "module",
5
5
  "engines": {
6
6
  "bun": ">=1.3.14"
@@ -343,11 +343,40 @@ function wakeDoorCommand(remotePort: number, form: WakeShellForm = "posix"): str
343
343
  /** Wrong-shell failure signature for the wake ladder: the remote shell could
344
344
  * not even parse the sent form, so the OTHER form is the retry — as opposed
345
345
  * to a missing-ukp failure, where the shell ran fine and named 'ukp' as the
346
- * thing it cannot find. cmd.exe errors quote the missing token ('sh' is not
347
- * recognized, exit 9009); POSIX shells print "<token>: command not found". */
346
+ * thing it cannot find. Language-independent: cmd.exe locales vary the
347
+ * message body (English, zh-CN, German … all differ) but always quote the
348
+ * missing ASCII token; POSIX shells prefix the
349
+ * token ("sh: command not found"). */
348
350
  function wrongShellSignature(form: WakeShellForm, diagnostics: string): boolean {
349
- if (form === "posix") return /'sh' is not recognized|sh: command not found/i.test(diagnostics);
350
- return /'cmd' is not recognized|cmd: command not found/i.test(diagnostics);
351
+ if (form === "posix") return /['"]sh['"]|sh: (?:command )?not found/i.test(diagnostics);
352
+ return /['"]cmd['"]|cmd: (?:command )?not found/i.test(diagnostics);
353
+ }
354
+
355
+ /** One cheap, language-independent roundtrip to learn the host shell family
356
+ * BEFORE the first wake attempt (2026-09-20, liku 9.5 finding): cmd.exe
357
+ * expands `%OS%` to `Windows_NT`; a POSIX shell echoes the literal token.
358
+ * Choosing the form up front avoids the blind POSIX-first attempt costing a
359
+ * full ready-timeout on every Windows invocation, and picks the right tty
360
+ * stance — `-tt` exists for POSIX SIGHUP reaping; on Windows there is no
361
+ * SIGHUP (max-idle bounds the door), and at least one Win32-OpenSSH 9.5
362
+ * build LOSES a quoted remote command under a pty (an interactive cmd
363
+ * swallows it and waits on stdin forever — the no-pty path executes it
364
+ * correctly). Defaults to POSIX (the common server shape) when the probe
365
+ * errors or times out; the ladder remains as the backstop. */
366
+ async function probeHostShellFamily(
367
+ options: { sshCommand?: readonly string[] },
368
+ target: string,
369
+ ): Promise<WakeShellForm> {
370
+ const proc = Bun.spawn(
371
+ [...(options.sshCommand ?? ["ssh"]), "-o", "BatchMode=yes", target, "echo %OS%"],
372
+ { stdout: "pipe", stderr: "pipe", stdin: "ignore" },
373
+ );
374
+ const output = await Promise.race([
375
+ new Response(proc.stdout).text().catch(() => ""),
376
+ Bun.sleep(5_000).then(() => ""),
377
+ ]);
378
+ proc.kill();
379
+ return /Windows_NT/i.test(output) ? "cmd" : "posix";
351
380
  }
352
381
 
353
382
  /** HTTP-level readiness probe through the tunnel (W9): a TCP accept on the
@@ -428,11 +457,10 @@ async function openSshTunnel(
428
457
  // options for the remaining attempts so the endpoint degrades to Tier 0
429
458
  // instead of hard-failing what worked without mux.
430
459
  let muxDisabled = false;
431
- // Shell-form ladder (2026-09-20): POSIX first (the common server shell),
432
- // cmd second — a Windows host whose OpenSSH default shell is cmd.exe fails
433
- // the POSIX form with an 'sh'-not-recognized signature (not a missing-ukp
434
- // failure: the shell never got as far as looking for ukp).
435
- let form: WakeShellForm = "posix";
460
+ // Shell form (2026-09-20): probed up front via `echo %OS%` (language-
461
+ // independent — see probeHostShellFamily), POSIX by default; the wrong-
462
+ // shell ladder below remains the backstop for a lying or timed-out probe.
463
+ let form: WakeShellForm = await probeHostShellFamily(options, target);
436
464
  const attempts = 3;
437
465
  for (let attempt = 1; attempt <= attempts; attempt++) {
438
466
  const localPort = await freePort();
@@ -443,8 +471,11 @@ async function openSshTunnel(
443
471
  // SIGHUP on disconnect — normal disconnect included, the door only ever
444
472
  // died to --max-idle. Forcing a pty gives the session a controlling
445
473
  // terminal, so killing the client reaps the door in seconds (verified:
446
- // kill -9 of the local ssh → door gone within 3s).
447
- "-tt",
474
+ // kill -9 of the local ssh → door gone within 3s). POSIX hosts only:
475
+ // Windows has no SIGHUP (max-idle bounds the door there), and pty
476
+ // sessions lose quoted remote commands on at least one
477
+ // Win32-OpenSSH 9.5 build — the cmd form goes without -tt.
478
+ ...(form === "posix" ? ["-tt"] : []),
448
479
  "-o", "ExitOnForwardFailure=yes",
449
480
  "-o", "BatchMode=yes",
450
481
  ...(muxDisabled ? [] : muxClientOptions()),
@@ -173,6 +173,7 @@ export function renderServeBanner(info: ServeInfo): string {
173
173
  ` endpoints: ${info.door!.endpoints.length > 0 ? info.door!.endpoints.join(", ") : "(none — register endpoints on this host)"}`,
174
174
  ` write: ${info.door!.write.length > 0 ? info.door!.write.join(", ") + " (propose via PUT /e/<name>/v1/propose/<id>)" : "(no endpoint declares propose)"}`,
175
175
  ` auth: ${info.authRequired ? "bearer token required" : "no token (loopback bind; ssh-forwarded clients authenticate by SSH key)"}`,
176
+ ` rg: ${info.rg === "ok" ? "ok" : "missing (rg calls skip with a warning — install ripgrep on this door's PATH)"}`,
176
177
  ...(info.maxIdleSeconds !== undefined
177
178
  ? [` idle: exits after ${info.maxIdleSeconds}s without requests (--max-idle)`]
178
179
  : []),
@@ -187,6 +188,7 @@ export function renderServeBanner(info: ServeInfo): string {
187
188
  ` listening: ${info.url}`,
188
189
  ` discovery: ${info.url.startsWith("http") ? `${info.url}${DISCOVERY_PATH}` : info.url}`,
189
190
  ` auth: ${info.authRequired ? "bearer token required" : "no token (loopback only)"}`,
191
+ ` rg: ${info.rg === "ok" ? "ok" : "missing (rg calls skip with a warning — install ripgrep on this door's PATH)"}`,
190
192
  ...(info.maxIdleSeconds !== undefined
191
193
  ? [` idle: exits after ${info.maxIdleSeconds}s without requests (--max-idle)`]
192
194
  : []),
package/src/server.ts CHANGED
@@ -29,6 +29,7 @@ import {
29
29
  type NavRequest,
30
30
  } from "./capabilities/nav.ts";
31
31
  import {
32
+ rgExecutableAvailable,
32
33
  runRg,
33
34
  projectRgEnvelope,
34
35
  RG_DEFAULT_LIMIT,
@@ -150,6 +151,14 @@ export interface ServeInfo {
150
151
  host: string;
151
152
  port: number;
152
153
  authRequired: boolean;
154
+ /** ripgrep availability probed once at startup IN THE DOOR'S OWN PROCESS
155
+ * (2026-09-20, liku feedback): /v1/rg serves every endpoint and a missing
156
+ * binary degrades per-endpoint on the wire by design — but that truth is
157
+ * consumer-channel only. The banner line is the operator channel, and the
158
+ * probe environment is by construction the one that matters (ukp diagnose
159
+ * measures the invoking shell instead, which can differ from the door's
160
+ * schtasks / wake environment). */
161
+ rg: "ok" | "missing";
153
162
  /** TLS identity summary (W5' / D-079) when serving over HTTPS; the pin is
154
163
  * what remote clients TOFU-pin at registration. `source` "re-signed" marks
155
164
  * the one start that grew SAN coverage over the existing key (--tls-san
@@ -893,6 +902,7 @@ function startDoorServer(config: ServeConfig, host: string, tlsMaterial: ReturnT
893
902
  host,
894
903
  port: listener.port,
895
904
  authRequired: (config.tokens?.length ?? 0) > 0,
905
+ rg: rgExecutableAvailable() ? "ok" : "missing",
896
906
  ...(tlsMaterial !== undefined
897
907
  ? { tls: { pin: tlsMaterial.pin, san: tlsMaterial.san, source: tlsMaterial.source } }
898
908
  : {}),
@@ -1026,6 +1036,7 @@ export function startUkpServer(config: ServeConfig): StartedServe {
1026
1036
  host,
1027
1037
  port: listener.port,
1028
1038
  authRequired: (config.tokens?.length ?? 0) > 0,
1039
+ rg: rgExecutableAvailable() ? "ok" : "missing",
1029
1040
  ...(tlsMaterial !== undefined
1030
1041
  ? { tls: { pin: tlsMaterial.pin, san: tlsMaterial.san, source: tlsMaterial.source } }
1031
1042
  : {}),