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 +2 -0
- package/dist/cli.js +145 -0
- package/dist/index.js +67 -16
- package/package.json +4 -1
package/dist/cli.d.ts
ADDED
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
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
|
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.
|
|
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": {
|