tokenjam 0.6.9 → 0.6.11
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/bin/tj.js +90 -6
- 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.
|
|
35
|
-
*
|
|
36
|
-
*
|
|
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 (
|
|
253
|
-
|
|
254
|
-
|
|
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.
|
|
3
|
+
"version": "0.6.11",
|
|
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",
|