claudeup 4.17.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.
- package/package.json +1 -1
- package/src/__tests__/alias-parser.test.ts +317 -0
- package/src/__tests__/alias-shell-writer.test.ts +661 -0
- package/src/__tests__/alias-store.test.ts +86 -0
- package/src/__tests__/gitignore-fixer.test.ts +64 -1
- package/src/__tests__/gitignore-prerun.test.ts +2 -2
- package/src/__tests__/gitignore-service.test.ts +42 -0
- package/src/__tests__/marketplaces.test.ts +40 -0
- package/src/__tests__/plugin-manager-fallback.test.ts +120 -0
- package/src/__tests__/useGitignoreModal.test.ts +2 -2
- package/src/data/alias-flags.js +196 -0
- package/src/data/alias-flags.ts +291 -0
- package/src/data/gitignore-reasons.js +97 -0
- package/src/data/gitignore-reasons.ts +103 -0
- package/src/data/marketplaces.js +5 -3
- package/src/data/marketplaces.ts +5 -4
- package/src/services/alias-settings.js +51 -0
- package/src/services/alias-settings.ts +63 -0
- package/src/services/alias-shell-writer.js +764 -0
- package/src/services/alias-shell-writer.ts +873 -0
- package/src/services/alias-store.js +77 -0
- package/src/services/alias-store.ts +112 -0
- package/src/services/gitignore-fixer.js +70 -10
- package/src/services/gitignore-fixer.ts +76 -9
- package/src/services/gitignore-prerun.js +3 -3
- package/src/services/gitignore-prerun.ts +3 -3
- package/src/services/gitignore-service.js +20 -2
- package/src/services/gitignore-service.ts +23 -1
- package/src/services/marketplace-fetcher.js +96 -0
- package/src/services/marketplace-fetcher.ts +137 -0
- package/src/services/plugin-manager.js +6 -59
- package/src/services/plugin-manager.ts +16 -91
- package/src/services/skillsmp-client.js +29 -9
- package/src/services/skillsmp-client.ts +38 -8
- package/src/types/gitignore.ts +1 -1
- package/src/ui/App.js +10 -4
- package/src/ui/App.tsx +9 -3
- package/src/ui/components/TabBar.js +2 -1
- package/src/ui/components/TabBar.tsx +2 -1
- package/src/ui/components/layout/FooterHints.js +29 -0
- package/src/ui/components/layout/FooterHints.tsx +52 -0
- package/src/ui/components/layout/ScreenLayout.js +2 -1
- package/src/ui/components/layout/ScreenLayout.tsx +12 -3
- package/src/ui/components/layout/index.js +1 -0
- package/src/ui/components/layout/index.ts +5 -0
- package/src/ui/components/modals/SelectModal.js +8 -1
- package/src/ui/components/modals/SelectModal.tsx +12 -1
- package/src/ui/hooks/useGitignoreModal.js +7 -8
- package/src/ui/hooks/useGitignoreModal.ts +8 -9
- package/src/ui/renderers/gitignoreRenderers.js +36 -23
- package/src/ui/renderers/gitignoreRenderers.tsx +50 -41
- package/src/ui/screens/AliasScreen.js +1008 -0
- package/src/ui/screens/AliasScreen.tsx +1402 -0
- package/src/ui/screens/CliToolsScreen.js +6 -1
- package/src/ui/screens/CliToolsScreen.tsx +6 -1
- package/src/ui/screens/EnvVarsScreen.js +6 -1
- package/src/ui/screens/EnvVarsScreen.tsx +6 -1
- package/src/ui/screens/GitignoreScreen.js +189 -88
- package/src/ui/screens/GitignoreScreen.tsx +312 -132
- package/src/ui/screens/McpRegistryScreen.js +13 -2
- package/src/ui/screens/McpRegistryScreen.tsx +13 -2
- package/src/ui/screens/McpScreen.js +6 -1
- package/src/ui/screens/McpScreen.tsx +6 -1
- package/src/ui/screens/ModelSelectorScreen.js +8 -2
- package/src/ui/screens/ModelSelectorScreen.tsx +8 -2
- package/src/ui/screens/PluginsScreen.js +13 -2
- package/src/ui/screens/PluginsScreen.tsx +13 -2
- package/src/ui/screens/ProfilesScreen.js +8 -1
- package/src/ui/screens/ProfilesScreen.tsx +8 -1
- package/src/ui/screens/SkillsScreen.js +21 -4
- package/src/ui/screens/SkillsScreen.tsx +39 -5
- package/src/ui/screens/StatusLineScreen.js +7 -1
- package/src/ui/screens/StatusLineScreen.tsx +7 -1
- package/src/ui/screens/index.js +1 -0
- package/src/ui/screens/index.ts +1 -0
- package/src/ui/state/types.ts +4 -2
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
import { describe, it, expect } from "bun:test";
|
|
2
|
+
import {
|
|
3
|
+
defaultAliasConfig,
|
|
4
|
+
defaultValueFor,
|
|
5
|
+
validateAliasName,
|
|
6
|
+
DEFAULT_ALIAS_NAME,
|
|
7
|
+
} from "../services/alias-store";
|
|
8
|
+
import { ALIAS_FLAGS } from "../data/alias-flags";
|
|
9
|
+
|
|
10
|
+
describe("defaultAliasConfig", () => {
|
|
11
|
+
it("seeds every known flag with a sane default and the default alias name", () => {
|
|
12
|
+
const c = defaultAliasConfig();
|
|
13
|
+
expect(c.aliasName).toBe(DEFAULT_ALIAS_NAME);
|
|
14
|
+
expect(c.aliasName).toBe("c");
|
|
15
|
+
expect(c.flags["dangerously-skip-permissions"]).toEqual({
|
|
16
|
+
kind: "boolean",
|
|
17
|
+
enabled: false,
|
|
18
|
+
});
|
|
19
|
+
expect(c.flags["chrome"]).toEqual({ kind: "tri-state", state: "unset" });
|
|
20
|
+
expect(c.flags["effort"]).toEqual({
|
|
21
|
+
kind: "select",
|
|
22
|
+
enabled: false,
|
|
23
|
+
value: "low",
|
|
24
|
+
});
|
|
25
|
+
expect(c.flags["dangerously-load-development-channels"]).toEqual({
|
|
26
|
+
kind: "text-list",
|
|
27
|
+
enabled: false,
|
|
28
|
+
values: ["plugin:claudish@magus"],
|
|
29
|
+
});
|
|
30
|
+
expect(c.flags["debug"]).toEqual({
|
|
31
|
+
kind: "multi-with-custom",
|
|
32
|
+
enabled: false,
|
|
33
|
+
picked: [],
|
|
34
|
+
custom: [],
|
|
35
|
+
});
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
it("covers every flag in the catalog", () => {
|
|
39
|
+
const c = defaultAliasConfig();
|
|
40
|
+
for (const flag of ALIAS_FLAGS) {
|
|
41
|
+
expect(c.flags[flag.id]).toBeDefined();
|
|
42
|
+
}
|
|
43
|
+
});
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
describe("defaultValueFor", () => {
|
|
47
|
+
it("returns the right default for each kind", () => {
|
|
48
|
+
const ide = ALIAS_FLAGS.find((f) => f.id === "ide")!;
|
|
49
|
+
expect(defaultValueFor(ide)).toEqual({ kind: "boolean", enabled: false });
|
|
50
|
+
|
|
51
|
+
const chrome = ALIAS_FLAGS.find((f) => f.id === "chrome")!;
|
|
52
|
+
expect(defaultValueFor(chrome)).toEqual({
|
|
53
|
+
kind: "tri-state",
|
|
54
|
+
state: "unset",
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
const debug = ALIAS_FLAGS.find((f) => f.id === "debug")!;
|
|
58
|
+
expect(defaultValueFor(debug)).toEqual({
|
|
59
|
+
kind: "multi-with-custom",
|
|
60
|
+
enabled: false,
|
|
61
|
+
picked: [],
|
|
62
|
+
custom: [],
|
|
63
|
+
});
|
|
64
|
+
});
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
describe("validateAliasName", () => {
|
|
68
|
+
it("accepts safe identifiers", () => {
|
|
69
|
+
expect(validateAliasName("c")).toBeNull();
|
|
70
|
+
expect(validateAliasName("claude")).toBeNull();
|
|
71
|
+
expect(validateAliasName("cc-yolo_2")).toBeNull();
|
|
72
|
+
expect(validateAliasName("_underscore")).toBeNull();
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
it("rejects empty, leading-digit, and special-char names", () => {
|
|
76
|
+
expect(validateAliasName("")).toMatch(/empty/);
|
|
77
|
+
expect(validateAliasName("9live")).toMatch(/letter/);
|
|
78
|
+
expect(validateAliasName("c laude")).toMatch(/letters/);
|
|
79
|
+
expect(validateAliasName("c'")).toMatch(/letters/);
|
|
80
|
+
expect(validateAliasName("c=d")).toMatch(/letters/);
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
it("rejects pathologically long names", () => {
|
|
84
|
+
expect(validateAliasName("a".repeat(64))).toMatch(/long/);
|
|
85
|
+
});
|
|
86
|
+
});
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { describe, it, expect, beforeEach, afterEach } from "bun:test";
|
|
2
|
-
import { mkdtemp, rm, writeFile, readFile } from "node:fs/promises";
|
|
2
|
+
import { mkdtemp, rm, writeFile, readFile, mkdir } from "node:fs/promises";
|
|
3
3
|
import { tmpdir } from "node:os";
|
|
4
4
|
import { join } from "node:path";
|
|
5
5
|
import { spawnSync } from "node:child_process";
|
|
@@ -50,6 +50,8 @@ describe("applySafeFixes", () => {
|
|
|
50
50
|
expect(content).toContain(".env");
|
|
51
51
|
expect(content).toContain(".mnemex/");
|
|
52
52
|
expect(content).toContain("# Added by claudeup");
|
|
53
|
+
expect(content).toContain("Environment files commonly contain");
|
|
54
|
+
expect(content).toContain("Local memory/index data");
|
|
53
55
|
});
|
|
54
56
|
|
|
55
57
|
it("creates .gitignore if absent", async () => {
|
|
@@ -151,6 +153,67 @@ describe("applyDestructiveFix", () => {
|
|
|
151
153
|
expect(ls.stdout).toContain(".mcp.json");
|
|
152
154
|
});
|
|
153
155
|
|
|
156
|
+
it("adds unignore exceptions when a parent rule ignores a tracked path", async () => {
|
|
157
|
+
await mkdir(join(repo, ".claude"), { recursive: true });
|
|
158
|
+
await writeFile(join(repo, ".claude", "settings.json"), "{}");
|
|
159
|
+
await writeFile(join(repo, ".gitignore"), "/*\n");
|
|
160
|
+
|
|
161
|
+
const r = await applyDestructiveFix(repo, {
|
|
162
|
+
kind: "ignored-but-should-track",
|
|
163
|
+
path: ".claude/settings.json",
|
|
164
|
+
source: "builtin",
|
|
165
|
+
severity: "destructive",
|
|
166
|
+
});
|
|
167
|
+
expect(r.applied).toBe(true);
|
|
168
|
+
|
|
169
|
+
const ignore = await readFile(join(repo, ".gitignore"), "utf8");
|
|
170
|
+
expect(ignore).toContain("!/.claude/");
|
|
171
|
+
expect(ignore).toContain("!/.claude/settings.json");
|
|
172
|
+
expect(ignore).toContain("Team Claude settings should be committed");
|
|
173
|
+
|
|
174
|
+
const ls = git(repo, "ls-files");
|
|
175
|
+
expect(ls.stdout).toContain(".claude/settings.json");
|
|
176
|
+
});
|
|
177
|
+
|
|
178
|
+
it("does not fail when making an absent tracked path trackable", async () => {
|
|
179
|
+
await writeFile(join(repo, ".gitignore"), "/*\n");
|
|
180
|
+
|
|
181
|
+
const r = await applyDestructiveFix(repo, {
|
|
182
|
+
kind: "ignored-but-should-track",
|
|
183
|
+
path: "plugin.json",
|
|
184
|
+
source: "builtin",
|
|
185
|
+
severity: "destructive",
|
|
186
|
+
});
|
|
187
|
+
expect(r.applied).toBe(true);
|
|
188
|
+
expect(r.message).toContain("does not exist yet");
|
|
189
|
+
|
|
190
|
+
const ignore = await readFile(join(repo, ".gitignore"), "utf8");
|
|
191
|
+
expect(ignore).toContain("!/plugin.json");
|
|
192
|
+
});
|
|
193
|
+
|
|
194
|
+
it("uses the managed rule pattern when untracking child violations", async () => {
|
|
195
|
+
await mkdir(join(repo, "logs"), { recursive: true });
|
|
196
|
+
await writeFile(join(repo, "logs", "app.log"), "x");
|
|
197
|
+
git(repo, "add", "logs/app.log");
|
|
198
|
+
git(repo, "commit", "-q", "-m", "add log");
|
|
199
|
+
|
|
200
|
+
const r = await applyDestructiveFix(
|
|
201
|
+
repo,
|
|
202
|
+
{
|
|
203
|
+
kind: "tracked-but-should-ignore",
|
|
204
|
+
path: "logs/app.log",
|
|
205
|
+
source: "builtin",
|
|
206
|
+
severity: "destructive",
|
|
207
|
+
},
|
|
208
|
+
"logs/",
|
|
209
|
+
);
|
|
210
|
+
expect(r.applied).toBe(true);
|
|
211
|
+
|
|
212
|
+
const ignore = await readFile(join(repo, ".gitignore"), "utf8");
|
|
213
|
+
expect(ignore).toContain("logs/");
|
|
214
|
+
expect(ignore).not.toMatch(/^logs\/app\.log$/m);
|
|
215
|
+
});
|
|
216
|
+
|
|
154
217
|
it("stages an untracked file", async () => {
|
|
155
218
|
await writeFile(join(repo, ".mcp.json"), "{}");
|
|
156
219
|
const r = await applyDestructiveFix(repo, {
|
|
@@ -69,7 +69,7 @@ describe("formatPrerunWarning", () => {
|
|
|
69
69
|
it("returns a warning string when violations exist", () => {
|
|
70
70
|
const w = formatPrerunWarning({ violationCount: 3, warnings: [] });
|
|
71
71
|
expect(w).not.toBeNull();
|
|
72
|
-
expect(w).toContain("3
|
|
73
|
-
expect(w).toContain("
|
|
72
|
+
expect(w).toContain("3 git state");
|
|
73
|
+
expect(w).toContain("Git State tab");
|
|
74
74
|
});
|
|
75
75
|
});
|
|
@@ -6,6 +6,7 @@ import { spawnSync } from "node:child_process";
|
|
|
6
6
|
import {
|
|
7
7
|
loadGitignoreState,
|
|
8
8
|
applyTemplate,
|
|
9
|
+
ensureProjectManifestForEdit,
|
|
9
10
|
} from "../services/gitignore-service";
|
|
10
11
|
|
|
11
12
|
function git(cwd: string, ...args: string[]): void {
|
|
@@ -91,3 +92,44 @@ describe("applyTemplate", () => {
|
|
|
91
92
|
);
|
|
92
93
|
});
|
|
93
94
|
});
|
|
95
|
+
|
|
96
|
+
describe("ensureProjectManifestForEdit", () => {
|
|
97
|
+
let projectDir: string;
|
|
98
|
+
|
|
99
|
+
beforeEach(async () => {
|
|
100
|
+
projectDir = await mkdtemp(join(tmpdir(), "gitig-edit-"));
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
afterEach(async () => {
|
|
104
|
+
await rm(projectDir, { recursive: true, force: true });
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
it("creates an editable project manifest from resolved rules", async () => {
|
|
108
|
+
const path = await ensureProjectManifestForEdit(projectDir, {
|
|
109
|
+
rules: [
|
|
110
|
+
{ pattern: ".env", action: "ignore", source: "builtin" },
|
|
111
|
+
{ pattern: ".mcp.json", action: "track", source: "builtin" },
|
|
112
|
+
],
|
|
113
|
+
conflicts: [],
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
expect(path).toBe(join(projectDir, ".claude", "gitignore.json"));
|
|
117
|
+
const written = JSON.parse(await readFile(path, "utf8"));
|
|
118
|
+
expect(written.ignore).toEqual([".env"]);
|
|
119
|
+
expect(written.track).toEqual([".mcp.json"]);
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
it("keeps an existing non-empty project manifest", async () => {
|
|
123
|
+
const path = join(projectDir, ".claude", "gitignore.json");
|
|
124
|
+
await mkdir(join(projectDir, ".claude"), { recursive: true });
|
|
125
|
+
await writeFile(path, JSON.stringify({ ignore: ["custom"], track: [] }));
|
|
126
|
+
|
|
127
|
+
await ensureProjectManifestForEdit(projectDir, {
|
|
128
|
+
rules: [{ pattern: ".env", action: "ignore", source: "builtin" }],
|
|
129
|
+
conflicts: [],
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
const written = JSON.parse(await readFile(path, "utf8"));
|
|
133
|
+
expect(written.ignore).toEqual(["custom"]);
|
|
134
|
+
});
|
|
135
|
+
});
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { describe, expect, it } from "bun:test";
|
|
2
|
+
import {
|
|
3
|
+
defaultMarketplaces,
|
|
4
|
+
deprecatedMarketplaces,
|
|
5
|
+
getAllMarketplaces,
|
|
6
|
+
} from "../data/marketplaces.js";
|
|
7
|
+
import type { LocalMarketplace } from "../services/local-marketplace.js";
|
|
8
|
+
|
|
9
|
+
describe("marketplaces", () => {
|
|
10
|
+
it("uses the canonical Superpowers marketplace by default", () => {
|
|
11
|
+
const superpowers = defaultMarketplaces.find(
|
|
12
|
+
(mp) => mp.name === "superpowers-marketplace",
|
|
13
|
+
);
|
|
14
|
+
|
|
15
|
+
expect(superpowers).toBeDefined();
|
|
16
|
+
expect(superpowers?.displayName).toBe("Superpowers");
|
|
17
|
+
expect(superpowers?.source.repo).toBe("obra/superpowers-marketplace");
|
|
18
|
+
expect(superpowers?.featured).toBe(true);
|
|
19
|
+
expect(deprecatedMarketplaces.superpowers).toBe("superpowers-marketplace");
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
it("hides stale local superpowers entries behind the canonical marketplace", () => {
|
|
23
|
+
const local = new Map<string, LocalMarketplace>([
|
|
24
|
+
[
|
|
25
|
+
"superpowers",
|
|
26
|
+
{
|
|
27
|
+
name: "superpowers-dev",
|
|
28
|
+
description: "Development marketplace",
|
|
29
|
+
gitRepo: "obra/superpowers",
|
|
30
|
+
plugins: [],
|
|
31
|
+
},
|
|
32
|
+
],
|
|
33
|
+
]);
|
|
34
|
+
|
|
35
|
+
const names = getAllMarketplaces(local).map((mp) => mp.name);
|
|
36
|
+
|
|
37
|
+
expect(names).toContain("superpowers-marketplace");
|
|
38
|
+
expect(names).not.toContain("superpowers");
|
|
39
|
+
});
|
|
40
|
+
});
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
import { describe, it, expect, beforeEach } from "bun:test";
|
|
2
|
+
import {
|
|
3
|
+
resolveMarketplacePlugins,
|
|
4
|
+
clearMarketplaceCache,
|
|
5
|
+
} from "../services/marketplace-fetcher";
|
|
6
|
+
import type {
|
|
7
|
+
LocalMarketplace,
|
|
8
|
+
LocalMarketplacePlugin,
|
|
9
|
+
} from "../services/local-marketplace";
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Regression coverage for the "deprecated everything" bug.
|
|
13
|
+
*
|
|
14
|
+
* When `fetchMarketplacePlugins` returns an empty list (network down,
|
|
15
|
+
* repo moved, GitHub blocked, etc.), the orphan-detection loop in
|
|
16
|
+
* `getAvailablePlugins` would flag every installed plugin as
|
|
17
|
+
* `isOrphaned: true` and the UI rendered them all as "deprecated".
|
|
18
|
+
*
|
|
19
|
+
* `resolveMarketplacePlugins` adds the local-marketplace cache as a
|
|
20
|
+
* fallback so installed plugins keep rendering with their real metadata
|
|
21
|
+
* even when the machine is offline.
|
|
22
|
+
*/
|
|
23
|
+
|
|
24
|
+
function buildLocalMarketplace(
|
|
25
|
+
name: string,
|
|
26
|
+
plugins: LocalMarketplacePlugin[],
|
|
27
|
+
): LocalMarketplace {
|
|
28
|
+
return { name, description: "", plugins };
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
describe("resolveMarketplacePlugins — offline fallback", () => {
|
|
32
|
+
beforeEach(() => {
|
|
33
|
+
// Session-level cache could otherwise carry over a previous test's
|
|
34
|
+
// successful fetch and short-circuit the fallback path.
|
|
35
|
+
clearMarketplaceCache();
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
it("falls back to the local cache when the remote fetch returns empty", async () => {
|
|
39
|
+
const local = new Map<string, LocalMarketplace>([
|
|
40
|
+
[
|
|
41
|
+
"magus",
|
|
42
|
+
buildLocalMarketplace("magus", [
|
|
43
|
+
{
|
|
44
|
+
name: "code-analysis",
|
|
45
|
+
version: "5.3.0",
|
|
46
|
+
description: "Codebase investigation tools",
|
|
47
|
+
category: "development",
|
|
48
|
+
author: { name: "Jack Rudenko" },
|
|
49
|
+
},
|
|
50
|
+
{
|
|
51
|
+
name: "dev",
|
|
52
|
+
version: "2.7.0",
|
|
53
|
+
description: "Dev workflow",
|
|
54
|
+
},
|
|
55
|
+
]),
|
|
56
|
+
],
|
|
57
|
+
]);
|
|
58
|
+
|
|
59
|
+
// An invalid repo string short-circuits `fetchMarketplacePlugins` to
|
|
60
|
+
// `[]` without any network call, simulating the offline case.
|
|
61
|
+
const resolved = await resolveMarketplacePlugins(
|
|
62
|
+
"magus",
|
|
63
|
+
"not a valid repo string",
|
|
64
|
+
local,
|
|
65
|
+
);
|
|
66
|
+
|
|
67
|
+
expect(resolved).toHaveLength(2);
|
|
68
|
+
expect(resolved.map((p) => p.name).sort()).toEqual([
|
|
69
|
+
"code-analysis",
|
|
70
|
+
"dev",
|
|
71
|
+
]);
|
|
72
|
+
const codeAnalysis = resolved.find((p) => p.name === "code-analysis");
|
|
73
|
+
expect(codeAnalysis?.version).toBe("5.3.0");
|
|
74
|
+
expect(codeAnalysis?.description).toBe("Codebase investigation tools");
|
|
75
|
+
expect(codeAnalysis?.category).toBe("development");
|
|
76
|
+
expect(codeAnalysis?.author?.name).toBe("Jack Rudenko");
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
it("returns empty when both remote and cache are empty", async () => {
|
|
80
|
+
const local = new Map<string, LocalMarketplace>();
|
|
81
|
+
const resolved = await resolveMarketplacePlugins(
|
|
82
|
+
"magus",
|
|
83
|
+
"not a valid repo string",
|
|
84
|
+
local,
|
|
85
|
+
);
|
|
86
|
+
expect(resolved).toEqual([]);
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
it("returns empty when remote fails AND cache exists but has no plugins", async () => {
|
|
90
|
+
const local = new Map<string, LocalMarketplace>([
|
|
91
|
+
["magus", buildLocalMarketplace("magus", [])],
|
|
92
|
+
]);
|
|
93
|
+
const resolved = await resolveMarketplacePlugins(
|
|
94
|
+
"magus",
|
|
95
|
+
"not a valid repo string",
|
|
96
|
+
local,
|
|
97
|
+
);
|
|
98
|
+
expect(resolved).toEqual([]);
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
it("does not consult the cache when the remote returns plugins", async () => {
|
|
102
|
+
// We can't easily exercise the success branch without a live network,
|
|
103
|
+
// but we can verify that a cache entry for a DIFFERENT marketplace
|
|
104
|
+
// doesn't bleed into the requested one.
|
|
105
|
+
const local = new Map<string, LocalMarketplace>([
|
|
106
|
+
[
|
|
107
|
+
"other-marketplace",
|
|
108
|
+
buildLocalMarketplace("other-marketplace", [
|
|
109
|
+
{ name: "should-not-leak", version: "0.0.1", description: "" },
|
|
110
|
+
]),
|
|
111
|
+
],
|
|
112
|
+
]);
|
|
113
|
+
const resolved = await resolveMarketplacePlugins(
|
|
114
|
+
"magus",
|
|
115
|
+
"not a valid repo string",
|
|
116
|
+
local,
|
|
117
|
+
);
|
|
118
|
+
expect(resolved).toEqual([]);
|
|
119
|
+
});
|
|
120
|
+
});
|
|
@@ -31,7 +31,7 @@ describe("buildGitignoreModal", () => {
|
|
|
31
31
|
expect(dismissed).toBe(true);
|
|
32
32
|
});
|
|
33
33
|
|
|
34
|
-
it("shows
|
|
34
|
+
it("shows no missing entries label when safeCount is 0", () => {
|
|
35
35
|
const modal = buildGitignoreModal({
|
|
36
36
|
violationCount: 2,
|
|
37
37
|
safeCount: 0,
|
|
@@ -40,7 +40,7 @@ describe("buildGitignoreModal", () => {
|
|
|
40
40
|
onDismiss: () => {},
|
|
41
41
|
});
|
|
42
42
|
if (modal.type !== "select") throw new Error("type guard");
|
|
43
|
-
expect(modal.options[0].label).toContain("no
|
|
43
|
+
expect(modal.options[0].label).toContain("no missing .gitignore");
|
|
44
44
|
});
|
|
45
45
|
|
|
46
46
|
it("includes violation count in the message", () => {
|
|
@@ -0,0 +1,196 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Catalog of `claude` CLI flags that the Alias tab can compose into a managed
|
|
3
|
+
* shell alias. Verified against `claude --help` output and binary string scan
|
|
4
|
+
* of the shipped Claude Code 2.1.x binary.
|
|
5
|
+
*
|
|
6
|
+
* The catalog drives both the UI editor for each flag and the renderer that
|
|
7
|
+
* emits the final `alias claude='claude …'` string per shell.
|
|
8
|
+
*/
|
|
9
|
+
/**
|
|
10
|
+
* Tokens supported in `templated: true` flag values. Description text is
|
|
11
|
+
* shown to the user; the actual shell-substitution code lives in the writer
|
|
12
|
+
* because it needs per-shell variants.
|
|
13
|
+
*
|
|
14
|
+
* Intentionally excludes `{hash}` for v1 — its only portable form is
|
|
15
|
+
* `od -An -N4 -tx1 /dev/urandom | tr -d ' \n'`, which looks frightening in a
|
|
16
|
+
* shell rc file. Users who want randomness can pair this prefix with bare
|
|
17
|
+
* `--remote-control` (which already auto-generates a unique suffix).
|
|
18
|
+
*/
|
|
19
|
+
export const TEMPLATE_TOKENS = [
|
|
20
|
+
{ token: "{folder}", description: "current directory's basename" },
|
|
21
|
+
{ token: "{day}", description: "day of month, zero-padded (01-31)" },
|
|
22
|
+
{ token: "{month}", description: "month, zero-padded (01-12)" },
|
|
23
|
+
{ token: "{year}", description: "4-digit year" },
|
|
24
|
+
];
|
|
25
|
+
export const FLAG_GROUPS = [
|
|
26
|
+
{ id: "danger", label: "Permissions" },
|
|
27
|
+
{ id: "session", label: "Session" },
|
|
28
|
+
{ id: "context", label: "Context" },
|
|
29
|
+
{ id: "channels", label: "Development channels" },
|
|
30
|
+
{ id: "debug", label: "Debug" },
|
|
31
|
+
{ id: "integration", label: "Integrations" },
|
|
32
|
+
{ id: "worktree", label: "Worktree" },
|
|
33
|
+
];
|
|
34
|
+
export const ALIAS_FLAGS = [
|
|
35
|
+
{
|
|
36
|
+
id: "dangerously-skip-permissions",
|
|
37
|
+
label: "Skip permission checks",
|
|
38
|
+
flag: "--dangerously-skip-permissions",
|
|
39
|
+
kind: "boolean",
|
|
40
|
+
group: "danger",
|
|
41
|
+
description: "Bypass all permission checks. Recommended only for sandboxes with no internet access.",
|
|
42
|
+
},
|
|
43
|
+
{
|
|
44
|
+
id: "allow-dangerously-skip-permissions",
|
|
45
|
+
label: "Allow skip-permissions opt-in",
|
|
46
|
+
flag: "--allow-dangerously-skip-permissions",
|
|
47
|
+
kind: "boolean",
|
|
48
|
+
group: "danger",
|
|
49
|
+
description: "Enable the skip-permissions option without making it the default.",
|
|
50
|
+
},
|
|
51
|
+
{
|
|
52
|
+
id: "remote-control",
|
|
53
|
+
label: "Remote control",
|
|
54
|
+
flag: "--remote-control",
|
|
55
|
+
kind: "boolean",
|
|
56
|
+
group: "session",
|
|
57
|
+
description: "Start an interactive session with Remote Control enabled. Claude Code auto-generates a unique session name; use --remote-control-session-name-prefix to customize it.",
|
|
58
|
+
},
|
|
59
|
+
{
|
|
60
|
+
id: "remote-control-session-name-prefix",
|
|
61
|
+
label: "Remote control name prefix",
|
|
62
|
+
flag: "--remote-control-session-name-prefix",
|
|
63
|
+
kind: "text",
|
|
64
|
+
templated: true,
|
|
65
|
+
group: "session",
|
|
66
|
+
description: "Prefix for auto-generated Remote Control session names. Default: hostname. Supports template tokens that expand on each shell invocation: {folder} (basename of $PWD), {day}, {month}, {year}. Example: dev-{folder}-{day} → dev-myproject-15.",
|
|
67
|
+
},
|
|
68
|
+
{
|
|
69
|
+
id: "ide",
|
|
70
|
+
label: "Auto-connect to IDE",
|
|
71
|
+
flag: "--ide",
|
|
72
|
+
kind: "boolean",
|
|
73
|
+
group: "integration",
|
|
74
|
+
description: "Automatically connect to IDE on startup if exactly one valid IDE is available.",
|
|
75
|
+
},
|
|
76
|
+
{
|
|
77
|
+
id: "chrome",
|
|
78
|
+
label: "Chrome integration",
|
|
79
|
+
flag: "--chrome",
|
|
80
|
+
triStateOff: "--no-chrome",
|
|
81
|
+
kind: "tri-state",
|
|
82
|
+
group: "integration",
|
|
83
|
+
description: "Enable or disable Claude in Chrome integration. Unset leaves the project default in place.",
|
|
84
|
+
},
|
|
85
|
+
{
|
|
86
|
+
id: "effort",
|
|
87
|
+
label: "Effort level",
|
|
88
|
+
flag: "--effort",
|
|
89
|
+
kind: "select",
|
|
90
|
+
group: "session",
|
|
91
|
+
description: "Effort level for the current session.",
|
|
92
|
+
options: [
|
|
93
|
+
{ label: "low", value: "low" },
|
|
94
|
+
{ label: "medium", value: "medium" },
|
|
95
|
+
{ label: "high", value: "high" },
|
|
96
|
+
{ label: "xhigh", value: "xhigh" },
|
|
97
|
+
{ label: "max", value: "max" },
|
|
98
|
+
],
|
|
99
|
+
},
|
|
100
|
+
{
|
|
101
|
+
id: "system-prompt",
|
|
102
|
+
label: "System prompt",
|
|
103
|
+
flag: "--system-prompt",
|
|
104
|
+
kind: "text",
|
|
105
|
+
group: "context",
|
|
106
|
+
xorGroup: "system-prompt",
|
|
107
|
+
description: "System prompt text to use for the session. Mutually exclusive with --system-prompt-file.",
|
|
108
|
+
},
|
|
109
|
+
{
|
|
110
|
+
id: "system-prompt-file",
|
|
111
|
+
label: "System prompt file",
|
|
112
|
+
flag: "--system-prompt-file",
|
|
113
|
+
kind: "text",
|
|
114
|
+
group: "context",
|
|
115
|
+
xorGroup: "system-prompt",
|
|
116
|
+
description: "Path to a file whose contents replace the default system prompt. Mutually exclusive with --system-prompt.",
|
|
117
|
+
},
|
|
118
|
+
{
|
|
119
|
+
id: "append-system-prompt",
|
|
120
|
+
label: "Append system prompt",
|
|
121
|
+
flag: "--append-system-prompt",
|
|
122
|
+
kind: "text",
|
|
123
|
+
group: "context",
|
|
124
|
+
description: "Text appended to the default system prompt. Composes with --system-prompt.",
|
|
125
|
+
},
|
|
126
|
+
{
|
|
127
|
+
id: "append-system-prompt-file",
|
|
128
|
+
label: "Append system prompt file",
|
|
129
|
+
flag: "--append-system-prompt-file",
|
|
130
|
+
kind: "text",
|
|
131
|
+
group: "context",
|
|
132
|
+
description: "Path to a file whose contents are appended to the default system prompt.",
|
|
133
|
+
},
|
|
134
|
+
{
|
|
135
|
+
id: "dangerously-load-development-channels",
|
|
136
|
+
label: "Load development channels",
|
|
137
|
+
flag: "--dangerously-load-development-channels",
|
|
138
|
+
kind: "text-list",
|
|
139
|
+
group: "channels",
|
|
140
|
+
description: "Sideload development channels from magus plugins. Default seed: claudish (the only magus plugin currently shipping channels).",
|
|
141
|
+
defaultValues: ["plugin:claudish@magus"],
|
|
142
|
+
},
|
|
143
|
+
{
|
|
144
|
+
id: "debug",
|
|
145
|
+
label: "Debug categories",
|
|
146
|
+
flag: "--debug",
|
|
147
|
+
shortFlag: "-d",
|
|
148
|
+
kind: "multi-with-custom",
|
|
149
|
+
group: "debug",
|
|
150
|
+
description: "Enable debug mode with optional category filter. Pick from common categories or add custom tokens (including negations like !file).",
|
|
151
|
+
picklist: [
|
|
152
|
+
"api",
|
|
153
|
+
"hooks",
|
|
154
|
+
"mcp",
|
|
155
|
+
"tools",
|
|
156
|
+
"plugins",
|
|
157
|
+
"skills",
|
|
158
|
+
"settings",
|
|
159
|
+
"permissions",
|
|
160
|
+
"auth",
|
|
161
|
+
"cache",
|
|
162
|
+
"statsig",
|
|
163
|
+
"file",
|
|
164
|
+
"1p",
|
|
165
|
+
],
|
|
166
|
+
defaultValues: [],
|
|
167
|
+
},
|
|
168
|
+
{
|
|
169
|
+
id: "worktree",
|
|
170
|
+
label: "Use worktree",
|
|
171
|
+
flag: "--worktree",
|
|
172
|
+
kind: "boolean",
|
|
173
|
+
group: "worktree",
|
|
174
|
+
description: "Create a git worktree for the session. Prerequisite for --tmux.",
|
|
175
|
+
},
|
|
176
|
+
{
|
|
177
|
+
id: "tmux",
|
|
178
|
+
label: "tmux session",
|
|
179
|
+
flag: "--tmux",
|
|
180
|
+
kind: "select",
|
|
181
|
+
group: "worktree",
|
|
182
|
+
requires: "worktree",
|
|
183
|
+
description: "Create a tmux session for the worktree. Requires --worktree. iTerm2 native panes when available; classic for traditional tmux.",
|
|
184
|
+
options: [
|
|
185
|
+
{ label: "auto (iTerm2 native)", value: "" },
|
|
186
|
+
{ label: "classic", value: "classic" },
|
|
187
|
+
],
|
|
188
|
+
},
|
|
189
|
+
];
|
|
190
|
+
/** Look up a flag by id. Throws if missing — keeps callers honest. */
|
|
191
|
+
export function getFlagById(id) {
|
|
192
|
+
const f = ALIAS_FLAGS.find((flag) => flag.id === id);
|
|
193
|
+
if (!f)
|
|
194
|
+
throw new Error(`Unknown alias flag id: ${id}`);
|
|
195
|
+
return f;
|
|
196
|
+
}
|