@oh-my-pi/pi-coding-agent 15.13.0 → 15.13.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/CHANGELOG.md +1656 -613
- package/dist/cli.js +12765 -12731
- package/dist/types/autolearn/managed-skills.d.ts +1 -1
- package/dist/types/capability/mcp.d.ts +2 -1
- package/dist/types/cli/args.d.ts +2 -0
- package/dist/types/cli/flag-tables.d.ts +126 -0
- package/dist/types/cli/profile-alias.d.ts +29 -0
- package/dist/types/cli/profile-bootstrap.d.ts +55 -0
- package/dist/types/commands/launch.d.ts +6 -0
- package/dist/types/config/model-roles.d.ts +3 -2
- package/dist/types/config/settings-schema.d.ts +2 -0
- package/dist/types/edit/file-snapshot-store.d.ts +14 -0
- package/dist/types/extensibility/extensions/runner.d.ts +11 -0
- package/dist/types/mcp/manager.d.ts +5 -1
- package/dist/types/mcp/oauth-credentials.d.ts +17 -0
- package/dist/types/mcp/oauth-flow.d.ts +41 -0
- package/dist/types/mcp/types.d.ts +2 -0
- package/dist/types/modes/components/background-tan-message.d.ts +9 -0
- package/dist/types/modes/components/mcp-add-wizard.d.ts +9 -5
- package/dist/types/modes/interactive-mode.d.ts +4 -0
- package/dist/types/modes/types.d.ts +3 -0
- package/dist/types/sdk.d.ts +1 -1
- package/dist/types/session/messages.d.ts +8 -0
- package/dist/types/session/session-manager.d.ts +6 -0
- package/dist/types/tools/builtin-names.d.ts +2 -0
- package/dist/types/tools/index.d.ts +3 -2
- package/dist/types/utils/external-editor.d.ts +11 -1
- package/package.json +12 -12
- package/src/autolearn/managed-skills.ts +3 -5
- package/src/capability/mcp.ts +2 -1
- package/src/cli/args.ts +61 -103
- package/src/cli/completion-gen.ts +2 -2
- package/src/cli/flag-tables.ts +270 -0
- package/src/cli/profile-alias.ts +338 -0
- package/src/cli/profile-bootstrap.ts +243 -0
- package/src/cli.ts +83 -16
- package/src/commands/launch.ts +7 -0
- package/src/config/mcp-schema.json +4 -0
- package/src/config/model-roles.ts +17 -4
- package/src/config/settings-schema.ts +2 -0
- package/src/discovery/builtin.ts +15 -9
- package/src/discovery/helpers.ts +25 -0
- package/src/discovery/mcp-json.ts +1 -0
- package/src/discovery/omp-extension-roots.ts +2 -2
- package/src/edit/file-snapshot-store.ts +43 -0
- package/src/eval/__tests__/agent-bridge.test.ts +3 -2
- package/src/eval/__tests__/helpers-local-roots.test.ts +1 -1
- package/src/eval/js/shared/runtime.ts +54 -0
- package/src/extensibility/extensions/runner.ts +25 -2
- package/src/goals/runtime.ts +4 -1
- package/src/internal-urls/docs-index.generated.ts +6 -6
- package/src/mcp/manager.ts +108 -71
- package/src/mcp/oauth-credentials.ts +104 -0
- package/src/mcp/oauth-flow.ts +67 -0
- package/src/mcp/types.ts +2 -0
- package/src/modes/components/agent-hub.ts +6 -0
- package/src/modes/components/background-tan-message.ts +36 -0
- package/src/modes/components/mcp-add-wizard.ts +17 -10
- package/src/modes/components/model-selector.ts +50 -6
- package/src/modes/components/tool-execution.ts +12 -0
- package/src/modes/controllers/input-controller.ts +21 -10
- package/src/modes/controllers/mcp-command-controller.ts +184 -112
- package/src/modes/controllers/tan-command-controller.ts +27 -11
- package/src/modes/interactive-mode.ts +6 -0
- package/src/modes/types.ts +3 -0
- package/src/modes/utils/ui-helpers.ts +6 -0
- package/src/prompts/bench.md +9 -4
- package/src/sdk.ts +6 -5
- package/src/session/agent-session.ts +30 -1
- package/src/session/messages.ts +9 -0
- package/src/session/session-manager.ts +7 -2
- package/src/tiny/text.ts +5 -1
- package/src/tools/ast-grep.ts +5 -1
- package/src/tools/builtin-names.ts +35 -0
- package/src/tools/index.ts +3 -2
- package/src/tools/read.ts +9 -0
- package/src/tools/search.ts +5 -1
- package/src/tts/tts-worker.ts +13 -5
- package/src/utils/external-editor.ts +15 -2
- package/src/utils/title-generator.ts +1 -1
- package/src/workspace-tree.ts +46 -6
- package/dist/types/utils/tools-manager.test.d.ts +0 -1
- package/src/utils/tools-manager.test.ts +0 -25
|
@@ -0,0 +1,338 @@
|
|
|
1
|
+
import * as os from "node:os";
|
|
2
|
+
import * as path from "node:path";
|
|
3
|
+
import { normalizeProfileName } from "@oh-my-pi/pi-utils/dirs";
|
|
4
|
+
|
|
5
|
+
export type ProfileAliasShell = "bash" | "zsh" | "fish" | "powershell" | "pwsh";
|
|
6
|
+
|
|
7
|
+
function quoteForShell(pathValue: string): string {
|
|
8
|
+
return `'${pathValue.replace(/'/g, `'"'"'`)}'`;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
function quoteForPowerShell(pathValue: string): string {
|
|
12
|
+
return `'${pathValue.replace(/'/g, `''`)}'`;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export interface ProfileAliasCommand {
|
|
16
|
+
display: string;
|
|
17
|
+
posix: string;
|
|
18
|
+
fish: string;
|
|
19
|
+
powerShell: string;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
const DEFAULT_ALIAS_COMMAND: ProfileAliasCommand = {
|
|
23
|
+
display: "omp",
|
|
24
|
+
posix: "omp",
|
|
25
|
+
fish: "omp",
|
|
26
|
+
powerShell: "omp",
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
export interface ProfileAliasInstallOptions {
|
|
30
|
+
profile: string;
|
|
31
|
+
aliasName: string;
|
|
32
|
+
shellPath?: string;
|
|
33
|
+
platform?: NodeJS.Platform;
|
|
34
|
+
homeDir?: string;
|
|
35
|
+
env?: NodeJS.ProcessEnv;
|
|
36
|
+
readFile?: (filePath: string) => Promise<string>;
|
|
37
|
+
command?: ProfileAliasCommand;
|
|
38
|
+
writeFile?: (filePath: string, content: string) => Promise<void>;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export interface ProfileAliasInstallResult {
|
|
42
|
+
shell: ProfileAliasShell;
|
|
43
|
+
configPath: string;
|
|
44
|
+
aliasName: string;
|
|
45
|
+
profile: string;
|
|
46
|
+
command: string;
|
|
47
|
+
reloadedWith: string;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
const ALIAS_NAME_RE = /^[A-Za-z_][A-Za-z0-9_-]{0,63}$/;
|
|
51
|
+
const POSIX_RESERVED_ALIAS_NAMES: ReadonlySet<string> = new Set([
|
|
52
|
+
"case",
|
|
53
|
+
"coproc",
|
|
54
|
+
"do",
|
|
55
|
+
"done",
|
|
56
|
+
"elif",
|
|
57
|
+
"else",
|
|
58
|
+
"esac",
|
|
59
|
+
"fi",
|
|
60
|
+
"for",
|
|
61
|
+
"function",
|
|
62
|
+
"if",
|
|
63
|
+
"in",
|
|
64
|
+
"select",
|
|
65
|
+
"then",
|
|
66
|
+
"time",
|
|
67
|
+
"until",
|
|
68
|
+
"while",
|
|
69
|
+
]);
|
|
70
|
+
const FISH_RESERVED_ALIAS_NAMES: ReadonlySet<string> = new Set([
|
|
71
|
+
"and",
|
|
72
|
+
"begin",
|
|
73
|
+
"break",
|
|
74
|
+
"builtin",
|
|
75
|
+
"case",
|
|
76
|
+
"command",
|
|
77
|
+
"continue",
|
|
78
|
+
"else",
|
|
79
|
+
"end",
|
|
80
|
+
"exec",
|
|
81
|
+
"for",
|
|
82
|
+
"function",
|
|
83
|
+
"if",
|
|
84
|
+
"not",
|
|
85
|
+
"or",
|
|
86
|
+
"return",
|
|
87
|
+
"switch",
|
|
88
|
+
"while",
|
|
89
|
+
]);
|
|
90
|
+
const POWERSHELL_RESERVED_ALIAS_NAMES: ReadonlySet<string> = new Set([
|
|
91
|
+
"begin",
|
|
92
|
+
"break",
|
|
93
|
+
"catch",
|
|
94
|
+
"class",
|
|
95
|
+
"continue",
|
|
96
|
+
"data",
|
|
97
|
+
"do",
|
|
98
|
+
"dynamicparam",
|
|
99
|
+
"else",
|
|
100
|
+
"elseif",
|
|
101
|
+
"end",
|
|
102
|
+
"enum",
|
|
103
|
+
"exit",
|
|
104
|
+
"filter",
|
|
105
|
+
"finally",
|
|
106
|
+
"for",
|
|
107
|
+
"foreach",
|
|
108
|
+
"from",
|
|
109
|
+
"function",
|
|
110
|
+
"if",
|
|
111
|
+
"in",
|
|
112
|
+
"param",
|
|
113
|
+
"process",
|
|
114
|
+
"return",
|
|
115
|
+
"switch",
|
|
116
|
+
"throw",
|
|
117
|
+
"trap",
|
|
118
|
+
"try",
|
|
119
|
+
"until",
|
|
120
|
+
"using",
|
|
121
|
+
"var",
|
|
122
|
+
"while",
|
|
123
|
+
"workflow",
|
|
124
|
+
]);
|
|
125
|
+
|
|
126
|
+
// Keep local: importing the pi-utils root here would eagerly load env before
|
|
127
|
+
// cli.ts has applied --profile, regressing profile-specific .env loading.
|
|
128
|
+
function isEnoentError(error: unknown): boolean {
|
|
129
|
+
return typeof error === "object" && error !== null && (error as { code?: unknown }).code === "ENOENT";
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
function getReservedAliasNames(shell: ProfileAliasShell): ReadonlySet<string> {
|
|
133
|
+
switch (shell) {
|
|
134
|
+
case "bash":
|
|
135
|
+
case "zsh":
|
|
136
|
+
return POSIX_RESERVED_ALIAS_NAMES;
|
|
137
|
+
case "fish":
|
|
138
|
+
return FISH_RESERVED_ALIAS_NAMES;
|
|
139
|
+
case "powershell":
|
|
140
|
+
case "pwsh":
|
|
141
|
+
return POWERSHELL_RESERVED_ALIAS_NAMES;
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
function validateAliasName(aliasName: string, shell: ProfileAliasShell): string {
|
|
146
|
+
const normalized = aliasName.trim();
|
|
147
|
+
if (!ALIAS_NAME_RE.test(normalized)) {
|
|
148
|
+
throw new Error(`Invalid alias "${aliasName}". Alias names must match ${ALIAS_NAME_RE.source}.`);
|
|
149
|
+
}
|
|
150
|
+
if (normalized.toLowerCase() === "omp") {
|
|
151
|
+
throw new Error('Invalid alias "omp". Refusing to shadow the base omp command.');
|
|
152
|
+
}
|
|
153
|
+
if (getReservedAliasNames(shell).has(normalized.toLowerCase())) {
|
|
154
|
+
throw new Error(`Invalid alias "${aliasName}". Refusing to create a ${shell} reserved word.`);
|
|
155
|
+
}
|
|
156
|
+
return normalized;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
// On Windows the launching shell is rarely exported through $SHELL, so when it
|
|
160
|
+
// is missing we infer the PowerShell edition from the inherited environment.
|
|
161
|
+
// PowerShell 7 (pwsh) always seeds PSModulePath with separator-delimited
|
|
162
|
+
// ".../PowerShell/..." module directories (plus the Windows PowerShell ones for
|
|
163
|
+
// back-compat), whereas Windows PowerShell 5.1 only ever lists
|
|
164
|
+
// ".../WindowsPowerShell/...". The separator anchors keep "WindowsPowerShell"
|
|
165
|
+
// from matching. POWERSHELL_DISTRIBUTION_CHANNEL is set only by some pwsh
|
|
166
|
+
// distributions, so it stays a secondary hint rather than the primary signal.
|
|
167
|
+
function detectWindowsPowerShell(env: NodeJS.ProcessEnv): ProfileAliasShell {
|
|
168
|
+
const modulePath = env.PSModulePath ?? env.PSMODULEPATH ?? env.psmodulepath ?? "";
|
|
169
|
+
if (/[\\/]PowerShell[\\/]/i.test(modulePath)) return "pwsh";
|
|
170
|
+
if (env.POWERSHELL_DISTRIBUTION_CHANNEL) return "pwsh";
|
|
171
|
+
return "powershell";
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
function normalizeShellName(
|
|
175
|
+
shellPath: string | undefined,
|
|
176
|
+
platform: NodeJS.Platform,
|
|
177
|
+
env: NodeJS.ProcessEnv,
|
|
178
|
+
): ProfileAliasShell {
|
|
179
|
+
const shell = path
|
|
180
|
+
.basename(shellPath ?? "")
|
|
181
|
+
.toLowerCase()
|
|
182
|
+
.replace(/\.exe$/, "");
|
|
183
|
+
if (shell === "zsh") return "zsh";
|
|
184
|
+
if (shell === "bash") return "bash";
|
|
185
|
+
if (shell === "fish") return "fish";
|
|
186
|
+
if (shell === "pwsh") return "pwsh";
|
|
187
|
+
if (shell === "powershell") return "powershell";
|
|
188
|
+
if (platform === "win32") return detectWindowsPowerShell(env);
|
|
189
|
+
throw new Error(`Unsupported shell${shell ? ` "${shell}"` : ""}. Supported shells: bash, zsh, fish, PowerShell.`);
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
export function resolveProfileAliasCommandFromProcess(
|
|
193
|
+
argv: readonly string[] = process.argv,
|
|
194
|
+
cwd: string = process.cwd(),
|
|
195
|
+
): ProfileAliasCommand {
|
|
196
|
+
const runtime = argv[0];
|
|
197
|
+
const script = argv[1];
|
|
198
|
+
if (!runtime || !script || !/\.[cm]?[jt]s$/.test(script)) return DEFAULT_ALIAS_COMMAND;
|
|
199
|
+
|
|
200
|
+
const scriptPath = path.resolve(cwd, script);
|
|
201
|
+
const posix = `${quoteForShell(runtime)} ${quoteForShell(scriptPath)}`;
|
|
202
|
+
return {
|
|
203
|
+
display: `${runtime} ${scriptPath}`,
|
|
204
|
+
posix,
|
|
205
|
+
fish: posix,
|
|
206
|
+
powerShell: `${quoteForPowerShell(runtime)} ${quoteForPowerShell(scriptPath)}`,
|
|
207
|
+
};
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
function resolveShellConfigPath(
|
|
211
|
+
shell: ProfileAliasShell,
|
|
212
|
+
homeDir: string,
|
|
213
|
+
platform: NodeJS.Platform,
|
|
214
|
+
env: NodeJS.ProcessEnv,
|
|
215
|
+
): string {
|
|
216
|
+
switch (shell) {
|
|
217
|
+
case "zsh":
|
|
218
|
+
return path.join(env.ZDOTDIR || homeDir, ".zshrc");
|
|
219
|
+
case "bash":
|
|
220
|
+
return platform === "darwin" ? path.join(homeDir, ".bash_profile") : path.join(homeDir, ".bashrc");
|
|
221
|
+
case "fish": {
|
|
222
|
+
// fish sources conf.d from $XDG_CONFIG_HOME/fish (default ~/.config/fish);
|
|
223
|
+
// a hard-coded ~/.config would be silently ignored when the user relocates
|
|
224
|
+
// their XDG config root, leaving the alias unsourced after a restart.
|
|
225
|
+
const configHome = env.XDG_CONFIG_HOME || path.join(homeDir, ".config");
|
|
226
|
+
return path.join(configHome, "fish", "conf.d", "omp-profiles.fish");
|
|
227
|
+
}
|
|
228
|
+
case "pwsh":
|
|
229
|
+
return platform === "win32"
|
|
230
|
+
? path.join(homeDir, "Documents", "PowerShell", "Microsoft.PowerShell_profile.ps1")
|
|
231
|
+
: path.join(homeDir, ".config", "powershell", "Microsoft.PowerShell_profile.ps1");
|
|
232
|
+
case "powershell":
|
|
233
|
+
return path.join(homeDir, "Documents", "WindowsPowerShell", "Microsoft.PowerShell_profile.ps1");
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
function renderAliasBlock(
|
|
238
|
+
shell: ProfileAliasShell,
|
|
239
|
+
aliasName: string,
|
|
240
|
+
profile: string,
|
|
241
|
+
command: ProfileAliasCommand,
|
|
242
|
+
): { block: string; command: string } {
|
|
243
|
+
const profiledCommand = `${command.display} --profile=${profile}`;
|
|
244
|
+
const start = `# >>> omp profile alias: ${aliasName} >>>`;
|
|
245
|
+
const end = `# <<< omp profile alias: ${aliasName} <<<`;
|
|
246
|
+
let body: string;
|
|
247
|
+
switch (shell) {
|
|
248
|
+
case "fish":
|
|
249
|
+
body = [
|
|
250
|
+
`function ${aliasName} --wraps omp --description 'OMP profile ${profile}'`,
|
|
251
|
+
` command ${command.fish} --profile=${profile} $argv`,
|
|
252
|
+
"end",
|
|
253
|
+
].join("\n");
|
|
254
|
+
break;
|
|
255
|
+
case "powershell":
|
|
256
|
+
case "pwsh":
|
|
257
|
+
body = [`function ${aliasName} {`, ` & ${command.powerShell} --profile=${profile} @args`, "}"].join("\n");
|
|
258
|
+
break;
|
|
259
|
+
default:
|
|
260
|
+
body = [`${aliasName}() {`, ` command ${command.posix} --profile=${profile} "$@"`, "}"].join("\n");
|
|
261
|
+
break;
|
|
262
|
+
}
|
|
263
|
+
return { block: `${start}\n${body}\n${end}`, command: profiledCommand };
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
function upsertBlock(content: string, aliasName: string, block: string): string {
|
|
267
|
+
const start = `# >>> omp profile alias: ${aliasName} >>>`;
|
|
268
|
+
const end = `# <<< omp profile alias: ${aliasName} <<<`;
|
|
269
|
+
const startIndex = content.indexOf(start);
|
|
270
|
+
if (startIndex !== -1) {
|
|
271
|
+
const endIndex = content.indexOf(end, startIndex + start.length);
|
|
272
|
+
if (endIndex === -1) {
|
|
273
|
+
throw new Error(
|
|
274
|
+
`Found "${start}" without a matching "${end}" in the shell config. ` +
|
|
275
|
+
`The managed alias block is malformed; remove the stale marker line and rerun --alias.`,
|
|
276
|
+
);
|
|
277
|
+
}
|
|
278
|
+
const afterEnd = endIndex + end.length;
|
|
279
|
+
const prefix = content.slice(0, startIndex).replace(/[\t ]*\n?$/, "");
|
|
280
|
+
const suffix = content.slice(afterEnd).replace(/^\n?/, "");
|
|
281
|
+
return [prefix, block, suffix].filter(Boolean).join("\n\n").replace(/\n*$/, "\n");
|
|
282
|
+
}
|
|
283
|
+
const trimmed = content.replace(/\s*$/, "");
|
|
284
|
+
return `${trimmed}${trimmed ? "\n\n" : ""}${block}\n`;
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
function readAliasConfigText(filePath: string): Promise<string> {
|
|
288
|
+
return Bun.file(filePath).text();
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
export async function readProfileAliasConfigFile(
|
|
292
|
+
filePath: string,
|
|
293
|
+
readText: (filePath: string) => Promise<string> = readAliasConfigText,
|
|
294
|
+
): Promise<string> {
|
|
295
|
+
try {
|
|
296
|
+
return await readText(filePath);
|
|
297
|
+
} catch (error) {
|
|
298
|
+
if (isEnoentError(error)) return "";
|
|
299
|
+
throw error;
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
export async function installProfileAlias(options: ProfileAliasInstallOptions): Promise<ProfileAliasInstallResult> {
|
|
304
|
+
const profile = normalizeProfileName(options.profile);
|
|
305
|
+
if (!profile) {
|
|
306
|
+
throw new Error("--alias requires a named --profile value.");
|
|
307
|
+
}
|
|
308
|
+
const platform = options.platform ?? process.platform;
|
|
309
|
+
const homeDir = options.homeDir ?? os.homedir();
|
|
310
|
+
const env = options.env ?? process.env;
|
|
311
|
+
const shell = normalizeShellName(options.shellPath ?? env.SHELL, platform, env);
|
|
312
|
+
const aliasName = validateAliasName(options.aliasName, shell);
|
|
313
|
+
const configPath = resolveShellConfigPath(shell, homeDir, platform, env);
|
|
314
|
+
const { block, command } = renderAliasBlock(shell, aliasName, profile, options.command ?? DEFAULT_ALIAS_COMMAND);
|
|
315
|
+
const readFile = options.readFile ?? readProfileAliasConfigFile;
|
|
316
|
+
const writeFile =
|
|
317
|
+
options.writeFile ??
|
|
318
|
+
(async (filePath, content) => {
|
|
319
|
+
await Bun.write(filePath, content);
|
|
320
|
+
});
|
|
321
|
+
|
|
322
|
+
const current = await readFile(configPath);
|
|
323
|
+
await writeFile(configPath, upsertBlock(current, aliasName, block));
|
|
324
|
+
|
|
325
|
+
return {
|
|
326
|
+
shell,
|
|
327
|
+
configPath,
|
|
328
|
+
aliasName,
|
|
329
|
+
profile,
|
|
330
|
+
command,
|
|
331
|
+
reloadedWith:
|
|
332
|
+
shell === "fish"
|
|
333
|
+
? `source ${quoteForShell(configPath)}`
|
|
334
|
+
: shell === "powershell" || shell === "pwsh"
|
|
335
|
+
? `. ${quoteForPowerShell(configPath)}`
|
|
336
|
+
: `. ${quoteForShell(configPath)}`,
|
|
337
|
+
};
|
|
338
|
+
}
|
|
@@ -0,0 +1,243 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Bootstrap-time argv preparser for the global `--profile` / `--alias` flags.
|
|
3
|
+
*
|
|
4
|
+
* Profile selection MUST happen before any module reads `getAgentDir()` (notably
|
|
5
|
+
* `@oh-my-pi/pi-utils/env`, which eagerly loads `.env` from the agent directory
|
|
6
|
+
* during its own import). The full `parseArgs` from `./args.ts` lives downstream
|
|
7
|
+
* of those imports, so we can't rely on it for profile bootstrap — we have to
|
|
8
|
+
* crack open argv before the lazy command modules load.
|
|
9
|
+
*
|
|
10
|
+
* Because of that, this preparser must respect the same value-consumption
|
|
11
|
+
* contract as `args.ts`: known string-valued flags usually consume the next
|
|
12
|
+
* token even when it starts with `-`, except for string flags that can be
|
|
13
|
+
* shadowed by preloaded boolean extensions (currently `--plan`). Optional-value
|
|
14
|
+
* flags (`--resume`, `--session`, `-r`) consume the next token only when it
|
|
15
|
+
* doesn't look like another flag. Without this, `omp --system-prompt --profile
|
|
16
|
+
* foo` silently activates profile `foo`
|
|
17
|
+
* instead of passing the literal `--profile` to the system prompt and `foo`
|
|
18
|
+
* as a positional message.
|
|
19
|
+
*
|
|
20
|
+
* The shared classification lives in {@link ./flag-tables}, imported below,
|
|
21
|
+
* so the bootstrap and `args.ts` reference one source of truth instead of
|
|
22
|
+
* maintaining parallel constants.
|
|
23
|
+
*
|
|
24
|
+
* An unclassified bare long option (one not in any flag table) is treated as a
|
|
25
|
+
* possible extension string flag, but the bootstrap mirrors `parseArgs`'
|
|
26
|
+
* extension-flag rules ({@link ./args}): a string extension flag consumes its
|
|
27
|
+
* successor ONLY when that successor is value-like (does not start with `-`), and
|
|
28
|
+
* a boolean extension flag consumes nothing. So the successor is forwarded
|
|
29
|
+
* untouched (and never read as a global `--profile`/`--alias`) only when it is
|
|
30
|
+
* value-like; a flag-looking successor is left for normal processing, so
|
|
31
|
+
* `omp --some-ext-flag --profile work` still selects a profile. Known value-less
|
|
32
|
+
* launch flags ({@link VALUELESS_FLAGS}) are exempt so a trailing profile after
|
|
33
|
+
* them also activates (`omp --print --profile work`).
|
|
34
|
+
*/
|
|
35
|
+
|
|
36
|
+
import { isSubcommand } from "../cli-commands";
|
|
37
|
+
import {
|
|
38
|
+
EXTENSION_SHADOWABLE_STRING_FLAGS,
|
|
39
|
+
OPTIONAL_FLAGS,
|
|
40
|
+
OPTIONAL_VALUE_FLAGS,
|
|
41
|
+
PROFILE_BOOTSTRAP_BOUNDARY_ARG,
|
|
42
|
+
STRING_VALUE_FLAGS,
|
|
43
|
+
VALUELESS_FLAGS,
|
|
44
|
+
} from "./flag-tables";
|
|
45
|
+
|
|
46
|
+
function isProfileBootstrapSubcommand(arg: string): boolean {
|
|
47
|
+
return arg === "launch" || arg === "acp";
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function isUnknownLongValueCandidate(arg: string): boolean {
|
|
51
|
+
return (
|
|
52
|
+
arg.startsWith("--") &&
|
|
53
|
+
!arg.includes("=") &&
|
|
54
|
+
!STRING_VALUE_FLAGS.has(arg) &&
|
|
55
|
+
!OPTIONAL_VALUE_FLAGS.has(arg) &&
|
|
56
|
+
!VALUELESS_FLAGS.has(arg)
|
|
57
|
+
);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function needsBoundaryAfterGlobalStrip(stripped: readonly string[]): boolean {
|
|
61
|
+
const previous = stripped[stripped.length - 1];
|
|
62
|
+
return (
|
|
63
|
+
previous !== undefined &&
|
|
64
|
+
(OPTIONAL_VALUE_FLAGS.has(previous) ||
|
|
65
|
+
EXTENSION_SHADOWABLE_STRING_FLAGS.has(previous) ||
|
|
66
|
+
isUnknownLongValueCandidate(previous))
|
|
67
|
+
);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
export interface ProfileBootstrapResult {
|
|
71
|
+
argv: string[];
|
|
72
|
+
profile?: string;
|
|
73
|
+
aliasName?: string;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Strip `--profile` / `--alias` from argv while preserving the surrounding
|
|
78
|
+
* argument structure, returning the residual argv to hand to the launch parser
|
|
79
|
+
* and the captured flag values.
|
|
80
|
+
*
|
|
81
|
+
* Global flag extraction stops only when the first residual argv token names a
|
|
82
|
+
* registered command that owns its own flags (e.g. `grep`): everything from
|
|
83
|
+
* that token onward is forwarded verbatim so a subcommand's own flags and
|
|
84
|
+
* positionals are never stolen (`omp grep --profile <path>` greps for
|
|
85
|
+
* `--profile`; it does not select a profile). `launch` and `acp` are explicit
|
|
86
|
+
* spellings of launch-shaped commands, so `omp launch --profile work` and
|
|
87
|
+
* `omp acp --profile work` still select profile `work`.
|
|
88
|
+
*
|
|
89
|
+
* Throws when either flag is supplied without a value.
|
|
90
|
+
*/
|
|
91
|
+
export function extractProfileFlags(argv: readonly string[]): ProfileBootstrapResult {
|
|
92
|
+
const stripped: string[] = [];
|
|
93
|
+
let profile: string | undefined;
|
|
94
|
+
let aliasName: string | undefined;
|
|
95
|
+
let passThrough = false;
|
|
96
|
+
let sawSubcommand = false;
|
|
97
|
+
let canDispatchSubcommand = true;
|
|
98
|
+
let insertBoundaryBeforeNextValue = false;
|
|
99
|
+
for (let index = 0; index < argv.length; index += 1) {
|
|
100
|
+
const arg = argv[index];
|
|
101
|
+
|
|
102
|
+
if (passThrough || sawSubcommand) {
|
|
103
|
+
stripped.push(arg);
|
|
104
|
+
continue;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
if (insertBoundaryBeforeNextValue) {
|
|
108
|
+
if (!arg.startsWith("-")) {
|
|
109
|
+
stripped.push(PROFILE_BOOTSTRAP_BOUNDARY_ARG);
|
|
110
|
+
}
|
|
111
|
+
insertBoundaryBeforeNextValue = false;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
// `--` ends option processing. Anything that follows is forwarded verbatim
|
|
115
|
+
// so users can pass arbitrary tokens (including a literal `--profile`) to
|
|
116
|
+
// downstream tools without the bootstrap stealing them.
|
|
117
|
+
if (arg === "--") {
|
|
118
|
+
passThrough = true;
|
|
119
|
+
stripped.push(arg);
|
|
120
|
+
continue;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
if (arg === "--profile") {
|
|
124
|
+
const value = argv[index + 1];
|
|
125
|
+
if (!value || value.startsWith("-")) {
|
|
126
|
+
throw new Error("--profile requires a profile name");
|
|
127
|
+
}
|
|
128
|
+
profile = value;
|
|
129
|
+
insertBoundaryBeforeNextValue = needsBoundaryAfterGlobalStrip(stripped);
|
|
130
|
+
index += 1;
|
|
131
|
+
continue;
|
|
132
|
+
}
|
|
133
|
+
if (arg.startsWith("--profile=")) {
|
|
134
|
+
const value = arg.slice("--profile=".length);
|
|
135
|
+
if (!value) {
|
|
136
|
+
throw new Error("--profile requires a profile name");
|
|
137
|
+
}
|
|
138
|
+
profile = value;
|
|
139
|
+
insertBoundaryBeforeNextValue = needsBoundaryAfterGlobalStrip(stripped);
|
|
140
|
+
continue;
|
|
141
|
+
}
|
|
142
|
+
if (arg === "--alias") {
|
|
143
|
+
const value = argv[index + 1];
|
|
144
|
+
if (!value || value.startsWith("-")) {
|
|
145
|
+
throw new Error("--alias requires a command name");
|
|
146
|
+
}
|
|
147
|
+
aliasName = value;
|
|
148
|
+
insertBoundaryBeforeNextValue = needsBoundaryAfterGlobalStrip(stripped);
|
|
149
|
+
index += 1;
|
|
150
|
+
continue;
|
|
151
|
+
}
|
|
152
|
+
if (arg.startsWith("--alias=")) {
|
|
153
|
+
const value = arg.slice("--alias=".length);
|
|
154
|
+
if (!value) {
|
|
155
|
+
throw new Error("--alias requires a command name");
|
|
156
|
+
}
|
|
157
|
+
aliasName = value;
|
|
158
|
+
insertBoundaryBeforeNextValue = needsBoundaryAfterGlobalStrip(stripped);
|
|
159
|
+
continue;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
// Known string flags normally consume flag-looking values (for example
|
|
163
|
+
// `--system-prompt --profile foo` means the system prompt is literally
|
|
164
|
+
// `--profile`). A small allow-list of built-ins can be shadowed by boolean
|
|
165
|
+
// extensions before extension metadata is loaded; those mirror extension
|
|
166
|
+
// consumption here so `--plan --profile work` still activates `work`.
|
|
167
|
+
if (EXTENSION_SHADOWABLE_STRING_FLAGS.has(arg)) {
|
|
168
|
+
canDispatchSubcommand = false;
|
|
169
|
+
stripped.push(arg);
|
|
170
|
+
const next = argv[index + 1];
|
|
171
|
+
if (next !== undefined && !next.startsWith("-")) {
|
|
172
|
+
stripped.push(next);
|
|
173
|
+
index += 1;
|
|
174
|
+
}
|
|
175
|
+
continue;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
// Forward both the flag and its value untouched so the downstream parser
|
|
179
|
+
// gets exactly what the user typed. Critical for `--system-prompt
|
|
180
|
+
// --profile foo`: the bootstrap must NOT interpret `--profile` here, it
|
|
181
|
+
// belongs to `--system-prompt`.
|
|
182
|
+
if (STRING_VALUE_FLAGS.has(arg)) {
|
|
183
|
+
canDispatchSubcommand = false;
|
|
184
|
+
stripped.push(arg);
|
|
185
|
+
if (index + 1 < argv.length) {
|
|
186
|
+
stripped.push(argv[index + 1]);
|
|
187
|
+
index += 1;
|
|
188
|
+
}
|
|
189
|
+
continue;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
if (OPTIONAL_VALUE_FLAGS.has(arg)) {
|
|
193
|
+
canDispatchSubcommand = false;
|
|
194
|
+
stripped.push(arg);
|
|
195
|
+
const config = OPTIONAL_FLAGS[arg];
|
|
196
|
+
const next = argv[index + 1];
|
|
197
|
+
if (next !== undefined && !next.startsWith("-") && !(config.rejectEmpty === true && next.length === 0)) {
|
|
198
|
+
stripped.push(next);
|
|
199
|
+
index += 1;
|
|
200
|
+
}
|
|
201
|
+
continue;
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
// An unclassified bare long option (`--xxx` with no `=`) may be an extension
|
|
205
|
+
// string flag that consumes the next token as its value. The bootstrap runs
|
|
206
|
+
// before extensions load, so it cannot consult the extension flag table; it
|
|
207
|
+
// therefore mirrors the value-consumption rule `parseArgs` applies to
|
|
208
|
+
// extension flags (./args.ts): a string extension flag consumes its successor
|
|
209
|
+
// ONLY when that successor is value-like (does not start with `-`), and a
|
|
210
|
+
// boolean extension flag consumes nothing. So protect (forward + skip) the
|
|
211
|
+
// successor only when it is value-like — `omp --bar val --profile work` keeps
|
|
212
|
+
// `val` with `--bar` and still extracts the trailing profile — and otherwise
|
|
213
|
+
// forward just the flag, letting the loop process a flag-looking successor so
|
|
214
|
+
// a trailing global flag still applies (`omp --some-ext-bool --profile work`
|
|
215
|
+
// selects profile `work`). A `--` successor is deliberately NOT protected
|
|
216
|
+
// here: it falls through to the end-of-options arm above, keeping `--` a
|
|
217
|
+
// single, consistent meaning instead of being swallowed as a flag value.
|
|
218
|
+
// Known value-less launch flags are exempt so a trailing profile still
|
|
219
|
+
// activates (`omp --print --profile work`).
|
|
220
|
+
if (isUnknownLongValueCandidate(arg)) {
|
|
221
|
+
canDispatchSubcommand = false;
|
|
222
|
+
stripped.push(arg);
|
|
223
|
+
const next = argv[index + 1];
|
|
224
|
+
if (next !== undefined && !next.startsWith("-")) {
|
|
225
|
+
stripped.push(next);
|
|
226
|
+
index += 1;
|
|
227
|
+
}
|
|
228
|
+
continue;
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
// Only the first residual argv token can be the dispatched subcommand. Once
|
|
232
|
+
// any other token has been forwarded, later subcommand names are launch text.
|
|
233
|
+
// `launch` and `acp` are explicit spellings of launch-shaped commands, so
|
|
234
|
+
// global launch flags that follow them must still be extracted.
|
|
235
|
+
if (canDispatchSubcommand && isSubcommand(arg) && !isProfileBootstrapSubcommand(arg)) {
|
|
236
|
+
sawSubcommand = true;
|
|
237
|
+
}
|
|
238
|
+
canDispatchSubcommand = false;
|
|
239
|
+
stripped.push(arg);
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
return { argv: stripped, profile, aliasName };
|
|
243
|
+
}
|