@restregular/dsh-powerup 0.0.0-stage → 0.2.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +103 -2
- package/client.js +2543 -0
- package/cordis.patch.yml +7 -0
- package/icon.svg +22 -0
- package/index.js +119 -0
- package/lib/restart-helper.cjs +196 -0
- package/lib/restart.js +136 -0
- package/locale/en.json +6 -0
- package/locale/zh.json +6 -0
- package/package.json +70 -4
package/cordis.patch.yml
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
# dsh-powerup bundle patch: inserts the Host row. The Client half is resolved
|
|
2
|
+
# from the same package through `dsh.client` in package.json; `name` must be
|
|
3
|
+
# byte-for-byte the package name because the client module graph locates the
|
|
4
|
+
# package by the Loader row name.
|
|
5
|
+
- insert:
|
|
6
|
+
- id: dsh-powerup
|
|
7
|
+
name: '@restregular/dsh-powerup'
|
package/icon.svg
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
<svg xmlns="http://www.w3.org/2000/svg" width="256" height="256" viewBox="0 0 256 256" fill="none">
|
|
2
|
+
<defs>
|
|
3
|
+
<linearGradient id="dsh-powerup-bg" x1="32" y1="24" x2="224" y2="232" gradientUnits="userSpaceOnUse">
|
|
4
|
+
<stop stop-color="#8B5CF6"/>
|
|
5
|
+
<stop offset="0.55" stop-color="#6D3BE0"/>
|
|
6
|
+
<stop offset="1" stop-color="#4C1D95"/>
|
|
7
|
+
</linearGradient>
|
|
8
|
+
<radialGradient id="dsh-powerup-glow" cx="0" cy="0" r="1" gradientUnits="userSpaceOnUse" gradientTransform="translate(80 56) rotate(45) scale(190)">
|
|
9
|
+
<stop stop-color="#FFFFFF" stop-opacity="0.26"/>
|
|
10
|
+
<stop offset="1" stop-color="#FFFFFF" stop-opacity="0"/>
|
|
11
|
+
</radialGradient>
|
|
12
|
+
<linearGradient id="dsh-powerup-chevron" x1="78" y1="56" x2="178" y2="178" gradientUnits="userSpaceOnUse">
|
|
13
|
+
<stop stop-color="#FFFFFF"/>
|
|
14
|
+
<stop offset="1" stop-color="#D8FFF4"/>
|
|
15
|
+
</linearGradient>
|
|
16
|
+
</defs>
|
|
17
|
+
<rect x="16" y="16" width="224" height="224" rx="56" fill="url(#dsh-powerup-bg)"/>
|
|
18
|
+
<rect x="16" y="16" width="224" height="224" rx="56" fill="url(#dsh-powerup-glow)"/>
|
|
19
|
+
<path d="M78 102 128 56l50 46" stroke="url(#dsh-powerup-chevron)" stroke-width="22" stroke-linecap="round" stroke-linejoin="round"/>
|
|
20
|
+
<path d="M78 178 128 132l50 46" stroke="#9DF2DB" stroke-width="22" stroke-linecap="round" stroke-linejoin="round"/>
|
|
21
|
+
<path d="M96 212h64" stroke="#FFC94D" stroke-width="10" stroke-linecap="round"/>
|
|
22
|
+
</svg>
|
package/index.js
ADDED
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Host half of dsh-powerup.
|
|
3
|
+
*
|
|
4
|
+
* The plugin is primarily a Client UI upgrade. This Host entry exists for one
|
|
5
|
+
* operation the browser cannot perform itself: restarting the whole DSH
|
|
6
|
+
* desktop application after a plugin update. It exposes a small loopback-only
|
|
7
|
+
* route under `/dsh-powerup/api`:
|
|
8
|
+
*
|
|
9
|
+
* GET /dsh-powerup/api/restart → { ok, supported, reason? }
|
|
10
|
+
* POST /dsh-powerup/api/restart → schedules the restart, then answers
|
|
11
|
+
*
|
|
12
|
+
* The Client half drives plugin installation through the built-in
|
|
13
|
+
* `pluginManager` Remote; only the process restart needs Host code.
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
import { describeRestart, requestRestart } from './lib/restart.js';
|
|
17
|
+
|
|
18
|
+
/** Prefix owned by this plugin. */
|
|
19
|
+
const ROUTE = '/dsh-powerup/api';
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* @param {import('node:http').ServerResponse} res
|
|
23
|
+
* @param {unknown} payload
|
|
24
|
+
* @param {number} status
|
|
25
|
+
*/
|
|
26
|
+
function sendJson(res, payload, status = 200) {
|
|
27
|
+
if (res.headersSent) return;
|
|
28
|
+
const body = JSON.stringify(payload);
|
|
29
|
+
res.writeHead(status, {
|
|
30
|
+
'content-type': 'application/json; charset=utf-8',
|
|
31
|
+
'cache-control': 'no-store',
|
|
32
|
+
'content-length': String(Buffer.byteLength(body)),
|
|
33
|
+
});
|
|
34
|
+
res.end(body);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* The request came from this machine.
|
|
39
|
+
* @param {import('node:http').IncomingMessage} req
|
|
40
|
+
* @returns {boolean}
|
|
41
|
+
*/
|
|
42
|
+
function isLoopback(req) {
|
|
43
|
+
const address = req.socket?.remoteAddress ?? '';
|
|
44
|
+
return address === '127.0.0.1'
|
|
45
|
+
|| address === '::1'
|
|
46
|
+
|| address.startsWith('::ffff:127.')
|
|
47
|
+
|| address === '';
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* A browser request carries an Origin matching Host. The desktop document
|
|
52
|
+
* proxy strips both headers, which is accepted here — a browser cannot produce
|
|
53
|
+
* that shape, and a loopback peer without an Origin is the desktop shell.
|
|
54
|
+
* @param {import('node:http').IncomingMessage} req
|
|
55
|
+
* @returns {boolean}
|
|
56
|
+
*/
|
|
57
|
+
function sameOrigin(req) {
|
|
58
|
+
const origin = req.headers.origin;
|
|
59
|
+
if (typeof origin !== 'string' || origin === '') return true;
|
|
60
|
+
try {
|
|
61
|
+
return new URL(origin).host === req.headers.host;
|
|
62
|
+
} catch {
|
|
63
|
+
return false;
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* @param {import('node:http').IncomingMessage} req
|
|
69
|
+
* @param {import('node:http').ServerResponse} res
|
|
70
|
+
*/
|
|
71
|
+
async function handle(req, res) {
|
|
72
|
+
if (!isLoopback(req) || !sameOrigin(req)) {
|
|
73
|
+
sendJson(res, { ok: false, error: 'forbidden' }, 403);
|
|
74
|
+
return;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
const url = new URL(req.url ?? ROUTE, 'http://localhost');
|
|
78
|
+
const op = url.pathname.slice(ROUTE.length).replace(/^\/+/u, '') || url.searchParams.get('op') || '';
|
|
79
|
+
if (op !== '' && op !== 'restart') {
|
|
80
|
+
sendJson(res, { ok: false, error: 'not-found' }, 404);
|
|
81
|
+
return;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
if (req.method === 'GET') {
|
|
85
|
+
sendJson(res, { ok: true, action: 'restart', ...describeRestart() });
|
|
86
|
+
return;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
if (req.method === 'POST') {
|
|
90
|
+
try {
|
|
91
|
+
const scheduled = requestRestart();
|
|
92
|
+
sendJson(res, { ok: true, restarting: true, ...scheduled });
|
|
93
|
+
} catch (error) {
|
|
94
|
+
sendJson(res, { ok: false, code: error?.code, error: String(error?.message ?? error) }, 409);
|
|
95
|
+
}
|
|
96
|
+
return;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
sendJson(res, { ok: false, error: 'method-not-allowed' }, 405);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* Mount the restart route once the Web server exists.
|
|
104
|
+
* @param {import('@deepseek-ai/cordis').Context} ctx
|
|
105
|
+
*/
|
|
106
|
+
export function apply(ctx) {
|
|
107
|
+
if (ctx === undefined || typeof ctx.inject !== 'function') return;
|
|
108
|
+
ctx.inject(['webServer'], (scope) => {
|
|
109
|
+
const webServer = scope.get('webServer');
|
|
110
|
+
if (webServer === undefined || typeof webServer.register !== 'function') return;
|
|
111
|
+
scope.effect(
|
|
112
|
+
() => webServer.register({ kind: 'prefix', path: ROUTE, handler: handle }),
|
|
113
|
+
'dsh-powerup: restart route',
|
|
114
|
+
);
|
|
115
|
+
});
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/** Exported for tests only; the Host never reads this. */
|
|
119
|
+
export const __test = { handle, isLoopback, sameOrigin, ROUTE };
|
|
@@ -0,0 +1,196 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* dsh-powerup — detached restart helper (runs in Electron's Node mode).
|
|
3
|
+
*
|
|
4
|
+
* Started by `lib/restart.js` with `detached: true`. It waits for the UI
|
|
5
|
+
* notice and the HTTP response to settle, ends the Electron main process, waits
|
|
6
|
+
* for the old Host to exit, then relaunches the same executable.
|
|
7
|
+
*
|
|
8
|
+
* Arguments:
|
|
9
|
+
* 1. exe desktop executable (`process.execPath`)
|
|
10
|
+
* 2. mainPid Electron main process pid (the Host's parent)
|
|
11
|
+
* 3. hostPid this Host process pid
|
|
12
|
+
* 4. appDir application directory, used as the new process cwd
|
|
13
|
+
* 5. argsJson optional JSON array of launch arguments (normally empty)
|
|
14
|
+
* 6. mode `restart` (default); `shutdown` is reserved for future use
|
|
15
|
+
* 7. delayMs grace period before acting (default 1500 ms)
|
|
16
|
+
*
|
|
17
|
+
* Environment:
|
|
18
|
+
* DSH_POWERUP_RESTART_LOG optional path for a one-line run log.
|
|
19
|
+
*
|
|
20
|
+
* Adapted from dsh-pocket's restart helper.
|
|
21
|
+
*
|
|
22
|
+
* @module dsh-powerup/restart-helper
|
|
23
|
+
*/
|
|
24
|
+
|
|
25
|
+
'use strict';
|
|
26
|
+
|
|
27
|
+
const { spawn } = require('node:child_process');
|
|
28
|
+
const fs = require('node:fs');
|
|
29
|
+
|
|
30
|
+
/** Wait for the UI notice and the HTTP response before ending the main process. */
|
|
31
|
+
const RESPONSE_GRACE_MS = 1500;
|
|
32
|
+
/** Upper bound for the old Host's graceful shutdown. */
|
|
33
|
+
const HOST_EXIT_TIMEOUT_MS = 15000;
|
|
34
|
+
/** Let the OS release the single-instance lock and file handles. */
|
|
35
|
+
const RELAUNCH_DELAY_MS = 1200;
|
|
36
|
+
|
|
37
|
+
const sleep = (ms) => new Promise((resolve) => {
|
|
38
|
+
setTimeout(resolve, ms);
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Whether a process is alive; an access-denied answer counts as alive.
|
|
43
|
+
* @param {number} pid
|
|
44
|
+
* @returns {boolean}
|
|
45
|
+
*/
|
|
46
|
+
function alive(pid) {
|
|
47
|
+
if (!Number.isInteger(pid) || pid <= 0) return false;
|
|
48
|
+
try {
|
|
49
|
+
process.kill(pid, 0);
|
|
50
|
+
return true;
|
|
51
|
+
} catch (error) {
|
|
52
|
+
return error !== null && typeof error === 'object' && error.code === 'EPERM';
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Poll until the process exits or the deadline passes.
|
|
58
|
+
* @param {number} pid
|
|
59
|
+
* @param {number} timeout
|
|
60
|
+
* @returns {Promise<boolean>}
|
|
61
|
+
*/
|
|
62
|
+
async function waitGone(pid, timeout) {
|
|
63
|
+
const deadline = Date.now() + timeout;
|
|
64
|
+
while (Date.now() < deadline) {
|
|
65
|
+
if (!alive(pid)) return true;
|
|
66
|
+
await sleep(120);
|
|
67
|
+
}
|
|
68
|
+
return !alive(pid);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Terminate a process; failures are logged, not thrown.
|
|
73
|
+
* @param {number} pid
|
|
74
|
+
* @param {(message: string) => void} log
|
|
75
|
+
*/
|
|
76
|
+
function terminate(pid, log) {
|
|
77
|
+
if (!alive(pid)) return;
|
|
78
|
+
try {
|
|
79
|
+
process.kill(pid);
|
|
80
|
+
log(`terminated pid=${String(pid)}`);
|
|
81
|
+
} catch (error) {
|
|
82
|
+
log(`terminate pid=${String(pid)} failed: ${String(error && error.message ? error.message : error)}`);
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* Parse the optional launch arguments.
|
|
88
|
+
* @param {string | undefined} raw
|
|
89
|
+
* @returns {string[]}
|
|
90
|
+
*/
|
|
91
|
+
function parseArgs(raw) {
|
|
92
|
+
if (typeof raw !== 'string' || raw.length === 0) return [];
|
|
93
|
+
try {
|
|
94
|
+
const value = JSON.parse(raw);
|
|
95
|
+
return Array.isArray(value) ? value.filter((item) => typeof item === 'string') : [];
|
|
96
|
+
} catch {
|
|
97
|
+
return [];
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* Parse the grace period, clamped to 0–10 seconds.
|
|
103
|
+
* @param {string | undefined} raw
|
|
104
|
+
* @returns {number}
|
|
105
|
+
*/
|
|
106
|
+
function parseDelay(raw) {
|
|
107
|
+
if (typeof raw !== 'string' || raw.length === 0) return RESPONSE_GRACE_MS;
|
|
108
|
+
const value = Number(raw);
|
|
109
|
+
if (!Number.isFinite(value) || value < 0) return RESPONSE_GRACE_MS;
|
|
110
|
+
return Math.min(value, 10000);
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
async function main() {
|
|
114
|
+
const [exe, mainPidRaw, hostPidRaw, appDir, argsRaw, modeRaw, delayRaw] = process.argv.slice(2);
|
|
115
|
+
const mode = modeRaw === 'shutdown' ? 'shutdown' : 'restart';
|
|
116
|
+
const delayMs = parseDelay(delayRaw);
|
|
117
|
+
const mainPid = Number(mainPidRaw);
|
|
118
|
+
const hostPid = Number(hostPidRaw);
|
|
119
|
+
const logPath = process.env.DSH_POWERUP_RESTART_LOG;
|
|
120
|
+
/** @param {string} message */
|
|
121
|
+
const log = (message) => {
|
|
122
|
+
if (typeof logPath !== 'string' || logPath.length === 0) return;
|
|
123
|
+
try {
|
|
124
|
+
fs.appendFileSync(logPath, `${new Date().toISOString()} ${message}\n`);
|
|
125
|
+
} catch {
|
|
126
|
+
/* A missing log never blocks the action. */
|
|
127
|
+
}
|
|
128
|
+
};
|
|
129
|
+
|
|
130
|
+
if (typeof exe !== 'string' || exe.length === 0) {
|
|
131
|
+
log('missing executable');
|
|
132
|
+
process.exit(2);
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
log(`scheduled mode=${mode} exe=${exe} main=${String(mainPid)} host=${String(hostPid)} delay=${String(delayMs)}`);
|
|
136
|
+
await sleep(delayMs);
|
|
137
|
+
|
|
138
|
+
// The Host exits on its own when the main process goes away; only the main
|
|
139
|
+
// process is terminated outright.
|
|
140
|
+
terminate(mainPid, log);
|
|
141
|
+
if (!(await waitGone(mainPid, 5000))) {
|
|
142
|
+
log('main still alive; retrying');
|
|
143
|
+
terminate(mainPid, log);
|
|
144
|
+
if (!(await waitGone(mainPid, 3000))) {
|
|
145
|
+
log('main did not exit; aborting');
|
|
146
|
+
process.exit(1);
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
if (!(await waitGone(hostPid, HOST_EXIT_TIMEOUT_MS))) {
|
|
150
|
+
log('host did not exit in time; terminating it');
|
|
151
|
+
terminate(hostPid, log);
|
|
152
|
+
await waitGone(hostPid, 3000);
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
if (mode === 'shutdown') {
|
|
156
|
+
log('shutdown complete; not relaunching');
|
|
157
|
+
process.exit(0);
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
await sleep(RELAUNCH_DELAY_MS);
|
|
161
|
+
|
|
162
|
+
const env = { ...process.env };
|
|
163
|
+
// These keep a new process in Node mode; the relaunched app must not inherit them.
|
|
164
|
+
delete env.ELECTRON_RUN_AS_NODE;
|
|
165
|
+
delete env.ELECTRON_NO_ATTACH_CONSOLE;
|
|
166
|
+
delete env.ELECTRON_ENABLE_LOGGING;
|
|
167
|
+
delete env.DSH_DESKTOP_NODE_EXECUTABLE;
|
|
168
|
+
delete env.DSH_POWERUP_RESTART_LOG;
|
|
169
|
+
|
|
170
|
+
const child = spawn(exe, parseArgs(argsRaw), {
|
|
171
|
+
cwd: typeof appDir === 'string' && appDir.length > 0 ? appDir : undefined,
|
|
172
|
+
env,
|
|
173
|
+
detached: true,
|
|
174
|
+
windowsHide: false,
|
|
175
|
+
stdio: 'ignore',
|
|
176
|
+
});
|
|
177
|
+
child.on('error', (error) => {
|
|
178
|
+
log(`spawn failed: ${String(error && error.message ? error.message : error)}`);
|
|
179
|
+
process.exit(1);
|
|
180
|
+
});
|
|
181
|
+
child.unref();
|
|
182
|
+
log(`launched pid=${String(child.pid)}`);
|
|
183
|
+
|
|
184
|
+
// Give spawn a moment to report a pid or an error, then exit.
|
|
185
|
+
await sleep(400);
|
|
186
|
+
process.exit(0);
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
main().catch((error) => {
|
|
190
|
+
try {
|
|
191
|
+
console.error(error);
|
|
192
|
+
} catch {
|
|
193
|
+
/* ignore */
|
|
194
|
+
}
|
|
195
|
+
process.exit(1);
|
|
196
|
+
});
|
package/lib/restart.js
ADDED
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* dsh-powerup — restart the whole DSH desktop app.
|
|
3
|
+
*
|
|
4
|
+
* The Host half runs inside the `dsh-desktop-host` child process, whose parent
|
|
5
|
+
* is the Electron main process. The desktop shell exposes shutdown and update
|
|
6
|
+
* controls over IPC but no "restart application" request, so this module
|
|
7
|
+
* starts a detached helper (`restart-helper.cjs`), lets the HTTP response
|
|
8
|
+
* settle, then ends the Electron main process. The helper waits for this Host
|
|
9
|
+
* to exit and relaunches the same executable.
|
|
10
|
+
*
|
|
11
|
+
* Only the Electron desktop build supports this. A plain `dsh web` process
|
|
12
|
+
* reports the reason instead, and the UI hides the action.
|
|
13
|
+
*
|
|
14
|
+
* The mechanism is adapted from dsh-pocket's restart module (same profile,
|
|
15
|
+
* verified on this machine).
|
|
16
|
+
*
|
|
17
|
+
* @module dsh-powerup/restart
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
import { spawn } from 'node:child_process';
|
|
21
|
+
import { existsSync, mkdirSync } from 'node:fs';
|
|
22
|
+
import { dirname, join } from 'node:path';
|
|
23
|
+
import { homedir } from 'node:os';
|
|
24
|
+
import { fileURLToPath } from 'node:url';
|
|
25
|
+
|
|
26
|
+
/** Helper shipped beside this module. */
|
|
27
|
+
const HELPER_PATH = fileURLToPath(new URL('./restart-helper.cjs', import.meta.url));
|
|
28
|
+
|
|
29
|
+
/** Let the UI paint its "restarting" state and the HTTP response settle. */
|
|
30
|
+
const ACTION_DELAY_MS = 1500;
|
|
31
|
+
|
|
32
|
+
/** One helper per process, even if the button is clicked twice. */
|
|
33
|
+
let scheduled = false;
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* The DSH home directory, matching the launcher's `DSH_HOME` handling.
|
|
37
|
+
* @returns {string}
|
|
38
|
+
*/
|
|
39
|
+
function dshHome() {
|
|
40
|
+
const configured = process.env.DSH_HOME;
|
|
41
|
+
if (typeof configured === 'string' && configured.trim().length > 0) return configured.trim();
|
|
42
|
+
return join(homedir(), '.dsh');
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Whether this runtime can restart the whole application.
|
|
47
|
+
* @returns {{ supported: boolean, reason?: string }}
|
|
48
|
+
*/
|
|
49
|
+
export function describeRestart() {
|
|
50
|
+
if (process.versions.electron === undefined) {
|
|
51
|
+
return {
|
|
52
|
+
supported: false,
|
|
53
|
+
reason: 'DSH is running as a command-line web process (dsh web); restart it from the terminal.',
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
if (typeof process.execPath !== 'string' || process.execPath.length === 0 || !existsSync(process.execPath)) {
|
|
57
|
+
return { supported: false, reason: 'The DSH executable could not be located.' };
|
|
58
|
+
}
|
|
59
|
+
const mainPid = process.ppid;
|
|
60
|
+
if (!Number.isInteger(mainPid) || mainPid <= 1) {
|
|
61
|
+
return { supported: false, reason: 'The DSH main process could not be located.' };
|
|
62
|
+
}
|
|
63
|
+
return { supported: true };
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Schedule a full application restart. The caller must finish its HTTP
|
|
68
|
+
* response; the helper waits {@link ACTION_DELAY_MS} before acting.
|
|
69
|
+
*
|
|
70
|
+
* @returns {{ supported: true, action: 'restart', pid: number | undefined, pending: boolean }}
|
|
71
|
+
* @throws {Error} with `code = 'restart-unsupported'` outside the desktop app.
|
|
72
|
+
*/
|
|
73
|
+
export function requestRestart() {
|
|
74
|
+
const capability = describeRestart();
|
|
75
|
+
if (!capability.supported) {
|
|
76
|
+
const error = new Error(capability.reason ?? 'This runtime cannot restart DSH.');
|
|
77
|
+
error.code = 'restart-unsupported';
|
|
78
|
+
throw error;
|
|
79
|
+
}
|
|
80
|
+
if (scheduled) return { supported: true, action: 'restart', pid: undefined, pending: true };
|
|
81
|
+
scheduled = true;
|
|
82
|
+
|
|
83
|
+
const executable = process.execPath;
|
|
84
|
+
const logPath = join(dshHome(), 'powerup', 'restart.log');
|
|
85
|
+
try {
|
|
86
|
+
mkdirSync(dirname(logPath), { recursive: true });
|
|
87
|
+
} catch {
|
|
88
|
+
/* The log directory is optional; the helper tolerates a missing path. */
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
let child;
|
|
92
|
+
try {
|
|
93
|
+
child = spawn(process.execPath, [
|
|
94
|
+
HELPER_PATH,
|
|
95
|
+
executable,
|
|
96
|
+
String(process.ppid),
|
|
97
|
+
String(process.pid),
|
|
98
|
+
dirname(executable),
|
|
99
|
+
'[]',
|
|
100
|
+
'restart',
|
|
101
|
+
String(ACTION_DELAY_MS),
|
|
102
|
+
], {
|
|
103
|
+
cwd: dirname(executable),
|
|
104
|
+
detached: true,
|
|
105
|
+
windowsHide: true,
|
|
106
|
+
stdio: 'ignore',
|
|
107
|
+
env: {
|
|
108
|
+
...process.env,
|
|
109
|
+
ELECTRON_RUN_AS_NODE: '1',
|
|
110
|
+
DSH_POWERUP_RESTART_LOG: logPath,
|
|
111
|
+
},
|
|
112
|
+
});
|
|
113
|
+
} catch (error) {
|
|
114
|
+
scheduled = false;
|
|
115
|
+
throw error;
|
|
116
|
+
}
|
|
117
|
+
if (child.pid === undefined) {
|
|
118
|
+
scheduled = false;
|
|
119
|
+
const error = new Error('The restart helper could not be started.');
|
|
120
|
+
error.code = 'restart-helper-failed';
|
|
121
|
+
throw error;
|
|
122
|
+
}
|
|
123
|
+
child.unref();
|
|
124
|
+
child.on('error', () => {
|
|
125
|
+
scheduled = false;
|
|
126
|
+
});
|
|
127
|
+
return { supported: true, action: 'restart', pid: child.pid, pending: false };
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/**
|
|
131
|
+
* Reset the one-shot latch; exported for tests only.
|
|
132
|
+
* @internal
|
|
133
|
+
*/
|
|
134
|
+
export function __resetScheduled() {
|
|
135
|
+
scheduled = false;
|
|
136
|
+
}
|
package/locale/en.json
ADDED
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
{
|
|
2
|
+
"meta": {
|
|
3
|
+
"title": "Power-Up",
|
|
4
|
+
"description": "Adds a right-click action menu to the Trajectory ledger: copy a record, copy a Markdown quote, quote it into the composer, open the record inspector, fork the session at that record, export the trajectory snapshot, and copy the session ID. The Plugins page gains an update control on every third-party installed plugin: choose a version from the registry, uninstall the old copy, reinstall through the Add plugin path, then enable and optionally restart DSH."
|
|
5
|
+
}
|
|
6
|
+
}
|
package/locale/zh.json
ADDED
package/package.json
CHANGED
|
@@ -1,6 +1,72 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@restregular/dsh-powerup",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"
|
|
5
|
-
"
|
|
6
|
-
|
|
3
|
+
"version": "0.2.1",
|
|
4
|
+
"description": "Power-Up — small, opt-in upgrades for DeepSeek Harness. Adds a right-click action menu to the Trajectory ledger and an update control for third-party plugins installed from the Plugins page, with version lookup, uninstall-and-reinstall through the Add plugin path, and an optional DSH restart. · 为 DeepSeek Harness 提供小型增强:在轨迹页面右键记录即可复制、引用、查看详情或从该记录 Fork;并在插件页为第三方已安装插件提供带版本查询的更新按钮,沿用「添加插件」的安装流程,并可重启 DSH 使更改生效。",
|
|
5
|
+
"icon": "./icon.svg",
|
|
6
|
+
"type": "module",
|
|
7
|
+
"main": "index.js",
|
|
8
|
+
"exports": {
|
|
9
|
+
".": "./index.js",
|
|
10
|
+
"./client": "./client.js",
|
|
11
|
+
"./locale/*.json": "./locale/*.json",
|
|
12
|
+
"./icon": "./icon.svg",
|
|
13
|
+
"./cordis.patch.yml": "./cordis.patch.yml",
|
|
14
|
+
"./package.json": "./package.json"
|
|
15
|
+
},
|
|
16
|
+
"files": [
|
|
17
|
+
"index.js",
|
|
18
|
+
"client.js",
|
|
19
|
+
"cordis.patch.yml",
|
|
20
|
+
"lib/restart.js",
|
|
21
|
+
"lib/restart-helper.cjs",
|
|
22
|
+
"locale/*.json",
|
|
23
|
+
"icon.svg",
|
|
24
|
+
"README.md",
|
|
25
|
+
"LICENSE"
|
|
26
|
+
],
|
|
27
|
+
"dsh": {
|
|
28
|
+
"bundle": {
|
|
29
|
+
"patch": "./cordis.patch.yml"
|
|
30
|
+
},
|
|
31
|
+
"client": {
|
|
32
|
+
"platform": "web",
|
|
33
|
+
"immediately": true,
|
|
34
|
+
"inject": [
|
|
35
|
+
"@deepseek-ai/dsh-client-locale",
|
|
36
|
+
"@deepseek-ai/dsh-client-ui-layout",
|
|
37
|
+
"@deepseek-ai/dsh-client-ui-conversation",
|
|
38
|
+
"@deepseek-ai/dsh-client-ui-session",
|
|
39
|
+
"@deepseek-ai/dsh-api-session-controller",
|
|
40
|
+
"@deepseek-ai/dsh-client-ui-workspace"
|
|
41
|
+
]
|
|
42
|
+
}
|
|
43
|
+
},
|
|
44
|
+
"repository": {
|
|
45
|
+
"type": "git",
|
|
46
|
+
"url": "git+https://github.com/RestRegular/dsh-powerup.git"
|
|
47
|
+
},
|
|
48
|
+
"homepage": "https://github.com/RestRegular/dsh-powerup#readme",
|
|
49
|
+
"bugs": {
|
|
50
|
+
"url": "https://github.com/RestRegular/dsh-powerup/issues"
|
|
51
|
+
},
|
|
52
|
+
"keywords": [
|
|
53
|
+
"dsh-plugin",
|
|
54
|
+
"deepseek-harness",
|
|
55
|
+
"deepseek",
|
|
56
|
+
"trajectory",
|
|
57
|
+
"context-menu",
|
|
58
|
+
"power-up"
|
|
59
|
+
],
|
|
60
|
+
"publishConfig": {
|
|
61
|
+
"access": "public",
|
|
62
|
+
"registry": "https://registry.npmjs.org/"
|
|
63
|
+
},
|
|
64
|
+
"engines": {
|
|
65
|
+
"node": ">=22.13.0"
|
|
66
|
+
},
|
|
67
|
+
"scripts": {
|
|
68
|
+
"test": "node test/smoke.mjs"
|
|
69
|
+
},
|
|
70
|
+
"license": "MIT",
|
|
71
|
+
"author": "RestRegular"
|
|
72
|
+
}
|