claudeup 6.2.0 → 6.3.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/README.md +22 -0
- package/bin/claudeup.js +6 -1
- package/package.json +6 -6
- package/src/__tests__/alias-adopt.test.ts +331 -329
- package/src/__tests__/alias-parser.test.ts +384 -376
- package/src/__tests__/alias-shell-writer.test.ts +638 -625
- package/src/__tests__/alias-store.test.ts +143 -143
- package/src/__tests__/catalog-cache-store.test.ts +265 -265
- package/src/__tests__/catalog-notice.test.ts +123 -123
- package/src/__tests__/cli-apply-seams.test.ts +12 -8
- package/src/__tests__/cli-router.test.ts +132 -3
- package/src/__tests__/cli-update-view.test.ts +27 -8
- package/src/__tests__/community-staleness.test.ts +0 -2
- package/src/__tests__/component-badge.test.ts +4 -4
- package/src/__tests__/content-drift.test.ts +5 -1
- package/src/__tests__/conventions-integration.test.ts +774 -689
- package/src/__tests__/conventions-manager.test.ts +1216 -1152
- package/src/__tests__/doctor-bins.test.ts +14 -6
- package/src/__tests__/doctor.test.ts +438 -425
- package/src/__tests__/dual-write-prevention.test.ts +277 -282
- package/src/__tests__/enabled-not-installed.test.ts +71 -63
- package/src/__tests__/file-locking.test.ts +196 -196
- package/src/__tests__/gap-fill-versions.test.ts +318 -311
- package/src/__tests__/github-budget.test.ts +170 -164
- package/src/__tests__/gitignore-detector.test.ts +160 -152
- package/src/__tests__/gitignore-fixer.test.ts +285 -247
- package/src/__tests__/gitignore-prerun.test.ts +57 -57
- package/src/__tests__/gitignore-resolver.test.ts +173 -146
- package/src/__tests__/gitignore-service.test.ts +121 -119
- package/src/__tests__/go-module.test.ts +43 -0
- package/src/__tests__/install-command.test.ts +3 -3
- package/src/__tests__/install-plan.test.ts +13 -7
- package/src/__tests__/manifest.test.ts +16 -5
- package/src/__tests__/markdown-renderer.test.ts +0 -1
- package/src/__tests__/marketplace-badge.test.ts +111 -90
- package/src/__tests__/marketplace-version.test.ts +64 -61
- package/src/__tests__/plugin-manager-fallback.test.ts +249 -239
- package/src/__tests__/plugin-requires.test.ts +55 -1
- package/src/__tests__/plugin-setup.test.ts +111 -9
- package/src/__tests__/profile-command.test.ts +1 -7
- package/src/__tests__/profile-materializer.test.ts +31 -8
- package/src/__tests__/profile-sync.test.ts +23 -5
- package/src/__tests__/registry-version-resolution.test.ts +61 -47
- package/src/__tests__/resolve-executable.test.ts +1 -1
- package/src/__tests__/resolver.test.ts +38 -13
- package/src/__tests__/scope-action.test.ts +52 -50
- package/src/__tests__/scope-squares.test.tsx +4 -5
- package/src/__tests__/skill-install-files.test.ts +111 -103
- package/src/__tests__/symlink-manager.test.ts +27 -10
- package/src/__tests__/theme-env.test.ts +281 -0
- package/src/__tests__/theme-resolve.test.ts +380 -0
- package/src/__tests__/update-apply.test.ts +1 -3
- package/src/__tests__/useGitignoreModal.test.ts +69 -61
- package/src/__tests__/version-snapshot.test.ts +183 -165
- package/src/cli/ansi.ts +23 -15
- package/src/cli/doctor.ts +10 -9
- package/src/cli/install.ts +3 -3
- package/src/cli/profile.ts +5 -4
- package/src/cli/prompt.ts +0 -1
- package/src/cli/router.ts +69 -2
- package/src/cli/update-view.ts +11 -6
- package/src/cli/update.ts +9 -4
- package/src/cli/upgrade.ts +6 -4
- package/src/data/alias-flags.ts +260 -260
- package/src/data/gitignore-defaults.ts +20 -20
- package/src/data/gitignore-reasons.ts +86 -92
- package/src/data/gitignore-templates.ts +14 -14
- package/src/data/predefined-profiles.ts +1 -2
- package/src/data/skill-repos.ts +43 -35
- package/src/main.tsx +29 -6
- package/src/opentui.d.ts +1 -3
- package/src/prerunner/index.ts +7 -8
- package/src/services/alias-settings.ts +20 -23
- package/src/services/alias-shell-writer.ts +800 -803
- package/src/services/alias-store.ts +77 -77
- package/src/services/claude-settings.ts +27 -18
- package/src/services/content-drift.ts +14 -4
- package/src/services/conventions-manager.ts +692 -617
- package/src/services/doctor.ts +464 -403
- package/src/services/gitignore-detector.ts +113 -103
- package/src/services/gitignore-fixer.ts +204 -189
- package/src/services/gitignore-prerun.ts +26 -26
- package/src/services/gitignore-resolver.ts +162 -144
- package/src/services/gitignore-service.ts +120 -117
- package/src/services/install-plan.ts +4 -4
- package/src/services/local-marketplace.ts +7 -5
- package/src/services/manifest.ts +1 -1
- package/src/services/marketplace-catalog-git.ts +5 -2
- package/src/services/marketplace-fetcher.ts +241 -235
- package/src/services/plugin-manager.ts +17 -20
- package/src/services/plugin-mcp-config.ts +2 -2
- package/src/services/plugin-requires.ts +16 -8
- package/src/services/plugin-setup.ts +32 -15
- package/src/services/plugin-version-check.ts +4 -4
- package/src/services/profile-materializer.ts +9 -5
- package/src/services/profile-sync.ts +12 -4
- package/src/services/profiles.ts +3 -3
- package/src/services/resolver.ts +10 -6
- package/src/services/settings-manager.ts +82 -46
- package/src/services/skills-manager.ts +11 -9
- package/src/services/symlink-manager.ts +2 -5
- package/src/services/toolchain.ts +3 -3
- package/src/services/update-cache.ts +1 -1
- package/src/services/update-engine.ts +4 -4
- package/src/services/update-plan.ts +9 -11
- package/src/services/version-snapshot.ts +88 -88
- package/src/types/gitignore.ts +28 -28
- package/src/ui/App.tsx +36 -36
- package/src/ui/adapters/settingsAdapter.ts +2 -2
- package/src/ui/components/CategoryHeader.tsx +1 -1
- package/src/ui/components/FlagDetailEditor.tsx +0 -0
- package/src/ui/components/ScopeIndicator.tsx +3 -1
- package/src/ui/components/ScrollableList.tsx +2 -1
- package/src/ui/components/StyledText.tsx +1 -1
- package/src/ui/components/layout/FooterHints.tsx +5 -3
- package/src/ui/components/layout/Panel.tsx +2 -2
- package/src/ui/components/layout/ScreenLayout.tsx +5 -4
- package/src/ui/components/modals/InputModal.tsx +7 -1
- package/src/ui/components/modals/ModalContainer.tsx +3 -3
- package/src/ui/components/modals/SelectModal.tsx +1 -1
- package/src/ui/components/primitives/DetailSection.tsx +1 -1
- package/src/ui/components/primitives/KeyValueLine.tsx +1 -1
- package/src/ui/components/primitives/ScopeSquares.tsx +2 -2
- package/src/ui/hooks/index.ts +0 -1
- package/src/ui/hooks/useGitignoreModal.ts +1 -5
- package/src/ui/hooks/useMismatchModal.ts +4 -10
- package/src/ui/renderers/cliToolRenderers.tsx +2 -2
- package/src/ui/renderers/gitignoreRenderers.tsx +2 -2
- package/src/ui/renderers/mcpRenderers.tsx +1 -1
- package/src/ui/renderers/pluginRenderers.tsx +3 -9
- package/src/ui/renderers/profileRenderers.tsx +9 -8
- package/src/ui/renderers/settingsRenderers.tsx +4 -4
- package/src/ui/renderers/skillRenderers.tsx +14 -14
- package/src/ui/screens/AliasScreen.tsx +31 -34
- package/src/ui/screens/CliToolsScreen.tsx +14 -13
- package/src/ui/screens/EnvVarsScreen.tsx +8 -8
- package/src/ui/screens/GitignoreScreen.tsx +15 -14
- package/src/ui/screens/McpRegistryScreen.tsx +10 -7
- package/src/ui/screens/McpScreen.tsx +10 -10
- package/src/ui/screens/PluginsScreen.tsx +17 -21
- package/src/ui/screens/ProfilesScreen.tsx +25 -24
- package/src/ui/screens/SkillsScreen.tsx +16 -14
- package/src/ui/state/AppContext.tsx +4 -8
- package/src/ui/state/DimensionsContext.tsx +3 -2
- package/src/ui/theme-mode.ts +20 -38
- package/src/ui/theme-resolve.ts +148 -0
- package/src/utils/clipboard.ts +5 -5
- package/src/utils/command-utils.ts +2 -2
- package/src/utils/config-dir.ts +1 -3
- package/src/utils/file-locking.ts +88 -86
- package/src/utils/go-module.ts +36 -0
- package/src/ui/hooks/useAsyncData.ts +0 -127
- package/src/ui/state/AnimationContext.tsx +0 -76
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { describe, expect, test } from "bun:test";
|
|
2
2
|
import {
|
|
3
|
-
|
|
4
|
-
|
|
3
|
+
isInstalledInScope,
|
|
4
|
+
resolveScopeAction,
|
|
5
5
|
} from "../services/plugin-manager.js";
|
|
6
6
|
|
|
7
7
|
/**
|
|
@@ -19,64 +19,66 @@ import {
|
|
|
19
19
|
*/
|
|
20
20
|
|
|
21
21
|
describe("isInstalledInScope", () => {
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
22
|
+
test("enabled with a registry version is installed", () => {
|
|
23
|
+
expect(isInstalledInScope({ enabled: true, version: "3.0.2" })).toBe(true);
|
|
24
|
+
});
|
|
25
25
|
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
26
|
+
test("enabled with NO version is not installed — the broken state", () => {
|
|
27
|
+
// dev@magus, measured: `.claude/settings.json` had "dev@magus": true while
|
|
28
|
+
// installed_plugins.json had no entry resolving for the project path.
|
|
29
|
+
expect(isInstalledInScope({ enabled: true })).toBe(false);
|
|
30
|
+
});
|
|
31
31
|
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
32
|
+
test("a version with the flag off is not installed either", () => {
|
|
33
|
+
expect(isInstalledInScope({ enabled: false, version: "3.0.2" })).toBe(
|
|
34
|
+
false,
|
|
35
|
+
);
|
|
36
|
+
});
|
|
35
37
|
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
38
|
+
test("an absent scope is not installed", () => {
|
|
39
|
+
expect(isInstalledInScope(undefined)).toBe(false);
|
|
40
|
+
});
|
|
39
41
|
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
42
|
+
test('"0.0.0" counts as installed — Anthropic official plugins record it', () => {
|
|
43
|
+
expect(isInstalledInScope({ enabled: true, version: "0.0.0" })).toBe(true);
|
|
44
|
+
});
|
|
43
45
|
});
|
|
44
46
|
|
|
45
47
|
describe("resolveScopeAction", () => {
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
48
|
+
test("REGRESSION: enabled but not installed resolves to install, never uninstall", () => {
|
|
49
|
+
// This is the reported bug. Before the fix this returned "uninstall".
|
|
50
|
+
expect(resolveScopeAction({ enabled: true }, "3.0.2")).toBe("install");
|
|
51
|
+
});
|
|
50
52
|
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
53
|
+
test("a scope that was never enabled resolves to install", () => {
|
|
54
|
+
expect(resolveScopeAction(undefined, "3.0.2")).toBe("install");
|
|
55
|
+
expect(resolveScopeAction({ enabled: false }, "3.0.2")).toBe("install");
|
|
56
|
+
});
|
|
55
57
|
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
58
|
+
test("a healthy install at the latest version resolves to uninstall (toggle off)", () => {
|
|
59
|
+
expect(
|
|
60
|
+
resolveScopeAction({ enabled: true, version: "3.0.2" }, "3.0.2"),
|
|
61
|
+
).toBe("uninstall");
|
|
62
|
+
});
|
|
61
63
|
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
64
|
+
test("a healthy install behind the catalog resolves to update", () => {
|
|
65
|
+
expect(
|
|
66
|
+
resolveScopeAction({ enabled: true, version: "2.9.0" }, "3.0.2"),
|
|
67
|
+
).toBe("update");
|
|
68
|
+
});
|
|
67
69
|
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
70
|
+
test('an unknown catalog version ("0.0.0") never fabricates an update', () => {
|
|
71
|
+
// There is nothing to update *to*, so toggling must still mean uninstall.
|
|
72
|
+
expect(
|
|
73
|
+
resolveScopeAction({ enabled: true, version: "3.0.2" }, "0.0.0"),
|
|
74
|
+
).toBe("uninstall");
|
|
75
|
+
});
|
|
74
76
|
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
77
|
+
test("installed with unknown version resolves to update when a real version is published", () => {
|
|
78
|
+
// "0.0.0" installed against a real catalog version is a genuine upgrade
|
|
79
|
+
// path, not the broken state — it must not be treated as uninstall.
|
|
80
|
+
expect(
|
|
81
|
+
resolveScopeAction({ enabled: true, version: "0.0.0" }, "3.0.2"),
|
|
82
|
+
).toBe("update");
|
|
83
|
+
});
|
|
82
84
|
});
|
|
@@ -2,7 +2,6 @@ import { afterEach, describe, expect, test } from "bun:test";
|
|
|
2
2
|
import type React from "react";
|
|
3
3
|
import { ScopeSquares } from "../ui/components/primitives/ScopeSquares.js";
|
|
4
4
|
import {
|
|
5
|
-
SCOPE_OFF_FALLBACK,
|
|
6
5
|
SCOPE_OFF_FILL,
|
|
7
6
|
resetThemeMode,
|
|
8
7
|
setThemeMode,
|
|
@@ -19,8 +18,8 @@ import { CONTRAST_REFERENCE, theme } from "../ui/theme.js";
|
|
|
19
18
|
*
|
|
20
19
|
* An unlit segment has to recede toward the page, which is exactly what a fixed
|
|
21
20
|
* colour cannot do — a pale fill that sits quietly on cream is a bright block on
|
|
22
|
-
* near-black. So the fill follows the
|
|
23
|
-
* that behaviour and
|
|
21
|
+
* near-black. So the fill follows the resolved mode, and these tests pin both
|
|
22
|
+
* that behaviour and the dark default that applies before resolution.
|
|
24
23
|
*/
|
|
25
24
|
|
|
26
25
|
interface SpanLike {
|
|
@@ -94,10 +93,10 @@ describe("fill distinguishes lit from unlit", () => {
|
|
|
94
93
|
]);
|
|
95
94
|
});
|
|
96
95
|
|
|
97
|
-
test("
|
|
96
|
+
test("before resolution the fill is the dark pair", () => {
|
|
98
97
|
resetThemeMode();
|
|
99
98
|
const el = ScopeSquares({ user: false, project: false, local: false });
|
|
100
|
-
expect(new Set(fills(el))).toEqual(new Set([
|
|
99
|
+
expect(new Set(fills(el))).toEqual(new Set([SCOPE_OFF_FILL.dark]));
|
|
101
100
|
});
|
|
102
101
|
});
|
|
103
102
|
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import { describe, expect, test } from "bun:test";
|
|
2
2
|
import path from "node:path";
|
|
3
3
|
import {
|
|
4
|
-
|
|
5
|
-
|
|
4
|
+
resolveSkillFilePath,
|
|
5
|
+
selectSkillFiles,
|
|
6
6
|
} from "../services/skills-manager.js";
|
|
7
7
|
|
|
8
8
|
/**
|
|
@@ -21,108 +21,116 @@ const blob = (p: string) => ({ path: p, type: "blob" });
|
|
|
21
21
|
const tree = (p: string) => ({ path: p, type: "tree" });
|
|
22
22
|
|
|
23
23
|
describe("selectSkillFiles", () => {
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
24
|
+
test("returns paths relative to the skill directory", () => {
|
|
25
|
+
expect(
|
|
26
|
+
selectSkillFiles(
|
|
27
|
+
[
|
|
28
|
+
blob("skills/audit-website/SKILL.md"),
|
|
29
|
+
blob("skills/audit-website/references/OUTPUT-FORMAT.md"),
|
|
30
|
+
blob("skills/audit-website/assets/icon-small.svg"),
|
|
31
|
+
],
|
|
32
|
+
"skills/audit-website",
|
|
33
|
+
),
|
|
34
|
+
).toEqual([
|
|
35
|
+
"SKILL.md",
|
|
36
|
+
"references/OUTPUT-FORMAT.md",
|
|
37
|
+
"assets/icon-small.svg",
|
|
38
|
+
]);
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
test("REGRESSION: a sibling with a shared name prefix is excluded", () => {
|
|
42
|
+
// Without the trailing slash on the prefix, "skills/audit" also matches
|
|
43
|
+
// "skills/audit-website/..." and dumps a second skill's files into the
|
|
44
|
+
// first one's directory.
|
|
45
|
+
expect(
|
|
46
|
+
selectSkillFiles(
|
|
47
|
+
[
|
|
48
|
+
blob("skills/audit/SKILL.md"),
|
|
49
|
+
blob("skills/audit-website/SKILL.md"),
|
|
50
|
+
blob("skills/audit-website/references/OUTPUT-FORMAT.md"),
|
|
51
|
+
],
|
|
52
|
+
"skills/audit",
|
|
53
|
+
),
|
|
54
|
+
).toEqual(["SKILL.md"]);
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
test("tree entries are skipped — only blobs are downloadable", () => {
|
|
58
|
+
expect(
|
|
59
|
+
selectSkillFiles(
|
|
60
|
+
[
|
|
61
|
+
tree("skills/x/references"),
|
|
62
|
+
blob("skills/x/SKILL.md"),
|
|
63
|
+
blob("skills/x/references/a.md"),
|
|
64
|
+
],
|
|
65
|
+
"skills/x",
|
|
66
|
+
),
|
|
67
|
+
).toEqual(["SKILL.md", "references/a.md"]);
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
test("files outside the skill directory are excluded", () => {
|
|
71
|
+
expect(
|
|
72
|
+
selectSkillFiles(
|
|
73
|
+
[blob("README.md"), blob("other/SKILL.md"), blob("skills/x/SKILL.md")],
|
|
74
|
+
"skills/x",
|
|
75
|
+
),
|
|
76
|
+
).toEqual(["SKILL.md"]);
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
test("a trailing slash on the skill path does not break matching", () => {
|
|
80
|
+
expect(selectSkillFiles([blob("skills/x/SKILL.md")], "skills/x/")).toEqual([
|
|
81
|
+
"SKILL.md",
|
|
82
|
+
]);
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
test("a skill directory with no files yields nothing", () => {
|
|
86
|
+
expect(
|
|
87
|
+
selectSkillFiles([blob("skills/other/SKILL.md")], "skills/x"),
|
|
88
|
+
).toEqual([]);
|
|
89
|
+
});
|
|
84
90
|
});
|
|
85
91
|
|
|
86
92
|
describe("resolveSkillFilePath", () => {
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
93
|
+
const dir = path.join(path.sep, "tmp", "skills", "demo");
|
|
94
|
+
|
|
95
|
+
test("resolves a plain relative path inside the install directory", () => {
|
|
96
|
+
expect(resolveSkillFilePath(dir, "SKILL.md")).toBe(
|
|
97
|
+
path.join(dir, "SKILL.md"),
|
|
98
|
+
);
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
test("resolves a nested path", () => {
|
|
102
|
+
expect(resolveSkillFilePath(dir, "references/OUTPUT-FORMAT.md")).toBe(
|
|
103
|
+
path.join(dir, "references", "OUTPUT-FORMAT.md"),
|
|
104
|
+
);
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
test("SECURITY: rejects paths escaping via ..", () => {
|
|
108
|
+
// These paths come from a remote repository and are untrusted input to a
|
|
109
|
+
// filesystem write.
|
|
110
|
+
expect(resolveSkillFilePath(dir, "../evil.md")).toBeNull();
|
|
111
|
+
expect(resolveSkillFilePath(dir, "../../.bashrc")).toBeNull();
|
|
112
|
+
expect(resolveSkillFilePath(dir, "refs/../../../evil.md")).toBeNull();
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
test("SECURITY: rejects absolute paths", () => {
|
|
116
|
+
expect(
|
|
117
|
+
resolveSkillFilePath(dir, path.join(path.sep, "etc", "passwd")),
|
|
118
|
+
).toBeNull();
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
test("rejects an empty path", () => {
|
|
122
|
+
expect(resolveSkillFilePath(dir, "")).toBeNull();
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
test("a .. that stays inside the directory is allowed", () => {
|
|
126
|
+
expect(resolveSkillFilePath(dir, "refs/../SKILL.md")).toBe(
|
|
127
|
+
path.join(dir, "SKILL.md"),
|
|
128
|
+
);
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
test("REGRESSION: a sibling directory sharing the name prefix is rejected", () => {
|
|
132
|
+
// path.startsWith without a separator would accept /tmp/skills/demo-evil
|
|
133
|
+
// as being inside /tmp/skills/demo.
|
|
134
|
+
expect(resolveSkillFilePath(dir, "../demo-evil/x.md")).toBeNull();
|
|
135
|
+
});
|
|
128
136
|
});
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { afterEach, beforeEach, describe, expect, test } from "bun:test";
|
|
2
|
-
import {
|
|
2
|
+
import { lstat, mkdtemp, readlink, rm } from "node:fs/promises";
|
|
3
3
|
import { tmpdir } from "node:os";
|
|
4
4
|
import { join } from "node:path";
|
|
5
5
|
import fs from "fs-extra";
|
|
@@ -17,7 +17,8 @@ async function materialize(
|
|
|
17
17
|
) {
|
|
18
18
|
const dir = profileDir(name, project);
|
|
19
19
|
await fs.ensureDir(dir);
|
|
20
|
-
if (files.settings)
|
|
20
|
+
if (files.settings)
|
|
21
|
+
await fs.writeJson(join(dir, "settings.json"), files.settings);
|
|
21
22
|
if (files.mcp) await fs.writeJson(join(dir, "mcp.json"), files.mcp);
|
|
22
23
|
if (files.skills) await fs.ensureDir(join(dir, "skills"));
|
|
23
24
|
}
|
|
@@ -40,12 +41,20 @@ describe("symlink-manager", () => {
|
|
|
40
41
|
await activateProfile("frontend", project);
|
|
41
42
|
|
|
42
43
|
// settings.json is a symlink into the profile dir
|
|
43
|
-
expect(
|
|
44
|
-
|
|
45
|
-
|
|
44
|
+
expect(
|
|
45
|
+
(await lstat(join(project, ".claude", "settings.json"))).isSymbolicLink(),
|
|
46
|
+
).toBe(true);
|
|
47
|
+
expect((await lstat(join(project, ".mcp.json"))).isSymbolicLink()).toBe(
|
|
48
|
+
true,
|
|
49
|
+
);
|
|
50
|
+
expect(
|
|
51
|
+
(await lstat(join(project, ".claude", "skills"))).isSymbolicLink(),
|
|
52
|
+
).toBe(true);
|
|
46
53
|
|
|
47
54
|
// reading through the link returns the profile's content
|
|
48
|
-
const settings = await fs.readJson(
|
|
55
|
+
const settings = await fs.readJson(
|
|
56
|
+
join(project, ".claude", "settings.json"),
|
|
57
|
+
);
|
|
49
58
|
expect(settings.enabledPlugins["dev@magus"]).toBe(true);
|
|
50
59
|
});
|
|
51
60
|
|
|
@@ -54,8 +63,12 @@ describe("symlink-manager", () => {
|
|
|
54
63
|
await activateProfile("frontend", project);
|
|
55
64
|
await fs.writeJson(join(project, ".claude", "settings.json"), { a: 2 });
|
|
56
65
|
// The real file inside the profile dir was updated (symlink preserved).
|
|
57
|
-
expect(
|
|
58
|
-
|
|
66
|
+
expect(
|
|
67
|
+
(await lstat(join(project, ".claude", "settings.json"))).isSymbolicLink(),
|
|
68
|
+
).toBe(true);
|
|
69
|
+
expect(
|
|
70
|
+
(await fs.readJson(`${profileDir("frontend", project)}/settings.json`)).a,
|
|
71
|
+
).toBe(2);
|
|
59
72
|
});
|
|
60
73
|
|
|
61
74
|
test("switching repoints links and activeProfile tracks it", async () => {
|
|
@@ -64,11 +77,15 @@ describe("symlink-manager", () => {
|
|
|
64
77
|
|
|
65
78
|
await activateProfile("frontend", project);
|
|
66
79
|
expect(await activeProfile(project)).toBe("frontend");
|
|
67
|
-
expect(
|
|
80
|
+
expect(
|
|
81
|
+
(await fs.readJson(join(project, ".claude", "settings.json"))).which,
|
|
82
|
+
).toBe("frontend");
|
|
68
83
|
|
|
69
84
|
await activateProfile("backend", project);
|
|
70
85
|
expect(await activeProfile(project)).toBe("backend");
|
|
71
|
-
expect(
|
|
86
|
+
expect(
|
|
87
|
+
(await fs.readJson(join(project, ".claude", "settings.json"))).which,
|
|
88
|
+
).toBe("backend");
|
|
72
89
|
});
|
|
73
90
|
|
|
74
91
|
test("uses relative (not absolute) symlinks", async () => {
|