@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 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 (built by `npm run build`) | always all — the CI bundle |
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
- Interactive (manual sharing, `/share` command, sidebar chip):
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/tui"] }
28
+ { "plugin": ["@mirrowel/opencode-openshare"] }
27
29
  ```
28
30
 
29
- CI (share everything, link to a file):
31
+ **v2 hosts** — the CLI writes the config for you:
30
32
 
31
- ```jsonc
32
- // OPENCODE_CONFIG one-file setup
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
- Then configure (`openshare.config.json` next to the package — see the
42
- [main README](../README.md#configuration) for all keys):
37
+ Restart opencode; `/share` and `/openshare` appear in the TUI.
43
38
 
44
- ```jsonc
45
- { "workerUrl": "https://your-worker.workers.dev", "createToken": "…" }
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 the real production path — local
127
- `wrangler dev` worker + real `opencode serve` with a mock OpenAI-compatible
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: enrollment, code burn, rotate,
140
- freeze (secret dead too), limits, reassign, public mode (48 checks).
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-to-end (single file + package +
148
- silenced outputs), on-demand only (uses the default model).
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 dialog + servers section + admin console
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
@@ -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`, `/unshare` in the TUI).
16
+ // (`/share` + `/openshare` in the TUI).
17
17
  //
18
18
  // Config: env (OPENSHARE_*) > openshare.config.json (next to this file)
19
- // > code defaults. Keys: workerUrl, createToken, debounceMs, modelLabel,
20
- // ttlDays, openTasksInNewTab, authLogin, authPassword, spoolDir, urlFile,
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 [url2, entry] of Object.entries(cfg.servers))
121
- servers[url2] = entry;
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 || spec.startsWith(`${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 num2 = (v, d) => {
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: num2(p.turnGraceMs, prev.turnGraceMs),
579
- idleDropMs: num2(p.idleDropMs, prev.idleDropMs),
580
- finalizeAfterMs: num2(p.finalizeAfterMs, prev.finalizeAfterMs),
581
- debounceMinMs: num2(p.debounceMinMs, 0),
582
- debounceMaxMs: Math.max(num2(p.debounceMaxMs, Infinity), num2(p.debounceMinMs, 0)),
583
- completedDelayMaxMs: num2(p.completedDelayMaxMs, Infinity)
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 st3 = sessions.get(parent);
1609
- if (st3?.activated && !st3.closed) {
1610
- if (!st3.prefsSent) {
1611
- st3.prefsSent = true;
1612
- enqueue(st3, itemFor("prefs", { openTasksInNewTab: cfg.openTasksInNewTab !== false }, sid));
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(st3, itemFor("session", redact(info), sid));
1620
+ enqueue(st, itemFor("session", redact(info), sid));
1616
1621
  }
1617
1622
  return;
1618
1623
  }
1619
1624
  const flag = shareFlag(info);
1620
- const st2 = sessions.get(sid);
1625
+ const st = sessions.get(sid);
1621
1626
  if (flag.enabled) {
1622
- if (st2?.activated && !st2.closed) {
1627
+ if (st?.activated && !st.closed) {
1623
1628
  if (flag.opts)
1624
- st2.opts = flag.opts;
1625
- if (!st2.prefsSent) {
1626
- st2.prefsSent = true;
1627
- enqueue(st2, itemFor("prefs", { openTasksInNewTab: cfg.openTasksInNewTab !== false }, sid));
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(st2, itemFor("session", redact(info), sid));
1635
+ enqueue(st, itemFor("session", redact(info), sid));
1631
1636
  if (t === "session.updated")
1632
- chain = chain.then(() => refreshOptOuts(st2)).catch(() => {});
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 (st2?.activated && !st2.closed && t === "session.updated") {
1648
+ if (st?.activated && !st.closed && t === "session.updated") {
1644
1649
  log(`share disabled for ${sid} - removing`);
1645
- chain = chain.then(() => remove(st2)).catch((e) => logError("remove error", e?.message || e));
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 info2 = await sessionInfo(created.client, main);
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(info2);
1796
- const hasBinding = !!info2?.metadata?.[METADATA_KEY];
1797
- const createdMs = info2?.time?.created;
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=${!info2}`);
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
- server_default as default,
2267
+ OpenSharePlugin,
2263
2268
  PLUGIN_ID,
