@bike4mind/cli 0.13.0 → 0.15.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.
@@ -0,0 +1,218 @@
1
+ #!/usr/bin/env node
2
+ import { execSync } from "child_process";
3
+ import { constants, promises } from "fs";
4
+ import { homedir } from "os";
5
+ import path from "path";
6
+ import axios from "axios";
7
+ //#region src/utils/updateChecker.ts
8
+ /**
9
+ * Update checker utility for B4M CLI
10
+ * Checks the NPM registry for newer versions and caches results.
11
+ * Used by the startup banner, `b4m update`, and `b4m doctor` commands.
12
+ */
13
+ const CACHE_FILE = path.join(homedir(), ".bike4mind", "update-check.json");
14
+ const CACHE_TTL_MS = 1440 * 60 * 1e3;
15
+ const NPM_REGISTRY_URL = "https://registry.npmjs.org/@bike4mind/cli/latest";
16
+ const FETCH_TIMEOUT_MS = 5e3;
17
+ /**
18
+ * Canonical global-install command for the CLI.
19
+ * Single source of truth shared by the manual `b4m update` path and the
20
+ * auto-update-on-launch bootstrap so they can never drift.
21
+ */
22
+ const INSTALL_CMD = "npm install -g @bike4mind/cli@latest";
23
+ /**
24
+ * Env var set on the re-exec'd child after an auto-update so it skips the
25
+ * update block and can't loop. Single source of truth for both the reader
26
+ * (shouldAttemptAutoUpdate) and the writer (maybeAutoUpdateOnLaunch).
27
+ */
28
+ const REEXEC_GUARD_ENV = "B4M_UPDATED_REEXEC";
29
+ /**
30
+ * Check whether the global npm prefix is writable by the current user.
31
+ * When it isn't (e.g. Homebrew/system node), `npm install -g` needs sudo —
32
+ * which an unattended auto-updater cannot provide — so callers should fall
33
+ * back to a manual-update notice instead of attempting a silent install.
34
+ * Non-throwing — returns false on any error.
35
+ *
36
+ * Pass `prefix` to reuse an already-resolved `npm config get prefix` (each
37
+ * `execSync` is ~50-200ms); omit it and the prefix is resolved internally.
38
+ */
39
+ async function isNpmPrefixWritable(prefix) {
40
+ try {
41
+ const resolved = prefix ?? execSync("npm config get prefix", {
42
+ encoding: "utf-8",
43
+ timeout: 1e4
44
+ }).trim();
45
+ if (!resolved) return false;
46
+ await promises.access(resolved, constants.W_OK);
47
+ return true;
48
+ } catch {
49
+ return false;
50
+ }
51
+ }
52
+ /**
53
+ * Cheap, synchronous pre-checks gating auto-update-on-launch. Excludes the
54
+ * async config-file and network lookups so it stays easy to unit-test.
55
+ * Returns false (skip the update) when already re-exec'd this launch, opted
56
+ * out via env, or not attached to an interactive TTY.
57
+ */
58
+ function shouldAttemptAutoUpdate(opts) {
59
+ const env = opts.env ?? process.env;
60
+ if (env["B4M_UPDATED_REEXEC"] === "1") return false;
61
+ if (env.B4M_AUTO_UPDATE === "0") return false;
62
+ if (!opts.isTTY) return false;
63
+ return true;
64
+ }
65
+ const CONFIG_FILE = path.join(homedir(), ".bike4mind", "config.json");
66
+ /**
67
+ * Read the user's tri-state `autoUpdate` preference from the global config.
68
+ * Kept deliberately lightweight (no Zod / ConfigStore) because it runs in the
69
+ * bin bootstrap before the app loads. Defaults to `'ask'` (consent-first) when
70
+ * the file or flag is absent/unreadable.
71
+ */
72
+ async function getAutoUpdatePreference() {
73
+ try {
74
+ const raw = await promises.readFile(CONFIG_FILE, "utf-8");
75
+ const value = JSON.parse(raw)?.preferences?.autoUpdate;
76
+ if (value === true) return "auto";
77
+ if (value === false) return "never";
78
+ return "ask";
79
+ } catch {
80
+ return "ask";
81
+ }
82
+ }
83
+ /**
84
+ * Persist the user's `autoUpdate` choice (`true` = always, `false` = never).
85
+ *
86
+ * Writes the **global** config file directly — the mirror of how
87
+ * `getAutoUpdatePreference()` reads it — rather than routing through
88
+ * `ConfigStore.save()`. `ConfigStore.load()` merges global → project → local,
89
+ * and `ConfigStore.save()` writes that merged result back to the global path;
90
+ * answering "Always"/"Never" inside a project would therefore bake the
91
+ * project's other overrides (model, theme, temperature, …) into the user's
92
+ * global config as a silent side effect. This call fires during the bin
93
+ * bootstrap any time an update is available, so that blast radius is
94
+ * unacceptable. A direct read-merge-write of only `preferences.autoUpdate`
95
+ * avoids it. If the on-disk file is schema-incomplete, the next
96
+ * `ConfigStore.load()` backfills defaults harmlessly — exactly as
97
+ * `getAutoUpdatePreference` already tolerates a partial file. Best-effort:
98
+ * a failure is swallowed (we simply ask again next launch rather than blocking).
99
+ */
100
+ async function setAutoUpdatePreference(value) {
101
+ try {
102
+ let raw = {};
103
+ try {
104
+ const parsed = JSON.parse(await promises.readFile(CONFIG_FILE, "utf-8"));
105
+ if (parsed && typeof parsed === "object") raw = parsed;
106
+ } catch {}
107
+ const prefs = raw.preferences && typeof raw.preferences === "object" ? raw.preferences : {};
108
+ raw.preferences = {
109
+ ...prefs,
110
+ autoUpdate: value
111
+ };
112
+ await promises.mkdir(path.dirname(CONFIG_FILE), { recursive: true });
113
+ await promises.writeFile(CONFIG_FILE, JSON.stringify(raw, null, 2), "utf-8");
114
+ await promises.chmod(CONFIG_FILE, 384);
115
+ } catch {}
116
+ }
117
+ /**
118
+ * Compare two semver strings (MAJOR.MINOR.PATCH).
119
+ * Returns -1 if a < b, 0 if equal, 1 if a > b.
120
+ */
121
+ function compareSemver(a, b) {
122
+ const partsA = a.split(".").map(Number);
123
+ const partsB = b.split(".").map(Number);
124
+ for (let i = 0; i < 3; i++) {
125
+ const segA = partsA[i] ?? 0;
126
+ const segB = partsB[i] ?? 0;
127
+ if (segA < segB) return -1;
128
+ if (segA > segB) return 1;
129
+ }
130
+ return 0;
131
+ }
132
+ /**
133
+ * Fetch the latest published version from the NPM registry.
134
+ * Returns the version string or null on any error.
135
+ */
136
+ async function fetchLatestVersion() {
137
+ try {
138
+ const version = (await axios.get(NPM_REGISTRY_URL, {
139
+ timeout: FETCH_TIMEOUT_MS,
140
+ headers: { Accept: "application/json" }
141
+ })).data?.version;
142
+ return typeof version === "string" ? version : null;
143
+ } catch {
144
+ return null;
145
+ }
146
+ }
147
+ /**
148
+ * Read the cached update check result.
149
+ */
150
+ async function readCache() {
151
+ try {
152
+ const data = await promises.readFile(CACHE_FILE, "utf-8");
153
+ const parsed = JSON.parse(data);
154
+ if (parsed && typeof parsed.lastChecked === "string" && typeof parsed.latestVersion === "string" && typeof parsed.currentVersion === "string") return parsed;
155
+ return null;
156
+ } catch {
157
+ return null;
158
+ }
159
+ }
160
+ /**
161
+ * Write the update check result to cache.
162
+ */
163
+ async function writeCache(cache) {
164
+ try {
165
+ await promises.mkdir(path.dirname(CACHE_FILE), { recursive: true });
166
+ await promises.writeFile(CACHE_FILE, JSON.stringify(cache, null, 2), "utf-8");
167
+ } catch {}
168
+ }
169
+ /**
170
+ * Check for updates using cache when fresh.
171
+ * Non-throwing — returns null on any error.
172
+ */
173
+ async function checkForUpdate(currentVersion) {
174
+ try {
175
+ const cache = await readCache();
176
+ if (cache && cache.currentVersion === currentVersion) {
177
+ if (Date.now() - new Date(cache.lastChecked).getTime() < CACHE_TTL_MS) return {
178
+ currentVersion,
179
+ latestVersion: cache.latestVersion,
180
+ updateAvailable: compareSemver(cache.latestVersion, currentVersion) > 0
181
+ };
182
+ }
183
+ const latestVersion = await fetchLatestVersion();
184
+ if (!latestVersion) return null;
185
+ await writeCache({
186
+ lastChecked: (/* @__PURE__ */ new Date()).toISOString(),
187
+ latestVersion,
188
+ currentVersion
189
+ });
190
+ return {
191
+ currentVersion,
192
+ latestVersion,
193
+ updateAvailable: compareSemver(latestVersion, currentVersion) > 0
194
+ };
195
+ } catch {
196
+ return null;
197
+ }
198
+ }
199
+ /**
200
+ * Force-check for updates (ignores cache).
201
+ * Used by `b4m update` and `b4m doctor`.
202
+ */
203
+ async function forceCheckForUpdate(currentVersion) {
204
+ const latestVersion = await fetchLatestVersion();
205
+ if (!latestVersion) return null;
206
+ await writeCache({
207
+ lastChecked: (/* @__PURE__ */ new Date()).toISOString(),
208
+ latestVersion,
209
+ currentVersion
210
+ });
211
+ return {
212
+ currentVersion,
213
+ latestVersion,
214
+ updateAvailable: compareSemver(latestVersion, currentVersion) > 0
215
+ };
216
+ }
217
+ //#endregion
218
+ export { fetchLatestVersion as a, isNpmPrefixWritable as c, compareSemver as i, setAutoUpdatePreference as l, REEXEC_GUARD_ENV as n, forceCheckForUpdate as o, checkForUpdate as r, getAutoUpdatePreference as s, INSTALL_CMD as t, shouldAttemptAutoUpdate as u };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bike4mind/cli",
3
- "version": "0.13.0",
3
+ "version": "0.15.0",
4
4
  "type": "module",
