dsh-glass-plugin 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 +89 -2
- package/assets/approval.png +0 -0
- package/assets/capsule.png +0 -0
- package/assets/expanded.png +0 -0
- package/assets/peek.png +0 -0
- package/assets/settings.png +0 -0
- package/cordis.patch.yml +12 -0
- package/lib/index.js +340 -0
- package/package.json +45 -4
- package/screenshots.json +7 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 wfz2006
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
CHANGED
|
@@ -1,3 +1,90 @@
|
|
|
1
|
-
#
|
|
1
|
+
# dsh-glass-plugin
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
DSH bundle for **DSH Status Bar** — a floating glass status bar for DeepSeek Harness.
|
|
4
|
+
|
|
5
|
+
## What it does
|
|
6
|
+
|
|
7
|
+
The plugin mounts one host row in DSH. At boot it waits for the local web server,
|
|
8
|
+
asks the `connection` service for the authenticated host URL
|
|
9
|
+
(`connection.authenticatedUrl(origin)`), and starts the desktop app with that link:
|
|
10
|
+
|
|
11
|
+
```text
|
|
12
|
+
electron <app dir> --dsh-link=<authenticated url> --launcher-pid=<host pid>
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
Glass connects with that link on launch, so nothing has to be pasted. The link
|
|
16
|
+
travels only in the process arguments and is never logged — the plugin prints
|
|
17
|
+
`[dsh-glass-plugin] Glass desktop companion launched (authenticated link redacted)`.
|
|
18
|
+
The app rejects non-loopback links, credentials, hashes and sub-paths before it
|
|
19
|
+
touches the network. `--launcher-pid` makes the companion exit when the DSH
|
|
20
|
+
process it belongs to disappears; disposing the plugin kills the spawned tree.
|
|
21
|
+
|
|
22
|
+
If Glass is already running, the second launch hits its single-instance lock: the
|
|
23
|
+
existing window is focused and re-connected with the new link.
|
|
24
|
+
|
|
25
|
+
## Install
|
|
26
|
+
|
|
27
|
+
From npm, from the plugin market (once listed), or straight from the release
|
|
28
|
+
tarball:
|
|
29
|
+
|
|
30
|
+
```sh
|
|
31
|
+
# npm: ~1.6 MB, fetches the 155 MB runtime once on first launch
|
|
32
|
+
dsh plugin --profile web add dsh-glass-plugin
|
|
33
|
+
|
|
34
|
+
# release tarball: 155 MB, self-contained, nothing downloaded later
|
|
35
|
+
dsh plugin --profile web add https://github.com/wfz2006/dsh-status-bar/releases/latest/download/dsh-glass-plugin.tgz
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
Both end up as the same companion: the built desktop app plus an Electron
|
|
39
|
+
runtime. The tarball carries them; the npm package downloads the very same
|
|
40
|
+
tarball the first time DSH starts it (progress goes to the host log, roughly
|
|
41
|
+
`[dsh-glass-plugin] downloading the Glass runtime: 40% of 155 MB`) and unpacks it
|
|
42
|
+
into a per-version cache — `%LOCALAPPDATA%\dsh-glass-plugin\runtime-<version>` on
|
|
43
|
+
Windows, `~/.cache/dsh-glass-plugin/runtime-<version>` elsewhere. Later launches
|
|
44
|
+
reuse that cache, so the download happens once per version.
|
|
45
|
+
|
|
46
|
+
### From a checkout (development)
|
|
47
|
+
|
|
48
|
+
The desktop profile can link the package instead of installing it:
|
|
49
|
+
|
|
50
|
+
```jsonc
|
|
51
|
+
// <profile>/package.json
|
|
52
|
+
"dependencies": { "dsh-glass-plugin": "link:D:/codexwork/dsh/dsh-glass-spec/packages/dsh-plugin" },
|
|
53
|
+
"dsh": { "profile": { "bundles": [/* ... */ "dsh-glass-plugin"] } }
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
In a checkout the plugin uses `apps/desktop/out` as the app directory and the
|
|
57
|
+
workspace `electron` package, so run `pnpm build` first. Point a junction or
|
|
58
|
+
symlink at `node_modules/dsh-glass-plugin` if pnpm did not run, then restart DSH.
|
|
59
|
+
|
|
60
|
+
## Configuration
|
|
61
|
+
|
|
62
|
+
The bundle patch sets `autoLaunch: true`. To keep the plugin installed but stop it
|
|
63
|
+
from starting Glass, override the row config in the profile patch:
|
|
64
|
+
|
|
65
|
+
```yaml
|
|
66
|
+
- id: dsh-glass-plugin
|
|
67
|
+
config:
|
|
68
|
+
autoLaunch: false
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
Other overrides, all optional:
|
|
72
|
+
|
|
73
|
+
| key | meaning |
|
|
74
|
+
|:--|:--|
|
|
75
|
+
| `appDir` | directory holding `out/main/index.js` |
|
|
76
|
+
| `electronBinary` | path to an Electron executable |
|
|
77
|
+
| `autoReload` | relaunch the companion when the desktop bundle changes (default `true`) |
|
|
78
|
+
|
|
79
|
+
`DSH_GLASS_APP` and `DSH_GLASS_ELECTRON` do the same two paths through the environment.
|
|
80
|
+
|
|
81
|
+
## Requirements
|
|
82
|
+
|
|
83
|
+
- Windows 11 x64 (the glass refraction reads the screen behind the window).
|
|
84
|
+
- The desktop app is a separate Electron process; it needs a GPU-backed Electron
|
|
85
|
+
runtime, which the release tarball provides.
|
|
86
|
+
|
|
87
|
+
## License
|
|
88
|
+
|
|
89
|
+
MIT. The bundled Electron runtime keeps its own license files
|
|
90
|
+
(`electron/dist/LICENSE`, `LICENSES.chromium.html`).
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
package/assets/peek.png
ADDED
|
Binary file
|
|
Binary file
|
package/cordis.patch.yml
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
# dsh-glass-plugin bundle patch
|
|
2
|
+
#
|
|
3
|
+
# This is the `dsh.bundle.patch` layer of the local bundle package. When the
|
|
4
|
+
# package is declared in a profile's dependencies, adding it to
|
|
5
|
+
# `dsh.profile.bundles` mounts the row below at boot. The row resolves
|
|
6
|
+
# `dsh-glass-plugin` from the profile's node_modules, where the `link:`
|
|
7
|
+
# dependency points back at this directory.
|
|
8
|
+
- insert:
|
|
9
|
+
- id: dsh-glass-plugin
|
|
10
|
+
name: 'dsh-glass-plugin'
|
|
11
|
+
config:
|
|
12
|
+
autoLaunch: true
|
package/lib/index.js
ADDED
|
@@ -0,0 +1,340 @@
|
|
|
1
|
+
// DSH bundle entry for the Glass companion.
|
|
2
|
+
//
|
|
3
|
+
// The desktop app is its own Electron process. This host plugin starts it once
|
|
4
|
+
// the local web server is up, handing over the authenticated URL through
|
|
5
|
+
// `--dsh-link` so Glass connects without the user pasting anything. When Glass
|
|
6
|
+
// is already running, its single-instance lock focuses the existing window and
|
|
7
|
+
// re-connects it with the new link.
|
|
8
|
+
//
|
|
9
|
+
// Three install shapes have to work:
|
|
10
|
+
//
|
|
11
|
+
// - the release tarball, which ships the built app in `app/` and an Electron
|
|
12
|
+
// runtime in `electron/dist/` (self-contained, nothing to build);
|
|
13
|
+
// - the npm package, which ships only this plugin and fetches the same
|
|
14
|
+
// tarball once into a cache directory on first launch;
|
|
15
|
+
// - a checkout of the repository, where both live outside this package
|
|
16
|
+
// (`apps/desktop/out` and the workspace `electron` dependency).
|
|
17
|
+
//
|
|
18
|
+
// The child also receives `--launcher-pid`; Glass exits when that host process
|
|
19
|
+
// disappears, and disposal kills the spawned tree so the companion follows DSH.
|
|
20
|
+
import { spawn, spawnSync } from 'node:child_process';
|
|
21
|
+
import { createWriteStream, existsSync, mkdirSync, readFileSync, renameSync, rmSync, statSync } from 'node:fs';
|
|
22
|
+
import { get as httpsGet } from 'node:https';
|
|
23
|
+
import { createRequire } from 'node:module';
|
|
24
|
+
import { homedir, tmpdir } from 'node:os';
|
|
25
|
+
import { dirname, join } from 'node:path';
|
|
26
|
+
import { fileURLToPath } from 'node:url';
|
|
27
|
+
|
|
28
|
+
export const name = 'dsh-glass-plugin';
|
|
29
|
+
export const inject = ['connection', 'webServer'];
|
|
30
|
+
|
|
31
|
+
/** Package root: `lib/index.js` -> the directory that holds `package.json`. */
|
|
32
|
+
const pluginRoot = fileURLToPath(new URL('../', import.meta.url));
|
|
33
|
+
|
|
34
|
+
/** Where the light npm package writes the runtime it downloads on first run. */
|
|
35
|
+
const DEFAULT_RUNTIME_URL = 'https://github.com/wfz2006/dsh-status-bar/releases/latest/download/dsh-glass-plugin.tgz';
|
|
36
|
+
|
|
37
|
+
/** Electron's binary sits at these relative paths inside a `dist/` directory. */
|
|
38
|
+
function electronExecutables() {
|
|
39
|
+
if (process.platform === 'win32') return ['electron.exe'];
|
|
40
|
+
if (process.platform === 'darwin') return [join('Electron.app', 'Contents', 'MacOS', 'Electron')];
|
|
41
|
+
return ['electron'];
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function firstFile(candidates) {
|
|
45
|
+
for (const candidate of candidates) {
|
|
46
|
+
if (typeof candidate === 'string' && candidate.length > 0 && existsSync(candidate)) return candidate;
|
|
47
|
+
}
|
|
48
|
+
return undefined;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function hasApp(candidate) {
|
|
52
|
+
return typeof candidate === 'string' && candidate.length > 0 && existsSync(join(candidate, 'out', 'main', 'index.js'));
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function distBinary(distDir) {
|
|
56
|
+
return firstFile(electronExecutables().map(name => join(distDir, name)));
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function packageVersion() {
|
|
60
|
+
try {
|
|
61
|
+
const manifest = JSON.parse(readFileSync(join(pluginRoot, 'package.json'), 'utf8'));
|
|
62
|
+
return typeof manifest.version === 'string' ? manifest.version : '0.0.0';
|
|
63
|
+
} catch {
|
|
64
|
+
return '0.0.0';
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* The app directory holds `out/main/index.js`. The packaged copy wins, then an
|
|
70
|
+
* explicit override, then the checkout layout used while developing.
|
|
71
|
+
*/
|
|
72
|
+
function resolveAppDir(config) {
|
|
73
|
+
return firstFile([
|
|
74
|
+
config.appDir,
|
|
75
|
+
process.env.DSH_GLASS_APP,
|
|
76
|
+
join(pluginRoot, 'app'),
|
|
77
|
+
join(pluginRoot, '..', '..', 'apps', 'desktop'),
|
|
78
|
+
].filter(hasApp));
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function resolveElectron(config, appDir) {
|
|
82
|
+
const explicit = firstFile([config.electronBinary, process.env.DSH_GLASS_ELECTRON].filter(value => typeof value === 'string'));
|
|
83
|
+
if (explicit) return explicit;
|
|
84
|
+
|
|
85
|
+
// Release tarball: the runtime travels with the package.
|
|
86
|
+
const bundled = distBinary(join(pluginRoot, 'electron', 'dist'));
|
|
87
|
+
if (bundled) return bundled;
|
|
88
|
+
|
|
89
|
+
// Source install: use the `electron` package installed next to the app (the
|
|
90
|
+
// workspace checkout) or next to this package (a registry dependency).
|
|
91
|
+
for (const base of [appDir, pluginRoot]) {
|
|
92
|
+
if (!base) continue;
|
|
93
|
+
try {
|
|
94
|
+
const manifest = createRequire(join(base, 'package.json')).resolve('electron/package.json');
|
|
95
|
+
const binary = distBinary(join(dirname(manifest), 'dist'));
|
|
96
|
+
if (binary) return binary;
|
|
97
|
+
} catch {
|
|
98
|
+
// Not installed this way; try the next base.
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
return undefined;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/** A layout that already has both halves of the companion. */
|
|
105
|
+
function resolveInstalled(config) {
|
|
106
|
+
const appDir = resolveAppDir(config);
|
|
107
|
+
if (!appDir) return undefined;
|
|
108
|
+
const electron = resolveElectron(config, appDir);
|
|
109
|
+
return electron ? { appDir, electron } : undefined;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
function runtimeRoot(config) {
|
|
113
|
+
const base = config.runtimeDir
|
|
114
|
+
?? process.env.DSH_GLASS_RUNTIME
|
|
115
|
+
?? (process.platform === 'win32'
|
|
116
|
+
? join(process.env.LOCALAPPDATA ?? join(homedir(), 'AppData', 'Local'), 'dsh-glass-plugin')
|
|
117
|
+
: join(process.env.XDG_CACHE_HOME ?? join(homedir(), '.cache'), 'dsh-glass-plugin'));
|
|
118
|
+
return join(base, `runtime-${packageVersion()}`);
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
function describeError(error) {
|
|
122
|
+
return error instanceof Error ? error.message : String(error);
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/** Follows redirects; GitHub's release download resolves to a CDN host. */
|
|
126
|
+
function download(url, destination, { redirects = 5 } = {}) {
|
|
127
|
+
return new Promise((resolve, reject) => {
|
|
128
|
+
const request = httpsGet(url, (response) => {
|
|
129
|
+
const status = response.statusCode ?? 0;
|
|
130
|
+
if (status >= 300 && status < 400 && response.headers.location) {
|
|
131
|
+
response.resume();
|
|
132
|
+
if (redirects <= 0) {
|
|
133
|
+
reject(new Error(`too many redirects while downloading ${url}`));
|
|
134
|
+
return;
|
|
135
|
+
}
|
|
136
|
+
const next = new URL(response.headers.location, url).href;
|
|
137
|
+
download(next, destination, { redirects: redirects - 1 }).then(resolve, reject);
|
|
138
|
+
return;
|
|
139
|
+
}
|
|
140
|
+
if (status !== 200) {
|
|
141
|
+
response.resume();
|
|
142
|
+
reject(new Error(`download failed with HTTP ${status}: ${url}`));
|
|
143
|
+
return;
|
|
144
|
+
}
|
|
145
|
+
const total = Number(response.headers['content-length'] ?? 0);
|
|
146
|
+
let received = 0;
|
|
147
|
+
let reported = 0;
|
|
148
|
+
const file = createWriteStream(destination);
|
|
149
|
+
response.on('data', (chunk) => {
|
|
150
|
+
received += chunk.length;
|
|
151
|
+
if (total > 0) {
|
|
152
|
+
const percent = Math.floor((received / total) * 100);
|
|
153
|
+
if (percent >= reported + 10) {
|
|
154
|
+
reported = percent - (percent % 10);
|
|
155
|
+
console.info(`[dsh-glass-plugin] downloading the Glass runtime: ${reported}% of ${Math.round(total / (1024 * 1024))} MB`);
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
});
|
|
159
|
+
response.pipe(file);
|
|
160
|
+
file.on('finish', () => file.close(() => resolve(received)));
|
|
161
|
+
file.on('error', (error) => {
|
|
162
|
+
rmSync(destination, { force: true });
|
|
163
|
+
reject(error);
|
|
164
|
+
});
|
|
165
|
+
});
|
|
166
|
+
request.on('error', (error) => {
|
|
167
|
+
rmSync(destination, { force: true });
|
|
168
|
+
reject(error);
|
|
169
|
+
});
|
|
170
|
+
request.setTimeout(15 * 60 * 1000, () => request.destroy(new Error(`timed out downloading ${url}`)));
|
|
171
|
+
});
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
/** `tar` ships with Windows 10+ (bsdtar), macOS and every mainstream Linux. */
|
|
175
|
+
function extract(tarball, directory) {
|
|
176
|
+
const result = spawnSync('tar', ['-xzf', tarball, '-C', directory], { stdio: 'ignore', windowsHide: true });
|
|
177
|
+
if (result.status !== 0) {
|
|
178
|
+
throw new Error(`could not unpack ${tarball} (tar exited with ${result.status ?? result.signal}); extract it manually to ${directory}`);
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
/**
|
|
183
|
+
* Downloads and unpacks the release tarball into the per-version cache, then
|
|
184
|
+
* returns the same `{ appDir, electron }` shape as a bundled install.
|
|
185
|
+
*/
|
|
186
|
+
async function fetchRuntime(config) {
|
|
187
|
+
const url = config.downloadUrl ?? process.env.DSH_GLASS_RUNTIME_URL ?? DEFAULT_RUNTIME_URL;
|
|
188
|
+
const root = runtimeRoot(config);
|
|
189
|
+
const cached = { appDir: join(root, 'app'), electron: distBinary(join(root, 'electron', 'dist')) };
|
|
190
|
+
if (hasApp(cached.appDir) && cached.electron) return cached;
|
|
191
|
+
|
|
192
|
+
const staging = `${root}.staging`;
|
|
193
|
+
const tarball = join(tmpdir(), `dsh-glass-plugin-${process.pid}-${Date.now()}.tgz`);
|
|
194
|
+
rmSync(staging, { recursive: true, force: true });
|
|
195
|
+
mkdirSync(staging, { recursive: true });
|
|
196
|
+
console.info(`[dsh-glass-plugin] first run: fetching the ~155 MB Glass runtime from ${url}`);
|
|
197
|
+
try {
|
|
198
|
+
const bytes = await download(url, tarball);
|
|
199
|
+
console.info(`[dsh-glass-plugin] unpacking the Glass runtime (${Math.round(bytes / (1024 * 1024))} MB)`);
|
|
200
|
+
extract(tarball, staging);
|
|
201
|
+
const unpacked = join(staging, 'package');
|
|
202
|
+
if (!hasApp(join(unpacked, 'app')) || !distBinary(join(unpacked, 'electron', 'dist'))) {
|
|
203
|
+
throw new Error('the downloaded archive does not contain app/ and electron/dist/');
|
|
204
|
+
}
|
|
205
|
+
rmSync(root, { recursive: true, force: true });
|
|
206
|
+
mkdirSync(root, { recursive: true });
|
|
207
|
+
renameSync(join(unpacked, 'app'), join(root, 'app'));
|
|
208
|
+
renameSync(join(unpacked, 'electron'), join(root, 'electron'));
|
|
209
|
+
} finally {
|
|
210
|
+
rmSync(tarball, { force: true });
|
|
211
|
+
rmSync(staging, { recursive: true, force: true });
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
const electron = distBinary(join(root, 'electron', 'dist'));
|
|
215
|
+
if (!electron) throw new Error(`the runtime in ${root} has no Electron executable`);
|
|
216
|
+
return { appDir: join(root, 'app'), electron };
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
function killChild(child) {
|
|
220
|
+
if (!child || child.exitCode !== null || child.pid === undefined) return;
|
|
221
|
+
try { child.kill(); } catch { /* already gone */ }
|
|
222
|
+
if (process.platform === 'win32') spawn('taskkill', ['/pid', String(child.pid), '/t', '/f'], { stdio: 'ignore', windowsHide: true });
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
function bundleMtimeOf(appDir) {
|
|
226
|
+
try { return statSync(join(appDir, 'out', 'main', 'index.js')).mtimeMs; } catch { return 0; }
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
export function apply(ctx, config = {}) {
|
|
230
|
+
if (config.autoLaunch === false) return;
|
|
231
|
+
let child;
|
|
232
|
+
let launched = false;
|
|
233
|
+
let attempts = 0;
|
|
234
|
+
let timer;
|
|
235
|
+
let watcher;
|
|
236
|
+
let bundleStamp = 0;
|
|
237
|
+
let target = resolveInstalled(config);
|
|
238
|
+
let preparing = false;
|
|
239
|
+
let fatal;
|
|
240
|
+
|
|
241
|
+
const stopClock = () => {
|
|
242
|
+
if (timer) clearInterval(timer);
|
|
243
|
+
timer = undefined;
|
|
244
|
+
};
|
|
245
|
+
|
|
246
|
+
const startClock = () => {
|
|
247
|
+
if (!timer) timer = setInterval(tryLaunch, 250);
|
|
248
|
+
};
|
|
249
|
+
|
|
250
|
+
const useTarget = (next) => {
|
|
251
|
+
target = next;
|
|
252
|
+
bundleStamp = bundleMtimeOf(next.appDir);
|
|
253
|
+
startClock();
|
|
254
|
+
};
|
|
255
|
+
|
|
256
|
+
const prepare = () => {
|
|
257
|
+
if (preparing || target || fatal) return;
|
|
258
|
+
preparing = true;
|
|
259
|
+
fetchRuntime(config).then((next) => {
|
|
260
|
+
preparing = false;
|
|
261
|
+
console.info(`[dsh-glass-plugin] Glass runtime ready at ${next.appDir}`);
|
|
262
|
+
useTarget(next);
|
|
263
|
+
}, (error) => {
|
|
264
|
+
preparing = false;
|
|
265
|
+
fatal = `[dsh-glass-plugin] could not prepare the Glass runtime: ${describeError(error)}`;
|
|
266
|
+
console.warn(fatal);
|
|
267
|
+
stopClock();
|
|
268
|
+
});
|
|
269
|
+
};
|
|
270
|
+
|
|
271
|
+
const tryLaunch = () => {
|
|
272
|
+
if (fatal) return;
|
|
273
|
+
if (!target) {
|
|
274
|
+
// The npm build has no app yet: fetch it once, then keep waiting for the
|
|
275
|
+
// web server in the background instead of counting down against it.
|
|
276
|
+
prepare();
|
|
277
|
+
return;
|
|
278
|
+
}
|
|
279
|
+
attempts += 1;
|
|
280
|
+
if (attempts > 600) {
|
|
281
|
+
stopClock();
|
|
282
|
+
console.warn('[dsh-glass-plugin] no web server origin after 150s; the Glass companion was not started');
|
|
283
|
+
return;
|
|
284
|
+
}
|
|
285
|
+
let origin;
|
|
286
|
+
try { origin = ctx.webServer.getOrigin(); } catch { return; }
|
|
287
|
+
let link;
|
|
288
|
+
try { link = ctx.connection.authenticatedUrl(origin); } catch { return; }
|
|
289
|
+
if (!link || !/^https?:\/\//.test(link)) return;
|
|
290
|
+
|
|
291
|
+
const env = { ...process.env };
|
|
292
|
+
delete env.ELECTRON_RUN_AS_NODE;
|
|
293
|
+
const spawned = spawn(target.electron, [target.appDir, `--dsh-link=${link}`, `--launcher-pid=${process.pid}`], {
|
|
294
|
+
cwd: target.appDir,
|
|
295
|
+
detached: true,
|
|
296
|
+
stdio: 'ignore',
|
|
297
|
+
env,
|
|
298
|
+
});
|
|
299
|
+
child = spawned;
|
|
300
|
+
spawned.on('exit', () => { if (child === spawned) child = undefined; });
|
|
301
|
+
spawned.unref();
|
|
302
|
+
launched = true;
|
|
303
|
+
bundleStamp = bundleMtimeOf(target.appDir);
|
|
304
|
+
stopClock();
|
|
305
|
+
console.info('[dsh-glass-plugin] Glass desktop companion launched (authenticated link redacted)');
|
|
306
|
+
};
|
|
307
|
+
|
|
308
|
+
if (typeof ctx.effect === 'function') {
|
|
309
|
+
ctx.effect(() => () => {
|
|
310
|
+
stopClock();
|
|
311
|
+
if (watcher) clearInterval(watcher);
|
|
312
|
+
watcher = undefined;
|
|
313
|
+
killChild(child);
|
|
314
|
+
child = undefined;
|
|
315
|
+
});
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
if (target) {
|
|
319
|
+
startClock();
|
|
320
|
+
} else {
|
|
321
|
+
prepare();
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
if (config.autoReload !== false) {
|
|
325
|
+
// Rebuild-and-reload loop: when the desktop bundle changes, relaunch the
|
|
326
|
+
// running companion with the current authenticated link. A user-initiated
|
|
327
|
+
// quit leaves `child` empty, so it is never resurrected by a build.
|
|
328
|
+
watcher = setInterval(() => {
|
|
329
|
+
if (!target) return;
|
|
330
|
+
const stamp = bundleMtimeOf(target.appDir);
|
|
331
|
+
if (!child || !stamp || bundleStamp === 0 || stamp === bundleStamp) return;
|
|
332
|
+
bundleStamp = stamp;
|
|
333
|
+
console.info('[dsh-glass-plugin] desktop bundle changed; relaunching the Glass companion');
|
|
334
|
+
killChild(child);
|
|
335
|
+
child = undefined;
|
|
336
|
+
launched = false;
|
|
337
|
+
startClock();
|
|
338
|
+
}, 1500);
|
|
339
|
+
}
|
|
340
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,47 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-glass-plugin",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"
|
|
5
|
-
"
|
|
6
|
-
|
|
3
|
+
"version": "0.2.1",
|
|
4
|
+
"description": "Floating glass status bar for DeepSeek Harness: a frameless always-on-top window that shows session progress, pending approvals and questions.",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"author": "wfz2006",
|
|
7
|
+
"type": "module",
|
|
8
|
+
"main": "lib/index.js",
|
|
9
|
+
"engines": {
|
|
10
|
+
"node": ">=22"
|
|
11
|
+
},
|
|
12
|
+
"keywords": [
|
|
13
|
+
"dsh",
|
|
14
|
+
"dsh-plugin",
|
|
15
|
+
"deepseek-harness",
|
|
16
|
+
"glass",
|
|
17
|
+
"status-bar",
|
|
18
|
+
"desktop",
|
|
19
|
+
"electron",
|
|
20
|
+
"windows"
|
|
21
|
+
],
|
|
22
|
+
"repository": {
|
|
23
|
+
"type": "git",
|
|
24
|
+
"url": "git+https://github.com/wfz2006/dsh-status-bar.git",
|
|
25
|
+
"directory": "packages/dsh-plugin"
|
|
26
|
+
},
|
|
27
|
+
"homepage": "https://github.com/wfz2006/dsh-status-bar",
|
|
28
|
+
"files": [
|
|
29
|
+
"lib",
|
|
30
|
+
"cordis.patch.yml",
|
|
31
|
+
"README.md",
|
|
32
|
+
"LICENSE",
|
|
33
|
+
"assets",
|
|
34
|
+
"screenshots.json"
|
|
35
|
+
],
|
|
36
|
+
"scripts": {
|
|
37
|
+
"pack:release": "node ../../scripts/pack-plugin-release.mjs"
|
|
38
|
+
},
|
|
39
|
+
"dsh": {
|
|
40
|
+
"bundle": {
|
|
41
|
+
"patch": "./cordis.patch.yml"
|
|
42
|
+
},
|
|
43
|
+
"glass": {
|
|
44
|
+
"runtime": "https://github.com/wfz2006/dsh-status-bar/releases/latest/download/dsh-glass-plugin.tgz"
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
}
|