- profile-alias: refuse to rewrite a managed block whose start marker lacks a matching end marker instead of appending, which on the next install would splice from the stale start through the new end and delete intervening user shell config (data loss in dotfiles). - dirs/cli: add resolveProfileEnv so OMP_PROFILE takes precedence and an explicitly-empty OMP_PROFILE selects the default profile instead of falling through to PI_PROFILE; share the rule across both env-read sites. - dirs: reject uppercase profile names so profile identity/isolation is stable across case-sensitive and case-insensitive filesystems. - profile-bootstrap: treat an unclassified bare long option as a possible extension string flag and forward its successor untouched (never as a global --profile/--alias), while exempting known value-less launch flags via VALUELESS_FLAGS so 'omp --print --profile work' still selects a profile. - tests: cover all four contracts.
174 lines
6.1 KiB
TypeScript
174 lines
6.1 KiB
TypeScript
/**
|
|
* Bootstrap-time argv preparser for the global `--profile` / `--alias` flags.
|
|
*
|
|
* Profile selection MUST happen before any module reads `getAgentDir()` (notably
|
|
* `@oh-my-pi/pi-utils/env`, which eagerly loads `.env` from the agent directory
|
|
* during its own import). The full `parseArgs` from `./args.ts` lives downstream
|
|
* of those imports, so we can't rely on it for profile bootstrap — we have to
|
|
* crack open argv before the lazy command modules load.
|
|
*
|
|
* Because of that, this preparser must respect the same value-consumption
|
|
* contract as `args.ts`: known string-valued flags consume the next token
|
|
* unconditionally (so the value can legitimately start with `-`), and the
|
|
* optional-value flags (`--resume`, `--session`, `-r`, `--list-models`)
|
|
* consume the next token only when it doesn't look like another flag. Without
|
|
* this, `omp --system-prompt --profile foo` silently activates profile `foo`
|
|
* instead of passing the literal `--profile` to the system prompt and `foo`
|
|
* as a positional message (issue raised by code review).
|
|
*
|
|
* The shared classification lives in {@link ./flag-tables}, imported below,
|
|
* so the bootstrap and `args.ts` reference one source of truth instead of
|
|
* maintaining parallel constants.
|
|
*
|
|
* An unclassified bare long option (one not in any flag table) is treated as a
|
|
* possible extension string flag: its successor token is forwarded untouched and
|
|
* never read as a global `--profile`/`--alias`. Known value-less launch flags
|
|
* ({@link VALUELESS_FLAGS}) are exempt so a trailing profile still activates
|
|
* (`omp --print --profile work`).
|
|
*/
|
|
|
|
import { isSubcommand } from "../cli-commands";
|
|
import { OPTIONAL_FLAGS, OPTIONAL_VALUE_FLAGS, STRING_VALUE_FLAGS, VALUELESS_FLAGS } from "./flag-tables";
|
|
|
|
export interface ProfileBootstrapResult {
|
|
argv: string[];
|
|
profile?: string;
|
|
aliasName?: string;
|
|
}
|
|
|
|
/**
|
|
* Strip `--profile` / `--alias` from argv while preserving the surrounding
|
|
* argument structure, returning the residual argv to hand to the launch parser
|
|
* and the captured flag values.
|
|
*
|
|
* Global flag extraction stops only when the first residual argv token names a
|
|
* registered subcommand (e.g. `grep`): everything from that token onward is
|
|
* forwarded verbatim so a subcommand's own flags and positionals are never
|
|
* stolen (`omp grep --profile <path>` greps for `--profile`; it does not select
|
|
* a profile). Later subcommand-shaped words still belong to `launch` when an
|
|
* earlier token already made `launch` the dispatched command.
|
|
*
|
|
* Throws when either flag is supplied without a value.
|
|
*/
|
|
export function extractProfileFlags(argv: readonly string[]): ProfileBootstrapResult {
|
|
const stripped: string[] = [];
|
|
let profile: string | undefined;
|
|
let aliasName: string | undefined;
|
|
let passThrough = false;
|
|
let sawSubcommand = false;
|
|
let canDispatchSubcommand = true;
|
|
|
|
for (let index = 0; index < argv.length; index += 1) {
|
|
const arg = argv[index];
|
|
|
|
if (passThrough || sawSubcommand) {
|
|
stripped.push(arg);
|
|
continue;
|
|
}
|
|
|
|
// `--` ends option processing. Anything that follows is forwarded verbatim
|
|
// so users can pass arbitrary tokens (including a literal `--profile`) to
|
|
// downstream tools without the bootstrap stealing them.
|
|
if (arg === "--") {
|
|
passThrough = true;
|
|
stripped.push(arg);
|
|
continue;
|
|
}
|
|
|
|
if (arg === "--profile") {
|
|
const value = argv[index + 1];
|
|
if (!value || value.startsWith("-")) {
|
|
throw new Error("--profile requires a profile name");
|
|
}
|
|
profile = value;
|
|
index += 1;
|
|
continue;
|
|
}
|
|
if (arg.startsWith("--profile=")) {
|
|
const value = arg.slice("--profile=".length);
|
|
if (!value) {
|
|
throw new Error("--profile requires a profile name");
|
|
}
|
|
profile = value;
|
|
continue;
|
|
}
|
|
if (arg === "--alias") {
|
|
const value = argv[index + 1];
|
|
if (!value || value.startsWith("-")) {
|
|
throw new Error("--alias requires a command name");
|
|
}
|
|
aliasName = value;
|
|
index += 1;
|
|
continue;
|
|
}
|
|
if (arg.startsWith("--alias=")) {
|
|
const value = arg.slice("--alias=".length);
|
|
if (!value) {
|
|
throw new Error("--alias requires a command name");
|
|
}
|
|
aliasName = value;
|
|
continue;
|
|
}
|
|
|
|
// Forward both the flag and its value untouched so the downstream parser
|
|
// gets exactly what the user typed. Critical for `--system-prompt
|
|
// --profile foo`: the bootstrap must NOT interpret `--profile` here, it
|
|
// belongs to `--system-prompt`.
|
|
if (STRING_VALUE_FLAGS.has(arg)) {
|
|
canDispatchSubcommand = false;
|
|
stripped.push(arg);
|
|
if (index + 1 < argv.length) {
|
|
stripped.push(argv[index + 1]);
|
|
index += 1;
|
|
}
|
|
continue;
|
|
}
|
|
|
|
if (OPTIONAL_VALUE_FLAGS.has(arg)) {
|
|
canDispatchSubcommand = false;
|
|
stripped.push(arg);
|
|
const config = OPTIONAL_FLAGS[arg];
|
|
const next = argv[index + 1];
|
|
if (
|
|
next !== undefined &&
|
|
!next.startsWith("-") &&
|
|
!(config.rejectAtPrefix === true && next.startsWith("@")) &&
|
|
!(config.rejectEmpty === true && next.length === 0)
|
|
) {
|
|
stripped.push(next);
|
|
index += 1;
|
|
}
|
|
continue;
|
|
}
|
|
|
|
// An unclassified bare long option (`--xxx` with no `=`) may be an extension
|
|
// string flag that consumes the next token as its value. The bootstrap runs
|
|
// before extensions load, so it cannot consult the extension flag table; to
|
|
// avoid stealing a value that belongs to such a flag (e.g. `omp --bar --alias
|
|
// foo` where an extension registers string flag `bar`), forward the flag AND
|
|
// its immediate successor untouched, never interpreting that successor as a
|
|
// global --profile/--alias. Known value-less launch flags are exempt so a
|
|
// trailing profile still activates (`omp --print --profile work`).
|
|
if (arg.startsWith("--") && !arg.includes("=") && !VALUELESS_FLAGS.has(arg)) {
|
|
canDispatchSubcommand = false;
|
|
stripped.push(arg);
|
|
if (index + 1 < argv.length) {
|
|
stripped.push(argv[index + 1]);
|
|
index += 1;
|
|
}
|
|
continue;
|
|
}
|
|
|
|
// Only the first residual argv token can be the dispatched subcommand. Once
|
|
// any other token has been forwarded, later subcommand names are launch text.
|
|
if (canDispatchSubcommand && isSubcommand(arg)) {
|
|
sawSubcommand = true;
|
|
} else {
|
|
canDispatchSubcommand = false;
|
|
}
|
|
stripped.push(arg);
|
|
}
|
|
|
|
return { argv: stripped, profile, aliasName };
|
|
}
|