@mirrowel/opencode-openshare 0.3.0-dev.13 → 0.3.0-dev.15
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/ci/openshare.js +40 -14
- package/dist/config.d.ts +8 -3
- package/dist/config.js +25 -2
- package/dist/core.js +31 -14
- package/dist/settings.js +4 -8
- package/dist/tui.js +23 -10
- package/package.json +1 -1
- package/src/banner.txt +3 -1
- package/src/config.ts +30 -5
- package/src/core.ts +30 -14
- package/src/settings.ts +4 -6
package/ci/openshare.js
CHANGED
|
@@ -21,7 +21,9 @@
|
|
|
21
21
|
// shareMode ("all" | "manual"; default = TUI presence), debounceMs,
|
|
22
22
|
// completedDelayMs, modelLabel, ttlDays,
|
|
23
23
|
// authLogin, authPassword, spoolDir, urlFile ("none" = off),
|
|
24
|
-
// announceStderr
|
|
24
|
+
// announceStderr ("all" | "status" | "url" | "off"; status keeps the
|
|
25
|
+
// status/banner lines and never prints the share URL - the public-CI
|
|
26
|
+
// shape; url prints announce lines only).
|
|
25
27
|
//
|
|
26
28
|
// src/config.ts
|
|
27
29
|
import { existsSync, readFileSync, writeFileSync } from "node:fs";
|
|
@@ -42,7 +44,7 @@ var DEFAULTS = {
|
|
|
42
44
|
urlFile: "",
|
|
43
45
|
urlFileFormat: "url",
|
|
44
46
|
configSet: "",
|
|
45
|
-
announceStderr:
|
|
47
|
+
announceStderr: "all"
|
|
46
48
|
};
|
|
47
49
|
var CONFIG_NAME = "openshare.config.json";
|
|
48
50
|
var LOCAL_NAME = "openshare.local.json";
|
|
@@ -225,6 +227,22 @@ function saveServerEntry(url, patch, target) {
|
|
|
225
227
|
function saveServerToken(url, token, target) {
|
|
226
228
|
return saveServerEntry(url, { token }, target);
|
|
227
229
|
}
|
|
230
|
+
function normalizeAnnounce(value, fallback) {
|
|
231
|
+
if (value === true)
|
|
232
|
+
return "all";
|
|
233
|
+
if (value === false)
|
|
234
|
+
return "off";
|
|
235
|
+
if (typeof value === "string") {
|
|
236
|
+
const v = value.trim().toLowerCase();
|
|
237
|
+
if (v === "all" || v === "status" || v === "url" || v === "off")
|
|
238
|
+
return v;
|
|
239
|
+
if (/^(1|true|yes|on)$/.test(v))
|
|
240
|
+
return "all";
|
|
241
|
+
if (/^(0|false|no)$/.test(v))
|
|
242
|
+
return "off";
|
|
243
|
+
}
|
|
244
|
+
return fallback;
|
|
245
|
+
}
|
|
228
246
|
function loadConfig(pluginDir, options) {
|
|
229
247
|
const cfg = { ...DEFAULTS };
|
|
230
248
|
const emptyMeansUnset = new Set(["workerUrl", "createToken", "urlFile", "spoolDir"]);
|
|
@@ -332,11 +350,12 @@ function loadConfig(pluginDir, options) {
|
|
|
332
350
|
if (env.OPENSHARE_SPOOL_DIR)
|
|
333
351
|
cfg.spoolDir = env.OPENSHARE_SPOOL_DIR;
|
|
334
352
|
if (env.OPENSHARE_ANNOUNCE_STDERR !== undefined)
|
|
335
|
-
cfg.announceStderr =
|
|
353
|
+
cfg.announceStderr = env.OPENSHARE_ANNOUNCE_STDERR;
|
|
336
354
|
if (env.OPENSHARE_SHARE_MODE === "all" || env.OPENSHARE_SHARE_MODE === "manual")
|
|
337
355
|
cfg.shareMode = env.OPENSHARE_SHARE_MODE;
|
|
338
356
|
if (cfg.urlFile.trim().toLowerCase() === URL_FILE_NONE)
|
|
339
357
|
cfg.urlFile = URL_FILE_NONE;
|
|
358
|
+
cfg.announceStderr = normalizeAnnounce(cfg.announceStderr, "all");
|
|
340
359
|
if (cfg.workerUrl)
|
|
341
360
|
cfg.workerUrl = cfg.workerUrl.trim().replace(/\/+$/, "");
|
|
342
361
|
if (cfg.servers) {
|
|
@@ -889,6 +908,12 @@ function createEngine(deps) {
|
|
|
889
908
|
const mode = resolveShareMode(cfg, deps);
|
|
890
909
|
if (mode === "manual")
|
|
891
910
|
stderrInfoAllowed = false;
|
|
911
|
+
const wantAnnounce = () => cfg.announceStderr === "all" || cfg.announceStderr === "url";
|
|
912
|
+
const logStatus = (...a) => {
|
|
913
|
+
if (cfg.announceStderr !== "all" && cfg.announceStderr !== "status")
|
|
914
|
+
return;
|
|
915
|
+
log(...a);
|
|
916
|
+
};
|
|
892
917
|
if (process.env.OPENSHARE_DEBUG_FILE) {
|
|
893
918
|
try {
|
|
894
919
|
writeFileSync2(process.env.OPENSHARE_DEBUG_FILE, JSON.stringify({
|
|
@@ -902,7 +927,7 @@ function createEngine(deps) {
|
|
|
902
927
|
}
|
|
903
928
|
const server = resolveServer(cfg);
|
|
904
929
|
if (!server || !server.url) {
|
|
905
|
-
|
|
930
|
+
logStatus("disabled: no share server configured (workerUrl or a servers entry)");
|
|
906
931
|
return {
|
|
907
932
|
mode,
|
|
908
933
|
config: () => cfg,
|
|
@@ -912,7 +937,7 @@ function createEngine(deps) {
|
|
|
912
937
|
}
|
|
913
938
|
};
|
|
914
939
|
}
|
|
915
|
-
|
|
940
|
+
logStatus(`share mode: ${mode} (directory: ${deps.directory ?? "?"} worktree: ${deps.worktree ?? "?"})`);
|
|
916
941
|
const resolveTargets = () => {
|
|
917
942
|
const merged = {};
|
|
918
943
|
const hasServers = !!cfg.servers && Object.keys(cfg.servers).length > 0;
|
|
@@ -920,7 +945,7 @@ function createEngine(deps) {
|
|
|
920
945
|
if (cfg.workerUrl && (!hasServers || envWorker))
|
|
921
946
|
merged[cfg.workerUrl] = { token: cfg.createToken || "", active: true };
|
|
922
947
|
if (cfg.workerUrl && hasServers && !envWorker && !cfg.servers[cfg.workerUrl]) {
|
|
923
|
-
|
|
948
|
+
logStatus(`legacy workerUrl ${cfg.workerUrl} ignored: a servers group is configured - remove one of the two`);
|
|
924
949
|
}
|
|
925
950
|
if (cfg.servers)
|
|
926
951
|
for (const [u, e] of Object.entries(cfg.servers))
|
|
@@ -993,12 +1018,12 @@ function createEngine(deps) {
|
|
|
993
1018
|
if (adoptModel && doc.modelLabel === undefined) {
|
|
994
1019
|
doc.modelLabel = wantModel;
|
|
995
1020
|
dirty = true;
|
|
996
|
-
|
|
1021
|
+
logStatus(`${url}: adopted modelLabel from the server config set`);
|
|
997
1022
|
}
|
|
998
1023
|
if (adoptMode && doc.shareMode === undefined) {
|
|
999
1024
|
doc.shareMode = wantMode;
|
|
1000
1025
|
dirty = true;
|
|
1001
|
-
|
|
1026
|
+
logStatus(`${url}: adopted shareMode from the server config set`);
|
|
1002
1027
|
}
|
|
1003
1028
|
if (dirty)
|
|
1004
1029
|
writeFileSync2(home, JSON.stringify(doc, null, 2) + `
|
|
@@ -1043,7 +1068,7 @@ function createEngine(deps) {
|
|
|
1043
1068
|
c.credential = b.token;
|
|
1044
1069
|
c.error = "";
|
|
1045
1070
|
const savedTo = saveServerToken(url, c.credential);
|
|
1046
|
-
|
|
1071
|
+
logStatus(`enrolled at ${url} (user ${b.userId} "${b.label}")${savedTo ? " - token saved" : ""}`);
|
|
1047
1072
|
adoptServerSuggestions(url, c.credential);
|
|
1048
1073
|
return c.credential;
|
|
1049
1074
|
}
|
|
@@ -1190,7 +1215,7 @@ function createEngine(deps) {
|
|
|
1190
1215
|
const after = targets.map((t) => t.url);
|
|
1191
1216
|
const added = after.filter((u) => !before.has(u));
|
|
1192
1217
|
if (added.length)
|
|
1193
|
-
|
|
1218
|
+
logStatus(`fan-out set grew: ${added.join(", ")}`);
|
|
1194
1219
|
}
|
|
1195
1220
|
}
|
|
1196
1221
|
};
|
|
@@ -1368,10 +1393,11 @@ function createEngine(deps) {
|
|
|
1368
1393
|
const scheduleBindingWrite = (st) => {
|
|
1369
1394
|
if (st.metaTimer)
|
|
1370
1395
|
return;
|
|
1396
|
+
const debounce = Number(process.env.OPENSHARE_BINDING_WRITE_MS) > 0 ? Number(process.env.OPENSHARE_BINDING_WRITE_MS) : 30000;
|
|
1371
1397
|
st.metaTimer = setTimeout(() => {
|
|
1372
1398
|
st.metaTimer = null;
|
|
1373
1399
|
chain = chain.then(() => writeBinding(st)).catch(() => {});
|
|
1374
|
-
},
|
|
1400
|
+
}, debounce);
|
|
1375
1401
|
};
|
|
1376
1402
|
const h8 = (url) => {
|
|
1377
1403
|
let h = 5381;
|
|
@@ -1538,7 +1564,7 @@ function createEngine(deps) {
|
|
|
1538
1564
|
return;
|
|
1539
1565
|
r.announced = true;
|
|
1540
1566
|
const url = `${r.url}/s/${r.shareId}`;
|
|
1541
|
-
if (
|
|
1567
|
+
if (wantAnnounce())
|
|
1542
1568
|
log(`live: ${url}`);
|
|
1543
1569
|
if (mode === "all" && cfg.urlFile !== URL_FILE_NONE) {
|
|
1544
1570
|
const file = cfg.urlFile ? isAbsolute(cfg.urlFile) ? cfg.urlFile : join2(deps.directory ?? process.cwd(), cfg.urlFile) : join2(deps.directory ?? process.cwd(), "openshare.urls.txt");
|
|
@@ -1552,7 +1578,7 @@ function createEngine(deps) {
|
|
|
1552
1578
|
` : url + `
|
|
1553
1579
|
`;
|
|
1554
1580
|
appendFileSync(file, line);
|
|
1555
|
-
if (
|
|
1581
|
+
if (wantAnnounce() && (file !== lastUrlFile || !urlFileAnnounced)) {
|
|
1556
1582
|
urlFileAnnounced = true;
|
|
1557
1583
|
lastUrlFile = file;
|
|
1558
1584
|
log(`url file: ${file}`);
|
|
@@ -1604,7 +1630,7 @@ function createEngine(deps) {
|
|
|
1604
1630
|
attach.ttlHours = st.opts.ttlHours;
|
|
1605
1631
|
const sent = wsSend(r, { op: "init", attach });
|
|
1606
1632
|
if (sent)
|
|
1607
|
-
|
|
1633
|
+
logStatus(`opts re-synced to ${url.slice(0, 24)}${st.opts?.ttlHours ? ` (ttl ${st.opts.ttlHours}h)` : ""}`);
|
|
1608
1634
|
}
|
|
1609
1635
|
};
|
|
1610
1636
|
const deleteAbandoned = (r) => {
|
package/dist/config.d.ts
CHANGED
|
@@ -36,9 +36,14 @@ export type ShareConfig = {
|
|
|
36
36
|
* "json" (one JSON object per line: id, url, status, gated) for CI
|
|
37
37
|
* consumers that want machine-readable lines without parsing URLs. */
|
|
38
38
|
urlFileFormat: "url" | "json";
|
|
39
|
-
/**
|
|
40
|
-
* lines
|
|
41
|
-
|
|
39
|
+
/** Stderr ladder with two independently controllable line classes:
|
|
40
|
+
* announce lines (`live:`, `url file:`) and status lines (mode banner,
|
|
41
|
+
* enrollment, adoption notes). "all" (default) prints both; "status"
|
|
42
|
+
* prints status lines only - the share URL never touches stderr
|
|
43
|
+
* (public CI logs); "url" prints announce lines only (private runners
|
|
44
|
+
* scraping the URL); "off" prints neither. Error-class lines ALWAYS
|
|
45
|
+
* reach stderr. */
|
|
46
|
+
announceStderr: "all" | "status" | "url" | "off";
|
|
42
47
|
/** Wave 3 redaction: user-supplied regex rules (client-side only)
|
|
43
48
|
* applied beside the built-in detectors on UNGATED shares. */
|
|
44
49
|
redact?: {
|
package/dist/config.js
CHANGED
|
@@ -42,7 +42,7 @@ export const DEFAULTS = {
|
|
|
42
42
|
urlFile: "",
|
|
43
43
|
urlFileFormat: "url",
|
|
44
44
|
configSet: "",
|
|
45
|
-
announceStderr:
|
|
45
|
+
announceStderr: "all",
|
|
46
46
|
};
|
|
47
47
|
const CONFIG_NAME = "openshare.config.json";
|
|
48
48
|
const LOCAL_NAME = "openshare.local.json";
|
|
@@ -331,6 +331,24 @@ export function deleteServerEntry(url, target) {
|
|
|
331
331
|
return undefined;
|
|
332
332
|
}
|
|
333
333
|
}
|
|
334
|
+
/** Coerce an announceStderr value from any layer (enum string, legacy
|
|
335
|
+
* boolean, truthy/falsy word) to the 4-position ladder. */
|
|
336
|
+
function normalizeAnnounce(value, fallback) {
|
|
337
|
+
if (value === true)
|
|
338
|
+
return "all";
|
|
339
|
+
if (value === false)
|
|
340
|
+
return "off";
|
|
341
|
+
if (typeof value === "string") {
|
|
342
|
+
const v = value.trim().toLowerCase();
|
|
343
|
+
if (v === "all" || v === "status" || v === "url" || v === "off")
|
|
344
|
+
return v;
|
|
345
|
+
if (/^(1|true|yes|on)$/.test(v))
|
|
346
|
+
return "all";
|
|
347
|
+
if (/^(0|false|no)$/.test(v))
|
|
348
|
+
return "off";
|
|
349
|
+
}
|
|
350
|
+
return fallback;
|
|
351
|
+
}
|
|
334
352
|
/** Load the effective config. `options` is the plugin-array options tuple
|
|
335
353
|
* object (env still wins over it). Files MERGE lowest→highest priority
|
|
336
354
|
* (later files override per key); empty strings in credential/path keys
|
|
@@ -460,12 +478,17 @@ export function loadConfig(pluginDir, options) {
|
|
|
460
478
|
if (env.OPENSHARE_SPOOL_DIR)
|
|
461
479
|
cfg.spoolDir = env.OPENSHARE_SPOOL_DIR;
|
|
462
480
|
if (env.OPENSHARE_ANNOUNCE_STDERR !== undefined)
|
|
463
|
-
cfg.announceStderr =
|
|
481
|
+
cfg.announceStderr = env.OPENSHARE_ANNOUNCE_STDERR;
|
|
464
482
|
if (env.OPENSHARE_SHARE_MODE === "all" || env.OPENSHARE_SHARE_MODE === "manual")
|
|
465
483
|
cfg.shareMode = env.OPENSHARE_SHARE_MODE;
|
|
466
484
|
// normalize the disable sentinel (case-insensitive from any source)
|
|
467
485
|
if (cfg.urlFile.trim().toLowerCase() === URL_FILE_NONE)
|
|
468
486
|
cfg.urlFile = URL_FILE_NONE;
|
|
487
|
+
// announceStderr may arrive from ANY layer as the enum string or a
|
|
488
|
+
// legacy boolean/word (old saved configs carry true/false) - coerce the
|
|
489
|
+
// merged value once; unknown values fall back to the default ladder
|
|
490
|
+
// position
|
|
491
|
+
cfg.announceStderr = normalizeAnnounce(cfg.announceStderr, "all");
|
|
469
492
|
// URL hygiene: a trailing slash on a server url becomes
|
|
470
493
|
// wss://host//api/ws and https://host//s/<id> downstream - strip once,
|
|
471
494
|
// at the source, so every consumer agrees
|
package/dist/core.js
CHANGED
|
@@ -61,8 +61,11 @@ const DEFAULT_POLICY = {
|
|
|
61
61
|
// Stderr policy: in manual mode (TUI present) stderr is for ACTUAL ERRORS
|
|
62
62
|
// only. Informational lines (live:/url file:/mode banner, lifecycle
|
|
63
63
|
// traces) are suppressed because the TUI surfaces the URL itself.
|
|
64
|
-
// All-mode (CI/server-only) keeps its stderr contract
|
|
65
|
-
//
|
|
64
|
+
// All-mode (CI/server-only) keeps its stderr contract, layered by the
|
|
65
|
+
// announceStderr ladder: "all" = announce + status lines, "status" =
|
|
66
|
+
// status lines only (the share URL never touches stderr), "url" =
|
|
67
|
+
// announce lines only, "off" = neither. Error-class lines always reach
|
|
68
|
+
// stderr.
|
|
66
69
|
let stderrInfoAllowed = true;
|
|
67
70
|
const log = (...a) => {
|
|
68
71
|
if (!stderrInfoAllowed)
|
|
@@ -185,9 +188,19 @@ export function engineTargetUrls() {
|
|
|
185
188
|
export function createEngine(deps) {
|
|
186
189
|
let cfg = loadConfig(deps.pluginDir, deps.options);
|
|
187
190
|
const mode = resolveShareMode(cfg, deps);
|
|
188
|
-
//
|
|
191
|
+
// Stderr = errors only when the TUI part exists (the TUI surfaces URLs)
|
|
189
192
|
if (mode === "manual")
|
|
190
193
|
stderrInfoAllowed = false;
|
|
194
|
+
// The ladder controls two line classes independently: announce lines
|
|
195
|
+
// (live:, url file:) print at all/url; status lines (mode banner,
|
|
196
|
+
// enrollment, adoption notes) print at all/status. "status" is the
|
|
197
|
+
// public-CI shape - the share URL never reaches the log.
|
|
198
|
+
const wantAnnounce = () => cfg.announceStderr === "all" || cfg.announceStderr === "url";
|
|
199
|
+
const logStatus = (...a) => {
|
|
200
|
+
if (cfg.announceStderr !== "all" && cfg.announceStderr !== "status")
|
|
201
|
+
return;
|
|
202
|
+
log(...a);
|
|
203
|
+
};
|
|
191
204
|
if (process.env.OPENSHARE_DEBUG_FILE) {
|
|
192
205
|
// diagnostic ground truth for the suites (never set in production)
|
|
193
206
|
try {
|
|
@@ -203,7 +216,7 @@ export function createEngine(deps) {
|
|
|
203
216
|
}
|
|
204
217
|
const server = resolveServer(cfg);
|
|
205
218
|
if (!server || !server.url) {
|
|
206
|
-
|
|
219
|
+
logStatus("disabled: no share server configured (workerUrl or a servers entry)");
|
|
207
220
|
return {
|
|
208
221
|
mode,
|
|
209
222
|
config: () => cfg,
|
|
@@ -213,7 +226,7 @@ export function createEngine(deps) {
|
|
|
213
226
|
},
|
|
214
227
|
};
|
|
215
228
|
}
|
|
216
|
-
|
|
229
|
+
logStatus(`share mode: ${mode} (directory: ${deps.directory ?? "?"} worktree: ${deps.worktree ?? "?"})`);
|
|
217
230
|
const resolveTargets = () => {
|
|
218
231
|
const merged = {};
|
|
219
232
|
const hasServers = !!cfg.servers && Object.keys(cfg.servers).length > 0;
|
|
@@ -224,7 +237,7 @@ export function createEngine(deps) {
|
|
|
224
237
|
if (cfg.workerUrl && (!hasServers || envWorker))
|
|
225
238
|
merged[cfg.workerUrl] = { token: cfg.createToken || "", active: true };
|
|
226
239
|
if (cfg.workerUrl && hasServers && !envWorker && !cfg.servers[cfg.workerUrl]) {
|
|
227
|
-
|
|
240
|
+
logStatus(`legacy workerUrl ${cfg.workerUrl} ignored: a servers group is configured - remove one of the two`);
|
|
228
241
|
}
|
|
229
242
|
if (cfg.servers)
|
|
230
243
|
for (const [u, e] of Object.entries(cfg.servers))
|
|
@@ -314,12 +327,12 @@ export function createEngine(deps) {
|
|
|
314
327
|
if (adoptModel && doc.modelLabel === undefined) {
|
|
315
328
|
doc.modelLabel = wantModel;
|
|
316
329
|
dirty = true;
|
|
317
|
-
|
|
330
|
+
logStatus(`${url}: adopted modelLabel from the server config set`);
|
|
318
331
|
}
|
|
319
332
|
if (adoptMode && doc.shareMode === undefined) {
|
|
320
333
|
doc.shareMode = wantMode;
|
|
321
334
|
dirty = true;
|
|
322
|
-
|
|
335
|
+
logStatus(`${url}: adopted shareMode from the server config set`);
|
|
323
336
|
}
|
|
324
337
|
if (dirty)
|
|
325
338
|
writeFileSync(home, JSON.stringify(doc, null, 2) + "\n");
|
|
@@ -372,7 +385,7 @@ export function createEngine(deps) {
|
|
|
372
385
|
c.credential = b.token;
|
|
373
386
|
c.error = "";
|
|
374
387
|
const savedTo = saveServerToken(url, c.credential);
|
|
375
|
-
|
|
388
|
+
logStatus(`enrolled at ${url} (user ${b.userId} "${b.label}")${savedTo ? " - token saved" : ""}`);
|
|
376
389
|
void adoptServerSuggestions(url, c.credential);
|
|
377
390
|
return c.credential;
|
|
378
391
|
}
|
|
@@ -540,7 +553,7 @@ export function createEngine(deps) {
|
|
|
540
553
|
const after = targets.map((t) => t.url);
|
|
541
554
|
const added = after.filter((u) => !before.has(u));
|
|
542
555
|
if (added.length)
|
|
543
|
-
|
|
556
|
+
logStatus(`fan-out set grew: ${added.join(", ")}`);
|
|
544
557
|
}
|
|
545
558
|
}
|
|
546
559
|
};
|
|
@@ -765,10 +778,14 @@ export function createEngine(deps) {
|
|
|
765
778
|
const scheduleBindingWrite = (st) => {
|
|
766
779
|
if (st.metaTimer)
|
|
767
780
|
return;
|
|
781
|
+
// env knob exists for the mock suites: their two-remote scenarios
|
|
782
|
+
// assert the SETTLED binding, and waiting out the production 30s
|
|
783
|
+
// coalescing window per scenario would dominate suite time
|
|
784
|
+
const debounce = Number(process.env.OPENSHARE_BINDING_WRITE_MS) > 0 ? Number(process.env.OPENSHARE_BINDING_WRITE_MS) : 30000;
|
|
768
785
|
st.metaTimer = setTimeout(() => {
|
|
769
786
|
st.metaTimer = null;
|
|
770
787
|
chain = chain.then(() => writeBinding(st)).catch(() => { });
|
|
771
|
-
},
|
|
788
|
+
}, debounce);
|
|
772
789
|
};
|
|
773
790
|
// ---- spool (temporary buffer for unacked frames) ----------------------
|
|
774
791
|
// One spool per session per server: `<sid>.<h8(url)>.meta/queue`. The
|
|
@@ -1010,7 +1027,7 @@ export function createEngine(deps) {
|
|
|
1010
1027
|
return;
|
|
1011
1028
|
r.announced = true;
|
|
1012
1029
|
const url = `${r.url}/s/${r.shareId}`;
|
|
1013
|
-
if (
|
|
1030
|
+
if (wantAnnounce())
|
|
1014
1031
|
log(`live: ${url}`);
|
|
1015
1032
|
if (mode === "all" && cfg.urlFile !== URL_FILE_NONE) {
|
|
1016
1033
|
// default: the file lives where the session lives (its directory,
|
|
@@ -1038,7 +1055,7 @@ export function createEngine(deps) {
|
|
|
1038
1055
|
}) + "\n"
|
|
1039
1056
|
: url + "\n";
|
|
1040
1057
|
appendFileSync(file, line);
|
|
1041
|
-
if (
|
|
1058
|
+
if (wantAnnounce() && (file !== lastUrlFile || !urlFileAnnounced)) {
|
|
1042
1059
|
urlFileAnnounced = true;
|
|
1043
1060
|
lastUrlFile = file;
|
|
1044
1061
|
log(`url file: ${file}`);
|
|
@@ -1109,7 +1126,7 @@ export function createEngine(deps) {
|
|
|
1109
1126
|
attach.ttlHours = st.opts.ttlHours;
|
|
1110
1127
|
const sent = wsSend(r, { op: "init", attach });
|
|
1111
1128
|
if (sent)
|
|
1112
|
-
|
|
1129
|
+
logStatus(`opts re-synced to ${url.slice(0, 24)}${st.opts?.ttlHours ? ` (ttl ${st.opts.ttlHours}h)` : ""}`);
|
|
1113
1130
|
}
|
|
1114
1131
|
};
|
|
1115
1132
|
/** Best-effort cleanup of a share we are about to abandon (refused
|
package/dist/settings.js
CHANGED
|
@@ -131,12 +131,12 @@ const LABELS = {
|
|
|
131
131
|
},
|
|
132
132
|
announceStderr: {
|
|
133
133
|
label: "Announce on stderr",
|
|
134
|
-
help: '"
|
|
134
|
+
help: '"all" | "status" | "url" | "off": which [openshare] info lines reach stderr.',
|
|
135
135
|
kind: "text",
|
|
136
|
-
options: ["
|
|
136
|
+
options: ["all", "status", "url", "off"],
|
|
137
137
|
section: "Advanced",
|
|
138
|
-
info: "
|
|
139
|
-
def: "
|
|
138
|
+
info: "The stderr ladder for informational [openshare] lines (all-mode/CI). all: announce lines (live:/url file:) plus status lines (mode banner, enrollment, adoption notes). status: status lines only - the share URL never touches stderr (public CI logs). url: announce lines only (private runners scraping the share URL). off: no info lines. Error-class lines (worker down, refused, timeouts) ALWAYS reach stderr regardless. In manual mode stderr is errors-only by design.",
|
|
139
|
+
def: "all",
|
|
140
140
|
},
|
|
141
141
|
};
|
|
142
142
|
/** [i] pane text for a config key row: long info + live value + source + default. */
|
|
@@ -156,8 +156,6 @@ function helpForKey(key, cfg, sources) {
|
|
|
156
156
|
function displayValue(key, cfg) {
|
|
157
157
|
if (key === "createToken" || key === "authPassword")
|
|
158
158
|
return mask(String(cfg[key] ?? ""));
|
|
159
|
-
if (key === "announceStderr")
|
|
160
|
-
return cfg.announceStderr !== false ? "on" : "off";
|
|
161
159
|
const value = cfg[key];
|
|
162
160
|
return value === undefined || value === "" ? "(not set)" : String(value);
|
|
163
161
|
}
|
|
@@ -183,8 +181,6 @@ async function editKey(host, cfg, key) {
|
|
|
183
181
|
const next = { ...cfg };
|
|
184
182
|
if (key === "shareMode")
|
|
185
183
|
next[key] = picked === "auto" ? undefined : picked;
|
|
186
|
-
else if (key === "announceStderr")
|
|
187
|
-
next[key] = picked === "true";
|
|
188
184
|
else
|
|
189
185
|
next[key] = picked;
|
|
190
186
|
return next;
|
package/dist/tui.js
CHANGED
|
@@ -29,7 +29,7 @@ var DEFAULTS = {
|
|
|
29
29
|
urlFile: "",
|
|
30
30
|
urlFileFormat: "url",
|
|
31
31
|
configSet: "",
|
|
32
|
-
announceStderr:
|
|
32
|
+
announceStderr: "all"
|
|
33
33
|
};
|
|
34
34
|
var CONFIG_NAME = "openshare.config.json";
|
|
35
35
|
var LOCAL_NAME = "openshare.local.json";
|
|
@@ -260,6 +260,22 @@ function deleteServerEntry(url, target) {
|
|
|
260
260
|
return;
|
|
261
261
|
}
|
|
262
262
|
}
|
|
263
|
+
function normalizeAnnounce(value, fallback) {
|
|
264
|
+
if (value === true)
|
|
265
|
+
return "all";
|
|
266
|
+
if (value === false)
|
|
267
|
+
return "off";
|
|
268
|
+
if (typeof value === "string") {
|
|
269
|
+
const v = value.trim().toLowerCase();
|
|
270
|
+
if (v === "all" || v === "status" || v === "url" || v === "off")
|
|
271
|
+
return v;
|
|
272
|
+
if (/^(1|true|yes|on)$/.test(v))
|
|
273
|
+
return "all";
|
|
274
|
+
if (/^(0|false|no)$/.test(v))
|
|
275
|
+
return "off";
|
|
276
|
+
}
|
|
277
|
+
return fallback;
|
|
278
|
+
}
|
|
263
279
|
function loadConfig(pluginDir, options) {
|
|
264
280
|
const cfg = { ...DEFAULTS };
|
|
265
281
|
const emptyMeansUnset = new Set(["workerUrl", "createToken", "urlFile", "spoolDir"]);
|
|
@@ -367,11 +383,12 @@ function loadConfig(pluginDir, options) {
|
|
|
367
383
|
if (env.OPENSHARE_SPOOL_DIR)
|
|
368
384
|
cfg.spoolDir = env.OPENSHARE_SPOOL_DIR;
|
|
369
385
|
if (env.OPENSHARE_ANNOUNCE_STDERR !== undefined)
|
|
370
|
-
cfg.announceStderr =
|
|
386
|
+
cfg.announceStderr = env.OPENSHARE_ANNOUNCE_STDERR;
|
|
371
387
|
if (env.OPENSHARE_SHARE_MODE === "all" || env.OPENSHARE_SHARE_MODE === "manual")
|
|
372
388
|
cfg.shareMode = env.OPENSHARE_SHARE_MODE;
|
|
373
389
|
if (cfg.urlFile.trim().toLowerCase() === URL_FILE_NONE)
|
|
374
390
|
cfg.urlFile = URL_FILE_NONE;
|
|
391
|
+
cfg.announceStderr = normalizeAnnounce(cfg.announceStderr, "all");
|
|
375
392
|
if (cfg.workerUrl)
|
|
376
393
|
cfg.workerUrl = cfg.workerUrl.trim().replace(/\/+$/, "");
|
|
377
394
|
if (cfg.servers) {
|
|
@@ -1247,12 +1264,12 @@ Manual mode (TUI) never writes it \u2014 the link lives in the TUI.`,
|
|
|
1247
1264
|
},
|
|
1248
1265
|
announceStderr: {
|
|
1249
1266
|
label: "Announce on stderr",
|
|
1250
|
-
help: '"
|
|
1267
|
+
help: '"all" | "status" | "url" | "off": which [openshare] info lines reach stderr.',
|
|
1251
1268
|
kind: "text",
|
|
1252
|
-
options: ["
|
|
1269
|
+
options: ["all", "status", "url", "off"],
|
|
1253
1270
|
section: "Advanced",
|
|
1254
|
-
info: "
|
|
1255
|
-
def: "
|
|
1271
|
+
info: "The stderr ladder for informational [openshare] lines (all-mode/CI). all: announce lines (live:/url file:) plus status lines (mode banner, enrollment, adoption notes). status: status lines only - the share URL never touches stderr (public CI logs). url: announce lines only (private runners scraping the share URL). off: no info lines. Error-class lines (worker down, refused, timeouts) ALWAYS reach stderr regardless. In manual mode stderr is errors-only by design.",
|
|
1272
|
+
def: "all"
|
|
1256
1273
|
}
|
|
1257
1274
|
};
|
|
1258
1275
|
function helpForKey(key, cfg, sources) {
|
|
@@ -1272,8 +1289,6 @@ function helpForKey(key, cfg, sources) {
|
|
|
1272
1289
|
function displayValue(key, cfg) {
|
|
1273
1290
|
if (key === "createToken" || key === "authPassword")
|
|
1274
1291
|
return mask(String(cfg[key] ?? ""));
|
|
1275
|
-
if (key === "announceStderr")
|
|
1276
|
-
return cfg.announceStderr !== false ? "on" : "off";
|
|
1277
1292
|
const value = cfg[key];
|
|
1278
1293
|
return value === undefined || value === "" ? "(not set)" : String(value);
|
|
1279
1294
|
}
|
|
@@ -1297,8 +1312,6 @@ async function editKey(host, cfg, key) {
|
|
|
1297
1312
|
const next = { ...cfg };
|
|
1298
1313
|
if (key === "shareMode")
|
|
1299
1314
|
next[key] = picked === "auto" ? undefined : picked;
|
|
1300
|
-
else if (key === "announceStderr")
|
|
1301
|
-
next[key] = picked === "true";
|
|
1302
1315
|
else
|
|
1303
1316
|
next[key] = picked;
|
|
1304
1317
|
return next;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@mirrowel/opencode-openshare",
|
|
3
|
-
"version": "0.3.0-dev.
|
|
3
|
+
"version": "0.3.0-dev.15",
|
|
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",
|
package/src/banner.txt
CHANGED
|
@@ -21,5 +21,7 @@
|
|
|
21
21
|
// shareMode ("all" | "manual"; default = TUI presence), debounceMs,
|
|
22
22
|
// completedDelayMs, modelLabel, ttlDays,
|
|
23
23
|
// authLogin, authPassword, spoolDir, urlFile ("none" = off),
|
|
24
|
-
// announceStderr
|
|
24
|
+
// announceStderr ("all" | "status" | "url" | "off"; status keeps the
|
|
25
|
+
// status/banner lines and never prints the share URL - the public-CI
|
|
26
|
+
// shape; url prints announce lines only).
|
|
25
27
|
//
|
package/src/config.ts
CHANGED
|
@@ -62,9 +62,14 @@ export type ShareConfig = {
|
|
|
62
62
|
* "json" (one JSON object per line: id, url, status, gated) for CI
|
|
63
63
|
* consumers that want machine-readable lines without parsing URLs. */
|
|
64
64
|
urlFileFormat: "url" | "json"
|
|
65
|
-
/**
|
|
66
|
-
* lines
|
|
67
|
-
|
|
65
|
+
/** Stderr ladder with two independently controllable line classes:
|
|
66
|
+
* announce lines (`live:`, `url file:`) and status lines (mode banner,
|
|
67
|
+
* enrollment, adoption notes). "all" (default) prints both; "status"
|
|
68
|
+
* prints status lines only - the share URL never touches stderr
|
|
69
|
+
* (public CI logs); "url" prints announce lines only (private runners
|
|
70
|
+
* scraping the URL); "off" prints neither. Error-class lines ALWAYS
|
|
71
|
+
* reach stderr. */
|
|
72
|
+
announceStderr: "all" | "status" | "url" | "off"
|
|
68
73
|
/** Wave 3 redaction: user-supplied regex rules (client-side only)
|
|
69
74
|
* applied beside the built-in detectors on UNGATED shares. */
|
|
70
75
|
redact?: { patterns?: string[] }
|
|
@@ -103,7 +108,7 @@ export const DEFAULTS: ShareConfig = {
|
|
|
103
108
|
urlFile: "",
|
|
104
109
|
urlFileFormat: "url",
|
|
105
110
|
configSet: "",
|
|
106
|
-
announceStderr:
|
|
111
|
+
announceStderr: "all",
|
|
107
112
|
}
|
|
108
113
|
|
|
109
114
|
const CONFIG_NAME = "openshare.config.json"
|
|
@@ -369,6 +374,20 @@ export function deleteServerEntry(url: string, target?: string): string | undefi
|
|
|
369
374
|
}
|
|
370
375
|
}
|
|
371
376
|
|
|
377
|
+
/** Coerce an announceStderr value from any layer (enum string, legacy
|
|
378
|
+
* boolean, truthy/falsy word) to the 4-position ladder. */
|
|
379
|
+
function normalizeAnnounce(value: unknown, fallback: "all" | "status" | "url" | "off"): "all" | "status" | "url" | "off" {
|
|
380
|
+
if (value === true) return "all"
|
|
381
|
+
if (value === false) return "off"
|
|
382
|
+
if (typeof value === "string") {
|
|
383
|
+
const v = value.trim().toLowerCase()
|
|
384
|
+
if (v === "all" || v === "status" || v === "url" || v === "off") return v
|
|
385
|
+
if (/^(1|true|yes|on)$/.test(v)) return "all"
|
|
386
|
+
if (/^(0|false|no)$/.test(v)) return "off"
|
|
387
|
+
}
|
|
388
|
+
return fallback
|
|
389
|
+
}
|
|
390
|
+
|
|
372
391
|
/** Load the effective config. `options` is the plugin-array options tuple
|
|
373
392
|
* object (env still wins over it). Files MERGE lowest→highest priority
|
|
374
393
|
* (later files override per key); empty strings in credential/path keys
|
|
@@ -473,12 +492,18 @@ export function loadConfig(pluginDir: string, options?: Record<string, unknown>)
|
|
|
473
492
|
if (env.OPENSHARE_URL_FILE_FORMAT === "json" || env.OPENSHARE_URL_FILE_FORMAT === "url") cfg.urlFileFormat = env.OPENSHARE_URL_FILE_FORMAT
|
|
474
493
|
if (env.OPENSHARE_CONFIG_SET) cfg.configSet = env.OPENSHARE_CONFIG_SET
|
|
475
494
|
if (env.OPENSHARE_SPOOL_DIR) cfg.spoolDir = env.OPENSHARE_SPOOL_DIR
|
|
476
|
-
if (env.OPENSHARE_ANNOUNCE_STDERR !== undefined) cfg.announceStderr =
|
|
495
|
+
if (env.OPENSHARE_ANNOUNCE_STDERR !== undefined) cfg.announceStderr = env.OPENSHARE_ANNOUNCE_STDERR as ShareConfig["announceStderr"]
|
|
477
496
|
if (env.OPENSHARE_SHARE_MODE === "all" || env.OPENSHARE_SHARE_MODE === "manual") cfg.shareMode = env.OPENSHARE_SHARE_MODE
|
|
478
497
|
|
|
479
498
|
// normalize the disable sentinel (case-insensitive from any source)
|
|
480
499
|
if (cfg.urlFile.trim().toLowerCase() === URL_FILE_NONE) cfg.urlFile = URL_FILE_NONE
|
|
481
500
|
|
|
501
|
+
// announceStderr may arrive from ANY layer as the enum string or a
|
|
502
|
+
// legacy boolean/word (old saved configs carry true/false) - coerce the
|
|
503
|
+
// merged value once; unknown values fall back to the default ladder
|
|
504
|
+
// position
|
|
505
|
+
cfg.announceStderr = normalizeAnnounce(cfg.announceStderr, "all")
|
|
506
|
+
|
|
482
507
|
// URL hygiene: a trailing slash on a server url becomes
|
|
483
508
|
// wss://host//api/ws and https://host//s/<id> downstream - strip once,
|
|
484
509
|
// at the source, so every consumer agrees
|
package/src/core.ts
CHANGED
|
@@ -233,8 +233,11 @@ export type Engine = {
|
|
|
233
233
|
// Stderr policy: in manual mode (TUI present) stderr is for ACTUAL ERRORS
|
|
234
234
|
// only. Informational lines (live:/url file:/mode banner, lifecycle
|
|
235
235
|
// traces) are suppressed because the TUI surfaces the URL itself.
|
|
236
|
-
// All-mode (CI/server-only) keeps its stderr contract
|
|
237
|
-
//
|
|
236
|
+
// All-mode (CI/server-only) keeps its stderr contract, layered by the
|
|
237
|
+
// announceStderr ladder: "all" = announce + status lines, "status" =
|
|
238
|
+
// status lines only (the share URL never touches stderr), "url" =
|
|
239
|
+
// announce lines only, "off" = neither. Error-class lines always reach
|
|
240
|
+
// stderr.
|
|
238
241
|
let stderrInfoAllowed = true
|
|
239
242
|
|
|
240
243
|
const log = (...a: unknown[]) => {
|
|
@@ -351,8 +354,17 @@ export function engineTargetUrls(): string[] {
|
|
|
351
354
|
export function createEngine(deps: EngineDeps): Engine {
|
|
352
355
|
let cfg = loadConfig(deps.pluginDir, deps.options)
|
|
353
356
|
const mode: ShareMode = resolveShareMode(cfg, deps)
|
|
354
|
-
//
|
|
357
|
+
// Stderr = errors only when the TUI part exists (the TUI surfaces URLs)
|
|
355
358
|
if (mode === "manual") stderrInfoAllowed = false
|
|
359
|
+
// The ladder controls two line classes independently: announce lines
|
|
360
|
+
// (live:, url file:) print at all/url; status lines (mode banner,
|
|
361
|
+
// enrollment, adoption notes) print at all/status. "status" is the
|
|
362
|
+
// public-CI shape - the share URL never reaches the log.
|
|
363
|
+
const wantAnnounce = () => cfg.announceStderr === "all" || cfg.announceStderr === "url"
|
|
364
|
+
const logStatus = (...a: unknown[]) => {
|
|
365
|
+
if (cfg.announceStderr !== "all" && cfg.announceStderr !== "status") return
|
|
366
|
+
log(...a)
|
|
367
|
+
}
|
|
356
368
|
if (process.env.OPENSHARE_DEBUG_FILE) {
|
|
357
369
|
// diagnostic ground truth for the suites (never set in production)
|
|
358
370
|
try {
|
|
@@ -374,7 +386,7 @@ export function createEngine(deps: EngineDeps): Engine {
|
|
|
374
386
|
}
|
|
375
387
|
const server = resolveServer(cfg)
|
|
376
388
|
if (!server || !server.url) {
|
|
377
|
-
|
|
389
|
+
logStatus("disabled: no share server configured (workerUrl or a servers entry)")
|
|
378
390
|
return {
|
|
379
391
|
mode,
|
|
380
392
|
config: () => cfg,
|
|
@@ -384,7 +396,7 @@ export function createEngine(deps: EngineDeps): Engine {
|
|
|
384
396
|
},
|
|
385
397
|
}
|
|
386
398
|
}
|
|
387
|
-
|
|
399
|
+
logStatus(`share mode: ${mode} (directory: ${deps.directory ?? "?"} worktree: ${deps.worktree ?? "?"})`)
|
|
388
400
|
|
|
389
401
|
// ---- share servers (the fan-out set) -----------------------------------
|
|
390
402
|
// Every entry flagged active:true shares new sessions; when none is
|
|
@@ -401,7 +413,7 @@ export function createEngine(deps: EngineDeps): Engine {
|
|
|
401
413
|
const envWorker = !!process.env.OPENSHARE_WORKER_URL
|
|
402
414
|
if (cfg.workerUrl && (!hasServers || envWorker)) merged[cfg.workerUrl] = { token: cfg.createToken || "", active: true }
|
|
403
415
|
if (cfg.workerUrl && hasServers && !envWorker && !cfg.servers![cfg.workerUrl]) {
|
|
404
|
-
|
|
416
|
+
logStatus(`legacy workerUrl ${cfg.workerUrl} ignored: a servers group is configured - remove one of the two`)
|
|
405
417
|
}
|
|
406
418
|
if (cfg.servers) for (const [u, e] of Object.entries(cfg.servers)) merged[u] = { ...merged[u], ...e }
|
|
407
419
|
const urls = Object.keys(merged)
|
|
@@ -480,8 +492,8 @@ export function createEngine(deps: EngineDeps): Engine {
|
|
|
480
492
|
if (!doc || typeof doc !== "object") doc = {}
|
|
481
493
|
} catch {}
|
|
482
494
|
let dirty = false
|
|
483
|
-
if (adoptModel && doc.modelLabel === undefined) { doc.modelLabel = wantModel; dirty = true;
|
|
484
|
-
if (adoptMode && doc.shareMode === undefined) { doc.shareMode = wantMode; dirty = true;
|
|
495
|
+
if (adoptModel && doc.modelLabel === undefined) { doc.modelLabel = wantModel; dirty = true; logStatus(`${url}: adopted modelLabel from the server config set`) }
|
|
496
|
+
if (adoptMode && doc.shareMode === undefined) { doc.shareMode = wantMode; dirty = true; logStatus(`${url}: adopted shareMode from the server config set`) }
|
|
485
497
|
if (dirty) writeFileSync(home, JSON.stringify(doc, null, 2) + "\n")
|
|
486
498
|
} catch {
|
|
487
499
|
// suggestions are best-effort; a failed fetch never blocks sharing
|
|
@@ -530,7 +542,7 @@ export function createEngine(deps: EngineDeps): Engine {
|
|
|
530
542
|
c.credential = b.token
|
|
531
543
|
c.error = ""
|
|
532
544
|
const savedTo = saveServerToken(url, c.credential)
|
|
533
|
-
|
|
545
|
+
logStatus(`enrolled at ${url} (user ${b.userId} "${b.label}")${savedTo ? " - token saved" : ""}`)
|
|
534
546
|
void adoptServerSuggestions(url, c.credential)
|
|
535
547
|
return c.credential
|
|
536
548
|
}
|
|
@@ -700,7 +712,7 @@ export function createEngine(deps: EngineDeps): Engine {
|
|
|
700
712
|
if (debugEvents) {
|
|
701
713
|
const after = targets.map((t) => t.url)
|
|
702
714
|
const added = after.filter((u) => !before.has(u))
|
|
703
|
-
if (added.length)
|
|
715
|
+
if (added.length) logStatus(`fan-out set grew: ${added.join(", ")}`)
|
|
704
716
|
}
|
|
705
717
|
}
|
|
706
718
|
}
|
|
@@ -913,10 +925,14 @@ export function createEngine(deps: EngineDeps): Engine {
|
|
|
913
925
|
|
|
914
926
|
const scheduleBindingWrite = (st: SessionState) => {
|
|
915
927
|
if (st.metaTimer) return
|
|
928
|
+
// env knob exists for the mock suites: their two-remote scenarios
|
|
929
|
+
// assert the SETTLED binding, and waiting out the production 30s
|
|
930
|
+
// coalescing window per scenario would dominate suite time
|
|
931
|
+
const debounce = Number(process.env.OPENSHARE_BINDING_WRITE_MS) > 0 ? Number(process.env.OPENSHARE_BINDING_WRITE_MS) : 30000
|
|
916
932
|
st.metaTimer = setTimeout(() => {
|
|
917
933
|
st.metaTimer = null
|
|
918
934
|
chain = chain.then(() => writeBinding(st)).catch(() => {})
|
|
919
|
-
},
|
|
935
|
+
}, debounce)
|
|
920
936
|
}
|
|
921
937
|
|
|
922
938
|
// ---- spool (temporary buffer for unacked frames) ----------------------
|
|
@@ -1142,7 +1158,7 @@ export function createEngine(deps: EngineDeps): Engine {
|
|
|
1142
1158
|
if (r.announced) return
|
|
1143
1159
|
r.announced = true
|
|
1144
1160
|
const url = `${r.url}/s/${r.shareId}`
|
|
1145
|
-
if (
|
|
1161
|
+
if (wantAnnounce()) log(`live: ${url}`)
|
|
1146
1162
|
if (mode === "all" && cfg.urlFile !== URL_FILE_NONE) {
|
|
1147
1163
|
// default: the file lives where the session lives (its directory,
|
|
1148
1164
|
// falling back to the process cwd) — never a fixed global path.
|
|
@@ -1170,7 +1186,7 @@ export function createEngine(deps: EngineDeps): Engine {
|
|
|
1170
1186
|
}) + "\n"
|
|
1171
1187
|
: url + "\n"
|
|
1172
1188
|
appendFileSync(file, line)
|
|
1173
|
-
if (
|
|
1189
|
+
if (wantAnnounce() && (file !== lastUrlFile || !urlFileAnnounced)) {
|
|
1174
1190
|
urlFileAnnounced = true
|
|
1175
1191
|
lastUrlFile = file
|
|
1176
1192
|
log(`url file: ${file}`)
|
|
@@ -1237,7 +1253,7 @@ export function createEngine(deps: EngineDeps): Engine {
|
|
|
1237
1253
|
const attach: Record<string, unknown> = { id: r.shareId, secret: r.secret, ...authFrame(st) }
|
|
1238
1254
|
if (st.opts?.ttlHours) attach.ttlHours = st.opts.ttlHours
|
|
1239
1255
|
const sent = wsSend(r, { op: "init", attach })
|
|
1240
|
-
if (sent)
|
|
1256
|
+
if (sent) logStatus(`opts re-synced to ${url.slice(0, 24)}${st.opts?.ttlHours ? ` (ttl ${st.opts.ttlHours}h)` : ""}`)
|
|
1241
1257
|
}
|
|
1242
1258
|
}
|
|
1243
1259
|
|
package/src/settings.ts
CHANGED
|
@@ -191,12 +191,12 @@ const LABELS: Record<string, LabelMeta> = {
|
|
|
191
191
|
},
|
|
192
192
|
announceStderr: {
|
|
193
193
|
label: "Announce on stderr",
|
|
194
|
-
help: '"
|
|
194
|
+
help: '"all" | "status" | "url" | "off": which [openshare] info lines reach stderr.',
|
|
195
195
|
kind: "text",
|
|
196
|
-
options: ["
|
|
196
|
+
options: ["all", "status", "url", "off"],
|
|
197
197
|
section: "Advanced",
|
|
198
|
-
info: "
|
|
199
|
-
def: "
|
|
198
|
+
info: "The stderr ladder for informational [openshare] lines (all-mode/CI). all: announce lines (live:/url file:) plus status lines (mode banner, enrollment, adoption notes). status: status lines only - the share URL never touches stderr (public CI logs). url: announce lines only (private runners scraping the share URL). off: no info lines. Error-class lines (worker down, refused, timeouts) ALWAYS reach stderr regardless. In manual mode stderr is errors-only by design.",
|
|
199
|
+
def: "all",
|
|
200
200
|
},
|
|
201
201
|
}
|
|
202
202
|
|
|
@@ -214,7 +214,6 @@ function helpForKey(key: string, cfg: ShareConfig, sources: Record<string, strin
|
|
|
214
214
|
|
|
215
215
|
function displayValue(key: string, cfg: ShareConfig): string {
|
|
216
216
|
if (key === "createToken" || key === "authPassword") return mask(String(cfg[key] ?? ""))
|
|
217
|
-
if (key === "announceStderr") return cfg.announceStderr !== false ? "on" : "off"
|
|
218
217
|
const value = (cfg as unknown as Record<string, unknown>)[key]
|
|
219
218
|
return value === undefined || value === "" ? "(not set)" : String(value)
|
|
220
219
|
}
|
|
@@ -238,7 +237,6 @@ async function editKey(host: ShareHostApi, cfg: ShareConfig, key: string): Promi
|
|
|
238
237
|
if (picked === undefined) return undefined
|
|
239
238
|
const next = { ...cfg } as unknown as Record<string, unknown>
|
|
240
239
|
if (key === "shareMode") next[key] = picked === "auto" ? undefined : picked
|
|
241
|
-
else if (key === "announceStderr") next[key] = picked === "true"
|
|
242
240
|
else next[key] = picked
|
|
243
241
|
return next as ShareConfig
|
|
244
242
|
}
|