magus-cli 7.5.2 → 7.5.3
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__/gitignore-check.test.ts +11 -29
- package/src/__tests__/gitignore-profile.test.ts +248 -0
- package/src/__tests__/models-cli.test.ts +11 -0
- package/src/__tests__/profile-skills.test.ts +16 -21
- package/src/__tests__/profile-state.test.ts +132 -149
- package/src/cli/bootstrap.ts +0 -4
- package/src/cli/doctor.ts +6 -20
- package/src/cli/router.ts +5 -4
- package/src/cli/update.ts +7 -0
- package/src/services/claude-cli.ts +0 -24
- package/src/services/claude-settings.ts +0 -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/profile-absorb.ts +31 -17
- package/src/services/profile-activate.ts +74 -120
- package/src/services/profile-state.ts +0 -62
- package/src/services/tracked-live-files.ts +88 -74
- package/src/ui/App.tsx +34 -33
- package/src/ui/hooks/useGitignoreModal.ts +63 -17
- package/src/ui/hooks/useProfileStartupModal.ts +7 -2
- package/src/ui/screens/PluginsScreen.tsx +2 -11
- package/src/ui/screens/ProfilesScreen.tsx +3 -10
|
@@ -13,7 +13,7 @@ import { ensureManifest, profileGate } from "../cli/bootstrap.js";
|
|
|
13
13
|
import { reportProfileState, runDoctorCommand } from "../cli/doctor.js";
|
|
14
14
|
import { runModelsCommand } from "../cli/models.js";
|
|
15
15
|
import { route } from "../cli/router.js";
|
|
16
|
-
import {
|
|
16
|
+
import { runUpdateCommand } from "../cli/update.js";
|
|
17
17
|
import {
|
|
18
18
|
readSettings,
|
|
19
19
|
writeMcpConfig,
|
|
@@ -25,6 +25,7 @@ import type { ModelsConfig } from "../services/models-core.js";
|
|
|
25
25
|
import { reconcileActiveProfile } from "../services/profile-absorb.js";
|
|
26
26
|
import {
|
|
27
27
|
activateProfile,
|
|
28
|
+
activationNotes,
|
|
28
29
|
regenerateLiveFiles,
|
|
29
30
|
trackedGeneratedFiles,
|
|
30
31
|
} from "../services/profile-activate.js";
|
|
@@ -44,10 +45,6 @@ import {
|
|
|
44
45
|
switchToProfile,
|
|
45
46
|
} from "../services/profile-state.js";
|
|
46
47
|
import { applyUserProfile } from "../services/profiles.js";
|
|
47
|
-
import {
|
|
48
|
-
GitCheckError,
|
|
49
|
-
TrackedLiveFileError,
|
|
50
|
-
} from "../services/tracked-live-files.js";
|
|
51
48
|
import type { ProfileManifest } from "../types/index.js";
|
|
52
49
|
|
|
53
50
|
const MANIFEST: ProfileManifest = {
|
|
@@ -686,7 +683,7 @@ function git(cwd: string, ...args: string[]) {
|
|
|
686
683
|
);
|
|
687
684
|
}
|
|
688
685
|
|
|
689
|
-
describe("generated files git still tracks", () => {
|
|
686
|
+
describe("generated files git still tracks: written, and warned about", () => {
|
|
690
687
|
/** A repo that committed profiles.json AND settings.json, like magus-src. */
|
|
691
688
|
async function commitTrackedSettings(): Promise<void> {
|
|
692
689
|
expect(git(project, "init", "-q").status).toBe(0);
|
|
@@ -698,61 +695,63 @@ describe("generated files git still tracks", () => {
|
|
|
698
695
|
expect(git(project, "commit", "-q", "-m", "init").status).toBe(0);
|
|
699
696
|
}
|
|
700
697
|
|
|
701
|
-
/**
|
|
702
|
-
|
|
703
|
-
expect(git(project, "
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
expect(await fs.pathExists(backupRoot(project))).toBe(false);
|
|
708
|
-
expect(await fs.pathExists(join(project, ".gitignore"))).toBe(false);
|
|
709
|
-
}
|
|
698
|
+
/** magus never runs git: nothing is staged, whatever it wrote. */
|
|
699
|
+
const nothingStaged = () =>
|
|
700
|
+
expect(git(project, "diff", "--cached", "--name-only").stdout).toBe("");
|
|
701
|
+
|
|
702
|
+
const WARNING =
|
|
703
|
+
".claude/settings.json is generated from .claude/profiles.json but tracked by git — run `git rm --cached .claude/settings.json`, then commit";
|
|
710
704
|
|
|
711
|
-
test("
|
|
705
|
+
test("B7: activating over a tracked settings.json writes it and names the fix", async () => {
|
|
712
706
|
await commitTrackedSettings();
|
|
713
707
|
|
|
714
|
-
const
|
|
708
|
+
const result = await activateProfile(MANIFEST, "team", project, {
|
|
715
709
|
onDisplace: () => {},
|
|
716
|
-
})
|
|
717
|
-
() => null,
|
|
718
|
-
(e: unknown) => e,
|
|
719
|
-
);
|
|
710
|
+
});
|
|
720
711
|
|
|
721
|
-
expect(
|
|
722
|
-
|
|
723
|
-
expect(
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
712
|
+
expect(result.tracked).toEqual([".claude/settings.json"]);
|
|
713
|
+
expect(activationNotes(result)).toContain(WARNING);
|
|
714
|
+
expect((await fs.readJson(settingsPath())).enabledPlugins).toEqual({
|
|
715
|
+
"dev@magus": true,
|
|
716
|
+
"terminal@magus": false,
|
|
717
|
+
});
|
|
718
|
+
expect((await readActiveMarker(project))?.profile).toBe("team");
|
|
719
|
+
expect(git(project, "status", "--porcelain").stdout).toContain(
|
|
720
|
+
" M .claude/settings.json",
|
|
721
|
+
);
|
|
722
|
+
nothingStaged();
|
|
727
723
|
});
|
|
728
724
|
|
|
729
|
-
test("
|
|
725
|
+
test("B7: the gate, the switch path and a startup choice all write", async () => {
|
|
730
726
|
await commitTrackedSettings();
|
|
731
727
|
|
|
732
|
-
const prep = await prepareProfileState(project);
|
|
733
|
-
expect(prep.refusal).toBeInstanceOf(TrackedLiveFileError);
|
|
734
|
-
expect(prep.problems[0]).toContain("git rm --cached .claude/settings.json");
|
|
735
|
-
expect(await profileGate(project)).toBe(false);
|
|
736
|
-
|
|
737
|
-
const switched = await switchToProfile(project, "other", {
|
|
738
|
-
onDisplace: () => {},
|
|
739
|
-
});
|
|
740
|
-
expect(switched.activation).toBeNull();
|
|
741
|
-
expect(switched.problems[0]).toContain(".claude/settings.json");
|
|
742
|
-
|
|
743
728
|
for (const choice of [
|
|
744
729
|
{ kind: "activate", id: "team" },
|
|
745
730
|
{ kind: "adopt", id: "default-2" },
|
|
746
731
|
] as const) {
|
|
747
|
-
await
|
|
748
|
-
|
|
749
|
-
)
|
|
732
|
+
const result = await applyProfileChoice(choice, MANIFEST, project, {
|
|
733
|
+
onDisplace: () => {},
|
|
734
|
+
});
|
|
735
|
+
expect(result.profileId).toBe(choice.id);
|
|
750
736
|
}
|
|
751
|
-
await
|
|
737
|
+
const prep = await prepareProfileState(project);
|
|
738
|
+
expect(prep.problems).toEqual([]);
|
|
739
|
+
expect(await profileGate(project)).toBe(true);
|
|
740
|
+
|
|
741
|
+
const switched = await switchToProfile(project, "other", {
|
|
742
|
+
onDisplace: () => {},
|
|
743
|
+
});
|
|
744
|
+
expect(switched.problems).toEqual([]);
|
|
745
|
+
expect(switched.activation?.profileId).toBe("other");
|
|
746
|
+
expect(
|
|
747
|
+
(await fs.readJson(settingsPath())).enabledPlugins["terminal@magus"],
|
|
748
|
+
).toBe(true);
|
|
749
|
+
nothingStaged();
|
|
752
750
|
});
|
|
753
751
|
|
|
754
|
-
test("
|
|
752
|
+
test("B7: `magus profile switch` exits 0 and warns once, running no git", async () => {
|
|
755
753
|
await commitTrackedSettings();
|
|
754
|
+
await activateProfile(MANIFEST, "team", project, { onDisplace: () => {} });
|
|
756
755
|
const cwd = process.cwd();
|
|
757
756
|
process.chdir(project);
|
|
758
757
|
let outcome: Awaited<ReturnType<typeof route>>;
|
|
@@ -761,63 +760,47 @@ describe("generated files git still tracks", () => {
|
|
|
761
760
|
} finally {
|
|
762
761
|
process.chdir(cwd);
|
|
763
762
|
}
|
|
764
|
-
expect(outcome.exitCode).toBe(
|
|
765
|
-
expect(printed()).
|
|
766
|
-
await
|
|
763
|
+
expect(outcome.exitCode).toBe(0);
|
|
764
|
+
expect(printed().split(WARNING).length - 1).toBe(1);
|
|
765
|
+
expect((await readActiveMarker(project))?.profile).toBe("other");
|
|
766
|
+
nothingStaged();
|
|
767
|
+
});
|
|
768
|
+
|
|
769
|
+
test("B7: `magus update` warns once about a tracked generated file, running no git", async () => {
|
|
770
|
+
await commitTrackedSettings();
|
|
771
|
+
await activateProfile(MANIFEST, "team", project, { onDisplace: () => {} });
|
|
772
|
+
// A named profile that does not exist: the command stops right after the
|
|
773
|
+
// warning, before anything reaches the network.
|
|
774
|
+
expect(await runUpdateCommand(["no-such-profile"], project)).toBe(1);
|
|
775
|
+
expect(printed().split(WARNING).length - 1).toBe(1);
|
|
776
|
+
nothingStaged();
|
|
767
777
|
});
|
|
768
778
|
|
|
769
|
-
test("
|
|
779
|
+
test("B7: once settings.json is tracked, a pulled change is still regenerated", async () => {
|
|
770
780
|
expect(git(project, "init", "-q").status).toBe(0);
|
|
771
781
|
await activateProfile(MANIFEST, "team", project, { onDisplace: () => {} });
|
|
772
782
|
// Somebody commits the generated file after all.
|
|
773
783
|
git(project, "add", "-f", ".claude/settings.json");
|
|
774
784
|
git(project, "add", ".claude/profiles.json");
|
|
775
785
|
expect(git(project, "commit", "-q", "-m", "oops").status).toBe(0);
|
|
776
|
-
const before = await fs.readFile(settingsPath(), "utf8");
|
|
777
786
|
// A teammate's pull changes the profile.
|
|
778
787
|
const pulled = structuredClone(MANIFEST);
|
|
779
788
|
(pulled.profiles.team as { plugins: Record<string, string> }).plugins[
|
|
780
789
|
"mnemex@magus"
|
|
781
790
|
] = "latest";
|
|
782
791
|
await writeManifest(pulled, project);
|
|
783
|
-
const manifestBefore = await fs.readFile(
|
|
784
|
-
join(project, ".claude", "profiles.json"),
|
|
785
|
-
"utf8",
|
|
786
|
-
);
|
|
787
792
|
|
|
788
793
|
const prep = await prepareProfileState(project);
|
|
789
|
-
expect(prep.problems[0]).toContain("git rm --cached .claude/settings.json");
|
|
790
|
-
expect((await reconcileActiveProfile(project)).problems[0]).toContain(
|
|
791
|
-
".claude/settings.json",
|
|
792
|
-
);
|
|
793
|
-
await expect(regenerateLiveFiles(project)).rejects.toBeInstanceOf(
|
|
794
|
-
TrackedLiveFileError,
|
|
795
|
-
);
|
|
796
|
-
expect(await fs.readFile(settingsPath(), "utf8")).toBe(before);
|
|
797
|
-
expect(
|
|
798
|
-
await fs.readFile(join(project, ".claude", "profiles.json"), "utf8"),
|
|
799
|
-
).toBe(manifestBefore);
|
|
800
|
-
});
|
|
801
794
|
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
git(project, "rm", "-q", "--cached", ".claude/settings.json");
|
|
805
|
-
expect(git(project, "commit", "-q", "-m", "untrack").status).toBe(0);
|
|
806
|
-
const manifestBytes = await fs.readFile(
|
|
807
|
-
join(project, ".claude", "profiles.json"),
|
|
808
|
-
"utf8",
|
|
809
|
-
);
|
|
810
|
-
|
|
811
|
-
await activateProfile(MANIFEST, "team", project, { onDisplace: () => {} });
|
|
812
|
-
|
|
813
|
-
expect((await lstat(settingsPath())).isFile()).toBe(true);
|
|
814
|
-
expect((await readActiveMarker(project))?.profile).toBe("team");
|
|
795
|
+
expect(prep.problems).toEqual([]);
|
|
796
|
+
expect(prep.lines.join("\n")).toContain("regenerated");
|
|
815
797
|
expect(
|
|
816
|
-
await fs.
|
|
817
|
-
).toBe(
|
|
798
|
+
(await fs.readJson(settingsPath())).enabledPlugins["mnemex@magus"],
|
|
799
|
+
).toBe(true);
|
|
800
|
+
nothingStaged();
|
|
818
801
|
});
|
|
819
802
|
|
|
820
|
-
test("
|
|
803
|
+
test("B7: adopting a repo with no profiles.json and a tracked settings.json writes and warns", async () => {
|
|
821
804
|
await fs.remove(join(project, ".claude", "profiles.json"));
|
|
822
805
|
expect(git(project, "init", "-q").status).toBe(0);
|
|
823
806
|
await fs.outputJson(settingsPath(), {
|
|
@@ -829,112 +812,112 @@ describe("generated files git still tracks", () => {
|
|
|
829
812
|
const savedHome = process.env.HOME;
|
|
830
813
|
process.env.HOME = home;
|
|
831
814
|
try {
|
|
832
|
-
|
|
833
|
-
ensureManifest(project, { allowAdopt: true }),
|
|
834
|
-
).
|
|
815
|
+
expect(
|
|
816
|
+
await ensureManifest(project, { allowAdopt: true }),
|
|
817
|
+
).not.toBeNull();
|
|
835
818
|
} finally {
|
|
836
819
|
process.env.HOME = savedHome;
|
|
837
820
|
await rm(home, { recursive: true, force: true });
|
|
838
821
|
}
|
|
839
|
-
|
|
840
|
-
|
|
822
|
+
const manifest = await readManifest(project);
|
|
823
|
+
expect(Object.keys(manifest.profiles)).toHaveLength(1);
|
|
824
|
+
expect(await activeProfile(project)).toBe(
|
|
825
|
+
Object.keys(manifest.profiles)[0],
|
|
841
826
|
);
|
|
842
|
-
|
|
827
|
+
expect(printed()).toContain(WARNING);
|
|
828
|
+
nothingStaged();
|
|
843
829
|
});
|
|
844
830
|
|
|
845
|
-
test("
|
|
831
|
+
test("B7: doctor notes a tracked live file with the fix, and does not count it", async () => {
|
|
846
832
|
await commitTrackedSettings();
|
|
833
|
+
await activateProfile(MANIFEST, "team", project, { onDisplace: () => {} });
|
|
847
834
|
const counts = await reportProfileState(project, false);
|
|
848
|
-
expect(counts.unfixable).
|
|
849
|
-
expect(printed()).toContain(
|
|
850
|
-
await untouched();
|
|
835
|
+
expect(counts.unfixable).toBe(0);
|
|
836
|
+
expect(printed()).toContain(`• ${WARNING}`);
|
|
851
837
|
});
|
|
852
838
|
|
|
853
|
-
test("
|
|
839
|
+
test("B7: writeSettings and writeMcpConfig write a tracked live file", async () => {
|
|
854
840
|
await commitTrackedSettings();
|
|
855
841
|
await fs.outputJson(join(project, ".mcp.json"), { mcpServers: {} });
|
|
856
842
|
git(project, "add", ".mcp.json");
|
|
857
843
|
expect(git(project, "commit", "-q", "-m", "mcp").status).toBe(0);
|
|
858
|
-
const before = await fs.readFile(settingsPath(), "utf8");
|
|
859
844
|
|
|
860
845
|
// What the TUI Profiles tab's "apply predefined" does.
|
|
861
846
|
const settings = await readSettings(project);
|
|
862
|
-
(settings.enabledPlugins as Record<string, boolean>)["frontend@magus"] =
|
|
863
|
-
true;
|
|
864
|
-
await expect(writeSettings(settings, project)).rejects.toBeInstanceOf(
|
|
865
|
-
TrackedLiveFileError,
|
|
866
|
-
);
|
|
867
|
-
await expect(
|
|
868
|
-
writeMcpConfig({ mcpServers: { x: { command: "x" } } }, project),
|
|
869
|
-
).rejects.toBeInstanceOf(TrackedLiveFileError);
|
|
870
|
-
|
|
871
|
-
expect(await fs.readFile(settingsPath(), "utf8")).toBe(before);
|
|
872
|
-
await untouched();
|
|
873
|
-
});
|
|
874
|
-
|
|
875
|
-
test("review M1: a project-scope Claude CLI install or uninstall is refused before the CLI runs", async () => {
|
|
876
|
-
await commitTrackedSettings();
|
|
877
|
-
const cwd = process.cwd();
|
|
878
|
-
const pathBefore = process.env.PATH;
|
|
879
|
-
// git stays reachable; `claude` does not — were the refusal missing, the
|
|
880
|
-
// CLI could not run here and touch this machine's plugins anyway.
|
|
881
|
-
const gitDir = spawnSync("sh", ["-c", "command -v git"], {
|
|
882
|
-
encoding: "utf8",
|
|
883
|
-
}).stdout.trim();
|
|
884
|
-
process.env.PATH = gitDir.slice(0, gitDir.lastIndexOf("/"));
|
|
885
|
-
process.chdir(project);
|
|
886
|
-
try {
|
|
887
|
-
await expect(
|
|
888
|
-
installPlugin("dev@magus", "project"),
|
|
889
|
-
).rejects.toBeInstanceOf(TrackedLiveFileError);
|
|
890
|
-
await expect(
|
|
891
|
-
uninstallPlugin("dev@magus", "project", project),
|
|
892
|
-
).rejects.toBeInstanceOf(TrackedLiveFileError);
|
|
893
|
-
} finally {
|
|
894
|
-
process.chdir(cwd);
|
|
895
|
-
process.env.PATH = pathBefore;
|
|
896
|
-
}
|
|
897
|
-
await untouched();
|
|
898
|
-
});
|
|
899
|
-
|
|
900
|
-
test("review M1: a project without profiles writes its own settings.json as before", async () => {
|
|
901
|
-
await commitTrackedSettings();
|
|
902
|
-
await writeManifest({ version: 2, profiles: {} }, project);
|
|
903
|
-
const settings = await readSettings(project);
|
|
904
847
|
(settings.enabledPlugins as Record<string, boolean>)["frontend@magus"] =
|
|
905
848
|
true;
|
|
906
849
|
await writeSettings(settings, project);
|
|
850
|
+
await writeMcpConfig({ mcpServers: { x: { command: "x" } } }, project);
|
|
851
|
+
|
|
907
852
|
expect(
|
|
908
853
|
(await fs.readJson(settingsPath())).enabledPlugins["frontend@magus"],
|
|
909
854
|
).toBe(true);
|
|
855
|
+
expect((await fs.readJson(join(project, ".mcp.json"))).mcpServers).toEqual({
|
|
856
|
+
x: { command: "x" },
|
|
857
|
+
});
|
|
858
|
+
nothingStaged();
|
|
910
859
|
});
|
|
911
860
|
|
|
912
|
-
test("
|
|
861
|
+
test("B7: inside a work tree git cannot read, activation writes and says git could not be asked", async () => {
|
|
913
862
|
// A .git pointing at a repository that is not there: git exits 128.
|
|
914
863
|
await fs.outputFile(
|
|
915
864
|
join(project, ".git"),
|
|
916
865
|
`gitdir: ${join(project, "no-such-repository")}\n`,
|
|
917
866
|
);
|
|
918
867
|
|
|
919
|
-
const
|
|
868
|
+
const result = await activateProfile(MANIFEST, "team", project, {
|
|
920
869
|
onDisplace: () => {},
|
|
921
|
-
})
|
|
922
|
-
|
|
923
|
-
|
|
870
|
+
});
|
|
871
|
+
|
|
872
|
+
expect(await fs.pathExists(settingsPath())).toBe(true);
|
|
873
|
+
expect(typeof result.tracked).toBe("string");
|
|
874
|
+
expect(activationNotes(result).join("\n")).toContain(
|
|
875
|
+
"magus could not check which files git tracks in this project",
|
|
924
876
|
);
|
|
877
|
+
expect((await prepareProfileState(project)).problems).toEqual([]);
|
|
878
|
+
});
|
|
925
879
|
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
|
|
931
|
-
|
|
880
|
+
test("B7 review L3: converting an older layout where git cannot answer exits 1 with git's message, no stack trace", async () => {
|
|
881
|
+
// 7.4.1 layout, a listed skill linked: the conversion must ask git which
|
|
882
|
+
// skill folders the team committed. `--dry-run` skips the entry gate, so
|
|
883
|
+
// the error reaches the router's own handler.
|
|
884
|
+
const withSkill = structuredClone(MANIFEST);
|
|
885
|
+
(withSkill.profiles.team as { skills: unknown[] }).skills = [
|
|
886
|
+
{ name: "alpha", repo: "a/b", path: "skills/alpha" },
|
|
887
|
+
];
|
|
888
|
+
await writeManifest(withSkill, project);
|
|
889
|
+
const old = join(project, ".claude", "_profiles", "team");
|
|
890
|
+
await fs.outputFile(join(old, "skills", "alpha", "SKILL.md"), "# alpha\n");
|
|
891
|
+
await fs.outputJson(join(old, "settings.json"), {
|
|
892
|
+
enabledPlugins: { "dev@magus": true },
|
|
893
|
+
});
|
|
894
|
+
await fs.symlink("_profiles/team/settings.json", settingsPath());
|
|
895
|
+
await fs.symlink(
|
|
896
|
+
"_profiles/team/skills",
|
|
897
|
+
join(project, ".claude", "skills"),
|
|
898
|
+
);
|
|
899
|
+
await fs.outputFile(
|
|
900
|
+
join(project, ".git"),
|
|
901
|
+
`gitdir: ${join(project, "no-such-repository")}\n`,
|
|
902
|
+
);
|
|
903
|
+
const cwd = process.cwd();
|
|
904
|
+
process.chdir(project);
|
|
905
|
+
let outcome: Awaited<ReturnType<typeof route>>;
|
|
906
|
+
try {
|
|
907
|
+
outcome = await route(
|
|
908
|
+
["profile", "switch", "team", "--dry-run"],
|
|
909
|
+
"0.0.0-test",
|
|
910
|
+
);
|
|
911
|
+
} finally {
|
|
912
|
+
process.chdir(cwd);
|
|
913
|
+
}
|
|
914
|
+
expect(outcome.exitCode).toBe(1);
|
|
915
|
+
expect(printed()).toContain(
|
|
916
|
+
"magus could not check which files git tracks in this project. git said:",
|
|
932
917
|
);
|
|
933
|
-
const prep = await prepareProfileState(project);
|
|
934
|
-
expect(prep.refusal).toBeInstanceOf(GitCheckError);
|
|
935
918
|
});
|
|
936
919
|
|
|
937
|
-
test("
|
|
920
|
+
test("git missing is an error inside a work tree, and no check outside one", async () => {
|
|
938
921
|
expect(git(project, "init", "-q").status).toBe(0);
|
|
939
922
|
const outside = await mkdtemp(join(tmpdir(), "no-git-"));
|
|
940
923
|
const pathBefore = process.env.PATH;
|
package/src/cli/bootstrap.ts
CHANGED
|
@@ -18,7 +18,6 @@ import {
|
|
|
18
18
|
type ActivationResult,
|
|
19
19
|
activateProfile,
|
|
20
20
|
activationNotes,
|
|
21
|
-
assertNoTrackedWriteTargets,
|
|
22
21
|
reportDisplaced,
|
|
23
22
|
} from "../services/profile-activate.js";
|
|
24
23
|
import {
|
|
@@ -84,9 +83,6 @@ async function writeAndActivate(
|
|
|
84
83
|
// their hand-written files went.
|
|
85
84
|
const rel = (p: string) => path.relative(projectPath, p);
|
|
86
85
|
|
|
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
86
|
await writeManifest(result.manifest, projectPath);
|
|
91
87
|
console.log(`✓ Wrote ${rel(getManifestPath(projectPath))}`);
|
|
92
88
|
|
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 { trackedGeneratedWarnings } 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,11 @@ 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
|
-
} catch (error) {
|
|
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 line of await trackedGeneratedWarnings(projectPath)) {
|
|
559
|
+
console.log(`• ${line}`);
|
|
574
560
|
}
|
|
575
561
|
return { fixable, unfixable };
|
|
576
562
|
}
|
package/src/cli/router.ts
CHANGED
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
|
|
9
9
|
import type { ThemeMode } from "@opentui/core";
|
|
10
10
|
import { SettingsParseError } from "../services/settings-file.js";
|
|
11
|
-
import {
|
|
11
|
+
import { GitCheckError } from "../services/tracked-live-files.js";
|
|
12
12
|
import { checkForUpdates } from "../services/version-check.js";
|
|
13
13
|
import { type ThemeEnv, parseThemeWord } from "../ui/theme-resolve.js";
|
|
14
14
|
import { profileGate, reportUnparseable } from "./bootstrap.js";
|
|
@@ -224,9 +224,10 @@ export async function route(
|
|
|
224
224
|
reportUnparseable(error);
|
|
225
225
|
return HANDLED(1);
|
|
226
226
|
}
|
|
227
|
-
//
|
|
228
|
-
//
|
|
229
|
-
|
|
227
|
+
// git could not say what it tracks, and converting an older layout needs
|
|
228
|
+
// to know which skill folders the team committed. git's own message, not
|
|
229
|
+
// a stack trace.
|
|
230
|
+
if (error instanceof GitCheckError) {
|
|
230
231
|
console.error(error.message);
|
|
231
232
|
return HANDLED(1);
|
|
232
233
|
}
|
package/src/cli/update.ts
CHANGED
|
@@ -51,6 +51,7 @@ import {
|
|
|
51
51
|
import {
|
|
52
52
|
recordRefetchedSkills,
|
|
53
53
|
trackFetchedSkills,
|
|
54
|
+
trackedGeneratedWarnings,
|
|
54
55
|
updatableSkills,
|
|
55
56
|
} from "../services/profile-activate.js";
|
|
56
57
|
import { activeProfile } from "../services/profile-live.js";
|
|
@@ -202,6 +203,12 @@ export async function runUpdateCommand(
|
|
|
202
203
|
return 1;
|
|
203
204
|
}
|
|
204
205
|
|
|
206
|
+
// A generated file git tracks is written like any other; say so once, with
|
|
207
|
+
// the fix. magus never runs git itself.
|
|
208
|
+
for (const line of await trackedGeneratedWarnings(projectPath)) {
|
|
209
|
+
console.log(warnLine(line));
|
|
210
|
+
}
|
|
211
|
+
|
|
205
212
|
// No active profile, no update: that is where plugin state is read from and
|
|
206
213
|
// where changes are written. Asked before the profile is resolved, so what
|
|
207
214
|
// the user activates here is what this run uses.
|
|
@@ -10,7 +10,6 @@
|
|
|
10
10
|
*/
|
|
11
11
|
|
|
12
12
|
import { execFile } from "node:child_process";
|
|
13
|
-
import path from "node:path";
|
|
14
13
|
import { promisify } from "node:util";
|
|
15
14
|
import { defaultMarketplaces } from "../data/marketplaces.js";
|
|
16
15
|
import { which } from "../utils/command-utils.js";
|
|
@@ -20,7 +19,6 @@ import {
|
|
|
20
19
|
removeLocalInstalledPluginVersion,
|
|
21
20
|
} from "./claude-settings.js";
|
|
22
21
|
import { removeInstalledPluginVersion } from "./plugin-manager.js";
|
|
23
|
-
import { refuseTrackedLiveFiles } from "./tracked-live-files.js";
|
|
24
22
|
|
|
25
23
|
const execFileAsync = promisify(execFile);
|
|
26
24
|
|
|
@@ -140,23 +138,6 @@ async function execPluginCommand(
|
|
|
140
138
|
}
|
|
141
139
|
}
|
|
142
140
|
|
|
143
|
-
/**
|
|
144
|
-
* Every `claude plugin … --scope project` command writes the project's
|
|
145
|
-
* `.claude/settings.json` (its enabledPlugins entry). In a project with
|
|
146
|
-
* profiles that file is generated; where git tracks it, refuse — before the
|
|
147
|
-
* CLI runs — rather than let the CLI write it (see `tracked-live-files.ts`).
|
|
148
|
-
* The CLI works on the current directory.
|
|
149
|
-
*/
|
|
150
|
-
async function refuseProjectSettingsWrite(
|
|
151
|
-
scope: PluginScope,
|
|
152
|
-
projectPath: string = process.cwd(),
|
|
153
|
-
): Promise<void> {
|
|
154
|
-
if (scope !== "project") return;
|
|
155
|
-
await refuseTrackedLiveFiles(projectPath, [
|
|
156
|
-
path.join(".claude", "settings.json"),
|
|
157
|
-
]);
|
|
158
|
-
}
|
|
159
|
-
|
|
160
141
|
/**
|
|
161
142
|
* Install a plugin using claude CLI
|
|
162
143
|
* Handles enabling + version tracking + cache copy in one shot.
|
|
@@ -173,7 +154,6 @@ export async function installPlugin(
|
|
|
173
154
|
pluginId: string,
|
|
174
155
|
scope: PluginScope = "user",
|
|
175
156
|
): Promise<void> {
|
|
176
|
-
await refuseProjectSettingsWrite(scope);
|
|
177
157
|
// 60s, matching update and repair. Before the install/update split this path
|
|
178
158
|
// ran through `updatePlugin`'s 60s budget; leaving it on the 30s default
|
|
179
159
|
// would have quietly halved the time a fresh install — the SLOWEST of the
|
|
@@ -192,7 +172,6 @@ export async function uninstallPlugin(
|
|
|
192
172
|
scope: PluginScope = "user",
|
|
193
173
|
projectPath?: string,
|
|
194
174
|
): Promise<void> {
|
|
195
|
-
await refuseProjectSettingsWrite(scope, projectPath);
|
|
196
175
|
try {
|
|
197
176
|
await execClaude(["plugin", "uninstall", pluginId, "--scope", scope]);
|
|
198
177
|
} catch {
|
|
@@ -245,7 +224,6 @@ export async function enablePlugin(
|
|
|
245
224
|
pluginId: string,
|
|
246
225
|
scope: PluginScope = "user",
|
|
247
226
|
): Promise<void> {
|
|
248
|
-
await refuseProjectSettingsWrite(scope);
|
|
249
227
|
await execClaude(["plugin", "enable", pluginId, "--scope", scope]);
|
|
250
228
|
}
|
|
251
229
|
|
|
@@ -256,7 +234,6 @@ export async function disablePlugin(
|
|
|
256
234
|
pluginId: string,
|
|
257
235
|
scope: PluginScope = "user",
|
|
258
236
|
): Promise<void> {
|
|
259
|
-
await refuseProjectSettingsWrite(scope);
|
|
260
237
|
await execClaude(["plugin", "disable", pluginId, "--scope", scope]);
|
|
261
238
|
}
|
|
262
239
|
|
|
@@ -299,7 +276,6 @@ export async function updatePlugin(
|
|
|
299
276
|
pluginId: string,
|
|
300
277
|
scope: PluginScope = "user",
|
|
301
278
|
): Promise<void> {
|
|
302
|
-
await refuseProjectSettingsWrite(scope);
|
|
303
279
|
await execPluginCommand("update", pluginId, scope, 60000);
|
|
304
280
|
}
|
|
305
281
|
|
|
@@ -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
|
}
|