claudeup 4.16.0 → 4.18.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.
Files changed (77) hide show
  1. package/package.json +1 -1
  2. package/src/__tests__/alias-parser.test.ts +317 -0
  3. package/src/__tests__/alias-shell-writer.test.ts +661 -0
  4. package/src/__tests__/alias-store.test.ts +86 -0
  5. package/src/__tests__/gitignore-fixer.test.ts +64 -1
  6. package/src/__tests__/gitignore-prerun.test.ts +2 -2
  7. package/src/__tests__/gitignore-service.test.ts +42 -0
  8. package/src/__tests__/marketplaces.test.ts +40 -0
  9. package/src/__tests__/plugin-manager-fallback.test.ts +120 -0
  10. package/src/__tests__/useGitignoreModal.test.ts +2 -2
  11. package/src/data/alias-flags.js +196 -0
  12. package/src/data/alias-flags.ts +291 -0
  13. package/src/data/gitignore-reasons.js +97 -0
  14. package/src/data/gitignore-reasons.ts +103 -0
  15. package/src/data/marketplaces.js +19 -1
  16. package/src/data/marketplaces.ts +17 -1
  17. package/src/services/alias-settings.js +51 -0
  18. package/src/services/alias-settings.ts +63 -0
  19. package/src/services/alias-shell-writer.js +764 -0
  20. package/src/services/alias-shell-writer.ts +873 -0
  21. package/src/services/alias-store.js +77 -0
  22. package/src/services/alias-store.ts +112 -0
  23. package/src/services/gitignore-fixer.js +70 -10
  24. package/src/services/gitignore-fixer.ts +76 -9
  25. package/src/services/gitignore-prerun.js +3 -3
  26. package/src/services/gitignore-prerun.ts +3 -3
  27. package/src/services/gitignore-service.js +20 -2
  28. package/src/services/gitignore-service.ts +23 -1
  29. package/src/services/marketplace-fetcher.js +96 -0
  30. package/src/services/marketplace-fetcher.ts +137 -0
  31. package/src/services/plugin-manager.js +6 -59
  32. package/src/services/plugin-manager.ts +16 -91
  33. package/src/services/skillsmp-client.js +29 -9
  34. package/src/services/skillsmp-client.ts +38 -8
  35. package/src/types/gitignore.ts +1 -1
  36. package/src/types/index.ts +1 -0
  37. package/src/ui/App.js +10 -4
  38. package/src/ui/App.tsx +9 -3
  39. package/src/ui/components/TabBar.js +2 -1
  40. package/src/ui/components/TabBar.tsx +2 -1
  41. package/src/ui/components/layout/FooterHints.js +29 -0
  42. package/src/ui/components/layout/FooterHints.tsx +52 -0
  43. package/src/ui/components/layout/ScreenLayout.js +2 -1
  44. package/src/ui/components/layout/ScreenLayout.tsx +12 -3
  45. package/src/ui/components/layout/index.js +1 -0
  46. package/src/ui/components/layout/index.ts +5 -0
  47. package/src/ui/components/modals/SelectModal.js +8 -1
  48. package/src/ui/components/modals/SelectModal.tsx +12 -1
  49. package/src/ui/hooks/useGitignoreModal.js +7 -8
  50. package/src/ui/hooks/useGitignoreModal.ts +8 -9
  51. package/src/ui/renderers/gitignoreRenderers.js +36 -23
  52. package/src/ui/renderers/gitignoreRenderers.tsx +50 -41
  53. package/src/ui/screens/AliasScreen.js +1008 -0
  54. package/src/ui/screens/AliasScreen.tsx +1402 -0
  55. package/src/ui/screens/CliToolsScreen.js +6 -1
  56. package/src/ui/screens/CliToolsScreen.tsx +6 -1
  57. package/src/ui/screens/EnvVarsScreen.js +6 -1
  58. package/src/ui/screens/EnvVarsScreen.tsx +6 -1
  59. package/src/ui/screens/GitignoreScreen.js +189 -88
  60. package/src/ui/screens/GitignoreScreen.tsx +312 -132
  61. package/src/ui/screens/McpRegistryScreen.js +13 -2
  62. package/src/ui/screens/McpRegistryScreen.tsx +13 -2
  63. package/src/ui/screens/McpScreen.js +6 -1
  64. package/src/ui/screens/McpScreen.tsx +6 -1
  65. package/src/ui/screens/ModelSelectorScreen.js +8 -2
  66. package/src/ui/screens/ModelSelectorScreen.tsx +8 -2
  67. package/src/ui/screens/PluginsScreen.js +13 -2
  68. package/src/ui/screens/PluginsScreen.tsx +13 -2
  69. package/src/ui/screens/ProfilesScreen.js +8 -1
  70. package/src/ui/screens/ProfilesScreen.tsx +8 -1
  71. package/src/ui/screens/SkillsScreen.js +21 -4
  72. package/src/ui/screens/SkillsScreen.tsx +39 -5
  73. package/src/ui/screens/StatusLineScreen.js +7 -1
  74. package/src/ui/screens/StatusLineScreen.tsx +7 -1
  75. package/src/ui/screens/index.js +1 -0
  76. package/src/ui/screens/index.ts +1 -0
  77. package/src/ui/state/types.ts +4 -2
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "claudeup",
3
- "version": "4.16.0",
3
+ "version": "4.18.0",
4
4
  "description": "TUI tool for managing Claude Code plugins, MCPs, and configuration",
