plugin-updater 1.1.2 → 1.2.0

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
@@ -29,8 +29,8 @@ flowchart TD
29
29
 
30
30
  %% External & Storage
31
31
  subgraph Storage_and_Network [Storage & External]
32
- GH_REPOS[GitHub (intisy/plugin-*)]
33
- LOCAL_WORKSPACE[(.config/github/repos/intisy/)]
32
+ GH_REPOS[GitHub (intisy-ai/plugin-*)]
33
+ LOCAL_WORKSPACE[(.config/github/repos/intisy-ai/)]
34
34
  CC_PLUGINS[(.claude/plugin/)]
35
35
  OC_PLUGINS[(.config/opencode/plugin/)]
36
36
 
@@ -41,17 +41,32 @@ flowchart TD
41
41
  end
42
42
  ```
43
43
 
44
+ ## Structure
45
+
46
+ - `src/` — TypeScript source (`index` engine + `git`, `npm`, `deploy`, `config`, `log`, `env`, `syncbridge`, `cli`, `commands`).
47
+ - `core/` — git submodule ([`intisy-ai/core`](https://github.com/intisy-ai/core)): shared config + the cross-app command framework, bundled to `core/dist/index.js`.
48
+ - `dist/` — compiled output (generated; not committed). `dist/index.js` is the plugin entry + the `node … config` CLI; `dist/cli.js` is the `plugin-updater` bin.
49
+
50
+ ## Installation
51
+
52
+ plugin-updater is the one plugin added directly to OpenCode's `opencode.jsonc` (every other plugin goes through `plugins.json`); the loaders also resolve and run it on startup. To add it manually:
53
+ ```bash
54
+ npm install -g plugin-updater
55
+ plugin-updater add https://github.com/intisy-ai/<plugin> # register a git plugin
56
+ plugin-updater add https://github.com/intisy-ai/<plugin> --sync # …and mirror it to the other app
57
+ ```
58
+
44
59
  ## Cross-app plugin sync (`sync: true`)
45
60
 
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.
61
+ 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-ai/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
62
 
48
63
  ```jsonc
49
- { "name": "antigravity-auth", "url": "https://github.com/intisy/antigravity-auth", "enabled": true, "autoUpdate": false, "sync": true }
64
+ { "name": "antigravity-auth", "url": "https://github.com/intisy-ai/antigravity-auth", "enabled": true, "autoUpdate": false, "sync": true }
50
65
  ```
51
66
 
52
67
  Set it from the CLI with `--sync`:
53
68
  ```bash
