wotann 0.5.91 → 0.5.92

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.
@@ -0,0 +1,60 @@
1
+ /**
2
+ * npx-self-update-hint — one-line nudge at startup when a wotann
3
+ * binary is running from the `~/.npm/_npx/<hash>/` cache AND a newer
4
+ * version is published on the npm registry.
5
+ *
6
+ * Background: `npx wotann` caches packages by name-hash for ~7 days.
7
+ * When wotann ships a new version, users on the cached older copy
8
+ * silently miss the fix until either (a) the cache expires, (b) they
9
+ * `npx --yes -p wotann@latest wotann`, or (c) they install globally.
10
+ * This module surfaces the upgrade path BEFORE the TUI mounts so the
11
+ * stale-cache failure mode is self-explanatory.
12
+ *
13
+ * Design: pure functions where possible, dependency-injected network +
14
+ * stderr for tests, short timeout (~1.5 s) so a slow registry never
15
+ * blocks startup, swallow all errors (a hint is best-effort, never a
16
+ * gate). Skip in CI/non-interactive contexts.
17
+ */
18
+ export interface NpxUpdateHintOptions {
19
+ readonly currentVersion: string;
20
+ /** `__dirname` of the wotann install — used to detect npx cache path. */
21
+ readonly currentDirname: string;
22
+ /** Override the registry fetch (tests). */
23
+ readonly fetchLatest?: (timeoutMs: number) => Promise<string | null>;
24
+ /** Override the output sink (tests). */
25
+ readonly stderr?: {
26
+ write(s: string): unknown;
27
+ };
28
+ /** Maximum time to wait for the registry call. */
29
+ readonly timeoutMs?: number;
30
+ /** Skip the hint entirely when CI=true (default: true). */
31
+ readonly skipInCI?: boolean;
32
+ /** Skip when env signals non-interactive (default: true). */
33
+ readonly skipNonInteractive?: boolean;
34
+ }
35
+ /**
36
+ * True iff the given directory path is inside an npm npx cache slot
37
+ * (`~/.npm/_npx/<hash>/` on POSIX, `npm-cache\_npx\<hash>` on Windows).
38
+ */
39
+ export declare function isRunningFromNpxCache(dirname: string): boolean;
40
+ /**
41
+ * Returns `1` if `a > b`, `-1` if `a < b`, `0` if equal. Stable for
42
+ * unequal-length segments (treats missing trailers as 0).
43
+ */
44
+ export declare function compareVersions(a: string, b: string): number;
45
+ /**
46
+ * GET https://registry.npmjs.org/wotann/latest with a hard timeout.
47
+ * Returns the published version string, or `null` on any failure
48
+ * (network error, non-JSON response, timeout, missing version field).
49
+ * Never throws.
50
+ */
51
+ export declare function fetchLatestWotannVersion(timeoutMs: number): Promise<string | null>;
52
+ /**
53
+ * Run the full hint sequence: detect cache path → fetch latest →
54
+ * compare → optionally print. Never blocks longer than `timeoutMs`,
55
+ * never throws, never gates the rest of the startup.
56
+ *
57
+ * Returns the printed hint string (for tests) or `null` if no hint
58
+ * was printed.
59
+ */
60
+ export declare function printNpxUpgradeHint(opts: NpxUpdateHintOptions): Promise<string | null>;
@@ -0,0 +1,145 @@
1
+ /**
2
+ * npx-self-update-hint — one-line nudge at startup when a wotann
3
+ * binary is running from the `~/.npm/_npx/<hash>/` cache AND a newer
4
+ * version is published on the npm registry.
5
+ *
6
+ * Background: `npx wotann` caches packages by name-hash for ~7 days.
7
+ * When wotann ships a new version, users on the cached older copy
8
+ * silently miss the fix until either (a) the cache expires, (b) they
9
+ * `npx --yes -p wotann@latest wotann`, or (c) they install globally.
10
+ * This module surfaces the upgrade path BEFORE the TUI mounts so the
11
+ * stale-cache failure mode is self-explanatory.
12
+ *
13
+ * Design: pure functions where possible, dependency-injected network +
14
+ * stderr for tests, short timeout (~1.5 s) so a slow registry never
15
+ * blocks startup, swallow all errors (a hint is best-effort, never a
16
+ * gate). Skip in CI/non-interactive contexts.
17
+ */
18
+ import { request as httpsRequest } from "node:https";
19
+ /**
20
+ * True iff the given directory path is inside an npm npx cache slot
21
+ * (`~/.npm/_npx/<hash>/` on POSIX, `npm-cache\_npx\<hash>` on Windows).
22
+ */
23
+ export function isRunningFromNpxCache(dirname) {
24
+ const normalized = dirname.replace(/\\/g, "/");
25
+ return normalized.includes("/.npm/_npx/") || normalized.includes("/npm-cache/_npx/");
26
+ }
27
+ /**
28
+ * Parse a dot-separated semver-ish version into numeric segments.
29
+ * Non-numeric trailers (e.g. "-rc.1") are dropped — npm registry
30
+ * "latest" tag points at stable releases so leading numeric segments
31
+ * are enough for the "is current older than latest?" decision.
32
+ */
33
+ function parseNumericVersionPrefix(v) {
34
+ const trimmed = v.trim();
35
+ const head = trimmed.split(/[-+]/)[0] ?? trimmed;
36
+ return head.split(".").map((s) => {
37
+ const n = Number.parseInt(s, 10);
38
+ return Number.isFinite(n) ? n : 0;
39
+ });
40
+ }
41
+ /**
42
+ * Returns `1` if `a > b`, `-1` if `a < b`, `0` if equal. Stable for
43
+ * unequal-length segments (treats missing trailers as 0).
44
+ */
45
+ export function compareVersions(a, b) {
46
+ const A = parseNumericVersionPrefix(a);
47
+ const B = parseNumericVersionPrefix(b);
48
+ const len = Math.max(A.length, B.length);
49
+ for (let i = 0; i < len; i++) {
50
+ const x = A[i] ?? 0;
51
+ const y = B[i] ?? 0;
52
+ if (x > y)
53
+ return 1;
54
+ if (x < y)
55
+ return -1;
56
+ }
57
+ return 0;
58
+ }
59
+ /**
60
+ * GET https://registry.npmjs.org/wotann/latest with a hard timeout.
61
+ * Returns the published version string, or `null` on any failure
62
+ * (network error, non-JSON response, timeout, missing version field).
63
+ * Never throws.
64
+ */
65
+ export function fetchLatestWotannVersion(timeoutMs) {
66
+ return new Promise((resolve) => {
67
+ let settled = false;
68
+ const finish = (v) => {
69
+ if (settled)
70
+ return;
71
+ settled = true;
72
+ resolve(v);
73
+ };
74
+ const req = httpsRequest({
75
+ host: "registry.npmjs.org",
76
+ path: "/wotann/latest",
77
+ method: "GET",
78
+ headers: { "user-agent": "wotann-startup-check", accept: "application/json" },
79
+ }, (res) => {
80
+ if (res.statusCode !== 200) {
81
+ res.resume();
82
+ finish(null);
83
+ return;
84
+ }
85
+ const chunks = [];
86
+ res.on("data", (c) => chunks.push(c));
87
+ res.on("end", () => {
88
+ try {
89
+ const body = Buffer.concat(chunks).toString("utf-8");
90
+ const parsed = JSON.parse(body);
91
+ finish(typeof parsed.version === "string" ? parsed.version : null);
92
+ }
93
+ catch {
94
+ finish(null);
95
+ }
96
+ });
97
+ res.on("error", () => finish(null));
98
+ });
99
+ req.on("error", () => finish(null));
100
+ req.setTimeout(timeoutMs, () => {
101
+ req.destroy();
102
+ finish(null);
103
+ });
104
+ req.end();
105
+ });
106
+ }
107
+ const HINT_LINE = (current, latest) => `[wotann] You're running v${current} from the npx cache; v${latest} is available. ` +
108
+ `Refresh with: npx --yes -p wotann@latest wotann\n`;
109
+ /**
110
+ * Run the full hint sequence: detect cache path → fetch latest →
111
+ * compare → optionally print. Never blocks longer than `timeoutMs`,
112
+ * never throws, never gates the rest of the startup.
113
+ *
114
+ * Returns the printed hint string (for tests) or `null` if no hint
115
+ * was printed.
116
+ */
117
+ export async function printNpxUpgradeHint(opts) {
118
+ if ((opts.skipInCI ?? true) && process.env["CI"] === "true")
119
+ return null;
120
+ if ((opts.skipNonInteractive ?? true) && !process.stdout.isTTY)
121
+ return null;
122
+ if (!isRunningFromNpxCache(opts.currentDirname))
123
+ return null;
124
+ const fetcher = opts.fetchLatest ?? fetchLatestWotannVersion;
125
+ let latest;
126
+ try {
127
+ latest = await fetcher(opts.timeoutMs ?? 1500);
128
+ }
129
+ catch {
130
+ return null;
131
+ }
132
+ if (latest === null)
133
+ return null;
134
+ if (compareVersions(opts.currentVersion, latest) >= 0)
135
+ return null;
136
+ const sink = opts.stderr ?? process.stderr;
137
+ const line = HINT_LINE(opts.currentVersion, latest);
138
+ try {
139
+ sink.write(line);
140
+ }
141
+ catch {
142
+ return null;
143
+ }
144
+ return line;
145
+ }
package/dist/index.js CHANGED
@@ -349,10 +349,26 @@ program
349
349
  return; // thin TUI rendered and exited
