severally-mcp 2.3.1 → 2.4.2
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/README.ja.md +52 -17
- package/README.md +52 -16
- package/bin/severally-mcp.mjs +14 -2
- package/docs/install.ja.md +48 -20
- package/docs/install.md +48 -21
- package/package.json +15 -3
- package/plugins/severally/.claude-plugin/plugin.json +1 -1
- package/plugins/severally/.codex-plugin/plugin.json +1 -1
- package/plugins/severally/dist/severally-mcp.mjs +6573 -5978
- package/plugins/severally/skills/_template/reference.md.tmpl +2 -2
- package/plugins/severally/skills/antigravity/severally/reference.md +2 -2
- package/plugins/severally/skills/claude/severally/reference.md +2 -2
- package/plugins/severally/skills/codex/severally/reference.md +2 -2
- package/plugins/severally/skills/opencode/severally/reference.md +2 -2
- package/scripts/build.mjs +76 -68
- package/scripts/doctor.mjs +66 -20
- package/scripts/init-config.mjs +8 -6
- package/scripts/install.mjs +327 -90
- package/scripts/live-check.mjs +62 -23
- package/scripts/live-mcp-check.mjs +52 -16
- package/scripts/test.mjs +10 -4
- package/src/adapters/antigravity-sandbox.mjs +62 -28
- package/src/adapters/antigravity.mjs +111 -30
- package/src/adapters/claude-code.mjs +121 -28
- package/src/adapters/codex.mjs +125 -33
- package/src/adapters/opencode-sandbox.mjs +59 -22
- package/src/adapters/opencode.mjs +144 -30
- package/src/brief.mjs +148 -47
- package/src/consultants.mjs +255 -48
- package/src/cross-spawn-escape.d.ts +7 -0
- package/src/export.mjs +99 -18
- package/src/expose-paths.mjs +310 -50
- package/src/failures.mjs +31 -14
- package/src/jobs.mjs +863 -249
- package/src/jsonc.mjs +11 -3
- package/src/parse-result.mjs +172 -40
- package/src/platform.mjs +56 -8
- package/src/policy.mjs +245 -52
- package/src/redact.mjs +59 -16
- package/src/result-schema.mjs +13 -0
- package/src/run.mjs +225 -30
- package/src/schema.mjs +132 -27
- package/src/server.mjs +178 -81
- package/src/status.mjs +94 -13
- package/src/store.mjs +418 -35
package/src/jsonc.mjs
CHANGED
|
@@ -11,8 +11,12 @@
|
|
|
11
11
|
// byte offsets -- and therefore JSON.parse's error positions -- still point at
|
|
12
12
|
// the right place in the original text.
|
|
13
13
|
|
|
14
|
-
/**
|
|
14
|
+
/**
|
|
15
|
+
* Remove // and /* *\/ comments and trailing commas, preserving offsets.
|
|
16
|
+
* @param {string} text
|
|
17
|
+
*/
|
|
15
18
|
export function stripJsonc(text) {
|
|
19
|
+
/** @type {string[]} */
|
|
16
20
|
const out = [];
|
|
17
21
|
let inString = false;
|
|
18
22
|
let i = 0;
|
|
@@ -28,7 +32,7 @@ export function stripJsonc(text) {
|
|
|
28
32
|
};
|
|
29
33
|
|
|
30
34
|
while (i < text.length) {
|
|
31
|
-
const c = text
|
|
35
|
+
const c = text.charAt(i);
|
|
32
36
|
const next = text[i + 1];
|
|
33
37
|
|
|
34
38
|
if (inString) {
|
|
@@ -78,7 +82,11 @@ export function stripJsonc(text) {
|
|
|
78
82
|
return out.join('');
|
|
79
83
|
}
|
|
80
84
|
|
|
81
|
-
/**
|
|
85
|
+
/**
|
|
86
|
+
* JSON.parse, tolerating comments and trailing commas.
|
|
87
|
+
* @param {string} text
|
|
88
|
+
* @returns {unknown}
|
|
89
|
+
*/
|
|
82
90
|
export function parseJsonc(text) {
|
|
83
91
|
return JSON.parse(stripJsonc(text));
|
|
84
92
|
}
|
package/src/parse-result.mjs
CHANGED
|
@@ -8,6 +8,10 @@ import { RESULT_KEYS } from './result-schema.mjs';
|
|
|
8
8
|
import { redact } from './redact.mjs';
|
|
9
9
|
|
|
10
10
|
export class OutputError extends Error {
|
|
11
|
+
/**
|
|
12
|
+
* @param {string} message
|
|
13
|
+
* @param {string} [detail]
|
|
14
|
+
*/
|
|
11
15
|
constructor(message, detail) {
|
|
12
16
|
super(message);
|
|
13
17
|
this.name = 'OutputError';
|
|
@@ -17,6 +21,38 @@ export class OutputError extends Error {
|
|
|
17
21
|
|
|
18
22
|
const O = POLICY.output;
|
|
19
23
|
|
|
24
|
+
/** @typedef {'high' | 'medium' | 'low'} Level */
|
|
25
|
+
/** @typedef {'sufficient' | 'thin' | 'insufficient'} EvidenceBasis */
|
|
26
|
+
/** @typedef {(typeof STANCES)[number]} Stance */
|
|
27
|
+
/** @typedef {{ id: string, point: string, grounds: string | null, impact: string | null, severity: Level | null, confidence: Level | null }} Finding */
|
|
28
|
+
/** @typedef {{ option: string, tradeoffs: string | null, when_preferred: string | null }} Alternative */
|
|
29
|
+
/** @typedef {{ id: string, item: string, why_it_matters: string | null, how_to_obtain: string | null }} Unknown */
|
|
30
|
+
/** @typedef {{ condition: string, changes_to: string | null }} DecisionChanger */
|
|
31
|
+
/** @typedef {{ id: string, check: string, method: string | null, expected_signal: string | null }} NextCheck */
|
|
32
|
+
/** @typedef {{ topic: string, your_position: string | null, why_unresolved: string | null }} RemainingDisagreement */
|
|
33
|
+
/** @typedef {{ title: string | null, url: string | null, relevance: string | null }} Reference */
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* The validated consult result, in the shape of CONSULT_RESULT_SCHEMA.
|
|
37
|
+
* @typedef {object} ConsultResult
|
|
38
|
+
* @property {string} summary
|
|
39
|
+
* @property {Level | null} confidence
|
|
40
|
+
* @property {EvidenceBasis | null} evidence_basis
|
|
41
|
+
* @property {Stance | null} stance
|
|
42
|
+
* @property {Finding[]} findings
|
|
43
|
+
* @property {Alternative[]} alternatives
|
|
44
|
+
* @property {Unknown[]} unknowns
|
|
45
|
+
* @property {DecisionChanger[]} decision_changers
|
|
46
|
+
* @property {NextCheck[]} next_checks
|
|
47
|
+
* @property {RemainingDisagreement[]} remaining_disagreements
|
|
48
|
+
* @property {Reference[]} references
|
|
49
|
+
*/
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* @param {unknown} v
|
|
53
|
+
* @param {number} max
|
|
54
|
+
* @returns {string | null}
|
|
55
|
+
*/
|
|
20
56
|
function clampText(v, max) {
|
|
21
57
|
if (typeof v !== 'string') return null;
|
|
22
58
|
const t = v.trim();
|
|
@@ -24,34 +60,61 @@ function clampText(v, max) {
|
|
|
24
60
|
return t.length > max ? `${t.slice(0, max)}\n…[truncated by severally]` : t;
|
|
25
61
|
}
|
|
26
62
|
|
|
63
|
+
/**
|
|
64
|
+
* Narrows a string to one of the listed values.
|
|
65
|
+
* @template {string} T
|
|
66
|
+
* @param {readonly T[]} list
|
|
67
|
+
* @param {string} t
|
|
68
|
+
* @returns {t is T}
|
|
69
|
+
*/
|
|
70
|
+
const oneOf = (list, t) => list.some((x) => x === t);
|
|
71
|
+
|
|
72
|
+
/** @type {Level[]} */
|
|
27
73
|
const LEVELS = ['high', 'medium', 'low'];
|
|
74
|
+
/** @type {EvidenceBasis[]} */
|
|
28
75
|
const BASIS = ['sufficient', 'thin', 'insufficient'];
|
|
29
76
|
|
|
77
|
+
/**
|
|
78
|
+
* @param {unknown} v
|
|
79
|
+
* @param {Level | null} [fallback]
|
|
80
|
+
* @returns {Level | null}
|
|
81
|
+
*/
|
|
30
82
|
function level(v, fallback = null) {
|
|
31
83
|
const t = typeof v === 'string' ? v.trim().toLowerCase() : '';
|
|
32
|
-
return LEVELS
|
|
84
|
+
return oneOf(LEVELS, t) ? t : fallback;
|
|
33
85
|
}
|
|
34
86
|
|
|
87
|
+
/**
|
|
88
|
+
* @param {unknown} v
|
|
89
|
+
* @returns {EvidenceBasis | null}
|
|
90
|
+
*/
|
|
35
91
|
function basis(v) {
|
|
36
92
|
const t = typeof v === 'string' ? v.trim().toLowerCase() : '';
|
|
37
|
-
return BASIS
|
|
93
|
+
return oneOf(BASIS, t) ? t : null;
|
|
38
94
|
}
|
|
39
95
|
|
|
96
|
+
/**
|
|
97
|
+
* @param {unknown} v
|
|
98
|
+
* @returns {Stance | null}
|
|
99
|
+
*/
|
|
40
100
|
function stance(v) {
|
|
41
101
|
const t = typeof v === 'string' ? v.trim().toLowerCase() : '';
|
|
42
|
-
return STANCES
|
|
102
|
+
return oneOf(STANCES, t) ? t : null;
|
|
43
103
|
}
|
|
44
104
|
|
|
45
|
-
/**
|
|
105
|
+
/**
|
|
106
|
+
* Extract a JSON object from raw model text (fenced block, or first balanced object).
|
|
107
|
+
* @param {unknown} text
|
|
108
|
+
* @returns {Record<string, unknown> | null}
|
|
109
|
+
*/
|
|
46
110
|
export function extractJson(text) {
|
|
47
111
|
if (typeof text !== 'string') return null;
|
|
48
112
|
const trimmed = text.trim();
|
|
49
113
|
if (!trimmed) return null;
|
|
114
|
+
/** @type {string[]} */
|
|
50
115
|
const candidates = [];
|
|
51
116
|
if (trimmed.startsWith('{')) candidates.push(trimmed);
|
|
52
|
-
const
|
|
53
|
-
let m;
|
|
54
|
-
while ((m = fence.exec(trimmed)) !== null) candidates.push(m[1].trim());
|
|
117
|
+
for (const [, body = ''] of trimmed.matchAll(/```(?:json)?\s*([\s\S]*?)```/gi)) candidates.push(body.trim());
|
|
55
118
|
// Longest balanced {...} run as a last resort.
|
|
56
119
|
const start = trimmed.indexOf('{');
|
|
57
120
|
if (start !== -1) {
|
|
@@ -60,14 +123,26 @@ export function extractJson(text) {
|
|
|
60
123
|
let esc = false;
|
|
61
124
|
for (let i = start; i < trimmed.length; i++) {
|
|
62
125
|
const ch = trimmed[i];
|
|
63
|
-
if (esc) {
|
|
64
|
-
|
|
65
|
-
|
|
126
|
+
if (esc) {
|
|
127
|
+
esc = false;
|
|
128
|
+
continue;
|
|
129
|
+
}
|
|
130
|
+
if (ch === '\\') {
|
|
131
|
+
esc = true;
|
|
132
|
+
continue;
|
|
133
|
+
}
|
|
134
|
+
if (ch === '"') {
|
|
135
|
+
inStr = !inStr;
|
|
136
|
+
continue;
|
|
137
|
+
}
|
|
66
138
|
if (inStr) continue;
|
|
67
139
|
if (ch === '{') depth++;
|
|
68
140
|
else if (ch === '}') {
|
|
69
141
|
depth--;
|
|
70
|
-
if (depth === 0) {
|
|
142
|
+
if (depth === 0) {
|
|
143
|
+
candidates.push(trimmed.slice(start, i + 1));
|
|
144
|
+
break;
|
|
145
|
+
}
|
|
71
146
|
}
|
|
72
147
|
}
|
|
73
148
|
}
|
|
@@ -75,7 +150,9 @@ export function extractJson(text) {
|
|
|
75
150
|
try {
|
|
76
151
|
const parsed = JSON.parse(c);
|
|
77
152
|
if (parsed && typeof parsed === 'object' && !Array.isArray(parsed)) return parsed;
|
|
78
|
-
} catch {
|
|
153
|
+
} catch {
|
|
154
|
+
/* try the next candidate */
|
|
155
|
+
}
|
|
79
156
|
}
|
|
80
157
|
return null;
|
|
81
158
|
}
|
|
@@ -84,24 +161,34 @@ export function extractJson(text) {
|
|
|
84
161
|
// verdict against one point later. The id is ours, assigned at parse time and
|
|
85
162
|
// never read from the answer: a consultant that numbered its own findings
|
|
86
163
|
// cannot make one of them impersonate another.
|
|
164
|
+
/**
|
|
165
|
+
* @template T
|
|
166
|
+
* @param {unknown} raw
|
|
167
|
+
* @param {Record<string, 'text' | 'level'>} fields
|
|
168
|
+
* @param {{ requiredField: string, idPrefix?: string | null }} options
|
|
169
|
+
* @returns {T[]}
|
|
170
|
+
*/
|
|
87
171
|
function mapList(raw, fields, { requiredField, idPrefix = null }) {
|
|
88
172
|
if (!Array.isArray(raw)) return [];
|
|
173
|
+
/** @type {T[]} */
|
|
89
174
|
const out = [];
|
|
90
175
|
for (const entry of raw.slice(0, O.listMax)) {
|
|
91
176
|
if (!entry || typeof entry !== 'object') continue;
|
|
177
|
+
/** @type {Record<string, unknown>} */
|
|
92
178
|
const item = idPrefix ? { id: `${idPrefix}${out.length + 1}` } : {};
|
|
93
179
|
for (const [key, kind] of Object.entries(fields)) {
|
|
94
180
|
item[key] = kind === 'level' ? level(entry[key]) : clampText(entry[key], O.itemTextMax);
|
|
95
181
|
}
|
|
96
182
|
if (!item[requiredField]) continue;
|
|
97
|
-
|
|
183
|
+
// Every key of `fields` is a key of T, and the required one is set.
|
|
184
|
+
out.push(/** @type {T} */ (item));
|
|
98
185
|
}
|
|
99
186
|
return out;
|
|
100
187
|
}
|
|
101
188
|
|
|
102
189
|
/**
|
|
103
|
-
* @param {
|
|
104
|
-
* @returns {{result:
|
|
190
|
+
* @param {Record<string, unknown> | string} raw parsed JSON object, or raw text to mine one out of
|
|
191
|
+
* @returns {{result: ConsultResult, quality: {missing_sections: string[], findings_without_grounds: number, unsourced: boolean, list_truncated?: Record<string, number>}}}
|
|
105
192
|
*/
|
|
106
193
|
export function normalizeResult(raw) {
|
|
107
194
|
const objIn = typeof raw === 'string' ? extractJson(raw) : raw;
|
|
@@ -115,45 +202,90 @@ export function normalizeResult(raw) {
|
|
|
115
202
|
throw new OutputError('consultant response has no usable "summary" field', JSON.stringify(src).slice(0, 2000));
|
|
116
203
|
}
|
|
117
204
|
|
|
205
|
+
/** @type {ConsultResult} */
|
|
118
206
|
const result = {
|
|
119
207
|
summary,
|
|
120
208
|
confidence: level(src.confidence),
|
|
121
209
|
evidence_basis: basis(src.evidence_basis),
|
|
122
210
|
stance: stance(src.stance),
|
|
123
|
-
findings: mapList(
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
211
|
+
findings: mapList(
|
|
212
|
+
src.findings,
|
|
213
|
+
{
|
|
214
|
+
point: 'text',
|
|
215
|
+
grounds: 'text',
|
|
216
|
+
impact: 'text',
|
|
217
|
+
severity: 'level',
|
|
218
|
+
confidence: 'level',
|
|
219
|
+
},
|
|
220
|
+
{ requiredField: 'point', idPrefix: 'f' },
|
|
221
|
+
),
|
|
222
|
+
alternatives: mapList(
|
|
223
|
+
src.alternatives,
|
|
224
|
+
{
|
|
225
|
+
option: 'text',
|
|
226
|
+
tradeoffs: 'text',
|
|
227
|
+
when_preferred: 'text',
|
|
228
|
+
},
|
|
229
|
+
{ requiredField: 'option' },
|
|
230
|
+
),
|
|
231
|
+
unknowns: mapList(
|
|
232
|
+
src.unknowns,
|
|
233
|
+
{
|
|
234
|
+
item: 'text',
|
|
235
|
+
why_it_matters: 'text',
|
|
236
|
+
how_to_obtain: 'text',
|
|
237
|
+
},
|
|
238
|
+
{ requiredField: 'item', idPrefix: 'u' },
|
|
239
|
+
),
|
|
240
|
+
decision_changers: mapList(
|
|
241
|
+
src.decision_changers,
|
|
242
|
+
{
|
|
243
|
+
condition: 'text',
|
|
244
|
+
changes_to: 'text',
|
|
245
|
+
},
|
|
246
|
+
{ requiredField: 'condition' },
|
|
247
|
+
),
|
|
248
|
+
next_checks: mapList(
|
|
249
|
+
src.next_checks,
|
|
250
|
+
{
|
|
251
|
+
check: 'text',
|
|
252
|
+
method: 'text',
|
|
253
|
+
expected_signal: 'text',
|
|
254
|
+
},
|
|
255
|
+
{ requiredField: 'check', idPrefix: 'c' },
|
|
256
|
+
),
|
|
257
|
+
remaining_disagreements: mapList(
|
|
258
|
+
src.remaining_disagreements,
|
|
259
|
+
{
|
|
260
|
+
topic: 'text',
|
|
261
|
+
your_position: 'text',
|
|
262
|
+
why_unresolved: 'text',
|
|
263
|
+
},
|
|
264
|
+
{ requiredField: 'topic' },
|
|
265
|
+
),
|
|
141
266
|
references: (Array.isArray(src.references) ? src.references : [])
|
|
142
267
|
.slice(0, O.referencesMax)
|
|
143
|
-
.map((r) =>
|
|
144
|
-
|
|
145
|
-
|
|
268
|
+
.map((r) =>
|
|
269
|
+
r && typeof r === 'object'
|
|
270
|
+
? { title: clampText(r.title, 300), url: clampText(r.url, 1000), relevance: clampText(r.relevance, 600) }
|
|
271
|
+
: { title: clampText(r, 300), url: null, relevance: null },
|
|
272
|
+
)
|
|
146
273
|
.filter((r) => r.title || r.url),
|
|
147
274
|
};
|
|
148
275
|
|
|
149
276
|
// The lists are cut to O.listMax before anything else looks at them; say how
|
|
150
277
|
// long the ones that were cut had been, so the cut is not silent.
|
|
151
|
-
const listTruncated = Object.fromEntries(
|
|
152
|
-
.
|
|
153
|
-
|
|
278
|
+
const listTruncated = Object.fromEntries(
|
|
279
|
+
['findings', 'alternatives', 'unknowns', 'decision_changers', 'next_checks', 'remaining_disagreements'].flatMap((k) => {
|
|
280
|
+
const v = src[k];
|
|
281
|
+
return Array.isArray(v) && v.length > O.listMax ? [[k, v.length]] : [];
|
|
282
|
+
}),
|
|
283
|
+
);
|
|
154
284
|
|
|
285
|
+
/** @type {Record<string, unknown>} */
|
|
286
|
+
const fields = result;
|
|
155
287
|
const missing = RESULT_KEYS.filter((k) => {
|
|
156
|
-
const v =
|
|
288
|
+
const v = fields[k];
|
|
157
289
|
return v === null || v === undefined || (Array.isArray(v) && v.length === 0);
|
|
158
290
|
});
|
|
159
291
|
|
package/src/platform.mjs
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
/// <reference path="./cross-spawn-escape.d.ts" />
|
|
1
2
|
import fs from 'node:fs';
|
|
2
3
|
import path from 'node:path';
|
|
3
4
|
import which from 'which';
|
|
@@ -5,12 +6,24 @@ import spawn from 'cross-spawn';
|
|
|
5
6
|
import escape from 'cross-spawn/lib/util/escape.js';
|
|
6
7
|
import { spawn as nativeSpawn, spawnSync as nativeSpawnSync } from 'node:child_process';
|
|
7
8
|
|
|
9
|
+
/**
|
|
10
|
+
* @param {string} source
|
|
11
|
+
* @param {string} destination
|
|
12
|
+
*/
|
|
8
13
|
export function copyTree(source, destination) {
|
|
9
14
|
// Node 22.23.2's native cpSync fast path corrupts non-ASCII Windows paths.
|
|
10
15
|
// A filter selects its JS traversal, preserving Japanese directory names.
|
|
11
16
|
fs.cpSync(source, destination, { recursive: true, filter: () => true });
|
|
12
17
|
}
|
|
13
18
|
|
|
19
|
+
/** @typedef {import('node:child_process').SpawnOptions & { cwd?: string | undefined, env?: NodeJS.ProcessEnv | undefined }} SpawnOpts */
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* @template {SpawnOpts} O
|
|
23
|
+
* @param {string} command
|
|
24
|
+
* @param {readonly string[]} args
|
|
25
|
+
* @param {O} options
|
|
26
|
+
*/
|
|
14
27
|
function batchInvocation(command, args, options) {
|
|
15
28
|
if (process.platform !== 'win32') return null;
|
|
16
29
|
const resolved = findCommand(command, options);
|
|
@@ -26,23 +39,46 @@ function batchInvocation(command, args, options) {
|
|
|
26
39
|
};
|
|
27
40
|
}
|
|
28
41
|
|
|
42
|
+
/**
|
|
43
|
+
* @overload
|
|
44
|
+
* @param {string} command
|
|
45
|
+
* @param {readonly string[]} args
|
|
46
|
+
* @param {SpawnOpts & { stdio: ['pipe', 'pipe', 'pipe'] }} options
|
|
47
|
+
* @returns {import('node:child_process').ChildProcessWithoutNullStreams}
|
|
48
|
+
*/
|
|
49
|
+
/**
|
|
50
|
+
* @overload
|
|
51
|
+
* @param {string} command
|
|
52
|
+
* @param {readonly string[]} args
|
|
53
|
+
* @param {SpawnOpts} [options]
|
|
54
|
+
* @returns {import('node:child_process').ChildProcess}
|
|
55
|
+
*/
|
|
56
|
+
/**
|
|
57
|
+
* @param {string} command
|
|
58
|
+
* @param {readonly string[]} args
|
|
59
|
+
* @param {SpawnOpts} [options]
|
|
60
|
+
*/
|
|
29
61
|
export function spawnCommand(command, args, options = {}) {
|
|
30
62
|
const batch = batchInvocation(command, args, options);
|
|
31
63
|
return batch ? nativeSpawn(batch.command, batch.args, batch.options) : spawn(command, args, options);
|
|
32
64
|
}
|
|
33
65
|
|
|
34
66
|
// Explicit script paths are supported by cross-spawn's shebang handling too.
|
|
67
|
+
/**
|
|
68
|
+
* @param {string} command
|
|
69
|
+
* @param {{ cwd?: string | undefined, env?: NodeJS.ProcessEnv | undefined }} [options]
|
|
70
|
+
* @returns {string | null}
|
|
71
|
+
*/
|
|
35
72
|
export function findCommand(command, { cwd = process.cwd(), env = process.env } = {}) {
|
|
36
73
|
if (typeof command !== 'string' || !command) return null;
|
|
37
74
|
cwd = path.resolve(cwd);
|
|
38
75
|
const windows = process.platform === 'win32';
|
|
39
|
-
const value = (name) =>
|
|
40
|
-
? Object.keys(env).find((
|
|
76
|
+
const value = (/** @type {string} */ name) => {
|
|
77
|
+
const key = windows ? Object.keys(env).find((k) => k.toUpperCase() === name) : name;
|
|
78
|
+
return key === undefined ? undefined : env[key];
|
|
79
|
+
};
|
|
41
80
|
const explicit = command.includes('/') || (windows && command.includes('\\'));
|
|
42
|
-
const dirs = explicit ? [cwd] : [
|
|
43
|
-
...(windows ? [cwd] : []),
|
|
44
|
-
...(value('PATH') ?? '').split(path.delimiter).filter(Boolean),
|
|
45
|
-
];
|
|
81
|
+
const dirs = explicit ? [cwd] : [...(windows ? [cwd] : []), ...(value('PATH') ?? '').split(path.delimiter).filter(Boolean)];
|
|
46
82
|
for (const dir of dirs) {
|
|
47
83
|
// Absolute candidates prevent which's own cwd lookup from resolving a
|
|
48
84
|
// wrapper in the server directory instead of the child's directory.
|
|
@@ -56,13 +92,25 @@ export function findCommand(command, { cwd = process.cwd(), env = process.env }
|
|
|
56
92
|
if (!fs.statSync(candidate).isFile()) return null;
|
|
57
93
|
fs.accessSync(candidate, fs.constants.X_OK);
|
|
58
94
|
return candidate;
|
|
59
|
-
} catch {
|
|
95
|
+
} catch {
|
|
96
|
+
/* not installed */
|
|
97
|
+
}
|
|
60
98
|
}
|
|
61
99
|
return null;
|
|
62
100
|
}
|
|
63
101
|
|
|
102
|
+
/** @typedef {Omit<import('node:child_process').SpawnSyncOptionsWithStringEncoding, 'cwd' | 'encoding'> & { cwd?: string, encoding?: BufferEncoding }} ExecOptions */
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* @param {string} command
|
|
106
|
+
* @param {readonly string[]} args
|
|
107
|
+
* @param {ExecOptions} [options]
|
|
108
|
+
* @returns {string}
|
|
109
|
+
*/
|
|
64
110
|
export function execCommandSync(command, args, options = {}) {
|
|
65
|
-
|
|
111
|
+
/** @type {BufferEncoding} */
|
|
112
|
+
const encoding = 'utf8';
|
|
113
|
+
const opts = { windowsHide: true, encoding, ...options };
|
|
66
114
|
const batch = batchInvocation(command, args, opts);
|
|
67
115
|
const result = batch ? nativeSpawnSync(batch.command, batch.args, batch.options) : spawn.sync(command, args, opts);
|
|
68
116
|
if (result.error) throw result.error;
|