martty 0.2.16 → 0.2.17
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/README.md +14 -0
- package/cordis.patch.yml +3 -0
- package/lib/boot.js +110 -7
- package/lib/client-process.js +2 -1
- package/lib/client-run.js +10 -0
- package/lib/creator-overlay.js +166 -1
- package/lib/index.js +15 -2
- package/lib/inspect.js +40 -21
- package/lib/runner.js +5 -2
- package/lib/tui-client-plugin-registry.js +70 -0
- package/lib/tui-local-plugins.js +213 -0
- package/lib/tui-plugin-store.js +197 -0
- package/lib/tui-theme.js +41 -9
- package/package.json +4 -3
- package/skills/tui-plugin-development/SKILL.md +33 -13
- 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
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
/** Host-side directory of installed package entries for the separate TUI Client tree. */
|
|
2
|
+
|
|
3
|
+
import path from 'node:path'
|
|
4
|
+
import { pathToFileURL } from 'node:url'
|
|
5
|
+
import { Service } from '@deepseek-ai/cordis'
|
|
6
|
+
|
|
7
|
+
export const name = 'tui-client-plugin-registry'
|
|
8
|
+
export const inject = []
|
|
9
|
+
|
|
10
|
+
const ID = /^[a-z0-9][a-z0-9-]*$/
|
|
11
|
+
const KINDS = new Set(['theme', 'ui-preset'])
|
|
12
|
+
|
|
13
|
+
function normalizeEntry(value) {
|
|
14
|
+
if (typeof value !== 'string' || value.length === 0) {
|
|
15
|
+
throw new Error('tuiClientPlugins.register: entry must be an absolute path or file URL')
|
|
16
|
+
}
|
|
17
|
+
if (path.isAbsolute(value)) return pathToFileURL(value).href
|
|
18
|
+
let url
|
|
19
|
+
try {
|
|
20
|
+
url = new URL(value)
|
|
21
|
+
} catch {
|
|
22
|
+
throw new Error('tuiClientPlugins.register: entry must be an absolute path or file URL')
|
|
23
|
+
}
|
|
24
|
+
if (url.protocol !== 'file:') {
|
|
25
|
+
throw new Error('tuiClientPlugins.register: entry must use the file protocol')
|
|
26
|
+
}
|
|
27
|
+
return url.href
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
class TuiClientPluginsService extends Service {
|
|
31
|
+
constructor(ctx) {
|
|
32
|
+
super(ctx, 'tuiClientPlugins')
|
|
33
|
+
this.entries = new Map()
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
register(options) {
|
|
37
|
+
if (options === null || typeof options !== 'object' || Array.isArray(options)) {
|
|
38
|
+
throw new Error('tuiClientPlugins.register: options must be an object')
|
|
39
|
+
}
|
|
40
|
+
if (typeof options.id !== 'string' || !ID.test(options.id)) {
|
|
41
|
+
throw new Error('tuiClientPlugins.register: id must be a lowercase package identifier')
|
|
42
|
+
}
|
|
43
|
+
if (!KINDS.has(options.kind)) {
|
|
44
|
+
throw new Error('tuiClientPlugins.register: kind must be "theme" or "ui-preset"')
|
|
45
|
+
}
|
|
46
|
+
if (this.entries.has(options.id)) {
|
|
47
|
+
throw new Error(`tuiClientPlugins.register: id ${JSON.stringify(options.id)} is already registered`)
|
|
48
|
+
}
|
|
49
|
+
const entry = {
|
|
50
|
+
id: options.id,
|
|
51
|
+
kind: options.kind,
|
|
52
|
+
entry: normalizeEntry(options.entry),
|
|
53
|
+
}
|
|
54
|
+
this.entries.set(entry.id, entry)
|
|
55
|
+
let disposed = false
|
|
56
|
+
return () => {
|
|
57
|
+
if (disposed) return
|
|
58
|
+
disposed = true
|
|
59
|
+
if (this.entries.get(entry.id) === entry) this.entries.delete(entry.id)
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
list() {
|
|
64
|
+
return [...this.entries.values()].map((entry) => ({ ...entry }))
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
export function apply(ctx) {
|
|
69
|
+
return new TuiClientPluginsService(ctx)
|
|
70
|
+
}
|
|
@@ -0,0 +1,213 @@
|
|
|
1
|
+
/** Merge installed package entries and Creator-authored TUI Client plugins. */
|
|
2
|
+
|
|
3
|
+
import { applyClientHalf, applyClientPlugin, createClientTimer } from './client-run.js'
|
|
4
|
+
|
|
5
|
+
const LOCAL_PREFIX = 'tui-local:'
|
|
6
|
+
const PACKAGE_PREFIX = 'tui-package:'
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* @param {object} ctx
|
|
10
|
+
* @param {{
|
|
11
|
+
* store: object,
|
|
12
|
+
* packages?: Array<{ id: string, kind: 'theme' | 'ui-preset', entry: string }>,
|
|
13
|
+
* tuiTheme?: object,
|
|
14
|
+
* tuiPresets?: object,
|
|
15
|
+
* tuiSlots?: object,
|
|
16
|
+
* tuiCommands?: object,
|
|
17
|
+
* tuiOverlay?: object,
|
|
18
|
+
* acpSessionConfig?: object,
|
|
19
|
+
* acpSessionPlan?: object,
|
|
20
|
+
* acpSessionStats?: object,
|
|
21
|
+
* acpSessionStatus?: object,
|
|
22
|
+
* }} options
|
|
23
|
+
*/
|
|
24
|
+
export function installTuiLocalPlugins(ctx, options) {
|
|
25
|
+
const { store } = options
|
|
26
|
+
if (store === undefined || typeof store.list !== 'function' || typeof store.resolve !== 'function') {
|
|
27
|
+
throw new Error('tuiLocalPlugins: a durable store is required')
|
|
28
|
+
}
|
|
29
|
+
const records = new Map()
|
|
30
|
+
const failures = []
|
|
31
|
+
const timer = createClientTimer()
|
|
32
|
+
let discovered = false
|
|
33
|
+
let disposed = false
|
|
34
|
+
|
|
35
|
+
const env = (pluginId) => ({
|
|
36
|
+
pluginId,
|
|
37
|
+
tuiTheme: options.tuiTheme,
|
|
38
|
+
tuiPresets: options.tuiPresets,
|
|
39
|
+
tuiSlots: options.tuiSlots,
|
|
40
|
+
tuiCommands: options.tuiCommands,
|
|
41
|
+
tuiOverlay: options.tuiOverlay,
|
|
42
|
+
acpSessionConfig: options.acpSessionConfig,
|
|
43
|
+
acpSessionPlan: options.acpSessionPlan,
|
|
44
|
+
acpSessionStats: options.acpSessionStats,
|
|
45
|
+
acpSessionStatus: options.acpSessionStatus,
|
|
46
|
+
timer,
|
|
47
|
+
})
|
|
48
|
+
|
|
49
|
+
async function mount(record) {
|
|
50
|
+
let applied
|
|
51
|
+
if (record.entry !== undefined) {
|
|
52
|
+
record.module ??= await import(record.entry)
|
|
53
|
+
const plugin = typeof record.module.apply === 'function'
|
|
54
|
+
? record.module
|
|
55
|
+
: record.module.default
|
|
56
|
+
applied = await applyClientPlugin(plugin, env(record.pluginId))
|
|
57
|
+
} else {
|
|
58
|
+
applied = await applyClientHalf(record.artifact.code.client, env(record.pluginId))
|
|
59
|
+
}
|
|
60
|
+
if (applied.waitingFor.length > 0) {
|
|
61
|
+
applied.dispose()
|
|
62
|
+
throw new Error(`waiting for unavailable service(s): ${applied.waitingFor.join(', ')}`)
|
|
63
|
+
}
|
|
64
|
+
record.release = applied.dispose
|
|
65
|
+
record.loaded = true
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
async function recover(record) {
|
|
69
|
+
records.set(record.pluginId, record)
|
|
70
|
+
try {
|
|
71
|
+
await mount(record)
|
|
72
|
+
if (record.kind === 'theme') {
|
|
73
|
+
record.release?.()
|
|
74
|
+
record.release = undefined
|
|
75
|
+
record.loaded = false
|
|
76
|
+
}
|
|
77
|
+
return true
|
|
78
|
+
} catch (error) {
|
|
79
|
+
record.release?.()
|
|
80
|
+
records.delete(record.pluginId)
|
|
81
|
+
failures.push({
|
|
82
|
+
artifactId: record.artifactId,
|
|
83
|
+
message: error instanceof Error ? error.message : String(error),
|
|
84
|
+
})
|
|
85
|
+
return false
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
async function discover() {
|
|
90
|
+
if (disposed) throw new Error('tuiLocalPlugins: registry is disposed')
|
|
91
|
+
if (discovered) return
|
|
92
|
+
discovered = true
|
|
93
|
+
const installedIds = new Set()
|
|
94
|
+
for (const entry of options.packages ?? []) {
|
|
95
|
+
installedIds.add(entry.id)
|
|
96
|
+
await recover({
|
|
97
|
+
artifactId: entry.id,
|
|
98
|
+
pluginId: `${PACKAGE_PREFIX}${entry.id}`,
|
|
99
|
+
kind: entry.kind,
|
|
100
|
+
entry: entry.entry,
|
|
101
|
+
module: undefined,
|
|
102
|
+
loaded: false,
|
|
103
|
+
release: undefined,
|
|
104
|
+
})
|
|
105
|
+
}
|
|
106
|
+
for (const row of store.list()) {
|
|
107
|
+
if (installedIds.has(row.id)) {
|
|
108
|
+
failures.push({
|
|
109
|
+
artifactId: row.id,
|
|
110
|
+
message: 'Creator artifact is shadowed by an installed package with the same id',
|
|
111
|
+
})
|
|
112
|
+
continue
|
|
113
|
+
}
|
|
114
|
+
if (row.broken !== undefined) {
|
|
115
|
+
failures.push({ artifactId: row.id, message: row.broken })
|
|
116
|
+
continue
|
|
117
|
+
}
|
|
118
|
+
let artifact
|
|
119
|
+
try {
|
|
120
|
+
artifact = store.resolve(row.id)
|
|
121
|
+
} catch (error) {
|
|
122
|
+
failures.push({
|
|
123
|
+
artifactId: row.id,
|
|
124
|
+
message: error instanceof Error ? error.message : String(error),
|
|
125
|
+
})
|
|
126
|
+
continue
|
|
127
|
+
}
|
|
128
|
+
const pluginId = `${LOCAL_PREFIX}${artifact.id}`
|
|
129
|
+
const record = {
|
|
130
|
+
artifact,
|
|
131
|
+
artifactId: artifact.id,
|
|
132
|
+
pluginId,
|
|
133
|
+
kind: artifact.kind,
|
|
134
|
+
loaded: false,
|
|
135
|
+
release: undefined,
|
|
136
|
+
}
|
|
137
|
+
await recover(record)
|
|
138
|
+
}
|
|
139
|
+
const preferredTheme = options.tuiTheme?.preferred?.()
|
|
140
|
+
const preferredOwner = typeof preferredTheme === 'string'
|
|
141
|
+
? options.tuiTheme?.owner?.(preferredTheme)
|
|
142
|
+
: undefined
|
|
143
|
+
if (typeof preferredOwner === 'string' && records.has(preferredOwner)) {
|
|
144
|
+
try {
|
|
145
|
+
await start(preferredOwner)
|
|
146
|
+
options.tuiTheme.activate(preferredTheme)
|
|
147
|
+
} catch (error) {
|
|
148
|
+
failures.push({
|
|
149
|
+
artifactId: records.get(preferredOwner)?.artifactId ?? preferredOwner,
|
|
150
|
+
message: `failed to restore preferred theme: ${error instanceof Error ? error.message : String(error)}`,
|
|
151
|
+
})
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
async function start(pluginId) {
|
|
157
|
+
if (disposed) throw new Error('tuiLocalPlugins: registry is disposed')
|
|
158
|
+
const record = records.get(pluginId)
|
|
159
|
+
if (record === undefined) throw new Error(`tuiLocalPlugins: unknown Plugin ${JSON.stringify(pluginId)}`)
|
|
160
|
+
if (record.loaded) return
|
|
161
|
+
await mount(record)
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
async function stop(pluginId) {
|
|
165
|
+
const record = records.get(pluginId)
|
|
166
|
+
if (record === undefined || !record.loaded) return false
|
|
167
|
+
record.release?.()
|
|
168
|
+
record.release = undefined
|
|
169
|
+
record.loaded = false
|
|
170
|
+
return true
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
function has(pluginId) {
|
|
174
|
+
return records.has(pluginId)
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
function isLoaded(pluginId) {
|
|
178
|
+
return records.get(pluginId)?.loaded === true
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
function list() {
|
|
182
|
+
return [...records.values()].map((record) => ({
|
|
183
|
+
artifactId: record.artifactId,
|
|
184
|
+
pluginId: record.pluginId,
|
|
185
|
+
kind: record.kind,
|
|
186
|
+
loaded: record.loaded,
|
|
187
|
+
}))
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
function diagnostics() {
|
|
191
|
+
return failures.map((failure) => ({ ...failure }))
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
async function dispose() {
|
|
195
|
+
if (disposed) return
|
|
196
|
+
disposed = true
|
|
197
|
+
for (const record of [...records.values()].reverse()) {
|
|
198
|
+
record.release?.()
|
|
199
|
+
record.release = undefined
|
|
200
|
+
record.loaded = false
|
|
201
|
+
}
|
|
202
|
+
records.clear()
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
const service = { discover, start, stop, has, isLoaded, list, diagnostics, dispose }
|
|
206
|
+
if (typeof ctx.provide === 'function') ctx.provide('tuiLocalPlugins', service)
|
|
207
|
+
if (typeof ctx.effect === 'function') {
|
|
208
|
+
ctx.effect(() => () => service.dispose(), 'tui-local-plugins')
|
|
209
|
+
} else {
|
|
210
|
+
ctx.tuiLocalPlugins = service
|
|
211
|
+
}
|
|
212
|
+
return service
|
|
213
|
+
}
|
|
@@ -0,0 +1,197 @@
|
|
|
1
|
+
/** Durable user-authored TUI Client plugins. */
|
|
2
|
+
|
|
3
|
+
import {
|
|
4
|
+
cpSync,
|
|
5
|
+
existsSync,
|
|
6
|
+
mkdirSync,
|
|
7
|
+
readFileSync,
|
|
8
|
+
readdirSync,
|
|
9
|
+
renameSync,
|
|
10
|
+
rmSync,
|
|
11
|
+
writeFileSync,
|
|
12
|
+
} from 'node:fs'
|
|
13
|
+
import { homedir } from 'node:os'
|
|
14
|
+
import path from 'node:path'
|
|
15
|
+
|
|
16
|
+
const ARTIFACT_ID = /^[a-z0-9][a-z0-9-]*$/
|
|
17
|
+
const KINDS = new Set(['theme', 'ui-preset'])
|
|
18
|
+
const MANIFEST = 'plugin.json'
|
|
19
|
+
|
|
20
|
+
function artifactId(value) {
|
|
21
|
+
if (typeof value !== 'string' || !ARTIFACT_ID.test(value)) {
|
|
22
|
+
throw new Error('tuiPluginStore: id must be a lowercase artifact identifier')
|
|
23
|
+
}
|
|
24
|
+
return value
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function nonEmpty(value, field) {
|
|
28
|
+
if (typeof value !== 'string' || value.trim().length === 0) {
|
|
29
|
+
throw new Error(`tuiPluginStore: ${field} must be a non-empty string`)
|
|
30
|
+
}
|
|
31
|
+
return value
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function validateManifest(value, expectedId) {
|
|
35
|
+
if (value === null || typeof value !== 'object' || Array.isArray(value)) {
|
|
36
|
+
throw new Error('manifest must be an object')
|
|
37
|
+
}
|
|
38
|
+
if (value.schemaVersion !== 0) throw new Error('schemaVersion must be 0')
|
|
39
|
+
const id = artifactId(value.id)
|
|
40
|
+
if (id !== expectedId) throw new Error(`manifest id must match directory ${JSON.stringify(expectedId)}`)
|
|
41
|
+
if (!KINDS.has(value.kind)) throw new Error('kind must be "theme" or "ui-preset"')
|
|
42
|
+
const name = nonEmpty(value.name, 'name')
|
|
43
|
+
const purpose = nonEmpty(value.purpose, 'purpose')
|
|
44
|
+
if (value.source === null || typeof value.source !== 'object' || Array.isArray(value.source)) {
|
|
45
|
+
throw new Error('source must be an object')
|
|
46
|
+
}
|
|
47
|
+
const pluginId = nonEmpty(value.source.pluginId, 'source.pluginId')
|
|
48
|
+
const packageId = nonEmpty(value.source.packageId, 'source.packageId')
|
|
49
|
+
if (value.code === null || typeof value.code !== 'object' || Array.isArray(value.code)) {
|
|
50
|
+
throw new Error('code must be an object')
|
|
51
|
+
}
|
|
52
|
+
const client = nonEmpty(value.code.client, 'code.client')
|
|
53
|
+
return {
|
|
54
|
+
schemaVersion: 0,
|
|
55
|
+
id,
|
|
56
|
+
kind: value.kind,
|
|
57
|
+
name,
|
|
58
|
+
purpose,
|
|
59
|
+
source: { pluginId, packageId },
|
|
60
|
+
code: { client },
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function readManifest(file, id) {
|
|
65
|
+
let parsed
|
|
66
|
+
try {
|
|
67
|
+
parsed = JSON.parse(readFileSync(file, 'utf8'))
|
|
68
|
+
} catch (error) {
|
|
69
|
+
throw new Error(`invalid JSON: ${error instanceof Error ? error.message : String(error)}`)
|
|
70
|
+
}
|
|
71
|
+
return validateManifest(parsed, id)
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
export function tuiPluginRoot(env = process.env) {
|
|
75
|
+
return path.join(marttyHome(env), 'plugins')
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
export function marttyHome(env = process.env, userHome = homedir()) {
|
|
79
|
+
if (typeof env.MARTTY_HOME === 'string' && env.MARTTY_HOME.length > 0) {
|
|
80
|
+
return env.MARTTY_HOME
|
|
81
|
+
}
|
|
82
|
+
if (typeof env.DSH_HOME === 'string' && env.DSH_HOME.length > 0) {
|
|
83
|
+
return path.join(env.DSH_HOME, '.martty')
|
|
84
|
+
}
|
|
85
|
+
return path.join(userHome, '.martty')
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
export function legacyTuiPluginRoot(env = process.env, userHome = homedir()) {
|
|
89
|
+
const dshHome = typeof env.DSH_HOME === 'string' && env.DSH_HOME.length > 0
|
|
90
|
+
? env.DSH_HOME
|
|
91
|
+
: path.join(userHome, '.dsh')
|
|
92
|
+
return path.join(dshHome, '.tui-plugins')
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* @param {{ root?: string }} [options]
|
|
97
|
+
*/
|
|
98
|
+
export function createTuiPluginStore(options = {}) {
|
|
99
|
+
const root = options.root ?? tuiPluginRoot()
|
|
100
|
+
if (typeof root !== 'string' || root.length === 0 || !path.isAbsolute(root)) {
|
|
101
|
+
throw new Error('tuiPluginStore: root must be an absolute path')
|
|
102
|
+
}
|
|
103
|
+
const legacyRoot = options.legacyRoot
|
|
104
|
+
?? (options.root === undefined ? legacyTuiPluginRoot() : undefined)
|
|
105
|
+
if (legacyRoot !== undefined && legacyRoot !== root && existsSync(legacyRoot)) {
|
|
106
|
+
mkdirSync(root, { recursive: true })
|
|
107
|
+
for (const entry of readdirSync(legacyRoot, { withFileTypes: true })) {
|
|
108
|
+
if (!entry.isDirectory() || !ARTIFACT_ID.test(entry.name)) continue
|
|
109
|
+
const source = path.join(legacyRoot, entry.name)
|
|
110
|
+
const target = path.join(root, entry.name)
|
|
111
|
+
if (!existsSync(target)) cpSync(source, target, { recursive: true, errorOnExist: true })
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
const paths = (id) => {
|
|
116
|
+
const safe = artifactId(id)
|
|
117
|
+
const dir = path.join(root, safe)
|
|
118
|
+
return { dir, file: path.join(dir, MANIFEST) }
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
function list() {
|
|
122
|
+
if (!existsSync(root)) return []
|
|
123
|
+
return readdirSync(root, { withFileTypes: true })
|
|
124
|
+
.filter((entry) => entry.isDirectory() && ARTIFACT_ID.test(entry.name))
|
|
125
|
+
.sort((left, right) => left.name.localeCompare(right.name))
|
|
126
|
+
.map((entry) => {
|
|
127
|
+
const { file } = paths(entry.name)
|
|
128
|
+
try {
|
|
129
|
+
const artifact = readManifest(file, entry.name)
|
|
130
|
+
return {
|
|
131
|
+
id: artifact.id,
|
|
132
|
+
kind: artifact.kind,
|
|
133
|
+
name: artifact.name,
|
|
134
|
+
purpose: artifact.purpose,
|
|
135
|
+
path: file,
|
|
136
|
+
broken: undefined,
|
|
137
|
+
}
|
|
138
|
+
} catch (error) {
|
|
139
|
+
return {
|
|
140
|
+
id: entry.name,
|
|
141
|
+
kind: undefined,
|
|
142
|
+
name: entry.name,
|
|
143
|
+
purpose: '',
|
|
144
|
+
path: file,
|
|
145
|
+
broken: error instanceof Error ? error.message : String(error),
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
})
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
function resolve(id) {
|
|
152
|
+
const { file } = paths(id)
|
|
153
|
+
try {
|
|
154
|
+
return { ...readManifest(file, id), path: file }
|
|
155
|
+
} catch (error) {
|
|
156
|
+
throw new Error(
|
|
157
|
+
`tuiPluginStore: artifact ${JSON.stringify(id)} is broken: `
|
|
158
|
+
+ `${error instanceof Error ? error.message : String(error)}`,
|
|
159
|
+
)
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
function save(input, saveOptions = {}) {
|
|
164
|
+
const manifest = validateManifest({
|
|
165
|
+
schemaVersion: 0,
|
|
166
|
+
id: input?.id,
|
|
167
|
+
kind: input?.kind,
|
|
168
|
+
name: input?.name,
|
|
169
|
+
purpose: input?.purpose,
|
|
170
|
+
source: input?.source,
|
|
171
|
+
code: { client: input?.clientCode },
|
|
172
|
+
}, input?.id)
|
|
173
|
+
const { dir, file } = paths(manifest.id)
|
|
174
|
+
const replace = saveOptions.replace === true
|
|
175
|
+
if (existsSync(dir) && !replace) {
|
|
176
|
+
throw new Error(`tuiPluginStore: artifact ${JSON.stringify(manifest.id)} already exists`)
|
|
177
|
+
}
|
|
178
|
+
mkdirSync(dir, { recursive: true })
|
|
179
|
+
const temporary = path.join(dir, `.${MANIFEST}.${process.pid}.${Date.now()}.tmp`)
|
|
180
|
+
try {
|
|
181
|
+
writeFileSync(temporary, `${JSON.stringify(manifest, null, 2)}\n`, { flag: 'wx' })
|
|
182
|
+
renameSync(temporary, file)
|
|
183
|
+
} finally {
|
|
184
|
+
rmSync(temporary, { force: true })
|
|
185
|
+
}
|
|
186
|
+
return { id: manifest.id, path: file }
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
function remove(id) {
|
|
190
|
+
const { dir } = paths(id)
|
|
191
|
+
if (!existsSync(dir)) return false
|
|
192
|
+
rmSync(dir, { recursive: true, force: false })
|
|
193
|
+
return true
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
return { root, list, resolve, save, remove }
|
|
197
|
+
}
|
package/lib/tui-theme.js
CHANGED
|
@@ -7,8 +7,8 @@
|
|
|
7
7
|
* flushed from `bindNotify`.
|
|
8
8
|
*/
|
|
9
9
|
|
|
10
|
-
import { readFileSync } from 'node:fs'
|
|
11
|
-
import { isAbsolute } from 'node:path'
|
|
10
|
+
import { mkdirSync, readFileSync, writeFileSync } from 'node:fs'
|
|
11
|
+
import { dirname, isAbsolute } from 'node:path'
|
|
12
12
|
import { CORDIS_METHODS } from './cordis-protocol.js'
|
|
13
13
|
|
|
14
14
|
export const name = 'tui-theme'
|
|
@@ -77,6 +77,24 @@ const BACKGROUND_SOURCE_FIELDS = new Set(['kind', 'path', 'mediaType', 'base64']
|
|
|
77
77
|
const BACKGROUND_ANCHOR_FIELDS = new Set(['x', 'y'])
|
|
78
78
|
const PROTOCOL = 0
|
|
79
79
|
|
|
80
|
+
function readSettings(settingsPath) {
|
|
81
|
+
if (typeof settingsPath !== 'string' || settingsPath.length === 0) return {}
|
|
82
|
+
try {
|
|
83
|
+
const value = JSON.parse(readFileSync(settingsPath, 'utf8'))
|
|
84
|
+
return value !== null && typeof value === 'object' && !Array.isArray(value) ? value : {}
|
|
85
|
+
} catch {
|
|
86
|
+
return {}
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
function writePreferred(settingsPath, id) {
|
|
91
|
+
if (typeof settingsPath !== 'string' || settingsPath.length === 0) return
|
|
92
|
+
const settings = readSettings(settingsPath)
|
|
93
|
+
settings.theme = id
|
|
94
|
+
mkdirSync(dirname(settingsPath), { recursive: true })
|
|
95
|
+
writeFileSync(settingsPath, `${JSON.stringify(settings, null, 2)}\n`)
|
|
96
|
+
}
|
|
97
|
+
|
|
80
98
|
/**
|
|
81
99
|
* Validate a palette against tui-palette protocol 0 (closed token names).
|
|
82
100
|
* @param {unknown} palette
|
|
@@ -207,13 +225,18 @@ function validateTokenMap(map, which) {
|
|
|
207
225
|
* `register` grows the catalog (like adding an agent preset). `/theme` and
|
|
208
226
|
* `activate` switch which pack covers. Builtin `default` is never mutated.
|
|
209
227
|
* @param {object} ctx
|
|
210
|
-
* @param {{ notify?: (method: string, params: object) => void }} [options]
|
|
211
|
-
* @returns {{ register: Function, activate: Function, list: Function, active: Function, subscribe: Function, observeSelected: Function, exportInspectTokens: Function, bindNotify: Function, flush: Function }}
|
|
228
|
+
* @param {{ notify?: (method: string, params: object) => void, settingsPath?: string }} [options]
|
|
229
|
+
* @returns {{ register: Function, activate: Function, list: Function, active: Function, preferred: Function, subscribe: Function, observeSelected: Function, exportInspectTokens: Function, bindNotify: Function, flush: Function }}
|
|
212
230
|
*/
|
|
213
231
|
export function installTuiTheme(ctx, options = {}) {
|
|
214
232
|
const palettes = new Map()
|
|
215
233
|
const queue = []
|
|
216
234
|
const listeners = new Set()
|
|
235
|
+
const settingsPath = options.settingsPath
|
|
236
|
+
const savedTheme = readSettings(settingsPath).theme
|
|
237
|
+
let preferredId = typeof savedTheme === 'string' && savedTheme.length > 0
|
|
238
|
+
? savedTheme
|
|
239
|
+
: 'default'
|
|
217
240
|
let send = typeof options.notify === 'function' ? options.notify : undefined
|
|
218
241
|
let activeId = 'default'
|
|
219
242
|
let activationRevision = 0
|
|
@@ -271,6 +294,8 @@ export function installTuiTheme(ctx, options = {}) {
|
|
|
271
294
|
|
|
272
295
|
function activate(id) {
|
|
273
296
|
select(id, true)
|
|
297
|
+
preferredId = id
|
|
298
|
+
writePreferred(settingsPath, id)
|
|
274
299
|
}
|
|
275
300
|
|
|
276
301
|
function observeSelected(params) {
|
|
@@ -279,6 +304,8 @@ export function installTuiTheme(ctx, options = {}) {
|
|
|
279
304
|
}
|
|
280
305
|
if (params.protocol !== PROTOCOL) return false
|
|
281
306
|
select(params.id, false)
|
|
307
|
+
preferredId = params.id
|
|
308
|
+
writePreferred(settingsPath, params.id)
|
|
282
309
|
return true
|
|
283
310
|
}
|
|
284
311
|
|
|
@@ -319,7 +346,7 @@ export function installTuiTheme(ctx, options = {}) {
|
|
|
319
346
|
...(owner === undefined ? {} : { owner: { pluginId: owner } }),
|
|
320
347
|
})
|
|
321
348
|
if (cover) {
|
|
322
|
-
|
|
349
|
+
select(validated.id, true)
|
|
323
350
|
leaseRevision = activationRevision
|
|
324
351
|
}
|
|
325
352
|
return () => {
|
|
@@ -331,9 +358,9 @@ export function installTuiTheme(ctx, options = {}) {
|
|
|
331
358
|
const restoreId = previousId !== undefined && palettes.get(previousId)?.loaded === true
|
|
332
359
|
? previousId
|
|
333
360
|
: 'default'
|
|
334
|
-
|
|
361
|
+
select(restoreId, true)
|
|
335
362
|
} else if (wasActive) {
|
|
336
|
-
|
|
363
|
+
select('default', true)
|
|
337
364
|
}
|
|
338
365
|
if (owner === undefined) {
|
|
339
366
|
palettes.delete(validated.id)
|
|
@@ -403,6 +430,10 @@ export function installTuiTheme(ctx, options = {}) {
|
|
|
403
430
|
return activeId
|
|
404
431
|
}
|
|
405
432
|
|
|
433
|
+
function preferred() {
|
|
434
|
+
return preferredId
|
|
435
|
+
}
|
|
436
|
+
|
|
406
437
|
function owner(id) {
|
|
407
438
|
return palettes.get(id)?.owner
|
|
408
439
|
}
|
|
@@ -440,6 +471,7 @@ export function installTuiTheme(ctx, options = {}) {
|
|
|
440
471
|
activate,
|
|
441
472
|
list,
|
|
442
473
|
active,
|
|
474
|
+
preferred,
|
|
443
475
|
owner,
|
|
444
476
|
isLoaded,
|
|
445
477
|
subscribe,
|
|
@@ -458,6 +490,6 @@ export function installTuiTheme(ctx, options = {}) {
|
|
|
458
490
|
}
|
|
459
491
|
|
|
460
492
|
/** Cordis plugin entry: provide the client-tree theme registry. */
|
|
461
|
-
export function apply(ctx) {
|
|
462
|
-
installTuiTheme(ctx)
|
|
493
|
+
export function apply(ctx, options) {
|
|
494
|
+
installTuiTheme(ctx, options)
|
|
463
495
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "martty",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.17",
|
|
4
4
|
"description": "Terminal-native ACP client UI; Cordis client tree, any ACP agent",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"repository": {
|
|
@@ -15,7 +15,7 @@
|
|
|
15
15
|
"main": "lib/index.js",
|
|
16
16
|
"scripts": {
|
|
17
17
|
"pretest": "node --test ../scripts/workflow-release.test.mjs ../scripts/package-alias.test.mjs",
|
|
18
|
-
"test": "node --test ../scripts/package-native.test.mjs ../scripts/check-release-tag.test.mjs ../scripts/check-static-elf.test.mjs ../scripts/smoke-old-linux.test.mjs ../scripts/cargo-guard.test.mjs ../scripts/build-npm.test.mjs ../scripts/client-profile.test.mjs ../scripts/plugin-runner.test.mjs ../scripts/profile-link-resolution.test.mjs ../scripts/release.test.mjs ../scripts/jsonrpc-line-transport.test.mjs ../scripts/tui-theme.test.mjs ../scripts/tui-presets.test.mjs ../scripts/tui-slots.test.mjs ../scripts/tui-commands.test.mjs ../scripts/tui-overlay.test.mjs ../scripts/mux.test.mjs ../scripts/acp-client.test.mjs ../scripts/acp-client-events.test.mjs ../scripts/acp-session-config.test.mjs ../scripts/acp-session-plan.test.mjs ../scripts/acp-session-stats.test.mjs ../scripts/acp-session-status.test.mjs ../scripts/plan-view.test.mjs ../scripts/stats-view.test.mjs ../scripts/status-view.test.mjs ../scripts/deepseek-logo.test.mjs ../scripts/runner.test.mjs ../scripts/inspect.test.mjs ../scripts/creator-overlay.test.mjs ../scripts/real-agent-e2e.test.mjs"
|
|
18
|
+
"test": "node --test ../scripts/package-native.test.mjs ../scripts/check-release-tag.test.mjs ../scripts/check-static-elf.test.mjs ../scripts/smoke-old-linux.test.mjs ../scripts/cargo-guard.test.mjs ../scripts/build-npm.test.mjs ../scripts/client-profile.test.mjs ../scripts/plugin-runner.test.mjs ../scripts/profile-link-resolution.test.mjs ../scripts/release.test.mjs ../scripts/jsonrpc-line-transport.test.mjs ../scripts/tui-theme.test.mjs ../scripts/tui-presets.test.mjs ../scripts/tui-plugin-store.test.mjs ../scripts/tui-local-plugins.test.mjs ../scripts/tui-client-plugin-registry.test.mjs ../scripts/tui-slots.test.mjs ../scripts/tui-commands.test.mjs ../scripts/tui-overlay.test.mjs ../scripts/mux.test.mjs ../scripts/acp-client.test.mjs ../scripts/acp-client-events.test.mjs ../scripts/acp-session-config.test.mjs ../scripts/acp-session-plan.test.mjs ../scripts/acp-session-stats.test.mjs ../scripts/acp-session-status.test.mjs ../scripts/plan-view.test.mjs ../scripts/stats-view.test.mjs ../scripts/status-view.test.mjs ../scripts/deepseek-logo.test.mjs ../scripts/runner.test.mjs ../scripts/inspect.test.mjs ../scripts/creator-overlay.test.mjs ../scripts/real-agent-e2e.test.mjs"
|
|
19
19
|
},
|
|
20
20
|
"publishConfig": {
|
|
21
21
|
"access": "public",
|
|
@@ -43,6 +43,7 @@
|
|
|
43
43
|
"./profile-acp-client": "./lib/profile-acp-client.js",
|
|
44
44
|
"./acp-host": "./lib/acp-host.js",
|
|
45
45
|
"./creator-overlay": "./lib/creator-overlay.js",
|
|
46
|
+
"./client-plugin-registry": "./lib/tui-client-plugin-registry.js",
|
|
46
47
|
"./cordis-client-runner": "./lib/inspect.js",
|
|
47
48
|
"./runner": "./lib/runner.js",
|
|
48
49
|
"./cordis.patch.yml": "./cordis.patch.yml",
|
|
@@ -69,7 +70,7 @@
|
|
|
69
70
|
},
|
|
70
71
|
"dependencies": {
|
|
71
72
|
"@deepseek-ai/cordis": "^4.0.1",
|
|
72
|
-
"@openma/deepseek-harness-acp": "0.4.
|
|
73
|
+
"@openma/deepseek-harness-acp": "0.4.19"
|
|
73
74
|
},
|
|
74
75
|
"devDependencies": {
|
|
75
76
|
"@deepseek-ai/dsh": "0.1.0-rc.6"
|