claudeup 6.2.1 → 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__/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 +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 +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
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import { describe,
|
|
2
|
-
import { buildCatalogNoticeText } from "../ui/adapters/catalogNotice";
|
|
1
|
+
import { describe, expect, it } from "bun:test";
|
|
3
2
|
import type { MarketplaceFetchFailure } from "../services/marketplace-fetcher";
|
|
3
|
+
import { buildCatalogNoticeText } from "../ui/adapters/catalogNotice";
|
|
4
4
|
|
|
5
5
|
/**
|
|
6
6
|
* The banner is the only thing standing between the user and a screen that
|
|
@@ -17,139 +17,139 @@ import type { MarketplaceFetchFailure } from "../services/marketplace-fetcher";
|
|
|
17
17
|
const NOW = 1_786_980_000_000;
|
|
18
18
|
|
|
19
19
|
function failure(
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
20
|
+
marketplace: string,
|
|
21
|
+
kind: MarketplaceFetchFailure["kind"] = "rate-limited",
|
|
22
|
+
retryAt?: number,
|
|
23
23
|
): MarketplaceFetchFailure {
|
|
24
|
-
|
|
24
|
+
return { marketplace, kind, detail: `${kind} detail`, retryAt };
|
|
25
25
|
}
|
|
26
26
|
|
|
27
27
|
describe("buildCatalogNoticeText", () => {
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
28
|
+
it("says nothing when every catalog was actually checked", () => {
|
|
29
|
+
// The negative control. A banner that is always on carries no information.
|
|
30
|
+
expect(
|
|
31
|
+
buildCatalogNoticeText({
|
|
32
|
+
failures: [],
|
|
33
|
+
unverifiedPlugins: 0,
|
|
34
|
+
width: 120,
|
|
35
|
+
now: NOW,
|
|
36
|
+
}),
|
|
37
|
+
).toBeNull();
|
|
38
|
+
});
|
|
39
39
|
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
40
|
+
it("names the reason, because it determines what the user should do", () => {
|
|
41
|
+
const rate = buildCatalogNoticeText({
|
|
42
|
+
failures: [failure("magus")],
|
|
43
|
+
unverifiedPlugins: 3,
|
|
44
|
+
width: 120,
|
|
45
|
+
now: NOW,
|
|
46
|
+
});
|
|
47
|
+
expect(rate).toContain("GitHub rate limit");
|
|
48
48
|
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
49
|
+
const slow = buildCatalogNoticeText({
|
|
50
|
+
failures: [failure("magus", "timeout")],
|
|
51
|
+
unverifiedPlugins: 3,
|
|
52
|
+
width: 120,
|
|
53
|
+
now: NOW,
|
|
54
|
+
});
|
|
55
|
+
expect(slow).toContain("GitHub timed out");
|
|
56
|
+
});
|
|
57
57
|
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
58
|
+
it("names one or two failing marketplaces but only counts many", () => {
|
|
59
|
+
const few = buildCatalogNoticeText({
|
|
60
|
+
failures: [failure("magus"), failure("magus-alpha")],
|
|
61
|
+
unverifiedPlugins: 0,
|
|
62
|
+
width: 160,
|
|
63
|
+
now: NOW,
|
|
64
|
+
});
|
|
65
|
+
expect(few).toContain("magus-alpha");
|
|
66
66
|
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
67
|
+
const many = buildCatalogNoticeText({
|
|
68
|
+
failures: ["a", "b", "c", "d", "e", "f"].map((n) => failure(n)),
|
|
69
|
+
unverifiedPlugins: 0,
|
|
70
|
+
width: 160,
|
|
71
|
+
now: NOW,
|
|
72
|
+
});
|
|
73
|
+
expect(many).toContain("6 catalogs unchecked");
|
|
74
|
+
});
|
|
75
75
|
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
76
|
+
it("counts down to the retry instead of saying 'later'", () => {
|
|
77
|
+
// Answering "when can I try again" is the point. Guessing costs another
|
|
78
|
+
// request against the very limit the user is waiting out.
|
|
79
|
+
const line = buildCatalogNoticeText({
|
|
80
|
+
failures: [failure("magus", "rate-limited", NOW + 240_000)],
|
|
81
|
+
unverifiedPlugins: 17,
|
|
82
|
+
width: 160,
|
|
83
|
+
now: NOW,
|
|
84
|
+
}) as string;
|
|
85
|
+
expect(line).toContain("retrying in 4m");
|
|
86
|
+
});
|
|
87
87
|
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
88
|
+
it("uses the soonest retry when several catalogs are waiting", () => {
|
|
89
|
+
const line = buildCatalogNoticeText({
|
|
90
|
+
failures: [
|
|
91
|
+
failure("magus", "rate-limited", NOW + 600_000),
|
|
92
|
+
failure("magus-alpha", "rate-limited", NOW + 45_000),
|
|
93
|
+
],
|
|
94
|
+
unverifiedPlugins: 2,
|
|
95
|
+
width: 160,
|
|
96
|
+
now: NOW,
|
|
97
|
+
}) as string;
|
|
98
|
+
expect(line).toContain("retrying in 45s");
|
|
99
|
+
});
|
|
100
100
|
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
101
|
+
it("falls back to the manual hint when no retry time is known", () => {
|
|
102
|
+
// raw.githubusercontent.com sends no retry-after, so for some failures there
|
|
103
|
+
// is genuinely no time to show. Inventing one would be worse than omitting it.
|
|
104
|
+
const line = buildCatalogNoticeText({
|
|
105
|
+
failures: [failure("magus", "network")],
|
|
106
|
+
unverifiedPlugins: 1,
|
|
107
|
+
width: 160,
|
|
108
|
+
now: NOW,
|
|
109
|
+
}) as string;
|
|
110
|
+
expect(line).toContain("r to retry");
|
|
111
|
+
expect(line).not.toContain("retrying in");
|
|
112
|
+
});
|
|
113
113
|
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
114
|
+
it("ignores a retry time that has already passed", () => {
|
|
115
|
+
const line = buildCatalogNoticeText({
|
|
116
|
+
failures: [failure("magus", "rate-limited", NOW - 5_000)],
|
|
117
|
+
unverifiedPlugins: 1,
|
|
118
|
+
width: 160,
|
|
119
|
+
now: NOW,
|
|
120
|
+
}) as string;
|
|
121
|
+
expect(line).toContain("r to retry");
|
|
122
|
+
});
|
|
123
123
|
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
124
|
+
it("keeps a retry affordance at every width, however narrow", () => {
|
|
125
|
+
const failures = ["a", "b", "c", "d", "e", "f"].map((n) =>
|
|
126
|
+
failure(n, "rate-limited", NOW + 90_000),
|
|
127
|
+
);
|
|
128
|
+
for (const width of [40, 60, 80, 100, 120, 200]) {
|
|
129
|
+
const line = buildCatalogNoticeText({
|
|
130
|
+
failures,
|
|
131
|
+
unverifiedPlugins: 331,
|
|
132
|
+
width,
|
|
133
|
+
now: NOW,
|
|
134
|
+
}) as string;
|
|
135
|
+
expect(line).not.toBeNull();
|
|
136
|
+
expect(line).toContain("retrying in");
|
|
137
|
+
}
|
|
138
|
+
});
|
|
139
139
|
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
140
|
+
it("never renders wider than the pane", () => {
|
|
141
|
+
const failures = ["a", "b", "c", "d", "e", "f"].map((n) =>
|
|
142
|
+
failure(n, "rate-limited", NOW + 90_000),
|
|
143
|
+
);
|
|
144
|
+
for (const width of [40, 60, 80, 100, 120, 200]) {
|
|
145
|
+
const line = buildCatalogNoticeText({
|
|
146
|
+
failures,
|
|
147
|
+
unverifiedPlugins: 331,
|
|
148
|
+
width,
|
|
149
|
+
now: NOW,
|
|
150
|
+
}) as string;
|
|
151
|
+
// +1 for `⚠` rendering double-width while String.length counts it once.
|
|
152
|
+
expect(line.length + 1).toBeLessThanOrEqual(width);
|
|
153
|
+
}
|
|
154
|
+
});
|
|
155
155
|
});
|
|
@@ -11,16 +11,16 @@
|
|
|
11
11
|
*/
|
|
12
12
|
|
|
13
13
|
import { describe, expect, mock, test } from "bun:test";
|
|
14
|
+
import { parseArgs } from "../cli/update.js";
|
|
15
|
+
import type { PluginScope } from "../services/claude-cli.js";
|
|
16
|
+
import { checkBinaries } from "../services/doctor-bins.js";
|
|
14
17
|
import {
|
|
15
18
|
type ApplyReporter,
|
|
16
19
|
type PluginApplyDeps,
|
|
17
20
|
applyPlugins,
|
|
18
|
-
pluginNeedsWork,
|
|
19
21
|
installPluginInScope,
|
|
22
|
+
pluginNeedsWork,
|
|
20
23
|
} from "../services/update-engine.js";
|
|
21
|
-
import { parseArgs } from "../cli/update.js";
|
|
22
|
-
import { checkBinaries } from "../services/doctor-bins.js";
|
|
23
|
-
import type { PluginScope } from "../services/claude-cli.js";
|
|
24
24
|
import type { PluginUpdateItem } from "../services/update-plan.js";
|
|
25
25
|
import type { ResolvedBin } from "../types/index.js";
|
|
26
26
|
|
|
@@ -259,8 +259,10 @@ describe("checkBinaries onProbe", () => {
|
|
|
259
259
|
// A meter fed the raw index reads 0/3 while the first probe runs, which
|
|
260
260
|
// is correct — it has finished none of them.
|
|
261
261
|
const first: number[] = [];
|
|
262
|
-
await checkBinaries(
|
|
263
|
-
|
|
262
|
+
await checkBinaries(
|
|
263
|
+
bins,
|
|
264
|
+
async () => true,
|
|
265
|
+
(_n, index) => first.push(index),
|
|
264
266
|
);
|
|
265
267
|
expect(first[0]).toBe(0);
|
|
266
268
|
expect(first.at(-1)).toBe(bins.length - 1);
|
|
@@ -273,8 +275,10 @@ describe("checkBinaries onProbe", () => {
|
|
|
273
275
|
|
|
274
276
|
test("an empty bin list announces nothing", async () => {
|
|
275
277
|
const seen: string[] = [];
|
|
276
|
-
const results = await checkBinaries(
|
|
277
|
-
|
|
278
|
+
const results = await checkBinaries(
|
|
279
|
+
[],
|
|
280
|
+
async () => true,
|
|
281
|
+
(n) => seen.push(n),
|
|
278
282
|
);
|
|
279
283
|
expect(results).toEqual([]);
|
|
280
284
|
expect(seen).toEqual([]);
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import { afterEach, beforeEach, describe, expect, mock, test } from "bun:test";
|
|
2
|
-
import {
|
|
2
|
+
import { spawnSync } from "node:child_process";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
import { parseThemeFlag, route } from "../cli/router.js";
|
|
3
5
|
|
|
4
6
|
// Silence the help banner and capture what the router prints, without
|
|
5
7
|
// touching the network (--version) or spawning processes (claude/update).
|
|
@@ -17,9 +19,7 @@ beforeEach(() => {
|
|
|
17
19
|
errSpy = mock((...a: unknown[]) => {
|
|
18
20
|
errs.push(a.join(" "));
|
|
19
21
|
});
|
|
20
|
-
// biome-ignore lint/suspicious/noExplicitAny: test double
|
|
21
22
|
console.log = logSpy as any;
|
|
22
|
-
// biome-ignore lint/suspicious/noExplicitAny: test double
|
|
23
23
|
console.error = errSpy as any;
|
|
24
24
|
});
|
|
25
25
|
|
|
@@ -109,3 +109,132 @@ describe("update / upgrade split", () => {
|
|
|
109
109
|
expect(help).not.toContain("update Update claudeup itself");
|
|
110
110
|
});
|
|
111
111
|
});
|
|
112
|
+
|
|
113
|
+
describe("--theme", () => {
|
|
114
|
+
test("T-flag-ok: --theme light launches the TUI carrying the theme", async () => {
|
|
115
|
+
const r = await route(["--theme", "light"], "1.0.0");
|
|
116
|
+
expect(r).toEqual({ handled: false, launchTui: true, theme: "light" });
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
test("T-flag-ok: --theme=dark is the same flag", async () => {
|
|
120
|
+
const r = await route(["--theme=dark"], "1.0.0");
|
|
121
|
+
expect(r).toEqual({ handled: false, launchTui: true, theme: "dark" });
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
test("no --theme leaves the outcome without a theme key", async () => {
|
|
125
|
+
const r = await route([], "1.0.0");
|
|
126
|
+
expect("theme" in r).toBe(false);
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
test.each([
|
|
130
|
+
[["--theme", "solarized"]],
|
|
131
|
+
[["--theme", "purple"]],
|
|
132
|
+
[["--theme", "Dark"]],
|
|
133
|
+
[["--theme", "auto"]],
|
|
134
|
+
[["--theme"]],
|
|
135
|
+
[["--theme", "--no-refresh"]],
|
|
136
|
+
[["--theme="]],
|
|
137
|
+
])(
|
|
138
|
+
"T-flag-bad: %j is handled with exit 1 and names both words",
|
|
139
|
+
async (argv) => {
|
|
140
|
+
const r = await route(argv, "1.0.0");
|
|
141
|
+
|
|
142
|
+
expect(r).toEqual({ handled: true, exitCode: 1, launchTui: false });
|
|
143
|
+
const err = errs.join("\n");
|
|
144
|
+
expect(err).toContain("light");
|
|
145
|
+
expect(err).toContain("dark");
|
|
146
|
+
expect(err).toContain("--theme");
|
|
147
|
+
},
|
|
148
|
+
);
|
|
149
|
+
|
|
150
|
+
test("T-flag-cmd: --theme dark update strips the flag and dispatches update", () => {
|
|
151
|
+
// The seam: parseThemeFlag is what route() dispatches on. Asserting on
|
|
152
|
+
// `rest` proves `update` is the command token that reaches the switch,
|
|
153
|
+
// without running the real update handler against this machine.
|
|
154
|
+
const parsed = parseThemeFlag(["--theme", "dark", "update", "--dry-run"]);
|
|
155
|
+
|
|
156
|
+
expect(parsed).toEqual({
|
|
157
|
+
ok: true,
|
|
158
|
+
theme: "dark",
|
|
159
|
+
rest: ["update", "--dry-run"],
|
|
160
|
+
});
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
test("T-flag-cmd-route: route() hands update the stripped argv, not the original", async () => {
|
|
164
|
+
// The parse test above cannot catch a route() that re-reads the original
|
|
165
|
+
// argv inside the switch. Mock the command seam and assert what reached it.
|
|
166
|
+
// mock.module is process-global and survives mock.restore(), so the real
|
|
167
|
+
// module is put back at the end — a later file in the same `bun test`
|
|
168
|
+
// process must not inherit the fake.
|
|
169
|
+
const actual = { ...(await import("../cli/update.js")) };
|
|
170
|
+
const calls: string[][] = [];
|
|
171
|
+
mock.module("../cli/update.js", () => ({
|
|
172
|
+
...actual,
|
|
173
|
+
runUpdateCommand: async (args: string[]) => {
|
|
174
|
+
calls.push(args);
|
|
175
|
+
return 0;
|
|
176
|
+
},
|
|
177
|
+
}));
|
|
178
|
+
try {
|
|
179
|
+
const { route: routeWithMock } = await import("../cli/router.js");
|
|
180
|
+
const r = await routeWithMock(
|
|
181
|
+
["--theme", "dark", "update", "--dry-run"],
|
|
182
|
+
"1.0.0",
|
|
183
|
+
);
|
|
184
|
+
|
|
185
|
+
expect(r).toEqual({ handled: true, exitCode: 0, launchTui: false });
|
|
186
|
+
expect(calls).toEqual([["--dry-run"]]);
|
|
187
|
+
} finally {
|
|
188
|
+
mock.module("../cli/update.js", () => actual);
|
|
189
|
+
}
|
|
190
|
+
});
|
|
191
|
+
|
|
192
|
+
test("T-flag-bad: an invalid --theme does not launch the TUI even when a command follows", async () => {
|
|
193
|
+
const r = await route(["--theme", "purple", "update"], "1.0.0");
|
|
194
|
+
|
|
195
|
+
expect(r).toEqual({ handled: true, exitCode: 1, launchTui: false });
|
|
196
|
+
expect(errs.join("\n")).toContain("purple");
|
|
197
|
+
});
|
|
198
|
+
|
|
199
|
+
test("T-flag-cmd: the flag may follow the command too", () => {
|
|
200
|
+
expect(parseThemeFlag(["update", "--theme=light"])).toEqual({
|
|
201
|
+
ok: true,
|
|
202
|
+
theme: "light",
|
|
203
|
+
rest: ["update"],
|
|
204
|
+
});
|
|
205
|
+
});
|
|
206
|
+
|
|
207
|
+
test("T-flag-help: --help wins over a bad --theme, and documents the flag", async () => {
|
|
208
|
+
const r = await route(["--theme", "bogus", "--help"], "1.0.0");
|
|
209
|
+
|
|
210
|
+
expect(r).toEqual({ handled: true, exitCode: 0, launchTui: false });
|
|
211
|
+
const help = logs.join("\n");
|
|
212
|
+
expect(help).toContain("--theme light|dark");
|
|
213
|
+
expect(help).toContain("CLAUDEUP_THEME");
|
|
214
|
+
expect(help).toContain("TERM_THEME");
|
|
215
|
+
expect(errs).toEqual([]);
|
|
216
|
+
});
|
|
217
|
+
|
|
218
|
+
test("T-flag-live: `bun src/main.tsx --theme bogus` exits 1 before the renderer", () => {
|
|
219
|
+
// Exits in the router, so no tty is needed. stdin is a pipe on purpose:
|
|
220
|
+
// were the renderer reached, createCliRenderer would fail differently.
|
|
221
|
+
// --no-env-file: Bun autoloads the cwd's .env before user code, and the
|
|
222
|
+
// theme snapshot must precede any .env merge — same flag as the launchers.
|
|
223
|
+
const result = spawnSync(
|
|
224
|
+
"bun",
|
|
225
|
+
[
|
|
226
|
+
"--no-env-file",
|
|
227
|
+
path.join(import.meta.dir, "..", "main.tsx"),
|
|
228
|
+
"--theme",
|
|
229
|
+
"bogus",
|
|
230
|
+
],
|
|
231
|
+
{ encoding: "utf-8", input: "" },
|
|
232
|
+
);
|
|
233
|
+
|
|
234
|
+
expect(result.status).toBe(1);
|
|
235
|
+
expect(result.stderr).toContain("--theme");
|
|
236
|
+
expect(result.stderr).toContain("light");
|
|
237
|
+
expect(result.stderr).toContain("dark");
|
|
238
|
+
expect(result.stderr).toContain("bogus");
|
|
239
|
+
});
|
|
240
|
+
});
|
|
@@ -13,7 +13,6 @@
|
|
|
13
13
|
|
|
14
14
|
import { afterEach, describe, expect, test } from "bun:test";
|
|
15
15
|
import { setColorEnabled, stripAnsi, width } from "../cli/ansi.js";
|
|
16
|
-
import type { UpdatePlan } from "../services/update-plan.js";
|
|
17
16
|
import {
|
|
18
17
|
type ApplyState,
|
|
19
18
|
type Step,
|
|
@@ -25,6 +24,7 @@ import {
|
|
|
25
24
|
stepRecord,
|
|
26
25
|
summaryLine,
|
|
27
26
|
} from "../cli/update-view.js";
|
|
27
|
+
import type { UpdatePlan } from "../services/update-plan.js";
|
|
28
28
|
|
|
29
29
|
afterEach(() => setColorEnabled(null));
|
|
30
30
|
|
|
@@ -159,7 +159,11 @@ describe("NO_COLOR", () => {
|
|
|
159
159
|
...stepFrame(steps(), 3, 500),
|
|
160
160
|
...stepRecord(steps(), 500),
|
|
161
161
|
...planReport(plan()),
|
|
162
|
-
...applyFrame(
|
|
162
|
+
...applyFrame(
|
|
163
|
+
{ done: 1, total: 4, current: "dev@magus", startedAt: 0 },
|
|
164
|
+
2,
|
|
165
|
+
900,
|
|
166
|
+
),
|
|
163
167
|
applyRow(true, "dev@magus", 20, "project", 400),
|
|
164
168
|
...summaryLine(2, 5, 1, 0, 9000),
|
|
165
169
|
].join("\n");
|
|
@@ -218,8 +222,20 @@ describe("bars", () => {
|
|
|
218
222
|
setColorEnabled(false);
|
|
219
223
|
const record = stepRecord(
|
|
220
224
|
[
|
|
221
|
-
{
|
|
222
|
-
|
|
225
|
+
{
|
|
226
|
+
label: "fast",
|
|
227
|
+
state: "done",
|
|
228
|
+
detail: "",
|
|
229
|
+
startedAt: 0,
|
|
230
|
+
endedAt: 100,
|
|
231
|
+
},
|
|
232
|
+
{
|
|
233
|
+
label: "slow",
|
|
234
|
+
state: "done",
|
|
235
|
+
detail: "",
|
|
236
|
+
startedAt: 0,
|
|
237
|
+
endedAt: 1000,
|
|
238
|
+
},
|
|
223
239
|
],
|
|
224
240
|
1000,
|
|
225
241
|
);
|
|
@@ -230,7 +246,10 @@ describe("bars", () => {
|
|
|
230
246
|
|
|
231
247
|
test("a step that never ran does not divide by zero", () => {
|
|
232
248
|
setColorEnabled(false);
|
|
233
|
-
const record = stepRecord(
|
|
249
|
+
const record = stepRecord(
|
|
250
|
+
[{ label: "x", state: "pending", detail: "" }],
|
|
251
|
+
0,
|
|
252
|
+
);
|
|
234
253
|
expect(record[0]).toContain("░");
|
|
235
254
|
});
|
|
236
255
|
|
|
@@ -259,9 +278,9 @@ describe("bars", () => {
|
|
|
259
278
|
describe("sections", () => {
|
|
260
279
|
test("an empty section is omitted entirely, not printed as a heading", () => {
|
|
261
280
|
setColorEnabled(false);
|
|
262
|
-
const out = planReport(
|
|
263
|
-
|
|
264
|
-
)
|
|
281
|
+
const out = planReport(plan({ bins: [], skills: [] }) as UpdatePlan).join(
|
|
282
|
+
"\n",
|
|
283
|
+
);
|
|
265
284
|
expect(out).toContain("Plugins");
|
|
266
285
|
expect(out).not.toContain("CLI");
|
|
267
286
|
expect(out).not.toContain("Skills");
|
|
@@ -73,7 +73,6 @@ afterEach(async () => {
|
|
|
73
73
|
|
|
74
74
|
const source: CommunityStyleSource = COMMUNITY_SOURCES[0];
|
|
75
75
|
const STYLE_ID = "attention-span--spartan";
|
|
76
|
-
// biome-ignore lint/style/noNonNullAssertion: a registry entry the tests pin
|
|
77
76
|
const style = resolveCommunityStyle(STYLE_ID)!.style;
|
|
78
77
|
|
|
79
78
|
function upstreamText(body: string): string {
|
|
@@ -596,7 +595,6 @@ describe("the budget, which is the binding constraint", () => {
|
|
|
596
595
|
"nattergabriel--socratic",
|
|
597
596
|
];
|
|
598
597
|
for (const id of oneEach) {
|
|
599
|
-
// biome-ignore lint/style/noNonNullAssertion: ids pinned from the registry
|
|
600
598
|
const sourceId = resolveCommunityStyle(id)!.source.id;
|
|
601
599
|
await writeCommunityCheck(sourceId, {
|
|
602
600
|
checkedAt: NOW - 2 * COMMUNITY_CHECK_TTL_MS,
|
|
@@ -33,11 +33,11 @@ function contrast(a: string, b: string): number {
|
|
|
33
33
|
const KINDS = ["skill", "command", "agent", "mcp"] as const;
|
|
34
34
|
|
|
35
35
|
describe("componentBadge", () => {
|
|
36
|
-
it("
|
|
37
|
-
//
|
|
38
|
-
//
|
|
36
|
+
it("serves the dark pair before any resolution", () => {
|
|
37
|
+
// There is no unknown: dark is the stated default, so a chip is always a
|
|
38
|
+
// pair, never null, and the caller has no bare-glyph branch to keep.
|
|
39
39
|
for (const kind of KINDS) {
|
|
40
|
-
expect(componentBadge(kind)).
|
|
40
|
+
expect(componentBadge(kind)).toEqual(COMPONENT_BADGE[kind].dark);
|
|
41
41
|
}
|
|
42
42
|
});
|
|
43
43
|
|
|
@@ -53,7 +53,10 @@ beforeEach(async () => {
|
|
|
53
53
|
recursive: true,
|
|
54
54
|
});
|
|
55
55
|
await fs.mkdir(path.join(repo, "plugins", "beta"), { recursive: true });
|
|
56
|
-
await fs.writeFile(
|
|
56
|
+
await fs.writeFile(
|
|
57
|
+
path.join(repo, "plugins", "alpha", "skills", "s.md"),
|
|
58
|
+
"v1",
|
|
59
|
+
);
|
|
57
60
|
await fs.writeFile(path.join(repo, "plugins", "beta", "readme.md"), "v1");
|
|
58
61
|
await setVersion("alpha", "1.0.0");
|
|
59
62
|
await setVersion("beta", "1.0.0");
|
|
@@ -66,6 +69,7 @@ beforeEach(async () => {
|
|
|
66
69
|
});
|
|
67
70
|
|
|
68
71
|
afterEach(async () => {
|
|
72
|
+
// biome-ignore lint/performance/noDelete: assigning undefined to process.env stores the string "undefined"; only delete unsets the variable
|
|
69
73
|
if (prevConfigDir === undefined) delete process.env.CLAUDE_CONFIG_DIR;
|
|
70
74
|
else process.env.CLAUDE_CONFIG_DIR = prevConfigDir;
|
|
71
75
|
await fs.rm(configDir, { recursive: true, force: true });
|