@jelou/cli 1.108.1 → 1.109.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.
- package/bin/postinstall.js +103 -8
- package/package.json +22 -14
package/bin/postinstall.js
CHANGED
|
@@ -9,8 +9,15 @@
|
|
|
9
9
|
// cold-start) with a relative symlink to the platform binary itself.
|
|
10
10
|
// When npm wires `<global>/bin/jelou` -> `<wrapper>/bin/jelou`, the
|
|
11
11
|
// chain now resolves straight to the native binary, no node hop.
|
|
12
|
+
// 3. Record where that shim is, and what it points at, in
|
|
13
|
+
// `~/.jelou/install-manifest.json`. The CLI cannot rediscover this at
|
|
14
|
+
// runtime: `Deno.execPath()` returns the invoked path on macOS and the
|
|
15
|
+
// resolved one on Linux, so walking up to node_modules works on one and
|
|
16
|
+
// not the other. Writing it down here is the only reliable source.
|
|
17
|
+
// Best-effort — a manifest failure must never fail an install.
|
|
12
18
|
|
|
13
19
|
const fs = require("fs");
|
|
20
|
+
const os = require("os");
|
|
14
21
|
const path = require("path");
|
|
15
22
|
const {
|
|
16
23
|
PLATFORMS,
|
|
@@ -50,6 +57,71 @@ if (!fs.existsSync(binPath)) {
|
|
|
50
57
|
process.exit(1);
|
|
51
58
|
}
|
|
52
59
|
|
|
60
|
+
// --- install manifest ---
|
|
61
|
+
// Keyed by shim path, and merged rather than replaced: one machine can carry
|
|
62
|
+
// several independent global installs (e.g. a Homebrew node prefix and a
|
|
63
|
+
// user-owned ~/.local one), each with its own postinstall. A single-record
|
|
64
|
+
// manifest would let the last install to run claim a shim it does not own.
|
|
65
|
+
function manifestPath() {
|
|
66
|
+
const home = os.homedir();
|
|
67
|
+
return home ? path.join(home, ".jelou", "install-manifest.json") : null;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function readManifest(file) {
|
|
71
|
+
try {
|
|
72
|
+
const parsed = JSON.parse(fs.readFileSync(file, "utf8"));
|
|
73
|
+
if (parsed && typeof parsed === "object" && parsed.installs) return parsed;
|
|
74
|
+
} catch (_) {
|
|
75
|
+
// Missing or corrupt — start fresh rather than refusing to install.
|
|
76
|
+
}
|
|
77
|
+
return { version: 1, installs: {} };
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
function recordManifest(shimKind) {
|
|
81
|
+
const file = manifestPath();
|
|
82
|
+
if (!file) return;
|
|
83
|
+
try {
|
|
84
|
+
let wrapperVersion = null;
|
|
85
|
+
try {
|
|
86
|
+
wrapperVersion = require("../package.json").version || null;
|
|
87
|
+
} catch (_) {
|
|
88
|
+
// Version is informational; a missing one must not skip the manifest.
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
const manifest = readManifest(file);
|
|
92
|
+
manifest.version = 1;
|
|
93
|
+
|
|
94
|
+
// Drop entries whose shim is gone (uninstalled prefix), but never touch a
|
|
95
|
+
// live sibling install. lstat, not existsSync: existsSync follows the
|
|
96
|
+
// link, so a sibling whose target is momentarily missing — that prefix
|
|
97
|
+
// running its own install — would look gone and be pruned.
|
|
98
|
+
for (const key of Object.keys(manifest.installs)) {
|
|
99
|
+
if (key === shimPath) continue;
|
|
100
|
+
try {
|
|
101
|
+
fs.lstatSync(key);
|
|
102
|
+
} catch {
|
|
103
|
+
delete manifest.installs[key];
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
manifest.installs[shimPath] = {
|
|
108
|
+
shimKind: shimKind,
|
|
109
|
+
baselineTarget: relativeTarget,
|
|
110
|
+
baselineBinary: binPath,
|
|
111
|
+
version: wrapperVersion,
|
|
112
|
+
writtenAt: new Date().toISOString(),
|
|
113
|
+
};
|
|
114
|
+
|
|
115
|
+
fs.mkdirSync(path.dirname(file), { recursive: true });
|
|
116
|
+
const tmp = file + ".tmp-" + process.pid;
|
|
117
|
+
fs.writeFileSync(tmp, JSON.stringify(manifest, null, 2) + "\n");
|
|
118
|
+
fs.renameSync(tmp, file);
|
|
119
|
+
} catch (_) {
|
|
120
|
+
// Best-effort by design. Losing the manifest only disables background
|
|
121
|
+
// updates; failing the install here would be far worse.
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
|
|
53
125
|
// --- fast-shim rewrite (Unix only for now) ---
|
|
54
126
|
// On Windows npm generates `.cmd` and `.ps1` shims that hardcode
|
|
55
127
|
// `node "<wrapper>/bin/jelou"`; replacing the wrapper script doesn't
|
|
@@ -71,28 +143,51 @@ const relativeTarget = path.relative(wrapperBin, binPath);
|
|
|
71
143
|
try {
|
|
72
144
|
const stat = fs.lstatSync(shimPath);
|
|
73
145
|
if (stat.isSymbolicLink() && fs.readlinkSync(shimPath) === relativeTarget) {
|
|
146
|
+
// The shim is already right, but the manifest still has to be refreshed:
|
|
147
|
+
// relativeTarget is identical across versions, so this branch is the one
|
|
148
|
+
// taken on every ordinary upgrade.
|
|
149
|
+
recordManifest("symlink");
|
|
74
150
|
process.exit(0);
|
|
75
151
|
}
|
|
76
152
|
} catch (_) {
|
|
77
153
|
// shim not present yet — fine, we'll create it.
|
|
78
154
|
}
|
|
79
155
|
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
156
|
+
// Write through a tmp sibling + rename so the shim is never briefly absent.
|
|
157
|
+
// The old rmSync-then-symlinkSync left a window where `<wrapper>/bin/jelou`
|
|
158
|
+
// did not exist, and a concurrent `jelou` invocation got ENOENT. rename(2)
|
|
159
|
+
// is atomic, so a reader sees the old shim or the new one, never neither.
|
|
160
|
+
const tmpPath = shimPath + ".tmp-" + process.pid;
|
|
161
|
+
|
|
162
|
+
function swapIn(write) {
|
|
163
|
+
try {
|
|
164
|
+
fs.rmSync(tmpPath, { force: true });
|
|
165
|
+
} catch (_) {
|
|
166
|
+
// nothing to remove
|
|
167
|
+
}
|
|
168
|
+
write(tmpPath);
|
|
169
|
+
fs.renameSync(tmpPath, shimPath);
|
|
84
170
|
}
|
|
85
171
|
|
|
172
|
+
let shimKind;
|
|
86
173
|
try {
|
|
87
|
-
fs.symlinkSync(relativeTarget,
|
|
88
|
-
|
|
174
|
+
swapIn((p) => fs.symlinkSync(relativeTarget, p));
|
|
175
|
+
shimKind = "symlink";
|
|
89
176
|
} catch (err) {
|
|
90
177
|
// EPERM (Windows w/o developer mode), EXDEV (cross-device install),
|
|
91
178
|
// EROFS (read-only FS in some sandboxes). Fall back to a tiny shell
|
|
92
179
|
// wrapper. Adds ~1-3ms vs ~0ms for symlink, but still much better than
|
|
93
180
|
// the 20ms node-boot hop the wrapper script costs.
|
|
181
|
+
try {
|
|
182
|
+
fs.rmSync(tmpPath, { force: true });
|
|
183
|
+
} catch (_) {
|
|
184
|
+
// nothing to remove
|
|
185
|
+
}
|
|
94
186
|
const safeTarget = relativeTarget.replace(/(["\\$`])/g, "\\$1");
|
|
95
187
|
const shell = `#!/bin/sh\nexec "$(dirname "$0")/${safeTarget}" "$@"\n`;
|
|
96
|
-
fs.writeFileSync(
|
|
97
|
-
|
|
188
|
+
swapIn((p) => fs.writeFileSync(p, shell, { mode: 0o755 }));
|
|
189
|
+
shimKind = "shell";
|
|
98
190
|
}
|
|
191
|
+
|
|
192
|
+
recordManifest(shimKind);
|
|
193
|
+
process.exit(0);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@jelou/cli",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.109.0",
|
|
4
4
|
"description": "Build AI agents on the Jelou platform — from your terminal or your AI editor.",
|
|
5
5
|
"author": "Jelou Inc. (https://jelou.ai)",
|
|
6
6
|
"license": "SEE LICENSE IN LICENSE",
|
|
@@ -23,11 +23,11 @@
|
|
|
23
23
|
"THIRD_PARTY_NOTICES.md"
|
|
24
24
|
],
|
|
25
25
|
"optionalDependencies": {
|
|
26
|
-
"@jelou/cli-darwin-arm64": "1.
|
|
27
|
-
"@jelou/cli-darwin-x64": "1.
|
|
28
|
-
"@jelou/cli-linux-x64": "1.
|
|
29
|
-
"@jelou/cli-linux-arm64": "1.
|
|
30
|
-
"@jelou/cli-win32-x64": "1.
|
|
26
|
+
"@jelou/cli-darwin-arm64": "1.109.0",
|
|
27
|
+
"@jelou/cli-darwin-x64": "1.109.0",
|
|
28
|
+
"@jelou/cli-linux-x64": "1.109.0",
|
|
29
|
+
"@jelou/cli-linux-arm64": "1.109.0",
|
|
30
|
+
"@jelou/cli-win32-x64": "1.109.0"
|
|
31
31
|
},
|
|
32
32
|
"keywords": [
|
|
33
33
|
"jelou",
|
|
@@ -58,21 +58,29 @@
|
|
|
58
58
|
"jelou": {
|
|
59
59
|
"minVersion": "1.100.2",
|
|
60
60
|
"release_notes": {
|
|
61
|
-
"version": "1.
|
|
62
|
-
"date": "2026-09-
|
|
63
|
-
"headline": "
|
|
61
|
+
"version": "1.109.0",
|
|
62
|
+
"date": "2026-09-03",
|
|
63
|
+
"headline": "Updates now install themselves in the background — no more `npm install -g` after every release.",
|
|
64
64
|
"highlights": [
|
|
65
65
|
{
|
|
66
|
-
"type": "
|
|
67
|
-
"text": "
|
|
66
|
+
"type": "new",
|
|
67
|
+
"text": "The CLI keeps itself current on its own. When a newer release exists, it downloads and activates in the background after your command finishes; the next `jelou` you run is already the new version and tells you so in one line. No prompt, and no second command to refresh skills — those come with it."
|
|
68
68
|
},
|
|
69
69
|
{
|
|
70
|
-
"type": "
|
|
71
|
-
"text": "`jelou
|
|
70
|
+
"type": "new",
|
|
71
|
+
"text": "`jelou update --rollback` undoes a background update, putting the previously installed binary back and pausing background updates for 30 days."
|
|
72
72
|
},
|
|
73
73
|
{
|
|
74
74
|
"type": "improved",
|
|
75
|
-
"text": "
|
|
75
|
+
"text": "Agents stop being told to run `npm install -g` for a release the CLI is already installing on its own."
|
|
76
|
+
},
|
|
77
|
+
{
|
|
78
|
+
"type": "heads_up",
|
|
79
|
+
"text": "Your `jelou` binary can now change without asking. It only ever moves forward to the version already published on npm — the same one the old prompt asked you to install. Set `JELOU_NO_UPDATE_CHECK=1` to turn it off entirely."
|
|
80
|
+
},
|
|
81
|
+
{
|
|
82
|
+
"type": "heads_up",
|
|
83
|
+
"text": "Background updates need macOS or Linux, an npm install, and a prefix you own. Windows, `sudo`-installed prefixes, and non-npm installs keep the previous behavior and simply tell you an update is available."
|
|
76
84
|
}
|
|
77
85
|
]
|
|
78
86
|
}
|