baldart 3.12.0 → 3.14.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/CHANGELOG.md +68 -0
- package/README.md +24 -0
- package/VERSION +1 -1
- package/bin/baldart.js +14 -0
- package/framework/docs/LSP-LAYER.md +66 -9
- package/package.json +1 -1
- package/src/commands/add.js +24 -0
- package/src/commands/configure.js +44 -0
- package/src/commands/doctor.js +166 -8
- package/src/commands/push.js +0 -0
- package/src/commands/update.js +38 -1
- package/src/commands/version.js +5 -1
- package/src/utils/git.js +93 -0
- package/src/utils/lsp-adapters/go.js +1 -1
- package/src/utils/lsp-adapters/python.js +1 -1
- package/src/utils/lsp-adapters/ruby.js +1 -1
- package/src/utils/lsp-adapters/rust.js +1 -1
- package/src/utils/lsp-adapters/typescript.js +8 -2
- package/src/utils/lsp-installer.js +243 -0
- package/src/utils/state.js +27 -0
- package/src/utils/update-notifier.js +181 -0
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* CLI self-update notifier (v3.13.0+).
|
|
3
|
+
*
|
|
4
|
+
* The framework payload inside `.framework/` is updated by `baldart update`.
|
|
5
|
+
* The CLI itself (this package) is updated by `npm i -g baldart@latest`.
|
|
6
|
+
* Until now those two layers drifted silently — users on an old CLI never
|
|
7
|
+
* learned new flags, new doctor checks, new adapters had shipped.
|
|
8
|
+
*
|
|
9
|
+
* This module surfaces CLI drift WITHOUT auto-installing anything. It is
|
|
10
|
+
* deliberately non-invasive:
|
|
11
|
+
*
|
|
12
|
+
* - Best-effort HTTP check against the npm registry with a short timeout.
|
|
13
|
+
* - Result cached at `~/.baldart/cli-update-check.json` (TTL 24h).
|
|
14
|
+
* - Banner printed from the cache on the NEXT invocation (zero latency on
|
|
15
|
+
* every run). First-ever run shows nothing — that's intentional.
|
|
16
|
+
* - Suppressed in CI, non-TTY, --offline, NODE_ENV=test, or via
|
|
17
|
+
* BALDART_NO_UPDATE_CHECK=1.
|
|
18
|
+
* - Never blocks the process: the background refresh is fire-and-forget,
|
|
19
|
+
* its timer/socket are unref'd so they cannot hold the event loop open.
|
|
20
|
+
*
|
|
21
|
+
* Design rationale (see CHANGELOG v3.13.0): we explicitly DO NOT run
|
|
22
|
+
* `npm i -g baldart@latest` automatically. Auto-updates inside a per-project
|
|
23
|
+
* CLI invocation would (a) impact every other project on the user's machine,
|
|
24
|
+
* (b) potentially require sudo, (c) break determinism in CI runs. The banner
|
|
25
|
+
* gives the user the choice; the cache means they don't pay for it.
|
|
26
|
+
*/
|
|
27
|
+
|
|
28
|
+
const fs = require('fs');
|
|
29
|
+
const path = require('path');
|
|
30
|
+
const os = require('os');
|
|
31
|
+
|
|
32
|
+
const REGISTRY_URL = 'https://registry.npmjs.org/baldart/latest';
|
|
33
|
+
const CACHE_DIR = path.join(os.homedir(), '.baldart');
|
|
34
|
+
const CACHE_FILE = path.join(CACHE_DIR, 'cli-update-check.json');
|
|
35
|
+
const DEFAULT_TTL_MS = 24 * 60 * 60 * 1000; // 24h
|
|
36
|
+
const DEFAULT_TIMEOUT_MS = 1500;
|
|
37
|
+
|
|
38
|
+
function isSuppressed({ offline } = {}) {
|
|
39
|
+
if (offline) return true;
|
|
40
|
+
if (process.env.BALDART_NO_UPDATE_CHECK === '1') return true;
|
|
41
|
+
if (process.env.CI === 'true' || process.env.CI === '1') return true;
|
|
42
|
+
if (process.env.NODE_ENV === 'test') return true;
|
|
43
|
+
// `process.stdout.isTTY` is `true` when attached to a terminal and
|
|
44
|
+
// `undefined` (NOT `false`) when not — so we must test for "not strictly
|
|
45
|
+
// true" rather than "=== false", otherwise piped/redirected runs silently
|
|
46
|
+
// bypass suppression.
|
|
47
|
+
if (!(process.stdout && process.stdout.isTTY === true)) return true;
|
|
48
|
+
return false;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function readCache() {
|
|
52
|
+
try {
|
|
53
|
+
if (!fs.existsSync(CACHE_FILE)) return null;
|
|
54
|
+
const data = JSON.parse(fs.readFileSync(CACHE_FILE, 'utf8'));
|
|
55
|
+
if (!data || typeof data !== 'object') return null;
|
|
56
|
+
return data;
|
|
57
|
+
} catch (_) {
|
|
58
|
+
return null;
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function writeCache(data) {
|
|
63
|
+
try {
|
|
64
|
+
if (!fs.existsSync(CACHE_DIR)) fs.mkdirSync(CACHE_DIR, { recursive: true });
|
|
65
|
+
fs.writeFileSync(CACHE_FILE, JSON.stringify(data, null, 2) + '\n');
|
|
66
|
+
} catch (_) {
|
|
67
|
+
// Best-effort: a read-only HOME or denied write is not worth surfacing.
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function cmpSemver(a, b) {
|
|
72
|
+
// Returns 1 if a>b, -1 if a<b, 0 if equal. Strips prerelease and build
|
|
73
|
+
// metadata; only X.Y.Z is compared. That is enough for a "is there a newer
|
|
74
|
+
// stable on npm?" prompt — anything fancier belongs in `semver`, and we are
|
|
75
|
+
// deliberately staying dep-free here.
|
|
76
|
+
if (!a || !b) return 0;
|
|
77
|
+
const norm = (v) => String(v).split(/[-+]/)[0].split('.').map((n) => Number(n) || 0);
|
|
78
|
+
const pa = norm(a);
|
|
79
|
+
const pb = norm(b);
|
|
80
|
+
for (let i = 0; i < 3; i++) {
|
|
81
|
+
const x = pa[i] || 0, y = pb[i] || 0;
|
|
82
|
+
if (x > y) return 1;
|
|
83
|
+
if (x < y) return -1;
|
|
84
|
+
}
|
|
85
|
+
return 0;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
async function fetchLatest(timeoutMs) {
|
|
89
|
+
if (typeof fetch !== 'function') return null; // Node < 18 fallback — node engine constraint covers this, but defensive.
|
|
90
|
+
const ctrl = new AbortController();
|
|
91
|
+
const timer = setTimeout(() => ctrl.abort(), timeoutMs);
|
|
92
|
+
if (typeof timer.unref === 'function') timer.unref();
|
|
93
|
+
try {
|
|
94
|
+
const res = await fetch(REGISTRY_URL, {
|
|
95
|
+
signal: ctrl.signal,
|
|
96
|
+
headers: { Accept: 'application/json' },
|
|
97
|
+
});
|
|
98
|
+
if (!res.ok) return null;
|
|
99
|
+
const body = await res.json();
|
|
100
|
+
return body && body.version ? String(body.version) : null;
|
|
101
|
+
} catch (_) {
|
|
102
|
+
return null;
|
|
103
|
+
} finally {
|
|
104
|
+
clearTimeout(timer);
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
async function refresh({ currentVersion, timeoutMs = DEFAULT_TIMEOUT_MS, force = false } = {}) {
|
|
109
|
+
const cached = readCache();
|
|
110
|
+
const now = Date.now();
|
|
111
|
+
if (!force && cached && cached.checked_at) {
|
|
112
|
+
const age = now - new Date(cached.checked_at).getTime();
|
|
113
|
+
if (Number.isFinite(age) && age < DEFAULT_TTL_MS) return cached;
|
|
114
|
+
}
|
|
115
|
+
const latest = await fetchLatest(timeoutMs);
|
|
116
|
+
if (!latest) return cached; // Keep stale cache rather than zeroing it.
|
|
117
|
+
const data = {
|
|
118
|
+
checked_at: new Date().toISOString(),
|
|
119
|
+
latest,
|
|
120
|
+
current_at_check: currentVersion,
|
|
121
|
+
};
|
|
122
|
+
writeCache(data);
|
|
123
|
+
return data;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
function hint({ currentVersion } = {}) {
|
|
127
|
+
if (!currentVersion) return null;
|
|
128
|
+
const cached = readCache();
|
|
129
|
+
if (!cached || !cached.latest) return null;
|
|
130
|
+
if (cmpSemver(cached.latest, currentVersion) <= 0) return null;
|
|
131
|
+
return {
|
|
132
|
+
latest: cached.latest,
|
|
133
|
+
current: currentVersion,
|
|
134
|
+
checked_at: cached.checked_at,
|
|
135
|
+
};
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
function printBanner(info) {
|
|
139
|
+
if (!info) return;
|
|
140
|
+
const chalk = require('chalk');
|
|
141
|
+
const line1 = chalk.yellow('↑') + ' ' + chalk.bold(`baldart ${info.latest}`) +
|
|
142
|
+
chalk.gray(` available (you have ${info.current})`);
|
|
143
|
+
const line2 = chalk.gray(' Update with: ') + chalk.cyan('npm i -g baldart@latest');
|
|
144
|
+
const line3 = chalk.gray(' Suppress with: BALDART_NO_UPDATE_CHECK=1');
|
|
145
|
+
console.log('');
|
|
146
|
+
console.log(line1);
|
|
147
|
+
console.log(line2);
|
|
148
|
+
console.log(line3);
|
|
149
|
+
console.log('');
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
function backgroundCheck({ currentVersion, offline = false } = {}) {
|
|
153
|
+
// Fire-and-forget. Returns immediately; never throws; cannot block exit.
|
|
154
|
+
if (isSuppressed({ offline })) return;
|
|
155
|
+
refresh({ currentVersion }).catch(() => {});
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
/**
|
|
159
|
+
* Cheap entry point for bin/baldart.js: print the banner if a cached update
|
|
160
|
+
* is known, then kick off a refresh for next time. No-op when suppressed.
|
|
161
|
+
*/
|
|
162
|
+
function announceAndRefresh({ currentVersion, offline = false } = {}) {
|
|
163
|
+
if (isSuppressed({ offline })) return null;
|
|
164
|
+
const info = hint({ currentVersion });
|
|
165
|
+
if (info) printBanner(info);
|
|
166
|
+
backgroundCheck({ currentVersion, offline });
|
|
167
|
+
return info;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
module.exports = {
|
|
171
|
+
isSuppressed,
|
|
172
|
+
refresh,
|
|
173
|
+
hint,
|
|
174
|
+
printBanner,
|
|
175
|
+
backgroundCheck,
|
|
176
|
+
announceAndRefresh,
|
|
177
|
+
// Exported for tests / introspection:
|
|
178
|
+
_cmpSemver: cmpSemver,
|
|
179
|
+
_CACHE_FILE: CACHE_FILE,
|
|
180
|
+
_DEFAULT_TTL_MS: DEFAULT_TTL_MS,
|
|
181
|
+
};
|