@basein/runner 0.2.11 → 0.3.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/README.md +540 -350
- package/dist/auth/client.d.ts +65 -3
- package/dist/auth/client.js +305 -53
- package/dist/bin/bir-hooks.d.ts +72 -31
- package/dist/bin/bir-hooks.js +176 -284
- package/dist/bin/bir-proxy.d.ts +17 -2
- package/dist/bin/bir-proxy.js +222 -28
- package/dist/bin/bir-scenario.d.ts +61 -10
- package/dist/bin/bir-scenario.js +704 -135
- package/dist/bin/bir.d.ts +14 -8
- package/dist/bin/bir.js +2937 -488
- package/dist/bin/investigate.js +41 -16
- package/dist/bin/scenario-edit.d.ts +43 -10
- package/dist/bin/scenario-edit.js +281 -80
- package/dist/bin/scenario-liveness.d.ts +194 -0
- package/dist/bin/scenario-liveness.js +459 -0
- package/dist/bin/scenario-stderr.d.ts +43 -0
- package/dist/bin/scenario-stderr.js +76 -0
- package/dist/bin/service-read.d.ts +88 -0
- package/dist/bin/service-read.js +128 -0
- package/dist/bin/setup.d.ts +19 -0
- package/dist/bin/setup.js +146 -20
- package/dist/config/adapters/claude-code.d.ts +92 -14
- package/dist/config/adapters/claude-code.js +291 -101
- package/dist/config/adapters/generic.d.ts +4 -1
- package/dist/config/adapters/generic.js +13 -14
- package/dist/config/generate.d.ts +392 -14
- package/dist/config/generate.js +666 -19
- package/dist/config/mcp-approval.d.ts +76 -0
- package/dist/config/mcp-approval.js +210 -0
- package/dist/config/node-path.d.ts +42 -0
- package/dist/config/node-path.js +102 -0
- package/dist/config/overrides.d.ts +62 -0
- package/dist/config/overrides.js +136 -0
- package/dist/config/resolve.d.ts +70 -8
- package/dist/config/resolve.js +186 -15
- package/dist/control/client.d.ts +22 -0
- package/dist/control/client.js +26 -7
- package/dist/control/correlation.d.ts +23 -5
- package/dist/control/correlation.js +0 -0
- package/dist/control/daemon.d.ts +400 -18
- package/dist/control/daemon.js +1105 -102
- package/dist/control/discovery.d.ts +77 -5
- package/dist/control/discovery.js +186 -11
- package/dist/control/ensure-hook.d.ts +61 -0
- package/dist/control/ensure-hook.js +160 -8
- package/dist/control/paths.d.ts +26 -0
- package/dist/control/paths.js +34 -0
- package/dist/control/pin-guard.d.ts +83 -0
- package/dist/control/pin-guard.js +534 -0
- package/dist/control/recorder-keeper.d.ts +229 -0
- package/dist/control/recorder-keeper.js +721 -0
- package/dist/control/recorder-main.d.ts +73 -0
- package/dist/control/recorder-main.js +778 -0
- package/dist/control/server.d.ts +707 -31
- package/dist/control/server.js +2803 -306
- package/dist/control/transcript.d.ts +21 -1
- package/dist/control/transcript.js +378 -112
- package/dist/index.d.ts +5 -1
- package/dist/index.js +5 -1
- package/dist/jsonrpc/framing.d.ts +76 -12
- package/dist/jsonrpc/framing.js +216 -59
- package/dist/proxy/intercept.d.ts +49 -5
- package/dist/proxy/intercept.js +188 -59
- package/dist/proxy/relay.d.ts +16 -0
- package/dist/proxy/relay.js +77 -5
- package/dist/proxy/revive.d.ts +122 -0
- package/dist/proxy/revive.js +251 -0
- package/dist/proxy/session.d.ts +222 -14
- package/dist/proxy/session.js +708 -99
- package/dist/record/housekeeping.d.ts +71 -0
- package/dist/record/housekeeping.js +417 -0
- package/dist/record/recorder.d.ts +51 -3
- package/dist/record/recorder.js +3 -0
- package/dist/record/redact.d.ts +103 -9
- package/dist/record/redact.js +659 -30
- package/dist/record/remote-recorder.d.ts +327 -32
- package/dist/record/remote-recorder.js +1517 -187
- package/dist/record/switchable-recorder.d.ts +126 -0
- package/dist/record/switchable-recorder.js +351 -0
- package/dist/record/truncate.d.ts +40 -1
- package/dist/record/truncate.js +111 -4
- package/dist/replay/bash-channel.d.ts +35 -0
- package/dist/replay/bash-channel.js +133 -0
- package/dist/replay/bundle.d.ts +10 -0
- package/dist/replay/bundle.js +10 -0
- package/dist/replay/controller.d.ts +371 -20
- package/dist/replay/controller.js +1185 -111
- package/dist/replay/derive.d.ts +1 -0
- package/dist/replay/derive.js +81 -24
- package/dist/replay/executor.d.ts +51 -16
- package/dist/replay/executor.js +114 -41
- package/dist/replay/handover.d.ts +54 -1
- package/dist/replay/handover.js +55 -5
- package/dist/replay/logic.d.ts +62 -10
- package/dist/replay/logic.js +140 -22
- package/dist/replay/plan.d.ts +201 -27
- package/dist/replay/plan.js +581 -141
- package/dist/replay/safety.d.ts +72 -0
- package/dist/replay/safety.js +303 -0
- package/dist/replay/source-run.d.ts +10 -0
- package/dist/replay/source-run.js +80 -35
- package/dist/replay/targets.d.ts +15 -1
- package/dist/replay/targets.js +62 -4
- package/dist/replay/tool-error.d.ts +15 -1
- package/dist/replay/tool-error.js +33 -1
- package/dist/replay/types.d.ts +7 -4
- package/dist/upstream/client.d.ts +56 -3
- package/dist/upstream/client.js +61 -4
- package/dist/upstream/headers.d.ts +43 -0
- package/dist/upstream/headers.js +90 -0
- package/dist/upstream/http-client.d.ts +213 -22
- package/dist/upstream/http-client.js +1034 -168
- package/dist/upstream/http-transport.d.ts +143 -0
- package/dist/upstream/http-transport.js +391 -0
- package/dist/upstream/lazy-client.d.ts +8 -0
- package/dist/upstream/lazy-client.js +29 -1
- package/dist/upstream/sse.d.ts +78 -0
- package/dist/upstream/sse.js +189 -0
- package/dist/upstream/stdio-client.d.ts +61 -1
- package/dist/upstream/stdio-client.js +356 -33
- package/dist/util/atomic-write.d.ts +29 -0
- package/dist/util/atomic-write.js +96 -0
- package/dist/util/journal.d.ts +15 -0
- package/dist/util/journal.js +48 -3
- package/dist/util/log.d.ts +10 -0
- package/dist/util/log.js +28 -1
- package/dist/util/off-switch.d.ts +46 -0
- package/dist/util/off-switch.js +91 -0
- package/dist/util/service-health.d.ts +133 -0
- package/dist/util/service-health.js +217 -0
- package/docs/BaseInstRunner.md +694 -621
- package/docs/calculatedReplay.md +1625 -1244
- package/docs/calculatedReplayGuide.md +1212 -879
- package/docs/installRun.md +1013 -772
- package/docs/loginWeb.md +627 -607
- package/docs/mcpmark.md +49 -16
- package/docs/my-first-sample.md +569 -545
- package/docs/quickstart.md +384 -271
- package/docs/t-bench.md +406 -394
- package/package.json +58 -57
- package/production.md +1371 -0
- package/scripts/install.ps1 +97 -6
- package/scripts/install.sh +104 -7
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* config/mcp-approval — would Claude Code start a `.mcp.json` server without
|
|
3
|
+
* asking the person first?
|
|
4
|
+
*
|
|
5
|
+
* WHY THIS EXISTS. `bir install` routes a `.mcp.json` server through the proxy
|
|
6
|
+
* by writing a wrapped copy into this project's LOCAL scope (bir.ts,
|
|
7
|
+
* installClaudeCode), so the team's file never carries this machine's paths.
|
|
8
|
+
* But Claude Code treats the two scopes differently: it asks before it starts
|
|
9
|
+
* a server from `.mcp.json` ("New MCP server found in this project"), and it
|
|
10
|
+
* never asks about a local-scope one — that is the person's own config. A local
|
|
11
|
+
* copy of a server they rejected, or were never asked about (a freshly cloned
|
|
12
|
+
* repository), would start silently. The runner must never change what the
|
|
13
|
+
* person is asked to approve, so a `.mcp.json` server is overridden only when
|
|
14
|
+
* Claude Code itself would already start it.
|
|
15
|
+
*
|
|
16
|
+
* The rules are Claude Code's own (read out of 2.1.280, `getProjectMcpServerStatus`):
|
|
17
|
+
*
|
|
18
|
+
* - REJECTED when the name is in `disabledMcpjsonServers` of any settings
|
|
19
|
+
* source — what "Continue without using this MCP server" writes;
|
|
20
|
+
* - APPROVED when it is in `enabledMcpjsonServers`, or
|
|
21
|
+
* `enableAllProjectMcpServers` is true, in a source Claude Code honours for
|
|
22
|
+
* that decision: every source once the folder is trusted; before that only
|
|
23
|
+
* the person's own (user settings, the managed policy, and a
|
|
24
|
+
* `settings.local.json` git does not track — a committed one could approve
|
|
25
|
+
* anything for whoever clones the repository);
|
|
26
|
+
* - PENDING otherwise: Claude Code would ask.
|
|
27
|
+
*
|
|
28
|
+
* Names match the way Claude Code matches them (anything outside
|
|
29
|
+
* `[A-Za-z0-9_-]` compares as `_`). The legacy copies of those three fields
|
|
30
|
+
* under `projects[<key>]` in `~/.claude.json` count too: Claude Code migrates
|
|
31
|
+
* them into `settings.local.json` on its next start, so they are still the
|
|
32
|
+
* person's answer. A settings file that cannot be read makes the answer
|
|
33
|
+
* "pending" — when in doubt, the server is left exactly as it is.
|
|
34
|
+
*/
|
|
35
|
+
export type McpjsonApproval = "approved" | "rejected" | "pending";
|
|
36
|
+
export interface ApprovalVerdict {
|
|
37
|
+
state: McpjsonApproval;
|
|
38
|
+
/** Why, in words, for the install and doctor lines. */
|
|
39
|
+
why: string;
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* The person's own Claude Code settings (`~/.claude/settings.json`).
|
|
43
|
+
*
|
|
44
|
+
* `BIR_CLAUDE_SETTINGS` names it outright. A sandboxed `BIR_CLAUDE_JSON`
|
|
45
|
+
* (tests, a second profile) moves it beside that file, the way `~/.claude.json`
|
|
46
|
+
* and `~/.claude/` sit side by side in a home directory; `CLAUDE_CONFIG_DIR`
|
|
47
|
+
* moves it the way Claude Code itself does.
|
|
48
|
+
*/
|
|
49
|
+
export declare function claudeUserSettingsPath(env?: NodeJS.ProcessEnv): string;
|
|
50
|
+
/**
|
|
51
|
+
* Where an organisation's managed settings live (Claude Code's own paths).
|
|
52
|
+
* `BIR_CLAUDE_MANAGED_DIR` overrides it — tests point it at an empty sandbox.
|
|
53
|
+
*/
|
|
54
|
+
export declare function claudeManagedSettingsDir(env?: NodeJS.ProcessEnv, platform?: NodeJS.Platform): string;
|
|
55
|
+
/** A server name the way Claude Code compares them. */
|
|
56
|
+
export declare function mcpNameKey(name: string): string;
|
|
57
|
+
/**
|
|
58
|
+
* A reader for this directory's `.mcp.json` approvals: the files are read once,
|
|
59
|
+
* then each name is answered from them. `claude` is `~/.claude.json` already
|
|
60
|
+
* parsed, when the caller has it.
|
|
61
|
+
*/
|
|
62
|
+
export declare function mcpjsonApprovals(cwd: string, claude?: {
|
|
63
|
+
projects?: Record<string, unknown>;
|
|
64
|
+
}): (name: string) => ApprovalVerdict;
|
|
65
|
+
/**
|
|
66
|
+
* A reason the managed policy forbids wrapping ANY server, or undefined.
|
|
67
|
+
*
|
|
68
|
+
* An organisation's `allowedMcpServers` / `deniedMcpServers` can match a
|
|
69
|
+
* server by its command or URL. A wrapped entry's command is the runner's, so
|
|
70
|
+
* wrapping would get an allowed server blocked (broken) or a denied one past
|
|
71
|
+
* the denylist. `allowManagedMcpServersOnly` means only the organisation's own
|
|
72
|
+
* servers run at all. In any of those cases nothing is wrapped: the policy is
|
|
73
|
+
* not the runner's to reinterpret.
|
|
74
|
+
*/
|
|
75
|
+
export declare function managedMcpPolicyBlocker(): string | undefined;
|
|
76
|
+
//# sourceMappingURL=mcp-approval.d.ts.map
|
|
@@ -0,0 +1,210 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* config/mcp-approval — would Claude Code start a `.mcp.json` server without
|
|
3
|
+
* asking the person first?
|
|
4
|
+
*
|
|
5
|
+
* WHY THIS EXISTS. `bir install` routes a `.mcp.json` server through the proxy
|
|
6
|
+
* by writing a wrapped copy into this project's LOCAL scope (bir.ts,
|
|
7
|
+
* installClaudeCode), so the team's file never carries this machine's paths.
|
|
8
|
+
* But Claude Code treats the two scopes differently: it asks before it starts
|
|
9
|
+
* a server from `.mcp.json` ("New MCP server found in this project"), and it
|
|
10
|
+
* never asks about a local-scope one — that is the person's own config. A local
|
|
11
|
+
* copy of a server they rejected, or were never asked about (a freshly cloned
|
|
12
|
+
* repository), would start silently. The runner must never change what the
|
|
13
|
+
* person is asked to approve, so a `.mcp.json` server is overridden only when
|
|
14
|
+
* Claude Code itself would already start it.
|
|
15
|
+
*
|
|
16
|
+
* The rules are Claude Code's own (read out of 2.1.280, `getProjectMcpServerStatus`):
|
|
17
|
+
*
|
|
18
|
+
* - REJECTED when the name is in `disabledMcpjsonServers` of any settings
|
|
19
|
+
* source — what "Continue without using this MCP server" writes;
|
|
20
|
+
* - APPROVED when it is in `enabledMcpjsonServers`, or
|
|
21
|
+
* `enableAllProjectMcpServers` is true, in a source Claude Code honours for
|
|
22
|
+
* that decision: every source once the folder is trusted; before that only
|
|
23
|
+
* the person's own (user settings, the managed policy, and a
|
|
24
|
+
* `settings.local.json` git does not track — a committed one could approve
|
|
25
|
+
* anything for whoever clones the repository);
|
|
26
|
+
* - PENDING otherwise: Claude Code would ask.
|
|
27
|
+
*
|
|
28
|
+
* Names match the way Claude Code matches them (anything outside
|
|
29
|
+
* `[A-Za-z0-9_-]` compares as `_`). The legacy copies of those three fields
|
|
30
|
+
* under `projects[<key>]` in `~/.claude.json` count too: Claude Code migrates
|
|
31
|
+
* them into `settings.local.json` on its next start, so they are still the
|
|
32
|
+
* person's answer. A settings file that cannot be read makes the answer
|
|
33
|
+
* "pending" — when in doubt, the server is left exactly as it is.
|
|
34
|
+
*/
|
|
35
|
+
import { spawnSync } from "node:child_process";
|
|
36
|
+
import { readdirSync } from "node:fs";
|
|
37
|
+
import { homedir } from "node:os";
|
|
38
|
+
import { dirname, join } from "node:path";
|
|
39
|
+
import { claudeConfigHome, claudeJsonPath, parseJsonFile, projectKeysFor } from "./resolve.js";
|
|
40
|
+
/**
|
|
41
|
+
* The person's own Claude Code settings (`~/.claude/settings.json`).
|
|
42
|
+
*
|
|
43
|
+
* `BIR_CLAUDE_SETTINGS` names it outright. A sandboxed `BIR_CLAUDE_JSON`
|
|
44
|
+
* (tests, a second profile) moves it beside that file, the way `~/.claude.json`
|
|
45
|
+
* and `~/.claude/` sit side by side in a home directory; `CLAUDE_CONFIG_DIR`
|
|
46
|
+
* moves it the way Claude Code itself does.
|
|
47
|
+
*/
|
|
48
|
+
export function claudeUserSettingsPath(env = process.env) {
|
|
49
|
+
if (env.BIR_CLAUDE_SETTINGS)
|
|
50
|
+
return env.BIR_CLAUDE_SETTINGS;
|
|
51
|
+
if (env.BIR_CLAUDE_JSON)
|
|
52
|
+
return join(dirname(env.BIR_CLAUDE_JSON), ".claude", "settings.json");
|
|
53
|
+
return join(claudeConfigHome(env) ?? join(homedir(), ".claude"), "settings.json");
|
|
54
|
+
}
|
|
55
|
+
/**
|
|
56
|
+
* Where an organisation's managed settings live (Claude Code's own paths).
|
|
57
|
+
* `BIR_CLAUDE_MANAGED_DIR` overrides it — tests point it at an empty sandbox.
|
|
58
|
+
*/
|
|
59
|
+
export function claudeManagedSettingsDir(env = process.env, platform = process.platform) {
|
|
60
|
+
if (env.BIR_CLAUDE_MANAGED_DIR !== undefined)
|
|
61
|
+
return env.BIR_CLAUDE_MANAGED_DIR;
|
|
62
|
+
if (platform === "darwin")
|
|
63
|
+
return "/Library/Application Support/ClaudeCode";
|
|
64
|
+
if (platform === "win32")
|
|
65
|
+
return "C:\\Program Files\\ClaudeCode";
|
|
66
|
+
return "/etc/claude-code";
|
|
67
|
+
}
|
|
68
|
+
/** `managed-settings.json` and its drop-in directory, parsed; unreadable ones reported. */
|
|
69
|
+
function readManaged() {
|
|
70
|
+
const dir = claudeManagedSettingsDir();
|
|
71
|
+
const settings = [];
|
|
72
|
+
const unreadable = [];
|
|
73
|
+
if (!dir)
|
|
74
|
+
return { settings, unreadable };
|
|
75
|
+
const files = [join(dir, "managed-settings.json")];
|
|
76
|
+
try {
|
|
77
|
+
const dropIn = join(dir, "managed-settings.d");
|
|
78
|
+
for (const f of readdirSync(dropIn).filter((n) => n.endsWith(".json")).sort())
|
|
79
|
+
files.push(join(dropIn, f));
|
|
80
|
+
}
|
|
81
|
+
catch {
|
|
82
|
+
/* no drop-in directory */
|
|
83
|
+
}
|
|
84
|
+
for (const file of files) {
|
|
85
|
+
try {
|
|
86
|
+
const parsed = parseJsonFile(file);
|
|
87
|
+
if (parsed && typeof parsed === "object" && !Array.isArray(parsed))
|
|
88
|
+
settings.push(parsed);
|
|
89
|
+
}
|
|
90
|
+
catch {
|
|
91
|
+
unreadable.push(file);
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
return { settings, unreadable };
|
|
95
|
+
}
|
|
96
|
+
/** A server name the way Claude Code compares them. */
|
|
97
|
+
export function mcpNameKey(name) {
|
|
98
|
+
return name.startsWith("plugin:") ? name : name.replace(/[^a-zA-Z0-9_-]/g, "_");
|
|
99
|
+
}
|
|
100
|
+
function lists(fields, key, name) {
|
|
101
|
+
const value = fields?.[key];
|
|
102
|
+
if (!Array.isArray(value))
|
|
103
|
+
return false;
|
|
104
|
+
const want = mcpNameKey(name);
|
|
105
|
+
return value.some((n) => typeof n === "string" && mcpNameKey(n) === want);
|
|
106
|
+
}
|
|
107
|
+
/**
|
|
108
|
+
* True when git says `.claude/settings.local.json` is NOT tracked here. Claude
|
|
109
|
+
* Code ignores the approvals in a tracked one (and outside a repository, or
|
|
110
|
+
* when git cannot answer) until the folder is trusted; so does this. Asked
|
|
111
|
+
* at most once per reader, and only when that file is the one thing approving —
|
|
112
|
+
* never on a hook's path unless a caller puts it there.
|
|
113
|
+
*/
|
|
114
|
+
function localSettingsUntracked(cwd) {
|
|
115
|
+
try {
|
|
116
|
+
// Forward slashes: a pathspec git reads the same way on every platform.
|
|
117
|
+
const r = spawnSync("git", ["ls-files", "--error-unmatch", "--", ".claude/settings.local.json"], {
|
|
118
|
+
cwd,
|
|
119
|
+
timeout: 2_000,
|
|
120
|
+
windowsHide: true,
|
|
121
|
+
stdio: "ignore",
|
|
122
|
+
});
|
|
123
|
+
return r.status === 1;
|
|
124
|
+
}
|
|
125
|
+
catch {
|
|
126
|
+
return false;
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
/**
|
|
130
|
+
* A reader for this directory's `.mcp.json` approvals: the files are read once,
|
|
131
|
+
* then each name is answered from them. `claude` is `~/.claude.json` already
|
|
132
|
+
* parsed, when the caller has it.
|
|
133
|
+
*/
|
|
134
|
+
export function mcpjsonApprovals(cwd, claude) {
|
|
135
|
+
const unreadable = [];
|
|
136
|
+
const read = (file) => {
|
|
137
|
+
try {
|
|
138
|
+
return parseJsonFile(file);
|
|
139
|
+
}
|
|
140
|
+
catch {
|
|
141
|
+
unreadable.push(file);
|
|
142
|
+
return undefined;
|
|
143
|
+
}
|
|
144
|
+
};
|
|
145
|
+
let parsedClaude = claude;
|
|
146
|
+
if (!parsedClaude) {
|
|
147
|
+
try {
|
|
148
|
+
parsedClaude = parseJsonFile(claudeJsonPath()) ?? {};
|
|
149
|
+
}
|
|
150
|
+
catch {
|
|
151
|
+
unreadable.push(claudeJsonPath());
|
|
152
|
+
parsedClaude = {};
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
const user = read(claudeUserSettingsPath());
|
|
156
|
+
const project = read(join(cwd, ".claude", "settings.json"));
|
|
157
|
+
const local = read(join(cwd, ".claude", "settings.local.json"));
|
|
158
|
+
const managed = readManaged();
|
|
159
|
+
unreadable.push(...managed.unreadable);
|
|
160
|
+
const projects = (parsedClaude.projects ?? {});
|
|
161
|
+
const legacy = projectKeysFor(projects, cwd).map((key) => projects[key]).filter((p) => p !== undefined);
|
|
162
|
+
const trusted = legacy.some((p) => p?.hasTrustDialogAccepted === true);
|
|
163
|
+
let untracked;
|
|
164
|
+
const localHonoured = () => trusted || (untracked ??= localSettingsUntracked(cwd));
|
|
165
|
+
return (name) => {
|
|
166
|
+
if (unreadable.length > 0) {
|
|
167
|
+
return { state: "pending", why: `whether Claude Code approved it could not be read (${unreadable[0]} is not plain JSON)` };
|
|
168
|
+
}
|
|
169
|
+
const everySource = [user, project, local, ...managed.settings, ...legacy];
|
|
170
|
+
if (everySource.some((s) => lists(s, "disabledMcpjsonServers", name))) {
|
|
171
|
+
return { state: "rejected", why: "you told Claude Code not to use it (disabledMcpjsonServers)" };
|
|
172
|
+
}
|
|
173
|
+
const approves = (s) => s?.enableAllProjectMcpServers === true || lists(s, "enabledMcpjsonServers", name);
|
|
174
|
+
// git is asked only when settings.local.json is the one thing approving.
|
|
175
|
+
const approved = trusted
|
|
176
|
+
? everySource.some(approves)
|
|
177
|
+
: [user, ...managed.settings, ...legacy].some(approves) || (approves(local) && localHonoured());
|
|
178
|
+
if (approved)
|
|
179
|
+
return { state: "approved", why: "approved in Claude Code" };
|
|
180
|
+
return { state: "pending", why: "Claude Code has not been told to use it yet (it asks first)" };
|
|
181
|
+
};
|
|
182
|
+
}
|
|
183
|
+
/**
|
|
184
|
+
* A reason the managed policy forbids wrapping ANY server, or undefined.
|
|
185
|
+
*
|
|
186
|
+
* An organisation's `allowedMcpServers` / `deniedMcpServers` can match a
|
|
187
|
+
* server by its command or URL. A wrapped entry's command is the runner's, so
|
|
188
|
+
* wrapping would get an allowed server blocked (broken) or a denied one past
|
|
189
|
+
* the denylist. `allowManagedMcpServersOnly` means only the organisation's own
|
|
190
|
+
* servers run at all. In any of those cases nothing is wrapped: the policy is
|
|
191
|
+
* not the runner's to reinterpret.
|
|
192
|
+
*/
|
|
193
|
+
export function managedMcpPolicyBlocker() {
|
|
194
|
+
const { settings } = readManaged();
|
|
195
|
+
for (const s of settings) {
|
|
196
|
+
if (s.allowManagedMcpServersOnly === true) {
|
|
197
|
+
return "your organisation's managed settings allow only its own MCP servers";
|
|
198
|
+
}
|
|
199
|
+
for (const key of ["allowedMcpServers", "deniedMcpServers"]) {
|
|
200
|
+
const list = s[key];
|
|
201
|
+
if (!Array.isArray(list))
|
|
202
|
+
continue;
|
|
203
|
+
if (list.some((e) => e && typeof e === "object" && ("serverCommand" in e || "serverUrl" in e))) {
|
|
204
|
+
return `your organisation's managed settings match MCP servers by command or URL (${key}), which wrapping would change`;
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
return undefined;
|
|
209
|
+
}
|
|
210
|
+
//# sourceMappingURL=mcp-approval.js.map
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* node-path — which `node` the generated entries and the SessionStart hook name.
|
|
3
|
+
*
|
|
4
|
+
* Every wrapped MCP server, the scenario server and the self-starting hook
|
|
5
|
+
* spawn an interpreter by ABSOLUTE path. If that path disappears, every one of
|
|
6
|
+
* them fails to start — the user's own MCP servers included, because the proxy
|
|
7
|
+
* is now what the host spawns. So the path written must outlive the things
|
|
8
|
+
* that routinely move a Node:
|
|
9
|
+
*
|
|
10
|
+
* - Homebrew: `process.execPath` is already the versioned keg
|
|
11
|
+
* (`/opt/homebrew/Cellar/node/24.1.0/bin/node`), and the next
|
|
12
|
+
* `brew upgrade` deletes it. Homebrew keeps a stable symlink to the current
|
|
13
|
+
* keg at `<prefix>/opt/<formula>/bin/node`; that is the one to name.
|
|
14
|
+
* - Windows: `process.execPath` is the path Node was launched through, which
|
|
15
|
+
* for nvm-windows (`C:\nvm4w\nodejs`) and Scoop (`…\apps\nodejs\current`)
|
|
16
|
+
* is the stable link that follows the active version, while its realpath is
|
|
17
|
+
* the versioned directory the next switch abandons. Keep it — except for
|
|
18
|
+
* fnm's per-shell `fnm_multishells` directory, which is gone with the shell.
|
|
19
|
+
* - Version managers with no stable link (nvm, fnm, Volta, asdf, mise,
|
|
20
|
+
* nodenv, n): nothing better exists, so the path is kept and the install
|
|
21
|
+
* says, once, what will break it. The install scripts avoid all of this by
|
|
22
|
+
* installing a private Node under ~/.baseinstrunner/node.
|
|
23
|
+
*
|
|
24
|
+
* Proxies, the scenario server and the hook all use the SAME choice, so a
|
|
25
|
+
* re-run compares like with like and does not re-write entries for nothing.
|
|
26
|
+
*/
|
|
27
|
+
export interface NodeChoice {
|
|
28
|
+
/** The interpreter path to write. */
|
|
29
|
+
path: string;
|
|
30
|
+
/** Set when that path belongs to something that moves it; printed once. */
|
|
31
|
+
warning?: string;
|
|
32
|
+
}
|
|
33
|
+
export interface NodeChoiceDeps {
|
|
34
|
+
execPath?: string;
|
|
35
|
+
platform?: NodeJS.Platform;
|
|
36
|
+
realpath?: (p: string) => string;
|
|
37
|
+
exists?: (p: string) => boolean;
|
|
38
|
+
}
|
|
39
|
+
/** A version manager (or per-version package directory) that owns this path, if any. */
|
|
40
|
+
export declare function versionManagerOf(path: string): string | undefined;
|
|
41
|
+
export declare function chooseNodePath(input?: NodeChoiceDeps): NodeChoice;
|
|
42
|
+
//# sourceMappingURL=node-path.d.ts.map
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* node-path — which `node` the generated entries and the SessionStart hook name.
|
|
3
|
+
*
|
|
4
|
+
* Every wrapped MCP server, the scenario server and the self-starting hook
|
|
5
|
+
* spawn an interpreter by ABSOLUTE path. If that path disappears, every one of
|
|
6
|
+
* them fails to start — the user's own MCP servers included, because the proxy
|
|
7
|
+
* is now what the host spawns. So the path written must outlive the things
|
|
8
|
+
* that routinely move a Node:
|
|
9
|
+
*
|
|
10
|
+
* - Homebrew: `process.execPath` is already the versioned keg
|
|
11
|
+
* (`/opt/homebrew/Cellar/node/24.1.0/bin/node`), and the next
|
|
12
|
+
* `brew upgrade` deletes it. Homebrew keeps a stable symlink to the current
|
|
13
|
+
* keg at `<prefix>/opt/<formula>/bin/node`; that is the one to name.
|
|
14
|
+
* - Windows: `process.execPath` is the path Node was launched through, which
|
|
15
|
+
* for nvm-windows (`C:\nvm4w\nodejs`) and Scoop (`…\apps\nodejs\current`)
|
|
16
|
+
* is the stable link that follows the active version, while its realpath is
|
|
17
|
+
* the versioned directory the next switch abandons. Keep it — except for
|
|
18
|
+
* fnm's per-shell `fnm_multishells` directory, which is gone with the shell.
|
|
19
|
+
* - Version managers with no stable link (nvm, fnm, Volta, asdf, mise,
|
|
20
|
+
* nodenv, n): nothing better exists, so the path is kept and the install
|
|
21
|
+
* says, once, what will break it. The install scripts avoid all of this by
|
|
22
|
+
* installing a private Node under ~/.baseinstrunner/node.
|
|
23
|
+
*
|
|
24
|
+
* Proxies, the scenario server and the hook all use the SAME choice, so a
|
|
25
|
+
* re-run compares like with like and does not re-write entries for nothing.
|
|
26
|
+
*/
|
|
27
|
+
import { existsSync, realpathSync } from "node:fs";
|
|
28
|
+
/** A version manager (or per-version package directory) that owns this path, if any. */
|
|
29
|
+
export function versionManagerOf(path) {
|
|
30
|
+
const p = path.replace(/\\/g, "/");
|
|
31
|
+
const rules = [
|
|
32
|
+
[/fnm_multishells/i, "fnm"],
|
|
33
|
+
[/\/(\.?fnm)\//i, "fnm"],
|
|
34
|
+
[/\/\.nvm\/|\/nvm\/versions\//i, "nvm"],
|
|
35
|
+
[/\/(AppData\/Roaming\/)?nvm\/v\d/i, "nvm-windows"],
|
|
36
|
+
[/\/\.?volta\//i, "Volta"],
|
|
37
|
+
[/\/\.?nodenv\//i, "nodenv"],
|
|
38
|
+
[/\/\.?asdf\//i, "asdf"],
|
|
39
|
+
[/\/\.?mise\//i, "mise"],
|
|
40
|
+
[/\/n\/versions\//i, "n"],
|
|
41
|
+
[/\/scoop\/apps\/nodejs[^/]*\/(?!current\/)[^/]+\//i, "Scoop"],
|
|
42
|
+
[/\/Cellar\//, "Homebrew"],
|
|
43
|
+
[/^\/snap\/node\//, "snap"],
|
|
44
|
+
];
|
|
45
|
+
for (const [re, name] of rules)
|
|
46
|
+
if (re.test(p))
|
|
47
|
+
return name;
|
|
48
|
+
return undefined;
|
|
49
|
+
}
|
|
50
|
+
/**
|
|
51
|
+
* Homebrew's stable `opt` link for a Cellar path, when there is one and it
|
|
52
|
+
* points at this very binary (not at a newer keg the running process is not).
|
|
53
|
+
*/
|
|
54
|
+
function homebrewOptPath(real, deps) {
|
|
55
|
+
const m = /^(.*)\/Cellar\/(node(?:@\d+)?)\/[^/]+\/bin\/node$/.exec(real);
|
|
56
|
+
if (!m)
|
|
57
|
+
return undefined;
|
|
58
|
+
const opt = `${m[1]}/opt/${m[2]}/bin/node`;
|
|
59
|
+
try {
|
|
60
|
+
if (deps.exists(opt) && deps.realpath(opt) === real)
|
|
61
|
+
return opt;
|
|
62
|
+
}
|
|
63
|
+
catch {
|
|
64
|
+
/* no link: fall through */
|
|
65
|
+
}
|
|
66
|
+
return undefined;
|
|
67
|
+
}
|
|
68
|
+
export function chooseNodePath(input = {}) {
|
|
69
|
+
const deps = {
|
|
70
|
+
execPath: input.execPath ?? process.execPath,
|
|
71
|
+
platform: input.platform ?? process.platform,
|
|
72
|
+
realpath: input.realpath ?? ((p) => realpathSync(p)),
|
|
73
|
+
exists: input.exists ?? existsSync,
|
|
74
|
+
};
|
|
75
|
+
const real = (() => {
|
|
76
|
+
try {
|
|
77
|
+
return deps.realpath(deps.execPath);
|
|
78
|
+
}
|
|
79
|
+
catch {
|
|
80
|
+
return deps.execPath;
|
|
81
|
+
}
|
|
82
|
+
})();
|
|
83
|
+
let path;
|
|
84
|
+
if (deps.platform === "win32") {
|
|
85
|
+
// The launch path, unless it is a per-shell directory that dies with the shell.
|
|
86
|
+
path = /fnm_multishells/i.test(deps.execPath) ? real : deps.execPath;
|
|
87
|
+
}
|
|
88
|
+
else {
|
|
89
|
+
path = homebrewOptPath(real, deps) ?? real;
|
|
90
|
+
}
|
|
91
|
+
const manager = versionManagerOf(path);
|
|
92
|
+
if (!manager)
|
|
93
|
+
return { path };
|
|
94
|
+
return {
|
|
95
|
+
path,
|
|
96
|
+
warning: `node is ${path}, a ${manager}-managed version: when ${manager} removes or moves it, ` +
|
|
97
|
+
"the wrapped MCP servers and the SessionStart hook stop starting. Run `bir setup` here " +
|
|
98
|
+
"again after switching Node, or install with the install line, which keeps a private Node " +
|
|
99
|
+
"under ~/.baseinstrunner/node",
|
|
100
|
+
};
|
|
101
|
+
}
|
|
102
|
+
//# sourceMappingURL=node-path.js.map
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* config/overrides — is each local-scope copy `bir install` wrote still a copy
|
|
3
|
+
* of the entry it overrides?
|
|
4
|
+
*
|
|
5
|
+
* An override (bir.ts, installClaudeCode) is a FROZEN copy of a `.mcp.json` or
|
|
6
|
+
* user-scope server, wrapped, in this project's local scope — which outranks
|
|
7
|
+
* both. When the team changes `.mcp.json` (a new version, a new host) or the
|
|
8
|
+
* person rotates a token in the user scope, Claude Code keeps starting the old
|
|
9
|
+
* copy here, and the server is broken because of the runner. Nothing about the
|
|
10
|
+
* copy itself says so; the record's {@link InstalledRecord.sourceSha} does.
|
|
11
|
+
*
|
|
12
|
+
* {@link checkOverrides} is read-only (doctor, status). {@link resyncOverrides}
|
|
13
|
+
* repairs: a changed source is copied again with the same runner, a removed or
|
|
14
|
+
* no-longer-approved one loses its copy. `bir setup` does the same through a
|
|
15
|
+
* full install; this is the cheap path for a process that runs every session.
|
|
16
|
+
*/
|
|
17
|
+
import { type InstalledRecord, type InstalledSidecar } from "./generate.js";
|
|
18
|
+
import { type McpServerConfig } from "./resolve.js";
|
|
19
|
+
/** The server name a sidecar key names, given the record's own cwd (either key shape, generate.ts `recordKey`). */
|
|
20
|
+
export declare function recordServerName(key: string, record: InstalledRecord): string;
|
|
21
|
+
export type OverrideState = "current" | "changed" | "removed" | "unapproved";
|
|
22
|
+
export interface OverrideCheck {
|
|
23
|
+
name: string;
|
|
24
|
+
/** Its record's key in installed.json. */
|
|
25
|
+
key: string;
|
|
26
|
+
record: InstalledRecord;
|
|
27
|
+
state: OverrideState;
|
|
28
|
+
/** Where the entry it should copy comes from now. */
|
|
29
|
+
from: "project" | "user";
|
|
30
|
+
/** That entry, unwrapped — absent when it was removed. */
|
|
31
|
+
source?: McpServerConfig;
|
|
32
|
+
/** The local copy as it is now. */
|
|
33
|
+
local: McpServerConfig;
|
|
34
|
+
/** The `~/.claude.json` project keys holding the copy. */
|
|
35
|
+
keys: string[];
|
|
36
|
+
/** What is wrong, in words (for everything but "current"). */
|
|
37
|
+
why?: string;
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* Every override this directory's install wrote, and whether it still copies
|
|
41
|
+
* what it overrides. A copy the person has since removed or replaced is not
|
|
42
|
+
* listed: it is theirs now. Files that cannot be read yield no verdicts —
|
|
43
|
+
* Claude Code cannot read them either, and that is reported elsewhere.
|
|
44
|
+
*/
|
|
45
|
+
export declare function checkOverrides(cwd: string, sidecar?: InstalledSidecar): OverrideCheck[];
|
|
46
|
+
/**
|
|
47
|
+
* Bring every stale override of `cwd` back in line with what it copies, in
|
|
48
|
+
* `~/.claude.json` and installed.json. A changed source is wrapped again with
|
|
49
|
+
* the runner the old copy named (same node, flags, script, control URL); a
|
|
50
|
+
* removed, rejected or never-approved one, a remote Claude Code signs in
|
|
51
|
+
* itself, or anything under a managed policy that matches by command, loses
|
|
52
|
+
* its copy, which lets the entry under it run as Claude Code would run it.
|
|
53
|
+
*
|
|
54
|
+
* Touches only wrapped local entries this directory's records name, and never
|
|
55
|
+
* throws: a failure leaves things as they were, for doctor to report.
|
|
56
|
+
*/
|
|
57
|
+
export declare function resyncOverrides(cwd: string): Array<{
|
|
58
|
+
name: string;
|
|
59
|
+
action: "rewrapped" | "removed";
|
|
60
|
+
why: string;
|
|
61
|
+
}>;
|
|
62
|
+
//# sourceMappingURL=overrides.d.ts.map
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* config/overrides — is each local-scope copy `bir install` wrote still a copy
|
|
3
|
+
* of the entry it overrides?
|
|
4
|
+
*
|
|
5
|
+
* An override (bir.ts, installClaudeCode) is a FROZEN copy of a `.mcp.json` or
|
|
6
|
+
* user-scope server, wrapped, in this project's local scope — which outranks
|
|
7
|
+
* both. When the team changes `.mcp.json` (a new version, a new host) or the
|
|
8
|
+
* person rotates a token in the user scope, Claude Code keeps starting the old
|
|
9
|
+
* copy here, and the server is broken because of the runner. Nothing about the
|
|
10
|
+
* copy itself says so; the record's {@link InstalledRecord.sourceSha} does.
|
|
11
|
+
*
|
|
12
|
+
* {@link checkOverrides} is read-only (doctor, status). {@link resyncOverrides}
|
|
13
|
+
* repairs: a changed source is copied again with the same runner, a removed or
|
|
14
|
+
* no-longer-approved one loses its copy. `bir setup` does the same through a
|
|
15
|
+
* full install; this is the cheap path for a process that runs every session.
|
|
16
|
+
*/
|
|
17
|
+
import { join } from "node:path";
|
|
18
|
+
import { normalizePath } from "../control/paths.js";
|
|
19
|
+
import { setLocalEntries } from "./adapters/claude-code.js";
|
|
20
|
+
import { configSha, isWrapped, readSidecar, REDACTED_KEY_PREFIX, remoteWrapBlocker, sameServerConfig, unwrapEntry, wrapEntry, wrapOptionsOf, writeSidecar, } from "./generate.js";
|
|
21
|
+
import { managedMcpPolicyBlocker, mcpjsonApprovals } from "./mcp-approval.js";
|
|
22
|
+
import { claudeJsonPath, isRemote, parseJsonFile, projectKeysFor } from "./resolve.js";
|
|
23
|
+
/** The server name a sidecar key names, given the record's own cwd (either key shape, generate.ts `recordKey`). */
|
|
24
|
+
export function recordServerName(key, record) {
|
|
25
|
+
const bare = key.startsWith(REDACTED_KEY_PREFIX) ? key.slice(REDACTED_KEY_PREFIX.length) : key;
|
|
26
|
+
const prefix = `${record.cwd}::`;
|
|
27
|
+
return bare.startsWith(prefix) ? bare.slice(prefix.length) : bare.slice(bare.lastIndexOf("::") + 2);
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* Every override this directory's install wrote, and whether it still copies
|
|
31
|
+
* what it overrides. A copy the person has since removed or replaced is not
|
|
32
|
+
* listed: it is theirs now. Files that cannot be read yield no verdicts —
|
|
33
|
+
* Claude Code cannot read them either, and that is reported elsewhere.
|
|
34
|
+
*/
|
|
35
|
+
export function checkOverrides(cwd, sidecar = readSidecar()) {
|
|
36
|
+
const here = normalizePath(cwd);
|
|
37
|
+
const mine = Object.entries(sidecar.servers).filter(([, r]) => r.override && normalizePath(r.cwd) === here);
|
|
38
|
+
if (mine.length === 0)
|
|
39
|
+
return [];
|
|
40
|
+
let claude;
|
|
41
|
+
let mcp;
|
|
42
|
+
try {
|
|
43
|
+
claude = parseJsonFile(claudeJsonPath()) ?? {};
|
|
44
|
+
mcp = parseJsonFile(join(cwd, ".mcp.json"))?.mcpServers ?? {};
|
|
45
|
+
}
|
|
46
|
+
catch {
|
|
47
|
+
return [];
|
|
48
|
+
}
|
|
49
|
+
let approval;
|
|
50
|
+
const unwrapped = (c) => c !== undefined && isWrapped(c) ? unwrapEntry(c) : c;
|
|
51
|
+
const checks = [];
|
|
52
|
+
for (const [key, record] of mine) {
|
|
53
|
+
const name = recordServerName(key, record);
|
|
54
|
+
const keys = (record.keys ?? projectKeysFor(claude.projects, cwd)).filter((k) => claude.projects?.[k]?.mcpServers?.[name] !== undefined);
|
|
55
|
+
const local = keys.map((k) => claude.projects?.[k]?.mcpServers?.[name]).find((e) => e !== undefined);
|
|
56
|
+
if (local === undefined || !isWrapped(local))
|
|
57
|
+
continue;
|
|
58
|
+
const P = mcp[name];
|
|
59
|
+
const from = P !== undefined ? "project" : "user";
|
|
60
|
+
const source = unwrapped(P ?? claude.mcpServers?.[name]);
|
|
61
|
+
const was = record.override?.source === "project" ? ".mcp.json" : "your user scope";
|
|
62
|
+
const now = from === "project" ? ".mcp.json" : "your user scope";
|
|
63
|
+
const check = { name, key, record, state: "current", from, source, local, keys };
|
|
64
|
+
if (source === undefined) {
|
|
65
|
+
check.state = "removed";
|
|
66
|
+
check.why = `it was removed from ${was}, but this project still runs the copy`;
|
|
67
|
+
}
|
|
68
|
+
else if (from === "project") {
|
|
69
|
+
// Claude Code asks before it starts a `.mcp.json` server and never about
|
|
70
|
+
// the copy: one it would not start unasked must not have one. `--server`
|
|
71
|
+
// (optedIn) answers "pending" for the person; nothing answers "rejected".
|
|
72
|
+
approval ??= mcpjsonApprovals(cwd, claude);
|
|
73
|
+
const verdict = approval(name);
|
|
74
|
+
if (verdict.state === "rejected" || (verdict.state === "pending" && !record.optedIn)) {
|
|
75
|
+
check.state = "unapproved";
|
|
76
|
+
check.why = verdict.why;
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
if (check.state === "current" && source !== undefined) {
|
|
80
|
+
const same = record.sourceSha ? configSha(source) === record.sourceSha : sameServerConfig(unwrapEntry(local), source);
|
|
81
|
+
if (!same) {
|
|
82
|
+
check.state = "changed";
|
|
83
|
+
check.why =
|
|
84
|
+
record.override?.source === "project" && from === "user"
|
|
85
|
+
? "it is gone from .mcp.json, so your user-scope entry of that name would run instead"
|
|
86
|
+
: `${now} changed since the copy was made, and this project still runs the old one`;
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
checks.push(check);
|
|
90
|
+
}
|
|
91
|
+
return checks;
|
|
92
|
+
}
|
|
93
|
+
/**
|
|
94
|
+
* Bring every stale override of `cwd` back in line with what it copies, in
|
|
95
|
+
* `~/.claude.json` and installed.json. A changed source is wrapped again with
|
|
96
|
+
* the runner the old copy named (same node, flags, script, control URL); a
|
|
97
|
+
* removed, rejected or never-approved one, a remote Claude Code signs in
|
|
98
|
+
* itself, or anything under a managed policy that matches by command, loses
|
|
99
|
+
* its copy, which lets the entry under it run as Claude Code would run it.
|
|
100
|
+
*
|
|
101
|
+
* Touches only wrapped local entries this directory's records name, and never
|
|
102
|
+
* throws: a failure leaves things as they were, for doctor to report.
|
|
103
|
+
*/
|
|
104
|
+
export function resyncOverrides(cwd) {
|
|
105
|
+
const done = [];
|
|
106
|
+
try {
|
|
107
|
+
const sidecar = readSidecar();
|
|
108
|
+
const stale = checkOverrides(cwd, sidecar).filter((c) => c.state !== "current");
|
|
109
|
+
if (stale.length === 0)
|
|
110
|
+
return done;
|
|
111
|
+
const claudeJson = claudeJsonPath();
|
|
112
|
+
const policy = managedMcpPolicyBlocker();
|
|
113
|
+
const ours = (cur) => cur !== undefined && isWrapped(cur);
|
|
114
|
+
for (const c of stale) {
|
|
115
|
+
const opts = wrapOptionsOf(c.local);
|
|
116
|
+
const blocked = policy ?? (c.source && isRemote(c.source) && !c.record.optedIn ? remoteWrapBlocker(c.source) : undefined);
|
|
117
|
+
if (c.state === "changed" && c.source && opts && !blocked) {
|
|
118
|
+
setLocalEntries(claudeJson, c.keys, c.name, wrapEntry(c.source, { ...opts, serverName: c.name }), ours);
|
|
119
|
+
c.record.sourceSha = configSha(c.source);
|
|
120
|
+
c.record.override = { source: c.from };
|
|
121
|
+
done.push({ name: c.name, action: "rewrapped", why: c.why ?? "" });
|
|
122
|
+
}
|
|
123
|
+
else {
|
|
124
|
+
setLocalEntries(claudeJson, c.keys, c.name, null, ours);
|
|
125
|
+
delete sidecar.servers[c.key];
|
|
126
|
+
done.push({ name: c.name, action: "removed", why: blocked ?? c.why ?? "" });
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
writeSidecar(sidecar);
|
|
130
|
+
}
|
|
131
|
+
catch {
|
|
132
|
+
/* left as it was; `bir doctor` names what is stale */
|
|
133
|
+
}
|
|
134
|
+
return done;
|
|
135
|
+
}
|
|
136
|
+
//# sourceMappingURL=overrides.js.map
|