plugin-updater 1.0.31 → 1.0.33

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/cli.d.ts ADDED
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env node
2
+ export {};
package/dist/cli.js ADDED
@@ -0,0 +1,145 @@
1
+ #!/usr/bin/env node
2
+ process.env.PLUGIN_UPDATER_LIBRARY_MODE = "1";
3
+ process.env.PLUGIN_UPDATER_CLI = "1";
4
+ import fs from "fs";
5
+ import path from "path";
6
+ import os from "os";
7
+ function parseArgs(argv) {
8
+ const parsed = { command: argv[0] ?? "", urls: [] };
9
+ for (let i = 1; i < argv.length; i++) {
10
+ if (argv[i] === "--app")
11
+ parsed.app = argv[++i];
12
+ else if (argv[i] === "--branch")
13
+ parsed.branch = argv[++i];
14
+ else
15
+ parsed.urls.push(argv[i]);
16
+ }
17
+ return parsed;
18
+ }
19
+ function detectApp(explicit) {
20
+ if (explicit === "claude" || explicit === "opencode")
21
+ return explicit;
22
+ if (explicit)
23
+ throw new Error(`Unknown app "${explicit}" - use claude or opencode`);
24
+ const hasClaude = fs.existsSync(path.join(os.homedir(), ".claude"));
25
+ const hasOpencode = fs.existsSync(path.join(os.homedir(), ".opencode"))
26
+ || fs.existsSync(path.join(os.homedir(), ".config", "opencode"));
27
+ if (hasClaude && !hasOpencode)
28
+ return "claude";
29
+ if (hasOpencode && !hasClaude)
30
+ return "opencode";
31
+ throw new Error("Cannot detect the app automatically - pass --app claude or --app opencode");
32
+ }
33
+ function getConfigDir(app) {
34
+ const home = os.homedir();
35
+ const directPath = path.join(home, `.${app}`);
36
+ const configPath = path.join(home, ".config", app);
37
+ return fs.existsSync(directPath) ? directPath : app === "claude" ? directPath : configPath;
38
+ }
39
+ function readJson(file) {
40
+ try {
41
+ return JSON.parse(fs.readFileSync(file, "utf8").replace(/^\s*\/\/[^\n]*/gm, ""));
42
+ }
43
+ catch {
44
+ return null;
45
+ }
46
+ }
47
+ function pluginsJsonPath(configDir) {
48
+ return path.join(configDir, "config", "plugins.json");
49
+ }
50
+ function ensurePluginsJson(configDir) {
51
+ const file = pluginsJsonPath(configDir);
52
+ if (!fs.existsSync(path.dirname(file)))
53
+ fs.mkdirSync(path.dirname(file), { recursive: true });
54
+ if (!fs.existsSync(file))
55
+ fs.writeFileSync(file, "[]\n", "utf8");
56
+ }
57
+ function registerClaudeHook(configDir) {
58
+ const settingsPath = path.join(configDir, "settings.json");
59
+ const settings = (fs.existsSync(settingsPath) ? readJson(settingsPath) : {}) ?? {};
60
+ const hooks = (settings.hooks ?? {});
61
+ const sessionStart = (hooks.SessionStart ?? []);
62
+ if (!JSON.stringify(sessionStart).includes("plugin-updater run")) {
63
+ sessionStart.push({ hooks: [{ type: "command", command: "npx -y plugin-updater run --app claude" }] });
64
+ }
65
+ hooks.SessionStart = sessionStart;
66
+ settings.hooks = hooks;
67
+ fs.writeFileSync(settingsPath, JSON.stringify(settings, null, 2), "utf8");
68
+ console.log(`Registered SessionStart hook in ${settingsPath}`);
69
+ }
70
+ function registerOpencodePlugin(configDir) {
71
+ const ocPath = path.join(configDir, "opencode.json");
72
+ const oc = (fs.existsSync(ocPath) ? readJson(ocPath) : {}) ?? {};
73
+ const plugins = Array.isArray(oc.plugin) ? oc.plugin : [];
74
+ if (!plugins.some((p) => p === "plugin-updater" || p.startsWith("plugin-updater@"))) {
75
+ plugins.unshift("plugin-updater");
76
+ }
77
+ oc.plugin = plugins;
78
+ if (!oc.$schema)
79
+ oc.$schema = "https://opencode.ai/config.json";
80
+ fs.writeFileSync(ocPath, JSON.stringify(oc, null, 2), "utf8");
81
+ console.log(`Registered plugin-updater in ${ocPath}`);
82
+ }
83
+ function addPluginEntry(configDir, url, branch) {
84
+ const cleanUrl = url.replace(/\.git$/, "");
85
+ const name = cleanUrl.split("/").pop() ?? cleanUrl;
86
+ ensurePluginsJson(configDir);
87
+ const file = pluginsJsonPath(configDir);
88
+ const entries = readJson(file) ?? [];
89
+ if (!entries.some((e) => e.name === name)) {
90
+ const entry = { name, url: cleanUrl, enabled: true, autoUpdate: true };
91
+ if (branch)
92
+ entry.branch = branch;
93
+ entries.push(entry);
94
+ fs.writeFileSync(file, JSON.stringify(entries, null, 2), "utf8");
95
+ console.log(`Added ${name} to ${file}`);
96
+ }
97
+ else {
98
+ console.log(`${name} already present in ${file}`);
99
+ }
100
+ return { name, url: cleanUrl, branch };
101
+ }
102
+ async function main() {
103
+ const parsed = parseArgs(process.argv.slice(2));
104
+ if (!["init", "add", "run"].includes(parsed.command)) {
105
+ console.log("usage: plugin-updater <init|add|run> [git-urls...] [--app claude|opencode] [--branch name]");
106
+ process.exit(parsed.command ? 1 : 0);
107
+ }
108
+ const app = detectApp(parsed.app);
109
+ process.env.PLUGIN_UPDATER_APP = app;
110
+ const configDir = getConfigDir(app);
111
+ if (!fs.existsSync(configDir))
112
+ fs.mkdirSync(configDir, { recursive: true });
113
+ console.log(`App: ${app} (${configDir})`);
114
+ const updater = await import("./index.js");
115
+ if (parsed.command === "init") {
116
+ ensurePluginsJson(configDir);
117
+ if (app === "claude")
118
+ registerClaudeHook(configDir);
119
+ else
120
+ registerOpencodePlugin(configDir);
121
+ for (const url of parsed.urls) {
122
+ const entry = addPluginEntry(configDir, url, parsed.branch);
123
+ console.log(`Setting up ${entry.name}...`);
124
+ await updater.updatePluginPublic(entry.name, entry.url, entry.branch);
125
+ }
126
+ console.log("Init complete.");
127
+ }
128
+ else if (parsed.command === "add") {
129
+ if (parsed.urls.length === 0)
130
+ throw new Error("add requires at least one git url");
131
+ for (const url of parsed.urls) {
132
+ const entry = addPluginEntry(configDir, url, parsed.branch);
133
+ console.log(`Setting up ${entry.name}...`);
134
+ await updater.updatePluginPublic(entry.name, entry.url, entry.branch);
135
+ }
136
+ }
137
+ else {
138
+ const entries = readJson(pluginsJsonPath(configDir)) ?? [];
139
+ await updater.earlyLaunch(configDir, entries);
140
+ }
141
+ }
142
+ main().catch((e) => {
143
+ console.error(String(e.message ?? e));
144
+ process.exit(1);
145
+ });
package/dist/index.js CHANGED
@@ -5,13 +5,18 @@ import { execSync } from "child_process";
5
5
  let EARLY_LAUNCH_CONFIG_DIR = null;
