@sjawhar/opencode-legion-envoy 0.5.2 → 0.6.1
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/dist/bin/dispatch-mcp-shim.js +13905 -0
- package/dist/src/server.js +14512 -0
- package/package.json +15 -6
- package/src/config/index.ts +2 -1
- package/src/dispatch-mcp.ts +15 -4
- package/src/dispatch-subscribe.ts +1 -1
- package/src/port.ts +3 -10
- package/src/server.ts +12 -9
- package/src/ss.ts +19 -0
- package/src/tui-port.ts +5 -13
- package/AGENTS.md +0 -34
- package/bin/dispatch-mcp-shim.ts +0 -75
- package/scripts/sync-host.sh +0 -73
- package/src/__tests__/clipboard.test.ts +0 -56
- package/src/__tests__/dispatch-mcp-bridge.test.ts +0 -339
- package/src/__tests__/dispatch-mcp.test.ts +0 -116
- package/src/__tests__/dispatch-subscribe.test.ts +0 -66
- package/src/__tests__/index.test.ts +0 -634
- package/src/__tests__/log.test.ts +0 -135
- package/src/__tests__/port.test.ts +0 -124
- package/src/__tests__/tui-port.test.ts +0 -85
- package/src/config/__tests__/index.test.ts +0 -93
- package/src/dispatch-mcp-bridge.ts +0 -310
- package/tsconfig.json +0 -20
package/package.json
CHANGED
|
@@ -1,11 +1,14 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@sjawhar/opencode-legion-envoy",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.6.1",
|
|
4
4
|
"type": "module",
|
|
5
|
-
"main": "src/server.
|
|
5
|
+
"main": "dist/src/server.js",
|
|
6
6
|
"exports": {
|
|
7
|
+
".": {
|
|
8
|
+
"import": "./dist/src/server.js"
|
|
9
|
+
},
|
|
7
10
|
"./server": {
|
|
8
|
-
"import": "./src/server.
|
|
11
|
+
"import": "./dist/src/server.js"
|
|
9
12
|
},
|
|
10
13
|
"./tui": {
|
|
11
14
|
"import": "./src/tui.tsx"
|
|
@@ -15,15 +18,19 @@
|
|
|
15
18
|
"type": "git",
|
|
16
19
|
"url": "https://github.com/sjawhar/legion"
|
|
17
20
|
},
|
|
18
|
-
"
|
|
21
|
+
"files": [
|
|
22
|
+
"dist",
|
|
23
|
+
"src",
|
|
24
|
+
"!src/**/__tests__"
|
|
25
|
+
],
|
|
19
26
|
"scripts": {
|
|
27
|
+
"build": "bun build src/server.ts bin/dispatch-mcp-shim.ts --outdir dist --target bun --format esm --external '@opencode-ai/*'",
|
|
28
|
+
"prepack": "bun run build",
|
|
20
29
|
"typecheck": "bunx tsc --noEmit",
|
|
21
30
|
"test": "bun test",
|
|
22
31
|
"lint": "bunx biome check src/"
|
|
23
32
|
},
|
|
24
33
|
"dependencies": {
|
|
25
|
-
"@legion/contracts": "workspace:*",
|
|
26
|
-
"@legion/envoy-client": "workspace:*",
|
|
27
34
|
"@opencode-ai/plugin": "~1.14.46"
|
|
28
35
|
},
|
|
29
36
|
"peerDependencies": {
|
|
@@ -32,6 +39,8 @@
|
|
|
32
39
|
},
|
|
33
40
|
"devDependencies": {
|
|
34
41
|
"@biomejs/biome": "^2.3.14",
|
|
42
|
+
"@legion/contracts": "0.10.0",
|
|
43
|
+
"@legion/envoy-client": "0.1.0",
|
|
35
44
|
"@types/bun": "latest",
|
|
36
45
|
"solid-js": "^1.9.0",
|
|
37
46
|
"typescript": "^5.3.0"
|
package/src/config/index.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { existsSync, readFileSync } from "node:fs";
|
|
2
2
|
import os from "node:os";
|
|
3
3
|
import path from "node:path";
|
|
4
|
+
import { messageFor } from "@legion/envoy-client/errors";
|
|
4
5
|
import { logger } from "../log";
|
|
5
6
|
import { type EnvoyConfig, EnvoyConfigSchema } from "./schema";
|
|
6
7
|
|
|
@@ -23,7 +24,7 @@ function readConfigFile(filePath: string): EnvoyConfig | null {
|
|
|
23
24
|
}
|
|
24
25
|
return parsed.data as EnvoyConfig;
|
|
25
26
|
} catch (error) {
|
|
26
|
-
const message =
|
|
27
|
+
const message = messageFor(error);
|
|
27
28
|
logger.warn(`[envoy-plugin] Failed to load config at ${filePath}: ${message}`);
|
|
28
29
|
return null;
|
|
29
30
|
}
|
package/src/dispatch-mcp.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { existsSync } from "node:fs";
|
|
1
2
|
import path from "node:path";
|
|
2
3
|
import type { DispatchConfig } from "./config";
|
|
3
4
|
|
|
@@ -38,10 +39,20 @@ export interface BuildDispatchMcpEntryOptions {
|
|
|
38
39
|
const DEFAULT_SERVER_URL = "http://localhost:8766";
|
|
39
40
|
|
|
40
41
|
function defaultShimPath(): string {
|
|
41
|
-
//
|
|
42
|
-
// /
|
|
43
|
-
//
|
|
44
|
-
|
|
42
|
+
// Source layout: this module runs from src/ and the shim wrapper lives at
|
|
43
|
+
// ../bin/dispatch-mcp-shim.ts. Packed layout: this module is bundled to
|
|
44
|
+
// dist/src/server.js and the self-contained shim bundle lives at
|
|
45
|
+
// dist/bin/dispatch-mcp-shim.js — same ../bin relationship, built artifact.
|
|
46
|
+
const packageRoot = path.join(import.meta.dir, "..");
|
|
47
|
+
const candidates = [
|
|
48
|
+
path.join(packageRoot, "bin", "dispatch-mcp-shim.js"),
|
|
49
|
+
path.join(packageRoot, "bin", "dispatch-mcp-shim.ts"),
|
|
50
|
+
];
|
|
51
|
+
const found = candidates.find((candidate) => existsSync(candidate));
|
|
52
|
+
if (!found) {
|
|
53
|
+
throw new Error(`dispatch MCP shim not found; tried: ${candidates.join(", ")}`);
|
|
54
|
+
}
|
|
55
|
+
return found;
|
|
45
56
|
}
|
|
46
57
|
|
|
47
58
|
/**
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
// Auto-subscription wiring for the envoy_dispatch MCP tool
|
|
1
|
+
// Auto-subscription wiring for the envoy_dispatch MCP tool.
|
|
2
2
|
//
|
|
3
3
|
// When an agent opens a Dispatch thread via the envoy_dispatch MCP tool, the
|
|
4
4
|
// human answers by commenting on the resulting GitHub sub-issue. For the agent
|
package/src/port.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { execFile } from "node:child_process";
|
|
2
|
+
import { portFromSsOutput } from "./ss";
|
|
2
3
|
|
|
3
4
|
type ExecFn = (
|
|
4
5
|
command: string,
|
|
@@ -33,17 +34,9 @@ export async function resolvePort(
|
|
|
33
34
|
// Fallback: find listening port via ss(8) by PID
|
|
34
35
|
try {
|
|
35
36
|
const output = await exec("ss", ["-tlnp"], { encoding: "utf-8" });
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
const parts = line.trim().split(/\s+/);
|
|
39
|
-
const local = parts[3];
|
|
40
|
-
const match = local?.match(/:(\d+)$/);
|
|
41
|
-
if (!match) continue;
|
|
42
|
-
const port = Number.parseInt(match[1], 10);
|
|
43
|
-
if (Number.isFinite(port) && port > 0) return port;
|
|
44
|
-
}
|
|
37
|
+
const port = portFromSsOutput(output, process.pid);
|
|
38
|
+
if (port !== null) return port;
|
|
45
39
|
} catch {}
|
|
46
40
|
|
|
47
41
|
return null;
|
|
48
42
|
}
|
|
49
|
-
// trigger publish
|
package/src/server.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { agentSubject } from "@legion/contracts";
|
|
2
2
|
import { envoyDefaultsFromEnvironment } from "@legion/envoy-client/defaults";
|
|
3
|
+
import { machineID } from "@legion/envoy-client/machine";
|
|
3
4
|
import { envoyToolSpecs } from "@legion/envoy-client/tool-contract";
|
|
4
5
|
import { createEnvoyClient } from "@legion/envoy-client/transport";
|
|
5
6
|
import { tool } from "@opencode-ai/plugin/tool";
|
|
@@ -41,7 +42,10 @@ export default async (input: { serverUrl: URL }) => {
|
|
|
41
42
|
if (!port && !portWarningLogged) {
|
|
42
43
|
portWarningLogged = true;
|
|
43
44
|
logger.error(
|
|
44
|
-
|
|
45
|
+
[
|
|
46
|
+
`[envoy-plugin] Could not resolve serve port: serverUrl=${input.serverUrl.href},`,
|
|
47
|
+
`pid=${process.pid}`,
|
|
48
|
+
].join(" ")
|
|
45
49
|
);
|
|
46
50
|
}
|
|
47
51
|
if (port) {
|
|
@@ -218,10 +222,10 @@ export default async (input: { serverUrl: URL }) => {
|
|
|
218
222
|
input: { tool: string; sessionID: string; callID: string; args: unknown },
|
|
219
223
|
output: { title: string; output: string; metadata: unknown }
|
|
220
224
|
) => {
|
|
221
|
-
//
|
|
222
|
-
//
|
|
223
|
-
//
|
|
224
|
-
//
|
|
225
|
+
// When this session opens a Dispatch thread via the envoy_dispatch MCP
|
|
226
|
+
// tool, auto-subscribe it to the thread's GitHub topic so the human's
|
|
227
|
+
// reply is delivered back through Envoy. Best-effort — a subscribe
|
|
228
|
+
// failure must never surface to the model or fail the tool call.
|
|
225
229
|
const topic = dispatchSubscriptionTopic(input.tool, output.output);
|
|
226
230
|
if (!topic) return;
|
|
227
231
|
try {
|
|
@@ -234,9 +238,8 @@ export default async (input: { serverUrl: URL }) => {
|
|
|
234
238
|
driving: true,
|
|
235
239
|
});
|
|
236
240
|
} catch (err) {
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
);
|
|
241
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
242
|
+
logger.warn(`[envoy-plugin] dispatch auto-subscribe failed: ${message}`);
|
|
240
243
|
}
|
|
241
244
|
},
|
|
242
245
|
// Cleanup hook (used by tests; production relies on process 'exit').
|
|
@@ -325,7 +328,7 @@ export default async (input: { serverUrl: URL }) => {
|
|
|
325
328
|
return JSON.stringify(
|
|
326
329
|
{
|
|
327
330
|
session_id: sessionID,
|
|
328
|
-
machine_id:
|
|
331
|
+
machine_id: machineID(),
|
|
329
332
|
port,
|
|
330
333
|
dir: ctx.directory,
|
|
331
334
|
},
|
package/src/ss.ts
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Parse the listening TCP port for `pid` from `ss -tlnp` output.
|
|
3
|
+
*
|
|
4
|
+
* Shared by the server-side and TUI-side port resolvers, which differ only in
|
|
5
|
+
* how they discover the pid and whether they exec ss(8) sync or async.
|
|
6
|
+
*/
|
|
7
|
+
export function portFromSsOutput(output: string, pid: number): number | null {
|
|
8
|
+
const pidPattern = new RegExp(`\\bpid=${pid}\\b`);
|
|
9
|
+
for (const line of output.split("\n")) {
|
|
10
|
+
if (!pidPattern.test(line)) continue;
|
|
11
|
+
const parts = line.trim().split(/\s+/);
|
|
12
|
+
const local = parts[3];
|
|
13
|
+
const match = local?.match(/:(\d+)$/);
|
|
14
|
+
if (!match) continue;
|
|
15
|
+
const port = Number.parseInt(match[1], 10);
|
|
16
|
+
if (Number.isFinite(port) && port > 0) return port;
|
|
17
|
+
}
|
|
18
|
+
return null;
|
|
19
|
+
}
|
package/src/tui-port.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { execFileSync } from "node:child_process";
|
|
2
|
+
import { portFromSsOutput } from "./ss";
|
|
2
3
|
|
|
3
4
|
type ExecSyncFn = (command: string, args: string[], options: { encoding: string }) => string;
|
|
4
5
|
|
|
@@ -36,19 +37,10 @@ export function resolveCurrentProcessPort(exec: ExecSyncFn = defaultExecSync): n
|
|
|
36
37
|
|
|
37
38
|
function resolveProcessPort(pid: number, exec: ExecSyncFn = defaultExecSync): number | null {
|
|
38
39
|
try {
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
const local = parts[3];
|
|
44
|
-
const match = local?.match(/:(\d+)$/);
|
|
45
|
-
if (!match) continue;
|
|
46
|
-
const port = Number.parseInt(match[1], 10);
|
|
47
|
-
if (Number.isFinite(port) && port > 0) return port;
|
|
48
|
-
}
|
|
49
|
-
} catch {}
|
|
50
|
-
|
|
51
|
-
return null;
|
|
40
|
+
return portFromSsOutput(exec("ss", ["-tlnp"], { encoding: "utf-8" }), pid);
|
|
41
|
+
} catch {
|
|
42
|
+
return null;
|
|
43
|
+
}
|
|
52
44
|
}
|
|
53
45
|
|
|
54
46
|
export function resolveSessionProcessPort(
|
package/AGENTS.md
DELETED
|
@@ -1,34 +0,0 @@
|
|
|
1
|
-
# Envoy Plugin Package
|
|
2
|
-
|
|
3
|
-
OpenCode plugin package for Legion's Envoy subsystem.
|
|
4
|
-
|
|
5
|
-
## Overview
|
|
6
|
-
|
|
7
|
-
This plugin exposes the Envoy tools and maintains the live session registry metadata Envoy needs for hot delivery.
|
|
8
|
-
|
|
9
|
-
It is the user-facing bridge between OpenCode sessions and Envoy transport.
|
|
10
|
-
|
|
11
|
-
## Where to look
|
|
12
|
-
|
|
13
|
-
| Task | Location | Notes |
|
|
14
|
-
| ------------------- | ---------------------- | ------------------------------------------------------------------ |
|
|
15
|
-
| Tool definitions | `src/server.ts` | `envoy_subscribe`, `envoy_unsubscribe`, `envoy_list`, `envoy_send`, `envoy_publish`, `envoy_role_set`, `envoy_whoami`, `envoy_sessions` |
|
|
16
|
-
| Packaging metadata | `package.json` | npm identity, `exports` map, scripts |
|
|
17
|
-
| TUI: `/whoami` + sidebar | `src/tui.tsx` | slash command + session-id/port sidebar; loaded via the `./tui` export. Ships as `.tsx` source (no build/`dist`) — Bun transpiles it natively at load, so `@opentui/core` + `@opentui/solid` MUST be `peerDependencies` (not `devDependencies`) so the `@jsxImportSource @opentui/solid` runtime resolves in the consumer's install tree |
|
|
18
|
-
| Host rollout helper | `scripts/sync-host.sh` | sync packed release tarball + shim to remote host |
|
|
19
|
-
| Dispatch MCP + auto-subscribe | `src/dispatch-mcp.ts`, `src/dispatch-subscribe.ts` | injects the dispatch MCP server (shim); `tool.execute.after` auto-subscribes the caller to the new thread's topic so answers route back (Dispatch AC#4) |
|
|
20
|
-
|
|
21
|
-
## Critical conventions
|
|
22
|
-
|
|
23
|
-
- Tool descriptions must be self-describing enough that agents can infer correct topic formats.
|
|
24
|
-
- Slack examples must use real `team_id` values, not workspace slugs.
|
|
25
|
-
- This package owns the session-registry/port-backfill behavior now; do not split that back into a second plugin casually.
|
|
26
|
-
- Keep the plugin source-of-truth here even if a dotfiles shim is still used for rollout convenience.
|
|
27
|
-
|
|
28
|
-
## Topic reminders
|
|
29
|
-
|
|
30
|
-
- Agent: `notifications.agent.<session_id>`
|
|
31
|
-
- GitHub: `notifications.github.<owner>.<repo>.<kind>`
|
|
32
|
-
- Slack: `notifications.slack.<team_id>.<channel_id>.<message|mention>`
|
|
33
|
-
|
|
34
|
-
If you are unsure what a session is subscribed to, use `envoy_list()`.
|
package/bin/dispatch-mcp-shim.ts
DELETED
|
@@ -1,75 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env bun
|
|
2
|
-
// envoy-plugin local MCP shim.
|
|
3
|
-
//
|
|
4
|
-
// Spawned by OpenCode as a `type: "local"` MCP transport. Reads JSON-RPC
|
|
5
|
-
// messages from stdin (newline-delimited), forwards each to the remote
|
|
6
|
-
// dispatch server's Streamable HTTP /mcp endpoint with a fresh GitHub
|
|
7
|
-
// bearer minted via the user's `gh` shim, and writes responses to stdout.
|
|
8
|
-
//
|
|
9
|
-
// Token rotation is invisible to OpenCode — the shim handles 50-minute
|
|
10
|
-
// refresh cycles + immediate retry on 401. This avoids the "MCP dies
|
|
11
|
-
// after 1 hour" failure mode of static-header configurations.
|
|
12
|
-
|
|
13
|
-
import * as readline from "node:readline";
|
|
14
|
-
import {
|
|
15
|
-
createBridge,
|
|
16
|
-
defaultGhTokenGetter,
|
|
17
|
-
type JsonRpcRequest,
|
|
18
|
-
} from "../src/dispatch-mcp-bridge";
|
|
19
|
-
|
|
20
|
-
const remoteUrl = process.env.DISPATCH_MCP_URL;
|
|
21
|
-
if (!remoteUrl) {
|
|
22
|
-
process.stderr.write("envoy-dispatch shim: DISPATCH_MCP_URL is required\n");
|
|
23
|
-
process.exit(1);
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
const bridge = createBridge({
|
|
27
|
-
remoteUrl,
|
|
28
|
-
getToken: defaultGhTokenGetter,
|
|
29
|
-
});
|
|
30
|
-
|
|
31
|
-
const rl = readline.createInterface({ input: process.stdin });
|
|
32
|
-
|
|
33
|
-
let inflight = 0;
|
|
34
|
-
let closed = false;
|
|
35
|
-
|
|
36
|
-
// Serialize incoming requests. MCP requires the initialize handshake to
|
|
37
|
-
// complete before any tool calls are processed; running rl.on("line")
|
|
38
|
-
// callbacks in parallel would race tools/call against initialize and hit
|
|
39
|
-
// `invalid during session initialization` from the server. Even after
|
|
40
|
-
// init, sequencing keeps the wire ordering deterministic, which is what
|
|
41
|
-
// OpenCode expects for stdio MCP transports.
|
|
42
|
-
let chain: Promise<void> = Promise.resolve();
|
|
43
|
-
|
|
44
|
-
function maybeExit(): void {
|
|
45
|
-
if (closed && inflight === 0) process.exit(0);
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
rl.on("line", (line) => {
|
|
49
|
-
const trimmed = line.trim();
|
|
50
|
-
if (!trimmed) return;
|
|
51
|
-
inflight++;
|
|
52
|
-
chain = chain.then(async () => {
|
|
53
|
-
try {
|
|
54
|
-
const request = JSON.parse(trimmed) as JsonRpcRequest;
|
|
55
|
-
const response = await bridge.handle(request);
|
|
56
|
-
if (response !== null) {
|
|
57
|
-
process.stdout.write(`${JSON.stringify(response)}\n`);
|
|
58
|
-
}
|
|
59
|
-
} catch (err) {
|
|
60
|
-
const msg = err instanceof Error ? err.message : String(err);
|
|
61
|
-
process.stderr.write(`envoy-dispatch shim: ${msg}\n`);
|
|
62
|
-
} finally {
|
|
63
|
-
inflight--;
|
|
64
|
-
maybeExit();
|
|
65
|
-
}
|
|
66
|
-
});
|
|
67
|
-
});
|
|
68
|
-
|
|
69
|
-
rl.on("close", () => {
|
|
70
|
-
closed = true;
|
|
71
|
-
maybeExit();
|
|
72
|
-
});
|
|
73
|
-
|
|
74
|
-
process.on("SIGTERM", () => process.exit(0));
|
|
75
|
-
process.on("SIGINT", () => process.exit(0));
|
package/scripts/sync-host.sh
DELETED
|
@@ -1,73 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env bash
|
|
2
|
-
set -euo pipefail
|
|
3
|
-
|
|
4
|
-
host="${1:?usage: sync-host.sh user@host}"
|
|
5
|
-
|
|
6
|
-
PLUGIN_DIR="legion/default/packages/envoy-plugin"
|
|
7
|
-
PLUGIN_REF="file://{env:HOME}/${PLUGIN_DIR}"
|
|
8
|
-
REPO="sjawhar/legion"
|
|
9
|
-
|
|
10
|
-
# Find latest envoy release tag
|
|
11
|
-
tag=$(gh release list --repo "$REPO" --limit 10 --json tagName \
|
|
12
|
-
--jq '[.[] | select(.tagName | startswith("legion-envoy-"))][0].tagName')
|
|
13
|
-
if [ -z "$tag" ]; then
|
|
14
|
-
echo "ERROR: No legion-envoy release found" >&2
|
|
15
|
-
exit 1
|
|
16
|
-
fi
|
|
17
|
-
echo "Using release: $tag"
|
|
18
|
-
|
|
19
|
-
# Download plugin tarball
|
|
20
|
-
tmpdir=$(mktemp -d)
|
|
21
|
-
trap 'rm -rf "$tmpdir"' EXIT
|
|
22
|
-
|
|
23
|
-
gh release download "$tag" \
|
|
24
|
-
--repo "$REPO" \
|
|
25
|
-
--pattern '*.tgz' \
|
|
26
|
-
--dir "$tmpdir" \
|
|
27
|
-
--clobber
|
|
28
|
-
|
|
29
|
-
tgz=$(find "$tmpdir" -name '*.tgz' -print -quit)
|
|
30
|
-
if [ -z "$tgz" ]; then
|
|
31
|
-
echo "ERROR: No .tgz found in release $tag" >&2
|
|
32
|
-
exit 1
|
|
33
|
-
fi
|
|
34
|
-
echo "Downloaded: $(basename "$tgz")"
|
|
35
|
-
|
|
36
|
-
# Install on remote: extract tarball (strip package/ prefix from npm pack output)
|
|
37
|
-
ssh "$host" "rm -rf ~/${PLUGIN_DIR} && mkdir -p ~/${PLUGIN_DIR}"
|
|
38
|
-
scp -q "$tgz" "$host:/tmp/envoy-plugin.tgz"
|
|
39
|
-
ssh "$host" "tar xzf /tmp/envoy-plugin.tgz --strip-components=1 -C ~/${PLUGIN_DIR} && rm /tmp/envoy-plugin.tgz"
|
|
40
|
-
echo "Installed plugin to ~/${PLUGIN_DIR}"
|
|
41
|
-
|
|
42
|
-
# Update opencode.json: replace npm package reference with file:// path
|
|
43
|
-
# Resolves symlinks so we don't clobber dotfiles symlink structure
|
|
44
|
-
ssh "$host" "
|
|
45
|
-
CONFIG=\$(readlink -f \$HOME/.config/opencode/opencode.json 2>/dev/null || echo \$HOME/.config/opencode/opencode.json)
|
|
46
|
-
if [ -f \"\$CONFIG\" ]; then
|
|
47
|
-
jq --arg ref '$PLUGIN_REF' \\
|
|
48
|
-
'(.plugin // []) |= [.[] | if (test(\"opencode-legion-envoy\") and (test(\"^file://\") | not)) then \$ref else . end]' \\
|
|
49
|
-
\"\$CONFIG\" > /tmp/opencode.json.tmp && mv /tmp/opencode.json.tmp \"\$CONFIG\"
|
|
50
|
-
echo \"Updated opencode.json: \$CONFIG\"
|
|
51
|
-
else
|
|
52
|
-
echo \"WARNING: opencode.json not found at \$CONFIG — add plugin manually: $PLUGIN_REF\"
|
|
53
|
-
fi
|
|
54
|
-
"
|
|
55
|
-
|
|
56
|
-
# Update tui.json: ensure file:// ref is present in the TUI plugin list.
|
|
57
|
-
# tui.json is a separate config file from opencode.json; opencode loads TUI
|
|
58
|
-
# plugins (slash commands, sidebar slots) only from here.
|
|
59
|
-
ssh "$host" "
|
|
60
|
-
TUI_CONFIG=\$(readlink -f \$HOME/.config/opencode/tui.json 2>/dev/null || echo \$HOME/.config/opencode/tui.json)
|
|
61
|
-
if [ -f \"\$TUI_CONFIG\" ]; then
|
|
62
|
-
jq --arg ref '$PLUGIN_REF' \\
|
|
63
|
-
'(.plugin // []) |= ((map(if (test(\"opencode-legion-envoy\") and (test(\"^file://\") | not)) then \$ref else . end)) | if any(. == \$ref) then . else . + [\$ref] end)' \\
|
|
64
|
-
\"\$TUI_CONFIG\" > /tmp/tui.json.tmp && mv /tmp/tui.json.tmp \"\$TUI_CONFIG\"
|
|
65
|
-
echo \"Updated tui.json: \$TUI_CONFIG\"
|
|
66
|
-
else
|
|
67
|
-
mkdir -p \$(dirname \"\$TUI_CONFIG\")
|
|
68
|
-
jq -n --arg ref '$PLUGIN_REF' '{\"\\\$schema\":\"https://opencode.ai/tui.json\",\"plugin\":[\$ref]}' > \"\$TUI_CONFIG\"
|
|
69
|
-
echo \"Created tui.json: \$TUI_CONFIG\"
|
|
70
|
-
fi
|
|
71
|
-
"
|
|
72
|
-
|
|
73
|
-
echo "Done: $host envoy-plugin synced from release $tag"
|
|
@@ -1,56 +0,0 @@
|
|
|
1
|
-
import { describe, expect, it, mock } from "bun:test";
|
|
2
|
-
import { copyNative, copyToClipboard } from "../clipboard";
|
|
3
|
-
|
|
4
|
-
describe("copyToClipboard", () => {
|
|
5
|
-
it("copies via the renderer OSC 52 writer and does not fall back when it succeeds", () => {
|
|
6
|
-
const copyToClipboardOSC52 = mock((_text: string) => true);
|
|
7
|
-
const ok = copyToClipboard("ses_abc", { copyToClipboardOSC52 });
|
|
8
|
-
expect(ok).toBe(true);
|
|
9
|
-
expect(copyToClipboardOSC52).toHaveBeenCalledWith("ses_abc");
|
|
10
|
-
});
|
|
11
|
-
});
|
|
12
|
-
|
|
13
|
-
describe("copyNative", () => {
|
|
14
|
-
it("uses wl-copy on Linux/Wayland when available", () => {
|
|
15
|
-
const run = mock((_cmd: string, _args: string[], _text: string) => true);
|
|
16
|
-
const ok = copyNative("ses_abc", {
|
|
17
|
-
os: "linux",
|
|
18
|
-
env: { WAYLAND_DISPLAY: "wayland-0" },
|
|
19
|
-
which: (cmd) => cmd === "wl-copy",
|
|
20
|
-
run,
|
|
21
|
-
});
|
|
22
|
-
expect(ok).toBe(true);
|
|
23
|
-
expect(run).toHaveBeenCalledWith("wl-copy", [], "ses_abc");
|
|
24
|
-
});
|
|
25
|
-
|
|
26
|
-
it("falls back to xclip on Linux/X11", () => {
|
|
27
|
-
const run = mock((_cmd: string, _args: string[], _text: string) => true);
|
|
28
|
-
const ok = copyNative("ses_abc", {
|
|
29
|
-
os: "linux",
|
|
30
|
-
env: {},
|
|
31
|
-
which: (cmd) => cmd === "xclip",
|
|
32
|
-
run,
|
|
33
|
-
});
|
|
34
|
-
expect(ok).toBe(true);
|
|
35
|
-
expect(run).toHaveBeenCalledWith("xclip", ["-selection", "clipboard"], "ses_abc");
|
|
36
|
-
});
|
|
37
|
-
|
|
38
|
-
it("falls back to xsel when xclip is missing", () => {
|
|
39
|
-
const run = mock((_cmd: string, _args: string[], _text: string) => true);
|
|
40
|
-
copyNative("x", { os: "linux", env: {}, which: (cmd) => cmd === "xsel", run });
|
|
41
|
-
expect(run).toHaveBeenCalledWith("xsel", ["--clipboard", "--input"], "x");
|
|
42
|
-
});
|
|
43
|
-
|
|
44
|
-
it("uses osascript on macOS with escaped quotes", () => {
|
|
45
|
-
const run = mock((_cmd: string, _args: string[], _text: string) => true);
|
|
46
|
-
copyNative('a"b\\c', { os: "darwin", which: () => true, run });
|
|
47
|
-
expect(run).toHaveBeenCalledWith("osascript", ["-e", 'set the clipboard to "a\\"b\\\\c"'], "");
|
|
48
|
-
});
|
|
49
|
-
|
|
50
|
-
it("returns false on Linux when no clipboard tool is installed", () => {
|
|
51
|
-
const run = mock(() => true);
|
|
52
|
-
const ok = copyNative("x", { os: "linux", env: {}, which: () => false, run });
|
|
53
|
-
expect(ok).toBe(false);
|
|
54
|
-
expect(run).not.toHaveBeenCalled();
|
|
55
|
-
});
|
|
56
|
-
});
|