baldart 3.9.0 → 3.10.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.
@@ -30,6 +30,7 @@ const GitUtils = require('../utils/git');
30
30
  const UI = require('../utils/ui');
31
31
  const State = require('../utils/state');
32
32
  const Hooks = require('../utils/hooks');
33
+ const LspInstaller = require('../utils/lsp-installer');
33
34
 
34
35
  const FRAMEWORK_DIR = '.framework';
35
36
  const REPO_DEFAULT = 'antbald/BALDART';
@@ -196,6 +197,25 @@ async function detectState(cwd, opts = {}) {
196
197
 
197
198
  state.editGateRegistered = false;
198
199
  try { state.editGateRegistered = Hooks.isRegistered(cwd); } catch (_) {}
200
+
201
+ // ---- LSP layer (since v3.10.0) -------------------------------------
202
+ state.lspEnabled = false;
203
+ state.lspInstalled = [];
204
+ state.lspBroken = [];
205
+ try {
206
+ // Reuse the `config` already parsed above instead of re-reading the file.
207
+ if (config && !config.__malformed && config.features && config.features.has_lsp_layer === true) {
208
+ state.lspEnabled = true;
209
+ const installed = (config.lsp && config.lsp.installed_servers) || [];
210
+ state.lspInstalled = installed;
211
+ const autoVerify = !(config.lsp && config.lsp.auto_verify === false);
212
+ if (autoVerify && installed.length) {
213
+ const installer = new LspInstaller(cwd);
214
+ const results = installer.verifyServers(installed);
215
+ state.lspBroken = results.filter(r => !r.ok).map(r => r.name);
216
+ }
217
+ }
218
+ } catch (_) { /* never block doctor on LSP probe */ }
199
219
  }
200
220
 
201
221
  return state;
@@ -306,6 +326,27 @@ function planActions(state) {
306
326
  });
307
327
  }
308
328
 
329
+ if (state.lspEnabled && state.lspBroken && state.lspBroken.length > 0) {
330
+ actions.push({
331
+ key: 'lsp-fix',
332
+ label: `Reinstall ${state.lspBroken.length} broken LSP server(s)`,
333
+ why: `LSP layer is enabled but these language servers are not reachable: ${state.lspBroken.join(', ')}. Symbol search will silently fall back to Grep until they are reinstalled.`,
334
+ autoOk: false, // touches dev-deps and is the kind of action a user may
335
+ // have made deliberate (switching from npm pyright to system pyright);
336
+ // keep the outer prompt + the per-server confirm both alive.
337
+ run: async () => {
338
+ const installer = new LspInstaller(state.cwd);
339
+ const { installed, manual, skipped } = await installer.installServers(state.lspBroken);
340
+ if (installed.length) UI.success(`Reinstalled: ${installed.map(i => i.name).join(', ')}`);
341
+ for (const m of manual) {
342
+ UI.warning(`System-level reinstall required for ${m.label}:`);
343
+ console.log(` ${m.command}`);
344
+ }
345
+ if (skipped.length) UI.warning(`Skipped: ${skipped.map(s => `${s.name} (${s.reason})`).join(', ')}`);
346
+ },
347
+ });
348
+ }
349
+
309
350
  const remoteAhead = state.remote.fetched && state.remote.behind > 0;
310
351
  const hasLocalWork = state.workingTreeDirty > 0 || state.commitsAhead > 0;
311
352
 
@@ -437,6 +478,20 @@ function renderDiagnostic(state) {
437
478
  state.editGateRegistered ? 'ok' : 'warn'
438
479
  ));
439
480
 
