@morlay/dsh-desktopify 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.
@@ -0,0 +1,4 @@
1
+ //#region src/preload-app.ts
2
+ /** Minimal marker that selects the desktop custom-protocol API carrier. */
3
+ require("electron").contextBridge.exposeInMainWorld("dshDesktop", { protocolVersion: 1 });
4
+ //#endregion
@@ -0,0 +1,4 @@
1
+ //#region src/preload.ts
2
+ /** Placeholder preload for the shell management surface (unused in the custom shell). */
3
+ require("electron").contextBridge.exposeInMainWorld("dshDesktop", { protocolVersion: 1 });
4
+ //#endregion
@@ -0,0 +1,152 @@
1
+ import { dirname, join, relative } from "node:path";
2
+ import { chmodSync, copyFileSync, existsSync, lstatSync, mkdirSync, readFileSync, readdirSync, readlinkSync, rmSync, symlinkSync, unlinkSync, writeFileSync } from "node:fs";
3
+ //#region src/appconfig.ts
4
+ /**
5
+ * Runtime configuration contract between the bundle step and the Electron shell.
6
+ * Written beside the shell executable as `appconfig.json`; the shell reads it at
7
+ * startup. `dshHome` follows the reference desktop semantics:
8
+ * - `xdg` (default) — XDG data home (`~/Library/Application Support` on
9
+ * macOS, `$XDG_DATA_HOME` on Linux) joined with the application name;
10
+ * - `env` — leave `DSH_HOME` unset and inherit the environment;
11
+ * - an absolute path — pin `DSH_HOME` to that directory.
12
+ * @module @morlay/dsh-desktopify
13
+ */
14
+ /** The dsh profile the desktop shell hosts (upstream desktop semantics). */
15
+ const PROFILE_NAME = "desktop";
16
+ function isRecord(value) {
17
+ return typeof value === "object" && value !== null;
18
+ }
19
+ function windowConfig(value) {
20
+ const record = isRecord(value) ? value : {};
21
+ return {
22
+ width: numberOr(record.width, 1280),
23
+ height: numberOr(record.height, 800),
24
+ minWidth: numberOr(record.minWidth, 800),
25
+ minHeight: numberOr(record.minHeight, 600)
26
+ };
27
+ }
28
+ function numberOr(value, fallback) {
29
+ return typeof value === "number" && Number.isFinite(value) && value > 0 ? value : fallback;
30
+ }
31
+ /** Read and validate the shell configuration beside the executable. */
32
+ function loadAppConfig(exeDir) {
33
+ const path = join(exeDir, "appconfig.json");
34
+ const value = JSON.parse(readFileSync(path, "utf8"));
35
+ if (!isRecord(value) || typeof value.name !== "string" || value.name === "" || typeof value.id !== "string" || value.id === "" || typeof value.version !== "string" || value.version === "" || value.profile !== "desktop" || value.dshHome !== "xdg" && value.dshHome !== "env" && typeof value.dshHome !== "string") throw new Error(`dsh desktop: invalid shell configuration ${path}`);
36
+ return {
37
+ name: value.name,
38
+ id: value.id,
39
+ version: value.version,
40
+ profile: PROFILE_NAME,
41
+ dshHome: value.dshHome,
42
+ window: windowConfig(value.window)
43
+ };
44
+ }
45
+ /** Write the shell configuration beside the executable (bundle step). */
46
+ function writeAppConfig(exeDir, config) {
47
+ writeFileSync(join(exeDir, "appconfig.json"), `${JSON.stringify(config, void 0, 2)}\n`, { mode: 384 });
48
+ }
49
+ //#endregion
50
+ //#region src/seed.ts
51
+ /**
52
+ * Packaged profile seed: the bundle step writes the workspace profile into
53
+ * `dsh-home/profiles/desktop` beside the shell executable — package.json,
54
+ * cordis.patch.yml, the declared `files` entries, and a real copy of the
55
+ * production dependency closure as `node_modules` (the upstream desktop host
56
+ * resolves its own entry and every profile bundle inside the project
57
+ * directory, so links to the app closure would fail its containment check).
58
+ * The seed is stamped with a `.seed-hash` fingerprint of the workspace
59
+ * content. At startup the shell forces the runtime home's profile to be a
60
+ * real copy of that seed: a mismatched fingerprint (or a stale symlink)
61
+ * replaces the profile, a matching fingerprint skips the copy so the closure
62
+ * is not re-copied on every launch. User data lives at the home root and is
63
+ * never touched by profile replacement.
64
+ * @module @morlay/dsh-desktopify
65
+ */
66
+ /** Fingerprint file name inside the seeded profile. */
67
+ const SEED_HASH_NAME = ".seed-hash";
68
+ /** Directories skipped when copying the seed (installation bookkeeping). */
69
+ const SEED_SKIP_DIRS = /* @__PURE__ */ new Set([
70
+ ".nub-store",
71
+ ".store",
72
+ ".nub"
73
+ ]);
74
+ function readSeedHash(path) {
75
+ try {
76
+ return readFileSync(path, "utf8");
77
+ } catch {
78
+ return "";
79
+ }
80
+ }
81
+ function isDirectory(path) {
82
+ try {
83
+ return lstatSync(path).isDirectory();
84
+ } catch {
85
+ return false;
86
+ }
87
+ }
88
+ /**
89
+ * Force the runtime home's profile to a real copy of the packaged seed.
90
+ * @param seedDir - packaged seed root (`<exeDir>/../dsh-home`).
91
+ * @param home - resolved runtime `DSH_HOME`.
92
+ * @returns whether the profile was (re)written.
93
+ */
94
+ function ensureSeedProfile(seedDir, home) {
95
+ const seedProfile = join(seedDir, "profiles", PROFILE_NAME);
96
+ if (!isDirectory(seedProfile)) return false;
97
+ const profileDir = join(home, "profiles", PROFILE_NAME);
98
+ const seedHash = readSeedHash(join(seedProfile, SEED_HASH_NAME));
99
+ if (seedHash !== "") try {
100
+ if (!lstatSync(profileDir).isSymbolicLink() && readSeedHash(join(profileDir, ".seed-hash")) === seedHash) return false;
101
+ } catch {}
102
+ try {
103
+ const stat = lstatSync(profileDir);
104
+ if (stat.isSymbolicLink()) unlinkSync(profileDir);
105
+ else if (stat.isDirectory()) rmSync(profileDir, { recursive: true });
106
+ else unlinkSync(profileDir);
107
+ } catch (error) {
108
+ if (error.code !== "ENOENT") throw error;
109
+ }
110
+ copySeed(seedDir, home);
111
+ return true;
112
+ }
113
+ /**
114
+ * Copy the seed into the home, skipping bookkeeping directories and never
115
+ * overwriting user data. The seed's node_modules is a self-contained pnpm
116
+ * isolated layout (real copies in `.pnpm`, relative links everywhere else),
117
+ * so symlinks are recreated as-is — dereferencing would break the layout.
118
+ */
119
+ function copySeed(seedDir, home) {
120
+ mkdirSync(home, { recursive: true });
121
+ const visit = (directory) => {
122
+ for (const entry of readdirSync(directory, { withFileTypes: true })) {
123
+ const source = join(directory, entry.name);
124
+ const target = join(home, relative(seedDir, source));
125
+ if (entry.isSymbolicLink()) {
126
+ if (existsSync(target)) continue;
127
+ mkdirSync(dirname(target), { recursive: true });
128
+ symlinkSync(readlinkSync(source), target, process.platform === "win32" ? "junction" : "dir");
129
+ continue;
130
+ }
131
+ let stat;
132
+ try {
133
+ stat = lstatSync(source);
134
+ } catch {
135
+ continue;
136
+ }
137
+ if (stat.isDirectory()) {
138
+ if (SEED_SKIP_DIRS.has(entry.name)) continue;
139
+ mkdirSync(target, { recursive: true });
140
+ visit(source);
141
+ continue;
142
+ }
143
+ if (!stat.isFile() || existsSync(target)) continue;
144
+ mkdirSync(dirname(target), { recursive: true });
145
+ copyFileSync(source, target);
146
+ chmodSync(target, stat.mode & 511);
147
+ }
148
+ };
149
+ visit(seedDir);
150
+ }
151
+ //#endregion
152
+ export { writeAppConfig as a, loadAppConfig as i, ensureSeedProfile as n, PROFILE_NAME as r, SEED_HASH_NAME as t };
package/package.json ADDED
@@ -0,0 +1,51 @@
1
+ {
2
+ "name": "@morlay/dsh-desktopify",
3
+ "version": "0.1.0",
4
+ "description": "Desktop packaging tool for dsh workspaces: hosts the upstream dsh-desktop-host child over framed byte pipes, with dev (workspace-linked) and bundle (static, unsigned) modes.",
5
+ "keywords": [
6
+ "desktop",
7
+ "dsh",
8
+ "electron",
9
+ "electron-builder",
10
+ "packaging"
11
+ ],
12
+ "license": "MIT",
13
+ "author": "morlay",
14
+ "repository": {
15
+ "type": "git",
16
+ "url": "https://github.com/morlay/better-session.git"
17
+ },
18
+ "bin": {
19
+ "dsh-desktopify": "./dist/cli/index.mjs"
20
+ },
21
+ "files": [
22
+ "dist"
23
+ ],
24
+ "type": "module",
25
+ "main": "./dist/index.mjs",
26
+ "module": "./dist/index.mjs",
27
+ "exports": {
28
+ ".": "./dist/index.mjs",
29
+ "./package.json": "./package.json"
30
+ },
31
+ "dependencies": {
32
+ "@deepseek-ai/dsh": "^0.1.5-rc.1",
33
+ "@deepseek-ai/dsh-desktop-host": "^0.1.5-rc.1",
34
+ "commander": "^15.0.0",
35
+ "electron": "^44.3.0",
36
+ "electron-builder": "^26.16.1",
37
+ "extract-zip": "^2.0.1",
38
+ "sharp": "^0.35.4",
39
+ "tar": "^7.5.0",
40
+ "tsdown": "0.22.x"
41
+ },
42
+ "devDependencies": {
43
+ "@types/node": "^26.5.0"
44
+ },
45
+ "engines": {
46
+ "node": ">=24.11.0"
47
+ },
48
+ "scripts": {
49
+ "build": "pnpm exec tsdown"
50
+ }
51
+ }