plugin-updater 1.3.0 → 1.3.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/dist/commands.js CHANGED
@@ -4,7 +4,7 @@
4
4
  // so the command shells into this same bundle's index.js by its absolute path.
5
5
  import { fileURLToPath } from "url";
6
6
  import { dirname, join } from "path";
7
- import { runConfigCli, deployCommands } from "../core/dist/index.js";
7
+ import { runConfigCli, deployCommands } from "../lib/core.js";
8
8
  const PLUGIN = "plugin-updater";
9
9
  // the deployed entry that carries the maybeRunCli guard (dist/index.js, sibling).
10
10
  const SELF = join(dirname(fileURLToPath(import.meta.url)), "index.js");
package/dist/git.js CHANGED
@@ -4,7 +4,10 @@ import os from "os";
4
4
  import { execSync } from "child_process";
5
5
  import { getReposDir } from "./env.js";
6
6
  import { writeLog } from "./log.js";
7
- const BUILD_OUTPUT_DIRS = ["dist", path.join("core", "dist")];
7
+ // dirs copied back from the temp build into the repo clone. core-loader/dist holds
8
+ // the loaders' TUI (tui.js), run as a separate process — without it `oc`/`cc` find
9
+ // no TUI and fall through to plain opencode/claude.
10
+ const BUILD_OUTPUT_DIRS = ["dist", path.join("core", "dist"), path.join("core-loader", "dist")];
8
11
  export function executeGit(command, cwd) {
9
12
  writeLog(`Executing git: ${command} in ${cwd}`);
10
13
  try {
package/dist/log.js CHANGED
@@ -5,8 +5,9 @@
5
5
  import fs from "fs";
6
6
  import path from "path";
7
7
  import { getAppConfigDir, getAppName } from "./env.js";
8
- // @ts-ignore — generated bundle, no .d.ts
9
- import { makeWriteLog } from "../core/dist/index.js";
8
+ // @ts-ignore — generated bundle (core, esbuild-bundled to lib/ so it ships in the
9
+ // npm tarball; the submodule's own core/dist is gitignored and never published)
10
+ import { makeWriteLog } from "../lib/core.js";
10
11
  let pluginConfig = null;
11
12
  export function getPluginConfig() {
12
13
  if (pluginConfig !== null)
package/lib/core.js ADDED
@@ -0,0 +1,297 @@
1
+ // core/src/env.ts
2
+ import { existsSync } from "fs";
3
+ import { join } from "path";
4
+ import { homedir } from "os";
5
+ function getApp() {
6
+ const override = process.env.CORE_APP;
7
+ if (override === "claude" || override === "opencode") return override;
8
+ return process.argv.join(" ").includes("claude") ? "claude" : "opencode";
9
+ }
10
+ function isClaude() {
11
+ return getApp() === "claude";
12
+ }
13
+ function resolveDir(app) {
14
+ const home = homedir();
15
+ if (app === "claude") {
16
+ const override2 = process.env.HUB_CLAUDE_DIR;
17
+ if (override2 && override2.trim()) return override2.trim();
18
+ const direct = join(home, ".claude");
19
+ return existsSync(direct) ? direct : join(home, ".config", "claude");
20
+ }
21
+ const override = process.env.HUB_OPENCODE_DIR;
22
+ if (override && override.trim()) return override.trim();
23
+ const xdg = join(home, ".config", "opencode");
24
+ return existsSync(xdg) ? xdg : join(home, ".opencode");
25
+ }
26
+ function getAppConfigDir() {
27
+ return resolveDir(getApp());
28
+ }
29
+ function existingConfigDirs() {
30
+ return existingApps().map((a) => a.configDir);
31
+ }
32
+ function existingApps() {
33
+ const out = [];
34
+ for (const app of ["claude", "opencode"]) {
35
+ const dir = resolveDir(app);
36
+ if (existsSync(dir) && !out.some((o) => o.configDir === dir)) {
37
+ out.push({ app, configDir: dir, commandDir: app === "claude" ? "commands" : "command" });
38
+ }
39
+ }
40
+ return out;
41
+ }
42
+
43
+ // core/src/files.ts
44
+ import { existsSync as existsSync2, readFileSync, writeFileSync, mkdirSync, renameSync } from "fs";
45
+ import { dirname } from "path";
46
+ import { randomBytes } from "crypto";
47
+ function ensureDir(dir) {
48
+ if (!existsSync2(dir)) mkdirSync(dir, { recursive: true });
49
+ }
50
+ function atomicWrite(file, content) {
51
+ ensureDir(dirname(file));
52
+ const tmp = file + "." + randomBytes(6).toString("hex") + ".tmp";
53
+ writeFileSync(tmp, content, "utf8");
54
+ renameSync(tmp, file);
55
+ }
56
+ function readJson(file, fallback = null) {
57
+ if (!existsSync2(file)) return fallback;
58
+ try {
59
+ return JSON.parse(readFileSync(file, "utf8").replace(/^\s*\/\/[^\n]*/gm, ""));
60
+ } catch {
61
+ return fallback;
62
+ }
63
+ }
64
+ function writeJson(file, value) {
65
+ atomicWrite(file, JSON.stringify(value, null, 2) + "\n");
66
+ }
67
+
68
+ // core/src/config.ts
69
+ import { join as join2 } from "path";
70
+ import { existsSync as existsSync3 } from "fs";
71
+ var CACHE = {};
72
+ function configPath(name, configDir = getAppConfigDir()) {
73
+ const preferred = join2(configDir, "config", `${name}.json`);
74
+ const fallback = join2(configDir, `${name}.json`);
75
+ if (existsSync3(preferred)) return preferred;
76
+ if (existsSync3(fallback)) return fallback;
77
+ return preferred;
78
+ }
79
+ function loadConfig(name, configDir = getAppConfigDir()) {
80
+ const key = configDir + "::" + name;
81
+ if (CACHE[key]) return CACHE[key];
82
+ const data = readJson(configPath(name, configDir), {});
83
+ CACHE[key] = data && typeof data === "object" && !Array.isArray(data) ? data : {};
84
+ return CACHE[key];
85
+ }
86
+ function getConfigValue(name, key, configDir = getAppConfigDir()) {
87
+ let node = loadConfig(name, configDir);
88
+ for (const part of key.split(".")) {
89
+ if (node && typeof node === "object") node = node[part];
90
+ else return void 0;
91
+ }
92
+ return node;
93
+ }
94
+ function coerce(value) {
95
+ if (value === "true") return true;
96
+ if (value === "false") return false;
97
+ if (value === "null") return null;
98
+ if (value !== "" && !isNaN(Number(value))) return Number(value);
99
+ if (/^[[{]/.test(value.trim())) {
100
+ try {
101
+ return JSON.parse(value);
102
+ } catch {
103
+ }
104
+ }
105
+ return value;
106
+ }
107
+ function setConfigValue(name, key, value, configDir = getAppConfigDir()) {
108
+ const root = { ...loadConfig(name, configDir) };
109
+ const parts = key.split(".");
110
+ let node = root;
111
+ for (let i = 0; i < parts.length - 1; i++) {
112
+ const next = node[parts[i]];
113
+ node[parts[i]] = next && typeof next === "object" && !Array.isArray(next) ? { ...next } : {};
114
+ node = node[parts[i]];
115
+ }
116
+ node[parts[parts.length - 1]] = value;
117
+ const target = join2(configDir, "config", `${name}.json`);
118
+ writeJson(target, root);
119
+ CACHE[configDir + "::" + name] = root;
120
+ }
121
+ function listConfig(name, configDir = getAppConfigDir()) {
122
+ return loadConfig(name, configDir);
123
+ }
124
+
125
+ // core/src/log.ts
126
+ import { join as join3 } from "path";
127
+ import { appendFileSync } from "fs";
128
+ var START_TIME = (/* @__PURE__ */ new Date()).toISOString().replace(/:/g, "-").split(".")[0];
129
+ function globalSetting(key, fallback, configDir = getAppConfigDir()) {
130
+ const v = loadConfig("core", configDir)[key];
131
+ return v === void 0 ? fallback : v;
132
+ }
133
+ function envTruthy(v) {
134
+ return !!v && v !== "0" && v.toLowerCase() !== "false";
135
+ }
136
+ function consoleEnabled(configDir) {
137
+ if (process.env.CORE_LOG_CONSOLE !== void 0) return envTruthy(process.env.CORE_LOG_CONSOLE);
138
+ return globalSetting("logConsole", false, configDir) === true;
139
+ }
140
+ function colorEnabled(configDir) {
141
+ if (process.env.NO_COLOR !== void 0) return false;
142
+ return globalSetting("logColor", true, configDir) !== false;
143
+ }
144
+ var RESET = "\x1B[0m";
145
+ var RED = 31;
146
+ var PALETTE = [36, 32, 33, 35, 34, 96, 92, 93, 95, 94];
147
+ function prefixColor(name) {
148
+ let h = 0;
149
+ for (let i = 0; i < name.length; i++) h = h * 31 + name.charCodeAt(i) >>> 0;
150
+ return PALETTE[h % PALETTE.length];
151
+ }
152
+ function paint(code, s) {
153
+ return `\x1B[${code}m${s}${RESET}`;
154
+ }
155
+ function isLoggingEnabled(name, configDir = getAppConfigDir()) {
156
+ return loadConfig(name, configDir).logging !== false;
157
+ }
158
+ function makeWriteLog(name, configDir = getAppConfigDir()) {
159
+ return function writeLog(message, isError = false) {
160
+ try {
161
+ if (isError || consoleEnabled(configDir)) {
162
+ if (colorEnabled(configDir)) {
163
+ const tag = paint(prefixColor(name), `[${name}]`);
164
+ console.error(`${tag} ${isError ? paint(RED, message) : message}`);
165
+ } else {
166
+ console.error(`[${name}] ${message}`);
167
+ }
168
+ }
169
+ if (!isLoggingEnabled(name, configDir)) return;
170
+ const date = /* @__PURE__ */ new Date();
171
+ const dir = join3(configDir, "logs", date.toISOString().split("T")[0]);
172
+ ensureDir(dir);
173
+ const prefix = isError ? "[ERROR]" : "[INFO]";
174
+ appendFileSync(join3(dir, `${name}-${START_TIME}.log`), `[${date.toISOString()}] ${prefix} ${message}
175
+ `);
176
+ } catch {
177
+ }
178
+ };
179
+ }
180
+ function createLogger(name, configDir = getAppConfigDir()) {
181
+ return {
182
+ getConfig: () => loadConfig(name, configDir),
183
+ isLoggingEnabled: () => isLoggingEnabled(name, configDir),
184
+ writeLog: makeWriteLog(name, configDir)
185
+ };
186
+ }
187
+
188
+ // core/src/hook.ts
189
+ function isHookInvocation(firstArg) {
190
+ return firstArg !== void 0 && typeof firstArg !== "string";
191
+ }
192
+
193
+ // core/src/command.ts
194
+ import { join as join4 } from "path";
195
+ function render(def, bundlePath2) {
196
+ const fm = ["---", `description: ${def.description}`];
197
+ if (def.argumentHint) fm.push(`argument-hint: ${def.argumentHint}`);
198
+ fm.push("---", "");
199
+ const lines = [fm.join("\n")];
200
+ if (def.shell) lines.push("!`" + def.shell.replace(/\{\{BUNDLE\}\}/g, bundlePath2) + "`", "");
201
+ lines.push(def.body || "");
202
+ return lines.join("\n").replace(/\n{3,}/g, "\n\n").trimEnd() + "\n";
203
+ }
204
+ function bundlePath(configDir, pluginName) {
205
+ return join4(configDir, "plugin", `${pluginName}.js`);
206
+ }
207
+ function deployCommands(pluginName, defs) {
208
+ const written = [];
209
+ for (const { configDir, commandDir } of existingApps()) {
210
+ const dir = join4(configDir, commandDir);
211
+ for (const def of defs) {
212
+ const file = join4(dir, `${def.name}.md`);
213
+ atomicWrite(file, render(def, bundlePath(configDir, pluginName)));
214
+ written.push(file);
215
+ }
216
+ }
217
+ return written;
218
+ }
219
+ function configCommand(pluginName, commandName = `${pluginName}-config`) {
220
+ return {
221
+ name: commandName,
222
+ description: `View and change ${pluginName} configuration`,
223
+ argumentHint: "list | get <key> | set <key> <value>",
224
+ shell: `node "{{BUNDLE}}" config $ARGUMENTS`,
225
+ body: `Above is the result of the ${pluginName} config command. Report it to the user; if they asked to change a setting, confirm the new value.`
226
+ };
227
+ }
228
+
229
+ // core/src/configcli.ts
230
+ function runConfigCli(pluginName, argv) {
231
+ const [action, key, ...rest] = argv;
232
+ if (!action || action === "list") {
233
+ const cfg = listConfig(pluginName);
234
+ const keys = Object.keys(cfg);
235
+ if (!keys.length) {
236
+ console.log(`${pluginName}: no config set (using defaults).`);
237
+ return;
238
+ }
239
+ for (const k of keys) console.log(`${k} = ${JSON.stringify(cfg[k])}`);
240
+ return;
241
+ }
242
+ if (action === "get") {
243
+ if (!key) {
244
+ console.log("usage: get <key>");
245
+ return;
246
+ }
247
+ console.log(`${key} = ${JSON.stringify(getConfigValue(pluginName, key))}`);
248
+ return;
249
+ }
250
+ if (action === "set") {
251
+ if (!key || rest.length === 0) {
252
+ console.log("usage: set <key> <value>");
253
+ return;
254
+ }
255
+ const value = coerce(rest.join(" "));
256
+ setConfigValue(pluginName, key, value);
257
+ console.log(`set ${key} = ${JSON.stringify(value)}`);
258
+ return;
259
+ }
260
+ console.log(`${pluginName} config \u2014 usage: list | get <key> | set <key> <value>`);
261
+ }
262
+ function maybeRunConfigCli(pluginName) {
263
+ const argv = process.argv.slice(2);
264
+ if (argv[0] !== "config") return false;
265
+ try {
266
+ runConfigCli(pluginName, argv.slice(1));
267
+ } catch (e) {
268
+ console.error(String(e.message ?? e));
269
+ }
270
+ return true;
271
+ }
272
+ export {
273
+ atomicWrite,
274
+ coerce,
275
+ configCommand,
276
+ configPath,
277
+ createLogger,
278
+ deployCommands,
279
+ ensureDir,
280
+ existingApps,
281
+ existingConfigDirs,
282
+ getApp,
283
+ getAppConfigDir,
284
+ getConfigValue,
285
+ globalSetting,
286
+ isClaude,
287
+ isHookInvocation,
288
+ isLoggingEnabled,
289
+ listConfig,
290
+ loadConfig,
291
+ makeWriteLog,
292
+ maybeRunConfigCli,
293
+ readJson,
294
+ runConfigCli,
295
+ setConfigValue,
296
+ writeJson
297
+ };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "plugin-updater",
3
- "version": "1.3.0",
3
+ "version": "1.3.2",
4
4
  "description": "Plugin lifecycle manager for OpenCode and Claude Code launchers",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
@@ -22,7 +22,7 @@
22
22
  ],
23
23
  "files": [
24
24
  "dist/",
25
- "core/dist/",
25
+ "lib/",
26
26
  "README.md",
27
27
  "LICENSE"
28
28
  ],
@@ -30,7 +30,7 @@
30
30
  "node": ">=20.0.0"
31
31
  },
32
32
  "scripts": {
33
- "build": "npx esbuild core/src/index.ts --bundle --platform=node --format=esm --outfile=core/dist/index.js && tsc",
33
+ "build": "npx esbuild core/src/index.ts --bundle --platform=node --format=esm --outfile=lib/core.js && tsc",
34
34
  "test": "npm run build && vitest run",
35
35
  "prepublishOnly": "npm run build"
36
36
  },
package/core/LICENSE DELETED
@@ -1,21 +0,0 @@
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 DELETED
@@ -1,84 +0,0 @@
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