magus-cli 7.10.4 → 7.11.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__/blind/plugin-deps/doctor.test.ts +2 -2
- package/src/__tests__/blind/plugin-deps/hook.test.ts +2 -2
- package/src/__tests__/blind/plugin-deps/read-only.test.ts +1 -1
- package/src/__tests__/blind/plugin-deps/schema.test.ts +6 -6
- package/src/__tests__/blind/requires-min-version/TEST-PLAN.md +54 -0
- package/src/__tests__/blind/requires-min-version/manifests.test.ts +68 -0
- package/src/__tests__/blind/requires-min-version/parse.test.ts +266 -0
- package/src/__tests__/blind/requires-min-version/probe.test.ts +359 -0
- package/src/__tests__/blind/requires-min-version/render.test.ts +252 -0
- package/src/__tests__/blind/requires-min-version/support.ts +83 -0
- package/src/__tests__/blind/requires-min-version/surfaces.test.ts +231 -0
- package/src/__tests__/blind/review-followups/bun-install-path.test.ts +1 -1
- package/src/__tests__/blind/review-followups/ledger-permissions.test.ts +1 -1
- package/src/__tests__/blind/review-followups/ledger-pruning.test.ts +1 -1
- package/src/__tests__/blind/review-followups/path-advice.test.ts +1 -1
- package/src/__tests__/deps-check-hook.test.ts +9 -9
- package/src/__tests__/deps-plan.test.ts +1 -1
- package/src/__tests__/deps-report.test.ts +5 -5
- package/src/__tests__/doctor-json.test.ts +2 -2
- package/src/__tests__/marketplace-refresh.test.ts +1 -1
- package/src/__tests__/plugin-requires.test.ts +2 -2
- package/src/__tests__/profile-worktree-include.test.ts +212 -0
- package/src/__tests__/requires-probe.test.ts +2 -2
- package/src/cli/deps-view.ts +5 -3
- package/src/services/deps-plan.ts +38 -14
- package/src/services/deps-report.ts +24 -7
- package/src/services/doctor-bins.ts +40 -12
- package/src/services/profile-activate.ts +9 -0
- package/src/services/profile-live.ts +26 -0
- package/src/services/profile-skills.ts +14 -4
- package/src/services/profile-state.ts +18 -0
- package/src/services/resolver.ts +1 -0
- package/src/shared/plugin/hooks/deps-check.ts +7 -3
- package/src/shared/plugin/hooks/lib/requires-probe.ts +257 -33
- package/src/ui/adapters/cliToolsAdapter.ts +6 -1
- package/src/ui/renderers/cliToolRenderers.tsx +7 -1
- package/src/ui/screens/CliToolsScreen.tsx +8 -1
package/src/cli/deps-view.ts
CHANGED
|
@@ -18,7 +18,7 @@ import {
|
|
|
18
18
|
requiredByOf,
|
|
19
19
|
} from "../services/deps-plan.js";
|
|
20
20
|
import type { DepsState } from "../services/deps-report.js";
|
|
21
|
-
import { installText } from "../services/doctor-bins.js";
|
|
21
|
+
import { installReason, installText } from "../services/doctor-bins.js";
|
|
22
22
|
import {
|
|
23
23
|
type Os,
|
|
24
24
|
type Presence,
|
|
@@ -33,6 +33,8 @@ function presenceLine(name: string, p: Presence, by: string): string | null {
|
|
|
33
33
|
return null;
|
|
34
34
|
case "missing":
|
|
35
35
|
return ` ✗ ${name} — missing${p.blockedBy ? ` (needs ${p.blockedBy} first)` : ""} (${by})`;
|
|
36
|
+
case "outdated":
|
|
37
|
+
return ` ✗ ${name} — outdated: ${p.found}, needs ${p.minVersion} or later (${by})`;
|
|
36
38
|
case "off-path":
|
|
37
39
|
return ` ⚠ ${name} — installed in ${p.prefix}, which is not on the PATH Claude Code starts with (${by})`;
|
|
38
40
|
case "unknown":
|
|
@@ -57,8 +59,8 @@ export function depsSectionLines(
|
|
|
57
59
|
const line = presenceLine(bin.name, p, requiredByOf(bin).join(", "));
|
|
58
60
|
if (!line) return;
|
|
59
61
|
lines.push(line);
|
|
60
|
-
if (p.status === "missing")
|
|
61
|
-
lines.push(` fix: ${installText(bin, env)}`);
|
|
62
|
+
if (p.status === "missing" || p.status === "outdated")
|
|
63
|
+
lines.push(` fix: ${installText(bin, env, installReason(p))}`);
|
|
62
64
|
if (p.status === "off-path") unfixable++;
|
|
63
65
|
});
|
|
64
66
|
for (const problem of collected.problems) {
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* The one installer executor: plan what is missing, then install it.
|
|
3
3
|
*
|
|
4
|
-
* `planDeps` turns the requirements a probe found missing
|
|
4
|
+
* `planDeps` turns the requirements a probe found missing or outdated into
|
|
5
|
+
* ordered steps,
|
|
5
6
|
* each rendered by requires-probe's `renderInstall` (the only renderer).
|
|
6
7
|
* `applyDepsPlan` runs them through `runArgv` and returns one `ApplyOutcome`
|
|
7
8
|
* per step. Nothing here prints: `magus doctor` (text and `--json`),
|
|
@@ -42,6 +43,7 @@ import {
|
|
|
42
43
|
BUN_PREFIX,
|
|
43
44
|
type Bootstrap,
|
|
44
45
|
type BootstrapTool,
|
|
46
|
+
type InstallReason,
|
|
45
47
|
type InstallStep,
|
|
46
48
|
LOCAL_PREFIX,
|
|
47
49
|
type Os,
|
|
@@ -240,10 +242,11 @@ function fromMagusMarketplace(bin: ResolvedBin): boolean {
|
|
|
240
242
|
}
|
|
241
243
|
|
|
242
244
|
/**
|
|
243
|
-
* The steps that install every `missing` requirement
|
|
244
|
-
* run. `presence[i]` is the probe's
|
|
245
|
-
* requirement is not reinstalled: it needs
|
|
246
|
-
* carries. An `unknown` one (a check too slow
|
|
245
|
+
* The steps that install every `missing` requirement and replace every
|
|
246
|
+
* `outdated` one, in the order they must run. `presence[i]` is the probe's
|
|
247
|
+
* answer for `bins[i]`. An `off-path` requirement is not reinstalled: it needs
|
|
248
|
+
* a PATH line, which `pathAdvice` carries. An `unknown` one (a check too slow
|
|
249
|
+
* to answer, or one that printed no version) is left alone.
|
|
247
250
|
*/
|
|
248
251
|
export function planDeps(
|
|
249
252
|
bins: ResolvedBin[],
|
|
@@ -253,10 +256,11 @@ export function planDeps(
|
|
|
253
256
|
const unfixable: DepsPlan["unfixable"] = [];
|
|
254
257
|
/** The prefixes off the PATH, in the order they came up. */
|
|
255
258
|
const offPathPrefixes = new Set<UserPrefix>();
|
|
256
|
-
const missing: ResolvedBin[] = [];
|
|
259
|
+
const missing: { bin: ResolvedBin; reason: InstallReason }[] = [];
|
|
257
260
|
bins.forEach((bin, i) => {
|
|
258
261
|
const p = presence[i];
|
|
259
|
-
if (p?.status === "missing") missing.push(bin);
|
|
262
|
+
if (p?.status === "missing") missing.push({ bin, reason: "missing" });
|
|
263
|
+
if (p?.status === "outdated") missing.push({ bin, reason: "outdated" });
|
|
260
264
|
if (p?.status === "off-path") offPathPrefixes.add(p.prefix);
|
|
261
265
|
});
|
|
262
266
|
|
|
@@ -285,7 +289,7 @@ export function planDeps(
|
|
|
285
289
|
pluginRoot: bin.root ?? "",
|
|
286
290
|
});
|
|
287
291
|
|
|
288
|
-
for (const bin of missing) {
|
|
292
|
+
for (const { bin, reason } of missing) {
|
|
289
293
|
const option = bin.chosen;
|
|
290
294
|
if (!option) {
|
|
291
295
|
unfixable.push({
|
|
@@ -294,11 +298,12 @@ export function planDeps(
|
|
|
294
298
|
});
|
|
295
299
|
continue;
|
|
296
300
|
}
|
|
297
|
-
const step = renderInstall(
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
pluginRoot: bin.root ?? "",
|
|
301
|
-
|
|
301
|
+
const step = renderInstall(
|
|
302
|
+
bin,
|
|
303
|
+
option,
|
|
304
|
+
{ os: env.os, arch: env.arch, pluginRoot: bin.root ?? "" },
|
|
305
|
+
reason,
|
|
306
|
+
);
|
|
302
307
|
if (
|
|
303
308
|
step.kind === "uv-module" &&
|
|
304
309
|
step.escalation === "sudo" &&
|
|
@@ -895,7 +900,9 @@ async function landingDir(
|
|
|
895
900
|
* After a step's installer exited 0, re-probe everything it installs with the
|
|
896
901
|
* same probe doctor runs. Installed only when the probe now finds each one.
|
|
897
902
|
* Found in a directory off the PATH Claude Code starts with: off-path, naming
|
|
898
|
-
* the directory and the rc line. Not found at all: failed.
|
|
903
|
+
* the directory and the rc line. Not found at all: failed. Still below its
|
|
904
|
+
* `minVersion`: failed, naming the version found, the floor and the file the
|
|
905
|
+
* PATH resolves to — usually an older copy ahead of the one just installed.
|
|
899
906
|
*/
|
|
900
907
|
async function verifyInstalled(
|
|
901
908
|
p: PlannedStep,
|
|
@@ -904,6 +911,7 @@ async function verifyInstalled(
|
|
|
904
911
|
const { env } = ctx;
|
|
905
912
|
const offPath: Verified[] = [];
|
|
906
913
|
const missing: string[] = [];
|
|
914
|
+
const outdated: string[] = [];
|
|
907
915
|
for (const target of p.installs) {
|
|
908
916
|
const presence = await ctx.io.verify(target, env);
|
|
909
917
|
if (presence.status === "present") continue;
|
|
@@ -911,6 +919,18 @@ async function verifyInstalled(
|
|
|
911
919
|
offPath.push({ status: "off-path", dir: presence.prefix });
|
|
912
920
|
continue;
|
|
913
921
|
}
|
|
922
|
+
if (presence.status === "outdated") {
|
|
923
|
+
const tool = target.requirement.check[0] ?? target.requirement.name;
|
|
924
|
+
const resolved = onPath(tool, env.path);
|
|
925
|
+
outdated.push(
|
|
926
|
+
`${target.requirement.name} is still ${presence.found}, below the ${presence.minVersion} it needs: ${
|
|
927
|
+
resolved
|
|
928
|
+
? `the PATH Claude Code starts with runs ${resolved}`
|
|
929
|
+
: `${tool} is not on the PATH Claude Code starts with`
|
|
930
|
+
} — remove that older copy or put the new one ahead of it, then restart Claude Code from a new terminal`,
|
|
931
|
+
);
|
|
932
|
+
continue;
|
|
933
|
+
}
|
|
914
934
|
const tool = target.requirement.check[0] ?? target.requirement.name;
|
|
915
935
|
const landed = await landingDir(p.step, ctx);
|
|
916
936
|
if (landed && onPath(tool, landed)) {
|
|
@@ -924,6 +944,10 @@ async function verifyInstalled(
|
|
|
924
944
|
if (missing.length > 0) {
|
|
925
945
|
return { status: "failed", why: missing.join("\n") };
|
|
926
946
|
}
|
|
947
|
+
if (outdated.length > 0) {
|
|
948
|
+
const why = outdated.join("\n");
|
|
949
|
+
return { status: "failed", why, remedy: why };
|
|
950
|
+
}
|
|
927
951
|
const first = offPath[0];
|
|
928
952
|
if (first?.dir) {
|
|
929
953
|
return {
|
|
@@ -33,13 +33,20 @@ import {
|
|
|
33
33
|
prefixOnPath,
|
|
34
34
|
requiredByOf,
|
|
35
35
|
} from "./deps-plan.js";
|
|
36
|
-
import {
|
|
36
|
+
import {
|
|
37
|
+
DOCTOR_CHECK_CAP_MS,
|
|
38
|
+
installReason,
|
|
39
|
+
probeBins,
|
|
40
|
+
} from "./doctor-bins.js";
|
|
37
41
|
import { readManifest } from "./manifest.js";
|
|
38
42
|
import { resolvePluginRequires, selectBins } from "./plugin-requires.js";
|
|
39
43
|
import { binKey, cliToolBins, mergeBins } from "./resolver.js";
|
|
40
44
|
|
|
41
|
-
/**
|
|
42
|
-
|
|
45
|
+
/**
|
|
46
|
+
* The `--json` document's shape version. 2 added the `outdated` status and a
|
|
47
|
+
* dep's `found` and `minVersion`.
|
|
48
|
+
*/
|
|
49
|
+
export const DEPS_REPORT_SCHEMA = 2;
|
|
43
50
|
|
|
44
51
|
export interface PluginParseProblem {
|
|
45
52
|
plugin: string;
|
|
@@ -187,6 +194,10 @@ export interface DepJson {
|
|
|
187
194
|
blockedBy?: string;
|
|
188
195
|
prefix?: string;
|
|
189
196
|
reason?: string;
|
|
197
|
+
/** Only when `status` is "outdated": the version the check reported. */
|
|
198
|
+
found?: string;
|
|
199
|
+
/** Only when `status` is "outdated": the lowest version that will do. */
|
|
200
|
+
minVersion?: string;
|
|
190
201
|
fix?: {
|
|
191
202
|
via: string;
|
|
192
203
|
escalation: "none" | "sudo";
|
|
@@ -227,11 +238,17 @@ export function depsReportJson(
|
|
|
227
238
|
if (p.status === "missing" && p.blockedBy) entry.blockedBy = p.blockedBy;
|
|
228
239
|
if (p.status === "off-path") entry.prefix = p.prefix;
|
|
229
240
|
if (p.status === "unknown") entry.reason = p.reason;
|
|
241
|
+
if (p.status === "outdated") {
|
|
242
|
+
entry.found = p.found;
|
|
243
|
+
entry.minVersion = p.minVersion;
|
|
244
|
+
}
|
|
230
245
|
if (p.status !== "present" && bin.chosen) {
|
|
231
|
-
const step = renderInstall(
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
246
|
+
const step = renderInstall(
|
|
247
|
+
bin,
|
|
248
|
+
bin.chosen,
|
|
249
|
+
{ ...env, pluginRoot: bin.root ?? "" },
|
|
250
|
+
installReason(p),
|
|
251
|
+
);
|
|
235
252
|
const prefix = prefixOf(step);
|
|
236
253
|
entry.fix = {
|
|
237
254
|
via: bin.chosen.via,
|
|
@@ -9,6 +9,7 @@
|
|
|
9
9
|
*/
|
|
10
10
|
|
|
11
11
|
import {
|
|
12
|
+
type InstallReason,
|
|
12
13
|
type Presence,
|
|
13
14
|
type ProbeDepth,
|
|
14
15
|
type Requirement,
|
|
@@ -68,6 +69,7 @@ export async function probeBins(
|
|
|
68
69
|
const reqs: Requirement[] = batch.map(({ bin }) => ({
|
|
69
70
|
name: bin.name,
|
|
70
71
|
check: expandCheck(bin, bin.root ?? ""),
|
|
72
|
+
...(bin.minVersion !== undefined ? { minVersion: bin.minVersion } : {}),
|
|
71
73
|
install: bin.install,
|
|
72
74
|
}));
|
|
73
75
|
const answers = await probe(reqs, {
|
|
@@ -93,13 +95,25 @@ export async function probeBins(
|
|
|
93
95
|
return out;
|
|
94
96
|
}
|
|
95
97
|
|
|
96
|
-
/**
|
|
97
|
-
|
|
98
|
+
/**
|
|
99
|
+
* The line a person runs to install `bin` here — or, `outdated`, to replace
|
|
100
|
+
* the copy that is too old — or why nothing can.
|
|
101
|
+
*/
|
|
102
|
+
export function installText(
|
|
103
|
+
bin: ResolvedBin,
|
|
104
|
+
env: PlanEnv,
|
|
105
|
+
reason: InstallReason = "missing",
|
|
106
|
+
): string {
|
|
98
107
|
if (!bin.chosen) {
|
|
99
108
|
return whyUnselectable(bin, env, (t) => onPath(t, env.path) !== null);
|
|
100
109
|
}
|
|
101
110
|
return describeStep(
|
|
102
|
-
renderInstall(
|
|
111
|
+
renderInstall(
|
|
112
|
+
bin,
|
|
113
|
+
bin.chosen,
|
|
114
|
+
{ ...env, pluginRoot: bin.root ?? "" },
|
|
115
|
+
reason,
|
|
116
|
+
),
|
|
103
117
|
env.os,
|
|
104
118
|
);
|
|
105
119
|
}
|
|
@@ -107,7 +121,10 @@ export function installText(bin: ResolvedBin, env: PlanEnv): string {
|
|
|
107
121
|
export interface BinCheckResult {
|
|
108
122
|
name: string;
|
|
109
123
|
presence: Presence;
|
|
110
|
-
/**
|
|
124
|
+
/**
|
|
125
|
+
* Advisory text after "run:" for a human to copy, an upgrade when the
|
|
126
|
+
* presence is `outdated`. Never executed.
|
|
127
|
+
*/
|
|
111
128
|
installCommand: string;
|
|
112
129
|
/** Where the requirement came from (profile cliTools / plugin ids). */
|
|
113
130
|
sources: string[];
|
|
@@ -119,17 +136,28 @@ export async function checkBinaries(
|
|
|
119
136
|
opts: ProbeBinsOptions,
|
|
120
137
|
): Promise<BinCheckResult[]> {
|
|
121
138
|
const presences = await probeBins(bins, opts);
|
|
122
|
-
return bins.map((bin, i) =>
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
139
|
+
return bins.map((bin, i) => {
|
|
140
|
+
const presence = presences[i] as Presence;
|
|
141
|
+
return {
|
|
142
|
+
name: bin.name,
|
|
143
|
+
presence,
|
|
144
|
+
installCommand: installText(bin, opts.env, installReason(presence)),
|
|
145
|
+
sources: bin.sources,
|
|
146
|
+
};
|
|
147
|
+
});
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
/** Why a requirement with this presence would be installed. */
|
|
151
|
+
export function installReason(presence: Presence): InstallReason {
|
|
152
|
+
return presence.status === "outdated" ? "outdated" : "missing";
|
|
128
153
|
}
|
|
129
154
|
|
|
130
|
-
/** The actionable subset: missing, or installed but not on PATH. */
|
|
155
|
+
/** The actionable subset: missing, outdated, or installed but not on PATH. */
|
|
131
156
|
export function missingBinaries(results: BinCheckResult[]): BinCheckResult[] {
|
|
132
157
|
return results.filter(
|
|
133
|
-
(r) =>
|
|
158
|
+
(r) =>
|
|
159
|
+
r.presence.status === "missing" ||
|
|
160
|
+
r.presence.status === "outdated" ||
|
|
161
|
+
r.presence.status === "off-path",
|
|
134
162
|
);
|
|
135
163
|
}
|
|
@@ -42,6 +42,8 @@ import {
|
|
|
42
42
|
OBSOLETE_GITIGNORE_ENTRIES,
|
|
43
43
|
PROFILE_GITIGNORE_COMMENT,
|
|
44
44
|
PROFILE_GITIGNORE_ENTRIES,
|
|
45
|
+
PROFILE_WORKTREE_INCLUDE_COMMENT,
|
|
46
|
+
PROFILE_WORKTREE_INCLUDE_ENTRIES,
|
|
45
47
|
assertLiveFilesParse,
|
|
46
48
|
backupRoot,
|
|
47
49
|
backupStamp,
|
|
@@ -324,6 +326,13 @@ export async function activateProfile(
|
|
|
324
326
|
remove: conversion.converted ? OBSOLETE_GITIGNORE_ENTRIES : [],
|
|
325
327
|
},
|
|
326
328
|
);
|
|
329
|
+
// Claude Code copies git-ignored files into a worktree it creates only when
|
|
330
|
+
// `.worktreeinclude` lists them — see PROFILE_WORKTREE_INCLUDE_ENTRIES.
|
|
331
|
+
await updateManagedGitignoreBlock(
|
|
332
|
+
path.join(projectPath, ".worktreeinclude"),
|
|
333
|
+
PROFILE_WORKTREE_INCLUDE_COMMENT,
|
|
334
|
+
{ add: PROFILE_WORKTREE_INCLUDE_ENTRIES },
|
|
335
|
+
);
|
|
327
336
|
|
|
328
337
|
const backupDir = path.join(backupRoot(projectPath), backupStamp());
|
|
329
338
|
const displaced: DisplacedArtifact[] = [];
|
|
@@ -72,6 +72,32 @@ export const PROFILE_GITIGNORE_ENTRIES = [
|
|
|
72
72
|
export const PROFILE_GITIGNORE_COMMENT =
|
|
73
73
|
"# magus: generated profile config (source of truth: .claude/profiles.json)";
|
|
74
74
|
|
|
75
|
+
/**
|
|
76
|
+
* Paths magus lists in a project's `.worktreeinclude`. Claude Code copies a
|
|
77
|
+
* file into a worktree it creates (`claude -w`, EnterWorktree, agent
|
|
78
|
+
* isolation) only when that file is both git-ignored and listed there, at
|
|
79
|
+
* creation time. magus git-ignores the set-up state above, so without this
|
|
80
|
+
* list every new worktree gets `.claude/profiles.json` from the commit and none
|
|
81
|
+
* of the project's plugins.
|
|
82
|
+
*
|
|
83
|
+
* The marker FILE is listed, not `.claude/profiles/`: the directory would also
|
|
84
|
+
* copy a crash-orphaned temp file. It holds only hashes and ids, so a
|
|
85
|
+
* byte-for-byte copy reconciles as unchanged in the worktree. Backups are never
|
|
86
|
+
* listed — they are this checkout's hand-written files, possibly credentials,
|
|
87
|
+
* and nothing in a worktree uses them. Each magus-installed skill folder gets
|
|
88
|
+
* its own line (see `profile-skills.ts`).
|
|
89
|
+
*/
|
|
90
|
+
export const PROFILE_WORKTREE_INCLUDE_ENTRIES = [
|
|
91
|
+
".claude/settings.json",
|
|
92
|
+
".mcp.json",
|
|
93
|
+
".claude/models.json",
|
|
94
|
+
".claude/profiles/active.json",
|
|
95
|
+
];
|
|
96
|
+
|
|
97
|
+
/** The comment heading magus's lines in a project's `.worktreeinclude`. */
|
|
98
|
+
export const PROFILE_WORKTREE_INCLUDE_COMMENT =
|
|
99
|
+
"# magus: copied into new worktrees so they start with this project's plugins (source of truth: .claude/profiles.json)";
|
|
100
|
+
|
|
75
101
|
/**
|
|
76
102
|
* Lines older releases wrote into magus's managed `.gitignore` block that must
|
|
77
103
|
* go when an older layout is converted: the blanket skills ignore hid committed
|
|
@@ -28,6 +28,7 @@ import type {
|
|
|
28
28
|
import { updateManagedGitignoreBlock } from "./conventions-manager.js";
|
|
29
29
|
import {
|
|
30
30
|
PROFILE_GITIGNORE_COMMENT,
|
|
31
|
+
PROFILE_WORKTREE_INCLUDE_COMMENT,
|
|
31
32
|
backupRoot,
|
|
32
33
|
backupStamp,
|
|
33
34
|
folderHash,
|
|
@@ -251,6 +252,9 @@ export async function inspectManagedSkills(
|
|
|
251
252
|
* `.gitignore` block: `before` are the folders the marker listed, `after` the
|
|
252
253
|
* ones it lists now. Lines are taken out of magus's block only — the same
|
|
253
254
|
* pattern a person wrote elsewhere in the file stays.
|
|
255
|
+
*
|
|
256
|
+
* The same lines go into magus's `.worktreeinclude` block, so a worktree
|
|
257
|
+
* Claude Code creates gets the folders the copied marker records.
|
|
254
258
|
*/
|
|
255
259
|
export async function updateSkillIgnoreLines(
|
|
256
260
|
projectPath: string,
|
|
@@ -258,13 +262,19 @@ export async function updateSkillIgnoreLines(
|
|
|
258
262
|
after: readonly string[],
|
|
259
263
|
): Promise<void> {
|
|
260
264
|
const keep = new Set(after);
|
|
265
|
+
const edits = {
|
|
266
|
+
add: [...keep].map(skillIgnoreLine),
|
|
267
|
+
remove: before.filter((f) => !keep.has(f)).map(skillIgnoreLine),
|
|
268
|
+
};
|
|
261
269
|
await updateManagedGitignoreBlock(
|
|
262
270
|
path.join(projectPath, ".gitignore"),
|
|
263
271
|
PROFILE_GITIGNORE_COMMENT,
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
272
|
+
edits,
|
|
273
|
+
);
|
|
274
|
+
await updateManagedGitignoreBlock(
|
|
275
|
+
path.join(projectPath, ".worktreeinclude"),
|
|
276
|
+
PROFILE_WORKTREE_INCLUDE_COMMENT,
|
|
277
|
+
edits,
|
|
268
278
|
);
|
|
269
279
|
}
|
|
270
280
|
|
|
@@ -28,6 +28,7 @@
|
|
|
28
28
|
import path from "node:path";
|
|
29
29
|
import type { ProfileManifest, ProfileManifestEntry } from "../types/index.js";
|
|
30
30
|
import { selectProfileId } from "./active-closure.js";
|
|
31
|
+
import { updateManagedGitignoreBlock } from "./conventions-manager.js";
|
|
31
32
|
import { readManifest, updateProfileEntry, writeManifest } from "./manifest.js";
|
|
32
33
|
import {
|
|
33
34
|
type ReconcileResult,
|
|
@@ -48,6 +49,8 @@ import {
|
|
|
48
49
|
type ActiveMarker,
|
|
49
50
|
LIVE_FILE_KEYS,
|
|
50
51
|
MACHINE_LOCAL_SETTINGS_KEYS,
|
|
52
|
+
PROFILE_WORKTREE_INCLUDE_COMMENT,
|
|
53
|
+
PROFILE_WORKTREE_INCLUDE_ENTRIES,
|
|
51
54
|
activeProfile,
|
|
52
55
|
folderHash,
|
|
53
56
|
hasCurrentMarker,
|
|
@@ -69,6 +72,7 @@ import {
|
|
|
69
72
|
SkillClashError,
|
|
70
73
|
inspectManagedSkills,
|
|
71
74
|
skillFolder,
|
|
75
|
+
skillIgnoreLine,
|
|
72
76
|
} from "./profile-skills.js";
|
|
73
77
|
import { resolveExtends } from "./resolver.js";
|
|
74
78
|
import type { SettingsParseError } from "./settings-file.js";
|
|
@@ -371,6 +375,20 @@ export async function prepareProfileState(
|
|
|
371
375
|
out.lines.push(...activationNotes(result, { tracked: false }));
|
|
372
376
|
out.activations.push(result);
|
|
373
377
|
}
|
|
378
|
+
// A project set up by a release that never wrote `.worktreeinclude`
|
|
379
|
+
// re-activates rarely, so the gate lists the set-up state here. A no-op
|
|
380
|
+
// when every line is already there.
|
|
381
|
+
const marker = await readActiveMarker(projectPath);
|
|
382
|
+
await updateManagedGitignoreBlock(
|
|
383
|
+
path.join(projectPath, ".worktreeinclude"),
|
|
384
|
+
PROFILE_WORKTREE_INCLUDE_COMMENT,
|
|
385
|
+
{
|
|
386
|
+
add: [
|
|
387
|
+
...PROFILE_WORKTREE_INCLUDE_ENTRIES,
|
|
388
|
+
...Object.keys(marker?.skills ?? {}).map(skillIgnoreLine),
|
|
389
|
+
],
|
|
390
|
+
},
|
|
391
|
+
);
|
|
374
392
|
out.check = { state: "active", profile, restored: false };
|
|
375
393
|
}
|
|
376
394
|
return out;
|
package/src/services/resolver.ts
CHANGED
|
@@ -314,6 +314,7 @@ export function requirementOf(bin: Requirement): Requirement {
|
|
|
314
314
|
name: bin.name,
|
|
315
315
|
check: bin.check,
|
|
316
316
|
...(bin.os ? { os: bin.os } : {}),
|
|
317
|
+
...(bin.minVersion !== undefined ? { minVersion: bin.minVersion } : {}),
|
|
317
318
|
install: bin.install,
|
|
318
319
|
};
|
|
319
320
|
}
|
|
@@ -6,8 +6,9 @@
|
|
|
6
6
|
* Reads the plugin's own `.claude-plugin/plugin.json` through
|
|
7
7
|
* lib/requires-probe.ts, probes each requirement at presence depth (a PATH
|
|
8
8
|
* lookup for a check that runs the requirement's own binary, the declared check
|
|
9
|
-
* for every other
|
|
10
|
-
*
|
|
9
|
+
* for every other, and for any requirement with a `minVersion`), and prints a
|
|
10
|
+
* `systemMessage` carrying the `magus-deps` marker when something is missing,
|
|
11
|
+
* outdated or off PATH. Prints nothing when healthy. Its "Fix all" line runs
|
|
11
12
|
* doctor with `--project <cwd>`, the session's directory from the hook input:
|
|
12
13
|
* doctor reads the plugins enabled in the project it checks, and the banner is
|
|
13
14
|
* read in a terminal that may be anywhere.
|
|
@@ -200,8 +201,11 @@ async function main(): Promise<void> {
|
|
|
200
201
|
userShell: process.env.SHELL ?? "",
|
|
201
202
|
};
|
|
202
203
|
const presences = await probe(reqs, env);
|
|
204
|
+
// Outdated is a finding like missing: claimed once, reported once.
|
|
203
205
|
const reportable = (p: Presence) =>
|
|
204
|
-
p.status === "missing" ||
|
|
206
|
+
p.status === "missing" ||
|
|
207
|
+
p.status === "outdated" ||
|
|
208
|
+
p.status === "off-path";
|
|
205
209
|
if (!presences.some(reportable)) return;
|
|
206
210
|
|
|
207
211
|
const byName = new Map(reqs.map((r) => [r.name, r]));
|