@jhizzard/termdeck 1.2.0 → 1.4.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/package.json +2 -2
- package/packages/cli/src/index.js +53 -16
- package/packages/cli/src/init-mnestra.js +131 -0
- package/packages/cli/src/init.js +617 -0
- package/packages/cli/src/mcp-supabase-provision.js +685 -0
- package/packages/cli/src/os-detect.js +297 -0
- package/packages/client/public/app.js +555 -8
- package/packages/client/public/index.html +28 -6
- package/packages/client/public/style.css +127 -0
- package/packages/server/src/agent-adapters/claude.js +11 -0
- package/packages/server/src/agent-adapters/codex.js +203 -1
- package/packages/server/src/agent-adapters/gemini.js +4 -0
- package/packages/server/src/agent-adapters/grok.js +4 -0
- package/packages/server/src/database.js +20 -1
- package/packages/server/src/index.js +364 -12
- package/packages/server/src/session.js +25 -5
- package/packages/server/src/setup/supabase-mcp.js +42 -3
- package/packages/stack-installer/assets/hooks/memory-pre-compact.js +277 -0
- package/packages/stack-installer/assets/hooks/memory-session-end.js +14 -2
|
@@ -0,0 +1,297 @@
|
|
|
1
|
+
// Sprint 64 T1 — OS detection module for the unified `termdeck init` wizard.
|
|
2
|
+
//
|
|
3
|
+
// Returns a normalized facts object the wizard branches on for: default shell,
|
|
4
|
+
// node-pty rebuild guidance, install path, autostart unit emission, and the
|
|
5
|
+
// in-Docker fixture detection that Path B / Sprint 67+ multi-port work depends on.
|
|
6
|
+
//
|
|
7
|
+
// All inputs are injectable via the `deps` parameter so tests can pin every
|
|
8
|
+
// branch without touching the actual host. Production callers pass nothing
|
|
9
|
+
// and get real `process.platform` / `os.arch()` / `fs.readFileSync` behavior.
|
|
10
|
+
//
|
|
11
|
+
// Cross-references:
|
|
12
|
+
// - Sprint 59 T2's `resolveSpawnShell` at packages/server/src/index.js — that
|
|
13
|
+
// runtime helper chains `config.shell` → `$SHELL` → `/bin/sh`. THIS module's
|
|
14
|
+
// `defaultShell` field is what the wizard reports / writes; both should agree.
|
|
15
|
+
// - BACKLOG §D.5 multi-port verification 2026-05-14 15:28 ET — second-instance
|
|
16
|
+
// boot under WAL-mode SQLite confirmed safe. Path B (per-instance DB, signal
|
|
17
|
+
// handling, autostart) is Sprint 67+. This module emits autostart STUBS only
|
|
18
|
+
// with a TODO marker — full wiring deferred per T1 brief §1.2.
|
|
19
|
+
|
|
20
|
+
const fs = require('fs');
|
|
21
|
+
const os = require('os');
|
|
22
|
+
const path = require('path');
|
|
23
|
+
|
|
24
|
+
// Minimal /etc/os-release parser. Strips matched surrounding quotes. Returns
|
|
25
|
+
// an object of all parsed keys (ID, VERSION_ID, PRETTY_NAME, ID_LIKE, etc.)
|
|
26
|
+
// or null if nothing usable parsed.
|
|
27
|
+
function parseOsRelease(content) {
|
|
28
|
+
if (!content || typeof content !== 'string') return null;
|
|
29
|
+
const out = {};
|
|
30
|
+
for (const rawLine of content.split(/\r?\n/)) {
|
|
31
|
+
const line = rawLine.trim();
|
|
32
|
+
if (!line || line.startsWith('#')) continue;
|
|
33
|
+
const eq = line.indexOf('=');
|
|
34
|
+
if (eq <= 0) continue;
|
|
35
|
+
const key = line.slice(0, eq).trim();
|
|
36
|
+
if (!/^[A-Z][A-Z0-9_]*$/.test(key)) continue;
|
|
37
|
+
let value = line.slice(eq + 1).trim();
|
|
38
|
+
if (value.length >= 2) {
|
|
39
|
+
const first = value[0];
|
|
40
|
+
const last = value[value.length - 1];
|
|
41
|
+
if ((first === '"' && last === '"') || (first === "'" && last === "'")) {
|
|
42
|
+
value = value.slice(1, -1);
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
out[key] = value;
|
|
46
|
+
}
|
|
47
|
+
return Object.keys(out).length > 0 ? out : null;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// Distro → defaults map. Used by detectOS() for the Linux branch.
|
|
51
|
+
// Keep in sync with INSTALLER-PITFALLS.md taxonomy if new distros show up
|
|
52
|
+
// in Brad's tester pool (currently Ubuntu 24.04 r730; macOS for Joshua).
|
|
53
|
+
const LINUX_DEFAULTS = {
|
|
54
|
+
ubuntu: { defaultShell: 'bash', rebuildHint: 'sudo apt install -y build-essential python3' },
|
|
55
|
+
debian: { defaultShell: 'bash', rebuildHint: 'sudo apt install -y build-essential python3' },
|
|
56
|
+
pop: { defaultShell: 'bash', rebuildHint: 'sudo apt install -y build-essential python3' },
|
|
57
|
+
linuxmint:{ defaultShell: 'bash', rebuildHint: 'sudo apt install -y build-essential python3' },
|
|
58
|
+
fedora: { defaultShell: 'bash', rebuildHint: 'sudo dnf install -y gcc-c++ make python3' },
|
|
59
|
+
rhel: { defaultShell: 'bash', rebuildHint: 'sudo dnf install -y gcc-c++ make python3' },
|
|
60
|
+
centos: { defaultShell: 'bash', rebuildHint: 'sudo dnf install -y gcc-c++ make python3' },
|
|
61
|
+
rocky: { defaultShell: 'bash', rebuildHint: 'sudo dnf install -y gcc-c++ make python3' },
|
|
62
|
+
alma: { defaultShell: 'bash', rebuildHint: 'sudo dnf install -y gcc-c++ make python3' },
|
|
63
|
+
alpine: { defaultShell: 'sh', rebuildHint: 'apk add --no-cache build-base python3' },
|
|
64
|
+
arch: { defaultShell: 'bash', rebuildHint: 'sudo pacman -S --needed base-devel python' },
|
|
65
|
+
manjaro: { defaultShell: 'bash', rebuildHint: 'sudo pacman -S --needed base-devel python' },
|
|
66
|
+
opensuse: { defaultShell: 'bash', rebuildHint: 'sudo zypper install -t pattern devel_C_C++' },
|
|
67
|
+
};
|
|
68
|
+
|
|
69
|
+
const ID_LIKE_FALLBACK = [
|
|
70
|
+
{ match: /\b(debian|ubuntu)\b/, defaults: LINUX_DEFAULTS.debian },
|
|
71
|
+
{ match: /\b(rhel|fedora|centos)\b/, defaults: LINUX_DEFAULTS.fedora },
|
|
72
|
+
{ match: /\b(alpine|musl)\b/, defaults: LINUX_DEFAULTS.alpine },
|
|
73
|
+
{ match: /\b(arch)\b/, defaults: LINUX_DEFAULTS.arch },
|
|
74
|
+
{ match: /\b(suse)\b/, defaults: LINUX_DEFAULTS.opensuse },
|
|
75
|
+
];
|
|
76
|
+
|
|
77
|
+
// Heuristic in-container detection. Multiple signals because no single one
|
|
78
|
+
// is reliable across container runtimes (Docker, Podman, BuildKit, K8s).
|
|
79
|
+
function detectInContainer({ existsSync, readFile, getEnv }) {
|
|
80
|
+
if (getEnv('container')) return true; // systemd-nspawn / Podman set this
|
|
81
|
+
if (existsSync('/.dockerenv')) return true;
|
|
82
|
+
if (existsSync('/run/.containerenv')) return true; // Podman fixture
|
|
83
|
+
const cgroup = readFile('/proc/1/cgroup');
|
|
84
|
+
if (cgroup && /\b(docker|kubepods|containerd|libpod)\b/.test(cgroup)) return true;
|
|
85
|
+
const mountinfo = readFile('/proc/self/mountinfo');
|
|
86
|
+
if (mountinfo && /\b(overlay|docker|kubelet)\b/.test(mountinfo)) return true;
|
|
87
|
+
return false;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// launchd plist stub for macOS. Path B / Sprint 65+ replaces the stub with
|
|
91
|
+
// full wiring including KeepAlive policy variants, log rotation, EnvironmentVariables,
|
|
92
|
+
// and per-port instance labels. v1.3.0 ships stub only.
|
|
93
|
+
function launchdPlistStub(homedir) {
|
|
94
|
+
return [
|
|
95
|
+
'<?xml version="1.0" encoding="UTF-8"?>',
|
|
96
|
+
'<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTD/PropertyList-1.0.dtd">',
|
|
97
|
+
'<plist version="1.0">',
|
|
98
|
+
'<dict>',
|
|
99
|
+
' <!-- TermDeck launchd autostart STUB — Sprint 64 T1. Full wiring in Sprint 65+. -->',
|
|
100
|
+
' <!-- TODO(sprint-65): per-port label, log rotation, EnvironmentVariables block. -->',
|
|
101
|
+
' <key>Label</key>',
|
|
102
|
+
' <string>com.jhizzard.termdeck</string>',
|
|
103
|
+
' <key>ProgramArguments</key>',
|
|
104
|
+
' <array>',
|
|
105
|
+
' <string>/usr/local/bin/termdeck</string>',
|
|
106
|
+
' <string>--service</string>',
|
|
107
|
+
' </array>',
|
|
108
|
+
' <key>RunAtLoad</key>',
|
|
109
|
+
' <true/>',
|
|
110
|
+
' <key>KeepAlive</key>',
|
|
111
|
+
' <true/>',
|
|
112
|
+
' <key>StandardOutPath</key>',
|
|
113
|
+
` <string>${homedir}/.termdeck/termdeck.log</string>`,
|
|
114
|
+
' <key>StandardErrorPath</key>',
|
|
115
|
+
` <string>${homedir}/.termdeck/termdeck.err.log</string>`,
|
|
116
|
+
'</dict>',
|
|
117
|
+
'</plist>',
|
|
118
|
+
'',
|
|
119
|
+
].join('\n');
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
// systemd user unit stub for Linux (outside Docker). Path B / Sprint 65+
|
|
123
|
+
// replaces the stub with full wiring (per-port unit instance, Environment=,
|
|
124
|
+
// proper Restart policy tuning, journald integration). v1.3.0 ships stub only.
|
|
125
|
+
function systemdUserUnitStub() {
|
|
126
|
+
return [
|
|
127
|
+
'# TermDeck systemd user unit STUB — Sprint 64 T1. Full wiring in Sprint 65+.',
|
|
128
|
+
'# TODO(sprint-65): templated unit @<port>, Environment=TERMDECK_PORT=, journald gates.',
|
|
129
|
+
'#',
|
|
130
|
+
'# Install: systemctl --user enable --now termdeck.service',
|
|
131
|
+
'# Status: systemctl --user status termdeck.service',
|
|
132
|
+
'# Logs: journalctl --user -u termdeck.service -f',
|
|
133
|
+
'',
|
|
134
|
+
'[Unit]',
|
|
135
|
+
'Description=TermDeck — browser terminal multiplexer',
|
|
136
|
+
'After=network-online.target',
|
|
137
|
+
'',
|
|
138
|
+
'[Service]',
|
|
139
|
+
'Type=simple',
|
|
140
|
+
'ExecStart=/usr/local/bin/termdeck --service',
|
|
141
|
+
'Restart=on-failure',
|
|
142
|
+
'RestartSec=5s',
|
|
143
|
+
'StandardOutput=append:%h/.termdeck/termdeck.log',
|
|
144
|
+
'StandardError=append:%h/.termdeck/termdeck.err.log',
|
|
145
|
+
'',
|
|
146
|
+
'[Install]',
|
|
147
|
+
'WantedBy=default.target',
|
|
148
|
+
'',
|
|
149
|
+
].join('\n');
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
// Detect the host OS and build the wizard-facing facts object.
|
|
153
|
+
//
|
|
154
|
+
// Deps (all optional; tests inject):
|
|
155
|
+
// platform — process.platform replacement ('darwin'|'linux'|'win32'|...)
|
|
156
|
+
// arch — os.arch() replacement
|
|
157
|
+
// homedir — os.homedir() replacement
|
|
158
|
+
// getEnv — (key) => string|undefined (defaults to process.env[key])
|
|
159
|
+
// readFile — (path) => string|null
|
|
160
|
+
// existsSync — (path) => boolean
|
|
161
|
+
// macosVersion — pre-resolved 'YY.YY.YY' string (e.g. '14.3.1'); when omitted
|
|
162
|
+
// the function does NOT shell out (no spawnSync to `sw_vers`)
|
|
163
|
+
// — keeps boot deterministic for the wizard. Callers wanting
|
|
164
|
+
// the version can pass it via deps.
|
|
165
|
+
//
|
|
166
|
+
// Returns: {
|
|
167
|
+
// family: 'macos' | 'linux' | 'docker' | 'unknown'
|
|
168
|
+
// distro: string | null (e.g. 'ubuntu', 'fedora', 'alpine')
|
|
169
|
+
// version: string | null (VERSION_ID for linux, macosVersion for macos)
|
|
170
|
+
// isAppleSilicon:boolean (true only on darwin+arm64)
|
|
171
|
+
// inDocker: boolean (always present)
|
|
172
|
+
// defaultShell: 'zsh' | 'bash' | 'sh'
|
|
173
|
+
// rebuildHint: string (node-pty rebuild remediation)
|
|
174
|
+
// paths: {
|
|
175
|
+
// home, termdeck, secretsEnv, configYaml, autostartDir
|
|
176
|
+
// }
|
|
177
|
+
// autostartUnit: { kind, path, content, note? } (kind: 'launchd'|'systemd'|null)
|
|
178
|
+
// }
|
|
179
|
+
function detectOS(deps) {
|
|
180
|
+
deps = deps || {};
|
|
181
|
+
const platform = deps.platform || process.platform;
|
|
182
|
+
const arch = deps.arch || os.arch();
|
|
183
|
+
const homedir = deps.homedir || os.homedir();
|
|
184
|
+
const getEnv = deps.getEnv || ((k) => process.env[k]);
|
|
185
|
+
const readFile = deps.readFile || ((p) => {
|
|
186
|
+
try { return fs.readFileSync(p, 'utf8'); }
|
|
187
|
+
catch (_e) { return null; }
|
|
188
|
+
});
|
|
189
|
+
const existsSync = deps.existsSync || ((p) => fs.existsSync(p));
|
|
190
|
+
|
|
191
|
+
const termdeckDir = path.join(homedir, '.termdeck');
|
|
192
|
+
const basePaths = {
|
|
193
|
+
home: homedir,
|
|
194
|
+
termdeck: termdeckDir,
|
|
195
|
+
secretsEnv: path.join(termdeckDir, 'secrets.env'),
|
|
196
|
+
configYaml: path.join(termdeckDir, 'config.yaml'),
|
|
197
|
+
};
|
|
198
|
+
|
|
199
|
+
if (platform === 'darwin') {
|
|
200
|
+
const isAppleSilicon = arch === 'arm64';
|
|
201
|
+
const macosVersion = deps.macosVersion || null;
|
|
202
|
+
return {
|
|
203
|
+
family: 'macos',
|
|
204
|
+
distro: null,
|
|
205
|
+
version: macosVersion,
|
|
206
|
+
isAppleSilicon,
|
|
207
|
+
inDocker: false,
|
|
208
|
+
defaultShell: 'zsh',
|
|
209
|
+
rebuildHint: 'xcode-select --install # installs the macOS Command Line Tools (clang, make)',
|
|
210
|
+
paths: {
|
|
211
|
+
...basePaths,
|
|
212
|
+
autostartDir: path.join(homedir, 'Library', 'LaunchAgents'),
|
|
213
|
+
},
|
|
214
|
+
autostartUnit: {
|
|
215
|
+
kind: 'launchd',
|
|
216
|
+
path: path.join(homedir, 'Library', 'LaunchAgents', 'com.jhizzard.termdeck.plist'),
|
|
217
|
+
content: launchdPlistStub(homedir),
|
|
218
|
+
note: 'STUB only — full wiring deferred to Sprint 65+',
|
|
219
|
+
},
|
|
220
|
+
};
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
if (platform === 'linux') {
|
|
224
|
+
const inDocker = detectInContainer({ existsSync, readFile, getEnv });
|
|
225
|
+
const osRelease = parseOsRelease(readFile('/etc/os-release')) || {};
|
|
226
|
+
const rawId = (osRelease.ID || '').toLowerCase();
|
|
227
|
+
const idLike = (osRelease.ID_LIKE || '').toLowerCase();
|
|
228
|
+
const version = osRelease.VERSION_ID || null;
|
|
229
|
+
|
|
230
|
+
let distro = rawId || null;
|
|
231
|
+
let defaults = LINUX_DEFAULTS[rawId];
|
|
232
|
+
if (!defaults && idLike) {
|
|
233
|
+
for (const f of ID_LIKE_FALLBACK) {
|
|
234
|
+
if (f.match.test(idLike)) {
|
|
235
|
+
defaults = f.defaults;
|
|
236
|
+
if (!distro) distro = idLike.split(/\s+/)[0];
|
|
237
|
+
break;
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
if (!defaults) {
|
|
242
|
+
defaults = { defaultShell: 'bash', rebuildHint: 'install your distro\'s C++ build toolchain (gcc, make, python3)' };
|
|
243
|
+
if (!distro) distro = 'unknown';
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
const autostartDir = path.join(homedir, '.config', 'systemd', 'user');
|
|
247
|
+
return {
|
|
248
|
+
family: inDocker ? 'docker' : 'linux',
|
|
249
|
+
distro,
|
|
250
|
+
version,
|
|
251
|
+
isAppleSilicon: false,
|
|
252
|
+
inDocker,
|
|
253
|
+
defaultShell: defaults.defaultShell,
|
|
254
|
+
rebuildHint: defaults.rebuildHint,
|
|
255
|
+
paths: {
|
|
256
|
+
...basePaths,
|
|
257
|
+
autostartDir: inDocker ? null : autostartDir,
|
|
258
|
+
},
|
|
259
|
+
autostartUnit: inDocker
|
|
260
|
+
? {
|
|
261
|
+
kind: null,
|
|
262
|
+
path: null,
|
|
263
|
+
content: null,
|
|
264
|
+
note: 'in-container fixture — TermDeck runs as the container entrypoint; no per-user systemd unit',
|
|
265
|
+
}
|
|
266
|
+
: {
|
|
267
|
+
kind: 'systemd',
|
|
268
|
+
path: path.join(autostartDir, 'termdeck.service'),
|
|
269
|
+
content: systemdUserUnitStub(),
|
|
270
|
+
note: 'STUB only — full wiring deferred to Sprint 65+',
|
|
271
|
+
},
|
|
272
|
+
};
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
return {
|
|
276
|
+
family: 'unknown',
|
|
277
|
+
distro: null,
|
|
278
|
+
version: null,
|
|
279
|
+
isAppleSilicon: false,
|
|
280
|
+
inDocker: false,
|
|
281
|
+
defaultShell: 'sh',
|
|
282
|
+
rebuildHint: 'install your platform\'s C++ toolchain manually (gcc, make, python3) before re-running termdeck init',
|
|
283
|
+
paths: { ...basePaths, autostartDir: null },
|
|
284
|
+
autostartUnit: { kind: null, path: null, content: null, note: 'unknown platform — autostart unit not auto-emitted' },
|
|
285
|
+
};
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
module.exports = {
|
|
289
|
+
detectOS,
|
|
290
|
+
parseOsRelease,
|
|
291
|
+
// Exported for tests:
|
|
292
|
+
_LINUX_DEFAULTS: LINUX_DEFAULTS,
|
|
293
|
+
_ID_LIKE_FALLBACK: ID_LIKE_FALLBACK,
|
|
294
|
+
_launchdPlistStub: launchdPlistStub,
|
|
295
|
+
_systemdUserUnitStub: systemdUserUnitStub,
|
|
296
|
+
_detectInContainer: detectInContainer,
|
|
297
|
+
};
|