6
6
  let PLUGIN_CONFIG = null;
7
7
  const START_TIME = new Date().toISOString().replace(/:/g, "-").split(".")[0];
8
+ // the CLI runs without "claude" in argv, so it forces the app via env
9
+ function getAppName() {
10
+ const override = process.env.PLUGIN_UPDATER_APP;
11
+ if (override === "claude" || override === "opencode")
12
+ return override;
13
+ return process.argv.join(" ").includes("claude") ? "claude" : "opencode";
14
+ }
8
15
  function getPluginConfig() {
9
16
  if (PLUGIN_CONFIG !== null)
10
17
  return PLUGIN_CONFIG;
11
18
  try {
12
- const isClaude = process.argv.join(" ").includes("claude");
13
- const appName = isClaude ? "claude" : "opencode";
14
- const configDir = getAppConfigDir(appName);
19
+ const configDir = getAppConfigDir(getAppName());
15
20
  const preferred = path.join(configDir, "config", "plugin-updater.json");
16
21
  const fallback = path.join(configDir, "plugin-updater.json");
17
22
  const p = fs.existsSync(preferred) ? preferred : fs.existsSync(fallback) ? fallback : null;
@@ -36,9 +41,7 @@ function writeLog(message, isError = false) {
36
41
  if (loggingEnabled) {
37
42
  const date = new Date();
38
43
  const dateStr = date.toISOString().split("T")[0];
39
- const isClaude = process.argv.join(" ").includes("claude");
40
- const appName = isClaude ? "claude" : "opencode";
41
- const configDir = getAppConfigDir(appName);
44
+ const configDir = getAppConfigDir(getAppName());
42
45
  const logsDir = path.join(configDir, "logs", dateStr);
43
46
  if (!fs.existsSync(logsDir))
44
47
  fs.mkdirSync(logsDir, { recursive: true });
@@ -48,7 +51,7 @@ function writeLog(message, isError = false) {
48
51
  }
49
52
  }
50
53
  catch { /* never crash on log failure */ }
51
- if (process.env.PLUGIN_UPDATER_LIBRARY_MODE === "1")
54
+ if (process.env.PLUGIN_UPDATER_LIBRARY_MODE === "1" && process.env.PLUGIN_UPDATER_CLI !== "1")
52
55
  return;
53
56
  if (isError)
54
57
  console.error(message);
@@ -57,9 +60,7 @@ function writeLog(message, isError = false) {
57
60
  }
58
61
  let NPM_GLOBAL_ROOT = null;
59
62
  function getReposDir() {
60
- const isClaude = process.argv.join(" ").includes("claude");
61
- const appName = isClaude ? "claude" : "opencode";
62
- return path.join(getAppConfigDir(appName), "repos");
63
+ return path.join(getAppConfigDir(getAppName()), "repos");
63
64
  }
64
65
  function executeGit(command, cwd) {
65
66
  writeLog(`Executing git: ${command} in ${cwd}`);
@@ -365,6 +366,12 @@ async function deployToExecutionDir(pluginName, executionPath, changed, configDi
365
366
  if (fs.existsSync(packageJsonPath)) {
366
367
  try {
367
368
  buildInTempDir(pluginName, sourceDir);
369
+ const runtimeDeps = JSON.parse(fs.readFileSync(packageJsonPath, "utf8")).dependencies;
370
+ if (runtimeDeps && Object.keys(runtimeDeps).length > 0) {
371
+ writeLog(`Installing runtime dependencies for ${pluginName}`);
372
+ execSync("npm install --omit=dev", { cwd: sourceDir, stdio: "pipe" });
373
+ writeLog(`Finished runtime dependencies for ${pluginName}`);
374
+ }
368
375
  }
369
376
  catch (error) {
370
377
  const err = error;
@@ -406,11 +413,57 @@ async function deployToExecutionDir(pluginName, executionPath, changed, configDi
406
413
  const err = e;
407
414
  writeLog(`Copy failed for ${pluginName}: ${err.message}`, true);
408
415
  }
416
+ applyClaudeManifest(sourceDir, configDir, pluginName);
417
+ // Claude Code never imports deployed plugin files, so under claude the
418
+ // updater is the runtime and invokes the plugin's activate() itself
419
+ if (getAppName() === "claude") {
420
+ try {
421
+ const deployed = await import(pluginExecutionFile);
422
+ if (typeof deployed.activate === "function") {
423
+ writeLog(`Activating ${pluginName}`);
424
+ await deployed.activate();
425
+ writeLog(`Activated ${pluginName}`);
426
+ }
427
+ }
428
+ catch (e) {
429
+ writeLog(`Activation failed for ${pluginName}: ${e.message}`, true);
430
+ }
431
+ }
409
432
  return true;
410
433
  }
434
+ function applyClaudeManifest(sourceDir, configDir, pluginName) {
435
+ if (getAppName() !== "claude")
436
+ return;
437
+ try {
438
+ const pkg = JSON.parse(fs.readFileSync(path.join(sourceDir, "package.json"), "utf8"));
439
+ const manifest = pkg.claudeHub;
440
+ if (!manifest)
441
+ return;
442
+ if (manifest.env && typeof manifest.env === "object") {
443
+ const settingsPath = path.join(configDir, "settings.json");
444
+ let settings = {};
445
+ try {
446
+ settings = JSON.parse(fs.readFileSync(settingsPath, "utf8"));
447
+ }
448
+ catch { /* fresh file */ }
449
+ const env = (settings.env ?? {});
450
+ for (const [key, value] of Object.entries(manifest.env)) {
451
+ env[key] = String(value);
452
+ writeLog(`settings.json env ${key} set by ${pluginName}`);
453
+ }
454
+ settings.env = env;
455
+ fs.writeFileSync(settingsPath, JSON.stringify(settings, null, 2), "utf8");
456
+ }
457
+ if (manifest.daemon?.script) {
458
+ writeLog(`${pluginName} defines a daemon (${manifest.daemon.script}) which the updater does not manage yet`, true);
459
+ }
460
+ }
461
+ catch (e) {
462
+ writeLog(`claudeHub manifest handling failed for ${pluginName}: ${e.message}`, true);
463
+ }
464
+ }
411
465
  async function pluginUpdaterEntry(input) {
412
- const isClaude = process.argv.join(" ").includes("claude");
413
- const appName = isClaude ? "claude" : "opencode";
466
+ const appName = getAppName();
414
467
  const configDir = getAppConfigDir(appName);
415
468
  const pluginsDir = path.join(configDir, "plugin");
416
469
  writeLog(`Starting plugin updater for ${appName}`);
@@ -425,8 +478,7 @@ export async function updatePluginPublic(pluginName, gitUrl, branch, commitHash)
425
478
  if (isOpencodeHookInvocation(pluginName))
426
479
  return {};
427
480
  writeLog(`Public API update call for ${pluginName}`);
428
- const appName = process.argv.join(" ").includes("claude") ? "claude" : "opencode";
429
- const configDir = getAppConfigDir(appName);
481
+ const configDir = getAppConfigDir(getAppName());
430
482
  // interval 0: an explicit update request must never fast-path-skip
431
483
  const result = updatePlugin(pluginName, gitUrl, branch, commitHash ?? null, 0);
432
484
  await deployToExecutionDir(pluginName, path.join(configDir, "plugin"), result.changed, configDir);
@@ -487,8 +539,7 @@ export async function activate(opencodeHookInput) {
487
539
  // context object when re-invoking the export — return an inert plugin instance
488
540
  if (opencodeHookInput !== undefined)
489
541
  return {};
490
- const isClaude = process.argv.join(" ").includes("claude");
491
- const appName = isClaude ? "claude" : "opencode";
542
+ const appName = getAppName();
492
543
  const configDir = getAppConfigDir(appName);
493
544
  writeLog(`Plugin updater activating for ${appName}`);
494
545
  const pluginsJsonPath = path.join(configDir, "config", "plugins.json");
package/package.json CHANGED
@@ -1,9 +1,12 @@
1
1
  {
2
2
  "name": "plugin-updater",
3
- "version": "1.0.31",
3
+ "version": "1.0.33",
4
4
  "description": "Plugin lifecycle manager for OpenCode and Claude Code launchers",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
7
+ "bin": {
8
+ "plugin-updater": "dist/cli.js"
9
+ },
7
10
  "license": "MIT",
8
11
  "author": "intisy",
9
12
  "repository": {