@polderlabs/bizar 4.4.10 → 4.4.12

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.
@@ -0,0 +1,543 @@
1
+ // src/views/MiniMaxUsage.tsx — v4.5.0
2
+ //
3
+ // Token Plan usage tracking dashboard.
4
+ //
5
+ // Renders the user's current 5-hour rolling + weekly remaining quota
6
+ // per model, fetched from https://www.minimax.io/v1/token_plan/remains.
7
+ // The Subscription Key is set inline on this page (no separate Settings
8
+ // trip needed) — the value is written to settings.json under
9
+ // `minimax.apiKey` and read back on next load.
10
+
11
+ import { useCallback, useEffect, useState } from 'react';
12
+ import {
13
+ Coins,
14
+ RefreshCw,
15
+ AlertTriangle,
16
+ CheckCircle2,
17
+ KeyRound,
18
+ Send,
19
+ Loader2,
20
+ Calendar,
21
+ Activity,
22
+ Save,
23
+ Eye,
24
+ EyeOff,
25
+ } from 'lucide-react';
26
+ import { Card, CardTitle, CardMeta } from '../components/Card';
27
+ import { Button } from '../components/Button';
28
+ import { Spinner } from '../components/Spinner';
29
+ import { useToast } from '../components/Toast';
30
+ import { api } from '../lib/api';
31
+ import { cn } from '../lib/utils';
32
+ import type { Settings, Snapshot } from '../lib/types';
33
+
34
+ type Props = {
35
+ snapshot: Snapshot;
36
+ settings: Settings;
37
+ activeTab: string;
38
+ setActiveTab: (id: string) => void;
39
+ refreshSnapshot: () => Promise<void>;
40
+ };
41
+
42
+ type RemainsModel = {
43
+ model_name: string;
44
+ current_interval_remaining_percent: number;
45
+ current_weekly_remaining_percent: number;
46
+ current_interval_consumed_percent?: number;
47
+ weekly_consumed_percent?: number;
48
+ current_interval_status: number;
49
+ current_weekly_status: number;
50
+ current_interval_total_count: number;
51
+ current_interval_usage_count: number;
52
+ current_weekly_total_count: number;
53
+ current_weekly_usage_count: number;
54
+ endTimeISO?: string;
55
+ weeklyEndTimeISO?: string;
56
+ intervalResetInMs?: number;
57
+ weeklyResetInMs?: number;
58
+ intervalResetInHuman?: string;
59
+ weeklyResetInHuman?: string;
60
+ intervalUsed?: number;
61
+ intervalTotal?: number;
62
+ weeklyUsed?: number;
63
+ weeklyTotal?: number;
64
+ };
65
+
66
+ type RemainsSnapshot = {
67
+ ok: boolean;
68
+ cached?: boolean;
69
+ fetchedAt?: number;
70
+ apiKeyHint?: string;
71
+ groupId?: string;
72
+ baseUrl?: string;
73
+ models?: RemainsModel[];
74
+ baseResp?: { status_code: number; status_msg: string };
75
+ error?: string;
76
+ message?: string;
77
+ };
78
+
79
+ type Status = {
80
+ enabled: boolean;
81
+ configured: boolean;
82
+ apiKeyHint: string;
83
+ groupId: string;
84
+ baseUrl: string;
85
+ chatBaseUrl: string;
86
+ cache: { fetchedAt: number; apiKeyHint: string; modelCount: number } | null;
87
+ };
88
+
89
+ type TestResult = {
90
+ ok: boolean;
91
+ error?: string;
92
+ message?: string;
93
+ model?: string;
94
+ content?: string;
95
+ reasoning?: string;
96
+ finishReason?: string;
97
+ usage?: {
98
+ total_tokens?: number;
99
+ prompt_tokens?: number;
100
+ completion_tokens?: number;
101
+ prompt_tokens_details?: { cached_tokens?: number };
102
+ };
103
+ status?: number;
104
+ raw?: unknown;
105
+ };
106
+
107
+ export function MiniMaxUsage({ snapshot, settings, setActiveTab, refreshSnapshot }: Props) {
108
+ const toast = useToast();
109
+ const [status, setStatus] = useState<Status | null>(null);
110
+ const [remains, setRemains] = useState<RemainsSnapshot | null>(null);
111
+ const [loading, setLoading] = useState(true);
112
+ const [refreshing, setRefreshing] = useState(false);
113
+ const [error, setError] = useState<string | null>(null);
114
+ const [testResult, setTestResult] = useState<TestResult | null>(null);
115
+ const [testing, setTesting] = useState(false);
116
+ // Inline key config — saves a trip to Settings.
117
+ const [keyDraft, setKeyDraft] = useState('');
118
+ const [showKey, setShowKey] = useState(false);
119
+ const [savingKey, setSavingKey] = useState(false);
120
+
121
+ const load = useCallback(async () => {
122
+ setLoading(true);
123
+ setError(null);
124
+ try {
125
+ const [s, r] = await Promise.all([
126
+ api.get<Status>('/minimax/status'),
127
+ api.get<RemainsSnapshot>('/minimax/remains'),
128
+ ]);
129
+ setStatus(s);
130
+ setRemains(r);
131
+ setKeyDraft(s.configured ? '' : ''); // never prefill — security
132
+ } catch (err) {
133
+ setError((err as Error).message || 'Failed to load MiniMax data');
134
+ } finally {
135
+ setLoading(false);
136
+ }
137
+ }, []);
138
+
139
+ useEffect(() => { load(); }, [load]);
140
+
141
+ const saveKey = useCallback(async () => {
142
+ if (!keyDraft.trim()) {
143
+ toast.error('Paste a Subscription Key first.');
144
+ return;
145
+ }
146
+ setSavingKey(true);
147
+ try {
148
+ const r = await api.put<{ minimax: { apiKey: string } }>('/settings', {
149
+ minimax: {
150
+ ...(settings.minimax || {}),
151
+ apiKey: keyDraft.trim(),
152
+ },
153
+ });
154
+ // Wipe the in-memory draft + clear the remains cache so the
155
+ // next load picks up the new key.
156
+ setKeyDraft('');
157
+ clearRemainsCacheClient();
158
+ toast.success('Subscription Key saved. Refreshing…');
159
+ void load();
160
+ } catch (err) {
161
+ toast.error(`Save failed: ${(err as Error).message}`);
162
+ } finally {
163
+ setSavingKey(false);
164
+ }
165
+ }, [keyDraft, settings, toast, load]);
166
+
167
+ // Clears the on-disk + in-memory cache via the dashboard's
168
+ // /api/minimax/cache DELETE endpoint. Wrapped in a helper so the
169
+ // success path can await the round-trip.
170
+ const clearRemainsCacheClient = useCallback(async () => {
171
+ try { await api.del('/minimax/cache'); } catch { /* best-effort */ }
172
+ }, []);
173
+
174
+ const refresh = useCallback(async () => {
175
+ setRefreshing(true);
176
+ setError(null);
177
+ try {
178
+ await api.post('/minimax/remains/refresh');
179
+ await load();
180
+ toast.success('Refreshed');
181
+ } catch (err) {
182
+ toast.error(`Refresh failed: ${(err as Error).message}`);
183
+ } finally {
184
+ setRefreshing(false);
185
+ }
186
+ }, [load, toast]);
187
+
188
+ const sendTest = useCallback(async () => {
189
+ setTesting(true);
190
+ setTestResult(null);
191
+ try {
192
+ const r = await api.post<TestResult>('/minimax/test', {
193
+ prompt: 'Reply with a single word: pong',
194
+ model: 'MiniMax-M3',
195
+ maxTokens: 16,
196
+ });
197
+ setTestResult(r);
198
+ if (r.ok) {
199
+ toast.success(`Test ok — used ${r.usage?.total_tokens ?? '?'} tokens`);
200
+ } else {
201
+ toast.error(`Test failed: ${r.message ?? r.error ?? 'unknown'}`);
202
+ }
203
+ } catch (err) {
204
+ toast.error(`Test request failed: ${(err as Error).message}`);
205
+ } finally {
206
+ setTesting(false);
207
+ }
208
+ }, [toast]);
209
+
210
+ if (loading && !status) {
211
+ return (
212
+ <div className="view-container">
213
+ <Spinner /> Loading MiniMax data…
214
+ </div>
215
+ );
216
+ }
217
+
218
+ if (!status) {
219
+ return (
220
+ <div className="view-container">
221
+ <div className="error-card">
222
+ <AlertTriangle size={20} />
223
+ <div>
224
+ <strong>Couldn't load MiniMax status</strong>
225
+ <pre>{error ?? 'unknown error'}</pre>
226
+ </div>
227
+ </div>
228
+ </div>
229
+ );
230
+ }
231
+
232
+ return (
233
+ <div className="view-container view-minimax-usage">
234
+ {/* ── Header ─────────────────────────────────────────────────── */}
235
+ <header className="view-header">
236
+ <div className="view-header-titles">
237
+ <h1 className="view-title">
238
+ <Coins size={20} style={{ verticalAlign: 'text-bottom', marginRight: 6 }} />
239
+ MiniMax Token Plan
240
+ </h1>
241
+ <p className="view-subtitle">
242
+ Remaining 5-hour + weekly quota per model. Fetches live from{' '}
243
+ <code>www.minimax.io/v1/token_plan/remains</code>.
244
+ </p>
245
+ </div>
246
+ <div className="view-header-actions">
247
+ <Button
248
+ variant="secondary"
249
+ size="sm"
250
+ onClick={() => setActiveTab('settings')}
251
+ title="Configure your Subscription Key"
252
+ >
253
+ <KeyRound size={14} /> {status.configured ? `Key ${status.apiKeyHint}` : 'Add key'}
254
+ </Button>
255
+ <Button variant="secondary" size="sm" onClick={refresh} disabled={refreshing}>
256
+ {refreshing ? <Loader2 size={14} className="spin" /> : <RefreshCw size={14} />}
257
+ Refresh
258
+ </Button>
259
+ </div>
260
+ </header>
261
+
262
+ {/* ── Status banners ─────────────────────────────────────────── */}
263
+ {!status.enabled && (
264
+ <div className="banner banner-warn">
265
+ <AlertTriangle size={16} />
266
+ <span>
267
+ MiniMax integration is <strong>disabled</strong> in settings. Enable it in Settings → MiniMax.
268
+ </span>
269
+ </div>
270
+ )}
271
+
272
+ {/* ── Subscription Key config (inline) ──────────────────── */}
273
+ {status.enabled && (
274
+ <Card id="minimax-key">
275
+ <CardTitle>
276
+ <KeyRound size={14} /> Subscription Key
277
+ </CardTitle>
278
+ <CardMeta>
279
+ Get it from{' '}
280
+ <a
281
+ href="https://platform.minimax.io/user-center/payment/token-plan"
282
+ target="_blank"
283
+ rel="noreferrer"
284
+ className="minimax-link"
285
+ >
286
+ platform.minimax.io/user-center/payment/token-plan
287
+ </a>
288
+ . Stored locally in <code>~/.config/bizar/settings.json</code>;
289
+ never sent to any other host. Status:{' '}
290
+ {status.configured ? (
291
+ <strong className="is-ok">configured ({status.apiKeyHint})</strong>
292
+ ) : (
293
+ <strong className="is-warn">not configured</strong>
294
+ )}
295
+ .
296
+ </CardMeta>
297
+ <div className="minimax-key-row">
298
+ <div className="minimax-key-input-wrap">
299
+ <KeyRound size={12} className="minimax-key-icon" />
300
+ <input
301
+ type={showKey ? 'text' : 'password'}
302
+ value={keyDraft}
303
+ onChange={(e) => setKeyDraft(e.target.value)}
304
+ placeholder={status.configured ? '•••• paste a new key to replace' : 'eyJhbGciOi...'}
305
+ spellCheck={false}
306
+ autoComplete="off"
307
+ className="minimax-key-input"
308
+ />
309
+ <button
310
+ type="button"
311
+ onClick={() => setShowKey((v) => !v)}
312
+ className="minimax-key-toggle"
313
+ aria-label={showKey ? 'Hide key' : 'Show key'}
314
+ title={showKey ? 'Hide key' : 'Show key'}
315
+ >
316
+ {showKey ? <EyeOff size={13} /> : <Eye size={13} />}
317
+ </button>
318
+ </div>
319
+ <Button onClick={saveKey} disabled={savingKey || !keyDraft.trim()} size="sm">
320
+ {savingKey ? <Loader2 size={14} className="spin" /> : <Save size={14} />}
321
+ Save key
322
+ </Button>
323
+ </div>
324
+ </Card>
325
+ )}
326
+
327
+ {remains && !remains.ok && (
328
+ <div className="banner banner-err">
329
+ <AlertTriangle size={16} />
330
+ <span>
331
+ <strong>Couldn't load quota</strong>: {remains.message ?? remains.error ?? 'unknown error'}
332
+ </span>
333
+ </div>
334
+ )}
335
+
336
+ {/* ── Summary stats ─────────────────────────────────────────── */}
337
+ {remains?.ok && remains.models && (
338
+ <>
339
+ <div className="minimax-stats-row">
340
+ <Stat
341
+ label="Models tracked"
342
+ value={String(remains.models.length)}
343
+ hint="Distinct model quotas returned by the API"
344
+ />
345
+ <Stat
346
+ label="Last fetched"
347
+ value={remains.fetchedAt ? relativeTime(remains.fetchedAt) : '—'}
348
+ hint="Refreshed on demand + every 60s"
349
+ />
350
+ <Stat
351
+ label="Group"
352
+ value={remains.groupId ?? '—'}
353
+ hint="Usually 'default' for an individual team"
354
+ />
355
+ <Stat
356
+ label="API key"
357
+ value={remains.apiKeyHint ?? '—'}
358
+ hint="Masked; the real key never leaves settings.json"
359
+ />
360
+ </div>
361
+
362
+ {/* ── Per-model quota cards ──────────────────────────────── */}
363
+ <div className="minimax-models-grid">
364
+ {remains.models.map((m) => (
365
+ <ModelQuotaCard key={m.model_name} model={m} />
366
+ ))}
367
+ </div>
368
+ </>
369
+ )}
370
+
371
+ {/* ── Test prompt card ─────────────────────────────────────── */}
372
+ {status.configured && (
373
+ <Card id="minimax-test">
374
+ <CardTitle>
375
+ <Send size={14} /> Test the API key
376
+ </CardTitle>
377
+ <CardMeta>
378
+ Sends a one-shot chat completion to{' '}
379
+ <code>{status.chatBaseUrl}/chat/completions</code> to verify the
380
+ key works and surface live token usage.
381
+ </CardMeta>
382
+ <div className="minimax-test-actions">
383
+ <Button onClick={sendTest} disabled={testing} variant="primary" size="sm">
384
+ {testing ? <Loader2 size={14} className="spin" /> : <Send size={14} />}
385
+ Send test prompt
386
+ </Button>
387
+ <code className="minimax-test-prompt">"Reply with a single word: pong"</code>
388
+ </div>
389
+ {testResult && (
390
+ <div className={cn('minimax-test-result', testResult.ok ? 'ok' : 'err')}>
391
+ {testResult.ok ? <CheckCircle2 size={14} /> : <AlertTriangle size={14} />}
392
+ <div className="minimax-test-body">
393
+ {testResult.ok ? (
394
+ <>
395
+ <div className="minimax-test-line">
396
+ <strong>model:</strong> <code>{testResult.model}</code> ·{' '}
397
+ <strong>finish:</strong> {testResult.finishReason ?? '—'}
398
+ </div>
399
+ {testResult.content && (
400
+ <div className="minimax-test-content">
401
+ <code>{testResult.content}</code>
402
+ </div>
403
+ )}
404
+ {testResult.usage && (
405
+ <div className="minimax-test-usage">
406
+ <strong>usage:</strong> total{' '}
407
+ <code>{testResult.usage.total_tokens ?? '?'}</code> · prompt{' '}
408
+ <code>{testResult.usage.prompt_tokens ?? '?'}</code> · completion{' '}
409
+ <code>{testResult.usage.completion_tokens ?? '?'}</code>
410
+ {testResult.usage.prompt_tokens_details?.cached_tokens != null && (
411
+ <> · cached <code>{testResult.usage.prompt_tokens_details.cached_tokens}</code></>
412
+ )}
413
+ </div>
414
+ )}
415
+ </>
416
+ ) : (
417
+ <pre>{testResult.message ?? testResult.error ?? 'unknown error'}</pre>
418
+ )}
419
+ </div>
420
+ </div>
421
+ )}
422
+ </Card>
423
+ )}
424
+ </div>
425
+ );
426
+ }
427
+
428
+ // ─── Sub-components ────────────────────────────────────────────────────────
429
+
430
+ function Stat({ label, value, hint }: { label: string; value: string; hint?: string }) {
431
+ return (
432
+ <div className="stat">
433
+ <div className="stat-label">{label}</div>
434
+ <div className="stat-value">{value}</div>
435
+ {hint && <div className="stat-hint">{hint}</div>}
436
+ </div>
437
+ );
438
+ }
439
+
440
+ function ModelQuotaCard({ model }: { model: RemainsModel }) {
441
+ const fivePct = clamp(model.current_interval_remaining_percent ?? 0, 0, 100);
442
+ const weekPct = clamp(model.current_weekly_remaining_percent ?? 0, 0, 100);
443
+ const fiveConsumed = clamp(100 - fivePct, 0, 100);
444
+ const weekConsumed = clamp(100 - weekPct, 0, 100);
445
+ return (
446
+ <Card id={`minimax-model-${model.model_name}`} className="minimax-model-card">
447
+ <div className="minimax-model-head">
448
+ <div className="minimax-model-name">{model.model_name}</div>
449
+ <div
450
+ className={cn(
451
+ 'minimax-model-status',
452
+ fivePct < 25 || weekPct < 25 ? 'is-warn' : 'is-ok'
453
+ )}
454
+ >
455
+ {model.current_interval_status === 1 ? 'active' : 'idle'} ·{' '}
456
+ {model.current_weekly_status === 1 ? 'week active' : 'week idle'}
457
+ </div>
458
+ </div>
459
+
460
+ <div className="minimax-quota-rows">
461
+ <QuotaBar
462
+ icon={<Activity size={12} />}
463
+ label="5-hour rolling"
464
+ remainingPct={fivePct}
465
+ consumedPct={fiveConsumed}
466
+ used={model.current_interval_usage_count ?? 0}
467
+ total={model.current_interval_total_count ?? 0}
468
+ resetIn={model.intervalResetInHuman}
469
+ resetISO={model.endTimeISO}
470
+ />
471
+ <QuotaBar
472
+ icon={<Calendar size={12} />}
473
+ label="Weekly"
474
+ remainingPct={weekPct}
475
+ consumedPct={weekConsumed}
476
+ used={model.current_weekly_usage_count ?? 0}
477
+ total={model.current_weekly_total_count ?? 0}
478
+ resetIn={model.weeklyResetInHuman}
479
+ resetISO={model.weeklyEndTimeISO}
480
+ />
481
+ </div>
482
+ </Card>
483
+ );
484
+ }
485
+
486
+ function QuotaBar({
487
+ icon,
488
+ label,
489
+ remainingPct,
490
+ consumedPct,
491
+ used,
492
+ total,
493
+ resetIn,
494
+ resetISO,
495
+ }: {
496
+ icon: React.ReactNode;
497
+ label: string;
498
+ remainingPct: number;
499
+ consumedPct: number;
500
+ used: number;
501
+ total: number;
502
+ resetIn?: string;
503
+ resetISO?: string;
504
+ }) {
505
+ const tone = remainingPct >= 75 ? 'good' : remainingPct >= 25 ? 'warn' : 'low';
506
+ return (
507
+ <div className={`minimax-quota-row is-${tone}`}>
508
+ <div className="minimax-quota-head">
509
+ <span className="minimax-quota-label">
510
+ {icon} {label}
511
+ </span>
512
+ <span className="minimax-quota-remaining">{remainingPct}% remaining</span>
513
+ </div>
514
+ <div className="minimax-bar">
515
+ <div
516
+ className="minimax-bar-fill"
517
+ style={{ width: `${consumedPct}%` }}
518
+ aria-label={`${consumedPct}% consumed`}
519
+ />
520
+ </div>
521
+ <div className="minimax-quota-meta">
522
+ <span>
523
+ <strong>{used}</strong> / {total || '—'} requests
524
+ </span>
525
+ <span title={resetISO}>
526
+ {resetIn ? `resets in ${resetIn}` : '—'}
527
+ </span>
528
+ </div>
529
+ </div>
530
+ );
531
+ }
532
+
533
+ function clamp(n: number, lo: number, hi: number) {
534
+ return Math.max(lo, Math.min(hi, n));
535
+ }
536
+
537
+ function relativeTime(ts: number): string {
538
+ const ms = Date.now() - ts;
539
+ if (ms < 5_000) return 'just now';
540
+ if (ms < 60_000) return `${Math.floor(ms / 1000)}s ago`;
541
+ if (ms < 3_600_000) return `${Math.floor(ms / 60_000)}m ago`;
542
+ return `${Math.floor(ms / 3_600_000)}h ago`;
543
+ }
package/cli/bin.mjs CHANGED
@@ -156,10 +156,11 @@ function showInstallHelp() {
156
156
  bizar install — Run the unified BizarHarness installer
157
157
 
158
158
  Usage:
159
- bizar install Install (or refresh) every component
160
- bizar install --dry-run Print what would happen, change nothing
161
- bizar install --force Overwrite existing files
162
- bizar install --help Show this help
159
+ bizar install Install (or refresh) every component
160
+ bizar install --dry-run Print what would happen, change nothing
161
+ bizar install --force Overwrite existing files
162
+ bizar install --with-mods a,b,c Opt-in: install specific mods as part of the run
163
+ bizar install --help Show this help
163
164
 
164
165
  Description:
165
166
  v4.4.7+ — unified installer. Same code path as 'bizar update'; the
@@ -194,6 +195,7 @@ function showUpdateHelp() {
194
195
  bizar update --dry-run Print what would happen, change nothing
195
196
  bizar update --force Override .bizar/PRE_PUSH_NOTES.md blockers
196
197
  bizar update --yes Same as --force, but named for one-line scripts
198
+ bizar update --with-mods a,b,c Opt-in: install specific mods as part of the run
197
199
  bizar update --help Show this help
198
200
 
199
201
  Components updated:
@@ -580,6 +582,22 @@ function parseFlag(name) {
580
582
  return args[idx + 1] || null;
581
583
  }
582
584
 
585
+ /**
586
+ * v4.4.11 — Parse `--with-mods <csv>` from the given subargs slice.
587
+ * Returns `null` if the flag isn't present (the provisioner's
588
+ * "don't touch mods" default), or a string[] of mod ids if it is.
589
+ */
590
+ function parseWithModsFlag(subargs) {
591
+ const idx = subargs.indexOf('--with-mods');
592
+ if (idx === -1) return null;
593
+ const raw = subargs[idx + 1];
594
+ if (!raw || raw.startsWith('--')) return [];
595
+ return raw
596
+ .split(',')
597
+ .map((s) => s.trim())
598
+ .filter(Boolean);
599
+ }
600
+
583
601
  async function readAutoLaunchWeb() {
584
602
  try {
585
603
  const fs = await import('node:fs');
@@ -677,7 +695,19 @@ async function main() {
677
695
  else await runTestGate();
678
696
  } else if (args[0] === 'update') {
679
697
  if (isHelpRequest) showUpdateHelp();
680
- else await runUpdate(args.slice(1));
698
+ else {
699
+ // v4.4.11 — Same --with-mods opt-in for update.
700
+ const withMods = parseWithModsFlag(args.slice(1));
701
+ // runUpdate expects (subargs: string[], opts?: object). Splice
702
+ // --with-mods <csv> out of subargs since the provisioner now
703
+ // takes it via opts, not as a positional arg.
704
+ const subargs = args.slice(1).filter((a, i, arr) => {
705
+ if (a === '--with-mods') return false;
706
+ if (arr[i - 1] === '--with-mods') return false;
707
+ return true;
708
+ });
709
+ await runUpdate(subargs, { withMods });
710
+ }
681
711
  } else if (args[0] === 'dev-link') {
682
712
  if (isHelpRequest) showDevLinkHelp();
683
713
  else {
@@ -741,7 +771,10 @@ async function main() {
741
771
  } else if (args[0] === 'install') {
742
772
  if (isHelpRequest) showInstallHelp();
743
773
  else {
744
- await runInstaller();
774
+ // v4.4.11 — Parse --with-mods <csv> to opt into mod installs
775
+ // during the run. Default: mods are NEVER touched.
776
+ const withMods = parseWithModsFlag(args.slice(1));
777
+ await runInstaller({ withMods });
745
778
  // v4.4.3 — After install, repair any stale bin symlinks so the
746
779
  // user picks up the new code (the installer itself may have
747
780
  // been running from a stale install path).