54
- plugin-updater add https://github.com/intisy/antigravity-auth --sync
69
+ plugin-updater add https://github.com/intisy-ai/antigravity-auth --sync
55
70
  ```
56
71
 
57
72
  ## API
@@ -64,6 +79,33 @@ plugin-updater add https://github.com/intisy/antigravity-auth --sync
64
79
  | `uninstall(pluginItem)` | Remove repo and deployed files |
65
80
  | `registerTests(testApi)` | Register sync verification tests |
66
81
 
82
+ ## Commands
83
+
84
+ Deployed automatically to both apps on each `earlyLaunch` (`~/.config/opencode/command/` and `~/.claude/commands/`):
85
+
86
+ | Command | Description |
87
+ | --- | --- |
88
+ | `/plugin-updater-config` | View/change plugin-updater config: `list`, `get <key>`, `set <key> <value>`. 100% of the config is reachable here. |
89
+
90
+ (The loaders own `/plugins`; plugin-updater drives the actual install/update lifecycle behind it.)
91
+
92
+ ## Configuration
93
+
94
+ Config file: `~/.config/opencode/config/plugin-updater.json` (preferred) or `~/.config/opencode/plugin-updater.json` (fallback); same under `~/.claude` for Claude Code.
95
+
96
+ | Key | Type | Default | Description |
97
+ | --- | --- | --- | --- |
98
+ | `logging` | boolean | `true` | Write a per-session log file. Set `false` to disable. |
99
+
100
+ ## Dependencies
101
+
102
+ - **`core`** (required) — bundled git submodule (config + command framework); no separate install.
103
+ - **`sync-bridge`** (optional) — loaded at runtime for `syncPlugins()`; absent, cross-app sync no-ops.
104
+
105
+ ## Logging
106
+
107
+ Logs to `~/.config/opencode/logs/YYYY-MM-DD/plugin-updater-HH-MM-SS.log` (Claude: under `~/.claude/`). Set `"logging": false` to disable.
108
+
67
109
  ## License
68
110
 
69
111
  MIT
package/core/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025 intisy
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/core/README.md ADDED
@@ -0,0 +1,84 @@
1
+ # core
2
+
3
+ The shared foundation every plugin in the ecosystem builds on. Consumed as a git
4
+ submodule and bundled into each plugin (like `core-auth` / `core-loader`), so there
5
+ is no runtime install. It supersedes `core-log` (whose config + logging API lives
6
+ here now) and adds app detection, the opencode/claude hook guard, file helpers, and
7
+ a **cross-app command + config-command framework**.
8
+
9
+ ## Under-the-Hood Architecture
10
+
11
+ ```mermaid
12
+ flowchart TD
13
+ PLUGIN["any plugin (utility / provider / loader)"] -->|imports + bundles| CORE["core (this repo, submodule)"]
14
+ CORE --> ENV["env: getApp / getAppConfigDir / existingApps"]
15
+ CORE --> CFG["config: load / get / set / list (config/<name>.json)"]
16
+ CORE --> LOG["log: createLogger / makeWriteLog"]
17
+ CORE --> FILES["files: atomicWrite / readJson / writeJson"]
18
+ CORE --> HOOK["hook: isHookInvocation guard"]
19
+ CORE --> CMD["command: deployCommands / configCommand"]
20
+ CMD -->|writes *.md| OCDIR["~/.config/opencode/command/"]
21
+ CMD -->|writes *.md| CCDIR["~/.claude/commands/"]
22
+ OCDIR -->|/<plugin>-config runs| CLI["node <bundle> config … (maybeRunConfigCli)"]
23
+ CCDIR -->|/<plugin>-config runs| CLI
24
+ CLI --> CFG
25
+ ```
26
+
27
+ ## Structure
28
+ - `src/` — `env`, `config`, `log`, `files`, `hook`, `command`, `configcli`, `index` (barrel)
29
+ - `dist/` — single bundled `index.js` (generated; not committed). The config CLI ships inside it.
30
+
31
+ ## Installation (for a plugin author)
32
+ Add as a submodule and bundle it (esbuild `bundle: true`), importing from `../core/dist/index.js`:
33
+ ```bash
34
+ git submodule add https://github.com/intisy-ai/core core
35
+ ```
36
+ `core` is **not published to npm** — it's a bundled submodule. (Loaders/providers that already carry `core-loader`/`core-auth` can nest `core` inside those, or add it as a second submodule.)
37
+
38
+ ## API
39
+ ```ts
40
+ import {
41
+ getApp, isClaude, getAppConfigDir, existingApps, // env
42
+ loadConfig, getConfigValue, setConfigValue, listConfig, // config
43
+ createLogger, makeWriteLog, // log
44
+ atomicWrite, readJson, writeJson, ensureDir, // files
45
+ isHookInvocation, // hook guard
46
+ deployCommands, configCommand, maybeRunConfigCli, // commands
47
+ } from "../core/dist/index.js";
48
+ ```
49
+
50
+ ### Commands (work in both opencode and Claude Code)
51
+ Both apps read markdown slash-commands from a directory (`<cfg>/command/` for opencode,
52
+ `<cfg>/commands/` for claude). `deployCommands(pluginName, defs)` writes each command to
53
+ **both**, so one definition works everywhere. A command may run a shell line whose output
54
+ is injected, and `{{BUNDLE}}` resolves to the plugin's deployed file:
55
+
56
+ ```ts
57
+ import { deployCommands, configCommand } from "../core/dist/index.js";
58
+ deployCommands("wakatime-sync", [
59
+ configCommand("wakatime-sync"), // /wakatime-sync-config (100% config)
60
+ { name: "wakatime", description: "Today's tracked time", shell: 'node "{{BUNDLE}}" today' },
61
+ ]);
62
+ ```
63
+
64
+ ### 100% configurable via commands
65
+ `configCommand(name)` generates a `/<name>-config` command with `list | get <key> | set <key> <value>`.
66
+ It shells into the plugin's own bundle, which must call `maybeRunConfigCli` at the top of its entry:
67
+
68
+ ```ts
69
+ import { maybeRunConfigCli } from "../core/dist/index.js";
70
+ if (maybeRunConfigCli("wakatime-sync")) { /* ran as `node bundle config …`; stop here */ }
71
+ else { /* normal plugin activation */ }
72
+ ```
73
+ Every key in `config/<name>.json` is then reachable (`set` coerces `true`/`false`/numbers/JSON).
74
+
75
+ ## Configuration
76
+ `core` itself has no config. It reads each consuming plugin's `config/<name>.json`
77
+ (preferred) or `<name>.json` (fallback) under the app's config dir.
78
+
79
+ ## Logging
80
+ Via `createLogger(name)` / `makeWriteLog(name)` → `<configDir>/logs/YYYY-MM-DD/<name>-HH-MM-SS.log`,
81
+ toggle with `"logging": false` in the plugin's config.
82
+
83
+ ## License
84
+ MIT
@@ -0,0 +1,2 @@
1
+ export declare function deployUpdaterCommands(): void;
2
+ export declare function maybeRunCli(): Promise<boolean>;
@@ -0,0 +1,36 @@
1
+ // @ts-nocheck
2
+ // Cross-app slash-command for plugin-updater: /plugin-updater-config (the loaders
3
+ // own /plugins). plugin-updater is an npm package (not deployed at plugin/<name>.js),
4
+ // so the command shells into this same bundle's index.js by its absolute path.
5
+ import { fileURLToPath } from "url";
6
+ import { dirname, join } from "path";
7
+ import { runConfigCli, deployCommands } from "../core/dist/index.js";
8
+ const PLUGIN = "plugin-updater";
9
+ // the deployed entry that carries the maybeRunCli guard (dist/index.js, sibling).
10
+ const SELF = join(dirname(fileURLToPath(import.meta.url)), "index.js");
11
+ export function deployUpdaterCommands() {
12
+ try {
13
+ deployCommands(PLUGIN, [
14
+ {
15
+ name: "plugin-updater-config",
16
+ description: "View/change plugin-updater configuration",
17
+ argumentHint: "list | get <key> | set <key> <value>",
18
+ shell: `node "${SELF}" config $ARGUMENTS`,
19
+ body: "Above is the plugin-updater config result. Report it; if the user changed a setting, confirm the new value.",
20
+ },
21
+ ]);
22
+ }
23
+ catch {
24
+ /* best-effort */
25
+ }
26
+ }
27
+ // If invoked as `node dist/index.js config …`, run the config CLI and return true
28
+ // so the entry exits before the updater/self-activate sequence.
29
+ export async function maybeRunCli() {
30
+ const argv = process.argv.slice(2);
31
+ if (argv[0] === "config") {
32
+ runConfigCli(PLUGIN, argv.slice(1));
33
+ return true;
34
+ }
35
+ return false;
36
+ }
package/dist/index.js CHANGED
@@ -5,8 +5,15 @@ import { selfUpdate, updateNpmPlugin } from "./npm.js";
5
5
  import { updatePlugin } from "./git.js";
6
6
  import { deployToExecutionDir } from "./deploy.js";
7
7
  import { syncPluginsAcrossApps } from "./syncbridge.js";
8
+ // @ts-ignore — generated bundle, no .d.ts
9
+ import { maybeRunCli, deployUpdaterCommands } from "./commands.js";
8
10
  import path from "path";
9
11
  import fs from "fs";
12
+ // `node dist/index.js config …` (from the /plugin-updater-config command) runs the
13
+ // config CLI and exits, before the self-activation/updater sequence below.
14
+ if (await maybeRunCli()) {
15
+ process.exit(0);
16
+ }
10
17
  // remove repos/ clones and deployed plugin/ files for plugins no longer in
11
18
  // plugins.json, so a removed/renamed plugin stops showing up
12
19
  function pruneOrphans(configDir, plugins) {
@@ -57,6 +64,11 @@ export async function earlyLaunch(configDir, plugins) {
57
64
  return {};
58
65
  setEarlyLaunchConfigDir(configDir);
59
66
  writeLog("Starting earlyLaunch updater sequence");
67
+ // keep the cross-app /plugin-updater-config command deployed (idempotent)
68
+ try {
69
+ deployUpdaterCommands();
70
+ }
71
+ catch { /* best-effort */ }
60
72
  // pull in any `sync: true` plugins from the other app BEFORE building, then
61
73
  // re-read the list so a freshly-synced-in plugin is cloned/built this pass.
62
74
  await syncPluginsAcrossApps(configDir);
@@ -123,10 +135,12 @@ export async function activate(opencodeHookInput) {
123
135
  await earlyLaunch(configDir, gitPlugins);
124
136
  }
125
137
  // consumers like the loader TUI import this module for its API only — running
126
- // the full updater sequence on import would print over their screen
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
138
+ // the full updater sequence on import would print over their screen.
139
+ // The ACTIVATION guard makes self-activation idempotent PER PROCESS: opencode may
140
+ // load plugin-updater as more than one module instance (its npm-plugin copy plus a
141
+ // loader's separately-resolved copy), and each would otherwise run earlyLaunch. The
142
+ // first sets the flag; later instances (and the loaders' runEarlyLaunchHooks) skip.
143
+ if (process.env.PLUGIN_UPDATER_LIBRARY_MODE !== "1" && process.env.PLUGIN_UPDATER_ACTIVATION !== "1") {
130
144
  process.env.PLUGIN_UPDATER_ACTIVATION = "1";
131
145
  activate();
132
146
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "plugin-updater",
3
- "version": "1.1.2",
3
+ "version": "1.2.0",
4
4
  "description": "Plugin lifecycle manager for OpenCode and Claude Code launchers",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
@@ -11,7 +11,7 @@
11
11
  "author": "intisy",
12
12
  "repository": {
13
13
  "type": "git",
14
- "url": "git+https://github.com/intisy/plugin-updater.git"
14
+ "url": "git+https://github.com/intisy-ai/plugin-updater.git"
15
15
  },
16
16
  "keywords": [
17
17
  "opencode",
@@ -22,6 +22,7 @@
22
22
  ],
23
23
  "files": [
24
24
  "dist/",
25
+ "core/dist/",
25
26
  "README.md",
26
27
  "LICENSE"
27
28
  ],
@@ -29,10 +30,11 @@
29
30
  "node": ">=20.0.0"
30
31
  },
31
32
  "scripts": {
32
- "build": "tsc",
33
+ "build": "npx esbuild core/src/index.ts --bundle --platform=node --format=esm --outfile=core/dist/index.js && tsc",
33
34
  "prepublishOnly": "npm run build"
34
35
  },
35
36
  "devDependencies": {
37
+ "esbuild": "^0.25.12",
36
38
  "@types/node": "^22.0.0",
37
39
  "typescript": "^5.0.0"
38
40
  }