baldart 4.40.0 → 4.41.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 +21 -0
- package/README.md +6 -2
- package/VERSION +1 -1
- package/framework/.claude/agents/coder.md +4 -2
- package/framework/.claude/agents/qa-sentinel.md +10 -1
- package/framework/.claude/commands/qa.md +2 -0
- package/framework/.claude/skills/toolchain-bootstrap/SKILL.md +127 -0
- package/framework/.claude/workflows/new-card-review.js +17 -2
- package/framework/.claude/workflows/new2.js +3 -0
- package/framework/agents/index.md +2 -0
- package/framework/agents/toolchain-protocol.md +80 -0
- package/framework/docs/TOOLCHAIN-LAYER.md +135 -0
- package/framework/templates/baldart.config.template.yml +40 -0
- package/package.json +1 -1
- package/src/commands/configure.js +81 -0
- package/src/commands/doctor.js +67 -0
- package/src/commands/update.js +12 -0
- package/src/utils/tool-currency.js +52 -0
- package/src/utils/toolchain-adapters/biome.js +92 -0
- package/src/utils/toolchain-adapters/eslint.js +39 -0
- package/src/utils/toolchain-adapters/husky.js +30 -0
- package/src/utils/toolchain-adapters/index.js +83 -0
- package/src/utils/toolchain-adapters/jest.js +34 -0
- package/src/utils/toolchain-adapters/lefthook.js +84 -0
- package/src/utils/toolchain-adapters/prettier.js +39 -0
- package/src/utils/toolchain-adapters/tsc.js +50 -0
- package/src/utils/toolchain-adapters/vitest.js +46 -0
- package/src/utils/toolchain-installer.js +233 -0
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
const fs = require('fs');
|
|
2
|
+
const path = require('path');
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Prettier INCUMBENT detector — detection only, no install.
|
|
6
|
+
*
|
|
7
|
+
* Same contract as the ESLint detector: BALDART never installs or removes
|
|
8
|
+
* Prettier; this lets `configure` detect an existing formatter and offer a
|
|
9
|
+
* manual migration to Biome (which subsumes formatting), never overwriting
|
|
10
|
+
* the user's config.
|
|
11
|
+
*/
|
|
12
|
+
class PrettierDetector {
|
|
13
|
+
constructor(cwd = process.cwd()) { this.cwd = cwd; }
|
|
14
|
+
|
|
15
|
+
get name() { return 'prettier'; }
|
|
16
|
+
get label() { return 'Prettier'; }
|
|
17
|
+
get replacedBy() { return 'biome'; }
|
|
18
|
+
|
|
19
|
+
static detect(cwd = process.cwd()) {
|
|
20
|
+
const markers = [
|
|
21
|
+
'.prettierrc', '.prettierrc.js', '.prettierrc.cjs', '.prettierrc.json',
|
|
22
|
+
'.prettierrc.yml', '.prettierrc.yaml', '.prettierrc.toml',
|
|
23
|
+
'prettier.config.js', 'prettier.config.cjs', 'prettier.config.mjs',
|
|
24
|
+
];
|
|
25
|
+
for (const m of markers) {
|
|
26
|
+
if (fs.existsSync(path.join(cwd, m))) return true;
|
|
27
|
+
}
|
|
28
|
+
try {
|
|
29
|
+
const pkgPath = path.join(cwd, 'package.json');
|
|
30
|
+
if (!fs.existsSync(pkgPath)) return false;
|
|
31
|
+
const pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf8'));
|
|
32
|
+
if (pkg.prettier) return true;
|
|
33
|
+
const allDeps = { ...(pkg.dependencies || {}), ...(pkg.devDependencies || {}) };
|
|
34
|
+
return Boolean(allDeps.prettier);
|
|
35
|
+
} catch { return false; }
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
module.exports = PrettierDetector;
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
const fs = require('fs');
|
|
2
|
+
const path = require('path');
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* TypeScript type-checker (tsc) toolchain adapter.
|
|
6
|
+
*
|
|
7
|
+
* `npm-dev` install of the `typescript` package (often already present). The
|
|
8
|
+
* gate is `npx tsc --noEmit`. `initConfig` is a NO-OP — BALDART NEVER creates or
|
|
9
|
+
* touches `tsconfig.json` (it is deeply project-specific and the #1 data-loss
|
|
10
|
+
* risk). In scope ONLY for TypeScript projects (a tsconfig or a typescript dep);
|
|
11
|
+
* a plain-JS project is not offered tsc. No incumbent (`replaces` empty).
|
|
12
|
+
*/
|
|
13
|
+
class TscAdapter {
|
|
14
|
+
constructor(cwd = process.cwd()) { this.cwd = cwd; }
|
|
15
|
+
|
|
16
|
+
get name() { return 'tsc'; }
|
|
17
|
+
get label() { return 'TypeScript type-checker (tsc)'; }
|
|
18
|
+
get binary() { return 'tsc'; }
|
|
19
|
+
get installMode() { return 'npm-dev'; }
|
|
20
|
+
get npmPackage() { return 'typescript'; }
|
|
21
|
+
|
|
22
|
+
installCommand() { return 'npm install --save-dev typescript'; }
|
|
23
|
+
verifyCommand() { return 'npx --no-install tsc --version'; }
|
|
24
|
+
|
|
25
|
+
/** Never managed by BALDART — tsconfig.json is project-owned. */
|
|
26
|
+
get configFile() { return null; }
|
|
27
|
+
|
|
28
|
+
commands() {
|
|
29
|
+
return { typecheck: 'npx tsc --noEmit' };
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
initConfig() { return { status: 'noop' }; }
|
|
33
|
+
|
|
34
|
+
static get replaces() { return []; }
|
|
35
|
+
|
|
36
|
+
/** In scope ONLY for TypeScript projects. */
|
|
37
|
+
static detect(cwd = process.cwd()) {
|
|
38
|
+
if (fs.existsSync(path.join(cwd, 'tsconfig.json'))) return true;
|
|
39
|
+
if (fs.existsSync(path.join(cwd, 'jsconfig.json'))) return true;
|
|
40
|
+
try {
|
|
41
|
+
const pkgPath = path.join(cwd, 'package.json');
|
|
42
|
+
if (!fs.existsSync(pkgPath)) return false;
|
|
43
|
+
const pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf8'));
|
|
44
|
+
const allDeps = { ...(pkg.dependencies || {}), ...(pkg.devDependencies || {}) };
|
|
45
|
+
return Boolean(allDeps.typescript);
|
|
46
|
+
} catch { return false; }
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
module.exports = TscAdapter;
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
const fs = require('fs');
|
|
2
|
+
const path = require('path');
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Vitest toolchain adapter — the curated test runner.
|
|
6
|
+
*
|
|
7
|
+
* `npm-dev` install. No config is scaffolded: Vitest runs with sensible defaults
|
|
8
|
+
* and many projects configure it inside an existing `vite.config.*`, so writing a
|
|
9
|
+
* standalone `vitest.config.ts` would risk a conflict — `initConfig` is a no-op
|
|
10
|
+
* (non-destructive). `replaces` = ['jest'] so a Jest project routes to the
|
|
11
|
+
* migration proposal instead of an automatic install alongside Jest.
|
|
12
|
+
*/
|
|
13
|
+
class VitestAdapter {
|
|
14
|
+
constructor(cwd = process.cwd()) { this.cwd = cwd; }
|
|
15
|
+
|
|
16
|
+
get name() { return 'vitest'; }
|
|
17
|
+
get label() { return 'Vitest (test runner)'; }
|
|
18
|
+
get binary() { return 'vitest'; }
|
|
19
|
+
get installMode() { return 'npm-dev'; }
|
|
20
|
+
get npmPackage() { return 'vitest'; }
|
|
21
|
+
|
|
22
|
+
installCommand() { return 'npm install --save-dev vitest'; }
|
|
23
|
+
verifyCommand() { return 'npx --no-install vitest --version'; }
|
|
24
|
+
|
|
25
|
+
/** No standalone config — Vitest defaults + optional vite.config integration. */
|
|
26
|
+
get configFile() { return null; }
|
|
27
|
+
|
|
28
|
+
commands() {
|
|
29
|
+
return {
|
|
30
|
+
test: 'npx vitest run',
|
|
31
|
+
test_related: 'npx vitest related --run',
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/** Non-destructive: never scaffolds a config (avoids clobbering vite.config). */
|
|
36
|
+
initConfig() { return { status: 'noop' }; }
|
|
37
|
+
|
|
38
|
+
static get replaces() { return ['jest']; }
|
|
39
|
+
|
|
40
|
+
/** In scope for any JS/TS project (a project needs a test runner). */
|
|
41
|
+
static detect(cwd = process.cwd()) {
|
|
42
|
+
return fs.existsSync(path.join(cwd, 'package.json'));
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
module.exports = VitestAdapter;
|
|
@@ -0,0 +1,233 @@
|
|
|
1
|
+
const { execSync } = require('child_process');
|
|
2
|
+
const fs = require('fs');
|
|
3
|
+
const path = require('path');
|
|
4
|
+
const UI = require('./ui');
|
|
5
|
+
const adapters = require('./toolchain-adapters');
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* High-level installer/wrapper for the curated dev toolchain (Biome today;
|
|
9
|
+
* Vitest, tsc, Lefthook next), used by `baldart configure`, the
|
|
10
|
+
* `/toolchain-bootstrap` skill, and `doctor`.
|
|
11
|
+
*
|
|
12
|
+
* Design (since v4.41.0):
|
|
13
|
+
* - Curated, OPINIONATED defaults: the framework recommends the tools it
|
|
14
|
+
* considers best for a JS/TS project. "Opinionated but askable" — configure
|
|
15
|
+
* preselects, the user opts out.
|
|
16
|
+
* - NON-DESTRUCTIVE: `initConfigs` writes a config file only when absent;
|
|
17
|
+
* incumbent tools (ESLint/Prettier/…) are detected, never installed or
|
|
18
|
+
* removed. Flipping `features.has_toolchain` true→false does NOT uninstall.
|
|
19
|
+
* - NEVER silent in CI: the command layer only invokes `install` in the
|
|
20
|
+
* interactive `configure` path; `--non-interactive`/`--yes` writes the flag
|
|
21
|
+
* and `doctor` backfills.
|
|
22
|
+
* - All mutating ops return plain `{ ok|status, … }` objects and never throw,
|
|
23
|
+
* so the command layers stay thin and can render results / hints.
|
|
24
|
+
*
|
|
25
|
+
* Per-tool adapters under src/utils/toolchain-adapters/ describe HOW to install
|
|
26
|
+
* and verify each tool; this class composes them. The CONSUME side (agents
|
|
27
|
+
* running the gates) reads the resolved literal commands from
|
|
28
|
+
* `toolchain.commands.*` in baldart.config.yml — see
|
|
29
|
+
* framework/agents/toolchain-protocol.md.
|
|
30
|
+
*/
|
|
31
|
+
class ToolchainInstaller {
|
|
32
|
+
constructor(cwd = process.cwd()) { this.cwd = cwd; }
|
|
33
|
+
|
|
34
|
+
// ── Detection ───────────────────────────────────────────────────────────
|
|
35
|
+
|
|
36
|
+
/** True iff this project has a package.json (required to install JS devDeps). */
|
|
37
|
+
hasPackageJson() {
|
|
38
|
+
return fs.existsSync(path.join(this.cwd, 'package.json'));
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/** Curated tools in scope for this project (e.g. ['biome'] on a JS/TS repo). */
|
|
42
|
+
inScope() { return adapters.detectAll(this.cwd); }
|
|
43
|
+
|
|
44
|
+
/** Incumbent tools already present: `[{ name, label, replacedBy }]`. */
|
|
45
|
+
detectIncumbents() { return adapters.detectIncumbents(this.cwd); }
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Curated tools to install on the CLEAN path: in scope, not yet usable, and
|
|
49
|
+
* NOT blocked by an incumbent they would replace (a Jest/husky/ESLint project
|
|
50
|
+
* routes the corresponding tool to `migrations()` instead — never auto-installed
|
|
51
|
+
* over an existing setup). Never throws.
|
|
52
|
+
*/
|
|
53
|
+
recommend() {
|
|
54
|
+
const present = new Set(this.detectIncumbents().map((i) => i.name));
|
|
55
|
+
return this.inScope().filter((name) => {
|
|
56
|
+
if (this._toolUsable(name)) return false;
|
|
57
|
+
return !this._replacesOf(name).some((inc) => present.has(inc));
|
|
58
|
+
});
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Curated tools in scope but BLOCKED by an incumbent they would replace.
|
|
63
|
+
* Returns `[{ tool, replaces:[incumbent names present] }]` — the migration
|
|
64
|
+
* proposal surface (manual, never automatic). Never throws.
|
|
65
|
+
*/
|
|
66
|
+
migrations() {
|
|
67
|
+
const present = new Set(this.detectIncumbents().map((i) => i.name));
|
|
68
|
+
return this.inScope()
|
|
69
|
+
.filter((name) => !this._toolUsable(name))
|
|
70
|
+
.map((name) => ({ tool: name, replaces: this._replacesOf(name).filter((inc) => present.has(inc)) }))
|
|
71
|
+
.filter((m) => m.replaces.length);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/** True iff at least one curated tool is already usable in this project. */
|
|
75
|
+
detect() {
|
|
76
|
+
return this.inScope().some((name) => this._toolUsable(name));
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* In-scope curated tools that RESOLVE right now (devDep present). This is the
|
|
81
|
+
* set whose configs + gate commands are written into baldart.config.yml — a
|
|
82
|
+
* declined migration (incumbent kept) leaves its tool out, so the command
|
|
83
|
+
* stays empty and the consume side falls back. Never throws.
|
|
84
|
+
*/
|
|
85
|
+
activeTools() {
|
|
86
|
+
return this.inScope().filter((name) => this._toolUsable(name));
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
// ── Install (devDeps) ─────────────────────────────────────────────────────
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* Install the given curated tools as project devDependencies. Returns
|
|
93
|
+
* `{ ok, installed:[], failed:[{tool,error}], skipped:[{tool,reason}] }`.
|
|
94
|
+
* Never throws. NOT run in CI / `--non-interactive` by the command layer.
|
|
95
|
+
*/
|
|
96
|
+
install({ tools = this.recommend(), spinner = true } = {}) {
|
|
97
|
+
if (!this.hasPackageJson()) {
|
|
98
|
+
return { ok: false, installed: [], failed: [], skipped: tools.map((t) => ({ tool: t, reason: 'no package.json' })) };
|
|
99
|
+
}
|
|
100
|
+
const installed = [];
|
|
101
|
+
const failed = [];
|
|
102
|
+
const skipped = [];
|
|
103
|
+
for (const name of tools) {
|
|
104
|
+
let adapter;
|
|
105
|
+
try { adapter = adapters.getAdapter(name, this.cwd); }
|
|
106
|
+
catch (err) { skipped.push({ tool: name, reason: (err.message || '').split('\n')[0] }); continue; }
|
|
107
|
+
if (this._toolUsable(name)) { skipped.push({ tool: name, reason: 'already present' }); continue; }
|
|
108
|
+
const sp = spinner ? UI.spinner(`Installing ${adapter.label}…`).start() : null;
|
|
109
|
+
try {
|
|
110
|
+
// Write the tool's default config (if absent) BEFORE `npm install`, so a
|
|
111
|
+
// package whose own postinstall scaffolds from its config (Lefthook's
|
|
112
|
+
// npm postinstall runs `lefthook install` and would otherwise drop a
|
|
113
|
+
// commented-out example, then register no hooks) sees OURS — a biome
|
|
114
|
+
// pre-commit — and registers it. Non-destructive (skips if present).
|
|
115
|
+
if (typeof adapter.initConfig === 'function') {
|
|
116
|
+
try { adapter.initConfig(this.cwd); } catch (_) { /* non-fatal */ }
|
|
117
|
+
}
|
|
118
|
+
execSync(adapter.installCommand(), { cwd: this.cwd, stdio: 'pipe', timeout: 300000 });
|
|
119
|
+
if (this._toolUsable(name)) {
|
|
120
|
+
// Optional post-install side effect (e.g. re-assert Lefthook's git hooks
|
|
121
|
+
// in case the package's own postinstall ran before our config existed).
|
|
122
|
+
// Only runs on this clean-install path — never over an incumbent setup.
|
|
123
|
+
let postWarn = null;
|
|
124
|
+
if (typeof adapter.postInstall === 'function') {
|
|
125
|
+
const pi = adapter.postInstall(this.cwd);
|
|
126
|
+
if (pi && !pi.ok) postWarn = pi.error;
|
|
127
|
+
}
|
|
128
|
+
if (sp) sp.succeed(`Installed ${adapter.label}`);
|
|
129
|
+
if (postWarn) UI.warning(`${adapter.label} installed but post-install step failed: ${postWarn}`);
|
|
130
|
+
installed.push(name);
|
|
131
|
+
} else {
|
|
132
|
+
if (sp) sp.fail(`${adapter.label} install did not resolve`);
|
|
133
|
+
failed.push({ tool: name, error: 'verify failed after install' });
|
|
134
|
+
}
|
|
135
|
+
} catch (err) {
|
|
136
|
+
if (sp) sp.fail(`${adapter.label} install failed`);
|
|
137
|
+
failed.push({ tool: name, error: (err.message || '').split('\n')[0] });
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
return { ok: failed.length === 0, installed, failed, skipped };
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
/**
|
|
144
|
+
* Write each tool's default config when absent (non-destructive). Returns
|
|
145
|
+
* `[{ tool, status:'written'|'skipped'|'noop', reason?, path? }]`. Never throws.
|
|
146
|
+
*/
|
|
147
|
+
initConfigs({ tools = this.inScope() } = {}) {
|
|
148
|
+
const out = [];
|
|
149
|
+
for (const name of tools) {
|
|
150
|
+
let adapter;
|
|
151
|
+
try { adapter = adapters.getAdapter(name, this.cwd); }
|
|
152
|
+
catch { out.push({ tool: name, status: 'noop', reason: 'unknown adapter' }); continue; }
|
|
153
|
+
if (typeof adapter.initConfig !== 'function') { out.push({ tool: name, status: 'noop' }); continue; }
|
|
154
|
+
try { out.push({ tool: name, ...adapter.initConfig(this.cwd) }); }
|
|
155
|
+
catch (err) { out.push({ tool: name, status: 'noop', reason: (err.message || '').split('\n')[0] }); }
|
|
156
|
+
}
|
|
157
|
+
return out;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
/**
|
|
161
|
+
* Merge the literal commands contributed by the given tools into a flat
|
|
162
|
+
* `{ lint, format, typecheck, test, test_related, build, audit }` map (empty
|
|
163
|
+
* string for unprovided keys). Written verbatim into `toolchain.commands.*`.
|
|
164
|
+
*/
|
|
165
|
+
commandsFor(tools = this.inScope()) {
|
|
166
|
+
const merged = {
|
|
167
|
+
lint: '', format: '', typecheck: '', test: '', test_related: '', build: '', audit: '',
|
|
168
|
+
};
|
|
169
|
+
for (const name of tools) {
|
|
170
|
+
let adapter;
|
|
171
|
+
try { adapter = adapters.getAdapter(name, this.cwd); } catch { continue; }
|
|
172
|
+
if (typeof adapter.commands !== 'function') continue;
|
|
173
|
+
const c = adapter.commands() || {};
|
|
174
|
+
for (const k of Object.keys(c)) {
|
|
175
|
+
if (k in merged && c[k]) merged[k] = c[k];
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
return merged;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
// ── Version currency (stub in Phase 1; real impl lands with tool-currency) ──
|
|
182
|
+
|
|
183
|
+
/** Placeholder — devDep currency arrives with `_toolchainRecords` (Phase 3). */
|
|
184
|
+
async checkUpgrade() { return { tool: 'toolchain', installed: null, latest: null, outdated: false, source: 'npm' }; }
|
|
185
|
+
|
|
186
|
+
/** Placeholder — devDep upgrades touch package.json; user runs them. */
|
|
187
|
+
upgrade() { return { ok: false, error: 'devDep upgrades are user-driven (npm install -D -E <pkg>@latest)' }; }
|
|
188
|
+
|
|
189
|
+
// ── Certification ─────────────────────────────────────────────────────────
|
|
190
|
+
|
|
191
|
+
/**
|
|
192
|
+
* One-shot certification of the selected tools, used by `configure`, the
|
|
193
|
+
* bootstrap skill, and `doctor`. Per tool, attests: the devDep RESOLVES
|
|
194
|
+
* (`npx --no-install <bin> --version`) and its config file EXISTS. Returns a
|
|
195
|
+
* plain object; never throws. `ok` = every selected tool resolves (a missing
|
|
196
|
+
* config is reported but does not fail `ok` — it is restorable).
|
|
197
|
+
*/
|
|
198
|
+
certify({ tools = this.inScope() } = {}) {
|
|
199
|
+
const perTool = tools.map((name) => {
|
|
200
|
+
let adapter;
|
|
201
|
+
try { adapter = adapters.getAdapter(name, this.cwd); }
|
|
202
|
+
catch { return { tool: name, devDep: false, config: false }; }
|
|
203
|
+
const devDep = this._toolUsable(name);
|
|
204
|
+
const config = adapter.configFile
|
|
205
|
+
? fs.existsSync(path.join(this.cwd, adapter.configFile))
|
|
206
|
+
: true;
|
|
207
|
+
return { tool: name, devDep, config };
|
|
208
|
+
});
|
|
209
|
+
const ok = perTool.length > 0 && perTool.every((t) => t.devDep);
|
|
210
|
+
return { ok, perTool };
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
// ── internals ─────────────────────────────────────────────────────────────
|
|
214
|
+
|
|
215
|
+
/** Static `replaces` (incumbents superseded) declared by a curated adapter. */
|
|
216
|
+
_replacesOf(name) {
|
|
217
|
+
const Cls = adapters.REGISTRY[name];
|
|
218
|
+
return (Cls && Array.isArray(Cls.replaces)) ? Cls.replaces : [];
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
/** True iff the tool's binary resolves via its verifyCommand in this cwd. */
|
|
222
|
+
_toolUsable(name) {
|
|
223
|
+
let adapter;
|
|
224
|
+
try { adapter = adapters.getAdapter(name, this.cwd); } catch { return false; }
|
|
225
|
+
if (typeof adapter.verifyCommand !== 'function') return false;
|
|
226
|
+
try {
|
|
227
|
+
execSync(adapter.verifyCommand(), { cwd: this.cwd, stdio: 'ignore', timeout: 15000 });
|
|
228
|
+
return true;
|
|
229
|
+
} catch { return false; }
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
module.exports = ToolchainInstaller;
|