magus-cli 7.5.3 → 7.6.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/__tests__/blackbox-convergence.test.ts +1 -2
- package/src/__tests__/blackbox-parity.test.ts +16 -14
- package/src/__tests__/blackbox-plugin-state.test.ts +13 -15
- package/src/__tests__/cli-apply-seams.test.ts +3 -3
- package/src/__tests__/cli-profile-prompts.test.ts +1165 -0
- package/src/__tests__/content-drift.test.ts +0 -1
- package/src/__tests__/files-missing-scope.test.ts +235 -0
- package/src/__tests__/{enabled-not-installed.test.ts → files-missing.test.ts} +32 -26
- package/src/__tests__/format-update-outcome.test.ts +367 -0
- package/src/__tests__/installed-version-scope.test.tsx +425 -0
- package/src/__tests__/key-focus-guard.test.tsx +260 -0
- package/src/__tests__/mate-availability.test.ts +11 -13
- package/src/__tests__/plugin-contents.test.ts +0 -1
- package/src/__tests__/plugin-detail-scopes.test.tsx +270 -0
- package/src/__tests__/plugin-row-text.test.ts +18 -21
- package/src/__tests__/profile-command.test.ts +2 -2
- package/src/__tests__/profile-migration-offer.test.ts +5 -4
- package/src/__tests__/profile-name-on-save.test.ts +443 -0
- package/src/__tests__/profile-skills.test.ts +3 -3
- package/src/__tests__/profile-startup-nested.test.ts +329 -0
- package/src/__tests__/profile-startup-old-format.test.ts +390 -0
- package/src/__tests__/profile-startup.test.ts +1 -1
- package/src/__tests__/profile-state.test.ts +27 -22
- package/src/__tests__/prompt-eof.test.ts +245 -0
- package/src/__tests__/scope-action.test.ts +35 -38
- package/src/__tests__/scope-row-lookup.test.ts +126 -0
- package/src/__tests__/select-restore.test.ts +209 -0
- package/src/__tests__/stale-load-repair.test.ts +1 -2
- package/src/__tests__/update-apply.test.ts +18 -2
- package/src/__tests__/update-null-readback.test.ts +300 -0
- package/src/__tests__/update-plan-parity.test.ts +249 -0
- package/src/__tests__/update-plan.test.ts +66 -67
- package/src/__tests__/version-unknown-state.test.ts +38 -39
- package/src/cli/bootstrap.ts +271 -66
- package/src/cli/doctor.ts +4 -3
- package/src/cli/install.ts +83 -27
- package/src/cli/prompt.ts +19 -5
- package/src/cli/router.ts +1 -0
- package/src/cli/select.ts +168 -0
- package/src/cli/update-view.ts +120 -6
- package/src/cli/update.ts +6 -4
- package/src/services/claude-settings.ts +28 -0
- package/src/services/manifest.ts +31 -0
- package/src/services/mate-availability.ts +23 -29
- package/src/services/plugin-manager.ts +247 -105
- package/src/services/profile-absorb.ts +10 -7
- package/src/services/profile-activate.ts +41 -19
- package/src/services/profile-adopt.ts +9 -2
- package/src/services/profile-startup.ts +206 -5
- package/src/services/profile-state.ts +8 -7
- package/src/services/tracked-live-files.ts +24 -3
- package/src/services/update-engine.ts +28 -10
- package/src/services/update-plan.ts +78 -94
- package/src/ui/App.tsx +96 -56
- package/src/ui/adapters/pluginsAdapter.ts +1 -1
- package/src/ui/components/modals/ModalContainer.tsx +3 -0
- package/src/ui/components/primitives/ScopeSquares.tsx +10 -5
- package/src/ui/hooks/useGitignoreModal.ts +21 -3
- package/src/ui/hooks/useProfileStartupModal.ts +214 -15
- package/src/ui/hooks/useStaleLoadModal.ts +5 -1
- package/src/ui/input/KeyRouter.tsx +43 -2
- package/src/ui/keymap.ts +1 -1
- package/src/ui/renderers/pluginRenderers.tsx +39 -45
- package/src/ui/renderers/pluginRowText.ts +6 -2
- package/src/ui/renderers/pluginStatusText.ts +4 -3
- package/src/ui/screens/PluginsScreen.tsx +28 -41
- package/src/cli/tui-prompt.tsx +0 -143
package/src/cli/bootstrap.ts
CHANGED
|
@@ -18,6 +18,7 @@ import {
|
|
|
18
18
|
type ActivationResult,
|
|
19
19
|
activateProfile,
|
|
20
20
|
activationNotes,
|
|
21
|
+
activationTrackedNotices,
|
|
21
22
|
reportDisplaced,
|
|
22
23
|
} from "../services/profile-activate.js";
|
|
23
24
|
import {
|
|
@@ -25,12 +26,16 @@ import {
|
|
|
25
26
|
adoptCurrentProject,
|
|
26
27
|
} from "../services/profile-adopt.js";
|
|
27
28
|
import {
|
|
29
|
+
type ProfileStartupNeed,
|
|
30
|
+
UPDATE_FORMAT_VALUE,
|
|
28
31
|
buildProfileOptions,
|
|
29
32
|
defaultOptionIndex,
|
|
30
33
|
describeNeed,
|
|
31
34
|
detectProfileStartupNeed,
|
|
32
35
|
needsNothing,
|
|
36
|
+
oldFormatNeed,
|
|
33
37
|
parseProfileChoice,
|
|
38
|
+
profileIdForName,
|
|
34
39
|
} from "../services/profile-startup.js";
|
|
35
40
|
import {
|
|
36
41
|
applyProfileChoice,
|
|
@@ -41,10 +46,23 @@ import {
|
|
|
41
46
|
} from "../services/profile-state.js";
|
|
42
47
|
import { SettingsParseError } from "../services/settings-file.js";
|
|
43
48
|
import type { ProfileManifest } from "../types/index.js";
|
|
44
|
-
import {
|
|
45
|
-
import {
|
|
46
|
-
import {
|
|
47
|
-
import {
|
|
49
|
+
import { brand } from "../ui/theme.js";
|
|
50
|
+
import { badge, bold, dim } from "./ansi.js";
|
|
51
|
+
import { askYesNo, isPromptAborted, promptValue } from "./prompt.js";
|
|
52
|
+
import { selectOption } from "./select.js";
|
|
53
|
+
import {
|
|
54
|
+
type ScopeName,
|
|
55
|
+
fail as failLine,
|
|
56
|
+
field,
|
|
57
|
+
info as infoLine,
|
|
58
|
+
listItem,
|
|
59
|
+
noteBlock,
|
|
60
|
+
note as noteLine,
|
|
61
|
+
ok as okLine,
|
|
62
|
+
question,
|
|
63
|
+
scopeBadge,
|
|
64
|
+
warn as warnLine,
|
|
65
|
+
} from "./update-view.js";
|
|
48
66
|
|
|
49
67
|
/**
|
|
50
68
|
* Report a file that exists and cannot be used — it does not parse, or parses
|
|
@@ -76,6 +94,13 @@ export function reportUnparseable(error: SettingsParseError): void {
|
|
|
76
94
|
async function writeAndActivate(
|
|
77
95
|
result: AdoptResult,
|
|
78
96
|
projectPath: string,
|
|
97
|
+
/**
|
|
98
|
+
* Report a generated file git tracks. Only when adopting IS the command
|
|
99
|
+
* (`profile init`): `install` and `update` report it themselves right
|
|
100
|
+
* after — install's switch, update's own check — so adoption inside them
|
|
101
|
+
* said it twice in one run.
|
|
102
|
+
*/
|
|
103
|
+
reportTracked: boolean,
|
|
79
104
|
): Promise<void> {
|
|
80
105
|
// Relative to the project throughout. These are all project paths, and an
|
|
81
106
|
// absolute one wraps over two terminal lines — several wrapped lines in a row
|
|
@@ -84,7 +109,12 @@ async function writeAndActivate(
|
|
|
84
109
|
const rel = (p: string) => path.relative(projectPath, p);
|
|
85
110
|
|
|
86
111
|
await writeManifest(result.manifest, projectPath);
|
|
87
|
-
console.log(
|
|
112
|
+
console.log(okLine(`Saved profile ${bold(result.profileId)}`));
|
|
113
|
+
console.log(
|
|
114
|
+
noteLine(
|
|
115
|
+
`Commit ${rel(getManifestPath(projectPath))} so your team gets the same plugins.`,
|
|
116
|
+
),
|
|
117
|
+
);
|
|
88
118
|
|
|
89
119
|
// Each move is printed as it happens, not from the returned list afterwards.
|
|
90
120
|
// Activation writes three files in sequence, so a throw on the third would
|
|
@@ -96,14 +126,28 @@ async function writeAndActivate(
|
|
|
96
126
|
projectPath,
|
|
97
127
|
{ onDisplace: reportDisplaced(projectPath) },
|
|
98
128
|
);
|
|
99
|
-
console.log(
|
|
100
|
-
printActivationNotes(activation);
|
|
129
|
+
console.log(okLine(`Using profile ${bold(result.profileId)}`));
|
|
130
|
+
printActivationNotes(activation, { tracked: reportTracked });
|
|
101
131
|
console.log();
|
|
102
132
|
}
|
|
103
133
|
|
|
104
|
-
/**
|
|
105
|
-
|
|
106
|
-
|
|
134
|
+
/**
|
|
135
|
+
* Print what activation did beyond the switch itself: notes one line each, and
|
|
136
|
+
* — unless `tracked` is false — a tracked generated file as a warning with its
|
|
137
|
+
* fix beneath.
|
|
138
|
+
*/
|
|
139
|
+
export function printActivationNotes(
|
|
140
|
+
result: ActivationResult,
|
|
141
|
+
opts: { tracked?: boolean } = {},
|
|
142
|
+
): void {
|
|
143
|
+
for (const line of activationNotes(result, { tracked: false })) {
|
|
144
|
+
console.log(noteLine(line));
|
|
145
|
+
}
|
|
146
|
+
if (opts.tracked === false) return;
|
|
147
|
+
for (const n of activationTrackedNotices(result)) {
|
|
148
|
+
console.log(warnLine(n.problem));
|
|
149
|
+
for (const line of n.fix) console.log(noteLine(line));
|
|
150
|
+
}
|
|
107
151
|
}
|
|
108
152
|
|
|
109
153
|
export interface ProfileGateOptions {
|
|
@@ -113,6 +157,39 @@ export interface ProfileGateOptions {
|
|
|
113
157
|
* exists — `profile switch`, `install`.
|
|
114
158
|
*/
|
|
115
159
|
allowUnknown?: boolean;
|
|
160
|
+
/** `--yes`: update an old profile format without asking. */
|
|
161
|
+
yes?: boolean;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
/**
|
|
165
|
+
* An old magus profile format: the shared `old-format` state, stated with the
|
|
166
|
+
* TUI's own title and message and — with a person at the terminal and no
|
|
167
|
+
* `--yes` — asked with the TUI's own rows. Returns false when the user
|
|
168
|
+
* declines; the command then stops, since magus cannot manage a project in a
|
|
169
|
+
* format it no longer writes. With nobody to ask it updates, as before: CI and
|
|
170
|
+
* `--yes` must not hang on a question.
|
|
171
|
+
*/
|
|
172
|
+
async function confirmFormatUpdate(
|
|
173
|
+
projectPath: string,
|
|
174
|
+
opts: ProfileGateOptions,
|
|
175
|
+
): Promise<boolean> {
|
|
176
|
+
const need = await oldFormatNeed(projectPath);
|
|
177
|
+
if (need === null || need.state !== "old-format") return true;
|
|
178
|
+
const { title, message } = describeNeed(need);
|
|
179
|
+
console.log("");
|
|
180
|
+
console.log(warnLine(bold(title)));
|
|
181
|
+
for (const line of noteBlock(message, process.stdout.columns)) {
|
|
182
|
+
console.log(line);
|
|
183
|
+
}
|
|
184
|
+
console.log("");
|
|
185
|
+
if (opts.yes || !process.stdin.isTTY || !process.stdout.isTTY) return true;
|
|
186
|
+
const options = buildProfileOptions(need);
|
|
187
|
+
const picked = await selectOption("Update the profile format?", options, 0);
|
|
188
|
+
if (picked !== null && options[picked]?.value === UPDATE_FORMAT_VALUE) {
|
|
189
|
+
return true;
|
|
190
|
+
}
|
|
191
|
+
console.log(infoLine("Nothing changed."));
|
|
192
|
+
return false;
|
|
116
193
|
}
|
|
117
194
|
|
|
118
195
|
/**
|
|
@@ -129,6 +206,7 @@ export async function profileGate(
|
|
|
129
206
|
projectPath: string,
|
|
130
207
|
opts: ProfileGateOptions = {},
|
|
131
208
|
): Promise<boolean> {
|
|
209
|
+
if (!(await confirmFormatUpdate(projectPath, opts))) return false;
|
|
132
210
|
let prep: Awaited<ReturnType<typeof prepareProfileState>>;
|
|
133
211
|
try {
|
|
134
212
|
prep = await prepareProfileState(projectPath);
|
|
@@ -154,64 +232,128 @@ export async function profileGate(
|
|
|
154
232
|
return true;
|
|
155
233
|
}
|
|
156
234
|
|
|
157
|
-
/**
|
|
235
|
+
/** The plugin as a user reads it: its name, and its marketplace only when that is not magus's own. */
|
|
236
|
+
function pluginLabel(id: string): string {
|
|
237
|
+
const at = id.lastIndexOf("@");
|
|
238
|
+
if (at <= 0) return id;
|
|
239
|
+
const market = id.slice(at + 1);
|
|
240
|
+
return market === "magus"
|
|
241
|
+
? id.slice(0, at)
|
|
242
|
+
: `${id.slice(0, at)} ${dim(market)}`;
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
/** Longest list shown before it is cut to "+ N more". */
|
|
246
|
+
const LIST_MAX = 12;
|
|
247
|
+
|
|
248
|
+
/** Print up to LIST_MAX entries under a field, then how many were left out. */
|
|
249
|
+
function printList(entries: string[]): void {
|
|
250
|
+
for (const entry of entries.slice(0, LIST_MAX)) console.log(listItem(entry));
|
|
251
|
+
if (entries.length > LIST_MAX) {
|
|
252
|
+
console.log(listItem(dim(`+ ${entries.length - LIST_MAX} more`)));
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
/**
|
|
257
|
+
* Print the state and what adoption would capture, so the user can judge it
|
|
258
|
+
* before the question below.
|
|
259
|
+
*
|
|
260
|
+
* In the user's terms only: the state ("no profiles yet"), the plugins as a
|
|
261
|
+
* list with the SCOPE they come from, and anything that affects the team.
|
|
262
|
+
* Never where magus stores things — the owner, on the version that printed
|
|
263
|
+
* "from this folder's .claude/settings.json" and a relative path: users
|
|
264
|
+
* operate in scopes (user, project, local), not in files, and a mechanism
|
|
265
|
+
* they never chose is noise.
|
|
266
|
+
*/
|
|
158
267
|
function printAdoptionPlan(result: AdoptResult): void {
|
|
268
|
+
console.log("");
|
|
269
|
+
console.log(warnLine(bold("No profiles in this project yet")));
|
|
159
270
|
console.log(
|
|
160
|
-
|
|
271
|
+
noteLine(
|
|
272
|
+
"magus keeps a project's plugins in a profile: a named set your team gets",
|
|
273
|
+
),
|
|
161
274
|
);
|
|
162
|
-
console.log(` profile ${result.profileId} (${result.entry.name})`);
|
|
163
275
|
console.log(
|
|
164
|
-
|
|
276
|
+
noteLine(
|
|
277
|
+
"by committing it. Save what this project uses now as the first one.",
|
|
278
|
+
),
|
|
165
279
|
);
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
"
|
|
172
|
-
)
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
280
|
+
console.log("");
|
|
281
|
+
|
|
282
|
+
console.log(
|
|
283
|
+
field(
|
|
284
|
+
"profile",
|
|
285
|
+
`${bold(result.profileId)} ${badge("NEW", brand.success)}`,
|
|
286
|
+
),
|
|
287
|
+
);
|
|
288
|
+
const scope: ScopeName = result.source === "user" ? "user" : "project";
|
|
289
|
+
console.log(
|
|
290
|
+
field(
|
|
291
|
+
"plugins",
|
|
292
|
+
`${bold(String(result.counts.plugins))} ${scopeBadge(scope)}`,
|
|
293
|
+
),
|
|
294
|
+
);
|
|
295
|
+
printList(Object.keys(result.entry.plugins ?? {}).map(pluginLabel));
|
|
296
|
+
const servers = Object.keys(result.entry.mcpServers ?? {});
|
|
297
|
+
if (servers.length > 0) {
|
|
298
|
+
console.log(field("mcp servers", bold(String(servers.length))));
|
|
299
|
+
printList(servers);
|
|
179
300
|
}
|
|
180
|
-
|
|
301
|
+
console.log("");
|
|
302
|
+
|
|
303
|
+
const warned =
|
|
304
|
+
result.source === "user" ||
|
|
305
|
+
result.skipped.length > 0 ||
|
|
306
|
+
result.machinePaths.length > 0;
|
|
307
|
+
if (result.source === "user") {
|
|
308
|
+
// Never let this be invisible: the profile is meant to be committed, and
|
|
309
|
+
// these are one person's global set, not anything this project chose.
|
|
310
|
+
console.log(warnLine("This project has no plugins of its own"));
|
|
181
311
|
console.log(
|
|
182
|
-
|
|
312
|
+
noteLine(
|
|
313
|
+
"These are your USER plugins, on in every project. Check them before committing.",
|
|
314
|
+
),
|
|
183
315
|
);
|
|
184
316
|
}
|
|
185
317
|
for (const id of result.skipped) {
|
|
186
318
|
console.log(
|
|
187
|
-
|
|
319
|
+
warnLine(`${id} can't be shared — it isn't from a marketplace`),
|
|
188
320
|
);
|
|
189
321
|
}
|
|
190
322
|
for (const hit of result.machinePaths) {
|
|
191
|
-
console.log(` ⚠ machine path ${hit}`);
|
|
192
|
-
}
|
|
193
|
-
if (result.machinePaths.length > 0) {
|
|
194
323
|
console.log(
|
|
195
|
-
|
|
324
|
+
warnLine(`A setting points to a folder on this computer: ${hit}`),
|
|
196
325
|
);
|
|
197
326
|
}
|
|
198
|
-
|
|
199
|
-
"
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
327
|
+
if (result.machinePaths.length > 0) {
|
|
328
|
+
console.log(noteLine("Your teammates won't have it."));
|
|
329
|
+
}
|
|
330
|
+
if (warned) console.log("");
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
/**
|
|
334
|
+
* A state that stops the command, stated the way the TUI states it: the same
|
|
335
|
+
* title and message from `describeNeed`, dim — nothing is wrong, and there is
|
|
336
|
+
* nothing to choose from the CLI.
|
|
337
|
+
*/
|
|
338
|
+
function printNeedNote(need: ProfileStartupNeed): void {
|
|
339
|
+
const { title, message } = describeNeed(need);
|
|
340
|
+
console.log("");
|
|
341
|
+
console.log(noteLine(bold(title)));
|
|
342
|
+
for (const line of noteBlock(message, process.stdout.columns)) {
|
|
343
|
+
console.log(line);
|
|
344
|
+
}
|
|
345
|
+
console.log("");
|
|
210
346
|
}
|
|
211
347
|
|
|
212
348
|
export interface EnsureManifestOptions {
|
|
213
349
|
/** Read-only mode: never write, just report that adoption is needed. */
|
|
214
350
|
check?: boolean;
|
|
351
|
+
/**
|
|
352
|
+
* Report a tracked generated file after activating. False for a caller that
|
|
353
|
+
* already reported it this run (`update` checks before it asks), so it is
|
|
354
|
+
* said once.
|
|
355
|
+
*/
|
|
356
|
+
reportTracked?: boolean;
|
|
215
357
|
/**
|
|
216
358
|
* Adopt without prompting. Set only by `magus profile init`, where
|
|
217
359
|
* creating the manifest IS the command the user asked for.
|
|
@@ -241,14 +383,30 @@ export async function ensureManifest(
|
|
|
241
383
|
}
|
|
242
384
|
if (Object.keys(manifest.profiles).length > 0) return manifest;
|
|
243
385
|
|
|
386
|
+
// Inside a project that has profiles: the shared startup state says so, and
|
|
387
|
+
// the command stops — the TUI shows the same state as its startup question.
|
|
388
|
+
// Only `profile init` (`allowAdopt`), run on purpose, gives a subfolder a
|
|
389
|
+
// setup of its own.
|
|
390
|
+
if (!opts.allowAdopt) {
|
|
391
|
+
const need = await detectProfileStartupNeed(projectPath);
|
|
392
|
+
if (need.state === "nested") {
|
|
393
|
+
printNeedNote(need);
|
|
394
|
+
return null;
|
|
395
|
+
}
|
|
396
|
+
}
|
|
397
|
+
|
|
244
398
|
const result = await adoptCurrentProject(projectPath);
|
|
245
399
|
|
|
246
400
|
if (result.counts.plugins === 0) {
|
|
247
401
|
console.error(
|
|
248
|
-
|
|
402
|
+
failLine(
|
|
403
|
+
`No profile in ${path.basename(projectPath)}, and no plugins to save as one`,
|
|
404
|
+
),
|
|
249
405
|
);
|
|
250
406
|
console.error(
|
|
251
|
-
|
|
407
|
+
noteLine(
|
|
408
|
+
"Open magus, pick the plugins this folder needs, then run this again.",
|
|
409
|
+
),
|
|
252
410
|
);
|
|
253
411
|
return null;
|
|
254
412
|
}
|
|
@@ -257,8 +415,9 @@ export async function ensureManifest(
|
|
|
257
415
|
|
|
258
416
|
if (opts.check) {
|
|
259
417
|
console.error(
|
|
260
|
-
"
|
|
418
|
+
failLine("--check is read-only, so it does not create the profile"),
|
|
261
419
|
);
|
|
420
|
+
console.error(noteLine("Run `magus install` once to create it."));
|
|
262
421
|
return null;
|
|
263
422
|
}
|
|
264
423
|
|
|
@@ -272,18 +431,48 @@ export async function ensureManifest(
|
|
|
272
431
|
// fall through to the write
|
|
273
432
|
} else if (!process.stdin.isTTY) {
|
|
274
433
|
console.error(
|
|
275
|
-
"
|
|
434
|
+
failLine("No profile in this folder, and nobody here to ask"),
|
|
435
|
+
);
|
|
436
|
+
console.error(
|
|
437
|
+
noteLine("Run `magus profile init` to create one deliberately."),
|
|
276
438
|
);
|
|
277
439
|
return null;
|
|
278
|
-
} else if (
|
|
279
|
-
|
|
440
|
+
} else if (
|
|
441
|
+
!(await askYesNo(question(`Save as profile "${result.profileId}"?`)))
|
|
442
|
+
) {
|
|
443
|
+
console.log(infoLine("Nothing written."));
|
|
280
444
|
return null;
|
|
281
445
|
}
|
|
282
446
|
|
|
283
|
-
await writeAndActivate(result, projectPath);
|
|
447
|
+
await writeAndActivate(result, projectPath, opts.allowAdopt === true);
|
|
284
448
|
return result.manifest;
|
|
285
449
|
}
|
|
286
450
|
|
|
451
|
+
/**
|
|
452
|
+
* The name for a profile saved from what the project has, or null when the
|
|
453
|
+
* user gave none (Ctrl+C, or input ended). Empty accepts `suggestion`. An
|
|
454
|
+
* invalid name is explained and asked again — never swapped for another id.
|
|
455
|
+
*/
|
|
456
|
+
async function askProfileName(
|
|
457
|
+
suggestion: string,
|
|
458
|
+
manifest: ProfileManifest,
|
|
459
|
+
): Promise<{ id: string; name: string } | null> {
|
|
460
|
+
for (;;) {
|
|
461
|
+
let typed: string;
|
|
462
|
+
try {
|
|
463
|
+
typed = await promptValue(
|
|
464
|
+
question("Name for the new profile", `[${suggestion}]`),
|
|
465
|
+
);
|
|
466
|
+
} catch (error) {
|
|
467
|
+
if (isPromptAborted(error)) return null;
|
|
468
|
+
throw error;
|
|
469
|
+
}
|
|
470
|
+
const check = profileIdForName(typed === "" ? suggestion : typed, manifest);
|
|
471
|
+
if (check.ok) return { id: check.id, name: check.name };
|
|
472
|
+
console.log(warnLine(check.reason));
|
|
473
|
+
}
|
|
474
|
+
}
|
|
475
|
+
|
|
287
476
|
/**
|
|
288
477
|
* Require an active profile, or stop the command.
|
|
289
478
|
*
|
|
@@ -331,7 +520,7 @@ export async function requireActiveProfile(
|
|
|
331
520
|
console.log(
|
|
332
521
|
`✓ Active profile: ${result.profileId} ${dim("(declared by this project)")}`,
|
|
333
522
|
);
|
|
334
|
-
printActivationNotes(result);
|
|
523
|
+
printActivationNotes(result, { tracked: opts.reportTracked !== false });
|
|
335
524
|
console.log("");
|
|
336
525
|
return readManifest(projectPath);
|
|
337
526
|
}
|
|
@@ -347,7 +536,9 @@ export async function requireActiveProfile(
|
|
|
347
536
|
}
|
|
348
537
|
|
|
349
538
|
// CI has nobody to ask. Exiting beats hanging on an answer that cannot come.
|
|
350
|
-
|
|
539
|
+
// Both ends: with stdout piped the list cannot draw, and "No profile chosen"
|
|
540
|
+
// would claim an answer nobody was asked for.
|
|
541
|
+
if (!process.stdin.isTTY || !process.stdout.isTTY) {
|
|
351
542
|
console.error(failLine(title));
|
|
352
543
|
console.error(
|
|
353
544
|
noteLine(
|
|
@@ -362,27 +553,41 @@ export async function requireActiveProfile(
|
|
|
362
553
|
return null;
|
|
363
554
|
}
|
|
364
555
|
|
|
556
|
+
// The choices the TUI's startup modal offers, built by the same functions,
|
|
557
|
+
// as an arrow-key list in the report style — the CLI mounts no TUI.
|
|
558
|
+
const options = buildProfileOptions(need);
|
|
559
|
+
const def = defaultOptionIndex(need);
|
|
365
560
|
console.log("");
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
561
|
+
console.log(warnLine(bold(title)));
|
|
562
|
+
for (const line of noteBlock(
|
|
563
|
+
describeNeed(need).message,
|
|
564
|
+
process.stdout.columns,
|
|
565
|
+
)) {
|
|
566
|
+
console.log(line);
|
|
567
|
+
}
|
|
568
|
+
console.log("");
|
|
569
|
+
const picked = await selectOption("Choose a profile", options, def);
|
|
372
570
|
|
|
373
|
-
//
|
|
571
|
+
// "Quit magus", Esc and Ctrl+C all land here, and there is no third
|
|
374
572
|
// outcome: the command stops rather than proceed without a profile.
|
|
375
|
-
const
|
|
573
|
+
const value = picked === null ? undefined : options[picked]?.value;
|
|
574
|
+
let choice = value === undefined ? null : parseProfileChoice(value, need);
|
|
575
|
+
// Saving asks for the name, and an unanswered name is an unanswered
|
|
576
|
+
// question like any other: nothing is created and the command stops.
|
|
577
|
+
if (choice?.kind === "adopt") {
|
|
578
|
+
const named = await askProfileName(choice.id, need.manifest);
|
|
579
|
+
choice = named === null ? null : { kind: "adopt", ...named };
|
|
580
|
+
}
|
|
376
581
|
if (!choice) {
|
|
377
|
-
console.error(failLine("No
|
|
582
|
+
console.error(failLine("No profile chosen — stopping."));
|
|
378
583
|
return null;
|
|
379
584
|
}
|
|
380
585
|
|
|
381
586
|
const result = await applyProfileChoice(choice, need.manifest, projectPath, {
|
|
382
587
|
onDisplace: reportDisplaced(projectPath),
|
|
383
588
|
});
|
|
384
|
-
console.log(
|
|
385
|
-
printActivationNotes(result);
|
|
589
|
+
console.log(okLine(`Using profile ${bold(result.profileId)}`));
|
|
590
|
+
printActivationNotes(result, { tracked: opts.reportTracked !== false });
|
|
386
591
|
console.log("");
|
|
387
592
|
|
|
388
593
|
// Re-read rather than return `need.manifest`: adopting wrote a new profile
|
package/src/cli/doctor.ts
CHANGED
|
@@ -46,7 +46,7 @@ import {
|
|
|
46
46
|
type RepoRootCache,
|
|
47
47
|
pickLoadedEntry,
|
|
48
48
|
} from "../services/plugin-resolution.js";
|
|
49
|
-
import {
|
|
49
|
+
import { trackedGeneratedNotices } from "../services/profile-activate.js";
|
|
50
50
|
import {
|
|
51
51
|
LIVE_FILE_KEYS,
|
|
52
52
|
liveKind,
|
|
@@ -555,8 +555,9 @@ export async function reportProfileState(
|
|
|
555
555
|
// A .gitignore entry does not untrack a file committed before it. A note
|
|
556
556
|
// with the fix, not a failure: magus writes the file all the same, and
|
|
557
557
|
// never runs git rm itself.
|
|
558
|
-
for (const
|
|
559
|
-
console.log(`• ${
|
|
558
|
+
for (const n of await trackedGeneratedNotices(projectPath)) {
|
|
559
|
+
console.log(`• ${n.problem}`);
|
|
560
|
+
for (const line of n.fix) console.log(` ${line}`);
|
|
560
561
|
}
|
|
561
562
|
return { fixable, unfixable };
|
|
562
563
|
}
|