plugin-updater 1.0.31 → 1.0.32

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}`);
@@ -406,11 +407,25 @@ async function deployToExecutionDir(pluginName, executionPath, changed, configDi
406
407
  const err = e;
407
408
  writeLog(`Copy failed for ${pluginName}: ${err.message}`, true);
408
409
  }
410
+ // Claude Code never imports deployed plugin files, so under claude the
411
+ // updater is the runtime and invokes the plugin's activate() itself
412
+ if (getAppName() === "claude") {
413
+ try {
414
+ const deployed = await import(pluginExecutionFile);
415
+ if (typeof deployed.activate === "function") {
416
+ writeLog(`Activating ${pluginName}`);
417
+ await deployed.activate();
418
+ writeLog(`Activated ${pluginName}`);
419
+ }
420
+ }
421
+ catch (e) {
422
+ writeLog(`Activation failed for ${pluginName}: ${e.message}`, true);
423
+ }
424
+ }
409
425
  return true;
410
426
  }
411
427
  async function pluginUpdaterEntry(input) {
412
- const isClaude = process.argv.join(" ").includes("claude");
413
- const appName = isClaude ? "claude" : "opencode";
428
+ const appName = getAppName();
414
429
  const configDir = getAppConfigDir(appName);
415
430
  const pluginsDir = path.join(configDir, "plugin");
416
431
  writeLog(`Starting plugin updater for ${appName}`);
@@ -425,8 +440,7 @@ export async function updatePluginPublic(pluginName, gitUrl, branch, commitHash)
425
440
  if (isOpencodeHookInvocation(pluginName))
426
441
  return {};
427
442
  writeLog(`Public API update call for ${pluginName}`);
428
- const appName = process.argv.join(" ").includes("claude") ? "claude" : "opencode";
429
- const configDir = getAppConfigDir(appName);
443
+ const configDir = getAppConfigDir(getAppName());
430
444
  // interval 0: an explicit update request must never fast-path-skip
431
445
  const result = updatePlugin(pluginName, gitUrl, branch, commitHash ?? null, 0);
432
446
  await deployToExecutionDir(pluginName, path.join(configDir, "plugin"), result.changed, configDir);
@@ -487,8 +501,7 @@ export async function activate(opencodeHookInput) {
487
501
  // context object when re-invoking the export — return an inert plugin instance
488
502
  if (opencodeHookInput !== undefined)
489
503
  return {};
490
- const isClaude = process.argv.join(" ").includes("claude");
491
- const appName = isClaude ? "claude" : "opencode";
504
+ const appName = getAppName();
492
505
  const configDir = getAppConfigDir(appName);
493
506
  writeLog(`Plugin updater activating for ${appName}`);
494
507
  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.32",
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": {