2264
- OpenSharePlugin
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 || spec.startsWith(`${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) {
@@ -1,4 +1,4 @@
1
- // OpenShare share flows — host-agnostic logic behind /share, /unshare and
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
- // /unshare resolve root -> confirm -> read binding -> clear flag ->
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 __defProp = Object.defineProperty;
3
- var __returnValue = (v) => v;
4
- function __exportSetter(name, newValue) {
5
- this[name] = __returnValue.bind(null, newValue);
6
- }
7
- var __export = (target, all) => {
8
- for (var name in all)
9
- __defProp(target, name, {
10
- get: all[name],
11
- enumerable: true,
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 [url2, entry] of Object.entries(cfg.servers))
169
- servers[url2] = entry;
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 next2 = { ...cfg };
1129
+ const next = { ...cfg };
1173
1130
  if (key === "shareMode")
1174
- next2[key] = picked === "auto" ? undefined : picked;
1131
+ next[key] = picked === "auto" ? undefined : picked;
1175
1132
  else if (key === "openTasksInNewTab" || key === "announceStderr")
1176
- next2[key] = picked === "true";
1133
+ next[key] = picked === "true";
1177
1134
  else
1178
- next2[key] = picked;
1179
- return next2;
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 res2 = await client.createCode(label.trim() || "invite", activations);
1685
- if (res2.ok && res2.data?.code) {
1686
- await host.clipboard.write(res2.data.code);
1687
- await showInfo(host.ui, { title: "Code created", message: `${label.trim() || "invite"}: ${res2.data.code}
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: res2.error ?? "unknown error" });
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 choice2 = await host.ui.showMenu({
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 (choice2 === "again") {
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 (choice2) => {
2270
- if (choice2 === "copy") {
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 (choice2 === "stop") {
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
- const { saveServerEntry: saveServerEntry2 } = await Promise.resolve().then(() => (init_share_flow(), exports_share_flow));
2371
+ await Promise.resolve().then(() => init_share_flow());
2415
2372
  for (const s of serverEntries())
2416
2373
  if (s.url !== pick)
2417
- saveServerEntry2(s.url, { active: undefined });
2418
- saveServerEntry2(pick, { active: true });
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((b2) => ({ title: hostOf(b2.url || b2.server), value: b2.server, description: b2.url })), { title: "Cancel", value: "__cancel" }]
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 section2 of sections) {
2956
- while (section2.lines.length && section2.lines.at(-1) === "")
2957
- section2.lines.pop();
2958
- while (section2.lines.length && section2.lines[0] === "")
2959
- section2.lines.shift();
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((section2, index) => index === 0 ? section2.lines.length > 0 : true).map((section2) => ({
2963
- title: section2.title,
2964
- lines: preWrap(section2.lines.join(`
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, section2) => rows + section2.lines.length + (section2.title ? 2 : 0), 0) + 1, maxRows(props.api, dimensions().height, 8, 4), 4));
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
- tui_default as default,
4761
- buildV1HostApi
4717
+ buildV1HostApi,
4718
+ tui_default as default
4762
4719
  };
@@ -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.0",
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.14.0"
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`, `/unshare` in the TUI).
16
+ // (`/share` + `/openshare` in the TUI).
17
17
  //
18
18
  // Config: env (OPENSHARE_*) > openshare.config.json (next to this file)
19
- // > code defaults. Keys: workerUrl, createToken, debounceMs, modelLabel,
20
- // ttlDays, openTasksInNewTab, authLogin, authPassword, spoolDir, urlFile,
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 || spec.startsWith(`${NPM_NAME}@`)) return true
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, /unshare and
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
- // /unshare resolve root -> confirm -> read binding -> clear flag ->
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, /unshare (shadowing the deprecated opncd.ai
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.