mu-coding 0.15.0 → 0.16.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 +9 -123
- package/bin/coding-agent.ts +95 -0
- package/package.json +10 -21
- package/src/config.ts +122 -0
- package/src/harness.test.ts +159 -0
- package/src/main.ts +53 -3
- package/src/plugins.ts +49 -0
- package/src/systemPrompt.ts +22 -0
- package/src/ui/ChatApp.ts +959 -0
- package/src/ui/commands.ts +35 -0
- package/src/ui/editor.ts +166 -0
- package/src/ui/markdown.ts +363 -0
- package/src/ui/picker.ts +126 -0
- package/src/ui/status.ts +61 -0
- package/src/ui/theme.ts +241 -0
- package/src/ui/transcript.test.ts +121 -0
- package/src/ui/transcript.ts +399 -0
- package/tsconfig.json +8 -0
- package/bin/mu.js +0 -2
- package/prompts/SYSTEM.md +0 -16
- package/src/app/shutdown.ts +0 -94
- package/src/app/startApp.ts +0 -49
- package/src/cli/args.ts +0 -133
- package/src/cli/install.ts +0 -107
- package/src/cli/subcommands.ts +0 -29
- package/src/cli/update.ts +0 -205
- package/src/config/index.test.ts +0 -77
- package/src/config/index.ts +0 -199
- package/src/plugin.ts +0 -124
- package/src/runtime/codingTools/bash.ts +0 -114
- package/src/runtime/codingTools/edit-file.ts +0 -60
- package/src/runtime/codingTools/index.ts +0 -39
- package/src/runtime/codingTools/read-file.ts +0 -83
- package/src/runtime/codingTools/utils.ts +0 -21
- package/src/runtime/codingTools/write-file.ts +0 -42
- package/src/runtime/createRegistry.test.ts +0 -147
- package/src/runtime/createRegistry.ts +0 -195
- package/src/runtime/fileMentionProvider.ts +0 -117
- package/src/runtime/messageBus.test.ts +0 -62
- package/src/runtime/messageBus.ts +0 -78
- package/src/runtime/pluginLoader.ts +0 -153
- package/src/runtime/startupUpdateCheck.ts +0 -163
- package/src/runtime/updateCheck.ts +0 -136
- package/src/sessions/index.test.ts +0 -66
- package/src/sessions/index.ts +0 -183
- package/src/sessions/peek.test.ts +0 -88
- package/src/sessions/project.ts +0 -51
- package/src/tui/channel/tuiChannel.test.ts +0 -107
- package/src/tui/channel/tuiChannel.ts +0 -62
- package/src/tui/chat/ChatContext.ts +0 -10
- package/src/tui/chat/MessageRendererContext.ts +0 -44
- package/src/tui/chat/ToolDisplayContext.ts +0 -33
- package/src/tui/chat/useAbort.ts +0 -85
- package/src/tui/chat/useAttachment.ts +0 -74
- package/src/tui/chat/useChat.ts +0 -113
- package/src/tui/chat/useChatPanel.ts +0 -120
- package/src/tui/chat/useChatSession.ts +0 -384
- package/src/tui/chat/useModels.ts +0 -83
- package/src/tui/chat/usePluginStatus.ts +0 -44
- package/src/tui/chat/useSessionPersistence.ts +0 -84
- package/src/tui/chat/useStatusSegments.ts +0 -85
- package/src/tui/chat/useSubagentBrowser.ts +0 -133
- package/src/tui/components/chat/ChatPanel.tsx +0 -54
- package/src/tui/components/chat/ChatPanelBody.tsx +0 -86
- package/src/tui/components/chat/Pickers.tsx +0 -44
- package/src/tui/components/chat/SubagentBrowserPanel.tsx +0 -145
- package/src/tui/components/messageView.tsx +0 -72
- package/src/tui/components/messages/EditOutput.tsx +0 -112
- package/src/tui/components/messages/ReadOutput.tsx +0 -48
- package/src/tui/components/messages/ToolHeader.tsx +0 -30
- package/src/tui/components/messages/WebFetchOutput.tsx +0 -30
- package/src/tui/components/messages/WriteOutput.tsx +0 -64
- package/src/tui/components/messages/assistantMessage.tsx +0 -72
- package/src/tui/components/messages/markdown.tsx +0 -407
- package/src/tui/components/messages/messageItem.tsx +0 -43
- package/src/tui/components/messages/reasoningBlock.tsx +0 -18
- package/src/tui/components/messages/streamingOutput.tsx +0 -18
- package/src/tui/components/messages/toolCallBlock.tsx +0 -125
- package/src/tui/components/messages/userMessage.tsx +0 -44
- package/src/tui/components/primitives/dropdown.tsx +0 -125
- package/src/tui/components/primitives/modal.tsx +0 -47
- package/src/tui/components/primitives/pickerModal.tsx +0 -47
- package/src/tui/components/primitives/scrollbar.tsx +0 -27
- package/src/tui/components/primitives/toast.tsx +0 -100
- package/src/tui/components/statusBar.tsx +0 -41
- package/src/tui/components/ui/dialogLayer.tsx +0 -175
- package/src/tui/context/ThemeContext.tsx +0 -18
- package/src/tui/hooks/useChordKeyboard.ts +0 -87
- package/src/tui/hooks/useInputInfoSegments.ts +0 -22
- package/src/tui/hooks/useScroll.ts +0 -64
- package/src/tui/hooks/useTerminal.ts +0 -40
- package/src/tui/hooks/useUI.ts +0 -15
- package/src/tui/input/InputBox.tsx +0 -6
- package/src/tui/input/InputBoxView.tsx +0 -293
- package/src/tui/input/commands.test.ts +0 -71
- package/src/tui/input/commands.ts +0 -55
- package/src/tui/input/cursor.test.ts +0 -136
- package/src/tui/input/cursor.ts +0 -214
- package/src/tui/input/dumpContext.ts +0 -107
- package/src/tui/input/sanitize.ts +0 -33
- package/src/tui/input/useCommandExecutor.ts +0 -32
- package/src/tui/input/useInputBox.ts +0 -265
- package/src/tui/input/useInputHandler.ts +0 -455
- package/src/tui/input/useMentionPicker.ts +0 -133
- package/src/tui/input/usePluginShortcuts.ts +0 -29
- package/src/tui/plugins/InkApprovalChannel.test.ts +0 -51
- package/src/tui/plugins/InkApprovalChannel.ts +0 -30
- package/src/tui/plugins/InkUIService.ts +0 -188
- package/src/tui/renderApp.tsx +0 -66
- package/src/tui/theme/index.ts +0 -1
- package/src/tui/theme/merge.test.ts +0 -49
- package/src/tui/theme/merge.ts +0 -43
- package/src/tui/theme/presets.ts +0 -90
- package/src/tui/theme/types.ts +0 -138
- package/src/tui/update/runUpdateInTui.ts +0 -127
- package/src/utils/clipboard.ts +0 -97
- package/src/utils/diff.test.ts +0 -56
- package/src/utils/diff.ts +0 -81
package/src/cli/args.ts
DELETED
|
@@ -1,133 +0,0 @@
|
|
|
1
|
-
import { readFileSync } from 'node:fs';
|
|
2
|
-
import { dirname, join } from 'node:path';
|
|
3
|
-
import { fileURLToPath } from 'node:url';
|
|
4
|
-
import { parseArgs as nodeParseArgs } from 'node:util';
|
|
5
|
-
import type { ChatMessage } from 'mu-core';
|
|
6
|
-
import { getLatestSession, loadSession } from '../sessions/index';
|
|
7
|
-
|
|
8
|
-
interface CliArgs {
|
|
9
|
-
model?: string;
|
|
10
|
-
continueSession?: boolean;
|
|
11
|
-
sessionPath?: string;
|
|
12
|
-
}
|
|
13
|
-
|
|
14
|
-
function printHelp(): never {
|
|
15
|
-
console.log(`mu — minimal terminal AI assistant
|
|
16
|
-
|
|
17
|
-
Usage:
|
|
18
|
-
mu Start interactive chat
|
|
19
|
-
mu -m, --model <model> Interactive with specific model
|
|
20
|
-
mu -c, --continue Continue most recent session
|
|
21
|
-
mu --session <path> Resume a specific session file
|
|
22
|
-
mu install npm:<package> Install a plugin from npm
|
|
23
|
-
mu uninstall npm:<package> Remove an installed plugin
|
|
24
|
-
mu update [plugins|self|all] Update plugins and/or mu (default: all)
|
|
25
|
-
mu outdated [plugins|self] List available updates without applying
|
|
26
|
-
mu -v, --version Print version and exit
|
|
27
|
-
mu -h, --help Show this help
|
|
28
|
-
|
|
29
|
-
Config (XDG):
|
|
30
|
-
~/.config/mu/config.json — configuration (baseUrl, model, streamTimeoutMs)
|
|
31
|
-
~/.config/mu/SYSTEM.md — system prompt
|
|
32
|
-
~/.local/share/mu/sessions/ — saved conversation sessions (JSONL)
|
|
33
|
-
~/.cache/mu/repomap/ — code index cache
|
|
34
|
-
|
|
35
|
-
Keyboard shortcuts (interactive):
|
|
36
|
-
Ctrl+C Abort / Quit (press twice)
|
|
37
|
-
Esc Stop generation (press twice while streaming)
|
|
38
|
-
Enter Send message
|
|
39
|
-
Shift+Enter New line
|
|
40
|
-
Ctrl+S Send message
|
|
41
|
-
← / → Move cursor (Ctrl/Alt+arrow: by word)
|
|
42
|
-
Home/End Start/end of line (or Ctrl+A / Ctrl+E)
|
|
43
|
-
↑ / ↓ Move between lines; navigate history at edges
|
|
44
|
-
Backspace/Del Delete around cursor (Ctrl+W word, Ctrl+U/K line)
|
|
45
|
-
Ctrl+N New conversation
|
|
46
|
-
Ctrl+M Cycle models
|
|
47
|
-
Ctrl+O Model picker
|
|
48
|
-
Ctrl+V Paste image from clipboard`);
|
|
49
|
-
process.exit(0);
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
function printVersion(): never {
|
|
53
|
-
// Walk up from this file to find mu-coding's package.json. Works whether
|
|
54
|
-
// the file is loaded from `src/cli/args.ts` (bun --watch) or `dist/cli/args.js`.
|
|
55
|
-
const here = dirname(fileURLToPath(import.meta.url));
|
|
56
|
-
const candidates = [join(here, '..', '..', 'package.json'), join(here, '..', 'package.json')];
|
|
57
|
-
for (const path of candidates) {
|
|
58
|
-
try {
|
|
59
|
-
const pkg = JSON.parse(readFileSync(path, 'utf-8'));
|
|
60
|
-
if (pkg?.name === 'mu-coding') {
|
|
61
|
-
console.log(`mu ${pkg.version}`);
|
|
62
|
-
process.exit(0);
|
|
63
|
-
}
|
|
64
|
-
} catch {
|
|
65
|
-
// Try next candidate.
|
|
66
|
-
}
|
|
67
|
-
}
|
|
68
|
-
console.log('mu (version unknown)');
|
|
69
|
-
process.exit(0);
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
export function parseArgs(): CliArgs {
|
|
73
|
-
let parsed: ReturnType<typeof nodeParseArgs>;
|
|
74
|
-
try {
|
|
75
|
-
parsed = nodeParseArgs({
|
|
76
|
-
options: {
|
|
77
|
-
model: { type: 'string', short: 'm' },
|
|
78
|
-
continue: { type: 'boolean', short: 'c' },
|
|
79
|
-
session: { type: 'string' },
|
|
80
|
-
version: { type: 'boolean', short: 'v' },
|
|
81
|
-
help: { type: 'boolean', short: 'h' },
|
|
82
|
-
},
|
|
83
|
-
// Subcommands like `install`/`uninstall` are routed before parseArgs(),
|
|
84
|
-
// so we shouldn't see them here. Allow positionals just in case the
|
|
85
|
-
// user passes stray args (we ignore them rather than erroring).
|
|
86
|
-
allowPositionals: true,
|
|
87
|
-
strict: true,
|
|
88
|
-
});
|
|
89
|
-
} catch (err) {
|
|
90
|
-
console.error(err instanceof Error ? err.message : String(err));
|
|
91
|
-
console.error('Run `mu --help` for usage.');
|
|
92
|
-
process.exit(1);
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
if (parsed.values.help) {
|
|
96
|
-
printHelp();
|
|
97
|
-
}
|
|
98
|
-
if (parsed.values.version) {
|
|
99
|
-
printVersion();
|
|
100
|
-
}
|
|
101
|
-
|
|
102
|
-
return {
|
|
103
|
-
model: typeof parsed.values.model === 'string' ? parsed.values.model : undefined,
|
|
104
|
-
continueSession: parsed.values.continue === true,
|
|
105
|
-
sessionPath: typeof parsed.values.session === 'string' ? parsed.values.session : undefined,
|
|
106
|
-
};
|
|
107
|
-
}
|
|
108
|
-
|
|
109
|
-
export function resolveInitialMessages(cliArgs: CliArgs): ChatMessage[] | undefined {
|
|
110
|
-
if (cliArgs.sessionPath) {
|
|
111
|
-
const msgs = loadSession(cliArgs.sessionPath);
|
|
112
|
-
if (msgs.length === 0) {
|
|
113
|
-
console.error(`Error: session file is empty or not found: ${cliArgs.sessionPath}`);
|
|
114
|
-
process.exit(1);
|
|
115
|
-
}
|
|
116
|
-
return msgs;
|
|
117
|
-
}
|
|
118
|
-
if (cliArgs.continueSession) {
|
|
119
|
-
const latest = getLatestSession();
|
|
120
|
-
if (!latest) {
|
|
121
|
-
console.error('Error: no sessions found');
|
|
122
|
-
process.exit(1);
|
|
123
|
-
}
|
|
124
|
-
const msgs = loadSession(latest);
|
|
125
|
-
if (msgs.length === 0) {
|
|
126
|
-
console.error('Error: latest session is empty');
|
|
127
|
-
process.exit(1);
|
|
128
|
-
}
|
|
129
|
-
console.log(`Resuming session: ${latest}`);
|
|
130
|
-
return msgs;
|
|
131
|
-
}
|
|
132
|
-
return undefined;
|
|
133
|
-
}
|
package/src/cli/install.ts
DELETED
|
@@ -1,107 +0,0 @@
|
|
|
1
|
-
import { execFileSync } from 'node:child_process';
|
|
2
|
-
import { existsSync, mkdirSync, writeFileSync } from 'node:fs';
|
|
3
|
-
import { join } from 'node:path';
|
|
4
|
-
import { canonicalNpmSpecifier, getDataDir, loadConfig, parseBareNpmSpec, saveConfig } from '../config/index';
|
|
5
|
-
|
|
6
|
-
const INIT_PACKAGE_JSON = JSON.stringify({ private: true, dependencies: {} }, null, 2);
|
|
7
|
-
|
|
8
|
-
export function ensureDataDir(): string {
|
|
9
|
-
const dataDir = getDataDir();
|
|
10
|
-
mkdirSync(dataDir, { recursive: true });
|
|
11
|
-
|
|
12
|
-
const pkgPath = join(dataDir, 'package.json');
|
|
13
|
-
if (!existsSync(pkgPath)) {
|
|
14
|
-
writeFileSync(pkgPath, INIT_PACKAGE_JSON, 'utf-8');
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
return dataDir;
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
/**
|
|
21
|
-
* Install an npm package into the mu data dir using `bun add`. Shared by the
|
|
22
|
-
* `mu install` CLI and the runtime auto-installer (pluginLoader). `bare` is
|
|
23
|
-
* the spec without the `npm:` prefix (e.g. `mu-coding-agents`,
|
|
24
|
-
* `@scope/foo@^1.0.0`). When `silent`, stdout is suppressed (used by the
|
|
25
|
-
* runtime path so the TUI isn't garbled at startup).
|
|
26
|
-
*/
|
|
27
|
-
export function installNpmPackage(bare: string, options: { silent?: boolean } = {}): void {
|
|
28
|
-
const dataDir = ensureDataDir();
|
|
29
|
-
execFileSync('bun', ['add', bare], {
|
|
30
|
-
cwd: dataDir,
|
|
31
|
-
stdio: options.silent ? 'pipe' : 'inherit',
|
|
32
|
-
});
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
function stripNpmPrefix(specifier: string): string {
|
|
36
|
-
if (!specifier.startsWith('npm:')) {
|
|
37
|
-
console.error(`Error: package specifier must start with npm: — got "${specifier}"`);
|
|
38
|
-
process.exit(1);
|
|
39
|
-
}
|
|
40
|
-
return specifier.slice(4);
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
export async function runInstall(args: string[]): Promise<void> {
|
|
44
|
-
if (args.length === 0) {
|
|
45
|
-
console.error('Usage: mu install npm:<package>');
|
|
46
|
-
process.exit(1);
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
ensureDataDir();
|
|
50
|
-
const config = loadConfig();
|
|
51
|
-
const plugins = config.plugins ?? [];
|
|
52
|
-
|
|
53
|
-
for (const specifier of args) {
|
|
54
|
-
const bare = stripNpmPrefix(specifier);
|
|
55
|
-
const canonical = canonicalNpmSpecifier(bare);
|
|
56
|
-
|
|
57
|
-
console.log(`Installing ${bare}...`);
|
|
58
|
-
try {
|
|
59
|
-
installNpmPackage(bare);
|
|
60
|
-
} catch (err) {
|
|
61
|
-
console.error(`Failed to install ${bare}: ${err instanceof Error ? err.message : err}`);
|
|
62
|
-
process.exit(1);
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
// Add to plugins if not already present (compare by canonical form so
|
|
66
|
-
// `npm:foo` and `npm:foo@1.2.3` deduplicate correctly).
|
|
67
|
-
const existing = plugins.some((p) => (typeof p === 'string' ? p : p.name) === canonical);
|
|
68
|
-
if (!existing) {
|
|
69
|
-
plugins.push(canonical);
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
console.log(`✓ ${canonical}`);
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
saveConfig({ plugins });
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
export async function runUninstall(args: string[]): Promise<void> {
|
|
79
|
-
if (args.length === 0) {
|
|
80
|
-
console.error('Usage: mu uninstall npm:<package>');
|
|
81
|
-
process.exit(1);
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
const dataDir = ensureDataDir();
|
|
85
|
-
const config = loadConfig();
|
|
86
|
-
let plugins = config.plugins ?? [];
|
|
87
|
-
|
|
88
|
-
for (const specifier of args) {
|
|
89
|
-
const bare = stripNpmPrefix(specifier);
|
|
90
|
-
const canonical = canonicalNpmSpecifier(bare);
|
|
91
|
-
const { name } = parseBareNpmSpec(bare);
|
|
92
|
-
|
|
93
|
-
console.log(`Removing ${name}...`);
|
|
94
|
-
try {
|
|
95
|
-
execFileSync('bun', ['remove', name], { cwd: dataDir, stdio: 'inherit' });
|
|
96
|
-
} catch (err) {
|
|
97
|
-
console.error(`Failed to remove ${name}: ${err instanceof Error ? err.message : err}`);
|
|
98
|
-
process.exit(1);
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
plugins = plugins.filter((p) => (typeof p === 'string' ? p : p.name) !== canonical);
|
|
102
|
-
|
|
103
|
-
console.log(`✓ Removed ${canonical}`);
|
|
104
|
-
}
|
|
105
|
-
|
|
106
|
-
saveConfig({ plugins });
|
|
107
|
-
}
|
package/src/cli/subcommands.ts
DELETED
|
@@ -1,29 +0,0 @@
|
|
|
1
|
-
import { runInstall, runUninstall } from './install';
|
|
2
|
-
import { runOutdated, runUpdate } from './update';
|
|
3
|
-
|
|
4
|
-
/**
|
|
5
|
-
* Handle CLI subcommands that run before the TUI.
|
|
6
|
-
* Returns true if a subcommand was handled (caller should exit).
|
|
7
|
-
*/
|
|
8
|
-
export async function handleSubcommand(): Promise<boolean> {
|
|
9
|
-
const sub = process.argv[2];
|
|
10
|
-
|
|
11
|
-
switch (sub) {
|
|
12
|
-
case 'install':
|
|
13
|
-
await runInstall(process.argv.slice(3));
|
|
14
|
-
return true;
|
|
15
|
-
case 'uninstall':
|
|
16
|
-
await runUninstall(process.argv.slice(3));
|
|
17
|
-
return true;
|
|
18
|
-
case 'update':
|
|
19
|
-
case 'upgrade':
|
|
20
|
-
await runUpdate(process.argv.slice(3));
|
|
21
|
-
return true;
|
|
22
|
-
case 'outdated':
|
|
23
|
-
case 'ping':
|
|
24
|
-
await runOutdated(process.argv.slice(3));
|
|
25
|
-
return true;
|
|
26
|
-
default:
|
|
27
|
-
return false;
|
|
28
|
-
}
|
|
29
|
-
}
|
package/src/cli/update.ts
DELETED
|
@@ -1,205 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* `mu update` — bring installed plugins (and optionally mu itself) to the
|
|
3
|
-
* latest version published on the npm registry.
|
|
4
|
-
*
|
|
5
|
-
* Plugins live in `~/.local/share/mu/node_modules/<name>` and are listed in
|
|
6
|
-
* `config.plugins` as `npm:<name>` specifiers. Updating them is a thin
|
|
7
|
-
* wrapper around `bun update --latest <name>` against the data dir, which
|
|
8
|
-
* already owns its own `package.json`.
|
|
9
|
-
*
|
|
10
|
-
* mu itself is updated by re-running its global installer. We probe the
|
|
11
|
-
* binary path to detect which manager owns the install (bun, npm, pnpm) and
|
|
12
|
-
* fall back to a generic `npm i -g` instructions message on unknown setups.
|
|
13
|
-
*/
|
|
14
|
-
|
|
15
|
-
import { execFileSync } from 'node:child_process';
|
|
16
|
-
import { realpathSync } from 'node:fs';
|
|
17
|
-
import {
|
|
18
|
-
listConfiguredNpmPlugins,
|
|
19
|
-
type NpmRegistryView,
|
|
20
|
-
PACKAGE_NAME,
|
|
21
|
-
probePluginSync,
|
|
22
|
-
probeSelfSync,
|
|
23
|
-
} from '../runtime/updateCheck';
|
|
24
|
-
import { ensureDataDir } from './install';
|
|
25
|
-
|
|
26
|
-
function printRow(name: string, view: NpmRegistryView | null) {
|
|
27
|
-
if (!view) {
|
|
28
|
-
console.log(` ${name.padEnd(28)} ? (npm view failed)`);
|
|
29
|
-
return;
|
|
30
|
-
}
|
|
31
|
-
const arrow = view.hasUpdate ? '→' : '=';
|
|
32
|
-
const tail = view.hasUpdate ? `${view.current} ${arrow} ${view.latest}` : `${view.current} (up to date)`;
|
|
33
|
-
console.log(` ${name.padEnd(28)} ${tail}`);
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
// ─── outdated ────────────────────────────────────────────────────────────────
|
|
37
|
-
|
|
38
|
-
export async function runOutdated(args: string[]): Promise<void> {
|
|
39
|
-
const scope = args[0];
|
|
40
|
-
const wantsPlugins = !scope || scope === 'plugins' || scope === 'all';
|
|
41
|
-
const wantsSelf = !scope || scope === 'self' || scope === 'mu' || scope === 'all';
|
|
42
|
-
|
|
43
|
-
const dataDir = ensureDataDir();
|
|
44
|
-
let anyUpdate = false;
|
|
45
|
-
|
|
46
|
-
if (wantsSelf) {
|
|
47
|
-
console.log('mu:');
|
|
48
|
-
const view = probeSelfSync();
|
|
49
|
-
printRow(PACKAGE_NAME, view);
|
|
50
|
-
if (view?.hasUpdate) anyUpdate = true;
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
if (wantsPlugins) {
|
|
54
|
-
const names = listConfiguredNpmPlugins();
|
|
55
|
-
console.log(`\nplugins (${names.length}):`);
|
|
56
|
-
if (names.length === 0) {
|
|
57
|
-
console.log(' (none configured)');
|
|
58
|
-
} else {
|
|
59
|
-
for (const name of names) {
|
|
60
|
-
const view = probePluginSync(name, dataDir);
|
|
61
|
-
printRow(name, view);
|
|
62
|
-
if (view?.hasUpdate) anyUpdate = true;
|
|
63
|
-
}
|
|
64
|
-
}
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
if (!anyUpdate) {
|
|
68
|
-
console.log('\nEverything is up to date.');
|
|
69
|
-
} else {
|
|
70
|
-
console.log("\nRun 'mu update' to apply.");
|
|
71
|
-
}
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
// ─── update plugins ──────────────────────────────────────────────────────────
|
|
75
|
-
|
|
76
|
-
function updatePlugin(name: string, dataDir: string): boolean {
|
|
77
|
-
try {
|
|
78
|
-
execFileSync('bun', ['update', '--latest', name], { cwd: dataDir, stdio: 'inherit' });
|
|
79
|
-
return true;
|
|
80
|
-
} catch (err) {
|
|
81
|
-
console.error(`Failed to update ${name}: ${err instanceof Error ? err.message : err}`);
|
|
82
|
-
return false;
|
|
83
|
-
}
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
export async function runUpdatePlugins(): Promise<{ ok: number; failed: number }> {
|
|
87
|
-
const dataDir = ensureDataDir();
|
|
88
|
-
const names = listConfiguredNpmPlugins();
|
|
89
|
-
if (names.length === 0) {
|
|
90
|
-
console.log('No npm plugins configured.');
|
|
91
|
-
return { ok: 0, failed: 0 };
|
|
92
|
-
}
|
|
93
|
-
let ok = 0;
|
|
94
|
-
let failed = 0;
|
|
95
|
-
for (const name of names) {
|
|
96
|
-
console.log(`\nUpdating ${name}…`);
|
|
97
|
-
if (updatePlugin(name, dataDir)) {
|
|
98
|
-
ok += 1;
|
|
99
|
-
console.log(`✓ ${name}`);
|
|
100
|
-
} else {
|
|
101
|
-
failed += 1;
|
|
102
|
-
}
|
|
103
|
-
}
|
|
104
|
-
return { ok, failed };
|
|
105
|
-
}
|
|
106
|
-
|
|
107
|
-
// ─── update self ─────────────────────────────────────────────────────────────
|
|
108
|
-
|
|
109
|
-
interface SelfInstallStrategy {
|
|
110
|
-
manager: 'bun' | 'npm' | 'pnpm' | 'yarn' | 'unknown';
|
|
111
|
-
command?: [string, string[]];
|
|
112
|
-
}
|
|
113
|
-
|
|
114
|
-
/**
|
|
115
|
-
* Best-effort detection of which package manager installed `mu` globally.
|
|
116
|
-
* We resolve the absolute binary path of the running process and look for
|
|
117
|
-
* tell-tale path segments.
|
|
118
|
-
*/
|
|
119
|
-
function detectSelfInstall(): SelfInstallStrategy {
|
|
120
|
-
let bin = process.argv[1] ?? '';
|
|
121
|
-
try {
|
|
122
|
-
bin = realpathSync(bin);
|
|
123
|
-
} catch {
|
|
124
|
-
// keep raw argv path
|
|
125
|
-
}
|
|
126
|
-
const norm = bin.replace(/\\/g, '/');
|
|
127
|
-
|
|
128
|
-
if (norm.includes('/.bun/') || norm.includes('/bun/install/')) {
|
|
129
|
-
return { manager: 'bun', command: ['bun', ['add', '-g', `${PACKAGE_NAME}@latest`]] };
|
|
130
|
-
}
|
|
131
|
-
if (norm.includes('/pnpm/')) {
|
|
132
|
-
return { manager: 'pnpm', command: ['pnpm', ['add', '-g', `${PACKAGE_NAME}@latest`]] };
|
|
133
|
-
}
|
|
134
|
-
if (norm.includes('/.yarn/') || norm.includes('/yarn/global/')) {
|
|
135
|
-
return { manager: 'yarn', command: ['yarn', ['global', 'add', `${PACKAGE_NAME}@latest`]] };
|
|
136
|
-
}
|
|
137
|
-
if (norm.includes('/npm/') || norm.includes('node_modules/.bin/mu')) {
|
|
138
|
-
return { manager: 'npm', command: ['npm', ['i', '-g', `${PACKAGE_NAME}@latest`]] };
|
|
139
|
-
}
|
|
140
|
-
return { manager: 'unknown' };
|
|
141
|
-
}
|
|
142
|
-
|
|
143
|
-
export async function runUpdateSelf(): Promise<boolean> {
|
|
144
|
-
const view = probeSelfSync();
|
|
145
|
-
if (view && !view.hasUpdate) {
|
|
146
|
-
console.log(`mu is already up to date (${view.current}).`);
|
|
147
|
-
return true;
|
|
148
|
-
}
|
|
149
|
-
|
|
150
|
-
const strategy = detectSelfInstall();
|
|
151
|
-
if (!strategy.command) {
|
|
152
|
-
console.log(
|
|
153
|
-
[
|
|
154
|
-
'Could not auto-detect how mu was installed.',
|
|
155
|
-
'Re-install with one of:',
|
|
156
|
-
` bun add -g ${PACKAGE_NAME}@latest`,
|
|
157
|
-
` npm i -g ${PACKAGE_NAME}@latest`,
|
|
158
|
-
` pnpm add -g ${PACKAGE_NAME}@latest`,
|
|
159
|
-
].join('\n'),
|
|
160
|
-
);
|
|
161
|
-
return false;
|
|
162
|
-
}
|
|
163
|
-
|
|
164
|
-
const [bin, args] = strategy.command;
|
|
165
|
-
console.log(`Updating mu via ${strategy.manager}…`);
|
|
166
|
-
console.log(`$ ${bin} ${args.join(' ')}`);
|
|
167
|
-
try {
|
|
168
|
-
execFileSync(bin, args, { stdio: 'inherit' });
|
|
169
|
-
console.log('✓ mu updated. Restart any running mu sessions.');
|
|
170
|
-
return true;
|
|
171
|
-
} catch (err) {
|
|
172
|
-
console.error(`Failed to update mu: ${err instanceof Error ? err.message : err}`);
|
|
173
|
-
return false;
|
|
174
|
-
}
|
|
175
|
-
}
|
|
176
|
-
|
|
177
|
-
// ─── dispatcher ──────────────────────────────────────────────────────────────
|
|
178
|
-
|
|
179
|
-
export async function runUpdate(args: string[]): Promise<void> {
|
|
180
|
-
const scope = args[0];
|
|
181
|
-
|
|
182
|
-
if (scope === 'plugins') {
|
|
183
|
-
const { failed } = await runUpdatePlugins();
|
|
184
|
-
if (failed > 0) process.exit(1);
|
|
185
|
-
return;
|
|
186
|
-
}
|
|
187
|
-
|
|
188
|
-
if (scope === 'self' || scope === 'mu') {
|
|
189
|
-
const ok = await runUpdateSelf();
|
|
190
|
-
if (!ok) process.exit(1);
|
|
191
|
-
return;
|
|
192
|
-
}
|
|
193
|
-
|
|
194
|
-
if (scope && scope !== 'all') {
|
|
195
|
-
console.error('Usage: mu update [plugins|self|all]');
|
|
196
|
-
process.exit(1);
|
|
197
|
-
}
|
|
198
|
-
|
|
199
|
-
// Default: update everything.
|
|
200
|
-
console.log('=== Updating plugins ===');
|
|
201
|
-
const pluginRes = await runUpdatePlugins();
|
|
202
|
-
console.log('\n=== Updating mu ===');
|
|
203
|
-
const selfOk = await runUpdateSelf();
|
|
204
|
-
if (pluginRes.failed > 0 || !selfOk) process.exit(1);
|
|
205
|
-
}
|
package/src/config/index.test.ts
DELETED
|
@@ -1,77 +0,0 @@
|
|
|
1
|
-
import { afterAll, beforeAll, describe, expect, it } from 'bun:test';
|
|
2
|
-
import { mkdirSync, mkdtempSync, readFileSync, writeFileSync } from 'node:fs';
|
|
3
|
-
import { tmpdir } from 'node:os';
|
|
4
|
-
import { join } from 'node:path';
|
|
5
|
-
|
|
6
|
-
// Important: stub XDG_CONFIG_HOME *before* importing './index' so the module
|
|
7
|
-
// captures the test paths in its top-level constants. Bun resolves dynamic
|
|
8
|
-
// imports per-call, so we use `await import` inside each test.
|
|
9
|
-
let tmpRoot: string;
|
|
10
|
-
let configPath: string;
|
|
11
|
-
let originalConfigHome: string | undefined;
|
|
12
|
-
|
|
13
|
-
beforeAll(() => {
|
|
14
|
-
tmpRoot = mkdtempSync(join(tmpdir(), 'mu-config-'));
|
|
15
|
-
originalConfigHome = process.env.XDG_CONFIG_HOME;
|
|
16
|
-
process.env.XDG_CONFIG_HOME = tmpRoot;
|
|
17
|
-
mkdirSync(join(tmpRoot, 'mu'), { recursive: true });
|
|
18
|
-
configPath = join(tmpRoot, 'mu', 'config.json');
|
|
19
|
-
});
|
|
20
|
-
|
|
21
|
-
afterAll(() => {
|
|
22
|
-
if (originalConfigHome === undefined) {
|
|
23
|
-
delete process.env.XDG_CONFIG_HOME;
|
|
24
|
-
} else {
|
|
25
|
-
process.env.XDG_CONFIG_HOME = originalConfigHome;
|
|
26
|
-
}
|
|
27
|
-
});
|
|
28
|
-
|
|
29
|
-
describe('saveConfig', () => {
|
|
30
|
-
it('preserves unknown keys in config.json', async () => {
|
|
31
|
-
const { saveConfig } = await import('./index');
|
|
32
|
-
|
|
33
|
-
// Seed with a custom key the loader doesn't know about.
|
|
34
|
-
writeFileSync(configPath, JSON.stringify({ baseUrl: 'http://x', customKey: 42 }, null, 2));
|
|
35
|
-
|
|
36
|
-
saveConfig({ model: 'qwen' });
|
|
37
|
-
|
|
38
|
-
const persisted = JSON.parse(readFileSync(configPath, 'utf-8'));
|
|
39
|
-
expect(persisted.customKey).toBe(42);
|
|
40
|
-
expect(persisted.model).toBe('qwen');
|
|
41
|
-
expect(persisted.baseUrl).toBe('http://x');
|
|
42
|
-
});
|
|
43
|
-
|
|
44
|
-
it('removes a key when explicitly set to undefined', async () => {
|
|
45
|
-
const { saveConfig } = await import('./index');
|
|
46
|
-
writeFileSync(configPath, JSON.stringify({ model: 'before' }));
|
|
47
|
-
saveConfig({ model: undefined });
|
|
48
|
-
const persisted = JSON.parse(readFileSync(configPath, 'utf-8'));
|
|
49
|
-
expect('model' in persisted).toBe(false);
|
|
50
|
-
});
|
|
51
|
-
});
|
|
52
|
-
|
|
53
|
-
describe('loadConfig theme', () => {
|
|
54
|
-
it('reads the theme field from config.json', async () => {
|
|
55
|
-
const { loadConfig } = await import('./index');
|
|
56
|
-
writeFileSync(
|
|
57
|
-
configPath,
|
|
58
|
-
JSON.stringify({ baseUrl: 'http://x', theme: { preset: 'light', input: { cursor: '#abcdef' } } }),
|
|
59
|
-
);
|
|
60
|
-
const cfg = loadConfig();
|
|
61
|
-
expect(cfg.theme).toEqual({ preset: 'light', input: { cursor: '#abcdef' } });
|
|
62
|
-
});
|
|
63
|
-
|
|
64
|
-
it('accepts a preset name string', async () => {
|
|
65
|
-
const { loadConfig } = await import('./index');
|
|
66
|
-
writeFileSync(configPath, JSON.stringify({ baseUrl: 'http://x', theme: 'solarized-dark' }));
|
|
67
|
-
const cfg = loadConfig();
|
|
68
|
-
expect(cfg.theme).toBe('solarized-dark');
|
|
69
|
-
});
|
|
70
|
-
|
|
71
|
-
it('omits theme when not present', async () => {
|
|
72
|
-
const { loadConfig } = await import('./index');
|
|
73
|
-
writeFileSync(configPath, JSON.stringify({ baseUrl: 'http://x' }));
|
|
74
|
-
const cfg = loadConfig();
|
|
75
|
-
expect(cfg.theme).toBeUndefined();
|
|
76
|
-
});
|
|
77
|
-
});
|