probe-research 0.10.6 → 0.11.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.
@@ -27,6 +27,20 @@ const os = require("node:os");
27
27
 
28
28
  const DIST = "probe-research";
29
29
 
30
+ /**
31
+ * Where the published-version manifest lives. The SessionStart hook already
32
+ * reads this exact endpoint to decide whether to nudge, so the launcher and the
33
+ * nudge cannot disagree about what "latest" means.
34
+ *
35
+ * `PROBE_BASE_URL` is honoured for the same reason the hook honours it: a
36
+ * self-hosted tenant's latest is not this one's, and asking the wrong host
37
+ * would tell them they are behind forever.
38
+ */
39
+ const MANIFEST_PATH = "/v1/client-version";
40
+ const DEFAULT_BASE = "https://api.research.prbe.ai";
41
+ /** How long a currency check may delay a command that could already run. */
42
+ const MANIFEST_TIMEOUT_MS = 1500;
43
+
30
44
  /**
31
45
  * The minimum CLI version this launcher needs — NOT this package's own version.
32
46
  *
@@ -86,10 +100,43 @@ function installedVersion() {
86
100
  return m ? m[1] : null;
87
101
  }
88
102
 
103
+ function baseUrl() {
104
+ const env = process.env.PROBE_BASE_URL;
105
+ if (env && /^https?:\/\//.test(env)) return env.replace(/\/+$/, "");
106
+ return DEFAULT_BASE;
107
+ }
108
+
109
+ /**
110
+ * The newest published CLI, or null if that cannot be established quickly.
111
+ *
112
+ * NULL IS THE IMPORTANT RETURN. Offline, behind a proxy, self-hosted with the
113
+ * route absent, or simply slow -- every one of those must leave a working local
114
+ * install runnable. A currency check that can strand a user offline is worse
115
+ * than the staleness it fixes, so every failure here falls open.
116
+ */
117
+ async function latestVersion() {
118
+ try {
119
+ const res = await fetch(`${baseUrl()}${MANIFEST_PATH}`, {
120
+ signal: AbortSignal.timeout(MANIFEST_TIMEOUT_MS),
121
+ headers: { accept: "application/json" },
122
+ });
123
+ if (!res.ok) return null;
124
+ const body = await res.json();
125
+ const found = body && body.cli && body.cli.latest;
126
+ return typeof found === "string" && /^\d+(\.\d+)+$/.test(found) ? found : null;
127
+ } catch {
128
+ return null;
129
+ }
130
+ }
131
+
89
132
  function has(cmd) {
90
- const probe = process.platform === "win32" ? "where" : "command";
91
- const args = process.platform === "win32" ? [cmd] : ["-v", cmd];
92
- const r = spawnSync(probe, args, { stdio: "ignore", shell: true });
133
+ // ONE shell string, never (command, args[], shell:true): Node deprecated that
134
+ // pairing in DEP0190 because the args are concatenated rather than escaped,
135
+ // and it printed a security warning on every single launcher run. `cmd` is a
136
+ // literal at all three call sites, so there is nothing to escape here -- but
137
+ // a warning users see on the from-zero entry point is its own problem.
138
+ const line = process.platform === "win32" ? `where ${cmd}` : `command -v ${cmd}`;
139
+ const r = spawnSync(line, { stdio: "ignore", shell: true });
93
140
  return r.status === 0;
94
141
  }
95
142
 
@@ -104,27 +151,50 @@ function run(cmd, args) {
104
151
  process.exit(r.status === null ? 1 : r.status);
105
152
  }
106
153
 
107
- function main() {
154
+ async function main() {
108
155
  const args = process.argv.slice(2);
109
156
  // `npx probe-research` with no arguments runs the wizard — that is the entire
110
157
  // reason this package exists, so it should not require remembering a verb.
111
158
  const forwarded = args.length ? args : ["wizard"];
112
159
  const wanted = MIN_CLI;
113
160
 
114
- // Only hand off to an existing install if it is at least as new as this
115
- // launcher. Otherwise fall through and let uv/pipx fetch a matching CLI.
161
+ // Hand off to an existing install only when it is BOTH above the floor and
162
+ // current. The floor alone is not enough, and that was the bug: `npx <tool>`
163
+ // means "run the latest", but a floor is satisfied forever, so anyone who had
164
+ // ever installed the CLI was frozen on it and npx silently never delivered
165
+ // another update.
166
+ //
167
+ // That is the same failure the `--refresh` flag below exists to fix -- uv
168
+ // serving whatever it resolved on day one -- in the other branch. It was
169
+ // fixed there and left standing here.
170
+ let latest = null;
116
171
  if (has("probe")) {
117
172
  const found = installedVersion();
118
- if (found && atLeast(found, wanted)) return run("probe", forwarded);
119
- console.error(
120
- `probe-research: installed probe ${found || "(unknown version)"} is older than ` +
121
- `${wanted}; fetching the latest…`,
122
- );
173
+ if (found && atLeast(found, wanted)) {
174
+ latest = await latestVersion();
175
+ // Unknown latest means run what we have: see latestVersion's contract.
176
+ if (!latest || atLeast(found, latest)) return run("probe", forwarded);
177
+ console.error(
178
+ `probe-research: probe ${found} is behind ${latest}; fetching the latest…`,
179
+ );
180
+ } else {
181
+ console.error(
182
+ `probe-research: installed probe ${found || "(unknown version)"} is older than ` +
183
+ `${wanted}; fetching the latest…`,
184
+ );
185
+ }
123
186
  }
124
187
  // A FLOOR, not an exact pin. Unpinned, `uv tool run --from probe-research`
125
188
  // reuses an already-installed 0.8.2 tool; pinned exactly, it demands a PyPI
126
189
  // version that may not exist. `>=` fixes both.
127
- const spec = `${DIST}>=${wanted}`;
190
+ // Resolve to the newest version we actually know about, not merely the floor.
191
+ // `uv tool run` reuses an installed tool that satisfies the constraint, so a
192
+ // stale 0.46.0 satisfies `>=0.36.0` and uv hands back the very version we
193
+ // just decided was out of date -- the currency check above would print
194
+ // "fetching the latest" and then change nothing. Measured exactly that before
195
+ // this line existed.
196
+ const target = latest && atLeast(latest, wanted) ? latest : wanted;
197
+ const spec = `${DIST}>=${target}`;
128
198
  // REFRESH. uv caches the ENVIRONMENT it built for a requirement, so a range
129
199
  // like `>=0.10.0` keeps serving whatever it resolved the FIRST time — every
130
200
  // user frozen on the version they happened to run on day one, with the
@@ -162,8 +232,13 @@ function main() {
162
232
  // because it was resolved before the install ran.
163
233
  const uv = `${os.homedir()}/.local/bin/uv`;
164
234
  return run(uv, [
165
- "tool", "run", "--refresh", "--from", `${DIST}>=${wanted}`, "probe", ...forwarded,
235
+ "tool", "run", "--refresh", "--from", spec, "probe", ...forwarded,
166
236
  ]);
167
237
  }
168
238
 
169
- main();
239
+ main().catch((err) => {
240
+ // A throw here would exit 0 under an unhandled rejection on some Node builds,
241
+ // which reads as success to whatever invoked the launcher.
242
+ console.error(`probe-research: ${err && err.message ? err.message : err}`);
243
+ process.exit(1);
244
+ });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "probe-research",
3
- "version": "0.10.6",
3
+ "version": "0.11.0",
4
4
  "description": "Setup wizard for Probe Research in Claude Code. Runs from zero with npx.",
5
5
  "bin": {
6
6
  "probe-research": "bin/probe-research.js"