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 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
- # Temporary Holding Version
1
+ # dsh-glass-plugin
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
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
Binary file
Binary file
@@ -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.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
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
+ }
@@ -0,0 +1,7 @@
1
+ [
2
+ "assets/capsule.png",
3
+ "assets/peek.png",
4
+ "assets/expanded.png",
5
+ "assets/approval.png",
6
+ "assets/settings.png"
7
+ ]