350
350
  // else fall through to full runtime
351
351
  }
352
- const ReactModule = await import("react");
353
- const { bootstrapInteractiveSession } = await import("./ui/bootstrap.js");
352
+ // Parallel-load the 3 heavy boot modules + the lightweight npx
353
+ // self-update-hint module, then fire the hint check in parallel
354
+ // with the runtime bootstrap. Net cost: zero added latency in the
355
+ // common case (the registry call completes during bootstrap);
356
+ // worst case: a sub-second wait if the network is slow.
357
+ const [ReactModule, { bootstrapInteractiveSession }, { printNpxUpgradeHint }] = await Promise.all([
358
+ import("react"),
359
+ import("./ui/bootstrap.js"),
360
+ import("./cli/npx-self-update-hint.js"),
361
+ ]);
354
362
  const React = ReactModule.default;
363
+ const npxHintPromise = printNpxUpgradeHint({
364
+ currentVersion: VERSION,
365
+ currentDirname: dirname(fileURLToPath(import.meta.url)),
366
+ }).catch(() => null);
355
367
  let interactive = await bootstrapInteractiveSession(process.cwd(), options);
368
+ // Drain the hint promise so the line lands BEFORE the TUI takes
369
+ // the screen (or before we exit on refusal). It's a best-effort
370
+ // no-op when not applicable.
371
+ await npxHintPromise;
356
372
  if (options.workspace !== undefined) {
357
373
  const { writeWorkspaceResumeRecord } = await import("./cli/workspace-resume.js");
358
374
  writeWorkspaceResumeRecord({
@@ -21,6 +21,7 @@
21
21
  */
22
22
  import type { ReactElement } from "react";
23
23
  import type { Instance } from "ink";
24
+ import { type StdinDiagnosis } from "./raw-mode-guard.js";
24
25
  /** Actionable guidance when there is no raw-mode-capable terminal. */
25
26
  export declare const DEFAULT_REFUSAL_MESSAGE: string;
26
27
  type InkRender = (el: ReactElement, opts?: {
@@ -53,6 +54,14 @@ export interface MountInteractiveOptions {
53
54
  * the stderr diagnostic stays visible in the main buffer.
54
55
  */
55
56
  readonly onResolved?: () => void;
57
+ /**
58
+ * Override the stdin diagnostic walker (tests). Default: the real
59
+ * `diagnoseStdin` from raw-mode-guard. The diagnosis is appended to
60
+ * the DEFAULT refusal message — when a `refusalMessage` is provided
61
+ * explicitly, the diagnosis is NOT appended (preserve strict-equality
62
+ * test contracts and let callers control the full output).
63
+ */
64
+ readonly diagnoseStdinFn?: () => StdinDiagnosis;
56
65
  }
57
66
  export interface MountInteractiveResult {
58
67
  /**
@@ -20,7 +20,7 @@
20
20
  * bug class cannot silently regress when a new mount is added.
21
21
  */
22
22
  import { ensureRenderableViewport } from "./viewport-guard.js";
23
- import { resolveInteractiveStdin } from "./raw-mode-guard.js";
23
+ import { resolveInteractiveStdin, diagnoseStdin, formatStdinDiagnosis, } from "./raw-mode-guard.js";
24
24
  /** Actionable guidance when there is no raw-mode-capable terminal. */
25
25
  export const DEFAULT_REFUSAL_MESSAGE = "[wotann] No raw-mode-capable terminal available for the interactive TUI.\n" +
26
26
  " stdin is piped or the launcher degraded the TTY (some `npx`\n" +
@@ -51,7 +51,24 @@ export async function mountInteractiveInk(element, opts = {}) {
51
51
  const inputStdin = resolve();
52
52
  if (inputStdin === null) {
53
53
  const sink = opts.stderr ?? process.stderr;
54
- sink.write(opts.refusalMessage ?? DEFAULT_REFUSAL_MESSAGE);
54
+ if (opts.refusalMessage !== undefined) {
55
+ // Caller provided a full custom message — write it verbatim, no
56
+ // diagnosis appended (preserves strict-equality test contracts).
57
+ sink.write(opts.refusalMessage);
58
+ }
59
+ else {
60
+ sink.write(DEFAULT_REFUSAL_MESSAGE);
61
+ // Append a per-step diagnosis so the user sees WHY the guard
62
+ // refused (which step failed and what it observed). Future bug
63
+ // reports include the evidence automatically.
64
+ try {
65
+ const diagnose = opts.diagnoseStdinFn ?? diagnoseStdin;
66
+ sink.write(formatStdinDiagnosis(diagnose()));
67
+ }
68
+ catch {
69
+ // Diagnostic must never block the refusal — swallow.
70
+ }
71
+ }
55
72
  return { instance: null, refused: true };
56
73
  }
57
74
  const inkRender = opts.inkRender ?? (await import("ink")).render;
@@ -53,3 +53,40 @@ export interface ResolveStdinOptions {
53
53
  * Never throws.
54
54
  */
55
55
  export declare function resolveInteractiveStdin(stdin?: unknown, opts?: ResolveStdinOptions): NodeJS.ReadStream | null;
56
+ /**
57
+ * Structured per-step diagnosis of why the resolver did or did not return
58
+ * a raw-capable stream. The shape mirrors the steps of
59
+ * `resolveInteractiveStdin` so a "guard refused under a raw-capable
60
+ * terminal" report carries enough evidence to identify which step failed
61
+ * without shipping a custom diagnostic build.
62
+ */
63
+ export interface StdinDiagnosis {
64
+ /** Whatever `stdin.isTTY` returned (true/false/undefined/etc.). */
65
+ readonly stdinIsTTY: unknown;
66
+ /** Whether `stdin.setRawMode` is a function (not whether it works). */
67
+ readonly stdinHasSetRawMode: boolean;
68
+ /** "ok" if the probe call succeeded, "missing" if no setRawMode, else the error message. */
69
+ readonly stdinProbeResult: string;
70
+ /** "ok", "not-attempted", "returned null", or the open error message. */
71
+ readonly ttyOpenResult: string;
72
+ /** Whatever the opened /dev/tty stream's `isTTY` returned, when applicable. */
73
+ readonly ttyIsTTY?: unknown;
74
+ /** Whether the opened /dev/tty stream has a `setRawMode` function. */
75
+ readonly ttyHasSetRawMode?: boolean;
76
+ /** "ok", "missing", or the error message — only set when /dev/tty open succeeded. */
77
+ readonly ttyProbeResult?: string;
78
+ }
79
+ /**
80
+ * Walks the same `resolveInteractiveStdin` steps and records what each
81
+ * one saw. Use this when the resolver returns `null` to print a
82
+ * self-explanatory refusal — future bug reports will include the
83
+ * diagnosis automatically instead of just "guard refused, why?".
84
+ */
85
+ export declare function diagnoseStdin(stdin?: unknown, opts?: ResolveStdinOptions): StdinDiagnosis;
86
+ /**
87
+ * Format a `StdinDiagnosis` as a short multi-line human-readable block
88
+ * suitable for stderr. Compact (~4 lines) so it doesn't dominate the
89
+ * refusal output but carries every piece of evidence needed to
90
+ * identify which step failed.
91
+ */
92
+ export declare function formatStdinDiagnosis(d: StdinDiagnosis): string;
@@ -91,3 +91,96 @@ export function resolveInteractiveStdin(stdin = process.stdin, opts = {}) {
91
91
  }
92
92
  return isRawModeCapable(tty) ? tty : null;
93
93
  }
94
+ /**
95
+ * Same observation pattern as `isRawModeCapable` but captures the
96
+ * probe outcome instead of collapsing to true/false. Pure, never throws.
97
+ */
98
+ function probeStdinDetail(stream) {
99
+ if (stream === null || typeof stream !== "object") {
100
+ return { isTTY: stream, hasSetRawMode: false, probeResult: "missing" };
101
+ }
102
+ const s = stream;
103
+ const hasSetRawMode = typeof s.setRawMode === "function";
104
+ if (s.isTTY !== true || !hasSetRawMode) {
105
+ return {
106
+ isTTY: s.isTTY,
107
+ hasSetRawMode,
108
+ probeResult: hasSetRawMode ? "skipped (isTTY!=true)" : "missing",
109
+ };
110
+ }
111
+ try {
112
+ const setRawMode = s.setRawMode;
113
+ const currentIsRaw = s.isRaw === true;
114
+ setRawMode(currentIsRaw);
115
+ return { isTTY: true, hasSetRawMode: true, probeResult: "ok" };
116
+ }
117
+ catch (e) {
118
+ return {
119
+ isTTY: true,
120
+ hasSetRawMode: true,
121
+ probeResult: e instanceof Error ? e.message : String(e),
122
+ };
123
+ }
124
+ }
125
+ /**
126
+ * Walks the same `resolveInteractiveStdin` steps and records what each
127
+ * one saw. Use this when the resolver returns `null` to print a
128
+ * self-explanatory refusal — future bug reports will include the
129
+ * diagnosis automatically instead of just "guard refused, why?".
130
+ */
131
+ export function diagnoseStdin(stdin = process.stdin, opts = {}) {
132
+ const stdinProbe = probeStdinDetail(stdin);
133
+ if (stdinProbe.probeResult === "ok") {
134
+ return {
135
+ stdinIsTTY: stdinProbe.isTTY,
136
+ stdinHasSetRawMode: stdinProbe.hasSetRawMode,
137
+ stdinProbeResult: stdinProbe.probeResult,
138
+ ttyOpenResult: "not-attempted",
139
+ };
140
+ }
141
+ const open = opts.openControllingTty ?? defaultOpenControllingTty;
142
+ let tty = null;
143
+ let ttyOpenResult;
144
+ try {
145
+ tty = open();
146
+ ttyOpenResult = tty === null ? "returned null" : "ok";
147
+ }
148
+ catch (e) {
149
+ ttyOpenResult = e instanceof Error ? e.message : String(e);
150
+ }
151
+ if (tty === null) {
152
+ return {
153
+ stdinIsTTY: stdinProbe.isTTY,
154
+ stdinHasSetRawMode: stdinProbe.hasSetRawMode,
155
+ stdinProbeResult: stdinProbe.probeResult,
156
+ ttyOpenResult,
157
+ };
158
+ }
159
+ const ttyProbe = probeStdinDetail(tty);
160
+ return {
161
+ stdinIsTTY: stdinProbe.isTTY,
162
+ stdinHasSetRawMode: stdinProbe.hasSetRawMode,
163
+ stdinProbeResult: stdinProbe.probeResult,
164
+ ttyOpenResult,
165
+ ttyIsTTY: ttyProbe.isTTY,
166
+ ttyHasSetRawMode: ttyProbe.hasSetRawMode,
167
+ ttyProbeResult: ttyProbe.probeResult,
168
+ };
169
+ }
170
+ /**
171
+ * Format a `StdinDiagnosis` as a short multi-line human-readable block
172
+ * suitable for stderr. Compact (~4 lines) so it doesn't dominate the
173
+ * refusal output but carries every piece of evidence needed to
174
+ * identify which step failed.
175
+ */
176
+ export function formatStdinDiagnosis(d) {
177
+ const lines = [
178
+ " Diagnostic:",
179
+ ` stdin: isTTY=${String(d.stdinIsTTY)} setRawMode=${d.stdinHasSetRawMode ? "yes" : "no"} probe=${d.stdinProbeResult}`,
180
+ ` /dev/tty: open=${d.ttyOpenResult}`,
181
+ ];
182
+ if (d.ttyProbeResult !== undefined) {
183
+ lines.push(` isTTY=${String(d.ttyIsTTY)} setRawMode=${d.ttyHasSetRawMode ? "yes" : "no"} probe=${d.ttyProbeResult}`);
184
+ }
185
+ return lines.join("\n") + "\n";
186
+ }
package/install.sh CHANGED
@@ -96,10 +96,42 @@ if ! command -v npm >/dev/null 2>&1; then
96
96
  exit 1
97
97
  fi
98
98
 
99
+ # ---------------------------------------------------------------- Disk-space pre-flight
100
+ # Partial installs are the #1 silent-failure class on disk-pressured
101
+ # systems: `npm install -g` exits 0, but dist/index.js gets truncated
102
+ # during the atomic rename, leaving a wotann binary on PATH that runs
103
+ # `node empty.js` → exits 0 with zero output. Refuse install when free
104
+ # space is below 5GB so the user notices BEFORE the install pretends
105
+ # to succeed. Override with WOTANN_FORCE_INSTALL=1 if you know better.
106
+ AVAIL_KB="$(df -k "$HOME" 2>/dev/null | awk 'NR==2 {print $4}')"
107
+ if [ -n "${AVAIL_KB:-}" ] && [ "$AVAIL_KB" -lt 5242880 ]; then
108
+ AVAIL_GB="$(( AVAIL_KB / 1048576 ))"
109
+ echo -e "${YELLOW}WARN: Only ${AVAIL_GB}GB free on \$HOME — risky for npm install -g.${NC}"
110
+ echo -e "${DIM} Below 5GB, partial installs can leave a broken binary on PATH"
111
+ echo -e "${DIM} (causes 'wotann does nothing'). Free some space or override:${NC}"
112
+ echo -e "${DIM} WOTANN_FORCE_INSTALL=1 ./install.sh${NC}"
113
+ if [ "${WOTANN_FORCE_INSTALL:-0}" != "1" ]; then
114
+ exit 1
115
+ fi
116
+ fi
117
+
99
118
  # ---------------------------------------------------------------- Idempotency
119
+ # Broken-install detection: if wotann is on PATH but `--version` doesn't
120
+ # print a `\d+\.\d+\.\d+` within 5 seconds, the previous install is
121
+ # half-applied. Uninstall before reinstalling so we don't keep the
122
+ # broken bin on PATH and shadow the fresh `npm install -g` we're about
123
+ # to run. This is the failure mode that masked 3 prior "fixes" — see
124
+ # case_npm_install_global_postinstall.md.
100
125
  if command -v wotann >/dev/null 2>&1; then
101
- CURRENT="$(wotann --version 2>/dev/null || echo unknown)"
102
- echo -e "${DIM}Existing install detected: ${CURRENT}. Reinstalling...${NC}"
126
+ CURRENT="$(perl -e 'alarm 5; exec @ARGV' wotann --version 2>/dev/null || true)"
127
+ if [ -z "$CURRENT" ] || ! printf "%s" "$CURRENT" | grep -qE "^[0-9]+\.[0-9]+\.[0-9]+"; then
128
+ echo -e "${YELLOW}Existing wotann install is broken (no version output).${NC}"
129
+ echo -e "${DIM}Uninstalling before fresh install...${NC}"
130
+ npm uninstall -g wotann >/dev/null 2>&1 || true
131
+ hash -r 2>/dev/null || true
132
+ else
133
+ echo -e "${DIM}Existing install detected: ${CURRENT}. Reinstalling...${NC}"
134
+ fi
103
135
  fi
104
136
 
105
137
  # ---------------------------------------------------------------- Install
@@ -161,7 +193,20 @@ if ! command -v wotann >/dev/null 2>&1; then
161
193
  case ":$PATH:" in *":$NPM_BIN:"*) ;; *) PATH="$NPM_BIN:$PATH" ;; esac
162
194
  fi
163
195
  if command -v wotann >/dev/null 2>&1; then
164
- echo -e "${GREEN}OK: WOTANN $(wotann --version) installed${NC}"
196
+ # Post-install verification: a successful `npm install -g` doesn't
197
+ # imply a working binary. Time-limit the version probe so a hang
198
+ # from a partially-installed dist/index.js doesn't lock the script.
199
+ VERIFY_OUT="$(perl -e 'alarm 10; exec @ARGV' wotann --version 2>&1 || true)"
200
+ if [ -z "$VERIFY_OUT" ] || ! printf "%s" "$VERIFY_OUT" | grep -qE "^[0-9]+\.[0-9]+\.[0-9]+"; then
201
+ echo -e "${RED}ERROR: wotann installed but \`--version\` returned no version string.${NC}"
202
+ echo -e "${DIM} Captured output: ${VERIFY_OUT:-<empty>}${NC}"
203
+ echo -e "${DIM} This is the partial-install class — likely disk pressure or"
204
+ echo -e "${DIM} a postinstall script that didn't complete. Recovery:${NC}"
205
+ echo -e "${DIM} npm uninstall -g wotann${NC}"
206
+ echo -e "${DIM} # free disk space (need ≥5GB), then re-run install.sh${NC}"
207
+ exit 1
208
+ fi
209
+ echo -e "${GREEN}OK: WOTANN ${VERIFY_OUT} installed and verified${NC}"
165
210
  else
166
211
  echo -e "${RED}wotann command still not on PATH. Add npm global bin to PATH:${NC}"
167
212
  echo -e "${DIM} export PATH=\"\$(npm bin -g):\$PATH\"${NC}"
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "wotann",
3
- "version": "0.5.91",
3
+ "version": "0.5.92",
4
4
  "description": "WOTANN — The All-Father of AI Agent Harnesses",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",