@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.
- package/memini.js +74 -41
- 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
|
|
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
|
-
*
|
|
91
|
-
*
|
|
92
|
-
*
|
|
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
|
|
102
|
+
export function resolveInstallContextFrom(startPath) {
|
|
95
103
|
try {
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
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
|
|
133
|
-
* removes the installed node_modules package, and cleans
|
|
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
|
-
|
|
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
|
|
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
|
|
163
|
-
|
|
164
|
-
|
|
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
|
-
|
|
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
|
-
//
|
|
179
|
-
// will reconcile.
|
|
210
|
+
// A lock we can't remove isn't fatal — npm install reconciles it.
|
|
180
211
|
}
|
|
181
212
|
}
|
|
182
213
|
return ctx.installDir;
|
|
183
214
|
}
|
|
184
215
|
|
|
185
216
|
/**
|
|
186
|
-
*
|
|
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
|
|
222
|
+
export function runNpmInstall(installDir) {
|
|
190
223
|
try {
|
|
191
|
-
const result = spawnSync(
|
|
224
|
+
const result = spawnSync("npm", ["install"], {
|
|
192
225
|
cwd: installDir,
|
|
193
226
|
stdio: ["ignore", "pipe", "pipe"],
|
|
194
|
-
timeout:
|
|
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 =
|
|
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:
|
|
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)}`);
|