@mindbridgeio/muse 0.1.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 ADDED
@@ -0,0 +1,35 @@
1
+ # @mindbridgeio/muse
2
+
3
+ MindBridge MCP tools and lifecycle hooks for Muse, using durable OAuth.
4
+
5
+ ```sh
6
+ npx -y @mindbridgeio/muse@latest setup
7
+ ```
8
+
9
+ The command writes the `streamable_http` MCP server and native lifecycle hooks
10
+ into `~/.config/muse/settings.json` (or `$XDG_CONFIG_HOME/muse/settings.json`),
11
+ starts Muse's MCP login, then signs lifecycle hooks in through the system
12
+ keychain. It uses `https://memory.ifelse.io` by default; pass
13
+ `--server-url https://…` or set `MINDBRIDGE_MCP_SERVER_URL` for a nonsecret
14
+ server override.
15
+
16
+ ```sh
17
+ npx -y @mindbridgeio/muse@latest auth status
18
+ npx -y @mindbridgeio/muse@latest auth logout
19
+ npx -y @mindbridgeio/muse@latest doctor
20
+ ```
21
+
22
+ No Bearer [REDACTED] is placed in Muse configuration, command arguments, or an
23
+ environment variable. A keychain is required; setup fails safely when one is
24
+ unavailable.
25
+
26
+ Setup validates the server, Muse CLI, and keychain before changing config. If
27
+ MindBridge is temporarily unavailable, hooks remain non-blocking and replay
28
+ redacted pending events from a bounded local outbox. `doctor --json` reports
29
+ MCP configuration, lifecycle OAuth, and outbox health. The MCP server is
30
+ registered as `optional` so a MindBridge outage can never abort a session.
31
+
32
+ New sessions are tagged automatically from Muse's working directory. Git
33
+ repositories use a credential-free remote identity; non-Git directories use a
34
+ private install-scoped local identity. Recall prefers the current project and
35
+ then fills from the wider MindBridge workspace.
@@ -0,0 +1,212 @@
1
+ #!/usr/bin/env node
2
+
3
+ import { realpathSync } from "node:fs";
4
+ import { cp, mkdir, readFile } from "node:fs/promises";
5
+ import { homedir } from "node:os";
6
+ import { dirname, join } from "node:path";
7
+ import { fileURLToPath } from "node:url";
8
+
9
+ import {
10
+ agentHookStateRoot,
11
+ createOAuthClient,
12
+ copyKeyringRuntime,
13
+ oauthReason,
14
+ outboxStatus,
15
+ preflightServer,
16
+ readServerConfig,
17
+ resolveServerURL,
18
+ saveServerConfig,
19
+ } from "../dist/agent-oauth.mjs";
20
+ import { atomicWrite, phase, readJSON, runCommand, withoutManagedHooks } from "../dist/agent-install.mjs";
21
+
22
+ const PACKAGE_ROOT = dirname(dirname(fileURLToPath(import.meta.url)));
23
+ const MANAGED_HOOK_STATUSES = ["loading mindbridge memory", "recording mindbridge context", "recording mindbridge response", "finalizing mindbridge context"];
24
+
25
+ export function defaultMuseHome(env = process.env) {
26
+ if (env.MUSE_CONFIG_DIR) return env.MUSE_CONFIG_DIR;
27
+ if (env.XDG_CONFIG_HOME) return join(env.XDG_CONFIG_HOME, "muse");
28
+ return join(homedir(), ".config", "muse");
29
+ }
30
+
31
+ export function installSettings(existing, hookPath, serverURL) {
32
+ const hooks = { ...(existing?.hooks && typeof existing.hooks === "object" ? existing.hooks : {}) };
33
+ const specs = {
34
+ SessionStart: ["Loading MindBridge memory", 30],
35
+ UserPromptSubmit: ["Recording MindBridge context", 10],
36
+ Stop: ["Recording MindBridge response", 10],
37
+ SessionEnd: ["Finalizing MindBridge context", 30],
38
+ };
39
+ for (const [event, [statusMessage, timeout]] of Object.entries(specs)) {
40
+ hooks[event] = [
41
+ ...withoutManagedHooks(hooks[event], MANAGED_HOOK_STATUSES),
42
+ { hooks: [{ type: "command", command: `node ${JSON.stringify(hookPath)} muse`, timeout, statusMessage }] },
43
+ ];
44
+ }
45
+ // Muse has no `mcp add` command; the server entry is written directly. It
46
+ // stays optional so a memory-server outage can never abort a session.
47
+ const mcpServers = { ...(existing?.mcpServers && typeof existing.mcpServers === "object" ? existing.mcpServers : {}) };
48
+ mcpServers.mindbridge = { transport: "streamable_http", url: `${serverURL}/mcp`, mode: "optional" };
49
+ return { ...existing, schema_version: 1, hooks, mcpServers };
50
+ }
51
+
52
+ function unavailable(error) {
53
+ return error?.code === "ENOENT";
54
+ }
55
+
56
+ async function hostLogin(commandRunner) {
57
+ try {
58
+ await commandRunner("muse", ["mcp", "login", "mindbridge"]);
59
+ return "started";
60
+ } catch (error) {
61
+ if (unavailable(error)) return "unavailable";
62
+ throw error;
63
+ }
64
+ }
65
+
66
+ export async function setup({
67
+ museHome = defaultMuseHome(),
68
+ packageRoot = PACKAGE_ROOT,
69
+ env = process.env,
70
+ serverURL,
71
+ configRoot,
72
+ commandRunner = runCommand,
73
+ copyRuntime = copyKeyringRuntime,
74
+ preflight = preflightServer,
75
+ oauthClientFactory = (url) => createOAuthClient({ host: "muse", serverURL: url, env, configRoot }),
76
+ } = {}) {
77
+ const resolvedServerURL = await resolveServerURL({ host: "muse", serverURL, env, configRoot });
78
+ const oauth = oauthClientFactory(resolvedServerURL);
79
+ await phase("server OAuth discovery", () => preflight(resolvedServerURL));
80
+ await phase("Muse CLI preflight", () => commandRunner("muse", ["mcp", "--help"]));
81
+ await phase("lifecycle keychain preflight", () => oauth.status());
82
+ const installRoot = join(museHome, "mindbridge");
83
+ const distSource = join(packageRoot, "dist");
84
+ const distTarget = join(installRoot, "dist");
85
+ const hookPath = join(distTarget, "oauth-hook.mjs");
86
+ await mkdir(installRoot, { recursive: true, mode: 0o700 });
87
+ await cp(distSource, distTarget, { recursive: true, force: true });
88
+ await copyRuntime({ targetRoot: installRoot });
89
+
90
+ const settingsPath = join(museHome, "settings.json");
91
+ await atomicWrite(settingsPath, `${JSON.stringify(installSettings(await readJSON(settingsPath, {}), hookPath, resolvedServerURL), null, 2)}\n`);
92
+ await saveServerConfig("muse", resolvedServerURL, { configRoot, env });
93
+ const mcpLogin = await phase("Muse MCP authorization", () => hostLogin(commandRunner));
94
+ await phase("lifecycle-hook authorization", () => oauth.authenticate());
95
+ return { museHome, settingsPath, hookPath, serverURL: resolvedServerURL, mcpLogin };
96
+ }
97
+
98
+ export const install = setup;
99
+
100
+ export async function authStatus({ env = process.env, serverURL, configRoot, oauthClientFactory } = {}) {
101
+ const resolvedServerURL = await resolveServerURL({ host: "muse", serverURL, env, configRoot });
102
+ const client = (oauthClientFactory || ((url) => createOAuthClient({ host: "muse", serverURL: url, env, configRoot })))(resolvedServerURL);
103
+ const stored = await client.status();
104
+ if (!stored.authenticated || !client.check) return { serverURL: resolvedServerURL, ...stored };
105
+ try {
106
+ await client.check();
107
+ return { serverURL: resolvedServerURL, ...stored, healthy: true };
108
+ } catch (error) {
109
+ return { serverURL: resolvedServerURL, ...stored, healthy: false, reason: oauthReason(error) };
110
+ }
111
+ }
112
+
113
+ export async function doctor({
114
+ museHome = defaultMuseHome(),
115
+ env = process.env,
116
+ serverURL,
117
+ configRoot,
118
+ commandRunner = runCommand,
119
+ preflight = preflightServer,
120
+ oauthClientFactory = (url) => createOAuthClient({ host: "muse", serverURL: url, env, configRoot }),
121
+ } = {}) {
122
+ const resolvedServerURL = await resolveServerURL({ host: "muse", serverURL, env, configRoot });
123
+ const checks = {};
124
+ try { await preflight(resolvedServerURL); checks.server = "pass"; } catch (error) { checks.server = oauthReason(error); }
125
+ try { await commandRunner("muse", ["--version"]); checks.host = "pass"; } catch { checks.host = "muse_unavailable"; }
126
+ try {
127
+ const settings = JSON.parse(await readFile(join(museHome, "settings.json"), "utf8"));
128
+ const serialized = JSON.stringify(settings.hooks || {});
129
+ checks.installation = serialized.includes("mindbridge") && settings.mcpServers?.mindbridge?.url === `${resolvedServerURL}/mcp` ? "pass" : "stale";
130
+ } catch { checks.installation = "missing"; }
131
+ try { await oauthClientFactory(resolvedServerURL).check(); checks.lifecycleAuth = "pass"; } catch (error) { checks.lifecycleAuth = oauthReason(error); }
132
+ const outbox = await outboxStatus(agentHookStateRoot({ env }));
133
+ return {
134
+ ok: Object.values(checks).every((value) => value === "pass") && outbox.losses === 0,
135
+ serverURL: resolvedServerURL,
136
+ checks,
137
+ outbox,
138
+ };
139
+ }
140
+
141
+ export async function authLogout({ env = process.env, serverURL, configRoot, oauthClientFactory } = {}) {
142
+ const resolvedServerURL = await resolveServerURL({ host: "muse", serverURL, env, configRoot });
143
+ const client = (oauthClientFactory || ((url) => createOAuthClient({ host: "muse", serverURL: url, env, configRoot })))(resolvedServerURL);
144
+ return { serverURL: resolvedServerURL, loggedOut: await client.logout() };
145
+ }
146
+
147
+ function parseArgs(args) {
148
+ let serverURL;
149
+ const rest = [];
150
+ for (let index = 0; index < args.length; index += 1) {
151
+ if (args[index] === "--server-url" && args[index + 1]) {
152
+ serverURL = args[index + 1];
153
+ index += 1;
154
+ } else rest.push(args[index]);
155
+ }
156
+ return { serverURL, rest };
157
+ }
158
+
159
+ async function main() {
160
+ const { serverURL, rest } = parseArgs(process.argv.slice(2));
161
+ if (rest[0] === "setup" || rest[0] === "install") {
162
+ const result = await setup({ serverURL });
163
+ console.log(`MindBridge setup complete for Muse (${result.serverURL}).`);
164
+ return;
165
+ }
166
+ if (rest[0] === "auth" && rest[1] === "status") {
167
+ const result = await authStatus({ serverURL });
168
+ const status = !result.authenticated
169
+ ? "signed out"
170
+ : result.healthy
171
+ ? "signed in (healthy)"
172
+ : `signed in but needs repair (${result.reason || "health_check_failed"})`;
173
+ console.log(`MindBridge lifecycle OAuth: ${status}`);
174
+ if (!result.authenticated || !result.healthy) process.exitCode = 1;
175
+ return;
176
+ }
177
+ if (rest[0] === "auth" && rest[1] === "logout") {
178
+ await authLogout({ serverURL });
179
+ console.log("MindBridge lifecycle OAuth: signed out");
180
+ return;
181
+ }
182
+ if (rest[0] === "doctor") {
183
+ const result = await doctor({ serverURL });
184
+ if (rest.includes("--json")) console.log(JSON.stringify(result));
185
+ else {
186
+ console.log(`MindBridge Muse doctor: ${result.ok ? "healthy" : "needs attention"}`);
187
+ for (const [name, status] of Object.entries(result.checks)) console.log(`${status === "pass" ? "PASS" : "FAIL"} ${name}: ${status}`);
188
+ console.log(`Outbox: ${result.outbox.pending} pending across ${result.outbox.sessions} sessions`);
189
+ }
190
+ if (!result.ok) process.exitCode = 1;
191
+ return;
192
+ }
193
+ console.error("Usage: mindbridge-muse setup [--server-url URL] | doctor [--json] | auth <status|logout>");
194
+ process.exitCode = 2;
195
+ }
196
+
197
+ function isMainModule() {
198
+ if (!process.argv[1]) return false;
199
+ try {
200
+ return realpathSync(process.argv[1]) === realpathSync(fileURLToPath(import.meta.url));
201
+ } catch {
202
+ return false;
203
+ }
204
+ }
205
+
206
+ if (isMainModule()) {
207
+ main().catch((error) => {
208
+ const detail = error?.phase || oauthReason(error);
209
+ console.error(`MindBridge Muse setup failed (${detail}). Run mindbridge-muse setup in an interactive desktop session.`);
210
+ process.exitCode = 1;
211
+ });
212
+ }
@@ -0,0 +1,233 @@
1
+ import {
2
+ agentHookStateRoot,
3
+ createOAuthClient,
4
+ createOAuthSessionClient,
5
+ oauthReason,
6
+ resolveServerURL,
7
+ } from "./agent-oauth.mjs";
8
+ import { resolveProjectMetadata } from "./project-metadata.mjs";
9
+
10
+ const HOOK_START_AUTH_TIMEOUT_MS = 25_000;
11
+ const HOOK_EVENT_AUTH_TIMEOUT_MS = 8_000;
12
+
13
+ function text(value) {
14
+ return typeof value === "string" ? value.trim() : "";
15
+ }
16
+
17
+ const SECRET_PATTERNS = [
18
+ /\b(?:api[\s_-]?key|access[\s_-]?token|auth[\s_-]?token|password|passwd|secret|token)\b\s*[:=]\s*\S{8,}/gi,
19
+ /\bsk-[A-Za-z0-9_-]{16,}\b/g,
20
+ /\bgh[pousr]_[A-Za-z0-9_]{20,}\b/g,
21
+ /\bAKIA[0-9A-Z]{16}\b/g,
22
+ /-----BEGIN PRIVATE KEY-----/gi,
23
+ ];
24
+
25
+ function redact(content) {
26
+ return SECRET_PATTERNS.reduce((value, pattern) => value.replace(pattern, "[REDACTED]"), content);
27
+ }
28
+
29
+ function hookExternalRef(host, event) {
30
+ const sessionID = text(event.session_id) || text(event.sessionId);
31
+ return sessionID ? `${host}:session=${encodeURIComponent(sessionID)}` : "";
32
+ }
33
+
34
+ function hookNotice(event, reason, reinjected) {
35
+ if (reinjected.has(text(event.source))) return undefined;
36
+ return {
37
+ hookSpecificOutput: {
38
+ hookEventName: "SessionStart",
39
+ additionalContext: `MindBridge: this session is not being recorded (${reason}).`,
40
+ },
41
+ };
42
+ }
43
+
44
+ function hookFailure(reason, executable) {
45
+ if ([
46
+ "authentication_required",
47
+ "authorization_cancelled",
48
+ "authorization_denied",
49
+ "authorization_timed_out",
50
+ "browser_unavailable",
51
+ "keychain_unavailable",
52
+ "refresh_rejected",
53
+ "unauthorized",
54
+ ].includes(reason)) return `run ${executable} setup`;
55
+ return reason;
56
+ }
57
+
58
+ function logProjectMetadataOmitted(options, host) {
59
+ try {
60
+ (options.logger || ((record) => process.stderr.write(`${JSON.stringify(record)}\n`)))({
61
+ component: "mindbridge-agent-hook",
62
+ event: "project_metadata_omitted",
63
+ host,
64
+ reason: "detection_failed",
65
+ });
66
+ } catch {
67
+ // Diagnostics must not affect the host runtime.
68
+ }
69
+ }
70
+
71
+ function createOAuthHookRuntime({ host, adapterName, endEvent, endReason, reinjected, executable, ...options }) {
72
+ let remote;
73
+ const stateRoot = agentHookStateRoot(options);
74
+ async function client(authorizationTimeoutMs) {
75
+ remote ??= (async () => {
76
+ const serverURL = await resolveServerURL({ host, env: options.env, configRoot: options.configRoot, home: options.home });
77
+ const oauth = options.oauth || createOAuthClient({
78
+ host,
79
+ serverURL,
80
+ env: options.env,
81
+ configRoot: options.configRoot,
82
+ stateRoot: options.stateRoot,
83
+ keyring: options.keyring,
84
+ keyringLoader: options.keyringLoader,
85
+ fetchImpl: options.fetchImpl,
86
+ browser: options.browser,
87
+ authorizationTimeoutMs,
88
+ requestTimeoutMs: Math.max(1_000, Math.min(6_000, Math.floor(authorizationTimeoutMs / 2) - 250)),
89
+ });
90
+ return createOAuthSessionClient({ serverURL, oauth, stateRoot });
91
+ })();
92
+ return remote;
93
+ }
94
+ async function startPayload(event, externalRef) {
95
+ const payload = {
96
+ externalRef,
97
+ host,
98
+ userName: "MindBridge User",
99
+ agentName: adapterName,
100
+ adapterName,
101
+ adapterRef: `${host}:hook`,
102
+ };
103
+ try {
104
+ const metadata = await (options.projectMetadataResolver || resolveProjectMetadata)({
105
+ cwd: text(event.cwd),
106
+ projectRoot: host === "claude" ? text((options.env || process.env).CLAUDE_PROJECT_DIR) : "",
107
+ stateRoot,
108
+ runGit: options.runGit,
109
+ });
110
+ if (metadata) payload.metadata = metadata;
111
+ else logProjectMetadataOmitted(options, host);
112
+ } catch {
113
+ logProjectMetadataOmitted(options, host);
114
+ }
115
+ return payload;
116
+ }
117
+ async function invoke(operation, payload, authorizationTimeoutMs) {
118
+ try {
119
+ return await (await client(authorizationTimeoutMs))[operation](payload);
120
+ } catch (error) {
121
+ return { error: oauthReason(error), status: error?.status };
122
+ }
123
+ }
124
+ async function appendMessage(event, externalRef, role, content, prefix, authorizationTimeoutMs) {
125
+ const turn = text(event.turn_id) || text(event.turnId);
126
+ const append = () => invoke("append", {
127
+ externalRef,
128
+ role,
129
+ eventType: "message",
130
+ content: redact(content),
131
+ messageRef: turn ? `${prefix}:${turn}` : "",
132
+ }, authorizationTimeoutMs);
133
+ let result = await append();
134
+ if (result?.error === "missing_session_state") {
135
+ const started = await invoke("start", await startPayload(event, externalRef), authorizationTimeoutMs);
136
+ if (!started?.error) result = await append();
137
+ } else if (result?.error === "session_ended" || result?.status === 409) {
138
+ const appendWasQueued = result.status === 409;
139
+ result = await invoke("reopen", { externalRef }, authorizationTimeoutMs);
140
+ if (!appendWasQueued && !result?.error) result = await append();
141
+ }
142
+ return result;
143
+ }
144
+ return {
145
+ async handle(event = {}) {
146
+ const externalRef = hookExternalRef(host, event);
147
+ const name = text(event.hook_event_name) || text(event.hookEventName);
148
+ if (!externalRef) return undefined;
149
+ const authorizationTimeoutMs = options.authorizationTimeoutMs ?? (name === "SessionStart" ? HOOK_START_AUTH_TIMEOUT_MS : HOOK_EVENT_AUTH_TIMEOUT_MS);
150
+ if (name === "SessionStart") {
151
+ await (await client(authorizationTimeoutMs)).flushAll({ excludeExternalRef: externalRef, limit: 1 });
152
+ const result = await invoke("start", await startPayload(event, externalRef), authorizationTimeoutMs);
153
+ if (!result || result.error) return hookNotice(event, hookFailure(result?.error, executable), reinjected);
154
+ if (!result.prompt_block || reinjected.has(text(event.source))) return undefined;
155
+ return { hookSpecificOutput: { hookEventName: "SessionStart", additionalContext: result.prompt_block } };
156
+ }
157
+ if (name === "UserPromptSubmit") {
158
+ const content = text(event.prompt);
159
+ if (!content) return undefined;
160
+ await appendMessage(event, externalRef, "user", content, "in", authorizationTimeoutMs);
161
+ return undefined;
162
+ }
163
+ let appendSucceeded = true;
164
+ if (name === "Stop") {
165
+ const content = text(event.last_assistant_message) || text(event.lastAssistantMessage);
166
+ if (content) {
167
+ const result = await appendMessage(event, externalRef, "assistant", content, "out", authorizationTimeoutMs);
168
+ appendSucceeded = Boolean(result && !result.error);
169
+ }
170
+ }
171
+ if (name === endEvent && appendSucceeded) await invoke("end", { externalRef, reason: endReason(event) }, authorizationTimeoutMs);
172
+ return undefined;
173
+ },
174
+ };
175
+ }
176
+
177
+ export function createCodexOAuthRuntime(options = {}) {
178
+ return createOAuthHookRuntime({
179
+ ...options,
180
+ host: "codex",
181
+ adapterName: "Codex",
182
+ executable: "npx -y @mindbridgeio/codex@latest",
183
+ endEvent: "Stop",
184
+ endReason: () => "stop",
185
+ reinjected: new Set(["compact"]),
186
+ });
187
+ }
188
+
189
+ export function createClaudeOAuthRuntime(options = {}) {
190
+ return createOAuthHookRuntime({
191
+ ...options,
192
+ host: "claude",
193
+ adapterName: "Claude",
194
+ executable: "npx -y @mindbridgeio/claude@latest",
195
+ endEvent: "SessionEnd",
196
+ endReason: (event) => text(event.reason) || "session_end",
197
+ reinjected: new Set(["compact", "resume"]),
198
+ });
199
+ }
200
+
201
+ export function createMuseOAuthRuntime(options = {}) {
202
+ return createOAuthHookRuntime({
203
+ ...options,
204
+ host: "muse",
205
+ adapterName: "Muse",
206
+ executable: "npx -y @mindbridgeio/muse@latest",
207
+ endEvent: "SessionEnd",
208
+ endReason: (event) => text(event.reason) || "session_end",
209
+ reinjected: new Set(["compact", "resume"]),
210
+ });
211
+ }
212
+
213
+ /** Shared stdin/stdout runner. Hooks always fail safely and never block a host. */
214
+ export async function runOAuthHook(createRuntime) {
215
+ let event = {};
216
+ try {
217
+ let raw = "";
218
+ process.stdin.setEncoding("utf8");
219
+ for await (const chunk of process.stdin) raw += chunk;
220
+ if (raw.trim()) event = JSON.parse(raw);
221
+ } catch {
222
+ event = {};
223
+ }
224
+ let output;
225
+ try {
226
+ output = await createRuntime().handle(event);
227
+ } catch {
228
+ output = undefined;
229
+ }
230
+ process.stdout.on("error", () => process.exit(0));
231
+ if (output) process.stdout.write(JSON.stringify(output), () => process.exit(0));
232
+ else process.exit(0);
233
+ }
@@ -0,0 +1,62 @@
1
+ import { spawn } from "node:child_process";
2
+ import { mkdir, readFile, rename, unlink, writeFile } from "node:fs/promises";
3
+ import { basename, dirname, join } from "node:path";
4
+
5
+ export async function readJSON(path, fallback) {
6
+ try {
7
+ return JSON.parse(await readFile(path, "utf8"));
8
+ } catch (error) {
9
+ if (error?.code === "ENOENT") return fallback;
10
+ throw new Error(`cannot read ${path}`);
11
+ }
12
+ }
13
+
14
+ export async function atomicWrite(path, content) {
15
+ await mkdir(dirname(path), { recursive: true, mode: 0o700 });
16
+ const temp = join(dirname(path), `.${basename(path)}.${process.pid}.tmp`);
17
+ try {
18
+ await writeFile(temp, content, { mode: 0o600 });
19
+ await rename(temp, path);
20
+ } finally {
21
+ await unlink(temp).catch(() => {});
22
+ }
23
+ }
24
+
25
+ function managedHook(hook, statuses) {
26
+ const command = typeof hook?.command === "string" ? hook.command.toLowerCase() : "";
27
+ const status = typeof hook?.statusMessage === "string" ? hook.statusMessage.toLowerCase() : "";
28
+ return (command.includes("mindbridge") && command.includes("hook.mjs")) || statuses.includes(status);
29
+ }
30
+
31
+ export function withoutManagedHooks(groups, statuses) {
32
+ if (!Array.isArray(groups)) return [];
33
+ return groups.flatMap((group) => {
34
+ if (!Array.isArray(group?.hooks)) return [group];
35
+ const hooks = group.hooks.filter((hook) => !managedHook(hook, statuses));
36
+ return hooks.length ? [{ ...group, hooks }] : [];
37
+ });
38
+ }
39
+
40
+ export function runCommand(command, args) {
41
+ return new Promise((resolve, reject) => {
42
+ const child = spawn(command, args, { stdio: "inherit", shell: false, windowsHide: true });
43
+ child.once("error", reject);
44
+ child.once("close", (code) => {
45
+ if (code === 0) resolve();
46
+ else {
47
+ const error = new Error("host command failed");
48
+ error.exitCode = code;
49
+ reject(error);
50
+ }
51
+ });
52
+ });
53
+ }
54
+
55
+ export async function phase(name, operation) {
56
+ try {
57
+ return await operation();
58
+ } catch (error) {
59
+ error.phase = name;
60
+ throw error;
61
+ }
62
+ }