@opencode-cockpit/client 0.1.1 → 0.1.3
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/package.json +3 -3
- package/src/client.ts +35 -1
- package/src/feature.ts +55 -0
- package/src/index.ts +8 -1
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@opencode-cockpit/client",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.3",
|
|
4
4
|
"description": "Auto-spawning, reconnecting, typed client for cockpitd",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -31,10 +31,10 @@
|
|
|
31
31
|
"access": "public"
|
|
32
32
|
},
|
|
33
33
|
"dependencies": {
|
|
34
|
-
"@opencode-cockpit/protocol": "0.1.
|
|
34
|
+
"@opencode-cockpit/protocol": "0.1.3"
|
|
35
35
|
},
|
|
36
36
|
"devDependencies": {
|
|
37
|
-
"@opencode-cockpit/daemon": "0.1.
|
|
37
|
+
"@opencode-cockpit/daemon": "0.1.3"
|
|
38
38
|
},
|
|
39
39
|
"engines": {
|
|
40
40
|
"bun": ">=1.3.5"
|
package/src/client.ts
CHANGED
|
@@ -39,6 +39,38 @@ export interface OutdatedDaemon {
|
|
|
39
39
|
expected: string
|
|
40
40
|
}
|
|
41
41
|
|
|
42
|
+
/**
|
|
43
|
+
* Orders build ids (`<semver>+<hash>`, see daemonBuildId). Higher semver wins; equal versions with
|
|
44
|
+
* different hashes are local development builds, where the client's code counts as newer. A
|
|
45
|
+
* daemon without a build id predates build ids and is always older.
|
|
46
|
+
*/
|
|
47
|
+
export function compareBuilds(client: string, daemon: string | undefined): number {
|
|
48
|
+
if (!daemon) return 1
|
|
49
|
+
if (client === daemon) return 0
|
|
50
|
+
const [cv = "", ch = ""] = client.split("+")
|
|
51
|
+
const [dv = "", dh = ""] = daemon.split("+")
|
|
52
|
+
const order = compareSemver(cv, dv)
|
|
53
|
+
if (order !== 0) return order
|
|
54
|
+
return ch === dh ? 0 : 1
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function compareSemver(a: string, b: string): number {
|
|
58
|
+
const parse = (v: string) => {
|
|
59
|
+
const [core = "", pre] = v.split("-", 2)
|
|
60
|
+
return { nums: core.split(".").map((n) => Number.parseInt(n, 10) || 0), pre }
|
|
61
|
+
}
|
|
62
|
+
const x = parse(a)
|
|
63
|
+
const y = parse(b)
|
|
64
|
+
for (let i = 0; i < 3; i++) {
|
|
65
|
+
const d = (x.nums[i] ?? 0) - (y.nums[i] ?? 0)
|
|
66
|
+
if (d !== 0) return Math.sign(d)
|
|
67
|
+
}
|
|
68
|
+
if (x.pre === y.pre) return 0
|
|
69
|
+
if (x.pre === undefined) return 1 // 1.0.0 > 1.0.0-beta
|
|
70
|
+
if (y.pre === undefined) return -1
|
|
71
|
+
return x.pre < y.pre ? -1 : 1
|
|
72
|
+
}
|
|
73
|
+
|
|
42
74
|
const IDEMPOTENT = new Set<string>([
|
|
43
75
|
"daemon.hello",
|
|
44
76
|
"daemon.status",
|
|
@@ -199,7 +231,9 @@ export class CockpitClient {
|
|
|
199
231
|
}
|
|
200
232
|
|
|
201
233
|
const expected = this.options.expectedBuild
|
|
202
|
-
|
|
234
|
+
// Only move forward: several plugins at different versions share one daemon, and letting an
|
|
235
|
+
// older one replace a newer daemon would make them take turns replacing each other.
|
|
236
|
+
if (expected && compareBuilds(expected, this.hello.build) > 0) {
|
|
203
237
|
const status = (await conn.request("daemon.status", {})) as { modules: { busy: boolean }[] }
|
|
204
238
|
const busy = status.modules.some((m) => m.busy)
|
|
205
239
|
if (!busy && this.options.spawn && !replaced) {
|
package/src/feature.ts
ADDED
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Guards against loading the same cockpit feature twice, e.g. when both `opencode-cockpit` and
|
|
3
|
+
* `@opencode-cockpit/shell` are configured. OpenCode does not deduplicate plugin tools, and
|
|
4
|
+
* duplicate tool names make model requests fail, so the first copy loaded wins and later copies
|
|
5
|
+
* stay inactive.
|
|
6
|
+
*
|
|
7
|
+
* Claims are scoped to an object that all plugins of one OpenCode instance share: the plugin
|
|
8
|
+
* input on the server side, the renderer in the TUI. Separate instances claim independently.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
const REGISTRY = Symbol.for("opencode-cockpit.features")
|
|
12
|
+
|
|
13
|
+
type Registry = WeakMap<object, Map<string, string>>
|
|
14
|
+
|
|
15
|
+
function registry(): Registry {
|
|
16
|
+
const host = globalThis as { [REGISTRY]?: Registry }
|
|
17
|
+
host[REGISTRY] ??= new WeakMap()
|
|
18
|
+
return host[REGISTRY]
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export interface FeatureClaim {
|
|
22
|
+
/** True when this copy owns the feature and should register itself. */
|
|
23
|
+
active: boolean
|
|
24
|
+
/** Source label of the copy that owns the feature. */
|
|
25
|
+
owner: string
|
|
26
|
+
/** Give up ownership, so a reloaded plugin can claim again. No-op for inactive claims. */
|
|
27
|
+
release(): void
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export function claimFeature(scope: object, feature: string, source: string): FeatureClaim {
|
|
31
|
+
const reg = registry()
|
|
32
|
+
let claims = reg.get(scope)
|
|
33
|
+
if (!claims) {
|
|
34
|
+
claims = new Map()
|
|
35
|
+
reg.set(scope, claims)
|
|
36
|
+
}
|
|
37
|
+
const owner = claims.get(feature)
|
|
38
|
+
if (owner !== undefined) return { active: false, owner, release() {} }
|
|
39
|
+
|
|
40
|
+
claims.set(feature, source)
|
|
41
|
+
let released = false
|
|
42
|
+
return {
|
|
43
|
+
active: true,
|
|
44
|
+
owner: source,
|
|
45
|
+
release() {
|
|
46
|
+
if (released) return
|
|
47
|
+
released = true
|
|
48
|
+
if (claims.get(feature) === source) claims.delete(feature)
|
|
49
|
+
},
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export function duplicateFeatureMessage(feature: string, owner: string, skipped: string): string {
|
|
54
|
+
return `${feature} is configured twice (${owner} and ${skipped}). Using ${owner}; remove one of them from your OpenCode config.`
|
|
55
|
+
}
|
package/src/index.ts
CHANGED
|
@@ -1,2 +1,9 @@
|
|
|
1
|
-
export {
|
|
1
|
+
export {
|
|
2
|
+
type ClientOptions,
|
|
3
|
+
CockpitClient,
|
|
4
|
+
type ConnectionState,
|
|
5
|
+
compareBuilds,
|
|
6
|
+
type OutdatedDaemon,
|
|
7
|
+
} from "./client.ts"
|
|
8
|
+
export { claimFeature, duplicateFeatureMessage, type FeatureClaim } from "./feature.ts"
|
|
2
9
|
export type { SpawnOptions } from "./spawn.ts"
|