481
+ if (state.lspEnabled) {
482
+ const total = state.lspInstalled.length;
483
+ const broken = state.lspBroken.length;
484
+ const value = total === 0
485
+ ? 'enabled but no servers installed'
486
+ : broken === 0
487
+ ? `${total} server(s) verified (${state.lspInstalled.join(', ')})`
488
+ : `${broken}/${total} broken (${state.lspBroken.join(', ')})`;
489
+ const severity = total === 0 || broken > 0 ? 'warn' : 'ok';
490
+ console.log(statusLine('LSP layer', value, severity));
491
+ } else {
492
+ console.log(statusLine('LSP layer', 'disabled', 'ok'));
493
+ }
494
+
440
495
  console.log();
441
496
  }
442
497
 
@@ -0,0 +1,29 @@
1
+ const fs = require('fs');
2
+ const path = require('path');
3
+
4
+ /**
5
+ * Go LSP adapter — gopls.
6
+ *
7
+ * gopls is a Go binary; we can't install it into node_modules. installMode
8
+ * is 'system' which means the configure flow prints the canonical install
9
+ * command and the user runs it themselves. The verify step still checks
10
+ * that the binary is in PATH.
11
+ */
12
+ class GoAdapter {
13
+ constructor(cwd = process.cwd()) { this.cwd = cwd; }
14
+
15
+ get name() { return 'go'; }
16
+ get label() { return 'Go (gopls)'; }
17
+ get binary() { return 'gopls'; }
18
+ get installMode() { return 'system'; }
19
+
20
+ installCommand() { return 'go install golang.org/x/tools/gopls@latest'; }
21
+ verifyCommand() { return 'gopls version'; }
22
+ claudePluginId() { return null; }
23
+
24
+ static detect(cwd = process.cwd()) {
25
+ return fs.existsSync(path.join(cwd, 'go.mod'));
26
+ }
27
+ }
28
+
29
+ module.exports = GoAdapter;
@@ -0,0 +1,49 @@
1
+ const TypeScriptAdapter = require('./typescript');
2
+ const PythonAdapter = require('./python');
3
+ const GoAdapter = require('./go');
4
+ const RustAdapter = require('./rust');
5
+ const RubyAdapter = require('./ruby');
6
+
7
+ /**
8
+ * LSP adapter registry.
9
+ *
10
+ * Adding a new language (Java, C/C++, PHP, Elixir, …):
11
+ * 1. Create `src/utils/lsp-adapters/<lang>.js` exporting a class with the
12
+ * same shape as TypeScriptAdapter (name, label, binary, installMode,
13
+ * installCommand, verifyCommand, claudePluginId, static detect).
14
+ * 2. Add it to the REGISTRY below.
15
+ * 3. (Optional) Wire a Claude Code plugin id if one exists.
16
+ *
17
+ * Adapters describe *how* to install/verify a language server; they do NOT
18
+ * speak LSP themselves. The actual symbol search at runtime happens through
19
+ * whatever Claude Code plugin or MCP server the user has loaded.
20
+ */
21
+ const REGISTRY = {
22
+ typescript: TypeScriptAdapter,
23
+ python: PythonAdapter,
24
+ go: GoAdapter,
25
+ rust: RustAdapter,
26
+ ruby: RubyAdapter
27
+ };
28
+
29
+ function listAdapters() { return Object.keys(REGISTRY); }
30
+
31
+ function getAdapter(name, cwd) {
32
+ const Cls = REGISTRY[name];
33
+ if (!Cls) throw new Error(`Unknown LSP adapter: ${name}. Available: ${listAdapters().join(', ')}`);
34
+ return new Cls(cwd);
35
+ }
36
+
37
+ /**
38
+ * Probe the cwd for every known language and return the names of adapters
39
+ * whose static detect() fires. Pure: no install side effects.
40
+ */
41
+ function detectAll(cwd = process.cwd()) {
42
+ return Object.entries(REGISTRY)
43
+ .filter(([, Cls]) => {
44
+ try { return Cls.detect(cwd); } catch { return false; }
45
+ })
46
+ .map(([name]) => name);
47
+ }
48
+
49
+ module.exports = { REGISTRY, listAdapters, getAdapter, detectAll };
@@ -0,0 +1,42 @@
1
+ const fs = require('fs');
2
+ const path = require('path');
3
+
4
+ /**
5
+ * Python LSP adapter — Pyright.
6
+ *
7
+ * Pyright ships as both an npm package and a pip package. We default to the
8
+ * npm devDep route so installs stay project-local and version-pinned without
9
+ * touching the user's Python env. Users who already drive Python via pip/uv
10
+ * can override by removing pyright from devDeps and installing system-wide.
11
+ *
12
+ * Detection signal: pyproject.toml, setup.py, setup.cfg, requirements.txt,
13
+ * Pipfile, or a top-level .py source tree.
14
+ */
15
+ class PythonAdapter {
16
+ constructor(cwd = process.cwd()) { this.cwd = cwd; }
17
+
18
+ get name() { return 'python'; }
19
+ get label() { return 'Python (Pyright)'; }
20
+ get binary() { return 'pyright'; }
21
+ get installMode() { return 'npm-dev'; }
22
+ get npmPackage() { return 'pyright'; }
23
+
24
+ installCommand() { return 'npm install --save-dev pyright'; }
25
+ verifyCommand() { return 'npx --no-install pyright --version'; }
26
+ claudePluginId() { return null; }
27
+
28
+ static detect(cwd = process.cwd()) {
29
+ const markers = ['pyproject.toml', 'setup.py', 'setup.cfg', 'requirements.txt', 'Pipfile'];
30
+ for (const m of markers) {
31
+ if (fs.existsSync(path.join(cwd, m))) return true;
32
+ }
33
+ // Fallback: a `src/` or top-level *.py file is a strong enough signal.
34
+ try {
35
+ const entries = fs.readdirSync(cwd);
36
+ if (entries.some(e => e.endsWith('.py'))) return true;
37
+ } catch { /* ignore */ }
38
+ return false;
39
+ }
40
+ }
41
+
42
+ module.exports = PythonAdapter;
@@ -0,0 +1,30 @@
1
+ const fs = require('fs');
2
+ const path = require('path');
3
+
4
+ /**
5
+ * Ruby LSP adapter — ruby-lsp (Shopify).
6
+ *
7
+ * Installed as a gem; system-mode. Users on Bundler-managed projects may
8
+ * prefer to add `gem 'ruby-lsp', group: :development` to their Gemfile and
9
+ * `bundle install` instead — we print the simple gem install command for
10
+ * portability.
11
+ */
12
+ class RubyAdapter {
13
+ constructor(cwd = process.cwd()) { this.cwd = cwd; }
14
+
15
+ get name() { return 'ruby'; }
16
+ get label() { return 'Ruby (ruby-lsp)'; }
17
+ get binary() { return 'ruby-lsp'; }
18
+ get installMode() { return 'system'; }
19
+
20
+ installCommand() { return 'gem install ruby-lsp'; }
21
+ verifyCommand() { return 'ruby-lsp --version'; }
22
+ claudePluginId() { return null; }
23
+
24
+ static detect(cwd = process.cwd()) {
25
+ const markers = ['Gemfile', '.ruby-version', 'config.ru'];
26
+ return markers.some(m => fs.existsSync(path.join(cwd, m)));
27
+ }
28
+ }
29
+
30
+ module.exports = RubyAdapter;
@@ -0,0 +1,26 @@
1
+ const fs = require('fs');
2
+ const path = require('path');
3
+
4
+ /**
5
+ * Rust LSP adapter — rust-analyzer.
6
+ *
7
+ * Installed through rustup as a toolchain component. System-mode only.
8
+ */
9
+ class RustAdapter {
10
+ constructor(cwd = process.cwd()) { this.cwd = cwd; }
11
+
12
+ get name() { return 'rust'; }
13
+ get label() { return 'Rust (rust-analyzer)'; }
14
+ get binary() { return 'rust-analyzer'; }
15
+ get installMode() { return 'system'; }
16
+
17
+ installCommand() { return 'rustup component add rust-analyzer'; }
18
+ verifyCommand() { return 'rust-analyzer --version'; }
19
+ claudePluginId() { return null; }
20
+
21
+ static detect(cwd = process.cwd()) {
22
+ return fs.existsSync(path.join(cwd, 'Cargo.toml'));
23
+ }
24
+ }
25
+
26
+ module.exports = RustAdapter;
@@ -0,0 +1,54 @@
1
+ const fs = require('fs');
2
+ const path = require('path');
3
+
4
+ /**
5
+ * TypeScript / JavaScript LSP adapter.
6
+ *
7
+ * Language server: `typescript-language-server` (Node, npm).
8
+ * Install mode: project-local devDep — keeps the binary inside the consumer
9
+ * repo's node_modules and avoids polluting the user's global env.
10
+ *
11
+ * Detection signal: presence of `tsconfig.json`, `jsconfig.json`, or a
12
+ * `package.json` referencing typescript in deps/devDeps.
13
+ */
14
+ class TypeScriptAdapter {
15
+ constructor(cwd = process.cwd()) { this.cwd = cwd; }
16
+
17
+ get name() { return 'typescript'; }
18
+ get label() { return 'TypeScript / JavaScript'; }
19
+ get binary() { return 'typescript-language-server'; }
20
+
21
+ /** 'npm-dev' = npm devDependency, 'system' = printed install instructions. */
22
+ get installMode() { return 'npm-dev'; }
23
+
24
+ /** npm package to install when mode='npm-dev'. */
25
+ get npmPackage() { return 'typescript-language-server typescript'; }
26
+
27
+ /** Shell command the user (or installer) should run. */
28
+ installCommand() {
29
+ return 'npm install --save-dev typescript-language-server typescript';
30
+ }
31
+
32
+ /** Verify the binary is reachable (no execution side effects). */
33
+ verifyCommand() { return 'npx --no-install typescript-language-server --version'; }
34
+
35
+ /** Claude Code code-intelligence plugin id, if any. Null = use generic LSP. */
36
+ claudePluginId() { return null; }
37
+
38
+ /** Autodetect whether this language is in scope for the project. */
39
+ static detect(cwd = process.cwd()) {
40
+ const markers = ['tsconfig.json', 'jsconfig.json'];
41
+ for (const m of markers) {
42
+ if (fs.existsSync(path.join(cwd, m))) return true;
43
+ }
44
+ try {
45
+ const pkgPath = path.join(cwd, 'package.json');
46
+ if (!fs.existsSync(pkgPath)) return false;
47
+ const pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf8'));
48
+ const allDeps = { ...(pkg.dependencies || {}), ...(pkg.devDependencies || {}) };
49
+ return Boolean(allDeps.typescript);
50
+ } catch { return false; }
51
+ }
52
+ }
53
+
54
+ module.exports = TypeScriptAdapter;
@@ -0,0 +1,118 @@
1
+ const { execSync } = require('child_process');
2
+ const { detectAll, getAdapter, listAdapters } = require('./lsp-adapters');
3
+ const UI = require('./ui');
4
+
5
+ /**
6
+ * High-level LSP installer used by `baldart configure` and the `lsp-bootstrap`
7
+ * skill / doctor step. Owns the install + verify side effects so the command
8
+ * layers stay thin.
9
+ *
10
+ * Contract:
11
+ * - Pure detection (no install) when only `detectLanguages` is called.
12
+ * - All mutating ops respect `--non-interactive` via the `interactive` flag.
13
+ * - Returns plain objects (no throws) so the caller can render results.
14
+ */
15
+ class LspInstaller {
16
+ constructor(cwd = process.cwd()) { this.cwd = cwd; }
17
+
18
+ /** Languages whose marker files exist under cwd. */
19
+ detectLanguages() { return detectAll(this.cwd); }
20
+
21
+ /** Adapters that BALDART knows about (regardless of detection). */
22
+ knownAdapters() { return listAdapters(); }
23
+
24
+ /**
25
+ * Build a recommendation list from detected languages. Each entry carries
26
+ * the install command and the install mode so the UI can decide whether
27
+ * to run it automatically (npm-dev) or just print it (system).
28
+ */
29
+ recommend(langs = this.detectLanguages()) {
30
+ return langs.map(name => {
31
+ const a = getAdapter(name, this.cwd);
32
+ return {
33
+ name: a.name,
34
+ label: a.label,
35
+ binary: a.binary,
36
+ installMode: a.installMode,
37
+ installCommand: a.installCommand(),
38
+ verifyCommand: a.verifyCommand(),
39
+ claudePluginId: a.claudePluginId()
40
+ };
41
+ });
42
+ }
43
+
44
+ /**
45
+ * Install one or more language servers. Returns `{ installed, skipped, manual }`.
46
+ * - npm-dev adapters: executed in-process (npm install --save-dev …).
47
+ * - system adapters: never executed automatically; surfaced under `manual`
48
+ * so the caller can print the command for the user to run.
49
+ *
50
+ * `opts.interactive`: when true, asks confirmation per server (default true).
51
+ * `opts.dryRun`: when true, no commands are executed.
52
+ */
53
+ async installServers(serverNames, opts = {}) {
54
+ const { interactive = true, dryRun = false } = opts;
55
+ const installed = [];
56
+ const skipped = [];
57
+ const manual = [];
58
+
59
+ for (const name of serverNames) {
60
+ let a;
61
+ try { a = getAdapter(name, this.cwd); }
62
+ catch (err) { skipped.push({ name, reason: err.message }); continue; }
63
+
64
+ if (a.installMode === 'system') {
65
+ manual.push({
66
+ name: a.name,
67
+ label: a.label,
68
+ command: a.installCommand()
69
+ });
70
+ continue;
71
+ }
72
+
73
+ if (interactive) {
74
+ const ok = await UI.confirm(`Install ${a.label} via \`${a.installCommand()}\`?`, true);
75
+ if (!ok) { skipped.push({ name, reason: 'user declined' }); continue; }
76
+ }
77
+
78
+ if (dryRun) { installed.push({ name, dryRun: true }); continue; }
79
+
80
+ const spinner = UI.spinner(`Installing ${a.label}…`).start();
81
+ try {
82
+ execSync(a.installCommand(), { cwd: this.cwd, stdio: 'pipe' });
83
+ spinner.succeed(`Installed ${a.label}`);
84
+ installed.push({ name });
85
+ } catch (err) {
86
+ spinner.fail(`Failed to install ${a.label}`);
87
+ skipped.push({ name, reason: err.message.split('\n')[0] });
88
+ }
89
+ }
90
+
91
+ return { installed, skipped, manual };
92
+ }
93
+
94
+ /**
95
+ * Verify each named server's binary is reachable. Returns
96
+ * `[{ name, ok, output | error }]`. Never throws.
97
+ */
98
+ verifyServers(serverNames) {
99
+ return serverNames.map(name => {
100
+ let a;
101
+ try { a = getAdapter(name, this.cwd); }
102
+ catch (err) { return { name, ok: false, error: err.message }; }
103
+
104
+ try {
105
+ const out = execSync(a.verifyCommand(), {
106
+ cwd: this.cwd,
107
+ stdio: ['ignore', 'pipe', 'pipe'],
108
+ timeout: 8000
109
+ }).toString().trim();
110
+ return { name, ok: true, output: out };
111
+ } catch (err) {
112
+ return { name, ok: false, error: (err.message || '').split('\n')[0] };
113
+ }
114
+ });
115
+ }
116
+ }
117
+
118
+ module.exports = LspInstaller;