@mirrowel/opencode-openshare 0.2.0-dev.2 → 0.2.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 +2 -0
- package/ci/README.md +15 -4
- package/ci/openshare.js +112 -26
- package/dist/commands.js +10 -7
- package/dist/config.d.ts +0 -1
- package/dist/config.js +19 -2
- package/dist/core.d.ts +1 -0
- package/dist/core.js +141 -28
- package/dist/settings.d.ts +1 -1
- package/dist/settings.js +104 -44
- package/dist/share-flow.d.ts +60 -4
- package/dist/share-flow.js +110 -15
- package/dist/tui.js +207 -65
- package/openshare.config.json +1 -2
- package/package.json +1 -1
- package/src/banner.txt +1 -1
- package/src/commands.ts +10 -7
- package/src/config.ts +17 -3
- package/src/core.ts +137 -29
- package/src/settings.ts +99 -37
- package/src/share-flow.ts +118 -17
package/README.md
CHANGED
|
@@ -3,6 +3,8 @@
|
|
|
3
3
|
The opencode side of [Open(code)Share](../README.md) — session sharing for
|
|
4
4
|
OpenCode v1 and v2, self-hosted on your own worker.
|
|
5
5
|
|
|
6
|
+
Every plugin config key, its file chain, and the cross-package config surface are inventoried in [../CONFIGURATION.md](../CONFIGURATION.md).
|
|
7
|
+
|
|
6
8
|
## Artifacts
|
|
7
9
|
|
|
8
10
|
| Artifact | What it is | Share mode |
|
package/ci/README.md
CHANGED
|
@@ -46,12 +46,23 @@ OPENSHARE_URL_FILE=share-urls.txt # each share URL appended + stderr line
|
|
|
46
46
|
|
|
47
47
|
With no token held, the bundle **enrolls itself** on first contact
|
|
48
48
|
(public servers take anyone; private ones consume the one-time code) and
|
|
49
|
-
persists the token to `~/.config/opencode/openshare.config.json`
|
|
49
|
+
persists the token to `~/.config/opencode/openshare.config.json` -
|
|
50
50
|
enroll once, then every run just shares. A revoked token re-enrolls the
|
|
51
51
|
same way; if enrollment is impossible the error lands on stderr and
|
|
52
|
-
sharing stops (no retry storms).
|
|
53
|
-
|
|
54
|
-
(
|
|
52
|
+
sharing stops (no retry storms).
|
|
53
|
+
|
|
54
|
+
**Ephemeral runners** (GitHub-hosted, containers): do NOT enroll through
|
|
55
|
+
a code - each fresh machine would burn one activation. Ask the hoster
|
|
56
|
+
for a pre-minted registered token (`osk_…`) and pass it via
|
|
57
|
+
`OPENSHARE_CREATE_TOKEN` (or the config's token field) - registered
|
|
58
|
+
tokens skip enrollment entirely and never expire on their own. Point
|
|
59
|
+
`OPENSHARE_CONFIG_DIR` (or mount) the settings home at a cached path
|
|
60
|
+
only if you want the token persisted; env alone is enough. Note:
|
|
61
|
+
revoking that token (when it is the user's last) freezes control of all
|
|
62
|
+
its shares server-side, even ones holding the share secret.
|
|
63
|
+
|
|
64
|
+
The bundle also supports **fan-out**: multiple `active` servers in the
|
|
65
|
+
`servers` group each get the session (one urlFile line per server).
|
|
55
66
|
|
|
56
67
|
On OpenCode v2, plugin targets should be directories/packages — wrap this
|
|
57
68
|
file in a folder with a `package.json` exporting it (`{"exports": {".": "./openshare.js"}}`)
|
package/ci/openshare.js
CHANGED
|
@@ -19,7 +19,7 @@
|
|
|
19
19
|
// > code defaults. Keys: servers (multi-server: url -> {token, code,
|
|
20
20
|
// label, active, adminToken}), workerUrl, createToken (legacy pair),
|
|
21
21
|
// shareMode ("all" | "manual"; default = TUI presence), debounceMs,
|
|
22
|
-
// completedDelayMs, modelLabel, ttlDays,
|
|
22
|
+
// completedDelayMs, modelLabel, ttlDays,
|
|
23
23
|
// authLogin, authPassword, spoolDir, urlFile ("none" = off),
|
|
24
24
|
// announceStderr.
|
|
25
25
|
//
|
|
@@ -36,7 +36,6 @@ var DEFAULTS = {
|
|
|
36
36
|
completedDelayMs: 5000,
|
|
37
37
|
modelLabel: "$Provider/$Model",
|
|
38
38
|
ttlDays: 90,
|
|
39
|
-
openTasksInNewTab: true,
|
|
40
39
|
authLogin: "",
|
|
41
40
|
authPassword: "",
|
|
42
41
|
spoolDir: "",
|
|
@@ -176,7 +175,10 @@ function loadConfig(pluginDir, options) {
|
|
|
176
175
|
continue;
|
|
177
176
|
cfg[key] = value;
|
|
178
177
|
}
|
|
179
|
-
} catch {
|
|
178
|
+
} catch (e) {
|
|
179
|
+
if (existsSync(path))
|
|
180
|
+
console.error(`[openshare] config file ${path} could not be parsed - ignored:`, e?.message ?? e);
|
|
181
|
+
}
|
|
180
182
|
}
|
|
181
183
|
if (options && typeof options === "object") {
|
|
182
184
|
const serversOpt = normalizeServers(options.servers);
|
|
@@ -221,6 +223,14 @@ function loadConfig(pluginDir, options) {
|
|
|
221
223
|
cfg.shareMode = env.OPENSHARE_SHARE_MODE;
|
|
222
224
|
if (cfg.urlFile.trim().toLowerCase() === URL_FILE_NONE)
|
|
223
225
|
cfg.urlFile = URL_FILE_NONE;
|
|
226
|
+
if (cfg.workerUrl)
|
|
227
|
+
cfg.workerUrl = cfg.workerUrl.trim().replace(/\/+$/, "");
|
|
228
|
+
if (cfg.servers) {
|
|
229
|
+
const fixed = {};
|
|
230
|
+
for (const [u, e] of Object.entries(cfg.servers))
|
|
231
|
+
fixed[u.trim().replace(/\/+$/, "")] = e;
|
|
232
|
+
cfg.servers = fixed;
|
|
233
|
+
}
|
|
224
234
|
return cfg;
|
|
225
235
|
}
|
|
226
236
|
function spoolDirFor(cfg) {
|
|
@@ -346,7 +356,7 @@ function resolveShareMode(cfg, input) {
|
|
|
346
356
|
// src/core.ts
|
|
347
357
|
import { appendFileSync, existsSync as existsSync2, mkdirSync, readFileSync as readFileSync2, rmSync, writeFileSync as writeFileSync2 } from "node:fs";
|
|
348
358
|
import { createHash } from "node:crypto";
|
|
349
|
-
import { join as join2 } from "node:path";
|
|
359
|
+
import { isAbsolute, join as join2 } from "node:path";
|
|
350
360
|
|
|
351
361
|
// src/contract.ts
|
|
352
362
|
function messageTombstone(sessionID, messageID) {
|
|
@@ -638,6 +648,7 @@ function shareFlag(info) {
|
|
|
638
648
|
}
|
|
639
649
|
return { enabled: true, opts: Object.keys(opts).length ? opts : undefined };
|
|
640
650
|
}
|
|
651
|
+
var liveTargets = [];
|
|
641
652
|
function createEngine(deps) {
|
|
642
653
|
let cfg = loadConfig(deps.pluginDir, deps.options);
|
|
643
654
|
const mode = resolveShareMode(cfg, deps);
|
|
@@ -669,8 +680,13 @@ function createEngine(deps) {
|
|
|
669
680
|
log(`share mode: ${mode} (directory: ${deps.directory ?? "?"} worktree: ${deps.worktree ?? "?"})`);
|
|
670
681
|
const resolveTargets = () => {
|
|
671
682
|
const merged = {};
|
|
672
|
-
|
|
683
|
+
const hasServers = !!cfg.servers && Object.keys(cfg.servers).length > 0;
|
|
684
|
+
const envWorker = !!process.env.OPENSHARE_WORKER_URL;
|
|
685
|
+
if (cfg.workerUrl && (!hasServers || envWorker))
|
|
673
686
|
merged[cfg.workerUrl] = { token: cfg.createToken || "", active: true };
|
|
687
|
+
if (cfg.workerUrl && hasServers && !envWorker && !cfg.servers[cfg.workerUrl]) {
|
|
688
|
+
log(`legacy workerUrl ${cfg.workerUrl} ignored: a servers group is configured - remove one of the two`);
|
|
689
|
+
}
|
|
674
690
|
if (cfg.servers)
|
|
675
691
|
for (const [u, e] of Object.entries(cfg.servers))
|
|
676
692
|
merged[u] = { ...merged[u], ...e };
|
|
@@ -680,6 +696,7 @@ function createEngine(deps) {
|
|
|
680
696
|
return chosen.map((url) => ({ url, entry: merged[url] }));
|
|
681
697
|
};
|
|
682
698
|
let targets = resolveTargets();
|
|
699
|
+
liveTargets = targets.map((t) => t.url);
|
|
683
700
|
const credState = new Map;
|
|
684
701
|
const credOf = (url) => {
|
|
685
702
|
let s = credState.get(url);
|
|
@@ -853,7 +870,8 @@ function createEngine(deps) {
|
|
|
853
870
|
badSignal: false,
|
|
854
871
|
lastSentStatus: null,
|
|
855
872
|
closed: false,
|
|
856
|
-
|
|
873
|
+
lastError: null,
|
|
874
|
+
ttlClamped: null,
|
|
857
875
|
binding: null,
|
|
858
876
|
bindingQueried: false,
|
|
859
877
|
optOuts: new Set,
|
|
@@ -872,6 +890,7 @@ function createEngine(deps) {
|
|
|
872
890
|
spoolRoot = spoolDirFor(cfg);
|
|
873
891
|
const before = new Set(targets.map((t) => t.url));
|
|
874
892
|
targets = resolveTargets();
|
|
893
|
+
liveTargets = targets.map((t) => t.url);
|
|
875
894
|
seedCredentials();
|
|
876
895
|
clampToPolicy();
|
|
877
896
|
if (debugEvents) {
|
|
@@ -1045,6 +1064,10 @@ function createEngine(deps) {
|
|
|
1045
1064
|
const value = { v: 2, servers, enabled: true };
|
|
1046
1065
|
if (mode === "manual" && st.opts)
|
|
1047
1066
|
value.opts = st.opts;
|
|
1067
|
+
if (st.lastError)
|
|
1068
|
+
value.lastError = st.lastError;
|
|
1069
|
+
if (st.ttlClamped)
|
|
1070
|
+
value.ttlClamped = st.ttlClamped;
|
|
1048
1071
|
st.binding = value;
|
|
1049
1072
|
st.wroteBinding = true;
|
|
1050
1073
|
await writeMetadata(st, value);
|
|
@@ -1225,7 +1248,7 @@ function createEngine(deps) {
|
|
|
1225
1248
|
if (cfg.announceStderr)
|
|
1226
1249
|
log(`live: ${url}`);
|
|
1227
1250
|
if (mode === "all" && cfg.urlFile !== URL_FILE_NONE) {
|
|
1228
|
-
const file = cfg.urlFile
|
|
1251
|
+
const file = cfg.urlFile ? isAbsolute(cfg.urlFile) ? cfg.urlFile : join2(deps.directory ?? process.cwd(), cfg.urlFile) : join2(deps.directory ?? process.cwd(), "openshare.urls.txt");
|
|
1229
1252
|
try {
|
|
1230
1253
|
appendFileSync(file, url + `
|
|
1231
1254
|
`);
|
|
@@ -1234,15 +1257,43 @@ function createEngine(deps) {
|
|
|
1234
1257
|
lastUrlFile = file;
|
|
1235
1258
|
log(`url file: ${file}`);
|
|
1236
1259
|
}
|
|
1237
|
-
} catch {
|
|
1260
|
+
} catch (e) {
|
|
1261
|
+
logError(`url file write failed (${file}):`, e?.message || e);
|
|
1262
|
+
}
|
|
1238
1263
|
}
|
|
1239
1264
|
};
|
|
1265
|
+
let halfAuthWarned = false;
|
|
1240
1266
|
const authFrame = (st) => {
|
|
1241
1267
|
const login = st.opts?.authLogin ?? cfg.authLogin ?? "";
|
|
1242
1268
|
const password = st.opts?.authPassword ?? cfg.authPassword ?? "";
|
|
1269
|
+
if (login === "" !== (password === "")) {
|
|
1270
|
+
if (!halfAuthWarned) {
|
|
1271
|
+
halfAuthWarned = true;
|
|
1272
|
+
logError("read auth needs BOTH a login and a password - gate disabled until fixed");
|
|
1273
|
+
}
|
|
1274
|
+
return { authLogin: "", authPassword: "" };
|
|
1275
|
+
}
|
|
1243
1276
|
return { authLogin: login, authPassword: password };
|
|
1244
1277
|
};
|
|
1245
1278
|
const ttlFor = (st) => st.opts?.ttlDays ?? cfg.ttlDays;
|
|
1279
|
+
const applyOpts = (st, opts) => {
|
|
1280
|
+
if (!opts)
|
|
1281
|
+
return;
|
|
1282
|
+
const prev = st.opts ? JSON.stringify(st.opts) : "";
|
|
1283
|
+
st.opts = opts;
|
|
1284
|
+
if (prev === JSON.stringify(opts))
|
|
1285
|
+
return;
|
|
1286
|
+
for (const [url, r] of st.remotes) {
|
|
1287
|
+
if (!r.shareId || !r.secret || !r.wsReady)
|
|
1288
|
+
continue;
|
|
1289
|
+
const attach = { id: r.shareId, secret: r.secret, ...authFrame(st) };
|
|
1290
|
+
if (st.opts?.ttlDays)
|
|
1291
|
+
attach.ttlDays = st.opts.ttlDays;
|
|
1292
|
+
const sent = wsSend(r, { op: "init", attach });
|
|
1293
|
+
if (sent)
|
|
1294
|
+
log(`opts re-synced to ${url.slice(0, 24)}${st.opts?.ttlDays ? ` (ttl ${st.opts.ttlDays}d)` : ""}`);
|
|
1295
|
+
}
|
|
1296
|
+
};
|
|
1246
1297
|
const deleteAbandoned = (r) => {
|
|
1247
1298
|
const id = r.shareId;
|
|
1248
1299
|
const secret = r.secret;
|
|
@@ -1326,6 +1377,25 @@ function createEngine(deps) {
|
|
|
1326
1377
|
r.announced = false;
|
|
1327
1378
|
queueConnect(st, r);
|
|
1328
1379
|
}
|
|
1380
|
+
let rearmed = false;
|
|
1381
|
+
for (const [url, e] of Object.entries(servers)) {
|
|
1382
|
+
if (st.optOuts.has(url) || st.remotes.has(url))
|
|
1383
|
+
continue;
|
|
1384
|
+
const ee = e;
|
|
1385
|
+
if (typeof ee.id !== "string" || typeof ee.secret !== "string" || !ee.id)
|
|
1386
|
+
continue;
|
|
1387
|
+
const r = remoteFor(st, url);
|
|
1388
|
+
r.shareId = ee.id;
|
|
1389
|
+
r.secret = ee.secret;
|
|
1390
|
+
r.ackedRev = typeof ee.rev === "number" ? ee.rev : 0;
|
|
1391
|
+
log(`re-share: re-arming ${url.slice(0, 24)} on share ${ee.id}`);
|
|
1392
|
+
queueConnect(st, r);
|
|
1393
|
+
rearmed = true;
|
|
1394
|
+
}
|
|
1395
|
+
if (rearmed) {
|
|
1396
|
+
st.sentHash.clear();
|
|
1397
|
+
chain = chain.then(() => backfill(st)).catch((e) => logError("re-share backfill error", e?.message || e));
|
|
1398
|
+
}
|
|
1329
1399
|
} catch {}
|
|
1330
1400
|
};
|
|
1331
1401
|
const connect = async (st, r) => {
|
|
@@ -1534,6 +1604,8 @@ function createEngine(deps) {
|
|
|
1534
1604
|
const err = String(frame.error ?? "");
|
|
1535
1605
|
if (/frozen/i.test(err)) {
|
|
1536
1606
|
r.authBlocked = true;
|
|
1607
|
+
st.lastError = `share frozen by the hoster (owner frozen) - contact the server admin`;
|
|
1608
|
+
scheduleBindingWrite(st);
|
|
1537
1609
|
logError(`${r.url}: share frozen by the hoster (owner frozen) - edits refused; contact the server admin`);
|
|
1538
1610
|
try {
|
|
1539
1611
|
r.ws?.close(4000, "frozen");
|
|
@@ -1568,6 +1640,8 @@ function createEngine(deps) {
|
|
|
1568
1640
|
return;
|
|
1569
1641
|
}
|
|
1570
1642
|
if (/limit|too many|rate/i.test(err)) {
|
|
1643
|
+
st.lastError = `create refused: ${frame.error ?? "rate limited"}`;
|
|
1644
|
+
scheduleBindingWrite(st);
|
|
1571
1645
|
logError(`${r.url}: create refused:`, frame.error, "- retrying with backoff");
|
|
1572
1646
|
r.connecting = false;
|
|
1573
1647
|
try {
|
|
@@ -1578,6 +1652,8 @@ function createEngine(deps) {
|
|
|
1578
1652
|
return;
|
|
1579
1653
|
}
|
|
1580
1654
|
logError(`${r.url}: init refused:`, frame.error, "- creating fresh share");
|
|
1655
|
+
st.lastError = `init refused: ${frame.error ?? "unknown"}`;
|
|
1656
|
+
scheduleBindingWrite(st);
|
|
1581
1657
|
deleteAbandoned(r);
|
|
1582
1658
|
r.shareId = null;
|
|
1583
1659
|
r.secret = null;
|
|
@@ -1623,6 +1699,18 @@ function createEngine(deps) {
|
|
|
1623
1699
|
applyPolicy(frame.policy, r.url);
|
|
1624
1700
|
r.intentionalClose = false;
|
|
1625
1701
|
touch(st);
|
|
1702
|
+
if (typeof frame.ttlDays === "number") {
|
|
1703
|
+
const wanted = ttlFor(st);
|
|
1704
|
+
const clamp = wanted != null && frame.ttlDays !== wanted ? { requested: wanted, effective: frame.ttlDays } : null;
|
|
1705
|
+
if (JSON.stringify(clamp) !== JSON.stringify(st.ttlClamped)) {
|
|
1706
|
+
st.ttlClamped = clamp;
|
|
1707
|
+
scheduleBindingWrite(st);
|
|
1708
|
+
}
|
|
1709
|
+
}
|
|
1710
|
+
if (st.lastError) {
|
|
1711
|
+
st.lastError = null;
|
|
1712
|
+
scheduleBindingWrite(st);
|
|
1713
|
+
}
|
|
1626
1714
|
announceUrl(st, r);
|
|
1627
1715
|
if (!st.wroteBinding) {
|
|
1628
1716
|
chain = chain.then(() => writeBinding(st)).catch(() => {});
|
|
@@ -2396,10 +2484,6 @@ function createEngine(deps) {
|
|
|
2396
2484
|
if (parent) {
|
|
2397
2485
|
const st = sessions.get(parent);
|
|
2398
2486
|
if (st?.activated && !st.closed) {
|
|
2399
|
-
if (!st.prefsSent) {
|
|
2400
|
-
st.prefsSent = true;
|
|
2401
|
-
enqueue(st, itemFor("prefs", { openTasksInNewTab: cfg.openTasksInNewTab !== false }, sid));
|
|
2402
|
-
}
|
|
2403
2487
|
if (info)
|
|
2404
2488
|
enqueue(st, itemFor("session", redact(info), sid));
|
|
2405
2489
|
}
|
|
@@ -2410,11 +2494,7 @@ function createEngine(deps) {
|
|
|
2410
2494
|
if (flag.enabled) {
|
|
2411
2495
|
if (st?.activated && !st.closed) {
|
|
2412
2496
|
if (flag.opts)
|
|
2413
|
-
st
|
|
2414
|
-
if (!st.prefsSent) {
|
|
2415
|
-
st.prefsSent = true;
|
|
2416
|
-
enqueue(st, itemFor("prefs", { openTasksInNewTab: cfg.openTasksInNewTab !== false }, sid));
|
|
2417
|
-
}
|
|
2497
|
+
applyOpts(st, flag.opts);
|
|
2418
2498
|
if (info)
|
|
2419
2499
|
enqueue(st, itemFor("session", redact(info), sid));
|
|
2420
2500
|
if (t === "session.updated")
|
|
@@ -2422,11 +2502,6 @@ function createEngine(deps) {
|
|
|
2422
2502
|
return;
|
|
2423
2503
|
}
|
|
2424
2504
|
activate(sid, flag.opts, info);
|
|
2425
|
-
const fresh = sessions.get(sid);
|
|
2426
|
-
if (fresh && !fresh.prefsSent) {
|
|
2427
|
-
fresh.prefsSent = true;
|
|
2428
|
-
enqueue(fresh, itemFor("prefs", { openTasksInNewTab: cfg.openTasksInNewTab !== false }, sid));
|
|
2429
|
-
}
|
|
2430
2505
|
return;
|
|
2431
2506
|
}
|
|
2432
2507
|
if (st?.activated && !st.closed && t === "session.updated") {
|
|
@@ -2472,10 +2547,6 @@ function createEngine(deps) {
|
|
|
2472
2547
|
}
|
|
2473
2548
|
if (!st)
|
|
2474
2549
|
return;
|
|
2475
|
-
if (!st.prefsSent) {
|
|
2476
|
-
st.prefsSent = true;
|
|
2477
|
-
enqueue(st, itemFor("prefs", { openTasksInNewTab: cfg.openTasksInNewTab !== false }, sid));
|
|
2478
|
-
}
|
|
2479
2550
|
if (info)
|
|
2480
2551
|
enqueue(st, itemFor("session", redact(info), sid));
|
|
2481
2552
|
if (t === "session.updated" && st.activated && !st.closed) {
|
|
@@ -2685,6 +2756,21 @@ function createEngine(deps) {
|
|
|
2685
2756
|
st.metaTimer = null;
|
|
2686
2757
|
await writeBinding(st).catch(() => {});
|
|
2687
2758
|
}
|
|
2759
|
+
const barrierStart = Date.now();
|
|
2760
|
+
while (Date.now() - barrierStart < 5000) {
|
|
2761
|
+
let pendingAnnounce = false;
|
|
2762
|
+
for (const st of sessions.values()) {
|
|
2763
|
+
for (const r of st.remotes.values()) {
|
|
2764
|
+
if (r.authBlocked || r.collided)
|
|
2765
|
+
continue;
|
|
2766
|
+
if (r.shareId && !r.announced || !r.shareId && (r.ws || r.connecting))
|
|
2767
|
+
pendingAnnounce = true;
|
|
2768
|
+
}
|
|
2769
|
+
}
|
|
2770
|
+
if (!pendingAnnounce)
|
|
2771
|
+
break;
|
|
2772
|
+
await new Promise((res) => setTimeout(res, 100));
|
|
2773
|
+
}
|
|
2688
2774
|
for (const st of sessions.values()) {
|
|
2689
2775
|
for (const r of st.remotes.values()) {
|
|
2690
2776
|
try {
|
package/dist/commands.js
CHANGED
|
@@ -15,6 +15,7 @@
|
|
|
15
15
|
// deliberately NOT joined into the shared family palette category).
|
|
16
16
|
import { clearGoneBindingEntries, currentConfig, hostOf, isConfigured, patchSessionBinding, readBindings, resolveRoot, shareWithConsent, verifyShare, } from "./share-flow.js";
|
|
17
17
|
import { onboardingDialog, settingsDialog } from "./settings.js";
|
|
18
|
+
import { engineTargetUrls } from "./core.js";
|
|
18
19
|
import { dbgLog } from "./sidebar.js";
|
|
19
20
|
export const PALETTE_CATEGORY = "OpenShare";
|
|
20
21
|
function finish(host, result) {
|
|
@@ -135,7 +136,9 @@ export function buildShareCommand(host, sessionID) {
|
|
|
135
136
|
dbgLog(`/share run: entered (sid=${sessionID() ?? "none"})`);
|
|
136
137
|
host.ui.dialog.clear();
|
|
137
138
|
const cfg = currentConfig();
|
|
138
|
-
|
|
139
|
+
// engine-aware gate: project-level plugin options can configure
|
|
140
|
+
// targets the TUI's file chain cannot see - trust the live engine
|
|
141
|
+
if (!isConfigured(cfg) && !engineTargetUrls().length) {
|
|
139
142
|
void onboardingDialog(host);
|
|
140
143
|
return;
|
|
141
144
|
}
|
|
@@ -155,12 +158,12 @@ export function buildShareCommand(host, sessionID) {
|
|
|
155
158
|
// enabled:true; legacy all-mode bindings without the field are live
|
|
156
159
|
// too — only an explicit false is dormant)
|
|
157
160
|
if (all.some((b) => b.url && b.enabled !== false)) {
|
|
158
|
-
// THE session menu
|
|
159
|
-
// construction (one surface, no duplicated manager)
|
|
160
|
-
//
|
|
161
|
-
//
|
|
162
|
-
//
|
|
163
|
-
await settingsDialog(host, sid);
|
|
161
|
+
// THE session menu - identical to /openshare This session by
|
|
162
|
+
// construction (one surface, no duplicated manager), opened
|
|
163
|
+
// DIRECTLY (owner: "/share is supposed to be the same menu, not
|
|
164
|
+
// the settings root one hop away"). No dialog.clear() after:
|
|
165
|
+
// settings owns its chain end-to-end.
|
|
166
|
+
await settingsDialog(host, sid, "session");
|
|
164
167
|
return;
|
|
165
168
|
}
|
|
166
169
|
// everything was just cleared by the gone modal and the user chose
|
package/dist/config.d.ts
CHANGED
package/dist/config.js
CHANGED
|
@@ -36,7 +36,6 @@ export const DEFAULTS = {
|
|
|
36
36
|
completedDelayMs: 5000,
|
|
37
37
|
modelLabel: "$Provider/$Model",
|
|
38
38
|
ttlDays: 90,
|
|
39
|
-
openTasksInNewTab: true,
|
|
40
39
|
authLogin: "",
|
|
41
40
|
authPassword: "",
|
|
42
41
|
spoolDir: "",
|
|
@@ -327,7 +326,14 @@ export function loadConfig(pluginDir, options) {
|
|
|
327
326
|
cfg[key] = value;
|
|
328
327
|
}
|
|
329
328
|
}
|
|
330
|
-
catch {
|
|
329
|
+
catch (e) {
|
|
330
|
+
// a malformed config file is a real setup error the user must hear
|
|
331
|
+
// about - silence here looks like "no share server configured".
|
|
332
|
+
// (Absent files are normal chain candidates - only broken content
|
|
333
|
+
// is worth a line.)
|
|
334
|
+
if (existsSync(path))
|
|
335
|
+
console.error(`[openshare] config file ${path} could not be parsed - ignored:`, e?.message ?? e);
|
|
336
|
+
}
|
|
331
337
|
}
|
|
332
338
|
if (options && typeof options === "object") {
|
|
333
339
|
const serversOpt = normalizeServers(options.servers);
|
|
@@ -374,6 +380,17 @@ export function loadConfig(pluginDir, options) {
|
|
|
374
380
|
// normalize the disable sentinel (case-insensitive from any source)
|
|
375
381
|
if (cfg.urlFile.trim().toLowerCase() === URL_FILE_NONE)
|
|
376
382
|
cfg.urlFile = URL_FILE_NONE;
|
|
383
|
+
// URL hygiene: a trailing slash on a server url becomes
|
|
384
|
+
// wss://host//api/ws and https://host//s/<id> downstream - strip once,
|
|
385
|
+
// at the source, so every consumer agrees
|
|
386
|
+
if (cfg.workerUrl)
|
|
387
|
+
cfg.workerUrl = cfg.workerUrl.trim().replace(/\/+$/, "");
|
|
388
|
+
if (cfg.servers) {
|
|
389
|
+
const fixed = {};
|
|
390
|
+
for (const [u, e] of Object.entries(cfg.servers))
|
|
391
|
+
fixed[u.trim().replace(/\/+$/, "")] = e;
|
|
392
|
+
cfg.servers = fixed;
|
|
393
|
+
}
|
|
377
394
|
return cfg;
|
|
378
395
|
}
|
|
379
396
|
export function spoolDirFor(cfg) {
|