plugin-updater 1.0.0 → 1.0.9
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/.github/workflows/publish.yml +29 -0
- package/.idea/misc.xml +7 -0
- package/.idea/modules.xml +8 -0
- package/.idea/plugin-updater.iml +9 -0
- package/.idea/vcs.xml +6 -0
- package/SPEC.md +13 -0
- package/index.js +166 -39
- package/package.json +8 -2
|
@@ -0,0 +1,29 @@
|
|
|
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
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
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>
|
|
@@ -0,0 +1,8 @@
|
|
|
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>
|
|
@@ -0,0 +1,9 @@
|
|
|
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
ADDED
package/SPEC.md
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
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
CHANGED
|
@@ -1,32 +1,78 @@
|
|
|
1
1
|
const fs = require('fs');
|
|
2
2
|
const path = require('path');
|
|
3
|
+
const os = require('os');
|
|
3
4
|
const { execSync } = require('child_process');
|
|
4
5
|
|
|
5
|
-
|
|
6
|
+
let EARLY_LAUNCH_CONFIG_DIR = null;
|
|
6
7
|
|
|
7
|
-
function
|
|
8
|
+
function getAppConfigDir(appName) {
|
|
9
|
+
if (EARLY_LAUNCH_CONFIG_DIR) {
|
|
10
|
+
return EARLY_LAUNCH_CONFIG_DIR;
|
|
11
|
+
}
|
|
12
|
+
const home = os.homedir();
|
|
13
|
+
const directPath = path.join(home, `.${appName}`);
|
|
14
|
+
const configPath = path.join(home, ".config", appName);
|
|
15
|
+
return fs.existsSync(directPath) ? directPath : configPath;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function writeLog(message, isError = false) {
|
|
8
19
|
try {
|
|
9
|
-
|
|
20
|
+
const date = new Date();
|
|
21
|
+
const dateStr = date.toISOString().split('T')[0];
|
|
22
|
+
const isClaude = process.argv.join(' ').includes('claude');
|
|
23
|
+
const appName = isClaude ? "claude" : "opencode";
|
|
24
|
+
const configDir = getAppConfigDir(appName);
|
|
25
|
+
|
|
26
|
+
const logsDir = path.join(configDir, "logs", dateStr);
|
|
27
|
+
if (!fs.existsSync(logsDir)) {
|
|
28
|
+
fs.mkdirSync(logsDir, { recursive: true });
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
const logFile = path.join(logsDir, `updater-${dateStr}.log`);
|
|
32
|
+
const prefix = isError ? "[ERROR]" : "[INFO]";
|
|
33
|
+
const logMsg = `[${date.toISOString()}] ${prefix} ${message}\n`;
|
|
34
|
+
|
|
35
|
+
fs.appendFileSync(logFile, logMsg);
|
|
10
36
|
} catch (e) {
|
|
11
|
-
|
|
37
|
+
// Silent fallback if logging fails
|
|
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, { cwd, stdio: "ignore" });
|
|
53
|
+
return true;
|
|
54
|
+
} catch (error) {
|
|
55
|
+
writeLog(`Git error in ${cwd}: ${error.message}`, true);
|
|
12
56
|
return false;
|
|
13
57
|
}
|
|
14
58
|
}
|
|
15
59
|
|
|
16
|
-
|
|
60
|
+
const updaterAPI = {
|
|
17
61
|
name: "plugin-updater",
|
|
18
62
|
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
63
|
+
earlyLaunch: function(configDir) {
|
|
64
|
+
EARLY_LAUNCH_CONFIG_DIR = configDir;
|
|
65
|
+
global.__PLUGIN_UPDATER_HANDLED_BY_HUB__ = true;
|
|
66
|
+
},
|
|
67
|
+
|
|
22
68
|
updatePlugin: function(pluginName, gitUrl, branch = null, commitHash = null) {
|
|
23
|
-
const
|
|
69
|
+
const reposDir = getReposDir();
|
|
70
|
+
const targetDir = path.join(reposDir, pluginName);
|
|
24
71
|
|
|
25
|
-
// 1. Ensure directory exists and clone or pull
|
|
26
72
|
if (!fs.existsSync(targetDir)) {
|
|
27
|
-
if (!fs.existsSync(
|
|
73
|
+
if (!fs.existsSync(reposDir)) fs.mkdirSync(reposDir, { recursive: true });
|
|
28
74
|
const branchFlag = branch ? `--branch ${branch}` : "";
|
|
29
|
-
executeGit(`git clone --recurse-submodules ${branchFlag} ${gitUrl} ${pluginName}`,
|
|
75
|
+
executeGit(`git clone --recurse-submodules ${branchFlag} ${gitUrl} ${pluginName}`, reposDir);
|
|
30
76
|
} else {
|
|
31
77
|
executeGit("git fetch origin", targetDir);
|
|
32
78
|
if (commitHash) {
|
|
@@ -40,53 +86,134 @@ module.exports = {
|
|
|
40
86
|
}
|
|
41
87
|
executeGit("git submodule update --init --recursive", targetDir);
|
|
42
88
|
}
|
|
43
|
-
|
|
44
89
|
return true;
|
|
45
90
|
},
|
|
46
91
|
|
|
47
|
-
/**
|
|
48
|
-
* Called to deploy the compiled output to the execution directory
|
|
49
|
-
*/
|
|
50
92
|
deployToExecutionDir: function(pluginName, executionPath) {
|
|
51
|
-
const sourceDir = path.join(
|
|
93
|
+
const sourceDir = path.join(getReposDir(), pluginName);
|
|
52
94
|
if (!fs.existsSync(sourceDir)) return false;
|
|
53
95
|
|
|
54
|
-
|
|
55
|
-
if (fs.existsSync(
|
|
96
|
+
const packageJsonPath = path.join(sourceDir, "package.json");
|
|
97
|
+
if (fs.existsSync(packageJsonPath)) {
|
|
56
98
|
try {
|
|
99
|
+
writeLog(`Running npm install for ${pluginName}`);
|
|
57
100
|
execSync("npm install", { cwd: sourceDir, stdio: "ignore" });
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
//
|
|
101
|
+
writeLog(`Finished npm install for ${pluginName}`);
|
|
102
|
+
|
|
103
|
+
// Safely check if a build script exists before executing
|
|
104
|
+
const pkg = JSON.parse(fs.readFileSync(packageJsonPath, 'utf8'));
|
|
105
|
+
if (pkg.scripts && pkg.scripts.build) {
|
|
106
|
+
execSync("npm run build", { cwd: sourceDir, stdio: "ignore" });
|
|
107
|
+
writeLog(`Finished npm run build for ${pluginName}`);
|
|
108
|
+
} else {
|
|
109
|
+
writeLog(`Skipped npm run build for ${pluginName} (no build script found)`);
|
|
110
|
+
}
|
|
111
|
+
} catch (error) {
|
|
112
|
+
writeLog(`Build/Install failed for ${pluginName}: ${error.message}`, true);
|
|
61
113
|
}
|
|
62
114
|
}
|
|
63
115
|
|
|
64
|
-
// Determine deployment source (prefer dist, fallback to root)
|
|
65
116
|
const distPath = path.join(sourceDir, "dist");
|
|
66
117
|
const deploySource = fs.existsSync(distPath) ? distPath : sourceDir;
|
|
118
|
+
const pluginExecutionPath = path.join(executionPath, pluginName);
|
|
67
119
|
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
fs.mkdirSync(executionPath, { recursive: true });
|
|
120
|
+
if (!fs.existsSync(pluginExecutionPath)) {
|
|
121
|
+
fs.mkdirSync(pluginExecutionPath, { recursive: true });
|
|
71
122
|
}
|
|
72
123
|
|
|
73
124
|
try {
|
|
74
|
-
|
|
75
|
-
fs.cpSync(deploySource,
|
|
76
|
-
|
|
125
|
+
writeLog(`Running cpSync for ${pluginName}`);
|
|
126
|
+
fs.cpSync(deploySource, pluginExecutionPath, { recursive: true, force: true });
|
|
127
|
+
writeLog(`Finished cpSync for ${pluginName}`);
|
|
77
128
|
} catch (e) {
|
|
78
|
-
|
|
79
|
-
|
|
129
|
+
writeLog(`cpSync failed for ${pluginName}: ${e.message}`, true);
|
|
130
|
+
}
|
|
131
|
+
return true;
|
|
132
|
+
},
|
|
133
|
+
|
|
134
|
+
rebuild: function(pluginName) {
|
|
135
|
+
const isClaude = process.argv.join(' ').includes('claude');
|
|
136
|
+
const configDir = getAppConfigDir(isClaude ? "claude" : "opencode");
|
|
137
|
+
this.deployToExecutionDir(pluginName, path.join(configDir, "plugin"));
|
|
138
|
+
return "Rebuilt " + pluginName;
|
|
139
|
+
},
|
|
140
|
+
|
|
141
|
+
downgrade: function(pluginName, commitHash) {
|
|
142
|
+
const reposDir = getReposDir();
|
|
143
|
+
const targetDir = path.join(reposDir, pluginName);
|
|
144
|
+
if (fs.existsSync(targetDir)) {
|
|
145
|
+
executeGit(`git fetch origin`, targetDir);
|
|
146
|
+
executeGit(`git checkout ${commitHash}`, targetDir);
|
|
147
|
+
executeGit(`git submodule update --init --recursive`, targetDir);
|
|
148
|
+
return this.rebuild(pluginName);
|
|
80
149
|
}
|
|
150
|
+
return "Repo not found";
|
|
81
151
|
},
|
|
82
152
|
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
153
|
+
disable: function(plugin) {
|
|
154
|
+
const isClaude = process.argv.join(' ').includes('claude');
|
|
155
|
+
const configDir = getAppConfigDir(isClaude ? "claude" : "opencode");
|
|
156
|
+
const pluginsJsonPath = path.join(configDir, "config", "plugins.json");
|
|
157
|
+
if (fs.existsSync(pluginsJsonPath)) {
|
|
158
|
+
let plugins = JSON.parse(fs.readFileSync(pluginsJsonPath, "utf-8"));
|
|
159
|
+
const pluginIndex = plugins.findIndex(p => p.name === plugin.name);
|
|
160
|
+
if (pluginIndex >= 0) {
|
|
161
|
+
plugins[pluginIndex].enabled = false;
|
|
162
|
+
fs.writeFileSync(pluginsJsonPath, JSON.stringify(plugins, null, 2), "utf-8");
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
const pluginExecutionPath = path.join(configDir, "plugin", plugin.name);
|
|
166
|
+
if (fs.existsSync(pluginExecutionPath)) {
|
|
167
|
+
try { fs.rmSync(pluginExecutionPath, { recursive: true, force: true }); } catch (e) {}
|
|
168
|
+
}
|
|
169
|
+
},
|
|
170
|
+
|
|
171
|
+
uninstall: function(plugin) {
|
|
172
|
+
this.disable(plugin);
|
|
173
|
+
const targetDir = path.join(getReposDir(), plugin.name);
|
|
174
|
+
if (fs.existsSync(targetDir)) {
|
|
175
|
+
try { fs.rmSync(targetDir, { recursive: true, force: true }); } catch (e) {}
|
|
176
|
+
}
|
|
91
177
|
}
|
|
92
178
|
};
|
|
179
|
+
|
|
180
|
+
const pluginUpdaterEntry = async function(input) {
|
|
181
|
+
const configDir = (input && input.configDir) ? input.configDir : path.dirname(getReposDir());
|
|
182
|
+
|
|
183
|
+
// 1. GUARANTEE BASE DIRECTORIES EXIST ON LAUNCH
|
|
184
|
+
const reposDir = path.join(configDir, "repos");
|
|
185
|
+
const pluginsDir = path.join(configDir, "plugin");
|
|
186
|
+
if (!fs.existsSync(reposDir)) fs.mkdirSync(reposDir, { recursive: true });
|
|
187
|
+
if (!fs.existsSync(pluginsDir)) fs.mkdirSync(pluginsDir, { recursive: true });
|
|
188
|
+
|
|
189
|
+
if (!global.__PLUGIN_UPDATER_HANDLED_BY_HUB__) {
|
|
190
|
+
updaterAPI.earlyLaunch(configDir);
|
|
191
|
+
|
|
192
|
+
const pluginsJsonPath = path.join(configDir, "config", "plugins.json");
|
|
193
|
+
if (fs.existsSync(pluginsJsonPath)) {
|
|
194
|
+
try {
|
|
195
|
+
const plugins = JSON.parse(fs.readFileSync(pluginsJsonPath, "utf-8"));
|
|
196
|
+
for (const plugin of plugins) {
|
|
197
|
+
if (plugin.url && plugin.enabled !== false && plugin.type !== "npm") {
|
|
198
|
+
const branch = plugin.branch || null;
|
|
199
|
+
const commit = plugin.commit || null;
|
|
200
|
+
updaterAPI.updatePlugin(plugin.name, plugin.url, branch, commit);
|
|
201
|
+
updaterAPI.deployToExecutionDir(plugin.name, pluginsDir);
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
} catch (e) {
|
|
205
|
+
writeLog(`Failed to parse plugins.json: ${e.message}`, true);
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
return {};
|
|
210
|
+
};
|
|
211
|
+
|
|
212
|
+
const apiMethods = { ...updaterAPI };
|
|
213
|
+
delete apiMethods.name;
|
|
214
|
+
Object.assign(pluginUpdaterEntry, apiMethods);
|
|
215
|
+
|
|
216
|
+
// Guarantee the entry point can be activated as a standard plugin
|
|
217
|
+
pluginUpdaterEntry.activate = async function() { return await pluginUpdaterEntry(); };
|
|
218
|
+
|
|
219
|
+
module.exports = pluginUpdaterEntry;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "plugin-updater",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.9",
|
|
4
4
|
"description": "Plugin lifecycle manager for OpenCode and Claude Code launchers",
|
|
5
5
|
"main": "index.js",
|
|
6
6
|
"license": "MIT",
|
|
@@ -9,5 +9,11 @@
|
|
|
9
9
|
"type": "git",
|
|
10
10
|
"url": "git+https://github.com/intisy/plugin-updater.git"
|
|
11
11
|
},
|
|
12
|
-
"keywords": [
|
|
12
|
+
"keywords": [
|
|
13
|
+
"opencode",
|
|
14
|
+
"claude",
|
|
15
|
+
"plugin",
|
|
16
|
+
"updater",
|
|
17
|
+
"lifecycle"
|
|
18
|
+
]
|
|
13
19
|
}
|