@raysonmeng/agentbridge 0.1.6 → 0.1.7

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.
@@ -0,0 +1,209 @@
1
+ #!/usr/bin/env node
2
+
3
+ /**
4
+ * Shared install-time safety checks.
5
+ *
6
+ * This intentionally stays CommonJS so both the ESM local installer and npm's
7
+ * CommonJS postinstall hook can use the same stop/verify behavior.
8
+ */
9
+
10
+ const { spawnSync } = require("node:child_process");
11
+ const { existsSync, readFileSync, statSync } = require("node:fs");
12
+ const path = require("node:path");
13
+
14
+ const PACKAGE_ROOT = path.resolve(__dirname, "..");
15
+
16
+ const REQUIRED_ARTIFACTS = Object.freeze([
17
+ "dist/cli.js",
18
+ "dist/daemon.js",
19
+ ".claude-plugin/marketplace.json",
20
+ "plugins/agentbridge/.claude-plugin/plugin.json",
21
+ "plugins/agentbridge/.mcp.json",
22
+ "plugins/agentbridge/README.md",
23
+ "plugins/agentbridge/commands/init.md",
24
+ "plugins/agentbridge/hooks/hooks.json",
25
+ "plugins/agentbridge/scripts/health-check.sh",
26
+ "plugins/agentbridge/scripts/plugin-update-notice.mjs",
27
+ "plugins/agentbridge/server/bridge-server.js",
28
+ "plugins/agentbridge/server/daemon.js",
29
+ "package.json",
30
+ "README.md",
31
+ "scripts/install-safety.cjs",
32
+ "scripts/postinstall.cjs",
33
+ ]);
34
+
35
+ function quote(arg) {
36
+ return /^[A-Za-z0-9_@%+=:,./<>-]+$/.test(arg) ? arg : JSON.stringify(arg);
37
+ }
38
+
39
+ function commandLine(cmd, args) {
40
+ return [cmd, ...args].map(quote).join(" ");
41
+ }
42
+
43
+ function fail(message, details = []) {
44
+ process.stderr.write(`install-safety: ${message}\n`);
45
+ for (const detail of details) {
46
+ process.stderr.write(` - ${detail}\n`);
47
+ }
48
+ process.exit(1);
49
+ }
50
+
51
+ function readPackageJson() {
52
+ return JSON.parse(readFileSync(path.join(PACKAGE_ROOT, "package.json"), "utf-8"));
53
+ }
54
+
55
+ function requiredPackagePaths() {
56
+ const pkg = readPackageJson();
57
+ const binTargets = Object.values(pkg.bin ?? {});
58
+ return [...new Set([...REQUIRED_ARTIFACTS, ...binTargets])];
59
+ }
60
+
61
+ function buildStopCommand() {
62
+ const sourceCli = path.join(PACKAGE_ROOT, "src", "cli.ts");
63
+ const bundledCli = path.join(PACKAGE_ROOT, "dist", "cli.js");
64
+ if (existsSync(sourceCli)) return ["bun", ["run", "src/cli.ts", "kill", "--all"]];
65
+ return ["bun", ["run", bundledCli, "kill", "--all"]];
66
+ }
67
+
68
+ function agentBridgeInstallEnv(baseEnv = process.env) {
69
+ const env = { ...baseEnv };
70
+ for (const key of Object.keys(env)) {
71
+ if (key.startsWith("AGENTBRIDGE_")) delete env[key];
72
+ }
73
+ delete env.CODEX_WS_PORT;
74
+ delete env.CODEX_PROXY_PORT;
75
+ return env;
76
+ }
77
+
78
+ function stopRunningAgentBridge(options = {}) {
79
+ const { dryRun = false, bestEffort = false } = options;
80
+ const [cmd, args] = buildStopCommand();
81
+ if (dryRun) {
82
+ process.stdout.write(`$ ${commandLine(cmd, args)} # stop running AgentBridge daemons/TUIs\n`);
83
+ return { status: 0 };
84
+ }
85
+
86
+ process.stdout.write(`$ ${commandLine(cmd, args)}\n`);
87
+ const res = spawnSync(cmd, args, {
88
+ cwd: PACKAGE_ROOT,
89
+ env: agentBridgeInstallEnv(),
90
+ stdio: "inherit",
91
+ });
92
+
93
+ if (res.error || res.status !== 0) {
94
+ const message = res.error
95
+ ? `failed to start stop command: ${res.error.message}`
96
+ : `stop command exited with ${res.status}`;
97
+ if (bestEffort) {
98
+ process.stdout.write(`install-safety: ${message}; continuing because this hook is best-effort\n`);
99
+ return res;
100
+ }
101
+ fail(message);
102
+ }
103
+ return res;
104
+ }
105
+
106
+ function verifyBuiltArtifacts() {
107
+ const missing = [];
108
+ const empty = [];
109
+ const notExecutable = [];
110
+ const pkg = readPackageJson();
111
+ const required = requiredPackagePaths();
112
+ const binTargets = new Set(Object.values(pkg.bin ?? {}));
113
+
114
+ for (const rel of required) {
115
+ const absolute = path.join(PACKAGE_ROOT, rel);
116
+ if (!existsSync(absolute)) {
117
+ missing.push(rel);
118
+ continue;
119
+ }
120
+ const stat = statSync(absolute);
121
+ if (stat.size <= 0) empty.push(rel);
122
+ if (binTargets.has(rel) && (stat.mode & 0o111) === 0) {
123
+ notExecutable.push(rel);
124
+ }
125
+ }
126
+
127
+ const problems = [
128
+ ...missing.map((rel) => `missing: ${rel}`),
129
+ ...empty.map((rel) => `empty: ${rel}`),
130
+ ...notExecutable.map((rel) => `not executable: ${rel}`),
131
+ ];
132
+ if (problems.length > 0) {
133
+ fail("built artifact verification failed", problems);
134
+ }
135
+
136
+ process.stdout.write(`install-safety: verified ${required.length} built artifact(s)\n`);
137
+ }
138
+
139
+ function verifyTarball(tarballPath) {
140
+ if (!tarballPath) fail("verify-tarball requires a tarball path");
141
+ if (!existsSync(tarballPath)) fail(`tarball does not exist: ${tarballPath}`);
142
+
143
+ const res = spawnSync("tar", ["-tf", tarballPath], {
144
+ encoding: "utf-8",
145
+ stdio: ["ignore", "pipe", "pipe"],
146
+ });
147
+ if (res.error) fail(`failed to inspect tarball: ${res.error.message}`);
148
+ if (res.status !== 0) {
149
+ fail(`tar -tf failed with ${res.status}`, [res.stderr?.trim()].filter(Boolean));
150
+ }
151
+
152
+ const packed = new Set(
153
+ res.stdout
154
+ .split("\n")
155
+ .map((line) => line.trim())
156
+ .filter(Boolean)
157
+ .map((line) => line.replace(/^package\//, "")),
158
+ );
159
+ const required = requiredPackagePaths();
160
+ const missing = required.filter((rel) => !packed.has(rel));
161
+ if (missing.length > 0) {
162
+ fail("packed tarball is missing required artifact(s)", missing);
163
+ }
164
+
165
+ process.stdout.write(`install-safety: verified ${required.length} artifact(s) in ${tarballPath}\n`);
166
+ }
167
+
168
+ function usage() {
169
+ process.stderr.write(`Usage:
170
+ node scripts/install-safety.cjs stop-running [--dry-run] [--best-effort]
171
+ node scripts/install-safety.cjs verify-built
172
+ node scripts/install-safety.cjs verify-tarball <tarball>
173
+ `);
174
+ }
175
+
176
+ function main() {
177
+ const [command, ...args] = process.argv.slice(2);
178
+ if (command === "stop-running") {
179
+ stopRunningAgentBridge({
180
+ dryRun: args.includes("--dry-run"),
181
+ bestEffort: args.includes("--best-effort"),
182
+ });
183
+ return;
184
+ }
185
+ if (command === "verify-built") {
186
+ verifyBuiltArtifacts();
187
+ return;
188
+ }
189
+ if (command === "verify-tarball") {
190
+ verifyTarball(args[0]);
191
+ return;
192
+ }
193
+ usage();
194
+ process.exit(1);
195
+ }
196
+
197
+ if (require.main === module) {
198
+ main();
199
+ }
200
+
201
+ module.exports = {
202
+ REQUIRED_ARTIFACTS,
203
+ agentBridgeInstallEnv,
204
+ commandLine,
205
+ requiredPackagePaths,
206
+ stopRunningAgentBridge,
207
+ verifyBuiltArtifacts,
208
+ verifyTarball,
209
+ };
@@ -9,20 +9,78 @@
9
9
  */
10
10
 
11
11
  const { execFileSync } = require("child_process");
12
+ const { existsSync } = require("fs");
12
13
  const path = require("path");
14
+ const { stopRunningAgentBridge } = require("./install-safety.cjs");
13
15
 
14
16
  const PACKAGE_ROOT = path.resolve(__dirname, "..");
15
17
  const MARKETPLACE_NAME = "agentbridge";
16
18
  const PLUGIN_NAME = "agentbridge";
19
+ // Every external command gets a hard timeout: postinstall runs inside the
20
+ // user's `npm install`, and a hung `claude` CLI (auth prompt, broken update)
21
+ // previously hung the whole install with it. Timeouts fall through to the
22
+ // existing warn-and-continue paths (`abg init` is always the fallback).
23
+ const VERSION_PROBE_TIMEOUT_MS = 10_000;
24
+ const PLUGIN_STEP_TIMEOUT_MS = 30_000;
17
25
 
18
- // Step 1: Check Bun
19
- let bunOk = false;
20
- try {
21
- const version = execFileSync("bun", ["--version"], { encoding: "utf-8" }).trim();
22
- console.log(`\x1b[32m✔\x1b[0m AgentBridge: Bun ${version} detected.`);
23
- bunOk = true;
24
- } catch {
25
- console.warn(`
26
+ /**
27
+ * Declarative opt-out of the marketplace/plugin registration steps (CI image
28
+ * builds, air-gapped installs) — symmetric with AGENTBRIDGE_POSTINSTALL_STOP.
29
+ */
30
+ function skipPluginRegistration(env = process.env) {
31
+ return env.AGENTBRIDGE_POSTINSTALL_PLUGIN === "0";
32
+ }
33
+ /**
34
+ * Decide whether postinstall should stop ALL running AgentBridge pairs.
35
+ *
36
+ * Stop-the-world is destructive (it kills every running daemon/TUI), so it is
37
+ * gated to INTENTIONAL global self-installs only. The intentional installer
38
+ * (scripts/install-global.mjs) already calls install-safety stop-running
39
+ * directly, so postinstall only needs to react to an explicit global signal.
40
+ *
41
+ * Pure + injectable for unit testing.
42
+ *
43
+ * @param {{ env?: NodeJS.ProcessEnv, hasSourceCli?: boolean }} [opts]
44
+ * @returns {boolean}
45
+ */
46
+ function shouldStopRunningDaemons({
47
+ env = process.env,
48
+ hasSourceCli = existsSync(path.join(PACKAGE_ROOT, "src", "cli.ts")),
49
+ } = {}) {
50
+ // hasSourceCli is accepted for symmetry/future use; intentionally unused so
51
+ // that a packed (no-src) install no longer triggers stop-the-world on its own.
52
+ void hasSourceCli;
53
+ if (env.AGENTBRIDGE_POSTINSTALL_STOP === "0") return false;
54
+ if (env.AGENTBRIDGE_POSTINSTALL_STOP === "1") return true;
55
+ if (env.npm_config_global === "true") return true;
56
+ if (env.npm_config_location === "global") return true;
57
+ return false;
58
+ }
59
+
60
+ function runPostinstall() {
61
+ const dryRun = process.argv.includes("--dry-run");
62
+
63
+ if (dryRun) {
64
+ stopRunningAgentBridge({ dryRun: true });
65
+ console.log("$ claude --version");
66
+ console.log(`$ claude plugin marketplace add ${PACKAGE_ROOT}`);
67
+ console.log(`$ claude plugin install ${PLUGIN_NAME}@${MARKETPLACE_NAME}`);
68
+ process.exit(0);
69
+ }
70
+
71
+ // Step 1: Check Bun
72
+ let bunOk = false;
73
+ try {
74
+ const version = execFileSync("bun", ["--version"], { encoding: "utf-8", timeout: VERSION_PROBE_TIMEOUT_MS }).trim();
75
+ console.log(`\x1b[32m✔\x1b[0m AgentBridge: Bun ${version} detected.`);
76
+ bunOk = true;
77
+ } catch (e) {
78
+ // A timeout means bun exists but hung — "install Bun" advice would be
79
+ // misleading there; tell the truth and point at the abg init fallback.
80
+ if (e && e.code === "ETIMEDOUT") {
81
+ console.warn(`\x1b[33m⚠\x1b[0m AgentBridge: \`bun --version\` timed out — run \`abg init\` after the install completes.`);
82
+ } else {
83
+ console.warn(`
26
84
  \x1b[33m⚠ AgentBridge requires Bun (v1.0+) as its runtime.\x1b[0m
27
85
 
28
86
  The CLI was installed, but it won't work without Bun.
@@ -34,34 +92,56 @@ Then restart your terminal and run:
34
92
 
35
93
  abg init
36
94
  `);
37
- }
38
-
39
- // Step 2: Register marketplace + install plugin (requires Claude Code)
40
- if (bunOk) {
41
- try {
42
- execFileSync("claude", ["--version"], { encoding: "utf-8" });
43
- } catch {
44
- console.log(`\x1b[33m⚠\x1b[0m AgentBridge: Claude Code not found — skipping plugin install.`);
45
- console.log(` After installing Claude Code, run: abg init`);
46
- process.exit(0);
95
+ }
47
96
  }
48
97
 
49
- try {
50
- execFileSync("claude", ["plugin", "marketplace", "add", PACKAGE_ROOT], {
51
- stdio: "pipe",
52
- });
53
- console.log(`\x1b[32m✔\x1b[0m AgentBridge: Marketplace registered.`);
54
- } catch (e) {
55
- console.log(`\x1b[33m⚠\x1b[0m AgentBridge: Marketplace registration failed — run \`abg init\` to retry.`);
56
- process.exit(0);
57
- }
98
+ // Step 2: Register marketplace + install plugin (requires Claude Code)
99
+ if (bunOk && !skipPluginRegistration()) {
100
+ if (shouldStopRunningDaemons()) {
101
+ try {
102
+ stopRunningAgentBridge({ bestEffort: true });
103
+ } catch {
104
+ console.log(`\x1b[33m⚠\x1b[0m AgentBridge: could not stop running daemons — run \`abg kill --all\` before relying on this install.`);
105
+ }
106
+ } else {
107
+ console.log(`\x1b[33m⚠\x1b[0m AgentBridge: not an explicit global self-install — leaving running daemons untouched (use \`abg kill --all\` or install-global to stop).`);
108
+ }
58
109
 
59
- try {
60
- execFileSync("claude", ["plugin", "install", `${PLUGIN_NAME}@${MARKETPLACE_NAME}`], {
61
- stdio: "pipe",
62
- });
63
- console.log(`\x1b[32m✔\x1b[0m AgentBridge: Plugin installed. Run \`abg claude\` to start.`);
64
- } catch (e) {
65
- console.log(`\x1b[33m⚠\x1b[0m AgentBridge: Plugin install failed — run \`abg init\` to retry.`);
110
+ try {
111
+ execFileSync("claude", ["--version"], { encoding: "utf-8", timeout: VERSION_PROBE_TIMEOUT_MS });
112
+ } catch {
113
+ console.log(`\x1b[33m⚠\x1b[0m AgentBridge: Claude Code not found — skipping plugin install.`);
114
+ console.log(` After installing Claude Code, run: abg init`);
115
+ process.exit(0);
116
+ }
117
+
118
+ try {
119
+ execFileSync("claude", ["plugin", "marketplace", "add", PACKAGE_ROOT], {
120
+ stdio: "pipe",
121
+ timeout: PLUGIN_STEP_TIMEOUT_MS,
122
+ });
123
+ console.log(`\x1b[32m✔\x1b[0m AgentBridge: Marketplace registered.`);
124
+ } catch (e) {
125
+ console.log(`\x1b[33m⚠\x1b[0m AgentBridge: Marketplace registration failed — run \`abg init\` to retry.`);
126
+ process.exit(0);
127
+ }
128
+
129
+ try {
130
+ execFileSync("claude", ["plugin", "install", `${PLUGIN_NAME}@${MARKETPLACE_NAME}`], {
131
+ stdio: "pipe",
132
+ timeout: PLUGIN_STEP_TIMEOUT_MS,
133
+ });
134
+ console.log(`\x1b[32m✔\x1b[0m AgentBridge: Plugin installed. Run \`abg claude\` to start.`);
135
+ } catch (e) {
136
+ console.log(`\x1b[33m⚠\x1b[0m AgentBridge: Plugin install failed — run \`abg init\` to retry.`);
137
+ }
66
138
  }
67
139
  }
140
+
141
+ module.exports = { shouldStopRunningDaemons, skipPluginRegistration };
142
+
143
+ // Only run install side effects when invoked directly (not when require()'d
144
+ // from a unit test).
145
+ if (require.main === module) {
146
+ runPostinstall();
147
+ }