@gricha/perry 0.3.4 → 0.3.5

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/index.js CHANGED
@@ -662,9 +662,24 @@ sshCmd
662
662
  program
663
663
  .command('update')
664
664
  .description('Update Perry to the latest version')
665
- .action(async () => {
665
+ .option('-f, --force', 'Force update even if already on latest version')
666
+ .action(async (options) => {
667
+ const { fetchLatestVersion, compareVersions } = await import('./update-checker.js');
668
+ const currentVersion = pkg.version;
669
+ console.log(`Current version: ${currentVersion}`);
670
+ console.log('Checking for updates...');
671
+ const latestVersion = await fetchLatestVersion();
672
+ if (!latestVersion) {
673
+ console.error('Failed to fetch latest version. Please try again later.');
674
+ process.exit(1);
675
+ }
676
+ console.log(`Latest version: ${latestVersion}`);
677
+ if (compareVersions(currentVersion, latestVersion) <= 0 && !options.force) {
678
+ console.log('Already up to date.');
679
+ process.exit(0);
680
+ }
681
+ console.log(`Updating Perry from ${currentVersion} to ${latestVersion}...`);
666
682
  const { spawn } = await import('child_process');
667
- console.log('Updating Perry...');
668
683
  const child = spawn('bash', ['-c', 'curl -fsSL https://raw.githubusercontent.com/gricha/perry/main/install.sh | bash'], {
669
684
  stdio: 'inherit',
670
685
  });
package/dist/perry-worker CHANGED
Binary file
@@ -3,6 +3,9 @@ const MESSAGE_TIMEOUT_MS = 30000;
3
3
  const SSE_TIMEOUT_MS = 120000;
4
4
  const serverPorts = new Map();
5
5
  const serverStarting = new Map();
6
+ let hostServerPort = null;
7
+ let hostServerStarting = null;
8
+ let hostServerProcess = null;
6
9
  async function findAvailablePort(containerName) {
7
10
  const script = `import socket; s=socket.socket(); s.bind(('', 0)); print(s.getsockname()[1]); s.close()`;
8
11
  const result = await execInContainer(containerName, ['python3', '-c', script], {
@@ -70,6 +73,7 @@ export class OpenCodeAdapter {
70
73
  model;
71
74
  status = 'idle';
72
75
  port;
76
+ isHost = false;
73
77
  sseProcess = null;
74
78
  messageCallback;
75
79
  statusCallback;
@@ -84,14 +88,17 @@ export class OpenCodeAdapter {
84
88
  this.errorCallback = callback;
85
89
  }
86
90
  async start(options) {
87
- if (options.isHost) {
88
- throw new Error('OpenCode adapter does not support host mode');
89
- }
91
+ this.isHost = options.isHost;
90
92
  this.containerName = options.containerName;
91
93
  this.agentSessionId = options.agentSessionId;
92
94
  this.model = options.model;
93
95
  try {
94
- this.port = await startServer(this.containerName);
96
+ if (this.isHost) {
97
+ this.port = await this.startServerHost();
98
+ }
99
+ else {
100
+ this.port = await startServer(this.containerName);
101
+ }
95
102
  this.setStatus('idle');
96
103
  }
97
104
  catch (err) {
@@ -99,8 +106,61 @@ export class OpenCodeAdapter {
99
106
  throw err;
100
107
  }
101
108
  }
109
+ async startServerHost() {
110
+ if (hostServerPort && (await this.isServerRunningHost(hostServerPort))) {
111
+ return hostServerPort;
112
+ }
113
+ if (hostServerStarting) {
114
+ return hostServerStarting;
115
+ }
116
+ const startPromise = (async () => {
117
+ const port = await this.findAvailablePortHost();
118
+ console.log(`[opencode] Starting server on port ${port} on host`);
119
+ hostServerProcess = Bun.spawn(['opencode', 'serve', '--port', String(port), '--hostname', '127.0.0.1'], {
120
+ stdin: 'ignore',
121
+ stdout: 'pipe',
122
+ stderr: 'pipe',
123
+ });
124
+ for (let i = 0; i < 30; i++) {
125
+ await new Promise((resolve) => setTimeout(resolve, 500));
126
+ if (await this.isServerRunningHost(port)) {
127
+ console.log(`[opencode] Server ready on port ${port}`);
128
+ hostServerPort = port;
129
+ hostServerStarting = null;
130
+ return port;
131
+ }
132
+ }
133
+ hostServerStarting = null;
134
+ if (hostServerProcess) {
135
+ hostServerProcess.kill();
136
+ await hostServerProcess.exited;
137
+ hostServerProcess = null;
138
+ }
139
+ throw new Error('Failed to start OpenCode server on host');
140
+ })();
141
+ hostServerStarting = startPromise;
142
+ return startPromise;
143
+ }
144
+ async findAvailablePortHost() {
145
+ const server = Bun.serve({
146
+ port: 0,
147
+ fetch: () => new Response(''),
148
+ });
149
+ const port = server.port;
150
+ server.stop();
151
+ return port;
152
+ }
153
+ async isServerRunningHost(port) {
154
+ try {
155
+ const response = await fetch(`http://localhost:${port}/session`, { method: 'GET' });
156
+ return response.ok;
157
+ }
158
+ catch {
159
+ return false;
160
+ }
161
+ }
102
162
  async sendMessage(message) {
103
- if (!this.containerName || !this.port) {
163
+ if (!this.port) {
104
164
  const err = new Error('Adapter not started');
105
165
  this.emitError(err);
106
166
  throw err;
@@ -131,7 +191,20 @@ export class OpenCodeAdapter {
131
191
  }
132
192
  }
133
193
  async createSession(baseUrl) {
134
- const payload = this.model ? JSON.stringify({ model: this.model }) : '{}';
194
+ const payload = this.model ? { model: this.model } : {};
195
+ if (this.isHost) {
196
+ const response = await fetch(`${baseUrl}/session`, {
197
+ method: 'POST',
198
+ headers: { 'Content-Type': 'application/json' },
199
+ body: JSON.stringify(payload),
200
+ signal: AbortSignal.timeout(MESSAGE_TIMEOUT_MS),
201
+ });
202
+ if (!response.ok) {
203
+ throw new Error(`Failed to create session: ${response.statusText}`);
204
+ }
205
+ const session = await response.json();
206
+ return session.id;
207
+ }
135
208
  const result = await execInContainer(this.containerName, [
136
209
  'curl',
137
210
  '-s',
@@ -144,7 +217,7 @@ export class OpenCodeAdapter {
144
217
  '-H',
145
218
  'Content-Type: application/json',
146
219
  '-d',
147
- payload,
220
+ JSON.stringify(payload),
148
221
  ], { user: 'workspace' });
149
222
  if (result.exitCode !== 0) {
150
223
  throw new Error(`Failed to create session: ${result.stderr || 'Unknown error'}`);
@@ -155,23 +228,36 @@ export class OpenCodeAdapter {
155
228
  async sendAndStream(baseUrl, message) {
156
229
  const sseReady = this.startSSEStream();
157
230
  await new Promise((resolve) => setTimeout(resolve, 100));
158
- const payload = JSON.stringify({ parts: [{ type: 'text', text: message }] });
159
- const result = await execInContainer(this.containerName, [
160
- 'curl',
161
- '-s',
162
- '-f',
163
- '--max-time',
164
- String(MESSAGE_TIMEOUT_MS / 1000),
165
- '-X',
166
- 'POST',
167
- `${baseUrl}/session/${this.agentSessionId}/message`,
168
- '-H',
169
- 'Content-Type: application/json',
170
- '-d',
171
- payload,
172
- ], { user: 'workspace' });
173
- if (result.exitCode !== 0) {
174
- throw new Error(`Failed to send message: ${result.stderr || 'Connection failed'}`);
231
+ const payload = { parts: [{ type: 'text', text: message }] };
232
+ if (this.isHost) {
233
+ const response = await fetch(`${baseUrl}/session/${this.agentSessionId}/message`, {
234
+ method: 'POST',
235
+ headers: { 'Content-Type': 'application/json' },
236
+ body: JSON.stringify(payload),
237
+ signal: AbortSignal.timeout(MESSAGE_TIMEOUT_MS),
238
+ });
239
+ if (!response.ok) {
240
+ throw new Error(`Failed to send message: ${response.statusText}`);
241
+ }
242
+ }
243
+ else {
244
+ const result = await execInContainer(this.containerName, [
245
+ 'curl',
246
+ '-s',
247
+ '-f',
248
+ '--max-time',
249
+ String(MESSAGE_TIMEOUT_MS / 1000),
250
+ '-X',
251
+ 'POST',
252
+ `${baseUrl}/session/${this.agentSessionId}/message`,
253
+ '-H',
254
+ 'Content-Type: application/json',
255
+ '-d',
256
+ JSON.stringify(payload),
257
+ ], { user: 'workspace' });
258
+ if (result.exitCode !== 0) {
259
+ throw new Error(`Failed to send message: ${result.stderr || 'Connection failed'}`);
260
+ }
175
261
  }
176
262
  await sseReady;
177
263
  }
@@ -180,18 +266,18 @@ export class OpenCodeAdapter {
180
266
  const seenTools = new Set();
181
267
  let resolved = false;
182
268
  let receivedIdle = false;
183
- const proc = Bun.spawn([
184
- 'docker',
185
- 'exec',
186
- '-i',
187
- this.containerName,
269
+ const curlArgs = [
188
270
  'curl',
189
271
  '-s',
190
272
  '-N',
191
273
  '--max-time',
192
274
  String(SSE_TIMEOUT_MS / 1000),
193
275
  `http://localhost:${this.port}/event`,
194
- ], { stdin: 'ignore', stdout: 'pipe', stderr: 'pipe' });
276
+ ];
277
+ const spawnArgs = this.isHost
278
+ ? curlArgs
279
+ : ['docker', 'exec', '-i', this.containerName, ...curlArgs];
280
+ const proc = Bun.spawn(spawnArgs, { stdin: 'ignore', stdout: 'pipe', stderr: 'pipe' });
195
281
  this.sseProcess = proc;
196
282
  const decoder = new TextDecoder();
197
283
  let buffer = '';
@@ -27,7 +27,7 @@ async function writeCache(cache) {
27
27
  // Ignore cache write errors
28
28
  }
29
29
  }
30
- async function fetchLatestVersion() {
30
+ export async function fetchLatestVersion() {
31
31
  try {
32
32
  const response = await fetch(`https://api.github.com/repos/${GITHUB_REPO}/releases/latest`, {
33
33
  signal: AbortSignal.timeout(3000),
@@ -46,7 +46,7 @@ async function fetchLatestVersion() {
46
46
  return null;
47
47
  }
48
48
  }
49
- function compareVersions(current, latest) {
49
+ export function compareVersions(current, latest) {
50
50
  const currentParts = current.split('.').map(Number);
51
51
  const latestParts = latest.split('.').map(Number);
52
52
  for (let i = 0; i < Math.max(currentParts.length, latestParts.length); i++) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@gricha/perry",
3
- "version": "0.3.4",
3
+ "version": "0.3.5",
4
4
  "description": "Self-contained CLI for spinning up Docker-in-Docker development environments with SSH and proxy helpers.",
5
5
  "type": "module",
6
6
  "bin": {