plugin-updater 1.3.5 → 1.4.1
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 +1 -1
- package/dist/cli.js +97 -19
- package/dist/commands.js +29 -7
- package/dist/init.d.ts +13 -0
- package/dist/init.js +65 -0
- package/lib/core.js +48 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -91,7 +91,7 @@ Deployed automatically to both apps on each `earlyLaunch` (`~/.config/opencode/c
|
|
|
91
91
|
|
|
92
92
|
## Configuration
|
|
93
93
|
|
|
94
|
-
> Config files are **auto-created with defaults
|
|
94
|
+
> Config files are **never auto-created on launch** — settings are registered with defaults (core `defineConfig`) and edited in the loader's **Plugins → Configure** screen (or `/<plugin>-config`); a file is written only when you change a value. **Global console logging** for every plugin is toggled in `config/settings.json` (`logConsole: true`, the opencode.json-equivalent).
|
|
95
95
|
|
|
96
96
|
Config file: `~/.config/opencode/config/plugin-updater.json` (preferred) or `~/.config/opencode/plugin-updater.json` (fallback); same under `~/.claude` for Claude Code.
|
|
97
97
|
|
package/dist/cli.js
CHANGED
|
@@ -4,6 +4,7 @@ process.env.PLUGIN_UPDATER_CLI = "1";
|
|
|
4
4
|
import fs from "fs";
|
|
5
5
|
import path from "path";
|
|
6
6
|
import os from "os";
|
|
7
|
+
import { resolveOpencodeConfigPath, insertPluginIntoJsonc, resolveInitApps } from "./init.js";
|
|
7
8
|
function parseArgs(argv) {
|
|
8
9
|
const parsed = { command: argv[0] ?? "", urls: [] };
|
|
9
10
|
for (let i = 1; i < argv.length; i++) {
|
|
@@ -83,18 +84,79 @@ function registerClaudeHook(configDir) {
|
|
|
83
84
|
console.log(`Registered SessionStart hook in ${settingsPath}`);
|
|
84
85
|
}
|
|
85
86
|
function registerOpencodePlugin(configDir) {
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
const
|
|
89
|
-
|
|
90
|
-
|
|
87
|
+
// Edit the EXISTING opencode config (opencode.json, else opencode.jsonc) rather than
|
|
88
|
+
// always creating opencode.json — opencode reads either and two files are confusing.
|
|
89
|
+
const ocPath = resolveOpencodeConfigPath(configDir);
|
|
90
|
+
const exists = fs.existsSync(ocPath);
|
|
91
|
+
const raw = exists ? fs.readFileSync(ocPath, "utf8") : "";
|
|
92
|
+
const parsed = (exists ? readJson(ocPath) : null) ?? null;
|
|
93
|
+
// opencode plugin entries may be a string OR a [name, options] tuple — guard accordingly
|
|
94
|
+
const plugins = Array.isArray(parsed?.plugin) ? parsed.plugin : [];
|
|
95
|
+
const has = plugins.some((p) => p === "plugin-updater"
|
|
96
|
+
|| (typeof p === "string" && p.startsWith("plugin-updater@"))
|
|
97
|
+
|| (Array.isArray(p) && p[0] === "plugin-updater"));
|
|
98
|
+
if (has) {
|
|
99
|
+
console.log(`plugin-updater already registered in ${ocPath}`);
|
|
100
|
+
return;
|
|
91
101
|
}
|
|
92
|
-
|
|
102
|
+
// Comment-preserving in-place insert for an existing file; fall back to a fresh JSON
|
|
103
|
+
// write only when there's no file or the text can't be safely edited.
|
|
104
|
+
if (exists && raw.trim()) {
|
|
105
|
+
const edited = insertPluginIntoJsonc(raw, "plugin-updater", Array.isArray(parsed?.plugin));
|
|
106
|
+
if (edited) {
|
|
107
|
+
fs.writeFileSync(ocPath, edited, "utf8");
|
|
108
|
+
console.log(`Registered plugin-updater in ${ocPath}`);
|
|
109
|
+
return;
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
const oc = (parsed ?? {});
|
|
113
|
+
oc.plugin = ["plugin-updater", ...plugins];
|
|
93
114
|
if (!oc.$schema)
|
|
94
115
|
oc.$schema = "https://opencode.ai/config.json";
|
|
95
116
|
fs.writeFileSync(ocPath, JSON.stringify(oc, null, 2), "utf8");
|
|
96
117
|
console.log(`Registered plugin-updater in ${ocPath}`);
|
|
97
118
|
}
|
|
119
|
+
// which apps are installed on this machine (used when no --app is given)
|
|
120
|
+
function presentApps() {
|
|
121
|
+
const claude = fs.existsSync(path.join(os.homedir(), ".claude")) || binaryExists("claude");
|
|
122
|
+
const opencode = fs.existsSync(path.join(os.homedir(), ".opencode"))
|
|
123
|
+
|| fs.existsSync(path.join(os.homedir(), ".config", "opencode"))
|
|
124
|
+
|| binaryExists("opencode");
|
|
125
|
+
return { claude, opencode };
|
|
126
|
+
}
|
|
127
|
+
// infer the app from the current directory (the prompt's default suggestion)
|
|
128
|
+
function cwdApp() {
|
|
129
|
+
const cwd = process.cwd().replace(/\\/g, "/");
|
|
130
|
+
if (/(^|\/)\.claude(\/|$)/.test(cwd))
|
|
131
|
+
return "claude";
|
|
132
|
+
if (/(^|\/)\.opencode(\/|$)/.test(cwd) || /(^|\/)\.config\/opencode(\/|$)/.test(cwd))
|
|
133
|
+
return "opencode";
|
|
134
|
+
return null;
|
|
135
|
+
}
|
|
136
|
+
// interactive picker shown when both/neither app is detected and no --app was passed
|
|
137
|
+
async function promptInitApps(_present, defaultApp) {
|
|
138
|
+
const readline = await import("readline/promises");
|
|
139
|
+
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
140
|
+
try {
|
|
141
|
+
const menu = [
|
|
142
|
+
"Initialize plugins for which app?",
|
|
143
|
+
` [1] opencode${defaultApp === "opencode" ? " (default)" : ""}`,
|
|
144
|
+
` [2] claude${defaultApp === "claude" ? " (default)" : ""}`,
|
|
145
|
+
" [3] both",
|
|
146
|
+
].join("\n");
|
|
147
|
+
const ans = (await rl.question(`${menu}\n> [${defaultApp}] `)).trim().toLowerCase();
|
|
148
|
+
if (ans === "3" || ans === "both")
|
|
149
|
+
return ["opencode", "claude"];
|
|
150
|
+
if (ans === "1" || ans === "opencode")
|
|
151
|
+
return ["opencode"];
|
|
152
|
+
if (ans === "2" || ans === "claude")
|
|
153
|
+
return ["claude"];
|
|
154
|
+
return [defaultApp];
|
|
155
|
+
}
|
|
156
|
+
finally {
|
|
157
|
+
rl.close();
|
|
158
|
+
}
|
|
159
|
+
}
|
|
98
160
|
function addPluginEntry(configDir, url, branch, sync) {
|
|
99
161
|
const cleanUrl = url.replace(/\.git$/, "");
|
|
100
162
|
const name = cleanUrl.split("/").pop() ?? cleanUrl;
|
|
@@ -150,25 +212,41 @@ async function main() {
|
|
|
150
212
|
console.log("usage: plugin-updater <init|add|remove|run> [git-urls-or-names...] [--app claude|opencode] [--branch name] [--sync]");
|
|
151
213
|
process.exit(parsed.command ? 1 : 0);
|
|
152
214
|
}
|
|
215
|
+
const updater = await import("./index.js");
|
|
216
|
+
// `init` may target one or both apps: explicit --app wins; otherwise a single
|
|
217
|
+
// detected app is used, and an ambiguous (both/neither) interactive run is prompted.
|
|
218
|
+
if (parsed.command === "init") {
|
|
219
|
+
const apps = await resolveInitApps(parsed.app, {
|
|
220
|
+
present: presentApps,
|
|
221
|
+
isTTY: Boolean(process.stdin.isTTY),
|
|
222
|
+
cwdApp,
|
|
223
|
+
prompt: promptInitApps,
|
|
224
|
+
});
|
|
225
|
+
for (const app of apps) {
|
|
226
|
+
process.env.PLUGIN_UPDATER_APP = app;
|
|
227
|
+
const configDir = getConfigDir(app);
|
|
228
|
+
if (!fs.existsSync(configDir))
|
|
229
|
+
fs.mkdirSync(configDir, { recursive: true });
|
|
230
|
+
console.log(`App: ${app} (${configDir})`);
|
|
231
|
+
ensurePluginsJson(configDir);
|
|
232
|
+
if (app === "claude")
|
|
233
|
+
registerClaudeHook(configDir);
|
|
234
|
+
else
|
|
235
|
+
registerOpencodePlugin(configDir);
|
|
236
|
+
for (const url of parsed.urls) {
|
|
237
|
+
await setupEntry(updater, configDir, url, parsed.branch, parsed.sync);
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
console.log("Init complete.");
|
|
241
|
+
return;
|
|
242
|
+
}
|
|
153
243
|
const app = detectApp(parsed.app);
|
|
154
244
|
process.env.PLUGIN_UPDATER_APP = app;
|
|
155
245
|
const configDir = getConfigDir(app);
|
|
156
246
|
if (!fs.existsSync(configDir))
|
|
157
247
|
fs.mkdirSync(configDir, { recursive: true });
|
|
158
248
|
console.log(`App: ${app} (${configDir})`);
|
|
159
|
-
|
|
160
|
-
if (parsed.command === "init") {
|
|
161
|
-
ensurePluginsJson(configDir);
|
|
162
|
-
if (app === "claude")
|
|
163
|
-
registerClaudeHook(configDir);
|
|
164
|
-
else
|
|
165
|
-
registerOpencodePlugin(configDir);
|
|
166
|
-
for (const url of parsed.urls) {
|
|
167
|
-
await setupEntry(updater, configDir, url, parsed.branch, parsed.sync);
|
|
168
|
-
}
|
|
169
|
-
console.log("Init complete.");
|
|
170
|
-
}
|
|
171
|
-
else if (parsed.command === "add") {
|
|
249
|
+
if (parsed.command === "add") {
|
|
172
250
|
if (parsed.urls.length === 0)
|
|
173
251
|
throw new Error("add requires at least one git url");
|
|
174
252
|
for (const url of parsed.urls) {
|
package/dist/commands.js
CHANGED
|
@@ -1,12 +1,16 @@
|
|
|
1
1
|
// @ts-nocheck
|
|
2
|
-
// Cross-app slash-
|
|
3
|
-
//
|
|
4
|
-
//
|
|
2
|
+
// Cross-app slash-commands for plugin-updater:
|
|
3
|
+
// /plugin-updater-config — plugin-updater's OWN settings (scoped, like every plugin)
|
|
4
|
+
// /config — the UNIFIED entry: global settings + ANY installed plugin
|
|
5
|
+
// plugin-updater is an npm package (not deployed at plugin/<name>.js), so commands shell
|
|
6
|
+
// into this same bundle's index.js by absolute path. The /config dispatcher (config-all)
|
|
7
|
+
// enumerates plugins.json and resolves each plugin's deployed bundle.
|
|
5
8
|
import { fileURLToPath } from "url";
|
|
6
9
|
import { dirname, join } from "path";
|
|
7
|
-
import
|
|
10
|
+
import fs from "fs";
|
|
11
|
+
import { runConfigCli, runAllConfigCli, deployCommands, getAppConfigDir as coreGetAppConfigDir } from "../lib/core.js";
|
|
12
|
+
import { getPlugins } from "./config.js";
|
|
8
13
|
const PLUGIN = "plugin-updater";
|
|
9
|
-
// the deployed entry that carries the maybeRunCli guard (dist/index.js, sibling).
|
|
10
14
|
const SELF = join(dirname(fileURLToPath(import.meta.url)), "index.js");
|
|
11
15
|
export function deployUpdaterCommands() {
|
|
12
16
|
try {
|
|
@@ -18,19 +22,37 @@ export function deployUpdaterCommands() {
|
|
|
18
22
|
shell: `node "${SELF}" config $ARGUMENTS`,
|
|
19
23
|
body: "Above is the plugin-updater config result. Report it; if the user changed a setting, confirm the new value.",
|
|
20
24
|
},
|
|
25
|
+
{
|
|
26
|
+
name: "config",
|
|
27
|
+
description: "View/change ANY plugin's settings and the global settings",
|
|
28
|
+
argumentHint: "[global | <plugin>] [list | get <key> | set <key> <value>]",
|
|
29
|
+
shell: `node "${SELF}" config-all $ARGUMENTS`,
|
|
30
|
+
body: "Above is the unified ecosystem config: the global settings block plus one block per installed plugin. Present it clearly. If the user asked to change a setting, run `/config <target> set <key> <value>` (target is `global` or the plugin name) and confirm the new value.",
|
|
31
|
+
},
|
|
21
32
|
]);
|
|
22
33
|
}
|
|
23
34
|
catch {
|
|
24
35
|
/* best-effort */
|
|
25
36
|
}
|
|
26
37
|
}
|
|
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
38
|
export async function maybeRunCli() {
|
|
30
39
|
const argv = process.argv.slice(2);
|
|
31
40
|
if (argv[0] === "config") {
|
|
32
41
|
runConfigCli(PLUGIN, argv.slice(1));
|
|
33
42
|
return true;
|
|
34
43
|
}
|
|
44
|
+
if (argv[0] === "config-all") {
|
|
45
|
+
// Use core's getAppConfigDir (respects HUB_OPENCODE_DIR / HUB_CLAUDE_DIR env overrides)
|
|
46
|
+
const configDir = coreGetAppConfigDir();
|
|
47
|
+
const names = getPlugins(configDir).map((p) => p.name);
|
|
48
|
+
const resolveBundle = (name) => {
|
|
49
|
+
if (name === PLUGIN)
|
|
50
|
+
return SELF;
|
|
51
|
+
const p = join(configDir, "plugin", `${name}.js`);
|
|
52
|
+
return fs.existsSync(p) ? p : null;
|
|
53
|
+
};
|
|
54
|
+
runAllConfigCli(argv.slice(1), { plugins: [...names, PLUGIN], resolveBundle });
|
|
55
|
+
return true;
|
|
56
|
+
}
|
|
35
57
|
return false;
|
|
36
58
|
}
|
package/dist/init.d.ts
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
export declare function resolveOpencodeConfigPath(configDir: string, exists?: (p: string) => boolean): string;
|
|
2
|
+
export declare function insertPluginIntoJsonc(raw: string, pluginName: string, hasPluginKey: boolean): string | null;
|
|
3
|
+
export interface PresentApps {
|
|
4
|
+
claude: boolean;
|
|
5
|
+
opencode: boolean;
|
|
6
|
+
}
|
|
7
|
+
export interface InitAppDeps {
|
|
8
|
+
present: () => PresentApps;
|
|
9
|
+
isTTY: boolean;
|
|
10
|
+
cwdApp: () => string | null;
|
|
11
|
+
prompt: (present: PresentApps, defaultApp: string) => Promise<string[]>;
|
|
12
|
+
}
|
|
13
|
+
export declare function resolveInitApps(explicit: string | undefined, deps: InitAppDeps): Promise<string[]>;
|
package/dist/init.js
ADDED
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
// Init-time helpers split out of cli.ts so the comment-preserving opencode-config
|
|
2
|
+
// edit and the app-resolution logic are unit-testable without running the CLI entry.
|
|
3
|
+
import fs from "fs";
|
|
4
|
+
import path from "path";
|
|
5
|
+
// ── opencode config target ───────────────────────────────────────────────────
|
|
6
|
+
// `init` must NOT create a fresh opencode.json when the user already has an
|
|
7
|
+
// opencode.jsonc — opencode reads either, and a second file is confusing. Prefer an
|
|
8
|
+
// existing opencode.json, else an existing opencode.jsonc, else default to .json.
|
|
9
|
+
export function resolveOpencodeConfigPath(configDir, exists = fs.existsSync) {
|
|
10
|
+
const jsonPath = path.join(configDir, "opencode.json");
|
|
11
|
+
const jsoncPath = path.join(configDir, "opencode.jsonc");
|
|
12
|
+
if (exists(jsonPath))
|
|
13
|
+
return jsonPath;
|
|
14
|
+
if (exists(jsoncPath))
|
|
15
|
+
return jsoncPath;
|
|
16
|
+
return jsonPath;
|
|
17
|
+
}
|
|
18
|
+
// Insert `pluginName` as the first entry of the root `plugin` array, editing the raw
|
|
19
|
+
// text in place so // comments and formatting elsewhere survive (a JSON.stringify
|
|
20
|
+
// rewrite would strip them). `hasPluginKey` (from the PARSED root, so it can't be
|
|
21
|
+
// fooled by a nested "plugin" key or one inside a string) selects the branch: true →
|
|
22
|
+
// insert into the existing array; false → add a new `plugin` key. Returns the edited
|
|
23
|
+
// text, or null if it can't be safely edited (caller then falls back to a JSON write).
|
|
24
|
+
export function insertPluginIntoJsonc(raw, pluginName, hasPluginKey) {
|
|
25
|
+
const entry = JSON.stringify(pluginName);
|
|
26
|
+
if (hasPluginKey) {
|
|
27
|
+
// Insert right after the root array's `[`. Prefer a line-anchored match so a deeper
|
|
28
|
+
// `"plugin": [` earlier in the file can't be picked; fall back to the first match.
|
|
29
|
+
const m = raw.match(/^[ \t]*"plugin"\s*:\s*\[/m) ?? raw.match(/"plugin"\s*:\s*\[/);
|
|
30
|
+
if (!m || m.index === undefined)
|
|
31
|
+
return null;
|
|
32
|
+
const at = m.index + m[0].length;
|
|
33
|
+
const rest = raw.slice(at);
|
|
34
|
+
const isEmpty = /^\s*\]/.test(rest);
|
|
35
|
+
return raw.slice(0, at) + (isEmpty ? entry : `${entry}, `) + rest;
|
|
36
|
+
}
|
|
37
|
+
// No `plugin` key — insert one right after the root `{`.
|
|
38
|
+
const brace = raw.indexOf("{");
|
|
39
|
+
if (brace === -1)
|
|
40
|
+
return null;
|
|
41
|
+
const afterBrace = raw.slice(brace + 1);
|
|
42
|
+
const isEmptyObject = /^\s*}/.test(afterBrace);
|
|
43
|
+
if (isEmptyObject) {
|
|
44
|
+
return raw.slice(0, brace + 1) + `\n "plugin": [${entry}]\n` + afterBrace;
|
|
45
|
+
}
|
|
46
|
+
return raw.slice(0, brace + 1) + `\n "plugin": [${entry}],` + afterBrace;
|
|
47
|
+
}
|
|
48
|
+
// Decide which app(s) `init` targets. Explicit --app always wins. A single detected
|
|
49
|
+
// app is used directly. When both or neither are detected we PROMPT (if interactive)
|
|
50
|
+
// so the user can pick one or both; non-interactively we keep the hard error rather
|
|
51
|
+
// than guess.
|
|
52
|
+
export async function resolveInitApps(explicit, deps) {
|
|
53
|
+
if (explicit === "claude" || explicit === "opencode")
|
|
54
|
+
return [explicit];
|
|
55
|
+
if (explicit)
|
|
56
|
+
throw new Error(`Unknown app "${explicit}" - use claude or opencode`);
|
|
57
|
+
const p = deps.present();
|
|
58
|
+
if (p.claude !== p.opencode)
|
|
59
|
+
return [p.claude ? "claude" : "opencode"];
|
|
60
|
+
if (!deps.isTTY) {
|
|
61
|
+
throw new Error("Both apps (or neither) found - pass --app claude or --app opencode");
|
|
62
|
+
}
|
|
63
|
+
const defaultApp = deps.cwdApp() ?? "opencode";
|
|
64
|
+
return deps.prompt(p, defaultApp);
|
|
65
|
+
}
|
package/lib/core.js
CHANGED
|
@@ -285,7 +285,54 @@ function maybeRunConfigCli(pluginName) {
|
|
|
285
285
|
}
|
|
286
286
|
return true;
|
|
287
287
|
}
|
|
288
|
+
|
|
289
|
+
// core/src/configcli-all.ts
|
|
290
|
+
import { execFileSync } from "child_process";
|
|
291
|
+
var GLOBAL_SETTINGS_DEFAULTS = { logConsole: false, logColor: true };
|
|
292
|
+
var GLOBAL_NAME = "settings";
|
|
293
|
+
function defaultRunChild(bundle, args) {
|
|
294
|
+
return execFileSync(process.execPath, [bundle, "config", ...args], { encoding: "utf8" });
|
|
295
|
+
}
|
|
296
|
+
function msg(e) {
|
|
297
|
+
return String(e?.message ?? e);
|
|
298
|
+
}
|
|
299
|
+
function runAllConfigCli(argv, opts) {
|
|
300
|
+
defineConfig(GLOBAL_NAME, GLOBAL_SETTINGS_DEFAULTS);
|
|
301
|
+
const runChild = opts.runChild ?? defaultRunChild;
|
|
302
|
+
const [target, ...rest] = argv;
|
|
303
|
+
if (!target || target === "list") {
|
|
304
|
+
console.log("# global");
|
|
305
|
+
runConfigCli(GLOBAL_NAME, ["list"]);
|
|
306
|
+
for (const name of opts.plugins) {
|
|
307
|
+
const bundle2 = opts.resolveBundle(name);
|
|
308
|
+
if (!bundle2) continue;
|
|
309
|
+
console.log(`
|
|
310
|
+
# ${name}`);
|
|
311
|
+
try {
|
|
312
|
+
process.stdout.write(runChild(bundle2, ["list"]));
|
|
313
|
+
} catch (e) {
|
|
314
|
+
console.log(` (could not read ${name}: ${msg(e)})`);
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
return;
|
|
318
|
+
}
|
|
319
|
+
if (target === "global") {
|
|
320
|
+
runConfigCli(GLOBAL_NAME, rest.length ? rest : ["list"]);
|
|
321
|
+
return;
|
|
322
|
+
}
|
|
323
|
+
const bundle = opts.resolveBundle(target);
|
|
324
|
+
if (!bundle) {
|
|
325
|
+
console.log(`Unknown config target: ${target}`);
|
|
326
|
+
return;
|
|
327
|
+
}
|
|
328
|
+
try {
|
|
329
|
+
process.stdout.write(runChild(bundle, rest.length ? rest : ["list"]));
|
|
330
|
+
} catch (e) {
|
|
331
|
+
console.log(`config ${target} failed: ${msg(e)}`);
|
|
332
|
+
}
|
|
333
|
+
}
|
|
288
334
|
export {
|
|
335
|
+
GLOBAL_SETTINGS_DEFAULTS,
|
|
289
336
|
atomicWrite,
|
|
290
337
|
coerce,
|
|
291
338
|
configCommand,
|
|
@@ -309,6 +356,7 @@ export {
|
|
|
309
356
|
makeWriteLog,
|
|
310
357
|
maybeRunConfigCli,
|
|
311
358
|
readJson,
|
|
359
|
+
runAllConfigCli,
|
|
312
360
|
runConfigCli,
|
|
313
361
|
setConfigValue,
|
|
314
362
|
writeJson
|