5
5
  "type": "module",
6
6
  "main": "src/main.tsx",
@@ -0,0 +1,317 @@
1
+ import { describe, it, expect } from "bun:test";
2
+ import {
3
+ parseManagedBlock,
4
+ parseAliasLine,
5
+ parseAliasFromRc,
6
+ tokenizePosixAliasBody,
7
+ argsToFlagValues,
8
+ renderAlias,
9
+ } from "../services/alias-shell-writer";
10
+ import { defaultAliasConfig } from "../services/alias-store";
11
+
12
+ /**
13
+ * Parser round-trip tests. The strongest invariant: parse(render(config))
14
+ * should reproduce the config's enabled flags. We don't compare entire
15
+ * configs because the parser only produces the *enabled* subset (disabled
16
+ * defaults aren't in the rc file at all).
17
+ */
18
+
19
+ function buildAndParse(mutate: (c: ReturnType<typeof defaultAliasConfig>) => void) {
20
+ const c = defaultAliasConfig();
21
+ c.aliasName = "c";
22
+ mutate(c);
23
+ const rendered = renderAlias(c, "zsh").block;
24
+ const parsed = parseAliasFromRc(rendered);
25
+ return { config: c, rendered, parsed };
26
+ }
27
+
28
+ describe("parseManagedBlock", () => {
29
+ it("returns null when no managed block exists", () => {
30
+ expect(parseManagedBlock("export PATH=...\n")).toBeNull();
31
+ expect(parseManagedBlock("")).toBeNull();
32
+ });
33
+
34
+ it("returns null when only one marker is present", () => {
35
+ expect(parseManagedBlock("# >>> claudeup managed alias >>>\nalias c='claude'\n")).toBeNull();
36
+ });
37
+
38
+ it("extracts a minimal block with no args", () => {
39
+ const text =
40
+ "# >>> claudeup managed alias >>>\n" +
41
+ "alias c='claude'\n" +
42
+ "# <<< claudeup managed alias <<<\n";
43
+ const r = parseManagedBlock(text);
44
+ expect(r).not.toBeNull();
45
+ expect(r?.aliasName).toBe("c");
46
+ expect(r?.args).toEqual([]);
47
+ });
48
+
49
+ it("ignores text outside the block", () => {
50
+ const text =
51
+ "export FOO=bar\n" +
52
+ "# >>> claudeup managed alias >>>\n" +
53
+ "alias cc='claude --ide'\n" +
54
+ "# <<< claudeup managed alias <<<\n" +
55
+ "alias ll='ls -la'\n";
56
+ const r = parseManagedBlock(text);
57
+ expect(r?.aliasName).toBe("cc");
58
+ expect(r?.args).toEqual(["--ide"]);
59
+ });
60
+ });
61
+
62
+ describe("parseAliasLine", () => {
63
+ it("rejects lines that don't match the managed pattern", () => {
64
+ expect(parseAliasLine("alias ll='ls -la'")).toBeNull();
65
+ expect(parseAliasLine("alias =foo")).toBeNull();
66
+ expect(parseAliasLine("alias c='ls'")).toBeNull(); // not invoking claude
67
+ });
68
+
69
+ it("parses simple bare-flag lines", () => {
70
+ const r = parseAliasLine("alias c='claude --ide --dangerously-skip-permissions'");
71
+ expect(r?.aliasName).toBe("c");
72
+ expect(r?.args).toEqual(["--ide", "--dangerously-skip-permissions"]);
73
+ });
74
+ });
75
+
76
+ describe("tokenizePosixAliasBody — escape forms", () => {
77
+ it("splits bare tokens on whitespace", () => {
78
+ expect(tokenizePosixAliasBody("claude --ide --debug api,hooks")).toEqual([
79
+ "claude",
80
+ "--ide",
81
+ "--debug",
82
+ "api,hooks",
83
+ ]);
84
+ });
85
+
86
+ it("unescapes the close-escape-reopen sequence for spaces", () => {
87
+ // `'\''be terse'\''` inside outer quotes becomes the literal token `be terse`.
88
+ expect(
89
+ tokenizePosixAliasBody("claude --append-system-prompt '\\''be terse'\\''"),
90
+ ).toEqual(["claude", "--append-system-prompt", "be terse"]);
91
+ });
92
+
93
+ it("converts a substitution embed back into its template token", () => {
94
+ expect(
95
+ tokenizePosixAliasBody(
96
+ `claude --remote-control-session-name-prefix dev-'"$(basename "$PWD")"'-'"$(date +%d)"'`,
97
+ ),
98
+ ).toEqual([
99
+ "claude",
100
+ "--remote-control-session-name-prefix",
101
+ "dev-{folder}-{day}",
102
+ ]);
103
+ });
104
+
105
+ it("passes through unknown substitution code as the raw $(...) form", () => {
106
+ expect(
107
+ tokenizePosixAliasBody(`claude --remote-control-session-name-prefix '"$(whoami)"'`),
108
+ ).toEqual(["claude", "--remote-control-session-name-prefix", "$(whoami)"]);
109
+ });
110
+ });
111
+
112
+ describe("argsToFlagValues", () => {
113
+ it("returns an empty object for empty args", () => {
114
+ expect(argsToFlagValues([])).toEqual({});
115
+ });
116
+
117
+ it("marks boolean flags as enabled", () => {
118
+ const v = argsToFlagValues(["--ide", "--dangerously-skip-permissions"]);
119
+ expect(v["ide"]).toEqual({ kind: "boolean", enabled: true });
120
+ expect(v["dangerously-skip-permissions"]).toEqual({
121
+ kind: "boolean",
122
+ enabled: true,
123
+ });
124
+ });
125
+
126
+ it("recognises tri-state on/off variants", () => {
127
+ expect(argsToFlagValues(["--chrome"])).toMatchObject({
128
+ chrome: { kind: "tri-state", state: "on" },
129
+ });
130
+ expect(argsToFlagValues(["--no-chrome"])).toMatchObject({
131
+ chrome: { kind: "tri-state", state: "off" },
132
+ });
133
+ });
134
+
135
+ it("reads a select flag with a known value", () => {
136
+ expect(argsToFlagValues(["--effort", "high"])).toMatchObject({
137
+ effort: { kind: "select", enabled: true, value: "high" },
138
+ });
139
+ });
140
+
141
+ it("treats a bare --tmux (no variant) as the empty-value select", () => {
142
+ expect(argsToFlagValues(["--worktree", "--tmux"])).toMatchObject({
143
+ worktree: { kind: "boolean", enabled: true },
144
+ tmux: { kind: "select", enabled: true, value: "" },
145
+ });
146
+ });
147
+
148
+ it("reads multi-with-custom comma-joined tokens, splitting picked vs custom", () => {
149
+ expect(argsToFlagValues(["--debug", "api,hooks,!file"])).toMatchObject({
150
+ debug: {
151
+ kind: "multi-with-custom",
152
+ enabled: true,
153
+ picked: ["api", "hooks"],
154
+ custom: ["!file"],
155
+ },
156
+ });
157
+ });
158
+
159
+ it("collects repeated --dangerously-load-development-channels values", () => {
160
+ expect(
161
+ argsToFlagValues([
162
+ "--dangerously-load-development-channels",
163
+ "plugin:claudish@magus",
164
+ "--dangerously-load-development-channels",
165
+ "plugin:dev@magus",
166
+ ]),
167
+ ).toMatchObject({
168
+ "dangerously-load-development-channels": {
169
+ kind: "text-list",
170
+ enabled: true,
171
+ values: ["plugin:claudish@magus", "plugin:dev@magus"],
172
+ },
173
+ });
174
+ });
175
+
176
+ it("reads a text flag's value", () => {
177
+ expect(
178
+ argsToFlagValues(["--append-system-prompt", "be terse"]),
179
+ ).toMatchObject({
180
+ "append-system-prompt": {
181
+ kind: "text",
182
+ enabled: true,
183
+ value: "be terse",
184
+ },
185
+ });
186
+ });
187
+
188
+ it("recognises an optional-text flag in bare form (followed by another flag)", () => {
189
+ expect(
190
+ argsToFlagValues(["--remote-control", "--ide"]),
191
+ ).toMatchObject({
192
+ "remote-control": { kind: "boolean", enabled: true },
193
+ ide: { kind: "boolean", enabled: true },
194
+ });
195
+ });
196
+
197
+ it("silently skips unknown flag/value pairs without corrupting the rest", () => {
198
+ expect(
199
+ argsToFlagValues(["--unknown-future", "value", "--ide"]),
200
+ ).toMatchObject({ ide: { kind: "boolean", enabled: true } });
201
+ });
202
+ });
203
+
204
+ describe("round-trip: render → parse → enabled subset matches", () => {
205
+ it("preserves a complex POSIX config end-to-end", () => {
206
+ const { parsed } = buildAndParse((c) => {
207
+ c.flags["dangerously-skip-permissions"] = {
208
+ kind: "boolean",
209
+ enabled: true,
210
+ };
211
+ c.flags["debug"] = {
212
+ kind: "multi-with-custom",
213
+ enabled: true,
214
+ picked: ["api", "hooks"],
215
+ custom: ["!file"],
216
+ };
217
+ c.flags["effort"] = { kind: "select", enabled: true, value: "high" };
218
+ c.flags["dangerously-load-development-channels"] = {
219
+ kind: "text-list",
220
+ enabled: true,
221
+ values: ["plugin:claudish@magus"],
222
+ };
223
+ c.flags["worktree"] = { kind: "boolean", enabled: true };
224
+ c.flags["tmux"] = { kind: "select", enabled: true, value: "classic" };
225
+ });
226
+ expect(parsed).not.toBeNull();
227
+ expect(parsed!.aliasName).toBe("c");
228
+
229
+ const f = parsed!.flags;
230
+ expect(f["dangerously-skip-permissions"]).toEqual({
231
+ kind: "boolean",
232
+ enabled: true,
233
+ });
234
+ expect(f["debug"]).toEqual({
235
+ kind: "multi-with-custom",
236
+ enabled: true,
237
+ picked: ["api", "hooks"],
238
+ custom: ["!file"],
239
+ });
240
+ expect(f["effort"]).toEqual({
241
+ kind: "select",
242
+ enabled: true,
243
+ value: "high",
244
+ });
245
+ expect(f["dangerously-load-development-channels"]).toEqual({
246
+ kind: "text-list",
247
+ enabled: true,
248
+ values: ["plugin:claudish@magus"],
249
+ });
250
+ expect(f["worktree"]).toEqual({ kind: "boolean", enabled: true });
251
+ expect(f["tmux"]).toEqual({
252
+ kind: "select",
253
+ enabled: true,
254
+ value: "classic",
255
+ });
256
+ });
257
+
258
+ it("preserves a templated remote-control prefix", () => {
259
+ const { parsed } = buildAndParse((c) => {
260
+ c.flags["remote-control-session-name-prefix"] = {
261
+ kind: "text",
262
+ enabled: true,
263
+ value: "dev-{folder}-{day}",
264
+ };
265
+ });
266
+ expect(parsed!.flags["remote-control-session-name-prefix"]).toEqual({
267
+ kind: "text",
268
+ enabled: true,
269
+ value: "dev-{folder}-{day}",
270
+ });
271
+ });
272
+
273
+ it("preserves a quoted text value with spaces", () => {
274
+ const { parsed } = buildAndParse((c) => {
275
+ c.flags["append-system-prompt"] = {
276
+ kind: "text",
277
+ enabled: true,
278
+ value: "be terse",
279
+ };
280
+ });
281
+ expect(parsed!.flags["append-system-prompt"]).toEqual({
282
+ kind: "text",
283
+ enabled: true,
284
+ value: "be terse",
285
+ });
286
+ });
287
+
288
+ it("preserves a custom aliasName", () => {
289
+ const { parsed } = buildAndParse((c) => {
290
+ c.aliasName = "cc";
291
+ c.flags["ide"] = { kind: "boolean", enabled: true };
292
+ });
293
+ expect(parsed!.aliasName).toBe("cc");
294
+ expect(parsed!.flags["ide"]).toEqual({ kind: "boolean", enabled: true });
295
+ });
296
+ });
297
+
298
+ describe("parseAliasFromRc — entry point", () => {
299
+ it("returns null on text with no managed block", () => {
300
+ expect(parseAliasFromRc("export PATH=...\n")).toBeNull();
301
+ });
302
+
303
+ it("returns the alias name + flags for a well-formed block", () => {
304
+ const text =
305
+ "# >>> claudeup managed alias >>>\n" +
306
+ "alias c='claude --ide --effort high'\n" +
307
+ "# <<< claudeup managed alias <<<\n";
308
+ const r = parseAliasFromRc(text);
309
+ expect(r?.aliasName).toBe("c");
310
+ expect(r?.flags["ide"]).toEqual({ kind: "boolean", enabled: true });
311
+ expect(r?.flags["effort"]).toEqual({
312
+ kind: "select",
313
+ enabled: true,
314
+ value: "high",
315
+ });
316
+ });
317
+ });