pi-mcp-adapter 5.3.0 → 5.3.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/CHANGELOG.md +37 -0
- package/bearer-command-resolver.ts +8 -4
- package/cli.js +7 -2
- package/commands.ts +5 -3
- package/config.ts +201 -25
- package/dist/bearer-command-resolver.js +7 -3
- package/dist/bearer-command-resolver.js.map +1 -1
- package/dist/config.js +184 -25
- package/dist/config.js.map +1 -1
- package/dist/metadata-cache.d.ts +5 -0
- package/dist/metadata-cache.js +21 -0
- package/dist/metadata-cache.js.map +1 -1
- package/dist/project-server-trust.d.ts +8 -0
- package/dist/project-server-trust.js +45 -12
- package/dist/project-server-trust.js.map +1 -1
- package/dist/sampling-handler.d.ts +3 -2
- package/dist/sampling-handler.js +12 -3
- package/dist/sampling-handler.js.map +1 -1
- package/dist/server-manager.js +7 -8
- package/dist/server-manager.js.map +1 -1
- package/dist/types.d.ts +6 -2
- package/dist/types.js +7 -0
- package/dist/types.js.map +1 -1
- package/dist/utils.d.ts +7 -0
- package/dist/utils.js +42 -3
- package/dist/utils.js.map +1 -1
- package/docs/auth.md +1 -1
- package/docs/configuration.md +4 -2
- package/docs/prompts-and-ui.md +1 -1
- package/docs/servers.md +1 -1
- package/docs/tools.md +3 -1
- package/index.ts +5 -18
- package/init.ts +1 -0
- package/mcp-panel.ts +25 -23
- package/metadata-cache.ts +23 -0
- package/package.json +1 -1
- package/project-server-trust.ts +49 -12
- package/proxy-modes.ts +17 -2
- package/sampling-handler.ts +21 -15
- package/server-manager.ts +7 -8
- package/tool-metadata.ts +19 -0
- package/types.ts +11 -2
- package/utils.ts +42 -4
package/project-server-trust.ts
CHANGED
|
@@ -80,7 +80,8 @@ export function hashProjectServerDefinition(definition: ServerDefinition): strin
|
|
|
80
80
|
|
|
81
81
|
export function canonicalProjectRoot(cwd: string): string {
|
|
82
82
|
try {
|
|
83
|
-
|
|
83
|
+
// The native realpath returns the on-disk casing, so `C:\Users\Me` and `c:\users\me` share approvals.
|
|
84
|
+
return realpathSync.native(cwd);
|
|
84
85
|
} catch {
|
|
85
86
|
return resolve(cwd);
|
|
86
87
|
}
|
|
@@ -114,9 +115,9 @@ function linkedWorktreeRepoScope(dotGit: string): string | undefined {
|
|
|
114
115
|
if (!lstatSync(dotGit).isFile()) return undefined;
|
|
115
116
|
const pointer = /^gitdir: (.+)$/m.exec(readFileSync(dotGit, "utf8"))?.[1]?.trim();
|
|
116
117
|
if (!pointer) return undefined;
|
|
117
|
-
const adminDir = realpathSync(resolve(dirname(dotGit), pointer));
|
|
118
|
+
const adminDir = realpathSync.native(resolve(dirname(dotGit), pointer));
|
|
118
119
|
const backLink = readFileSync(join(adminDir, "gitdir"), "utf8").trim();
|
|
119
|
-
if (realpathSync(resolve(adminDir, backLink)) !== realpathSync(dotGit)) return undefined;
|
|
120
|
+
if (realpathSync.native(resolve(adminDir, backLink)) !== realpathSync.native(dotGit)) return undefined;
|
|
120
121
|
const commonDir = dirname(dirname(adminDir));
|
|
121
122
|
const bare = /^\s*bare\s*=\s*(true|yes|on|1)\s*$/im.test(readFileSync(join(commonDir, "config"), "utf8"));
|
|
122
123
|
return basename(commonDir) === ".git" && !bare ? dirname(commonDir) : `git-dir:${commonDir}`;
|
|
@@ -169,6 +170,49 @@ export function approveProjectServer(cwd: string, serverName: string, definition
|
|
|
169
170
|
});
|
|
170
171
|
}
|
|
171
172
|
|
|
173
|
+
function isApproved(approvals: ApprovalStore, projectRoot: string, serverName: string, definition: ServerDefinition): boolean {
|
|
174
|
+
const definitionHash = hashProjectServerDefinition(definition);
|
|
175
|
+
return approvals.approvals.some(entry =>
|
|
176
|
+
entry.projectRoot === projectRoot && entry.serverName === serverName && entry.definitionHash === definitionHash);
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
function isProjectTrusted(ctx: Pick<ExtensionContext, "isProjectTrusted">): boolean {
|
|
180
|
+
try {
|
|
181
|
+
return ctx.isProjectTrusted();
|
|
182
|
+
} catch {
|
|
183
|
+
return false;
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
/**
|
|
188
|
+
* After the adapter edits config on the user's behalf (a panel save), re-approve each named project
|
|
189
|
+
* server whose pre-edit definition was approved, so the edit does not revoke the approval. Only a
|
|
190
|
+
* `directTools` change carries the approval over; any other difference, such as a concurrent edit to
|
|
191
|
+
* the file, still needs a new approval. A server without a matching approval stays unapproved: an
|
|
192
|
+
* adapter edit never creates one.
|
|
193
|
+
*/
|
|
194
|
+
export function keepProjectServerApprovals(
|
|
195
|
+
ctx: Pick<ExtensionContext, "cwd" | "isProjectTrusted">,
|
|
196
|
+
before: McpConfig,
|
|
197
|
+
after: McpConfig,
|
|
198
|
+
serverNames: Iterable<string>,
|
|
199
|
+
): void {
|
|
200
|
+
if (!isProjectTrusted(ctx)) return;
|
|
201
|
+
const projectServers = asLoadedConfig(after).projectServers;
|
|
202
|
+
const projectRoot = projectApprovalScope(ctx.cwd);
|
|
203
|
+
const approvals = loadApprovals();
|
|
204
|
+
for (const name of serverNames) {
|
|
205
|
+
const previous = before.mcpServers[name];
|
|
206
|
+
const next = after.mcpServers[name];
|
|
207
|
+
if (!previous || !next || !projectServers.has(name)) continue;
|
|
208
|
+
if (!isApproved(approvals, projectRoot, name, previous)) continue;
|
|
209
|
+
const { directTools: _previousDirectTools, ...previousRest } = previous;
|
|
210
|
+
const { directTools: _nextDirectTools, ...nextRest } = next;
|
|
211
|
+
if (hashProjectServerDefinition(previousRest) !== hashProjectServerDefinition(nextRest)) continue;
|
|
212
|
+
approveProjectServer(ctx.cwd, name, next);
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
|
|
172
216
|
function describeServer(definition: ServerDefinition): string {
|
|
173
217
|
if (definition.command) {
|
|
174
218
|
return [definition.command, ...(definition.args ?? [])].map(value => JSON.stringify(value)).join(" ");
|
|
@@ -186,12 +230,7 @@ export async function applyProjectServerTrust(
|
|
|
186
230
|
const blockedServers = new Map<string, ProjectServerBlock>();
|
|
187
231
|
if (loaded.projectServers.size === 0) return { config, blockedServers };
|
|
188
232
|
|
|
189
|
-
|
|
190
|
-
try {
|
|
191
|
-
projectTrusted = ctx.isProjectTrusted();
|
|
192
|
-
} catch {
|
|
193
|
-
projectTrusted = false;
|
|
194
|
-
}
|
|
233
|
+
const projectTrusted = isProjectTrusted(ctx);
|
|
195
234
|
// RPC sessions also have a UI, but Pi reads RPC input only after session_start, so a dialog here never resolves.
|
|
196
235
|
const interactive = ctx.hasUI && ctx.mode === "tui";
|
|
197
236
|
const projectRoot = projectApprovalScope(ctx.cwd);
|
|
@@ -200,9 +239,7 @@ export async function applyProjectServerTrust(
|
|
|
200
239
|
for (const [name, source] of loaded.projectServers) {
|
|
201
240
|
const definition = config.mcpServers[name];
|
|
202
241
|
if (!definition || isServerDisabled(definition)) continue;
|
|
203
|
-
const
|
|
204
|
-
const approved = approvals.approvals.some(entry =>
|
|
205
|
-
entry.projectRoot === projectRoot && entry.serverName === name && entry.definitionHash === definitionHash);
|
|
242
|
+
const approved = isApproved(approvals, projectRoot, name, definition);
|
|
206
243
|
if (projectTrusted && (approved || (!interactive && loaded.projectServerPolicy === "allow"))) continue;
|
|
207
244
|
|
|
208
245
|
let reason: ProjectServerBlockReason;
|
package/proxy-modes.ts
CHANGED
|
@@ -7,7 +7,7 @@ import { getServerPrefix, getToolNameCandidates, isServerDisabled, parseUiPrompt
|
|
|
7
7
|
import { lazyConnect, markKeepAliveAfterConnect, notifyToolMetadataUpdated, updateServerMetadata, updateMetadataCache, getFailureAgeSeconds, updateStatusBar, clearFailure, recordFailure } from "./init.ts";
|
|
8
8
|
import { abortable, throwIfAborted } from "./abort.ts";
|
|
9
9
|
import { combineAbortSignals, isAbortError } from "./runtime-owner.ts";
|
|
10
|
-
import { buildToolMetadata, getToolNames, formatSchema } from "./tool-metadata.ts";
|
|
10
|
+
import { buildToolMetadata, getToolNames, formatSchema, isPlainTypeSchema } from "./tool-metadata.ts";
|
|
11
11
|
import { renderTsShape } from "./ts-shape.ts";
|
|
12
12
|
import { getObservedOutput, observedOutputRecorder, renderOutputShape } from "./output-shape.ts";
|
|
13
13
|
import { reconstructPromptMetadata } from "./metadata-cache.ts";
|
|
@@ -800,7 +800,9 @@ export function executeDescribe(state: McpExtensionState, toolName: string, serv
|
|
|
800
800
|
|
|
801
801
|
if (toolMeta.inputSchema && !toolMeta.resourceUri) {
|
|
802
802
|
const shape = renderTsShape(toolMeta.inputSchema);
|
|
803
|
-
text += shape
|
|
803
|
+
text += shape !== null && isPlainTypeSchema(toolMeta.inputSchema)
|
|
804
|
+
? `\nShape:\n${shape}`
|
|
805
|
+
: `\nInput schema (JSON Schema):\n\`\`\`json\n${JSON.stringify(toolMeta.inputSchema, null, 2)}\n\`\`\``;
|
|
804
806
|
} else if (toolMeta.resourceUri) {
|
|
805
807
|
text += `\nNo parameters required (resource tool).`;
|
|
806
808
|
} else {
|
|
@@ -819,6 +821,16 @@ export function executeDescribe(state: McpExtensionState, toolName: string, serv
|
|
|
819
821
|
};
|
|
820
822
|
}
|
|
821
823
|
|
|
824
|
+
export async function executeGuardedDescribe(state: McpExtensionState, toolName: string, serverOverride?: string): Promise<ProxyToolResult> {
|
|
825
|
+
const result = executeDescribe(state, toolName, serverOverride);
|
|
826
|
+
const guarded = await guardMcpOutput(result.content, resolveMcpOutputGuardOptions(state.config.settings));
|
|
827
|
+
return {
|
|
828
|
+
...result,
|
|
829
|
+
content: guarded.content,
|
|
830
|
+
details: { ...result.details, ...guardedMcpDetails(guarded) },
|
|
831
|
+
};
|
|
832
|
+
}
|
|
833
|
+
|
|
822
834
|
function renderSearchResults(
|
|
823
835
|
state: McpExtensionState,
|
|
824
836
|
query: string,
|
|
@@ -868,6 +880,9 @@ function renderSearchResults(
|
|
|
868
880
|
text += shape === null
|
|
869
881
|
? `\n Parameters:\n${formatSchema(match.tool.inputSchema, " ")}\n`
|
|
870
882
|
: `\n Shape:\n${shape.split("\n").map(line => ` ${line}`).join("\n")}\n`;
|
|
883
|
+
if (shape === null || !isPlainTypeSchema(match.tool.inputSchema)) {
|
|
884
|
+
text += ` Parameter summary only. Full input schema: mcp(${JSON.stringify({ describe: match.tool.name, server: match.server })})\n`;
|
|
885
|
+
}
|
|
871
886
|
} else if (match.tool.resourceUri) {
|
|
872
887
|
text += " No parameters (resource tool).\n";
|
|
873
888
|
}
|
package/sampling-handler.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { Api, AssistantMessage, Message, Model, TextContent } from "@earendil-works/pi-ai";
|
|
1
|
+
import type { Api, AssistantMessage, Message, Model, ModelThinkingLevel, TextContent } from "@earendil-works/pi-ai";
|
|
2
2
|
import { truncateAtWord } from "./utils.ts";
|
|
3
3
|
import { throwIfAborted } from "./abort.ts";
|
|
4
4
|
import type { ExtensionUIContext, ModelRegistry } from "@earendil-works/pi-coding-agent";
|
|
@@ -12,7 +12,7 @@ import {
|
|
|
12
12
|
} from "@modelcontextprotocol/client";
|
|
13
13
|
|
|
14
14
|
export type SamplingUIContext = Pick<ExtensionUIContext, "confirm">;
|
|
15
|
-
export type SamplingModelRegistry = Pick<ModelRegistry, "getAvailable" | "complete"
|
|
15
|
+
export type SamplingModelRegistry = Pick<ModelRegistry, "getAvailable" | "complete"> & Partial<Pick<ModelRegistry, "streamSimple">>;
|
|
16
16
|
|
|
17
17
|
export interface SamplingHandlerOptions {
|
|
18
18
|
serverName: string;
|
|
@@ -20,6 +20,7 @@ export interface SamplingHandlerOptions {
|
|
|
20
20
|
ui?: SamplingUIContext;
|
|
21
21
|
modelRegistry: SamplingModelRegistry;
|
|
22
22
|
getCurrentModel: () => Model<Api> | undefined;
|
|
23
|
+
getThinkingLevel: () => ModelThinkingLevel;
|
|
23
24
|
getSignal: () => AbortSignal | undefined;
|
|
24
25
|
}
|
|
25
26
|
|
|
@@ -65,19 +66,24 @@ export async function handleSamplingRequest(
|
|
|
65
66
|
);
|
|
66
67
|
throwIfAborted(signal);
|
|
67
68
|
|
|
68
|
-
const
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
{
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
69
|
+
const context = {
|
|
70
|
+
...(params.systemPrompt !== undefined ? { systemPrompt: params.systemPrompt } : {}),
|
|
71
|
+
messages,
|
|
72
|
+
};
|
|
73
|
+
const requestOptions = {
|
|
74
|
+
maxTokens: params.maxTokens,
|
|
75
|
+
...(params.temperature !== undefined ? { temperature: params.temperature } : {}),
|
|
76
|
+
...(params.metadata !== undefined ? { metadata: params.metadata as Record<string, unknown> } : {}),
|
|
77
|
+
...(signal ? { signal } : {}),
|
|
78
|
+
};
|
|
79
|
+
const thinkingLevel = options.getThinkingLevel();
|
|
80
|
+
// Only streamSimple applies the thinking level; Pi before 0.86 has no streamSimple, so sampling runs with thinking off there.
|
|
81
|
+
const result = options.modelRegistry.streamSimple
|
|
82
|
+
? await options.modelRegistry.streamSimple(model, context, {
|
|
83
|
+
...requestOptions,
|
|
84
|
+
...(thinkingLevel !== "off" ? { reasoning: thinkingLevel } : {}),
|
|
85
|
+
}).result()
|
|
86
|
+
: await options.modelRegistry.complete(model, context, requestOptions);
|
|
81
87
|
|
|
82
88
|
const converted = convertAssistantResult(result);
|
|
83
89
|
throwIfAborted(signal);
|
package/server-manager.ts
CHANGED
|
@@ -1307,7 +1307,7 @@ export class McpServerManager {
|
|
|
1307
1307
|
transport,
|
|
1308
1308
|
requestTimeoutMs: this.getResolvedRequestTimeoutMs(definition),
|
|
1309
1309
|
clientInfo: { name: `pi-mcp-${name}`, version: "1.0.0" },
|
|
1310
|
-
clientCapabilities: this.buildClientCapabilities(),
|
|
1310
|
+
clientCapabilities: this.buildClientCapabilities(definition),
|
|
1311
1311
|
...(this.elicitationConfig
|
|
1312
1312
|
? {
|
|
1313
1313
|
onElicitation: (request: Parameters<typeof handleElicitationRequest>[1], signal?: AbortSignal) =>
|
|
@@ -1449,13 +1449,12 @@ export class McpServerManager {
|
|
|
1449
1449
|
}
|
|
1450
1450
|
}
|
|
1451
1451
|
|
|
1452
|
-
private buildClientCapabilities() {
|
|
1452
|
+
private buildClientCapabilities(definition: ServerDefinition) {
|
|
1453
1453
|
return {
|
|
1454
|
-
|
|
1455
|
-
|
|
1456
|
-
|
|
1457
|
-
},
|
|
1458
|
-
},
|
|
1454
|
+
// openUi: false tells the server there is no UI, so MCP App tools return results instead of deferring to a form.
|
|
1455
|
+
...(definition.openUi === false
|
|
1456
|
+
? {}
|
|
1457
|
+
: { extensions: { "io.modelcontextprotocol/ui": { mimeTypes: [RESOURCE_MIME_TYPE] } } }),
|
|
1459
1458
|
...(this.samplingConfig ? { sampling: {} } : {}),
|
|
1460
1459
|
...(this.elicitationConfig
|
|
1461
1460
|
? {
|
|
@@ -1469,7 +1468,7 @@ export class McpServerManager {
|
|
|
1469
1468
|
}
|
|
1470
1469
|
|
|
1471
1470
|
private createClient(serverName: string, definition: ServerDefinition): Client {
|
|
1472
|
-
const capabilities = this.buildClientCapabilities();
|
|
1471
|
+
const capabilities = this.buildClientCapabilities(definition);
|
|
1473
1472
|
const versionNegotiation = resolveVersionNegotiation(definition);
|
|
1474
1473
|
let client: Client;
|
|
1475
1474
|
client = new AdapterClient(
|
package/tool-metadata.ts
CHANGED
|
@@ -154,6 +154,25 @@ export function findToolByName(metadata: ToolMetadata[] | undefined, toolName: s
|
|
|
154
154
|
return metadata.find(m => m.name.replace(/-/g, "_") === normalized);
|
|
155
155
|
}
|
|
156
156
|
|
|
157
|
+
/** Keep only plain structural schemas compact; preserve everything else as original JSON. */
|
|
158
|
+
export function isPlainTypeSchema(schema: unknown): boolean {
|
|
159
|
+
if (!schema || typeof schema !== "object" || Array.isArray(schema)) return false;
|
|
160
|
+
const s = schema as Record<string, unknown>;
|
|
161
|
+
if (Object.keys(s).some(key => !["type", "properties", "required", "items"].includes(key))) return false;
|
|
162
|
+
if (s.type === "integer" || Array.isArray(s.type)) return false;
|
|
163
|
+
|
|
164
|
+
const properties = s.properties;
|
|
165
|
+
if (properties !== undefined) {
|
|
166
|
+
if (s.type !== "object" || !properties || typeof properties !== "object" || Array.isArray(properties)) return false;
|
|
167
|
+
if (!Object.values(properties).every(isPlainTypeSchema)) return false;
|
|
168
|
+
}
|
|
169
|
+
if (s.required !== undefined) {
|
|
170
|
+
if (!Array.isArray(s.required) || s.required.some(name => typeof name !== "string" || !Object.hasOwn(properties ?? {}, name))) return false;
|
|
171
|
+
}
|
|
172
|
+
if (s.items !== undefined && (s.type !== "array" || !isPlainTypeSchema(s.items))) return false;
|
|
173
|
+
return true;
|
|
174
|
+
}
|
|
175
|
+
|
|
157
176
|
/** Whether the schema formatter has field descriptions worth showing beside a compact shape. */
|
|
158
177
|
export function hasSchemaDescriptions(schema: unknown, includeRoot = false): boolean {
|
|
159
178
|
if (!schema || typeof schema !== "object" || Array.isArray(schema)) return false;
|
package/types.ts
CHANGED
|
@@ -493,7 +493,7 @@ export interface ServerEntry {
|
|
|
493
493
|
searchKeywords?: Record<string, string[]>;
|
|
494
494
|
// Require interactive approval before calling matching MCP tools/resources.
|
|
495
495
|
approveTools?: boolean | "destructive" | string[];
|
|
496
|
-
/** Set to false to
|
|
496
|
+
/** Set to false to tell this server the host has no MCP App UI; its tools run and return results inline. */
|
|
497
497
|
openUi?: boolean;
|
|
498
498
|
// Debug
|
|
499
499
|
debug?: boolean; // Show server stderr (default: false)
|
|
@@ -785,8 +785,17 @@ export interface ServerProvenance {
|
|
|
785
785
|
path: string;
|
|
786
786
|
kind: "user" | "project" | "import";
|
|
787
787
|
importKind?: string;
|
|
788
|
-
/** Settings from Pi's `mcp.json` that the adapter could not translate
|
|
788
|
+
/** Settings from Pi's `mcp.json` that the adapter could not translate, or that OpenCode ignores in an imported server. */
|
|
789
789
|
ignoredSettings?: string[];
|
|
790
|
+
/** Set when `ignoredSettings` came from an imported OpenCode server rather than Pi's `mcp.json`. */
|
|
791
|
+
ignoredSettingsSource?: "opencode";
|
|
792
|
+
}
|
|
793
|
+
|
|
794
|
+
/** The line `/mcp-adapter` and `doctor` show for a server's ignored settings. */
|
|
795
|
+
export function describeIgnoredSettings(provenance: ServerProvenance | undefined): string | undefined {
|
|
796
|
+
if (!provenance?.ignoredSettings) return undefined;
|
|
797
|
+
const file = provenance.ignoredSettingsSource === "opencode" ? "OpenCode" : "Pi mcp.json";
|
|
798
|
+
return `Ignored ${file} settings: ${provenance.ignoredSettings.join(", ")}`;
|
|
790
799
|
}
|
|
791
800
|
|
|
792
801
|
export interface McpAuthResult {
|
package/utils.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
2
|
-
import { spawnSync } from "node:child_process";
|
|
2
|
+
import { spawnSync, type SpawnSyncOptionsWithStringEncoding } from "node:child_process";
|
|
3
3
|
import { existsSync, realpathSync } from "node:fs";
|
|
4
4
|
import { homedir, platform } from "node:os";
|
|
5
5
|
import { dirname, extname, isAbsolute, join, relative, resolve, sep } from "node:path";
|
|
@@ -180,6 +180,41 @@ export function interpolateEnvRecord(values: Record<string, string> | undefined,
|
|
|
180
180
|
const COMMAND_SECRET_TIMEOUT_MS = 10_000;
|
|
181
181
|
const COMMAND_SECRET_MAX_OUTPUT_BYTES = 1024 * 1024;
|
|
182
182
|
|
|
183
|
+
let windowsCommandBash: string | null | undefined;
|
|
184
|
+
|
|
185
|
+
function isLegacyWslBash(path: string): boolean {
|
|
186
|
+
return /^[a-z]:\\windows\\(?:system32|sysnative)\\bash\.exe$/.test(path.replace(/\//g, "\\").toLowerCase());
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
function findWindowsBash(): string | null {
|
|
190
|
+
const installs = [process.env.ProgramFiles, process.env["ProgramFiles(x86)"]]
|
|
191
|
+
.filter((dir): dir is string => Boolean(dir))
|
|
192
|
+
.map(dir => `${dir}\\Git\\bin\\bash.exe`);
|
|
193
|
+
const found = installs.find(path => existsSync(path));
|
|
194
|
+
if (found) return found;
|
|
195
|
+
try {
|
|
196
|
+
const result = spawnSync("where", ["bash.exe"], { encoding: "utf8", timeout: 5000, windowsHide: true });
|
|
197
|
+
if (result.status !== 0 || !result.stdout) return null;
|
|
198
|
+
return result.stdout.split(/\r?\n/)
|
|
199
|
+
.map(line => line.trim())
|
|
200
|
+
.find(path => path && !isLegacyWslBash(path) && existsSync(path)) ?? null;
|
|
201
|
+
} catch {
|
|
202
|
+
return null;
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
/**
|
|
207
|
+
* On Windows, the bash used to run `!command` values: Git Bash in its standard
|
|
208
|
+
* install locations, then bash.exe on PATH (skipping the legacy WSL launcher),
|
|
209
|
+
* as Pi does. Undefined elsewhere or when none is found, so callers keep the
|
|
210
|
+
* platform default shell.
|
|
211
|
+
*/
|
|
212
|
+
export function getWindowsCommandBash(): string | undefined {
|
|
213
|
+
if (process.platform !== "win32") return undefined;
|
|
214
|
+
if (windowsCommandBash === undefined) windowsCommandBash = findWindowsBash();
|
|
215
|
+
return windowsCommandBash ?? undefined;
|
|
216
|
+
}
|
|
217
|
+
|
|
183
218
|
/** Resolve a secret value, executing only a single leading `!` command marker. */
|
|
184
219
|
export function resolveCommandSecret(value: string, context: string): string;
|
|
185
220
|
export function resolveCommandSecret(value: undefined, context: string): undefined;
|
|
@@ -189,14 +224,17 @@ export function resolveCommandSecret(value: string | undefined, context: string)
|
|
|
189
224
|
if (value.startsWith("!!")) return interpolateEnvVars(value.slice(1));
|
|
190
225
|
if (!value.startsWith("!")) return interpolateEnvVars(value);
|
|
191
226
|
|
|
192
|
-
const
|
|
193
|
-
shell: true,
|
|
227
|
+
const options: SpawnSyncOptionsWithStringEncoding = {
|
|
194
228
|
encoding: "utf8",
|
|
195
229
|
timeout: COMMAND_SECRET_TIMEOUT_MS,
|
|
196
230
|
maxBuffer: COMMAND_SECRET_MAX_OUTPUT_BYTES,
|
|
197
231
|
stdio: ["ignore", "pipe", "ignore"],
|
|
198
232
|
windowsHide: true,
|
|
199
|
-
}
|
|
233
|
+
};
|
|
234
|
+
const bash = getWindowsCommandBash();
|
|
235
|
+
const result = bash === undefined
|
|
236
|
+
? spawnSync(value.slice(1), { ...options, shell: true })
|
|
237
|
+
: spawnSync(bash, ["-c", value.slice(1)], { ...options, shell: false });
|
|
200
238
|
if (result.error) {
|
|
201
239
|
const code = (result.error as NodeJS.ErrnoException).code;
|
|
202
240
|
const reason = code === "ETIMEDOUT"
|