plugin-updater 1.0.25 → 1.0.27

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.d.ts CHANGED
@@ -16,7 +16,7 @@ export declare function getNpmPlugins(configDir: string): NpmPlugin[];
16
16
  export declare function installNpmPlugin(name: string, configDir: string): string;
17
17
  export declare function uninstallNpmPlugin(name: string, configDir: string): string;
18
18
  export declare function updateNpmPlugin(name: string, configDir: string, updateInterval?: number): string;
19
- export declare function updatePluginPublic(pluginName: string, gitUrl: string, branch?: string, commitHash?: string): Promise<void>;
20
- export declare function earlyLaunch(configDir: string, plugins: Plugin[]): Promise<void>;
21
- export declare function activate(): Promise<void>;
19
+ export declare function updatePluginPublic(pluginName: string, gitUrl: string, branch?: string, commitHash?: string): Promise<void | object>;
20
+ export declare function earlyLaunch(configDir: string, plugins: Plugin[]): Promise<void | object>;
21
+ export declare function activate(opencodeHookInput?: unknown): Promise<void | object>;
22
22
  export {};
package/dist/index.js CHANGED
@@ -53,6 +53,7 @@ function writeLog(message, isError = false) {
53
53
  else if (loggingEnabled)
54
54
  console.log(message);
55
55
  }
56
+ let NPM_GLOBAL_ROOT = null;
56
57
  function getReposDir() {
57
58
  const isClaude = process.argv.join(" ").includes("claude");
58
59
  const appName = isClaude ? "claude" : "opencode";
@@ -75,17 +76,26 @@ function executeGit(command, cwd) {
75
76
  return false;
76
77
  }
77
78
  }