5
5
  "description": "Interactive CLI tool for Bike4Mind with ReAct agents",
6
6
  "license": "UNLICENSED",
@@ -107,8 +107,8 @@
107
107
  "zod": "^4.4.3",
108
108
  "zod-validation-error": "^5.0.0",
109
109
  "zustand": "^5.0.13",
110
- "@bike4mind/fab-pipeline": "0.2.9",
111
- "@bike4mind/llm-adapters": "0.3.4",
110
+ "@bike4mind/fab-pipeline": "0.3.0",
111
+ "@bike4mind/llm-adapters": "0.4.0",
112
112
  "@bike4mind/observability": "0.1.0"
113
113
  },
114
114
  "devDependencies": {
@@ -124,11 +124,11 @@
124
124
  "tsx": "^4.22.3",
125
125
  "typescript": "^5.9.3",
126
126
  "vitest": "^4.1.7",
127
- "@bike4mind/agents": "0.12.0",
128
- "@bike4mind/common": "2.107.0",
129
- "@bike4mind/mcp": "1.37.22",
130
- "@bike4mind/utils": "2.23.10",
131
- "@bike4mind/services": "2.93.0"
127
+ "@bike4mind/agents": "0.14.0",
128
+ "@bike4mind/common": "2.108.0",
129
+ "@bike4mind/mcp": "1.37.24",
130
+ "@bike4mind/services": "2.94.0",
131
+ "@bike4mind/utils": "2.24.0"
132
132
  },
