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.
Files changed (43) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +118 -0
  3. package/bin/dsh-tui.js +68 -0
  4. package/cordis.patch.yml +30 -0
  5. package/creator/cordis.patch.yml +6 -0
  6. package/creator/package.json +10 -0
  7. package/lib/acp-client-events.js +65 -0
  8. package/lib/acp-client.js +114 -0
  9. package/lib/acp-host.js +24 -0
  10. package/lib/acp-session-config.js +376 -0
  11. package/lib/acp-session-plan.js +196 -0
  12. package/lib/acp-session-stats.js +239 -0
  13. package/lib/agent.js +64 -0
  14. package/lib/boot.js +119 -0
  15. package/lib/client-process.js +11 -0
  16. package/lib/client-run.js +379 -0
  17. package/lib/cordis-protocol.js +51 -0
  18. package/lib/creator-overlay.js +77 -0
  19. package/lib/demo-skin.js +79 -0
  20. package/lib/ember.js +20 -0
  21. package/lib/index.js +226 -0
  22. package/lib/inspect.js +971 -0
  23. package/lib/jsonrpc-line-transport.js +155 -0
  24. package/lib/mux.js +281 -0
  25. package/lib/palettes/default.json +44 -0
  26. package/lib/palettes/ember.json +44 -0
  27. package/lib/plan-view.js +92 -0
  28. package/lib/profile-acp-client.js +11 -0
  29. package/lib/right-demo.js +55 -0
  30. package/lib/runner.js +94 -0
  31. package/lib/spawn-tui.js +179 -0
  32. package/lib/stats-view.js +90 -0
  33. package/lib/tui-commands.js +144 -0
  34. package/lib/tui-overlay.js +252 -0
  35. package/lib/tui-slots.js +351 -0
  36. package/lib/tui-theme.js +463 -0
  37. package/package.json +83 -0
  38. package/skills/tui-plugin-development/SKILL.md +172 -0
  39. package/vendor/darwin-arm64/dsh-tui +0 -0
  40. package/vendor/darwin-x64/dsh-tui +0 -0
  41. package/vendor/linux-arm64/dsh-tui +0 -0
  42. package/vendor/linux-x64/dsh-tui +0 -0
  43. package/vendor/win32-x64/dsh-tui.exe +0 -0
