opencode-plugin-auto-update 0.1.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 +127 -0
- package/dist/index.d.ts +31 -0
- package/dist/index.js +386 -0
- package/package.json +42 -0
package/README.md
ADDED
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
# Opencode Plugin Auto-Update
|
|
2
|
+
|
|
3
|
+
[](https://www.npmjs.com/package/opencode-plugin-auto-update)
|
|
4
|
+
[](https://opensource.org/licenses/MIT)
|
|
5
|
+
|
|
6
|
+
Automatically updates OpenCode plugins in the background on startup. No prompts, no blocking IO, and minimal logging by default.
|
|
7
|
+
|
|
8
|
+
## 🚀 Overview
|
|
9
|
+
|
|
10
|
+
- Runs a background update pass on startup
|
|
11
|
+
- Throttled to once every 24 hours
|
|
12
|
+
- Uses a lock file to prevent concurrent runs
|
|
13
|
+
- Updates to latest by default (optional pin-preserving mode)
|
|
14
|
+
|
|
15
|
+
## ✨ Features
|
|
16
|
+
|
|
17
|
+
- Zero startup delay (async, detached update)
|
|
18
|
+
- Bun-first installs with npm fallback
|
|
19
|
+
- Silent by default; debug logging via env
|
|
20
|
+
- Skips local/path/git plugins
|
|
21
|
+
|
|
22
|
+
## 📦 Installation
|
|
23
|
+
|
|
24
|
+
### 🤖 Let an LLM do it (Recommended)
|
|
25
|
+
|
|
26
|
+
Paste this into any LLM agent (OpenCode, Claude Code, Cursor, etc.):
|
|
27
|
+
|
|
28
|
+
```text
|
|
29
|
+
Install the opencode-plugin-auto-update plugin by following: https://raw.githubusercontent.com/AnganSamadder/opencode-plugin-auto-update/main/README.md
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
<details>
|
|
33
|
+
<summary><b>Instructions for LLM Agents</b></summary>
|
|
34
|
+
|
|
35
|
+
To install this plugin, please perform the following steps for the user:
|
|
36
|
+
|
|
37
|
+
1. **Install the NPM package globally:**
|
|
38
|
+
```bash
|
|
39
|
+
bun add -g opencode-plugin-auto-update
|
|
40
|
+
# or
|
|
41
|
+
npm install -g opencode-plugin-auto-update
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
2. **Configure OpenCode:**
|
|
45
|
+
Add `"opencode-plugin-auto-update"` to the `plugin` array in the user's config file (usually `~/.config/opencode/opencode.json`).
|
|
46
|
+
|
|
47
|
+
```json
|
|
48
|
+
{
|
|
49
|
+
"plugin": [
|
|
50
|
+
"opencode-plugin-auto-update"
|
|
51
|
+
]
|
|
52
|
+
}
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
3. **Verify & Finalize:**
|
|
56
|
+
Tell the user: **"Installation complete! Your OpenCode plugins will now update automatically in the background."**
|
|
57
|
+
|
|
58
|
+
</details>
|
|
59
|
+
|
|
60
|
+
### 👤 For Humans (Manual)
|
|
61
|
+
|
|
62
|
+
1. **Install:**
|
|
63
|
+
```bash
|
|
64
|
+
bun add -g opencode-plugin-auto-update
|
|
65
|
+
# or
|
|
66
|
+
npm install -g opencode-plugin-auto-update
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
2. **Enable the plugin:**
|
|
70
|
+
Add `"opencode-plugin-auto-update"` to `~/.config/opencode/opencode.json`:
|
|
71
|
+
```json
|
|
72
|
+
{
|
|
73
|
+
"plugin": [
|
|
74
|
+
"opencode-plugin-auto-update"
|
|
75
|
+
]
|
|
76
|
+
}
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
3. **Restart OpenCode:**
|
|
80
|
+
The plugin runs on the next OpenCode startup.
|
|
81
|
+
|
|
82
|
+
## 🛠️ How it works
|
|
83
|
+
|
|
84
|
+
1. On startup, schedules a background update task.
|
|
85
|
+
2. Applies a 24h throttle and lock to avoid repeated updates.
|
|
86
|
+
3. Installs latest versions into `~/.config/opencode/node_modules`.
|
|
87
|
+
4. Rewrites `opencode.json` plugin versions to the latest (unless pinned).
|
|
88
|
+
|
|
89
|
+
## ⚙️ Configuration
|
|
90
|
+
|
|
91
|
+
Configure via environment variables:
|
|
92
|
+
|
|
93
|
+
| Variable | Default | Description |
|
|
94
|
+
|----------|---------|-------------|
|
|
95
|
+
| `OPENCODE_AUTO_UPDATE_DISABLED` | `false` | Disable all updates when `true` |
|
|
96
|
+
| `OPENCODE_AUTO_UPDATE_INTERVAL_HOURS` | `24` | Throttle interval in hours |
|
|
97
|
+
| `OPENCODE_AUTO_UPDATE_DEBUG` | `false` | Enable debug logs |
|
|
98
|
+
| `OPENCODE_AUTO_UPDATE_PINNED` | `false` | Preserve pinned versions |
|
|
99
|
+
| `OPENCODE_AUTO_UPDATE_BYPASS_THROTTLE` | `false` | Ignore throttle (useful for testing) |
|
|
100
|
+
|
|
101
|
+
### CLI Flags
|
|
102
|
+
|
|
103
|
+
- `opencode --log-level DEBUG`: enable debug mode for OpenCode and trigger verbose update logs with throttle bypass.
|
|
104
|
+
|
|
105
|
+
## ❓ Troubleshooting
|
|
106
|
+
|
|
107
|
+
1. **Updates not running**: ensure `OPENCODE_AUTO_UPDATE_DISABLED` is not set to `true`.
|
|
108
|
+
2. **No logs**: run `opencode --log-level DEBUG` or set `OPENCODE_AUTO_UPDATE_DEBUG=true` for verbose output.
|
|
109
|
+
3. **Plugin not loading**: check the `plugin` array in `~/.config/opencode/opencode.json`.
|
|
110
|
+
4. **Testing updates**: run `opencode --log-level DEBUG` to bypass the 24h throttle and see detailed update logs.
|
|
111
|
+
|
|
112
|
+
## 🚀 Release Process
|
|
113
|
+
|
|
114
|
+
1. Update version in `package.json`
|
|
115
|
+
2. Update `CHANGELOG.md`
|
|
116
|
+
3. `bun run build`
|
|
117
|
+
4. `npm publish`
|
|
118
|
+
5. `git tag vX.Y.Z && git push --tags`
|
|
119
|
+
6. `gh release create vX.Y.Z --notes "..."`
|
|
120
|
+
|
|
121
|
+
## 📄 License
|
|
122
|
+
|
|
123
|
+
MIT
|
|
124
|
+
|
|
125
|
+
## 🙏 Acknowledgements
|
|
126
|
+
|
|
127
|
+
Inspired by opencode-agent-tmux and the OpenCode plugin ecosystem.
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
interface PluginInput {
|
|
2
|
+
directory: string;
|
|
3
|
+
serverUrl?: URL | string;
|
|
4
|
+
client: {
|
|
5
|
+
session: {
|
|
6
|
+
status(): Promise<{
|
|
7
|
+
data?: Record<string, {
|
|
8
|
+
type: string;
|
|
9
|
+
}>;
|
|
10
|
+
}>;
|
|
11
|
+
subscribe(callback: (event: {
|
|
12
|
+
type: string;
|
|
13
|
+
properties?: unknown;
|
|
14
|
+
}) => void): () => void;
|
|
15
|
+
};
|
|
16
|
+
};
|
|
17
|
+
}
|
|
18
|
+
interface PluginOutput {
|
|
19
|
+
name: string;
|
|
20
|
+
event?: (input: {
|
|
21
|
+
event: {
|
|
22
|
+
type: string;
|
|
23
|
+
properties?: unknown;
|
|
24
|
+
};
|
|
25
|
+
}) => Promise<void>;
|
|
26
|
+
tool?: Record<string, unknown>;
|
|
27
|
+
config?: unknown;
|
|
28
|
+
}
|
|
29
|
+
declare function export_default(ctx: PluginInput): Promise<PluginOutput>;
|
|
30
|
+
|
|
31
|
+
export { export_default as default };
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,386 @@
|
|
|
1
|
+
// src/update.ts
|
|
2
|
+
import { readFile as readFile2, writeFile as writeFile2, mkdir as mkdir2 } from "fs/promises";
|
|
3
|
+
import { dirname, join as join2 } from "path";
|
|
4
|
+
import { homedir as homedir2 } from "os";
|
|
5
|
+
import { spawn } from "child_process";
|
|
6
|
+
|
|
7
|
+
// src/lock.ts
|
|
8
|
+
import { readFile, writeFile, unlink, mkdir } from "fs/promises";
|
|
9
|
+
import { join } from "path";
|
|
10
|
+
import { homedir, hostname as osHostname } from "os";
|
|
11
|
+
var DEFAULT_CONFIG_DIR = join(homedir(), ".config", "opencode");
|
|
12
|
+
var STALE_LOCK_MS = 2 * 60 * 60 * 1e3;
|
|
13
|
+
function resolvePaths(configDir) {
|
|
14
|
+
const resolvedConfigDir = configDir ?? DEFAULT_CONFIG_DIR;
|
|
15
|
+
return {
|
|
16
|
+
configDir: resolvedConfigDir,
|
|
17
|
+
lockFile: join(resolvedConfigDir, ".auto-update.lock"),
|
|
18
|
+
throttleFile: join(resolvedConfigDir, ".auto-update.json")
|
|
19
|
+
};
|
|
20
|
+
}
|
|
21
|
+
async function isLockStale(lockPath) {
|
|
22
|
+
try {
|
|
23
|
+
const content = await readFile(lockPath, "utf-8");
|
|
24
|
+
const lockData = JSON.parse(content);
|
|
25
|
+
const age = Date.now() - lockData.timestamp;
|
|
26
|
+
return age > STALE_LOCK_MS;
|
|
27
|
+
} catch (error) {
|
|
28
|
+
return false;
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
async function acquireLock(options = {}) {
|
|
32
|
+
const { force = false, debug = false, configDir } = options;
|
|
33
|
+
const { configDir: resolvedConfigDir, lockFile } = resolvePaths(configDir);
|
|
34
|
+
try {
|
|
35
|
+
await mkdir(resolvedConfigDir, { recursive: true });
|
|
36
|
+
try {
|
|
37
|
+
const existingLock = await readFile(lockFile, "utf-8");
|
|
38
|
+
const lockData2 = JSON.parse(existingLock);
|
|
39
|
+
const stale = await isLockStale(lockFile);
|
|
40
|
+
if (stale && force) {
|
|
41
|
+
if (debug) {
|
|
42
|
+
console.log(`[lock] Stale lock detected (pid: ${lockData2.pid}), forcing acquisition`);
|
|
43
|
+
}
|
|
44
|
+
} else if (!stale) {
|
|
45
|
+
if (debug) {
|
|
46
|
+
console.log(`[lock] Lock already held by pid ${lockData2.pid}`);
|
|
47
|
+
}
|
|
48
|
+
return false;
|
|
49
|
+
} else {
|
|
50
|
+
if (debug) {
|
|
51
|
+
console.log(`[lock] Stale lock exists but force=false`);
|
|
52
|
+
}
|
|
53
|
+
return false;
|
|
54
|
+
}
|
|
55
|
+
} catch (error) {
|
|
56
|
+
}
|
|
57
|
+
const lockData = {
|
|
58
|
+
pid: process.pid,
|
|
59
|
+
timestamp: Date.now(),
|
|
60
|
+
hostname: await getHostname()
|
|
61
|
+
};
|
|
62
|
+
await writeFile(lockFile, JSON.stringify(lockData, null, 2), "utf-8");
|
|
63
|
+
if (debug) {
|
|
64
|
+
console.log(`[lock] Lock acquired by pid ${process.pid}`);
|
|
65
|
+
}
|
|
66
|
+
return true;
|
|
67
|
+
} catch (error) {
|
|
68
|
+
if (debug) {
|
|
69
|
+
console.error("[lock] Failed to acquire lock:", error);
|
|
70
|
+
}
|
|
71
|
+
return false;
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
async function releaseLock(options = {}) {
|
|
75
|
+
const { debug = false, configDir } = options;
|
|
76
|
+
const { lockFile } = resolvePaths(configDir);
|
|
77
|
+
try {
|
|
78
|
+
await unlink(lockFile);
|
|
79
|
+
if (debug) {
|
|
80
|
+
console.log(`[lock] Lock released by pid ${process.pid}`);
|
|
81
|
+
}
|
|
82
|
+
} catch (error) {
|
|
83
|
+
if (debug && error.code !== "ENOENT") {
|
|
84
|
+
console.error("[lock] Failed to release lock:", error);
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
async function readThrottleState(options = {}) {
|
|
89
|
+
const { configDir } = options;
|
|
90
|
+
const { throttleFile } = resolvePaths(configDir);
|
|
91
|
+
try {
|
|
92
|
+
const data = await readFile(throttleFile, "utf-8");
|
|
93
|
+
return JSON.parse(data);
|
|
94
|
+
} catch (error) {
|
|
95
|
+
return {};
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
async function writeThrottleState(state, options = {}) {
|
|
99
|
+
const { debug = false, configDir } = options;
|
|
100
|
+
const { configDir: resolvedConfigDir, throttleFile } = resolvePaths(configDir);
|
|
101
|
+
try {
|
|
102
|
+
await mkdir(resolvedConfigDir, { recursive: true });
|
|
103
|
+
await writeFile(throttleFile, JSON.stringify(state, null, 2), "utf-8");
|
|
104
|
+
if (debug) {
|
|
105
|
+
console.log("[throttle] State written:", state);
|
|
106
|
+
}
|
|
107
|
+
} catch (error) {
|
|
108
|
+
if (debug) {
|
|
109
|
+
console.error("[throttle] Failed to write state:", error);
|
|
110
|
+
}
|
|
111
|
+
throw error;
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
async function getHostname() {
|
|
115
|
+
try {
|
|
116
|
+
return osHostname();
|
|
117
|
+
} catch {
|
|
118
|
+
return "unknown";
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
// src/update.ts
|
|
123
|
+
var DEFAULT_CONFIG_DIR2 = join2(homedir2(), ".config", "opencode");
|
|
124
|
+
async function runAutoUpdate(options = {}) {
|
|
125
|
+
const disabled = options.disabled ?? envFlag("OPENCODE_AUTO_UPDATE_DISABLED");
|
|
126
|
+
if (disabled) {
|
|
127
|
+
return;
|
|
128
|
+
}
|
|
129
|
+
const debug = options.debug ?? envFlag("OPENCODE_AUTO_UPDATE_DEBUG");
|
|
130
|
+
const ignoreThrottle = options.ignoreThrottle ?? envFlag("OPENCODE_AUTO_UPDATE_BYPASS_THROTTLE");
|
|
131
|
+
const intervalHours = options.intervalHours ?? envNumber("OPENCODE_AUTO_UPDATE_INTERVAL_HOURS", 24);
|
|
132
|
+
const preservePinned = options.preservePinned ?? envFlag("OPENCODE_AUTO_UPDATE_PINNED");
|
|
133
|
+
const configDir = options.configDir ?? DEFAULT_CONFIG_DIR2;
|
|
134
|
+
const configPath = join2(configDir, "opencode.json");
|
|
135
|
+
const log = (...args) => {
|
|
136
|
+
if (debug) {
|
|
137
|
+
console.log(...args);
|
|
138
|
+
}
|
|
139
|
+
};
|
|
140
|
+
const error = (...args) => {
|
|
141
|
+
if (debug) {
|
|
142
|
+
console.error(...args);
|
|
143
|
+
}
|
|
144
|
+
};
|
|
145
|
+
const lockAcquired = await acquireLock({ debug, configDir });
|
|
146
|
+
if (!lockAcquired) {
|
|
147
|
+
log("[auto-update] Lock already held, skipping.");
|
|
148
|
+
return;
|
|
149
|
+
}
|
|
150
|
+
try {
|
|
151
|
+
const state = await readThrottleState({ configDir });
|
|
152
|
+
const now = Date.now();
|
|
153
|
+
const intervalMs = Math.max(intervalHours, 1) * 60 * 60 * 1e3;
|
|
154
|
+
if (!ignoreThrottle && state.lastRun && now - state.lastRun < intervalMs) {
|
|
155
|
+
log("[auto-update] Throttled, skipping update.");
|
|
156
|
+
return;
|
|
157
|
+
}
|
|
158
|
+
await writeThrottleState({ ...state, lastRun: now }, { debug, configDir });
|
|
159
|
+
const config = await readConfig(configPath);
|
|
160
|
+
const { plugins, key } = getPluginList(config);
|
|
161
|
+
if (!plugins || plugins.length === 0 || !key) {
|
|
162
|
+
log("[auto-update] No plugins found to update.");
|
|
163
|
+
return;
|
|
164
|
+
}
|
|
165
|
+
const useBun = await commandExists("bun");
|
|
166
|
+
log("[auto-update] Starting update", {
|
|
167
|
+
pluginCount: plugins.length,
|
|
168
|
+
useBun,
|
|
169
|
+
preservePinned,
|
|
170
|
+
ignoreThrottle
|
|
171
|
+
});
|
|
172
|
+
const updateResult = await updatePlugins({
|
|
173
|
+
plugins,
|
|
174
|
+
configDir,
|
|
175
|
+
preservePinned,
|
|
176
|
+
useBun,
|
|
177
|
+
log,
|
|
178
|
+
error
|
|
179
|
+
});
|
|
180
|
+
if (updateResult.changed) {
|
|
181
|
+
const updatedConfig = { ...config, [key]: updateResult.plugins };
|
|
182
|
+
await writeConfig(configPath, updatedConfig);
|
|
183
|
+
}
|
|
184
|
+
await writeThrottleState(
|
|
185
|
+
{ ...state, lastRun: now, lastSuccess: Date.now() },
|
|
186
|
+
{ debug, configDir }
|
|
187
|
+
);
|
|
188
|
+
log("[auto-update] Update complete.");
|
|
189
|
+
} catch (err) {
|
|
190
|
+
error("[auto-update] Failed to update plugins:", err);
|
|
191
|
+
} finally {
|
|
192
|
+
await releaseLock({ debug, configDir });
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
async function readConfig(configPath) {
|
|
196
|
+
try {
|
|
197
|
+
const contents = await readFile2(configPath, "utf-8");
|
|
198
|
+
return JSON.parse(contents);
|
|
199
|
+
} catch {
|
|
200
|
+
return null;
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
async function writeConfig(configPath, config) {
|
|
204
|
+
await mkdir2(dirname(configPath), { recursive: true });
|
|
205
|
+
const contents = JSON.stringify(config, null, 2);
|
|
206
|
+
await writeFile2(configPath, `${contents}
|
|
207
|
+
`, "utf-8");
|
|
208
|
+
}
|
|
209
|
+
function getPluginList(config) {
|
|
210
|
+
if (!config) {
|
|
211
|
+
return { plugins: null, key: null };
|
|
212
|
+
}
|
|
213
|
+
if (Array.isArray(config.plugin)) {
|
|
214
|
+
return { plugins: config.plugin, key: "plugin" };
|
|
215
|
+
}
|
|
216
|
+
if (Array.isArray(config.plugins)) {
|
|
217
|
+
return { plugins: config.plugins, key: "plugins" };
|
|
218
|
+
}
|
|
219
|
+
return { plugins: null, key: null };
|
|
220
|
+
}
|
|
221
|
+
async function updatePlugins(options) {
|
|
222
|
+
const { plugins, configDir, preservePinned, useBun, log, error } = options;
|
|
223
|
+
const updated = [];
|
|
224
|
+
let changed = false;
|
|
225
|
+
for (const entry of plugins) {
|
|
226
|
+
if (isNonRegistryPlugin(entry)) {
|
|
227
|
+
log("[auto-update] Skipping non-registry plugin:", entry);
|
|
228
|
+
updated.push(entry);
|
|
229
|
+
continue;
|
|
230
|
+
}
|
|
231
|
+
const { name, version } = parsePackageSpec(entry);
|
|
232
|
+
if (preservePinned && version) {
|
|
233
|
+
log("[auto-update] Preserving pinned plugin:", entry);
|
|
234
|
+
updated.push(entry);
|
|
235
|
+
continue;
|
|
236
|
+
}
|
|
237
|
+
log("[auto-update] Updating plugin:", name);
|
|
238
|
+
const installedVersion = await installLatest({
|
|
239
|
+
name,
|
|
240
|
+
configDir,
|
|
241
|
+
useBun,
|
|
242
|
+
log,
|
|
243
|
+
error
|
|
244
|
+
});
|
|
245
|
+
if (!installedVersion) {
|
|
246
|
+
updated.push(entry);
|
|
247
|
+
continue;
|
|
248
|
+
}
|
|
249
|
+
const nextEntry = `${name}@${installedVersion}`;
|
|
250
|
+
log("[auto-update] Installed:", nextEntry);
|
|
251
|
+
updated.push(nextEntry);
|
|
252
|
+
if (nextEntry !== entry) {
|
|
253
|
+
changed = true;
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
return { plugins: updated, changed };
|
|
257
|
+
}
|
|
258
|
+
async function installLatest(options) {
|
|
259
|
+
const { name, configDir, useBun, log, error } = options;
|
|
260
|
+
await mkdir2(configDir, { recursive: true });
|
|
261
|
+
if (useBun) {
|
|
262
|
+
const result = await runCommand("bun", ["add", `${name}@latest`, "--cwd", configDir]);
|
|
263
|
+
if (result.code !== 0) {
|
|
264
|
+
error("[auto-update] bun add failed:", result.stderr || result.stdout);
|
|
265
|
+
return null;
|
|
266
|
+
}
|
|
267
|
+
} else {
|
|
268
|
+
const result = await runCommand("npm", [
|
|
269
|
+
"install",
|
|
270
|
+
`${name}@latest`,
|
|
271
|
+
"--prefix",
|
|
272
|
+
configDir,
|
|
273
|
+
"--no-save"
|
|
274
|
+
]);
|
|
275
|
+
if (result.code !== 0) {
|
|
276
|
+
error("[auto-update] npm install failed:", result.stderr || result.stdout);
|
|
277
|
+
return null;
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
const version = await readInstalledVersion(name, configDir);
|
|
281
|
+
if (!version) {
|
|
282
|
+
log("[auto-update] Unable to read installed version for", name);
|
|
283
|
+
}
|
|
284
|
+
return version;
|
|
285
|
+
}
|
|
286
|
+
async function readInstalledVersion(name, configDir) {
|
|
287
|
+
try {
|
|
288
|
+
const packagePath = name.startsWith("@") ? join2(configDir, "node_modules", ...name.split("/"), "package.json") : join2(configDir, "node_modules", name, "package.json");
|
|
289
|
+
const data = await readFile2(packagePath, "utf-8");
|
|
290
|
+
const parsed = JSON.parse(data);
|
|
291
|
+
return parsed.version ?? null;
|
|
292
|
+
} catch {
|
|
293
|
+
return null;
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
function isNonRegistryPlugin(spec) {
|
|
297
|
+
const trimmed = spec.trim();
|
|
298
|
+
if (!trimmed) {
|
|
299
|
+
return true;
|
|
300
|
+
}
|
|
301
|
+
const lower = trimmed.toLowerCase();
|
|
302
|
+
if (lower.startsWith("file:") || lower.startsWith("git+") || lower.startsWith("git:") || lower.startsWith("ssh://") || lower.startsWith("http://") || lower.startsWith("https://") || lower.startsWith("github:") || lower.startsWith("workspace:")) {
|
|
303
|
+
return true;
|
|
304
|
+
}
|
|
305
|
+
return trimmed.startsWith("./") || trimmed.startsWith("../") || trimmed.startsWith("/") || trimmed.startsWith("~");
|
|
306
|
+
}
|
|
307
|
+
function parsePackageSpec(spec) {
|
|
308
|
+
if (spec.startsWith("@")) {
|
|
309
|
+
const secondAt = spec.indexOf("@", 1);
|
|
310
|
+
if (secondAt === -1) {
|
|
311
|
+
return { name: spec };
|
|
312
|
+
}
|
|
313
|
+
return {
|
|
314
|
+
name: spec.slice(0, secondAt),
|
|
315
|
+
version: spec.slice(secondAt + 1) || void 0
|
|
316
|
+
};
|
|
317
|
+
}
|
|
318
|
+
const at = spec.lastIndexOf("@");
|
|
319
|
+
if (at <= 0) {
|
|
320
|
+
return { name: spec };
|
|
321
|
+
}
|
|
322
|
+
return {
|
|
323
|
+
name: spec.slice(0, at),
|
|
324
|
+
version: spec.slice(at + 1) || void 0
|
|
325
|
+
};
|
|
326
|
+
}
|
|
327
|
+
async function commandExists(command) {
|
|
328
|
+
const result = await runCommand(command, ["--version"]);
|
|
329
|
+
return result.code === 0;
|
|
330
|
+
}
|
|
331
|
+
async function runCommand(command, args, options = {}) {
|
|
332
|
+
return new Promise((resolve) => {
|
|
333
|
+
const child = spawn(command, args, {
|
|
334
|
+
cwd: options.cwd,
|
|
335
|
+
stdio: ["ignore", "pipe", "pipe"]
|
|
336
|
+
});
|
|
337
|
+
let stdout = "";
|
|
338
|
+
let stderr = "";
|
|
339
|
+
child.stdout?.on("data", (chunk) => {
|
|
340
|
+
stdout += chunk.toString();
|
|
341
|
+
});
|
|
342
|
+
child.stderr?.on("data", (chunk) => {
|
|
343
|
+
stderr += chunk.toString();
|
|
344
|
+
});
|
|
345
|
+
child.on("error", (err) => {
|
|
346
|
+
resolve({ code: 1, stdout, stderr: `${stderr}${err.message}` });
|
|
347
|
+
});
|
|
348
|
+
child.on("close", (code) => {
|
|
349
|
+
resolve({ code: code ?? 0, stdout, stderr });
|
|
350
|
+
});
|
|
351
|
+
});
|
|
352
|
+
}
|
|
353
|
+
function envFlag(name) {
|
|
354
|
+
return process.env[name]?.toLowerCase() === "true";
|
|
355
|
+
}
|
|
356
|
+
function envNumber(name, fallback) {
|
|
357
|
+
const raw = process.env[name];
|
|
358
|
+
if (!raw) {
|
|
359
|
+
return fallback;
|
|
360
|
+
}
|
|
361
|
+
const parsed = Number(raw);
|
|
362
|
+
return Number.isFinite(parsed) ? parsed : fallback;
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
// src/index.ts
|
|
366
|
+
async function src_default(ctx) {
|
|
367
|
+
const args = process.argv ?? [];
|
|
368
|
+
const debugLevelFlag = args.includes("--log-level") && args.includes("DEBUG");
|
|
369
|
+
const envDebug = process.env.OPENCODE_AUTO_UPDATE_DEBUG?.toLowerCase() === "true";
|
|
370
|
+
const envBypassThrottle = process.env.OPENCODE_AUTO_UPDATE_BYPASS_THROTTLE?.toLowerCase() === "true";
|
|
371
|
+
const debug = debugLevelFlag || envDebug;
|
|
372
|
+
const ignoreThrottle = debugLevelFlag || envBypassThrottle;
|
|
373
|
+
setTimeout(() => {
|
|
374
|
+
runAutoUpdate({ debug, ignoreThrottle }).catch((error) => {
|
|
375
|
+
if (debug) {
|
|
376
|
+
console.error("[opencode-plugin-auto-update] Update check failed:", error);
|
|
377
|
+
}
|
|
378
|
+
});
|
|
379
|
+
}, 0);
|
|
380
|
+
return {
|
|
381
|
+
name: "opencode-plugin-auto-update"
|
|
382
|
+
};
|
|
383
|
+
}
|
|
384
|
+
export {
|
|
385
|
+
src_default as default
|
|
386
|
+
};
|
package/package.json
ADDED
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "opencode-plugin-auto-update",
|
|
3
|
+
"version": "0.1.1",
|
|
4
|
+
"description": "OpenCode plugin that auto-updates plugins in the background",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "dist/index.js",
|
|
7
|
+
"types": "dist/index.d.ts",
|
|
8
|
+
"files": [
|
|
9
|
+
"dist"
|
|
10
|
+
],
|
|
11
|
+
"exports": {
|
|
12
|
+
".": "./dist/index.js"
|
|
13
|
+
},
|
|
14
|
+
"scripts": {
|
|
15
|
+
"build": "tsup",
|
|
16
|
+
"dev": "tsup --watch",
|
|
17
|
+
"typecheck": "tsc --noEmit",
|
|
18
|
+
"prepublishOnly": "bun run build"
|
|
19
|
+
},
|
|
20
|
+
"keywords": [
|
|
21
|
+
"opencode",
|
|
22
|
+
"opencode-plugin",
|
|
23
|
+
"auto-update",
|
|
24
|
+
"dependencies"
|
|
25
|
+
],
|
|
26
|
+
"repository": {
|
|
27
|
+
"type": "git",
|
|
28
|
+
"url": "https://github.com/AnganSamadder/opencode-plugin-auto-update.git"
|
|
29
|
+
},
|
|
30
|
+
"bugs": {
|
|
31
|
+
"url": "https://github.com/AnganSamadder/opencode-plugin-auto-update/issues"
|
|
32
|
+
},
|
|
33
|
+
"homepage": "https://github.com/AnganSamadder/opencode-plugin-auto-update#readme",
|
|
34
|
+
"author": "Angan Samadder",
|
|
35
|
+
"license": "MIT",
|
|
36
|
+
"devDependencies": {
|
|
37
|
+
"@types/bun": "^1.2.4",
|
|
38
|
+
"@types/node": "^25.0.10",
|
|
39
|
+
"tsup": "^8.3.6",
|
|
40
|
+
"typescript": "^5.7.3"
|
|
41
|
+
}
|
|
42
|
+
}
|