133
133
  "optionalDependencies": {
134
134
  "@vscode/ripgrep": "^1.18.0"
@@ -1,117 +0,0 @@
1
- #!/usr/bin/env node
2
- import { promises } from "fs";
3
- import { homedir } from "os";
4
- import path from "path";
5
- import axios from "axios";
6
- //#region src/utils/updateChecker.ts
7
- /**
8
- * Update checker utility for B4M CLI
9
- * Checks the NPM registry for newer versions and caches results.
10
- * Used by the startup banner, `b4m update`, and `b4m doctor` commands.
11
- */
12
- const CACHE_FILE = path.join(homedir(), ".bike4mind", "update-check.json");
13
- const CACHE_TTL_MS = 1440 * 60 * 1e3;
14
- const NPM_REGISTRY_URL = "https://registry.npmjs.org/@bike4mind/cli/latest";
15
- const FETCH_TIMEOUT_MS = 5e3;
16
- /**
17
- * Compare two semver strings (MAJOR.MINOR.PATCH).
18
- * Returns -1 if a < b, 0 if equal, 1 if a > b.
19
- */
20
- function compareSemver(a, b) {
21
- const partsA = a.split(".").map(Number);
22
- const partsB = b.split(".").map(Number);
23
- for (let i = 0; i < 3; i++) {
24
- const segA = partsA[i] ?? 0;
25
- const segB = partsB[i] ?? 0;
26
- if (segA < segB) return -1;
27
- if (segA > segB) return 1;
28
- }
29
- return 0;
30
- }
31
- /**
32
- * Fetch the latest published version from the NPM registry.
33
- * Returns the version string or null on any error.
34
- */
35
- async function fetchLatestVersion() {
36
- try {
37
- const version = (await axios.get(NPM_REGISTRY_URL, {
38
- timeout: FETCH_TIMEOUT_MS,
39
- headers: { Accept: "application/json" }
40
- })).data?.version;
41
- return typeof version === "string" ? version : null;
42
- } catch {
43
- return null;
44
- }
45
- }
46
- /**
47
- * Read the cached update check result.
48
- */
49
- async function readCache() {
50
- try {
51
- const data = await promises.readFile(CACHE_FILE, "utf-8");
52
- const parsed = JSON.parse(data);
53
- if (parsed && typeof parsed.lastChecked === "string" && typeof parsed.latestVersion === "string" && typeof parsed.currentVersion === "string") return parsed;
54
- return null;
55
- } catch {
56
- return null;
57
- }
58
- }
59
- /**
60
- * Write the update check result to cache.
61
- */
62
- async function writeCache(cache) {
63
- try {
64
- await promises.mkdir(path.dirname(CACHE_FILE), { recursive: true });
65
- await promises.writeFile(CACHE_FILE, JSON.stringify(cache, null, 2), "utf-8");
66
- } catch {}
67
- }
68
- /**
69
- * Check for updates using cache when fresh.
70
- * Non-throwing — returns null on any error.
71
- */
72
- async function checkForUpdate(currentVersion) {
73
- try {
74
- const cache = await readCache();
75
- if (cache && cache.currentVersion === currentVersion) {
76
- if (Date.now() - new Date(cache.lastChecked).getTime() < CACHE_TTL_MS) return {
77
- currentVersion,
78
- latestVersion: cache.latestVersion,
79
- updateAvailable: compareSemver(cache.latestVersion, currentVersion) > 0
80
- };
81
- }
82
- const latestVersion = await fetchLatestVersion();
83
- if (!latestVersion) return null;
84
- await writeCache({
85
- lastChecked: (/* @__PURE__ */ new Date()).toISOString(),
86
- latestVersion,
87
- currentVersion
88
- });
89
- return {
90
- currentVersion,
91
- latestVersion,
92
- updateAvailable: compareSemver(latestVersion, currentVersion) > 0
93
- };
94
- } catch {
95
- return null;
96
- }
97
- }
98
- /**
99
- * Force-check for updates (ignores cache).
100
- * Used by `b4m update` and `b4m doctor`.
101
- */
102
- async function forceCheckForUpdate(currentVersion) {
103
- const latestVersion = await fetchLatestVersion();
104
- if (!latestVersion) return null;
105
- await writeCache({
106
- lastChecked: (/* @__PURE__ */ new Date()).toISOString(),
107
- latestVersion,
108
- currentVersion
109
- });
110
- return {
111
- currentVersion,
112
- latestVersion,
113
- updateAvailable: compareSemver(latestVersion, currentVersion) > 0
114
- };
115
- }
116
- //#endregion
117
- export { forceCheckForUpdate as i, compareSemver as n, fetchLatestVersion as r, checkForUpdate as t };