plugin-updater 1.0.38 → 1.0.39
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/config.d.ts +8 -0
- package/dist/config.js +48 -0
- package/dist/daemon.d.ts +1 -0
- package/dist/daemon.js +55 -0
- package/dist/deploy.d.ts +1 -0
- package/dist/deploy.js +135 -0
- package/dist/env.d.ts +5 -0
- package/dist/env.js +32 -0
- package/dist/git.d.ts +6 -0
- package/dist/git.js +128 -0
- package/dist/index.d.ts +3 -21
- package/dist/index.js +12 -566
- package/dist/log.d.ts +2 -0
- package/dist/log.js +43 -0
- package/dist/npm.d.ts +8 -0
- package/dist/npm.js +137 -0
- package/dist/types.d.ts +20 -0
- package/dist/types.js +1 -0
- package/package.json +1 -1
package/dist/config.d.ts
ADDED
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import type { Plugin } from "./types.js";
|
|
2
|
+
export declare function readOpencodeJson(configDir: string): {
|
|
3
|
+
plugins: string[];
|
|
4
|
+
raw: Record<string, unknown>;
|
|
5
|
+
};
|
|
6
|
+
export declare function writeOpencodeJson(configDir: string, data: Record<string, unknown>): void;
|
|
7
|
+
export declare function getPluginsPath(configDir: string): string;
|
|
8
|
+
export declare function getPlugins(configDir: string): Plugin[];
|
package/dist/config.js
ADDED
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import fs from "fs";
|
|
2
|
+
import path from "path";
|
|
3
|
+
import { isOpencodeHookInvocation } from "./env.js";
|
|
4
|
+
import { writeLog } from "./log.js";
|
|
5
|
+
export function readOpencodeJson(configDir) {
|
|
6
|
+
const ocPath = path.join(configDir, "opencode.json");
|
|
7
|
+
if (!fs.existsSync(ocPath))
|
|
8
|
+
return { plugins: [], raw: {} };
|
|
9
|
+
try {
|
|
10
|
+
const stripped = fs.readFileSync(ocPath, "utf8").replace(/^\s*\/\/[^\n]*/gm, "");
|
|
11
|
+
const parsed = JSON.parse(stripped);
|
|
12
|
+
const plugins = (parsed.plugin || []);
|
|
13
|
+
return { plugins: plugins.filter((p) => typeof p === "string"), raw: parsed };
|
|
14
|
+
}
|
|
15
|
+
catch {
|
|
16
|
+
return { plugins: [], raw: {} };
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
export function writeOpencodeJson(configDir, data) {
|
|
20
|
+
const ocPath = path.join(configDir, "opencode.json");
|
|
21
|
+
fs.writeFileSync(ocPath, JSON.stringify(data, null, 2), "utf8");
|
|
22
|
+
}
|
|
23
|
+
export function getPluginsPath(configDir) {
|
|
24
|
+
if (isOpencodeHookInvocation(configDir))
|
|
25
|
+
return "";
|
|
26
|
+
const preferred = path.join(configDir, "config", "plugins.json");
|
|
27
|
+
const fallback = path.join(configDir, "plugins.json");
|
|
28
|
+
if (fs.existsSync(preferred))
|
|
29
|
+
return preferred;
|
|
30
|
+
if (fs.existsSync(fallback))
|
|
31
|
+
return fallback;
|
|
32
|
+
return preferred;
|
|
33
|
+
}
|
|
34
|
+
// single source of truth for the git-plugin list; consumers (loaders, TUI)
|
|
35
|
+
// must read through this rather than touching plugins.json directly
|
|
36
|
+
export function getPlugins(configDir) {
|
|
37
|
+
if (isOpencodeHookInvocation(configDir))
|
|
38
|
+
return [];
|
|
39
|
+
const file = getPluginsPath(configDir);
|
|
40
|
+
try {
|
|
41
|
+
if (fs.existsSync(file))
|
|
42
|
+
return JSON.parse(fs.readFileSync(file, "utf-8"));
|
|
43
|
+
}
|
|
44
|
+
catch (e) {
|
|
45
|
+
writeLog(`Failed to parse ${file}: ${e.message}`, true);
|
|
46
|
+
}
|
|
47
|
+
return [];
|
|
48
|
+
}
|
package/dist/daemon.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function startDeclaredDaemon(sourceDir: string, configDir: string, pluginName: string): Promise<void>;
|
package/dist/daemon.js
ADDED
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import fs from "fs";
|
|
2
|
+
import path from "path";
|
|
3
|
+
import { getAppName } from "./env.js";
|
|
4
|
+
import { writeLog } from "./log.js";
|
|
5
|
+
async function isDaemonHealthy(url) {
|
|
6
|
+
try {
|
|
7
|
+
const controller = new AbortController();
|
|
8
|
+
const timer = setTimeout(() => controller.abort(), 1500);
|
|
9
|
+
const res = await fetch(url, { signal: controller.signal });
|
|
10
|
+
clearTimeout(timer);
|
|
11
|
+
return res.ok;
|
|
12
|
+
}
|
|
13
|
+
catch {
|
|
14
|
+
return false;
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
// idempotent: health-check the declared endpoint, spawn detached only if down.
|
|
18
|
+
// the daemon outlives this process so the proxy persists across the session.
|
|
19
|
+
export async function startDeclaredDaemon(sourceDir, configDir, pluginName) {
|
|
20
|
+
try {
|
|
21
|
+
const pkg = JSON.parse(fs.readFileSync(path.join(sourceDir, "package.json"), "utf8"));
|
|
22
|
+
const daemon = pkg.claudeHub?.daemon;
|
|
23
|
+
if (!daemon?.script)
|
|
24
|
+
return;
|
|
25
|
+
const healthUrl = daemon.healthCheckUrl
|
|
26
|
+
|| (daemon.port ? `http://127.0.0.1:${daemon.port}/health` : "");
|
|
27
|
+
if (healthUrl && (await isDaemonHealthy(healthUrl))) {
|
|
28
|
+
writeLog(`Daemon for ${pluginName} already running`);
|
|
29
|
+
return;
|
|
30
|
+
}
|
|
31
|
+
const scriptPath = path.join(sourceDir, daemon.script);
|
|
32
|
+
if (!fs.existsSync(scriptPath)) {
|
|
33
|
+
writeLog(`Daemon script missing for ${pluginName}: ${scriptPath}`, true);
|
|
34
|
+
return;
|
|
35
|
+
}
|
|
36
|
+
const runtime = daemon.runtime || "node";
|
|
37
|
+
const { spawn } = await import("child_process");
|
|
38
|
+
const child = spawn(runtime, [scriptPath], {
|
|
39
|
+
cwd: sourceDir,
|
|
40
|
+
detached: true,
|
|
41
|
+
stdio: "ignore",
|
|
42
|
+
env: {
|
|
43
|
+
...process.env,
|
|
44
|
+
HUB_CONFIG_DIR: configDir,
|
|
45
|
+
HUB_APP_NAME: getAppName() === "claude" ? "Claude Code" : "OpenCode",
|
|
46
|
+
...(daemon.port ? { HUB_PROXY_PORT: String(daemon.port) } : {}),
|
|
47
|
+
},
|
|
48
|
+
});
|
|
49
|
+
child.unref();
|
|
50
|
+
writeLog(`Started daemon for ${pluginName} (${runtime} ${daemon.script})`);
|
|
51
|
+
}
|
|
52
|
+
catch (e) {
|
|
53
|
+
writeLog(`Daemon start failed for ${pluginName}: ${e.message}`, true);
|
|
54
|
+
}
|
|
55
|
+
}
|
package/dist/deploy.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function deployToExecutionDir(pluginName: string, executionPath: string, changed: boolean, configDir: string): Promise<boolean>;
|
package/dist/deploy.js
ADDED
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
import fs from "fs";
|
|
2
|
+
import path from "path";
|
|
3
|
+
import { execSync } from "child_process";
|
|
4
|
+
import { getAppName, getReposDir } from "./env.js";
|
|
5
|
+
import { writeLog } from "./log.js";
|
|
6
|
+
import { buildInTempDir } from "./git.js";
|
|
7
|
+
import { startDeclaredDaemon } from "./daemon.js";
|
|
8
|
+
async function callPluginCleanup(pluginExecutionFile, configDir) {
|
|
9
|
+
if (!fs.existsSync(pluginExecutionFile))
|
|
10
|
+
return;
|
|
11
|
+
try {
|
|
12
|
+
const mod = await import(pluginExecutionFile);
|
|
13
|
+
if (typeof mod.cleanup === "function") {
|
|
14
|
+
writeLog(`Calling cleanup() on ${pluginExecutionFile}`);
|
|
15
|
+
await mod.cleanup(configDir);
|
|
16
|
+
writeLog(`cleanup() complete for ${pluginExecutionFile}`);
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
catch (e) {
|
|
20
|
+
writeLog(`cleanup() call failed for ${pluginExecutionFile}: ${e.message}`, true);
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
// under claude, deployed plugins declare env/daemon in package.json#claudeHub;
|
|
24
|
+
// merge the env into settings.json so providers work without a login
|
|
25
|
+
function applyClaudeManifest(sourceDir, configDir, pluginName) {
|
|
26
|
+
if (getAppName() !== "claude")
|
|
27
|
+
return;
|
|
28
|
+
try {
|
|
29
|
+
const pkg = JSON.parse(fs.readFileSync(path.join(sourceDir, "package.json"), "utf8"));
|
|
30
|
+
const manifest = pkg.claudeHub;
|
|
31
|
+
if (!manifest?.env || typeof manifest.env !== "object")
|
|
32
|
+
return;
|
|
33
|
+
const settingsPath = path.join(configDir, "settings.json");
|
|
34
|
+
let settings = {};
|
|
35
|
+
try {
|
|
36
|
+
settings = JSON.parse(fs.readFileSync(settingsPath, "utf8"));
|
|
37
|
+
}
|
|
38
|
+
catch { /* fresh file */ }
|
|
39
|
+
const env = (settings.env ?? {});
|
|
40
|
+
for (const [key, value] of Object.entries(manifest.env)) {
|
|
41
|
+
env[key] = String(value);
|
|
42
|
+
writeLog(`settings.json env ${key} set by ${pluginName}`);
|
|
43
|
+
}
|
|
44
|
+
settings.env = env;
|
|
45
|
+
fs.writeFileSync(settingsPath, JSON.stringify(settings, null, 2), "utf8");
|
|
46
|
+
}
|
|
47
|
+
catch (e) {
|
|
48
|
+
writeLog(`claudeHub manifest handling failed for ${pluginName}: ${e.message}`, true);
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
export async function deployToExecutionDir(pluginName, executionPath, changed, configDir) {
|
|
52
|
+
const sourceDir = path.join(getReposDir(), pluginName);
|
|
53
|
+
if (!fs.existsSync(sourceDir))
|
|
54
|
+
return false;
|
|
55
|
+
const packageJsonPath = path.join(sourceDir, "package.json");
|
|
56
|
+
let entryFile = "index.js";
|
|
57
|
+
const pluginExecutionFile = path.join(executionPath, `${pluginName}.js`);
|
|
58
|
+
if (!changed && fs.existsSync(pluginExecutionFile)) {
|
|
59
|
+
writeLog(`Skipping install/build for ${pluginName} (no changes and deployed file exists)`);
|
|
60
|
+
}
|
|
61
|
+
else if (fs.existsSync(packageJsonPath)) {
|
|
62
|
+
try {
|
|
63
|
+
buildInTempDir(pluginName, sourceDir);
|
|
64
|
+
const runtimeDeps = JSON.parse(fs.readFileSync(packageJsonPath, "utf8")).dependencies;
|
|
65
|
+
if (runtimeDeps && Object.keys(runtimeDeps).length > 0) {
|
|
66
|
+
writeLog(`Installing runtime dependencies for ${pluginName}`);
|
|
67
|
+
execSync("npm install --omit=dev", { cwd: sourceDir, stdio: "pipe" });
|
|
68
|
+
writeLog(`Finished runtime dependencies for ${pluginName}`);
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
catch (error) {
|
|
72
|
+
const err = error;
|
|
73
|
+
const stderr = err.stderr ? err.stderr.toString().trim() : "";
|
|
74
|
+
const stdout = err.stdout ? err.stdout.toString().trim() : "";
|
|
75
|
+
writeLog(`Build/Install failed for ${pluginName}: ${err.message}`, true);
|
|
76
|
+
if (stderr)
|
|
77
|
+
writeLog(`npm stderr: ${stderr}`, true);
|
|
78
|
+
if (stdout)
|
|
79
|
+
writeLog(`npm stdout: ${stdout}`, true);
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
if (fs.existsSync(packageJsonPath)) {
|
|
83
|
+
try {
|
|
84
|
+
const pkg = JSON.parse(fs.readFileSync(packageJsonPath, "utf8"));
|
|
85
|
+
if (pkg.main)
|
|
86
|
+
entryFile = pkg.main;
|
|
87
|
+
}
|
|
88
|
+
catch { /* ignore */ }
|
|
89
|
+
}
|
|
90
|
+
const distPath = path.join(sourceDir, "dist");
|
|
91
|
+
let deploySource = path.join(sourceDir, entryFile);
|
|
92
|
+
if (fs.existsSync(path.join(distPath, entryFile))) {
|
|
93
|
+
deploySource = path.join(distPath, entryFile);
|
|
94
|
+
}
|
|
95
|
+
else if (fs.existsSync(path.join(distPath, "index.js"))) {
|
|
96
|
+
deploySource = path.join(distPath, "index.js");
|
|
97
|
+
}
|
|
98
|
+
if (!fs.existsSync(executionPath))
|
|
99
|
+
fs.mkdirSync(executionPath, { recursive: true });
|
|
100
|
+
await callPluginCleanup(pluginExecutionFile, configDir);
|
|
101
|
+
try {
|
|
102
|
+
writeLog(`Running copy for ${pluginName}`);
|
|
103
|
+
fs.copyFileSync(deploySource, pluginExecutionFile);
|
|
104
|
+
writeLog(`Finished copy for ${pluginName}`);
|
|
105
|
+
}
|
|
106
|
+
catch (e) {
|
|
107
|
+
writeLog(`Copy failed for ${pluginName}: ${e.message}`, true);
|
|
108
|
+
}
|
|
109
|
+
applyClaudeManifest(sourceDir, configDir, pluginName);
|
|
110
|
+
await startDeclaredDaemon(sourceDir, configDir, pluginName);
|
|
111
|
+
// Claude Code never imports deployed plugin files, so under claude the
|
|
112
|
+
// updater is the runtime and invokes the plugin's activate() itself
|
|
113
|
+
if (getAppName() === "claude") {
|
|
114
|
+
try {
|
|
115
|
+
const deployed = await import(pluginExecutionFile);
|
|
116
|
+
if (typeof deployed.activate === "function") {
|
|
117
|
+
writeLog(`Activating ${pluginName}`);
|
|
118
|
+
// tells the plugin the updater is the caller, so it must not start
|
|
119
|
+
// another earlyLaunch and recurse back into the updater
|
|
120
|
+
process.env.PLUGIN_UPDATER_ACTIVATION = "1";
|
|
121
|
+
try {
|
|
122
|
+
await deployed.activate();
|
|
123
|
+
}
|
|
124
|
+
finally {
|
|
125
|
+
delete process.env.PLUGIN_UPDATER_ACTIVATION;
|
|
126
|
+
}
|
|
127
|
+
writeLog(`Activated ${pluginName}`);
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
catch (e) {
|
|
131
|
+
writeLog(`Activation failed for ${pluginName}: ${e.message}`, true);
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
return true;
|
|
135
|
+
}
|
package/dist/env.d.ts
ADDED
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
export declare function setEarlyLaunchConfigDir(dir: string): void;
|
|
2
|
+
export declare function getAppName(): string;
|
|
3
|
+
export declare function getAppConfigDir(appName: string): string;
|
|
4
|
+
export declare function getReposDir(): string;
|
|
5
|
+
export declare function isOpencodeHookInvocation(firstArgument: unknown): boolean;
|
package/dist/env.js
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import fs from "fs";
|
|
2
|
+
import path from "path";
|
|
3
|
+
import os from "os";
|
|
4
|
+
// set by earlyLaunch/direct-update so all path resolution targets that dir
|
|
5
|
+
let earlyLaunchConfigDir = null;
|
|
6
|
+
export function setEarlyLaunchConfigDir(dir) {
|
|
7
|
+
earlyLaunchConfigDir = dir;
|
|
8
|
+
}
|
|
9
|
+
// the CLI runs without "claude" in argv, so it forces the app via env
|
|
10
|
+
export function getAppName() {
|
|
11
|
+
const override = process.env.PLUGIN_UPDATER_APP;
|
|
12
|
+
if (override === "claude" || override === "opencode")
|
|
13
|
+
return override;
|
|
14
|
+
return process.argv.join(" ").includes("claude") ? "claude" : "opencode";
|
|
15
|
+
}
|
|
16
|
+
export function getAppConfigDir(appName) {
|
|
17
|
+
if (earlyLaunchConfigDir)
|
|
18
|
+
return earlyLaunchConfigDir;
|
|
19
|
+
const home = os.homedir();
|
|
20
|
+
const directPath = path.join(home, `.${appName}`);
|
|
21
|
+
const configPath = path.join(home, ".config", appName);
|
|
22
|
+
return fs.existsSync(directPath) ? directPath : configPath;
|
|
23
|
+
}
|
|
24
|
+
export function getReposDir() {
|
|
25
|
+
return path.join(getAppConfigDir(getAppName()), "repos");
|
|
26
|
+
}
|
|
27
|
+
// opencode invokes every exported function as a plugin hook, passing a context
|
|
28
|
+
// object instead of our protocol arguments; exports detect that and return an
|
|
29
|
+
// inert value so opencode gets a valid (empty) plugin instance
|
|
30
|
+
export function isOpencodeHookInvocation(firstArgument) {
|
|
31
|
+
return typeof firstArgument !== "string";
|
|
32
|
+
}
|
package/dist/git.d.ts
ADDED
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
export declare function executeGit(command: string, cwd: string): boolean;
|
|
2
|
+
export declare function updatePlugin(pluginName: string, gitUrl: string, branch: string | undefined, commitHash: string | null, updateInterval?: number): {
|
|
3
|
+
success: boolean;
|
|
4
|
+
changed: boolean;
|
|
5
|
+
};
|
|
6
|
+
export declare function buildInTempDir(pluginName: string, sourceDir: string): void;
|
package/dist/git.js
ADDED
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
import fs from "fs";
|
|
2
|
+
import path from "path";
|
|
3
|
+
import os from "os";
|
|
4
|
+
import { execSync } from "child_process";
|
|
5
|
+
import { getReposDir } from "./env.js";
|
|
6
|
+
import { writeLog } from "./log.js";
|
|
7
|
+
const BUILD_OUTPUT_DIRS = ["dist", path.join("core", "dist")];
|
|
8
|
+
export function executeGit(command, cwd) {
|
|
9
|
+
writeLog(`Executing git: ${command} in ${cwd}`);
|
|
10
|
+
try {
|
|
11
|
+
execSync(command, {
|
|
12
|
+
cwd,
|
|
13
|
+
stdio: "pipe",
|
|
14
|
+
env: { ...process.env, GCM_INTERACTIVE: "never", GIT_TERMINAL_PROMPT: "0" },
|
|
15
|
+
});
|
|
16
|
+
return true;
|
|
17
|
+
}
|
|
18
|
+
catch (error) {
|
|
19
|
+
const err = error;
|
|
20
|
+
const stderr = err.stderr ? err.stderr.toString().trim() : "";
|
|
21
|
+
writeLog(`Git error in ${cwd}: ${err.message} | stderr: ${stderr}`, true);
|
|
22
|
+
return false;
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
export function updatePlugin(pluginName, gitUrl, branch, commitHash, updateInterval = 1) {
|
|
26
|
+
const reposDir = getReposDir();
|
|
27
|
+
const targetDir = path.join(reposDir, pluginName);
|
|
28
|
+
const lastCheckFile = path.join(targetDir, ".lastcheck");
|
|
29
|
+
let didChange = false;
|
|
30
|
+
if (!fs.existsSync(targetDir)) {
|
|
31
|
+
if (!fs.existsSync(reposDir))
|
|
32
|
+
fs.mkdirSync(reposDir, { recursive: true });
|
|
33
|
+
const branchFlag = branch ? `--branch ${branch}` : "";
|
|
34
|
+
const cloned = executeGit(`git clone --recurse-submodules ${branchFlag} ${gitUrl} ${pluginName}`, reposDir);
|
|
35
|
+
if (!cloned)
|
|
36
|
+
return { success: false, changed: false };
|
|
37
|
+
fs.writeFileSync(lastCheckFile, Date.now().toString());
|
|
38
|
+
didChange = true;
|
|
39
|
+
}
|
|
40
|
+
else {
|
|
41
|
+
const lastCheck = fs.existsSync(lastCheckFile)
|
|
42
|
+
? parseInt(fs.readFileSync(lastCheckFile, "utf8"), 10)
|
|
43
|
+
: 0;
|
|
44
|
+
const intervalMs = updateInterval * 3_600_000;
|
|
45
|
+
const elapsed = Date.now() - lastCheck;
|
|
46
|
+
if (elapsed < intervalMs) {
|
|
47
|
+
writeLog(`Fast-path: ${pluginName} skipping update check (checked ${Math.floor(elapsed / 60_000)} min ago, interval ${updateInterval}h)`);
|
|
48
|
+
return { success: true, changed: false };
|
|
49
|
+
}
|
|
50
|
+
fs.writeFileSync(lastCheckFile, Date.now().toString());
|
|
51
|
+
executeGit("git fetch origin", targetDir);
|
|
52
|
+
let beforeHash = "";
|
|
53
|
+
try {
|
|
54
|
+
beforeHash = execSync("git rev-parse HEAD", { cwd: targetDir }).toString().trim();
|
|
55
|
+
}
|
|
56
|
+
catch { /* ignore */ }
|
|
57
|
+
if (commitHash) {
|
|
58
|
+
executeGit(`git checkout ${commitHash}`, targetDir);
|
|
59
|
+
}
|
|
60
|
+
else if (branch) {
|
|
61
|
+
executeGit(`git checkout ${branch}`, targetDir);
|
|
62
|
+
executeGit(`git pull --ff-only origin ${branch}`, targetDir);
|
|
63
|
+
}
|
|
64
|
+
else {
|
|
65
|
+
// the updater owns repos/: hard-sync to the remote so force-pushed
|
|
66
|
+
// branches and rewritten submodule history cannot strand the clone
|
|
67
|
+
executeGit("git fetch origin", targetDir);
|
|
68
|
+
executeGit("git checkout main || git checkout master", targetDir);
|
|
69
|
+
executeGit("git reset --hard @{upstream}", targetDir);
|
|
70
|
+
}
|
|
71
|
+
executeGit("git submodule sync --recursive", targetDir);
|
|
72
|
+
const submodulesOk = executeGit("git submodule update --init --recursive --force", targetDir);
|
|
73
|
+
if (!submodulesOk) {
|
|
74
|
+
writeLog(`Submodule sync failed for ${pluginName}, recloning`, true);
|
|
75
|
+
try {
|
|
76
|
+
fs.rmSync(targetDir, { recursive: true, force: true });
|
|
77
|
+
}
|
|
78
|
+
catch { /* ignore */ }
|
|
79
|
+
const recloneBranchFlag = branch ? `--branch ${branch}` : "";
|
|
80
|
+
executeGit(`git clone --recurse-submodules ${recloneBranchFlag} ${gitUrl} ${pluginName}`, reposDir);
|
|
81
|
+
fs.writeFileSync(lastCheckFile, Date.now().toString());
|
|
82
|
+
didChange = true;
|
|
83
|
+
}
|
|
84
|
+
let afterHash = "";
|
|
85
|
+
try {
|
|
86
|
+
afterHash = execSync("git rev-parse HEAD", { cwd: targetDir }).toString().trim();
|
|
87
|
+
}
|
|
88
|
+
catch { /* ignore */ }
|
|
89
|
+
if (beforeHash !== afterHash)
|
|
90
|
+
didChange = true;
|
|
91
|
+
}
|
|
92
|
+
return { success: true, changed: didChange };
|
|
93
|
+
}
|
|
94
|
+
// npm install creates node_modules/.bin symlinks, which fail on filesystems
|
|
95
|
+
// without symlink support (e.g. Windows-backed Docker bind mounts) — build in
|
|
96
|
+
// the OS temp dir and copy the outputs back instead
|
|
97
|
+
export function buildInTempDir(pluginName, sourceDir) {
|
|
98
|
+
const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), `plugin-updater-${pluginName}-`));
|
|
99
|
+
try {
|
|
100
|
+
fs.cpSync(sourceDir, tempDir, {
|
|
101
|
+
recursive: true,
|
|
102
|
+
filter: (src) => {
|
|
103
|
+
const name = path.basename(src);
|
|
104
|
+
return name !== ".git" && name !== "node_modules";
|
|
105
|
+
},
|
|
106
|
+
});
|
|
107
|
+
writeLog(`Running npm install for ${pluginName}`);
|
|
108
|
+
execSync("npm install", { cwd: tempDir, stdio: "pipe" });
|
|
109
|
+
writeLog(`Finished npm install for ${pluginName}`);
|
|
110
|
+
const pkg = JSON.parse(fs.readFileSync(path.join(tempDir, "package.json"), "utf8"));
|
|
111
|
+
if (pkg.scripts?.build) {
|
|
112
|
+
execSync("npm run build", { cwd: tempDir, stdio: "pipe" });
|
|
113
|
+
writeLog(`Finished npm run build for ${pluginName}`);
|
|
114
|
+
}
|
|
115
|
+
else {
|
|
116
|
+
writeLog(`Skipped npm run build for ${pluginName} (no build script found)`);
|
|
117
|
+
}
|
|
118
|
+
for (const outputDir of BUILD_OUTPUT_DIRS) {
|
|
119
|
+
const builtDir = path.join(tempDir, outputDir);
|
|
120
|
+
if (fs.existsSync(builtDir)) {
|
|
121
|
+
fs.cpSync(builtDir, path.join(sourceDir, outputDir), { recursive: true });
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
finally {
|
|
126
|
+
fs.rmSync(tempDir, { recursive: true, force: true });
|
|
127
|
+
}
|
|
128
|
+
}
|
package/dist/index.d.ts
CHANGED
|
@@ -1,24 +1,6 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
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;
|
|
1
|
+
import type { Plugin } from "./types.js";
|
|
2
|
+
export { getNpmPlugins, installNpmPlugin, uninstallNpmPlugin, updateNpmPlugin } from "./npm.js";
|
|
3
|
+
export { getPlugins, getPluginsPath } from "./config.js";
|
|
19
4
|
export declare function updatePluginPublic(pluginName: string, gitUrl: string, branch?: string, commitHash?: string): Promise<void | object>;
|
|
20
5
|
export declare function earlyLaunch(configDir: string, plugins: Plugin[]): Promise<void | object>;
|
|
21
|
-
export declare function getPluginsPath(configDir: string): string;
|
|
22
|
-
export declare function getPlugins(configDir: string): Plugin[];
|
|
23
6
|
export declare function activate(opencodeHookInput?: unknown): Promise<void | object>;
|
|
24
|
-
export {};
|
package/dist/index.js
CHANGED
|
@@ -1,538 +1,13 @@
|
|
|
1
|
-
import
|
|
1
|
+
import { getAppConfigDir, getAppName, isOpencodeHookInvocation, setEarlyLaunchConfigDir } from "./env.js";
|
|
2
|
+
import { writeLog } from "./log.js";
|
|
3
|
+
import { getPlugins, readOpencodeJson } from "./config.js";
|
|
4
|
+
import { selfUpdate, updateNpmPlugin } from "./npm.js";
|
|
5
|
+
import { updatePlugin } from "./git.js";
|
|
6
|
+
import { deployToExecutionDir } from "./deploy.js";
|
|
2
7
|
import path from "path";
|
|
3
|
-
import
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
let PLUGIN_CONFIG = null;
|
|
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
|
-
}
|
|
15
|
-
function getPluginConfig() {
|
|
16
|
-
if (PLUGIN_CONFIG !== null)
|
|
17
|
-
return PLUGIN_CONFIG;
|
|
18
|
-
try {
|
|
19
|
-
const configDir = getAppConfigDir(getAppName());
|
|
20
|
-
const preferred = path.join(configDir, "config", "plugin-updater.json");
|
|
21
|
-
const fallback = path.join(configDir, "plugin-updater.json");
|
|
22
|
-
const p = fs.existsSync(preferred) ? preferred : fs.existsSync(fallback) ? fallback : null;
|
|
23
|
-
PLUGIN_CONFIG = p ? JSON.parse(fs.readFileSync(p, "utf8")) : {};
|
|
24
|
-
}
|
|
25
|
-
catch {
|
|
26
|
-
PLUGIN_CONFIG = {};
|
|
27
|
-
}
|
|
28
|
-
return PLUGIN_CONFIG ?? {};
|
|
29
|
-
}
|
|
30
|
-
function getAppConfigDir(appName) {
|
|
31
|
-
if (EARLY_LAUNCH_CONFIG_DIR)
|
|
32
|
-
return EARLY_LAUNCH_CONFIG_DIR;
|
|
33
|
-
const home = os.homedir();
|
|
34
|
-
const directPath = path.join(home, `.${appName}`);
|
|
35
|
-
const configPath = path.join(home, ".config", appName);
|
|
36
|
-
return fs.existsSync(directPath) ? directPath : configPath;
|
|
37
|
-
}
|
|
38
|
-
function writeLog(message, isError = false) {
|
|
39
|
-
const loggingEnabled = getPluginConfig().logging !== false;
|
|
40
|
-
try {
|
|
41
|
-
if (loggingEnabled) {
|
|
42
|
-
const date = new Date();
|
|
43
|
-
const dateStr = date.toISOString().split("T")[0];
|
|
44
|
-
const configDir = getAppConfigDir(getAppName());
|
|
45
|
-
const logsDir = path.join(configDir, "logs", dateStr);
|
|
46
|
-
if (!fs.existsSync(logsDir))
|
|
47
|
-
fs.mkdirSync(logsDir, { recursive: true });
|
|
48
|
-
const logFile = path.join(logsDir, `plugin-updater-${START_TIME}.log`);
|
|
49
|
-
const prefix = isError ? "[ERROR]" : "[INFO]";
|
|
50
|
-
fs.appendFileSync(logFile, `[${date.toISOString()}] ${prefix} ${message}\n`);
|
|
51
|
-
}
|
|
52
|
-
}
|
|
53
|
-
catch { /* never crash on log failure */ }
|
|
54
|
-
if (process.env.PLUGIN_UPDATER_LIBRARY_MODE === "1" && process.env.PLUGIN_UPDATER_CLI !== "1")
|
|
55
|
-
return;
|
|
56
|
-
if (isError)
|
|
57
|
-
console.error(message);
|
|
58
|
-
else if (loggingEnabled)
|
|
59
|
-
console.log(message);
|
|
60
|
-
}
|
|
61
|
-
let NPM_GLOBAL_ROOT = null;
|
|
62
|
-
function getReposDir() {
|
|
63
|
-
return path.join(getAppConfigDir(getAppName()), "repos");
|
|
64
|
-
}
|
|
65
|
-
function executeGit(command, cwd) {
|
|
66
|
-
writeLog(`Executing git: ${command} in ${cwd}`);
|
|
67
|
-
try {
|
|
68
|
-
execSync(command, {
|
|
69
|
-
cwd,
|
|
70
|
-
stdio: "pipe",
|
|
71
|
-
env: { ...process.env, GCM_INTERACTIVE: "never", GIT_TERMINAL_PROMPT: "0" },
|
|
72
|
-
});
|
|
73
|
-
return true;
|
|
74
|
-
}
|
|
75
|
-
catch (error) {
|
|
76
|
-
const err = error;
|
|
77
|
-
const stderr = err.stderr ? err.stderr.toString().trim() : "";
|
|
78
|
-
writeLog(`Git error in ${cwd}: ${err.message} | stderr: ${stderr}`, true);
|
|
79
|
-
return false;
|
|
80
|
-
}
|
|
81
|
-
}
|
|
82
|
-
function getNpmGlobalRoot() {
|
|
83
|
-
if (NPM_GLOBAL_ROOT !== null)
|
|
84
|
-
return NPM_GLOBAL_ROOT;
|
|
85
|
-
try {
|
|
86
|
-
NPM_GLOBAL_ROOT = execSync("npm root -g", { stdio: ["ignore", "pipe", "ignore"] }).toString().trim();
|
|
87
|
-
}
|
|
88
|
-
catch {
|
|
89
|
-
NPM_GLOBAL_ROOT = "";
|
|
90
|
-
}
|
|
91
|
-
return NPM_GLOBAL_ROOT;
|
|
92
|
-
}
|
|
93
|
-
function resolveNpmPluginVersion(name, configDir) {
|
|
94
|
-
try {
|
|
95
|
-
// opencode installs npm plugins into ~/.cache/opencode/packages/<name>@<spec>/
|
|
96
|
-
const packageCache = path.join(os.homedir(), ".cache", "opencode", "packages");
|
|
97
|
-
if (fs.existsSync(packageCache)) {
|
|
98
|
-
for (const entry of fs.readdirSync(packageCache)) {
|
|
99
|
-
if (entry !== name && !entry.startsWith(`${name}@`))
|
|
100
|
-
continue;
|
|
101
|
-
const cachedPkg = path.join(packageCache, entry, "node_modules", name, "package.json");
|
|
102
|
-
if (fs.existsSync(cachedPkg)) {
|
|
103
|
-
return JSON.parse(fs.readFileSync(cachedPkg, "utf8")).version || "";
|
|
104
|
-
}
|
|
105
|
-
}
|
|
106
|
-
}
|
|
107
|
-
const cacheDir = path.join(configDir, "cache", "node_modules");
|
|
108
|
-
const globalNpm = getNpmGlobalRoot();
|
|
109
|
-
const candidates = [
|
|
110
|
-
path.join(cacheDir, name, "package.json"),
|
|
111
|
-
path.join(configDir, "node_modules", name, "package.json"),
|
|
112
|
-
globalNpm ? path.join(globalNpm, name, "package.json") : "",
|
|
113
|
-
].filter((p) => p !== "");
|
|
114
|
-
for (const p of candidates) {
|
|
115
|
-
if (fs.existsSync(p)) {
|
|
116
|
-
return JSON.parse(fs.readFileSync(p, "utf8")).version || "";
|
|
117
|
-
}
|
|
118
|
-
}
|
|
119
|
-
// Try resolving via node resolution as last resort
|
|
120
|
-
try {
|
|
121
|
-
const resolved = require.resolve(path.join(name, "package.json"));
|
|
122
|
-
return JSON.parse(fs.readFileSync(resolved, "utf8")).version || "";
|
|
123
|
-
}
|
|
124
|
-
catch { /* not resolvable */ }
|
|
125
|
-
}
|
|
126
|
-
catch { /* ignore */ }
|
|
127
|
-
return "";
|
|
128
|
-
}
|
|
129
|
-
function readOpencodeJson(configDir) {
|
|
130
|
-
const ocPath = path.join(configDir, "opencode.json");
|
|
131
|
-
if (!fs.existsSync(ocPath))
|
|
132
|
-
return { plugins: [], raw: {} };
|
|
133
|
-
try {
|
|
134
|
-
const stripped = fs.readFileSync(ocPath, "utf8").replace(/^\s*\/\/[^\n]*/gm, "");
|
|
135
|
-
const parsed = JSON.parse(stripped);
|
|
136
|
-
const plugins = (parsed.plugin || []);
|
|
137
|
-
return { plugins: plugins.filter((p) => typeof p === "string"), raw: parsed };
|
|
138
|
-
}
|
|
139
|
-
catch {
|
|
140
|
-
return { plugins: [], raw: {} };
|
|
141
|
-
}
|
|
142
|
-
}
|
|
143
|
-
function writeOpencodeJson(configDir, data) {
|
|
144
|
-
const ocPath = path.join(configDir, "opencode.json");
|
|
145
|
-
fs.writeFileSync(ocPath, JSON.stringify(data, null, 2), "utf8");
|
|
146
|
-
}
|
|
147
|
-
// ── Public API ────────────────────────────────────────────────────────────────
|
|
148
|
-
// opencode invokes every exported function as a plugin hook, passing a context
|
|
149
|
-
// object instead of our protocol arguments; exports detect that and return an
|
|
150
|
-
// inert value so opencode gets a valid (empty) plugin instance
|
|
151
|
-
function isOpencodeHookInvocation(firstArgument) {
|
|
152
|
-
return typeof firstArgument !== "string";
|
|
153
|
-
}
|
|
154
|
-
export function getNpmPlugins(configDir) {
|
|
155
|
-
if (isOpencodeHookInvocation(configDir))
|
|
156
|
-
return [];
|
|
157
|
-
const { plugins } = readOpencodeJson(configDir);
|
|
158
|
-
return plugins.map((raw) => {
|
|
159
|
-
const name = raw.replace(/@[^@/]+$/, "") || raw;
|
|
160
|
-
const version = resolveNpmPluginVersion(name, configDir);
|
|
161
|
-
return { name, version, installed: version !== "", raw };
|
|
162
|
-
});
|
|
163
|
-
}
|
|
164
|
-
export function installNpmPlugin(name, configDir) {
|
|
165
|
-
if (isOpencodeHookInvocation(name))
|
|
166
|
-
return "";
|
|
167
|
-
writeLog(`Installing npm plugin: ${name}`);
|
|
168
|
-
try {
|
|
169
|
-
const { plugins, raw } = readOpencodeJson(configDir);
|
|
170
|
-
if (!plugins.includes(name)) {
|
|
171
|
-
raw.plugin = [...plugins, name];
|
|
172
|
-
writeOpencodeJson(configDir, raw);
|
|
173
|
-
}
|
|
174
|
-
execSync(`npm install -g ${name}`, { stdio: "pipe" });
|
|
175
|
-
writeLog(`Installed npm plugin: ${name}`);
|
|
176
|
-
return "";
|
|
177
|
-
}
|
|
178
|
-
catch (e) {
|
|
179
|
-
const msg = e.message;
|
|
180
|
-
writeLog(`Failed to install ${name}: ${msg}`, true);
|
|
181
|
-
return msg;
|
|
182
|
-
}
|
|
183
|
-
}
|
|
184
|
-
export function uninstallNpmPlugin(name, configDir) {
|
|
185
|
-
if (isOpencodeHookInvocation(name))
|
|
186
|
-
return "";
|
|
187
|
-
writeLog(`Uninstalling npm plugin: ${name}`);
|
|
188
|
-
try {
|
|
189
|
-
const { plugins, raw } = readOpencodeJson(configDir);
|
|
190
|
-
raw.plugin = plugins.filter((p) => {
|
|
191
|
-
const pName = p.replace(/@[^@/]+$/, "") || p;
|
|
192
|
-
return pName !== name;
|
|
193
|
-
});
|
|
194
|
-
writeOpencodeJson(configDir, raw);
|
|
195
|
-
execSync(`npm uninstall -g ${name}`, { stdio: "pipe" });
|
|
196
|
-
writeLog(`Uninstalled npm plugin: ${name}`);
|
|
197
|
-
return "";
|
|
198
|
-
}
|
|
199
|
-
catch (e) {
|
|
200
|
-
const msg = e.message;
|
|
201
|
-
writeLog(`Failed to uninstall ${name}: ${msg}`, true);
|
|
202
|
-
return msg;
|
|
203
|
-
}
|
|
204
|
-
}
|
|
205
|
-
export function updateNpmPlugin(name, configDir, updateInterval = 1) {
|
|
206
|
-
if (isOpencodeHookInvocation(name))
|
|
207
|
-
return "";
|
|
208
|
-
writeLog(`Updating npm plugin: ${name}`);
|
|
209
|
-
const checkFile = path.join(configDir, "cache", `.npm-lastcheck-${name.replace(/[^a-z0-9]/gi, "_")}`);
|
|
210
|
-
try {
|
|
211
|
-
if (!fs.existsSync(path.join(configDir, "cache"))) {
|
|
212
|
-
fs.mkdirSync(path.join(configDir, "cache"), { recursive: true });
|
|
213
|
-
}
|
|
214
|
-
const lastCheck = fs.existsSync(checkFile)
|
|
215
|
-
? parseInt(fs.readFileSync(checkFile, "utf8"), 10)
|
|
216
|
-
: 0;
|
|
217
|
-
const elapsed = Date.now() - lastCheck;
|
|
218
|
-
if (elapsed < updateInterval * 3_600_000) {
|
|
219
|
-
writeLog(`Skipping npm update for ${name} (checked ${Math.floor(elapsed / 60_000)} min ago)`);
|
|
220
|
-
return "";
|
|
221
|
-
}
|
|
222
|
-
fs.writeFileSync(checkFile, Date.now().toString());
|
|
223
|
-
execSync(`npm update -g ${name}`, { stdio: "pipe" });
|
|
224
|
-
writeLog(`Updated npm plugin: ${name}`);
|
|
225
|
-
return "";
|
|
226
|
-
}
|
|
227
|
-
catch (e) {
|
|
228
|
-
const msg = e.message;
|
|
229
|
-
writeLog(`Failed to update ${name}: ${msg}`, true);
|
|
230
|
-
return msg;
|
|
231
|
-
}
|
|
232
|
-
}
|
|
233
|
-
function selfUpdate(configDir) {
|
|
234
|
-
writeLog("Running self-update for plugin-updater");
|
|
235
|
-
updateNpmPlugin("plugin-updater", configDir);
|
|
236
|
-
}
|
|
237
|
-
function updatePlugin(pluginName, gitUrl, branch, commitHash, updateInterval = 1) {
|
|
238
|
-
const reposDir = getReposDir();
|
|
239
|
-
const targetDir = path.join(reposDir, pluginName);
|
|
240
|
-
const lastCheckFile = path.join(targetDir, ".lastcheck");
|
|
241
|
-
let didChange = false;
|
|
242
|
-
if (!fs.existsSync(targetDir)) {
|
|
243
|
-
if (!fs.existsSync(reposDir))
|
|
244
|
-
fs.mkdirSync(reposDir, { recursive: true });
|
|
245
|
-
const branchFlag = branch ? `--branch ${branch}` : "";
|
|
246
|
-
const cloned = executeGit(`git clone --recurse-submodules ${branchFlag} ${gitUrl} ${pluginName}`, reposDir);
|
|
247
|
-
if (!cloned)
|
|
248
|
-
return { success: false, changed: false };
|
|
249
|
-
fs.writeFileSync(lastCheckFile, Date.now().toString());
|
|
250
|
-
didChange = true;
|
|
251
|
-
}
|
|
252
|
-
else {
|
|
253
|
-
const lastCheck = fs.existsSync(lastCheckFile)
|
|
254
|
-
? parseInt(fs.readFileSync(lastCheckFile, "utf8"), 10)
|
|
255
|
-
: 0;
|
|
256
|
-
const intervalMs = updateInterval * 3_600_000;
|
|
257
|
-
const elapsed = Date.now() - lastCheck;
|
|
258
|
-
if (elapsed < intervalMs) {
|
|
259
|
-
writeLog(`Fast-path: ${pluginName} skipping update check (checked ${Math.floor(elapsed / 60_000)} min ago, interval ${updateInterval}h)`);
|
|
260
|
-
return { success: true, changed: false };
|
|
261
|
-
}
|
|
262
|
-
fs.writeFileSync(lastCheckFile, Date.now().toString());
|
|
263
|
-
executeGit("git fetch origin", targetDir);
|
|
264
|
-
let beforeHash = "";
|
|
265
|
-
try {
|
|
266
|
-
beforeHash = execSync("git rev-parse HEAD", { cwd: targetDir }).toString().trim();
|
|
267
|
-
}
|
|
268
|
-
catch { /* ignore */ }
|
|
269
|
-
if (commitHash) {
|
|
270
|
-
executeGit(`git checkout ${commitHash}`, targetDir);
|
|
271
|
-
}
|
|
272
|
-
else if (branch) {
|
|
273
|
-
executeGit(`git checkout ${branch}`, targetDir);
|
|
274
|
-
executeGit(`git pull --ff-only origin ${branch}`, targetDir);
|
|
275
|
-
}
|
|
276
|
-
else {
|
|
277
|
-
// the updater owns repos/: hard-sync to the remote so force-pushed
|
|
278
|
-
// branches and rewritten submodule history cannot strand the clone
|
|
279
|
-
executeGit("git fetch origin", targetDir);
|
|
280
|
-
executeGit("git checkout main || git checkout master", targetDir);
|
|
281
|
-
executeGit("git reset --hard @{upstream}", targetDir);
|
|
282
|
-
}
|
|
283
|
-
executeGit("git submodule sync --recursive", targetDir);
|
|
284
|
-
const submodulesOk = executeGit("git submodule update --init --recursive --force", targetDir);
|
|
285
|
-
if (!submodulesOk) {
|
|
286
|
-
writeLog(`Submodule sync failed for ${pluginName}, recloning`, true);
|
|
287
|
-
try {
|
|
288
|
-
fs.rmSync(targetDir, { recursive: true, force: true });
|
|
289
|
-
}
|
|
290
|
-
catch { /* ignore */ }
|
|
291
|
-
const recloneBranchFlag = branch ? `--branch ${branch}` : "";
|
|
292
|
-
executeGit(`git clone --recurse-submodules ${recloneBranchFlag} ${gitUrl} ${pluginName}`, reposDir);
|
|
293
|
-
fs.writeFileSync(lastCheckFile, Date.now().toString());
|
|
294
|
-
didChange = true;
|
|
295
|
-
}
|
|
296
|
-
let afterHash = "";
|
|
297
|
-
try {
|
|
298
|
-
afterHash = execSync("git rev-parse HEAD", { cwd: targetDir }).toString().trim();
|
|
299
|
-
}
|
|
300
|
-
catch { /* ignore */ }
|
|
301
|
-
if (beforeHash !== afterHash)
|
|
302
|
-
didChange = true;
|
|
303
|
-
}
|
|
304
|
-
return { success: true, changed: didChange };
|
|
305
|
-
}
|
|
306
|
-
async function callPluginCleanup(pluginExecutionFile, configDir) {
|
|
307
|
-
if (!fs.existsSync(pluginExecutionFile))
|
|
308
|
-
return;
|
|
309
|
-
try {
|
|
310
|
-
const mod = await import(pluginExecutionFile);
|
|
311
|
-
if (typeof mod.cleanup === "function") {
|
|
312
|
-
writeLog(`Calling cleanup() on ${pluginExecutionFile}`);
|
|
313
|
-
await mod.cleanup(configDir);
|
|
314
|
-
writeLog(`cleanup() complete for ${pluginExecutionFile}`);
|
|
315
|
-
}
|
|
316
|
-
}
|
|
317
|
-
catch (e) {
|
|
318
|
-
writeLog(`cleanup() call failed for ${pluginExecutionFile}: ${e.message}`, true);
|
|
319
|
-
}
|
|
320
|
-
}
|
|
321
|
-
const BUILD_OUTPUT_DIRS = ["dist", path.join("core", "dist")];
|
|
322
|
-
// npm install creates node_modules/.bin symlinks, which fail on filesystems
|
|
323
|
-
// without symlink support (e.g. Windows-backed Docker bind mounts) — build in
|
|
324
|
-
// the OS temp dir and copy the outputs back instead
|
|
325
|
-
function buildInTempDir(pluginName, sourceDir) {
|
|
326
|
-
const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), `plugin-updater-${pluginName}-`));
|
|
327
|
-
try {
|
|
328
|
-
fs.cpSync(sourceDir, tempDir, {
|
|
329
|
-
recursive: true,
|
|
330
|
-
filter: (src) => {
|
|
331
|
-
const name = path.basename(src);
|
|
332
|
-
return name !== ".git" && name !== "node_modules";
|
|
333
|
-
},
|
|
334
|
-
});
|
|
335
|
-
writeLog(`Running npm install for ${pluginName}`);
|
|
336
|
-
execSync("npm install", { cwd: tempDir, stdio: "pipe" });
|
|
337
|
-
writeLog(`Finished npm install for ${pluginName}`);
|
|
338
|
-
const pkg = JSON.parse(fs.readFileSync(path.join(tempDir, "package.json"), "utf8"));
|
|
339
|
-
if (pkg.scripts?.build) {
|
|
340
|
-
execSync("npm run build", { cwd: tempDir, stdio: "pipe" });
|
|
341
|
-
writeLog(`Finished npm run build for ${pluginName}`);
|
|
342
|
-
}
|
|
343
|
-
else {
|
|
344
|
-
writeLog(`Skipped npm run build for ${pluginName} (no build script found)`);
|
|
345
|
-
}
|
|
346
|
-
for (const outputDir of BUILD_OUTPUT_DIRS) {
|
|
347
|
-
const builtDir = path.join(tempDir, outputDir);
|
|
348
|
-
if (fs.existsSync(builtDir)) {
|
|
349
|
-
fs.cpSync(builtDir, path.join(sourceDir, outputDir), { recursive: true });
|
|
350
|
-
}
|
|
351
|
-
}
|
|
352
|
-
}
|
|
353
|
-
finally {
|
|
354
|
-
fs.rmSync(tempDir, { recursive: true, force: true });
|
|
355
|
-
}
|
|
356
|
-
}
|
|
357
|
-
async function deployToExecutionDir(pluginName, executionPath, changed, configDir) {
|
|
358
|
-
const sourceDir = path.join(getReposDir(), pluginName);
|
|
359
|
-
if (!fs.existsSync(sourceDir))
|
|
360
|
-
return false;
|
|
361
|
-
const packageJsonPath = path.join(sourceDir, "package.json");
|
|
362
|
-
let entryFile = "index.js";
|
|
363
|
-
const pluginExecutionFile = path.join(executionPath, `${pluginName}.js`);
|
|
364
|
-
if (!changed && fs.existsSync(pluginExecutionFile)) {
|
|
365
|
-
writeLog(`Skipping install/build for ${pluginName} (no changes and deployed file exists)`);
|
|
366
|
-
}
|
|
367
|
-
else {
|
|
368
|
-
if (fs.existsSync(packageJsonPath)) {
|
|
369
|
-
try {
|
|
370
|
-
buildInTempDir(pluginName, sourceDir);
|
|
371
|
-
const runtimeDeps = JSON.parse(fs.readFileSync(packageJsonPath, "utf8")).dependencies;
|
|
372
|
-
if (runtimeDeps && Object.keys(runtimeDeps).length > 0) {
|
|
373
|
-
writeLog(`Installing runtime dependencies for ${pluginName}`);
|
|
374
|
-
execSync("npm install --omit=dev", { cwd: sourceDir, stdio: "pipe" });
|
|
375
|
-
writeLog(`Finished runtime dependencies for ${pluginName}`);
|
|
376
|
-
}
|
|
377
|
-
}
|
|
378
|
-
catch (error) {
|
|
379
|
-
const err = error;
|
|
380
|
-
const stderr = err.stderr ? err.stderr.toString().trim() : "";
|
|
381
|
-
const stdout = err.stdout ? err.stdout.toString().trim() : "";
|
|
382
|
-
writeLog(`Build/Install failed for ${pluginName}: ${err.message}`, true);
|
|
383
|
-
if (stderr)
|
|
384
|
-
writeLog(`npm stderr: ${stderr}`, true);
|
|
385
|
-
if (stdout)
|
|
386
|
-
writeLog(`npm stdout: ${stdout}`, true);
|
|
387
|
-
}
|
|
388
|
-
}
|
|
389
|
-
}
|
|
390
|
-
if (fs.existsSync(packageJsonPath)) {
|
|
391
|
-
try {
|
|
392
|
-
const pkg = JSON.parse(fs.readFileSync(packageJsonPath, "utf8"));
|
|
393
|
-
if (pkg.main)
|
|
394
|
-
entryFile = pkg.main;
|
|
395
|
-
}
|
|
396
|
-
catch { /* ignore */ }
|
|
397
|
-
}
|
|
398
|
-
const distPath = path.join(sourceDir, "dist");
|
|
399
|
-
let deploySource = path.join(sourceDir, entryFile);
|
|
400
|
-
if (fs.existsSync(path.join(distPath, entryFile))) {
|
|
401
|
-
deploySource = path.join(distPath, entryFile);
|
|
402
|
-
}
|
|
403
|
-
else if (fs.existsSync(path.join(distPath, "index.js"))) {
|
|
404
|
-
deploySource = path.join(distPath, "index.js");
|
|
405
|
-
}
|
|
406
|
-
if (!fs.existsSync(executionPath))
|
|
407
|
-
fs.mkdirSync(executionPath, { recursive: true });
|
|
408
|
-
await callPluginCleanup(pluginExecutionFile, configDir);
|
|
409
|
-
try {
|
|
410
|
-
writeLog(`Running copy for ${pluginName}`);
|
|
411
|
-
fs.copyFileSync(deploySource, pluginExecutionFile);
|
|
412
|
-
writeLog(`Finished copy for ${pluginName}`);
|
|
413
|
-
}
|
|
414
|
-
catch (e) {
|
|
415
|
-
const err = e;
|
|
416
|
-
writeLog(`Copy failed for ${pluginName}: ${err.message}`, true);
|
|
417
|
-
}
|
|
418
|
-
applyClaudeManifest(sourceDir, configDir, pluginName);
|
|
419
|
-
await startDeclaredDaemon(sourceDir, configDir, pluginName);
|
|
420
|
-
// Claude Code never imports deployed plugin files, so under claude the
|
|
421
|
-
// updater is the runtime and invokes the plugin's activate() itself
|
|
422
|
-
if (getAppName() === "claude") {
|
|
423
|
-
try {
|
|
424
|
-
const deployed = await import(pluginExecutionFile);
|
|
425
|
-
if (typeof deployed.activate === "function") {
|
|
426
|
-
writeLog(`Activating ${pluginName}`);
|
|
427
|
-
// tells the plugin the updater is the caller, so it must not start
|
|
428
|
-
// another earlyLaunch and recurse back into the updater
|
|
429
|
-
process.env.PLUGIN_UPDATER_ACTIVATION = "1";
|
|
430
|
-
try {
|
|
431
|
-
await deployed.activate();
|
|
432
|
-
}
|
|
433
|
-
finally {
|
|
434
|
-
delete process.env.PLUGIN_UPDATER_ACTIVATION;
|
|
435
|
-
}
|
|
436
|
-
writeLog(`Activated ${pluginName}`);
|
|
437
|
-
}
|
|
438
|
-
}
|
|
439
|
-
catch (e) {
|
|
440
|
-
writeLog(`Activation failed for ${pluginName}: ${e.message}`, true);
|
|
441
|
-
}
|
|
442
|
-
}
|
|
443
|
-
return true;
|
|
444
|
-
}
|
|
445
|
-
function applyClaudeManifest(sourceDir, configDir, pluginName) {
|
|
446
|
-
if (getAppName() !== "claude")
|
|
447
|
-
return;
|
|
448
|
-
try {
|
|
449
|
-
const pkg = JSON.parse(fs.readFileSync(path.join(sourceDir, "package.json"), "utf8"));
|
|
450
|
-
const manifest = pkg.claudeHub;
|
|
451
|
-
if (!manifest)
|
|
452
|
-
return;
|
|
453
|
-
if (manifest.env && typeof manifest.env === "object") {
|
|
454
|
-
const settingsPath = path.join(configDir, "settings.json");
|
|
455
|
-
let settings = {};
|
|
456
|
-
try {
|
|
457
|
-
settings = JSON.parse(fs.readFileSync(settingsPath, "utf8"));
|
|
458
|
-
}
|
|
459
|
-
catch { /* fresh file */ }
|
|
460
|
-
const env = (settings.env ?? {});
|
|
461
|
-
for (const [key, value] of Object.entries(manifest.env)) {
|
|
462
|
-
env[key] = String(value);
|
|
463
|
-
writeLog(`settings.json env ${key} set by ${pluginName}`);
|
|
464
|
-
}
|
|
465
|
-
settings.env = env;
|
|
466
|
-
fs.writeFileSync(settingsPath, JSON.stringify(settings, null, 2), "utf8");
|
|
467
|
-
}
|
|
468
|
-
}
|
|
469
|
-
catch (e) {
|
|
470
|
-
writeLog(`claudeHub manifest handling failed for ${pluginName}: ${e.message}`, true);
|
|
471
|
-
}
|
|
472
|
-
}
|
|
473
|
-
async function isDaemonHealthy(url) {
|
|
474
|
-
try {
|
|
475
|
-
const controller = new AbortController();
|
|
476
|
-
const timer = setTimeout(() => controller.abort(), 1500);
|
|
477
|
-
const res = await fetch(url, { signal: controller.signal });
|
|
478
|
-
clearTimeout(timer);
|
|
479
|
-
return res.ok;
|
|
480
|
-
}
|
|
481
|
-
catch {
|
|
482
|
-
return false;
|
|
483
|
-
}
|
|
484
|
-
}
|
|
485
|
-
// idempotent: health-check the declared endpoint, spawn detached only if down.
|
|
486
|
-
// the daemon outlives this process so the proxy persists across the session.
|
|
487
|
-
async function startDeclaredDaemon(sourceDir, configDir, pluginName) {
|
|
488
|
-
try {
|
|
489
|
-
const pkg = JSON.parse(fs.readFileSync(path.join(sourceDir, "package.json"), "utf8"));
|
|
490
|
-
const daemon = pkg.claudeHub?.daemon;
|
|
491
|
-
if (!daemon?.script)
|
|
492
|
-
return;
|
|
493
|
-
const healthUrl = daemon.healthCheckUrl
|
|
494
|
-
|| (daemon.port ? `http://127.0.0.1:${daemon.port}/health` : "");
|
|
495
|
-
if (healthUrl && (await isDaemonHealthy(healthUrl))) {
|
|
496
|
-
writeLog(`Daemon for ${pluginName} already running`);
|
|
497
|
-
return;
|
|
498
|
-
}
|
|
499
|
-
const scriptPath = path.join(sourceDir, daemon.script);
|
|
500
|
-
if (!fs.existsSync(scriptPath)) {
|
|
501
|
-
writeLog(`Daemon script missing for ${pluginName}: ${scriptPath}`, true);
|
|
502
|
-
return;
|
|
503
|
-
}
|
|
504
|
-
const runtime = daemon.runtime || "node";
|
|
505
|
-
const { spawn } = await import("child_process");
|
|
506
|
-
const child = spawn(runtime, [scriptPath], {
|
|
507
|
-
cwd: sourceDir,
|
|
508
|
-
detached: true,
|
|
509
|
-
stdio: "ignore",
|
|
510
|
-
env: {
|
|
511
|
-
...process.env,
|
|
512
|
-
HUB_CONFIG_DIR: configDir,
|
|
513
|
-
HUB_APP_NAME: getAppName() === "claude" ? "Claude Code" : "OpenCode",
|
|
514
|
-
...(daemon.port ? { HUB_PROXY_PORT: String(daemon.port) } : {}),
|
|
515
|
-
},
|
|
516
|
-
});
|
|
517
|
-
child.unref();
|
|
518
|
-
writeLog(`Started daemon for ${pluginName} (${runtime} ${daemon.script})`);
|
|
519
|
-
}
|
|
520
|
-
catch (e) {
|
|
521
|
-
writeLog(`Daemon start failed for ${pluginName}: ${e.message}`, true);
|
|
522
|
-
}
|
|
523
|
-
}
|
|
524
|
-
async function pluginUpdaterEntry(input) {
|
|
525
|
-
const appName = getAppName();
|
|
526
|
-
const configDir = getAppConfigDir(appName);
|
|
527
|
-
const pluginsDir = path.join(configDir, "plugin");
|
|
528
|
-
writeLog(`Starting plugin updater for ${appName}`);
|
|
529
|
-
if (input?.action === "updatePlugin" && input.configDir && input.pluginName && input.gitUrl) {
|
|
530
|
-
EARLY_LAUNCH_CONFIG_DIR = input.configDir;
|
|
531
|
-
writeLog(`Direct update request for ${input.pluginName}`);
|
|
532
|
-
const updateResult = updatePlugin(input.pluginName, input.gitUrl, input.branch, input.commitHash ?? null);
|
|
533
|
-
await deployToExecutionDir(input.pluginName, pluginsDir, updateResult.changed, input.configDir);
|
|
534
|
-
}
|
|
535
|
-
}
|
|
8
|
+
// re-exported public API (consumers import these from "plugin-updater")
|
|
9
|
+
export { getNpmPlugins, installNpmPlugin, uninstallNpmPlugin, updateNpmPlugin } from "./npm.js";
|
|
10
|
+
export { getPlugins, getPluginsPath } from "./config.js";
|
|
536
11
|
export async function updatePluginPublic(pluginName, gitUrl, branch, commitHash) {
|
|
537
12
|
if (isOpencodeHookInvocation(pluginName))
|
|
538
13
|
return {};
|
|
@@ -547,11 +22,10 @@ export async function updatePluginPublic(pluginName, gitUrl, branch, commitHash)
|
|
|
547
22
|
export async function earlyLaunch(configDir, plugins) {
|
|
548
23
|
if (isOpencodeHookInvocation(configDir))
|
|
549
24
|
return {};
|
|
550
|
-
|
|
25
|
+
setEarlyLaunchConfigDir(configDir);
|
|
551
26
|
writeLog("Starting earlyLaunch updater sequence");
|
|
552
|
-
// Self-update first
|
|
553
27
|
selfUpdate(configDir);
|
|
554
|
-
//
|
|
28
|
+
// npm plugins listed in opencode.json
|
|
555
29
|
const { plugins: npmNames } = readOpencodeJson(configDir);
|
|
556
30
|
for (const raw of npmNames) {
|
|
557
31
|
const name = raw.replace(/@[^@/]+$/, "") || raw;
|
|
@@ -565,7 +39,6 @@ export async function earlyLaunch(configDir, plugins) {
|
|
|
565
39
|
writeLog(`Failed npm update for ${name}: ${e.message}`, true);
|
|
566
40
|
}
|
|
567
41
|
}
|
|
568
|
-
// Git plugins
|
|
569
42
|
if (!plugins || !Array.isArray(plugins)) {
|
|
570
43
|
writeLog("No git plugins provided to earlyLaunch", true);
|
|
571
44
|
return;
|
|
@@ -594,37 +67,10 @@ export async function earlyLaunch(configDir, plugins) {
|
|
|
594
67
|
await deployToExecutionDir(plugin.name, path.join(configDir, "plugin"), updateResult.changed, configDir);
|
|
595
68
|
}
|
|
596
69
|
catch (e) {
|
|
597
|
-
|
|
598
|
-
writeLog(`Failed to process ${plugin.name}: ${err.message}`, true);
|
|
70
|
+
writeLog(`Failed to process ${plugin.name}: ${e.message}`, true);
|
|
599
71
|
}
|
|
600
72
|
}
|
|
601
73
|
}
|
|
602
|
-
export function getPluginsPath(configDir) {
|
|
603
|
-
if (isOpencodeHookInvocation(configDir))
|
|
604
|
-
return "";
|
|
605
|
-
const preferred = path.join(configDir, "config", "plugins.json");
|
|
606
|
-
const fallback = path.join(configDir, "plugins.json");
|
|
607
|
-
if (fs.existsSync(preferred))
|
|
608
|
-
return preferred;
|
|
609
|
-
if (fs.existsSync(fallback))
|
|
610
|
-
return fallback;
|
|
611
|
-
return preferred;
|
|
612
|
-
}
|
|
613
|
-
// single source of truth for the git-plugin list; consumers (loaders, TUI)
|
|
614
|
-
// must read through this rather than touching plugins.json directly
|
|
615
|
-
export function getPlugins(configDir) {
|
|
616
|
-
if (isOpencodeHookInvocation(configDir))
|
|
617
|
-
return [];
|
|
618
|
-
const file = getPluginsPath(configDir);
|
|
619
|
-
try {
|
|
620
|
-
if (fs.existsSync(file))
|
|
621
|
-
return JSON.parse(fs.readFileSync(file, "utf-8"));
|
|
622
|
-
}
|
|
623
|
-
catch (e) {
|
|
624
|
-
writeLog(`Failed to parse ${file}: ${e.message}`, true);
|
|
625
|
-
}
|
|
626
|
-
return [];
|
|
627
|
-
}
|
|
628
74
|
export async function activate(opencodeHookInput) {
|
|
629
75
|
// module load below calls activate() with no argument; opencode passes a
|
|
630
76
|
// context object when re-invoking the export — return an inert plugin instance
|
package/dist/log.d.ts
ADDED
package/dist/log.js
ADDED
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import fs from "fs";
|
|
2
|
+
import path from "path";
|
|
3
|
+
import { getAppConfigDir, getAppName } from "./env.js";
|
|
4
|
+
const START_TIME = new Date().toISOString().replace(/:/g, "-").split(".")[0];
|
|
5
|
+
let pluginConfig = null;
|
|
6
|
+
export function getPluginConfig() {
|
|
7
|
+
if (pluginConfig !== null)
|
|
8
|
+
return pluginConfig;
|
|
9
|
+
try {
|
|
10
|
+
const configDir = getAppConfigDir(getAppName());
|
|
11
|
+
const preferred = path.join(configDir, "config", "plugin-updater.json");
|
|
12
|
+
const fallback = path.join(configDir, "plugin-updater.json");
|
|
13
|
+
const p = fs.existsSync(preferred) ? preferred : fs.existsSync(fallback) ? fallback : null;
|
|
14
|
+
pluginConfig = p ? JSON.parse(fs.readFileSync(p, "utf8")) : {};
|
|
15
|
+
}
|
|
16
|
+
catch {
|
|
17
|
+
pluginConfig = {};
|
|
18
|
+
}
|
|
19
|
+
return pluginConfig ?? {};
|
|
20
|
+
}
|
|
21
|
+
export function writeLog(message, isError = false) {
|
|
22
|
+
const loggingEnabled = getPluginConfig().logging !== false;
|
|
23
|
+
try {
|
|
24
|
+
if (loggingEnabled) {
|
|
25
|
+
const date = new Date();
|
|
26
|
+
const dateStr = date.toISOString().split("T")[0];
|
|
27
|
+
const configDir = getAppConfigDir(getAppName());
|
|
28
|
+
const logsDir = path.join(configDir, "logs", dateStr);
|
|
29
|
+
if (!fs.existsSync(logsDir))
|
|
30
|
+
fs.mkdirSync(logsDir, { recursive: true });
|
|
31
|
+
const logFile = path.join(logsDir, `plugin-updater-${START_TIME}.log`);
|
|
32
|
+
const prefix = isError ? "[ERROR]" : "[INFO]";
|
|
33
|
+
fs.appendFileSync(logFile, `[${date.toISOString()}] ${prefix} ${message}\n`);
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
catch { /* never crash on log failure */ }
|
|
37
|
+
if (process.env.PLUGIN_UPDATER_LIBRARY_MODE === "1" && process.env.PLUGIN_UPDATER_CLI !== "1")
|
|
38
|
+
return;
|
|
39
|
+
if (isError)
|
|
40
|
+
console.error(message);
|
|
41
|
+
else if (loggingEnabled)
|
|
42
|
+
console.log(message);
|
|
43
|
+
}
|
package/dist/npm.d.ts
ADDED
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import type { NpmPlugin } from "./types.js";
|
|
2
|
+
export declare function getNpmGlobalRoot(): string;
|
|
3
|
+
export declare function resolveNpmPluginVersion(name: string, configDir: string): string;
|
|
4
|
+
export declare function getNpmPlugins(configDir: string): NpmPlugin[];
|
|
5
|
+
export declare function installNpmPlugin(name: string, configDir: string): string;
|
|
6
|
+
export declare function uninstallNpmPlugin(name: string, configDir: string): string;
|
|
7
|
+
export declare function updateNpmPlugin(name: string, configDir: string, updateInterval?: number): string;
|
|
8
|
+
export declare function selfUpdate(configDir: string): void;
|
package/dist/npm.js
ADDED
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
import fs from "fs";
|
|
2
|
+
import path from "path";
|
|
3
|
+
import os from "os";
|
|
4
|
+
import { execSync } from "child_process";
|
|
5
|
+
import { isOpencodeHookInvocation } from "./env.js";
|
|
6
|
+
import { writeLog } from "./log.js";
|
|
7
|
+
import { readOpencodeJson, writeOpencodeJson } from "./config.js";
|
|
8
|
+
let npmGlobalRoot = null;
|
|
9
|
+
export function getNpmGlobalRoot() {
|
|
10
|
+
if (npmGlobalRoot !== null)
|
|
11
|
+
return npmGlobalRoot;
|
|
12
|
+
try {
|
|
13
|
+
npmGlobalRoot = execSync("npm root -g", { stdio: ["ignore", "pipe", "ignore"] }).toString().trim();
|
|
14
|
+
}
|
|
15
|
+
catch {
|
|
16
|
+
npmGlobalRoot = "";
|
|
17
|
+
}
|
|
18
|
+
return npmGlobalRoot;
|
|
19
|
+
}
|
|
20
|
+
export function resolveNpmPluginVersion(name, configDir) {
|
|
21
|
+
try {
|
|
22
|
+
// opencode installs npm plugins into ~/.cache/opencode/packages/<name>@<spec>/
|
|
23
|
+
const packageCache = path.join(os.homedir(), ".cache", "opencode", "packages");
|
|
24
|
+
if (fs.existsSync(packageCache)) {
|
|
25
|
+
for (const entry of fs.readdirSync(packageCache)) {
|
|
26
|
+
if (entry !== name && !entry.startsWith(`${name}@`))
|
|
27
|
+
continue;
|
|
28
|
+
const cachedPkg = path.join(packageCache, entry, "node_modules", name, "package.json");
|
|
29
|
+
if (fs.existsSync(cachedPkg)) {
|
|
30
|
+
return JSON.parse(fs.readFileSync(cachedPkg, "utf8")).version || "";
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
const cacheDir = path.join(configDir, "cache", "node_modules");
|
|
35
|
+
const globalNpm = getNpmGlobalRoot();
|
|
36
|
+
const candidates = [
|
|
37
|
+
path.join(cacheDir, name, "package.json"),
|
|
38
|
+
path.join(configDir, "node_modules", name, "package.json"),
|
|
39
|
+
globalNpm ? path.join(globalNpm, name, "package.json") : "",
|
|
40
|
+
].filter((p) => p !== "");
|
|
41
|
+
for (const p of candidates) {
|
|
42
|
+
if (fs.existsSync(p)) {
|
|
43
|
+
return JSON.parse(fs.readFileSync(p, "utf8")).version || "";
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
try {
|
|
47
|
+
const resolved = require.resolve(path.join(name, "package.json"));
|
|
48
|
+
return JSON.parse(fs.readFileSync(resolved, "utf8")).version || "";
|
|
49
|
+
}
|
|
50
|
+
catch { /* not resolvable */ }
|
|
51
|
+
}
|
|
52
|
+
catch { /* ignore */ }
|
|
53
|
+
return "";
|
|
54
|
+
}
|
|
55
|
+
export function getNpmPlugins(configDir) {
|
|
56
|
+
if (isOpencodeHookInvocation(configDir))
|
|
57
|
+
return [];
|
|
58
|
+
const { plugins } = readOpencodeJson(configDir);
|
|
59
|
+
return plugins.map((raw) => {
|
|
60
|
+
const name = raw.replace(/@[^@/]+$/, "") || raw;
|
|
61
|
+
const version = resolveNpmPluginVersion(name, configDir);
|
|
62
|
+
return { name, version, installed: version !== "", raw };
|
|
63
|
+
});
|
|
64
|
+
}
|
|
65
|
+
export function installNpmPlugin(name, configDir) {
|
|
66
|
+
if (isOpencodeHookInvocation(name))
|
|
67
|
+
return "";
|
|
68
|
+
writeLog(`Installing npm plugin: ${name}`);
|
|
69
|
+
try {
|
|
70
|
+
const { plugins, raw } = readOpencodeJson(configDir);
|
|
71
|
+
if (!plugins.includes(name)) {
|
|
72
|
+
raw.plugin = [...plugins, name];
|
|
73
|
+
writeOpencodeJson(configDir, raw);
|
|
74
|
+
}
|
|
75
|
+
execSync(`npm install -g ${name}`, { stdio: "pipe" });
|
|
76
|
+
writeLog(`Installed npm plugin: ${name}`);
|
|
77
|
+
return "";
|
|
78
|
+
}
|
|
79
|
+
catch (e) {
|
|
80
|
+
const msg = e.message;
|
|
81
|
+
writeLog(`Failed to install ${name}: ${msg}`, true);
|
|
82
|
+
return msg;
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
export function uninstallNpmPlugin(name, configDir) {
|
|
86
|
+
if (isOpencodeHookInvocation(name))
|
|
87
|
+
return "";
|
|
88
|
+
writeLog(`Uninstalling npm plugin: ${name}`);
|
|
89
|
+
try {
|
|
90
|
+
const { plugins, raw } = readOpencodeJson(configDir);
|
|
91
|
+
raw.plugin = plugins.filter((p) => {
|
|
92
|
+
const pName = p.replace(/@[^@/]+$/, "") || p;
|
|
93
|
+
return pName !== name;
|
|
94
|
+
});
|
|
95
|
+
writeOpencodeJson(configDir, raw);
|
|
96
|
+
execSync(`npm uninstall -g ${name}`, { stdio: "pipe" });
|
|
97
|
+
writeLog(`Uninstalled npm plugin: ${name}`);
|
|
98
|
+
return "";
|
|
99
|
+
}
|
|
100
|
+
catch (e) {
|
|
101
|
+
const msg = e.message;
|
|
102
|
+
writeLog(`Failed to uninstall ${name}: ${msg}`, true);
|
|
103
|
+
return msg;
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
export function updateNpmPlugin(name, configDir, updateInterval = 1) {
|
|
107
|
+
if (isOpencodeHookInvocation(name))
|
|
108
|
+
return "";
|
|
109
|
+
writeLog(`Updating npm plugin: ${name}`);
|
|
110
|
+
const checkFile = path.join(configDir, "cache", `.npm-lastcheck-${name.replace(/[^a-z0-9]/gi, "_")}`);
|
|
111
|
+
try {
|
|
112
|
+
if (!fs.existsSync(path.join(configDir, "cache"))) {
|
|
113
|
+
fs.mkdirSync(path.join(configDir, "cache"), { recursive: true });
|
|
114
|
+
}
|
|
115
|
+
const lastCheck = fs.existsSync(checkFile)
|
|
116
|
+
? parseInt(fs.readFileSync(checkFile, "utf8"), 10)
|
|
117
|
+
: 0;
|
|
118
|
+
const elapsed = Date.now() - lastCheck;
|
|
119
|
+
if (elapsed < updateInterval * 3_600_000) {
|
|
120
|
+
writeLog(`Skipping npm update for ${name} (checked ${Math.floor(elapsed / 60_000)} min ago)`);
|
|
121
|
+
return "";
|
|
122
|
+
}
|
|
123
|
+
fs.writeFileSync(checkFile, Date.now().toString());
|
|
124
|
+
execSync(`npm update -g ${name}`, { stdio: "pipe" });
|
|
125
|
+
writeLog(`Updated npm plugin: ${name}`);
|
|
126
|
+
return "";
|
|
127
|
+
}
|
|
128
|
+
catch (e) {
|
|
129
|
+
const msg = e.message;
|
|
130
|
+
writeLog(`Failed to update ${name}: ${msg}`, true);
|
|
131
|
+
return msg;
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
export function selfUpdate(configDir) {
|
|
135
|
+
writeLog("Running self-update for plugin-updater");
|
|
136
|
+
updateNpmPlugin("plugin-updater", configDir);
|
|
137
|
+
}
|
package/dist/types.d.ts
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
export interface Plugin {
|
|
2
|
+
name: string;
|
|
3
|
+
url?: string;
|
|
4
|
+
branch?: string;
|
|
5
|
+
enabled?: boolean;
|
|
6
|
+
autoUpdate?: boolean;
|
|
7
|
+
updateInterval?: number;
|
|
8
|
+
}
|
|
9
|
+
export interface NpmPlugin {
|
|
10
|
+
name: string;
|
|
11
|
+
version: string;
|
|
12
|
+
installed: boolean;
|
|
13
|
+
raw: string;
|
|
14
|
+
}
|
|
15
|
+
export interface DaemonManifest {
|
|
16
|
+
script: string;
|
|
17
|
+
runtime?: string;
|
|
18
|
+
port?: number;
|
|
19
|
+
healthCheckUrl?: string;
|
|
20
|
+
}
|
package/dist/types.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|