plugin-updater 1.0.45 → 1.1.2

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/README.md CHANGED
@@ -41,6 +41,19 @@ flowchart TD
41
41
  end
42
42
  ```
43
43
 
44
+ ## Cross-app plugin sync (`sync: true`)
45
+
46
+ A `plugins.json` entry flagged `sync: true` is mirrored into the **other** app's `plugins.json`, so a plugin enabled in OpenCode is also installed in Claude Code (and vice versa). At the start of `earlyLaunch`, plugin-updater loads [sync-bridge](https://github.com/intisy/sync-bridge)'s library bundle (`dist/lib.js`) and calls `syncPlugins()`, then re-reads the list so a freshly-synced-in plugin is cloned and built in the **same** launch. It is additive (never removes) and a no-op when sync-bridge isn't installed.
47
+
48
+ ```jsonc
49
+ { "name": "antigravity-auth", "url": "https://github.com/intisy/antigravity-auth", "enabled": true, "autoUpdate": false, "sync": true }
50
+ ```
51
+
52
+ Set it from the CLI with `--sync`:
53
+ ```bash
54
+ plugin-updater add https://github.com/intisy/antigravity-auth --sync
55
+ ```
56
+
44
57
  ## API
45
58
 
46
59
  | Method | Description |
package/dist/cli.js CHANGED
@@ -11,6 +11,8 @@ function parseArgs(argv) {
11
11
  parsed.app = argv[++i];
12
12
  else if (argv[i] === "--branch")
13
13
  parsed.branch = argv[++i];
14
+ else if (argv[i] === "--sync")
15
+ parsed.sync = true;
14
16
  else
15
17
  parsed.urls.push(argv[i]);
16
18
  }
@@ -93,7 +95,7 @@ function registerOpencodePlugin(configDir) {
93
95
  fs.writeFileSync(ocPath, JSON.stringify(oc, null, 2), "utf8");
94
96
  console.log(`Registered plugin-updater in ${ocPath}`);
95
97
  }
96
- function addPluginEntry(configDir, url, branch) {
98
+ function addPluginEntry(configDir, url, branch, sync) {
97
99
  const cleanUrl = url.replace(/\.git$/, "");
98
100
  const name = cleanUrl.split("/").pop() ?? cleanUrl;
99
101
  ensurePluginsJson(configDir);
@@ -103,10 +105,24 @@ function addPluginEntry(configDir, url, branch) {
103
105
  const entry = { name, url: cleanUrl, enabled: true, autoUpdate: true };
104
106
  if (branch)
105
107
  entry.branch = branch;
108
+ if (sync)
109
+ entry.sync = true;
106
110
  entries.push(entry);
107
111
  fs.writeFileSync(file, JSON.stringify(entries, null, 2), "utf8");
108
112
  console.log(`Added ${name} to ${file}`);
109
113
  }
114
+ else if (sync) {
115
+ // already present: honor --sync by enabling sync on the existing entry
116
+ const existing = entries.find((e) => e.name === name);
117
+ if (existing && existing.sync !== true) {
118
+ existing.sync = true;
119
+ fs.writeFileSync(file, JSON.stringify(entries, null, 2), "utf8");
120
+ console.log(`Enabled sync on ${name} in ${file}`);
121
+ }
122
+ else {
123
+ console.log(`${name} already present (sync on) in ${file}`);
124
+ }
125
+ }
110
126
  else {
111
127
  console.log(`${name} already present in ${file}`);
112
128
  }
@@ -117,8 +133,8 @@ function removePluginEntry(configDir, name) {
117
133
  const entries = readJson(file) ?? [];
118
134
  fs.writeFileSync(file, JSON.stringify(entries.filter((e) => e.name !== name), null, 2), "utf8");
119
135
  }
120
- async function setupEntry(updater, configDir, url, branch) {
121
- const entry = addPluginEntry(configDir, url, branch);
136
+ async function setupEntry(updater, configDir, url, branch, sync) {
137
+ const entry = addPluginEntry(configDir, url, branch, sync);
122
138
  console.log(`Setting up ${entry.name}...`);
123
139
  try {
124
140
  await updater.updatePluginPublic(entry.name, entry.url, entry.branch);
@@ -131,7 +147,7 @@ async function setupEntry(updater, configDir, url, branch) {
131
147
  async function main() {
132
148
  const parsed = parseArgs(process.argv.slice(2));
133
149
  if (!["init", "add", "run", "remove"].includes(parsed.command)) {
134
- console.log("usage: plugin-updater <init|add|remove|run> [git-urls-or-names...] [--app claude|opencode] [--branch name]");
150
+ console.log("usage: plugin-updater <init|add|remove|run> [git-urls-or-names...] [--app claude|opencode] [--branch name] [--sync]");
135
151
  process.exit(parsed.command ? 1 : 0);
136
152
  }
137
153
  const app = detectApp(parsed.app);
@@ -148,7 +164,7 @@ async function main() {
148
164
  else
149
165
  registerOpencodePlugin(configDir);
150
166
  for (const url of parsed.urls) {
151
- await setupEntry(updater, configDir, url, parsed.branch);
167
+ await setupEntry(updater, configDir, url, parsed.branch, parsed.sync);
152
168
  }
153
169
  console.log("Init complete.");
154
170
  }
@@ -156,7 +172,7 @@ async function main() {
156
172
  if (parsed.urls.length === 0)
157
173
  throw new Error("add requires at least one git url");
158
174
  for (const url of parsed.urls) {
159
- await setupEntry(updater, configDir, url, parsed.branch);
175
+ await setupEntry(updater, configDir, url, parsed.branch, parsed.sync);
160
176
  }
161
177
  }
162
178
  else if (parsed.command === "remove") {
package/dist/deploy.js CHANGED
@@ -95,6 +95,13 @@ export async function deployToExecutionDir(pluginName, executionPath, changed, c
95
95
  else if (fs.existsSync(path.join(distPath, "index.js"))) {
96
96
  deploySource = path.join(distPath, "index.js");
97
97
  }
98
+ // the build may have produced nothing (e.g. it failed, or the repo was deployed
99
+ // bundle-only with its source stripped) — skip gracefully rather than throwing
100
+ // ENOENT on the copy. Any already-deployed plugin/<name>.js stays in place.
101
+ if (!fs.existsSync(deploySource)) {
102
+ writeLog(`Skipping deploy for ${pluginName}: built file not found at ${deploySource}`, true);
103
+ return fs.existsSync(pluginExecutionFile);
104
+ }
98
105
  if (!fs.existsSync(executionPath))
99
106
  fs.mkdirSync(executionPath, { recursive: true });
100
107
  await callPluginCleanup(pluginExecutionFile, configDir);
package/dist/index.js CHANGED
@@ -4,6 +4,7 @@ import { getPlugins, readOpencodeJson } from "./config.js";
4
4
  import { selfUpdate, updateNpmPlugin } from "./npm.js";
5
5
  import { updatePlugin } from "./git.js";
6
6
  import { deployToExecutionDir } from "./deploy.js";
7
+ import { syncPluginsAcrossApps } from "./syncbridge.js";
7
8
  import path from "path";
8
9
  import fs from "fs";
9
10
  // remove repos/ clones and deployed plugin/ files for plugins no longer in
@@ -56,6 +57,10 @@ export async function earlyLaunch(configDir, plugins) {
56
57
  return {};
57
58
  setEarlyLaunchConfigDir(configDir);
58
59
  writeLog("Starting earlyLaunch updater sequence");
60
+ // pull in any `sync: true` plugins from the other app BEFORE building, then
61
+ // re-read the list so a freshly-synced-in plugin is cloned/built this pass.
62
+ await syncPluginsAcrossApps(configDir);
63
+ plugins = getPlugins(configDir);
59
64
  selfUpdate(configDir);
60
65
  // npm plugins listed in opencode.json
61
66
  const { plugins: npmNames } = readOpencodeJson(configDir);
@@ -119,5 +124,9 @@ export async function activate(opencodeHookInput) {
119
124
  }
120
125
  // consumers like the loader TUI import this module for its API only — running
121
126
  // the full updater sequence on import would print over their screen
122
- if (process.env.PLUGIN_UPDATER_LIBRARY_MODE !== "1")
127
+ if (process.env.PLUGIN_UPDATER_LIBRARY_MODE !== "1") {
128
+ // signal loaders (which activate later in the same process) that we are
129
+ // self-driving updates, so their runEarlyLaunchHooks skips a duplicate pass
130
+ process.env.PLUGIN_UPDATER_ACTIVATION = "1";
123
131
  activate();
132
+ }
@@ -0,0 +1 @@
1
+ export declare function syncPluginsAcrossApps(configDir: string): Promise<void>;
@@ -0,0 +1,41 @@
1
+ import fs from "fs";
2
+ import path from "path";
3
+ import { pathToFileURL } from "url";
4
+ import { writeLog } from "./log.js";
5
+ // sync-bridge is the only component allowed to span both app homes, so the
6
+ // cross-app plugin-list merge lives there. It ships its in-process API as a
7
+ // separate bundle (dist/lib.js) — the plugin hook (dist/index.js) deliberately
8
+ // exports nothing usable. We load that library from the cloned-plugin location
9
+ // where plugin-updater itself deploys git plugins.
10
+ function resolveSyncBridgeLib(configDir) {
11
+ const candidates = [
12
+ path.join(configDir, "repos", "sync-bridge", "dist", "lib.js"),
13
+ ];
14
+ for (const candidate of candidates) {
15
+ if (fs.existsSync(candidate))
16
+ return candidate;
17
+ }
18
+ return null;
19
+ }
20
+ // Mirror every plugins.json entry flagged `sync: true` into the other app's
21
+ // plugins.json. A no-op (logged, never thrown) when sync-bridge isn't installed
22
+ // or is an older version without syncPlugins.
23
+ export async function syncPluginsAcrossApps(configDir) {
24
+ const libPath = resolveSyncBridgeLib(configDir);
25
+ if (!libPath) {
26
+ writeLog("sync-bridge not installed; skipping cross-app plugin sync");
27
+ return;
28
+ }
29
+ try {
30
+ const bridge = (await import(pathToFileURL(libPath).href));
31
+ if (typeof bridge.syncPlugins !== "function") {
32
+ writeLog("sync-bridge has no syncPlugins (older version); skipping cross-app plugin sync", true);
33
+ return;
34
+ }
35
+ const result = bridge.syncPlugins();
36
+ writeLog(`Cross-app plugin sync: ${JSON.stringify(result)}`);
37
+ }
38
+ catch (e) {
39
+ writeLog(`Cross-app plugin sync failed: ${e.message}`, true);
40
+ }
41
+ }
package/dist/types.d.ts CHANGED
@@ -5,6 +5,7 @@ export interface Plugin {
5
5
  enabled?: boolean;
6
6
  autoUpdate?: boolean;
7
7
  updateInterval?: number;
8
+ sync?: boolean;
8
9
  }
9
10
  export interface NpmPlugin {
10
11
  name: string;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "plugin-updater",
3
- "version": "1.0.45",
3
+ "version": "1.1.2",
4
4
  "description": "Plugin lifecycle manager for OpenCode and Claude Code launchers",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",