openai-ws-opencode 0.1.4 → 0.1.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.
Files changed (3) hide show
  1. package/README.md +1 -1
  2. package/bin/setup.js +143 -6
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -101,7 +101,7 @@ Then point OpenCode at the packed tarball through the plugin array:
101
101
 
102
102
  ```jsonc
103
103
  {
104
- "plugin": ["openai-ws-opencode@file:/absolute/path/openai-ws-opencode-0.1.4.tgz"]
104
+ "plugin": ["openai-ws-opencode@file:/absolute/path/openai-ws-opencode-0.1.5.tgz"]
105
105
  }
106
106
  ```
107
107
 
package/bin/setup.js CHANGED
@@ -166,20 +166,28 @@ function applyJsonPatch(text, jsonPath, value) {
166
166
  }));
167
167
  }
168
168
  function pluginPackageName(specifier) {
169
- if (specifier.startsWith("file://"))
170
- return path.basename(new URL(specifier).pathname).replace(/\.[cm]?[jt]s$/, "");
169
+ if (specifier.startsWith("file:")) {
170
+ const filePath = specifier.startsWith("file://") ? new URL(specifier).pathname : specifier.slice("file:".length);
171
+ const base = path.basename(filePath);
172
+ if (base === PACKAGE_NAME || base.startsWith(`${PACKAGE_NAME}-`) || base.startsWith(`${PACKAGE_NAME}@`))
173
+ return PACKAGE_NAME;
174
+ return base.replace(/(?:\.tgz|\.[cm]?[jt]s)$/, "");
175
+ }
171
176
  const lastAt = specifier.lastIndexOf("@");
172
177
  return lastAt > 0 ? specifier.slice(0, lastAt) : specifier;
173
178
  }
