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 +2 -0
- package/dist/cli.js +145 -0
- package/dist/index.js +48 -19
- 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}`);
|
|
@@ -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
|
|
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
|
|
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
|
|
414
|
-
|
|
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
|
|
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.
|
|
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": {
|