79
+ function getNpmGlobalRoot() {
80
+ if (NPM_GLOBAL_ROOT !== null)
81
+ return NPM_GLOBAL_ROOT;
82
+ try {
83
+ NPM_GLOBAL_ROOT = execSync("npm root -g", { stdio: ["ignore", "pipe", "ignore"] }).toString().trim();
84
+ }
85
+ catch {
86
+ NPM_GLOBAL_ROOT = "";
87
+ }
88
+ return NPM_GLOBAL_ROOT;
89
+ }
78
90
  function resolveNpmPluginVersion(name, configDir) {
79
91
  try {
80
92
  const cacheDir = path.join(configDir, "cache", "node_modules");
81
- const globalNpm = process.platform === "win32"
82
- ? path.join(os.homedir(), "AppData", "Roaming", "npm", "node_modules")
83
- : path.join("/usr", "lib", "node_modules");
93
+ const globalNpm = getNpmGlobalRoot();
84
94
  const candidates = [
85
95
  path.join(cacheDir, name, "package.json"),
86
96
  path.join(configDir, "node_modules", name, "package.json"),
87
- path.join(globalNpm, name, "package.json"),
88
- ];
97
+ globalNpm ? path.join(globalNpm, name, "package.json") : "",
98
+ ].filter((p) => p !== "");
89
99
  for (const p of candidates) {
90
100
  if (fs.existsSync(p)) {
91
101
  return JSON.parse(fs.readFileSync(p, "utf8")).version || "";
@@ -120,7 +130,15 @@ function writeOpencodeJson(configDir, data) {
120
130
  fs.writeFileSync(ocPath, JSON.stringify(data, null, 2), "utf8");
121
131
  }
122
132
  // ── Public API ────────────────────────────────────────────────────────────────
133
+ // opencode invokes every exported function as a plugin hook, passing a context
134
+ // object instead of our protocol arguments; exports detect that and return an
135
+ // inert value so opencode gets a valid (empty) plugin instance
136
+ function isOpencodeHookInvocation(firstArgument) {
137
+ return typeof firstArgument !== "string";
138
+ }
123
139
  export function getNpmPlugins(configDir) {
140
+ if (isOpencodeHookInvocation(configDir))
141
+ return [];
124
142
  const { plugins } = readOpencodeJson(configDir);
125
143
  return plugins.map((raw) => {
126
144
  const name = raw.replace(/@[^@/]+$/, "") || raw;
@@ -129,6 +147,8 @@ export function getNpmPlugins(configDir) {
129
147
  });
130
148
  }
131
149
  export function installNpmPlugin(name, configDir) {
150
+ if (isOpencodeHookInvocation(name))
151
+ return "";
132
152
  writeLog(`Installing npm plugin: ${name}`);
133
153
  try {
134
154
  const { plugins, raw } = readOpencodeJson(configDir);
@@ -147,6 +167,8 @@ export function installNpmPlugin(name, configDir) {
147
167
  }
148
168
  }
149
169
  export function uninstallNpmPlugin(name, configDir) {
170
+ if (isOpencodeHookInvocation(name))
171
+ return "";
150
172
  writeLog(`Uninstalling npm plugin: ${name}`);
151
173
  try {
152
174
  const { plugins, raw } = readOpencodeJson(configDir);
@@ -166,6 +188,8 @@ export function uninstallNpmPlugin(name, configDir) {
166
188
  }
167
189
  }
168
190
  export function updateNpmPlugin(name, configDir, updateInterval = 1) {
191
+ if (isOpencodeHookInvocation(name))
192
+ return "";
169
193
  writeLog(`Updating npm plugin: ${name}`);
170
194
  const checkFile = path.join(configDir, "cache", `.npm-lastcheck-${name.replace(/[^a-z0-9]/gi, "_")}`);
171
195
  try {
@@ -262,6 +286,42 @@ async function callPluginCleanup(pluginExecutionFile, configDir) {
262
286
  writeLog(`cleanup() call failed for ${pluginExecutionFile}: ${e.message}`, true);
263
287
  }
264
288
  }
289
+ const BUILD_OUTPUT_DIRS = ["dist", path.join("core", "dist")];
290
+ // npm install creates node_modules/.bin symlinks, which fail on filesystems
291
+ // without symlink support (e.g. Windows-backed Docker bind mounts) — build in
292
+ // the OS temp dir and copy the outputs back instead
293
+ function buildInTempDir(pluginName, sourceDir) {
294
+ const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), `plugin-updater-${pluginName}-`));
295
+ try {
296
+ fs.cpSync(sourceDir, tempDir, {
297
+ recursive: true,
298
+ filter: (src) => {
299
+ const name = path.basename(src);
300
+ return name !== ".git" && name !== "node_modules";
301
+ },
302
+ });
303
+ writeLog(`Running npm install for ${pluginName}`);
304
+ execSync("npm install", { cwd: tempDir, stdio: "pipe" });
305
+ writeLog(`Finished npm install for ${pluginName}`);
306
+ const pkg = JSON.parse(fs.readFileSync(path.join(tempDir, "package.json"), "utf8"));
307
+ if (pkg.scripts?.build) {
308
+ execSync("npm run build", { cwd: tempDir, stdio: "pipe" });
309
+ writeLog(`Finished npm run build for ${pluginName}`);
310
+ }
311
+ else {
312
+ writeLog(`Skipped npm run build for ${pluginName} (no build script found)`);
313
+ }
314
+ for (const outputDir of BUILD_OUTPUT_DIRS) {
315
+ const builtDir = path.join(tempDir, outputDir);
316
+ if (fs.existsSync(builtDir)) {
317
+ fs.cpSync(builtDir, path.join(sourceDir, outputDir), { recursive: true });
318
+ }
319
+ }
320
+ }
321
+ finally {
322
+ fs.rmSync(tempDir, { recursive: true, force: true });
323
+ }
324
+ }
265
325
  async function deployToExecutionDir(pluginName, executionPath, changed, configDir) {
266
326
  const sourceDir = path.join(getReposDir(), pluginName);
267
327
  if (!fs.existsSync(sourceDir))
@@ -275,19 +335,7 @@ async function deployToExecutionDir(pluginName, executionPath, changed, configDi
275
335
  else {
276
336
  if (fs.existsSync(packageJsonPath)) {
277
337
  try {
278
- writeLog(`Running npm install for ${pluginName}`);
279
- execSync("npm install", { cwd: sourceDir, stdio: "pipe" });
280
- writeLog(`Finished npm install for ${pluginName}`);
281
- const pkg = JSON.parse(fs.readFileSync(packageJsonPath, "utf8"));
282
- if (pkg.main)
283
- entryFile = pkg.main;
284
- if (pkg.scripts?.build) {
285
- execSync("npm run build", { cwd: sourceDir, stdio: "pipe" });
286
- writeLog(`Finished npm run build for ${pluginName}`);
287
- }
288
- else {
289
- writeLog(`Skipped npm run build for ${pluginName} (no build script found)`);
290
- }
338
+ buildInTempDir(pluginName, sourceDir);
291
339
  }
292
340
  catch (error) {
293
341
  const err = error;
@@ -345,6 +393,8 @@ async function pluginUpdaterEntry(input) {
345
393
  }
346
394
  }
347
395
  export async function updatePluginPublic(pluginName, gitUrl, branch, commitHash) {
396
+ if (isOpencodeHookInvocation(pluginName))
397
+ return {};
348
398
  writeLog(`Public API update call for ${pluginName}`);
349
399
  const appName = process.argv.join(" ").includes("claude") ? "claude" : "opencode";
350
400
  const configDir = getAppConfigDir(appName);
@@ -352,6 +402,8 @@ export async function updatePluginPublic(pluginName, gitUrl, branch, commitHash)
352
402
  await deployToExecutionDir(pluginName, path.join(configDir, "plugin"), result.changed, configDir);
353
403
  }
354
404
  export async function earlyLaunch(configDir, plugins) {
405
+ if (isOpencodeHookInvocation(configDir))
406
+ return {};
355
407
  EARLY_LAUNCH_CONFIG_DIR = configDir;
356
408
  writeLog("Starting earlyLaunch updater sequence");
357
409
  // Self-update first
@@ -393,7 +445,11 @@ export async function earlyLaunch(configDir, plugins) {
393
445
  }
394
446
  }
395
447
  }
396
- export async function activate() {
448
+ export async function activate(opencodeHookInput) {
449
+ // module load below calls activate() with no argument; opencode passes a
450
+ // context object when re-invoking the export — return an inert plugin instance
451
+ if (opencodeHookInput !== undefined)
452
+ return {};
397
453
  const isClaude = process.argv.join(" ").includes("claude");
398
454
  const appName = isClaude ? "claude" : "opencode";
399
455
  const configDir = getAppConfigDir(appName);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "plugin-updater",
3
- "version": "1.0.25",
3
+ "version": "1.0.27",
4
4
  "description": "Plugin lifecycle manager for OpenCode and Claude Code launchers",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",