magus-cli 7.6.0 → 7.7.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.
@@ -93,25 +93,6 @@ export async function applyStartupFixes(
93
93
  return result;
94
94
  }
95
95
 
96
- /** What {@link applyStartupFixes} did, for the message the TUI shows. */
97
- export function describeStartupFix(result: StartupFixResult): string {
98
- const lines: string[] = [];
99
- if (result.untracked.length > 0) {
100
- lines.push(
101
- "Stopped tracking (the removal is staged — commit it):",
102
- ...result.untracked.map((p) => ` ${p}`),
103
- ' git commit -m "Stop tracking generated Claude Code config"',
104
- );
105
- }
106
- if (result.appended.length > 0) {
107
- lines.push("Added to .gitignore:", ...result.appended.map((p) => ` ${p}`));
108
- }
109
- for (const f of result.failed) {
110
- lines.push(`Could not untrack ${f.path}: ${f.reason}`);
111
- }
112
- return lines.length > 0 ? lines.join("\n") : "Nothing needed changing.";
113
- }
114
-
115
96
  export async function applyAllSafeFixes(
116
97
  cwd: string,
117
98
  violations: Violation[],
@@ -59,6 +59,7 @@ import { readManifest, writeManifest } from "./manifest.js";
59
59
  import { validateModelsConfig } from "./models-core.js";
60
60
  import {
61
61
  type ActivateOptions,
62
+ type ActivationResult,
62
63
  activateProfile,
63
64
  activationNotes,
64
65
  releasedCommittedSkillNote,
@@ -588,6 +589,12 @@ export interface ReconcileResult {
588
589
  conflicts: string[];
589
590
  /** Everything worth printing, in order. */
590
591
  lines: string[];
592
+ /**
593
+ * The re-render, when profiles.json changed and the live config was
594
+ * generated again. `lines` already carries its CLI wording; this is for a
595
+ * surface that words it differently (the TUI's notices).
596
+ */
597
+ activation: ActivationResult | null;
591
598
  }
592
599
 
593
600
  function emptyResult(profile: string | null): ReconcileResult {
@@ -600,6 +607,7 @@ function emptyResult(profile: string | null): ReconcileResult {
600
607
  unreadable: [],
601
608
  conflicts: [],
602
609
  lines: [],
610
+ activation: null,
603
611
  };
604
612
  }
605
613
 
@@ -1529,6 +1537,7 @@ export async function reconcileActiveProfile(
1529
1537
  // command that ran it reports that once, itself, and repeating it here
1530
1538
  // printed the same warning three times in one `magus update`.
1531
1539
  notes.push(...activationNotes(activation, { tracked: false }));
1540
+ result.activation = activation;
1532
1541
  } catch (error) {
1533
1542
  result.problems.push(
1534
1543
  error instanceof Error ? error.message : String(error),
@@ -209,7 +209,8 @@ export function releasedCommittedSkillNote(folder: string): string {
209
209
 
210
210
  /**
211
211
  * Lines describing what activation did beyond the switch itself, phrased for
212
- * both the terminal and the TUI's outcome modal.
212
+ * the terminal. The TUI words its own, shorter set (`activationNotices` in
213
+ * ui/hooks/useProfileStartupModal.ts).
213
214
  *
214
215
  * `tracked: false` leaves the tracked-path notices out, for the CLI, which
215
216
  * prints them itself as a warning with the fix beneath it
@@ -69,7 +69,11 @@ import {
69
69
  findEnclosingManifest,
70
70
  readManifest,
71
71
  } from "./manifest.js";
72
- import type { ProfileChoice } from "./profile-activate.js";
72
+ import {
73
+ type ActivationResult,
74
+ type ProfileChoice,
75
+ activationNotes,
76
+ } from "./profile-activate.js";
73
77
  import { freeProfileId } from "./profile-adopt.js";
74
78
  import {
75
79
  activeProfile,
@@ -310,7 +314,10 @@ export async function oldFormatNeed(
310
314
  /** What the TUI shows at startup, beyond the question (if any). */
311
315
  export interface ProfileStartup {
312
316
  need: ProfileStartupNeed;
313
- /** One line per thing the gate did: absorbed edits, regeneration, repairs. */
317
+ /**
318
+ * One line per thing the gate did: absorbed edits, regeneration, repairs.
319
+ * Not what an activation reported — see `activations`.
320
+ */
314
321
  notices: string[];
315
322
  /**
316
323
  * One line per thing the gate REFUSED — the lines the CLI stops on. Kept
@@ -319,6 +326,14 @@ export interface ProfileStartup {
319
326
  * still names its profile, so only this says the update did not happen.
320
327
  */
321
328
  problems: string[];
329
+ /**
330
+ * The activations the gate ran (a re-render, a format update). Handed over
331
+ * as results rather than lines because the CLI's lines are the wrong words
332
+ * for the TUI: they name plugin.json and backups, and they say "will not
333
+ * load" in a line nothing marks as a warning. The TUI words them itself, in
334
+ * user terms and with a tone (`activationWarnings` in the startup hook).
335
+ */
336
+ activations: ActivationResult[];
322
337
  }
323
338
 
324
339
  /**
@@ -338,14 +353,24 @@ export async function prepareProfileStartup(
338
353
  ): Promise<ProfileStartup> {
339
354
  if (!opts.convert) {
340
355
  const format = await oldFormatNeed(projectPath);
341
- if (format !== null) return { need: format, notices: [], problems: [] };
356
+ if (format !== null) {
357
+ return { need: format, notices: [], problems: [], activations: [] };
358
+ }
342
359
  }
343
360
  const notices: string[] = [];
344
361
  const problems: string[] = [];
362
+ const activations: ActivationResult[] = [];
345
363
  try {
346
364
  const prep = await prepareProfileState(projectPath);
347
- notices.push(...prep.lines);
365
+ // The gate's lines, less the ones its activations contributed: those
366
+ // reach the TUI as `activations`, and passing both would say the same
367
+ // thing twice — once in the CLI's words, as if it were a success.
368
+ const fromActivations = new Set(
369
+ prep.activations.flatMap((a) => activationNotes(a, { tracked: false })),
370
+ );
371
+ notices.push(...prep.lines.filter((line) => !fromActivations.has(line)));
348
372
  problems.push(...prep.problems);
373
+ activations.push(...prep.activations);
349
374
  } catch (error) {
350
375
  if (!(error instanceof SettingsParseError)) {
351
376
  problems.push(error instanceof Error ? error.message : String(error));
@@ -355,13 +380,17 @@ export async function prepareProfileStartup(
355
380
  need: await detectProfileStartupNeed(projectPath),
356
381
  notices,
357
382
  problems,
383
+ activations,
358
384
  };
359
385
  }
360
386
 
361
- /** What the TUI shows once "Update it now" has run. */
387
+ /**
388
+ * What the TUI shows once "Update it now" has run: another question, a notice
389
+ * line, or an error modal.
390
+ */
362
391
  export type FormatUpdateOutcome =
363
392
  | { kind: "ask"; need: ProfileStartupNeed }
364
- | { kind: "updated"; title: string; message: string }
393
+ | { kind: "updated"; notice: string }
365
394
  | { kind: "failed"; title: string; message: string };
366
395
 
367
396
  /**
@@ -381,14 +410,12 @@ export function formatUpdateOutcome(
381
410
  };
382
411
  }
383
412
  if (!needsNothing(result.need)) return { kind: "ask", need: result.need };
384
- return {
385
- kind: "updated",
386
- title: "Profile format updated",
387
- message:
388
- result.notices.length > 0
389
- ? result.notices.join("\n")
390
- : "This project now uses the current profile format.",
391
- };
413
+ // One line, not the gate's notices: those are the conversion's working log
414
+ // (edits carried over, the old format's backup), and the result the user
415
+ // cares about is only that the update is done. What the conversion's
416
+ // activation warns about is NOT dropped: the caller words it from
417
+ // `result.activations`, as it does for any activation.
418
+ return { kind: "updated", notice: "Profile format updated" };
392
419
  }
393
420
 
394
421
  // ─── What to offer, as data both surfaces render ─────────────────────────────
@@ -426,9 +453,10 @@ export const UPDATE_FORMAT_VALUE = "update-format";
426
453
  * Neither mentions the backup, deliberately. A sentence promising "nothing is
427
454
  * deleted — the old files are moved to a backup" raises a question the reader
428
455
  * did not have (which files? what was at risk?) and implies data loss, when what
429
- * changes is settings. Where the displaced files went belongs in the outcome
430
- * modal AFTER the switch, where it is something to act on rather than a worry to
431
- * plant beforehand.
456
+ * changes is settings. The TUI does not name the backup afterwards either: the
457
+ * backup is magus's own storage, and the user acts on nothing there. Only a
458
+ * failed activation lists what was moved, because then it is something to act
459
+ * on (see `describeFailure` in ui/hooks/useProfileStartupModal.ts).
432
460
  *
433
461
  * No file paths in these strings, deliberately. The TUI modal soft-wraps its
434
462
  * message at whatever width the terminal gives it, and OpenTUI treats `.` as a
@@ -299,6 +299,12 @@ export interface ProfileStatePrep {
299
299
  problems: string[];
300
300
  /** The files behind `problems` that could not be parsed. */
301
301
  unreadable: SettingsParseError[];
302
+ /**
303
+ * Every activation the gate ran, in order: a re-render after profiles.json
304
+ * changed, an older layout converted. `lines` already carries them in the
305
+ * CLI's words; the TUI words them itself from these.
306
+ */
307
+ activations: ActivationResult[];
302
308
  }
303
309
 
304
310
  /**
@@ -319,11 +325,13 @@ export async function prepareProfileState(
319
325
  lines: [],
320
326
  problems: [],
321
327
  unreadable: [],
328
+ activations: [],
322
329
  };
323
330
  const take = (r: ReconcileResult) => {
324
331
  out.lines.push(...r.lines.filter((l) => !r.problems.includes(l)));
325
332
  out.problems.push(...r.problems);
326
333
  out.unreadable.push(...r.unreadable);
334
+ if (r.activation) out.activations.push(r.activation);
327
335
  };
328
336
 
329
337
  if (check.state === "active" && check.restored) {
@@ -358,6 +366,7 @@ export async function prepareProfileState(
358
366
  }
359
367
  // Tracked files are the command's to report, once (see profile-absorb).
360
368
  out.lines.push(...activationNotes(result, { tracked: false }));
369
+ out.activations.push(result);
361
370
  }
362
371
  out.check = { state: "active", profile, restored: false };
363
372
  }