@mirrowel/opencode-openshare 0.2.1 → 0.3.0-dev.2
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 +46 -26
- package/ci/README.md +30 -14
- package/ci/openshare.js +461 -61
- package/dist/binding-queue.js +2 -2
- package/dist/commands.js +8 -8
- package/dist/config.d.ts +34 -14
- package/dist/config.js +108 -33
- package/dist/contract.d.ts +5 -5
- package/dist/contract.js +5 -5
- package/dist/core.d.ts +12 -1
- package/dist/core.js +388 -138
- package/dist/dialogs.d.ts +9 -10
- package/dist/dialogs.js +25 -28
- package/dist/redact.d.ts +46 -0
- package/dist/redact.js +130 -0
- package/dist/settings.d.ts +10 -7
- package/dist/settings.js +136 -54
- package/dist/share-flow.d.ts +30 -6
- package/dist/share-flow.js +73 -18
- package/dist/tui.js +251 -59
- package/dist/ui-prefs.js +6 -6
- package/dist/v2-server.js +5 -6
- package/package.json +1 -1
- package/src/binding-queue.ts +2 -2
- package/src/commands.ts +8 -8
- package/src/config.ts +112 -29
- package/src/contract.ts +5 -5
- package/src/core.ts +364 -137
- package/src/dialogs.tsx +29 -32
- package/src/redact.ts +134 -0
- package/src/settings.ts +132 -52
- package/src/share-flow.ts +76 -20
- package/src/sidebar.tsx +7 -7
- package/src/tui.tsx +22 -24
- package/src/ui-prefs.ts +6 -6
- package/src/v2-server.ts +5 -6
package/dist/dialogs.d.ts
CHANGED
|
@@ -11,11 +11,11 @@ export declare function setDialogHeightPercent(api: TuiPluginApi, value: number)
|
|
|
11
11
|
* accounted); the estimator and the pre-wrapper both use this so heights
|
|
12
12
|
* match what actually renders. */
|
|
13
13
|
export declare function dialogContentWidth(api: TuiPluginApi): number;
|
|
14
|
-
/** Deterministic character-level pre-wrap
|
|
15
|
-
*
|
|
16
|
-
*
|
|
17
|
-
*
|
|
18
|
-
*
|
|
14
|
+
/** Deterministic character-level pre-wrap: opentui's word wrap cannot
|
|
15
|
+
* break unbreakable tokens like a JSON policy blob, so "wrapped" text still
|
|
16
|
+
* escapes the dialog border. Both the [i] pane and InfoDialog render THESE
|
|
17
|
+
* lines; the row estimate uses the same output, so heights match what
|
|
18
|
+
* renders. */
|
|
19
19
|
export declare function preWrap(text: string, width: number): string[];
|
|
20
20
|
export type MenuOption<Value = string> = {
|
|
21
21
|
title: string;
|
|
@@ -41,11 +41,10 @@ export declare function MenuDialog<Value>(props: {
|
|
|
41
41
|
hint?: string;
|
|
42
42
|
onDone: (value: Value | undefined) => void;
|
|
43
43
|
}): JSX.Element;
|
|
44
|
-
/** showMenuOnce, family discipline
|
|
45
|
-
*
|
|
46
|
-
*
|
|
47
|
-
*
|
|
48
|
-
* settings exit). */
|
|
44
|
+
/** showMenuOnce, family discipline: done() does NOT clear the host stack,
|
|
45
|
+
* because the next dialog's replace() is an atomic swap and clearing first
|
|
46
|
+
* paints one empty frame (the esc-to-parent flicker). Chain ends clear
|
|
47
|
+
* explicitly (command exits, settings exit). */
|
|
49
48
|
export declare function showMenuOnce<Value>(api: TuiPluginApi, props: {
|
|
50
49
|
title: string;
|
|
51
50
|
options: MenuOption<Value>[];
|
package/dist/dialogs.js
CHANGED
|
@@ -142,11 +142,11 @@ function cappedHeight(count, max, min = 1) {
|
|
|
142
142
|
function estimatedVisualRows(message, width) {
|
|
143
143
|
return message.split(/\r?\n/).reduce((rows, line) => rows + Math.max(1, Math.ceil(line.length / Math.max(1, width))), 0);
|
|
144
144
|
}
|
|
145
|
-
/** Deterministic character-level pre-wrap
|
|
146
|
-
*
|
|
147
|
-
*
|
|
148
|
-
*
|
|
149
|
-
*
|
|
145
|
+
/** Deterministic character-level pre-wrap: opentui's word wrap cannot
|
|
146
|
+
* break unbreakable tokens like a JSON policy blob, so "wrapped" text still
|
|
147
|
+
* escapes the dialog border. Both the [i] pane and InfoDialog render THESE
|
|
148
|
+
* lines; the row estimate uses the same output, so heights match what
|
|
149
|
+
* renders. */
|
|
150
150
|
export function preWrap(text, width) {
|
|
151
151
|
const out = [];
|
|
152
152
|
for (const raw of text.split(/\r?\n/)) {
|
|
@@ -200,10 +200,9 @@ function InfoLines(props) {
|
|
|
200
200
|
return line.length === 0 ? (_jsx("text", { children: " " })) : (_jsx("text", { fg: colorOf(attrs.fg), wrapMode: "none", children: attrs.bold ? _jsx("b", { children: line }) : line }));
|
|
201
201
|
} }) }));
|
|
202
202
|
}
|
|
203
|
-
/**
|
|
204
|
-
*
|
|
205
|
-
*
|
|
206
|
-
* Capped + ellipsized. */
|
|
203
|
+
/** Title column cap (family menuTitleWidth): a server URL as a row title
|
|
204
|
+
* would otherwise stretch the column toward half the dialog. Full URLs
|
|
205
|
+
* belong in the [i] pane, not the row. Capped and ellipsized. */
|
|
207
206
|
function menuTitleWidth(width, options) {
|
|
208
207
|
const longest = Math.max(0, ...options.map((option) => option.title.length)) + 2;
|
|
209
208
|
if (width === "xlarge")
|
|
@@ -217,13 +216,12 @@ function ellipsize(text, width) {
|
|
|
217
216
|
}
|
|
218
217
|
/** OpenShare visual frame: rounded accent border + accent title row. The
|
|
219
218
|
* logic inside the menus stays the ported family pattern; this wrapper is
|
|
220
|
-
* purely presentation
|
|
221
|
-
*
|
|
222
|
-
// Frame note
|
|
223
|
-
//
|
|
224
|
-
//
|
|
225
|
-
//
|
|
226
|
-
// identity kept.
|
|
219
|
+
* purely presentation. Natural width: the host dialog panel provides the
|
|
220
|
+
* container and the border hugs the content. */
|
|
221
|
+
// Frame note: the HOST dialog panel renders plugin content with
|
|
222
|
+
// paddingTop=1 (opencode dialog.tsx), so a BORDERED frame always shows a
|
|
223
|
+
// dead panel strip above its top border. Borderless family shape plus a
|
|
224
|
+
// solid accent rule under the title: zero dead space, accent identity kept.
|
|
227
225
|
function DialogFrame(props) {
|
|
228
226
|
const theme = () => props.api.theme.current;
|
|
229
227
|
return (_jsxs("box", { flexDirection: "column", width: "100%", paddingLeft: 2, paddingRight: 2, children: [_jsx("box", { width: "100%", marginBottom: 1, children: _jsx("text", { fg: theme().accent, children: _jsx("b", { children: props.title }) }) }), _jsx("box", { width: "100%", height: 1, backgroundColor: theme().accent, marginBottom: 1 }), props.children] }));
|
|
@@ -243,8 +241,8 @@ export function MenuDialog(props) {
|
|
|
243
241
|
const dimensions = useTerminalDimensions();
|
|
244
242
|
const listHeight = createMemo(() => cappedHeight(props.options.length, maxRows(props.api, dimensions().height, 12, 4), 4));
|
|
245
243
|
// fixed title column shared by every row (family menuTitleWidth): flex
|
|
246
|
-
// columns stagger per
|
|
247
|
-
// width is what makes the value/description column align
|
|
244
|
+
// columns stagger per row because flex splits by content, so a computed
|
|
245
|
+
// width is what makes the value/description column align.
|
|
248
246
|
const width = dialogWidthOf(props.api);
|
|
249
247
|
const titleWidth = createMemo(() => menuTitleWidth(width, props.options));
|
|
250
248
|
const hasHelp = createMemo(() => props.options.some((option) => option.help));
|
|
@@ -272,8 +270,8 @@ export function MenuDialog(props) {
|
|
|
272
270
|
});
|
|
273
271
|
// [i] pane sections: "## Header" lines in help split sections; the pane
|
|
274
272
|
// height comes from the CONTENT (pre-wrapped rows), capped by the same
|
|
275
|
-
// dialog metrics
|
|
276
|
-
//
|
|
273
|
+
// dialog metrics rather than the spawning menu's list height (which gave
|
|
274
|
+
// a 3-row menu a 3-row info pane).
|
|
277
275
|
const helpSections = createMemo(() => {
|
|
278
276
|
const raw = current()?.help ?? "";
|
|
279
277
|
const sections = [{ title: "", lines: [] }];
|
|
@@ -465,11 +463,10 @@ function claimDialog(api) {
|
|
|
465
463
|
pendingDialog = release;
|
|
466
464
|
return release;
|
|
467
465
|
}
|
|
468
|
-
/** showMenuOnce, family discipline
|
|
469
|
-
*
|
|
470
|
-
*
|
|
471
|
-
*
|
|
472
|
-
* settings exit). */
|
|
466
|
+
/** showMenuOnce, family discipline: done() does NOT clear the host stack,
|
|
467
|
+
* because the next dialog's replace() is an atomic swap and clearing first
|
|
468
|
+
* paints one empty frame (the esc-to-parent flicker). Chain ends clear
|
|
469
|
+
* explicitly (command exits, settings exit). */
|
|
473
470
|
export function showMenuOnce(api, props) {
|
|
474
471
|
return new Promise((resolve) => {
|
|
475
472
|
let settled = false;
|
|
@@ -579,9 +576,9 @@ export function showAlertMenu(api, props) {
|
|
|
579
576
|
return showInfoDialog(api, { title: props.title, message: props.message }).then(() => undefined);
|
|
580
577
|
}
|
|
581
578
|
// --- prompt (text entry — the one custom piece) -------------------------------
|
|
582
|
-
// Full printable ASCII
|
|
583
|
-
//
|
|
584
|
-
//
|
|
579
|
+
// Full printable ASCII: a lowercase-only set makes uppercase
|
|
580
|
+
// CREATE_TOKENs and codes UNTYPEABLE. Space is bound separately below;
|
|
581
|
+
// escape is the back binding, so it is excluded.
|
|
585
582
|
const PROMPT_CHARS = [
|
|
586
583
|
..."abcdefghijklmnopqrstuvwxyz",
|
|
587
584
|
..."ABCDEFGHIJKLMNOPQRSTUVWXYZ",
|
package/dist/redact.d.ts
ADDED
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Client-only redaction + light path transform (Wave 3 trust).
|
|
3
|
+
*
|
|
4
|
+
* Owner verdict: the server stays dumb - the client controls what leaves
|
|
5
|
+
* the machine. This pass runs once at enqueue (the single choke point
|
|
6
|
+
* every item crosses before any transport) on UNGATED shares only: a
|
|
7
|
+
* share with login+pass is private from casual observers already, and
|
|
8
|
+
* redacting there would corrupt content the server owner can see anyway
|
|
9
|
+
* (documented caveat: NOT private from the server owner until Wave 3b
|
|
10
|
+
* encryption lands).
|
|
11
|
+
*
|
|
12
|
+
* Extending: add a detector to BUILTINS (fork-friendly registry) or set
|
|
13
|
+
* `redact.patterns` in the config (regex source strings, global flag).
|
|
14
|
+
*/
|
|
15
|
+
/** A detector: stable `kind` names the marker, e.g. `[redacted:aws-key]`. */
|
|
16
|
+
export type RedactRule = {
|
|
17
|
+
kind: string;
|
|
18
|
+
re: RegExp;
|
|
19
|
+
};
|
|
20
|
+
export declare const BUILTINS: RedactRule[];
|
|
21
|
+
export type RedactionHit = {
|
|
22
|
+
kind: string;
|
|
23
|
+
sample: string;
|
|
24
|
+
};
|
|
25
|
+
/** Home-directory prefix -> `~`; applied consistently to session.directory
|
|
26
|
+
* AND tool path fields so viewer-side relativization still works (both
|
|
27
|
+
* ends share the ~-prefixed form). */
|
|
28
|
+
export declare function tildePath(p: string, home: string): string;
|
|
29
|
+
type Opts = {
|
|
30
|
+
rules: RedactRule[];
|
|
31
|
+
home: string;
|
|
32
|
+
enabled: boolean;
|
|
33
|
+
};
|
|
34
|
+
/**
|
|
35
|
+
* Apply redaction (when enabled) + the light path transform to one
|
|
36
|
+
* contract item. Returns the ORIGINAL untouched when nothing matched
|
|
37
|
+
* (zero-copy fast path for clean sessions); otherwise a mutated deep
|
|
38
|
+
* clone - the host's live object is never touched.
|
|
39
|
+
*/
|
|
40
|
+
export declare function redactItem(item: unknown, opts: Opts): {
|
|
41
|
+
item: unknown;
|
|
42
|
+
hits: RedactionHit[];
|
|
43
|
+
};
|
|
44
|
+
/** All active rules (built-ins + compiled user patterns). */
|
|
45
|
+
export declare function activeRules(userPatterns: unknown): RedactRule[];
|
|
46
|
+
export {};
|
package/dist/redact.js
ADDED
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Client-only redaction + light path transform (Wave 3 trust).
|
|
3
|
+
*
|
|
4
|
+
* Owner verdict: the server stays dumb - the client controls what leaves
|
|
5
|
+
* the machine. This pass runs once at enqueue (the single choke point
|
|
6
|
+
* every item crosses before any transport) on UNGATED shares only: a
|
|
7
|
+
* share with login+pass is private from casual observers already, and
|
|
8
|
+
* redacting there would corrupt content the server owner can see anyway
|
|
9
|
+
* (documented caveat: NOT private from the server owner until Wave 3b
|
|
10
|
+
* encryption lands).
|
|
11
|
+
*
|
|
12
|
+
* Extending: add a detector to BUILTINS (fork-friendly registry) or set
|
|
13
|
+
* `redact.patterns` in the config (regex source strings, global flag).
|
|
14
|
+
*/
|
|
15
|
+
export const BUILTINS = [
|
|
16
|
+
{ kind: "aws-key", re: /\bAKIA[0-9A-Z]{16}\b/g },
|
|
17
|
+
{ kind: "gh-token", re: /\bgh[pousr]_[A-Za-z0-9]{36,255}\b/g },
|
|
18
|
+
{ kind: "gh-token", re: /\bgithub_pat_[A-Za-z0-9_]{22,255}\b/g },
|
|
19
|
+
{ kind: "api-key", re: /\bsk-[A-Za-z0-9_-]{20,}\b/g },
|
|
20
|
+
{ kind: "slack-token", re: /\bxox[baprs]-[A-Za-z0-9-]{10,}\b/g },
|
|
21
|
+
{ kind: "google-key", re: /\bAIza[0-9A-Za-z_-]{35}\b/g },
|
|
22
|
+
{ kind: "bearer", re: /\bBearer\s+[A-Za-z0-9._~+/=-]{20,}/gi },
|
|
23
|
+
// PEM private-key blocks (whole block)
|
|
24
|
+
{ kind: "private-key", re: /-----BEGIN [A-Z ]*PRIVATE KEY-----[\s\S]*?-----END [A-Z ]*PRIVATE KEY-----/g },
|
|
25
|
+
// .env style assignments for secret-shaped names
|
|
26
|
+
{ kind: "env-secret", re: /(?<![A-Za-z0-9_])(?:export\s+)?[A-Z0-9_]*(?:API_?KEY|TOKEN|SECRET|PASSWORD|PASSWD|PASS|PWD|CREDENTIALS?)[A-Z0-9_]*\s*=\s*\S+/g },
|
|
27
|
+
// URL connection strings with embedded passwords (password masked)
|
|
28
|
+
{ kind: "conn-string", re: /\b[a-z][a-z0-9+.-]*:\/\/[^:@/\s"']+:[^@/\s"']{4,}@/gi },
|
|
29
|
+
];
|
|
30
|
+
/** Config-supplied rules (regex source strings, applied globally). */
|
|
31
|
+
function compile(patterns) {
|
|
32
|
+
if (!Array.isArray(patterns))
|
|
33
|
+
return [];
|
|
34
|
+
const out = [];
|
|
35
|
+
for (const p of patterns) {
|
|
36
|
+
if (typeof p !== "string" || !p)
|
|
37
|
+
continue;
|
|
38
|
+
try {
|
|
39
|
+
out.push({ kind: "user", re: new RegExp(p, "g") });
|
|
40
|
+
}
|
|
41
|
+
catch {
|
|
42
|
+
// a broken user regex must never kill sharing - skip it
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
return out;
|
|
46
|
+
}
|
|
47
|
+
/** Home-directory prefix -> `~`; applied consistently to session.directory
|
|
48
|
+
* AND tool path fields so viewer-side relativization still works (both
|
|
49
|
+
* ends share the ~-prefixed form). */
|
|
50
|
+
export function tildePath(p, home) {
|
|
51
|
+
if (!p || !home || p.length <= home.length)
|
|
52
|
+
return p;
|
|
53
|
+
const norm = (s) => s.replace(/[\\/]+/g, "/").replace(/\/$/, "");
|
|
54
|
+
const np = norm(p);
|
|
55
|
+
const nh = norm(home);
|
|
56
|
+
if (np === nh)
|
|
57
|
+
return "~";
|
|
58
|
+
if (np.startsWith(nh + "/"))
|
|
59
|
+
return "~" + np.slice(nh.length);
|
|
60
|
+
return p;
|
|
61
|
+
}
|
|
62
|
+
const PATH_KEYS = new Set(["file", "path", "filePath", "cwd", "directory", "root"]);
|
|
63
|
+
/**
|
|
64
|
+
* Apply redaction (when enabled) + the light path transform to one
|
|
65
|
+
* contract item. Returns the ORIGINAL untouched when nothing matched
|
|
66
|
+
* (zero-copy fast path for clean sessions); otherwise a mutated deep
|
|
67
|
+
* clone - the host's live object is never touched.
|
|
68
|
+
*/
|
|
69
|
+
export function redactItem(item, opts) {
|
|
70
|
+
const flat = JSON.stringify(item) ?? "null";
|
|
71
|
+
const needRedact = opts.enabled && opts.rules.some((r) => { r.re.lastIndex = 0; return r.re.test(flat); });
|
|
72
|
+
const needPaths = !!opts.home && flat.includes(opts.home);
|
|
73
|
+
if (!needRedact && !needPaths)
|
|
74
|
+
return { item, hits: [] };
|
|
75
|
+
const clone = JSON.parse(flat);
|
|
76
|
+
const stats = { hits: [], changed: false };
|
|
77
|
+
walk(clone, opts, stats);
|
|
78
|
+
return { item: clone, hits: stats.hits };
|
|
79
|
+
}
|
|
80
|
+
function walk(node, opts, stats, key) {
|
|
81
|
+
if (Array.isArray(node)) {
|
|
82
|
+
for (let i = 0; i < node.length; i++) {
|
|
83
|
+
const v = node[i];
|
|
84
|
+
if (typeof v === "string")
|
|
85
|
+
node[i] = applyString(v, opts, stats, key);
|
|
86
|
+
else
|
|
87
|
+
walk(v, opts, stats, key);
|
|
88
|
+
}
|
|
89
|
+
return;
|
|
90
|
+
}
|
|
91
|
+
if (node && typeof node === "object") {
|
|
92
|
+
for (const [k, v] of Object.entries(node)) {
|
|
93
|
+
if (typeof v === "string")
|
|
94
|
+
node[k] = applyString(v, opts, stats, k);
|
|
95
|
+
else
|
|
96
|
+
walk(v, opts, stats, k);
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
function applyString(v, opts, stats, key) {
|
|
101
|
+
let out = v;
|
|
102
|
+
if (opts.enabled) {
|
|
103
|
+
for (const r of opts.rules) {
|
|
104
|
+
r.re.lastIndex = 0;
|
|
105
|
+
out = out.replace(r.re, (full) => {
|
|
106
|
+
if (stats.hits.length < 8 && !stats.hits.some((h) => h.kind === r.kind)) {
|
|
107
|
+
stats.hits.push({ kind: r.kind, sample: full.slice(0, 60) });
|
|
108
|
+
}
|
|
109
|
+
stats.changed = true;
|
|
110
|
+
if (r.kind === "conn-string") {
|
|
111
|
+
// keep scheme://user, mask the password, keep @host
|
|
112
|
+
const at = full.lastIndexOf("@");
|
|
113
|
+
const colon = full.indexOf(":", full.indexOf("://") + 3) + 1;
|
|
114
|
+
if (at > colon && colon > 0)
|
|
115
|
+
return full.slice(0, colon) + "[redacted:conn-string]" + full.slice(at);
|
|
116
|
+
}
|
|
117
|
+
return `[redacted:${r.kind}]`;
|
|
118
|
+
});
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
if (key && PATH_KEYS.has(key) && opts.home && out.includes(opts.home)) {
|
|
122
|
+
out = tildePath(out, opts.home);
|
|
123
|
+
stats.changed = true;
|
|
124
|
+
}
|
|
125
|
+
return out;
|
|
126
|
+
}
|
|
127
|
+
/** All active rules (built-ins + compiled user patterns). */
|
|
128
|
+
export function activeRules(userPatterns) {
|
|
129
|
+
return [...BUILTINS, ...compile(userPatterns)];
|
|
130
|
+
}
|
package/dist/settings.d.ts
CHANGED
|
@@ -1,8 +1,12 @@
|
|
|
1
1
|
import type { ShareHostApi } from "./tui-host.js";
|
|
2
|
-
/** Connect (add + enroll) a server
|
|
2
|
+
/** Connect (add + enroll) a server - the /share first-run flow and the
|
|
3
3
|
* settings "Connect to a server" row share it. Prompts the URL, probes
|
|
4
|
-
* the auth mode,
|
|
5
|
-
*
|
|
4
|
+
* the auth mode, then ASKS ONCE: onboard to an account, or stay anonymous
|
|
5
|
+
* (public servers only; anonymous shares sit under the server's global
|
|
6
|
+
* caps). Onboarding detects whether a code is needed and only then prompts
|
|
7
|
+
* for one (a code already in config is used without asking). The outcome is
|
|
8
|
+
* remembered per server: a token means onboarded, an anonymous flag means
|
|
9
|
+
* declined. */
|
|
6
10
|
export declare function connectServerFlow(host: ShareHostApi): Promise<{
|
|
7
11
|
ok: boolean;
|
|
8
12
|
message: string;
|
|
@@ -13,10 +17,9 @@ export declare function connectServerFlow(host: ShareHostApi): Promise<{
|
|
|
13
17
|
* per-server submenu (Admin panel); pinnedUrl skips the server picker. */
|
|
14
18
|
export declare function adminConsole(host: ShareHostApi, pinnedUrl?: string): Promise<void>;
|
|
15
19
|
/** The settings dialog: a compact ROOT of group entries (with live
|
|
16
|
-
* summaries) opening focused submenus. Two-level by design
|
|
17
|
-
*
|
|
18
|
-
*
|
|
19
|
-
* just returns. */
|
|
20
|
+
* summaries) opening focused submenus. Two-level by design: one flat ~25-row
|
|
21
|
+
* dump is unreadable. Save semantics unchanged: root Exit saves when dirty;
|
|
22
|
+
* submenu Back just returns. */
|
|
20
23
|
export declare function settingsDialog(host: ShareHostApi, startAtSession?: string, startGroup?: "session"): Promise<void>;
|
|
21
24
|
/** Onboarding shown by /share when no server is connected: connect flow
|
|
22
25
|
* first (URL + optional code = enrolled + active), full settings as the
|