plugin-updater 1.0.0 → 1.0.8

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.
@@ -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 --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
@@ -0,0 +1,6 @@
1
+ <?xml version="1.0" encoding="UTF-8"?>
2
+ <project version="4">
3
+ <component name="VcsDirectoryMappings">
4
+ <mapping directory="" vcs="Git" />
5
+ </component>
6
+ </project>
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,52 @@
1
- const fs = require('fs');
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
- const REPOS_DIR = path.join(require('os').homedir(), '.config', 'github');
6
+ let EARLY_LAUNCH_CONFIG_DIR = null;
6
7
 
7
- function executeGit(cmd, dir) {
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 getReposDir() {
19
+ const isClaude = process.argv.join(' ').includes('claude');
20
+ const appName = isClaude ? "claude" : "opencode";
21
+ return path.join(getAppConfigDir(appName), "repos");
22
+ }
23
+
24
+ function executeGit(command, cwd) {
8
25
  try {
9
- return execSync(cmd, { cwd: dir, timeout: 60000, stdio: "ignore" });
10
- } catch (e) {
11
- console.error(`[Updater] Git command failed: ${cmd} in ${dir}`);
26
+ execSync(command, { cwd, stdio: "ignore" });
27
+ return true;
28
+ } catch (error) {
29
+ console.error(`[Updater] Git error in ${cwd}: ${error.message}`);
12
30
  return false;
13
31
  }
14
32
  }
15
33
 
16
- module.exports = {
34
+ const updaterAPI = {
17
35
  name: "plugin-updater",
18
36
 
19
- /**
20
- * Called by the launcher (OpenCode/Claude Code) to sync a specific plugin
21
- */
37
+ earlyLaunch: function(configDir) {
38
+ EARLY_LAUNCH_CONFIG_DIR = configDir;
39
+ global.__PLUGIN_UPDATER_HANDLED_BY_HUB__ = true;
40
+ },
41
+
22
42
  updatePlugin: function(pluginName, gitUrl, branch = null, commitHash = null) {
23
- const targetDir = path.join(REPOS_DIR, pluginName);
43
+ const reposDir = getReposDir();
44
+ const targetDir = path.join(reposDir, pluginName);
24
45
 
25
- // 1. Ensure directory exists and clone or pull
26
46
  if (!fs.existsSync(targetDir)) {
27
- if (!fs.existsSync(REPOS_DIR)) fs.mkdirSync(REPOS_DIR, { recursive: true });
47
+ if (!fs.existsSync(reposDir)) fs.mkdirSync(reposDir, { recursive: true });
28
48
  const branchFlag = branch ? `--branch ${branch}` : "";
29
- executeGit(`git clone --recurse-submodules ${branchFlag} ${gitUrl} ${pluginName}`, REPOS_DIR);
49
+ executeGit(`git clone --recurse-submodules ${branchFlag} ${gitUrl} ${pluginName}`, reposDir);
30
50
  } else {
31
51
  executeGit("git fetch origin", targetDir);
32
52
  if (commitHash) {
@@ -40,53 +60,91 @@ module.exports = {
40
60
  }
41
61
  executeGit("git submodule update --init --recursive", targetDir);
42
62
  }
43
-
44
63
  return true;
45
64
  },
46
65
 
47
- /**
48
- * Called to deploy the compiled output to the execution directory
49
- */
50
66
  deployToExecutionDir: function(pluginName, executionPath) {
51
- const sourceDir = path.join(REPOS_DIR, pluginName);
67
+ const sourceDir = path.join(getReposDir(), pluginName);
52
68
  if (!fs.existsSync(sourceDir)) return false;
53
69
 
54
- // Build if package.json exists
55
70
  if (fs.existsSync(path.join(sourceDir, "package.json"))) {
56
71
  try {
57
72
  execSync("npm install", { cwd: sourceDir, stdio: "ignore" });
58
73
  execSync("npm run build", { cwd: sourceDir, stdio: "ignore" });
59
- } catch (e) {
60
- // Fallback or ignore if no build step
74
+ } catch (error) {
75
+ console.error(`[Updater] Build failed for ${pluginName}: ${error.message}`);
61
76
  }
62
77
  }
63
78
 
64
- // Determine deployment source (prefer dist, fallback to root)
65
79
  const distPath = path.join(sourceDir, "dist");
66
80
  const deploySource = fs.existsSync(distPath) ? distPath : sourceDir;
81
+ const pluginExecutionPath = path.join(executionPath, pluginName);
67
82
 
68
- // Copy to execution path
69
- if (!fs.existsSync(executionPath)) {
70
- fs.mkdirSync(executionPath, { recursive: true });
83
+ if (!fs.existsSync(pluginExecutionPath)) {
84
+ fs.mkdirSync(pluginExecutionPath, { recursive: true });
71
85
  }
72
86
 
73
87
  try {
74
- // Platform agnostic copy (using Node fs)
75
- fs.cpSync(deploySource, executionPath, { recursive: true, force: true });
88
+ fs.cpSync(deploySource, pluginExecutionPath, { recursive: true, force: true });
76
89
  return true;
77
- } catch (e) {
78
- console.error(`[Updater] Deploy failed: ${e.message}`);
90
+ } catch (error) {
91
+ console.error(`[Updater] Deploy failed for ${pluginName}: ${error.message}`);
79
92
  return false;
80
93
  }
81
94
  },
82
95
 
83
- /**
84
- * Specific logic to install/update the launcher itself
85
- */
86
- installLauncher: function() {
87
- // Logic to install opencode-hub / claude-hub if they are missing
88
- this.updatePlugin("core-hub", "https://github.com/intisy/core-hub.git");
89
- this.updatePlugin("opencode-hub", "https://github.com/intisy/opencode-hub.git");
90
- this.updatePlugin("claude-hub", "https://github.com/intisy/claude-hub.git");
96
+ rebuild: function(pluginObjOrName) {
97
+ const pluginName = typeof pluginObjOrName === 'string' ? pluginObjOrName : pluginObjOrName.name;
98
+ const targetDir = path.join(getReposDir(), pluginName);
99
+ if (fs.existsSync(targetDir)) {
100
+ try { fs.rmSync(targetDir, { recursive: true, force: true }); } catch (e) {}
101
+ }
102
+ return null;
103
+ },
104
+
105
+ disable: function(plugin) {
106
+ try {
107
+ const configDir = EARLY_LAUNCH_CONFIG_DIR || path.dirname(getReposDir());
108
+ const pluginExecutionPath = path.join(configDir, "plugin", plugin.name);
109
+ if (fs.existsSync(pluginExecutionPath)) {
110
+ fs.rmSync(pluginExecutionPath, { recursive: true, force: true });
111
+ }
112
+ } catch (e) {}
113
+ },
114
+
115
+ uninstall: function(plugin) {
116
+ this.disable(plugin);
117
+ const targetDir = path.join(getReposDir(), plugin.name);
118
+ if (fs.existsSync(targetDir)) {
119
+ try { fs.rmSync(targetDir, { recursive: true, force: true }); } catch (e) {}
120
+ }
91
121
  }
92
122
  };
123
+
124
+ const pluginUpdaterEntry = async function(input) {
125
+ if (!global.__PLUGIN_UPDATER_HANDLED_BY_HUB__) {
126
+ const configDir = (input && input.configDir) ? input.configDir : path.dirname(getReposDir());
127
+ updaterAPI.earlyLaunch(configDir);
128
+
129
+ const pluginsJsonPath = path.join(configDir, "config", "plugins.json");
130
+ if (fs.existsSync(pluginsJsonPath)) {
131
+ try {
132
+ const plugins = JSON.parse(fs.readFileSync(pluginsJsonPath, "utf-8"));
133
+ for (const plugin of plugins) {
134
+ if (plugin.url && plugin.enabled !== false && plugin.type !== "npm") {
135
+ updaterAPI.updatePlugin(plugin.name, plugin.url, plugin.branch || null, plugin.commit || null);
136
+ updaterAPI.deployToExecutionDir(plugin.name, path.join(configDir, "plugin"));
137
+ }
138
+ }
139
+ } catch (e) {
140
+ console.error("[Updater] Failed to parse plugins.json", e);
141
+ }
142
+ }
143
+ }
144
+ return {};
145
+ };
146
+
147
+ const apiMethods = { ...updaterAPI };
148
+ delete apiMethods.name;
149
+ Object.assign(pluginUpdaterEntry, apiMethods);
150
+ module.exports = pluginUpdaterEntry;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "plugin-updater",
3
- "version": "1.0.0",
3
+ "version": "1.0.8",
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": ["opencode", "claude", "plugin", "updater", "lifecycle"]
12
+ "keywords": [
13
+ "opencode",
14
+ "claude",
15
+ "plugin",
16
+ "updater",
17
+ "lifecycle"
18
+ ]
13
19
  }