tokenjam 0.6.0 → 0.6.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/bin/tj.js +182 -62
- package/package.json +1 -1
package/bin/tj.js
CHANGED
|
@@ -12,8 +12,8 @@
|
|
|
12
12
|
* command.
|
|
13
13
|
*
|
|
14
14
|
* Runner preference (first that exists wins):
|
|
15
|
-
* 1. `uvx --from tokenjam tj …` — fully ephemeral, downloads nothing global
|
|
16
|
-
* 2. `pipx run --spec tokenjam tj …`
|
|
15
|
+
* 1. `uvx --from tokenjam==<own version> tj …` — fully ephemeral, downloads nothing global
|
|
16
|
+
* 2. `pipx run --spec tokenjam==<own version> tj …`
|
|
17
17
|
* 3. `tj …` — an already-installed CLI on PATH
|
|
18
18
|
*
|
|
19
19
|
* If none are present we print actionable install guidance and exit non-zero.
|
|
@@ -23,20 +23,27 @@
|
|
|
23
23
|
* wrapper keeps the explicit `--from tokenjam tj` / `--spec tokenjam tj` form
|
|
24
24
|
* below for back-compat with the 0.5.3 and earlier releases it also targets.
|
|
25
25
|
*
|
|
26
|
-
*
|
|
27
|
-
*
|
|
28
|
-
*
|
|
29
|
-
*
|
|
30
|
-
*
|
|
31
|
-
*
|
|
32
|
-
*
|
|
33
|
-
*
|
|
26
|
+
* Version pinning: `uv`/`pipx` cache a resolved tool environment and reuse it
|
|
27
|
+
* forever unless the requested spec changes — an unpinned `--from tokenjam`
|
|
28
|
+
* silently keeps reusing whatever was resolved first (e.g. a prior `uv tool
|
|
29
|
+
* install tokenjam` at an old version), never re-resolving on its own, no
|
|
30
|
+
* matter how many newer releases hit PyPI since. Pinning `--from
|
|
31
|
+
* tokenjam==<version>` / `--spec tokenjam==<version>` to this wrapper's OWN
|
|
32
|
+
* version (kept in sync with the release tag by publish-npm.yml's `npm
|
|
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.
|
|
37
|
+
*
|
|
38
|
+
* Staleness note: pinning only fixes what THIS wrapper runs. A bare `tj`
|
|
39
|
+
* invoked directly (no `npx`) still runs whatever was separately installed
|
|
40
|
+
* via `uv tool install` / `pipx install` / `pip install` / Homebrew, which can
|
|
41
|
+
* sit on an old version indefinitely. See `warnIfShadowedByStaleInstall`
|
|
42
|
+
* below: detect-and-tell only, never mutates, never auto-upgrades.
|
|
34
43
|
*/
|
|
35
44
|
"use strict";
|
|
36
45
|
|
|
37
46
|
const { spawnSync } = require("child_process");
|
|
38
|
-
const fs = require("fs");
|
|
39
|
-
const os = require("os");
|
|
40
47
|
const path = require("path");
|
|
41
48
|
|
|
42
49
|
// PyPI package name vs. command name differ (`tokenjam` ships the `tj` script),
|
|
@@ -50,65 +57,177 @@ function has(bin) {
|
|
|
50
57
|
return probe.status === 0 || probe.status === 1; // 1 = exists but no --version
|
|
51
58
|
}
|
|
52
59
|
|
|
53
|
-
|
|
60
|
+
// This wrapper's own version. `publish-npm.yml`'s wrapper-publish job runs
|
|
61
|
+
// `npm version ${GITHUB_REF_NAME#v}` against npm-wrapper/package.json before
|
|
62
|
+
// `npm publish`, so whatever version this file ships inside always matches
|
|
63
|
+
// the tokenjam release it was cut alongside — safe to read at runtime as the
|
|
64
|
+
// version to pin the Python side to.
|
|
65
|
+
function ownVersion() {
|
|
66
|
+
try {
|
|
67
|
+
return require(path.join(__dirname, "..", "package.json")).version;
|
|
68
|
+
} catch {
|
|
69
|
+
return null;
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function runners(version) {
|
|
74
|
+
const pinnedSpec = version ? `${PACKAGE}==${version}` : null;
|
|
54
75
|
return [
|
|
55
|
-
{
|
|
56
|
-
|
|
57
|
-
|
|
76
|
+
{
|
|
77
|
+
bin: "uvx",
|
|
78
|
+
pinnedPrefix: pinnedSpec ? ["--from", pinnedSpec, COMMAND] : null,
|
|
79
|
+
prefix: ["--from", PACKAGE, COMMAND],
|
|
80
|
+
},
|
|
81
|
+
{
|
|
82
|
+
bin: "pipx",
|
|
83
|
+
pinnedPrefix: pinnedSpec ? ["run", "--spec", pinnedSpec, COMMAND] : null,
|
|
84
|
+
prefix: ["run", "--spec", PACKAGE, COMMAND],
|
|
85
|
+
},
|
|
86
|
+
{ bin: COMMAND, pinnedPrefix: null, prefix: [] }, // already installed on PATH, nothing to pin
|
|
58
87
|
];
|
|
59
88
|
}
|
|
60
89
|
|
|
61
|
-
//
|
|
62
|
-
//
|
|
63
|
-
//
|
|
64
|
-
//
|
|
65
|
-
//
|
|
66
|
-
//
|
|
67
|
-
//
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
function
|
|
72
|
-
const
|
|
73
|
-
|
|
74
|
-
xdgCacheHome && xdgCacheHome.trim()
|
|
75
|
-
? xdgCacheHome
|
|
76
|
-
: path.join(os.homedir(), ".cache");
|
|
77
|
-
return path.join(base, "tokenjam-npx");
|
|
90
|
+
// Cheap, side-effect-free resolution probe: does `<bin> <args> --version`
|
|
91
|
+
// succeed? Used to decide, before the real invocation, whether the pinned
|
|
92
|
+
// package spec actually resolves on this runner — falls back to the
|
|
93
|
+
// unpinned prefix when it doesn't (wrapper published ahead of PyPI
|
|
94
|
+
// propagation). Unlike `has()`, this requires an exact status 0: `uv`
|
|
95
|
+
// exits 1 both for "resolution failed" AND for "binary ran fine but
|
|
96
|
+
// doesn't understand --version", so treating 1 as success here would
|
|
97
|
+
// silently paper over real resolution failures (verified: `uvx --from
|
|
98
|
+
// tokenjam==<bogus> tj --version` also exits 1, indistinguishable from the
|
|
99
|
+
// unsupported-flag case `has()` is built around).
|
|
100
|
+
function resolves(bin, args) {
|
|
101
|
+
const probe = spawnSync(bin, [...args, "--version"], { stdio: "ignore" });
|
|
102
|
+
return probe.status === 0;
|
|
78
103
|
}
|
|
79
104
|
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
105
|
+
// --- stale shadowing install detection ------------------------------------
|
|
106
|
+
//
|
|
107
|
+
// Detect-and-tell only, per design: this never mutates anything and never
|
|
108
|
+
// upgrades on the user's behalf, in interactive or non-interactive/CI
|
|
109
|
+
// contexts alike. It's a best-effort nudge — any detection failure (missing
|
|
110
|
+
// binary, unexpected output, timeout) is swallowed and skipped silently; it
|
|
111
|
+
// must never break or slow down the primary command above by much.
|
|
83
112
|
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
function shouldRefresh() {
|
|
113
|
+
const DETECT_TIMEOUT_MS = 2000;
|
|
114
|
+
|
|
115
|
+
function safeSpawn(bin, args) {
|
|
88
116
|
try {
|
|
89
|
-
|
|
90
|
-
|
|
117
|
+
return spawnSync(bin, args, {
|
|
118
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
119
|
+
timeout: DETECT_TIMEOUT_MS,
|
|
120
|
+
encoding: "utf8",
|
|
121
|
+
});
|
|
91
122
|
} catch {
|
|
92
|
-
return
|
|
123
|
+
return null;
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
function versionParts(v) {
|
|
128
|
+
return String(v)
|
|
129
|
+
.trim()
|
|
130
|
+
.split(".")
|
|
131
|
+
.map((n) => parseInt(n, 10) || 0);
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
function isOlder(a, b) {
|
|
135
|
+
const pa = versionParts(a);
|
|
136
|
+
const pb = versionParts(b);
|
|
137
|
+
const len = Math.max(pa.length, pb.length);
|
|
138
|
+
for (let i = 0; i < len; i++) {
|
|
139
|
+
const x = pa[i] || 0;
|
|
140
|
+
const y = pb[i] || 0;
|
|
141
|
+
if (x !== y) return x < y;
|
|
93
142
|
}
|
|
143
|
+
return false;
|
|
94
144
|
}
|
|
95
145
|
|
|
96
|
-
//
|
|
97
|
-
//
|
|
98
|
-
//
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
146
|
+
// Each detector below is skipped up front via `has()` when its own binary
|
|
147
|
+
// isn't even on PATH, so a machine without e.g. Homebrew never pays for a
|
|
148
|
+
// `brew list` spawn.
|
|
149
|
+
|
|
150
|
+
function detectUvTool() {
|
|
151
|
+
if (!has("uv")) return null;
|
|
152
|
+
const result = safeSpawn("uv", ["tool", "list"]);
|
|
153
|
+
if (!result || result.status !== 0 || !result.stdout) return null;
|
|
154
|
+
const match = result.stdout.match(/^tokenjam\s+v?(\S+)/m);
|
|
155
|
+
if (!match) return null;
|
|
156
|
+
return {
|
|
157
|
+
method: "uv tool",
|
|
158
|
+
version: match[1],
|
|
159
|
+
upgradeCmd: "uv tool upgrade tokenjam",
|
|
160
|
+
};
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
function detectPipx() {
|
|
164
|
+
if (!has("pipx")) return null;
|
|
165
|
+
const result = safeSpawn("pipx", ["list", "--json"]);
|
|
166
|
+
if (!result || result.status !== 0 || !result.stdout) return null;
|
|
107
167
|
try {
|
|
108
|
-
|
|
109
|
-
|
|
168
|
+
const data = JSON.parse(result.stdout);
|
|
169
|
+
const venv = data.venvs && data.venvs[PACKAGE];
|
|
170
|
+
const version =
|
|
171
|
+
venv &&
|
|
172
|
+
venv.metadata &&
|
|
173
|
+
venv.metadata.main_package &&
|
|
174
|
+
venv.metadata.main_package.package_version;
|
|
175
|
+
if (!version) return null;
|
|
176
|
+
return { method: "pipx", version, upgradeCmd: "pipx upgrade tokenjam" };
|
|
110
177
|
} catch {
|
|
111
|
-
|
|
178
|
+
return null;
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
function detectPip() {
|
|
183
|
+
for (const pipBin of ["pip3", "pip"]) {
|
|
184
|
+
if (!has(pipBin)) continue;
|
|
185
|
+
const result = safeSpawn(pipBin, ["show", PACKAGE]);
|
|
186
|
+
if (!result || result.status !== 0 || !result.stdout) continue;
|
|
187
|
+
const match = result.stdout.match(/^Version:\s*(\S+)/m);
|
|
188
|
+
if (!match) continue;
|
|
189
|
+
// Covers both a plain `pip install` and `pip install --user` — pip
|
|
190
|
+
// doesn't distinguish the two in `pip show` output, and either way the
|
|
191
|
+
// fix command is the same.
|
|
192
|
+
return {
|
|
193
|
+
method: "pip",
|
|
194
|
+
version: match[1],
|
|
195
|
+
upgradeCmd: `${pipBin} install --upgrade ${PACKAGE}`,
|
|
196
|
+
};
|
|
197
|
+
}
|
|
198
|
+
return null;
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
function detectHomebrew() {
|
|
202
|
+
if (!has("brew")) return null;
|
|
203
|
+
const result = safeSpawn("brew", ["list", "--versions", PACKAGE]);
|
|
204
|
+
if (!result || result.status !== 0 || !result.stdout) return null;
|
|
205
|
+
const match = result.stdout.trim().match(/^tokenjam\s+(\S+)/);
|
|
206
|
+
if (!match) return null;
|
|
207
|
+
return {
|
|
208
|
+
method: "Homebrew",
|
|
209
|
+
version: match[1],
|
|
210
|
+
upgradeCmd: "brew upgrade tokenjam",
|
|
211
|
+
};
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
function warnIfShadowedByStaleInstall(wrapperVersion) {
|
|
215
|
+
if (!wrapperVersion) return;
|
|
216
|
+
const detectors = [detectUvTool, detectPipx, detectPip, detectHomebrew];
|
|
217
|
+
for (const detect of detectors) {
|
|
218
|
+
let found = null;
|
|
219
|
+
try {
|
|
220
|
+
found = detect();
|
|
221
|
+
} catch {
|
|
222
|
+
found = null;
|
|
223
|
+
}
|
|
224
|
+
if (!found || !isOlder(found.version, wrapperVersion)) continue;
|
|
225
|
+
process.stderr.write(
|
|
226
|
+
"\n" +
|
|
227
|
+
`Note: a ${found.method} install of tokenjam is at v${found.version}, older than v${wrapperVersion} run here.\n` +
|
|
228
|
+
`Upgrade it with: ${found.upgradeCmd}\n`
|
|
229
|
+
);
|
|
230
|
+
return; // one line is enough — first stale install found wins
|
|
112
231
|
}
|
|
113
232
|
}
|
|
114
233
|
|
|
@@ -127,17 +246,18 @@ function main() {
|
|
|
127
246
|
? process.env
|
|
128
247
|
: { ...process.env, TJ_NPX_ZERO_INSTALL_REPORT: "1" };
|
|
129
248
|
|
|
130
|
-
|
|
249
|
+
const version = ownVersion();
|
|
250
|
+
|
|
251
|
+
for (const { bin, pinnedPrefix, prefix } of runners(version)) {
|
|
131
252
|
if (!has(bin)) continue;
|
|
132
|
-
const
|
|
133
|
-
|
|
134
|
-
const args = doRefresh ? ["--refresh", ...prefix] : prefix;
|
|
253
|
+
const args =
|
|
254
|
+
pinnedPrefix && resolves(bin, pinnedPrefix) ? pinnedPrefix : prefix;
|
|
135
255
|
const result = spawnSync(bin, [...args, ...passthrough], {
|
|
136
256
|
stdio: "inherit",
|
|
137
257
|
env: childEnv,
|
|
138
258
|
});
|
|
139
259
|
if (result.error) continue; // try the next runner on spawn failure
|
|
140
|
-
|
|
260
|
+
warnIfShadowedByStaleInstall(version);
|
|
141
261
|
process.exit(result.status === null ? 1 : result.status);
|
|
142
262
|
}
|
|
143
263
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "tokenjam",
|
|
3
|
-
"version": "0.6.
|
|
3
|
+
"version": "0.6.2",
|
|
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",
|