@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.
Files changed (2) hide show
  1. package/bin/postinstall.js +103 -8
  2. package/package.json +22 -14
@@ -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
- try {
81
- fs.rmSync(shimPath, { force: true });
82
- } catch (_) {
83
- // nothing to remove
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, shimPath);
88
- process.exit(0);
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(shimPath, shell, { mode: 0o755 });
97
- process.exit(0);
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.108.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.108.1",
27
- "@jelou/cli-darwin-x64": "1.108.1",
28
- "@jelou/cli-linux-x64": "1.108.1",
29
- "@jelou/cli-linux-arm64": "1.108.1",
30
- "@jelou/cli-win32-x64": "1.108.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.108.1",
62
- "date": "2026-09-02",
63
- "headline": "Marketplace nodes (biometría, payments) no longer render as \"Configuración incompleta\" in Studio.",
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": "fixed",
67
- "text": "Biometría and payment nodes created by `jelou push` no longer render in Studio with \"Configuración incompleta. Revisa los campos del nodo.\" The nodes always ran correctly — only the canvas warning was wrong."
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": "fixed",
71
- "text": "`jelou push` now repairs marketplace nodes already saved without that flag. Previously no edit to the local file could fix them: the push diff ignored the field and reported nothing to do."
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": "The workflow build lint now fails on a marketplace node missing `isValidConfig`, so agents write it up front instead of shipping a node Studio flags."
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
  }