magus-cli 7.12.0 → 7.12.1
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
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "magus-cli",
|
|
3
|
-
"version": "7.12.
|
|
3
|
+
"version": "7.12.1",
|
|
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.12.
|
|
69
|
-
"magus-cli-darwin-x64": "7.12.
|
|
70
|
-
"magus-cli-linux-x64": "7.12.
|
|
68
|
+
"magus-cli-darwin-arm64": "7.12.1",
|
|
69
|
+
"magus-cli-darwin-x64": "7.12.1",
|
|
70
|
+
"magus-cli-linux-x64": "7.12.1"
|
|
71
71
|
}
|
|
72
72
|
}
|
|
@@ -8,7 +8,7 @@ import {
|
|
|
8
8
|
renderArgsAsTokens,
|
|
9
9
|
renderEnv,
|
|
10
10
|
} from "../services/alias-shell-writer";
|
|
11
|
-
import type
|
|
11
|
+
import { type AliasConfig, defaultValueFor } from "../services/alias-store";
|
|
12
12
|
|
|
13
13
|
const assignment = "CLAUDE_CODE_EXPERIMENTAL_AGENT_TEAMS=1";
|
|
14
14
|
const shells: ShellKind[] = ["bash", "zsh", "fish"];
|
|
@@ -31,23 +31,20 @@ function expectAliasLine(
|
|
|
31
31
|
if (shell === "fish") {
|
|
32
32
|
const quotedArgs =
|
|
33
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]);
|
|
34
|
+
expect(lines).toEqual([`alias ${aliasName} '${prefix}${quotedArgs}'`]);
|
|
38
35
|
} else {
|
|
39
36
|
expect(lines).toEqual([`alias ${aliasName}='${prefix}${bareArgs}'`]);
|
|
40
37
|
}
|
|
41
38
|
}
|
|
42
39
|
|
|
43
40
|
describe("agent teams catalog", () => {
|
|
44
|
-
it("TEST-7 REQ-1 REQ-4 exposes
|
|
41
|
+
it("TEST-7 REQ-1 REQ-4 exposes one env-backed select and the Agent teams group", () => {
|
|
45
42
|
expect(getFlagById("agent-teams")).toMatchObject({
|
|
46
43
|
id: "agent-teams",
|
|
47
44
|
label: "Agent teams",
|
|
48
|
-
kind: "
|
|
45
|
+
kind: "select",
|
|
49
46
|
env: assignment,
|
|
50
|
-
flag:
|
|
47
|
+
flag: "--teammate-mode",
|
|
51
48
|
group: "agents",
|
|
52
49
|
});
|
|
53
50
|
expect(FLAG_GROUPS.filter((group) => group.id === "agents")).toEqual([
|
|
@@ -56,26 +53,39 @@ describe("agent teams catalog", () => {
|
|
|
56
53
|
expect(
|
|
57
54
|
ALIAS_FLAGS.filter((flag) => flag.id === "agent-teams"),
|
|
58
55
|
).toHaveLength(1);
|
|
56
|
+
expect(
|
|
57
|
+
ALIAS_FLAGS.filter(
|
|
58
|
+
(flag) => flag.kind === "boolean" && flag.env !== undefined,
|
|
59
|
+
),
|
|
60
|
+
).toEqual([]);
|
|
59
61
|
});
|
|
60
62
|
|
|
61
|
-
it("TEST-8 REQ-4 exposes
|
|
62
|
-
const flag = getFlagById("
|
|
63
|
+
it("TEST-8 REQ-4 exposes the ordered teammate choices without a dependency", () => {
|
|
64
|
+
const flag = getFlagById("agent-teams");
|
|
63
65
|
|
|
64
|
-
expect(flag).
|
|
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",
|
|
66
|
+
expect(flag.options?.map((option) => option.value)).toEqual([
|
|
76
67
|
"tmux",
|
|
68
|
+
"iterm2",
|
|
69
|
+
"in-process",
|
|
70
|
+
"auto",
|
|
77
71
|
]);
|
|
78
|
-
expect(flag.
|
|
72
|
+
expect(flag.requires).toBeUndefined();
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
it("TEST-22 REQ-1 REQ-4 has no separate teammate-mode catalog entry", () => {
|
|
76
|
+
expect(ALIAS_FLAGS.filter((flag) => flag.id === "teammate-mode")).toEqual(
|
|
77
|
+
[],
|
|
78
|
+
);
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
it("TEST-23 REQ-1 REQ-4 defaults agent teams to a disabled tmux select", () => {
|
|
82
|
+
const flag = getFlagById("agent-teams");
|
|
83
|
+
|
|
84
|
+
expect(defaultValueFor(flag)).toEqual({
|
|
85
|
+
kind: "select",
|
|
86
|
+
enabled: false,
|
|
87
|
+
value: "tmux",
|
|
88
|
+
});
|
|
79
89
|
});
|
|
80
90
|
});
|
|
81
91
|
|
|
@@ -85,15 +95,23 @@ describe("environment and argv separation", () => {
|
|
|
85
95
|
const config: AliasConfig = { aliasName: "c", flags: {} };
|
|
86
96
|
if (state !== "missing") {
|
|
87
97
|
config.flags["agent-teams"] = {
|
|
88
|
-
kind: "
|
|
98
|
+
kind: "select",
|
|
89
99
|
enabled: state === "enabled",
|
|
100
|
+
value: "tmux",
|
|
90
101
|
};
|
|
91
102
|
}
|
|
92
103
|
const expectedEnv = state === "enabled" ? [assignment] : [];
|
|
104
|
+
const expectedArgv =
|
|
105
|
+
state === "enabled" ? ["--teammate-mode", "tmux"] : [];
|
|
93
106
|
|
|
94
107
|
expect(renderEnv(config)).toEqual(expectedEnv);
|
|
95
|
-
expect(renderArgs(config)).toEqual(
|
|
96
|
-
|
|
108
|
+
expect(renderArgs(config)).toEqual(
|
|
109
|
+
expectedArgv.map((text) => ({ kind: "literal", text })),
|
|
110
|
+
);
|
|
111
|
+
expect(renderArgsAsTokens(config)).toEqual([
|
|
112
|
+
...expectedEnv,
|
|
113
|
+
...expectedArgv,
|
|
114
|
+
]);
|
|
97
115
|
});
|
|
98
116
|
}
|
|
99
117
|
});
|
|
@@ -101,20 +119,24 @@ describe("environment and argv separation", () => {
|
|
|
101
119
|
describe("agent teams shell output and POSIX round-trips", () => {
|
|
102
120
|
// Fish rendering is covered; the parse-back contract uses POSIX alias lines.
|
|
103
121
|
for (const shell of shells) {
|
|
104
|
-
it(`TEST-10 REQ-8 REQ-13 renders agent teams
|
|
122
|
+
it(`TEST-10 REQ-8 REQ-13 renders automatic agent teams with env before claude on ${shell}`, () => {
|
|
105
123
|
const config: AliasConfig = {
|
|
106
124
|
aliasName: "c",
|
|
107
|
-
flags: {
|
|
125
|
+
flags: {
|
|
126
|
+
"agent-teams": { kind: "select", enabled: true, value: "auto" },
|
|
127
|
+
},
|
|
108
128
|
};
|
|
109
129
|
|
|
110
130
|
const rendered = renderAlias(config, shell);
|
|
111
131
|
|
|
112
132
|
expect(rendered.shell).toBe(shell);
|
|
113
|
-
expectAliasLine(rendered.block, shell, true, []);
|
|
133
|
+
expectAliasLine(rendered.block, shell, true, ["--teammate-mode", "auto"]);
|
|
114
134
|
if (shell !== "fish") {
|
|
115
135
|
expect(parseAliasFromRc(rendered.block)).toEqual({
|
|
116
136
|
aliasName: "c",
|
|
117
|
-
flags: {
|
|
137
|
+
flags: {
|
|
138
|
+
"agent-teams": { kind: "select", enabled: true, value: "auto" },
|
|
139
|
+
},
|
|
118
140
|
});
|
|
119
141
|
}
|
|
120
142
|
});
|
|
@@ -124,8 +146,7 @@ describe("agent teams shell output and POSIX round-trips", () => {
|
|
|
124
146
|
const config: AliasConfig = {
|
|
125
147
|
aliasName: "c",
|
|
126
148
|
flags: {
|
|
127
|
-
"agent-teams": { kind: "
|
|
128
|
-
"teammate-mode": { kind: "select", enabled: true, value: mode },
|
|
149
|
+
"agent-teams": { kind: "select", enabled: true, value: mode },
|
|
129
150
|
},
|
|
130
151
|
};
|
|
131
152
|
|
|
@@ -146,8 +167,7 @@ describe("agent teams shell output and POSIX round-trips", () => {
|
|
|
146
167
|
expect(parseAliasFromRc(rendered.block)).toEqual({
|
|
147
168
|
aliasName: "c",
|
|
148
169
|
flags: {
|
|
149
|
-
"agent-teams": { kind: "
|
|
150
|
-
"teammate-mode": { kind: "select", enabled: true, value: mode },
|
|
170
|
+
"agent-teams": { kind: "select", enabled: true, value: mode },
|
|
151
171
|
},
|
|
152
172
|
});
|
|
153
173
|
}
|
|
@@ -155,21 +175,26 @@ describe("agent teams shell output and POSIX round-trips", () => {
|
|
|
155
175
|
}
|
|
156
176
|
|
|
157
177
|
for (const state of ["missing", "disabled"] as const) {
|
|
158
|
-
it(`TEST-12 REQ-14
|
|
178
|
+
it(`TEST-12 REQ-14 omits both team outputs with ${state} agent teams but keeps context size on ${shell}`, () => {
|
|
159
179
|
const config: AliasConfig = {
|
|
160
180
|
aliasName: "c",
|
|
161
181
|
flags: {
|
|
162
|
-
"teammate-mode": { kind: "select", enabled: true, value: "tmux" },
|
|
163
182
|
autocompact: { kind: "text", enabled: true, value: "200k" },
|
|
164
183
|
},
|
|
165
184
|
};
|
|
185
|
+
const withoutTeams = structuredClone(config);
|
|
166
186
|
if (state === "disabled") {
|
|
167
|
-
config.flags["agent-teams"] = {
|
|
187
|
+
config.flags["agent-teams"] = {
|
|
188
|
+
kind: "select",
|
|
189
|
+
enabled: false,
|
|
190
|
+
value: "tmux",
|
|
191
|
+
};
|
|
168
192
|
}
|
|
169
193
|
const original = structuredClone(config);
|
|
170
194
|
|
|
171
195
|
const rendered = renderAlias(config, shell);
|
|
172
196
|
|
|
197
|
+
expect(rendered).toEqual(renderAlias(withoutTeams, shell));
|
|
173
198
|
expect(renderEnv(config)).toEqual([]);
|
|
174
199
|
expect(renderArgs(config)).toEqual([
|
|
175
200
|
{ kind: "literal", text: "--autocompact" },
|
|
@@ -192,49 +217,47 @@ describe("agent teams shell output and POSIX round-trips", () => {
|
|
|
192
217
|
});
|
|
193
218
|
}
|
|
194
219
|
|
|
195
|
-
it(`TEST-13 REQ-4 REQ-7 REQ-8 REQ-13
|
|
220
|
+
it(`TEST-13 REQ-4 REQ-7 REQ-8 REQ-13 omits both outputs for disabled agent teams on ${shell}`, () => {
|
|
196
221
|
const config: AliasConfig = {
|
|
197
222
|
aliasName: "c",
|
|
198
223
|
flags: {
|
|
199
|
-
"agent-teams": { kind: "
|
|
200
|
-
"teammate-mode": { kind: "select", enabled: false, value: "tmux" },
|
|
224
|
+
"agent-teams": { kind: "select", enabled: false, value: "tmux" },
|
|
201
225
|
},
|
|
202
226
|
};
|
|
227
|
+
const withoutTeams: AliasConfig = { aliasName: "c", flags: {} };
|
|
203
228
|
|
|
204
229
|
const rendered = renderAlias(config, shell);
|
|
205
230
|
|
|
231
|
+
expect(rendered).toEqual(renderAlias(withoutTeams, shell));
|
|
232
|
+
expect(renderEnv(config)).toEqual([]);
|
|
206
233
|
expect(renderArgs(config)).toEqual([]);
|
|
207
|
-
expect(renderArgsAsTokens(config)).toEqual([
|
|
208
|
-
expectAliasLine(rendered.block, shell,
|
|
234
|
+
expect(renderArgsAsTokens(config)).toEqual([]);
|
|
235
|
+
expectAliasLine(rendered.block, shell, false, []);
|
|
209
236
|
if (shell !== "fish") {
|
|
210
237
|
expect(parseAliasFromRc(rendered.block)).toEqual({
|
|
211
238
|
aliasName: "c",
|
|
212
|
-
flags: {
|
|
239
|
+
flags: {},
|
|
213
240
|
});
|
|
214
241
|
}
|
|
215
242
|
});
|
|
216
243
|
|
|
217
|
-
it(`TEST-14 REQ-6 REQ-7 REQ-8 REQ-9 REQ-13 renders
|
|
244
|
+
it(`TEST-14 REQ-6 REQ-7 REQ-8 REQ-9 REQ-13 renders agent teams after context size with env first on ${shell}`, () => {
|
|
218
245
|
const config: AliasConfig = {
|
|
219
246
|
aliasName: "crew",
|
|
220
247
|
flags: {
|
|
221
|
-
"
|
|
248
|
+
"agent-teams": { kind: "select", enabled: true, value: "tmux" },
|
|
222
249
|
autocompact: { kind: "text", enabled: true, value: "200k" },
|
|
223
|
-
"agent-teams": { kind: "boolean", enabled: true },
|
|
224
250
|
},
|
|
225
251
|
};
|
|
226
252
|
const contextIndex = ALIAS_FLAGS.findIndex(
|
|
227
253
|
(flag) => flag.id === "autocompact",
|
|
228
254
|
);
|
|
229
|
-
const
|
|
230
|
-
(flag) => flag.id === "
|
|
255
|
+
const teamsIndex = ALIAS_FLAGS.findIndex(
|
|
256
|
+
(flag) => flag.id === "agent-teams",
|
|
231
257
|
);
|
|
232
258
|
expect(contextIndex).toBeGreaterThanOrEqual(0);
|
|
233
|
-
expect(
|
|
234
|
-
const argv =
|
|
235
|
-
contextIndex < modeIndex
|
|
236
|
-
? ["--autocompact", "200k", "--teammate-mode", "tmux"]
|
|
237
|
-
: ["--teammate-mode", "tmux", "--autocompact", "200k"];
|
|
259
|
+
expect(teamsIndex).toBeGreaterThan(contextIndex);
|
|
260
|
+
const argv = ["--autocompact", "200k", "--teammate-mode", "tmux"];
|
|
238
261
|
|
|
239
262
|
const rendered = renderAlias(config, shell);
|
|
240
263
|
|
|
@@ -248,8 +271,7 @@ describe("agent teams shell output and POSIX round-trips", () => {
|
|
|
248
271
|
expect(parseAliasFromRc(rendered.block)).toEqual({
|
|
249
272
|
aliasName: "crew",
|
|
250
273
|
flags: {
|
|
251
|
-
"agent-teams": { kind: "
|
|
252
|
-
"teammate-mode": { kind: "select", enabled: true, value: "tmux" },
|
|
274
|
+
"agent-teams": { kind: "select", enabled: true, value: "tmux" },
|
|
253
275
|
autocompact: { kind: "text", enabled: true, value: "200k" },
|
|
254
276
|
},
|
|
255
277
|
});
|
|
@@ -6,15 +6,16 @@ import {
|
|
|
6
6
|
parseAliasLine,
|
|
7
7
|
parseManagedBlock,
|
|
8
8
|
renderAlias,
|
|
9
|
+
renderArgsAsTokens,
|
|
9
10
|
} from "../services/alias-shell-writer";
|
|
10
11
|
import type { AliasConfig, FlagValue } from "../services/alias-store";
|
|
11
12
|
|
|
12
13
|
const assignment = "CLAUDE_CODE_EXPERIMENTAL_AGENT_TEAMS=1";
|
|
14
|
+
const modes = ["tmux", "iterm2", "in-process", "auto"];
|
|
13
15
|
|
|
14
16
|
function combinedFlags(): Record<string, FlagValue> {
|
|
15
17
|
return {
|
|
16
|
-
"agent-teams": { kind: "
|
|
17
|
-
"teammate-mode": { kind: "select", enabled: true, value: "tmux" },
|
|
18
|
+
"agent-teams": { kind: "select", enabled: true, value: "tmux" },
|
|
18
19
|
autocompact: { kind: "text", enabled: true, value: "200k" },
|
|
19
20
|
};
|
|
20
21
|
}
|
|
@@ -46,6 +47,80 @@ function commandBody(
|
|
|
46
47
|
].join(" ");
|
|
47
48
|
}
|
|
48
49
|
|
|
50
|
+
describe("single agent-team parse rows", () => {
|
|
51
|
+
type ParseRow = {
|
|
52
|
+
name: string;
|
|
53
|
+
env: string[];
|
|
54
|
+
argv: string[];
|
|
55
|
+
flags: () => Record<string, FlagValue>;
|
|
56
|
+
leftovers: string[];
|
|
57
|
+
};
|
|
58
|
+
const rows: ParseRow[] = [
|
|
59
|
+
...modes.map(
|
|
60
|
+
(mode): ParseRow => ({
|
|
61
|
+
name: `assignment and ${mode} argv become one select`,
|
|
62
|
+
env: [assignment],
|
|
63
|
+
argv: ["--teammate-mode", mode],
|
|
64
|
+
flags: () => ({
|
|
65
|
+
"agent-teams": { kind: "select", enabled: true, value: mode },
|
|
66
|
+
}),
|
|
67
|
+
leftovers: [],
|
|
68
|
+
}),
|
|
69
|
+
),
|
|
70
|
+
{
|
|
71
|
+
name: "assignment alone defaults to auto",
|
|
72
|
+
env: [assignment],
|
|
73
|
+
argv: [],
|
|
74
|
+
flags: () => ({
|
|
75
|
+
"agent-teams": { kind: "select", enabled: true, value: "auto" },
|
|
76
|
+
}),
|
|
77
|
+
leftovers: [],
|
|
78
|
+
},
|
|
79
|
+
...modes.map(
|
|
80
|
+
(mode): ParseRow => ({
|
|
81
|
+
name: `${mode} argv alone remains leftovers`,
|
|
82
|
+
env: [],
|
|
83
|
+
argv: ["--teammate-mode", mode],
|
|
84
|
+
flags: () => ({}),
|
|
85
|
+
leftovers: ["--teammate-mode", mode],
|
|
86
|
+
}),
|
|
87
|
+
),
|
|
88
|
+
];
|
|
89
|
+
|
|
90
|
+
for (const row of rows) {
|
|
91
|
+
it(`TEST-24 REQ-12 REQ-14 maps tokens so ${row.name}`, () => {
|
|
92
|
+
const args = [...row.env, ...row.argv];
|
|
93
|
+
|
|
94
|
+
expect(argsToFlagValuesWithLeftovers(args)).toEqual({
|
|
95
|
+
flags: row.flags(),
|
|
96
|
+
leftovers: [...row.leftovers],
|
|
97
|
+
});
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
for (const form of forms) {
|
|
101
|
+
it(`TEST-25 REQ-11 REQ-12 REQ-14 parses ${form.name} so ${row.name}`, () => {
|
|
102
|
+
const line = form.line(
|
|
103
|
+
commandBody(form.envLauncher, row.env, row.argv),
|
|
104
|
+
);
|
|
105
|
+
|
|
106
|
+
const parsed = parseAliasLine(line);
|
|
107
|
+
|
|
108
|
+
expect(parsed).toEqual({
|
|
109
|
+
aliasName: "crew",
|
|
110
|
+
args: [...row.env, ...row.argv],
|
|
111
|
+
});
|
|
112
|
+
if (parsed === null) {
|
|
113
|
+
throw new Error("Expected a parsed claude alias");
|
|
114
|
+
}
|
|
115
|
+
expect(argsToFlagValuesWithLeftovers(parsed.args)).toEqual({
|
|
116
|
+
flags: row.flags(),
|
|
117
|
+
leftovers: [...row.leftovers],
|
|
118
|
+
});
|
|
119
|
+
});
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
});
|
|
123
|
+
|
|
49
124
|
describe("managed assignment-prefixed alias parsing", () => {
|
|
50
125
|
const examples = [
|
|
51
126
|
{
|
|
@@ -126,6 +201,21 @@ describe("assignment mapping and leftovers", () => {
|
|
|
126
201
|
flags: combinedFlags,
|
|
127
202
|
leftovers: ["EMPTY=", "EXTRA=a=b"],
|
|
128
203
|
},
|
|
204
|
+
{
|
|
205
|
+
name: "unknown assignments on both sides of the known one",
|
|
206
|
+
args: [
|
|
207
|
+
"FIRST=one",
|
|
208
|
+
assignment,
|
|
209
|
+
"EMPTY=",
|
|
210
|
+
"EXTRA=a=b",
|
|
211
|
+
"--teammate-mode",
|
|
212
|
+
"tmux",
|
|
213
|
+
"--autocompact",
|
|
214
|
+
"200k",
|
|
215
|
+
],
|
|
216
|
+
flags: combinedFlags,
|
|
217
|
+
leftovers: ["FIRST=one", "EMPTY=", "EXTRA=a=b"],
|
|
218
|
+
},
|
|
129
219
|
{
|
|
130
220
|
name: "known env name with a different value",
|
|
131
221
|
args: ["CLAUDE_CODE_EXPERIMENTAL_AGENT_TEAMS=0", "--ide"],
|
|
@@ -159,7 +249,7 @@ describe("assignment mapping and leftovers", () => {
|
|
|
159
249
|
|
|
160
250
|
expect(argsToFlagValuesWithLeftovers(args)).toEqual({
|
|
161
251
|
flags: {
|
|
162
|
-
"agent-teams": { kind: "
|
|
252
|
+
"agent-teams": { kind: "select", enabled: true, value: "auto" },
|
|
163
253
|
ide: { kind: "boolean", enabled: true },
|
|
164
254
|
},
|
|
165
255
|
leftovers: ["EXTRA=keep"],
|
|
@@ -183,12 +273,14 @@ describe("assignment mapping and leftovers", () => {
|
|
|
183
273
|
});
|
|
184
274
|
|
|
185
275
|
describe("safe adoption of assignment-prefixed aliases", () => {
|
|
186
|
-
|
|
276
|
+
type LosslessCase = {
|
|
187
277
|
name: string;
|
|
188
278
|
env: string[];
|
|
189
279
|
argv: string[];
|
|
190
280
|
flags: () => Record<string, FlagValue>;
|
|
191
|
-
|
|
281
|
+
renderedArgs: string[];
|
|
282
|
+
};
|
|
283
|
+
const losslessCases: LosslessCase[] = [
|
|
192
284
|
{
|
|
193
285
|
name: "context size only",
|
|
194
286
|
env: [],
|
|
@@ -196,21 +288,41 @@ describe("safe adoption of assignment-prefixed aliases", () => {
|
|
|
196
288
|
flags: () => ({
|
|
197
289
|
autocompact: { kind: "text", enabled: true, value: "200k" },
|
|
198
290
|
}),
|
|
291
|
+
renderedArgs: ["--autocompact", "200k"],
|
|
199
292
|
},
|
|
200
293
|
{
|
|
201
|
-
name: "agent teams
|
|
294
|
+
name: "agent teams assignment alone as auto",
|
|
202
295
|
env: [assignment],
|
|
203
296
|
argv: [],
|
|
204
297
|
flags: () => ({
|
|
205
|
-
"agent-teams": { kind: "
|
|
298
|
+
"agent-teams": { kind: "select", enabled: true, value: "auto" },
|
|
206
299
|
}),
|
|
300
|
+
renderedArgs: [assignment, "--teammate-mode", "auto"],
|
|
207
301
|
},
|
|
208
302
|
{
|
|
209
303
|
name: "all new options",
|
|
210
304
|
env: [assignment],
|
|
211
305
|
argv: ["--teammate-mode", "tmux", "--autocompact", "200k"],
|
|
212
306
|
flags: combinedFlags,
|
|
307
|
+
renderedArgs: [
|
|
308
|
+
assignment,
|
|
309
|
+
"--autocompact",
|
|
310
|
+
"200k",
|
|
311
|
+
"--teammate-mode",
|
|
312
|
+
"tmux",
|
|
313
|
+
],
|
|
213
314
|
},
|
|
315
|
+
...modes.map(
|
|
316
|
+
(mode): LosslessCase => ({
|
|
317
|
+
name: `agent teams with explicit ${mode} mode`,
|
|
318
|
+
env: [assignment],
|
|
319
|
+
argv: ["--teammate-mode", mode],
|
|
320
|
+
flags: () => ({
|
|
321
|
+
"agent-teams": { kind: "select", enabled: true, value: mode },
|
|
322
|
+
}),
|
|
323
|
+
renderedArgs: [assignment, "--teammate-mode", mode],
|
|
324
|
+
}),
|
|
325
|
+
),
|
|
214
326
|
];
|
|
215
327
|
|
|
216
328
|
const unknownCases: Array<{
|
|
@@ -241,6 +353,13 @@ describe("safe adoption of assignment-prefixed aliases", () => {
|
|
|
241
353
|
flags: combinedFlags,
|
|
242
354
|
leftovers: ["EXTRA=a=b"],
|
|
243
355
|
},
|
|
356
|
+
{
|
|
357
|
+
name: "unknown assignments on both sides of known teams in their original order",
|
|
358
|
+
env: ["FIRST=one", assignment, "EMPTY=", "EXTRA=a=b"],
|
|
359
|
+
argv: ["--teammate-mode", "tmux", "--autocompact", "200k"],
|
|
360
|
+
flags: combinedFlags,
|
|
361
|
+
leftovers: ["FIRST=one", "EMPTY=", "EXTRA=a=b"],
|
|
362
|
+
},
|
|
244
363
|
{
|
|
245
364
|
name: "a known assignment name with an unsupported value",
|
|
246
365
|
env: ["CLAUDE_CODE_EXPERIMENTAL_AGENT_TEAMS=0"],
|
|
@@ -277,6 +396,12 @@ describe("safe adoption of assignment-prefixed aliases", () => {
|
|
|
277
396
|
aliasName: adopted.name,
|
|
278
397
|
flags: adopted.flags,
|
|
279
398
|
};
|
|
399
|
+
const renderedArgs = renderArgsAsTokens(config);
|
|
400
|
+
expect(renderedArgs).toEqual(example.renderedArgs);
|
|
401
|
+
expect(argsToFlagValuesWithLeftovers(renderedArgs)).toEqual({
|
|
402
|
+
flags: expectedFlags,
|
|
403
|
+
leftovers: [],
|
|
404
|
+
});
|
|
280
405
|
expect(parseAliasFromRc(renderAlias(config, "bash").block)).toEqual({
|
|
281
406
|
aliasName: "crew",
|
|
282
407
|
flags: expectedFlags,
|
|
@@ -310,13 +435,29 @@ describe("safe adoption of assignment-prefixed aliases", () => {
|
|
|
310
435
|
|
|
311
436
|
const adopted = findAdoptableAlias(`${line}\n`);
|
|
312
437
|
|
|
438
|
+
expect(adopted).toEqual({
|
|
439
|
+
name: "crew",
|
|
440
|
+
flags: {},
|
|
441
|
+
lineRange: [0, 0],
|
|
442
|
+
leftovers: ["--teammate-mode", "tmux"],
|
|
443
|
+
lossless: false,
|
|
444
|
+
rawLine: line,
|
|
445
|
+
others: [],
|
|
446
|
+
});
|
|
447
|
+
});
|
|
448
|
+
|
|
449
|
+
it(`TEST-26 REQ-12 REQ-13 REQ-14 keeps context size but refuses lossless argv-only teams in ${form.name}`, () => {
|
|
450
|
+
const line = form.line("claude --teammate-mode tmux --autocompact 200k");
|
|
451
|
+
|
|
452
|
+
const adopted = findAdoptableAlias(`${line}\n`);
|
|
453
|
+
|
|
313
454
|
expect(adopted).toEqual({
|
|
314
455
|
name: "crew",
|
|
315
456
|
flags: {
|
|
316
|
-
|
|
457
|
+
autocompact: { kind: "text", enabled: true, value: "200k" },
|
|
317
458
|
},
|
|
318
459
|
lineRange: [0, 0],
|
|
319
|
-
leftovers: [],
|
|
460
|
+
leftovers: ["--teammate-mode", "tmux"],
|
|
320
461
|
lossless: false,
|
|
321
462
|
rawLine: line,
|
|
322
463
|
others: [],
|
package/src/data/alias-flags.ts
CHANGED
|
@@ -73,10 +73,11 @@ export interface AliasFlag {
|
|
|
73
73
|
*/
|
|
74
74
|
templated?: boolean;
|
|
75
75
|
/**
|
|
76
|
-
* Environment assignment (`NAME=VALUE`) the alias sets before `claude
|
|
77
|
-
*
|
|
78
|
-
*
|
|
79
|
-
*
|
|
76
|
+
* Environment assignment (`NAME=VALUE`) the alias sets before `claude`
|
|
77
|
+
* whenever the flag is enabled. For `kind: "boolean"` the assignment is the
|
|
78
|
+
* whole flag, and `flag` carries the same string so lookups by `flag` keep
|
|
79
|
+
* working. For `kind: "select"` the flag also emits `flag <value>` as argv:
|
|
80
|
+
* the assignment and the argv are one setting that only works as a pair.
|
|
80
81
|
*/
|
|
81
82
|
env?: string;
|
|
82
83
|
/**
|
|
@@ -390,22 +391,12 @@ export const ALIAS_FLAGS: AliasFlag[] = [
|
|
|
390
391
|
{
|
|
391
392
|
id: "agent-teams",
|
|
392
393
|
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
394
|
flag: "--teammate-mode",
|
|
395
|
+
env: "CLAUDE_CODE_EXPERIMENTAL_AGENT_TEAMS=1",
|
|
404
396
|
kind: "select",
|
|
405
397
|
group: "agents",
|
|
406
|
-
requires: "agent-teams",
|
|
407
398
|
description:
|
|
408
|
-
"
|
|
399
|
+
"Turn on agent teams and pick where teammates run. The alias sets CLAUDE_CODE_EXPERIMENTAL_AGENT_TEAMS=1 and passes --teammate-mode, because one does nothing without the other. 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.",
|
|
409
400
|
options: [
|
|
410
401
|
{ label: "tmux", value: "tmux" },
|
|
411
402
|
{ label: "iterm2", value: "iterm2" },
|
|
@@ -125,8 +125,9 @@ export function renderArgs(config: AliasConfig): Segment[] {
|
|
|
125
125
|
const seenXor = new Set<string>();
|
|
126
126
|
|
|
127
127
|
for (const flag of ALIAS_FLAGS) {
|
|
128
|
-
//
|
|
129
|
-
|
|
128
|
+
// A boolean env flag is only an assignment before `claude`: see renderEnv.
|
|
129
|
+
// A select env flag adds its argv here as well.
|
|
130
|
+
if (isEnvOnly(flag)) continue;
|
|
130
131
|
if (flag.requires) {
|
|
131
132
|
const dep = config.flags[flag.requires];
|
|
132
133
|
if (!isFlagEnabled(dep)) continue;
|
|
@@ -187,6 +188,11 @@ export function renderArgsAsTokens(config: AliasConfig): string[] {
|
|
|
187
188
|
return [...renderEnv(config), ...argv];
|
|
188
189
|
}
|
|
189
190
|
|
|
191
|
+
/** A boolean env flag emits its assignment and no argv. */
|
|
192
|
+
function isEnvOnly(flag: AliasFlag): boolean {
|
|
193
|
+
return flag.env !== undefined && flag.kind === "boolean";
|
|
194
|
+
}
|
|
195
|
+
|
|
190
196
|
function isFlagEnabled(value: FlagValue | undefined): boolean {
|
|
191
197
|
if (!value) return false;
|
|
192
198
|
switch (value.kind) {
|
|
@@ -860,6 +866,11 @@ export interface ParsedFlagsWithLeftovers {
|
|
|
860
866
|
* catalog does not know lands in `leftovers` on its own, so adoption never
|
|
861
867
|
* removes a line it cannot reproduce. Env flags are matched only in that
|
|
862
868
|
* leading run, never among the argv after it.
|
|
869
|
+
*
|
|
870
|
+
* A select env flag is one setting written as an assignment plus
|
|
871
|
+
* `flag <value>`. Its assignment alone reads as the option Claude Code uses
|
|
872
|
+
* when no value is given (`auto`); its argv alone lands in `leftovers`,
|
|
873
|
+
* because the pair is the only form the writer reproduces.
|
|
863
874
|
*/
|
|
864
875
|
export function argsToFlagValuesWithLeftovers(
|
|
865
876
|
args: string[],
|
|
@@ -871,7 +882,7 @@ export function argsToFlagValuesWithLeftovers(
|
|
|
871
882
|
for (const flag of ALIAS_FLAGS) {
|
|
872
883
|
if (flag.env) {
|
|
873
884
|
byEnv.set(flag.env, flag);
|
|
874
|
-
continue;
|
|
885
|
+
if (isEnvOnly(flag)) continue;
|
|
875
886
|
}
|
|
876
887
|
byFlag.set(flag.flag, { flag, off: false });
|
|
877
888
|
if (flag.triStateOff) {
|
|
@@ -881,11 +892,15 @@ export function argsToFlagValuesWithLeftovers(
|
|
|
881
892
|
|
|
882
893
|
const out: Record<string, FlagValue> = {};
|
|
883
894
|
const leftovers: string[] = [];
|
|
895
|
+
const envSeen = new Set<string>();
|
|
884
896
|
let i = 0;
|
|
885
897
|
while (i < args.length && ENV_ASSIGNMENT_RE.test(args[i])) {
|
|
886
898
|
const envFlag = byEnv.get(args[i]);
|
|
887
|
-
if (envFlag)
|
|
899
|
+
if (envFlag) envSeen.add(envFlag.id);
|
|
888
900
|
else leftovers.push(args[i]);
|
|
901
|
+
if (envFlag && isEnvOnly(envFlag)) {
|
|
902
|
+
out[envFlag.id] = { kind: "boolean", enabled: true };
|
|
903
|
+
}
|
|
889
904
|
i += 1;
|
|
890
905
|
}
|
|
891
906
|
while (i < args.length) {
|
|
@@ -1041,6 +1056,20 @@ export function argsToFlagValuesWithLeftovers(
|
|
|
1041
1056
|
};
|
|
1042
1057
|
}
|
|
1043
1058
|
|
|
1059
|
+
// Pair each select env flag's assignment with its argv.
|
|
1060
|
+
for (const flag of ALIAS_FLAGS) {
|
|
1061
|
+
if (!flag.env || isEnvOnly(flag)) continue;
|
|
1062
|
+
const parsed = out[flag.id];
|
|
1063
|
+
const hasArgv = parsed !== undefined;
|
|
1064
|
+
if (envSeen.has(flag.id) && !hasArgv) {
|
|
1065
|
+
out[flag.id] = { kind: "select", enabled: true, value: "auto" };
|
|
1066
|
+
} else if (!envSeen.has(flag.id) && hasArgv) {
|
|
1067
|
+
delete out[flag.id];
|
|
1068
|
+
const value = parsed.kind === "select" ? parsed.value : "";
|
|
1069
|
+
leftovers.push(flag.flag, ...(value ? [value] : []));
|
|
1070
|
+
}
|
|
1071
|
+
}
|
|
1072
|
+
|
|
1044
1073
|
return { flags: out, leftovers };
|
|
1045
1074
|
}
|
|
1046
1075
|
|
|
@@ -61,6 +61,11 @@ export function InputModal({
|
|
|
61
61
|
if (typeof value === "string") onSubmit(value);
|
|
62
62
|
}}
|
|
63
63
|
focused
|
|
64
|
+
// OpenTUI paints input text white by default, which vanishes on a
|
|
65
|
+
// light terminal. The terminal's own foreground reads on both.
|
|
66
|
+
textColor={theme.colors.text}
|
|
67
|
+
focusedTextColor={theme.colors.text}
|
|
68
|
+
placeholderColor={theme.colors.muted}
|
|
64
69
|
width={50}
|
|
65
70
|
/>
|
|
66
71
|
</box>
|