plugin-updater 1.0.30 → 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}`);
@@ -271,10 +272,25 @@ function updatePlugin(pluginName, gitUrl, branch, commitHash, updateInterval = 1
271
272
  executeGit(`git pull --ff-only origin ${branch}`, targetDir);
272
273
  }
273
274
  else {
275
+ // the updater owns repos/: hard-sync to the remote so force-pushed
276
+ // branches and rewritten submodule history cannot strand the clone
277
+ executeGit("git fetch origin", targetDir);
274
278
  executeGit("git checkout main || git checkout master", targetDir);
275
- executeGit("git pull --ff-only", targetDir);
279
+ executeGit("git reset --hard @{upstream}", targetDir);
280
+ }
281
+ executeGit("git submodule sync --recursive", targetDir);
282
+ const submodulesOk = executeGit("git submodule update --init --recursive --force", targetDir);
283
+ if (!submodulesOk) {
284
+ writeLog(`Submodule sync failed for ${pluginName}, recloning`, true);
285
+ try {
286
+ fs.rmSync(targetDir, { recursive: true, force: true });
287
+ }
288
+ catch { /* ignore */ }
289
+ const recloneBranchFlag = branch ? `--branch ${branch}` : "";
290
+ executeGit(`git clone --recurse-submodules ${recloneBranchFlag} ${gitUrl} ${pluginName}`, reposDir);
291
+ fs.writeFileSync(lastCheckFile, Date.now().toString());
292
+ didChange = true;
276
293
  }
277
- executeGit("git submodule update --init --recursive", targetDir);
278
294
  let afterHash = "";
279
295
  try {
280
296
  afterHash = execSync("git rev-parse HEAD", { cwd: targetDir }).toString().trim();
@@ -391,11 +407,25 @@ async function deployToExecutionDir(pluginName, executionPath, changed, configDi
391
407
  const err = e;
392
408
  writeLog(`Copy failed for ${pluginName}: ${err.message}`, true);
393
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
+ }
394
425
  return true;
395
426
  }
396
427
  async function pluginUpdaterEntry(input) {
397
- const isClaude = process.argv.join(" ").includes("claude");
398
- const appName = isClaude ? "claude" : "opencode";
428
+ const appName = getAppName();
399
429
  const configDir = getAppConfigDir(appName);
400
430
  const pluginsDir = path.join(configDir, "plugin");
401
431
  writeLog(`Starting plugin updater for ${appName}`);
@@ -410,9 +440,9 @@ export async function updatePluginPublic(pluginName, gitUrl, branch, commitHash)
410
440
  if (isOpencodeHookInvocation(pluginName))
411
441
  return {};
412
442
  writeLog(`Public API update call for ${pluginName}`);
413
- const appName = process.argv.join(" ").includes("claude") ? "claude" : "opencode";
414
- const configDir = getAppConfigDir(appName);
415
- const result = updatePlugin(pluginName, gitUrl, branch, commitHash ?? null);
443
+ const configDir = getAppConfigDir(getAppName());
444
+ // interval 0: an explicit update request must never fast-path-skip
445
+ const result = updatePlugin(pluginName, gitUrl, branch, commitHash ?? null, 0);
416
446
  await deployToExecutionDir(pluginName, path.join(configDir, "plugin"), result.changed, configDir);
417
447
  }
418
448
  export async function earlyLaunch(configDir, plugins) {
@@ -471,8 +501,7 @@ export async function activate(opencodeHookInput) {
471
501
  // context object when re-invoking the export — return an inert plugin instance
472
502
  if (opencodeHookInput !== undefined)
473
503
  return {};
474
- const isClaude = process.argv.join(" ").includes("claude");
475
- const appName = isClaude ? "claude" : "opencode";
504
+ const appName = getAppName();
476
505
  const configDir = getAppConfigDir(appName);
477
506
  writeLog(`Plugin updater activating for ${appName}`);
478
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.30",
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": {