claudeup 6.2.1 → 6.3.2
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__/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 +18 -4
- package/src/__tests__/plugin-setup.test.ts +12 -12
- 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 +40 -15
- 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 +181 -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 +8 -9
- 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 +13 -3
- 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/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 +9 -6
- package/src/services/plugin-setup.ts +7 -9
- 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/ui/hooks/useAsyncData.ts +0 -127
- package/src/ui/state/AnimationContext.tsx +0 -76
|
@@ -3,11 +3,11 @@ import { promises as fs } from "node:fs";
|
|
|
3
3
|
import os from "node:os";
|
|
4
4
|
import path from "node:path";
|
|
5
5
|
import {
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
6
|
+
UNKNOWN_VERSION,
|
|
7
|
+
diffVersions,
|
|
8
|
+
isKnownVersion,
|
|
9
|
+
loadSeenVersions,
|
|
10
|
+
saveSeenVersions,
|
|
11
11
|
} from "../services/version-snapshot.js";
|
|
12
12
|
|
|
13
13
|
/**
|
|
@@ -18,179 +18,195 @@ import {
|
|
|
18
18
|
*/
|
|
19
19
|
|
|
20
20
|
describe("isKnownVersion", () => {
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
21
|
+
test('"0.0.0" is not a usable version', () => {
|
|
22
|
+
// Claude Code records this for a plugin whose version it cannot determine.
|
|
23
|
+
expect(isKnownVersion(UNKNOWN_VERSION)).toBe(false);
|
|
24
|
+
expect(isKnownVersion(undefined)).toBe(false);
|
|
25
|
+
expect(isKnownVersion("")).toBe(false);
|
|
26
|
+
expect(isKnownVersion("1.0.0")).toBe(true);
|
|
27
|
+
});
|
|
28
28
|
});
|
|
29
29
|
|
|
30
30
|
describe("diffVersions — first run", () => {
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
31
|
+
test("no baseline reports nothing at all", () => {
|
|
32
|
+
// Without this, the first launch would flag every installed plugin as new,
|
|
33
|
+
// which is both wrong and the fastest way to train someone to ignore badges.
|
|
34
|
+
expect(
|
|
35
|
+
diffVersions(null, { "dev@magus": "2.12.1", "a@mp": UNKNOWN_VERSION }),
|
|
36
|
+
).toEqual([]);
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
test("an empty baseline is NOT the same as no baseline", () => {
|
|
40
|
+
// A real snapshot that happens to be empty means we have looked before and
|
|
41
|
+
// saw nothing, so anything present now is genuinely new.
|
|
42
|
+
const changes = diffVersions({}, { "dev@magus": "2.12.1" });
|
|
43
|
+
expect(changes).toEqual([
|
|
44
|
+
{ pluginId: "dev@magus", kind: "installed", to: "2.12.1" },
|
|
45
|
+
]);
|
|
46
|
+
});
|
|
47
47
|
});
|
|
48
48
|
|
|
49
49
|
describe("diffVersions — installs", () => {
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
50
|
+
test("a plugin absent from the baseline is reported as installed, not updated", () => {
|
|
51
|
+
const changes = diffVersions(
|
|
52
|
+
{ "other@mp": "1.0.0" },
|
|
53
|
+
{ "new@mp": "3.1.0" },
|
|
54
|
+
);
|
|
55
|
+
expect(changes).toEqual([
|
|
56
|
+
{ pluginId: "new@mp", kind: "installed", to: "3.1.0" },
|
|
57
|
+
]);
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
test("an install with an unknown version is still reported", () => {
|
|
61
|
+
// The version is unusable but the install is real — the UI shows "new"
|
|
62
|
+
// with no version rather than suppressing the plugin entirely.
|
|
63
|
+
const changes = diffVersions({}, { "a@mp": UNKNOWN_VERSION });
|
|
64
|
+
expect(changes).toEqual([
|
|
65
|
+
{ pluginId: "a@mp", kind: "installed", to: UNKNOWN_VERSION },
|
|
66
|
+
]);
|
|
67
|
+
});
|
|
65
68
|
});
|
|
66
69
|
|
|
67
70
|
describe("diffVersions — updates", () => {
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
71
|
+
test("reports a genuine version transition", () => {
|
|
72
|
+
expect(
|
|
73
|
+
diffVersions({ "dev@magus": "2.12.0" }, { "dev@magus": "2.12.1" }),
|
|
74
|
+
).toEqual([
|
|
75
|
+
{ pluginId: "dev@magus", kind: "updated", from: "2.12.0", to: "2.12.1" },
|
|
76
|
+
]);
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
test("unchanged versions report nothing", () => {
|
|
80
|
+
expect(
|
|
81
|
+
diffVersions({ "dev@magus": "2.12.1" }, { "dev@magus": "2.12.1" }),
|
|
82
|
+
).toEqual([]);
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
test("learning a previously unknown version is not an update", () => {
|
|
86
|
+
// This is exactly what v4.22.0's registry overlay caused: versions that
|
|
87
|
+
// could not be resolved suddenly could be. Reporting it would announce
|
|
88
|
+
// claudeup's own bugfix as if the user's plugins had changed.
|
|
89
|
+
expect(
|
|
90
|
+
diffVersions({ "a@mp": UNKNOWN_VERSION }, { "a@mp": "5.3.0" }),
|
|
91
|
+
).toEqual([]);
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
test("losing a known version is not an update either", () => {
|
|
95
|
+
expect(
|
|
96
|
+
diffVersions({ "a@mp": "5.3.0" }, { "a@mp": UNKNOWN_VERSION }),
|
|
97
|
+
).toEqual([]);
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
test("a downgrade IS reported", () => {
|
|
101
|
+
// Rolling back is a real change, arguably the most important to surface.
|
|
102
|
+
expect(diffVersions({ "a@mp": "2.0.0" }, { "a@mp": "1.0.0" })).toEqual([
|
|
103
|
+
{ pluginId: "a@mp", kind: "updated", from: "2.0.0", to: "1.0.0" },
|
|
104
|
+
]);
|
|
105
|
+
});
|
|
99
106
|
});
|
|
100
107
|
|
|
101
108
|
describe("diffVersions — removals and mixed sets", () => {
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
109
|
+
test("a removed plugin is not reported", () => {
|
|
110
|
+
expect(diffVersions({ "gone@mp": "1.0.0" }, {})).toEqual([]);
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
test("classifies each plugin independently in one pass", () => {
|
|
114
|
+
const changes = diffVersions(
|
|
115
|
+
{ same: "1.0.0", moved: "2.0.0", learned: UNKNOWN_VERSION },
|
|
116
|
+
{ same: "1.0.0", moved: "2.1.0", learned: "9.0.0", fresh: "0.1.0" },
|
|
117
|
+
);
|
|
118
|
+
expect(changes).toEqual([
|
|
119
|
+
{ pluginId: "moved", kind: "updated", from: "2.0.0", to: "2.1.0" },
|
|
120
|
+
{ pluginId: "fresh", kind: "installed", to: "0.1.0" },
|
|
121
|
+
]);
|
|
122
|
+
});
|
|
116
123
|
});
|
|
117
124
|
|
|
118
125
|
describe("per-project baselines", () => {
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
126
|
+
/**
|
|
127
|
+
* The baseline used to be one flat map for the whole machine, but the
|
|
128
|
+
* versions written into it are resolved per project. Two projects
|
|
129
|
+
* legitimately on different versions overwrote each other's baseline, so
|
|
130
|
+
* every differing plugin was reported as "updated" on the next render — a
|
|
131
|
+
* change that never happened.
|
|
132
|
+
*
|
|
133
|
+
* CLAUDE_CONFIG_DIR, not HOME: os.homedir() reads the passwd database on
|
|
134
|
+
* macOS and ignores $HOME, so a HOME-only override writes to the operator's
|
|
135
|
+
* real ~/.claude. That is not hypothetical — it happened while writing these.
|
|
136
|
+
*/
|
|
137
|
+
const withSandbox = async (fn: () => Promise<void>): Promise<void> => {
|
|
138
|
+
const dir = await fs.mkdtemp(path.join(os.tmpdir(), "vsnap-"));
|
|
139
|
+
const prev = process.env.CLAUDE_CONFIG_DIR;
|
|
140
|
+
process.env.CLAUDE_CONFIG_DIR = dir;
|
|
141
|
+
try {
|
|
142
|
+
await fn();
|
|
143
|
+
} finally {
|
|
144
|
+
// biome-ignore lint/performance/noDelete: assigning undefined to process.env stores the string "undefined"; only delete unsets the variable
|
|
145
|
+
if (prev === undefined) delete process.env.CLAUDE_CONFIG_DIR;
|
|
146
|
+
else process.env.CLAUDE_CONFIG_DIR = prev;
|
|
147
|
+
await fs.rm(dir, { recursive: true, force: true });
|
|
148
|
+
}
|
|
149
|
+
};
|
|
150
|
+
|
|
151
|
+
test("a project with no baseline of its own reports nothing", async () => {
|
|
152
|
+
await withSandbox(async () => {
|
|
153
|
+
await saveSeenVersions("/proj/a", { "setup@magus": "2.2.0" });
|
|
154
|
+
const baseline = await loadSeenVersions("/proj/b");
|
|
155
|
+
expect(baseline).toBeNull();
|
|
156
|
+
expect(diffVersions(baseline, { "setup@magus": "2.5.0" })).toEqual([]);
|
|
157
|
+
});
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
test("saving one project preserves the others", async () => {
|
|
161
|
+
await withSandbox(async () => {
|
|
162
|
+
await saveSeenVersions("/proj/a", { "gtd@magus": "2.0.0" });
|
|
163
|
+
await saveSeenVersions("/proj/b", { "gtd@magus": "2.0.1" });
|
|
164
|
+
expect(await loadSeenVersions("/proj/a")).toEqual({
|
|
165
|
+
"gtd@magus": "2.0.0",
|
|
166
|
+
});
|
|
167
|
+
expect(await loadSeenVersions("/proj/b")).toEqual({
|
|
168
|
+
"gtd@magus": "2.0.1",
|
|
169
|
+
});
|
|
170
|
+
});
|
|
171
|
+
});
|
|
172
|
+
|
|
173
|
+
test("a real update inside one project is still reported", async () => {
|
|
174
|
+
await withSandbox(async () => {
|
|
175
|
+
await saveSeenVersions("/proj/a", { "dev@magus": "3.0.0" });
|
|
176
|
+
const previous = await loadSeenVersions("/proj/a");
|
|
177
|
+
expect(diffVersions(previous, { "dev@magus": "3.0.1" })).toEqual([
|
|
178
|
+
{ pluginId: "dev@magus", kind: "updated", from: "3.0.0", to: "3.0.1" },
|
|
179
|
+
]);
|
|
180
|
+
});
|
|
181
|
+
});
|
|
182
|
+
|
|
183
|
+
test("a pre-migration flat baseline is adopted once, then replaced", async () => {
|
|
184
|
+
await withSandbox(async () => {
|
|
185
|
+
const file = path.join(
|
|
186
|
+
process.env.CLAUDE_CONFIG_DIR as string,
|
|
187
|
+
"claudeup-version-snapshot.json",
|
|
188
|
+
);
|
|
189
|
+
await fs.writeFile(
|
|
190
|
+
file,
|
|
191
|
+
JSON.stringify({
|
|
192
|
+
seen: { "dev@magus": "3.0.0" },
|
|
193
|
+
seededAt: "2026-07-29T00:00:00.000Z",
|
|
194
|
+
updatedAt: "2026-07-29T00:00:00.000Z",
|
|
195
|
+
}),
|
|
196
|
+
);
|
|
197
|
+
|
|
198
|
+
// Legacy shape carries no project, so it stands in for whoever asks first
|
|
199
|
+
// rather than being thrown away.
|
|
200
|
+
expect(await loadSeenVersions("/proj/a")).toEqual({
|
|
201
|
+
"dev@magus": "3.0.0",
|
|
202
|
+
});
|
|
203
|
+
|
|
204
|
+
await saveSeenVersions("/proj/a", { "dev@magus": "3.0.1" });
|
|
205
|
+
// Once per-project entries exist, the flat map is no longer consulted.
|
|
206
|
+
expect(await loadSeenVersions("/proj/b")).toBeNull();
|
|
207
|
+
expect(await loadSeenVersions("/proj/a")).toEqual({
|
|
208
|
+
"dev@magus": "3.0.1",
|
|
209
|
+
});
|
|
210
|
+
});
|
|
211
|
+
});
|
|
196
212
|
});
|
package/src/cli/ansi.ts
CHANGED
|
@@ -59,7 +59,7 @@ const clamp01 = (n: number): number =>
|
|
|
59
59
|
/** `#rgb` and `#rrggbb`, hash optional. Anything else resolves to black. */
|
|
60
60
|
function parseHex(hex: string): [number, number, number] {
|
|
61
61
|
let h = hex.trim().replace(/^#/, "");
|
|
62
|
-
if (h.length === 3) h = h[0]
|
|
62
|
+
if (h.length === 3) h = h[0] + h[0] + h[1] + h[1] + h[2] + h[2];
|
|
63
63
|
if (!/^[0-9a-fA-F]{6}$/.test(h)) return [0, 0, 0];
|
|
64
64
|
return [
|
|
65
65
|
Number.parseInt(h.slice(0, 2), 16),
|
|
@@ -110,19 +110,22 @@ export function mix(from: string, to: string, t: number): string {
|
|
|
110
110
|
export function blend1D(steps: number, from: string, to: string): string[] {
|
|
111
111
|
if (!Number.isInteger(steps) || steps <= 0) return [];
|
|
112
112
|
if (steps === 1) return [toHex(parseHex(from))];
|
|
113
|
-
return Array.from({ length: steps }, (_, i) =>
|
|
113
|
+
return Array.from({ length: steps }, (_, i) =>
|
|
114
|
+
mix(from, to, i / (steps - 1)),
|
|
115
|
+
);
|
|
114
116
|
}
|
|
115
117
|
|
|
116
118
|
/** Multi-stop ramp: `steps` colours spread evenly across every stop, in order. */
|
|
117
119
|
export function blendStops(steps: number, ...stops: string[]): string[] {
|
|
118
120
|
if (!Number.isInteger(steps) || steps <= 0 || stops.length === 0) return [];
|
|
119
|
-
|
|
120
|
-
|
|
121
|
+
const first = stops[0];
|
|
122
|
+
const last = stops[stops.length - 1];
|
|
123
|
+
if (stops.length === 1 || steps === 1) return blend1D(steps, first, last);
|
|
121
124
|
const segs = stops.length - 1;
|
|
122
125
|
return Array.from({ length: steps }, (_, i) => {
|
|
123
126
|
const p = (i / (steps - 1)) * segs;
|
|
124
127
|
const s = Math.min(segs - 1, Math.floor(p));
|
|
125
|
-
return mix(stops[s]
|
|
128
|
+
return mix(stops[s], stops[s + 1], p - s);
|
|
126
129
|
});
|
|
127
130
|
}
|
|
128
131
|
|
|
@@ -283,7 +286,7 @@ function charWidth(cp: number): number {
|
|
|
283
286
|
/** Visible cell width, ignoring escape sequences. */
|
|
284
287
|
export function width(text: string): number {
|
|
285
288
|
let w = 0;
|
|
286
|
-
for (const ch of stripAnsi(text)) w += charWidth(ch.codePointAt(0)
|
|
289
|
+
for (const ch of stripAnsi(text)) w += charWidth(ch.codePointAt(0) ?? 0);
|
|
287
290
|
return w;
|
|
288
291
|
}
|
|
289
292
|
|
|
@@ -321,8 +324,10 @@ export function truncate(text: string, n: number): string {
|
|
|
321
324
|
i += m[0].length;
|
|
322
325
|
continue;
|
|
323
326
|
}
|
|
324
|
-
const
|
|
325
|
-
|
|
327
|
+
const cp = text.codePointAt(i);
|
|
328
|
+
if (cp === undefined) break;
|
|
329
|
+
const ch = String.fromCodePoint(cp);
|
|
330
|
+
const cw = charWidth(cp);
|
|
326
331
|
if (w + cw > n) break;
|
|
327
332
|
out += ch;
|
|
328
333
|
w += cw;
|
|
@@ -391,10 +396,13 @@ export function meter(
|
|
|
391
396
|
if (cells <= 0) return "";
|
|
392
397
|
const p = clamp01((Number.isFinite(pct) ? pct : 0) / 100);
|
|
393
398
|
const filled = Math.round(p * cells);
|
|
394
|
-
const cols = blendStops(
|
|
399
|
+
const cols = blendStops(
|
|
400
|
+
cells,
|
|
401
|
+
...(stops.length > 0 ? stops : ramps.progress),
|
|
402
|
+
);
|
|
395
403
|
let out = "";
|
|
396
404
|
for (let i = 0; i < cells; i++) {
|
|
397
|
-
out += i < filled ? fg(cols[i]
|
|
405
|
+
out += i < filled ? fg(cols[i], FILL) : fg(TRACK_COLOR, TRACK);
|
|
398
406
|
}
|
|
399
407
|
return out;
|
|
400
408
|
}
|
|
@@ -446,14 +454,14 @@ export function stackedBar(cells: number, segments: BarSegment[]): string {
|
|
|
446
454
|
while (drift !== 0) {
|
|
447
455
|
const i = widths.indexOf(Math.max(...widths));
|
|
448
456
|
if (drift > 0) {
|
|
449
|
-
widths[i]
|
|
457
|
+
widths[i]++;
|
|
450
458
|
drift--;
|
|
451
|
-
} else if (widths[i]
|
|
452
|
-
widths[i]
|
|
459
|
+
} else if (widths[i] > 1) {
|
|
460
|
+
widths[i]--;
|
|
453
461
|
drift++;
|
|
454
462
|
} else break;
|
|
455
463
|
}
|
|
456
|
-
return live.map((s, i) => fg(s.color, FILL.repeat(widths[i]
|
|
464
|
+
return live.map((s, i) => fg(s.color, FILL.repeat(widths[i]))).join("");
|
|
457
465
|
}
|
|
458
466
|
|
|
459
467
|
/**
|
|
@@ -498,7 +506,7 @@ export function badge(label: string, hex: string): string {
|
|
|
498
506
|
const SPINNER = ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"] as const;
|
|
499
507
|
|
|
500
508
|
export function spinner(tick: number): string {
|
|
501
|
-
return SPINNER[Math.abs(Math.trunc(tick)) % SPINNER.length]
|
|
509
|
+
return SPINNER[Math.abs(Math.trunc(tick)) % SPINNER.length];
|
|
502
510
|
}
|
|
503
511
|
|
|
504
512
|
/** Elapsed time sized to its magnitude: `340ms`, `1.4s`, `2m04s`. */
|
package/src/cli/doctor.ts
CHANGED
|
@@ -8,16 +8,13 @@
|
|
|
8
8
|
* 3. Convention compliance — .gitignore + CLAUDE.md blocks (ported engine).
|
|
9
9
|
*/
|
|
10
10
|
|
|
11
|
-
import fs from "fs-extra";
|
|
12
11
|
import path from "node:path";
|
|
13
|
-
import
|
|
14
|
-
import { resolveAllProfiles } from "../services/resolver.js";
|
|
12
|
+
import fs from "fs-extra";
|
|
15
13
|
import { checkBinaries, missingBinaries } from "../services/doctor-bins.js";
|
|
16
|
-
import {
|
|
17
|
-
activeProfile,
|
|
18
|
-
profileDir,
|
|
19
|
-
} from "../services/symlink-manager.js";
|
|
20
14
|
import { checkProject, fixDoctorIssues } from "../services/doctor.js";
|
|
15
|
+
import { readManifest } from "../services/manifest.js";
|
|
16
|
+
import { resolveAllProfiles } from "../services/resolver.js";
|
|
17
|
+
import { activeProfile, profileDir } from "../services/symlink-manager.js";
|
|
21
18
|
|
|
22
19
|
export async function runDoctorCommand(args: string[]): Promise<number> {
|
|
23
20
|
const fix = args.includes("--fix");
|
|
@@ -42,7 +39,9 @@ export async function runDoctorCommand(args: string[]): Promise<number> {
|
|
|
42
39
|
problems++;
|
|
43
40
|
}
|
|
44
41
|
} else {
|
|
45
|
-
console.log(
|
|
42
|
+
console.log(
|
|
43
|
+
`✓ All ${closure.bins.length} binary dependencies present.`,
|
|
44
|
+
);
|
|
46
45
|
}
|
|
47
46
|
}
|
|
48
47
|
}
|
|
@@ -70,7 +69,9 @@ export async function runDoctorCommand(args: string[]): Promise<number> {
|
|
|
70
69
|
if (actionable.length > 0) {
|
|
71
70
|
console.log("\nConventions:");
|
|
72
71
|
for (const check of actionable) {
|
|
73
|
-
console.log(
|
|
72
|
+
console.log(
|
|
73
|
+
` ${check.status === "fail" ? "✗" : "•"} ${check.message}`,
|
|
74
|
+
);
|
|
74
75
|
}
|
|
75
76
|
const fixable = actionable.filter((c) => c.fixable).length;
|
|
76
77
|
const failures = actionable.filter((c) => c.status === "fail").length;
|
package/src/cli/install.ts
CHANGED
|
@@ -48,9 +48,9 @@ import type {
|
|
|
48
48
|
SkillInfo,
|
|
49
49
|
} from "../types/index.js";
|
|
50
50
|
import { formatCommand, resolveExecutable } from "../utils/command-utils.js";
|
|
51
|
+
import { runCommand, runShellScript } from "../utils/run.js";
|
|
51
52
|
import { ensureManifest } from "./bootstrap.js";
|
|
52
53
|
import { confirm, promptValue } from "./prompt.js";
|
|
53
|
-
import { runCommand, runShellScript } from "../utils/run.js";
|
|
54
54
|
|
|
55
55
|
interface InstallFlags {
|
|
56
56
|
check: boolean;
|
|
@@ -292,8 +292,8 @@ export async function runInstallCommand(
|
|
|
292
292
|
);
|
|
293
293
|
for (const c of drift) console.error(` ${c.kind}: ${c.detail}`);
|
|
294
294
|
console.error(
|
|
295
|
-
|
|
296
|
-
|
|
295
|
+
"\nRun `claudeup profile sync` to promote them into .claude/profiles.json,\n" +
|
|
296
|
+
"or re-run with --force to discard them.",
|
|
297
297
|
);
|
|
298
298
|
return 1;
|
|
299
299
|
}
|
package/src/cli/profile.ts
CHANGED
|
@@ -66,7 +66,7 @@ export async function initProfile(
|
|
|
66
66
|
`${getManifestPath(projectPath)} already defines: ${Object.keys(existing.profiles).join(", ")}`,
|
|
67
67
|
);
|
|
68
68
|
console.error(
|
|
69
|
-
|
|
69
|
+
"Use `claudeup profile sync` to promote local edits into it.",
|
|
70
70
|
);
|
|
71
71
|
return 1;
|
|
72
72
|
}
|
|
@@ -85,7 +85,7 @@ export async function listProfiles(projectPath: string): Promise<number> {
|
|
|
85
85
|
const active = await activeProfile(projectPath);
|
|
86
86
|
console.log("Profiles:");
|
|
87
87
|
for (const id of ids) {
|
|
88
|
-
const entry = manifest.profiles[id]
|
|
88
|
+
const entry = manifest.profiles[id];
|
|
89
89
|
const marker = id === active ? "●" : " ";
|
|
90
90
|
const materialized = (await fs.pathExists(profileDir(id, projectPath)))
|
|
91
91
|
? ""
|
|
@@ -105,14 +105,15 @@ export async function showProfile(
|
|
|
105
105
|
return 1;
|
|
106
106
|
}
|
|
107
107
|
const manifest = await readManifest(projectPath);
|
|
108
|
-
|
|
108
|
+
const entry = manifest.profiles[name];
|
|
109
|
+
if (!entry) {
|
|
109
110
|
console.error(
|
|
110
111
|
`Profile "${name}" not found. Available: ${Object.keys(manifest.profiles).join(", ")}`,
|
|
111
112
|
);
|
|
112
113
|
return 1;
|
|
113
114
|
}
|
|
114
115
|
const closure = await resolveProfile(manifest, name);
|
|
115
|
-
console.log(`Profile: ${name} (${
|
|
116
|
+
console.log(`Profile: ${name} (${entry.name})`);
|
|
116
117
|
for (const line of summarizeClosure(closure)) console.log(` ${line}`);
|
|
117
118
|
return 0;
|
|
118
119
|
}
|
package/src/cli/prompt.ts
CHANGED