@wenrwa/marketplace-cli 0.8.1 → 0.8.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/__tests__/bridge-opencode-binary.test.d.ts +1 -0
- package/dist/__tests__/bridge-opencode-binary.test.js +128 -0
- package/dist/__tests__/bridge-opencode-launch.test.js +37 -0
- package/dist/bridge/opencodeBinary.d.ts +47 -0
- package/dist/bridge/opencodeBinary.js +161 -0
- package/dist/bridge/opencodeConfig.d.ts +2 -0
- package/dist/bridge/opencodeConfig.js +4 -2
- package/dist/bridge/relayClient.d.ts +7 -0
- package/dist/bridge/relayClient.js +11 -3
- package/dist/bridge/server.d.ts +9 -0
- package/dist/bridge/server.js +15 -5
- package/dist/commands/bridge.js +40 -0
- package/package.json +1 -1
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
/** oc-25 — the bridge provides OpenCode (resolution order, shared install, failure text). */
|
|
4
|
+
const opencodeBinary_1 = require("../bridge/opencodeBinary");
|
|
5
|
+
const opencodeConfig_1 = require("../bridge/opencodeConfig");
|
|
6
|
+
const BASE = '/h/.wen-market/opencode';
|
|
7
|
+
const PREFIX = (0, opencodeBinary_1.managedPrefix)(BASE);
|
|
8
|
+
const BIN = (0, opencodeBinary_1.managedBin)(PREFIX);
|
|
9
|
+
const launch = { command: 'opencode', args: ['-s', 'ses_a'], env: { ...opencodeConfig_1.OPENCODE_QUIET_ENV, X: '1' } };
|
|
10
|
+
function harness(over = {}) {
|
|
11
|
+
const files = new Map();
|
|
12
|
+
const npmCalls = [];
|
|
13
|
+
let release;
|
|
14
|
+
const gate = new Promise((r) => { release = r; });
|
|
15
|
+
const deps = {
|
|
16
|
+
exists: (p) => files.has(p),
|
|
17
|
+
writeFile: (p, c) => { files.set(p, c); },
|
|
18
|
+
mkdirp: () => { },
|
|
19
|
+
rm: (p) => { files.delete(p); },
|
|
20
|
+
runNpm: async (args, env) => {
|
|
21
|
+
npmCalls.push({ args, env });
|
|
22
|
+
await gate;
|
|
23
|
+
if ((over.npmCode ?? 0) === 0)
|
|
24
|
+
files.set(BIN, 'bin');
|
|
25
|
+
return { code: over.npmCode ?? 0, stderr: '' };
|
|
26
|
+
},
|
|
27
|
+
pathOpencode: () => null,
|
|
28
|
+
versionOf: () => opencodeConfig_1.OPENCODE_PINNED_VERSION,
|
|
29
|
+
env: { PATH: '/usr/bin', npm_config_offline: 'true' },
|
|
30
|
+
platform: 'darwin',
|
|
31
|
+
...over,
|
|
32
|
+
};
|
|
33
|
+
return { files, npmCalls, release, deps };
|
|
34
|
+
}
|
|
35
|
+
describe('oc-25 — pinned version lives in one place', () => {
|
|
36
|
+
it('the minimum IS the pinned version', () => {
|
|
37
|
+
expect(opencodeConfig_1.OPENCODE_MIN_VERSION).toBe(opencodeConfig_1.OPENCODE_PINNED_VERSION);
|
|
38
|
+
});
|
|
39
|
+
});
|
|
40
|
+
describe('oc-25 — resolution order', () => {
|
|
41
|
+
it('1. managed pinned binary wins over a newer PATH one; absolute path, no install', () => {
|
|
42
|
+
const h = harness({ pathOpencode: () => ({ path: '/usr/local/bin/opencode', version: '9.9.9' }) });
|
|
43
|
+
h.files.set(BIN, 'x');
|
|
44
|
+
h.files.set(`${PREFIX}/.ready`, '1');
|
|
45
|
+
const r = (0, opencodeBinary_1.createOpencodeProvisioner)(BASE, h.deps).prepare(launch);
|
|
46
|
+
expect(r).toEqual({ ok: true, launch: { ...launch, command: BIN } });
|
|
47
|
+
expect(h.npmCalls).toHaveLength(0);
|
|
48
|
+
});
|
|
49
|
+
it('2. PATH opencode at/above the minimum is used by absolute path', () => {
|
|
50
|
+
const h = harness({ pathOpencode: () => ({ path: '/opt/bin/opencode', version: '1.20.0' }) });
|
|
51
|
+
const r = (0, opencodeBinary_1.createOpencodeProvisioner)(BASE, h.deps).prepare(launch);
|
|
52
|
+
expect(r).toEqual({ ok: true, launch: { ...launch, command: '/opt/bin/opencode' } });
|
|
53
|
+
expect(h.npmCalls).toHaveLength(0);
|
|
54
|
+
});
|
|
55
|
+
it('3. a PATH opencode below the minimum is ignored and the pinned one is installed', async () => {
|
|
56
|
+
const h = harness({ pathOpencode: () => ({ path: '/opt/bin/opencode', version: '1.2.0' }) });
|
|
57
|
+
const p = (0, opencodeBinary_1.createOpencodeProvisioner)(BASE, h.deps);
|
|
58
|
+
const r = p.prepare(launch);
|
|
59
|
+
expect(h.npmCalls).toHaveLength(1);
|
|
60
|
+
expect(r.ok && r.launch.command).toBe('/bin/sh');
|
|
61
|
+
h.release();
|
|
62
|
+
await p.settled();
|
|
63
|
+
});
|
|
64
|
+
});
|
|
65
|
+
describe('oc-25 — install', () => {
|
|
66
|
+
it('installs the pinned version into the bridge prefix with npm_config_offline scrubbed', async () => {
|
|
67
|
+
const h = harness();
|
|
68
|
+
const p = (0, opencodeBinary_1.createOpencodeProvisioner)(BASE, h.deps);
|
|
69
|
+
const r = p.prepare(launch);
|
|
70
|
+
expect(h.npmCalls[0].args).toEqual(['install', '--prefix', PREFIX, `opencode-ai@${opencodeConfig_1.OPENCODE_PINNED_VERSION}`, '--no-audit', '--no-fund']);
|
|
71
|
+
expect(h.npmCalls[0].env.npm_config_offline).toBeUndefined();
|
|
72
|
+
expect(h.npmCalls[0].env.PATH).toBe('/usr/bin');
|
|
73
|
+
// the pane runs a waiter that execs the ABSOLUTE managed binary with the original args
|
|
74
|
+
expect(r.ok && r.launch.args).toEqual(['-c', opencodeBinary_1.OPENCODE_WAIT_SCRIPT, 'opencode-wait', `${PREFIX}/.ready`, `${PREFIX}/.install-failed`, BIN, '-s', 'ses_a']);
|
|
75
|
+
expect(r.ok && r.launch.env).toEqual(launch.env);
|
|
76
|
+
expect(h.files.has(`${PREFIX}/.ready`)).toBe(false);
|
|
77
|
+
h.release();
|
|
78
|
+
await p.settled();
|
|
79
|
+
expect(h.files.has(`${PREFIX}/.ready`)).toBe(true);
|
|
80
|
+
// and the next open uses it directly
|
|
81
|
+
expect(p.prepare(launch)).toEqual({ ok: true, launch: { ...launch, command: BIN } });
|
|
82
|
+
expect(h.npmCalls).toHaveLength(1);
|
|
83
|
+
});
|
|
84
|
+
it('two panes opening at once share ONE install', async () => {
|
|
85
|
+
const h = harness();
|
|
86
|
+
const p = (0, opencodeBinary_1.createOpencodeProvisioner)(BASE, h.deps);
|
|
87
|
+
p.prepare(launch);
|
|
88
|
+
p.prepare(launch);
|
|
89
|
+
expect(h.npmCalls).toHaveLength(1);
|
|
90
|
+
h.release();
|
|
91
|
+
await p.settled();
|
|
92
|
+
});
|
|
93
|
+
it('failure writes a short wrapped message with the manual command, no ready marker; a later open retries', async () => {
|
|
94
|
+
const h = harness({ npmCode: 1 });
|
|
95
|
+
const p = (0, opencodeBinary_1.createOpencodeProvisioner)(BASE, h.deps);
|
|
96
|
+
p.prepare(launch);
|
|
97
|
+
h.release();
|
|
98
|
+
await p.settled();
|
|
99
|
+
const msg = h.files.get(`${PREFIX}/.install-failed`);
|
|
100
|
+
expect(msg).toContain(`npm i -g opencode-ai@${opencodeConfig_1.OPENCODE_PINNED_VERSION}`);
|
|
101
|
+
for (const line of msg.split('\n'))
|
|
102
|
+
expect(line.length).toBeLessThanOrEqual(40);
|
|
103
|
+
expect(h.files.has(`${PREFIX}/.ready`)).toBe(false);
|
|
104
|
+
p.prepare(launch);
|
|
105
|
+
expect(h.npmCalls).toHaveLength(2);
|
|
106
|
+
expect(h.files.has(`${PREFIX}/.install-failed`)).toBe(false); // stale verdict cleared
|
|
107
|
+
h.release();
|
|
108
|
+
await p.settled();
|
|
109
|
+
});
|
|
110
|
+
it('a thrown npm spawn error is a failure, not a crash', async () => {
|
|
111
|
+
const h = harness({ runNpm: async () => { throw new Error('spawn npm ENOENT'); } });
|
|
112
|
+
const p = (0, opencodeBinary_1.createOpencodeProvisioner)(BASE, h.deps);
|
|
113
|
+
p.prepare(launch);
|
|
114
|
+
await p.settled();
|
|
115
|
+
expect(h.files.get(`${PREFIX}/.install-failed`)).toContain('spawn npm ENOENT');
|
|
116
|
+
});
|
|
117
|
+
it('refuses with the manual command on win32 (no sh waiter)', () => {
|
|
118
|
+
const h = harness({ platform: 'win32' });
|
|
119
|
+
const r = (0, opencodeBinary_1.createOpencodeProvisioner)(BASE, h.deps).prepare(launch);
|
|
120
|
+
expect(r.ok).toBe(false);
|
|
121
|
+
expect(!r.ok && r.message).toContain(`npm i -g opencode-ai@${opencodeConfig_1.OPENCODE_PINNED_VERSION}`);
|
|
122
|
+
expect(h.npmCalls).toHaveLength(0);
|
|
123
|
+
});
|
|
124
|
+
it('failure lines are short', () => {
|
|
125
|
+
for (const l of (0, opencodeBinary_1.opencodeInstallFailureLines)('x'.repeat(200)))
|
|
126
|
+
expect(l.length).toBeLessThanOrEqual(65);
|
|
127
|
+
});
|
|
128
|
+
});
|
|
@@ -177,3 +177,40 @@ describe('relay lane carries the server-delivered opencode block', () => {
|
|
|
177
177
|
await new Promise((r) => wss.close(r));
|
|
178
178
|
});
|
|
179
179
|
});
|
|
180
|
+
describe('oc-25 — relay lane spawns what the provisioner prepared', () => {
|
|
181
|
+
it('swaps in the absolute binary; a refusal becomes the typed error with its own message', async () => {
|
|
182
|
+
const { wss, port, ptys, factory } = await setup();
|
|
183
|
+
const frames = [];
|
|
184
|
+
let conn;
|
|
185
|
+
const got = new Promise((r) => wss.on('connection', (ws) => { conn = ws; ws.on('message', (m) => frames.push(JSON.parse(String(m)))); r(); }));
|
|
186
|
+
let refuse = false;
|
|
187
|
+
const handle = (0, relayClient_1.startRelayClient)({
|
|
188
|
+
relayUrl: `ws://127.0.0.1:${port}`, relayToken: 'tok',
|
|
189
|
+
sessions: new sessionManager_1.SessionManager(factory),
|
|
190
|
+
resolveLaunch: (o) => (0, agentLaunch_1.resolveAgentLaunch)({ autonomous: true, opencode: o.opencode, connectToken: o.connectToken }),
|
|
191
|
+
commandExists: () => true,
|
|
192
|
+
prepareOpencodeLaunch: (l) => refuse
|
|
193
|
+
? { ok: false, message: 'short refusal' }
|
|
194
|
+
: { ok: true, launch: { ...l, command: '/h/.wen-market/opencode/1.18.35/node_modules/.bin/opencode' } },
|
|
195
|
+
});
|
|
196
|
+
await got;
|
|
197
|
+
const wait = async (pred) => { for (let i = 0; i < 100; i++) {
|
|
198
|
+
const f = frames.find(pred);
|
|
199
|
+
if (f)
|
|
200
|
+
return f;
|
|
201
|
+
await new Promise((r) => setTimeout(r, 10));
|
|
202
|
+
} throw new Error('timeout'); };
|
|
203
|
+
conn.send(JSON.stringify({ t: 'open', cols: 80, rows: 24, sessionId: 'b-1', workspaceId: 'w', opencode: oc() }));
|
|
204
|
+
await wait((f) => f.t === 'opened');
|
|
205
|
+
expect(ptys[0].opts.command).toBe('/h/.wen-market/opencode/1.18.35/node_modules/.bin/opencode');
|
|
206
|
+
refuse = true;
|
|
207
|
+
conn.send(JSON.stringify({ t: 'open', cols: 80, rows: 24, sessionId: 'b-2', workspaceId: 'w', opencode: oc() }));
|
|
208
|
+
const err = await wait((f) => f.t === 'error' && f.sessionId === 'b-2');
|
|
209
|
+
expect(err.message).toBe('short refusal');
|
|
210
|
+
expect(ptys).toHaveLength(1);
|
|
211
|
+
handle.close();
|
|
212
|
+
for (const c of wss.clients)
|
|
213
|
+
c.terminate();
|
|
214
|
+
await new Promise((r) => wss.close(r));
|
|
215
|
+
});
|
|
216
|
+
});
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
export declare const OPENCODE_PACKAGE = "opencode-ai";
|
|
2
|
+
export declare const OPENCODE_INSTALL_TIMEOUT_MS: number;
|
|
3
|
+
export interface OpencodeBinaryDeps {
|
|
4
|
+
exists: (p: string) => boolean;
|
|
5
|
+
writeFile: (p: string, content: string) => void;
|
|
6
|
+
mkdirp: (dir: string) => void;
|
|
7
|
+
rm: (p: string) => void;
|
|
8
|
+
/** Run npm with exactly this env. Resolves { code } (never rejects on non-zero). */
|
|
9
|
+
runNpm: (args: string[], env: NodeJS.ProcessEnv) => Promise<{
|
|
10
|
+
code: number;
|
|
11
|
+
stderr?: string;
|
|
12
|
+
}>;
|
|
13
|
+
/** PATH lookup: absolute path + `--version` output of the `opencode` on PATH, or null. */
|
|
14
|
+
pathOpencode: () => {
|
|
15
|
+
path: string;
|
|
16
|
+
version: string | null;
|
|
17
|
+
} | null;
|
|
18
|
+
/** `<bin> --version` output, or null if it will not run. */
|
|
19
|
+
versionOf: (bin: string) => string | null;
|
|
20
|
+
env: NodeJS.ProcessEnv;
|
|
21
|
+
platform: NodeJS.Platform;
|
|
22
|
+
}
|
|
23
|
+
export interface LaunchShape {
|
|
24
|
+
command: string;
|
|
25
|
+
args: string[];
|
|
26
|
+
env: Record<string, string>;
|
|
27
|
+
}
|
|
28
|
+
export type PreparedLaunch = {
|
|
29
|
+
ok: true;
|
|
30
|
+
launch: LaunchShape;
|
|
31
|
+
} | {
|
|
32
|
+
ok: false;
|
|
33
|
+
message: string;
|
|
34
|
+
};
|
|
35
|
+
export declare function managedPrefix(baseDir: string, version?: string): string;
|
|
36
|
+
export declare const managedBin: (prefix: string) => string;
|
|
37
|
+
/** Short lines on purpose: a narrow pane wraps long ones badly. */
|
|
38
|
+
export declare function opencodeInstallFailureLines(reason: string): string[];
|
|
39
|
+
/** POSIX sh waiter: progress now, then exec once the bridge marks the install ready (or print the failure). */
|
|
40
|
+
export declare const OPENCODE_WAIT_SCRIPT: string;
|
|
41
|
+
export declare function createOpencodeProvisioner(baseDir: string, deps: OpencodeBinaryDeps): {
|
|
42
|
+
managedBin: string;
|
|
43
|
+
/** Resolve (or start provisioning) and return the launch to spawn. Synchronous. */
|
|
44
|
+
prepare(launch: LaunchShape): PreparedLaunch;
|
|
45
|
+
/** Test seam: resolves when the in-flight install (if any) settles. */
|
|
46
|
+
settled: () => Promise<void>;
|
|
47
|
+
};
|
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
+
}) : function(o, v) {
|
|
16
|
+
o["default"] = v;
|
|
17
|
+
});
|
|
18
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
19
|
+
var ownKeys = function(o) {
|
|
20
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
21
|
+
var ar = [];
|
|
22
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
23
|
+
return ar;
|
|
24
|
+
};
|
|
25
|
+
return ownKeys(o);
|
|
26
|
+
};
|
|
27
|
+
return function (mod) {
|
|
28
|
+
if (mod && mod.__esModule) return mod;
|
|
29
|
+
var result = {};
|
|
30
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
31
|
+
__setModuleDefault(result, mod);
|
|
32
|
+
return result;
|
|
33
|
+
};
|
|
34
|
+
})();
|
|
35
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
|
+
exports.OPENCODE_WAIT_SCRIPT = exports.managedBin = exports.OPENCODE_INSTALL_TIMEOUT_MS = exports.OPENCODE_PACKAGE = void 0;
|
|
37
|
+
exports.managedPrefix = managedPrefix;
|
|
38
|
+
exports.opencodeInstallFailureLines = opencodeInstallFailureLines;
|
|
39
|
+
exports.createOpencodeProvisioner = createOpencodeProvisioner;
|
|
40
|
+
/**
|
|
41
|
+
* oc-25 — the bridge provides OpenCode itself (ADR-0013 local-lane parity).
|
|
42
|
+
*
|
|
43
|
+
* A user must never install OpenCode by hand. Binary choice, in order:
|
|
44
|
+
* 1. the bridge-managed pinned copy (~/.wen-market/opencode/<pinned>), if there;
|
|
45
|
+
* 2. else a PATH `opencode` at or above the minimum;
|
|
46
|
+
* 3. else install the pinned version into the managed prefix (lazy: only on
|
|
47
|
+
* the first OpenCode launch, so Claude-only users never download it).
|
|
48
|
+
*
|
|
49
|
+
* The launch always carries an ABSOLUTE binary path. While an install runs the
|
|
50
|
+
* PTY runs a tiny `sh` waiter (progress line, then exec) — the pane shows
|
|
51
|
+
* "Setting up OpenCode" instead of a blank wait or a bare shell. Concurrent
|
|
52
|
+
* opens share ONE install (in-flight promise). The install must NOT inherit
|
|
53
|
+
* OPENCODE_QUIET_ENV's npm_config_offline=true, so it runs with a scrubbed env.
|
|
54
|
+
*/
|
|
55
|
+
const path = __importStar(require("path"));
|
|
56
|
+
const opencodeConfig_1 = require("./opencodeConfig");
|
|
57
|
+
exports.OPENCODE_PACKAGE = 'opencode-ai';
|
|
58
|
+
exports.OPENCODE_INSTALL_TIMEOUT_MS = 5 * 60 * 1000;
|
|
59
|
+
function managedPrefix(baseDir, version = opencodeConfig_1.OPENCODE_PINNED_VERSION) {
|
|
60
|
+
return path.join(baseDir, version);
|
|
61
|
+
}
|
|
62
|
+
const managedBin = (prefix) => path.join(prefix, 'node_modules', '.bin', 'opencode');
|
|
63
|
+
exports.managedBin = managedBin;
|
|
64
|
+
const readyMarker = (prefix) => path.join(prefix, '.ready');
|
|
65
|
+
const failMarker = (prefix) => path.join(prefix, '.install-failed');
|
|
66
|
+
/** Short lines on purpose: a narrow pane wraps long ones badly. */
|
|
67
|
+
function opencodeInstallFailureLines(reason) {
|
|
68
|
+
const why = reason.replace(/\s+/g, ' ').trim().slice(0, 60);
|
|
69
|
+
return [
|
|
70
|
+
'Could not set up OpenCode.',
|
|
71
|
+
...(why ? [why] : []),
|
|
72
|
+
'Install it by hand:',
|
|
73
|
+
`npm i -g ${exports.OPENCODE_PACKAGE}@${opencodeConfig_1.OPENCODE_PINNED_VERSION}`,
|
|
74
|
+
'Then reopen this pane.',
|
|
75
|
+
];
|
|
76
|
+
}
|
|
77
|
+
/** POSIX sh waiter: progress now, then exec once the bridge marks the install ready (or print the failure). */
|
|
78
|
+
exports.OPENCODE_WAIT_SCRIPT = [
|
|
79
|
+
'ready=$1; fail=$2; shift 2',
|
|
80
|
+
"printf '\\r\\n\\033[2mSetting up OpenCode (one-time, ~30s)...\\033[0m\\r\\n'",
|
|
81
|
+
'while [ ! -e "$ready" ]; do',
|
|
82
|
+
' if [ -e "$fail" ]; then',
|
|
83
|
+
" printf '\\r\\n'",
|
|
84
|
+
" while IFS= read -r l; do printf '\\033[31m%s\\033[0m\\r\\n' \"$l\"; done < \"$fail\"",
|
|
85
|
+
' exit 1',
|
|
86
|
+
' fi',
|
|
87
|
+
' sleep 1',
|
|
88
|
+
'done',
|
|
89
|
+
'exec "$@"',
|
|
90
|
+
].join('\n');
|
|
91
|
+
function createOpencodeProvisioner(baseDir, deps) {
|
|
92
|
+
const prefix = managedPrefix(baseDir);
|
|
93
|
+
const bin = (0, exports.managedBin)(prefix);
|
|
94
|
+
let inflight = null;
|
|
95
|
+
function install() {
|
|
96
|
+
if (inflight)
|
|
97
|
+
return inflight;
|
|
98
|
+
// Clear the previous verdict synchronously so a waiter spawned right after never reads a stale one.
|
|
99
|
+
try {
|
|
100
|
+
deps.rm(failMarker(prefix));
|
|
101
|
+
}
|
|
102
|
+
catch { /* none */ }
|
|
103
|
+
const env = { ...deps.env };
|
|
104
|
+
for (const k of Object.keys(env))
|
|
105
|
+
if (k.toLowerCase() === 'npm_config_offline')
|
|
106
|
+
delete env[k];
|
|
107
|
+
const fail = (reason) => {
|
|
108
|
+
try {
|
|
109
|
+
deps.writeFile(failMarker(prefix), opencodeInstallFailureLines(reason).join('\n') + '\n');
|
|
110
|
+
}
|
|
111
|
+
catch { /* best effort */ }
|
|
112
|
+
};
|
|
113
|
+
const run = (async () => {
|
|
114
|
+
try {
|
|
115
|
+
deps.mkdirp(prefix);
|
|
116
|
+
const pkg = path.join(prefix, 'package.json');
|
|
117
|
+
if (!deps.exists(pkg))
|
|
118
|
+
deps.writeFile(pkg, JSON.stringify({ name: 'wen-market-opencode', private: true }) + '\n');
|
|
119
|
+
const res = await deps.runNpm(['install', '--prefix', prefix, `${exports.OPENCODE_PACKAGE}@${opencodeConfig_1.OPENCODE_PINNED_VERSION}`, '--no-audit', '--no-fund'], env);
|
|
120
|
+
if (res.code !== 0)
|
|
121
|
+
return fail(`npm exited ${res.code}`);
|
|
122
|
+
if (!deps.exists(bin) || !(0, opencodeConfig_1.opencodeVersionOk)(deps.versionOf(bin)))
|
|
123
|
+
return fail('installed binary will not run');
|
|
124
|
+
deps.writeFile(readyMarker(prefix), opencodeConfig_1.OPENCODE_PINNED_VERSION);
|
|
125
|
+
}
|
|
126
|
+
catch (err) {
|
|
127
|
+
fail(err instanceof Error ? err.message : String(err));
|
|
128
|
+
}
|
|
129
|
+
})();
|
|
130
|
+
inflight = run;
|
|
131
|
+
void run.then(() => { if (inflight === run)
|
|
132
|
+
inflight = null; });
|
|
133
|
+
return run;
|
|
134
|
+
}
|
|
135
|
+
return {
|
|
136
|
+
managedBin: bin,
|
|
137
|
+
/** Resolve (or start provisioning) and return the launch to spawn. Synchronous. */
|
|
138
|
+
prepare(launch) {
|
|
139
|
+
const withBin = (b) => ({ ...launch, command: b });
|
|
140
|
+
if (deps.exists(bin) && deps.exists(readyMarker(prefix)))
|
|
141
|
+
return { ok: true, launch: withBin(bin) };
|
|
142
|
+
const onPath = deps.pathOpencode();
|
|
143
|
+
if (onPath && (0, opencodeConfig_1.opencodeVersionOk)(onPath.version))
|
|
144
|
+
return { ok: true, launch: withBin(onPath.path) };
|
|
145
|
+
if (deps.platform === 'win32') {
|
|
146
|
+
return { ok: false, message: opencodeInstallFailureLines('automatic setup is not on Windows yet').join('\r\n') };
|
|
147
|
+
}
|
|
148
|
+
void install();
|
|
149
|
+
return {
|
|
150
|
+
ok: true,
|
|
151
|
+
launch: {
|
|
152
|
+
...launch,
|
|
153
|
+
command: '/bin/sh',
|
|
154
|
+
args: ['-c', exports.OPENCODE_WAIT_SCRIPT, 'opencode-wait', readyMarker(prefix), failMarker(prefix), bin, ...launch.args],
|
|
155
|
+
},
|
|
156
|
+
};
|
|
157
|
+
},
|
|
158
|
+
/** Test seam: resolves when the in-flight install (if any) settles. */
|
|
159
|
+
settled: () => inflight ?? Promise.resolve(),
|
|
160
|
+
};
|
|
161
|
+
}
|
|
@@ -10,6 +10,8 @@
|
|
|
10
10
|
*/
|
|
11
11
|
/** Oldest OpenCode the local lane accepts. 1.18.35 is the version oc-04 verified
|
|
12
12
|
* (`permission.asked` / `session.idle` events, `-s <id>` resume, quiet env). */
|
|
13
|
+
export declare const OPENCODE_PINNED_VERSION = "1.18.35";
|
|
14
|
+
/** oc-25: the version the bridge installs for itself IS the minimum it accepts from PATH. */
|
|
13
15
|
export declare const OPENCODE_MIN_VERSION = "1.18.35";
|
|
14
16
|
export declare const OPENCODE_INSTALL_HINT = "Install OpenCode: npm i -g opencode-ai@>=1.18.35";
|
|
15
17
|
/** Every outbound call OpenCode makes at startup besides the gateway (oc-04 s5). */
|
|
@@ -10,12 +10,14 @@
|
|
|
10
10
|
* (`{env:HIVEOP_GATEWAY_TOKEN}`) so it never appears inside the JSON.
|
|
11
11
|
*/
|
|
12
12
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
13
|
-
exports.isCapturedOpencodeSessionId = exports.OPENCODE_GATEWAY_TOKEN_ENV = exports.OPENCODE_PROVIDER_ID = exports.OPENCODE_QUIET_ENV = exports.OPENCODE_INSTALL_HINT = exports.OPENCODE_MIN_VERSION = void 0;
|
|
13
|
+
exports.isCapturedOpencodeSessionId = exports.OPENCODE_GATEWAY_TOKEN_ENV = exports.OPENCODE_PROVIDER_ID = exports.OPENCODE_QUIET_ENV = exports.OPENCODE_INSTALL_HINT = exports.OPENCODE_MIN_VERSION = exports.OPENCODE_PINNED_VERSION = void 0;
|
|
14
14
|
exports.buildOpencodeConfig = buildOpencodeConfig;
|
|
15
15
|
exports.opencodeVersionOk = opencodeVersionOk;
|
|
16
16
|
/** Oldest OpenCode the local lane accepts. 1.18.35 is the version oc-04 verified
|
|
17
17
|
* (`permission.asked` / `session.idle` events, `-s <id>` resume, quiet env). */
|
|
18
|
-
exports.
|
|
18
|
+
exports.OPENCODE_PINNED_VERSION = '1.18.35';
|
|
19
|
+
/** oc-25: the version the bridge installs for itself IS the minimum it accepts from PATH. */
|
|
20
|
+
exports.OPENCODE_MIN_VERSION = exports.OPENCODE_PINNED_VERSION;
|
|
19
21
|
exports.OPENCODE_INSTALL_HINT = `Install OpenCode: npm i -g opencode-ai@>=${exports.OPENCODE_MIN_VERSION}`;
|
|
20
22
|
/** Every outbound call OpenCode makes at startup besides the gateway (oc-04 s5). */
|
|
21
23
|
exports.OPENCODE_QUIET_ENV = Object.freeze({
|
|
@@ -17,6 +17,7 @@
|
|
|
17
17
|
* `replay`, exactly as it would against the local server.
|
|
18
18
|
*/
|
|
19
19
|
import { type OpencodeLaunchInput } from './agentLaunch';
|
|
20
|
+
import type { PreparedLaunch } from './opencodeBinary';
|
|
20
21
|
import { SessionManager } from './sessionManager';
|
|
21
22
|
import { MemInfo } from './containerMemory';
|
|
22
23
|
import { BrowserDriver } from './browserDriver';
|
|
@@ -200,6 +201,12 @@ export interface RelayClientOpts {
|
|
|
200
201
|
* error frame the conductor can report.
|
|
201
202
|
*/
|
|
202
203
|
commandExists?: (command: string) => boolean;
|
|
204
|
+
/** oc-25 — see BridgeServerOpts#prepareOpencodeLaunch. */
|
|
205
|
+
prepareOpencodeLaunch?: (launch: {
|
|
206
|
+
command: string;
|
|
207
|
+
args: string[];
|
|
208
|
+
env: Record<string, string>;
|
|
209
|
+
}) => PreparedLaunch;
|
|
203
210
|
/**
|
|
204
211
|
* fcct-27 — RELAY-OWNED SESSIONS ONLY. The consent boundary, made structural.
|
|
205
212
|
*
|
|
@@ -1060,9 +1060,17 @@ function startRelayClient(opts) {
|
|
|
1060
1060
|
// no override, so realPty spawns $SHELL and autolaunch.sh turns it
|
|
1061
1061
|
// into claude. Present (laptop) → `claude` explicitly, since
|
|
1062
1062
|
// nothing on a user's machine does that for us.
|
|
1063
|
-
|
|
1064
|
-
|
|
1065
|
-
|
|
1063
|
+
let launch = opts.resolveLaunch?.({ workspaceId, sessionId: relaySessionId, autonomous, opencode, connectToken }) ?? null;
|
|
1064
|
+
let refusal = null;
|
|
1065
|
+
if (launch && launch.command === 'opencode' && opts.prepareOpencodeLaunch) {
|
|
1066
|
+
const prepared = opts.prepareOpencodeLaunch({ ...launch, env: launch.env ?? {} });
|
|
1067
|
+
if (prepared.ok)
|
|
1068
|
+
launch = prepared.launch;
|
|
1069
|
+
else
|
|
1070
|
+
refusal = prepared.message;
|
|
1071
|
+
}
|
|
1072
|
+
if (launch && (refusal || (opts.commandExists && !opts.commandExists(launch.command)))) {
|
|
1073
|
+
const message = refusal ?? (0, agentLaunch_1.missingCommandMessage)(launch.command);
|
|
1066
1074
|
console.error(`[bridge] refusing PTY spawn — ${message}`);
|
|
1067
1075
|
send(socket, {
|
|
1068
1076
|
t: 'error',
|
package/dist/bridge/server.d.ts
CHANGED
|
@@ -2,6 +2,7 @@ import { SessionManager } from './sessionManager';
|
|
|
2
2
|
import { ValidatePathResult } from './localPathValidation';
|
|
3
3
|
import { PickFolderResult } from './folderPicker';
|
|
4
4
|
import { LocalAddCoreResult } from './localAdd';
|
|
5
|
+
import type { PreparedLaunch } from './opencodeBinary';
|
|
5
6
|
import { VerifyOriginResult } from './originVerification';
|
|
6
7
|
import { PublishResult } from './localPublish';
|
|
7
8
|
import { PublishPreflightResult } from './publishPreflight';
|
|
@@ -108,6 +109,14 @@ export interface BridgeServerOpts {
|
|
|
108
109
|
* crashes the open — see the 'open' handler below.
|
|
109
110
|
*/
|
|
110
111
|
commandExists?: (command: string) => boolean;
|
|
112
|
+
/** oc-25 — resolves/provisions OpenCode for an `opencode` launch: returns the
|
|
113
|
+
* launch to spawn (absolute binary, or a progress waiter while the bridge
|
|
114
|
+
* installs the pinned copy) or a refusal message. Absent → bare `opencode`. */
|
|
115
|
+
prepareOpencodeLaunch?: (launch: {
|
|
116
|
+
command: string;
|
|
117
|
+
args: string[];
|
|
118
|
+
env: Record<string, string>;
|
|
119
|
+
}) => PreparedLaunch;
|
|
111
120
|
/**
|
|
112
121
|
* psp-01 (production bug, 2026-09-14; supersedes bres-01's `hasSessionHistory`)
|
|
113
122
|
* — does Claude Code already have a conversation on disk for THIS AGENT at
|
package/dist/bridge/server.js
CHANGED
|
@@ -733,7 +733,7 @@ function startBridgeServer(opts) {
|
|
|
733
733
|
// folder still falls back to $HOME for its PTY cwd, but the
|
|
734
734
|
// workgraph MCP pin should still match whatever workspace the
|
|
735
735
|
// arena client actually asked to open.
|
|
736
|
-
|
|
736
|
+
let launch = (0, agentLaunch_1.resolveAgentLaunch)({
|
|
737
737
|
autonomous: !!frame.autonomous,
|
|
738
738
|
...(frame.opencode ? { opencode: frame.opencode } : {}),
|
|
739
739
|
...(frame.connectToken ? { connectToken: frame.connectToken } : {}),
|
|
@@ -745,7 +745,15 @@ function startBridgeServer(opts) {
|
|
|
745
745
|
: {}),
|
|
746
746
|
...(frame.workspaceId ? { workspaceId: frame.workspaceId } : {}),
|
|
747
747
|
});
|
|
748
|
-
|
|
748
|
+
let refusal = null;
|
|
749
|
+
if (launch.command === 'opencode' && opts.prepareOpencodeLaunch) {
|
|
750
|
+
const prepared = opts.prepareOpencodeLaunch(launch);
|
|
751
|
+
if (prepared.ok)
|
|
752
|
+
launch = prepared.launch;
|
|
753
|
+
else
|
|
754
|
+
refusal = prepared.message;
|
|
755
|
+
}
|
|
756
|
+
const claudeAvailable = !refusal && (opts.commandExists ? opts.commandExists(launch.command) : true);
|
|
749
757
|
if (!claudeAvailable) {
|
|
750
758
|
// Never crash on a missing binary: open a plain session (no
|
|
751
759
|
// command override — realPty's $SHELL default) so the pane
|
|
@@ -771,9 +779,11 @@ function startBridgeServer(opts) {
|
|
|
771
779
|
send(ws, {
|
|
772
780
|
t: 'data',
|
|
773
781
|
sessionId,
|
|
774
|
-
data:
|
|
775
|
-
? `\r\n\x1b[31m${
|
|
776
|
-
:
|
|
782
|
+
data: refusal
|
|
783
|
+
? `\r\n\x1b[31m${refusal}\x1b[0m\r\n`
|
|
784
|
+
: launch.command === 'opencode'
|
|
785
|
+
? `\r\n\x1b[31m${(0, agentLaunch_1.missingCommandMessage)('opencode')}, then reopen this pane.\x1b[0m\r\n`
|
|
786
|
+
: '\r\n\x1b[31mclaude not found on PATH — install Claude Code CLI, then reopen this pane.\x1b[0m\r\n',
|
|
777
787
|
});
|
|
778
788
|
}).catch((err) => {
|
|
779
789
|
console.error('[bridge] session attach failed:', err);
|
package/dist/commands/bridge.js
CHANGED
|
@@ -70,6 +70,7 @@ const realPty_1 = require("../bridge/realPty");
|
|
|
70
70
|
const config_1 = require("../bridge/config");
|
|
71
71
|
const mcpRegister_1 = require("../bridge/mcpRegister");
|
|
72
72
|
const mcpInstall_1 = require("../bridge/mcpInstall");
|
|
73
|
+
const opencodeBinary_1 = require("../bridge/opencodeBinary");
|
|
73
74
|
const autostart_1 = require("../bridge/autostart");
|
|
74
75
|
const repoCheckout_1 = require("../bridge/repoCheckout");
|
|
75
76
|
const bundleInstaller_1 = require("../bridge/bundleInstaller");
|
|
@@ -192,6 +193,43 @@ function opencodeUsable() {
|
|
|
192
193
|
* pane). One implementation so the two lanes can never disagree about whether
|
|
193
194
|
* this machine can run Claude Code.
|
|
194
195
|
*/
|
|
196
|
+
/**
|
|
197
|
+
* oc-25 — ONE provisioner for the process, so the loopback lane and the crew
|
|
198
|
+
* relay lane (and every pane) share a single in-flight OpenCode install.
|
|
199
|
+
*/
|
|
200
|
+
const opencodeProvisioner = (0, opencodeBinary_1.createOpencodeProvisioner)(path.join(os.homedir(), '.wen-market', 'opencode'), {
|
|
201
|
+
exists: (p) => fs.existsSync(p),
|
|
202
|
+
writeFile: (p, c) => fs.writeFileSync(p, c),
|
|
203
|
+
mkdirp: (d) => fs.mkdirSync(d, { recursive: true }),
|
|
204
|
+
rm: (p) => fs.rmSync(p, { force: true }),
|
|
205
|
+
runNpm: (args, env) => new Promise((resolve) => {
|
|
206
|
+
(0, child_process_1.execFile)(process.platform === 'win32' ? 'npm.cmd' : 'npm', args, { env, timeout: opencodeBinary_1.OPENCODE_INSTALL_TIMEOUT_MS }, (err, _out, stderr) => {
|
|
207
|
+
resolve({ code: err ? (typeof err.code === 'number' ? err.code : 1) : 0, stderr: String(stderr ?? '') });
|
|
208
|
+
});
|
|
209
|
+
}),
|
|
210
|
+
pathOpencode: () => {
|
|
211
|
+
try {
|
|
212
|
+
const abs = (0, child_process_1.execFileSync)(process.platform === 'win32' ? 'where' : 'command', process.platform === 'win32' ? ['opencode'] : ['-v', 'opencode'], { encoding: 'utf8', stdio: ['ignore', 'pipe', 'ignore'], shell: process.platform !== 'win32' }).split(/\r?\n/)[0].trim();
|
|
213
|
+
if (!abs)
|
|
214
|
+
return null;
|
|
215
|
+
return { path: abs, version: opencodeVersionOf(abs) };
|
|
216
|
+
}
|
|
217
|
+
catch {
|
|
218
|
+
return null;
|
|
219
|
+
}
|
|
220
|
+
},
|
|
221
|
+
versionOf: (bin) => opencodeVersionOf(bin),
|
|
222
|
+
env: process.env,
|
|
223
|
+
platform: process.platform,
|
|
224
|
+
});
|
|
225
|
+
function opencodeVersionOf(bin) {
|
|
226
|
+
try {
|
|
227
|
+
return (0, child_process_1.execFileSync)(bin, ['--version'], { encoding: 'utf8', stdio: ['ignore', 'pipe', 'ignore'], timeout: 15000 });
|
|
228
|
+
}
|
|
229
|
+
catch {
|
|
230
|
+
return null;
|
|
231
|
+
}
|
|
232
|
+
}
|
|
195
233
|
function commandExistsOnPath(command) {
|
|
196
234
|
if (command === 'opencode')
|
|
197
235
|
return opencodeUsable();
|
|
@@ -1210,6 +1248,7 @@ exports.bridgeCommand = new commander_1.Command('bridge')
|
|
|
1210
1248
|
// agent's PTY, so a missing install surfaces a clear in-pane error
|
|
1211
1249
|
// line (server.ts) instead of a raw spawn ENOENT crash.
|
|
1212
1250
|
commandExists: (command) => commandExistsOnPath(command),
|
|
1251
|
+
prepareOpencodeLaunch: (l) => opencodeProvisioner.prepare(l),
|
|
1213
1252
|
// psp-01 — real on-disk check: does THIS AGENT already have a Claude
|
|
1214
1253
|
// Code conversation for this cwd (a "<agentId>.jsonl" transcript)?
|
|
1215
1254
|
// Drives server.ts's per-agent session-pin decision on the
|
|
@@ -1324,6 +1363,7 @@ exports.bridgeCommand = new commander_1.Command('bridge')
|
|
|
1324
1363
|
// A missing binary is refused rather than degraded to a shell — a crew
|
|
1325
1364
|
// member has no pane to print a red line into.
|
|
1326
1365
|
commandExists: (command) => commandExistsOnPath(command),
|
|
1366
|
+
prepareOpencodeLaunch: (l) => opencodeProvisioner.prepare(l),
|
|
1327
1367
|
// fcct-27 — this is what keeps "crews may run here" from becoming "the
|
|
1328
1368
|
// platform may drive any terminal on this machine". Both lanes share one
|
|
1329
1369
|
// SessionManager by necessity, so without it the relay could attach to,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@wenrwa/marketplace-cli",
|
|
3
|
-
"version": "0.8.
|
|
3
|
+
"version": "0.8.2",
|
|
4
4
|
"description": "CLI tool for the Wenrwa Agent Marketplace — monitor bounties, agents, workspaces, and treasury from your terminal",
|
|
5
5
|
"author": "Wenrwa <dev@wenrwa.com>",
|
|
6
6
|
"license": "MIT",
|