@mirrowel/opencode-openshare 0.1.0 → 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 +36 -30
- package/ci/openshare.config.json +7 -2
- package/ci/openshare.js +42 -37
- package/dist/config.d.ts +5 -1
- package/dist/config.js +9 -2
- package/dist/share-flow.js +2 -2
- package/dist/tui.js +42 -85
- package/openshare.config.json +1 -1
- package/package.json +2 -2
- package/src/banner.txt +7 -4
- package/src/config.ts +8 -2
- package/src/share-flow.ts +2 -2
- package/src/tui.tsx +1 -1
package/README.md
CHANGED
|
@@ -9,7 +9,7 @@ OpenCode v1 and v2, self-hosted on your own worker.
|
|
|
9
9
|
|---|---|---|
|
|
10
10
|
| `dist/server.js` | package server entry (opencode.json `plugin` array) | all, unless the TUI is registered |
|
|
11
11
|
| `dist/tui.js` | package TUI entry (tui.json `plugin` array) | its presence flips the server to manual |
|
|
12
|
-
| `openshare.js` | single-file, zero-dependency bundle of the same engine (
|
|
12
|
+
| `ci/openshare.js` | single-file, zero-dependency bundle of the same engine (committed; rebuilt by `npm run build`) | always all — the CI bundle |
|
|
13
13
|
|
|
14
14
|
All three are behavior-identical where they overlap — `npm run
|
|
15
15
|
test:regression` proves the single file and the package match the frozen
|
|
@@ -17,36 +17,29 @@ original (`reference/openshare.legacy.js`) on the full share lifecycle.
|
|
|
17
17
|
|
|
18
18
|
## Install
|
|
19
19
|
|
|
20
|
-
|
|
20
|
+
From the registry (published). **v1 hosts** — the bare package name in
|
|
21
|
+
BOTH config files (no `/tui` suffix; opencode resolves the entrypoints
|
|
22
|
+
itself):
|
|
21
23
|
|
|
22
24
|
```jsonc
|
|
23
25
|
// opencode.json
|
|
24
26
|
{ "plugin": ["@mirrowel/opencode-openshare"] }
|
|
25
27
|
// tui.json (global ~/.config/opencode/tui.json or project)
|
|
26
|
-
{ "plugin": ["@mirrowel/opencode-openshare
|
|
28
|
+
{ "plugin": ["@mirrowel/opencode-openshare"] }
|
|
27
29
|
```
|
|
28
30
|
|
|
29
|
-
|
|
31
|
+
**v2 hosts** — the CLI writes the config for you:
|
|
30
32
|
|
|
31
|
-
```
|
|
32
|
-
|
|
33
|
-
{
|
|
34
|
-
"plugin": ["/path/to/openshare.js"]
|
|
35
|
-
// + openshare.config.json next to the bundle (servers group or the
|
|
36
|
-
// legacy env pair), OPENSHARE_URL_FILE collects each share URL
|
|
37
|
-
// (also printed to stderr)
|
|
38
|
-
}
|
|
33
|
+
```sh
|
|
34
|
+
opencode plugin add @mirrowel/opencode-openshare
|
|
39
35
|
```
|
|
40
36
|
|
|
41
|
-
|
|
42
|
-
[main README](../README.md#configuration) for all keys):
|
|
37
|
+
Restart opencode; `/share` and `/openshare` appear in the TUI.
|
|
43
38
|
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
Or the multi-server form (canonical; `workerUrl`/`createToken` above is
|
|
49
|
-
the single-server shorthand):
|
|
39
|
+
Then configure. For an npm install the durable config file is the
|
|
40
|
+
**settings home** (`~/.config/opencode/openshare.config.json` — also
|
|
41
|
+
what `/openshare` writes); "next to the package" only applies to file
|
|
42
|
+
installs (the CI bundle):
|
|
50
43
|
|
|
51
44
|
```jsonc
|
|
52
45
|
{
|
|
@@ -57,6 +50,15 @@ the single-server shorthand):
|
|
|
57
50
|
}
|
|
58
51
|
```
|
|
59
52
|
|
|
53
|
+
`workerUrl`/`createToken` is the legacy single-server shorthand for the
|
|
54
|
+
same thing — the `servers` form above is canonical.
|
|
55
|
+
|
|
56
|
+
From a local checkout (developing / tracking main): use the directory
|
|
57
|
+
spec `"file:///…/openshare/share-plugin"` in both config files (run
|
|
58
|
+
`npm run build` first — `dist/` is not committed). The CI bundle needs
|
|
59
|
+
no build at all: [`ci/openshare.js`](ci/README.md), committed ready to
|
|
60
|
+
use.
|
|
61
|
+
|
|
60
62
|
**With no token the plugin enrolls itself** on first contact: public
|
|
61
63
|
servers (hoster set `AUTH_MODE=public`) take anyone; private ones need
|
|
62
64
|
a `code` from the hoster (activation-budgeted invite). The enrolled
|
|
@@ -116,16 +118,15 @@ stays all-mode.
|
|
|
116
118
|
|
|
117
119
|
```bash
|
|
118
120
|
npm install
|
|
119
|
-
npm run build # tsc -> dist/ + bun bundle -> openshare.js
|
|
121
|
+
npm run build # tsc -> dist/ + bun bundle -> ci/openshare.js (Bun on PATH)
|
|
120
122
|
npm run typecheck
|
|
121
123
|
npm run test # full matrix via the parallel driver (~4.5 min)
|
|
122
124
|
npm run test:quick # everyday gate (~90s): parity + manual core + TUI + v2 + auth
|
|
123
125
|
```
|
|
124
126
|
|
|
125
127
|
Suites (shared machinery in `scripts/suite-lib.mjs`; every run is
|
|
126
|
-
watchdogged, port-swept, and drives
|
|
127
|
-
|
|
128
|
-
provider):
|
|
128
|
+
watchdogged, port-swept, and drives real `opencode serve` instances with
|
|
129
|
+
a mock OpenAI-compatible provider — except where noted):
|
|
129
130
|
|
|
130
131
|
- `test:regression` — parity of legacy / single-file / package across the
|
|
131
132
|
full lifecycle (create → prompt → completed → delete → 404).
|
|
@@ -134,18 +135,21 @@ provider):
|
|
|
134
135
|
gates, undo tombstones, completed debounce, turn-mode drops.
|
|
135
136
|
- `test:tui-flow` — share/unshare flows, config round-trip, connect
|
|
136
137
|
flow (enroll + persist + rotate), admin console round-trip.
|
|
137
|
-
- `test:v2` — the v2 adapter against a scripted mock v2 host (event
|
|
138
|
+
- `test:v2` — the v2 adapter against a **scripted mock v2 host** (event
|
|
138
139
|
classification, message conversion, live re-sync, delete).
|
|
139
|
-
- `test:auth` — server identity contract
|
|
140
|
-
|
|
140
|
+
- `test:auth` — the **server identity contract** exercised over HTTP/WS
|
|
141
|
+
(no opencode instance): enrollment, code burn, rotate, freeze (secret
|
|
142
|
+
dead too), limits, reassign, public mode (48 checks).
|
|
141
143
|
- `test:enroll` — the plugin's enrollment lifecycle end to end (code
|
|
142
144
|
enroll, token persistence across processes, revoke → re-enroll,
|
|
143
145
|
blocked path, public zero-config).
|
|
144
146
|
- `test:fanout` — multi-server: v2 bindings, content parity across
|
|
145
147
|
servers, mid-session join with full-history backfill, per-session
|
|
146
148
|
opt-outs.
|
|
147
|
-
- `test:cli` — real `opencode run` end
|
|
148
|
-
silenced outputs
|
|
149
|
+
- `test:cli` — real `opencode run` end to end (single file + package +
|
|
150
|
+
silenced outputs; mock provider, sandboxed XDG homes) — on demand.
|
|
151
|
+
- `node scripts/npm-e2e.mjs` — the **published** npm package installed
|
|
152
|
+
by registry spec, end to end — on demand.
|
|
149
153
|
- `test:server-auth` / `test:server-enroll` — the SAME auth/enroll
|
|
150
154
|
suites against the portable Bun backend (`../share-server`): the
|
|
151
155
|
two-backends-one-protocol parity guarantee.
|
|
@@ -162,7 +166,9 @@ src/
|
|
|
162
166
|
commands.ts /share (toggle + fan-out manager + collision dialog) /openshare
|
|
163
167
|
share-flow.ts host-agnostic flows: flag, bindings v1/v2, enrollment,
|
|
164
168
|
rotate/disconnect, admin client, unshare multi-delete
|
|
165
|
-
settings.ts settings
|
|
169
|
+
settings.ts grouped settings tree + servers section + admin console
|
|
170
|
+
dialogs.tsx MenuDialog/InfoDialog/PromptDialog + the sizing core (75% budget)
|
|
171
|
+
ui-prefs.ts menu size prefs (host kv store)
|
|
166
172
|
sidebar.tsx sidebar_title slot chip (v1, box-rooted)
|
|
167
173
|
tui-host.ts host facade interface
|
|
168
174
|
v2-server.ts v2 setup: event mapping + client adapter + v2->v1 conversion
|
package/ci/openshare.config.json
CHANGED
|
@@ -1,5 +1,10 @@
|
|
|
1
1
|
{
|
|
2
|
-
"$comment": "Open(code)Share CI bundle config. This file sits NEXT TO openshare.js and is read at load; OPENSHARE_* env vars always win (the CI way). The single-file bundle ALWAYS shares all sessions.",
|
|
2
|
+
"$comment": "Open(code)Share CI bundle config. This file sits NEXT TO openshare.js and is read at load; OPENSHARE_* env vars always win (the CI way). The single-file bundle ALWAYS shares all sessions. Prefer the servers form below (workerUrl/createToken is the legacy single-server shorthand). Empty string means unset. urlFile/announceStderr control the two URL outputs; 'none' disables the file.",
|
|
3
|
+
"servers": {
|
|
4
|
+
"https://your-worker.workers.dev": { "token": "" }
|
|
5
|
+
},
|
|
3
6
|
"workerUrl": "",
|
|
4
|
-
"createToken": ""
|
|
7
|
+
"createToken": "",
|
|
8
|
+
"urlFile": "",
|
|
9
|
+
"announceStderr": true
|
|
5
10
|
}
|
package/ci/openshare.js
CHANGED
|
@@ -13,12 +13,15 @@
|
|
|
13
13
|
// lands in the log and optionally in `urlFile`).
|
|
14
14
|
// - Interactive opt-in: install the package's TUI part (tui.json) too —
|
|
15
15
|
// the server detects its presence and switches to manual mode
|
|
16
|
-
// (`/share
|
|
16
|
+
// (`/share` + `/openshare` in the TUI).
|
|
17
17
|
//
|
|
18
18
|
// Config: env (OPENSHARE_*) > openshare.config.json (next to this file)
|
|
19
|
-
// > code defaults. Keys:
|
|
20
|
-
//
|
|
21
|
-
// shareMode ("all" | "manual"; default = TUI presence)
|
|
19
|
+
// > code defaults. Keys: servers (multi-server: url -> {token, code,
|
|
20
|
+
// label, active, adminToken}), workerUrl, createToken (legacy pair),
|
|
21
|
+
// shareMode ("all" | "manual"; default = TUI presence), debounceMs,
|
|
22
|
+
// completedDelayMs, modelLabel, ttlDays, openTasksInNewTab,
|
|
23
|
+
// authLogin, authPassword, spoolDir, urlFile ("none" = off),
|
|
24
|
+
// announceStderr.
|
|
22
25
|
//
|
|
23
26
|
// src/config.ts
|
|
24
27
|
import { existsSync, readFileSync, writeFileSync } from "node:fs";
|
|
@@ -117,8 +120,8 @@ function resolveServer(cfg) {
|
|
|
117
120
|
if (cfg.workerUrl)
|
|
118
121
|
servers[cfg.workerUrl] = { token: cfg.createToken || "", adminToken: cfg.createToken || "", active: true };
|
|
119
122
|
if (cfg.servers)
|
|
120
|
-
for (const [
|
|
121
|
-
servers[
|
|
123
|
+
for (const [url, entry] of Object.entries(cfg.servers))
|
|
124
|
+
servers[url] = entry;
|
|
122
125
|
const urls = Object.keys(servers);
|
|
123
126
|
if (!urls.length)
|
|
124
127
|
return;
|
|
@@ -245,7 +248,9 @@ function specPath(spec) {
|
|
|
245
248
|
function isOpenShareSpec(spec) {
|
|
246
249
|
if (typeof spec !== "string" || spec.length === 0)
|
|
247
250
|
return false;
|
|
248
|
-
if (spec === NPM_NAME
|
|
251
|
+
if (spec === NPM_NAME)
|
|
252
|
+
return true;
|
|
253
|
+
if (spec.startsWith(`${NPM_NAME}@`) || spec.startsWith(`${NPM_NAME}/`))
|
|
249
254
|
return true;
|
|
250
255
|
const path = specPath(spec);
|
|
251
256
|
if (path) {
|
|
@@ -568,19 +573,19 @@ function createEngine(deps) {
|
|
|
568
573
|
if (!raw || typeof raw !== "object")
|
|
569
574
|
return;
|
|
570
575
|
const p = raw;
|
|
571
|
-
const
|
|
576
|
+
const num = (v, d) => {
|
|
572
577
|
const n = Number(v);
|
|
573
578
|
return Number.isFinite(n) && n >= 0 ? n : d;
|
|
574
579
|
};
|
|
575
580
|
const prev = policyOf(url);
|
|
576
581
|
policyMap.set(url, {
|
|
577
582
|
mode: p.mode === "turn" ? "turn" : "persistent",
|
|
578
|
-
turnGraceMs:
|
|
579
|
-
idleDropMs:
|
|
580
|
-
finalizeAfterMs:
|
|
581
|
-
debounceMinMs:
|
|
582
|
-
debounceMaxMs: Math.max(
|
|
583
|
-
completedDelayMaxMs:
|
|
583
|
+
turnGraceMs: num(p.turnGraceMs, prev.turnGraceMs),
|
|
584
|
+
idleDropMs: num(p.idleDropMs, prev.idleDropMs),
|
|
585
|
+
finalizeAfterMs: num(p.finalizeAfterMs, prev.finalizeAfterMs),
|
|
586
|
+
debounceMinMs: num(p.debounceMinMs, 0),
|
|
587
|
+
debounceMaxMs: Math.max(num(p.debounceMaxMs, Infinity), num(p.debounceMinMs, 0)),
|
|
588
|
+
completedDelayMaxMs: num(p.completedDelayMaxMs, Infinity)
|
|
584
589
|
});
|
|
585
590
|
clampToPolicy();
|
|
586
591
|
if (debugEvents)
|
|
@@ -1605,31 +1610,31 @@ function createEngine(deps) {
|
|
|
1605
1610
|
const parent = trackChild(sid, info);
|
|
1606
1611
|
if (mode === "manual") {
|
|
1607
1612
|
if (parent) {
|
|
1608
|
-
const
|
|
1609
|
-
if (
|
|
1610
|
-
if (!
|
|
1611
|
-
|
|
1612
|
-
enqueue(
|
|
1613
|
+
const st = sessions.get(parent);
|
|
1614
|
+
if (st?.activated && !st.closed) {
|
|
1615
|
+
if (!st.prefsSent) {
|
|
1616
|
+
st.prefsSent = true;
|
|
1617
|
+
enqueue(st, itemFor("prefs", { openTasksInNewTab: cfg.openTasksInNewTab !== false }, sid));
|
|
1613
1618
|
}
|
|
1614
1619
|
if (info)
|
|
1615
|
-
enqueue(
|
|
1620
|
+
enqueue(st, itemFor("session", redact(info), sid));
|
|
1616
1621
|
}
|
|
1617
1622
|
return;
|
|
1618
1623
|
}
|
|
1619
1624
|
const flag = shareFlag(info);
|
|
1620
|
-
const
|
|
1625
|
+
const st = sessions.get(sid);
|
|
1621
1626
|
if (flag.enabled) {
|
|
1622
|
-
if (
|
|
1627
|
+
if (st?.activated && !st.closed) {
|
|
1623
1628
|
if (flag.opts)
|
|
1624
|
-
|
|
1625
|
-
if (!
|
|
1626
|
-
|
|
1627
|
-
enqueue(
|
|
1629
|
+
st.opts = flag.opts;
|
|
1630
|
+
if (!st.prefsSent) {
|
|
1631
|
+
st.prefsSent = true;
|
|
1632
|
+
enqueue(st, itemFor("prefs", { openTasksInNewTab: cfg.openTasksInNewTab !== false }, sid));
|
|
1628
1633
|
}
|
|
1629
1634
|
if (info)
|
|
1630
|
-
enqueue(
|
|
1635
|
+
enqueue(st, itemFor("session", redact(info), sid));
|
|
1631
1636
|
if (t === "session.updated")
|
|
1632
|
-
chain = chain.then(() => refreshOptOuts(
|
|
1637
|
+
chain = chain.then(() => refreshOptOuts(st)).catch(() => {});
|
|
1633
1638
|
return;
|
|
1634
1639
|
}
|
|
1635
1640
|
activate(sid, flag.opts, info);
|
|
@@ -1640,9 +1645,9 @@ function createEngine(deps) {
|
|
|
1640
1645
|
}
|
|
1641
1646
|
return;
|
|
1642
1647
|
}
|
|
1643
|
-
if (
|
|
1648
|
+
if (st?.activated && !st.closed && t === "session.updated") {
|
|
1644
1649
|
log(`share disabled for ${sid} - removing`);
|
|
1645
|
-
chain = chain.then(() => remove(
|
|
1650
|
+
chain = chain.then(() => remove(st)).catch((e) => logError("remove error", e?.message || e));
|
|
1646
1651
|
return;
|
|
1647
1652
|
}
|
|
1648
1653
|
return;
|
|
@@ -1788,16 +1793,16 @@ function createEngine(deps) {
|
|
|
1788
1793
|
chain = chain.then(async () => {
|
|
1789
1794
|
if (created.closed)
|
|
1790
1795
|
return;
|
|
1791
|
-
const
|
|
1796
|
+
const info = await sessionInfo(created.client, main);
|
|
1792
1797
|
verifying.delete(main);
|
|
1793
1798
|
if (created.closed)
|
|
1794
1799
|
return;
|
|
1795
|
-
const flag = shareFlag(
|
|
1796
|
-
const hasBinding = !!
|
|
1797
|
-
const createdMs =
|
|
1800
|
+
const flag = shareFlag(info);
|
|
1801
|
+
const hasBinding = !!info?.metadata?.[METADATA_KEY];
|
|
1802
|
+
const createdMs = info?.time?.created;
|
|
1798
1803
|
const isPreExisting = typeof createdMs === "number" && createdMs < bootTime;
|
|
1799
1804
|
if (debugEvents) {
|
|
1800
|
-
log(`verify ${main}: created=${createdMs} boot=${bootTime} pre=${isPreExisting} flag=${flag.enabled} binding=${hasBinding} infoMissing=${!
|
|
1805
|
+
log(`verify ${main}: created=${createdMs} boot=${bootTime} pre=${isPreExisting} flag=${flag.enabled} binding=${hasBinding} infoMissing=${!info}`);
|
|
1801
1806
|
}
|
|
1802
1807
|
if (isPreExisting && !flag.enabled && !hasBinding) {
|
|
1803
1808
|
preExistingRetired.add(main);
|
|
@@ -2259,7 +2264,7 @@ var server_default = {
|
|
|
2259
2264
|
setup: createV2ServerSetup()
|
|
2260
2265
|
};
|
|
2261
2266
|
export {
|
|
2262
|
-
|
|
2267
|
+
OpenSharePlugin,
|
|
2263
2268
|
PLUGIN_ID,
|
|
2264
|
-
|
|
2269
|
+
server_default as default
|
|
2265
2270
|
};
|
package/dist/config.d.ts
CHANGED
|
@@ -109,7 +109,11 @@ export declare function deleteServerEntry(url: string, target?: string): string
|
|
|
109
109
|
* never shadows the real config elsewhere. */
|
|
110
110
|
export declare function loadConfig(pluginDir: string, options?: Record<string, unknown>): ShareConfig;
|
|
111
111
|
export declare function spoolDirFor(cfg: ShareConfig): string;
|
|
112
|
-
/** Does this plugin spec (tui.json entry) point at the openshare package?
|
|
112
|
+
/** Does this plugin spec (tui.json entry) point at the openshare package?
|
|
113
|
+
* Accepts the bare npm name, name@version, and the /tui (or any) subpath
|
|
114
|
+
* form — older docs shipped the subpath spec and presence detection must
|
|
115
|
+
* never silently miss it (all-mode would keep auto-sharing while the user
|
|
116
|
+
* believes they installed manual mode — live-caught in review). */
|
|
113
117
|
export declare function isOpenShareSpec(spec: unknown): spec is string;
|
|
114
118
|
/** tui.json layers the TUI reads (mirrors the family selfwire path rules,
|
|
115
119
|
* read-only). The walk runs from the session directory up to the worktree
|
package/dist/config.js
CHANGED
|
@@ -402,11 +402,18 @@ function specPath(spec) {
|
|
|
402
402
|
return undefined;
|
|
403
403
|
}
|
|
404
404
|
}
|
|
405
|
-
/** Does this plugin spec (tui.json entry) point at the openshare package?
|
|
405
|
+
/** Does this plugin spec (tui.json entry) point at the openshare package?
|
|
406
|
+
* Accepts the bare npm name, name@version, and the /tui (or any) subpath
|
|
407
|
+
* form — older docs shipped the subpath spec and presence detection must
|
|
408
|
+
* never silently miss it (all-mode would keep auto-sharing while the user
|
|
409
|
+
* believes they installed manual mode — live-caught in review). */
|
|
406
410
|
export function isOpenShareSpec(spec) {
|
|
407
411
|
if (typeof spec !== "string" || spec.length === 0)
|
|
408
412
|
return false;
|
|
409
|
-
if (spec === NPM_NAME
|
|
413
|
+
if (spec === NPM_NAME)
|
|
414
|
+
return true;
|
|
415
|
+
// name@version, name/tui, name@version/tui
|
|
416
|
+
if (spec.startsWith(`${NPM_NAME}@`) || spec.startsWith(`${NPM_NAME}/`))
|
|
410
417
|
return true;
|
|
411
418
|
const path = specPath(spec);
|
|
412
419
|
if (path) {
|
package/dist/share-flow.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
// OpenShare share flows — host-agnostic logic behind /share
|
|
1
|
+
// OpenShare share flows — host-agnostic logic behind /share and
|
|
2
2
|
// the settings dialog's "this session" actions. Runs against the
|
|
3
3
|
// ShareHostApi facade (tui-host.ts) and the shared config module, so the
|
|
4
4
|
// same compiled code serves the v1 TUI and the v2 adapter.
|
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
// (with per-session opts when present) -> wait for the server
|
|
10
10
|
// plugin to publish the binding URL into session metadata
|
|
11
11
|
// (~seconds; worker-down surfaces as a timeout toast) -> copy.
|
|
12
|
-
//
|
|
12
|
+
// stop-sharing resolve root -> confirm -> read binding -> clear flag ->
|
|
13
13
|
// DELETE the share directly via the worker (belt and braces:
|
|
14
14
|
// works even if the server plugin never saw the flag event) ->
|
|
15
15
|
// the server-side flag-removal path cleans up on its own.
|
package/dist/tui.js
CHANGED
|
@@ -1,19 +1,15 @@
|
|
|
1
1
|
// @bun
|
|
2
|
-
var
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
}
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
configurable: true,
|
|
13
|
-
set: __exportSetter.bind(all, name)
|
|
14
|
-
});
|
|
2
|
+
var __esm = (fn, res, err) => () => {
|
|
3
|
+
if (fn)
|
|
4
|
+
try {
|
|
5
|
+
res = fn(fn = 0);
|
|
6
|
+
} catch (e) {
|
|
7
|
+
err = [e];
|
|
8
|
+
}
|
|
9
|
+
if (err)
|
|
10
|
+
throw err[0];
|
|
11
|
+
return res;
|
|
15
12
|
};
|
|
16
|
-
var __esm = (fn, res) => () => (fn && (res = fn(fn = 0)), res);
|
|
17
13
|
|
|
18
14
|
// src/config.ts
|
|
19
15
|
import { existsSync, readFileSync, writeFileSync } from "fs";
|
|
@@ -165,8 +161,8 @@ function resolveServer(cfg) {
|
|
|
165
161
|
if (cfg.workerUrl)
|
|
166
162
|
servers[cfg.workerUrl] = { token: cfg.createToken || "", adminToken: cfg.createToken || "", active: true };
|
|
167
163
|
if (cfg.servers)
|
|
168
|
-
for (const [
|
|
169
|
-
servers[
|
|
164
|
+
for (const [url, entry] of Object.entries(cfg.servers))
|
|
165
|
+
servers[url] = entry;
|
|
170
166
|
const urls = Object.keys(servers);
|
|
171
167
|
if (!urls.length)
|
|
172
168
|
return;
|
|
@@ -467,46 +463,7 @@ var init_sidebar = __esm(() => {
|
|
|
467
463
|
});
|
|
468
464
|
|
|
469
465
|
// src/share-flow.ts
|
|
470
|
-
var exports_share_flow = {};
|
|
471
|
-
__export(exports_share_flow, {
|
|
472
|
-
workerDeleteShare: () => workerDeleteShare,
|
|
473
|
-
waitForBinding: () => waitForBinding,
|
|
474
|
-
verifyShare: () => verifyShare,
|
|
475
|
-
setShareFlag: () => setShareFlag,
|
|
476
|
-
serverEntries: () => serverEntries,
|
|
477
|
-
saveServerEntry: () => saveServerEntry,
|
|
478
|
-
saveConfig: () => saveConfig,
|
|
479
|
-
runUnshareFlow: () => runUnshareFlow,
|
|
480
|
-
runShareFlow: () => runShareFlow,
|
|
481
|
-
rotateAtServer: () => rotateAtServer,
|
|
482
|
-
resolveRoot: () => resolveRoot,
|
|
483
|
-
readBindings: () => readBindings,
|
|
484
|
-
readBinding: () => readBinding,
|
|
485
|
-
probeWorker: () => probeWorker,
|
|
486
|
-
probeAuthMode: () => probeAuthMode,
|
|
487
|
-
pluginConfigPaths: () => pluginConfigPaths,
|
|
488
|
-
patchSessionBinding: () => patchSessionBinding,
|
|
489
|
-
normServerUrl: () => normServerUrl,
|
|
490
|
-
isConfigured: () => isConfigured,
|
|
491
|
-
hostOf: () => hostOf,
|
|
492
|
-
enrollAtServer: () => enrollAtServer,
|
|
493
|
-
disconnectAtServer: () => disconnectAtServer,
|
|
494
|
-
deleteServerEntry: () => deleteServerEntry,
|
|
495
|
-
currentConfig: () => currentConfig,
|
|
496
|
-
confirmShare: () => confirmShare,
|
|
497
|
-
clearShareFlag: () => clearShareFlag,
|
|
498
|
-
clearGoneBindingEntries: () => clearGoneBindingEntries,
|
|
499
|
-
bindingsOf: () => bindingsOf,
|
|
500
|
-
bindingOf: () => bindingOf,
|
|
501
|
-
adminTokenFor: () => adminTokenFor,
|
|
502
|
-
adminClient: () => adminClient,
|
|
503
|
-
activeServer: () => activeServer,
|
|
504
|
-
CONSENT_KV_KEY: () => CONSENT_KV_KEY
|
|
505
|
-
});
|
|
506
466
|
import { existsSync as existsSync2, readFileSync as readFileSync2, writeFileSync as writeFileSync2 } from "fs";
|
|
507
|
-
function pluginConfigPaths(baseDir = moduleDir()) {
|
|
508
|
-
return configPaths(baseDir);
|
|
509
|
-
}
|
|
510
467
|
function currentConfig(baseDir = moduleDir()) {
|
|
511
468
|
return loadConfig(baseDir);
|
|
512
469
|
}
|
|
@@ -1169,14 +1126,14 @@ async function editKey(host, cfg, key) {
|
|
|
1169
1126
|
});
|
|
1170
1127
|
if (picked === undefined)
|
|
1171
1128
|
return;
|
|
1172
|
-
const
|
|
1129
|
+
const next = { ...cfg };
|
|
1173
1130
|
if (key === "shareMode")
|
|
1174
|
-
|
|
1131
|
+
next[key] = picked === "auto" ? undefined : picked;
|
|
1175
1132
|
else if (key === "openTasksInNewTab" || key === "announceStderr")
|
|
1176
|
-
|
|
1133
|
+
next[key] = picked === "true";
|
|
1177
1134
|
else
|
|
1178
|
-
|
|
1179
|
-
return
|
|
1135
|
+
next[key] = picked;
|
|
1136
|
+
return next;
|
|
1180
1137
|
}
|
|
1181
1138
|
const raw = await showPrompt(host.ui, {
|
|
1182
1139
|
title: `${meta.label} \u2014 ${displayValue(key, cfg)}`,
|
|
@@ -1681,15 +1638,15 @@ async function adminCodesMenu(host, client, codes) {
|
|
|
1681
1638
|
continue;
|
|
1682
1639
|
const n = Number(raw.trim() === "" ? "1" : raw.trim());
|
|
1683
1640
|
const activations = Number.isFinite(n) && n >= 1 ? Math.floor(n) : -1;
|
|
1684
|
-
const
|
|
1685
|
-
if (
|
|
1686
|
-
await host.clipboard.write(
|
|
1687
|
-
await showInfo(host.ui, { title: "Code created", message: `${label.trim() || "invite"}: ${
|
|
1641
|
+
const res = await client.createCode(label.trim() || "invite", activations);
|
|
1642
|
+
if (res.ok && res.data?.code) {
|
|
1643
|
+
await host.clipboard.write(res.data.code);
|
|
1644
|
+
await showInfo(host.ui, { title: "Code created", message: `${label.trim() || "invite"}: ${res.data.code}
|
|
1688
1645
|
(${activations === -1 ? "unlimited" : activations} activation${activations === 1 ? "" : "s"})
|
|
1689
1646
|
|
|
1690
1647
|
(copied to clipboard \u2014 shown once)` });
|
|
1691
1648
|
} else {
|
|
1692
|
-
await showInfo(host.ui, { title: "Create failed", message:
|
|
1649
|
+
await showInfo(host.ui, { title: "Create failed", message: res.error ?? "unknown error" });
|
|
1693
1650
|
}
|
|
1694
1651
|
continue;
|
|
1695
1652
|
}
|
|
@@ -2215,7 +2172,7 @@ async function showShareManager(host, sessionID) {
|
|
|
2215
2172
|
all = all.filter((b) => !gone.some((g) => g.server === b.server));
|
|
2216
2173
|
const list = gone.map((b) => `\u2022 ${hostOf(b.url || b.server)} \u2014 ${b.url}`).join(`
|
|
2217
2174
|
`);
|
|
2218
|
-
const
|
|
2175
|
+
const choice = await host.ui.showMenu({
|
|
2219
2176
|
title: "Share is gone",
|
|
2220
2177
|
hint: "The page was removed, expired, or purged on the server \u2014 sharing disabled for it.",
|
|
2221
2178
|
options: [
|
|
@@ -2229,7 +2186,7 @@ Gone:
|
|
|
2229
2186
|
${list}` }
|
|
2230
2187
|
]
|
|
2231
2188
|
});
|
|
2232
|
-
if (
|
|
2189
|
+
if (choice === "again") {
|
|
2233
2190
|
finish(host, await runShareFlow(host, sid));
|
|
2234
2191
|
return;
|
|
2235
2192
|
}
|
|
@@ -2266,13 +2223,13 @@ Anyone with the link can read the page (plus the login, if read auth is set).`
|
|
|
2266
2223
|
},
|
|
2267
2224
|
{ title: "Cancel", value: "cancel" }
|
|
2268
2225
|
]
|
|
2269
|
-
}).then(async (
|
|
2270
|
-
if (
|
|
2226
|
+
}).then(async (choice) => {
|
|
2227
|
+
if (choice === "copy") {
|
|
2271
2228
|
const ok = await host.clipboard.write(b.url);
|
|
2272
2229
|
finish(host, ok ? { ok: true, message: `Share URL copied to clipboard: ${b.url}`, url: b.url } : { ok: false, message: `Could not write the clipboard \u2014 link: ${b.url}`, url: b.url });
|
|
2273
2230
|
return;
|
|
2274
2231
|
}
|
|
2275
|
-
if (
|
|
2232
|
+
if (choice === "stop") {
|
|
2276
2233
|
const how = await host.ui.showMenu({
|
|
2277
2234
|
title: "Stop sharing",
|
|
2278
2235
|
hint: b.url,
|
|
@@ -2411,18 +2368,18 @@ ${otherServers.map((s) => `\u2022 ${s.url}`).join(`
|
|
|
2411
2368
|
});
|
|
2412
2369
|
if (!pick || pick === "__cancel")
|
|
2413
2370
|
return;
|
|
2414
|
-
|
|
2371
|
+
await Promise.resolve().then(() => init_share_flow());
|
|
2415
2372
|
for (const s of serverEntries())
|
|
2416
2373
|
if (s.url !== pick)
|
|
2417
|
-
|
|
2418
|
-
|
|
2374
|
+
saveServerEntry(s.url, { active: undefined });
|
|
2375
|
+
saveServerEntry(pick, { active: true });
|
|
2419
2376
|
finish(host, { ok: true, message: `${hostOf(pick)} activated \u2014 this session mirrors there within seconds (new sessions share to it too).` });
|
|
2420
2377
|
return;
|
|
2421
2378
|
}
|
|
2422
2379
|
if (choice === "manage") {
|
|
2423
2380
|
const pick = await host.ui.showMenu({
|
|
2424
2381
|
title: "Manage server",
|
|
2425
|
-
options: [...all.map((
|
|
2382
|
+
options: [...all.map((b) => ({ title: hostOf(b.url || b.server), value: b.server, description: b.url })), { title: "Cancel", value: "__cancel" }]
|
|
2426
2383
|
});
|
|
2427
2384
|
if (!pick || pick === "__cancel")
|
|
2428
2385
|
return;
|
|
@@ -2952,20 +2909,20 @@ function MenuDialog(props) {
|
|
|
2952
2909
|
else
|
|
2953
2910
|
sections.at(-1)?.lines.push(line);
|
|
2954
2911
|
}
|
|
2955
|
-
for (const
|
|
2956
|
-
while (
|
|
2957
|
-
|
|
2958
|
-
while (
|
|
2959
|
-
|
|
2912
|
+
for (const section of sections) {
|
|
2913
|
+
while (section.lines.length && section.lines.at(-1) === "")
|
|
2914
|
+
section.lines.pop();
|
|
2915
|
+
while (section.lines.length && section.lines[0] === "")
|
|
2916
|
+
section.lines.shift();
|
|
2960
2917
|
}
|
|
2961
2918
|
const contentWidth = dialogContentWidth(props.api) - 2;
|
|
2962
|
-
return sections.filter((
|
|
2963
|
-
title:
|
|
2964
|
-
lines: preWrap(
|
|
2919
|
+
return sections.filter((section, index) => index === 0 ? section.lines.length > 0 : true).map((section) => ({
|
|
2920
|
+
title: section.title,
|
|
2921
|
+
lines: preWrap(section.lines.join(`
|
|
2965
2922
|
`), contentWidth)
|
|
2966
2923
|
}));
|
|
2967
2924
|
});
|
|
2968
|
-
const infoHeight = createMemo(() => cappedHeight(helpSections().reduce((rows,
|
|
2925
|
+
const infoHeight = createMemo(() => cappedHeight(helpSections().reduce((rows, section) => rows + section.lines.length + (section.title ? 2 : 0), 0) + 1, maxRows(props.api, dimensions().height, 8, 4), 4));
|
|
2969
2926
|
const [section, setSection] = createSignal(0);
|
|
2970
2927
|
const jumpSection = (delta) => {
|
|
2971
2928
|
const count = helpSections().length;
|
|
@@ -4757,6 +4714,6 @@ var tui_default = {
|
|
|
4757
4714
|
setup: createV2TuiSetup()
|
|
4758
4715
|
};
|
|
4759
4716
|
export {
|
|
4760
|
-
|
|
4761
|
-
|
|
4717
|
+
buildV1HostApi,
|
|
4718
|
+
tui_default as default
|
|
4762
4719
|
};
|
package/openshare.config.json
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
{
|
|
2
|
-
"$comment": "Open(code)Share plugin config. Priority: OPENSHARE_* env vars > plugin options > this file > code defaults. Sync cadence keys are clamped into the worker's policy window (see worker SOCKET_POLICY/DEBOUNCE_MIN_MS/DEBOUNCE_MAX_MS/COMPLETED_DELAY_MAX_MS). Keys: workerUrl (your openshare-worker deployment; empty = plugin stays disabled), createToken (legacy: the worker's CREATE_TOKEN — keep OUT of git), servers (multi-server memberships: url -> {token, code, label, active}; the active entry (or the first) receives new sessions; with no token the plugin ENROLLS itself — public servers take anyone, private ones need a code from the hoster; enrolled tokens persist to ~/.config/opencode/openshare.config.json), debounceMs (sync interval: max time between syncs while streaming; default 10000), completedDelayMs (idle this long before the share says completed; default 5000, 0 = immediate), modelLabel ($Provider/$Model placeholders; literal text sends as-is; empty string hides the model), ttlDays (per-share TTL, capped by the worker), openTasksInNewTab (task rows open subagent pages in a new tab), authLogin/authPassword (optional read gate on the share pages; sent on create AND resume; empty on resume removes protection), spoolDir (temporary unacked-frame buffer; default XDG data home), urlFile (write each share URL to this file; empty = default openshare.urls.txt in the session's directory; \"none\" = no URL file), announceStderr (false silences the [openshare] live: / url file: log lines; the share itself is unaffected).",
|
|
2
|
+
"$comment": "Open(code)Share plugin config. Priority: OPENSHARE_* env vars > plugin options > this file > code defaults. Sync cadence keys are clamped into the worker's policy window (see worker SOCKET_POLICY/DEBOUNCE_MIN_MS/DEBOUNCE_MAX_MS/COMPLETED_DELAY_MAX_MS). Keys: workerUrl (your openshare-worker deployment; empty = plugin stays disabled), createToken (legacy: the worker's CREATE_TOKEN — keep OUT of git), servers (multi-server memberships: url -> {token, code, label, active, adminToken (root token for the TUI admin console)}; the active entry (or the first) receives new sessions; with no token the plugin ENROLLS itself — public servers take anyone, private ones need a code from the hoster; enrolled tokens persist to ~/.config/opencode/openshare.config.json), debounceMs (sync interval: max time between syncs while streaming; default 10000), completedDelayMs (idle this long before the share says completed; default 5000, 0 = immediate), modelLabel ($Provider/$Model placeholders; literal text sends as-is; empty string hides the model), ttlDays (per-share TTL, capped by the worker), openTasksInNewTab (task rows open subagent pages in a new tab), authLogin/authPassword (optional read gate on the share pages; sent on create AND resume; empty on resume removes protection), spoolDir (temporary unacked-frame buffer; default XDG data home), urlFile (write each share URL to this file; empty = default openshare.urls.txt in the session's directory; \"none\" = no URL file), announceStderr (false silences the [openshare] live: / url file: log lines; the share itself is unaffected).",
|
|
3
3
|
"workerUrl": "",
|
|
4
4
|
"createToken": "",
|
|
5
5
|
"servers": {},
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@mirrowel/opencode-openshare",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.1",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "Open(code)Share — re:share: self-hosted session sharing for opencode. Server plugin (single file or package), optional TUI for interactive opt-in sharing.",
|
|
6
6
|
"license": "MIT",
|
|
@@ -92,7 +92,7 @@
|
|
|
92
92
|
"@opentui/solid": "^0.4.1"
|
|
93
93
|
},
|
|
94
94
|
"peerDependencies": {
|
|
95
|
-
"@opencode-ai/plugin": ">=1.
|
|
95
|
+
"@opencode-ai/plugin": ">=1.18.0"
|
|
96
96
|
},
|
|
97
97
|
"engines": {
|
|
98
98
|
"node": ">=20"
|
package/src/banner.txt
CHANGED
|
@@ -13,10 +13,13 @@
|
|
|
13
13
|
// lands in the log and optionally in `urlFile`).
|
|
14
14
|
// - Interactive opt-in: install the package's TUI part (tui.json) too —
|
|
15
15
|
// the server detects its presence and switches to manual mode
|
|
16
|
-
// (`/share
|
|
16
|
+
// (`/share` + `/openshare` in the TUI).
|
|
17
17
|
//
|
|
18
18
|
// Config: env (OPENSHARE_*) > openshare.config.json (next to this file)
|
|
19
|
-
// > code defaults. Keys:
|
|
20
|
-
//
|
|
21
|
-
// shareMode ("all" | "manual"; default = TUI presence)
|
|
19
|
+
// > code defaults. Keys: servers (multi-server: url -> {token, code,
|
|
20
|
+
// label, active, adminToken}), workerUrl, createToken (legacy pair),
|
|
21
|
+
// shareMode ("all" | "manual"; default = TUI presence), debounceMs,
|
|
22
|
+
// completedDelayMs, modelLabel, ttlDays, openTasksInNewTab,
|
|
23
|
+
// authLogin, authPassword, spoolDir, urlFile ("none" = off),
|
|
24
|
+
// announceStderr.
|
|
22
25
|
//
|
package/src/config.ts
CHANGED
|
@@ -413,10 +413,16 @@ function specPath(spec: string): string | undefined {
|
|
|
413
413
|
}
|
|
414
414
|
}
|
|
415
415
|
|
|
416
|
-
/** Does this plugin spec (tui.json entry) point at the openshare package?
|
|
416
|
+
/** Does this plugin spec (tui.json entry) point at the openshare package?
|
|
417
|
+
* Accepts the bare npm name, name@version, and the /tui (or any) subpath
|
|
418
|
+
* form — older docs shipped the subpath spec and presence detection must
|
|
419
|
+
* never silently miss it (all-mode would keep auto-sharing while the user
|
|
420
|
+
* believes they installed manual mode — live-caught in review). */
|
|
417
421
|
export function isOpenShareSpec(spec: unknown): spec is string {
|
|
418
422
|
if (typeof spec !== "string" || spec.length === 0) return false
|
|
419
|
-
if (spec === NPM_NAME
|
|
423
|
+
if (spec === NPM_NAME) return true
|
|
424
|
+
// name@version, name/tui, name@version/tui
|
|
425
|
+
if (spec.startsWith(`${NPM_NAME}@`) || spec.startsWith(`${NPM_NAME}/`)) return true
|
|
420
426
|
const path = specPath(spec)
|
|
421
427
|
if (path) {
|
|
422
428
|
if (path.endsWith("/opencode-openshare") || path.endsWith("/share-plugin")) return true
|
package/src/share-flow.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
// OpenShare share flows — host-agnostic logic behind /share
|
|
1
|
+
// OpenShare share flows — host-agnostic logic behind /share and
|
|
2
2
|
// the settings dialog's "this session" actions. Runs against the
|
|
3
3
|
// ShareHostApi facade (tui-host.ts) and the shared config module, so the
|
|
4
4
|
// same compiled code serves the v1 TUI and the v2 adapter.
|
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
// (with per-session opts when present) -> wait for the server
|
|
10
10
|
// plugin to publish the binding URL into session metadata
|
|
11
11
|
// (~seconds; worker-down surfaces as a timeout toast) -> copy.
|
|
12
|
-
//
|
|
12
|
+
// stop-sharing resolve root -> confirm -> read binding -> clear flag ->
|
|
13
13
|
// DELETE the share directly via the worker (belt and braces:
|
|
14
14
|
// works even if the server plugin never saw the flag event) ->
|
|
15
15
|
// the server-side flag-removal path cleans up on its own.
|
package/src/tui.tsx
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
// OpenShare TUI plugin entry (OpenCode v1).
|
|
4
4
|
//
|
|
5
|
-
// Registers the /share,
|
|
5
|
+
// Registers the /share (toggle + manager, shadowing the deprecated opncd.ai
|
|
6
6
|
// built-ins via a priority keymap layer) and /openshare commands, plus the
|
|
7
7
|
// sidebar_title slot chip. The flows run against the ShareHostApi facade
|
|
8
8
|
// (tui-host.ts); v2 hosts run the same flows through the v2-tui adapter.
|