martty 0.2.11
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +118 -0
- package/bin/dsh-tui.js +68 -0
- package/cordis.patch.yml +30 -0
- package/creator/cordis.patch.yml +6 -0
- package/creator/package.json +10 -0
- package/lib/acp-client-events.js +65 -0
- package/lib/acp-client.js +114 -0
- package/lib/acp-host.js +24 -0
- package/lib/acp-session-config.js +376 -0
- package/lib/acp-session-plan.js +196 -0
- package/lib/acp-session-stats.js +239 -0
- package/lib/agent.js +64 -0
- package/lib/boot.js +119 -0
- package/lib/client-process.js +11 -0
- package/lib/client-run.js +379 -0
- package/lib/cordis-protocol.js +51 -0
- package/lib/creator-overlay.js +77 -0
- package/lib/demo-skin.js +79 -0
- package/lib/ember.js +20 -0
- package/lib/index.js +226 -0
- package/lib/inspect.js +971 -0
- package/lib/jsonrpc-line-transport.js +155 -0
- package/lib/mux.js +281 -0
- package/lib/palettes/default.json +44 -0
- package/lib/palettes/ember.json +44 -0
- package/lib/plan-view.js +92 -0
- package/lib/profile-acp-client.js +11 -0
- package/lib/right-demo.js +55 -0
- package/lib/runner.js +94 -0
- package/lib/spawn-tui.js +179 -0
- package/lib/stats-view.js +90 -0
- package/lib/tui-commands.js +144 -0
- package/lib/tui-overlay.js +252 -0
- package/lib/tui-slots.js +351 -0
- package/lib/tui-theme.js +463 -0
- package/package.json +83 -0
- package/skills/tui-plugin-development/SKILL.md +172 -0
- package/vendor/darwin-arm64/dsh-tui +0 -0
- package/vendor/darwin-x64/dsh-tui +0 -0
- package/vendor/linux-arm64/dsh-tui +0 -0
- package/vendor/linux-x64/dsh-tui +0 -0
- package/vendor/win32-x64/dsh-tui.exe +0 -0
package/lib/index.js
ADDED
|
@@ -0,0 +1,226 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* TUI shell Cordis plugin: spawn the native painter and mux ACP + palettes.
|
|
3
|
+
*
|
|
4
|
+
* Injects `acpClient`, `acpSessionConfig`, the TUI registries, and the sibling
|
|
5
|
+
* `tuiCordisClientRunner`.
|
|
6
|
+
* The shell owns the native painter and only binds transport; it does not
|
|
7
|
+
* evaluate dynamic Client code. Does not import dsh or dsh-acp.
|
|
8
|
+
*
|
|
9
|
+
* dsh-tui --agent dsh-acp
|
|
10
|
+
* dsh-tui --agent dsh --agent-arg --profile --agent-arg acp
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import { nativeBinary, spawnPluginTui } from './spawn-tui.js'
|
|
14
|
+
import { CORDIS_METHODS } from './cordis-protocol.js'
|
|
15
|
+
import { muxAcpAndCompositor } from './mux.js'
|
|
16
|
+
|
|
17
|
+
export const name = 'dsh-tui-shell'
|
|
18
|
+
export const inject = [
|
|
19
|
+
'acpClient', 'tuiTheme', 'tuiSlots', 'tuiCommands', 'tuiOverlay',
|
|
20
|
+
'acpClientEvents', 'acpSessionConfig', 'tuiCordisClientRunner',
|
|
21
|
+
]
|
|
22
|
+
|
|
23
|
+
const shellStateKey = Symbol.for('martty/shell-state')
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* A module reload must not create a second stdin reader. `globalThis` is the
|
|
27
|
+
* ownership boundary of the Node host process; ESM module locals are not,
|
|
28
|
+
* because Cordis can evaluate a fresh module instance during HMR.
|
|
29
|
+
* @type {{
|
|
30
|
+
* livePainter: null | ({ muxed: boolean } & Awaited<ReturnType<typeof spawnPluginTui>>),
|
|
31
|
+
* startingPainter: null | Promise<{ muxed: boolean } & Awaited<ReturnType<typeof spawnPluginTui>>>,
|
|
32
|
+
* refuseRespawn: boolean,
|
|
33
|
+
* }}
|
|
34
|
+
*/
|
|
35
|
+
const shellState = globalThis[shellStateKey] ??= {
|
|
36
|
+
livePainter: null,
|
|
37
|
+
startingPainter: null,
|
|
38
|
+
refuseRespawn: false,
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/** Drop the sticky painter so tests can apply() more than once in-process. */
|
|
42
|
+
export function resetShellForTests() {
|
|
43
|
+
shellState.livePainter = null
|
|
44
|
+
shellState.startingPainter = null
|
|
45
|
+
shellState.refuseRespawn = false
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Fiber dispose (HMR / config-watch recompose) SIGTERM is not a user quit.
|
|
50
|
+
* A normal exit or SIGINT is: the host must give the TTY back.
|
|
51
|
+
* @param {NodeJS.Signals | null} signal
|
|
52
|
+
* @returns {boolean}
|
|
53
|
+
*/
|
|
54
|
+
export function shouldQuitOnPainterExit(signal) {
|
|
55
|
+
return signal !== 'SIGTERM'
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/** Child raw-mode / alt-screen is the same TTY. Reset it if the painter died dirty. */
|
|
59
|
+
export function restoreHostTty() {
|
|
60
|
+
if (process.stdin.isTTY) {
|
|
61
|
+
try {
|
|
62
|
+
process.stdin.setRawMode(false)
|
|
63
|
+
} catch {
|
|
64
|
+
// stdin already closed
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
if (process.stdout.isTTY) {
|
|
68
|
+
process.stdout.write(
|
|
69
|
+
'\x1b[?2004l\x1b[?1006l\x1b[?1003l\x1b[?1002l\x1b[?1000l\x1b[?25h\x1b[?1049l',
|
|
70
|
+
)
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* @param {object} ctx
|
|
76
|
+
* @param {{ extraArgs?: string[] }} [options]
|
|
77
|
+
*/
|
|
78
|
+
export function apply(ctx, options = {}) {
|
|
79
|
+
return applyShell(ctx, options)
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* @param {object} ctx
|
|
84
|
+
* @param {{ extraArgs?: string[] }} [options]
|
|
85
|
+
*/
|
|
86
|
+
export async function applyShell(ctx, options = {}) {
|
|
87
|
+
const handle = ctx.tuiTheme ?? ctx.get?.('tuiTheme')
|
|
88
|
+
if (handle === undefined || typeof handle.bindNotify !== 'function'
|
|
89
|
+
|| typeof handle.observeSelected !== 'function') {
|
|
90
|
+
throw new Error('dsh-tui-shell: ctx.tuiTheme must expose bindNotify and observeSelected')
|
|
91
|
+
}
|
|
92
|
+
const slots = ctx.tuiSlots ?? ctx.get?.('tuiSlots')
|
|
93
|
+
if (slots === undefined || typeof slots.bindNotify !== 'function') {
|
|
94
|
+
throw new Error('dsh-tui-shell: ctx.tuiSlots must expose bindNotify')
|
|
95
|
+
}
|
|
96
|
+
const commands = ctx.tuiCommands ?? ctx.get?.('tuiCommands')
|
|
97
|
+
if (commands === undefined || typeof commands.bindNotify !== 'function'
|
|
98
|
+
|| typeof commands.dispatch !== 'function') {
|
|
99
|
+
throw new Error('dsh-tui-shell: ctx.tuiCommands must expose bindNotify and dispatch')
|
|
100
|
+
}
|
|
101
|
+
const overlay = ctx.tuiOverlay ?? ctx.get?.('tuiOverlay')
|
|
102
|
+
if (overlay === undefined || typeof overlay.bindNotify !== 'function'
|
|
103
|
+
|| typeof overlay.dispatch !== 'function') {
|
|
104
|
+
throw new Error('dsh-tui-shell: ctx.tuiOverlay must expose bindNotify and dispatch')
|
|
105
|
+
}
|
|
106
|
+
const agent = ctx.acpClient ?? ctx.get?.('acpClient')
|
|
107
|
+
if (agent === undefined || agent.stdin === undefined || agent.stdout === undefined) {
|
|
108
|
+
throw new Error('dsh-tui-shell: ctx.acpClient must expose stdin and stdout')
|
|
109
|
+
}
|
|
110
|
+
const clientRunner = ctx.tuiCordisClientRunner ?? ctx.get?.('tuiCordisClientRunner')
|
|
111
|
+
if (clientRunner === undefined || typeof clientRunner.bindTransport !== 'function'
|
|
112
|
+
|| typeof clientRunner.selectTheme !== 'function') {
|
|
113
|
+
throw new Error(
|
|
114
|
+
'dsh-tui-shell: ctx.tuiCordisClientRunner must expose bindTransport and selectTheme',
|
|
115
|
+
)
|
|
116
|
+
}
|
|
117
|
+
const sessionConfig = ctx.acpSessionConfig ?? ctx.get?.('acpSessionConfig')
|
|
118
|
+
if (sessionConfig === undefined || typeof sessionConfig.bindTransport !== 'function'
|
|
119
|
+
|| typeof sessionConfig.observeClient !== 'function'
|
|
120
|
+
|| typeof sessionConfig.observeAgent !== 'function') {
|
|
121
|
+
throw new Error(
|
|
122
|
+
'dsh-tui-shell: ctx.acpSessionConfig must expose bindTransport and ACP observers',
|
|
123
|
+
)
|
|
124
|
+
}
|
|
125
|
+
const clientEvents = ctx.acpClientEvents ?? ctx.get?.('acpClientEvents')
|
|
126
|
+
if (clientEvents === undefined || typeof clientEvents.observeClient !== 'function'
|
|
127
|
+
|| typeof clientEvents.observeAgent !== 'function') {
|
|
128
|
+
throw new Error(
|
|
129
|
+
'dsh-tui-shell: ctx.acpClientEvents must expose ACP observers',
|
|
130
|
+
)
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
// Config-watch recomposes the tree at boot and disposes this fiber. Keep
|
|
134
|
+
// one painter; do not kill it from ctx.effect or the screen never appears.
|
|
135
|
+
// After a user quit, do not spawn a second empty TUI on the same TTY.
|
|
136
|
+
if (shellState.refuseRespawn) {
|
|
137
|
+
return
|
|
138
|
+
}
|
|
139
|
+
let connection = shellState.livePainter
|
|
140
|
+
if (connection === null || connection.child.exitCode !== null) {
|
|
141
|
+
if (shellState.startingPainter === null) {
|
|
142
|
+
shellState.startingPainter = (async () => {
|
|
143
|
+
const bin = nativeBinary()
|
|
144
|
+
const started = await spawnPluginTui(
|
|
145
|
+
bin,
|
|
146
|
+
options.extraArgs ?? [],
|
|
147
|
+
options.tty,
|
|
148
|
+
)
|
|
149
|
+
const live = { ...started, muxed: false }
|
|
150
|
+
shellState.livePainter = live
|
|
151
|
+
const { child } = live
|
|
152
|
+
child.on('exit', (code, signal) => {
|
|
153
|
+
try {
|
|
154
|
+
agent.child?.kill('SIGTERM')
|
|
155
|
+
} catch {
|
|
156
|
+
// already gone
|
|
157
|
+
}
|
|
158
|
+
restoreHostTty()
|
|
159
|
+
if (!shouldQuitOnPainterExit(signal)) return
|
|
160
|
+
shellState.refuseRespawn = true
|
|
161
|
+
if (shellState.livePainter?.child === child) shellState.livePainter = null
|
|
162
|
+
process.exit(code ?? (signal === 'SIGINT' ? 130 : 0))
|
|
163
|
+
})
|
|
164
|
+
child.on('error', (err) => {
|
|
165
|
+
console.error(`dsh-tui: ${err.message}`)
|
|
166
|
+
process.exit(1)
|
|
167
|
+
})
|
|
168
|
+
process.once('exit', () => {
|
|
169
|
+
try {
|
|
170
|
+
child.kill('SIGTERM')
|
|
171
|
+
} catch {
|
|
172
|
+
// already gone
|
|
173
|
+
}
|
|
174
|
+
})
|
|
175
|
+
return live
|
|
176
|
+
})()
|
|
177
|
+
}
|
|
178
|
+
const starting = shellState.startingPainter
|
|
179
|
+
try {
|
|
180
|
+
connection = await starting
|
|
181
|
+
} finally {
|
|
182
|
+
if (shellState.startingPainter === starting) shellState.startingPainter = null
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
if (!connection.muxed) {
|
|
187
|
+
const mux = muxAcpAndCompositor({
|
|
188
|
+
agent,
|
|
189
|
+
tui: connection,
|
|
190
|
+
onHost(message) {
|
|
191
|
+
clientRunner.onHost(message)
|
|
192
|
+
},
|
|
193
|
+
onCordisReady() {
|
|
194
|
+
clientRunner.sync()
|
|
195
|
+
},
|
|
196
|
+
onAcp(direction, message) {
|
|
197
|
+
if (direction === 'client') {
|
|
198
|
+
clientEvents.observeClient(message)
|
|
199
|
+
} else {
|
|
200
|
+
clientEvents.observeAgent(message)
|
|
201
|
+
}
|
|
202
|
+
},
|
|
203
|
+
onCompositor(message) {
|
|
204
|
+
if (message.method === CORDIS_METHODS.themeSelected) {
|
|
205
|
+
return clientRunner.selectTheme(message.params)
|
|
206
|
+
}
|
|
207
|
+
if (message.method === CORDIS_METHODS.commandInvoke) {
|
|
208
|
+
return commands.dispatch(message.params)
|
|
209
|
+
}
|
|
210
|
+
if (message.method === CORDIS_METHODS.overlayEvent) {
|
|
211
|
+
return overlay.dispatch(message.params)
|
|
212
|
+
}
|
|
213
|
+
throw new Error(`unsupported Cordis TUI method: ${String(message.method)}`)
|
|
214
|
+
},
|
|
215
|
+
})
|
|
216
|
+
handle.bindNotify((method, params) => mux.notifyTui(method, params))
|
|
217
|
+
slots.bindNotify((method, params) => mux.notifyTui(method, params))
|
|
218
|
+
commands.bindNotify((method, params) => mux.notifyTui(method, params))
|
|
219
|
+
overlay.bindNotify((method, params) => mux.notifyTui(method, params))
|
|
220
|
+
clientRunner.bindTransport(mux.requestAgent)
|
|
221
|
+
sessionConfig.bindTransport(mux.requestTui)
|
|
222
|
+
connection.resume?.()
|
|
223
|
+
connection.muxed = true
|
|
224
|
+
}
|
|
225
|
+
// Cordis collects apply's return as an effect. Only a disposer or nothing.
|
|
226
|
+
}
|