174
- function patchConfigText(input, pluginSpec = DEFAULT_PLUGIN_SPEC) {
179
+ function patchConfigText(input, pluginSpec = DEFAULT_PLUGIN_SPEC, replacePluginSpec = false) {
175
180
  let text = input.trim() ? input : "{}";
176
181
  const config = parse(text) ?? {};
177
182
  if (!config.$schema)
178
183
  text = applyJsonPatch(text, ["$schema"], SCHEMA);
179
184
  const current = (parse(text) ?? {}).plugin;
180
185
  const plugins = Array.isArray(current) ? [...current] : [];
181
- if (!plugins.some((plugin) => typeof plugin === "string" && pluginPackageName(plugin) === PACKAGE_NAME)) {
186
+ const existingPluginIndex = plugins.findIndex((plugin) => typeof plugin === "string" && pluginPackageName(plugin) === PACKAGE_NAME);
187
+ if (existingPluginIndex === -1) {
182
188
  plugins.push(pluginSpec);
189
+ } else if (replacePluginSpec) {
190
+ plugins[existingPluginIndex] = pluginSpec;
183
191
  }
184
192
  text = applyJsonPatch(text, ["plugin"], plugins);
185
193
  const next = parse(text) ?? {};
@@ -192,6 +200,123 @@ function patchConfigText(input, pluginSpec = DEFAULT_PLUGIN_SPEC) {
192
200
  `
193
201
  }));
194
202
  }
203
+ function isNotFound(error) {
204
+ return typeof error === "object" && error !== null && "code" in error && error.code === "ENOENT";
205
+ }
206
+ function cacheHomePath(cacheHome = process.env.XDG_CACHE_HOME ?? path.join(os.homedir(), ".cache")) {
207
+ return cacheHome;
208
+ }
209
+ function openCodeLatestCachePath(cacheHome) {
210
+ return path.join(cacheHomePath(cacheHome), "opencode", "packages", `${PACKAGE_NAME}@latest`);
211
+ }
212
+ async function pathExists(file) {
213
+ try {
214
+ await fs.access(file);
215
+ return true;
216
+ } catch (error) {
217
+ if (isNotFound(error))
218
+ return false;
219
+ throw error;
220
+ }
221
+ }
222
+ async function readPackageVersion(packageJsonPath) {
223
+ try {
224
+ const text = await fs.readFile(packageJsonPath, "utf8");
225
+ const parsed = JSON.parse(text);
226
+ return typeof parsed.version === "string" ? parsed.version : undefined;
227
+ } catch (error) {
228
+ if (isNotFound(error))
229
+ return;
230
+ throw error;
231
+ }
232
+ }
233
+ async function setupPackageVersion() {
234
+ const packageJsonPath = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..", "package.json");
235
+ const version = await readPackageVersion(packageJsonPath);
236
+ if (!version)
237
+ throw new Error(`Unable to read setup package version from ${packageJsonPath}`);
238
+ return version;
239
+ }
240
+ function semverParts(version) {
241
+ const match = /^(\d+)\.(\d+)\.(\d+)/.exec(version);
242
+ if (!match)
243
+ return;
244
+ return [Number(match[1]), Number(match[2]), Number(match[3])];
245
+ }
246
+ function compareSemver(left, right) {
247
+ const leftParts = semverParts(left);
248
+ const rightParts = semverParts(right);
249
+ if (!leftParts || !rightParts)
250
+ return 0;
251
+ for (let index = 0;index < leftParts.length; index++) {
252
+ if (leftParts[index] !== rightParts[index])
253
+ return leftParts[index] < rightParts[index] ? -1 : 1;
254
+ }
255
+ return 0;
256
+ }
257
+ var SOURCE_EXTENSIONS = new Set([".cjs", ".js", ".mjs", ".ts"]);
258
+ var BAD_OAUTH_PORT = /\bOAUTH_PORT\b\s*[:=]\s*1456\b/;
259
+ var BAD_CODEX_ORIGINATOR = /\bCODEX_ORIGINATOR\b\s*[:=]\s*["']codex_cli_rs["']/;
260
+ async function hasKnownBadConstants(packageRoot) {
261
+ const directories = [packageRoot];
262
+ let checkedFiles = 0;
263
+ while (directories.length > 0 && checkedFiles < 500) {
264
+ const current = directories.pop();
265
+ let entries;
266
+ try {
267
+ entries = await fs.readdir(current, { withFileTypes: true });
268
+ } catch (error) {
269
+ if (isNotFound(error))
270
+ continue;
271
+ throw error;
272
+ }
273
+ for (const entry of entries) {
274
+ const entryPath = path.join(current, entry.name);
275
+ if (entry.isDirectory()) {
276
+ if (entry.name !== "node_modules" && entry.name !== ".git")
277
+ directories.push(entryPath);
278
+ continue;
279
+ }
280
+ if (!entry.isFile() || !SOURCE_EXTENSIONS.has(path.extname(entry.name)))
281
+ continue;
282
+ checkedFiles += 1;
283
+ const source = await fs.readFile(entryPath, "utf8");
284
+ if (BAD_OAUTH_PORT.test(source) || BAD_CODEX_ORIGINATOR.test(source))
285
+ return true;
286
+ if (checkedFiles >= 500)
287
+ break;
288
+ }
289
+ }
290
+ return false;
291
+ }
292
+ function timestampForPath(date) {
293
+ return date.toISOString().replace(/[:.]/g, "-");
294
+ }
295
+ async function staleDestination(cachePath, now) {
296
+ const base = `${cachePath}.stale-${timestampForPath(now)}`;
297
+ let candidate = base;
298
+ let suffix = 1;
299
+ while (await pathExists(candidate)) {
300
+ candidate = `${base}-${suffix}`;
301
+ suffix += 1;
302
+ }
303
+ return candidate;
304
+ }
305
+ async function repairStaleOpenCodeLatestCache(options = {}) {
306
+ const cachePath = openCodeLatestCachePath(options.cacheHome);
307
+ if (!await pathExists(cachePath))
308
+ return { cachePath, repaired: false };
309
+ const packageRoot = path.join(cachePath, "node_modules", PACKAGE_NAME);
310
+ const cachedVersion = await readPackageVersion(path.join(packageRoot, "package.json"));
311
+ const currentVersion = options.packageVersion ?? await setupPackageVersion();
312
+ const staleVersion = cachedVersion ? compareSemver(cachedVersion, currentVersion) < 0 : false;
313
+ const staleConstants = await hasKnownBadConstants(packageRoot);
314
+ if (!staleVersion && !staleConstants)
315
+ return { cachePath, repaired: false };
316
+ const movedTo = await staleDestination(cachePath, options.now?.() ?? new Date);
317
+ await fs.rename(cachePath, movedTo);
318
+ return { cachePath, movedTo, repaired: true };
319
+ }
195
320
  async function setupOpenCodeConfig(options = {}) {
196
321
  const file = targetPath(options);
197
322
  let existing = "";
@@ -201,11 +326,19 @@ async function setupOpenCodeConfig(options = {}) {
201
326
  if (error?.code !== "ENOENT")
202
327
  throw error;
203
328
  }
204
- const updated = patchConfigText(existing, options.pluginSpec ?? DEFAULT_PLUGIN_SPEC);
329
+ const updated = patchConfigText(existing, options.pluginSpec ?? DEFAULT_PLUGIN_SPEC, Boolean(options.pluginSpec));
205
330
  await fs.mkdir(path.dirname(file), { recursive: true });
206
331
  await fs.writeFile(file, updated.endsWith(`
207
332
  `) ? updated : `${updated}
208
333
  `, "utf8");
334
+ if (options.cacheRepair !== false) {
335
+ try {
336
+ await repairStaleOpenCodeLatestCache();
337
+ } catch (error) {
338
+ const message = error instanceof Error ? error.message : String(error);
339
+ console.warn(`Warning: could not repair OpenCode cache at ${openCodeLatestCachePath()}. Remove it manually if OpenCode keeps loading stale plugin code. ${message}`);
340
+ }
341
+ }
209
342
  return file;
210
343
  }
211
344
  function parseArgs(argv) {
@@ -220,8 +353,10 @@ function parseArgs(argv) {
220
353
  options.configPath = argv[++index];
221
354
  else if (arg === "--plugin")
222
355
  options.pluginSpec = argv[++index];
356
+ else if (arg === "--no-cache-repair")
357
+ options.cacheRepair = false;
223
358
  else if (arg === "--help" || arg === "-h") {
224
- console.log("Usage: openai-ws-opencode setup [--global|--project] [--path <opencode.json>] [--plugin <specifier>]");
359
+ console.log("Usage: openai-ws-opencode setup [--global|--project] [--path <opencode.json>] [--plugin <specifier>] [--no-cache-repair]");
225
360
  process.exit(0);
226
361
  }
227
362
  }
@@ -247,6 +382,8 @@ if (isDirectExecution()) {
247
382
  }
248
383
  export {
249
384
  setupOpenCodeConfig,
385
+ repairStaleOpenCodeLatestCache,
250
386
  patchConfigText,
387
+ parseArgs,
251
388
  isDirectExecution
252
389
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "openai-ws-opencode",
3
- "version": "0.1.4",
3
+ "version": "0.1.5",
4
4
  "description": "OpenCode plugin for OpenAI Responses WebSocket transport.",
5
5
  "type": "module",
6
6
  "license": "MIT",