martty 0.3.1 → 0.3.2-beta.1
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 +47 -12
- package/bin/martty.js +5 -0
- package/lib/acp-agent-pool.js +101 -4
- package/lib/acp-client.js +63 -4
- package/lib/acp-session-status.js +45 -3
- package/lib/agent.js +5 -10
- package/lib/creator-overlay.js +66 -9
- package/lib/harness-registry.js +2 -2
- package/lib/harnesses.js +11 -2
- package/lib/index.js +4 -1
- package/lib/install-migration.js +456 -0
- package/lib/queue-view.js +7 -1
- package/lib/status-view.js +15 -1
- package/lib/tui-plugin-store.js +34 -3
- package/lib/tui-queue.js +4 -0
- package/package.json +13 -12
- package/skills/tui-plugin-development/SKILL.md +1 -1
package/lib/index.js
CHANGED
|
@@ -72,8 +72,11 @@ export function restoreHostTty() {
|
|
|
72
72
|
}
|
|
73
73
|
}
|
|
74
74
|
if (process.stdout.isTTY) {
|
|
75
|
+
// Leave the alt screen with ?1047l, not ?1049l: ?1049l also restores the
|
|
76
|
+
// cursor saved at ?1049h, which a second emit on the main screen would
|
|
77
|
+
// move back over the `session <id>` line the painter just printed.
|
|
75
78
|
process.stdout.write(
|
|
76
|
-
'\x1b[?2004l\x1b[?1006l\x1b[?1003l\x1b[?1002l\x1b[?1000l\x1b[?25h\x1b[?
|
|
79
|
+
'\x1b[?2004l\x1b[?1006l\x1b[?1003l\x1b[?1002l\x1b[?1000l\x1b[?25h\x1b[?1047l',
|
|
77
80
|
)
|
|
78
81
|
}
|
|
79
82
|
}
|
|
@@ -0,0 +1,456 @@
|
|
|
1
|
+
// Migration aid for the package rename `martty` ->
|
|
2
|
+
// `martty`.
|
|
3
|
+
//
|
|
4
|
+
// npm refuses to link a global bin owned by another package, and it links
|
|
5
|
+
// bins before running any lifecycle script — so no script inside this
|
|
6
|
+
// package can stop an EEXIST on a name the legacy package may already own
|
|
7
|
+
// (`martty`, `dsh-tui`, `dsb`). The published `martty` package therefore
|
|
8
|
+
// declares only `martty-cli` — a name nothing else owns — and this
|
|
9
|
+
// postinstall step creates the user-facing `martty` command itself after
|
|
10
|
+
// retiring any legacy-owned links. It also strips `bin` from the legacy
|
|
11
|
+
// package's installed manifest, because `npm rm -g` deletes bin links by
|
|
12
|
+
// name without checking what they point at — left in place, removing the
|
|
13
|
+
// old package would take the fresh `martty` command down with it.
|
|
14
|
+
//
|
|
15
|
+
// `martty` is a small shim file, not a symlink: `npm rm -g martty` removes
|
|
16
|
+
// the package but not this command, and a shim can degrade gracefully —
|
|
17
|
+
// when its package is gone it prints a one-line hint and deletes itself,
|
|
18
|
+
// instead of leaving a dead symlink behind.
|
|
19
|
+
//
|
|
20
|
+
// npm hides postinstall output on success, so the binary re-checks its
|
|
21
|
+
// install root at launch: invoked as a real `martty`/`martty-cli` command
|
|
22
|
+
// it converges the prefix the same way the postinstall does (retire legacy
|
|
23
|
+
// links, recreate `martty`, sanitize) and warns while the legacy package is
|
|
24
|
+
// still present — which is also what recovers a `--ignore-scripts` install.
|
|
25
|
+
//
|
|
26
|
+
// The legacy name is intentionally assembled: `scripts/package-alias.mjs`
|
|
27
|
+
// rewrites the literal `martty` to `martty` in every
|
|
28
|
+
// shipped .js file, which would silently break detection inside the `martty`
|
|
29
|
+
// package itself.
|
|
30
|
+
|
|
31
|
+
import fs from 'node:fs'
|
|
32
|
+
import path from 'node:path'
|
|
33
|
+
import { fileURLToPath, pathToFileURL } from 'node:url'
|
|
34
|
+
|
|
35
|
+
export const LEGACY_PACKAGE = ['@openma', 'deepseek-harness-tui'].join('/')
|
|
36
|
+
const scopedParts = LEGACY_PACKAGE.split('/')
|
|
37
|
+
const LEGACY_PACKAGE_DIR = scopedParts[scopedParts.length - 1]
|
|
38
|
+
|
|
39
|
+
const COMMAND_NAME = 'martty'
|
|
40
|
+
const DECLARED_BIN_NAME = 'martty-cli'
|
|
41
|
+
const BIN_FILE = path.join('bin', 'martty.js')
|
|
42
|
+
|
|
43
|
+
// Bin names the legacy package may own globally: every published version
|
|
44
|
+
// ships `dsh-tui`, 0.2.14/0.2.19-era releases add `dsb`, and 0.2.14/0.2.20
|
|
45
|
+
// also claim `martty` itself. Links are only removed when they verifiably
|
|
46
|
+
// belong to the legacy package — an installed `martty` package keeps its
|
|
47
|
+
// own `martty` command and a third-party `dsh-tui` stays untouched.
|
|
48
|
+
const LEGACY_BIN_NAMES = [COMMAND_NAME, 'dsh-tui', 'dsb']
|
|
49
|
+
|
|
50
|
+
// Marks the `martty` shims this package writes so ownership checks can tell
|
|
51
|
+
// them from a foreign file that happens to mention a martty path.
|
|
52
|
+
const SHIM_MARKER = 'martty-command-shim'
|
|
53
|
+
const NOT_INSTALLED_LINE =
|
|
54
|
+
'martty: the martty package is not installed - reinstall with: npm i -g martty'
|
|
55
|
+
|
|
56
|
+
const stderrLine = (line) => process.stderr.write(`${line}\n`)
|
|
57
|
+
|
|
58
|
+
// Global installs land in <prefix>/lib/node_modules on POSIX and in
|
|
59
|
+
// <prefix>/node_modules on Windows; npm link uses the same layout.
|
|
60
|
+
export const legacyInstallDirs = (prefix) => [
|
|
61
|
+
path.join(prefix, 'lib', 'node_modules', ...scopedParts),
|
|
62
|
+
path.join(prefix, 'node_modules', ...scopedParts),
|
|
63
|
+
]
|
|
64
|
+
|
|
65
|
+
// POSIX global bins live in <prefix>/bin as symlinks; Windows creates a
|
|
66
|
+
// msys shim, a .cmd shim, and a .ps1 shim directly under the prefix.
|
|
67
|
+
export const binCandidates = (prefix, name) => [
|
|
68
|
+
path.join(prefix, 'bin', name),
|
|
69
|
+
path.join(prefix, name),
|
|
70
|
+
path.join(prefix, `${name}.cmd`),
|
|
71
|
+
path.join(prefix, `${name}.ps1`),
|
|
72
|
+
]
|
|
73
|
+
|
|
74
|
+
// A bin entry belongs to the legacy package when its symlink target (POSIX)
|
|
75
|
+
// or its shim contents (Windows) point into the legacy install directory.
|
|
76
|
+
// Anything else — a third-party `dsh-tui` package, a hand-written script —
|
|
77
|
+
// is left untouched.
|
|
78
|
+
export function isLegacyOwned(file) {
|
|
79
|
+
let stat
|
|
80
|
+
try {
|
|
81
|
+
stat = fs.lstatSync(file)
|
|
82
|
+
} catch {
|
|
83
|
+
return false
|
|
84
|
+
}
|
|
85
|
+
try {
|
|
86
|
+
if (stat.isSymbolicLink()) {
|
|
87
|
+
return fs.readlinkSync(file).includes(LEGACY_PACKAGE_DIR)
|
|
88
|
+
}
|
|
89
|
+
if (!stat.isFile()) return false
|
|
90
|
+
return fs.readFileSync(file, 'utf8').includes(LEGACY_PACKAGE_DIR)
|
|
91
|
+
} catch {
|
|
92
|
+
return false
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
// Classify the entry at `file` for the `martty` command slot:
|
|
97
|
+
// 'none' — nothing there
|
|
98
|
+
// 'ours' — resolves to (or points at) this package's bin file
|
|
99
|
+
// 'legacy' — belongs to the legacy package (isLegacyOwned)
|
|
100
|
+
// 'foreign' — something else; never overwritten
|
|
101
|
+
function commandOwner(file, binFile) {
|
|
102
|
+
let stat
|
|
103
|
+
try {
|
|
104
|
+
stat = fs.lstatSync(file)
|
|
105
|
+
} catch {
|
|
106
|
+
return 'none'
|
|
107
|
+
}
|
|
108
|
+
try {
|
|
109
|
+
if (stat.isSymbolicLink()) {
|
|
110
|
+
const target = fs.readlinkSync(file)
|
|
111
|
+
if (path.resolve(path.dirname(file), target) === binFile) return 'ours'
|
|
112
|
+
if (target.includes(LEGACY_PACKAGE_DIR)) return 'legacy'
|
|
113
|
+
// A live link whose target names a `martty/` directory points at some
|
|
114
|
+
// martty installation and is left alone; a dangling one is broken
|
|
115
|
+
// anyway and safe to replace. Anything else belongs to another tool.
|
|
116
|
+
if (/martty[/\\]/.test(target)) {
|
|
117
|
+
return fs.existsSync(path.resolve(path.dirname(file), target)) ? 'ours' : 'none'
|
|
118
|
+
}
|
|
119
|
+
return 'foreign'
|
|
120
|
+
}
|
|
121
|
+
if (!stat.isFile()) return 'foreign'
|
|
122
|
+
const content = fs.readFileSync(file, 'utf8')
|
|
123
|
+
if (content.includes(LEGACY_PACKAGE_DIR)) return 'legacy'
|
|
124
|
+
if (content.includes(SHIM_MARKER) || /node_modules[/\\]martty[/\\]/.test(content)) {
|
|
125
|
+
return 'ours'
|
|
126
|
+
}
|
|
127
|
+
return 'foreign'
|
|
128
|
+
} catch {
|
|
129
|
+
return 'foreign'
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
// The POSIX `martty` command is a shim file, not a symlink: `npm rm -g
|
|
134
|
+
// martty` removes the package but not this command, and a shim can notice
|
|
135
|
+
// its target is gone — it prints a one-line hint and deletes itself instead
|
|
136
|
+
// of leaving a dead symlink behind.
|
|
137
|
+
function posixShim(binFile) {
|
|
138
|
+
const quoted = `'${binFile.replaceAll("'", "'\\''")}'`
|
|
139
|
+
return `#!/bin/sh
|
|
140
|
+
# ${SHIM_MARKER} — written by the martty package; not managed by npm.
|
|
141
|
+
target=${quoted}
|
|
142
|
+
if [ -f "$target" ]; then
|
|
143
|
+
exec node "$target" "$@"
|
|
144
|
+
fi
|
|
145
|
+
echo "${NOT_INSTALLED_LINE}" >&2
|
|
146
|
+
rm -f "$0" 2>/dev/null
|
|
147
|
+
exit 127
|
|
148
|
+
`
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
// Windows receives the same shim triplet npm generates (msys shell wrapper,
|
|
152
|
+
// .cmd, .ps1), with the same degrade-and-remove behavior. `rel` is the bin
|
|
153
|
+
// file's path relative to the shim directory with forward slashes;
|
|
154
|
+
// `backRel` is the backslash variant for cmd.exe.
|
|
155
|
+
function windowsShims(dir, binFile) {
|
|
156
|
+
const rel = path.relative(dir, binFile).split(path.sep).join('/')
|
|
157
|
+
const backRel = rel.split('/').join('\\')
|
|
158
|
+
return {
|
|
159
|
+
[COMMAND_NAME]: `#!/bin/sh
|
|
160
|
+
# ${SHIM_MARKER} — written by the martty package; not managed by npm.
|
|
161
|
+
basedir=$(dirname "$(echo "$0" | sed -e 's,\\\\,/,g')")
|
|
162
|
+
|
|
163
|
+
case \`uname\` in
|
|
164
|
+
*CYGWIN*|*MINGW*|*MSYS*)
|
|
165
|
+
if command -v cygpath > /dev/null 2>&1; then
|
|
166
|
+
basedir=\`cygpath -w "$basedir"\`
|
|
167
|
+
fi
|
|
168
|
+
;;
|
|
169
|
+
esac
|
|
170
|
+
|
|
171
|
+
target="$basedir/${rel}"
|
|
172
|
+
if [ -f "$target" ]; then
|
|
173
|
+
exec node "$target" "$@"
|
|
174
|
+
fi
|
|
175
|
+
echo "${NOT_INSTALLED_LINE}" >&2
|
|
176
|
+
rm -f "$0" 2>/dev/null
|
|
177
|
+
exit 127
|
|
178
|
+
`,
|
|
179
|
+
[`${COMMAND_NAME}.cmd`]: `@echo off
|
|
180
|
+
rem ${SHIM_MARKER} - written by the martty package; not managed by npm.
|
|
181
|
+
set "target=%~dp0${backRel}"
|
|
182
|
+
if not exist "%target%" goto missing
|
|
183
|
+
node "%target%" %*
|
|
184
|
+
exit /b %ERRORLEVEL%
|
|
185
|
+
:missing
|
|
186
|
+
>&2 echo ${NOT_INSTALLED_LINE}
|
|
187
|
+
del "%~f0" 2>nul
|
|
188
|
+
exit /b 127
|
|
189
|
+
`,
|
|
190
|
+
[`${COMMAND_NAME}.ps1`]: `#!/usr/bin/env pwsh
|
|
191
|
+
# ${SHIM_MARKER} - written by the martty package; not managed by npm.
|
|
192
|
+
$target = Join-Path (Split-Path $MyInvocation.MyCommand.Definition -Parent) '${rel}'
|
|
193
|
+
if (Test-Path -LiteralPath $target) {
|
|
194
|
+
& node $target @args
|
|
195
|
+
exit $LASTEXITCODE
|
|
196
|
+
}
|
|
197
|
+
[Console]::Error.WriteLine('${NOT_INSTALLED_LINE}')
|
|
198
|
+
Remove-Item -LiteralPath $PSCommandPath -Force -ErrorAction SilentlyContinue
|
|
199
|
+
exit 127
|
|
200
|
+
`,
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
// Create the `martty` command next to wherever `martty-cli` was invoked
|
|
205
|
+
// from (`argv[1]`) or inside `binDir` — which keeps it correct for global
|
|
206
|
+
// prefixes, `npm link`, and a project's node_modules/.bin alike. Existing
|
|
207
|
+
// entries are only replaced when they are ours or verifiably legacy-owned;
|
|
208
|
+
// a foreign `martty` is left untouched with a note about `martty-cli`.
|
|
209
|
+
export function ensureMarttyLink({ argv1, binDir, packageDir, platform = process.platform, write = stderrLine } = {}) {
|
|
210
|
+
const selfPackageDir = packageDir ?? path.dirname(path.dirname(fileURLToPath(import.meta.url)))
|
|
211
|
+
const binFile = path.join(selfPackageDir, BIN_FILE)
|
|
212
|
+
let dir = binDir ?? null
|
|
213
|
+
if (!dir) {
|
|
214
|
+
// Only an invocation under the declared `martty-cli` name earns a
|
|
215
|
+
// `martty` sibling — a plain `node bin/martty.js` must not scribble a
|
|
216
|
+
// link next to whatever script the caller pointed node at.
|
|
217
|
+
const invokedAs = argv1 ? path.basename(argv1).replace(/\.(cmd|ps1)$/i, '') : null
|
|
218
|
+
if (invokedAs !== DECLARED_BIN_NAME) return false
|
|
219
|
+
dir = path.dirname(argv1)
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
const target = path.join(dir, COMMAND_NAME)
|
|
223
|
+
const owner = commandOwner(target, binFile)
|
|
224
|
+
if (owner === 'ours') return true
|
|
225
|
+
if (owner === 'foreign') {
|
|
226
|
+
write(`martty: \`martty\` in ${dir} belongs to another tool — leaving it untouched.`)
|
|
227
|
+
write('martty: this package\'s command is `martty-cli`.')
|
|
228
|
+
return false
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
try {
|
|
232
|
+
fs.mkdirSync(dir, { recursive: true })
|
|
233
|
+
if (platform === 'win32') {
|
|
234
|
+
for (const [name, content] of Object.entries(windowsShims(dir, binFile))) {
|
|
235
|
+
fs.writeFileSync(path.join(dir, name), content)
|
|
236
|
+
}
|
|
237
|
+
} else {
|
|
238
|
+
fs.rmSync(target, { force: true })
|
|
239
|
+
fs.writeFileSync(target, posixShim(binFile))
|
|
240
|
+
fs.chmodSync(target, 0o755)
|
|
241
|
+
}
|
|
242
|
+
} catch {
|
|
243
|
+
write(`martty: could not create the \`martty\` command in ${dir} — run \`martty-cli\` instead.`)
|
|
244
|
+
return false
|
|
245
|
+
}
|
|
246
|
+
return true
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
function legacyNotice(write) {
|
|
250
|
+
write(`${LEGACY_PACKAGE} is the previous name of this package — it now ships as \`martty\`.`)
|
|
251
|
+
write(`To migrate: npm rm -g ${LEGACY_PACKAGE} && npm install --global martty`)
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
function marttyNotice(write, hint) {
|
|
255
|
+
write(`martty: ${LEGACY_PACKAGE} is still installed. Remove it to finish the rename:`)
|
|
256
|
+
write(`martty: ${hint}`)
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
// Remove the bin entries the legacy package owns in `prefix` (POSIX links
|
|
260
|
+
// and Windows shims alike). Foreign entries never qualify; undeletable
|
|
261
|
+
// ones are skipped — they must never fail an install or a launch.
|
|
262
|
+
function retireLegacyBins(prefix, write) {
|
|
263
|
+
const removed = new Set()
|
|
264
|
+
for (const name of LEGACY_BIN_NAMES) {
|
|
265
|
+
for (const file of binCandidates(prefix, name)) {
|
|
266
|
+
if (!isLegacyOwned(file)) continue
|
|
267
|
+
try {
|
|
268
|
+
fs.rmSync(file, { force: true })
|
|
269
|
+
removed.add(name)
|
|
270
|
+
} catch {
|
|
271
|
+
// An undeletable link must not fail the install; the nudge below
|
|
272
|
+
// still names the blocking package.
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
if (removed.size > 0) {
|
|
277
|
+
const list = [...removed].map((name) => `\`${name}\``).join(', ')
|
|
278
|
+
write(`martty: removed the stale ${list} command${removed.size > 1 ? 's' : ''} left by ${LEGACY_PACKAGE} — the TUI now runs as \`martty\`.`)
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
// postinstall entry point (`npm_package_name`/`npm_config_*` env): retire
|
|
283
|
+
// the legacy package's bin links, create the `martty` command, and nudge
|
|
284
|
+
// the user to remove the old package. Never fails the install.
|
|
285
|
+
export function migrate(env, write = stderrLine, platform = process.platform) {
|
|
286
|
+
if (env.npm_config_global !== 'true') return
|
|
287
|
+
const prefix = env.npm_config_prefix
|
|
288
|
+
if (!prefix) return
|
|
289
|
+
const packageName = env.npm_package_name ?? ''
|
|
290
|
+
|
|
291
|
+
if (packageName === LEGACY_PACKAGE) {
|
|
292
|
+
legacyNotice(write)
|
|
293
|
+
return
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
const legacyInstalled = legacyInstallDirs(prefix).some((dir) => fs.existsSync(dir))
|
|
297
|
+
|
|
298
|
+
retireLegacyBins(prefix, write)
|
|
299
|
+
|
|
300
|
+
const packageDir = path.dirname(path.dirname(fileURLToPath(import.meta.url)))
|
|
301
|
+
const binDir = platform === 'win32' ? prefix : path.join(prefix, 'bin')
|
|
302
|
+
ensureMarttyLink({ binDir, packageDir, platform, write })
|
|
303
|
+
|
|
304
|
+
if (legacyInstalled) {
|
|
305
|
+
sanitizeLegacyInstall(prefix, write)
|
|
306
|
+
marttyNotice(write, `npm rm -g ${LEGACY_PACKAGE}`)
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
// Neutralize the legacy package's `bin` declarations so a later
|
|
311
|
+
// `npm rm -g martty` — which deletes bin links by
|
|
312
|
+
// name without checking what they point at — cannot take the new `martty`
|
|
313
|
+
// command down with it. A real install gets the `bin` field stripped from
|
|
314
|
+
// its installed manifest; an `npm link`ed one is a symlink into the user's
|
|
315
|
+
// checkout — editing that file would dirty their worktree, so the symlink
|
|
316
|
+
// is replaced with a stub package (name + version only) which `npm rm -g`
|
|
317
|
+
// still removes cleanly. Never fails the install.
|
|
318
|
+
export function sanitizeLegacyInstall(prefix, write = stderrLine) {
|
|
319
|
+
for (const legacyDir of legacyInstallDirs(prefix)) {
|
|
320
|
+
let stat
|
|
321
|
+
try {
|
|
322
|
+
stat = fs.lstatSync(legacyDir)
|
|
323
|
+
} catch {
|
|
324
|
+
continue
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
if (stat.isSymbolicLink()) {
|
|
328
|
+
let version
|
|
329
|
+
try {
|
|
330
|
+
const manifest = JSON.parse(fs.readFileSync(path.join(legacyDir, 'package.json'), 'utf8'))
|
|
331
|
+
if (manifest && typeof manifest.version === 'string') version = manifest.version
|
|
332
|
+
} catch {
|
|
333
|
+
// Unreadable linked manifest — the stub below omits the version.
|
|
334
|
+
}
|
|
335
|
+
try {
|
|
336
|
+
fs.rmSync(legacyDir, { force: true })
|
|
337
|
+
fs.mkdirSync(legacyDir, { recursive: true })
|
|
338
|
+
const stub = { name: LEGACY_PACKAGE, ...(version === undefined ? {} : { version }) }
|
|
339
|
+
fs.writeFileSync(path.join(legacyDir, 'package.json'), `${JSON.stringify(stub, null, 2)}\n`)
|
|
340
|
+
} catch {
|
|
341
|
+
write(`martty: could not detach the linked ${LEGACY_PACKAGE} — remove it with \`npm rm -g ${LEGACY_PACKAGE}\`.`)
|
|
342
|
+
}
|
|
343
|
+
continue
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
if (!stat.isDirectory()) continue
|
|
347
|
+
const manifestPath = path.join(legacyDir, 'package.json')
|
|
348
|
+
let manifest
|
|
349
|
+
try {
|
|
350
|
+
manifest = JSON.parse(fs.readFileSync(manifestPath, 'utf8'))
|
|
351
|
+
} catch {
|
|
352
|
+
// A missing or unreadable manifest has no readable bin table for a
|
|
353
|
+
// later `npm rm -g` to act on either — nothing to strip.
|
|
354
|
+
continue
|
|
355
|
+
}
|
|
356
|
+
try {
|
|
357
|
+
if (manifest && typeof manifest === 'object' && 'bin' in manifest) {
|
|
358
|
+
delete manifest.bin
|
|
359
|
+
fs.writeFileSync(manifestPath, `${JSON.stringify(manifest, null, 2)}\n`)
|
|
360
|
+
}
|
|
361
|
+
} catch {
|
|
362
|
+
write(`martty: could not edit the installed ${LEGACY_PACKAGE} manifest — remove it with \`npm rm -g ${LEGACY_PACKAGE}\`.`)
|
|
363
|
+
}
|
|
364
|
+
}
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
// Launch-time entry point: npm hides postinstall output unless it fails, so
|
|
368
|
+
// the binary re-checks its install root on every run. Invoked through a
|
|
369
|
+
// real command name (`martty`, `martty-cli`, `npx`) it converges the global
|
|
370
|
+
// prefix the same way the postinstall does — retire the legacy links,
|
|
371
|
+
// (re)create `martty`, sanitize the legacy install — which is also what
|
|
372
|
+
// recovers an install whose scripts were skipped. A bare `node
|
|
373
|
+
// bin/martty.js` or a dsh profile layout only gets the warning, never the
|
|
374
|
+
// filesystem changes.
|
|
375
|
+
export function warnAboutLegacyPackage({ argv1, libDir, write = stderrLine, platform = process.platform } = {}) {
|
|
376
|
+
const selfLibDir = libDir ?? path.dirname(fileURLToPath(import.meta.url))
|
|
377
|
+
const packageDir = path.dirname(selfLibDir)
|
|
378
|
+
if (path.basename(packageDir) === LEGACY_PACKAGE_DIR) {
|
|
379
|
+
legacyNotice(write)
|
|
380
|
+
return
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
try {
|
|
384
|
+
ensureMarttyLink({ argv1, packageDir, platform, write })
|
|
385
|
+
} catch {
|
|
386
|
+
// A read-only prefix or a missing bin dir must not break the launch.
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
// Candidate prefixes to converge: the global root this package sits in
|
|
390
|
+
// (lib/node_modules layout), plus the root behind the bin that launched
|
|
391
|
+
// us (covers `npm link` where the package resolves to a checkout).
|
|
392
|
+
const prefixes = []
|
|
393
|
+
const nodeModules = path.dirname(packageDir)
|
|
394
|
+
if (path.basename(nodeModules) === 'node_modules') {
|
|
395
|
+
const layoutDir = path.dirname(nodeModules)
|
|
396
|
+
prefixes.push(path.basename(layoutDir) === 'lib' ? path.dirname(layoutDir) : layoutDir)
|
|
397
|
+
}
|
|
398
|
+
if (argv1) {
|
|
399
|
+
const invokedAs = path.basename(argv1).replace(/\.(cmd|ps1)$/i, '')
|
|
400
|
+
const binDir = path.dirname(argv1)
|
|
401
|
+
if (
|
|
402
|
+
(invokedAs === COMMAND_NAME || invokedAs === DECLARED_BIN_NAME)
|
|
403
|
+
&& (path.basename(binDir) === 'bin' || platform === 'win32')
|
|
404
|
+
) {
|
|
405
|
+
prefixes.push(platform === 'win32' && path.basename(binDir) !== 'bin' ? binDir : path.dirname(binDir))
|
|
406
|
+
}
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
let hint = null
|
|
410
|
+
for (const prefix of new Set(prefixes)) {
|
|
411
|
+
if (!legacyInstallDirs(prefix).some((dir) => fs.existsSync(dir))) continue
|
|
412
|
+
const segments = prefix.split(path.sep)
|
|
413
|
+
const profileAt = segments.lastIndexOf('profiles')
|
|
414
|
+
if (profileAt >= 0) {
|
|
415
|
+
// A dsh profile: warn with the plugin-removal hint, touch nothing —
|
|
416
|
+
// profile layouts are managed by dsh, not npm.
|
|
417
|
+
hint ??= `dsh plugin --profile ${segments[profileAt + 1]} remove ${LEGACY_PACKAGE}`
|
|
418
|
+
continue
|
|
419
|
+
}
|
|
420
|
+
hint ??= `npm rm -g ${LEGACY_PACKAGE}`
|
|
421
|
+
try {
|
|
422
|
+
retireLegacyBins(prefix, write)
|
|
423
|
+
const binDir = platform === 'win32' ? prefix : path.join(prefix, 'bin')
|
|
424
|
+
ensureMarttyLink({ binDir, packageDir, platform, write })
|
|
425
|
+
sanitizeLegacyInstall(prefix, write)
|
|
426
|
+
} catch {
|
|
427
|
+
// Best effort: nothing here may break the launch.
|
|
428
|
+
}
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
if (hint) marttyNotice(write, hint)
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
// Deliver the launch notices collected by warnAboutLegacyPackage. On an
|
|
435
|
+
// interactive POSIX terminal the TUI repaints the whole screen for the
|
|
436
|
+
// session — printing now would bury the notice behind the first frame — so
|
|
437
|
+
// it is deferred to process exit, where it lands right above the next
|
|
438
|
+
// prompt. Piped stderr and Windows consoles get it immediately: an
|
|
439
|
+
// exit-handler write only flushes synchronously to a POSIX TTY or a file.
|
|
440
|
+
export function reportLaunchNotices(
|
|
441
|
+
lines,
|
|
442
|
+
{ tty = process.stderr.isTTY, platform = process.platform, write = stderrLine } = {},
|
|
443
|
+
) {
|
|
444
|
+
if (lines.length === 0) return
|
|
445
|
+
if (tty && platform !== 'win32') {
|
|
446
|
+
process.once('exit', () => {
|
|
447
|
+
for (const line of lines) write(line)
|
|
448
|
+
})
|
|
449
|
+
return
|
|
450
|
+
}
|
|
451
|
+
for (const line of lines) write(line)
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
if (process.argv[1] && import.meta.url === pathToFileURL(path.resolve(process.argv[1])).href) {
|
|
455
|
+
migrate(process.env)
|
|
456
|
+
}
|
package/lib/queue-view.js
CHANGED
|
@@ -29,11 +29,17 @@ function dockNodes(queue) {
|
|
|
29
29
|
if (!Array.isArray(queue?.items) || queue.items.length === 0) return []
|
|
30
30
|
const expanded = queue.selectedId !== null || queue.editingId !== null
|
|
31
31
|
const count = Number.isInteger(queue.count) ? queue.count : queue.items.length
|
|
32
|
+
// A finished-but-failed attach never binds: the head cannot be sent, so
|
|
33
|
+
// the collapsed title names the failure instead of promising it.
|
|
32
34
|
const title = expanded
|
|
33
35
|
? queue.editingId !== null
|
|
34
36
|
? `Queue · ${count} · enter save · ctrl+d delete · esc cancel`
|
|
35
37
|
: `Queue · ${count} · ↑/↓ choose · enter edit · esc close`
|
|
36
|
-
:
|
|
38
|
+
: queue.attachFailed === true
|
|
39
|
+
? `Queue · ${count} · attach failed · prompt stays queued`
|
|
40
|
+
: queue.authPending === true
|
|
41
|
+
? `Queue · ${count} · sign-in needed · prompt stays queued`
|
|
42
|
+
: `Queue · ${count} · enter send first · ⌥↑ edit`
|
|
37
43
|
const nodes = [{
|
|
38
44
|
id: 'summary',
|
|
39
45
|
kind: 'generic',
|
package/lib/status-view.js
CHANGED
|
@@ -41,13 +41,27 @@ function statusMarkdown(status, stats) {
|
|
|
41
41
|
const lines = []
|
|
42
42
|
lines.push(`- state · ${status.state ?? 'idle'}`)
|
|
43
43
|
lines.push(`- acp · ${status.connection ?? 'not attached'}`)
|
|
44
|
-
if (status.error)
|
|
44
|
+
if (status.error) {
|
|
45
|
+
// A failure carries its diagnostics tail as extra lines (e.g.
|
|
46
|
+
// "Agent stderr: …"); keep the bullet on the headline and give the rest
|
|
47
|
+
// a fenced block so long refusal text stays readable verbatim.
|
|
48
|
+
const [headline, ...rest] = status.error.split('\n')
|
|
49
|
+
lines.push(`- connection error · ${headline}`)
|
|
50
|
+
if (rest.length > 0) {
|
|
51
|
+
lines.push('', '```text', rest.join('\n'), '```')
|
|
52
|
+
}
|
|
53
|
+
}
|
|
45
54
|
if (status.auth?.status !== undefined) {
|
|
46
55
|
lines.push(`- auth · ${status.auth.status}${status.auth.method ? ` · ${status.auth.method}` : ''}`)
|
|
47
56
|
if (status.auth.message) lines.push(`- auth error · ${status.auth.message}`)
|
|
48
57
|
}
|
|
49
58
|
lines.push(`- session · ${status.session?.bound ? status.session.sessionId : 'unbound'}`)
|
|
50
59
|
if (status.server !== undefined) lines.push(`- server · ${status.server}`)
|
|
60
|
+
if (status.dshRuntime !== undefined) {
|
|
61
|
+
const runtime = [status.dshRuntime.version, status.dshRuntime.source && `via ${status.dshRuntime.source}`]
|
|
62
|
+
.filter(Boolean).join(' ')
|
|
63
|
+
lines.push(`- dsh · ${runtime}`)
|
|
64
|
+
}
|
|
51
65
|
if (status.model !== undefined) lines.push(`- model · ${status.model}`)
|
|
52
66
|
if (status.agent !== undefined) lines.push(`- agent · ${status.agent}`)
|
|
53
67
|
if (status.permission !== undefined) lines.push(`- permission · ${status.permission}`)
|
package/lib/tui-plugin-store.js
CHANGED
|
@@ -76,14 +76,45 @@ export function tuiPluginRoot(env = process.env) {
|
|
|
76
76
|
return path.join(marttyHome(env), 'plugins')
|
|
77
77
|
}
|
|
78
78
|
|
|
79
|
+
/**
|
|
80
|
+
* Martty's configuration home, shared with Backchat: `~/.oma`.
|
|
81
|
+
* Resolution order: explicit `MARTTY_HOME`/`OMA_HOME`, then `$DSH_HOME/.oma`,
|
|
82
|
+
* then `~/.oma`. When the new home is missing but a legacy `.martty`
|
|
83
|
+
* directory sits next to it, its contents are migrated once (config, cache,
|
|
84
|
+
* sessions, managed binaries) so existing installs keep their state.
|
|
85
|
+
*/
|
|
79
86
|
export function marttyHome(env = process.env, userHome = homedir()) {
|
|
80
87
|
if (typeof env.MARTTY_HOME === 'string' && env.MARTTY_HOME.length > 0) {
|
|
81
88
|
return env.MARTTY_HOME
|
|
82
89
|
}
|
|
83
|
-
if (typeof env.
|
|
84
|
-
return
|
|
90
|
+
if (typeof env.OMA_HOME === 'string' && env.OMA_HOME.length > 0) {
|
|
91
|
+
return env.OMA_HOME
|
|
85
92
|
}
|
|
86
|
-
|
|
93
|
+
const base = typeof env.DSH_HOME === 'string' && env.DSH_HOME.length > 0
|
|
94
|
+
? env.DSH_HOME
|
|
95
|
+
: userHome
|
|
96
|
+
return migrateMarttyHome(base)
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
function migrateMarttyHome(base) {
|
|
100
|
+
const home = path.join(base, '.oma')
|
|
101
|
+
const legacy = path.join(base, '.martty')
|
|
102
|
+
if (existsSync(home) || !existsSync(legacy)) return home
|
|
103
|
+
// Stage the copy under a temporary sibling so a crash cannot leave a
|
|
104
|
+
// half-migrated `.oma` behind; the legacy directory is left untouched.
|
|
105
|
+
const staged = `${home}.migrating-${process.pid}`
|
|
106
|
+
try {
|
|
107
|
+
rmSync(staged, { recursive: true, force: true })
|
|
108
|
+
cpSync(legacy, staged, { recursive: true, errorOnExist: true })
|
|
109
|
+
renameSync(staged, home)
|
|
110
|
+
process.stderr.write(`martty: migrated ${legacy} to ${home} (shared with Backchat)\n`)
|
|
111
|
+
} catch {
|
|
112
|
+
rmSync(staged, { recursive: true, force: true })
|
|
113
|
+
// Migration is best effort: an unmigrated legacy directory keeps working
|
|
114
|
+
// under its old name rather than blocking startup.
|
|
115
|
+
return legacy
|
|
116
|
+
}
|
|
117
|
+
return home
|
|
87
118
|
}
|
|
88
119
|
|
|
89
120
|
export function legacyTuiPluginRoot(env = process.env, userHome = homedir()) {
|
package/lib/tui-queue.js
CHANGED
|
@@ -22,6 +22,8 @@ const EMPTY = Object.freeze({
|
|
|
22
22
|
selectedId: null,
|
|
23
23
|
editingId: null,
|
|
24
24
|
deleteConfirm: false,
|
|
25
|
+
attachFailed: false,
|
|
26
|
+
authPending: false,
|
|
25
27
|
})
|
|
26
28
|
|
|
27
29
|
export function installTuiQueue(ctx) {
|
|
@@ -62,6 +64,8 @@ export function installTuiQueue(ctx) {
|
|
|
62
64
|
selectedId: idOrNull(snapshot.selectedId),
|
|
63
65
|
editingId: idOrNull(snapshot.editingId),
|
|
64
66
|
deleteConfirm: snapshot.deleteConfirm === true,
|
|
67
|
+
attachFailed: snapshot.attachFailed === true,
|
|
68
|
+
authPending: snapshot.authPending === true,
|
|
65
69
|
}
|
|
66
70
|
const next = current()
|
|
67
71
|
for (const listener of [...listeners]) listener(next)
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "martty",
|
|
3
|
-
"version": "0.3.1",
|
|
3
|
+
"version": "0.3.2-beta.1",
|
|
4
4
|
"description": "Terminal-native ACP client UI; Cordis client tree, any ACP agent",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"repository": {
|
|
@@ -14,9 +14,10 @@
|
|
|
14
14
|
"type": "module",
|
|
15
15
|
"main": "lib/index.js",
|
|
16
16
|
"scripts": {
|
|
17
|
+
"postinstall": "node lib/install-migration.js",
|
|
17
18
|
"test:harness-ui": "node --test ../scripts/harness-management-tui.test.mjs ../scripts/harness-session-tui.test.mjs",
|
|
18
19
|
"pretest": "node --test ../scripts/workflow-release.test.mjs ../scripts/package-alias.test.mjs ../scripts/harness-removal.test.mjs",
|
|
19
|
-
"test": "node --test ../scripts/download.test.mjs ../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/packaged-acp-permission.test.mjs ../scripts/profile-smoke-tui.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/tui-agents.test.mjs ../scripts/tui-queue.test.mjs ../scripts/mux.test.mjs ../scripts/acp-client.test.mjs ../scripts/acp-agent-pool.test.mjs ../scripts/harnesses.test.mjs ../scripts/harness-discovery.test.mjs ../scripts/harness-discovery-scenario.test.mjs ../scripts/harness-view.test.mjs ../scripts/harness-onboarding.test.mjs ../scripts/harness-registry.test.mjs ../scripts/harness-badge.test.mjs ../scripts/harness-package.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/agents-view.test.mjs ../scripts/plan-view.test.mjs ../scripts/queue-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",
|
|
20
|
+
"test": "node --test ../scripts/download.test.mjs ../scripts/package-native.test.mjs ../scripts/check-release-tag.test.mjs ../scripts/release-check.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/packaged-acp-permission.test.mjs ../scripts/profile-smoke-tui.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/tui-agents.test.mjs ../scripts/tui-queue.test.mjs ../scripts/mux.test.mjs ../scripts/acp-client.test.mjs ../scripts/acp-agent-pool.test.mjs ../scripts/harnesses.test.mjs ../scripts/harness-discovery.test.mjs ../scripts/harness-discovery-scenario.test.mjs ../scripts/harness-view.test.mjs ../scripts/harness-onboarding.test.mjs ../scripts/harness-registry.test.mjs ../scripts/harness-badge.test.mjs ../scripts/harness-package.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/agents-view.test.mjs ../scripts/plan-view.test.mjs ../scripts/queue-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/install-migration.test.mjs ../scripts/global-install-collision.test.mjs ../scripts/real-agent-e2e.test.mjs",
|
|
20
21
|
"test:profile-install-matrix": "node --test ../scripts/profile-install-matrix.test.mjs"
|
|
21
22
|
},
|
|
22
23
|
"publishConfig": {
|
|
@@ -56,8 +57,7 @@
|
|
|
56
57
|
"./package.json": "./package.json"
|
|
57
58
|
},
|
|
58
59
|
"bin": {
|
|
59
|
-
"martty": "bin/martty.js"
|
|
60
|
-
"dsh-tui": "bin/martty.js"
|
|
60
|
+
"martty-cli": "bin/martty.js"
|
|
61
61
|
},
|
|
62
62
|
"files": [
|
|
63
63
|
"bin",
|
|
@@ -71,18 +71,19 @@
|
|
|
71
71
|
"dsh": {
|
|
72
72
|
"bundle": {
|
|
73
73
|
"patch": "./cordis.patch.yml"
|
|
74
|
-
}
|
|
74
|
+
},
|
|
75
|
+
"supported": "0.1.7-rc.2 || 0.2.0-rc.1 || 0.2.0-rc.2"
|
|
75
76
|
},
|
|
76
77
|
"dependencies": {
|
|
77
78
|
"@deepseek-ai/cordis": "^4.0.1",
|
|
78
79
|
"@lobehub/icons-static-svg": "1.95.0",
|
|
79
|
-
"@openma/deepseek-harness-acp": "0.4.
|
|
80
|
+
"@openma/deepseek-harness-acp": "0.4.37",
|
|
80
81
|
"@resvg/resvg-js": "2.6.2",
|
|
81
82
|
"cross-spawn": "^7.0.6",
|
|
82
83
|
"node-downloader-helper": "2.1.11"
|
|
83
84
|
},
|
|
84
85
|
"devDependencies": {
|
|
85
|
-
"@deepseek-ai/dsh": "0.
|
|
86
|
+
"@deepseek-ai/dsh": "0.2.0-rc.2"
|
|
86
87
|
},
|
|
87
88
|
"engines": {
|
|
88
89
|
"node": ">=22.19.0"
|
|
@@ -96,10 +97,10 @@
|
|
|
96
97
|
"agent-client-protocol"
|
|
97
98
|
],
|
|
98
99
|
"optionalDependencies": {
|
|
99
|
-
"@openma/martty-darwin-arm64": "0.3.1",
|
|
100
|
-
"@openma/martty-darwin-x64": "0.3.1",
|
|
101
|
-
"@openma/martty-linux-x64": "0.3.1",
|
|
102
|
-
"@openma/martty-linux-arm64": "0.3.1",
|
|
103
|
-
"@openma/martty-win32-x64": "0.3.1"
|
|
100
|
+
"@openma/martty-darwin-arm64": "0.3.2-beta.1",
|
|
101
|
+
"@openma/martty-darwin-x64": "0.3.2-beta.1",
|
|
102
|
+
"@openma/martty-linux-x64": "0.3.2-beta.1",
|
|
103
|
+
"@openma/martty-linux-arm64": "0.3.2-beta.1",
|
|
104
|
+
"@openma/martty-win32-x64": "0.3.2-beta.1"
|
|
104
105
|
}
|
|
105
106
|
}
|
|
@@ -56,7 +56,7 @@ depend on a value returned by `apply()` as the disposer; register effects
|
|
|
56
56
|
through the inspected Cordis APIs.
|
|
57
57
|
|
|
58
58
|
The authoring root is `$MARTTY_HOME/plugins`; `MARTTY_HOME` resolves from the
|
|
59
|
-
explicit environment variable
|
|
59
|
+
explicit environment variable or `OMA_HOME`, then `$DSH_HOME/.oma`, then `~/.oma`.
|
|
60
60
|
User selections live separately in `$MARTTY_HOME/settings.json`.
|
|
61
61
|
|
|
62
62
|
## Inspect is the authority
|