magus-cli 7.11.8 → 7.12.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 +4 -4
- package/src/__tests__/alias-agent-teams.test.ts +259 -0
- package/src/__tests__/alias-autocompact.test.ts +169 -0
- package/src/__tests__/alias-env-adoption.test.ts +334 -0
- package/src/__tests__/alias-option-compatibility.test.ts +92 -0
- package/src/data/alias-flags.ts +123 -1
- package/src/services/alias-shell-writer.ts +100 -21
- package/src/ui/screens/AliasScreen.tsx +31 -6
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "magus-cli",
|
|
3
|
-
"version": "7.
|
|
3
|
+
"version": "7.12.0",
|
|
4
4
|
"description": "TUI tool for managing Claude Code plugins, MCPs, and configuration",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "src/main.tsx",
|
|
@@ -65,8 +65,8 @@
|
|
|
65
65
|
"typescript": "^5.6.3"
|
|
66
66
|
},
|
|
67
67
|
"optionalDependencies": {
|
|
68
|
-
"magus-cli-darwin-arm64": "7.
|
|
69
|
-
"magus-cli-darwin-x64": "7.
|
|
70
|
-
"magus-cli-linux-x64": "7.
|
|
68
|
+
"magus-cli-darwin-arm64": "7.12.0",
|
|
69
|
+
"magus-cli-darwin-x64": "7.12.0",
|
|
70
|
+
"magus-cli-linux-x64": "7.12.0"
|
|
71
71
|
}
|
|
72
72
|
}
|
|
@@ -0,0 +1,259 @@
|
|
|
1
|
+
import { describe, expect, it } from "bun:test";
|
|
2
|
+
import { ALIAS_FLAGS, FLAG_GROUPS, getFlagById } from "../data/alias-flags";
|
|
3
|
+
import {
|
|
4
|
+
type ShellKind,
|
|
5
|
+
parseAliasFromRc,
|
|
6
|
+
renderAlias,
|
|
7
|
+
renderArgs,
|
|
8
|
+
renderArgsAsTokens,
|
|
9
|
+
renderEnv,
|
|
10
|
+
} from "../services/alias-shell-writer";
|
|
11
|
+
import type { AliasConfig } from "../services/alias-store";
|
|
12
|
+
|
|
13
|
+
const assignment = "CLAUDE_CODE_EXPERIMENTAL_AGENT_TEAMS=1";
|
|
14
|
+
const shells: ShellKind[] = ["bash", "zsh", "fish"];
|
|
15
|
+
const modes = ["tmux", "iterm2", "in-process", "auto"];
|
|
16
|
+
|
|
17
|
+
function expectAliasLine(
|
|
18
|
+
block: string,
|
|
19
|
+
shell: ShellKind,
|
|
20
|
+
envEnabled: boolean,
|
|
21
|
+
argv: string[],
|
|
22
|
+
aliasName = "c",
|
|
23
|
+
) {
|
|
24
|
+
const lines = block.split("\n").filter((line) => line.startsWith("alias "));
|
|
25
|
+
const prefix = envEnabled
|
|
26
|
+
? `${(shell === "fish" ? "env " : "") + assignment} claude`
|
|
27
|
+
: "claude";
|
|
28
|
+
const bareArgs = argv.length === 0 ? "" : ` ${argv.join(" ")}`;
|
|
29
|
+
|
|
30
|
+
expect(lines).toHaveLength(1);
|
|
31
|
+
if (shell === "fish") {
|
|
32
|
+
const quotedArgs =
|
|
33
|
+
argv.length === 0 ? "" : ` ${argv.map((arg) => `'${arg}'`).join(" ")}`;
|
|
34
|
+
expect([
|
|
35
|
+
`alias ${aliasName} '${prefix}${bareArgs}'`,
|
|
36
|
+
`alias ${aliasName} '${prefix}${quotedArgs}'`,
|
|
37
|
+
]).toContain(lines[0]);
|
|
38
|
+
} else {
|
|
39
|
+
expect(lines).toEqual([`alias ${aliasName}='${prefix}${bareArgs}'`]);
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
describe("agent teams catalog", () => {
|
|
44
|
+
it("TEST-7 REQ-1 REQ-4 exposes an env-backed boolean and the Agent teams group", () => {
|
|
45
|
+
expect(getFlagById("agent-teams")).toMatchObject({
|
|
46
|
+
id: "agent-teams",
|
|
47
|
+
label: "Agent teams",
|
|
48
|
+
kind: "boolean",
|
|
49
|
+
env: assignment,
|
|
50
|
+
flag: assignment,
|
|
51
|
+
group: "agents",
|
|
52
|
+
});
|
|
53
|
+
expect(FLAG_GROUPS.filter((group) => group.id === "agents")).toEqual([
|
|
54
|
+
{ id: "agents", label: "Agent teams" },
|
|
55
|
+
]);
|
|
56
|
+
expect(
|
|
57
|
+
ALIAS_FLAGS.filter((flag) => flag.id === "agent-teams"),
|
|
58
|
+
).toHaveLength(1);
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
it("TEST-8 REQ-4 exposes exactly the teammate choices and their dependency", () => {
|
|
62
|
+
const flag = getFlagById("teammate-mode");
|
|
63
|
+
|
|
64
|
+
expect(flag).toMatchObject({
|
|
65
|
+
id: "teammate-mode",
|
|
66
|
+
label: "Teammate panes",
|
|
67
|
+
kind: "select",
|
|
68
|
+
flag: "--teammate-mode",
|
|
69
|
+
group: "agents",
|
|
70
|
+
requires: "agent-teams",
|
|
71
|
+
});
|
|
72
|
+
expect(flag.options?.map((option) => option.value).sort()).toEqual([
|
|
73
|
+
"auto",
|
|
74
|
+
"in-process",
|
|
75
|
+
"iterm2",
|
|
76
|
+
"tmux",
|
|
77
|
+
]);
|
|
78
|
+
expect(flag.env).toBeUndefined();
|
|
79
|
+
});
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
describe("environment and argv separation", () => {
|
|
83
|
+
for (const state of ["enabled", "disabled", "missing"] as const) {
|
|
84
|
+
it(`TEST-9 REQ-1 REQ-6 REQ-7 REQ-9 separates env from argv when agent teams is ${state}`, () => {
|
|
85
|
+
const config: AliasConfig = { aliasName: "c", flags: {} };
|
|
86
|
+
if (state !== "missing") {
|
|
87
|
+
config.flags["agent-teams"] = {
|
|
88
|
+
kind: "boolean",
|
|
89
|
+
enabled: state === "enabled",
|
|
90
|
+
};
|
|
91
|
+
}
|
|
92
|
+
const expectedEnv = state === "enabled" ? [assignment] : [];
|
|
93
|
+
|
|
94
|
+
expect(renderEnv(config)).toEqual(expectedEnv);
|
|
95
|
+
expect(renderArgs(config)).toEqual([]);
|
|
96
|
+
expect(renderArgsAsTokens(config)).toEqual(expectedEnv);
|
|
97
|
+
});
|
|
98
|
+
}
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
describe("agent teams shell output and POSIX round-trips", () => {
|
|
102
|
+
// Fish rendering is covered; the parse-back contract uses POSIX alias lines.
|
|
103
|
+
for (const shell of shells) {
|
|
104
|
+
it(`TEST-10 REQ-8 REQ-13 renders agent teams alone before claude on ${shell}`, () => {
|
|
105
|
+
const config: AliasConfig = {
|
|
106
|
+
aliasName: "c",
|
|
107
|
+
flags: { "agent-teams": { kind: "boolean", enabled: true } },
|
|
108
|
+
};
|
|
109
|
+
|
|
110
|
+
const rendered = renderAlias(config, shell);
|
|
111
|
+
|
|
112
|
+
expect(rendered.shell).toBe(shell);
|
|
113
|
+
expectAliasLine(rendered.block, shell, true, []);
|
|
114
|
+
if (shell !== "fish") {
|
|
115
|
+
expect(parseAliasFromRc(rendered.block)).toEqual({
|
|
116
|
+
aliasName: "c",
|
|
117
|
+
flags: { "agent-teams": { kind: "boolean", enabled: true } },
|
|
118
|
+
});
|
|
119
|
+
}
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
for (const mode of modes) {
|
|
123
|
+
it(`TEST-11 REQ-4 REQ-6 REQ-7 REQ-8 REQ-9 REQ-13 ${shell === "fish" ? "renders " : "renders and round-trips "}${mode} teammates on ${shell}`, () => {
|
|
124
|
+
const config: AliasConfig = {
|
|
125
|
+
aliasName: "c",
|
|
126
|
+
flags: {
|
|
127
|
+
"agent-teams": { kind: "boolean", enabled: true },
|
|
128
|
+
"teammate-mode": { kind: "select", enabled: true, value: mode },
|
|
129
|
+
},
|
|
130
|
+
};
|
|
131
|
+
|
|
132
|
+
const rendered = renderAlias(config, shell);
|
|
133
|
+
|
|
134
|
+
expect(renderEnv(config)).toEqual([assignment]);
|
|
135
|
+
expect(renderArgs(config)).toEqual([
|
|
136
|
+
{ kind: "literal", text: "--teammate-mode" },
|
|
137
|
+
{ kind: "literal", text: mode },
|
|
138
|
+
]);
|
|
139
|
+
expect(renderArgsAsTokens(config)).toEqual([
|
|
140
|
+
assignment,
|
|
141
|
+
"--teammate-mode",
|
|
142
|
+
mode,
|
|
143
|
+
]);
|
|
144
|
+
expectAliasLine(rendered.block, shell, true, ["--teammate-mode", mode]);
|
|
145
|
+
if (shell !== "fish") {
|
|
146
|
+
expect(parseAliasFromRc(rendered.block)).toEqual({
|
|
147
|
+
aliasName: "c",
|
|
148
|
+
flags: {
|
|
149
|
+
"agent-teams": { kind: "boolean", enabled: true },
|
|
150
|
+
"teammate-mode": { kind: "select", enabled: true, value: mode },
|
|
151
|
+
},
|
|
152
|
+
});
|
|
153
|
+
}
|
|
154
|
+
});
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
for (const state of ["missing", "disabled"] as const) {
|
|
158
|
+
it(`TEST-12 REQ-14 drops teammate-mode with ${state} agent teams but keeps context size on ${shell}`, () => {
|
|
159
|
+
const config: AliasConfig = {
|
|
160
|
+
aliasName: "c",
|
|
161
|
+
flags: {
|
|
162
|
+
"teammate-mode": { kind: "select", enabled: true, value: "tmux" },
|
|
163
|
+
autocompact: { kind: "text", enabled: true, value: "200k" },
|
|
164
|
+
},
|
|
165
|
+
};
|
|
166
|
+
if (state === "disabled") {
|
|
167
|
+
config.flags["agent-teams"] = { kind: "boolean", enabled: false };
|
|
168
|
+
}
|
|
169
|
+
const original = structuredClone(config);
|
|
170
|
+
|
|
171
|
+
const rendered = renderAlias(config, shell);
|
|
172
|
+
|
|
173
|
+
expect(renderEnv(config)).toEqual([]);
|
|
174
|
+
expect(renderArgs(config)).toEqual([
|
|
175
|
+
{ kind: "literal", text: "--autocompact" },
|
|
176
|
+
{ kind: "literal", text: "200k" },
|
|
177
|
+
]);
|
|
178
|
+
expect(renderArgsAsTokens(config)).toEqual(["--autocompact", "200k"]);
|
|
179
|
+
expectAliasLine(rendered.block, shell, false, [
|
|
180
|
+
"--autocompact",
|
|
181
|
+
"200k",
|
|
182
|
+
]);
|
|
183
|
+
if (shell !== "fish") {
|
|
184
|
+
expect(parseAliasFromRc(rendered.block)).toEqual({
|
|
185
|
+
aliasName: "c",
|
|
186
|
+
flags: {
|
|
187
|
+
autocompact: { kind: "text", enabled: true, value: "200k" },
|
|
188
|
+
},
|
|
189
|
+
});
|
|
190
|
+
}
|
|
191
|
+
expect(config).toEqual(original);
|
|
192
|
+
});
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
it(`TEST-13 REQ-4 REQ-7 REQ-8 REQ-13 keeps agent teams but omits a disabled teammate mode on ${shell}`, () => {
|
|
196
|
+
const config: AliasConfig = {
|
|
197
|
+
aliasName: "c",
|
|
198
|
+
flags: {
|
|
199
|
+
"agent-teams": { kind: "boolean", enabled: true },
|
|
200
|
+
"teammate-mode": { kind: "select", enabled: false, value: "tmux" },
|
|
201
|
+
},
|
|
202
|
+
};
|
|
203
|
+
|
|
204
|
+
const rendered = renderAlias(config, shell);
|
|
205
|
+
|
|
206
|
+
expect(renderArgs(config)).toEqual([]);
|
|
207
|
+
expect(renderArgsAsTokens(config)).toEqual([assignment]);
|
|
208
|
+
expectAliasLine(rendered.block, shell, true, []);
|
|
209
|
+
if (shell !== "fish") {
|
|
210
|
+
expect(parseAliasFromRc(rendered.block)).toEqual({
|
|
211
|
+
aliasName: "c",
|
|
212
|
+
flags: { "agent-teams": { kind: "boolean", enabled: true } },
|
|
213
|
+
});
|
|
214
|
+
}
|
|
215
|
+
});
|
|
216
|
+
|
|
217
|
+
it(`TEST-14 REQ-6 REQ-7 REQ-8 REQ-9 REQ-13 renders all new options with env first on ${shell}`, () => {
|
|
218
|
+
const config: AliasConfig = {
|
|
219
|
+
aliasName: "crew",
|
|
220
|
+
flags: {
|
|
221
|
+
"teammate-mode": { kind: "select", enabled: true, value: "tmux" },
|
|
222
|
+
autocompact: { kind: "text", enabled: true, value: "200k" },
|
|
223
|
+
"agent-teams": { kind: "boolean", enabled: true },
|
|
224
|
+
},
|
|
225
|
+
};
|
|
226
|
+
const contextIndex = ALIAS_FLAGS.findIndex(
|
|
227
|
+
(flag) => flag.id === "autocompact",
|
|
228
|
+
);
|
|
229
|
+
const modeIndex = ALIAS_FLAGS.findIndex(
|
|
230
|
+
(flag) => flag.id === "teammate-mode",
|
|
231
|
+
);
|
|
232
|
+
expect(contextIndex).toBeGreaterThanOrEqual(0);
|
|
233
|
+
expect(modeIndex).toBeGreaterThanOrEqual(0);
|
|
234
|
+
const argv =
|
|
235
|
+
contextIndex < modeIndex
|
|
236
|
+
? ["--autocompact", "200k", "--teammate-mode", "tmux"]
|
|
237
|
+
: ["--teammate-mode", "tmux", "--autocompact", "200k"];
|
|
238
|
+
|
|
239
|
+
const rendered = renderAlias(config, shell);
|
|
240
|
+
|
|
241
|
+
expect(renderEnv(config)).toEqual([assignment]);
|
|
242
|
+
expect(renderArgs(config)).toEqual(
|
|
243
|
+
argv.map((text) => ({ kind: "literal", text })),
|
|
244
|
+
);
|
|
245
|
+
expect(renderArgsAsTokens(config)).toEqual([assignment, ...argv]);
|
|
246
|
+
expectAliasLine(rendered.block, shell, true, argv, "crew");
|
|
247
|
+
if (shell !== "fish") {
|
|
248
|
+
expect(parseAliasFromRc(rendered.block)).toEqual({
|
|
249
|
+
aliasName: "crew",
|
|
250
|
+
flags: {
|
|
251
|
+
"agent-teams": { kind: "boolean", enabled: true },
|
|
252
|
+
"teammate-mode": { kind: "select", enabled: true, value: "tmux" },
|
|
253
|
+
autocompact: { kind: "text", enabled: true, value: "200k" },
|
|
254
|
+
},
|
|
255
|
+
});
|
|
256
|
+
}
|
|
257
|
+
});
|
|
258
|
+
}
|
|
259
|
+
});
|
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
import { describe, expect, it } from "bun:test";
|
|
2
|
+
import {
|
|
3
|
+
AUTOCOMPACT_MAX_TOKENS,
|
|
4
|
+
AUTOCOMPACT_MIN_TOKENS,
|
|
5
|
+
getFlagById,
|
|
6
|
+
parseAutocompactSize,
|
|
7
|
+
validateAutocompactSize,
|
|
8
|
+
} from "../data/alias-flags";
|
|
9
|
+
import {
|
|
10
|
+
type ShellKind,
|
|
11
|
+
parseAliasFromRc,
|
|
12
|
+
renderAlias,
|
|
13
|
+
renderArgs,
|
|
14
|
+
renderArgsAsTokens,
|
|
15
|
+
renderEnv,
|
|
16
|
+
} from "../services/alias-shell-writer";
|
|
17
|
+
import type { AliasConfig } from "../services/alias-store";
|
|
18
|
+
|
|
19
|
+
const shells: ShellKind[] = ["bash", "zsh", "fish"];
|
|
20
|
+
|
|
21
|
+
function expectSizeAlias(block: string, shell: ShellKind, value?: string) {
|
|
22
|
+
const lines = block.split("\n").filter((line) => line.startsWith("alias "));
|
|
23
|
+
const args = value === undefined ? "" : ` --autocompact ${value}`;
|
|
24
|
+
|
|
25
|
+
expect(lines).toHaveLength(1);
|
|
26
|
+
if (shell === "fish") {
|
|
27
|
+
const quotedArgs = value === undefined ? "" : ` '--autocompact' '${value}'`;
|
|
28
|
+
expect([
|
|
29
|
+
`alias c 'claude${args}'`,
|
|
30
|
+
`alias c 'claude${quotedArgs}'`,
|
|
31
|
+
]).toContain(lines[0]);
|
|
32
|
+
} else {
|
|
33
|
+
expect(lines).toEqual([`alias c='claude${args}'`]);
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
describe("context window size catalog and validation", () => {
|
|
38
|
+
it("TEST-1 REQ-2 exposes the context text option and its validator", () => {
|
|
39
|
+
const flag = getFlagById("autocompact");
|
|
40
|
+
|
|
41
|
+
expect(flag).toMatchObject({
|
|
42
|
+
id: "autocompact",
|
|
43
|
+
label: "Context window size",
|
|
44
|
+
kind: "text",
|
|
45
|
+
flag: "--autocompact",
|
|
46
|
+
group: "context",
|
|
47
|
+
});
|
|
48
|
+
expect(typeof flag.validateValue).toBe("function");
|
|
49
|
+
expect(flag.env).toBeUndefined();
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
const validSizes: Array<[string, number | "auto"]> = [
|
|
53
|
+
["auto", "auto"],
|
|
54
|
+
["AUTO", "auto"],
|
|
55
|
+
["aUtO", "auto"],
|
|
56
|
+
["200k", 200000],
|
|
57
|
+
["1m", 1000000],
|
|
58
|
+
["200000", 200000],
|
|
59
|
+
["500", 500000],
|
|
60
|
+
["100k", 100000],
|
|
61
|
+
["100000", 100000],
|
|
62
|
+
["100", 100000],
|
|
63
|
+
["1000k", 1000000],
|
|
64
|
+
["1000000", 1000000],
|
|
65
|
+
["1000", 1000000],
|
|
66
|
+
];
|
|
67
|
+
|
|
68
|
+
for (const [value, expected] of validSizes) {
|
|
69
|
+
it(`TEST-2 REQ-3 accepts ${JSON.stringify(value)} as ${expected}`, () => {
|
|
70
|
+
const catalogValidator = getFlagById("autocompact").validateValue;
|
|
71
|
+
|
|
72
|
+
expect(parseAutocompactSize(value)).toBe(expected);
|
|
73
|
+
expect(validateAutocompactSize(value)).toBeNull();
|
|
74
|
+
expect(catalogValidator?.(value)).toBeNull();
|
|
75
|
+
});
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
const invalidSizes = [
|
|
79
|
+
"50k",
|
|
80
|
+
"2m",
|
|
81
|
+
"abc",
|
|
82
|
+
"",
|
|
83
|
+
"0",
|
|
84
|
+
"99",
|
|
85
|
+
"1001",
|
|
86
|
+
"99k",
|
|
87
|
+
"1001k",
|
|
88
|
+
"99999",
|
|
89
|
+
"1000001",
|
|
90
|
+
];
|
|
91
|
+
|
|
92
|
+
for (const value of invalidSizes) {
|
|
93
|
+
it(`TEST-3 REQ-3 rejects ${JSON.stringify(value)} with a reason`, () => {
|
|
94
|
+
const catalogValidator = getFlagById("autocompact").validateValue;
|
|
95
|
+
|
|
96
|
+
expect(parseAutocompactSize(value)).toBeUndefined();
|
|
97
|
+
const reasons = [
|
|
98
|
+
validateAutocompactSize(value),
|
|
99
|
+
catalogValidator?.(value),
|
|
100
|
+
];
|
|
101
|
+
for (const reason of reasons) {
|
|
102
|
+
expect(reason).toEqual(expect.any(String));
|
|
103
|
+
expect(reason).toMatch(/\S/);
|
|
104
|
+
}
|
|
105
|
+
});
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
it("TEST-4 REQ-3 exposes the inclusive 100000–1000000 token limits", () => {
|
|
109
|
+
expect(AUTOCOMPACT_MIN_TOKENS).toBe(100000);
|
|
110
|
+
expect(AUTOCOMPACT_MAX_TOKENS).toBe(1000000);
|
|
111
|
+
});
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
describe("context window size shell output", () => {
|
|
115
|
+
// Fish rendering is covered; the parse-back contract uses POSIX alias lines.
|
|
116
|
+
for (const shell of shells) {
|
|
117
|
+
for (const value of ["auto", "200k", "1m", "200000", "500"]) {
|
|
118
|
+
it(`TEST-5 REQ-2 REQ-7 REQ-8 REQ-13 ${shell === "fish" ? "renders " : "renders and round-trips "}${value} on ${shell}`, () => {
|
|
119
|
+
const config: AliasConfig = {
|
|
120
|
+
aliasName: "c",
|
|
121
|
+
flags: {
|
|
122
|
+
autocompact: { kind: "text", enabled: true, value },
|
|
123
|
+
},
|
|
124
|
+
};
|
|
125
|
+
|
|
126
|
+
const rendered = renderAlias(config, shell);
|
|
127
|
+
|
|
128
|
+
expect(rendered.shell).toBe(shell);
|
|
129
|
+
expect(renderEnv(config)).toEqual([]);
|
|
130
|
+
expect(renderArgs(config)).toEqual([
|
|
131
|
+
{ kind: "literal", text: "--autocompact" },
|
|
132
|
+
{ kind: "literal", text: value },
|
|
133
|
+
]);
|
|
134
|
+
expect(renderArgsAsTokens(config)).toEqual(["--autocompact", value]);
|
|
135
|
+
expectSizeAlias(rendered.block, shell, value);
|
|
136
|
+
if (shell !== "fish") {
|
|
137
|
+
expect(parseAliasFromRc(rendered.block)).toEqual({
|
|
138
|
+
aliasName: "c",
|
|
139
|
+
flags: {
|
|
140
|
+
autocompact: { kind: "text", enabled: true, value },
|
|
141
|
+
},
|
|
142
|
+
});
|
|
143
|
+
}
|
|
144
|
+
});
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
it(`TEST-6 REQ-2 REQ-10 omits a disabled context size on ${shell}`, () => {
|
|
148
|
+
const config: AliasConfig = {
|
|
149
|
+
aliasName: "c",
|
|
150
|
+
flags: {
|
|
151
|
+
autocompact: { kind: "text", enabled: false, value: "200k" },
|
|
152
|
+
},
|
|
153
|
+
};
|
|
154
|
+
|
|
155
|
+
const rendered = renderAlias(config, shell);
|
|
156
|
+
|
|
157
|
+
expect(renderArgs(config)).toEqual([]);
|
|
158
|
+
expect(renderArgsAsTokens(config)).toEqual([]);
|
|
159
|
+
expect(renderEnv(config)).toEqual([]);
|
|
160
|
+
expectSizeAlias(rendered.block, shell);
|
|
161
|
+
if (shell !== "fish") {
|
|
162
|
+
expect(parseAliasFromRc(rendered.block)).toEqual({
|
|
163
|
+
aliasName: "c",
|
|
164
|
+
flags: {},
|
|
165
|
+
});
|
|
166
|
+
}
|
|
167
|
+
});
|
|
168
|
+
}
|
|
169
|
+
});
|
|
@@ -0,0 +1,334 @@
|
|
|
1
|
+
import { describe, expect, it } from "bun:test";
|
|
2
|
+
import {
|
|
3
|
+
argsToFlagValuesWithLeftovers,
|
|
4
|
+
findAdoptableAlias,
|
|
5
|
+
parseAliasFromRc,
|
|
6
|
+
parseAliasLine,
|
|
7
|
+
parseManagedBlock,
|
|
8
|
+
renderAlias,
|
|
9
|
+
} from "../services/alias-shell-writer";
|
|
10
|
+
import type { AliasConfig, FlagValue } from "../services/alias-store";
|
|
11
|
+
|
|
12
|
+
const assignment = "CLAUDE_CODE_EXPERIMENTAL_AGENT_TEAMS=1";
|
|
13
|
+
|
|
14
|
+
function combinedFlags(): Record<string, FlagValue> {
|
|
15
|
+
return {
|
|
16
|
+
"agent-teams": { kind: "boolean", enabled: true },
|
|
17
|
+
"teammate-mode": { kind: "select", enabled: true, value: "tmux" },
|
|
18
|
+
autocompact: { kind: "text", enabled: true, value: "200k" },
|
|
19
|
+
};
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
const forms: Array<{
|
|
23
|
+
name: string;
|
|
24
|
+
envLauncher: boolean;
|
|
25
|
+
line: (body: string) => string;
|
|
26
|
+
}> = [
|
|
27
|
+
{ name: "POSIX", envLauncher: false, line: (body) => `alias crew='${body}'` },
|
|
28
|
+
// The env launcher is a body form; the parser contract uses POSIX alias lines.
|
|
29
|
+
{
|
|
30
|
+
name: "POSIX with env launcher",
|
|
31
|
+
envLauncher: true,
|
|
32
|
+
line: (body) => `alias crew='${body}'`,
|
|
33
|
+
},
|
|
34
|
+
];
|
|
35
|
+
|
|
36
|
+
function commandBody(
|
|
37
|
+
envLauncher: boolean,
|
|
38
|
+
env: string[],
|
|
39
|
+
argv: string[],
|
|
40
|
+
): string {
|
|
41
|
+
return [
|
|
42
|
+
...(envLauncher && env.length > 0 ? ["env"] : []),
|
|
43
|
+
...env,
|
|
44
|
+
"claude",
|
|
45
|
+
...argv,
|
|
46
|
+
].join(" ");
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
describe("managed assignment-prefixed alias parsing", () => {
|
|
50
|
+
const examples = [
|
|
51
|
+
{
|
|
52
|
+
name: "POSIX known assignment",
|
|
53
|
+
line: `alias crew='${assignment} claude --teammate-mode tmux --autocompact 200k'`,
|
|
54
|
+
args: [assignment, "--teammate-mode", "tmux", "--autocompact", "200k"],
|
|
55
|
+
},
|
|
56
|
+
{
|
|
57
|
+
name: "POSIX alias with env launcher",
|
|
58
|
+
line: `alias crew='env ${assignment} claude --teammate-mode tmux --autocompact 200k'`,
|
|
59
|
+
args: [assignment, "--teammate-mode", "tmux", "--autocompact", "200k"],
|
|
60
|
+
},
|
|
61
|
+
{
|
|
62
|
+
name: "POSIX unknown before known assignment",
|
|
63
|
+
line: `alias crew='EXTRA=keep ${assignment} claude --autocompact 200k'`,
|
|
64
|
+
args: ["EXTRA=keep", assignment, "--autocompact", "200k"],
|
|
65
|
+
},
|
|
66
|
+
{
|
|
67
|
+
name: "POSIX env launcher with unknown after known assignment",
|
|
68
|
+
line: `alias crew='env ${assignment} EXTRA=a=b claude --teammate-mode tmux'`,
|
|
69
|
+
args: [assignment, "EXTRA=a=b", "--teammate-mode", "tmux"],
|
|
70
|
+
},
|
|
71
|
+
];
|
|
72
|
+
|
|
73
|
+
for (const example of examples) {
|
|
74
|
+
it(`TEST-15 REQ-11 preserves env-first tokens for ${example.name}`, () => {
|
|
75
|
+
const block = [
|
|
76
|
+
"# >>> magus managed alias >>>",
|
|
77
|
+
example.line,
|
|
78
|
+
"# <<< magus managed alias <<<",
|
|
79
|
+
"",
|
|
80
|
+
].join("\n");
|
|
81
|
+
const expected = { aliasName: "crew", args: [...example.args] };
|
|
82
|
+
|
|
83
|
+
expect(parseAliasLine(example.line)).toEqual(expected);
|
|
84
|
+
expect(parseManagedBlock(block)).toEqual(expected);
|
|
85
|
+
});
|
|
86
|
+
}
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
describe("assignment mapping and leftovers", () => {
|
|
90
|
+
const mappingCases: Array<{
|
|
91
|
+
name: string;
|
|
92
|
+
args: string[];
|
|
93
|
+
flags: () => Record<string, FlagValue>;
|
|
94
|
+
leftovers: string[];
|
|
95
|
+
}> = [
|
|
96
|
+
{
|
|
97
|
+
name: "known assignment and all new argv options",
|
|
98
|
+
args: [assignment, "--teammate-mode", "tmux", "--autocompact", "200k"],
|
|
99
|
+
flags: combinedFlags,
|
|
100
|
+
leftovers: [],
|
|
101
|
+
},
|
|
102
|
+
{
|
|
103
|
+
name: "unknown assignment before the known one",
|
|
104
|
+
args: [
|
|
105
|
+
"EXTRA=keep",
|
|
106
|
+
assignment,
|
|
107
|
+
"--teammate-mode",
|
|
108
|
+
"tmux",
|
|
109
|
+
"--autocompact",
|
|
110
|
+
"200k",
|
|
111
|
+
],
|
|
112
|
+
flags: combinedFlags,
|
|
113
|
+
leftovers: ["EXTRA=keep"],
|
|
114
|
+
},
|
|
115
|
+
{
|
|
116
|
+
name: "unknown assignments after the known one",
|
|
117
|
+
args: [
|
|
118
|
+
assignment,
|
|
119
|
+
"EMPTY=",
|
|
120
|
+
"EXTRA=a=b",
|
|
121
|
+
"--teammate-mode",
|
|
122
|
+
"tmux",
|
|
123
|
+
"--autocompact",
|
|
124
|
+
"200k",
|
|
125
|
+
],
|
|
126
|
+
flags: combinedFlags,
|
|
127
|
+
leftovers: ["EMPTY=", "EXTRA=a=b"],
|
|
128
|
+
},
|
|
129
|
+
{
|
|
130
|
+
name: "known env name with a different value",
|
|
131
|
+
args: ["CLAUDE_CODE_EXPERIMENTAL_AGENT_TEAMS=0", "--ide"],
|
|
132
|
+
flags: () => ({ ide: { kind: "boolean", enabled: true } }),
|
|
133
|
+
leftovers: ["CLAUDE_CODE_EXPERIMENTAL_AGENT_TEAMS=0"],
|
|
134
|
+
},
|
|
135
|
+
];
|
|
136
|
+
|
|
137
|
+
for (const example of mappingCases) {
|
|
138
|
+
it(`TEST-16 REQ-12 retains every unmatched token for ${example.name}`, () => {
|
|
139
|
+
const args = [...example.args];
|
|
140
|
+
|
|
141
|
+
expect(argsToFlagValuesWithLeftovers(args)).toEqual({
|
|
142
|
+
flags: example.flags(),
|
|
143
|
+
leftovers: [...example.leftovers],
|
|
144
|
+
});
|
|
145
|
+
});
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
it("TEST-17 REQ-12 keeps an assignment after an argv flag as a leftover rather than enabling teams", () => {
|
|
149
|
+
const args = ["--ide", assignment];
|
|
150
|
+
|
|
151
|
+
expect(argsToFlagValuesWithLeftovers(args)).toEqual({
|
|
152
|
+
flags: { ide: { kind: "boolean", enabled: true } },
|
|
153
|
+
leftovers: [assignment],
|
|
154
|
+
});
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
it("TEST-17 REQ-12 retains an argv assignment without losing an earlier recognized env assignment", () => {
|
|
158
|
+
const args = [assignment, "--ide", "EXTRA=keep"];
|
|
159
|
+
|
|
160
|
+
expect(argsToFlagValuesWithLeftovers(args)).toEqual({
|
|
161
|
+
flags: {
|
|
162
|
+
"agent-teams": { kind: "boolean", enabled: true },
|
|
163
|
+
ide: { kind: "boolean", enabled: true },
|
|
164
|
+
},
|
|
165
|
+
leftovers: ["EXTRA=keep"],
|
|
166
|
+
});
|
|
167
|
+
});
|
|
168
|
+
|
|
169
|
+
it("TEST-17 REQ-12 leaves an assignment used as a text value inside that text flag", () => {
|
|
170
|
+
const args = ["--append-system-prompt", assignment];
|
|
171
|
+
|
|
172
|
+
expect(argsToFlagValuesWithLeftovers(args)).toEqual({
|
|
173
|
+
flags: {
|
|
174
|
+
"append-system-prompt": {
|
|
175
|
+
kind: "text",
|
|
176
|
+
enabled: true,
|
|
177
|
+
value: assignment,
|
|
178
|
+
},
|
|
179
|
+
},
|
|
180
|
+
leftovers: [],
|
|
181
|
+
});
|
|
182
|
+
});
|
|
183
|
+
});
|
|
184
|
+
|
|
185
|
+
describe("safe adoption of assignment-prefixed aliases", () => {
|
|
186
|
+
const losslessCases: Array<{
|
|
187
|
+
name: string;
|
|
188
|
+
env: string[];
|
|
189
|
+
argv: string[];
|
|
190
|
+
flags: () => Record<string, FlagValue>;
|
|
191
|
+
}> = [
|
|
192
|
+
{
|
|
193
|
+
name: "context size only",
|
|
194
|
+
env: [],
|
|
195
|
+
argv: ["--autocompact", "200k"],
|
|
196
|
+
flags: () => ({
|
|
197
|
+
autocompact: { kind: "text", enabled: true, value: "200k" },
|
|
198
|
+
}),
|
|
199
|
+
},
|
|
200
|
+
{
|
|
201
|
+
name: "agent teams only",
|
|
202
|
+
env: [assignment],
|
|
203
|
+
argv: [],
|
|
204
|
+
flags: () => ({
|
|
205
|
+
"agent-teams": { kind: "boolean", enabled: true },
|
|
206
|
+
}),
|
|
207
|
+
},
|
|
208
|
+
{
|
|
209
|
+
name: "all new options",
|
|
210
|
+
env: [assignment],
|
|
211
|
+
argv: ["--teammate-mode", "tmux", "--autocompact", "200k"],
|
|
212
|
+
flags: combinedFlags,
|
|
213
|
+
},
|
|
214
|
+
];
|
|
215
|
+
|
|
216
|
+
const unknownCases: Array<{
|
|
217
|
+
name: string;
|
|
218
|
+
env: string[];
|
|
219
|
+
argv: string[];
|
|
220
|
+
flags: () => Record<string, FlagValue>;
|
|
221
|
+
leftovers: string[];
|
|
222
|
+
}> = [
|
|
223
|
+
{
|
|
224
|
+
name: "an unknown assignment alone",
|
|
225
|
+
env: ["EXTRA=keep"],
|
|
226
|
+
argv: ["--ide"],
|
|
227
|
+
flags: () => ({ ide: { kind: "boolean", enabled: true } }),
|
|
228
|
+
leftovers: ["EXTRA=keep"],
|
|
229
|
+
},
|
|
230
|
+
{
|
|
231
|
+
name: "an empty-valued unknown assignment before known teams",
|
|
232
|
+
env: ["EMPTY=", assignment],
|
|
233
|
+
argv: ["--teammate-mode", "tmux", "--autocompact", "200k"],
|
|
234
|
+
flags: combinedFlags,
|
|
235
|
+
leftovers: ["EMPTY="],
|
|
236
|
+
},
|
|
237
|
+
{
|
|
238
|
+
name: "an unknown assignment containing another equals sign after known teams",
|
|
239
|
+
env: [assignment, "EXTRA=a=b"],
|
|
240
|
+
argv: ["--teammate-mode", "tmux", "--autocompact", "200k"],
|
|
241
|
+
flags: combinedFlags,
|
|
242
|
+
leftovers: ["EXTRA=a=b"],
|
|
243
|
+
},
|
|
244
|
+
{
|
|
245
|
+
name: "a known assignment name with an unsupported value",
|
|
246
|
+
env: ["CLAUDE_CODE_EXPERIMENTAL_AGENT_TEAMS=0"],
|
|
247
|
+
argv: ["--ide"],
|
|
248
|
+
flags: () => ({ ide: { kind: "boolean", enabled: true } }),
|
|
249
|
+
leftovers: ["CLAUDE_CODE_EXPERIMENTAL_AGENT_TEAMS=0"],
|
|
250
|
+
},
|
|
251
|
+
];
|
|
252
|
+
|
|
253
|
+
for (const form of forms) {
|
|
254
|
+
for (const example of losslessCases) {
|
|
255
|
+
it(`TEST-18 REQ-11 REQ-13 adopts ${example.name} losslessly in ${form.name}`, () => {
|
|
256
|
+
const line = form.line(
|
|
257
|
+
commandBody(form.envLauncher, example.env, example.argv),
|
|
258
|
+
);
|
|
259
|
+
const rc = `# personal aliases\n${line}\n# end aliases\n`;
|
|
260
|
+
const expectedFlags = example.flags();
|
|
261
|
+
|
|
262
|
+
const adopted = findAdoptableAlias(rc);
|
|
263
|
+
|
|
264
|
+
expect(adopted).toEqual({
|
|
265
|
+
name: "crew",
|
|
266
|
+
flags: expectedFlags,
|
|
267
|
+
lineRange: [1, 1],
|
|
268
|
+
leftovers: [],
|
|
269
|
+
lossless: true,
|
|
270
|
+
rawLine: line,
|
|
271
|
+
others: [],
|
|
272
|
+
});
|
|
273
|
+
if (adopted === null) {
|
|
274
|
+
throw new Error("Expected a lossless adoptable alias");
|
|
275
|
+
}
|
|
276
|
+
const config: AliasConfig = {
|
|
277
|
+
aliasName: adopted.name,
|
|
278
|
+
flags: adopted.flags,
|
|
279
|
+
};
|
|
280
|
+
expect(parseAliasFromRc(renderAlias(config, "bash").block)).toEqual({
|
|
281
|
+
aliasName: "crew",
|
|
282
|
+
flags: expectedFlags,
|
|
283
|
+
});
|
|
284
|
+
});
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
for (const example of unknownCases) {
|
|
288
|
+
it(`TEST-19 REQ-11 REQ-12 REQ-13 preserves ${example.name} as a leftover in ${form.name}`, () => {
|
|
289
|
+
const line = form.line(
|
|
290
|
+
commandBody(form.envLauncher, example.env, example.argv),
|
|
291
|
+
);
|
|
292
|
+
const rc = `# personal aliases\n${line}\n# end aliases\n`;
|
|
293
|
+
|
|
294
|
+
const adopted = findAdoptableAlias(rc);
|
|
295
|
+
|
|
296
|
+
expect(adopted).toEqual({
|
|
297
|
+
name: "crew",
|
|
298
|
+
flags: example.flags(),
|
|
299
|
+
lineRange: [1, 1],
|
|
300
|
+
leftovers: [...example.leftovers],
|
|
301
|
+
lossless: false,
|
|
302
|
+
rawLine: line,
|
|
303
|
+
others: [],
|
|
304
|
+
});
|
|
305
|
+
});
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
it(`TEST-20 REQ-13 REQ-14 refuses lossless adoption of orphan teammate-mode in ${form.name}`, () => {
|
|
309
|
+
const line = form.line("claude --teammate-mode tmux");
|
|
310
|
+
|
|
311
|
+
const adopted = findAdoptableAlias(`${line}\n`);
|
|
312
|
+
|
|
313
|
+
expect(adopted).toEqual({
|
|
314
|
+
name: "crew",
|
|
315
|
+
flags: {
|
|
316
|
+
"teammate-mode": { kind: "select", enabled: true, value: "tmux" },
|
|
317
|
+
},
|
|
318
|
+
lineRange: [0, 0],
|
|
319
|
+
leftovers: [],
|
|
320
|
+
lossless: false,
|
|
321
|
+
rawLine: line,
|
|
322
|
+
others: [],
|
|
323
|
+
});
|
|
324
|
+
});
|
|
325
|
+
|
|
326
|
+
it(`TEST-21 REQ-11 rejects an assignment-prefixed non-claude alias in ${form.name}`, () => {
|
|
327
|
+
const prefix = form.envLauncher ? "env " : "";
|
|
328
|
+
const line = form.line(`${prefix + assignment} other-command --ide`);
|
|
329
|
+
|
|
330
|
+
expect(parseAliasLine(line)).toBeNull();
|
|
331
|
+
expect(findAdoptableAlias(`${line}\n`)).toBeNull();
|
|
332
|
+
});
|
|
333
|
+
}
|
|
334
|
+
});
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
import { describe, expect, it } from "bun:test";
|
|
2
|
+
import {
|
|
3
|
+
type ShellKind,
|
|
4
|
+
renderAlias,
|
|
5
|
+
renderArgsAsTokens,
|
|
6
|
+
renderEnv,
|
|
7
|
+
} from "../services/alias-shell-writer";
|
|
8
|
+
import {
|
|
9
|
+
type AliasConfig,
|
|
10
|
+
type FlagValue,
|
|
11
|
+
defaultAliasConfig,
|
|
12
|
+
} from "../services/alias-store";
|
|
13
|
+
|
|
14
|
+
const shells: ShellKind[] = ["bash", "zsh", "fish"];
|
|
15
|
+
|
|
16
|
+
describe("alias output when the new options are not enabled", () => {
|
|
17
|
+
const establishedCases: Array<{
|
|
18
|
+
name: string;
|
|
19
|
+
flags: () => Record<string, FlagValue>;
|
|
20
|
+
tokens: string[];
|
|
21
|
+
posixLine: string;
|
|
22
|
+
fishLine: string;
|
|
23
|
+
}> = [
|
|
24
|
+
{
|
|
25
|
+
name: "plain claude alias",
|
|
26
|
+
flags: () => ({}),
|
|
27
|
+
tokens: [],
|
|
28
|
+
posixLine: "alias c='claude'",
|
|
29
|
+
fishLine: "alias c 'claude'",
|
|
30
|
+
},
|
|
31
|
+
{
|
|
32
|
+
name: "ide-only alias",
|
|
33
|
+
flags: () => ({ ide: { kind: "boolean", enabled: true } }),
|
|
34
|
+
tokens: ["--ide"],
|
|
35
|
+
posixLine: "alias c='claude --ide'",
|
|
36
|
+
fishLine: "alias c 'claude '--ide''",
|
|
37
|
+
},
|
|
38
|
+
];
|
|
39
|
+
|
|
40
|
+
for (const shell of shells) {
|
|
41
|
+
for (const example of establishedCases) {
|
|
42
|
+
it(`TEST-22 REQ-10 preserves established ${example.name} bytes on ${shell} with absent or disabled new options`, () => {
|
|
43
|
+
const absent: AliasConfig = {
|
|
44
|
+
aliasName: "c",
|
|
45
|
+
flags: example.flags(),
|
|
46
|
+
};
|
|
47
|
+
const disabled: AliasConfig = {
|
|
48
|
+
aliasName: "c",
|
|
49
|
+
flags: {
|
|
50
|
+
...example.flags(),
|
|
51
|
+
autocompact: { kind: "text", enabled: false, value: "200k" },
|
|
52
|
+
"agent-teams": { kind: "boolean", enabled: false },
|
|
53
|
+
"teammate-mode": { kind: "select", enabled: false, value: "tmux" },
|
|
54
|
+
},
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
const absentOutput = renderAlias(absent, shell);
|
|
58
|
+
const disabledOutput = renderAlias(disabled, shell);
|
|
59
|
+
|
|
60
|
+
expect(absentOutput.shell).toBe(shell);
|
|
61
|
+
expect(disabledOutput).toEqual(absentOutput);
|
|
62
|
+
expect(
|
|
63
|
+
absentOutput.block
|
|
64
|
+
.split("\n")
|
|
65
|
+
.filter((line) => line.startsWith("alias ")),
|
|
66
|
+
).toEqual([shell === "fish" ? example.fishLine : example.posixLine]);
|
|
67
|
+
expect(
|
|
68
|
+
absentOutput.block.startsWith("# >>> magus managed alias >>>\n"),
|
|
69
|
+
).toBe(true);
|
|
70
|
+
expect(
|
|
71
|
+
absentOutput.block.endsWith("# <<< magus managed alias <<<\n"),
|
|
72
|
+
).toBe(true);
|
|
73
|
+
expect(renderEnv(absent)).toEqual([]);
|
|
74
|
+
expect(renderEnv(disabled)).toEqual([]);
|
|
75
|
+
expect(renderArgsAsTokens(absent)).toEqual([...example.tokens]);
|
|
76
|
+
expect(renderArgsAsTokens(disabled)).toEqual([...example.tokens]);
|
|
77
|
+
});
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
it(`TEST-23 REQ-10 leaves all new options off in the default ${shell} alias`, () => {
|
|
81
|
+
const config = defaultAliasConfig();
|
|
82
|
+
const emptyConfig: AliasConfig = { aliasName: "c", flags: {} };
|
|
83
|
+
|
|
84
|
+
expect(config.aliasName).toBe("c");
|
|
85
|
+
expect(renderEnv(config)).toEqual([]);
|
|
86
|
+
expect(renderArgsAsTokens(config)).toEqual([]);
|
|
87
|
+
expect(renderAlias(config, shell)).toEqual(
|
|
88
|
+
renderAlias(emptyConfig, shell),
|
|
89
|
+
);
|
|
90
|
+
});
|
|
91
|
+
}
|
|
92
|
+
});
|
package/src/data/alias-flags.ts
CHANGED
|
@@ -72,6 +72,18 @@ export interface AliasFlag {
|
|
|
72
72
|
* token. See `TEMPLATE_TOKENS` for the supported placeholder set.
|
|
73
73
|
*/
|
|
74
74
|
templated?: boolean;
|
|
75
|
+
/**
|
|
76
|
+
* Environment assignment (`NAME=VALUE`) the alias sets before `claude`,
|
|
77
|
+
* instead of an argv token. Only for `kind: "boolean"`: enabled emits the
|
|
78
|
+
* assignment, disabled emits nothing. `flag` carries the same string so
|
|
79
|
+
* lookups by `flag` keep working.
|
|
80
|
+
*/
|
|
81
|
+
env?: string;
|
|
82
|
+
/**
|
|
83
|
+
* Value check for `kind: "text"` flags, run when the user enters a value.
|
|
84
|
+
* Returns null when the value is valid, otherwise the reason it is not.
|
|
85
|
+
*/
|
|
86
|
+
validateValue?: (value: string) => string | null;
|
|
75
87
|
/** UI grouping; rendered as section headers in the list. */
|
|
76
88
|
group: FlagGroup;
|
|
77
89
|
}
|
|
@@ -100,7 +112,8 @@ export type FlagGroup =
|
|
|
100
112
|
| "channels"
|
|
101
113
|
| "debug"
|
|
102
114
|
| "integration"
|
|
103
|
-
| "worktree"
|
|
115
|
+
| "worktree"
|
|
116
|
+
| "agents";
|
|
104
117
|
|
|
105
118
|
export const FLAG_GROUPS: Array<{ id: FlagGroup; label: string }> = [
|
|
106
119
|
{ id: "danger", label: "Permissions" },
|
|
@@ -110,8 +123,81 @@ export const FLAG_GROUPS: Array<{ id: FlagGroup; label: string }> = [
|
|
|
110
123
|
{ id: "debug", label: "Debug" },
|
|
111
124
|
{ id: "integration", label: "Integrations" },
|
|
112
125
|
{ id: "worktree", label: "Worktree" },
|
|
126
|
+
{ id: "agents", label: "Agent teams" },
|
|
113
127
|
];
|
|
114
128
|
|
|
129
|
+
/** Smallest auto-compact window Claude Code accepts, in tokens. */
|
|
130
|
+
export const AUTOCOMPACT_MIN_TOKENS = 100_000;
|
|
131
|
+
/** Largest auto-compact window Claude Code accepts, in tokens. */
|
|
132
|
+
export const AUTOCOMPACT_MAX_TOKENS = 1_000_000;
|
|
133
|
+
|
|
134
|
+
/** Claude Code's own wording when it refuses an `--autocompact` value. */
|
|
135
|
+
export const AUTOCOMPACT_INVALID_REASON =
|
|
136
|
+
"It must be 'auto', or between 100k and 1M (e.g. 500k, 200000, or 200 as shorthand)";
|
|
137
|
+
|
|
138
|
+
const EXPONENT_NUMBER = /^[+-]?(\d+(\.\d*)?|\.\d+)[eE][+-]?\d+$/;
|
|
139
|
+
const GROUPED_DIGITS = /^[+-]?\d{1,3}([_, ])\d{3}(?:\1\d{3})*$/;
|
|
140
|
+
const DIGIT_SEPARATORS = /[_, ]/g;
|
|
141
|
+
|
|
142
|
+
/**
|
|
143
|
+
* Read an unsuffixed number the way Claude Code does: exponent notation
|
|
144
|
+
* (`2e5`) when it is an integer, digit groups (`200,000`, `200_000`), and
|
|
145
|
+
* otherwise `parseInt`, which keeps the leading digits of the string.
|
|
146
|
+
*/
|
|
147
|
+
function parsePlainNumber(text: string): number {
|
|
148
|
+
if (text.length <= 32) {
|
|
149
|
+
if (EXPONENT_NUMBER.test(text)) {
|
|
150
|
+
const n = Number(text);
|
|
151
|
+
return Number.isInteger(n) ? n : Number.NaN;
|
|
152
|
+
}
|
|
153
|
+
if (GROUPED_DIGITS.test(text)) {
|
|
154
|
+
return Number.parseInt(text.replace(DIGIT_SEPARATORS, ""), 10);
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
return Number.parseInt(text, 10);
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
/**
|
|
161
|
+
* Parse an `--autocompact` value exactly as Claude Code's option parser does.
|
|
162
|
+
*
|
|
163
|
+
* `auto` (any case) stays `"auto"`. A trailing `k` or `m` multiplies by a
|
|
164
|
+
* thousand or a million; an unsuffixed number from 100 to 1000 is read as
|
|
165
|
+
* thousands (`200` is 200,000 tokens). The result must fall between
|
|
166
|
+
* {@link AUTOCOMPACT_MIN_TOKENS} and {@link AUTOCOMPACT_MAX_TOKENS}.
|
|
167
|
+
*
|
|
168
|
+
* Returns the token count, `"auto"`, or `undefined` when Claude Code would
|
|
169
|
+
* refuse the value.
|
|
170
|
+
*/
|
|
171
|
+
export function parseAutocompactSize(
|
|
172
|
+
value: string,
|
|
173
|
+
): number | "auto" | undefined {
|
|
174
|
+
if (value.trim().toLowerCase() === "auto") return "auto";
|
|
175
|
+
const text = value.trim();
|
|
176
|
+
const suffix = text.slice(-1).toLowerCase();
|
|
177
|
+
const multiplier =
|
|
178
|
+
suffix === "m" ? 1_000_000 : suffix === "k" ? 1_000 : undefined;
|
|
179
|
+
const parsed =
|
|
180
|
+
multiplier === undefined
|
|
181
|
+
? parsePlainNumber(text)
|
|
182
|
+
: Number.parseFloat(text) * multiplier;
|
|
183
|
+
const tokens =
|
|
184
|
+
multiplier === undefined && parsed >= 100 && parsed <= 1000
|
|
185
|
+
? parsed * 1000
|
|
186
|
+
: parsed;
|
|
187
|
+
if (Number.isNaN(tokens) || tokens < 1) return undefined;
|
|
188
|
+
if (tokens < AUTOCOMPACT_MIN_TOKENS || tokens > AUTOCOMPACT_MAX_TOKENS) {
|
|
189
|
+
return undefined;
|
|
190
|
+
}
|
|
191
|
+
return Math.round(tokens);
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
/** `validateValue` for the context window size: Claude Code's reason, or null. */
|
|
195
|
+
export function validateAutocompactSize(value: string): string | null {
|
|
196
|
+
return parseAutocompactSize(value) === undefined
|
|
197
|
+
? AUTOCOMPACT_INVALID_REASON
|
|
198
|
+
: null;
|
|
199
|
+
}
|
|
200
|
+
|
|
115
201
|
export const ALIAS_FLAGS: AliasFlag[] = [
|
|
116
202
|
{
|
|
117
203
|
id: "dangerously-skip-permissions",
|
|
@@ -222,6 +308,16 @@ export const ALIAS_FLAGS: AliasFlag[] = [
|
|
|
222
308
|
description:
|
|
223
309
|
"Path to a file whose contents are appended to the default system prompt.",
|
|
224
310
|
},
|
|
311
|
+
{
|
|
312
|
+
id: "autocompact",
|
|
313
|
+
label: "Context window size",
|
|
314
|
+
flag: "--autocompact",
|
|
315
|
+
kind: "text",
|
|
316
|
+
group: "context",
|
|
317
|
+
validateValue: validateAutocompactSize,
|
|
318
|
+
description:
|
|
319
|
+
"The window size Claude Code compacts the conversation at: auto, or 100k to 1M tokens (e.g. 200k, 500k, 1m, 200000, or 200 as shorthand for 200k).",
|
|
320
|
+
},
|
|
225
321
|
{
|
|
226
322
|
id: "channels",
|
|
227
323
|
label: "Channels (approved)",
|
|
@@ -291,6 +387,32 @@ export const ALIAS_FLAGS: AliasFlag[] = [
|
|
|
291
387
|
{ label: "classic", value: "classic" },
|
|
292
388
|
],
|
|
293
389
|
},
|
|
390
|
+
{
|
|
391
|
+
id: "agent-teams",
|
|
392
|
+
label: "Agent teams",
|
|
393
|
+
flag: "CLAUDE_CODE_EXPERIMENTAL_AGENT_TEAMS=1",
|
|
394
|
+
env: "CLAUDE_CODE_EXPERIMENTAL_AGENT_TEAMS=1",
|
|
395
|
+
kind: "boolean",
|
|
396
|
+
group: "agents",
|
|
397
|
+
description:
|
|
398
|
+
"Turn on agent teams, so the session can start teammates. Claude Code has no flag for this: the alias sets CLAUDE_CODE_EXPERIMENTAL_AGENT_TEAMS=1 before claude.",
|
|
399
|
+
},
|
|
400
|
+
{
|
|
401
|
+
id: "teammate-mode",
|
|
402
|
+
label: "Teammate panes",
|
|
403
|
+
flag: "--teammate-mode",
|
|
404
|
+
kind: "select",
|
|
405
|
+
group: "agents",
|
|
406
|
+
requires: "agent-teams",
|
|
407
|
+
description:
|
|
408
|
+
"Where teammates run. tmux opens each teammate in a split pane, but only when the session starts inside tmux. iterm2 uses iTerm2 panes, in-process keeps them in this terminal, auto picks for you. Requires agent teams.",
|
|
409
|
+
options: [
|
|
410
|
+
{ label: "tmux", value: "tmux" },
|
|
411
|
+
{ label: "iterm2", value: "iterm2" },
|
|
412
|
+
{ label: "in-process", value: "in-process" },
|
|
413
|
+
{ label: "auto", value: "auto" },
|
|
414
|
+
],
|
|
415
|
+
},
|
|
294
416
|
];
|
|
295
417
|
|
|
296
418
|
/** Look up a flag by id. Throws if missing — keeps callers honest. */
|
|
@@ -125,6 +125,8 @@ export function renderArgs(config: AliasConfig): Segment[] {
|
|
|
125
125
|
const seenXor = new Set<string>();
|
|
126
126
|
|
|
127
127
|
for (const flag of ALIAS_FLAGS) {
|
|
128
|
+
// Env flags are assignments before `claude`, never argv: see renderEnv.
|
|
129
|
+
if (flag.env) continue;
|
|
128
130
|
if (flag.requires) {
|
|
129
131
|
const dep = config.flags[flag.requires];
|
|
130
132
|
if (!isFlagEnabled(dep)) continue;
|
|
@@ -154,18 +156,35 @@ export function renderArgs(config: AliasConfig): Segment[] {
|
|
|
154
156
|
}
|
|
155
157
|
|
|
156
158
|
/**
|
|
157
|
-
*
|
|
158
|
-
*
|
|
159
|
-
*
|
|
160
|
-
|
|
159
|
+
* The environment assignments (`NAME=VALUE`) the alias sets before `claude`,
|
|
160
|
+
* one per enabled env flag, in catalog order. `requires` applies here as it
|
|
161
|
+
* does to argv flags. Pure function.
|
|
162
|
+
*/
|
|
163
|
+
export function renderEnv(config: AliasConfig): string[] {
|
|
164
|
+
const out: string[] = [];
|
|
165
|
+
for (const flag of ALIAS_FLAGS) {
|
|
166
|
+
if (!flag.env) continue;
|
|
167
|
+
if (flag.requires && !isFlagEnabled(config.flags[flag.requires])) continue;
|
|
168
|
+
if (isFlagEnabled(config.flags[flag.id])) out.push(flag.env);
|
|
169
|
+
}
|
|
170
|
+
return out;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
/**
|
|
174
|
+
* Convenience: the whole command after the alias name as literal tokens, env
|
|
175
|
+
* assignments first, then argv. Composite segments are stringified by joining
|
|
176
|
+
* their literal parts with `<sub>` placeholders for raw parts. Used by the
|
|
177
|
+
* adoption fixpoint check, tests and validation summaries, where the actual
|
|
178
|
+
* shell substitution doesn't matter.
|
|
161
179
|
*/
|
|
162
180
|
export function renderArgsAsTokens(config: AliasConfig): string[] {
|
|
163
|
-
|
|
181
|
+
const argv = renderArgs(config).map((seg) => {
|
|
164
182
|
if (seg.kind === "literal") return seg.text;
|
|
165
183
|
return seg.parts
|
|
166
184
|
.map((p) => (p.kind === "literal" ? p.text : `<${p.posix}>`))
|
|
167
185
|
.join("");
|
|
168
186
|
});
|
|
187
|
+
return [...renderEnv(config), ...argv];
|
|
169
188
|
}
|
|
170
189
|
|
|
171
190
|
function isFlagEnabled(value: FlagValue | undefined): boolean {
|
|
@@ -345,24 +364,32 @@ export function renderAlias(
|
|
|
345
364
|
shell: ShellKind,
|
|
346
365
|
): RenderedAlias {
|
|
347
366
|
const args = renderArgs(config);
|
|
367
|
+
const env = renderEnv(config);
|
|
348
368
|
const name = config.aliasName;
|
|
349
369
|
if (shell === "fish") {
|
|
350
|
-
return { shell, block: renderFishBlock(name, args) };
|
|
370
|
+
return { shell, block: renderFishBlock(name, env, args) };
|
|
351
371
|
}
|
|
352
|
-
return { shell, block: renderPosixBlock(name, args) };
|
|
372
|
+
return { shell, block: renderPosixBlock(name, env, args) };
|
|
353
373
|
}
|
|
354
374
|
|
|
355
|
-
function renderPosixBlock(
|
|
375
|
+
function renderPosixBlock(
|
|
376
|
+
name: string,
|
|
377
|
+
env: string[],
|
|
378
|
+
segments: Segment[],
|
|
379
|
+
): string {
|
|
356
380
|
// The alias body lives inside outer single quotes:
|
|
357
|
-
// alias <name>='claude … '
|
|
358
|
-
//
|
|
381
|
+
// alias <name>='NAME=VALUE claude … '
|
|
382
|
+
// Env assignments lead, so the shell exports them to `claude` alone. Each
|
|
383
|
+
// segment becomes one shell-arg inside that body. Composite segments
|
|
359
384
|
// (templated values) interleave literal text with double-quoted shell
|
|
360
385
|
// substitutions via close-outer + double-quote + reopen-outer.
|
|
386
|
+
const prefix =
|
|
387
|
+
env.length === 0 ? "" : `${env.map(quoteInsidePosixAlias).join(" ")} `;
|
|
361
388
|
const body =
|
|
362
389
|
segments.length === 0
|
|
363
390
|
? ""
|
|
364
391
|
: ` ${segments.map(renderPosixSegment).join(" ")}`;
|
|
365
|
-
const line = `alias ${name}='claude${body}'`;
|
|
392
|
+
const line = `alias ${name}='${prefix}claude${body}'`;
|
|
366
393
|
return `${BLOCK_BEGIN}\n${line}\n${BLOCK_END}\n`;
|
|
367
394
|
}
|
|
368
395
|
|
|
@@ -425,13 +452,22 @@ export function findUnquotableTokens(segments: Segment[]): string[] {
|
|
|
425
452
|
return out;
|
|
426
453
|
}
|
|
427
454
|
|
|
428
|
-
function renderFishBlock(
|
|
455
|
+
function renderFishBlock(
|
|
456
|
+
name: string,
|
|
457
|
+
env: string[],
|
|
458
|
+
segments: Segment[],
|
|
459
|
+
): string {
|
|
429
460
|
// Fish: `alias <name> 'claude --foo bar'` — outer single quotes, no `=`.
|
|
461
|
+
// Env assignments go through `env NAME=VALUE claude …`: the bare
|
|
462
|
+
// `NAME=VALUE cmd` form needs fish 3.1, `env` works on every version. The
|
|
463
|
+
// assignments are catalog constants of `[A-Za-z0-9_=]`, so they need no
|
|
464
|
+
// quoting inside the outer single quotes.
|
|
465
|
+
const prefix = env.length === 0 ? "" : `env ${env.join(" ")} `;
|
|
430
466
|
const body =
|
|
431
467
|
segments.length === 0
|
|
432
468
|
? ""
|
|
433
469
|
: ` ${segments.map(renderFishSegment).join(" ")}`;
|
|
434
|
-
const line = `alias ${name} 'claude${body}'`;
|
|
470
|
+
const line = `alias ${name} '${prefix}claude${body}'`;
|
|
435
471
|
return `${BLOCK_BEGIN}\n${line}\n${BLOCK_END}\n`;
|
|
436
472
|
}
|
|
437
473
|
|
|
@@ -604,9 +640,33 @@ export function validateConfig(config: AliasConfig): FlagValidationIssue[] {
|
|
|
604
640
|
|
|
605
641
|
export interface ParseResult {
|
|
606
642
|
aliasName: string | null;
|
|
643
|
+
/**
|
|
644
|
+
* The env assignments before `claude` (`NAME=VALUE`), then the argv after
|
|
645
|
+
* it — the shape {@link argsToFlagValuesWithLeftovers} reads.
|
|
646
|
+
*/
|
|
607
647
|
args: string[];
|
|
608
648
|
}
|
|
609
649
|
|
|
650
|
+
/** A shell environment assignment token: `NAME=VALUE`. */
|
|
651
|
+
const ENV_ASSIGNMENT_RE = /^[A-Za-z_][A-Za-z0-9_]*=/;
|
|
652
|
+
|
|
653
|
+
/**
|
|
654
|
+
* Split an alias command into the tokens {@link argsToFlagValuesWithLeftovers}
|
|
655
|
+
* reads: leading env assignments, then the argv after `claude`. Accepts the
|
|
656
|
+
* POSIX `NAME=VALUE claude …` form and the `env NAME=VALUE claude …` form the
|
|
657
|
+
* fish renderer writes. Returns null when the command is not `claude`.
|
|
658
|
+
*/
|
|
659
|
+
function commandTokensAfterEnv(tokens: string[]): string[] | null {
|
|
660
|
+
let i = tokens[0] === "env" ? 1 : 0;
|
|
661
|
+
const env: string[] = [];
|
|
662
|
+
while (i < tokens.length && ENV_ASSIGNMENT_RE.test(tokens[i])) {
|
|
663
|
+
env.push(tokens[i]);
|
|
664
|
+
i += 1;
|
|
665
|
+
}
|
|
666
|
+
if (tokens[i] !== "claude") return null;
|
|
667
|
+
return [...env, ...tokens.slice(i + 1)];
|
|
668
|
+
}
|
|
669
|
+
|
|
610
670
|
/**
|
|
611
671
|
* Find the managed block in shell rc text, parse the alias line, and return
|
|
612
672
|
* the alias name plus the argv tokens after `claude`.
|
|
@@ -644,10 +704,11 @@ export function parseAliasLine(line: string): ParseResult | null {
|
|
|
644
704
|
const match = ALIAS_LINE_RE.exec(line);
|
|
645
705
|
if (!match) return null;
|
|
646
706
|
const [, aliasName, body] = match;
|
|
647
|
-
//
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
707
|
+
// The command is `claude`, optionally preceded by env assignments. Drop
|
|
708
|
+
// `claude` itself; keep the assignments ahead of the argv.
|
|
709
|
+
const args = commandTokensAfterEnv(tokenizePosixAliasBody(body));
|
|
710
|
+
if (!args) return null;
|
|
711
|
+
return { aliasName, args };
|
|
651
712
|
}
|
|
652
713
|
|
|
653
714
|
/**
|
|
@@ -793,13 +854,25 @@ export interface ParsedFlagsWithLeftovers {
|
|
|
793
854
|
* catalog flag. Unknown `--flag value` pairs contribute BOTH tokens to
|
|
794
855
|
* `leftovers` (the flag and its consumed value) so the adoption gate sees the
|
|
795
856
|
* full extent of what wouldn't round-trip.
|
|
857
|
+
*
|
|
858
|
+
* Leading `NAME=VALUE` tokens are the env assignments set before `claude`.
|
|
859
|
+
* Each matches an env flag by its assignment string; an assignment the
|
|
860
|
+
* catalog does not know lands in `leftovers` on its own, so adoption never
|
|
861
|
+
* removes a line it cannot reproduce. Env flags are matched only in that
|
|
862
|
+
* leading run, never among the argv after it.
|
|
796
863
|
*/
|
|
797
864
|
export function argsToFlagValuesWithLeftovers(
|
|
798
865
|
args: string[],
|
|
799
866
|
): ParsedFlagsWithLeftovers {
|
|
800
|
-
// Index
|
|
867
|
+
// Index argv flags by their primary `flag` string AND their triStateOff
|
|
868
|
+
// variant, and env flags by their assignment string.
|
|
801
869
|
const byFlag = new Map<string, { flag: AliasFlag; off?: boolean }>();
|
|
870
|
+
const byEnv = new Map<string, AliasFlag>();
|
|
802
871
|
for (const flag of ALIAS_FLAGS) {
|
|
872
|
+
if (flag.env) {
|
|
873
|
+
byEnv.set(flag.env, flag);
|
|
874
|
+
continue;
|
|
875
|
+
}
|
|
803
876
|
byFlag.set(flag.flag, { flag, off: false });
|
|
804
877
|
if (flag.triStateOff) {
|
|
805
878
|
byFlag.set(flag.triStateOff, { flag, off: true });
|
|
@@ -809,6 +882,12 @@ export function argsToFlagValuesWithLeftovers(
|
|
|
809
882
|
const out: Record<string, FlagValue> = {};
|
|
810
883
|
const leftovers: string[] = [];
|
|
811
884
|
let i = 0;
|
|
885
|
+
while (i < args.length && ENV_ASSIGNMENT_RE.test(args[i])) {
|
|
886
|
+
const envFlag = byEnv.get(args[i]);
|
|
887
|
+
if (envFlag) out[envFlag.id] = { kind: "boolean", enabled: true };
|
|
888
|
+
else leftovers.push(args[i]);
|
|
889
|
+
i += 1;
|
|
890
|
+
}
|
|
812
891
|
while (i < args.length) {
|
|
813
892
|
const tok = args[i];
|
|
814
893
|
const entry = byFlag.get(tok);
|
|
@@ -1122,9 +1201,9 @@ function parseAdoptableLine(
|
|
|
1122
1201
|
if (!match) return null;
|
|
1123
1202
|
const [, name, , body] = match;
|
|
1124
1203
|
const tokens = tokenizeGeneralAliasBody(body);
|
|
1125
|
-
if (!tokens
|
|
1126
|
-
|
|
1127
|
-
|
|
1204
|
+
if (!tokens) return null;
|
|
1205
|
+
const args = commandTokensAfterEnv(tokens);
|
|
1206
|
+
if (!args) return null;
|
|
1128
1207
|
const { flags, leftovers } = argsToFlagValuesWithLeftovers(args);
|
|
1129
1208
|
|
|
1130
1209
|
// Fixpoint: render the parsed flags back to tokens and re-parse. If the
|
|
@@ -7,6 +7,7 @@ import {
|
|
|
7
7
|
ALIAS_FLAGS,
|
|
8
8
|
type AliasFlag,
|
|
9
9
|
FLAG_GROUPS,
|
|
10
|
+
getFlagById,
|
|
10
11
|
} from "../../data/alias-flags.js";
|
|
11
12
|
import { loadAliasName, saveAliasName } from "../../services/alias-settings.js";
|
|
12
13
|
import {
|
|
@@ -18,6 +19,7 @@ import {
|
|
|
18
19
|
findAdoptableAlias,
|
|
19
20
|
parseAliasFromRc,
|
|
20
21
|
renderArgs,
|
|
22
|
+
renderEnv,
|
|
21
23
|
validateConfig,
|
|
22
24
|
writeAliasToShell,
|
|
23
25
|
} from "../../services/alias-shell-writer.js";
|
|
@@ -521,9 +523,10 @@ export function AliasScreen() {
|
|
|
521
523
|
selectedRow.flag,
|
|
522
524
|
selectedRow.value,
|
|
523
525
|
modal,
|
|
526
|
+
showFailures,
|
|
524
527
|
);
|
|
525
528
|
if (next) updateFlag(selectedRow.flag.id, next);
|
|
526
|
-
}, [selectedRow, modal, updateFlag, editAliasName]);
|
|
529
|
+
}, [selectedRow, modal, showFailures, updateFlag, editAliasName]);
|
|
527
530
|
|
|
528
531
|
const toggleSelected = useCallback(async () => {
|
|
529
532
|
if (!selectedRow) return;
|
|
@@ -680,6 +683,7 @@ export function AliasScreen() {
|
|
|
680
683
|
|
|
681
684
|
// ── Render ────────────────────────────────────────────────────────────
|
|
682
685
|
const previewSegments = config ? renderArgs(config) : [];
|
|
686
|
+
const previewEnv = config ? renderEnv(config) : [];
|
|
683
687
|
const previewAliasName = config?.aliasName ?? "";
|
|
684
688
|
|
|
685
689
|
// Native OpenTUI scrollboxes own the scroll math. We render every item
|
|
@@ -767,6 +771,7 @@ export function AliasScreen() {
|
|
|
767
771
|
selectedRow,
|
|
768
772
|
isGated,
|
|
769
773
|
previewAliasName,
|
|
774
|
+
previewEnv,
|
|
770
775
|
previewSegments,
|
|
771
776
|
);
|
|
772
777
|
// When editing --channels and there are derived rows, the right pane carries
|
|
@@ -795,6 +800,7 @@ export function AliasScreen() {
|
|
|
795
800
|
detailFlag,
|
|
796
801
|
detailValue,
|
|
797
802
|
previewAliasName,
|
|
803
|
+
previewEnv,
|
|
798
804
|
previewSegments,
|
|
799
805
|
derivedNote,
|
|
800
806
|
stateOverride,
|
|
@@ -1081,6 +1087,7 @@ async function editFlagValue(
|
|
|
1081
1087
|
flag: AliasFlag,
|
|
1082
1088
|
value: FlagValue,
|
|
1083
1089
|
modal: ReturnType<typeof useModal>,
|
|
1090
|
+
showFailures: ReturnType<typeof useFailurePopup>,
|
|
1084
1091
|
): Promise<FlagValue | null> {
|
|
1085
1092
|
switch (value.kind) {
|
|
1086
1093
|
case "boolean":
|
|
@@ -1154,6 +1161,17 @@ async function editFlagValue(
|
|
|
1154
1161
|
}
|
|
1155
1162
|
const v = await modal.input(flag.label, flag.description, value.value);
|
|
1156
1163
|
if (v === null) return null;
|
|
1164
|
+
// An empty value disables the flag, so only a non-empty one is checked.
|
|
1165
|
+
// A refused value leaves the old one in place.
|
|
1166
|
+
const reason = v.length > 0 ? (flag.validateValue?.(v) ?? null) : null;
|
|
1167
|
+
if (reason) {
|
|
1168
|
+
await showFailures(
|
|
1169
|
+
failureReport(`The ${flag.label.toLowerCase()} was not changed`, [
|
|
1170
|
+
{ subject: v, reason },
|
|
1171
|
+
]),
|
|
1172
|
+
);
|
|
1173
|
+
return null;
|
|
1174
|
+
}
|
|
1157
1175
|
return { kind: "text", enabled: v.length > 0, value: v };
|
|
1158
1176
|
}
|
|
1159
1177
|
|
|
@@ -1336,6 +1354,7 @@ function renderDetail(
|
|
|
1336
1354
|
row: Row | undefined,
|
|
1337
1355
|
isGated: (f: AliasFlag) => boolean,
|
|
1338
1356
|
aliasName: string,
|
|
1357
|
+
env: string[],
|
|
1339
1358
|
segments: Segment[],
|
|
1340
1359
|
): React.ReactNode {
|
|
1341
1360
|
if (!row) {
|
|
@@ -1357,7 +1376,7 @@ function renderDetail(
|
|
|
1357
1376
|
{headerAndBody}
|
|
1358
1377
|
<text fg={theme.colors.text}> </text>
|
|
1359
1378
|
<text fg={theme.colors.muted}>Preview:</text>
|
|
1360
|
-
{renderPreviewLines(aliasName, segments)}
|
|
1379
|
+
{renderPreviewLines(aliasName, env, segments)}
|
|
1361
1380
|
</box>
|
|
1362
1381
|
);
|
|
1363
1382
|
}
|
|
@@ -1407,7 +1426,7 @@ function renderFlagDetail(
|
|
|
1407
1426
|
</text>
|
|
1408
1427
|
{gated && (
|
|
1409
1428
|
<text fg={theme.colors.muted}>
|
|
1410
|
-
{`Depends on
|
|
1429
|
+
{`Depends on ${getFlagById(row.flag.requires ?? "").flag}. Enabling this will turn that on too.`}
|
|
1411
1430
|
</text>
|
|
1412
1431
|
)}
|
|
1413
1432
|
{row.issue && (
|
|
@@ -1438,18 +1457,23 @@ function renderFlagDetail(
|
|
|
1438
1457
|
*/
|
|
1439
1458
|
function renderPreviewLines(
|
|
1440
1459
|
aliasName: string,
|
|
1460
|
+
env: string[],
|
|
1441
1461
|
segments: Segment[],
|
|
1442
1462
|
): React.ReactNode {
|
|
1463
|
+
// Env assignments run before `claude`, so they sit on the head line.
|
|
1464
|
+
const command = [...env, "claude"].join(" ");
|
|
1443
1465
|
if (segments.length === 0) {
|
|
1444
1466
|
return (
|
|
1445
|
-
<text
|
|
1467
|
+
<text
|
|
1468
|
+
fg={theme.colors.text}
|
|
1469
|
+
>{`alias ${aliasName || "?"}='${command}'`}</text>
|
|
1446
1470
|
);
|
|
1447
1471
|
}
|
|
1448
1472
|
const lines: React.ReactNode[] = [
|
|
1449
1473
|
<text
|
|
1450
1474
|
key="head"
|
|
1451
1475
|
fg={theme.colors.text}
|
|
1452
|
-
>{`alias ${aliasName || "?"}='
|
|
1476
|
+
>{`alias ${aliasName || "?"}='${command}`}</text>,
|
|
1453
1477
|
];
|
|
1454
1478
|
segments.forEach((seg, i) => {
|
|
1455
1479
|
lines.push(
|
|
@@ -1530,6 +1554,7 @@ function renderFlagDetailRightPane(
|
|
|
1530
1554
|
flag: AliasFlag,
|
|
1531
1555
|
value: FlagValue,
|
|
1532
1556
|
aliasName: string,
|
|
1557
|
+
env: string[],
|
|
1533
1558
|
segments: Segment[],
|
|
1534
1559
|
derivedNote?: string,
|
|
1535
1560
|
stateOverride?: string,
|
|
@@ -1555,7 +1580,7 @@ function renderFlagDetailRightPane(
|
|
|
1555
1580
|
</text>
|
|
1556
1581
|
<text fg={theme.colors.text}> </text>
|
|
1557
1582
|
<text fg={theme.colors.muted}>Preview:</text>
|
|
1558
|
-
{renderPreviewLines(aliasName, segments)}
|
|
1583
|
+
{renderPreviewLines(aliasName, env, segments)}
|
|
1559
1584
|
<text fg={theme.colors.text}> </text>
|
|
1560
1585
|
<text fg={theme.colors.muted}>Keys:</text>
|
|
1561
1586
|
<text fg={theme.colors.text}>
|