magus-cli 7.3.1 → 7.4.1
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__/blind/c1-npm-package.test.ts +206 -0
- package/src/__tests__/blind/c10-colour-roles.test.ts +265 -0
- package/src/__tests__/blind/c11-cli-parse-refusal.test.ts +454 -0
- package/src/__tests__/blind/c12-skill-frontmatter.test.ts +323 -0
- package/src/__tests__/blind/c13-wrong-shape.test.ts +603 -0
- package/src/__tests__/blind/c14-cli-row-label.test.ts +259 -0
- package/src/__tests__/blind/c15-probe-ownership.test.ts +533 -0
- package/src/__tests__/blind/c16-nested-wrong-shape.test.ts +297 -0
- package/src/__tests__/blind/c17-enter-label.test.ts +625 -0
- package/src/__tests__/blind/c18-hook-shape.test.ts +354 -0
- package/src/__tests__/blind/c19-paste-routing.test.ts +252 -0
- package/src/__tests__/blind/c2-settings-parse.test.ts +350 -0
- package/src/__tests__/blind/c20-registry-and-file-safety.test.ts +560 -0
- package/src/__tests__/blind/c4-key-dispatch.test.ts +397 -0
- package/src/__tests__/blind/c5-keymap.test.ts +461 -0
- package/src/__tests__/blind/c6-request-gate.test.ts +133 -0
- package/src/__tests__/blind/c7-tab-tiers.test.ts +320 -0
- package/src/__tests__/blind/c8-text-width.test.ts +137 -0
- package/src/__tests__/blind/c9-cli-tool-version.test.ts +239 -0
- package/src/__tests__/blind/child-run.ts +114 -0
- package/src/__tests__/cli-tools-adapter.test.ts +5 -2
- package/src/__tests__/component-badge.test.ts +20 -36
- package/src/__tests__/footer-hints.test.ts +1 -1
- package/src/__tests__/manifest.test.ts +25 -17
- package/src/__tests__/marketplace-badge.test.ts +4 -4
- package/src/__tests__/models-screen-state.test.ts +36 -13
- package/src/__tests__/scope-squares.test.tsx +28 -114
- package/src/__tests__/shutdown.test.ts +578 -0
- package/src/__tests__/stale-load-repair.test.ts +5 -4
- package/src/__tests__/styles-manager.test.ts +4 -4
- package/src/__tests__/styles-screen-state.test.ts +26 -12
- package/src/__tests__/tabbar-layout.test.ts +44 -32
- package/src/__tests__/theme-adaptive-colors.test.ts +6 -59
- package/src/__tests__/tool-probe.test.ts +47 -22
- package/src/__tests__/uppercase-keybindings.test.ts +25 -12
- package/src/cli/ansi.ts +2 -2
- package/src/cli/bootstrap.ts +33 -2
- package/src/cli/doctor.ts +120 -15
- package/src/cli/install.ts +35 -4
- package/src/cli/profile.ts +18 -1
- package/src/cli/router.ts +31 -18
- package/src/cli/update-view.ts +1 -1
- package/src/data/cli-tools.ts +9 -1
- package/src/services/claude-settings.ts +434 -198
- package/src/services/cli-tool-commands.ts +45 -5
- package/src/services/cli-tool-version.ts +101 -0
- package/src/services/gitignore-service.ts +39 -22
- package/src/services/hook-registration.ts +23 -6
- package/src/services/manifest.ts +117 -52
- package/src/services/plugin-manager.ts +19 -5
- package/src/services/profile-adopt.ts +7 -2
- package/src/services/profile-startup.ts +87 -7
- package/src/services/profiles.ts +36 -23
- package/src/services/settings-file.ts +164 -4
- package/src/services/settings-manager.ts +19 -5
- package/src/services/skills-manager.ts +46 -0
- package/src/services/tool-probe.ts +316 -44
- package/src/services/toolchain.ts +39 -0
- package/src/services/update-engine.ts +5 -0
- package/src/services/update-plan.ts +9 -0
- package/src/tui.tsx +48 -21
- package/src/ui/App.tsx +58 -197
- package/src/ui/adapters/cliToolsAdapter.ts +141 -12
- package/src/ui/adapters/pluginsAdapter.ts +10 -7
- package/src/ui/adapters/settingsAdapter.ts +0 -21
- package/src/ui/adapters/skillsAdapter.ts +27 -8
- package/src/ui/adapters/stylesAdapter.ts +15 -11
- package/src/ui/components/CategoryHeader.tsx +7 -1
- package/src/ui/components/EmptyFilterState.tsx +2 -2
- package/src/ui/components/FlagDetailEditor.tsx +0 -0
- package/src/ui/components/ScreenErrorBoundary.tsx +105 -0
- package/src/ui/components/TabBar.tsx +93 -21
- package/src/ui/components/layout/FooterHints.tsx +6 -27
- package/src/ui/components/layout/ScreenLayout.tsx +68 -42
- package/src/ui/components/layout/index.ts +1 -5
- package/src/ui/components/modals/ConfirmModal.tsx +2 -19
- package/src/ui/components/modals/HelpOverlay.tsx +186 -0
- package/src/ui/components/modals/MessageModal.tsx +3 -15
- package/src/ui/components/modals/ModalContainer.tsx +107 -60
- package/src/ui/components/primitives/ErrorNotice.tsx +38 -0
- package/src/ui/components/primitives/ListCategoryRow.tsx +18 -7
- package/src/ui/components/primitives/MetaText.tsx +1 -1
- package/src/ui/components/primitives/ScopeDetail.tsx +31 -26
- package/src/ui/components/primitives/ScopeSquares.tsx +23 -28
- package/src/ui/components/primitives/SelectableRow.tsx +24 -1
- package/src/ui/components/primitives/index.ts +4 -3
- package/src/ui/hooks/useProfileStartupModal.ts +15 -3
- package/src/ui/input/KeyRouter.tsx +304 -0
- package/src/ui/input/key-dispatch.ts +197 -0
- package/src/ui/keymap.ts +905 -0
- package/src/ui/renderers/cliToolRenderers.tsx +192 -92
- package/src/ui/renderers/gitignoreRenderers.tsx +41 -43
- package/src/ui/renderers/markdownRenderer.tsx +2 -2
- package/src/ui/renderers/mcpRenderers.tsx +28 -14
- package/src/ui/renderers/modelRenderers.tsx +12 -13
- package/src/ui/renderers/modelVisuals.tsx +0 -2
- package/src/ui/renderers/pluginRenderers.tsx +47 -54
- package/src/ui/renderers/profileRenderers.tsx +32 -37
- package/src/ui/renderers/settingsRenderers.tsx +84 -55
- package/src/ui/renderers/skillRenderers.tsx +237 -190
- package/src/ui/renderers/styleRenderers.tsx +13 -2
- package/src/ui/runtime/shutdown.ts +268 -0
- package/src/ui/screens/AliasScreen.tsx +118 -100
- package/src/ui/screens/CliToolsScreen.tsx +173 -101
- package/src/ui/screens/EnvVarsScreen.tsx +44 -42
- package/src/ui/screens/GitignoreScreen.tsx +50 -55
- package/src/ui/screens/McpRegistryScreen.tsx +52 -86
- package/src/ui/screens/McpScreen.tsx +47 -33
- package/src/ui/screens/ModelsScreen.tsx +59 -104
- package/src/ui/screens/PluginsScreen.tsx +129 -129
- package/src/ui/screens/ProfilesScreen.tsx +49 -46
- package/src/ui/screens/SkillsScreen.tsx +184 -204
- package/src/ui/screens/StylesScreen.tsx +104 -157
- package/src/ui/state/AppContext.tsx +6 -0
- package/src/ui/state/DimensionsContext.tsx +1 -10
- package/src/ui/state/reducer.ts +5 -8
- package/src/ui/state/types.ts +11 -9
- package/src/ui/text-width.ts +341 -0
- package/src/ui/theme-mode.ts +34 -71
- package/src/ui/theme.ts +50 -28
- package/src/utils/request-gate.ts +37 -0
- package/src/ui/components/ScopeIndicator.tsx +0 -63
- package/src/ui/components/SearchInput.tsx +0 -57
- package/src/ui/hooks/index.ts +0 -2
- package/src/ui/hooks/useKeyboard.ts +0 -24
- package/src/ui/hooks/useKeyboardHandler.ts +0 -63
package/src/cli/doctor.ts
CHANGED
|
@@ -2,18 +2,27 @@
|
|
|
2
2
|
* `magus doctor [--fix]`
|
|
3
3
|
*
|
|
4
4
|
* Diagnoses (and with --fix, repairs) project health:
|
|
5
|
+
* 0. Global settings — `~/.claude/settings.json` parses.
|
|
5
6
|
* 1. Binary dependencies — every requires.bin / cliTools binary on PATH.
|
|
6
7
|
* This is the check that would have caught a dead `tmux-mcp`.
|
|
7
8
|
* 2. Profile symlink integrity — the active profile's links resolve.
|
|
8
9
|
* 3. Convention compliance — .gitignore + CLAUDE.md blocks (ported engine).
|
|
9
10
|
* 4. Model routing — config validity, hook registration, and drift.
|
|
10
11
|
* 5. Plugin loads — what Claude Code will load here vs what is installed.
|
|
12
|
+
*
|
|
13
|
+
* A file that exists and does not parse — the global settings, the project's
|
|
14
|
+
* `.claude/profiles.json` or its `.claude/settings.json`, Claude Code's
|
|
15
|
+
* `installed_plugins.json` — is a finding in the section that meets it, never an
|
|
16
|
+
* abort: the rest of the report is still worth having. `--fix` repairs none of
|
|
17
|
+
* them, because magus never rewrites a file it cannot parse.
|
|
11
18
|
*/
|
|
12
19
|
|
|
13
20
|
import path from "node:path";
|
|
14
21
|
import fs from "fs-extra";
|
|
15
22
|
import {
|
|
23
|
+
SettingsParseError,
|
|
16
24
|
getProjectInstalledPluginVersions,
|
|
25
|
+
readGlobalSettings,
|
|
17
26
|
readInstalledPluginsRegistry,
|
|
18
27
|
readSettings,
|
|
19
28
|
} from "../services/claude-settings.js";
|
|
@@ -35,17 +44,39 @@ import {
|
|
|
35
44
|
} from "../services/plugin-resolution.js";
|
|
36
45
|
import { resolveAllProfiles } from "../services/resolver.js";
|
|
37
46
|
import { activeProfile, profileDir } from "../services/symlink-manager.js";
|
|
47
|
+
import type {
|
|
48
|
+
ClaudeSettings,
|
|
49
|
+
InstalledPluginsRegistry,
|
|
50
|
+
ProfileManifest,
|
|
51
|
+
} from "../types/index.js";
|
|
38
52
|
import { resolveExecutable } from "../utils/command-utils.js";
|
|
39
53
|
|
|
40
54
|
export async function runDoctorCommand(args: string[]): Promise<number> {
|
|
41
55
|
const fix = args.includes("--fix");
|
|
42
56
|
const projectPath = process.cwd();
|
|
43
57
|
let problems = 0;
|
|
58
|
+
// Findings `--fix` does not repair, counted apart so it cannot silence
|
|
59
|
+
// them — see doctorExitCode.
|
|
60
|
+
let unfixable = 0;
|
|
44
61
|
|
|
45
|
-
//
|
|
46
|
-
|
|
62
|
+
// 0. Global settings. Its own check, first, because most of doctor never
|
|
63
|
+
// reads that file: a project without model routing reached it nowhere and
|
|
64
|
+
// reported "No problems found" over a damaged one, while the one section
|
|
65
|
+
// that did — hook registration under --fix — aborted the whole run on it.
|
|
66
|
+
const globalSettingsError = await checkGlobalSettings();
|
|
67
|
+
if (globalSettingsError) unfixable++;
|
|
68
|
+
|
|
69
|
+
// 1. Binary dependencies, which needs the manifest to know what to check.
|
|
70
|
+
let manifest: ProfileManifest | null = null;
|
|
71
|
+
try {
|
|
72
|
+
manifest = await readManifest(projectPath);
|
|
73
|
+
} catch (error) {
|
|
74
|
+
if (!(error instanceof SettingsParseError)) throw error;
|
|
75
|
+
reportUnparseable("Profiles", error);
|
|
76
|
+
unfixable++;
|
|
77
|
+
}
|
|
47
78
|
let pluginIds: string[] = [];
|
|
48
|
-
if (Object.keys(manifest.profiles).length > 0) {
|
|
79
|
+
if (manifest && Object.keys(manifest.profiles).length > 0) {
|
|
49
80
|
const closure = await resolveAllProfiles(manifest);
|
|
50
81
|
pluginIds = Object.keys(closure.plugins);
|
|
51
82
|
if (closure.bins.length > 0) {
|
|
@@ -120,14 +151,43 @@ export async function runDoctorCommand(args: string[]): Promise<number> {
|
|
|
120
151
|
}
|
|
121
152
|
|
|
122
153
|
// 4. Model routing.
|
|
123
|
-
|
|
154
|
+
const models = await checkModels(projectPath, fix, globalSettingsError);
|
|
155
|
+
problems += models.fixable;
|
|
156
|
+
unfixable += models.unfixable;
|
|
124
157
|
|
|
125
158
|
// 5. Stale plugin loads. Counted SEPARATELY, because `--fix` does not repair
|
|
126
159
|
// them — see doctorExitCode.
|
|
127
|
-
|
|
160
|
+
unfixable += await checkPluginLoads(projectPath);
|
|
128
161
|
|
|
129
|
-
if (problems === 0 &&
|
|
130
|
-
return doctorExitCode({ fixable: problems, unfixable
|
|
162
|
+
if (problems === 0 && unfixable === 0) console.log("\n✓ No problems found.");
|
|
163
|
+
return doctorExitCode({ fixable: problems, unfixable, fix });
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
/**
|
|
167
|
+
* A file that exists and does not parse, as a finding under `heading`.
|
|
168
|
+
*
|
|
169
|
+
* The same three lines wherever one turns up, so a damaged file reads the same
|
|
170
|
+
* whichever section met it.
|
|
171
|
+
*/
|
|
172
|
+
function reportUnparseable(heading: string, error: SettingsParseError): void {
|
|
173
|
+
console.log(`\n${heading}:`);
|
|
174
|
+
console.log(` ✗ ${error.message}`);
|
|
175
|
+
console.log(" Fix the JSON by hand; magus leaves the file as it is.");
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
/**
|
|
179
|
+
* The global settings file, if it does not parse — reported, and returned so
|
|
180
|
+
* the hook check can say why it cannot answer instead of guessing.
|
|
181
|
+
*/
|
|
182
|
+
async function checkGlobalSettings(): Promise<SettingsParseError | null> {
|
|
183
|
+
try {
|
|
184
|
+
await readGlobalSettings();
|
|
185
|
+
return null;
|
|
186
|
+
} catch (error) {
|
|
187
|
+
if (!(error instanceof SettingsParseError)) throw error;
|
|
188
|
+
reportUnparseable("Global settings", error);
|
|
189
|
+
return error;
|
|
190
|
+
}
|
|
131
191
|
}
|
|
132
192
|
|
|
133
193
|
/**
|
|
@@ -176,8 +236,25 @@ export function doctorExitCode(args: {
|
|
|
176
236
|
* loads — the loader's, `pickLoadedEntry`
|
|
177
237
|
*/
|
|
178
238
|
async function checkPluginLoads(projectPath: string): Promise<number> {
|
|
179
|
-
|
|
180
|
-
|
|
239
|
+
let registry: InstalledPluginsRegistry;
|
|
240
|
+
try {
|
|
241
|
+
registry = await readInstalledPluginsRegistry();
|
|
242
|
+
} catch (error) {
|
|
243
|
+
// A registry nothing can read is a finding, not an abort: doctor is the
|
|
244
|
+
// command someone runs BECAUSE something is wrong, and it has to reach its
|
|
245
|
+
// remaining sections and its exit code.
|
|
246
|
+
if (!(error instanceof SettingsParseError)) throw error;
|
|
247
|
+
reportUnparseable("Plugin loads", error);
|
|
248
|
+
return 1;
|
|
249
|
+
}
|
|
250
|
+
let settings: ClaudeSettings;
|
|
251
|
+
try {
|
|
252
|
+
settings = await readSettings(projectPath);
|
|
253
|
+
} catch (error) {
|
|
254
|
+
if (!(error instanceof SettingsParseError)) throw error;
|
|
255
|
+
reportUnparseable("Plugin loads", error);
|
|
256
|
+
return 1;
|
|
257
|
+
}
|
|
181
258
|
const installed = await getProjectInstalledPluginVersions(
|
|
182
259
|
settings.installedPluginVersions || {},
|
|
183
260
|
projectPath,
|
|
@@ -249,13 +326,24 @@ async function checkPluginLoads(projectPath: string): Promise<number> {
|
|
|
249
326
|
* fixed from here (the values are a human's decision); an unregistered hook
|
|
250
327
|
* routes nothing no matter how good the config is; drift only matters once
|
|
251
328
|
* both of those are sound.
|
|
329
|
+
*
|
|
330
|
+
* `fixable` is what `--fix` can act on; `unfixable` is what it cannot, which
|
|
331
|
+
* is a hook whose settings file does not parse.
|
|
252
332
|
*/
|
|
253
|
-
async function checkModels(
|
|
333
|
+
async function checkModels(
|
|
334
|
+
projectPath: string,
|
|
335
|
+
fix: boolean,
|
|
336
|
+
/** From `checkGlobalSettings`: the global settings file does not parse. */
|
|
337
|
+
globalSettingsError: SettingsParseError | null,
|
|
338
|
+
): Promise<{ fixable: number; unfixable: number }> {
|
|
254
339
|
const { config, errors, path: file } = await readModelsConfig(projectPath);
|
|
255
|
-
if (config === null && errors.length === 0)
|
|
340
|
+
if (config === null && errors.length === 0) {
|
|
341
|
+
return { fixable: 0, unfixable: 0 };
|
|
342
|
+
}
|
|
256
343
|
|
|
257
344
|
console.log("\nModel routing:");
|
|
258
345
|
let problems = 0;
|
|
346
|
+
let unfixable = 0;
|
|
259
347
|
|
|
260
348
|
if (errors.length > 0) {
|
|
261
349
|
console.log(` ✗ ${file} is invalid — nothing is routed:`);
|
|
@@ -268,15 +356,32 @@ async function checkModels(projectPath: string, fix: boolean): Promise<number> {
|
|
|
268
356
|
console.log(" re-run `magus models use <preset>`) and check again.");
|
|
269
357
|
// An invalid config is worth exactly one problem, not one per message —
|
|
270
358
|
// the count drives the exit code, not the severity.
|
|
271
|
-
return 1;
|
|
359
|
+
return { fixable: 1, unfixable: 0 };
|
|
272
360
|
}
|
|
273
361
|
|
|
274
362
|
// The hook is what applies the routing. Without it the config is inert.
|
|
275
363
|
if (await isAgentModelHookRegistered()) {
|
|
276
364
|
console.log(" ✓ agent-model hook registered at user scope.");
|
|
277
365
|
} else if (fix) {
|
|
278
|
-
|
|
279
|
-
|
|
366
|
+
try {
|
|
367
|
+
await registerAgentModelHook();
|
|
368
|
+
console.log(" ✓ agent-model hook registered at user scope (fixed).");
|
|
369
|
+
} catch (error) {
|
|
370
|
+
// Registering writes the settings file, and one that does not parse is
|
|
371
|
+
// never written. This used to escape and abort the whole run.
|
|
372
|
+
if (!(error instanceof SettingsParseError)) throw error;
|
|
373
|
+
console.log(
|
|
374
|
+
` ✗ agent-model hook not registered: ${error.path} ${error.problem}, and magus does not write a file it cannot read.`,
|
|
375
|
+
);
|
|
376
|
+
unfixable++;
|
|
377
|
+
}
|
|
378
|
+
} else if (globalSettingsError) {
|
|
379
|
+
// `isAgentModelHookRegistered` reads an unparseable file as "not
|
|
380
|
+
// registered". Offering `--fix` for that would offer a write magus refuses.
|
|
381
|
+
console.log(
|
|
382
|
+
` ✗ agent-model hook: cannot tell whether it is registered — the global settings file ${globalSettingsError.problem} (see above).`,
|
|
383
|
+
);
|
|
384
|
+
unfixable++;
|
|
280
385
|
} else {
|
|
281
386
|
console.log(
|
|
282
387
|
" ✗ agent-model hook is not registered — the config is there, but nothing runs it.",
|
|
@@ -310,7 +415,7 @@ async function checkModels(projectPath: string, fix: boolean): Promise<number> {
|
|
|
310
415
|
|
|
311
416
|
for (const warning of status.warnings) console.log(` ⚠ ${warning}`);
|
|
312
417
|
|
|
313
|
-
return problems;
|
|
418
|
+
return { fixable: problems, unfixable };
|
|
314
419
|
}
|
|
315
420
|
|
|
316
421
|
/** Which of the active profile's expected symlinks are missing or dangling. */
|
package/src/cli/install.ts
CHANGED
|
@@ -15,6 +15,7 @@
|
|
|
15
15
|
import path from "node:path";
|
|
16
16
|
import { installPlugin, isClaudeAvailable } from "../services/claude-cli.js";
|
|
17
17
|
import {
|
|
18
|
+
getLocalSettingsPath,
|
|
18
19
|
readInstalledPluginsRegistry,
|
|
19
20
|
readLocalSettings,
|
|
20
21
|
writeLocalSettings,
|
|
@@ -31,6 +32,7 @@ import { reportDisplaced } from "../services/profile-activate.js";
|
|
|
31
32
|
import { materializeProfile } from "../services/profile-materializer.js";
|
|
32
33
|
import { detectProfileDrift } from "../services/profile-sync.js";
|
|
33
34
|
import { resolveAllProfiles, resolveProfile } from "../services/resolver.js";
|
|
35
|
+
import { SettingsParseError, writableMap } from "../services/settings-file.js";
|
|
34
36
|
import { installSkill } from "../services/skills-manager.js";
|
|
35
37
|
import {
|
|
36
38
|
PROFILE_GITIGNORE_ENTRIES,
|
|
@@ -228,13 +230,42 @@ async function collectEnv(
|
|
|
228
230
|
return;
|
|
229
231
|
}
|
|
230
232
|
|
|
231
|
-
|
|
232
|
-
|
|
233
|
+
// EVERY answer first, then one read-modify-write.
|
|
234
|
+
//
|
|
235
|
+
// The read has to happen AFTER the last prompt. Claude Code writes
|
|
236
|
+
// settings.local.json while these prompts are on screen — it is where a
|
|
237
|
+
// "don't ask again" permission rule lands — and this used to read the file
|
|
238
|
+
// before the first question and write that copy back at the end, so every
|
|
239
|
+
// rule the user granted in between was erased. The prompts are the slow part,
|
|
240
|
+
// and a read from before them describes a file that no longer exists.
|
|
241
|
+
const answers: Array<[string, string]> = [];
|
|
233
242
|
for (const name of status.missingRequired) {
|
|
234
243
|
const value = await promptValue(` ${name} = `);
|
|
235
|
-
if (value)
|
|
244
|
+
if (value) answers.push([name, value]);
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
const localPath = getLocalSettingsPath(projectPath);
|
|
248
|
+
try {
|
|
249
|
+
// Refuses a file that stopped parsing while the prompts were up, and a
|
|
250
|
+
// malformed `env` map, before anything is written. Either way the file is
|
|
251
|
+
// left exactly as it is.
|
|
252
|
+
const local = await readLocalSettings(projectPath);
|
|
253
|
+
const env = writableMap(local, "env", localPath) ?? {};
|
|
254
|
+
local.env = env;
|
|
255
|
+
for (const [name, value] of answers) env[name] = value;
|
|
256
|
+
await writeLocalSettings(local, projectPath);
|
|
257
|
+
} catch (error) {
|
|
258
|
+
// Which variables went unsaved, by NAME — never the values. They are
|
|
259
|
+
// tokens, and a refusal is not a reason to print one to a terminal.
|
|
260
|
+
if (error instanceof SettingsParseError && answers.length > 0) {
|
|
261
|
+
console.error(
|
|
262
|
+
`Not saved: ${answers
|
|
263
|
+
.map(([name]) => name)
|
|
264
|
+
.join(", ")} — set them again once the file is fixed.`,
|
|
265
|
+
);
|
|
266
|
+
}
|
|
267
|
+
throw error;
|
|
236
268
|
}
|
|
237
|
-
await writeLocalSettings(local, projectPath);
|
|
238
269
|
console.log("Saved to .claude/settings.local.json (personal, gitignored).");
|
|
239
270
|
}
|
|
240
271
|
|
package/src/cli/profile.ts
CHANGED
|
@@ -16,15 +16,32 @@ import { getManifestPath, readManifest } from "../services/manifest.js";
|
|
|
16
16
|
import { reportDisplaced } from "../services/profile-activate.js";
|
|
17
17
|
import { syncProfile } from "../services/profile-sync.js";
|
|
18
18
|
import { resolveProfile } from "../services/resolver.js";
|
|
19
|
+
import { SettingsParseError } from "../services/settings-file.js";
|
|
19
20
|
import {
|
|
20
21
|
PROFILE_GITIGNORE_ENTRIES,
|
|
21
22
|
activateProfile,
|
|
22
23
|
activeProfile,
|
|
23
24
|
profileDir,
|
|
24
25
|
} from "../services/symlink-manager.js";
|
|
25
|
-
import { ensureManifest } from "./bootstrap.js";
|
|
26
|
+
import { ensureManifest, reportUnparseable } from "./bootstrap.js";
|
|
26
27
|
|
|
28
|
+
/**
|
|
29
|
+
* Every subcommand reads `.claude/profiles.json`, and one that does not parse
|
|
30
|
+
* is a report and exit 1 for all of them — named once here rather than guarded
|
|
31
|
+
* per subcommand. It never reads as "no profiles": `list` said exactly that,
|
|
32
|
+
* and `init` would then have written a new manifest over the file.
|
|
33
|
+
*/
|
|
27
34
|
export async function runProfileCommand(args: string[]): Promise<number> {
|
|
35
|
+
try {
|
|
36
|
+
return await dispatchProfileCommand(args);
|
|
37
|
+
} catch (error) {
|
|
38
|
+
if (!(error instanceof SettingsParseError)) throw error;
|
|
39
|
+
reportUnparseable(error);
|
|
40
|
+
return 1;
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
async function dispatchProfileCommand(args: string[]): Promise<number> {
|
|
28
45
|
const [sub, ...rest] = args;
|
|
29
46
|
const projectPath = process.cwd();
|
|
30
47
|
|
package/src/cli/router.ts
CHANGED
|
@@ -7,8 +7,10 @@
|
|
|
7
7
|
*/
|
|
8
8
|
|
|
9
9
|
import type { ThemeMode } from "@opentui/core";
|
|
10
|
+
import { SettingsParseError } from "../services/settings-file.js";
|
|
10
11
|
import { checkForUpdates } from "../services/version-check.js";
|
|
11
12
|
import { type ThemeEnv, parseThemeWord } from "../ui/theme-resolve.js";
|
|
13
|
+
import { reportUnparseable } from "./bootstrap.js";
|
|
12
14
|
import { runDoctorCommand } from "./doctor.js";
|
|
13
15
|
import { runInstallCommand } from "./install.js";
|
|
14
16
|
import { runModelsCommand } from "./models.js";
|
|
@@ -171,24 +173,35 @@ export async function route(
|
|
|
171
173
|
setThemeMode(mode);
|
|
172
174
|
}
|
|
173
175
|
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
176
|
+
try {
|
|
177
|
+
switch (command) {
|
|
178
|
+
case "update":
|
|
179
|
+
return HANDLED(await runUpdateCommand(rest));
|
|
180
|
+
case "upgrade":
|
|
181
|
+
return HANDLED(await runUpgradeCommand());
|
|
182
|
+
case "install":
|
|
183
|
+
return HANDLED(await runInstallCommand(rest));
|
|
184
|
+
case "profile":
|
|
185
|
+
return HANDLED(await runProfileCommand(rest));
|
|
186
|
+
case "models":
|
|
187
|
+
return HANDLED(await runModelsCommand(rest));
|
|
188
|
+
case "doctor":
|
|
189
|
+
return HANDLED(await runDoctorCommand(rest));
|
|
190
|
+
default:
|
|
191
|
+
// Bare `magus` or an unrecognized token → interactive TUI.
|
|
192
|
+
return themeFlag.theme === undefined
|
|
193
|
+
? LAUNCH_TUI
|
|
194
|
+
: { ...LAUNCH_TUI, theme: themeFlag.theme };
|
|
195
|
+
}
|
|
196
|
+
} catch (error) {
|
|
197
|
+
// A settings or manifest file that does not parse is the user's to fix, not a
|
|
198
|
+
// crash. Every writer already refuses to touch it; this is the one place that
|
|
199
|
+
// turns the refusal into a message for commands with no handling of their own.
|
|
200
|
+
if (error instanceof SettingsParseError) {
|
|
201
|
+
reportUnparseable(error);
|
|
202
|
+
return HANDLED(1);
|
|
203
|
+
}
|
|
204
|
+
throw error;
|
|
192
205
|
}
|
|
193
206
|
}
|
|
194
207
|
|
package/src/cli/update-view.ts
CHANGED
|
@@ -86,7 +86,7 @@ type ActionKey =
|
|
|
86
86
|
|
|
87
87
|
const ACTIONS: Record<ActionKey, ActionStyle> = {
|
|
88
88
|
current: { glyph: "=", label: "CURRENT", color: brand.muted, chrome: true },
|
|
89
|
-
install: { glyph: "+", label: "INSTALL", color: brand.
|
|
89
|
+
install: { glyph: "+", label: "INSTALL", color: brand.info },
|
|
90
90
|
// Green, the tone magus uses for an update everywhere else. Amber stays
|
|
91
91
|
// on `unknown` below, so the two states remain distinguishable: green is
|
|
92
92
|
// "a newer version exists", amber is "we could not tell".
|
package/src/data/cli-tools.ts
CHANGED
|
@@ -29,6 +29,14 @@ export interface CliTool {
|
|
|
29
29
|
packageName: string;
|
|
30
30
|
}
|
|
31
31
|
|
|
32
|
+
/**
|
|
33
|
+
* Claude Code's npm package. Named once because two readers need it: the
|
|
34
|
+
* catalogue entry below, and the probe, which asks this package's `latest`
|
|
35
|
+
* dist-tag for a copy the native installer put on PATH — that copy has no
|
|
36
|
+
* package.json to say which package it is.
|
|
37
|
+
*/
|
|
38
|
+
export const CLAUDE_CODE_NPM_PACKAGE = "@anthropic-ai/claude-code";
|
|
39
|
+
|
|
32
40
|
export const cliTools: CliTool[] = [
|
|
33
41
|
{
|
|
34
42
|
name: "magus",
|
|
@@ -72,7 +80,7 @@ export const cliTools: CliTool[] = [
|
|
|
72
80
|
website: "https://claude.ai/code",
|
|
73
81
|
category: "ai-coding",
|
|
74
82
|
packageManager: "npm",
|
|
75
|
-
packageName:
|
|
83
|
+
packageName: CLAUDE_CODE_NPM_PACKAGE,
|
|
76
84
|
},
|
|
77
85
|
{
|
|
78
86
|
name: "codex",
|