@@ -0,0 +1,379 @@
1
+ /**
2
+ * Evaluate a Cordis `code.client` function body on the TUI client tree.
3
+ *
4
+ * Same closure convention as the web runner: the source is an async function
5
+ * body that returns a plugin. Open services include `tuiTheme`, `tuiSlots`,
6
+ * `acpSessionConfig`, `acpSessionPlan`, `acpSessionStats`, and lifecycle-owned `timer`; `host.call` reaches this
7
+ * Package's Host half.
8
+ * No React, browser slots, TTY, or raw Host service names.
9
+ */
10
+
11
+ const ALLOWED_INJECT = new Set([
12
+ 'tuiTheme',
13
+ 'tuiSlots',
14
+ 'tuiCommands',
15
+ 'tuiOverlay',
16
+ 'acpSessionConfig',
17
+ 'acpSessionPlan',
18
+ 'acpSessionStats',
19
+ 'timer',
20
+ ])
21
+
22
+ const THEME_TEACHING =
23
+ 'TUI Client Theme is ctx.tuiTheme, not ctx.theme. '
24
+ + 'register a complete 18-token palette with tuiTheme.register({ id, label, dark, light }) '
25
+ + 'inside one Theme Plugin; /theme loads or replaces that whole Plugin. '
26
+ + 'Token names are closed #RRGGBB keys, not CSS variables.'
27
+
28
+ const SLOT_TEACHING =
29
+ 'TUI shell slots are ctx.tuiSlots, not browser ctx.slots. '
30
+ + 'Inspect tuiSlots.list(), then use tuiSlots.inject(name, () => tuiSlots.register('
31
+ + '{ name, id }, nodes)).'
32
+
33
+ /**
34
+ * @param {string} clientCode
35
+ * @param {{ pluginId?: string, tuiTheme?: object, tuiSlots?: object, tuiCommands?: object, tuiOverlay?: object, acpSessionConfig?: object, acpSessionPlan?: object, acpSessionStats?: object, timer?: object, invoke?: Function }} env
36
+ * @returns {Promise<{ waitingFor: string[], dispose: () => void }>}
37
+ */
38
+ export async function applyClientHalf(clientCode, env) {
39
+ if (typeof clientCode !== 'string' || clientCode.trim().length === 0) {
40
+ throw new Error('TUI Client half is empty')
41
+ }
42
+ let plugin
43
+ try {
44
+ const factory = new Function(
45
+ 'React',
46
+ 'styles',
47
+ 'host',
48
+ 'harness',
49
+ 'require',
50
+ 'process',
51
+ 'Buffer',
52
+ 'setTimeout',
53
+ 'setInterval',
54
+ `return (async () => {\n${clientCode}\n})()`,
55
+ )
56
+ plugin = await factory(
57
+ reactTrap,
58
+ stylesTrap,
59
+ hostFor(env),
60
+ harnessTrap,
61
+ requireTrap,
62
+ processTrap,
63
+ bufferTrap,
64
+ timerGlobalTrap,
65
+ timerGlobalTrap,
66
+ )
67
+ } catch (error) {
68
+ if (error instanceof SyntaxError) {
69
+ throw new Error(
70
+ `TUI Client half failed to parse: ${error.message}\n`
71
+ + 'The Client half is plain JavaScript (no JSX, no TypeScript).',
72
+ )
73
+ }
74
+ throw error
75
+ }
76
+ const evaluated = normalizePlugin(plugin)
77
+ const inject = evaluated.inject ?? []
78
+ for (const name of inject) {
79
+ if (name === 'theme') {
80
+ throw new Error(THEME_TEACHING)
81
+ }
82
+ if (name === 'slots') {
83
+ throw new Error(SLOT_TEACHING)
84
+ }
85
+ if (!ALLOWED_INJECT.has(name)) {
86
+ throw new Error(
87
+ `TUI Client inject "${name}" is not open. Open services: tuiTheme, tuiSlots, tuiCommands, tuiOverlay, acpSessionConfig, acpSessionPlan, acpSessionStats, timer.`,
88
+ )
89
+ }
90
+ }
91
+ const waitingFor = inject.filter((name) => env[name] === undefined)
92
+ if (waitingFor.length > 0) return { waitingFor, dispose() {} }
93
+
94
+ const owned = new Set()
95
+ const own = (dispose) => {
96
+ if (typeof dispose !== 'function') return dispose
97
+ let active = true
98
+ const release = () => {
99
+ if (!active) return
100
+ active = false
101
+ owned.delete(release)
102
+ return dispose()
103
+ }
104
+ owned.add(release)
105
+ return release
106
+ }
107
+ const ctx = restrictedCtx(env, own, new Set(inject))
108
+ const result = await evaluated.apply(ctx)
109
+ if (typeof result === 'function' && !owned.has(result)) own(result)
110
+ return {
111
+ waitingFor: [],
112
+ dispose() {
113
+ for (const release of [...owned].reverse()) release()
114
+ },
115
+ }
116
+ }
117
+
118
+ function normalizePlugin(value) {
119
+ if (typeof value === 'function') {
120
+ return { inject: [], apply: value }
121
+ }
122
+ if (value !== null && typeof value === 'object' && typeof value.apply === 'function') {
123
+ const inject = Array.isArray(value.inject) ? value.inject.map(String) : []
124
+ return { inject, apply: value.apply.bind(value) }
125
+ }
126
+ throw new Error(
127
+ 'TUI Client half must return a plugin object `{ inject, apply }` or an apply function. '
128
+ + THEME_TEACHING,
129
+ )
130
+ }
131
+
132
+ function restrictedCtx(env, own, inject) {
133
+ const sourceTheme = env.tuiTheme
134
+ const sourceSlots = env.tuiSlots
135
+ const sourceCommands = env.tuiCommands
136
+ const sourceOverlay = env.tuiOverlay
137
+ const sourceSessionConfig = env.acpSessionConfig
138
+ const sourceSessionPlan = env.acpSessionPlan
139
+ const sourceSessionStats = env.acpSessionStats
140
+ const sourceTimer = env.timer
141
+ const tuiTheme = sourceTheme === undefined
142
+ ? undefined
143
+ : {
144
+ register(palette, options) {
145
+ const register = typeof env.pluginId === 'string'
146
+ && typeof sourceTheme.registerOwned === 'function'
147
+ ? sourceTheme.registerOwned.bind(sourceTheme, env.pluginId)
148
+ : sourceTheme.register.bind(sourceTheme)
149
+ const registration = register(palette, options)
150
+ const dispose = own(
151
+ typeof registration?.dispose === 'function'
152
+ ? registration.dispose.bind(registration)
153
+ : registration,
154
+ )
155
+ if (typeof dispose !== 'function') return dispose
156
+ dispose.dispose = dispose
157
+ if (typeof registration?.update === 'function') {
158
+ dispose.update = (patch) => registration.update(patch)
159
+ }
160
+ return dispose
161
+ },
162
+ }
163
+ const tuiSlots = sourceSlots === undefined
164
+ ? undefined
165
+ : {
166
+ inject(name, callback) {
167
+ return own(sourceSlots.inject(name, callback))
168
+ },
169
+ register(options, nodes) {
170
+ const panel = sourceSlots.register(options, nodes)
171
+ const dispose = own(() => panel.dispose())
172
+ return {
173
+ update(nextNodes) {
174
+ return panel.update(nextNodes)
175
+ },
176
+ dispose,
177
+ }
178
+ },
179
+ list() {
180
+ return sourceSlots.list()
181
+ },
182
+ }
183
+ const tuiCommands = sourceCommands === undefined
184
+ ? undefined
185
+ : {
186
+ register(options, handler) {
187
+ return own(sourceCommands.register(options, handler))
188
+ },
189
+ list() {
190
+ return sourceCommands.list()
191
+ },
192
+ }
193
+ const tuiOverlay = sourceOverlay === undefined
194
+ ? undefined
195
+ : {
196
+ openSlider(options, handlers) {
197
+ const controller = sourceOverlay.openSlider(options, handlers)
198
+ const close = own(() => controller.close())
199
+ return { close }
200
+ },
201
+ openView(options, handlers) {
202
+ const controller = sourceOverlay.openView(options, handlers)
203
+ const close = own(() => controller.close())
204
+ return { close }
205
+ },
206
+ active() {
207
+ return sourceOverlay.active()
208
+ },
209
+ }
210
+ const acpSessionConfig = sourceSessionConfig === undefined
211
+ ? undefined
212
+ : {
213
+ list() {
214
+ return sourceSessionConfig.list()
215
+ },
216
+ current(id) {
217
+ return sourceSessionConfig.current(id)
218
+ },
219
+ byCategory(category) {
220
+ return sourceSessionConfig.byCategory(category)
221
+ },
222
+ transaction(selector) {
223
+ const transaction = sourceSessionConfig.transaction(selector)
224
+ own(() => transaction.rollback())
225
+ return transaction
226
+ },
227
+ set(id, value) {
228
+ return sourceSessionConfig.set(id, value)
229
+ },
230
+ subscribe(listener) {
231
+ return own(sourceSessionConfig.subscribe(listener))
232
+ },
233
+ }
234
+ const acpSessionPlan = sourceSessionPlan === undefined
235
+ ? undefined
236
+ : {
237
+ list() {
238
+ return sourceSessionPlan.list()
239
+ },
240
+ current() {
241
+ return sourceSessionPlan.current()
242
+ },
243
+ subscribe(listener) {
244
+ return own(sourceSessionPlan.subscribe(listener))
245
+ },
246
+ }
247
+ const acpSessionStats = sourceSessionStats === undefined
248
+ ? undefined
249
+ : {
250
+ current() {
251
+ return sourceSessionStats.current()
252
+ },
253
+ subscribe(listener) {
254
+ return own(sourceSessionStats.subscribe(listener))
255
+ },
256
+ }
257
+ const timer = sourceTimer === undefined || !inject.has('timer')
258
+ ? undefined
259
+ : {
260
+ interval(callback, delay) {
261
+ return own(sourceTimer.interval(callback, delay))
262
+ },
263
+ timeout(callback, delay) {
264
+ return own(sourceTimer.timeout(callback, delay))
265
+ },
266
+ }
267
+ return {
268
+ tuiTheme,
269
+ tuiSlots,
270
+ tuiCommands,
271
+ tuiOverlay,
272
+ acpSessionConfig,
273
+ acpSessionPlan,
274
+ acpSessionStats,
275
+ timer,
276
+ interval: timer?.interval,
277
+ timeout: timer?.timeout,
278
+ get(name) {
279
+ if (name === 'tuiTheme') return tuiTheme
280
+ if (name === 'tuiSlots') return tuiSlots
281
+ if (name === 'tuiCommands') return tuiCommands
282
+ if (name === 'tuiOverlay') return tuiOverlay
283
+ if (name === 'acpSessionConfig') return acpSessionConfig
284
+ if (name === 'acpSessionPlan') return acpSessionPlan
285
+ if (name === 'acpSessionStats') return acpSessionStats
286
+ if (name === 'timer') return timer
287
+ return undefined
288
+ },
289
+ effect(fn) {
290
+ return typeof fn === 'function' ? own(fn()) : undefined
291
+ },
292
+ on() {
293
+ throw new Error('TUI Client events are not open yet')
294
+ },
295
+ provide() {
296
+ throw new Error('TUI Client halves cannot provide services')
297
+ },
298
+ plugin() {
299
+ throw new Error('mount TUI contributions with tuiTheme/tuiSlots, not ctx.plugin')
300
+ },
301
+ }
302
+ }
303
+
304
+ function reactTrap() {
305
+ throw new Error(
306
+ 'React is not available on the TUI Client. Use tuiTheme palettes or tuiSlots TuiNode trees.',
307
+ )
308
+ }
309
+
310
+ const stylesTrap = {
311
+ insert() {
312
+ throw new Error('styles.insert is a browser Client symbol. TUI palettes use tuiTheme.register.')
313
+ },
314
+ }
315
+
316
+ function hostFor(env) {
317
+ return {
318
+ call(method, args = null) {
319
+ if (typeof env.invoke !== 'function') {
320
+ return Promise.reject(new Error('host.call is unavailable without an active TUI Cordis Host run'))
321
+ }
322
+ return Promise.resolve().then(() => env.invoke(method, args))
323
+ },
324
+ }
325
+ }
326
+
327
+ function timerGlobalTrap() {
328
+ throw new Error(
329
+ "timer globals are unavailable in dynamic TUI plugins; declare inject: ['timer'] and use ctx.interval/timeout",
330
+ )
331
+ }
332
+
333
+ /** Create lifecycle-neutral timer operations; the dynamic Context owns each returned disposer. */
334
+ export function createClientTimer() {
335
+ return {
336
+ interval(callback, delay) {
337
+ assertTimerArgs('interval', callback, delay)
338
+ const handle = globalThis.setInterval(callback, delay)
339
+ return () => globalThis.clearInterval(handle)
340
+ },
341
+ timeout(callback, delay) {
342
+ assertTimerArgs('timeout', callback, delay)
343
+ const handle = globalThis.setTimeout(callback, delay)
344
+ return () => globalThis.clearTimeout(handle)
345
+ },
346
+ }
347
+ }
348
+
349
+ function assertTimerArgs(name, callback, delay) {
350
+ if (typeof callback !== 'function') throw new Error(`ctx.${name}: callback must be a function`)
351
+ if (typeof delay !== 'number' || !Number.isFinite(delay) || delay < 0) {
352
+ throw new Error(`ctx.${name}: delay must be a non-negative finite number`)
353
+ }
354
+ }
355
+
356
+ const harnessTrap = new Proxy({}, {
357
+ get(_target, prop) {
358
+ throw new Error(
359
+ `harness.${String(prop)} belongs to the HOST half (code.host). `
360
+ + 'Register Package-private methods there with harness.handle; call them here with host.call.',
361
+ )
362
+ },
363
+ })
364
+
365
+ function requireTrap() {
366
+ throw new Error('modules cannot be imported in a dynamic Client half')
367
+ }
368
+
369
+ const processTrap = new Proxy({}, {
370
+ get() {
371
+ throw new Error('process is not available in a TUI Client half')
372
+ },
373
+ })
374
+
375
+ const bufferTrap = new Proxy({}, {
376
+ get() {
377
+ throw new Error('Buffer is not available in a TUI Client half')
378
+ },
379
+ })
@@ -0,0 +1,51 @@
1
+ /** ACP extension contract for DSH Cordis client capabilities. */
2
+
3
+ export const CORDIS_PROTOCOL = 0
4
+
5
+ export const CORDIS_CAPABILITY = Object.freeze({ protocol: CORDIS_PROTOCOL })
6
+
7
+ export const CORDIS_METHODS = Object.freeze({
8
+ inspectSync: '_dsh/cordis/inspect/sync',
9
+ inspectResolve: '_dsh/cordis/inspect/resolve',
10
+ inspectQuery: '_dsh/cordis/inspect/query',
11
+ inspectQueryResolved: '_dsh/cordis/inspect/query-resolved',
12
+ runHost: '_dsh/cordis/run/host',
13
+ getClientCode: '_dsh/cordis/run/client-code',
14
+ resolveRequestRun: '_dsh/cordis/run/resolve',
15
+ requestRun: '_dsh/cordis/run/request',
16
+ requestRunResolved: '_dsh/cordis/run/request-resolved',
17
+ userRun: '_dsh/cordis/run/user',
18
+ settleUserRun: '_dsh/cordis/run/settle',
19
+ pluginInvoke: '_dsh/cordis/plugin/invoke',
20
+ pluginsList: '_dsh/cordis/plugins/list',
21
+ pluginStart: '_dsh/cordis/plugins/start',
22
+ pluginStop: '_dsh/cordis/plugins/stop',
23
+ pluginRetract: '_dsh/cordis/plugins/retract',
24
+ themeUpdate: '_dsh/cordis/tui/theme/update',
25
+ themeRemove: '_dsh/cordis/tui/theme/remove',
26
+ themeSelected: '_dsh/cordis/tui/theme/selected',
27
+ slotsUpdate: '_dsh/cordis/tui/slots/update',
28
+ commandsUpdate: '_dsh/cordis/tui/commands/update',
29
+ commandInvoke: '_dsh/cordis/tui/commands/invoke',
30
+ overlayUpdate: '_dsh/cordis/tui/overlay/update',
31
+ overlayEvent: '_dsh/cordis/tui/overlay/event',
32
+ sessionConfigSet: '_dsh/cordis/tui/session-config/set',
33
+ })
34
+
35
+ /**
36
+ * Read the negotiated DSH Cordis capability from an ACP initialize result.
37
+ * @param {unknown} result
38
+ * @returns {{ protocol: number } | null}
39
+ */
40
+ export function readCordisCapability(result) {
41
+ if (result === null || typeof result !== 'object' || Array.isArray(result)) return null
42
+ const capabilities = result.agentCapabilities
43
+ if (capabilities === null || typeof capabilities !== 'object' || Array.isArray(capabilities)) return null
44
+ const meta = capabilities._meta
45
+ if (meta === null || typeof meta !== 'object' || Array.isArray(meta)) return null
46
+ const dsh = meta.dsh
47
+ if (dsh === null || typeof dsh !== 'object' || Array.isArray(dsh)) return null
48
+ const cordis = dsh.cordis
49
+ if (cordis === null || typeof cordis !== 'object' || Array.isArray(cordis)) return null
50
+ return cordis.protocol === CORDIS_PROTOCOL ? { protocol: CORDIS_PROTOCOL } : null
51
+ }
@@ -0,0 +1,77 @@
1
+ /**
2
+ * Add TUI plugin authoring guidance to the shipped Creator preset without
3
+ * copying or modifying that preset's composition.
4
+ */
5
+
6
+ import { readFileSync } from 'node:fs'
7
+
8
+ export const name = 'tui-creator-overlay'
9
+ export const inject = ['agentPresets', 'skills', 'systemPrompt', 'loader']
10
+
11
+ const SKILL_NAME = 'tui-plugin-development'
12
+ const ROUTING_PROMPT = `# Dynamic TUI Plugin routing
13
+
14
+ Load cordis-plugin-development for every dynamic Plugin; it owns the common Plugin, Package, lifecycle, approval, Host, RPC, and repair model.
15
+
16
+ - When any requested behavior belongs to the TUI — terminal themes or backgrounds, native TUI slots, local TUI commands, native overlays, or current ACP Session options — also load tui-plugin-development. Its TUI Provider guidance overrides generic browser UI assumptions only for that half.
17
+ - Browser/Web-only work needs no TUI companion. A mixed Plugin uses the generic skill for common, Host, and Web behavior and the TUI skill for its terminal behavior. Do not translate a TUI request into Web Slots or add Host code unless the requested behavior owns Host data.`
18
+
19
+ const skillDocument = readFileSync(
20
+ new URL('../skills/tui-plugin-development/SKILL.md', import.meta.url),
21
+ 'utf8',
22
+ )
23
+ const skillFrontmatter = skillDocument.match(/^---\r?\n([\s\S]*?)\r?\n---\r?\n/)
24
+ if (!skillFrontmatter) {
25
+ throw new Error('tui-creator-overlay: TUI skill frontmatter is unavailable')
26
+ }
27
+ const skillDescription = skillFrontmatter[1]
28
+ .match(/^description:\s*(.+)$/m)?.[1]
29
+ ?.trim()
30
+ if (!skillDescription) {
31
+ throw new Error('tui-creator-overlay: TUI skill description is unavailable')
32
+ }
33
+ const skillContent = skillDocument.slice(skillFrontmatter[0].length)
34
+
35
+ /**
36
+ * @param {object} ctx
37
+ * @param {{ preset?: string }} [config]
38
+ */
39
+ export async function apply(ctx, config = {}) {
40
+ const preset = config.preset ?? 'cordis'
41
+ if (typeof preset !== 'string' || preset.length === 0) {
42
+ throw new Error('tui-creator-overlay: preset must be a non-empty string')
43
+ }
44
+ const key = await ctx.agentPresets.standingKeyFor(preset)
45
+ const scopeModule = await ctx.loader.import('@deepseek-ai/dsh-scope')
46
+ if (typeof scopeModule?.createScope !== 'function') {
47
+ throw new Error('tui-creator-overlay: profile dsh-scope module is unavailable')
48
+ }
49
+ const { createScope } = scopeModule
50
+ const overlay = createScope(ctx, key)
51
+ try {
52
+ const skills = overlay.ctx.get('skills')
53
+ if (skills === undefined || typeof skills.register !== 'function') {
54
+ throw new Error('tui-creator-overlay: skills service is unavailable in the preset scope')
55
+ }
56
+ skills.register({
57
+ name: SKILL_NAME,
58
+ description: skillDescription,
59
+ source: 'martty/creator-overlay',
60
+ content: skillContent,
61
+ invocation: { modelInvocable: true, userInvocable: true },
62
+ })
63
+ const systemPrompt = overlay.ctx.get('systemPrompt')
64
+ if (systemPrompt === undefined || typeof systemPrompt.section !== 'function') {
65
+ throw new Error('tui-creator-overlay: systemPrompt service is unavailable in the preset scope')
66
+ }
67
+ systemPrompt.section({
68
+ name: 'tool:tui-cordis-routing',
69
+ order: 116,
70
+ text: ROUTING_PROMPT,
71
+ })
72
+ ctx.effect(() => () => overlay.dispose(), `tui-creator-overlay(${JSON.stringify(preset)})`)
73
+ } catch (error) {
74
+ await overlay.dispose()
75
+ throw error
76
+ }
77
+ }
@@ -0,0 +1,79 @@
1
+ /**
2
+ * `--demo-skin` entry: mount gallery pack `ember` through `tuiTheme.register`,
3
+ * spawn the native TUI in `--demo` attach mode, and flush the Cordis TUI
4
+ * theme update with
5
+ * `activate: true`. Runnable as `node npm/lib/demo-skin.js`.
6
+ */
7
+
8
+ import { readFileSync } from 'node:fs'
9
+ import path from 'node:path'
10
+ import { fileURLToPath } from 'node:url'
11
+ import * as emberPlugin from './ember.js'
12
+ import { JsonRpcLineTransport } from './jsonrpc-line-transport.js'
13
+ import { nativeBinary, spawnPluginTui } from './spawn-tui.js'
14
+ import * as themePlugin from './tui-theme.js'
15
+
16
+ const __dirname = path.dirname(fileURLToPath(import.meta.url))
17
+ const { version } = JSON.parse(readFileSync(path.join(__dirname, '..', 'package.json'), 'utf8'))
18
+
19
+ function extraBinaryArgs(argv) {
20
+ const extra = ['--demo']
21
+ for (const arg of argv) {
22
+ if (arg === '--demo-skin' || arg === '--demo') continue
23
+ extra.push(arg)
24
+ }
25
+ return extra
26
+ }
27
+
28
+ /**
29
+ * Register ember, activate it, spawn the TUI, and serve the attach handshake.
30
+ * @param {string[]} [argv]
31
+ */
32
+ export async function runDemoSkin(argv = process.argv.slice(2)) {
33
+ const { Context } = await import('@deepseek-ai/cordis')
34
+ const ctx = new Context()
35
+ await ctx.plugin(themePlugin)
36
+ await ctx.plugin(emberPlugin)
37
+ const theme = ctx.tuiTheme
38
+ ctx.tuiTheme.activate('ember')
39
+
40
+ const bin = process.env.DSH_TUI_BIN || nativeBinary()
41
+ const connection = await spawnPluginTui(bin, extraBinaryArgs(argv))
42
+ const { child } = connection
43
+ const transport = new JsonRpcLineTransport(connection.input, connection.output)
44
+
45
+ function notify(method, params) {
46
+ transport.notify(method, params)
47
+ }
48
+ theme.bindNotify(notify)
49
+
50
+ transport.onRequest(async (method) => {
51
+ switch (method) {
52
+ case 'initialize':
53
+ return { serverInfo: { name: 'dsh-tui-demo-skin', version } }
54
+ case 'session/prompt':
55
+ return { messageId: 'demo-skin' }
56
+ case 'shutdown':
57
+ return {}
58
+ default:
59
+ throw new Error(`unknown method: ${method}`)
60
+ }
61
+ })
62
+
63
+ transport.start()
64
+ connection.resume?.()
65
+
66
+ child.on('exit', (code) => {
67
+ process.exit(code === null ? 0 : code)
68
+ })
69
+ child.on('error', (err) => {
70
+ console.error(`dsh-tui demo-skin failed: ${err.message}`)
71
+ process.exit(1)
72
+ })
73
+ }
74
+
75
+ const isMain = process.argv[1] !== undefined
76
+ && path.resolve(fileURLToPath(import.meta.url)) === path.resolve(process.argv[1])
77
+ if (isMain) {
78
+ await runDemoSkin(process.argv.slice(2))
79
+ }
package/lib/ember.js ADDED
@@ -0,0 +1,20 @@
1
+ /**
2
+ * Gallery palette pack `ember`. Registers complete dark/light token maps.
3
+ * Does not activate: `--demo-skin` calls `tuiTheme.activate('ember')`.
4
+ * `inject = ['tuiTheme']`: sibling profile row, not `ctx.plugin` inside the runner.
5
+ */
6
+
7
+ import { readFileSync } from 'node:fs'
8
+
9
+ const emberPalette = JSON.parse(
10
+ readFileSync(new URL('./palettes/ember.json', import.meta.url), 'utf8'),
11
+ )
12
+
13
+ export const name = 'tui-theme-ember'
14
+ export const inject = ['tuiTheme']
15
+
16
+ export function apply(ctx) {
17
+ ctx.effect(() => ctx.tuiTheme.register(emberPalette, { activate: false }))
18
+ }
19
+
20
+ export { emberPalette }