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
|
@@ -37,7 +37,6 @@ import {
|
|
|
37
37
|
writableList,
|
|
38
38
|
writableMap,
|
|
39
39
|
} from "./settings-file.js";
|
|
40
|
-
import { refuseTrackedLiveFiles } from "./tracked-live-files.js";
|
|
41
40
|
|
|
42
41
|
/**
|
|
43
42
|
* What every reader below throws for a file that exists but does not parse.
|
|
@@ -149,11 +148,6 @@ export async function writeSettings(
|
|
|
149
148
|
settings: ClaudeSettings,
|
|
150
149
|
projectPath?: string,
|
|
151
150
|
): Promise<void> {
|
|
152
|
-
// In a project with profiles this file is generated: never written where
|
|
153
|
-
// git tracks it (see `tracked-live-files.ts`).
|
|
154
|
-
await refuseTrackedLiveFiles(projectPath ?? process.cwd(), [
|
|
155
|
-
getSettingsPath(projectPath),
|
|
156
|
-
]);
|
|
157
151
|
await refuseUnwritableFile(getSettingsPath(projectPath));
|
|
158
152
|
const claudeDir = await ensureClaudeDir(projectPath);
|
|
159
153
|
const settingsPath = path.join(claudeDir, SETTINGS_FILE);
|
|
@@ -197,7 +191,6 @@ export async function writeMcpConfig(
|
|
|
197
191
|
projectPath?: string,
|
|
198
192
|
): Promise<void> {
|
|
199
193
|
const mcpPath = getMcpConfigPath(projectPath);
|
|
200
|
-
await refuseTrackedLiveFiles(projectPath ?? process.cwd(), [mcpPath]);
|
|
201
194
|
await refuseUnwritableFile(mcpPath);
|
|
202
195
|
await fs.writeJson(mcpPath, config, { spaces: 2 });
|
|
203
196
|
}
|
|
@@ -413,6 +406,34 @@ export function pickRegistryEntry(
|
|
|
413
406
|
return entries.find((e) => e.scope === "user");
|
|
414
407
|
}
|
|
415
408
|
|
|
409
|
+
/**
|
|
410
|
+
* The row a `claude plugin … --scope <scope>` run from `projectPath` owns: the
|
|
411
|
+
* user row for user scope; for project or local scope, the row of exactly that
|
|
412
|
+
* scope pinned to exactly this path.
|
|
413
|
+
*
|
|
414
|
+
* NOT {@link pickRegistryEntry}, which answers "what version applies here" and
|
|
415
|
+
* so falls back to inherited rows and then to the user row. That fallback asked
|
|
416
|
+
* the apply engine's "does this project hold a row?" guard made a plugin
|
|
417
|
+
* installed only at user scope, and turned on in project settings, look like it
|
|
418
|
+
* held a project row — so `claude plugin update --scope project` ran from a
|
|
419
|
+
* folder with none, and Claude Code updated some other project's row instead.
|
|
420
|
+
*/
|
|
421
|
+
export function ownRegistryEntry(
|
|
422
|
+
entries: InstalledPluginEntry[] | undefined,
|
|
423
|
+
scope: "user" | "project" | "local",
|
|
424
|
+
projectPath: string,
|
|
425
|
+
): InstalledPluginEntry | undefined {
|
|
426
|
+
if (!Array.isArray(entries)) return undefined;
|
|
427
|
+
if (scope === "user") return entries.find((e) => e.scope === "user");
|
|
428
|
+
const target = path.resolve(projectPath);
|
|
429
|
+
return entries.find(
|
|
430
|
+
(e) =>
|
|
431
|
+
e.scope === scope &&
|
|
432
|
+
e.projectPath !== undefined &&
|
|
433
|
+
path.resolve(e.projectPath) === target,
|
|
434
|
+
);
|
|
435
|
+
}
|
|
436
|
+
|
|
416
437
|
async function overlayRegistryVersions(
|
|
417
438
|
base: Record<string, string>,
|
|
418
439
|
scope: "user" | "project" | "local",
|
|
@@ -1,63 +1,51 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
2
|
+
* The TUI's startup gitignore question.
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
4
|
+
* Asked on every open (and again after a first adoption). Resolves the
|
|
5
|
+
* manifest — including, in a project with profiles, the files magus generates
|
|
6
|
+
* from `.claude/profiles.json` — runs detection, and reports. Nothing here
|
|
7
|
+
* applies a fix: the startup question applies the in-place fix only when the
|
|
8
|
+
* user picks it (`applyStartupFixes`), and every other destructive fix is
|
|
9
|
+
* confirmed per item in the Git State tab.
|
|
9
10
|
*/
|
|
10
11
|
|
|
11
12
|
import type { Violation } from "../types/gitignore.js";
|
|
12
|
-
import {
|
|
13
|
-
import { formatViolationSummary } from "./gitignore-fixer.js";
|
|
14
|
-
import { resolveGitignore } from "./gitignore-resolver.js";
|
|
13
|
+
import { loadGitignoreState } from "./gitignore-service.js";
|
|
15
14
|
|
|
16
|
-
|
|
15
|
+
/** What the TUI's startup gitignore question needs to show. */
|
|
16
|
+
export interface StartupGitignoreNeed {
|
|
17
17
|
violationCount: number;
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
try {
|
|
26
|
-
const resolved = await resolveGitignore(cwd, {
|
|
27
|
-
logger: (m) => warnings.push(m),
|
|
28
|
-
});
|
|
29
|
-
const violations = await detectViolations(cwd, resolved);
|
|
30
|
-
return {
|
|
31
|
-
violationCount: violations.length,
|
|
32
|
-
warnings,
|
|
33
|
-
};
|
|
34
|
-
} catch (err) {
|
|
35
|
-
warnings.push(
|
|
36
|
-
`[gitignore] check failed: ${err instanceof Error ? err.message : String(err)}`,
|
|
37
|
-
);
|
|
38
|
-
return { violationCount: 0, warnings };
|
|
39
|
-
}
|
|
18
|
+
/** Missing `.gitignore` entries — the additive fix. */
|
|
19
|
+
safeCount: number;
|
|
20
|
+
/**
|
|
21
|
+
* Files magus generates from `.claude/profiles.json` that git tracks — the
|
|
22
|
+
* ones the startup fix untracks in place.
|
|
23
|
+
*/
|
|
24
|
+
generated: string[];
|
|
40
25
|
}
|
|
41
26
|
|
|
42
27
|
/**
|
|
43
|
-
*
|
|
44
|
-
*
|
|
28
|
+
* The startup gitignore question, or null when there is nothing to ask.
|
|
29
|
+
*
|
|
30
|
+
* Asked fresh on every call — the TUI calls it on every open, and again after
|
|
31
|
+
* a first adoption — and nothing remembers an earlier answer: a tracked
|
|
32
|
+
* generated file is asked about until it is fixed.
|
|
45
33
|
*/
|
|
46
|
-
export function
|
|
47
|
-
|
|
48
|
-
):
|
|
49
|
-
|
|
50
|
-
|
|
34
|
+
export async function startupGitignoreNeed(
|
|
35
|
+
cwd: string,
|
|
36
|
+
): Promise<StartupGitignoreNeed | null> {
|
|
37
|
+
const { violations } = await loadGitignoreState(cwd);
|
|
38
|
+
if (violations.length === 0) return null;
|
|
39
|
+
return {
|
|
40
|
+
violationCount: violations.length,
|
|
41
|
+
safeCount: violations.filter((v) => v.severity === "safe").length,
|
|
42
|
+
generated: generatedTracked(violations).map((v) => v.path),
|
|
43
|
+
};
|
|
51
44
|
}
|
|
52
45
|
|
|
53
|
-
/**
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
cwd: string,
|
|
59
|
-
violations: Violation[],
|
|
60
|
-
): Promise<string> {
|
|
61
|
-
void cwd;
|
|
62
|
-
return formatViolationSummary(violations);
|
|
46
|
+
/** The violations the startup fix handles in place: tracked generated files. */
|
|
47
|
+
export function generatedTracked(violations: Violation[]): Violation[] {
|
|
48
|
+
return violations.filter(
|
|
49
|
+
(v) => v.kind === "tracked-but-should-ignore" && v.source === "profile",
|
|
50
|
+
);
|
|
63
51
|
}
|
|
@@ -5,6 +5,10 @@
|
|
|
5
5
|
* 1. Built-in defaults (shipped with magus)
|
|
6
6
|
* 2. Global manifest at ~/.claude/gitignore.json (team-level, manually shared)
|
|
7
7
|
* 3. Project manifest at <cwd>/.claude/gitignore.json
|
|
8
|
+
* 4. Profile: in a project with profiles, the files magus generates from
|
|
9
|
+
* `.claude/profiles.json` (passed in as `opts.profile`). Highest, because
|
|
10
|
+
* those files are rewritten per developer — tracking one cannot be right
|
|
11
|
+
* whatever another manifest says.
|
|
8
12
|
*
|
|
9
13
|
* `extends` references are resolved against BUILTIN_TEMPLATES. Within a single
|
|
10
14
|
* tier, the same pattern cannot appear in both `ignore` and `track` — that
|
|
@@ -33,9 +37,11 @@ export interface ResolveOptions {
|
|
|
33
37
|
homeDir?: string;
|
|
34
38
|
/** Optional logger for non-fatal warnings (malformed JSON, missing template). */
|
|
35
39
|
logger?: (msg: string) => void;
|
|
40
|
+
/** The profile tier — see `profileGitignoreManifest`. */
|
|
41
|
+
profile?: GitignoreManifest | null;
|
|
36
42
|
}
|
|
37
43
|
|
|
38
|
-
const TIER_ORDER: Tier[] = ["builtin", "global", "project"];
|
|
44
|
+
const TIER_ORDER: Tier[] = ["builtin", "global", "project", "profile"];
|
|
39
45
|
|
|
40
46
|
export async function resolveGitignore(
|
|
41
47
|
projectCwd: string,
|
|
@@ -59,6 +65,9 @@ export async function resolveGitignore(
|
|
|
59
65
|
const projectManifest = await readManifest(projectPath, log);
|
|
60
66
|
if (projectManifest) tierManifests.set("project", projectManifest);
|
|
61
67
|
|
|
68
|
+
// 4. The files the profile model generates
|
|
69
|
+
if (opts.profile) tierManifests.set("profile", opts.profile);
|
|
70
|
+
|
|
62
71
|
// Apply `extends` for each tier (templates count as part of their tier)
|
|
63
72
|
for (const tier of TIER_ORDER) {
|
|
64
73
|
const m = tierManifests.get(tier);
|
|
@@ -28,6 +28,7 @@ import {
|
|
|
28
28
|
} from "./gitignore-fixer.js";
|
|
29
29
|
import { resolveGitignore } from "./gitignore-resolver.js";
|
|
30
30
|
import { readSettingsFile, writableList } from "./settings-file.js";
|
|
31
|
+
import { profileGitignoreManifest } from "./tracked-live-files.js";
|
|
31
32
|
|
|
32
33
|
export interface GitignoreState {
|
|
33
34
|
resolved: ResolvedManifest;
|
|
@@ -40,11 +41,77 @@ export async function loadGitignoreState(cwd: string): Promise<GitignoreState> {
|
|
|
40
41
|
const warnings: string[] = [];
|
|
41
42
|
const resolved = await resolveGitignore(cwd, {
|
|
42
43
|
logger: (m) => warnings.push(m),
|
|
44
|
+
profile: await profileGitignoreManifest(cwd),
|
|
43
45
|
});
|
|
44
46
|
const violations = await detectViolations(cwd, resolved);
|
|
45
47
|
return { resolved, violations, warnings };
|
|
46
48
|
}
|
|
47
49
|
|
|
50
|
+
export interface StartupFixResult {
|
|
51
|
+
/** Generated files removed from the index — staged, not committed. */
|
|
52
|
+
untracked: string[];
|
|
53
|
+
/** `.gitignore` entries added. */
|
|
54
|
+
appended: string[];
|
|
55
|
+
/** Files that could not be untracked, with git's reason. */
|
|
56
|
+
failed: Array<{ path: string; reason: string }>;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* The startup question's fix, applied in place: every file magus generates
|
|
61
|
+
* from `.claude/profiles.json` that git tracks is removed from the index
|
|
62
|
+
* (`git rm --cached`, the existing tracked-but-should-ignore fix, which also
|
|
63
|
+
* makes sure `.gitignore` ignores it), and the additive `.gitignore` fixes
|
|
64
|
+
* are applied too. Every other violation is left for the Git State tab.
|
|
65
|
+
*/
|
|
66
|
+
export async function applyStartupFixes(
|
|
67
|
+
cwd: string,
|
|
68
|
+
): Promise<StartupFixResult> {
|
|
69
|
+
const { resolved, violations } = await loadGitignoreState(cwd);
|
|
70
|
+
const generatedRules = resolved.rules
|
|
71
|
+
.filter((r) => r.source === "profile" && r.action === "ignore")
|
|
72
|
+
.map((r) => r.pattern);
|
|
73
|
+
const result: StartupFixResult = { untracked: [], appended: [], failed: [] };
|
|
74
|
+
for (const v of violations) {
|
|
75
|
+
if (v.kind !== "tracked-but-should-ignore" || v.source !== "profile") {
|
|
76
|
+
continue;
|
|
77
|
+
}
|
|
78
|
+
const rule =
|
|
79
|
+
generatedRules.find(
|
|
80
|
+
(p) => p === v.path || (p.endsWith("/") && v.path.startsWith(p)),
|
|
81
|
+
) ?? v.path;
|
|
82
|
+
try {
|
|
83
|
+
await coreApplyDestructiveFix(cwd, v, rule);
|
|
84
|
+
result.untracked.push(v.path);
|
|
85
|
+
} catch (error) {
|
|
86
|
+
result.failed.push({
|
|
87
|
+
path: v.path,
|
|
88
|
+
reason: error instanceof Error ? error.message : String(error),
|
|
89
|
+
});
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
result.appended = (await coreApplySafeFixes(cwd, violations)).appended;
|
|
93
|
+
return result;
|
|
94
|
+
}
|
|
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
|
+
|
|
48
115
|
export async function applyAllSafeFixes(
|
|
49
116
|
cwd: string,
|
|
50
117
|
violations: Violation[],
|
package/src/services/manifest.ts
CHANGED
|
@@ -7,6 +7,7 @@
|
|
|
7
7
|
* structural subset of a v2 manifest entry, so migration is a version bump.
|
|
8
8
|
*/
|
|
9
9
|
|
|
10
|
+
import os from "node:os";
|
|
10
11
|
import path from "node:path";
|
|
11
12
|
import fs from "fs-extra";
|
|
12
13
|
import {
|
|
@@ -41,6 +42,36 @@ export function getManifestPath(projectPath?: string): string {
|
|
|
41
42
|
return path.join(base, MANIFEST_FILE);
|
|
42
43
|
}
|
|
43
44
|
|
|
45
|
+
/**
|
|
46
|
+
* The nearest ANCESTOR directory holding a `.claude/profiles.json`, searched no
|
|
47
|
+
* higher than the repository root — so magus can say so when it runs in a
|
|
48
|
+
* subdirectory.
|
|
49
|
+
*
|
|
50
|
+
* magus manages the directory it runs in because that is the project Claude Code
|
|
51
|
+
* reads: launched in `tools/magus`, it watches `tools/magus/.claude/settings.json`
|
|
52
|
+
* and never the root's (MEASURED with `--debug-file`, Claude Code 2.1.281). A
|
|
53
|
+
* root profiles.json is therefore not this directory's — but someone who meant
|
|
54
|
+
* the root has to hear that it exists before creating a second one here.
|
|
55
|
+
*
|
|
56
|
+
* A directory holding `.git` (file or folder) is a repository root, a worktree's
|
|
57
|
+
* included: nothing above it is searched, or a worktree under
|
|
58
|
+
* `<repo>/.claude/worktrees/` would find the main checkout's file. The home
|
|
59
|
+
* directory is never checked — its `.claude/` is Claude Code's user config.
|
|
60
|
+
*/
|
|
61
|
+
export async function findEnclosingManifest(
|
|
62
|
+
projectPath: string,
|
|
63
|
+
): Promise<string | null> {
|
|
64
|
+
const home = os.homedir();
|
|
65
|
+
let dir = path.resolve(projectPath);
|
|
66
|
+
for (;;) {
|
|
67
|
+
if (await fs.pathExists(path.join(dir, ".git"))) return null;
|
|
68
|
+
const parent = path.dirname(dir);
|
|
69
|
+
if (parent === dir || parent === home) return null;
|
|
70
|
+
dir = parent;
|
|
71
|
+
if (await fs.pathExists(path.join(dir, MANIFEST_FILE))) return dir;
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
44
75
|
/** An empty, valid v2 manifest. */
|
|
45
76
|
export function emptyManifest(): ProfileManifest {
|
|
46
77
|
return { version: 2, profiles: {} };
|
|
@@ -28,15 +28,17 @@
|
|
|
28
28
|
* silently. `getAvailablePlugins` resolves all three scopes and hands back both facts per
|
|
29
29
|
* scope, which is all this module takes from it.
|
|
30
30
|
*
|
|
31
|
-
* The test on those facts is `servesAMate`, below, and it is
|
|
32
|
-
*
|
|
33
|
-
*
|
|
31
|
+
* The test on those facts is `servesAMate`, below, and it is built from `plugin-manager`'s
|
|
32
|
+
* own helpers rather than reading either fact itself, so a mate row and the plugin list
|
|
33
|
+
* cannot disagree about what "installed" means.
|
|
34
34
|
*/
|
|
35
35
|
|
|
36
36
|
import {
|
|
37
37
|
type PluginInfo,
|
|
38
38
|
type ScopeStatus,
|
|
39
39
|
getAvailablePlugins,
|
|
40
|
+
isInstalledInScope,
|
|
41
|
+
scopeFilesMissing,
|
|
40
42
|
} from "./plugin-manager.js";
|
|
41
43
|
|
|
42
44
|
/** The plugin that puts claudish's model tools in reach. */
|
|
@@ -71,11 +73,23 @@ export function matesForced(env: NodeJS.ProcessEnv = process.env): boolean {
|
|
|
71
73
|
}
|
|
72
74
|
|
|
73
75
|
/**
|
|
74
|
-
*
|
|
76
|
+
* Can this scope actually serve a mate? Installed there, with its files present.
|
|
77
|
+
*
|
|
78
|
+
* Installed alone is not enough. Files missing is installed with no version: Claude Code
|
|
79
|
+
* wants to load the plugin and has nothing to load, so claudish is not reachable through it,
|
|
80
|
+
* and a row drawn on the strength of it promises routing that cannot happen — the one thing
|
|
81
|
+
* these rows must never claim.
|
|
82
|
+
*/
|
|
83
|
+
function servesAMate(scope: ScopeStatus | undefined): boolean {
|
|
84
|
+
return isInstalledInScope(scope) && !scopeFilesMissing(scope);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* True when `multimodel@magus` is installed, with its files, in ANY scope.
|
|
75
89
|
*
|
|
76
90
|
* Any scope, because a mate is served by whatever Claude Code has loaded when the agent is
|
|
77
91
|
* spawned, and that is the union of the three — a user-scope install serves a project that
|
|
78
|
-
*
|
|
92
|
+
* installs nothing of its own.
|
|
79
93
|
*
|
|
80
94
|
* FAILS CLOSED. This reaches the marketplace resolution path, which does network work and can
|
|
81
95
|
* time out; a failure means "we do not know", and the honest rendering of not knowing is the
|
|
@@ -83,31 +97,11 @@ export function matesForced(env: NodeJS.ProcessEnv = process.env): boolean {
|
|
|
83
97
|
* rows on screen on the strength of an error.
|
|
84
98
|
*
|
|
85
99
|
* `listPlugins` is injected the same way `buildSettingsPatch` takes its `resolveFullId`: the
|
|
86
|
-
* DECISION here — which scopes count, what
|
|
87
|
-
*
|
|
88
|
-
*
|
|
89
|
-
*
|
|
100
|
+
* DECISION here — which scopes count, what files missing means, what a thrown error means —
|
|
101
|
+
* is the part worth testing, and it cannot be tested through a function that reaches the
|
|
102
|
+
* network and a real machine's registry. The default is the real one, so no caller has to
|
|
103
|
+
* know the seam exists.
|
|
90
104
|
*/
|
|
91
|
-
/**
|
|
92
|
-
* Can this scope actually serve a mate? Both facts, not either one.
|
|
93
|
-
*
|
|
94
|
-
* Deliberately NOT `isInstalledInScope`, which asks a narrower question: that one means the
|
|
95
|
-
* files are on disk, and says so — it dropped `enabled` from its test on purpose, so the
|
|
96
|
-
* plugin list would stop offering to install what was already there.
|
|
97
|
-
*
|
|
98
|
-
* Presence on disk is not what this module needs. A plugin the user has switched off is not
|
|
99
|
-
* loaded, so claudish is not reachable through it, so the routing a mate row advertises
|
|
100
|
-
* cannot happen — which is the one thing these rows must never claim. The other half matters
|
|
101
|
-
* for the mirror-image state: a settings flag with no registry version is the broken
|
|
102
|
-
* enabled-but-not-installed case, and a row drawn on the strength of it promises a plugin
|
|
103
|
-
* that never loaded.
|
|
104
|
-
*
|
|
105
|
-
* So both, and locally, rather than reaching for a shared helper whose meaning is owned by a
|
|
106
|
-
* different question.
|
|
107
|
-
*/
|
|
108
|
-
function servesAMate(scope: ScopeStatus | undefined): boolean {
|
|
109
|
-
return !!scope?.enabled && !!scope.version;
|
|
110
|
-
}
|
|
111
105
|
|
|
112
106
|
export async function areMatesAvailable(
|
|
113
107
|
projectPath?: string,
|