@eleboucher/opencode-memini 0.7.8 → 0.7.9

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 (2) hide show
  1. package/memini.js +74 -41
  2. package/package.json +1 -1
package/memini.js CHANGED
@@ -55,11 +55,16 @@ const CLIENT_VERSION = readPluginVersion();
55
55
 
56
56
  // Auto-update: opencode never re-fetches cached npm plugins, so the plugin
57
57
  // checks npm dist-tags once per process and self-updates (same major version
58
- // only) so the running copy stays current.
58
+ // only) so the running copy stays current. opencode installs each plugin spec
59
+ // into its own isolated wrapper directory under
60
+ // ~/.cache/opencode/packages/<spec>/ (e.g. .../opencode-memini@latest/) — the
61
+ // wrapper holds a package.json listing the plugin as a dependency plus a
62
+ // node_modules/ tree — so the wrapper dir is where we rewrite the pin and
63
+ // re-run npm install.
59
64
  const PACKAGE_NAME = "@eleboucher/opencode-memini";
60
65
  const NPM_REGISTRY_URL = `https://registry.npmjs.org/-/package/${PACKAGE_NAME}/dist-tags`;
61
66
  const NPM_FETCH_TIMEOUT = 5000;
62
- const BUN_INSTALL_TIMEOUT_MS = 60000;
67
+ const INSTALL_TIMEOUT_MS = 60000;
63
68
  let autoUpdateChecked = false;
64
69
 
65
70
  /**
@@ -87,23 +92,50 @@ export function compareVersions(a, b) {
87
92
  }
88
93
 
89
94
  /**
90
- * resolveInstallContext finds the opencode plugin cache directory that holds
91
- * this running plugin instance, by walking up from import.meta.url. Returns
92
- * { installDir, packageJsonPath } or null.
95
+ * resolveInstallContextFrom walks up from `startPath` looking for the opencode
96
+ * plugin cache wrapper dir: the first ancestor whose child is a `node_modules`
97
+ * directory AND that also has a `package.json` sibling. Returns
98
+ * { installDir, packageJsonPath } or null. Pure (no `import.meta.url` read) so
99
+ * it can be driven from a synthetic on-disk layout in tests. Exported for
100
+ * testing.
93
101
  */
