dsh-ssh-tui 0.7.2 → 0.7.4
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.en.md +113 -27
- package/README.md +90 -27
- package/cordis.patch.yml +15 -0
- package/docs/remote-ops.md +27 -1
- package/docs/terminals.md +126 -0
- package/docs/windows.md +128 -0
- package/lib/attach.js +14 -1
- package/lib/attach.js.map +1 -1
- package/lib/commands.js +1 -0
- package/lib/commands.js.map +1 -1
- package/lib/copy-text.js +2 -0
- package/lib/copy-text.js.map +1 -1
- package/lib/diag.js +38 -0
- package/lib/diag.js.map +1 -1
- package/lib/dialogs.js.map +1 -1
- package/lib/display-sock.js +572 -44
- package/lib/display-sock.js.map +1 -1
- package/lib/doctor.js +71 -37
- package/lib/doctor.js.map +1 -1
- package/lib/dsh-compat.js +252 -88
- package/lib/dsh-compat.js.map +1 -1
- package/lib/footer.js +4 -2
- package/lib/footer.js.map +1 -1
- package/lib/i18n/en.js +54 -10
- package/lib/i18n/en.js.map +1 -1
- package/lib/i18n/index.js +18 -7
- package/lib/i18n/index.js.map +1 -1
- package/lib/i18n/zh.js +54 -10
- package/lib/i18n/zh.js.map +1 -1
- package/lib/index.js +111 -26
- package/lib/index.js.map +1 -1
- package/lib/line-mode.js +4 -0
- package/lib/line-mode.js.map +1 -1
- package/lib/paint.js +37 -3
- package/lib/paint.js.map +1 -1
- package/lib/picker.js +109 -18
- package/lib/picker.js.map +1 -1
- package/lib/plan.js +8 -0
- package/lib/plan.js.map +1 -1
- package/lib/platform.js +377 -11
- package/lib/platform.js.map +1 -1
- package/lib/preset-authoring.js +10 -14
- package/lib/preset-authoring.js.map +1 -1
- package/lib/preset-compat.js +100 -0
- package/lib/preset-compat.js.map +1 -0
- package/lib/preset-picker.js +5 -1
- package/lib/preset-picker.js.map +1 -1
- package/lib/preset-rows.js +119 -19
- package/lib/preset-rows.js.map +1 -1
- package/lib/provider-catalog.js +4 -4
- package/lib/question-wait.js +418 -0
- package/lib/question-wait.js.map +1 -0
- package/lib/route-memory.js +3 -3
- package/lib/route-memory.js.map +1 -1
- package/lib/selection.js +26 -8
- package/lib/selection.js.map +1 -1
- package/lib/session-index.js +5 -0
- package/lib/session-index.js.map +1 -1
- package/lib/session-lock.js +4 -1
- package/lib/session-lock.js.map +1 -1
- package/lib/session-route.js +3 -0
- package/lib/session-route.js.map +1 -1
- package/lib/settings-routes.js +10 -0
- package/lib/settings-routes.js.map +1 -0
- package/lib/settings-subagent.js +10 -0
- package/lib/settings-subagent.js.map +1 -0
- package/lib/subagent-model.js +5 -5
- package/lib/subagent-model.js.map +1 -1
- package/lib/supergrok-token.js +4 -0
- package/lib/supergrok-token.js.map +1 -1
- package/lib/term-text.js +79 -8
- package/lib/term-text.js.map +1 -1
- package/lib/terminal-caps.js +358 -0
- package/lib/terminal-caps.js.map +1 -0
- package/lib/terminal-input.js +13 -0
- package/lib/terminal-input.js.map +1 -1
- package/lib/tui.js +775 -165
- package/lib/tui.js.map +1 -1
- package/lib/types/attach.d.ts +8 -0
- package/lib/types/commands.d.ts +3 -0
- package/lib/types/diag.d.ts +20 -1
- package/lib/types/dialogs.d.ts +16 -0
- package/lib/types/display-sock.d.ts +141 -24
- package/lib/types/doctor.d.ts +8 -1
- package/lib/types/dsh-compat.d.ts +173 -44
- package/lib/types/footer.d.ts +3 -1
- package/lib/types/i18n/index.d.ts +31 -15
- package/lib/types/index.d.ts +45 -0
- package/lib/types/paint.d.ts +21 -0
- package/lib/types/picker.d.ts +25 -0
- package/lib/types/platform.d.ts +197 -10
- package/lib/types/preset-authoring.d.ts +8 -14
- package/lib/types/preset-compat.d.ts +58 -0
- package/lib/types/preset-picker.d.ts +1 -1
- package/lib/types/preset-rows.d.ts +63 -13
- package/lib/types/question-wait.d.ts +221 -0
- package/lib/types/selection.d.ts +12 -4
- package/lib/types/settings-routes.d.ts +16 -0
- package/lib/types/settings-subagent.d.ts +24 -0
- package/lib/types/subagent-model.d.ts +10 -10
- package/lib/types/term-text.d.ts +14 -0
- package/lib/types/terminal-caps.d.ts +105 -0
- package/lib/types/terminal-input.d.ts +10 -0
- package/lib/types/transcript-types.d.ts +22 -0
- package/lib/types/tui.d.ts +167 -10
- package/lib/types/update-check.d.ts +19 -4
- package/lib/types/workspace-changes.d.ts +135 -0
- package/lib/update-check.js +25 -8
- package/lib/update-check.js.map +1 -1
- package/lib/workspace-changes.js +120 -0
- package/lib/workspace-changes.js.map +1 -0
- package/package.json +124 -59
|
@@ -10,23 +10,39 @@ import type { Context } from '@deepseek-ai/cordis';
|
|
|
10
10
|
export type Locale = 'zh' | 'en';
|
|
11
11
|
export type MessageVars = Record<string, string | number>;
|
|
12
12
|
export declare const UI_LOCALE_NAMESPACE: import("@deepseek-ai/dsh-settings").SettingsNamespace;
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
13
|
+
/**
|
|
14
|
+
* Fields `/language`, `/view`, `/disconnect` and `/autoapproval` persist.
|
|
15
|
+
*
|
|
16
|
+
* Every field is live: on 0.1.7 the section *is* this form, and only volatile
|
|
17
|
+
* paths may be written (see {@link liveField}).
|
|
18
|
+
*/
|
|
19
|
+
export declare const UI_LOCALE_SCHEMA: z<Schemastery.ObjectS<NoInfer<{
|
|
20
|
+
language: z<string>;
|
|
21
|
+
skipUpdate: z<string>;
|
|
22
|
+
view: z<string>;
|
|
23
|
+
disconnect: z<string>;
|
|
24
|
+
autoApproval: z<string>;
|
|
19
25
|
/** Milliseconds a leftover, finished Host waits before exiting; 0 = never. */
|
|
20
|
-
idleExit: z<number
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
26
|
+
idleExit: z<number>;
|
|
27
|
+
/**
|
|
28
|
+
* Command run once when a question starts waiting and nobody is attached.
|
|
29
|
+
* Empty means off. See `question-wait.ts` for the environment it receives.
|
|
30
|
+
*/
|
|
31
|
+
notify: z<string>;
|
|
32
|
+
}>>, Schemastery.ObjectT<NoInfer<{
|
|
33
|
+
language: z<string>;
|
|
34
|
+
skipUpdate: z<string>;
|
|
35
|
+
view: z<string>;
|
|
36
|
+
disconnect: z<string>;
|
|
37
|
+
autoApproval: z<string>;
|
|
27
38
|
/** Milliseconds a leftover, finished Host waits before exiting; 0 = never. */
|
|
28
|
-
idleExit: z<number
|
|
29
|
-
|
|
39
|
+
idleExit: z<number>;
|
|
40
|
+
/**
|
|
41
|
+
* Command run once when a question starts waiting and nobody is attached.
|
|
42
|
+
* Empty means off. See `question-wait.ts` for the environment it receives.
|
|
43
|
+
*/
|
|
44
|
+
notify: z<string>;
|
|
45
|
+
}>>, "plain">;
|
|
30
46
|
export declare function localeFromTag(tag: string): Locale | undefined;
|
|
31
47
|
/** Pick zh/en from env, optionally after a saved settings value. */
|
|
32
48
|
export declare function resolveLocale(env?: NodeJS.ProcessEnv, saved?: string): Locale;
|
package/lib/types/index.d.ts
CHANGED
|
@@ -6,6 +6,7 @@
|
|
|
6
6
|
*/
|
|
7
7
|
import type { Context } from '@deepseek-ai/cordis';
|
|
8
8
|
export { ATTACH_RECOVERY_WINDOW_MS, attachPeerVanished } from './attach.js';
|
|
9
|
+
import z from '@deepseek-ai/schemastery';
|
|
9
10
|
export declare const name = "ssh-tui";
|
|
10
11
|
/** Core services required before the terminal channel can drive an agent. */
|
|
11
12
|
export declare const inject: string[];
|
|
@@ -27,7 +28,51 @@ export interface Config {
|
|
|
27
28
|
model?: string;
|
|
28
29
|
/** Minimum milliseconds between paints; see DSH_TUI_PAINT_MS. */
|
|
29
30
|
paintIntervalMs?: number;
|
|
31
|
+
/**
|
|
32
|
+
* The fields below are the TUI's live settings, i.e. the `ssh-tui` section.
|
|
33
|
+
* They ride on this entry's schema so 0.1.7 can project a form for it — and so
|
|
34
|
+
* a pre-0.1.7 `$DSH_HOME/settings.yaml` `ssh-tui:` section is imported into
|
|
35
|
+
* this entry rather than left behind.
|
|
36
|
+
*/
|
|
37
|
+
/** UI language (`/language`); zh unless the environment says otherwise. */
|
|
38
|
+
language?: string;
|
|
39
|
+
/** Newest plugin version whose update notice was dismissed. */
|
|
40
|
+
skipUpdate?: string;
|
|
41
|
+
/** Workspace pane layout (`/view`). */
|
|
42
|
+
view?: string;
|
|
43
|
+
/** What a dropped display does (`/disconnect`). */
|
|
44
|
+
disconnect?: string;
|
|
45
|
+
/** Auto-approval mode (`/autoapproval`). */
|
|
46
|
+
autoApproval?: string;
|
|
47
|
+
/** Milliseconds a leftover finished Host waits before exiting; 0 = never. */
|
|
48
|
+
idleExit?: number;
|
|
30
49
|
}
|
|
50
|
+
/** Every field above, as schemastery resolves them (all optional). */
|
|
51
|
+
interface ConfigFields {
|
|
52
|
+
sessionId?: string;
|
|
53
|
+
showReasoning?: boolean;
|
|
54
|
+
maxToolOutputLines?: number;
|
|
55
|
+
color?: boolean;
|
|
56
|
+
welcome?: string;
|
|
57
|
+
resume?: boolean;
|
|
58
|
+
resumePicker?: boolean;
|
|
59
|
+
provider?: string;
|
|
60
|
+
model?: string;
|
|
61
|
+
paintIntervalMs?: number;
|
|
62
|
+
language?: string;
|
|
63
|
+
skipUpdate?: string;
|
|
64
|
+
view?: string;
|
|
65
|
+
disconnect?: string;
|
|
66
|
+
autoApproval?: string;
|
|
67
|
+
idleExit?: number;
|
|
68
|
+
}
|
|
69
|
+
/**
|
|
70
|
+
* The entry's schema. Everything a launch supplies (`config:` in
|
|
71
|
+
* `cordis.patch.yml`, including its `!!js` expressions) stays ordinary,
|
|
72
|
+
* non-live configuration; the TUI's own settings are the live fields, which is
|
|
73
|
+
* what makes them visible to, and writable through, the 0.1.7 settings service.
|
|
74
|
+
*/
|
|
75
|
+
export declare const Config: z<ConfigFields>;
|
|
31
76
|
/**
|
|
32
77
|
* Mount the SSH TUI. The `main` agent is created here after the loader
|
|
33
78
|
* settles, reading the saved default provider/model from
|
package/lib/types/paint.d.ts
CHANGED
|
@@ -37,6 +37,27 @@ export declare function ignoreFurtherHangupSignals(): void;
|
|
|
37
37
|
export declare function waitUntilIdleOrTimeout(isIdle: () => boolean, timeoutMs: number, now?: () => number, wait?: (ms: number) => Promise<void>): Promise<'idle' | 'timeout'>;
|
|
38
38
|
/** Map a CSI-6n round-trip to a paint cadence. Unknown RTT uses the SSH default. */
|
|
39
39
|
export declare function paintIntervalForRtt(rttMs: number | undefined): number;
|
|
40
|
+
/**
|
|
41
|
+
* How many measurements the link's reported round-trip is taken over.
|
|
42
|
+
*
|
|
43
|
+
* Three is the smallest window where one outlier cannot move the answer and two
|
|
44
|
+
* agreeing measurements can.
|
|
45
|
+
*/
|
|
46
|
+
export declare const RTT_HISTORY = 3;
|
|
47
|
+
/**
|
|
48
|
+
* The link's current round-trip, from the last few measurements.
|
|
49
|
+
*
|
|
50
|
+
* The median — not the latest, not the mean. The probe shares the wire with the
|
|
51
|
+
* paint it is measuring, so a single burst can read like an overloaded link, and
|
|
52
|
+
* because the cadence *and* the per-frame byte budget follow this number, one
|
|
53
|
+
* bad measurement used to sit on the footer chip (and in the paint budget) for
|
|
54
|
+
* the rest of the session. A median ignores one outlier, follows a real change
|
|
55
|
+
* as soon as two measurements agree, and averages the pair in between so the
|
|
56
|
+
* chip does not jump.
|
|
57
|
+
* @param samples - measured round-trips, oldest first; non-finite ones ignored.
|
|
58
|
+
* @returns the median in whole milliseconds, or undefined with nothing to go on.
|
|
59
|
+
*/
|
|
60
|
+
export declare function medianRtt(samples: readonly number[]): number | undefined;
|
|
40
61
|
export declare function paintLinkLabel(kind: PaintLinkKind, intervalMs: number, probed: boolean): string;
|
|
41
62
|
export type LinkQuality = 'local' | 'good' | 'ok' | 'slow' | 'poor' | 'unknown';
|
|
42
63
|
/** Signal-bar quality from a measured SSH round-trip, or local TTY. */
|
package/lib/types/picker.d.ts
CHANGED
|
@@ -11,6 +11,7 @@
|
|
|
11
11
|
*/
|
|
12
12
|
import type { Context } from '@deepseek-ai/cordis';
|
|
13
13
|
import { openResumableSessionPager, type ResumableSession } from './session-list.js';
|
|
14
|
+
import { type TerminalCapabilities } from './terminal-caps.js';
|
|
14
15
|
/** What the launch picker decided. */
|
|
15
16
|
export type SessionPickerResult = {
|
|
16
17
|
kind: 'resume';
|
|
@@ -43,6 +44,18 @@ export interface SessionPickerState {
|
|
|
43
44
|
loading?: boolean;
|
|
44
45
|
/** Sessions in history that have not been read yet. */
|
|
45
46
|
more?: number;
|
|
47
|
+
/**
|
|
48
|
+
* A row whose session another window is attached to, waiting for the user to
|
|
49
|
+
* confirm. Submitting such a row does not attach on its own: the lock says a
|
|
50
|
+
* window is on that session, and attaching kicks that window. The note under
|
|
51
|
+
* the row cannot prove the window is still alive, so the picker asks once
|
|
52
|
+
* instead of either refusing forever or silently stealing the session.
|
|
53
|
+
*/
|
|
54
|
+
confirmTakeover?: {
|
|
55
|
+
id: string;
|
|
56
|
+
sock: string;
|
|
57
|
+
pid: number;
|
|
58
|
+
};
|
|
46
59
|
}
|
|
47
60
|
/** One key / control action against {@link SessionPickerState}. */
|
|
48
61
|
export type SessionPickerAction = {
|
|
@@ -88,6 +101,16 @@ export type SessionPickerStep = {
|
|
|
88
101
|
export declare function sessionSearchHaystack(session: ResumableSession): string;
|
|
89
102
|
/** Whether one session matches a whitespace-separated query (every token). */
|
|
90
103
|
export declare function sessionMatchesQuery(session: ResumableSession, query: string): boolean;
|
|
104
|
+
/**
|
|
105
|
+
* Whether a live Host is holding this session in a window *right now*.
|
|
106
|
+
*
|
|
107
|
+
* The lock's `state` is written by the Host: `attached` while a display relay is
|
|
108
|
+
* connected, `paused` / `running-detached` once the window is gone (a dropped
|
|
109
|
+
* link, a closed window) and the Host stayed behind. Only an explicit
|
|
110
|
+
* `attached` counts — a lock from before the field existed has no state, and
|
|
111
|
+
* treating "unknown" as "in use" would block a resume that used to work.
|
|
112
|
+
*/
|
|
113
|
+
export declare function sessionAttachedElsewhere(session: ResumableSession): boolean;
|
|
91
114
|
/** Sessions still visible under the current filter, in list order. */
|
|
92
115
|
export declare function filterResumableSessions(sessions: readonly ResumableSession[], query: string): ResumableSession[];
|
|
93
116
|
/** Keep `cursor` inside `[0, total)`. Empty lists pin to 0. */
|
|
@@ -144,5 +167,7 @@ export interface SessionPickerOptions {
|
|
|
144
167
|
stdin?: NodeJS.ReadStream;
|
|
145
168
|
stdout?: NodeJS.WriteStream;
|
|
146
169
|
openPager?: typeof openResumableSessionPager;
|
|
170
|
+
/** The terminal to act on; defaults to reading the environment (tests inject). */
|
|
171
|
+
terminalCaps?: TerminalCapabilities;
|
|
147
172
|
}
|
|
148
173
|
export declare function showSessionPicker(ctx: Context, color: boolean, signal?: AbortSignal, options?: SessionPickerOptions): Promise<SessionPickerResult>;
|
package/lib/types/platform.d.ts
CHANGED
|
@@ -22,21 +22,125 @@ export declare function usesProcessIdentity(platform?: NodeJS.Platform): boolean
|
|
|
22
22
|
* POSIX wants `detached: true` (setsid) so the Host survives the launcher and a
|
|
23
23
|
* hung-up terminal.
|
|
24
24
|
*
|
|
25
|
-
* Windows is the opposite: `detached: true` maps to
|
|
26
|
-
*
|
|
27
|
-
*
|
|
28
|
-
*
|
|
29
|
-
*
|
|
30
|
-
*
|
|
31
|
-
*
|
|
32
|
-
*
|
|
33
|
-
*
|
|
34
|
-
* not
|
|
25
|
+
* Windows is the opposite, and the trade-off is forced: `detached: true` maps to
|
|
26
|
+
* DETACHED_PROCESS, and Windows ignores CREATE_NO_WINDOW (what `windowsHide`
|
|
27
|
+
* sets) when DETACHED_PROCESS is present. Every console child the Host then
|
|
28
|
+
* starts — each tool call, every shell, node, git — allocates its own console,
|
|
29
|
+
* which is a visible window flashing over the TUI. So the Host is spawned
|
|
30
|
+
* non-detached and inherits the launcher's console; descendants inherit it too
|
|
31
|
+
* instead of creating one.
|
|
32
|
+
*
|
|
33
|
+
* The price *was* on the lifecycle side, and it was measured (on the Windows CI
|
|
34
|
+
* leg by `scripts/tui-mock-probe.mjs --busy`, not reasoned about): libuv assigns
|
|
35
|
+
* a non-detached child to its global job object, which is created with
|
|
36
|
+
* KILL_ON_JOB_CLOSE. The launcher died, the job closed, and the Host was
|
|
37
|
+
* terminated with it — a closed terminal window ended the session's compute.
|
|
38
|
+
*
|
|
39
|
+
* This function still returns `detached: false` there, because it describes a
|
|
40
|
+
* *direct* spawn, and a direct spawn cannot have both properties: Node exposes
|
|
41
|
+
* `detached` (DETACHED_PROCESS, which makes Windows ignore CREATE_NO_WINDOW) and
|
|
42
|
+
* `windowsHide`, but not `CREATE_NEW_CONSOLE`. The way out is to not spawn the
|
|
43
|
+
* Host directly on Windows: {@link hostBootstrapCommand} starts it through the
|
|
44
|
+
* OS PowerShell, which *can* ask for a new console and hide it. This direct path
|
|
45
|
+
* stays as the fallback for a Windows box without PowerShell.
|
|
35
46
|
*/
|
|
36
47
|
export declare function hostSpawnOptions(platform?: NodeJS.Platform): {
|
|
37
48
|
detached: boolean;
|
|
38
49
|
windowsHide: boolean;
|
|
39
50
|
};
|
|
51
|
+
/**
|
|
52
|
+
* The Windows PowerShell that ships with the operating system.
|
|
53
|
+
*
|
|
54
|
+
* `powershell.exe` and not `pwsh.exe`: the latter is PowerShell 7, an optional
|
|
55
|
+
* install, while 5.1 is part of Windows 10/11. `%SystemRoot%` is where it lives
|
|
56
|
+
* (`%windir%` is the legacy spelling of the same thing); when neither is set
|
|
57
|
+
* there is nothing to find, and the caller falls back to a direct spawn.
|
|
58
|
+
*/
|
|
59
|
+
export declare function windowsPowerShellPath(env?: NodeJS.ProcessEnv): string | undefined;
|
|
60
|
+
/** A PowerShell single-quoted literal; `''` is the only escape inside one. */
|
|
61
|
+
export declare function psQuote(value: string): string;
|
|
62
|
+
/**
|
|
63
|
+
* Quote one argv array into a Windows command line, the way `CreateProcess`
|
|
64
|
+
* parses it back (the rule from "Everyone quotes command line arguments the
|
|
65
|
+
* wrong way"): wrap in double quotes when the argument is empty or contains
|
|
66
|
+
* whitespace or a quote, double the backslashes that precede a quote, and double
|
|
67
|
+
* trailing backslashes before the closing quote. `Start-Process -ArgumentList`
|
|
68
|
+
* joins its array with spaces and adds no quoting of its own, so the line has to
|
|
69
|
+
* be right before it is handed over — a `DSH_HOME` with a space in it is the
|
|
70
|
+
* normal case, not the exotic one.
|
|
71
|
+
*/
|
|
72
|
+
export declare function windowsCommandLine(argv: string[]): string;
|
|
73
|
+
/** `-EncodedCommand` wants base64 of UTF-16LE, which is also what dodges quoting. */
|
|
74
|
+
export declare function encodePowerShellCommand(script: string): string;
|
|
75
|
+
/**
|
|
76
|
+
* The bootstrap script: start the Host with **its own console, hidden**, and
|
|
77
|
+
* print its pid so the launcher can watch it.
|
|
78
|
+
*
|
|
79
|
+
* `Start-Process -WindowStyle Hidden` is the only way to ask for this from a
|
|
80
|
+
* Node process. It is ShellExecuteEx/CreateProcess with `CREATE_NEW_CONSOLE` and
|
|
81
|
+
* `SW_HIDE`: the Host gets a console of its own, so closing the user's terminal
|
|
82
|
+
* no longer takes it down, and that console is invisible, so the tool calls that
|
|
83
|
+
* inherit it do not flash. `-PassThru` gives the object whose `Id` is printed;
|
|
84
|
+
* `-RedirectStandardError` keeps the Host's stderr log, which the direct spawn
|
|
85
|
+
* used to feed through an inherited fd.
|
|
86
|
+
*
|
|
87
|
+
* The whole script travels as an encoded command, so nothing in it is ever
|
|
88
|
+
* re-parsed by a shell.
|
|
89
|
+
*/
|
|
90
|
+
export declare function hiddenConsoleHostScript(options: {
|
|
91
|
+
execPath: string;
|
|
92
|
+
argv: string[];
|
|
93
|
+
/** Where the Host's stderr goes; omitted only in tests. */
|
|
94
|
+
stderrFile?: string;
|
|
95
|
+
/** Where the Host's pid is written for the launcher to read. */
|
|
96
|
+
pidFile: string;
|
|
97
|
+
}): string;
|
|
98
|
+
/**
|
|
99
|
+
* Set on the Host's environment by the bootstrap below, through
|
|
100
|
+
* {@link bootstrapEnv}.
|
|
101
|
+
*
|
|
102
|
+
* The Host cannot ask whether it has a console of its own — Node exposes no such
|
|
103
|
+
* question — so the launcher tells it. That is what lets the one build where the
|
|
104
|
+
* lifecycle promise does not hold (Windows, no PowerShell, direct child) say so
|
|
105
|
+
* at boot instead of letting the user discover it by closing the window.
|
|
106
|
+
*/
|
|
107
|
+
export declare const TUI_HOST_START_ENV = "DSH_TUI_HOST_START";
|
|
108
|
+
/** Marker value for a Host started with a hidden console of its own. */
|
|
109
|
+
export declare const TUI_HOST_START_BOOTSTRAP = "hidden-console";
|
|
110
|
+
/** The environment the bootstrap hands to the Host: the marker is added here. */
|
|
111
|
+
export declare function bootstrapEnv(env?: NodeJS.ProcessEnv): NodeJS.ProcessEnv;
|
|
112
|
+
/**
|
|
113
|
+
* Whether this Host survives its terminal being closed.
|
|
114
|
+
*
|
|
115
|
+
* POSIX always does (`setsid`); Windows does exactly when the bootstrap started
|
|
116
|
+
* it. A Windows Host without the marker is the fallback direct child, which
|
|
117
|
+
* libuv's `KILL_ON_JOB_CLOSE` job takes down with the launcher — the one case
|
|
118
|
+
* worth a boot notice.
|
|
119
|
+
*/
|
|
120
|
+
export declare function hostHasOwnConsole(env?: NodeJS.ProcessEnv, platform?: NodeJS.Platform): boolean;
|
|
121
|
+
/** How the Host is started when it must not be a direct child. */
|
|
122
|
+
export interface HostBootstrapCommand {
|
|
123
|
+
command: string;
|
|
124
|
+
args: string[];
|
|
125
|
+
}
|
|
126
|
+
/**
|
|
127
|
+
* The hidden-console bootstrap for this platform, or `undefined` when the Host
|
|
128
|
+
* should be spawned directly.
|
|
129
|
+
*
|
|
130
|
+
* Windows only, and only when the OS PowerShell is really there: the caller
|
|
131
|
+
* falls back to {@link hostSpawnOptions}, which keeps the boot working (without
|
|
132
|
+
* the survival property) on a machine where PowerShell is missing or blocked.
|
|
133
|
+
* `exists` is injectable so the decision is assertable on Linux.
|
|
134
|
+
*/
|
|
135
|
+
export declare function hostBootstrapCommand(options: {
|
|
136
|
+
platform?: NodeJS.Platform;
|
|
137
|
+
env?: NodeJS.ProcessEnv;
|
|
138
|
+
exists?: (path: string) => boolean;
|
|
139
|
+
execPath: string;
|
|
140
|
+
argv: string[];
|
|
141
|
+
stderrFile?: string;
|
|
142
|
+
pidFile: string;
|
|
143
|
+
}): HostBootstrapCommand | undefined;
|
|
40
144
|
/**
|
|
41
145
|
* A path a human can read, with the platform's own shorthand.
|
|
42
146
|
*
|
|
@@ -48,3 +152,86 @@ export declare function displayHomePath(home: string, file: string, options?: {
|
|
|
48
152
|
env?: NodeJS.ProcessEnv;
|
|
49
153
|
userHome?: string;
|
|
50
154
|
}): string;
|
|
155
|
+
/**
|
|
156
|
+
* Who owns the files this plugin writes, and how to keep it that way.
|
|
157
|
+
*
|
|
158
|
+
* On POSIX the `mode` passed to `writeFile`/`mkdir` (`0o600`, `0o700`) is the
|
|
159
|
+
* whole story. **On Windows it is silently ignored**, and the files that matter
|
|
160
|
+
* here are not cosmetic: `env.cmd` carries API keys, the SuperGrok token file
|
|
161
|
+
* carries an OAuth grant, and the lock/socket directories carry session
|
|
162
|
+
* metadata. What they get instead is the ACL inherited from their parent — fine
|
|
163
|
+
* under `%USERPROFILE%\.dsh`, and *not* fine when `DSH_HOME` points somewhere
|
|
164
|
+
* shared (`C:\dsh`, a network share, a machine where `Users` can read the
|
|
165
|
+
* directory), which is exactly when nobody notices.
|
|
166
|
+
*
|
|
167
|
+
* So the intent is applied explicitly: `icacls` with inheritance removed and a
|
|
168
|
+
* single grant to the current user. The argv is built by a pure function so it
|
|
169
|
+
* can be asserted on Linux; applying it is best-effort by design — a machine
|
|
170
|
+
* without `icacls`, or a path held open by another process, must not fail the
|
|
171
|
+
* write that just succeeded. Failing closed here would mean a TUI that cannot
|
|
172
|
+
* save its own settings.
|
|
173
|
+
*/
|
|
174
|
+
/**
|
|
175
|
+
* The account `icacls` should grant, or undefined when the environment has none.
|
|
176
|
+
*
|
|
177
|
+
* `%USERDOMAIN%\%USERNAME%` where both are present: on a domain-joined machine a
|
|
178
|
+
* bare name can resolve to the machine-local account of the same name, and a
|
|
179
|
+
* grant to the wrong account — with inheritance already removed — would leave
|
|
180
|
+
* the user's own API keys inaccessible to them.
|
|
181
|
+
*/
|
|
182
|
+
export declare function aclUserName(env?: NodeJS.ProcessEnv, platform?: NodeJS.Platform): string | undefined;
|
|
183
|
+
/**
|
|
184
|
+
* The exact `icacls` arguments for one path.
|
|
185
|
+
*
|
|
186
|
+
* `/inheritance:r` drops what the parent offered (this is the part that matters:
|
|
187
|
+
* adding a grant without removing inheritance leaves `Users` in place), and
|
|
188
|
+
* `(OI)(CI)` makes a directory's grant apply to what is created inside it —
|
|
189
|
+
* without it every new lock file would need its own call.
|
|
190
|
+
*/
|
|
191
|
+
export declare function restrictPathArgs(path: string, user: string, options?: {
|
|
192
|
+
directory?: boolean;
|
|
193
|
+
}): string[];
|
|
194
|
+
/** Directories and files whose contents must not be readable by other users. */
|
|
195
|
+
export interface RestrictDeps {
|
|
196
|
+
/** Runner for `icacls`; injected by tests. Returns true on success. */
|
|
197
|
+
run?: (command: string, args: string[]) => boolean;
|
|
198
|
+
platform?: NodeJS.Platform;
|
|
199
|
+
env?: NodeJS.ProcessEnv;
|
|
200
|
+
}
|
|
201
|
+
/**
|
|
202
|
+
* Apply `mode` on POSIX, a single-user ACL on Windows.
|
|
203
|
+
*
|
|
204
|
+
* Returns whether the restriction was applied. Never throws: the caller has
|
|
205
|
+
* already written the file, and a permission call is not worth losing it over.
|
|
206
|
+
*/
|
|
207
|
+
export declare function restrictPathToUser(path: string, options?: {
|
|
208
|
+
mode: number;
|
|
209
|
+
directory?: boolean;
|
|
210
|
+
} & RestrictDeps): Promise<boolean>;
|
|
211
|
+
/** Synchronous twin, for the sites that create their file with `openSync`. */
|
|
212
|
+
export declare function restrictPathToUserSync(path: string, options?: {
|
|
213
|
+
mode: number;
|
|
214
|
+
directory?: boolean;
|
|
215
|
+
} & RestrictDeps): boolean;
|
|
216
|
+
/**
|
|
217
|
+
* Whether the terminal in front of us can paint UTF-8 at all.
|
|
218
|
+
*
|
|
219
|
+
* The chrome this TUI draws — rules, status dots, the warning glyph — is
|
|
220
|
+
* Unicode. A console whose output code page is not UTF-8 (a legacy Windows
|
|
221
|
+
* console left on CP936 or CP437) and a POSIX locale that is not UTF-8 both
|
|
222
|
+
* render those bytes as mojibake, which is worse than plain ASCII: the frame
|
|
223
|
+
* still has to line up. This answers only "can it decode UTF-8", never "which
|
|
224
|
+
* terminal", so the capability table stays where it is.
|
|
225
|
+
*
|
|
226
|
+
* The decision is read from the environment, which is all a portable test can
|
|
227
|
+
* assert: `DSH_TUI_ASCII=1` forces the fallback and `=0` forbids it; otherwise
|
|
228
|
+
* a locale naming a non-UTF-8 charset (or the bare `C` / `POSIX`) opts in,
|
|
229
|
+
* and so does a Windows console whose output code page is recorded as anything
|
|
230
|
+
* but 65001. An unset locale is left alone — a UTF-8 SSH session often exports
|
|
231
|
+
* nothing, and guessing "ASCII" there would strip the chrome from the audience
|
|
232
|
+
* this TUI is built for.
|
|
233
|
+
* @param env - the environment to read (tests pass their own).
|
|
234
|
+
* @param platform - the platform the decision is for.
|
|
235
|
+
* @returns true when chrome must be drawn in ASCII.
|
|
236
|
+
*/
|
|
237
|
+
export declare function asciiFallbackEnabled(env?: NodeJS.ProcessEnv, platform?: NodeJS.Platform): boolean;
|
|
@@ -20,21 +20,13 @@
|
|
|
20
20
|
* every later launch falls back to the default.
|
|
21
21
|
* @module dsh-ssh-tui/preset-authoring
|
|
22
22
|
*/
|
|
23
|
-
import { type AgentPreset, type PresetMetadata } from '
|
|
23
|
+
import { presetDirectory, type AgentPreset, type PresetMetadata } from './preset-compat.js';
|
|
24
|
+
export { presetDirectory };
|
|
24
25
|
/** The id shape discovery accepts; a directory that fails it is skipped. */
|
|
25
26
|
export declare const PRESET_ID_PATTERN: RegExp;
|
|
26
27
|
/** Whether an id would be discovered at all. */
|
|
27
28
|
export declare function validatePresetId(id: string): boolean;
|
|
28
|
-
|
|
29
|
-
* The directory a preset owns.
|
|
30
|
-
*
|
|
31
|
-
* `AgentPreset.path` is the composition file the preset publishes, not the
|
|
32
|
-
* directory, so anything that writes beside it (display metadata) starts here.
|
|
33
|
-
* @param preset - a discovered preset.
|
|
34
|
-
* @returns the absolute preset directory.
|
|
35
|
-
*/
|
|
36
|
-
export declare function presetDirectory(preset: AgentPreset): string;
|
|
37
|
-
export type PresetRefusalCode = 'invalid-id' | 'exists' | 'not-found' | 'read-only' | 'system' | 'running' | 'empty-metadata';
|
|
29
|
+
export type PresetRefusalCode = 'invalid-id' | 'exists' | 'not-found' | 'read-only' | 'system' | 'managed' | 'running' | 'empty-metadata';
|
|
38
30
|
export interface PresetRefusal {
|
|
39
31
|
error: PresetRefusalCode;
|
|
40
32
|
/** The id the refusal is about, when it names one. */
|
|
@@ -78,9 +70,11 @@ export interface PresetCompositionView {
|
|
|
78
70
|
}
|
|
79
71
|
/**
|
|
80
72
|
* The authoring subset of the service, feature-detected rather than assumed:
|
|
81
|
-
* the
|
|
82
|
-
* `
|
|
83
|
-
*
|
|
73
|
+
* the two supported lines expose different preset services under the same
|
|
74
|
+
* `agentPresets` name. 0.1.5's `dsh-agent-presets` carries `copy`, `remove`,
|
|
75
|
+
* `read` and `compositionInventory`; the 0.1.7 registry
|
|
76
|
+
* (`dsh-agent-preset-registry`) does not. A missing member turns into a clear
|
|
77
|
+
* message instead of a crash.
|
|
84
78
|
*/
|
|
85
79
|
export interface PresetAuthoringApi {
|
|
86
80
|
readonly authorable?: boolean;
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
/** The optional display-metadata file beside a preset's composition. */
|
|
2
|
+
export declare const METADATA_FILE = "preset.yml";
|
|
3
|
+
/** Display text a preset may publish about itself. */
|
|
4
|
+
export interface PresetMetadata {
|
|
5
|
+
/** Human-facing name; falls back to the preset id when absent. */
|
|
6
|
+
readonly name?: string;
|
|
7
|
+
/** One sentence on what this preset is for. */
|
|
8
|
+
readonly description?: string;
|
|
9
|
+
/** Position within its group; lower comes first. */
|
|
10
|
+
readonly order?: number;
|
|
11
|
+
}
|
|
12
|
+
/** Where a preset's composition came from, on the line that still records it. */
|
|
13
|
+
export type PresetTrust = 'system' | 'user';
|
|
14
|
+
/**
|
|
15
|
+
* One preset, as much of it as this plugin reads.
|
|
16
|
+
*
|
|
17
|
+
* `id`/`name`/`description`/`order`/`broken` are the fields both lines agree on.
|
|
18
|
+
* `path` (the composition file) and `trust` (the root it was discovered under)
|
|
19
|
+
* only exist on the 0.1.5 line; every consumer here treats them as optional,
|
|
20
|
+
* and the authoring paths refuse rather than guess when they are gone.
|
|
21
|
+
*/
|
|
22
|
+
export interface AgentPreset {
|
|
23
|
+
readonly id: string;
|
|
24
|
+
readonly name?: string;
|
|
25
|
+
readonly description?: string;
|
|
26
|
+
readonly order?: number;
|
|
27
|
+
readonly broken?: string;
|
|
28
|
+
/** 0.1.5 line only: absolute path of the preset's agent composition file. */
|
|
29
|
+
readonly path?: string;
|
|
30
|
+
/** 0.1.5 line only: trust recorded from the root it was discovered under. */
|
|
31
|
+
readonly trust?: PresetTrust;
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* The directory a preset owns, when the host still exposes one.
|
|
35
|
+
*
|
|
36
|
+
* `AgentPreset.path` is the composition file the preset publishes, not the
|
|
37
|
+
* directory, so anything that writes beside it (display metadata) starts here.
|
|
38
|
+
* `undefined` on a host that lists declarative presets instead of directories.
|
|
39
|
+
* @param preset - a discovered preset.
|
|
40
|
+
* @returns the absolute preset directory, or undefined when there is none.
|
|
41
|
+
*/
|
|
42
|
+
export declare function presetDirectory(preset: AgentPreset): string | undefined;
|
|
43
|
+
/**
|
|
44
|
+
* Read a preset's display metadata. Every failure degrades to no metadata: a
|
|
45
|
+
* preset whose display text is missing, malformed, or unreadable still mounts.
|
|
46
|
+
* @param directory - the preset directory to read `preset.yml` from.
|
|
47
|
+
* @returns the published name/description/order, or `{}`.
|
|
48
|
+
*/
|
|
49
|
+
export declare function readPresetMetadata(directory: string): Promise<PresetMetadata>;
|
|
50
|
+
/**
|
|
51
|
+
* Render display metadata as the file's contents.
|
|
52
|
+
*
|
|
53
|
+
* Absent fields are omitted rather than written empty, so a preset with no
|
|
54
|
+
* description does not ship a key that reads as an intentional blank.
|
|
55
|
+
* @param metadata - the display text to store.
|
|
56
|
+
* @returns the YAML document, or undefined when there is nothing to store.
|
|
57
|
+
*/
|
|
58
|
+
export declare function renderPresetMetadata(metadata: PresetMetadata): string | undefined;
|
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
* service — the caller passes what `list()` returned.
|
|
10
10
|
* @module dsh-ssh-tui/preset-picker
|
|
11
11
|
*/
|
|
12
|
-
import type { AgentPreset } from '
|
|
12
|
+
import type { AgentPreset } from './preset-compat.js';
|
|
13
13
|
/** One option as the question dialog wants it, plus the id it stands for. */
|
|
14
14
|
export interface PresetPickerOption {
|
|
15
15
|
id: string;
|