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.
Files changed (118) hide show
  1. package/README.md +9 -123
  2. package/bin/coding-agent.ts +95 -0
  3. package/package.json +10 -21
  4. package/src/config.ts +122 -0
  5. package/src/harness.test.ts +159 -0
  6. package/src/main.ts +53 -3
  7. package/src/plugins.ts +49 -0
  8. package/src/systemPrompt.ts +22 -0
  9. package/src/ui/ChatApp.ts +959 -0
  10. package/src/ui/commands.ts +35 -0
  11. package/src/ui/editor.ts +166 -0
  12. package/src/ui/markdown.ts +363 -0
  13. package/src/ui/picker.ts +126 -0
  14. package/src/ui/status.ts +61 -0
  15. package/src/ui/theme.ts +241 -0
  16. package/src/ui/transcript.test.ts +121 -0
  17. package/src/ui/transcript.ts +399 -0
  18. package/tsconfig.json +8 -0
  19. package/bin/mu.js +0 -2
  20. package/prompts/SYSTEM.md +0 -16
  21. package/src/app/shutdown.ts +0 -94
  22. package/src/app/startApp.ts +0 -49
  23. package/src/cli/args.ts +0 -133
  24. package/src/cli/install.ts +0 -107
  25. package/src/cli/subcommands.ts +0 -29
  26. package/src/cli/update.ts +0 -205
  27. package/src/config/index.test.ts +0 -77
  28. package/src/config/index.ts +0 -199
  29. package/src/plugin.ts +0 -124
  30. package/src/runtime/codingTools/bash.ts +0 -114
  31. package/src/runtime/codingTools/edit-file.ts +0 -60
  32. package/src/runtime/codingTools/index.ts +0 -39
  33. package/src/runtime/codingTools/read-file.ts +0 -83
  34. package/src/runtime/codingTools/utils.ts +0 -21
  35. package/src/runtime/codingTools/write-file.ts +0 -42
  36. package/src/runtime/createRegistry.test.ts +0 -147
  37. package/src/runtime/createRegistry.ts +0 -195
  38. package/src/runtime/fileMentionProvider.ts +0 -117
  39. package/src/runtime/messageBus.test.ts +0 -62
  40. package/src/runtime/messageBus.ts +0 -78
  41. package/src/runtime/pluginLoader.ts +0 -153
  42. package/src/runtime/startupUpdateCheck.ts +0 -163
  43. package/src/runtime/updateCheck.ts +0 -136
  44. package/src/sessions/index.test.ts +0 -66
  45. package/src/sessions/index.ts +0 -183
  46. package/src/sessions/peek.test.ts +0 -88
  47. package/src/sessions/project.ts +0 -51
  48. package/src/tui/channel/tuiChannel.test.ts +0 -107
  49. package/src/tui/channel/tuiChannel.ts +0 -62
  50. package/src/tui/chat/ChatContext.ts +0 -10
  51. package/src/tui/chat/MessageRendererContext.ts +0 -44
  52. package/src/tui/chat/ToolDisplayContext.ts +0 -33
  53. package/src/tui/chat/useAbort.ts +0 -85
  54. package/src/tui/chat/useAttachment.ts +0 -74
  55. package/src/tui/chat/useChat.ts +0 -113
  56. package/src/tui/chat/useChatPanel.ts +0 -120
  57. package/src/tui/chat/useChatSession.ts +0 -384
  58. package/src/tui/chat/useModels.ts +0 -83
  59. package/src/tui/chat/usePluginStatus.ts +0 -44
  60. package/src/tui/chat/useSessionPersistence.ts +0 -84
  61. package/src/tui/chat/useStatusSegments.ts +0 -85
  62. package/src/tui/chat/useSubagentBrowser.ts +0 -133
  63. package/src/tui/components/chat/ChatPanel.tsx +0 -54
  64. package/src/tui/components/chat/ChatPanelBody.tsx +0 -86
  65. package/src/tui/components/chat/Pickers.tsx +0 -44
  66. package/src/tui/components/chat/SubagentBrowserPanel.tsx +0 -145
  67. package/src/tui/components/messageView.tsx +0 -72
  68. package/src/tui/components/messages/EditOutput.tsx +0 -112
  69. package/src/tui/components/messages/ReadOutput.tsx +0 -48
  70. package/src/tui/components/messages/ToolHeader.tsx +0 -30
  71. package/src/tui/components/messages/WebFetchOutput.tsx +0 -30
  72. package/src/tui/components/messages/WriteOutput.tsx +0 -64
  73. package/src/tui/components/messages/assistantMessage.tsx +0 -72
  74. package/src/tui/components/messages/markdown.tsx +0 -407
  75. package/src/tui/components/messages/messageItem.tsx +0 -43
  76. package/src/tui/components/messages/reasoningBlock.tsx +0 -18
  77. package/src/tui/components/messages/streamingOutput.tsx +0 -18
  78. package/src/tui/components/messages/toolCallBlock.tsx +0 -125
  79. package/src/tui/components/messages/userMessage.tsx +0 -44
  80. package/src/tui/components/primitives/dropdown.tsx +0 -125
  81. package/src/tui/components/primitives/modal.tsx +0 -47
  82. package/src/tui/components/primitives/pickerModal.tsx +0 -47
  83. package/src/tui/components/primitives/scrollbar.tsx +0 -27
  84. package/src/tui/components/primitives/toast.tsx +0 -100
  85. package/src/tui/components/statusBar.tsx +0 -41
  86. package/src/tui/components/ui/dialogLayer.tsx +0 -175
  87. package/src/tui/context/ThemeContext.tsx +0 -18
  88. package/src/tui/hooks/useChordKeyboard.ts +0 -87
  89. package/src/tui/hooks/useInputInfoSegments.ts +0 -22
  90. package/src/tui/hooks/useScroll.ts +0 -64
  91. package/src/tui/hooks/useTerminal.ts +0 -40
  92. package/src/tui/hooks/useUI.ts +0 -15
  93. package/src/tui/input/InputBox.tsx +0 -6
  94. package/src/tui/input/InputBoxView.tsx +0 -293
  95. package/src/tui/input/commands.test.ts +0 -71
  96. package/src/tui/input/commands.ts +0 -55
  97. package/src/tui/input/cursor.test.ts +0 -136
  98. package/src/tui/input/cursor.ts +0 -214
  99. package/src/tui/input/dumpContext.ts +0 -107
  100. package/src/tui/input/sanitize.ts +0 -33
  101. package/src/tui/input/useCommandExecutor.ts +0 -32
  102. package/src/tui/input/useInputBox.ts +0 -265
  103. package/src/tui/input/useInputHandler.ts +0 -455
  104. package/src/tui/input/useMentionPicker.ts +0 -133
  105. package/src/tui/input/usePluginShortcuts.ts +0 -29
  106. package/src/tui/plugins/InkApprovalChannel.test.ts +0 -51
  107. package/src/tui/plugins/InkApprovalChannel.ts +0 -30
  108. package/src/tui/plugins/InkUIService.ts +0 -188
  109. package/src/tui/renderApp.tsx +0 -66
  110. package/src/tui/theme/index.ts +0 -1
  111. package/src/tui/theme/merge.test.ts +0 -49
  112. package/src/tui/theme/merge.ts +0 -43
  113. package/src/tui/theme/presets.ts +0 -90
  114. package/src/tui/theme/types.ts +0 -138
  115. package/src/tui/update/runUpdateInTui.ts +0 -127
  116. package/src/utils/clipboard.ts +0 -97
  117. package/src/utils/diff.test.ts +0 -56
  118. 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
- }
@@ -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
- }
@@ -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
- }
@@ -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
- });