@decibelsystems/tools 3.0.0 → 3.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.
- package/CHANGELOG.md +66 -0
- package/dist/agentCommands.d.ts +0 -1
- package/dist/agentCommands.d.ts.map +1 -1
- package/dist/agentCommands.js +19 -8
- package/dist/agentCommands.js.map +1 -1
- package/dist/agentPresence.d.ts +5 -3
- package/dist/agentPresence.d.ts.map +1 -1
- package/dist/agentPresence.js +20 -29
- package/dist/agentPresence.js.map +1 -1
- package/dist/architectAdrs.d.ts +13 -1
- package/dist/architectAdrs.d.ts.map +1 -1
- package/dist/architectAdrs.js +83 -1
- package/dist/architectAdrs.js.map +1 -1
- package/dist/artifacts.d.ts +60 -0
- package/dist/artifacts.d.ts.map +1 -0
- package/dist/artifacts.js +208 -0
- package/dist/artifacts.js.map +1 -0
- package/dist/asks.d.ts +93 -0
- package/dist/asks.d.ts.map +1 -0
- package/dist/asks.js +383 -0
- package/dist/asks.js.map +1 -0
- package/dist/chat.d.ts +47 -0
- package/dist/chat.d.ts.map +1 -0
- package/dist/chat.js +288 -0
- package/dist/chat.js.map +1 -0
- package/dist/daemon.d.ts +14 -3
- package/dist/daemon.d.ts.map +1 -1
- package/dist/daemon.js +44 -14
- package/dist/daemon.js.map +1 -1
- package/dist/daemonConfig.d.ts +22 -0
- package/dist/daemonConfig.d.ts.map +1 -1
- package/dist/daemonConfig.js +11 -0
- package/dist/daemonConfig.js.map +1 -1
- package/dist/domain/issue.d.ts +19 -15
- package/dist/domain/issue.d.ts.map +1 -1
- package/dist/domain/issue.js +4 -32
- package/dist/domain/issue.js.map +1 -1
- package/dist/domain/issueCodec.d.ts.map +1 -1
- package/dist/domain/issueCodec.js +10 -0
- package/dist/domain/issueCodec.js.map +1 -1
- package/dist/domain/issueRepository.d.ts +35 -2
- package/dist/domain/issueRepository.d.ts.map +1 -1
- package/dist/domain/issueRepository.js +40 -1
- package/dist/domain/issueRepository.js.map +1 -1
- package/dist/domain/uid.d.ts +16 -0
- package/dist/domain/uid.d.ts.map +1 -0
- package/dist/domain/uid.js +43 -0
- package/dist/domain/uid.js.map +1 -0
- package/dist/facades/definitions.d.ts.map +1 -1
- package/dist/facades/definitions.js +46 -3
- package/dist/facades/definitions.js.map +1 -1
- package/dist/hq/defineAgent.d.ts.map +1 -1
- package/dist/hq/defineAgent.js +7 -11
- package/dist/hq/defineAgent.js.map +1 -1
- package/dist/httpServer.d.ts.map +1 -1
- package/dist/httpServer.js +201 -48
- package/dist/httpServer.js.map +1 -1
- package/dist/identity.d.ts +123 -0
- package/dist/identity.d.ts.map +1 -0
- package/dist/identity.js +312 -0
- package/dist/identity.js.map +1 -0
- package/dist/kernel.d.ts.map +1 -1
- package/dist/kernel.js +24 -1
- package/dist/kernel.js.map +1 -1
- package/dist/killSwitch.d.ts +117 -0
- package/dist/killSwitch.d.ts.map +1 -0
- package/dist/killSwitch.js +161 -0
- package/dist/killSwitch.js.map +1 -0
- package/dist/lib/atomicWrite.d.ts +23 -0
- package/dist/lib/atomicWrite.d.ts.map +1 -1
- package/dist/lib/atomicWrite.js +84 -0
- package/dist/lib/atomicWrite.js.map +1 -1
- package/dist/lib/recordIdAllocator.d.ts +21 -5
- package/dist/lib/recordIdAllocator.d.ts.map +1 -1
- package/dist/lib/recordIdAllocator.js +25 -7
- package/dist/lib/recordIdAllocator.js.map +1 -1
- package/dist/orgId.d.ts +23 -0
- package/dist/orgId.d.ts.map +1 -0
- package/dist/orgId.js +73 -0
- package/dist/orgId.js.map +1 -0
- package/dist/pairCli.d.ts +2 -0
- package/dist/pairCli.d.ts.map +1 -0
- package/dist/pairCli.js +97 -0
- package/dist/pairCli.js.map +1 -0
- package/dist/peerBroker.d.ts +40 -0
- package/dist/peerBroker.d.ts.map +1 -0
- package/dist/peerBroker.js +70 -0
- package/dist/peerBroker.js.map +1 -0
- package/dist/peerBrokerSupervisor.d.ts +21 -0
- package/dist/peerBrokerSupervisor.d.ts.map +1 -0
- package/dist/peerBrokerSupervisor.js +131 -0
- package/dist/peerBrokerSupervisor.js.map +1 -0
- package/dist/projectPaths.d.ts.map +1 -1
- package/dist/projectPaths.js +1 -19
- package/dist/projectPaths.js.map +1 -1
- package/dist/projectRegistry.d.ts +22 -0
- package/dist/projectRegistry.d.ts.map +1 -1
- package/dist/projectRegistry.js +58 -2
- package/dist/projectRegistry.js.map +1 -1
- package/dist/pty/boundary.d.ts +39 -0
- package/dist/pty/boundary.d.ts.map +1 -0
- package/dist/pty/boundary.js +100 -0
- package/dist/pty/boundary.js.map +1 -0
- package/dist/pty/errors.d.ts +6 -0
- package/dist/pty/errors.d.ts.map +1 -0
- package/dist/pty/errors.js +12 -0
- package/dist/pty/errors.js.map +1 -0
- package/dist/pty/fleet.d.ts +159 -0
- package/dist/pty/fleet.d.ts.map +1 -0
- package/dist/pty/fleet.js +543 -0
- package/dist/pty/fleet.js.map +1 -0
- package/dist/pty/launchProfile.d.ts +45 -0
- package/dist/pty/launchProfile.d.ts.map +1 -0
- package/dist/pty/launchProfile.js +149 -0
- package/dist/pty/launchProfile.js.map +1 -0
- package/dist/pty/openTerminal.d.ts +48 -0
- package/dist/pty/openTerminal.d.ts.map +1 -0
- package/dist/pty/openTerminal.js +185 -0
- package/dist/pty/openTerminal.js.map +1 -0
- package/dist/pty/pairDialog.d.ts +16 -0
- package/dist/pty/pairDialog.d.ts.map +1 -0
- package/dist/pty/pairDialog.js +121 -0
- package/dist/pty/pairDialog.js.map +1 -0
- package/dist/pty/pairing.d.ts +86 -0
- package/dist/pty/pairing.d.ts.map +1 -0
- package/dist/pty/pairing.js +351 -0
- package/dist/pty/pairing.js.map +1 -0
- package/dist/pty/routes.d.ts +49 -0
- package/dist/pty/routes.d.ts.map +1 -0
- package/dist/pty/routes.js +624 -0
- package/dist/pty/routes.js.map +1 -0
- package/dist/pty/sessions.d.ts +167 -0
- package/dist/pty/sessions.d.ts.map +1 -0
- package/dist/pty/sessions.js +513 -0
- package/dist/pty/sessions.js.map +1 -0
- package/dist/pty/tmux.d.ts +109 -0
- package/dist/pty/tmux.d.ts.map +1 -0
- package/dist/pty/tmux.js +271 -0
- package/dist/pty/tmux.js.map +1 -0
- package/dist/roster.d.ts +37 -0
- package/dist/roster.d.ts.map +1 -0
- package/dist/roster.js +126 -0
- package/dist/roster.js.map +1 -0
- package/dist/runtime/daemonMeta.d.ts +53 -0
- package/dist/runtime/daemonMeta.d.ts.map +1 -0
- package/dist/runtime/daemonMeta.js +121 -0
- package/dist/runtime/daemonMeta.js.map +1 -0
- package/dist/runtime/ensureRuntime.d.ts.map +1 -1
- package/dist/runtime/ensureRuntime.js +5 -10
- package/dist/runtime/ensureRuntime.js.map +1 -1
- package/dist/sentinelIssues.d.ts +16 -0
- package/dist/sentinelIssues.d.ts.map +1 -1
- package/dist/sentinelIssues.js +42 -1
- package/dist/sentinelIssues.js.map +1 -1
- package/dist/server.js +66 -5
- package/dist/server.js.map +1 -1
- package/dist/toolConfig.d.ts.map +1 -1
- package/dist/toolConfig.js +12 -0
- package/dist/toolConfig.js.map +1 -1
- package/dist/tools/agentic/agentQueue.d.ts.map +1 -1
- package/dist/tools/agentic/agentQueue.js +4 -0
- package/dist/tools/agentic/agentQueue.js.map +1 -1
- package/dist/tools/architect/index.d.ts +1 -0
- package/dist/tools/architect/index.d.ts.map +1 -1
- package/dist/tools/architect/index.js +45 -1
- package/dist/tools/architect/index.js.map +1 -1
- package/dist/tools/architect.d.ts.map +1 -1
- package/dist/tools/architect.js +59 -8
- package/dist/tools/architect.js.map +1 -1
- package/dist/tools/artifacts.d.ts +5 -0
- package/dist/tools/artifacts.d.ts.map +1 -0
- package/dist/tools/artifacts.js +89 -0
- package/dist/tools/artifacts.js.map +1 -0
- package/dist/tools/asks.d.ts +6 -0
- package/dist/tools/asks.d.ts.map +1 -0
- package/dist/tools/asks.js +104 -0
- package/dist/tools/asks.js.map +1 -0
- package/dist/tools/coordinator/index.d.ts +1 -0
- package/dist/tools/coordinator/index.d.ts.map +1 -1
- package/dist/tools/coordinator/index.js +42 -2
- package/dist/tools/coordinator/index.js.map +1 -1
- package/dist/tools/data-inspector.d.ts +1 -1
- package/dist/tools/data-inspector.d.ts.map +1 -1
- package/dist/tools/data-inspector.js +1 -1
- package/dist/tools/data-inspector.js.map +1 -1
- package/dist/tools/dojo/index.d.ts.map +1 -1
- package/dist/tools/dojo/index.js +52 -3
- package/dist/tools/dojo/index.js.map +1 -1
- package/dist/tools/dojo.d.ts +62 -1
- package/dist/tools/dojo.d.ts.map +1 -1
- package/dist/tools/dojo.js +84 -1
- package/dist/tools/dojo.js.map +1 -1
- package/dist/tools/guardian.d.ts +7 -0
- package/dist/tools/guardian.d.ts.map +1 -1
- package/dist/tools/guardian.js +146 -9
- package/dist/tools/guardian.js.map +1 -1
- package/dist/tools/index.d.ts.map +1 -1
- package/dist/tools/index.js +6 -0
- package/dist/tools/index.js.map +1 -1
- package/dist/tools/killswitch/index.d.ts +13 -0
- package/dist/tools/killswitch/index.d.ts.map +1 -0
- package/dist/tools/killswitch/index.js +138 -0
- package/dist/tools/killswitch/index.js.map +1 -0
- package/dist/tools/oracle/index.d.ts.map +1 -1
- package/dist/tools/oracle/index.js +8 -0
- package/dist/tools/oracle/index.js.map +1 -1
- package/dist/tools/oracle.d.ts +7 -0
- package/dist/tools/oracle.d.ts.map +1 -1
- package/dist/tools/oracle.js +22 -6
- package/dist/tools/oracle.js.map +1 -1
- package/dist/tools/provenance.d.ts +11 -2
- package/dist/tools/provenance.d.ts.map +1 -1
- package/dist/tools/provenance.js +26 -24
- package/dist/tools/provenance.js.map +1 -1
- package/dist/tools/registry/index.d.ts.map +1 -1
- package/dist/tools/registry/index.js +24 -0
- package/dist/tools/registry/index.js.map +1 -1
- package/dist/tools/sentinel/index.d.ts.map +1 -1
- package/dist/tools/sentinel/index.js +54 -2
- package/dist/tools/sentinel/index.js.map +1 -1
- package/dist/tools/sentinel.d.ts +50 -0
- package/dist/tools/sentinel.d.ts.map +1 -1
- package/dist/tools/sentinel.js +72 -7
- package/dist/tools/sentinel.js.map +1 -1
- package/dist/tools/shared/runTracker.d.ts +2 -0
- package/dist/tools/shared/runTracker.d.ts.map +1 -1
- package/dist/tools/shared/runTracker.js +38 -0
- package/dist/tools/shared/runTracker.js.map +1 -1
- package/dist/tools/swarm.d.ts +3 -1
- package/dist/tools/swarm.d.ts.map +1 -1
- package/dist/tools/swarm.js +6 -12
- package/dist/tools/swarm.js.map +1 -1
- package/dist/tools/voice/index.d.ts +1 -0
- package/dist/tools/voice/index.d.ts.map +1 -1
- package/dist/tools/voice/index.js +33 -2
- package/dist/tools/voice/index.js.map +1 -1
- package/dist/tools/voice.d.ts +48 -1
- package/dist/tools/voice.d.ts.map +1 -1
- package/dist/tools/voice.js +63 -3
- package/dist/tools/voice.js.map +1 -1
- package/dist/transports/bridge.d.ts.map +1 -1
- package/dist/transports/bridge.js +2 -1
- package/dist/transports/bridge.js.map +1 -1
- package/dist/transports/mcp.d.ts.map +1 -1
- package/dist/transports/mcp.js +2 -1
- package/dist/transports/mcp.js.map +1 -1
- package/dist/version.d.ts +5 -0
- package/dist/version.d.ts.map +1 -0
- package/dist/version.js +23 -0
- package/dist/version.js.map +1 -0
- package/package.json +13 -2
- package/templates/AGENT.md +3 -0
- package/templates/CLAUDE-decibel.md +11 -0
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
// ============================================================================
|
|
2
|
+
// Launch profile — the flags every daemon spawn gets (decibel-hq ADR-0018 D3)
|
|
3
|
+
// ============================================================================
|
|
4
|
+
// ~/.decibel/launch.json, written by Ben, read by the daemon. Kept apart from
|
|
5
|
+
// config.yaml and env so it holds no secrets and can be read and diffed freely.
|
|
6
|
+
//
|
|
7
|
+
// The schema is CLOSED: an unknown key, a mode Claude Code does not have, or a
|
|
8
|
+
// channel that is not `server:<name>` makes the whole file invalid, and every
|
|
9
|
+
// spawn then refuses with LAUNCH_PROFILE_INVALID rather than silently starting
|
|
10
|
+
// claude without the flags Ben chose. HQ and the CLI never send argv or a
|
|
11
|
+
// profile; caller-chosen flags plus `claude` is code execution.
|
|
12
|
+
//
|
|
13
|
+
// No file means today's behaviour: no extra flags.
|
|
14
|
+
//
|
|
15
|
+
// The `tmux` block configures the daemon's own tmux server, which starts with
|
|
16
|
+
// -f /dev/null and so never reads ~/.tmux.conf. Mouse on lets the wheel scroll
|
|
17
|
+
// a session attached from Ghostty; HQ's pane scrolls through its own frames
|
|
18
|
+
// either way, and copies with Option-drag once tmux owns the mouse.
|
|
19
|
+
// ============================================================================
|
|
20
|
+
import { existsSync, readFileSync } from 'fs';
|
|
21
|
+
import { homedir } from 'os';
|
|
22
|
+
import { join } from 'path';
|
|
23
|
+
import { SessionError } from './errors.js';
|
|
24
|
+
/** `claude --permission-mode` choices, Claude Code 2.1.281. */
|
|
25
|
+
export const PERMISSION_MODES = ['acceptEdits', 'auto', 'bypassPermissions', 'manual', 'dontAsk', 'plan'];
|
|
26
|
+
const CHANNEL_RE = /^server:[a-z0-9-]+$/;
|
|
27
|
+
const KEYS = new Set(['permission_mode', 'dev_channels', 'period_gap_hours', 'restore_on_start', 'tmux']);
|
|
28
|
+
const TMUX_KEYS = new Set(['mouse', 'history_limit']);
|
|
29
|
+
export const DEFAULT_PERIOD_GAP_HOURS = 4;
|
|
30
|
+
/** Scrollback lines per pane. A pane's limit is fixed when it is created. */
|
|
31
|
+
export const DEFAULT_HISTORY_LIMIT = 50_000;
|
|
32
|
+
const MIN_HISTORY_LIMIT = 2_000;
|
|
33
|
+
const MAX_HISTORY_LIMIT = 1_000_000;
|
|
34
|
+
export function defaultProfile() {
|
|
35
|
+
return {
|
|
36
|
+
dev_channels: [],
|
|
37
|
+
period_gap_hours: DEFAULT_PERIOD_GAP_HOURS,
|
|
38
|
+
restore_on_start: false,
|
|
39
|
+
// Mouse on by default: Ben asked for wheel scrolling in Ghostty twice.
|
|
40
|
+
tmux: { mouse: true, history_limit: DEFAULT_HISTORY_LIMIT },
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
/** Resolved per call so tests (and a changed $HOME) are honoured. */
|
|
44
|
+
export function launchProfilePath() {
|
|
45
|
+
return process.env.DECIBEL_LAUNCH_PATH || join(homedir(), '.decibel', 'launch.json');
|
|
46
|
+
}
|
|
47
|
+
/** Validate a parsed launch.json. Throws a message naming the first problem. */
|
|
48
|
+
export function parseLaunchProfile(raw) {
|
|
49
|
+
if (!raw || typeof raw !== 'object' || Array.isArray(raw))
|
|
50
|
+
throw new Error('launch.json must be a JSON object');
|
|
51
|
+
const o = raw;
|
|
52
|
+
for (const k of Object.keys(o)) {
|
|
53
|
+
if (!KEYS.has(k))
|
|
54
|
+
throw new Error(`unknown key "${k}" (allowed: ${[...KEYS].join(', ')})`);
|
|
55
|
+
}
|
|
56
|
+
const p = defaultProfile();
|
|
57
|
+
if (o.permission_mode !== undefined) {
|
|
58
|
+
if (!PERMISSION_MODES.includes(o.permission_mode)) {
|
|
59
|
+
throw new Error(`permission_mode must be one of ${PERMISSION_MODES.join(', ')}`);
|
|
60
|
+
}
|
|
61
|
+
p.permission_mode = o.permission_mode;
|
|
62
|
+
}
|
|
63
|
+
if (o.dev_channels !== undefined) {
|
|
64
|
+
if (!Array.isArray(o.dev_channels) || !o.dev_channels.every((c) => typeof c === 'string' && CHANNEL_RE.test(c))) {
|
|
65
|
+
throw new Error('dev_channels must be a list of "server:<name>" entries ([a-z0-9-] names)');
|
|
66
|
+
}
|
|
67
|
+
p.dev_channels = [...new Set(o.dev_channels)];
|
|
68
|
+
}
|
|
69
|
+
if (o.period_gap_hours !== undefined) {
|
|
70
|
+
const h = o.period_gap_hours;
|
|
71
|
+
if (typeof h !== 'number' || !Number.isFinite(h) || h < 0.5 || h > 72) {
|
|
72
|
+
throw new Error('period_gap_hours must be a number between 0.5 and 72');
|
|
73
|
+
}
|
|
74
|
+
p.period_gap_hours = h;
|
|
75
|
+
}
|
|
76
|
+
if (o.restore_on_start !== undefined) {
|
|
77
|
+
if (typeof o.restore_on_start !== 'boolean')
|
|
78
|
+
throw new Error('restore_on_start must be true or false');
|
|
79
|
+
p.restore_on_start = o.restore_on_start;
|
|
80
|
+
}
|
|
81
|
+
if (o.tmux !== undefined) {
|
|
82
|
+
const t = o.tmux;
|
|
83
|
+
if (!t || typeof t !== 'object' || Array.isArray(t))
|
|
84
|
+
throw new Error('tmux must be an object');
|
|
85
|
+
const to = t;
|
|
86
|
+
for (const k of Object.keys(to)) {
|
|
87
|
+
if (!TMUX_KEYS.has(k))
|
|
88
|
+
throw new Error(`unknown key "tmux.${k}" (allowed: ${[...TMUX_KEYS].join(', ')})`);
|
|
89
|
+
}
|
|
90
|
+
if (to.mouse !== undefined) {
|
|
91
|
+
if (typeof to.mouse !== 'boolean')
|
|
92
|
+
throw new Error('tmux.mouse must be true or false');
|
|
93
|
+
p.tmux.mouse = to.mouse;
|
|
94
|
+
}
|
|
95
|
+
if (to.history_limit !== undefined) {
|
|
96
|
+
const n = to.history_limit;
|
|
97
|
+
if (typeof n !== 'number' || !Number.isInteger(n) || n < MIN_HISTORY_LIMIT || n > MAX_HISTORY_LIMIT) {
|
|
98
|
+
throw new Error(`tmux.history_limit must be a whole number from ${MIN_HISTORY_LIMIT} to ${MAX_HISTORY_LIMIT}`);
|
|
99
|
+
}
|
|
100
|
+
p.tmux.history_limit = n;
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
return p;
|
|
104
|
+
}
|
|
105
|
+
/**
|
|
106
|
+
* The profile, read per call (the file is tiny and spawns are rare, so an edit
|
|
107
|
+
* takes effect without a restart). Throws LAUNCH_PROFILE_INVALID when the file
|
|
108
|
+
* exists and is wrong.
|
|
109
|
+
*/
|
|
110
|
+
export function loadLaunchProfile() {
|
|
111
|
+
const path = launchProfilePath();
|
|
112
|
+
if (!existsSync(path))
|
|
113
|
+
return defaultProfile();
|
|
114
|
+
try {
|
|
115
|
+
return parseLaunchProfile(JSON.parse(readFileSync(path, 'utf-8')));
|
|
116
|
+
}
|
|
117
|
+
catch (err) {
|
|
118
|
+
throw new SessionError(500, 'LAUNCH_PROFILE_INVALID', `${path} is invalid: ${err instanceof Error ? err.message : String(err)}. Fix or remove it; no session starts until then.`);
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
/**
|
|
122
|
+
* claude flags for a profile. Each channel gets its own flag, placed last: the
|
|
123
|
+
* flag is undocumented, and repeating it is safe whether it takes one value or
|
|
124
|
+
* many, where a second bare value could otherwise be read as a prompt.
|
|
125
|
+
*/
|
|
126
|
+
export function profileArgs(p) {
|
|
127
|
+
const out = [];
|
|
128
|
+
if (p.permission_mode)
|
|
129
|
+
out.push('--permission-mode', p.permission_mode);
|
|
130
|
+
for (const c of p.dev_channels)
|
|
131
|
+
out.push('--dangerously-load-development-channels', c);
|
|
132
|
+
return out;
|
|
133
|
+
}
|
|
134
|
+
/**
|
|
135
|
+
* Lets tmux pass OSC 8 hyperlinks (which Claude Code emits) through to an
|
|
136
|
+
* xterm-family client, so links are clickable in Ghostty and HQ's pane. Always
|
|
137
|
+
* on; it costs nothing. A FIXED index, not `set -as`: this runs before every
|
|
138
|
+
* spawn, and appending would grow the array by one entry each time.
|
|
139
|
+
*/
|
|
140
|
+
export const HYPERLINKS_FEATURE_INDEX = 20;
|
|
141
|
+
/** Options for the owned server, as one ';'-separated tmux invocation. */
|
|
142
|
+
export function tmuxOptionArgs(t) {
|
|
143
|
+
return [
|
|
144
|
+
'set-option', '-g', 'history-limit', String(t.history_limit), ';',
|
|
145
|
+
'set-option', '-g', 'mouse', t.mouse ? 'on' : 'off', ';',
|
|
146
|
+
'set-option', '-s', `terminal-features[${HYPERLINKS_FEATURE_INDEX}]`, 'xterm*:hyperlinks',
|
|
147
|
+
];
|
|
148
|
+
}
|
|
149
|
+
//# sourceMappingURL=launchProfile.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"launchProfile.js","sourceRoot":"","sources":["../../src/pty/launchProfile.ts"],"names":[],"mappings":"AAAA,+EAA+E;AAC/E,8EAA8E;AAC9E,+EAA+E;AAC/E,8EAA8E;AAC9E,gFAAgF;AAChF,EAAE;AACF,+EAA+E;AAC/E,8EAA8E;AAC9E,+EAA+E;AAC/E,0EAA0E;AAC1E,gEAAgE;AAChE,EAAE;AACF,mDAAmD;AACnD,EAAE;AACF,8EAA8E;AAC9E,+EAA+E;AAC/E,4EAA4E;AAC5E,oEAAoE;AACpE,+EAA+E;AAE/E,OAAO,EAAE,UAAU,EAAE,YAAY,EAAE,MAAM,IAAI,CAAC;AAC9C,OAAO,EAAE,OAAO,EAAE,MAAM,IAAI,CAAC;AAC7B,OAAO,EAAE,IAAI,EAAE,MAAM,MAAM,CAAC;AAC5B,OAAO,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;AAE3C,+DAA+D;AAC/D,MAAM,CAAC,MAAM,gBAAgB,GAAG,CAAC,aAAa,EAAE,MAAM,EAAE,mBAAmB,EAAE,QAAQ,EAAE,SAAS,EAAE,MAAM,CAAU,CAAC;AAGnH,MAAM,UAAU,GAAG,qBAAqB,CAAC;AACzC,MAAM,IAAI,GAAG,IAAI,GAAG,CAAC,CAAC,iBAAiB,EAAE,cAAc,EAAE,kBAAkB,EAAE,kBAAkB,EAAE,MAAM,CAAC,CAAC,CAAC;AAC1G,MAAM,SAAS,GAAG,IAAI,GAAG,CAAC,CAAC,OAAO,EAAE,eAAe,CAAC,CAAC,CAAC;AAgBtD,MAAM,CAAC,MAAM,wBAAwB,GAAG,CAAC,CAAC;AAC1C,6EAA6E;AAC7E,MAAM,CAAC,MAAM,qBAAqB,GAAG,MAAM,CAAC;AAC5C,MAAM,iBAAiB,GAAG,KAAK,CAAC;AAChC,MAAM,iBAAiB,GAAG,SAAS,CAAC;AAEpC,MAAM,UAAU,cAAc;IAC5B,OAAO;QACL,YAAY,EAAE,EAAE;QAChB,gBAAgB,EAAE,wBAAwB;QAC1C,gBAAgB,EAAE,KAAK;QACvB,uEAAuE;QACvE,IAAI,EAAE,EAAE,KAAK,EAAE,IAAI,EAAE,aAAa,EAAE,qBAAqB,EAAE;KAC5D,CAAC;AACJ,CAAC;AAED,qEAAqE;AACrE,MAAM,UAAU,iBAAiB;IAC/B,OAAO,OAAO,CAAC,GAAG,CAAC,mBAAmB,IAAI,IAAI,CAAC,OAAO,EAAE,EAAE,UAAU,EAAE,aAAa,CAAC,CAAC;AACvF,CAAC;AAED,gFAAgF;AAChF,MAAM,UAAU,kBAAkB,CAAC,GAAY;IAC7C,IAAI,CAAC,GAAG,IAAI,OAAO,GAAG,KAAK,QAAQ,IAAI,KAAK,CAAC,OAAO,CAAC,GAAG,CAAC;QAAE,MAAM,IAAI,KAAK,CAAC,mCAAmC,CAAC,CAAC;IAChH,MAAM,CAAC,GAAG,GAA8B,CAAC;IACzC,KAAK,MAAM,CAAC,IAAI,MAAM,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,CAAC;QAC/B,IAAI,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,CAAC;YAAE,MAAM,IAAI,KAAK,CAAC,gBAAgB,CAAC,eAAe,CAAC,GAAG,IAAI,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC;IAC7F,CAAC;IACD,MAAM,CAAC,GAAG,cAAc,EAAE,CAAC;IAC3B,IAAI,CAAC,CAAC,eAAe,KAAK,SAAS,EAAE,CAAC;QACpC,IAAI,CAAC,gBAAgB,CAAC,QAAQ,CAAC,CAAC,CAAC,eAAiC,CAAC,EAAE,CAAC;YACpE,MAAM,IAAI,KAAK,CAAC,kCAAkC,gBAAgB,CAAC,IAAI,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC;QACnF,CAAC;QACD,CAAC,CAAC,eAAe,GAAG,CAAC,CAAC,eAAiC,CAAC;IAC1D,CAAC;IACD,IAAI,CAAC,CAAC,YAAY,KAAK,SAAS,EAAE,CAAC;QACjC,IAAI,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC,CAAC,YAAY,CAAC,IAAI,CAAC,CAAC,CAAC,YAAY,CAAC,KAAK,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,OAAO,CAAC,KAAK,QAAQ,IAAI,UAAU,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC;YAChH,MAAM,IAAI,KAAK,CAAC,0EAA0E,CAAC,CAAC;QAC9F,CAAC;QACD,CAAC,CAAC,YAAY,GAAG,CAAC,GAAG,IAAI,GAAG,CAAC,CAAC,CAAC,YAAwB,CAAC,CAAC,CAAC;IAC5D,CAAC;IACD,IAAI,CAAC,CAAC,gBAAgB,KAAK,SAAS,EAAE,CAAC;QACrC,MAAM,CAAC,GAAG,CAAC,CAAC,gBAAgB,CAAC;QAC7B,IAAI,OAAO,CAAC,KAAK,QAAQ,IAAI,CAAC,MAAM,CAAC,QAAQ,CAAC,CAAC,CAAC,IAAI,CAAC,GAAG,GAAG,IAAI,CAAC,GAAG,EAAE,EAAE,CAAC;YACtE,MAAM,IAAI,KAAK,CAAC,sDAAsD,CAAC,CAAC;QAC1E,CAAC;QACD,CAAC,CAAC,gBAAgB,GAAG,CAAC,CAAC;IACzB,CAAC;IACD,IAAI,CAAC,CAAC,gBAAgB,KAAK,SAAS,EAAE,CAAC;QACrC,IAAI,OAAO,CAAC,CAAC,gBAAgB,KAAK,SAAS;YAAE,MAAM,IAAI,KAAK,CAAC,wCAAwC,CAAC,CAAC;QACvG,CAAC,CAAC,gBAAgB,GAAG,CAAC,CAAC,gBAAgB,CAAC;IAC1C,CAAC;IACD,IAAI,CAAC,CAAC,IAAI,KAAK,SAAS,EAAE,CAAC;QACzB,MAAM,CAAC,GAAG,CAAC,CAAC,IAAI,CAAC;QACjB,IAAI,CAAC,CAAC,IAAI,OAAO,CAAC,KAAK,QAAQ,IAAI,KAAK,CAAC,OAAO,CAAC,CAAC,CAAC;YAAE,MAAM,IAAI,KAAK,CAAC,wBAAwB,CAAC,CAAC;QAC/F,MAAM,EAAE,GAAG,CAA4B,CAAC;QACxC,KAAK,MAAM,CAAC,IAAI,MAAM,CAAC,IAAI,CAAC,EAAE,CAAC,EAAE,CAAC;YAChC,IAAI,CAAC,SAAS,CAAC,GAAG,CAAC,CAAC,CAAC;gBAAE,MAAM,IAAI,KAAK,CAAC,qBAAqB,CAAC,eAAe,CAAC,GAAG,SAAS,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC;QAC5G,CAAC;QACD,IAAI,EAAE,CAAC,KAAK,KAAK,SAAS,EAAE,CAAC;YAC3B,IAAI,OAAO,EAAE,CAAC,KAAK,KAAK,SAAS;gBAAE,MAAM,IAAI,KAAK,CAAC,kCAAkC,CAAC,CAAC;YACvF,CAAC,CAAC,IAAI,CAAC,KAAK,GAAG,EAAE,CAAC,KAAK,CAAC;QAC1B,CAAC;QACD,IAAI,EAAE,CAAC,aAAa,KAAK,SAAS,EAAE,CAAC;YACnC,MAAM,CAAC,GAAG,EAAE,CAAC,aAAa,CAAC;YAC3B,IAAI,OAAO,CAAC,KAAK,QAAQ,IAAI,CAAC,MAAM,CAAC,SAAS,CAAC,CAAC,CAAC,IAAI,CAAC,GAAG,iBAAiB,IAAI,CAAC,GAAG,iBAAiB,EAAE,CAAC;gBACpG,MAAM,IAAI,KAAK,CAAC,kDAAkD,iBAAiB,OAAO,iBAAiB,EAAE,CAAC,CAAC;YACjH,CAAC;YACD,CAAC,CAAC,IAAI,CAAC,aAAa,GAAG,CAAC,CAAC;QAC3B,CAAC;IACH,CAAC;IACD,OAAO,CAAC,CAAC;AACX,CAAC;AAED;;;;GAIG;AACH,MAAM,UAAU,iBAAiB;IAC/B,MAAM,IAAI,GAAG,iBAAiB,EAAE,CAAC;IACjC,IAAI,CAAC,UAAU,CAAC,IAAI,CAAC;QAAE,OAAO,cAAc,EAAE,CAAC;IAC/C,IAAI,CAAC;QACH,OAAO,kBAAkB,CAAC,IAAI,CAAC,KAAK,CAAC,YAAY,CAAC,IAAI,EAAE,OAAO,CAAC,CAAC,CAAC,CAAC;IACrE,CAAC;IAAC,OAAO,GAAG,EAAE,CAAC;QACb,MAAM,IAAI,YAAY,CACpB,GAAG,EACH,wBAAwB,EACxB,GAAG,IAAI,gBAAgB,GAAG,YAAY,KAAK,CAAC,CAAC,CAAC,GAAG,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,GAAG,CAAC,mDAAmD,CAC3H,CAAC;IACJ,CAAC;AACH,CAAC;AAED;;;;GAIG;AACH,MAAM,UAAU,WAAW,CAAC,CAAgB;IAC1C,MAAM,GAAG,GAAa,EAAE,CAAC;IACzB,IAAI,CAAC,CAAC,eAAe;QAAE,GAAG,CAAC,IAAI,CAAC,mBAAmB,EAAE,CAAC,CAAC,eAAe,CAAC,CAAC;IACxE,KAAK,MAAM,CAAC,IAAI,CAAC,CAAC,YAAY;QAAE,GAAG,CAAC,IAAI,CAAC,yCAAyC,EAAE,CAAC,CAAC,CAAC;IACvF,OAAO,GAAG,CAAC;AACb,CAAC;AAED;;;;;GAKG;AACH,MAAM,CAAC,MAAM,wBAAwB,GAAG,EAAE,CAAC;AAE3C,0EAA0E;AAC1E,MAAM,UAAU,cAAc,CAAC,CAAc;IAC3C,OAAO;QACL,YAAY,EAAE,IAAI,EAAE,eAAe,EAAE,MAAM,CAAC,CAAC,CAAC,aAAa,CAAC,EAAE,GAAG;QACjE,YAAY,EAAE,IAAI,EAAE,OAAO,EAAE,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,KAAK,EAAE,GAAG;QACxD,YAAY,EAAE,IAAI,EAAE,qBAAqB,wBAAwB,GAAG,EAAE,mBAAmB;KAC1F,CAAC;AACJ,CAAC"}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { type HostedSession } from './sessions.js';
|
|
2
|
+
export declare const MAX_OPEN = 12;
|
|
3
|
+
export declare const GHOSTTY_APP = "/Applications/Ghostty.app";
|
|
4
|
+
export declare const GHOSTTY_TERM = "xterm-ghostty";
|
|
5
|
+
export type OpenStatus = 'opened' | 'already_open' | 'failed';
|
|
6
|
+
export type OpenCode = 'NOT_LIVE' | 'NOT_OWNED' | 'GHOSTTY_NOT_INSTALLED' | 'AUTOMATION_DENIED' | 'OSASCRIPT_FAILED' | 'BAD_PATH';
|
|
7
|
+
export interface OpenRow {
|
|
8
|
+
session_key: string;
|
|
9
|
+
status: OpenStatus;
|
|
10
|
+
code?: OpenCode;
|
|
11
|
+
error?: string;
|
|
12
|
+
}
|
|
13
|
+
export interface RunResult {
|
|
14
|
+
code: number;
|
|
15
|
+
stderr: string;
|
|
16
|
+
}
|
|
17
|
+
export interface OpenDeps {
|
|
18
|
+
sessions?: () => Promise<HostedSession[]>;
|
|
19
|
+
/** Session names that already have a Ghostty client on the owned server. */
|
|
20
|
+
ghosttyAttached?: () => Promise<Set<string>>;
|
|
21
|
+
ghosttyInstalled?: () => boolean;
|
|
22
|
+
/** Runs an AppleScript (tabs). */
|
|
23
|
+
osascript?: (script: string) => Promise<RunResult>;
|
|
24
|
+
/** Runs `open` with these args (windows). */
|
|
25
|
+
open?: (args: string[]) => Promise<RunResult>;
|
|
26
|
+
ensureTitles?: () => Promise<void>;
|
|
27
|
+
sleep?: (ms: number) => Promise<void>;
|
|
28
|
+
}
|
|
29
|
+
/** Global on the owned server, so sessions spawned before this also get titles. */
|
|
30
|
+
export declare function ensureTitles(): Promise<void>;
|
|
31
|
+
/** One shell word. Ghostty runs `command` through a shell. */
|
|
32
|
+
export declare function shellWord(s: string): string;
|
|
33
|
+
/**
|
|
34
|
+
* An AppleScript string literal. AppleScript has no \u escape, so JSON-style
|
|
35
|
+
* quoting breaks on anything it would escape that way; only backslash and the
|
|
36
|
+
* double quote are escaped, and control characters are refused outright.
|
|
37
|
+
*/
|
|
38
|
+
export declare function appleString(s: string): string;
|
|
39
|
+
export declare function tabScript(command: string, cwd: string): string;
|
|
40
|
+
export declare function windowArgs(argv: string[], cwd: string): string[];
|
|
41
|
+
export declare function validateOpen(body: unknown): {
|
|
42
|
+
keys: string[];
|
|
43
|
+
layout: 'tabs' | 'windows';
|
|
44
|
+
};
|
|
45
|
+
export declare function openTerminal(body: unknown, deps?: OpenDeps): Promise<{
|
|
46
|
+
results: OpenRow[];
|
|
47
|
+
}>;
|
|
48
|
+
//# sourceMappingURL=openTerminal.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"openTerminal.d.ts","sourceRoot":"","sources":["../../src/pty/openTerminal.ts"],"names":[],"mappings":"AA4BA,OAAO,EAAsB,KAAK,aAAa,EAAE,MAAM,eAAe,CAAC;AAEvE,eAAO,MAAM,QAAQ,KAAK,CAAC;AAC3B,eAAO,MAAM,WAAW,8BAA8B,CAAC;AACvD,eAAO,MAAM,YAAY,kBAAkB,CAAC;AAG5C,MAAM,MAAM,UAAU,GAAG,QAAQ,GAAG,cAAc,GAAG,QAAQ,CAAC;AAC9D,MAAM,MAAM,QAAQ,GAAG,UAAU,GAAG,WAAW,GAAG,uBAAuB,GAAG,mBAAmB,GAAG,kBAAkB,GAAG,UAAU,CAAC;AAElI,MAAM,WAAW,OAAO;IACtB,WAAW,EAAE,MAAM,CAAC;IACpB,MAAM,EAAE,UAAU,CAAC;IACnB,IAAI,CAAC,EAAE,QAAQ,CAAC;IAChB,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB;AAED,MAAM,WAAW,SAAS;IACxB,IAAI,EAAE,MAAM,CAAC;IACb,MAAM,EAAE,MAAM,CAAC;CAChB;AAED,MAAM,WAAW,QAAQ;IACvB,QAAQ,CAAC,EAAE,MAAM,OAAO,CAAC,aAAa,EAAE,CAAC,CAAC;IAC1C,4EAA4E;IAC5E,eAAe,CAAC,EAAE,MAAM,OAAO,CAAC,GAAG,CAAC,MAAM,CAAC,CAAC,CAAC;IAC7C,gBAAgB,CAAC,EAAE,MAAM,OAAO,CAAC;IACjC,kCAAkC;IAClC,SAAS,CAAC,EAAE,CAAC,MAAM,EAAE,MAAM,KAAK,OAAO,CAAC,SAAS,CAAC,CAAC;IACnD,6CAA6C;IAC7C,IAAI,CAAC,EAAE,CAAC,IAAI,EAAE,MAAM,EAAE,KAAK,OAAO,CAAC,SAAS,CAAC,CAAC;IAC9C,YAAY,CAAC,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC,CAAC;IACnC,KAAK,CAAC,EAAE,CAAC,EAAE,EAAE,MAAM,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;CACvC;AAMD,mFAAmF;AACnF,wBAAsB,YAAY,IAAI,OAAO,CAAC,IAAI,CAAC,CAGlD;AAMD,8DAA8D;AAC9D,wBAAgB,SAAS,CAAC,CAAC,EAAE,MAAM,GAAG,MAAM,CAE3C;AAED;;;;GAIG;AACH,wBAAgB,WAAW,CAAC,CAAC,EAAE,MAAM,GAAG,MAAM,CAG7C;AAED,wBAAgB,SAAS,CAAC,OAAO,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,GAAG,MAAM,CAc9D;AAED,wBAAgB,UAAU,CAAC,IAAI,EAAE,MAAM,EAAE,EAAE,GAAG,EAAE,MAAM,GAAG,MAAM,EAAE,CAEhE;AAiCD,wBAAgB,YAAY,CAAC,IAAI,EAAE,OAAO,GAAG;IAAE,IAAI,EAAE,MAAM,EAAE,CAAC;IAAC,MAAM,EAAE,MAAM,GAAG,SAAS,CAAA;CAAE,CAc1F;AAED,wBAAsB,YAAY,CAAC,IAAI,EAAE,OAAO,EAAE,IAAI,GAAE,QAAa,GAAG,OAAO,CAAC;IAAE,OAAO,EAAE,OAAO,EAAE,CAAA;CAAE,CAAC,CA6DtG"}
|
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
// ============================================================================
|
|
2
|
+
// POST /sessions/open-terminal: open hosted sessions in desktop Ghostty
|
|
3
|
+
// ============================================================================
|
|
4
|
+
// decibel-hq ADR-0018 D5, amended 2026-10-14 (tools ISS-0210). A browser
|
|
5
|
+
// cannot run AppleScript, so HQ asks the daemon to open a Ghostty tab or
|
|
6
|
+
// window attached to a live session the daemon itself spawned.
|
|
7
|
+
//
|
|
8
|
+
// What the caller controls is only WHICH sessions and the layout. The command
|
|
9
|
+
// is always the daemon's own attach argv and the cwd the daemon recorded, so
|
|
10
|
+
// no request text reaches a shell. Titles are not in the command either: the
|
|
11
|
+
// owned tmux server runs with set-titles on (see ensureTitles), so tmux titles
|
|
12
|
+
// every client itself, including tabs that were already open.
|
|
13
|
+
//
|
|
14
|
+
// tabs AppleScript: new tab in the front window, or a new window if
|
|
15
|
+
// none is open. Needs a macOS Automation (TCC) grant for whatever
|
|
16
|
+
// process macOS holds responsible: under launchd, node.
|
|
17
|
+
// windows `open -na Ghostty.app --args ...`: LaunchServices, not Apple
|
|
18
|
+
// Events, so no Automation grant. One window per session.
|
|
19
|
+
//
|
|
20
|
+
// After one AUTOMATION_DENIED, the rest of the batch is not attempted: every
|
|
21
|
+
// further try would raise the same macOS prompt or fail the same way.
|
|
22
|
+
// ============================================================================
|
|
23
|
+
import { execFile } from 'child_process';
|
|
24
|
+
import { existsSync } from 'fs';
|
|
25
|
+
import { log } from '../config.js';
|
|
26
|
+
import { SessionError } from './errors.js';
|
|
27
|
+
import { tmux, TITLE_FORMAT } from './tmux.js';
|
|
28
|
+
import { listHostedSessions } from './sessions.js';
|
|
29
|
+
export const MAX_OPEN = 12;
|
|
30
|
+
export const GHOSTTY_APP = '/Applications/Ghostty.app';
|
|
31
|
+
export const GHOSTTY_TERM = 'xterm-ghostty';
|
|
32
|
+
const STAGGER_MS = 300;
|
|
33
|
+
// ----------------------------------------------------------------------------
|
|
34
|
+
// Titles: tmux sends OSC 2 to every client on the owned server
|
|
35
|
+
// ----------------------------------------------------------------------------
|
|
36
|
+
/** Global on the owned server, so sessions spawned before this also get titles. */
|
|
37
|
+
export async function ensureTitles() {
|
|
38
|
+
const r = await tmux('owned', ['set-option', '-g', 'set-titles', 'on', ';', 'set-option', '-g', 'set-titles-string', TITLE_FORMAT]);
|
|
39
|
+
if (!r.ok)
|
|
40
|
+
log(`Sessions: could not turn on tmux titles: ${r.stderr.trim()}`);
|
|
41
|
+
}
|
|
42
|
+
// ----------------------------------------------------------------------------
|
|
43
|
+
// Quoting
|
|
44
|
+
// ----------------------------------------------------------------------------
|
|
45
|
+
/** One shell word. Ghostty runs `command` through a shell. */
|
|
46
|
+
export function shellWord(s) {
|
|
47
|
+
return `'${s.replace(/'/g, `'\\''`)}'`;
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
* An AppleScript string literal. AppleScript has no \u escape, so JSON-style
|
|
51
|
+
* quoting breaks on anything it would escape that way; only backslash and the
|
|
52
|
+
* double quote are escaped, and control characters are refused outright.
|
|
53
|
+
*/
|
|
54
|
+
export function appleString(s) {
|
|
55
|
+
if (/[\u0000-\u001f\u007f]/.test(s))
|
|
56
|
+
throw new Error('control character in AppleScript string');
|
|
57
|
+
return `"${s.replace(/\\/g, '\\\\').replace(/"/g, '\\"')}"`;
|
|
58
|
+
}
|
|
59
|
+
export function tabScript(command, cwd) {
|
|
60
|
+
return [
|
|
61
|
+
'tell application "Ghostty"',
|
|
62
|
+
' set cfg to new surface configuration',
|
|
63
|
+
` set command of cfg to ${appleString(command)}`,
|
|
64
|
+
` set initial working directory of cfg to ${appleString(cwd)}`,
|
|
65
|
+
' if (count of windows) = 0 then',
|
|
66
|
+
' new window with configuration cfg',
|
|
67
|
+
' else',
|
|
68
|
+
' new tab in front window with configuration cfg',
|
|
69
|
+
' end if',
|
|
70
|
+
'end tell',
|
|
71
|
+
'',
|
|
72
|
+
].join('\n');
|
|
73
|
+
}
|
|
74
|
+
export function windowArgs(argv, cwd) {
|
|
75
|
+
return ['-na', GHOSTTY_APP, '--args', `--working-directory=${cwd}`, `--command=${argv.map(shellWord).join(' ')}`];
|
|
76
|
+
}
|
|
77
|
+
// ----------------------------------------------------------------------------
|
|
78
|
+
// Default deps
|
|
79
|
+
// ----------------------------------------------------------------------------
|
|
80
|
+
function run(bin, args, input) {
|
|
81
|
+
return new Promise((resolve) => {
|
|
82
|
+
const child = execFile(bin, args, { timeout: 30_000 }, (err, _stdout, stderr) => {
|
|
83
|
+
// On a non-zero exit, execFile's error carries the exit code as `code`.
|
|
84
|
+
const exit = err?.code;
|
|
85
|
+
const code = !err ? 0 : typeof exit === 'number' ? exit : 1;
|
|
86
|
+
resolve({ code, stderr: String(stderr ?? '') || (err ? err.message : '') });
|
|
87
|
+
});
|
|
88
|
+
if (input !== undefined)
|
|
89
|
+
child.stdin?.end(input);
|
|
90
|
+
});
|
|
91
|
+
}
|
|
92
|
+
async function ghosttyAttachedDefault() {
|
|
93
|
+
const r = await tmux('owned', ['list-clients', '-F', '#{client_session}\t#{client_termname}']);
|
|
94
|
+
const out = new Set();
|
|
95
|
+
if (!r.ok)
|
|
96
|
+
return out;
|
|
97
|
+
for (const line of r.stdout.split('\n')) {
|
|
98
|
+
const [session, term] = line.split('\t');
|
|
99
|
+
if (session && term === GHOSTTY_TERM)
|
|
100
|
+
out.add(session);
|
|
101
|
+
}
|
|
102
|
+
return out;
|
|
103
|
+
}
|
|
104
|
+
// ----------------------------------------------------------------------------
|
|
105
|
+
// The route
|
|
106
|
+
// ----------------------------------------------------------------------------
|
|
107
|
+
export function validateOpen(body) {
|
|
108
|
+
const b = (body ?? {});
|
|
109
|
+
const layout = b.layout === undefined ? 'tabs' : b.layout;
|
|
110
|
+
if (layout !== 'tabs' && layout !== 'windows') {
|
|
111
|
+
throw new SessionError(400, 'BAD_REQUEST', 'layout must be "tabs" or "windows".');
|
|
112
|
+
}
|
|
113
|
+
if (!Array.isArray(b.session_keys) || b.session_keys.length === 0 || !b.session_keys.every((k) => typeof k === 'string' && k)) {
|
|
114
|
+
throw new SessionError(400, 'BAD_REQUEST', 'session_keys must be a non-empty array of session keys.');
|
|
115
|
+
}
|
|
116
|
+
const keys = [...new Set(b.session_keys)];
|
|
117
|
+
if (keys.length > MAX_OPEN) {
|
|
118
|
+
throw new SessionError(400, 'BAD_REQUEST', `At most ${MAX_OPEN} sessions per call.`);
|
|
119
|
+
}
|
|
120
|
+
return { keys, layout };
|
|
121
|
+
}
|
|
122
|
+
export async function openTerminal(body, deps = {}) {
|
|
123
|
+
const { keys, layout } = validateOpen(body);
|
|
124
|
+
const installed = (deps.ghosttyInstalled ?? (() => existsSync(GHOSTTY_APP)))();
|
|
125
|
+
if (!installed) {
|
|
126
|
+
return { results: keys.map((k) => ({ session_key: k, status: 'failed', code: 'GHOSTTY_NOT_INSTALLED', error: `${GHOSTTY_APP} not found.` })) };
|
|
127
|
+
}
|
|
128
|
+
await (deps.ensureTitles ?? ensureTitles)();
|
|
129
|
+
const [rows, attached] = await Promise.all([
|
|
130
|
+
(deps.sessions ?? (() => listHostedSessions()))(),
|
|
131
|
+
(deps.ghosttyAttached ?? ghosttyAttachedDefault)(),
|
|
132
|
+
]);
|
|
133
|
+
const byKey = new Map(rows.map((r) => [r.session_key, r]));
|
|
134
|
+
const osascript = deps.osascript ?? ((script) => run('osascript', ['-'], script));
|
|
135
|
+
const open = deps.open ?? ((args) => run('open', args));
|
|
136
|
+
const sleep = deps.sleep ?? ((ms) => new Promise((r) => setTimeout(r, ms)));
|
|
137
|
+
const results = [];
|
|
138
|
+
let denied = false;
|
|
139
|
+
let opened = 0;
|
|
140
|
+
for (const key of keys) {
|
|
141
|
+
const row = byKey.get(key);
|
|
142
|
+
if (!row || row.state !== 'live') {
|
|
143
|
+
results.push({ session_key: key, status: 'failed', code: 'NOT_LIVE', error: 'Not a live session.' });
|
|
144
|
+
continue;
|
|
145
|
+
}
|
|
146
|
+
if (row.origin !== 'spawned' || !row.attach_argv || !row.cwd) {
|
|
147
|
+
results.push({ session_key: key, status: 'failed', code: 'NOT_OWNED', error: 'Only sessions the daemon spawned can be opened.' });
|
|
148
|
+
continue;
|
|
149
|
+
}
|
|
150
|
+
if (attached.has(key)) {
|
|
151
|
+
results.push({ session_key: key, status: 'already_open' });
|
|
152
|
+
continue;
|
|
153
|
+
}
|
|
154
|
+
if (denied) {
|
|
155
|
+
results.push({ session_key: key, status: 'failed', code: 'AUTOMATION_DENIED', error: 'macOS denied control of Ghostty.' });
|
|
156
|
+
continue;
|
|
157
|
+
}
|
|
158
|
+
if (opened > 0)
|
|
159
|
+
await sleep(STAGGER_MS);
|
|
160
|
+
let r;
|
|
161
|
+
try {
|
|
162
|
+
r = layout === 'tabs'
|
|
163
|
+
? await osascript(tabScript(row.attach_argv.map(shellWord).join(' '), row.cwd))
|
|
164
|
+
: await open(windowArgs(row.attach_argv, row.cwd));
|
|
165
|
+
}
|
|
166
|
+
catch (err) {
|
|
167
|
+
results.push({ session_key: key, status: 'failed', code: 'BAD_PATH', error: err instanceof Error ? err.message : String(err) });
|
|
168
|
+
continue;
|
|
169
|
+
}
|
|
170
|
+
if (r.code === 0) {
|
|
171
|
+
opened++;
|
|
172
|
+
results.push({ session_key: key, status: 'opened' });
|
|
173
|
+
}
|
|
174
|
+
else if (/-1743/.test(r.stderr)) {
|
|
175
|
+
denied = true;
|
|
176
|
+
results.push({ session_key: key, status: 'failed', code: 'AUTOMATION_DENIED', error: 'macOS denied control of Ghostty.' });
|
|
177
|
+
}
|
|
178
|
+
else {
|
|
179
|
+
results.push({ session_key: key, status: 'failed', code: 'OSASCRIPT_FAILED', error: r.stderr.trim().slice(0, 300) || `exit ${r.code}` });
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
log(`Sessions: open-terminal (${layout}) ${results.map((r) => `${r.session_key}=${r.status}`).join(' ')}`);
|
|
183
|
+
return { results };
|
|
184
|
+
}
|
|
185
|
+
//# sourceMappingURL=openTerminal.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"openTerminal.js","sourceRoot":"","sources":["../../src/pty/openTerminal.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
export type DialogAnswer = 'allow' | 'deny' | 'timeout' | 'error';
|
|
2
|
+
export interface DialogDeps {
|
|
3
|
+
/** Runs the AppleScript; resolves with what the user did. */
|
|
4
|
+
run?: (script: string, timeoutSec: number) => Promise<DialogAnswer>;
|
|
5
|
+
platform?: string;
|
|
6
|
+
enabled?: boolean;
|
|
7
|
+
}
|
|
8
|
+
/** For tests. */
|
|
9
|
+
export declare function _resetPairDialog(): void;
|
|
10
|
+
export declare function dialogScript(origin: string, label: string, code: string, timeoutSec: number): string;
|
|
11
|
+
/**
|
|
12
|
+
* Ask the user on this Mac. Fire and forget from the /pair/request route: the
|
|
13
|
+
* HTTP response never waits on the dialog, and nothing here throws.
|
|
14
|
+
*/
|
|
15
|
+
export declare function confirmPairingLocally(pairingId: string, deps?: DialogDeps): Promise<DialogAnswer | 'skipped'>;
|
|
16
|
+
//# sourceMappingURL=pairDialog.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"pairDialog.d.ts","sourceRoot":"","sources":["../../src/pty/pairDialog.ts"],"names":[],"mappings":"AA2BA,MAAM,MAAM,YAAY,GAAG,OAAO,GAAG,MAAM,GAAG,SAAS,GAAG,OAAO,CAAC;AAElE,MAAM,WAAW,UAAU;IACzB,6DAA6D;IAC7D,GAAG,CAAC,EAAE,CAAC,MAAM,EAAE,MAAM,EAAE,UAAU,EAAE,MAAM,KAAK,OAAO,CAAC,YAAY,CAAC,CAAC;IACpE,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,OAAO,CAAC,EAAE,OAAO,CAAC;CACnB;AAID,iBAAiB;AACjB,wBAAgB,gBAAgB,IAAI,IAAI,CAEvC;AAOD,wBAAgB,YAAY,CAAC,MAAM,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,EAAE,UAAU,EAAE,MAAM,GAAG,MAAM,CAkBpG;AAmBD;;;GAGG;AACH,wBAAsB,qBAAqB,CAAC,SAAS,EAAE,MAAM,EAAE,IAAI,GAAE,UAAe,GAAG,OAAO,CAAC,YAAY,GAAG,SAAS,CAAC,CAqCvH"}
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
// ============================================================================
|
|
2
|
+
// Native pairing approval on macOS (ADR-0012 amendment, tools ISS-0228)
|
|
3
|
+
// ============================================================================
|
|
4
|
+
// ADR-0012's gate is that NO HTTP ROUTE APPROVES A PAIRING: a compromised HQ
|
|
5
|
+
// page can ask to pair, but it cannot say yes. Until now the yes was
|
|
6
|
+
// `decibel-tools pair approve <code>`, a developer command. On macOS the
|
|
7
|
+
// daemon now shows a native dialog on the user's own screen instead:
|
|
8
|
+
//
|
|
9
|
+
// "HQ (https://hq.example) wants to connect to Decibel on this Mac.
|
|
10
|
+
// Code 123456. Allow only if HQ shows the same code." [Deny] [Allow]
|
|
11
|
+
//
|
|
12
|
+
// The gate is unchanged. The request arrives over HTTP, but the approval is a
|
|
13
|
+
// local click in a window HQ cannot see or drive. Allow approves only the
|
|
14
|
+
// exact pending request that was shown, by id AND code, so a request replaced
|
|
15
|
+
// while the dialog was open is not approved by it. Deny removes the request.
|
|
16
|
+
// No answer within the pending window (10 min) leaves it to expire.
|
|
17
|
+
//
|
|
18
|
+
// One dialog at a time; while one is open, further requests still work through
|
|
19
|
+
// the CLI. Linux and Windows keep the CLI until each gets a native prompt.
|
|
20
|
+
// DECIBEL_PAIR_DIALOG=0 turns the dialog off.
|
|
21
|
+
// ============================================================================
|
|
22
|
+
import { execFile } from 'child_process';
|
|
23
|
+
import { log } from '../config.js';
|
|
24
|
+
import { appleString } from './openTerminal.js';
|
|
25
|
+
import { approvePendingLocally, revokePairing, listPairings, PENDING_TTL_MS } from './pairing.js';
|
|
26
|
+
let open = false;
|
|
27
|
+
/** For tests. */
|
|
28
|
+
export function _resetPairDialog() {
|
|
29
|
+
open = false;
|
|
30
|
+
}
|
|
31
|
+
/** Strip anything that could not, or should not, be shown in a dialog. */
|
|
32
|
+
function shown(s, max) {
|
|
33
|
+
return s.replace(/[\u0000-\u001f\u007f]/g, ' ').trim().slice(0, max);
|
|
34
|
+
}
|
|
35
|
+
export function dialogScript(origin, label, code, timeoutSec) {
|
|
36
|
+
const who = label && label !== 'HQ' ? `${shown(label, 60)} (${shown(origin, 200)})` : `HQ (${shown(origin, 200)})`;
|
|
37
|
+
const lines = [
|
|
38
|
+
`${who} wants to connect to Decibel on this Mac.`,
|
|
39
|
+
'',
|
|
40
|
+
`Code: ${code}`,
|
|
41
|
+
'',
|
|
42
|
+
"Allow only if HQ shows exactly this code. If you didn't just ask to connect, choose Deny.",
|
|
43
|
+
];
|
|
44
|
+
// Lines joined in AppleScript itself: appleString escapes backslashes, so a
|
|
45
|
+
// "\n" inside the literal would show as text, not a line break.
|
|
46
|
+
const body = lines.map((l) => appleString(l)).join(' & linefeed & ');
|
|
47
|
+
return [
|
|
48
|
+
`display dialog (${body}) ` +
|
|
49
|
+
`with title "Decibel" buttons {"Deny", "Allow"} default button "Deny" cancel button "Deny" ` +
|
|
50
|
+
`with icon caution giving up after ${timeoutSec}`,
|
|
51
|
+
'',
|
|
52
|
+
].join('\n');
|
|
53
|
+
}
|
|
54
|
+
function runDefault(script, timeoutSec) {
|
|
55
|
+
return new Promise((resolve) => {
|
|
56
|
+
const child = execFile('osascript', ['-'], { timeout: (timeoutSec + 30) * 1000 }, (err, stdout, stderr) => {
|
|
57
|
+
if (err) {
|
|
58
|
+
// The cancel button ("Deny") ends osascript with error -128.
|
|
59
|
+
resolve(/-128/.test(String(stderr)) ? 'deny' : 'error');
|
|
60
|
+
return;
|
|
61
|
+
}
|
|
62
|
+
const out = String(stdout);
|
|
63
|
+
if (/gave up:true/.test(out))
|
|
64
|
+
resolve('timeout');
|
|
65
|
+
else if (/button returned:Allow/.test(out))
|
|
66
|
+
resolve('allow');
|
|
67
|
+
else
|
|
68
|
+
resolve('deny');
|
|
69
|
+
});
|
|
70
|
+
child.stdin?.end(script);
|
|
71
|
+
});
|
|
72
|
+
}
|
|
73
|
+
/**
|
|
74
|
+
* Ask the user on this Mac. Fire and forget from the /pair/request route: the
|
|
75
|
+
* HTTP response never waits on the dialog, and nothing here throws.
|
|
76
|
+
*/
|
|
77
|
+
export async function confirmPairingLocally(pairingId, deps = {}) {
|
|
78
|
+
const platform = deps.platform ?? process.platform;
|
|
79
|
+
const enabled = deps.enabled ?? process.env.DECIBEL_PAIR_DIALOG !== '0';
|
|
80
|
+
if (platform !== 'darwin' || !enabled || open)
|
|
81
|
+
return 'skipped';
|
|
82
|
+
const p = listPairings().find((x) => x.id === pairingId && x.status === 'pending');
|
|
83
|
+
if (!p || !p.code)
|
|
84
|
+
return 'skipped';
|
|
85
|
+
open = true;
|
|
86
|
+
try {
|
|
87
|
+
const timeoutSec = Math.floor(PENDING_TTL_MS / 1000);
|
|
88
|
+
let script;
|
|
89
|
+
try {
|
|
90
|
+
script = dialogScript(p.origin, p.label, p.code, timeoutSec);
|
|
91
|
+
}
|
|
92
|
+
catch (err) {
|
|
93
|
+
log(`Pairing: dialog not shown: ${err instanceof Error ? err.message : String(err)}`);
|
|
94
|
+
return 'error';
|
|
95
|
+
}
|
|
96
|
+
const answer = await (deps.run ?? runDefault)(script, timeoutSec);
|
|
97
|
+
if (answer === 'allow') {
|
|
98
|
+
try {
|
|
99
|
+
await approvePendingLocally(p.id, p.code);
|
|
100
|
+
log(`Pairing: ${p.origin} approved in the native dialog`);
|
|
101
|
+
}
|
|
102
|
+
catch (err) {
|
|
103
|
+
log(`Pairing: dialog approval not applied: ${err instanceof Error ? err.message : String(err)}`);
|
|
104
|
+
return 'error';
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
else if (answer === 'deny') {
|
|
108
|
+
await revokePairing(p.id);
|
|
109
|
+
log(`Pairing: ${p.origin} denied in the native dialog`);
|
|
110
|
+
}
|
|
111
|
+
return answer;
|
|
112
|
+
}
|
|
113
|
+
catch (err) {
|
|
114
|
+
log(`Pairing: dialog failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
115
|
+
return 'error';
|
|
116
|
+
}
|
|
117
|
+
finally {
|
|
118
|
+
open = false;
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
//# sourceMappingURL=pairDialog.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"pairDialog.js","sourceRoot":"","sources":["../../src/pty/pairDialog.ts"],"names":[],"mappings":"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"}
|