mu-coding 0.12.0 → 0.15.0
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/package.json +4 -4
- package/src/runtime/startupUpdateCheck.ts +16 -3
- package/src/tui/chat/useChatPanel.ts +1 -0
- package/src/tui/chat/useChatSession.ts +5 -3
- package/src/tui/chat/useStatusSegments.ts +5 -2
- package/src/tui/input/commands.test.ts +22 -2
- package/src/tui/input/commands.ts +8 -2
- package/src/tui/input/useCommandExecutor.ts +1 -1
- package/src/tui/input/useInputBox.ts +33 -1
- package/src/tui/input/useInputHandler.ts +1 -0
- package/src/tui/renderApp.tsx +2 -0
- package/src/tui/update/runUpdateInTui.ts +127 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "mu-coding",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.15.0",
|
|
4
4
|
"description": "Minimal terminal AI assistant for local models",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -24,9 +24,9 @@
|
|
|
24
24
|
},
|
|
25
25
|
"dependencies": {
|
|
26
26
|
"ink": "^7.0.1",
|
|
27
|
-
"mu-agents": "0.
|
|
28
|
-
"mu-core": "0.
|
|
29
|
-
"mu-openai-provider": "0.
|
|
27
|
+
"mu-agents": "0.15.0",
|
|
28
|
+
"mu-core": "0.15.0",
|
|
29
|
+
"mu-openai-provider": "0.15.0",
|
|
30
30
|
"react": "^19.2.5"
|
|
31
31
|
}
|
|
32
32
|
}
|
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
* - Must never block startup. Caller fire-and-forgets the returned promise.
|
|
8
8
|
* - Must never crash the host on network / DNS errors. Every probe is wrapped.
|
|
9
9
|
* - Must not hammer npm on every boot. Results are cached in
|
|
10
|
-
* `<cacheDir>/update-check.json` for `CACHE_TTL_MS` (
|
|
10
|
+
* `<cacheDir>/update-check.json` for `CACHE_TTL_MS` (1h).
|
|
11
11
|
* - Disable with `MU_NO_UPDATE_CHECK=1` (kill switch for offline / CI / tests).
|
|
12
12
|
*
|
|
13
13
|
* Toasts are routed through the existing `InkUIService.notify` queue, which
|
|
@@ -16,7 +16,7 @@
|
|
|
16
16
|
* the alert still surfaces.
|
|
17
17
|
*/
|
|
18
18
|
|
|
19
|
-
import { mkdirSync, readFileSync, writeFileSync } from 'node:fs';
|
|
19
|
+
import { mkdirSync, readFileSync, rmSync, writeFileSync } from 'node:fs';
|
|
20
20
|
import { join } from 'node:path';
|
|
21
21
|
import { getCacheDir, getDataDir } from '../config/index';
|
|
22
22
|
import type { InkUIService } from '../tui/plugins/InkUIService';
|
|
@@ -28,7 +28,7 @@ import {
|
|
|
28
28
|
probeSelfAsync,
|
|
29
29
|
} from './updateCheck';
|
|
30
30
|
|
|
31
|
-
const CACHE_TTL_MS =
|
|
31
|
+
const CACHE_TTL_MS = 60 * 60 * 1000; // 1 h
|
|
32
32
|
const CACHE_FILENAME = 'update-check.json';
|
|
33
33
|
|
|
34
34
|
interface CacheShape {
|
|
@@ -61,6 +61,19 @@ function writeCache(cache: CacheShape): void {
|
|
|
61
61
|
}
|
|
62
62
|
}
|
|
63
63
|
|
|
64
|
+
/**
|
|
65
|
+
* Discard the cached probe result. Called after a `/update` so the next
|
|
66
|
+
* boot re-probes against fresh registry data instead of replaying the
|
|
67
|
+
* pre-update state from cache.
|
|
68
|
+
*/
|
|
69
|
+
export function invalidateUpdateCheckCache(): void {
|
|
70
|
+
try {
|
|
71
|
+
rmSync(cachePath(), { force: true });
|
|
72
|
+
} catch {
|
|
73
|
+
// best-effort
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
64
77
|
function isDisabled(): boolean {
|
|
65
78
|
const v = process.env.MU_NO_UPDATE_CHECK;
|
|
66
79
|
return v === '1' || v === 'true' || v === 'yes';
|
|
@@ -83,6 +83,7 @@ export function useChatPanel(options: UseChatPanelOptions) {
|
|
|
83
83
|
error: ctx.session.error,
|
|
84
84
|
modelError: ctx.models.modelError,
|
|
85
85
|
totalTokens: ctx.session.stream.totalTokens,
|
|
86
|
+
promptTokens: ctx.session.stream.promptTokens,
|
|
86
87
|
cachedTokens: ctx.session.stream.cachedTokens,
|
|
87
88
|
contextLimit,
|
|
88
89
|
pluginStatus,
|
|
@@ -10,10 +10,11 @@ export interface StreamState {
|
|
|
10
10
|
text: string;
|
|
11
11
|
reasoning: string;
|
|
12
12
|
totalTokens: number;
|
|
13
|
+
promptTokens: number;
|
|
13
14
|
cachedTokens: number;
|
|
14
15
|
}
|
|
15
16
|
|
|
16
|
-
const EMPTY_STREAM: StreamState = { text: '', reasoning: '', totalTokens: 0, cachedTokens: 0 };
|
|
17
|
+
const EMPTY_STREAM: StreamState = { text: '', reasoning: '', totalTokens: 0, promptTokens: 0, cachedTokens: 0 };
|
|
17
18
|
|
|
18
19
|
export interface ChatSessionState {
|
|
19
20
|
messages: ChatMessage[];
|
|
@@ -121,8 +122,9 @@ function makeSessionEventHandler(
|
|
|
121
122
|
if (event.type === 'usage') {
|
|
122
123
|
setStream((s) => ({
|
|
123
124
|
...s,
|
|
124
|
-
totalTokens:
|
|
125
|
-
|
|
125
|
+
totalTokens: event.totalTokens,
|
|
126
|
+
promptTokens: event.promptTokens,
|
|
127
|
+
cachedTokens: event.cachedTokens,
|
|
126
128
|
}));
|
|
127
129
|
return;
|
|
128
130
|
}
|
|
@@ -11,6 +11,9 @@ interface StatusSegmentOptions {
|
|
|
11
11
|
error: string | null;
|
|
12
12
|
modelError: string | null;
|
|
13
13
|
totalTokens: number;
|
|
14
|
+
/** Input tokens sent to the model for the current turn (prompt size). Used
|
|
15
|
+
* for the context window percentage calculation. */
|
|
16
|
+
promptTokens: number;
|
|
14
17
|
/** Tokens served from server-side prompt cache. Rendered as `(N cached)`
|
|
15
18
|
* next to the total when > 0. Omit (or pass 0) to hide the suffix. */
|
|
16
19
|
cachedTokens?: number;
|
|
@@ -46,10 +49,10 @@ export function useStatusSegments(options: StatusSegmentOptions): StatusBarSegme
|
|
|
46
49
|
}
|
|
47
50
|
if (options.totalTokens > 0) {
|
|
48
51
|
const cached = options.cachedTokens ?? 0;
|
|
49
|
-
const used = formatTokens(options.
|
|
52
|
+
const used = formatTokens(options.promptTokens);
|
|
50
53
|
let head: string;
|
|
51
54
|
if (options.contextLimit) {
|
|
52
|
-
const pct = (options.
|
|
55
|
+
const pct = (options.promptTokens / options.contextLimit) * 100;
|
|
53
56
|
const pctStr = pct >= 10 ? pct.toFixed(0) : pct.toFixed(1);
|
|
54
57
|
head = `${used} (${pctStr}%)`;
|
|
55
58
|
} else {
|
|
@@ -23,17 +23,37 @@ describe('BUILTIN_COMMANDS', () => {
|
|
|
23
23
|
const onTogglePicker = mock(() => undefined);
|
|
24
24
|
const onToggleSessionPicker = mock(() => undefined);
|
|
25
25
|
const onNew = mock(() => undefined);
|
|
26
|
+
const onCompact = mock(() => undefined);
|
|
26
27
|
const onShowContext = mock(() => undefined);
|
|
27
|
-
const
|
|
28
|
+
const onUpdate = mock(() => undefined);
|
|
29
|
+
const actions: InputActions = {
|
|
30
|
+
onTogglePicker,
|
|
31
|
+
onToggleSessionPicker,
|
|
32
|
+
onNew,
|
|
33
|
+
onCompact,
|
|
34
|
+
onShowContext,
|
|
35
|
+
onUpdate,
|
|
36
|
+
};
|
|
28
37
|
|
|
29
38
|
for (const cmd of BUILTIN_COMMANDS) {
|
|
30
|
-
cmd.invoke?.(actions);
|
|
39
|
+
cmd.invoke?.(actions, '');
|
|
31
40
|
}
|
|
32
41
|
|
|
33
42
|
expect(onTogglePicker).toHaveBeenCalledTimes(1);
|
|
34
43
|
expect(onToggleSessionPicker).toHaveBeenCalledTimes(1);
|
|
35
44
|
expect(onNew).toHaveBeenCalledTimes(1);
|
|
45
|
+
expect(onCompact).toHaveBeenCalledTimes(1);
|
|
36
46
|
expect(onShowContext).toHaveBeenCalledTimes(1);
|
|
47
|
+
expect(onUpdate).toHaveBeenCalledTimes(1);
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
it('/update forwards args to onUpdate', () => {
|
|
51
|
+
const onUpdate = mock(() => undefined);
|
|
52
|
+
const actions: InputActions = { onUpdate };
|
|
53
|
+
const cmd = BUILTIN_COMMANDS.find((c) => c.name === '/update');
|
|
54
|
+
expect(cmd).toBeDefined();
|
|
55
|
+
cmd?.invoke?.(actions, 'plugins');
|
|
56
|
+
expect(onUpdate).toHaveBeenCalledWith('plugins');
|
|
37
57
|
});
|
|
38
58
|
});
|
|
39
59
|
|
|
@@ -3,7 +3,8 @@ import type { InputActions } from './useInputHandler';
|
|
|
3
3
|
|
|
4
4
|
/**
|
|
5
5
|
* A slash command can either:
|
|
6
|
-
* - run via `invoke(actions)` — for builtins that just toggle UI state
|
|
6
|
+
* - run via `invoke(actions, args)` — for builtins that just toggle UI state
|
|
7
|
+
* (most ignore `args`), or
|
|
7
8
|
* - run via `execute(args)` — for plugin-supplied commands that produce
|
|
8
9
|
* side-effects through the agent runtime.
|
|
9
10
|
*
|
|
@@ -12,7 +13,7 @@ import type { InputActions } from './useInputHandler';
|
|
|
12
13
|
export interface SlashCommand {
|
|
13
14
|
name: string;
|
|
14
15
|
description: string;
|
|
15
|
-
invoke?: (actions: InputActions) => void;
|
|
16
|
+
invoke?: (actions: InputActions, args: string) => void;
|
|
16
17
|
execute?: (args: string) => Promise<string | undefined>;
|
|
17
18
|
}
|
|
18
19
|
|
|
@@ -30,6 +31,11 @@ export const BUILTIN_COMMANDS: SlashCommand[] = [
|
|
|
30
31
|
description: 'Show the LLM context (system prompt, messages, tools) as plain text',
|
|
31
32
|
invoke: (a) => a.onShowContext?.(),
|
|
32
33
|
},
|
|
34
|
+
{
|
|
35
|
+
name: '/update',
|
|
36
|
+
description: 'Update mu and installed npm plugins. Args: "plugins" | "self" (default: all)',
|
|
37
|
+
invoke: (a, args) => a.onUpdate?.(args),
|
|
38
|
+
},
|
|
33
39
|
];
|
|
34
40
|
|
|
35
41
|
export function fromPluginCommand(command: PluginSlashCommand, context: CommandContext): SlashCommand {
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import type { InputInfoSegment } from 'mu-core';
|
|
2
2
|
import { useCallback, useMemo, useRef } from 'react';
|
|
3
3
|
import { useChatContext } from '../chat/ChatContext';
|
|
4
|
+
import type { InkUIService } from '../plugins/InkUIService';
|
|
5
|
+
import { runUpdateInTui, type UpdateScope } from '../update/runUpdateInTui';
|
|
4
6
|
import { dumpContext } from './dumpContext';
|
|
5
7
|
import type { InputBoxViewProps } from './InputBoxView';
|
|
6
8
|
import { useCommandExecutor } from './useCommandExecutor';
|
|
@@ -8,6 +10,14 @@ import { type InputActions, type MentionMode, useInputHandler } from './useInput
|
|
|
8
10
|
import { type MentionPickerState, useMentionPicker } from './useMentionPicker';
|
|
9
11
|
import { usePluginShortcuts } from './usePluginShortcuts';
|
|
10
12
|
|
|
13
|
+
function parseUpdateScope(args: string): UpdateScope | null {
|
|
14
|
+
const trimmed = args.trim().toLowerCase();
|
|
15
|
+
if (trimmed === '' || trimmed === 'all') return 'all';
|
|
16
|
+
if (trimmed === 'plugins') return 'plugins';
|
|
17
|
+
if (trimmed === 'self' || trimmed === 'mu') return 'self';
|
|
18
|
+
return null;
|
|
19
|
+
}
|
|
20
|
+
|
|
11
21
|
export interface InputBoxProps {
|
|
12
22
|
onSubmit: (text: string) => void;
|
|
13
23
|
onScrollUp?: () => void;
|
|
@@ -96,12 +106,13 @@ interface ActionDeps {
|
|
|
96
106
|
models: ReturnType<typeof useChatContext>['models'];
|
|
97
107
|
toggles: ReturnType<typeof useChatContext>['toggles'];
|
|
98
108
|
onShowContext: () => Promise<void>;
|
|
109
|
+
onUpdate: (args: string) => void;
|
|
99
110
|
onScrollUp?: () => void;
|
|
100
111
|
onScrollDown?: () => void;
|
|
101
112
|
}
|
|
102
113
|
|
|
103
114
|
function useInputActions(deps: ActionDeps): InputActions {
|
|
104
|
-
const { abort, attachment, session, models, toggles, onShowContext, onScrollUp, onScrollDown } = deps;
|
|
115
|
+
const { abort, attachment, session, models, toggles, onShowContext, onUpdate, onScrollUp, onScrollDown } = deps;
|
|
105
116
|
return useMemo<InputActions>(
|
|
106
117
|
() => ({
|
|
107
118
|
onCtrlC: abort.onCtrlC,
|
|
@@ -115,6 +126,7 @@ function useInputActions(deps: ActionDeps): InputActions {
|
|
|
115
126
|
onTogglePicker: toggles.onTogglePicker,
|
|
116
127
|
onToggleSessionPicker: toggles.onToggleSessionPicker,
|
|
117
128
|
onShowContext,
|
|
129
|
+
onUpdate,
|
|
118
130
|
onScrollUp,
|
|
119
131
|
onScrollDown,
|
|
120
132
|
modelCount: models.models.length,
|
|
@@ -130,12 +142,29 @@ function useInputActions(deps: ActionDeps): InputActions {
|
|
|
130
142
|
toggles.onTogglePicker,
|
|
131
143
|
toggles.onToggleSessionPicker,
|
|
132
144
|
onShowContext,
|
|
145
|
+
onUpdate,
|
|
133
146
|
onScrollUp,
|
|
134
147
|
onScrollDown,
|
|
135
148
|
],
|
|
136
149
|
);
|
|
137
150
|
}
|
|
138
151
|
|
|
152
|
+
function useOnUpdate(uiService: InkUIService | undefined) {
|
|
153
|
+
return useCallback(
|
|
154
|
+
(args: string) => {
|
|
155
|
+
if (!uiService) return;
|
|
156
|
+
const scope = parseUpdateScope(args);
|
|
157
|
+
if (!scope) {
|
|
158
|
+
uiService.notify('Usage: /update [plugins|self|all]', 'warning');
|
|
159
|
+
return;
|
|
160
|
+
}
|
|
161
|
+
uiService.notify(scope === 'all' ? 'Starting update…' : `Starting ${scope} update…`, 'info');
|
|
162
|
+
void runUpdateInTui(scope, uiService);
|
|
163
|
+
},
|
|
164
|
+
[uiService],
|
|
165
|
+
);
|
|
166
|
+
}
|
|
167
|
+
|
|
139
168
|
const EMPTY_SEGMENTS: InputInfoSegment[] = [];
|
|
140
169
|
|
|
141
170
|
export function useInputBox({
|
|
@@ -164,6 +193,8 @@ export function useInputBox({
|
|
|
164
193
|
}
|
|
165
194
|
}, [config, session.messages, registry, uiService]);
|
|
166
195
|
|
|
196
|
+
const onUpdate = useOnUpdate(uiService);
|
|
197
|
+
|
|
167
198
|
const actions = useInputActions({
|
|
168
199
|
abort,
|
|
169
200
|
attachment,
|
|
@@ -171,6 +202,7 @@ export function useInputBox({
|
|
|
171
202
|
models,
|
|
172
203
|
toggles,
|
|
173
204
|
onShowContext,
|
|
205
|
+
onUpdate,
|
|
174
206
|
onScrollUp,
|
|
175
207
|
onScrollDown,
|
|
176
208
|
});
|
package/src/tui/renderApp.tsx
CHANGED
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* TUI-friendly update runner — same semantics as `mu update` from the CLI,
|
|
3
|
+
* but never writes to stdout/stderr (Ink owns the terminal). Progress and
|
|
4
|
+
* results are surfaced as toasts via `uiService.notify`.
|
|
5
|
+
*
|
|
6
|
+
* Mirrors `cli/update.ts` but uses `child_process.execFile` with `stdio:
|
|
7
|
+
* 'pipe'` so subprocess output is buffered, not streamed to the TUI's tty.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import { execFile } from 'node:child_process';
|
|
11
|
+
import { realpathSync } from 'node:fs';
|
|
12
|
+
import { promisify } from 'node:util';
|
|
13
|
+
import { ensureDataDir } from '../../cli/install';
|
|
14
|
+
import { invalidateUpdateCheckCache } from '../../runtime/startupUpdateCheck';
|
|
15
|
+
import { listConfiguredNpmPlugins, PACKAGE_NAME } from '../../runtime/updateCheck';
|
|
16
|
+
import type { InkUIService } from '../plugins/InkUIService';
|
|
17
|
+
|
|
18
|
+
const execFileAsync = promisify(execFile);
|
|
19
|
+
|
|
20
|
+
export type UpdateScope = 'all' | 'plugins' | 'self';
|
|
21
|
+
|
|
22
|
+
interface SelfInstallStrategy {
|
|
23
|
+
manager: 'bun' | 'npm' | 'pnpm' | 'yarn' | 'unknown';
|
|
24
|
+
command?: [string, string[]];
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function detectSelfInstall(): SelfInstallStrategy {
|
|
28
|
+
let bin = process.argv[1] ?? '';
|
|
29
|
+
try {
|
|
30
|
+
bin = realpathSync(bin);
|
|
31
|
+
} catch {
|
|
32
|
+
// keep raw argv
|
|
33
|
+
}
|
|
34
|
+
const norm = bin.replace(/\\/g, '/');
|
|
35
|
+
if (norm.includes('/.bun/') || norm.includes('/bun/install/')) {
|
|
36
|
+
return { manager: 'bun', command: ['bun', ['add', '-g', `${PACKAGE_NAME}@latest`]] };
|
|
37
|
+
}
|
|
38
|
+
if (norm.includes('/pnpm/')) {
|
|
39
|
+
return { manager: 'pnpm', command: ['pnpm', ['add', '-g', `${PACKAGE_NAME}@latest`]] };
|
|
40
|
+
}
|
|
41
|
+
if (norm.includes('/.yarn/') || norm.includes('/yarn/global/')) {
|
|
42
|
+
return { manager: 'yarn', command: ['yarn', ['global', 'add', `${PACKAGE_NAME}@latest`]] };
|
|
43
|
+
}
|
|
44
|
+
if (norm.includes('/npm/') || norm.includes('node_modules/.bin/mu')) {
|
|
45
|
+
return { manager: 'npm', command: ['npm', ['i', '-g', `${PACKAGE_NAME}@latest`]] };
|
|
46
|
+
}
|
|
47
|
+
return { manager: 'unknown' };
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
async function updatePlugin(name: string, dataDir: string): Promise<boolean> {
|
|
51
|
+
try {
|
|
52
|
+
await execFileAsync('bun', ['update', '--latest', name], { cwd: dataDir });
|
|
53
|
+
return true;
|
|
54
|
+
} catch {
|
|
55
|
+
return false;
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
async function updatePlugins(ui: InkUIService): Promise<{ ok: number; failed: number; total: number }> {
|
|
60
|
+
const dataDir = ensureDataDir();
|
|
61
|
+
const names = listConfiguredNpmPlugins();
|
|
62
|
+
if (names.length === 0) return { ok: 0, failed: 0, total: 0 };
|
|
63
|
+
|
|
64
|
+
let ok = 0;
|
|
65
|
+
let failed = 0;
|
|
66
|
+
for (const name of names) {
|
|
67
|
+
ui.notify(`Updating ${name}…`, 'info');
|
|
68
|
+
if (await updatePlugin(name, dataDir)) ok += 1;
|
|
69
|
+
else {
|
|
70
|
+
failed += 1;
|
|
71
|
+
ui.notify(`Failed to update ${name}`, 'error');
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
return { ok, failed, total: names.length };
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
async function updateSelf(ui: InkUIService): Promise<boolean> {
|
|
78
|
+
const strategy = detectSelfInstall();
|
|
79
|
+
if (!strategy.command) {
|
|
80
|
+
ui.notify(`Cannot auto-detect mu's installer. Re-install manually: bun add -g ${PACKAGE_NAME}@latest`, 'warning');
|
|
81
|
+
return false;
|
|
82
|
+
}
|
|
83
|
+
const [bin, args] = strategy.command;
|
|
84
|
+
ui.notify(`Updating mu via ${strategy.manager}…`, 'info');
|
|
85
|
+
try {
|
|
86
|
+
await execFileAsync(bin, args);
|
|
87
|
+
ui.notify('mu updated. Restart your session to pick up the new version.', 'success');
|
|
88
|
+
return true;
|
|
89
|
+
} catch (err) {
|
|
90
|
+
const message = err instanceof Error ? err.message.split('\n')[0] : String(err);
|
|
91
|
+
ui.notify(`Failed to update mu: ${message}`, 'error');
|
|
92
|
+
return false;
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* Entry point used by the `/update` slash command. Fire-and-forget — the
|
|
98
|
+
* caller voids the returned promise. All progress / errors land in toasts.
|
|
99
|
+
*/
|
|
100
|
+
export async function runUpdateInTui(scope: UpdateScope, ui: InkUIService): Promise<void> {
|
|
101
|
+
if (scope === 'plugins') {
|
|
102
|
+
const { ok, failed, total } = await updatePlugins(ui);
|
|
103
|
+
if (total === 0) ui.notify('No npm plugins configured.', 'info');
|
|
104
|
+
else if (failed === 0) ui.notify(`Updated ${ok}/${total} plugin${total === 1 ? '' : 's'}.`, 'success');
|
|
105
|
+
else ui.notify(`Plugins: ${ok} updated, ${failed} failed.`, 'warning');
|
|
106
|
+
invalidateUpdateCheckCache();
|
|
107
|
+
return;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
if (scope === 'self') {
|
|
111
|
+
await updateSelf(ui);
|
|
112
|
+
invalidateUpdateCheckCache();
|
|
113
|
+
return;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
// 'all'
|
|
117
|
+
const plugins = await updatePlugins(ui);
|
|
118
|
+
const selfOk = await updateSelf(ui);
|
|
119
|
+
if (plugins.total === 0 && selfOk) {
|
|
120
|
+
// mu-only success already toasted; nothing to add
|
|
121
|
+
} else if (plugins.failed === 0 && selfOk) {
|
|
122
|
+
ui.notify('Update complete.', 'success');
|
|
123
|
+
} else {
|
|
124
|
+
ui.notify('Update finished with errors — see prior messages.', 'warning');
|
|
125
|
+
}
|
|
126
|
+
invalidateUpdateCheckCache();
|
|
127
|
+
}
|