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