tokenjam 0.6.8 → 0.6.10

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.
Files changed (2) hide show
  1. package/bin/tj.js +90 -6
  2. package/package.json +1 -1
package/bin/tj.js CHANGED
@@ -31,9 +31,19 @@
31
31
  * tokenjam==<version>` / `--spec tokenjam==<version>` to this wrapper's OWN
32
32
  * version (kept in sync with the release tag by publish-npm.yml's `npm
33
33
  * version ${GITHUB_REF_NAME#v}` step) forces the resolver past that shortcut,
34
- * so `npx tokenjam` always runs the release it shipped with. If the pinned
35
- * spec can't be resolved yet (this wrapper published slightly ahead of PyPI
36
- * propagation), we fall back to the unpinned form rather than fail outright.
34
+ * so `npx tokenjam` always runs the release it shipped with.
35
+ *
36
+ * Fail-closed on an unresolved pin: publish-npm.yml's wrapper job now blocks
37
+ * on the matching PyPI release actually being visible before it publishes
38
+ * this wrapper, so "pinned spec doesn't resolve" should no longer happen in
39
+ * the ordinary run. Silently falling back to the unpinned form here traded a
40
+ * loud, correct, RETRYABLE failure for a silent PERMANENT one: uv/pipx cache
41
+ * whatever the unpinned spec resolves to and never re-resolve on their own,
42
+ * so one unlucky run during any remaining propagation gap left a stale
43
+ * version cached forever — surfacing as "0.6.2 shipped a palette rework but
44
+ * npx still shows the old colors" with no obvious cause. A resolution
45
+ * failure is now reported and the next runner (or an already-installed PATH
46
+ * `tj`) is tried instead — never a silent unpinned run.
37
47
  *
38
48
  * Staleness note: pinning only fixes what THIS wrapper runs. A bare `tj`
39
49
  * invoked directly (no `npx`) still runs whatever was separately installed
@@ -44,6 +54,7 @@
44
54
  "use strict";
45
55
 
46
56
  const { spawnSync } = require("child_process");
57
+ const fs = require("fs");
47
58
  const path = require("path");
48
59
 
49
60
  // PyPI package name vs. command name differ (`tokenjam` ships the `tj` script),
@@ -102,6 +113,43 @@ function resolves(bin, args) {
102
113
  return probe.status === 0;
103
114
  }
104
115
 
116
+ // Resolve `bin` to an absolute path the way the OS would launch it, without
117
+ // spawning it — a plain PATH walk, no shell involved.
118
+ function resolveOnPath(bin) {
119
+ const dirs = (process.env.PATH || "").split(path.delimiter);
120
+ const exts =
121
+ process.platform === "win32"
122
+ ? (process.env.PATHEXT || ".EXE;.CMD;.BAT").split(";")
123
+ : [""];
124
+ for (const dir of dirs) {
125
+ if (!dir) continue;
126
+ for (const ext of exts) {
127
+ const candidate = path.join(dir, bin + ext);
128
+ try {
129
+ fs.accessSync(candidate, fs.constants.X_OK);
130
+ return candidate;
131
+ } catch {
132
+ // not here, keep looking
133
+ }
134
+ }
135
+ }
136
+ return null;
137
+ }
138
+
139
+ // True when `candidatePath` resolves (through symlinks) to this very file.
140
+ // `npx` symlinks this package's own `tj`/`tokenjam` bin into a temp bin dir
141
+ // and puts that dir on PATH, so a naive "is tj on PATH" probe finds
142
+ // ourselves — re-spawning that would recurse into this same wrapper forever
143
+ // instead of ever reaching a real, separately-installed CLI.
144
+ function isOwnShim(candidatePath) {
145
+ if (!candidatePath) return false;
146
+ try {
147
+ return fs.realpathSync(candidatePath) === fs.realpathSync(__filename);
148
+ } catch {
149
+ return false; // couldn't stat either side — don't claim a match
150
+ }
151
+ }
152
+
105
153
  // --- stale shadowing install detection ------------------------------------
106
154
  //
107
155
  // Detect-and-tell only, per design: this never mutates anything and never
@@ -247,11 +295,36 @@ function main() {
247
295
  : { ...process.env, TJ_NPX_ZERO_INSTALL_REPORT: "1" };
248
296
 
249
297
  const version = ownVersion();
298
+ let sawUnresolvedPin = false;
250
299
 
251
300
  for (const { bin, pinnedPrefix, prefix } of runners(version)) {
252
- if (!has(bin)) continue;
253
- const args =
254
- pinnedPrefix && resolves(bin, pinnedPrefix) ? pinnedPrefix : prefix;
301
+ if (bin === COMMAND) {
302
+ // The installed-CLI fallback: only real if it resolves to something
303
+ // OTHER than this wrapper (see isOwnShim above). Under npx, `has(bin)`
304
+ // alone would find our own re-exec'd shim and spawn it recursively.
305
+ const resolved = resolveOnPath(bin);
306
+ if (!resolved || isOwnShim(resolved)) continue;
307
+ } else if (!has(bin)) {
308
+ continue;
309
+ }
310
+
311
+ let args;
312
+ if (pinnedPrefix) {
313
+ if (!resolves(bin, pinnedPrefix)) {
314
+ // Fail closed: never silently drop to the unpinned spec (see the
315
+ // version-pinning comment at the top of this file for why). Report
316
+ // and move on to the next runner instead.
317
+ sawUnresolvedPin = true;
318
+ process.stderr.write(
319
+ `Note: ${bin} can't resolve tokenjam==${version} yet — skipping it rather than running an unpinned (potentially stale-cached) version.\n`
320
+ );
321
+ continue;
322
+ }
323
+ args = pinnedPrefix;
324
+ } else {
325
+ args = prefix;
326
+ }
327
+
255
328
  const result = spawnSync(bin, [...args, ...passthrough], {
256
329
  stdio: "inherit",
257
330
  env: childEnv,
@@ -261,6 +334,17 @@ function main() {
261
334
  process.exit(result.status === null ? 1 : result.status);
262
335
  }
263
336
 
337
+ if (sawUnresolvedPin) {
338
+ process.stderr.write(
339
+ "\n" +
340
+ `tj (TokenJam): couldn't resolve tokenjam==${version} on PyPI through uv/pipx, and no already-installed 'tj' was found on PATH.\n` +
341
+ "This should be transient — PyPI's CDN can lag briefly right after a release. Wait a minute and re-run `npx tokenjam`.\n" +
342
+ "If it persists, check https://pypi.org/project/tokenjam/#history for the release, or install an unpinned copy yourself:\n" +
343
+ " uvx --from tokenjam tj / pipx run --spec tokenjam tj\n\n"
344
+ );
345
+ process.exit(1);
346
+ }
347
+
264
348
  process.stderr.write(
265
349
  "\n" +
266
350
  "tj (TokenJam) needs a Python runner to launch its CLI.\n" +
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "tokenjam",
3
- "version": "0.6.8",
3
+ "version": "0.6.10",
4
4
  "description": "Zero-install launcher for TokenJam (tj): npx tokenjam runs the Python CLI via uvx/pipx and reports the recurring mistakes your AI agent keeps repeating, no setup required.",
5
5
  "keywords": [
6
6
  "claude-code",