94
- function resolveInstallContext() {
102
+ export function resolveInstallContextFrom(startPath) {
95
103
  try {
96
- const pluginDir = dirname(fileURLToPath(import.meta.url));
97
- const nodeModulesDir = dirname(pluginDir);
98
- const installDir = dirname(nodeModulesDir);
99
- const packageJsonPath = join(installDir, "package.json");
100
- if (existsSync(packageJsonPath)) {
101
- return { installDir, packageJsonPath };
104
+ let dir = startPath;
105
+ for (let i = 0; i < 20; i++) {
106
+ const nodeModules = join(dir, "node_modules");
107
+ if (existsSync(nodeModules) && statSync(nodeModules).isDirectory()) {
108
+ const packageJsonPath = join(dir, "package.json");
109
+ if (existsSync(packageJsonPath)) {
110
+ return { installDir: dir, packageJsonPath };
111
+ }
112
+ return null; // node_modules found but no package.json — not a wrapper
113
+ }
114
+ const parent = dirname(dir);
115
+ if (parent === dir) break; // reached root
116
+ dir = parent;
102
117
  }
103
118
  } catch {}
104
119
  return null;
105
120
  }
106
121
 
122
+ /**
123
+ * resolveInstallContext finds the opencode plugin cache wrapper directory that
124
+ * holds this running plugin instance. opencode installs each npm plugin spec
125
+ * into its own isolated dir under ~/.cache/opencode/packages/<spec>/ (e.g.
126
+ * .../opencode-memini@latest/), containing a package.json listing the plugin as
127
+ * a dependency and a node_modules/ tree. The plugin's own file lives at
128
+ * <wrapper>/node_modules/@eleboucher/opencode-memini/memini.js, so the wrapper
129
+ * is the first ancestor whose child is a `node_modules` directory. Returns
130
+ * { installDir, packageJsonPath } or null. Exported for testing.
131
+ */
132
+ export function resolveInstallContext() {
133
+ try {
134
+ return resolveInstallContextFrom(dirname(fileURLToPath(import.meta.url)));
135
+ } catch {}
136
+ return null;
137
+ }
138
+
107
139
  /**
108
140
  * fetchLatestVersion queries npm dist-tags with a timeout. Returns the version
109
141
  * string or null on failure.
@@ -129,12 +161,19 @@ async function fetchLatestVersion() {
129
161
  }
130
162
 
131
163
  /**
132
- * prepareCacheUpdate rewrites the cache package.json to pin the new version,
133
- * removes the installed node_modules package, and cleans bun.lock. Returns
134
- * the installDir on success, null on failure.
164
+ * prepareCacheUpdate rewrites the wrapper package.json to pin the new version,
165
+ * removes the installed node_modules package, and cleans any lockfile so the
166
+ * next install re-fetches. Returns the installDir on success, null on failure.
167
+ *
168
+ * `ctx` is optional: when omitted, resolves from `import.meta.url` (the live
169
+ * path); tests pass a synthetic { installDir, packageJsonPath } so the full
170
+ * rewrite-and-clean flow can be exercised against a temp dir without touching
171
+ * the dev checkout. Exported for testing.
135
172
  */
136
- function prepareCacheUpdate(newVersion, log) {
137
- const ctx = resolveInstallContext();
173
+ export function prepareCacheUpdate(newVersion, log, ctx) {
174
+ if (!ctx) {
175
+ ctx = resolveInstallContext();
176
+ }
138
177
  if (!ctx) {
139
178
  log.warn("auto-update: could not resolve install context");
140
179
  return null;
@@ -151,7 +190,7 @@ function prepareCacheUpdate(newVersion, log) {
151
190
  log.warn(`auto-update: failed to rewrite cache package.json: ${String(err)}`);
152
191
  return null;
153
192
  }
154
- // Remove installed node_modules so bun install re-fetches
193
+ // Remove installed node_modules so the install re-fetches
155
194
  try {
156
195
  const pkgDir = join(ctx.installDir, "node_modules", "@eleboucher", "opencode-memini");
157
196
  if (existsSync(pkgDir)) rmSync(pkgDir, { recursive: true, force: true });
@@ -159,39 +198,33 @@ function prepareCacheUpdate(newVersion, log) {
159
198
  log.warn(`auto-update: failed to remove cached node_modules: ${String(err)}`);
160
199
  return null;
161
200
  }
162
- // Clean bun.lock entry if it exists
163
- const lockPath = join(ctx.installDir, "bun.lock");
164
- if (existsSync(lockPath)) {
201
+ // Clean lockfiles: opencode's installer may write either package-lock.json
202
+ // (npm/arborist) or bun.lock depending on the bundler. Remove both if
203
+ // present; the install regenerates them.
204
+ for (const lockName of ["package-lock.json", "bun.lock"]) {
205
+ const lockPath = join(ctx.installDir, lockName);
206
+ if (!existsSync(lockPath)) continue;
165
207
  try {
166
- const lock = JSON.parse(readFileSync(lockPath, "utf8"));
167
- let modified = false;
168
- if (lock.workspaces?.[""]?.dependencies?.[PACKAGE_NAME]) {
169
- delete lock.workspaces[""].dependencies[PACKAGE_NAME];
170
- modified = true;
171
- }
172
- if (lock.packages?.[PACKAGE_NAME]) {
173
- delete lock.packages[PACKAGE_NAME];
174
- modified = true;
175
- }
176
- if (modified) writeFileSync(lockPath, JSON.stringify(lock, null, 2));
208
+ rmSync(lockPath, { force: true });
177
209
  } catch {
178
- // bun.lock format varies; if we can't parse it, leave it bun install
179
- // will reconcile.
210
+ // A lock we can't remove isn't fatalnpm install reconciles it.
180
211
  }
181
212
  }
182
213
  return ctx.installDir;
183
214
  }
184
215
 
185
216
  /**
186
- * runBunInstall runs `bun install` in the given directory with a timeout.
187
- * Returns true on success (exit code 0), false otherwise.
217
+ * runNpmInstall runs `npm install` in the given directory with a timeout.
218
+ * Returns true on success (exit code 0), false otherwise. opencode uses
219
+ * @npmcli/arborist under the hood, so `npm install` matches the lockfile
220
+ * format it produces; `bun install` would rewrite it. Exported for testing.
188
221
  */
189
- function runBunInstall(installDir) {
222
+ export function runNpmInstall(installDir) {
190
223
  try {
191
- const result = spawnSync(process.execPath, ["install"], {
224
+ const result = spawnSync("npm", ["install"], {
192
225
  cwd: installDir,
193
226
  stdio: ["ignore", "pipe", "pipe"],
194
- timeout: BUN_INSTALL_TIMEOUT_MS,
227
+ timeout: INSTALL_TIMEOUT_MS,
195
228
  });
196
229
  return result.status === 0;
197
230
  } catch {
@@ -1453,11 +1486,11 @@ export const MeminiPlugin = async ({ client, worktree, directory }, options) =>
1453
1486
  log.warn(`auto-update: updating ${CLIENT_VERSION} → ${latest}`);
1454
1487
  const installDir = prepareCacheUpdate(latest, log);
1455
1488
  if (!installDir) return;
1456
- const ok = runBunInstall(installDir);
1489
+ const ok = runNpmInstall(installDir);
1457
1490
  if (ok) {
1458
1491
  log.warn(`auto-update: installed v${latest} — restart opencode to apply`);
1459
1492
  } else {
1460
- log.warn(`auto-update: bun install failed; will retry next session`);
1493
+ log.warn(`auto-update: npm install failed; will retry next session`);
1461
1494
  }
1462
1495
  } catch (err) {
1463
1496
  log.warn(`auto-update: check failed: ${String(err)}`);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@eleboucher/opencode-memini",
3
- "version": "0.7.8",
3
+ "version": "0.7.9",
4
4
  "description": "Automatic cross-session memory for opencode via memini — recall before each turn, capture after.",
5
5
  "keywords": [
6
6
  "memini",