@zhengjunyao/dsh-restart 0.1.0
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/CHANGELOG.md +27 -0
- package/LICENSE +21 -0
- package/README.md +116 -0
- package/README.zh.md +163 -0
- package/cordis.patch.yml +17 -0
- package/helper/restart-helper.mjs +828 -0
- package/lib/client.js +1706 -0
- package/lib/client.js.map +1 -0
- package/lib/index.js +1401 -0
- package/lib/types/client/RestartPanel.d.ts +5 -0
- package/lib/types/client/api.d.ts +230 -0
- package/lib/types/client/floating.d.ts +2 -0
- package/lib/types/client/index.d.ts +8 -0
- package/lib/types/client/overlay.d.ts +2 -0
- package/lib/types/client/state.d.ts +79 -0
- package/lib/types/config.d.ts +125 -0
- package/lib/types/index.d.ts +39 -0
- package/lib/types/launchd.d.ts +80 -0
- package/lib/types/restart.d.ts +236 -0
- package/lib/types/routes.d.ts +66 -0
- package/lib/types/tools.d.ts +25 -0
- package/package.json +96 -0
|
@@ -0,0 +1,230 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Browser-side API client for the /api/dsh-restart route family.
|
|
3
|
+
*
|
|
4
|
+
* Everything goes over same-origin fetch. The one endpoint that is *not* on the
|
|
5
|
+
* DSH server is the detached helper's recovery console (a different port,
|
|
6
|
+
* CORS-open) — that is the only thing still answering while DSH is down, so the
|
|
7
|
+
* panel uses it to show why a restart failed.
|
|
8
|
+
*/
|
|
9
|
+
/** Live helper state (mirrors the host contract; every field is optional). */
|
|
10
|
+
export interface HelperStatus {
|
|
11
|
+
ok?: boolean;
|
|
12
|
+
/** Marker set by our own helper; absent on anything else listening on that port. */
|
|
13
|
+
helper?: string;
|
|
14
|
+
mode?: 'spawn' | 'observe';
|
|
15
|
+
phase?: string;
|
|
16
|
+
attempt?: number;
|
|
17
|
+
maxAttempts?: number;
|
|
18
|
+
port?: number;
|
|
19
|
+
url?: string;
|
|
20
|
+
fallbackPort?: number | null;
|
|
21
|
+
fallbackUrl?: string;
|
|
22
|
+
oldPid?: number | null;
|
|
23
|
+
childPid?: number | null;
|
|
24
|
+
childExit?: {
|
|
25
|
+
code: number | null;
|
|
26
|
+
signal: string | null;
|
|
27
|
+
at: string;
|
|
28
|
+
} | null;
|
|
29
|
+
startedAt?: string;
|
|
30
|
+
elapsedMs?: number;
|
|
31
|
+
readyAt?: string | null;
|
|
32
|
+
bootMs?: number | null;
|
|
33
|
+
failure?: {
|
|
34
|
+
kind: string;
|
|
35
|
+
message: string;
|
|
36
|
+
exitCode?: number | null;
|
|
37
|
+
} | null;
|
|
38
|
+
logFile?: string | null;
|
|
39
|
+
errorLines?: {
|
|
40
|
+
t: number;
|
|
41
|
+
text: string;
|
|
42
|
+
}[];
|
|
43
|
+
tail?: string[];
|
|
44
|
+
dshVersion?: string | null;
|
|
45
|
+
profile?: string | null;
|
|
46
|
+
argv?: string[];
|
|
47
|
+
}
|
|
48
|
+
/** Host identity. */
|
|
49
|
+
export interface HostInfo {
|
|
50
|
+
pid: number;
|
|
51
|
+
ppid: number;
|
|
52
|
+
startedAt: string;
|
|
53
|
+
uptimeMs: number;
|
|
54
|
+
port: number;
|
|
55
|
+
host: string;
|
|
56
|
+
url: string;
|
|
57
|
+
cwd: string;
|
|
58
|
+
nodeVersion: string;
|
|
59
|
+
dshVersion: string;
|
|
60
|
+
profile: string;
|
|
61
|
+
command: string;
|
|
62
|
+
restarted: boolean;
|
|
63
|
+
platform: string;
|
|
64
|
+
logsDir: string;
|
|
65
|
+
statusFile: string;
|
|
66
|
+
helperFile: string;
|
|
67
|
+
helperExists: boolean;
|
|
68
|
+
launchd: {
|
|
69
|
+
managed: boolean;
|
|
70
|
+
label: string;
|
|
71
|
+
state: string;
|
|
72
|
+
pid: number | null;
|
|
73
|
+
plistPath: string;
|
|
74
|
+
logFile: string;
|
|
75
|
+
strategy: string;
|
|
76
|
+
};
|
|
77
|
+
}
|
|
78
|
+
/** One restart record. */
|
|
79
|
+
export interface RestartRecord {
|
|
80
|
+
at: string;
|
|
81
|
+
source: string;
|
|
82
|
+
reason: string;
|
|
83
|
+
oldPid: number;
|
|
84
|
+
helperPid: number | null;
|
|
85
|
+
port: number;
|
|
86
|
+
logFile: string;
|
|
87
|
+
statusFile: string;
|
|
88
|
+
outcome?: string;
|
|
89
|
+
}
|
|
90
|
+
/** Effective plugin config (mirrors the host contract). */
|
|
91
|
+
export interface RestartConfig {
|
|
92
|
+
enabled: boolean;
|
|
93
|
+
announceToAgent: boolean;
|
|
94
|
+
entry: 'sidebar' | 'ball' | 'both' | 'off';
|
|
95
|
+
restartMode: 'auto' | 'helper' | 'launchd';
|
|
96
|
+
fallbackPort: number;
|
|
97
|
+
bootTimeoutMs: number;
|
|
98
|
+
maxAttempts: number;
|
|
99
|
+
killGraceMs: number;
|
|
100
|
+
portFreeTimeoutMs: number;
|
|
101
|
+
lingerMs: number;
|
|
102
|
+
logLines: number;
|
|
103
|
+
autoReload: boolean;
|
|
104
|
+
showOverlay: boolean;
|
|
105
|
+
probeIntervalMs: number;
|
|
106
|
+
historyLimit: number;
|
|
107
|
+
}
|
|
108
|
+
/** GET /api/dsh-restart/status. */
|
|
109
|
+
export interface StatusPayload {
|
|
110
|
+
ok: boolean;
|
|
111
|
+
host: HostInfo;
|
|
112
|
+
helper: HelperStatus | null;
|
|
113
|
+
helperAlive: boolean;
|
|
114
|
+
helperAgeMs: number | null;
|
|
115
|
+
launchd: {
|
|
116
|
+
managed: boolean;
|
|
117
|
+
label: string;
|
|
118
|
+
state: string;
|
|
119
|
+
logFile: string;
|
|
120
|
+
strategy: string;
|
|
121
|
+
} | null;
|
|
122
|
+
config: RestartConfig;
|
|
123
|
+
configFile: string;
|
|
124
|
+
configExists: boolean;
|
|
125
|
+
statusFile: string;
|
|
126
|
+
consoleUrl: string;
|
|
127
|
+
history: RestartRecord[];
|
|
128
|
+
logFiles: {
|
|
129
|
+
name: string;
|
|
130
|
+
file: string;
|
|
131
|
+
size: number;
|
|
132
|
+
mtime: string;
|
|
133
|
+
}[];
|
|
134
|
+
}
|
|
135
|
+
/** GET /api/dsh-restart/logs. */
|
|
136
|
+
export interface LogPayload {
|
|
137
|
+
ok: boolean;
|
|
138
|
+
file: string;
|
|
139
|
+
exists: boolean;
|
|
140
|
+
mtime: string;
|
|
141
|
+
text: string;
|
|
142
|
+
lines: string[];
|
|
143
|
+
errorLines: string[];
|
|
144
|
+
logFiles: {
|
|
145
|
+
name: string;
|
|
146
|
+
file: string;
|
|
147
|
+
size: number;
|
|
148
|
+
mtime: string;
|
|
149
|
+
}[];
|
|
150
|
+
}
|
|
151
|
+
/** GET /api/dsh-restart/probe. */
|
|
152
|
+
export interface ProbePayload {
|
|
153
|
+
ok: boolean;
|
|
154
|
+
pid: number;
|
|
155
|
+
startedAt: string;
|
|
156
|
+
uptimeMs: number;
|
|
157
|
+
}
|
|
158
|
+
/** POST /api/dsh-restart/restart. */
|
|
159
|
+
export interface RestartAck {
|
|
160
|
+
ok: boolean;
|
|
161
|
+
helperPid: number | null;
|
|
162
|
+
logFile: string;
|
|
163
|
+
statusFile: string;
|
|
164
|
+
fallbackPort: number;
|
|
165
|
+
fallbackUrl: string;
|
|
166
|
+
exitInMs: number;
|
|
167
|
+
restartingAt: string;
|
|
168
|
+
oldPid: number;
|
|
169
|
+
mode?: 'helper' | 'launchd';
|
|
170
|
+
error?: string;
|
|
171
|
+
}
|
|
172
|
+
/** Error carrying the route's JSON error message. */
|
|
173
|
+
export declare class RestartApiError extends Error {
|
|
174
|
+
readonly status: number;
|
|
175
|
+
constructor(message: string, status?: number);
|
|
176
|
+
}
|
|
177
|
+
/** The dsh-restart panel API. */
|
|
178
|
+
export declare class RestartApi {
|
|
179
|
+
/** Host + helper + config + history. */
|
|
180
|
+
status(): Promise<StatusPayload>;
|
|
181
|
+
/** Liveness probe used while reconnecting (short timeout, tiny body). */
|
|
182
|
+
probe(timeoutMs?: number): Promise<ProbePayload>;
|
|
183
|
+
/** Ask for a restart; the host answers before it exits. */
|
|
184
|
+
restart(reason: string, source?: string): Promise<RestartAck>;
|
|
185
|
+
/** Boot-log tail; `which: 'latest'` = newest log file on disk. */
|
|
186
|
+
logs(which?: string, lines?: number): Promise<LogPayload>;
|
|
187
|
+
/** Restart history, newest first. */
|
|
188
|
+
history(limit?: number): Promise<{
|
|
189
|
+
ok: boolean;
|
|
190
|
+
history: RestartRecord[];
|
|
191
|
+
}>;
|
|
192
|
+
/** Patch (or reset) the plugin config. */
|
|
193
|
+
setConfig(patch: Partial<RestartConfig> & {
|
|
194
|
+
reset?: boolean;
|
|
195
|
+
}): Promise<{
|
|
196
|
+
ok: boolean;
|
|
197
|
+
config: RestartConfig;
|
|
198
|
+
}>;
|
|
199
|
+
/** Live helper state (through the host). */
|
|
200
|
+
helper(): Promise<{
|
|
201
|
+
ok: boolean;
|
|
202
|
+
alive: boolean;
|
|
203
|
+
ageMs: number | null;
|
|
204
|
+
consoleUrl: string;
|
|
205
|
+
status: HelperStatus | null;
|
|
206
|
+
}>;
|
|
207
|
+
/** Ask a failed helper to try again. */
|
|
208
|
+
helperRetry(): Promise<{
|
|
209
|
+
ok: boolean;
|
|
210
|
+
consoleUrl?: string;
|
|
211
|
+
error?: string;
|
|
212
|
+
}>;
|
|
213
|
+
}
|
|
214
|
+
/**
|
|
215
|
+
* Read the detached helper's live state straight from its console port.
|
|
216
|
+
*
|
|
217
|
+
* Used only while DSH itself is unreachable: the helper is a different origin
|
|
218
|
+
* (another port) but answers with `Access-Control-Allow-Origin: *`.
|
|
219
|
+
*/
|
|
220
|
+
export declare function fetchHelperDirect(consoleUrl: string, timeoutMs?: number): Promise<HelperStatus | null>;
|
|
221
|
+
/**
|
|
222
|
+
* Fetch the helper's copy-ready failure report.
|
|
223
|
+
*
|
|
224
|
+
* Served by the recovery console (a different port, CORS-open), so it is
|
|
225
|
+
* reachable exactly when the main server is not — which is when a failure
|
|
226
|
+
* report matters.
|
|
227
|
+
*/
|
|
228
|
+
export declare function fetchHelperReport(consoleUrl: string, timeoutMs?: number): Promise<string>;
|
|
229
|
+
/** Ask the helper (direct) to relaunch after a failure. */
|
|
230
|
+
export declare function requestHelperRetry(consoleUrl: string, timeoutMs?: number): Promise<boolean>;
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import type { Context as ClientContext } from '@deepseek-ai/cordis';
|
|
2
|
+
/** Required services. */
|
|
3
|
+
export declare const inject: string[];
|
|
4
|
+
/**
|
|
5
|
+
* Register the settings card, mount the sidebar entry and the overlay.
|
|
6
|
+
* @param ctx - client root context.
|
|
7
|
+
*/
|
|
8
|
+
export declare function apply(ctx: ClientContext): void;
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* dsh-restart — the browser-side restart state machine.
|
|
3
|
+
*
|
|
4
|
+
* One module-level store, subscribed to by every surface (settings card,
|
|
5
|
+
* sidebar popover, full-screen overlay). A restart is a process that outlives
|
|
6
|
+
* the page it started from, so the state is mirrored into sessionStorage: if
|
|
7
|
+
* the tab reloads mid-restart, the overlay picks the wait back up instead of
|
|
8
|
+
* leaving the user on a dead page with no explanation.
|
|
9
|
+
*
|
|
10
|
+
* The lifecycle:
|
|
11
|
+
*
|
|
12
|
+
* requesting ──POST /api/dsh-restart/restart──▶ waiting
|
|
13
|
+
* waiting ──probe /api/dsh-restart/probe every N ms──▶ ready ──▶ location.reload()
|
|
14
|
+
* waiting ──helper reports failure──▶ failed (keeps probing; the helper can
|
|
15
|
+
* be retried from the overlay)
|
|
16
|
+
*/
|
|
17
|
+
import { type HelperStatus, type RestartAck, type RestartConfig } from './api.ts';
|
|
18
|
+
/** Restart phase. */
|
|
19
|
+
export type Phase = 'idle' | 'requesting' | 'waiting' | 'ready' | 'failed';
|
|
20
|
+
/** What every surface renders from. */
|
|
21
|
+
export interface RestartState {
|
|
22
|
+
phase: Phase;
|
|
23
|
+
/** When this restart started (ms epoch). */
|
|
24
|
+
startedAt: number;
|
|
25
|
+
/** Wall-clock while waiting, refreshed by the ticker. */
|
|
26
|
+
elapsedMs: number;
|
|
27
|
+
/** Recovery console base URL ('' until the host answered). */
|
|
28
|
+
fallbackUrl: string;
|
|
29
|
+
/** Port DSH was serving on when the restart was requested. */
|
|
30
|
+
port: number;
|
|
31
|
+
/** Log file the new host writes to. */
|
|
32
|
+
logFile: string;
|
|
33
|
+
/** Human-readable failure text ('' when nothing failed). */
|
|
34
|
+
error: string;
|
|
35
|
+
/** Progress note for the overlay. */
|
|
36
|
+
note: string;
|
|
37
|
+
/** Live helper state (null when the helper is not reachable). */
|
|
38
|
+
helper: HelperStatus | null;
|
|
39
|
+
/** The host's acknowledgement of the restart request. */
|
|
40
|
+
ack: RestartAck | null;
|
|
41
|
+
/** Effective plugin config, once the status endpoint answered. */
|
|
42
|
+
config: RestartConfig | null;
|
|
43
|
+
/** Set when the page is about to reload itself. */
|
|
44
|
+
reloadAt: number | null;
|
|
45
|
+
/** Who asked for the restart. */
|
|
46
|
+
source: string;
|
|
47
|
+
/** Why (free text, recorded in the host's history). */
|
|
48
|
+
reason: string;
|
|
49
|
+
/** True while the helper is being asked to try again. */
|
|
50
|
+
retrying: boolean;
|
|
51
|
+
}
|
|
52
|
+
/** Current snapshot (stable identity between mutations). */
|
|
53
|
+
export declare function getState(): RestartState;
|
|
54
|
+
/** Subscribe to state changes. */
|
|
55
|
+
export declare function subscribe(listener: () => void): () => void;
|
|
56
|
+
/** Load the config once so the reconnect follows the user's preferences. */
|
|
57
|
+
export declare function refreshConfig(): Promise<RestartConfig | null>;
|
|
58
|
+
/**
|
|
59
|
+
* Start a restart and stay on top of it.
|
|
60
|
+
* @param reason - free-text reason recorded in the host's history.
|
|
61
|
+
* @param source - who asked (the panel passes 'web').
|
|
62
|
+
*/
|
|
63
|
+
export declare function startRestart(reason?: string, source?: string): Promise<void>;
|
|
64
|
+
/** Probe right now (the overlay's "立即重试" button). */
|
|
65
|
+
export declare function checkNow(): Promise<void>;
|
|
66
|
+
/** Ask the helper to relaunch after a failed boot. */
|
|
67
|
+
export declare function retryBoot(): Promise<void>;
|
|
68
|
+
/** Dismiss the overlay without touching the server. */
|
|
69
|
+
export declare function dismiss(): void;
|
|
70
|
+
/** Forget a finished restart (keeps config). */
|
|
71
|
+
export declare function reset(): void;
|
|
72
|
+
/**
|
|
73
|
+
* Resume a restart that was in flight when the page went away.
|
|
74
|
+
*
|
|
75
|
+
* Called once at mount by the overlay; a no-op when nothing is pending.
|
|
76
|
+
*/
|
|
77
|
+
export declare function resumeIfPending(): void;
|
|
78
|
+
/** React binding: re-renders the caller whenever the restart state changes. */
|
|
79
|
+
export declare function useRestartState(): RestartState;
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* dsh-restart — plugin config, on-disk layout, and restart history.
|
|
3
|
+
*
|
|
4
|
+
* Everything this plugin owns lives under one directory
|
|
5
|
+
* (`~/.dsh/dsh-restart` by default, override with `DSH_RESTART_HOME`):
|
|
6
|
+
*
|
|
7
|
+
* config.json plugin settings (0600)
|
|
8
|
+
* history.json one record per requested restart (newest first)
|
|
9
|
+
* status.json written by the detached helper — live restart state
|
|
10
|
+
* pending-spec.json handoff payload for the next helper run
|
|
11
|
+
* logs/<stamp>.log stdout+stderr of one restarted host
|
|
12
|
+
*/
|
|
13
|
+
/** Default plugin settings; every field is user-overridable. */
|
|
14
|
+
export interface RestartConfig {
|
|
15
|
+
/** Master switch — when false the routes and tools stay unmounted. */
|
|
16
|
+
enabled: boolean;
|
|
17
|
+
/** Announce the plugin (tools + behaviour) in the agent system prompt. */
|
|
18
|
+
announceToAgent: boolean;
|
|
19
|
+
/** Where the panel entry lives in the GUI. */
|
|
20
|
+
entry: 'sidebar' | 'ball' | 'both' | 'off';
|
|
21
|
+
/**
|
|
22
|
+
* How the host is restarted: `auto` asks launchd when the host turns out to be
|
|
23
|
+
* launchd-managed (otherwise it relaunches itself), `helper` always relaunches,
|
|
24
|
+
* `launchd` forces the launchctl path.
|
|
25
|
+
*/
|
|
26
|
+
restartMode: 'auto' | 'helper' | 'launchd';
|
|
27
|
+
/** Port for the detached recovery console (tries +9 further when busy). */
|
|
28
|
+
fallbackPort: number;
|
|
29
|
+
/** How long the new host may take to answer before the attempt fails. */
|
|
30
|
+
bootTimeoutMs: number;
|
|
31
|
+
/** Launch attempts per restart request (1 = no automatic retry). */
|
|
32
|
+
maxAttempts: number;
|
|
33
|
+
/** Grace period after SIGTERM before the helper SIGKILLs the old host. */
|
|
34
|
+
killGraceMs: number;
|
|
35
|
+
/** How long to wait for the old host to release its port. */
|
|
36
|
+
portFreeTimeoutMs: number;
|
|
37
|
+
/** How long the helper keeps its console up after success before exiting. */
|
|
38
|
+
lingerMs: number;
|
|
39
|
+
/** Lines of boot log kept for display. */
|
|
40
|
+
logLines: number;
|
|
41
|
+
/** Auto-reload the page once the new host answers. */
|
|
42
|
+
autoReload: boolean;
|
|
43
|
+
/** Show the full-screen restart overlay while waiting. */
|
|
44
|
+
showOverlay: boolean;
|
|
45
|
+
/** Reconnect probe interval (ms). */
|
|
46
|
+
probeIntervalMs: number;
|
|
47
|
+
/** Restart records kept in history.json. */
|
|
48
|
+
historyLimit: number;
|
|
49
|
+
}
|
|
50
|
+
/** Shipped defaults. */
|
|
51
|
+
export declare const DEFAULT_CONFIG: RestartConfig;
|
|
52
|
+
/** One completed restart request, as rendered in the panel history list. */
|
|
53
|
+
export interface RestartRecord {
|
|
54
|
+
at: string;
|
|
55
|
+
/** Who asked: the web panel, an agent tool, or another caller. */
|
|
56
|
+
source: string;
|
|
57
|
+
reason: string;
|
|
58
|
+
oldPid: number;
|
|
59
|
+
helperPid: number | null;
|
|
60
|
+
port: number;
|
|
61
|
+
logFile: string;
|
|
62
|
+
statusFile: string;
|
|
63
|
+
/** Outcome, filled in by the panel/host once the new process is up. */
|
|
64
|
+
outcome?: string;
|
|
65
|
+
}
|
|
66
|
+
/**
|
|
67
|
+
* Plugin directory.
|
|
68
|
+
*
|
|
69
|
+
* Resolution order mirrors DSH's own: an explicit `DSH_RESTART_HOME` (used by
|
|
70
|
+
* the tests and by anyone running a throwaway instance), then DSH's own
|
|
71
|
+
* `DSH_HOME` when it is set — a launcher or a rescue capsule may relocate the
|
|
72
|
+
* whole home — and only then the conventional `~/.dsh`. Hardcoding `~/.dsh`
|
|
73
|
+
* would silently write a second, wrong home on such setups.
|
|
74
|
+
*/
|
|
75
|
+
export declare function restartHome(): string;
|
|
76
|
+
/** Settings file (override with DSH_RESTART_CONFIG). */
|
|
77
|
+
export declare function configPath(): string;
|
|
78
|
+
/** Restart history file. */
|
|
79
|
+
export declare function historyPath(): string;
|
|
80
|
+
/** Live helper status file. */
|
|
81
|
+
export declare function statusPath(): string;
|
|
82
|
+
/** Handoff payload for the helper. */
|
|
83
|
+
export declare function specPath(): string;
|
|
84
|
+
/** Directory holding one log file per restarted host. */
|
|
85
|
+
export declare function logsDir(): string;
|
|
86
|
+
/**
|
|
87
|
+
* The detached helper shipped with this package. Resolved from the module URL
|
|
88
|
+
* so it works both from a local checkout and from an installed copy.
|
|
89
|
+
*/
|
|
90
|
+
export declare function helperPath(): string;
|
|
91
|
+
/** Normalize a partial config against the defaults (never throws). */
|
|
92
|
+
export declare function normalizeConfig(patch: Partial<RestartConfig> | undefined, base?: RestartConfig): RestartConfig;
|
|
93
|
+
/** The effective config (defaults + file), plus where it came from. */
|
|
94
|
+
export declare function loadConfig(): Promise<{
|
|
95
|
+
config: RestartConfig;
|
|
96
|
+
exists: boolean;
|
|
97
|
+
file: string;
|
|
98
|
+
}>;
|
|
99
|
+
/** Merge a patch into the stored config and return the fresh value. */
|
|
100
|
+
export declare function saveConfig(patch: Partial<RestartConfig>): Promise<RestartConfig>;
|
|
101
|
+
/** Delete the stored config (back to shipped defaults). */
|
|
102
|
+
export declare function resetConfig(): Promise<RestartConfig>;
|
|
103
|
+
/** Append one restart record (newest first, bounded by historyLimit). */
|
|
104
|
+
export declare function appendHistory(record: RestartRecord, limit: number): Promise<void>;
|
|
105
|
+
/** Read the restart history (newest first). */
|
|
106
|
+
export declare function readHistory(limit?: number): Promise<RestartRecord[]>;
|
|
107
|
+
/** Ensure the plugin directories exist (0600 where it matters). */
|
|
108
|
+
export declare function ensureLayout(): Promise<void>;
|
|
109
|
+
/**
|
|
110
|
+
* Synchronous config read, for the plugin's apply() path.
|
|
111
|
+
*
|
|
112
|
+
* Mounting must stay synchronous: cordis effects have to be created inside the
|
|
113
|
+
* plugin's own apply scope, so the roster cannot wait on a promise.
|
|
114
|
+
*/
|
|
115
|
+
export declare function loadConfigSync(): {
|
|
116
|
+
config: RestartConfig;
|
|
117
|
+
exists: boolean;
|
|
118
|
+
file: string;
|
|
119
|
+
};
|
|
120
|
+
/**
|
|
121
|
+
* Materialize the config file on first run so the settings become discoverable
|
|
122
|
+
* and editable; the composition row only seeds it, the file is authoritative.
|
|
123
|
+
* @param seed - values from the plugin row (enabled / announceToAgent / …).
|
|
124
|
+
*/
|
|
125
|
+
export declare function seedConfigSync(seed: Partial<RestartConfig>): boolean;
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* dsh-restart — one-click restart for DeepSeek Harness. Host half.
|
|
3
|
+
*
|
|
4
|
+
* Installing or updating a plugin changes host-side code, and only a fresh
|
|
5
|
+
* `dsh web` process picks it up. This plugin makes that a button: the web
|
|
6
|
+
* panel (and the agent, through dsh_restart) hands the relaunch to a detached
|
|
7
|
+
* helper, the page reconnects by itself, and if the new host fails to boot the
|
|
8
|
+
* helper's recovery console shows the error instead of a dead tab.
|
|
9
|
+
*
|
|
10
|
+
* Mounts:
|
|
11
|
+
* - /api/dsh-restart/* routes (status, probe, restart, logs, history, config)
|
|
12
|
+
* - dsh_restart / dsh_restart_status agent tools
|
|
13
|
+
* - one system-prompt section announcing the capability
|
|
14
|
+
* - a browser half (lib/client.js): settings card, sidebar entry, overlay
|
|
15
|
+
*
|
|
16
|
+
* No DSH source changes: everything rides public plugin surfaces.
|
|
17
|
+
*/
|
|
18
|
+
import type { Context } from '@deepseek-ai/cordis';
|
|
19
|
+
import { type RestartConfig } from './config.ts';
|
|
20
|
+
/** Stable cordis plugin name. */
|
|
21
|
+
export declare const name = "dsh-restart";
|
|
22
|
+
/** Services required before the plugin surfaces can mount. */
|
|
23
|
+
export declare const inject: string[];
|
|
24
|
+
/** Model-facing announcement: plugin presence, capabilities, and limits. */
|
|
25
|
+
export declare const RESTART_GUIDANCE: string;
|
|
26
|
+
/** Plugin config, read from the composition row (the JSON file wins once it exists). */
|
|
27
|
+
export type Config = Partial<RestartConfig>;
|
|
28
|
+
/**
|
|
29
|
+
* Mount the restart routes, tools and announcement.
|
|
30
|
+
* @param ctx - host plugin context carrying tools/systemPrompt/webServer.
|
|
31
|
+
* @param config - plugin config from the composition row (seeds the JSON file).
|
|
32
|
+
*/
|
|
33
|
+
export declare function apply(ctx: Context, config?: Config): void;
|
|
34
|
+
/** Re-exports for host consumers and the tests. */
|
|
35
|
+
export { DEFAULT_CONFIG, appendHistory, configPath, ensureLayout, helperPath, historyPath, loadConfig, loadConfigSync, logsDir, normalizeConfig, readHistory, resetConfig, restartHome, saveConfig, seedConfigSync, specPath, statusPath, type RestartConfig, type RestartRecord, } from './config.ts';
|
|
36
|
+
export { buildSpec, hostInfo, isAlive, launchSignature, launchdInfo, listLogs, newestLogFile, readHelperStatus, requestRestart, scheduleSelfExit, tailFile, type HelperStatus, type HostInfo, type LogTail, type RestartOutcome, type RestartSpec, } from './restart.ts';
|
|
37
|
+
export { detectLaunchd, detectLaunchdFor, kickCommand, labelForPid, kickstart, readLaunchdLog, type LaunchdInfo, } from './launchd.ts';
|
|
38
|
+
export { RESTART_API, makeRoutes, type RestartStatusPayload, type RouteContext } from './routes.ts';
|
|
39
|
+
export { buildTools, restartStatusTool, restartTool, type ToolContext } from './tools.ts';
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* dsh-restart — macOS launchd awareness.
|
|
3
|
+
*
|
|
4
|
+
* A DSH host is often managed by a launchd job (`com.dsh.web` with
|
|
5
|
+
* `KeepAlive: true` on this machine). That changes the correct restart from
|
|
6
|
+
* "relaunch it ourselves" to "ask launchd to restart it": a helper that spawns
|
|
7
|
+
* its own child would race the job for the listening port, and whichever loses
|
|
8
|
+
* dies with `EADDRINUSE`.
|
|
9
|
+
*
|
|
10
|
+
* So the plugin detects the situation and switches strategy:
|
|
11
|
+
*
|
|
12
|
+
* launchd-managed → `launchctl kickstart -k gui/<uid>/<label>` (the job comes
|
|
13
|
+
* back with the plist's own cwd/env/argv) + the helper runs
|
|
14
|
+
* in observe mode, following the plist's stdout/stderr.
|
|
15
|
+
* anything else → the helper relaunches the exact same command itself.
|
|
16
|
+
*
|
|
17
|
+
* Detection is a pure read of `XPC_SERVICE_NAME` (set by launchd on every
|
|
18
|
+
* process it starts) confirmed by `launchctl print`, and every failure mode
|
|
19
|
+
* falls back to the self-relaunch path — a host that is not launchd-managed must
|
|
20
|
+
* never end up depending on launchctl.
|
|
21
|
+
*/
|
|
22
|
+
/** What we know about the managing launchd job. */
|
|
23
|
+
export interface LaunchdInfo {
|
|
24
|
+
label: string;
|
|
25
|
+
uid: number;
|
|
26
|
+
domain: string;
|
|
27
|
+
plistPath: string;
|
|
28
|
+
/** Present when the plist could be parsed. */
|
|
29
|
+
stdoutPath: string;
|
|
30
|
+
stderrPath: string;
|
|
31
|
+
/** `launchctl print` state, e.g. "running". */
|
|
32
|
+
state: string;
|
|
33
|
+
pid: number | null;
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* Which launchd job owns a pid.
|
|
37
|
+
*
|
|
38
|
+
* `launchctl list` prints `PID Status Label`, so matching our own pid is the
|
|
39
|
+
* authoritative answer — and the only reliable one: **Node rewrites
|
|
40
|
+
* `XPC_SERVICE_NAME` to `0` in `process.env`**, so the environment variable
|
|
41
|
+
* launchd set (still visible in the kernel environment, e.g. `ps eww -p <pid>`)
|
|
42
|
+
* cannot be read from inside the process. The env var is kept only as a
|
|
43
|
+
* secondary signal for the case where `launchctl list` is unavailable.
|
|
44
|
+
*
|
|
45
|
+
* @param pid - the process to look up.
|
|
46
|
+
* @returns the job label, or null when the process is not a launchd job.
|
|
47
|
+
*/
|
|
48
|
+
export declare function labelForPid(pid: number): Promise<string | null>;
|
|
49
|
+
/**
|
|
50
|
+
* Detect a managing launchd job for this process.
|
|
51
|
+
*
|
|
52
|
+
* Returns null on non-macOS, when the process is not launchd-managed, or when
|
|
53
|
+
* `launchctl print` cannot see the job — every one of those means the caller
|
|
54
|
+
* must fall back to the self-relaunch strategy.
|
|
55
|
+
*/
|
|
56
|
+
export declare function detectLaunchd(): Promise<LaunchdInfo | null>;
|
|
57
|
+
/**
|
|
58
|
+
* Detect the launchd job owning an arbitrary pid.
|
|
59
|
+
*
|
|
60
|
+
* The plugin calls it for its own pid (the host is the job); tooling outside the
|
|
61
|
+
* host — a one-off handoff script, a test — needs it for a *different* pid, and
|
|
62
|
+
* in that case `process.env.XPC_SERVICE_NAME` says nothing useful.
|
|
63
|
+
*
|
|
64
|
+
* @param pid - the process to look up.
|
|
65
|
+
*/
|
|
66
|
+
export declare function detectLaunchdFor(pid: number): Promise<LaunchdInfo | null>;
|
|
67
|
+
/**
|
|
68
|
+
* Ask launchd to restart the job (`kickstart -k`).
|
|
69
|
+
*
|
|
70
|
+
* @param info - the detected job.
|
|
71
|
+
* @returns ok plus launchctl's own output when it refused.
|
|
72
|
+
*/
|
|
73
|
+
export declare function kickstart(info: LaunchdInfo): Promise<{
|
|
74
|
+
ok: boolean;
|
|
75
|
+
error: string;
|
|
76
|
+
}>;
|
|
77
|
+
/** The command a retry should re-run for a managed host. */
|
|
78
|
+
export declare function kickCommand(info: LaunchdInfo): string[];
|
|
79
|
+
/** Read the tail of a launchd log file ('' when missing). */
|
|
80
|
+
export declare function readLaunchdLog(file: string, lines: number): Promise<string>;
|