plugin-updater 1.0.19 → 1.0.22
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/index.d.ts +21 -0
- package/dist/index.js +390 -0
- package/package.json +19 -3
- package/.github/workflows/publish.yml +0 -29
- package/.idea/misc.xml +0 -7
- package/.idea/modules.xml +0 -8
- package/.idea/plugin-updater.iml +0 -9
- package/.idea/vcs.xml +0 -6
- package/SPEC.md +0 -13
- package/index.js +0 -227
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
interface Plugin {
|
|
2
|
+
name: string;
|
|
3
|
+
url?: string;
|
|
4
|
+
branch?: string;
|
|
5
|
+
enabled?: boolean;
|
|
6
|
+
autoUpdate?: boolean;
|
|
7
|
+
updateInterval?: number;
|
|
8
|
+
}
|
|
9
|
+
interface NpmPlugin {
|
|
10
|
+
name: string;
|
|
11
|
+
version: string;
|
|
12
|
+
installed: boolean;
|
|
13
|
+
raw: string;
|
|
14
|
+
}
|
|
15
|
+
export declare function getNpmPlugins(configDir: string): NpmPlugin[];
|
|
16
|
+
export declare function installNpmPlugin(name: string, configDir: string): string;
|
|
17
|
+
export declare function uninstallNpmPlugin(name: string, configDir: string): string;
|
|
18
|
+
export declare function updateNpmPlugin(name: string, configDir: string, updateInterval?: number): string;
|
|
19
|
+
export declare function updatePluginPublic(pluginName: string, gitUrl: string, branch?: string, commitHash?: string): Promise<void>;
|
|
20
|
+
export declare function earlyLaunch(configDir: string, plugins: Plugin[]): Promise<void>;
|
|
21
|
+
export {};
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,390 @@
|
|
|
1
|
+
import fs from "fs";
|
|
2
|
+
import path from "path";
|
|
3
|
+
import os from "os";
|
|
4
|
+
import { execSync } from "child_process";
|
|
5
|
+
let EARLY_LAUNCH_CONFIG_DIR = null;
|
|
6
|
+
let PLUGIN_CONFIG = null;
|
|
7
|
+
const START_TIME = new Date().toISOString().replace(/:/g, "-").split(".")[0];
|
|
8
|
+
function getPluginConfig() {
|
|
9
|
+
if (PLUGIN_CONFIG !== null)
|
|
10
|
+
return PLUGIN_CONFIG;
|
|
11
|
+
try {
|
|
12
|
+
const isClaude = process.argv.join(" ").includes("claude");
|
|
13
|
+
const appName = isClaude ? "claude" : "opencode";
|
|
14
|
+
const configDir = getAppConfigDir(appName);
|
|
15
|
+
const preferred = path.join(configDir, "config", "plugin-updater.json");
|
|
16
|
+
const fallback = path.join(configDir, "plugin-updater.json");
|
|
17
|
+
const p = fs.existsSync(preferred) ? preferred : fs.existsSync(fallback) ? fallback : null;
|
|
18
|
+
PLUGIN_CONFIG = p ? JSON.parse(fs.readFileSync(p, "utf8")) : {};
|
|
19
|
+
}
|
|
20
|
+
catch {
|
|
21
|
+
PLUGIN_CONFIG = {};
|
|
22
|
+
}
|
|
23
|
+
return PLUGIN_CONFIG ?? {};
|
|
24
|
+
}
|
|
25
|
+
function getAppConfigDir(appName) {
|
|
26
|
+
if (EARLY_LAUNCH_CONFIG_DIR)
|
|
27
|
+
return EARLY_LAUNCH_CONFIG_DIR;
|
|
28
|
+
const home = os.homedir();
|
|
29
|
+
const directPath = path.join(home, `.${appName}`);
|
|
30
|
+
const configPath = path.join(home, ".config", appName);
|
|
31
|
+
return fs.existsSync(directPath) ? directPath : configPath;
|
|
32
|
+
}
|
|
33
|
+
function writeLog(message, isError = false) {
|
|
34
|
+
const loggingEnabled = getPluginConfig().logging !== false;
|
|
35
|
+
try {
|
|
36
|
+
if (loggingEnabled) {
|
|
37
|
+
const date = new Date();
|
|
38
|
+
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);
|
|
42
|
+
const logsDir = path.join(configDir, "logs", dateStr);
|
|
43
|
+
if (!fs.existsSync(logsDir))
|
|
44
|
+
fs.mkdirSync(logsDir, { recursive: true });
|
|
45
|
+
const logFile = path.join(logsDir, `plugin-updater-${START_TIME}.log`);
|
|
46
|
+
const prefix = isError ? "[ERROR]" : "[INFO]";
|
|
47
|
+
fs.appendFileSync(logFile, `[${date.toISOString()}] ${prefix} ${message}\n`);
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
catch { /* never crash on log failure */ }
|
|
51
|
+
if (isError)
|
|
52
|
+
console.error(message);
|
|
53
|
+
else if (loggingEnabled)
|
|
54
|
+
console.log(message);
|
|
55
|
+
}
|
|
56
|
+
function getReposDir() {
|
|
57
|
+
const isClaude = process.argv.join(" ").includes("claude");
|
|
58
|
+
const appName = isClaude ? "claude" : "opencode";
|
|
59
|
+
return path.join(getAppConfigDir(appName), "repos");
|
|
60
|
+
}
|
|
61
|
+
function executeGit(command, cwd) {
|
|
62
|
+
writeLog(`Executing git: ${command} in ${cwd}`);
|
|
63
|
+
try {
|
|
64
|
+
execSync(command, {
|
|
65
|
+
cwd,
|
|
66
|
+
stdio: "pipe",
|
|
67
|
+
env: { ...process.env, GCM_INTERACTIVE: "never", GIT_TERMINAL_PROMPT: "0" },
|
|
68
|
+
});
|
|
69
|
+
return true;
|
|
70
|
+
}
|
|
71
|
+
catch (error) {
|
|
72
|
+
const err = error;
|
|
73
|
+
const stderr = err.stderr ? err.stderr.toString().trim() : "";
|
|
74
|
+
writeLog(`Git error in ${cwd}: ${err.message} | stderr: ${stderr}`, true);
|
|
75
|
+
return false;
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
function resolveNpmPluginVersion(name, configDir) {
|
|
79
|
+
try {
|
|
80
|
+
const cacheDir = path.join(configDir, "cache", "node_modules");
|
|
81
|
+
const globalNpm = process.platform === "win32"
|
|
82
|
+
? path.join(os.homedir(), "AppData", "Roaming", "npm", "node_modules")
|
|
83
|
+
: path.join("/usr", "lib", "node_modules");
|
|
84
|
+
const candidates = [
|
|
85
|
+
path.join(cacheDir, name, "package.json"),
|
|
86
|
+
path.join(configDir, "node_modules", name, "package.json"),
|
|
87
|
+
path.join(globalNpm, name, "package.json"),
|
|
88
|
+
];
|
|
89
|
+
for (const p of candidates) {
|
|
90
|
+
if (fs.existsSync(p)) {
|
|
91
|
+
return JSON.parse(fs.readFileSync(p, "utf8")).version || "";
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
// Try resolving via node resolution as last resort
|
|
95
|
+
try {
|
|
96
|
+
const resolved = require.resolve(path.join(name, "package.json"));
|
|
97
|
+
return JSON.parse(fs.readFileSync(resolved, "utf8")).version || "";
|
|
98
|
+
}
|
|
99
|
+
catch { /* not resolvable */ }
|
|
100
|
+
}
|
|
101
|
+
catch { /* ignore */ }
|
|
102
|
+
return "";
|
|
103
|
+
}
|
|
104
|
+
function readOpencodeJson(configDir) {
|
|
105
|
+
const ocPath = path.join(configDir, "opencode.json");
|
|
106
|
+
if (!fs.existsSync(ocPath))
|
|
107
|
+
return { plugins: [], raw: {} };
|
|
108
|
+
try {
|
|
109
|
+
const stripped = fs.readFileSync(ocPath, "utf8").replace(/^\s*\/\/[^\n]*/gm, "");
|
|
110
|
+
const parsed = JSON.parse(stripped);
|
|
111
|
+
const plugins = (parsed.plugin || []);
|
|
112
|
+
return { plugins: plugins.filter((p) => typeof p === "string"), raw: parsed };
|
|
113
|
+
}
|
|
114
|
+
catch {
|
|
115
|
+
return { plugins: [], raw: {} };
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
function writeOpencodeJson(configDir, data) {
|
|
119
|
+
const ocPath = path.join(configDir, "opencode.json");
|
|
120
|
+
fs.writeFileSync(ocPath, JSON.stringify(data, null, 2), "utf8");
|
|
121
|
+
}
|
|
122
|
+
// ── Public API ────────────────────────────────────────────────────────────────
|
|
123
|
+
export function getNpmPlugins(configDir) {
|
|
124
|
+
const { plugins } = readOpencodeJson(configDir);
|
|
125
|
+
return plugins.map((raw) => {
|
|
126
|
+
const name = raw.replace(/@[^@/]+$/, "") || raw;
|
|
127
|
+
const version = resolveNpmPluginVersion(name, configDir);
|
|
128
|
+
return { name, version, installed: version !== "", raw };
|
|
129
|
+
});
|
|
130
|
+
}
|
|
131
|
+
export function installNpmPlugin(name, configDir) {
|
|
132
|
+
writeLog(`Installing npm plugin: ${name}`);
|
|
133
|
+
try {
|
|
134
|
+
const { plugins, raw } = readOpencodeJson(configDir);
|
|
135
|
+
if (!plugins.includes(name)) {
|
|
136
|
+
raw.plugin = [...plugins, name];
|
|
137
|
+
writeOpencodeJson(configDir, raw);
|
|
138
|
+
}
|
|
139
|
+
execSync(`npm install -g ${name}`, { stdio: "pipe" });
|
|
140
|
+
writeLog(`Installed npm plugin: ${name}`);
|
|
141
|
+
return "";
|
|
142
|
+
}
|
|
143
|
+
catch (e) {
|
|
144
|
+
const msg = e.message;
|
|
145
|
+
writeLog(`Failed to install ${name}: ${msg}`, true);
|
|
146
|
+
return msg;
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
export function uninstallNpmPlugin(name, configDir) {
|
|
150
|
+
writeLog(`Uninstalling npm plugin: ${name}`);
|
|
151
|
+
try {
|
|
152
|
+
const { plugins, raw } = readOpencodeJson(configDir);
|
|
153
|
+
raw.plugin = plugins.filter((p) => {
|
|
154
|
+
const pName = p.replace(/@[^@/]+$/, "") || p;
|
|
155
|
+
return pName !== name;
|
|
156
|
+
});
|
|
157
|
+
writeOpencodeJson(configDir, raw);
|
|
158
|
+
execSync(`npm uninstall -g ${name}`, { stdio: "pipe" });
|
|
159
|
+
writeLog(`Uninstalled npm plugin: ${name}`);
|
|
160
|
+
return "";
|
|
161
|
+
}
|
|
162
|
+
catch (e) {
|
|
163
|
+
const msg = e.message;
|
|
164
|
+
writeLog(`Failed to uninstall ${name}: ${msg}`, true);
|
|
165
|
+
return msg;
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
export function updateNpmPlugin(name, configDir, updateInterval = 1) {
|
|
169
|
+
writeLog(`Updating npm plugin: ${name}`);
|
|
170
|
+
const checkFile = path.join(configDir, "cache", `.npm-lastcheck-${name.replace(/[^a-z0-9]/gi, "_")}`);
|
|
171
|
+
try {
|
|
172
|
+
if (!fs.existsSync(path.join(configDir, "cache"))) {
|
|
173
|
+
fs.mkdirSync(path.join(configDir, "cache"), { recursive: true });
|
|
174
|
+
}
|
|
175
|
+
const lastCheck = fs.existsSync(checkFile)
|
|
176
|
+
? parseInt(fs.readFileSync(checkFile, "utf8"), 10)
|
|
177
|
+
: 0;
|
|
178
|
+
const elapsed = Date.now() - lastCheck;
|
|
179
|
+
if (elapsed < updateInterval * 3_600_000) {
|
|
180
|
+
writeLog(`Skipping npm update for ${name} (checked ${Math.floor(elapsed / 60_000)} min ago)`);
|
|
181
|
+
return "";
|
|
182
|
+
}
|
|
183
|
+
fs.writeFileSync(checkFile, Date.now().toString());
|
|
184
|
+
execSync(`npm update -g ${name}`, { stdio: "pipe" });
|
|
185
|
+
writeLog(`Updated npm plugin: ${name}`);
|
|
186
|
+
return "";
|
|
187
|
+
}
|
|
188
|
+
catch (e) {
|
|
189
|
+
const msg = e.message;
|
|
190
|
+
writeLog(`Failed to update ${name}: ${msg}`, true);
|
|
191
|
+
return msg;
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
function selfUpdate(configDir) {
|
|
195
|
+
writeLog("Running self-update for plugin-updater");
|
|
196
|
+
updateNpmPlugin("plugin-updater", configDir);
|
|
197
|
+
}
|
|
198
|
+
function updatePlugin(pluginName, gitUrl, branch, commitHash, updateInterval = 1) {
|
|
199
|
+
const reposDir = getReposDir();
|
|
200
|
+
const targetDir = path.join(reposDir, pluginName);
|
|
201
|
+
const lastCheckFile = path.join(targetDir, ".lastcheck");
|
|
202
|
+
let didChange = false;
|
|
203
|
+
if (!fs.existsSync(targetDir)) {
|
|
204
|
+
if (!fs.existsSync(reposDir))
|
|
205
|
+
fs.mkdirSync(reposDir, { recursive: true });
|
|
206
|
+
const branchFlag = branch ? `--branch ${branch}` : "";
|
|
207
|
+
executeGit(`git clone --recurse-submodules ${branchFlag} ${gitUrl} ${pluginName}`, reposDir);
|
|
208
|
+
fs.writeFileSync(lastCheckFile, Date.now().toString());
|
|
209
|
+
didChange = true;
|
|
210
|
+
}
|
|
211
|
+
else {
|
|
212
|
+
const lastCheck = fs.existsSync(lastCheckFile)
|
|
213
|
+
? parseInt(fs.readFileSync(lastCheckFile, "utf8"), 10)
|
|
214
|
+
: 0;
|
|
215
|
+
const intervalMs = updateInterval * 3_600_000;
|
|
216
|
+
const elapsed = Date.now() - lastCheck;
|
|
217
|
+
if (elapsed < intervalMs) {
|
|
218
|
+
writeLog(`Fast-path: ${pluginName} skipping update check (checked ${Math.floor(elapsed / 60_000)} min ago, interval ${updateInterval}h)`);
|
|
219
|
+
return { success: true, changed: false };
|
|
220
|
+
}
|
|
221
|
+
fs.writeFileSync(lastCheckFile, Date.now().toString());
|
|
222
|
+
executeGit("git fetch origin", targetDir);
|
|
223
|
+
let beforeHash = "";
|
|
224
|
+
try {
|
|
225
|
+
beforeHash = execSync("git rev-parse HEAD", { cwd: targetDir }).toString().trim();
|
|
226
|
+
}
|
|
227
|
+
catch { /* ignore */ }
|
|
228
|
+
if (commitHash) {
|
|
229
|
+
executeGit(`git checkout ${commitHash}`, targetDir);
|
|
230
|
+
}
|
|
231
|
+
else if (branch) {
|
|
232
|
+
executeGit(`git checkout ${branch}`, targetDir);
|
|
233
|
+
executeGit(`git pull --ff-only origin ${branch}`, targetDir);
|
|
234
|
+
}
|
|
235
|
+
else {
|
|
236
|
+
executeGit("git checkout main || git checkout master", targetDir);
|
|
237
|
+
executeGit("git pull --ff-only", targetDir);
|
|
238
|
+
}
|
|
239
|
+
executeGit("git submodule update --init --recursive", targetDir);
|
|
240
|
+
let afterHash = "";
|
|
241
|
+
try {
|
|
242
|
+
afterHash = execSync("git rev-parse HEAD", { cwd: targetDir }).toString().trim();
|
|
243
|
+
}
|
|
244
|
+
catch { /* ignore */ }
|
|
245
|
+
if (beforeHash !== afterHash)
|
|
246
|
+
didChange = true;
|
|
247
|
+
}
|
|
248
|
+
return { success: true, changed: didChange };
|
|
249
|
+
}
|
|
250
|
+
async function callPluginCleanup(pluginExecutionFile, configDir) {
|
|
251
|
+
if (!fs.existsSync(pluginExecutionFile))
|
|
252
|
+
return;
|
|
253
|
+
try {
|
|
254
|
+
const mod = await import(pluginExecutionFile);
|
|
255
|
+
if (typeof mod.cleanup === "function") {
|
|
256
|
+
writeLog(`Calling cleanup() on ${pluginExecutionFile}`);
|
|
257
|
+
await mod.cleanup(configDir);
|
|
258
|
+
writeLog(`cleanup() complete for ${pluginExecutionFile}`);
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
catch (e) {
|
|
262
|
+
writeLog(`cleanup() call failed for ${pluginExecutionFile}: ${e.message}`, true);
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
async function deployToExecutionDir(pluginName, executionPath, changed, configDir) {
|
|
266
|
+
const sourceDir = path.join(getReposDir(), pluginName);
|
|
267
|
+
if (!fs.existsSync(sourceDir))
|
|
268
|
+
return false;
|
|
269
|
+
const packageJsonPath = path.join(sourceDir, "package.json");
|
|
270
|
+
let entryFile = "index.js";
|
|
271
|
+
const pluginExecutionFile = path.join(executionPath, `${pluginName}.js`);
|
|
272
|
+
if (!changed && fs.existsSync(pluginExecutionFile)) {
|
|
273
|
+
writeLog(`Skipping install/build for ${pluginName} (no changes and deployed file exists)`);
|
|
274
|
+
}
|
|
275
|
+
else {
|
|
276
|
+
if (fs.existsSync(packageJsonPath)) {
|
|
277
|
+
try {
|
|
278
|
+
writeLog(`Running npm install for ${pluginName}`);
|
|
279
|
+
execSync("npm install", { cwd: sourceDir, stdio: "ignore" });
|
|
280
|
+
writeLog(`Finished npm install for ${pluginName}`);
|
|
281
|
+
const pkg = JSON.parse(fs.readFileSync(packageJsonPath, "utf8"));
|
|
282
|
+
if (pkg.main)
|
|
283
|
+
entryFile = pkg.main;
|
|
284
|
+
if (pkg.scripts?.build) {
|
|
285
|
+
execSync("npm run build", { cwd: sourceDir, stdio: "ignore" });
|
|
286
|
+
writeLog(`Finished npm run build for ${pluginName}`);
|
|
287
|
+
}
|
|
288
|
+
else {
|
|
289
|
+
writeLog(`Skipped npm run build for ${pluginName} (no build script found)`);
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
catch (error) {
|
|
293
|
+
const err = error;
|
|
294
|
+
writeLog(`Build/Install failed for ${pluginName}: ${err.message}`, true);
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
if (fs.existsSync(packageJsonPath)) {
|
|
299
|
+
try {
|
|
300
|
+
const pkg = JSON.parse(fs.readFileSync(packageJsonPath, "utf8"));
|
|
301
|
+
if (pkg.main)
|
|
302
|
+
entryFile = pkg.main;
|
|
303
|
+
}
|
|
304
|
+
catch { /* ignore */ }
|
|
305
|
+
}
|
|
306
|
+
const distPath = path.join(sourceDir, "dist");
|
|
307
|
+
let deploySource = path.join(sourceDir, entryFile);
|
|
308
|
+
if (fs.existsSync(path.join(distPath, entryFile))) {
|
|
309
|
+
deploySource = path.join(distPath, entryFile);
|
|
310
|
+
}
|
|
311
|
+
else if (fs.existsSync(path.join(distPath, "index.js"))) {
|
|
312
|
+
deploySource = path.join(distPath, "index.js");
|
|
313
|
+
}
|
|
314
|
+
if (!fs.existsSync(executionPath))
|
|
315
|
+
fs.mkdirSync(executionPath, { recursive: true });
|
|
316
|
+
await callPluginCleanup(pluginExecutionFile, configDir);
|
|
317
|
+
try {
|
|
318
|
+
writeLog(`Running copy for ${pluginName}`);
|
|
319
|
+
fs.copyFileSync(deploySource, pluginExecutionFile);
|
|
320
|
+
writeLog(`Finished copy for ${pluginName}`);
|
|
321
|
+
}
|
|
322
|
+
catch (e) {
|
|
323
|
+
const err = e;
|
|
324
|
+
writeLog(`Copy failed for ${pluginName}: ${err.message}`, true);
|
|
325
|
+
}
|
|
326
|
+
return true;
|
|
327
|
+
}
|
|
328
|
+
async function pluginUpdaterEntry(input) {
|
|
329
|
+
const isClaude = process.argv.join(" ").includes("claude");
|
|
330
|
+
const appName = isClaude ? "claude" : "opencode";
|
|
331
|
+
const configDir = getAppConfigDir(appName);
|
|
332
|
+
const pluginsDir = path.join(configDir, "plugin");
|
|
333
|
+
writeLog(`Starting plugin updater for ${appName}`);
|
|
334
|
+
if (input?.action === "updatePlugin" && input.configDir && input.pluginName && input.gitUrl) {
|
|
335
|
+
EARLY_LAUNCH_CONFIG_DIR = input.configDir;
|
|
336
|
+
writeLog(`Direct update request for ${input.pluginName}`);
|
|
337
|
+
const updateResult = updatePlugin(input.pluginName, input.gitUrl, input.branch, input.commitHash ?? null);
|
|
338
|
+
await deployToExecutionDir(input.pluginName, pluginsDir, updateResult.changed, input.configDir);
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
export async function updatePluginPublic(pluginName, gitUrl, branch, commitHash) {
|
|
342
|
+
writeLog(`Public API update call for ${pluginName}`);
|
|
343
|
+
const appName = process.argv.join(" ").includes("claude") ? "claude" : "opencode";
|
|
344
|
+
const configDir = getAppConfigDir(appName);
|
|
345
|
+
const result = updatePlugin(pluginName, gitUrl, branch, commitHash ?? null);
|
|
346
|
+
await deployToExecutionDir(pluginName, path.join(configDir, "plugin"), result.changed, configDir);
|
|
347
|
+
}
|
|
348
|
+
export async function earlyLaunch(configDir, plugins) {
|
|
349
|
+
EARLY_LAUNCH_CONFIG_DIR = configDir;
|
|
350
|
+
writeLog("Starting earlyLaunch updater sequence");
|
|
351
|
+
// Self-update first
|
|
352
|
+
selfUpdate(configDir);
|
|
353
|
+
// Update npm plugins listed in opencode.json
|
|
354
|
+
const { plugins: npmNames } = readOpencodeJson(configDir);
|
|
355
|
+
for (const raw of npmNames) {
|
|
356
|
+
const name = raw.replace(/@[^@/]+$/, "") || raw;
|
|
357
|
+
if (name === "plugin-updater")
|
|
358
|
+
continue; // already self-updated above
|
|
359
|
+
writeLog(`npm earlyLaunch update for ${name}`);
|
|
360
|
+
try {
|
|
361
|
+
updateNpmPlugin(name, configDir);
|
|
362
|
+
}
|
|
363
|
+
catch (e) {
|
|
364
|
+
writeLog(`Failed npm update for ${name}: ${e.message}`, true);
|
|
365
|
+
}
|
|
366
|
+
}
|
|
367
|
+
// Git plugins
|
|
368
|
+
if (!plugins || !Array.isArray(plugins)) {
|
|
369
|
+
writeLog("No git plugins provided to earlyLaunch", true);
|
|
370
|
+
return;
|
|
371
|
+
}
|
|
372
|
+
for (const plugin of plugins) {
|
|
373
|
+
if (!plugin.enabled)
|
|
374
|
+
continue;
|
|
375
|
+
if (plugin.autoUpdate === false)
|
|
376
|
+
continue;
|
|
377
|
+
if (!plugin.url)
|
|
378
|
+
continue;
|
|
379
|
+
writeLog(`Processing earlyLaunch for ${plugin.name}`);
|
|
380
|
+
try {
|
|
381
|
+
const updateResult = updatePlugin(plugin.name, plugin.url, plugin.branch, null, plugin.updateInterval ?? 1);
|
|
382
|
+
await deployToExecutionDir(plugin.name, path.join(configDir, "plugin"), updateResult.changed, configDir);
|
|
383
|
+
}
|
|
384
|
+
catch (e) {
|
|
385
|
+
const err = e;
|
|
386
|
+
writeLog(`Failed to process ${plugin.name}: ${err.message}`, true);
|
|
387
|
+
}
|
|
388
|
+
}
|
|
389
|
+
}
|
|
390
|
+
pluginUpdaterEntry(null);
|
package/package.json
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "plugin-updater",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.22",
|
|
4
4
|
"description": "Plugin lifecycle manager for OpenCode and Claude Code launchers",
|
|
5
5
|
"type": "module",
|
|
6
|
-
"main": "index.js",
|
|
6
|
+
"main": "dist/index.js",
|
|
7
7
|
"license": "MIT",
|
|
8
8
|
"author": "intisy",
|
|
9
9
|
"repository": {
|
|
@@ -16,5 +16,21 @@
|
|
|
16
16
|
"plugin",
|
|
17
17
|
"updater",
|
|
18
18
|
"lifecycle"
|
|
19
|
-
]
|
|
19
|
+
],
|
|
20
|
+
"files": [
|
|
21
|
+
"dist/",
|
|
22
|
+
"README.md",
|
|
23
|
+
"LICENSE"
|
|
24
|
+
],
|
|
25
|
+
"engines": {
|
|
26
|
+
"node": ">=20.0.0"
|
|
27
|
+
},
|
|
28
|
+
"scripts": {
|
|
29
|
+
"build": "tsc",
|
|
30
|
+
"prepublishOnly": "npm run build"
|
|
31
|
+
},
|
|
32
|
+
"devDependencies": {
|
|
33
|
+
"@types/node": "^22.0.0",
|
|
34
|
+
"typescript": "^5.0.0"
|
|
35
|
+
}
|
|
20
36
|
}
|
|
@@ -1,29 +0,0 @@
|
|
|
1
|
-
name: Publish to npm
|
|
2
|
-
|
|
3
|
-
on:
|
|
4
|
-
push:
|
|
5
|
-
tags:
|
|
6
|
-
- 'v*'
|
|
7
|
-
|
|
8
|
-
permissions:
|
|
9
|
-
id-token: write
|
|
10
|
-
contents: read
|
|
11
|
-
|
|
12
|
-
jobs:
|
|
13
|
-
publish:
|
|
14
|
-
runs-on: ubuntu-latest
|
|
15
|
-
steps:
|
|
16
|
-
- uses: actions/checkout@v4
|
|
17
|
-
|
|
18
|
-
- uses: actions/setup-node@v4
|
|
19
|
-
with:
|
|
20
|
-
node-version: '24'
|
|
21
|
-
registry-url: 'https://registry.npmjs.org'
|
|
22
|
-
|
|
23
|
-
- name: Set version from tag
|
|
24
|
-
run: |
|
|
25
|
-
VERSION="${{ github.ref_name }}"
|
|
26
|
-
VERSION="${VERSION#v}"
|
|
27
|
-
npm version $VERSION --allow-same-version --no-git-tag-version
|
|
28
|
-
|
|
29
|
-
- run: npm publish --provenance --access public
|
package/.idea/misc.xml
DELETED
|
@@ -1,7 +0,0 @@
|
|
|
1
|
-
<?xml version="1.0" encoding="UTF-8"?>
|
|
2
|
-
<project version="4">
|
|
3
|
-
<component name="KubernetesApiProvider"><![CDATA[{}]]></component>
|
|
4
|
-
<component name="ProjectRootManager" version="2" languageLevel="JDK_25" default="true" project-jdk-name="25" project-jdk-type="JavaSDK">
|
|
5
|
-
<output url="file://$PROJECT_DIR$/out" />
|
|
6
|
-
</component>
|
|
7
|
-
</project>
|
package/.idea/modules.xml
DELETED
|
@@ -1,8 +0,0 @@
|
|
|
1
|
-
<?xml version="1.0" encoding="UTF-8"?>
|
|
2
|
-
<project version="4">
|
|
3
|
-
<component name="ProjectModuleManager">
|
|
4
|
-
<modules>
|
|
5
|
-
<module fileurl="file://$PROJECT_DIR$/.idea/plugin-updater.iml" filepath="$PROJECT_DIR$/.idea/plugin-updater.iml" />
|
|
6
|
-
</modules>
|
|
7
|
-
</component>
|
|
8
|
-
</project>
|
package/.idea/plugin-updater.iml
DELETED
|
@@ -1,9 +0,0 @@
|
|
|
1
|
-
<?xml version="1.0" encoding="UTF-8"?>
|
|
2
|
-
<module type="JAVA_MODULE" version="4">
|
|
3
|
-
<component name="NewModuleRootManager" inherit-compiler-output="true">
|
|
4
|
-
<exclude-output />
|
|
5
|
-
<content url="file://$MODULE_DIR$" />
|
|
6
|
-
<orderEntry type="inheritedJdk" />
|
|
7
|
-
<orderEntry type="sourceFolder" forTests="false" />
|
|
8
|
-
</component>
|
|
9
|
-
</module>
|
package/.idea/vcs.xml
DELETED
package/SPEC.md
DELETED
|
@@ -1,13 +0,0 @@
|
|
|
1
|
-
# Plugin Updater - Specifications & Test Requirements
|
|
2
|
-
|
|
3
|
-
## Goal
|
|
4
|
-
Reliable core update mechanism for all OpenCode and Claude Code plugins.
|
|
5
|
-
|
|
6
|
-
## Requirements
|
|
7
|
-
- [ ] **Installation Order**: Must be installed FIRST in OpenCode, as it is responsible for installing all other plugins.
|
|
8
|
-
- [ ] **Reliability**: Must never fail or crash, as the entire ecosystem depends on it.
|
|
9
|
-
- [ ] **Launch Detection (Early Launch)**:
|
|
10
|
-
- The updater exports an `earlyLaunch(configDir)` function.
|
|
11
|
-
- Hub plugins (opencode-hub / claude-hub) MUST detect the updater and call `earlyLaunch` before OpenCode invokes it, deferring update flow management to the Hub.
|
|
12
|
-
- If launched directly via the normal application command (no hub / optional dependency), the updater executes its update routine automatically and MUST NOT install the Hub, as the Hub is strictly optional.
|
|
13
|
-
- Path resolution relies on the `configDir` passed by `earlyLaunch` or inferred from `process.argv`/input, NEVER relying on static environment variables like `CC_LAUNCHER`.
|
package/index.js
DELETED
|
@@ -1,227 +0,0 @@
|
|
|
1
|
-
import fs from 'fs';
|
|
2
|
-
import path from 'path';
|
|
3
|
-
import os from 'os';
|
|
4
|
-
import { execSync } from 'child_process';
|
|
5
|
-
|
|
6
|
-
let EARLY_LAUNCH_CONFIG_DIR = null;
|
|
7
|
-
const START_TIME = new Date().toISOString().replace(/:/g, '-').split('.')[0];
|
|
8
|
-
|
|
9
|
-
function getAppConfigDir(appName) {
|
|
10
|
-
if (EARLY_LAUNCH_CONFIG_DIR) {
|
|
11
|
-
return EARLY_LAUNCH_CONFIG_DIR;
|
|
12
|
-
}
|
|
13
|
-
const home = os.homedir();
|
|
14
|
-
const directPath = path.join(home, `.${appName}`);
|
|
15
|
-
const configPath = path.join(home, ".config", appName);
|
|
16
|
-
return fs.existsSync(directPath) ? directPath : configPath;
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
function writeLog(message, isError = false) {
|
|
20
|
-
try {
|
|
21
|
-
const date = new Date();
|
|
22
|
-
const dateStr = date.toISOString().split('T')[0];
|
|
23
|
-
const isClaude = process.argv.join(' ').includes('claude');
|
|
24
|
-
const appName = isClaude ? "claude" : "opencode";
|
|
25
|
-
const configDir = getAppConfigDir(appName);
|
|
26
|
-
|
|
27
|
-
const logsDir = path.join(configDir, "logs", dateStr);
|
|
28
|
-
if (!fs.existsSync(logsDir)) {
|
|
29
|
-
fs.mkdirSync(logsDir, { recursive: true });
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
const logFile = path.join(logsDir, `updater-${START_TIME}.log`);
|
|
33
|
-
const prefix = isError ? "[ERROR]" : "[INFO]";
|
|
34
|
-
const logMsg = `[${date.toISOString()}] ${prefix} ${message}\n`;
|
|
35
|
-
|
|
36
|
-
fs.appendFileSync(logFile, logMsg);
|
|
37
|
-
} catch (e) {
|
|
38
|
-
}
|
|
39
|
-
if (isError) console.error(message);
|
|
40
|
-
else console.log(message);
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
function getReposDir() {
|
|
44
|
-
const isClaude = process.argv.join(' ').includes('claude');
|
|
45
|
-
const appName = isClaude ? "claude" : "opencode";
|
|
46
|
-
return path.join(getAppConfigDir(appName), "repos");
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
function executeGit(command, cwd) {
|
|
50
|
-
writeLog(`Executing git: ${command} in ${cwd}`);
|
|
51
|
-
try {
|
|
52
|
-
execSync(command, {
|
|
53
|
-
cwd,
|
|
54
|
-
stdio: "pipe",
|
|
55
|
-
env: { ...process.env, GCM_INTERACTIVE: 'never', GIT_TERMINAL_PROMPT: '0' }
|
|
56
|
-
});
|
|
57
|
-
return true;
|
|
58
|
-
} catch (error) {
|
|
59
|
-
const stderr = error.stderr ? error.stderr.toString().trim() : '';
|
|
60
|
-
writeLog(`Git error in ${cwd}: ${error.message} | stderr: ${stderr}`, true);
|
|
61
|
-
return false;
|
|
62
|
-
}
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
function updatePlugin(pluginName, gitUrl, branch, commitHash) {
|
|
66
|
-
const reposDir = getReposDir();
|
|
67
|
-
const targetDir = path.join(reposDir, pluginName);
|
|
68
|
-
|
|
69
|
-
if (!fs.existsSync(targetDir)) {
|
|
70
|
-
if (!fs.existsSync(reposDir)) fs.mkdirSync(reposDir, { recursive: true });
|
|
71
|
-
const branchFlag = branch ? `--branch ${branch}` : "";
|
|
72
|
-
executeGit(`git clone --recurse-submodules ${branchFlag} ${gitUrl} ${pluginName}`, reposDir);
|
|
73
|
-
} else {
|
|
74
|
-
executeGit("git fetch origin", targetDir);
|
|
75
|
-
if (commitHash) {
|
|
76
|
-
executeGit(`git checkout ${commitHash}`, targetDir);
|
|
77
|
-
} else if (branch) {
|
|
78
|
-
executeGit(`git checkout ${branch}`, targetDir);
|
|
79
|
-
executeGit(`git pull --ff-only origin ${branch}`, targetDir);
|
|
80
|
-
} else {
|
|
81
|
-
executeGit("git checkout main || git checkout master", targetDir);
|
|
82
|
-
executeGit("git pull --ff-only", targetDir);
|
|
83
|
-
}
|
|
84
|
-
executeGit("git submodule update --init --recursive", targetDir);
|
|
85
|
-
}
|
|
86
|
-
return true;
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
function deployToExecutionDir(pluginName, executionPath) {
|
|
90
|
-
const sourceDir = path.join(getReposDir(), pluginName);
|
|
91
|
-
if (!fs.existsSync(sourceDir)) return false;
|
|
92
|
-
|
|
93
|
-
const packageJsonPath = path.join(sourceDir, "package.json");
|
|
94
|
-
let entryFile = "index.js";
|
|
95
|
-
|
|
96
|
-
if (fs.existsSync(packageJsonPath)) {
|
|
97
|
-
try {
|
|
98
|
-
writeLog(`Running npm install for ${pluginName}`);
|
|
99
|
-
execSync("npm install", { cwd: sourceDir, stdio: "ignore" });
|
|
100
|
-
writeLog(`Finished npm install for ${pluginName}`);
|
|
101
|
-
|
|
102
|
-
const pkg = JSON.parse(fs.readFileSync(packageJsonPath, 'utf8'));
|
|
103
|
-
if (pkg.main) {
|
|
104
|
-
entryFile = pkg.main;
|
|
105
|
-
}
|
|
106
|
-
|
|
107
|
-
if (pkg.scripts && pkg.scripts.build) {
|
|
108
|
-
execSync("npm run build", { cwd: sourceDir, stdio: "ignore" });
|
|
109
|
-
writeLog(`Finished npm run build for ${pluginName}`);
|
|
110
|
-
} else {
|
|
111
|
-
writeLog(`Skipped npm run build for ${pluginName} (no build script found)`);
|
|
112
|
-
}
|
|
113
|
-
} catch (error) {
|
|
114
|
-
writeLog(`Build/Install failed for ${pluginName}: ${error.message}`, true);
|
|
115
|
-
}
|
|
116
|
-
}
|
|
117
|
-
|
|
118
|
-
const distPath = path.join(sourceDir, "dist");
|
|
119
|
-
let deploySource = path.join(sourceDir, entryFile);
|
|
120
|
-
|
|
121
|
-
if (fs.existsSync(path.join(distPath, entryFile))) {
|
|
122
|
-
deploySource = path.join(distPath, entryFile);
|
|
123
|
-
} else if (fs.existsSync(path.join(distPath, "index.js"))) {
|
|
124
|
-
deploySource = path.join(distPath, "index.js");
|
|
125
|
-
}
|
|
126
|
-
|
|
127
|
-
const pluginExecutionFile = path.join(executionPath, `${pluginName}.js`);
|
|
128
|
-
|
|
129
|
-
if (!fs.existsSync(executionPath)) {
|
|
130
|
-
fs.mkdirSync(executionPath, { recursive: true });
|
|
131
|
-
}
|
|
132
|
-
|
|
133
|
-
try {
|
|
134
|
-
writeLog(`Running copy for ${pluginName}`);
|
|
135
|
-
fs.copyFileSync(deploySource, pluginExecutionFile);
|
|
136
|
-
writeLog(`Finished copy for ${pluginName}`);
|
|
137
|
-
} catch (e) {
|
|
138
|
-
writeLog(`Copy failed for ${pluginName}: ${e.message}`, true);
|
|
139
|
-
}
|
|
140
|
-
return true;
|
|
141
|
-
}
|
|
142
|
-
|
|
143
|
-
async function pluginUpdaterEntry(input) {
|
|
144
|
-
const isClaude = process.argv.join(' ').includes('claude');
|
|
145
|
-
const configDir = getAppConfigDir(isClaude ? "claude" : "opencode");
|
|
146
|
-
|
|
147
|
-
const reposDir = path.join(configDir, "repos");
|
|
148
|
-
const pluginsDir = path.join(configDir, "plugin");
|
|
149
|
-
if (!fs.existsSync(reposDir)) fs.mkdirSync(reposDir, { recursive: true });
|
|
150
|
-
if (!fs.existsSync(pluginsDir)) fs.mkdirSync(pluginsDir, { recursive: true });
|
|
151
|
-
|
|
152
|
-
writeLog(`plugin-updater activated. configDir=${configDir}`);
|
|
153
|
-
|
|
154
|
-
if (!global.__PLUGIN_UPDATER_HANDLED_BY_HUB__) {
|
|
155
|
-
EARLY_LAUNCH_CONFIG_DIR = configDir;
|
|
156
|
-
global.__PLUGIN_UPDATER_HANDLED_BY_HUB__ = true;
|
|
157
|
-
|
|
158
|
-
const pluginsJsonPath = path.join(configDir, "config", "plugins.json");
|
|
159
|
-
if (fs.existsSync(pluginsJsonPath)) {
|
|
160
|
-
try {
|
|
161
|
-
const plugins = JSON.parse(fs.readFileSync(pluginsJsonPath, "utf-8"));
|
|
162
|
-
for (const plugin of plugins) {
|
|
163
|
-
if (plugin.url && plugin.enabled !== false && plugin.type !== "npm") {
|
|
164
|
-
const branch = plugin.branch || null;
|
|
165
|
-
const commit = plugin.commit || null;
|
|
166
|
-
updatePlugin(plugin.name, plugin.url, branch, commit);
|
|
167
|
-
deployToExecutionDir(plugin.name, pluginsDir);
|
|
168
|
-
}
|
|
169
|
-
}
|
|
170
|
-
} catch (e) {
|
|
171
|
-
writeLog(`Failed to parse plugins.json: ${e.message}`, true);
|
|
172
|
-
}
|
|
173
|
-
}
|
|
174
|
-
}
|
|
175
|
-
|
|
176
|
-
return {};
|
|
177
|
-
}
|
|
178
|
-
|
|
179
|
-
pluginUpdaterEntry.earlyLaunch = function(configDir) {
|
|
180
|
-
EARLY_LAUNCH_CONFIG_DIR = configDir;
|
|
181
|
-
global.__PLUGIN_UPDATER_HANDLED_BY_HUB__ = true;
|
|
182
|
-
};
|
|
183
|
-
pluginUpdaterEntry.updatePlugin = updatePlugin;
|
|
184
|
-
pluginUpdaterEntry.deployToExecutionDir = deployToExecutionDir;
|
|
185
|
-
pluginUpdaterEntry.rebuild = function(pluginName) {
|
|
186
|
-
const isClaude = process.argv.join(' ').includes('claude');
|
|
187
|
-
const configDir = getAppConfigDir(isClaude ? "claude" : "opencode");
|
|
188
|
-
deployToExecutionDir(pluginName, path.join(configDir, "plugin"));
|
|
189
|
-
return "Rebuilt " + pluginName;
|
|
190
|
-
};
|
|
191
|
-
pluginUpdaterEntry.downgrade = function(pluginName, commitHash) {
|
|
192
|
-
const reposDir = getReposDir();
|
|
193
|
-
const targetDir = path.join(reposDir, pluginName);
|
|
194
|
-
if (fs.existsSync(targetDir)) {
|
|
195
|
-
executeGit(`git fetch origin`, targetDir);
|
|
196
|
-
executeGit(`git checkout ${commitHash}`, targetDir);
|
|
197
|
-
executeGit(`git submodule update --init --recursive`, targetDir);
|
|
198
|
-
return pluginUpdaterEntry.rebuild(pluginName);
|
|
199
|
-
}
|
|
200
|
-
return "Repo not found";
|
|
201
|
-
};
|
|
202
|
-
pluginUpdaterEntry.disable = function(plugin) {
|
|
203
|
-
const isClaude = process.argv.join(' ').includes('claude');
|
|
204
|
-
const configDir = getAppConfigDir(isClaude ? "claude" : "opencode");
|
|
205
|
-
const pluginsJsonPath = path.join(configDir, "config", "plugins.json");
|
|
206
|
-
if (fs.existsSync(pluginsJsonPath)) {
|
|
207
|
-
let plugins = JSON.parse(fs.readFileSync(pluginsJsonPath, "utf-8"));
|
|
208
|
-
const pluginIndex = plugins.findIndex(p => p.name === plugin.name);
|
|
209
|
-
if (pluginIndex >= 0) {
|
|
210
|
-
plugins[pluginIndex].enabled = false;
|
|
211
|
-
fs.writeFileSync(pluginsJsonPath, JSON.stringify(plugins, null, 2), "utf-8");
|
|
212
|
-
}
|
|
213
|
-
}
|
|
214
|
-
const pluginExecutionPath = path.join(configDir, "plugin", `${plugin.name}.js`);
|
|
215
|
-
if (fs.existsSync(pluginExecutionPath)) {
|
|
216
|
-
try { fs.rmSync(pluginExecutionPath, { force: true }); } catch (e) {}
|
|
217
|
-
}
|
|
218
|
-
};
|
|
219
|
-
pluginUpdaterEntry.uninstall = function(plugin) {
|
|
220
|
-
pluginUpdaterEntry.disable(plugin);
|
|
221
|
-
const targetDir = path.join(getReposDir(), plugin.name);
|
|
222
|
-
if (fs.existsSync(targetDir)) {
|
|
223
|
-
try { fs.rmSync(targetDir, { recursive: true, force: true }); } catch (e) {}
|
|
224
|
-
}
|
|
225
|
-
};
|
|
226
|
-
|
|
227
|
-
export default pluginUpdaterEntry;
|