680917f02a
Added named OMP profiles that isolate agent state (auth credentials,
sessions, settings, model cache, history, memories, blobs, plus
config root subdirs) under `~/.omp/profiles/<name>/agent/`. Activated
via `--profile <name>` or `OMP_PROFILE=<name>`; `default` maps back to
the regular `~/.omp/agent/` tree.
Added `--alias <command>` to generate a shell shortcut (e.g.
`omp-work`) that forwards `omp --profile <name>`. Detects the active
shell (bash, zsh, fish, PowerShell, pwsh), writes a wrapper into the
correct rc file, and preserves subcommands like `update`, `--version`,
and `--model` because the wrapper passes through argv unchanged.
The `--profile`/`--alias` bootstrap pre-parser lives in
`packages/coding-agent/src/cli/profile-bootstrap.ts` and runs before
any module that touches `getAgentDir()` (notably `@oh-my-pi/pi-utils/env`,
which eagerly loads `.env` from the agent directory at its own import
time). The pre-parser mirrors `parseArgs` value-consumption rules and
honors `--`, so commands like `omp --system-prompt --profile foo` pass
the literal `--profile` through as the prompt body instead of silently
activating profile `foo`.
XDG resolution for named profiles is keyed on the profile-specific
XDG path (`$XDG_*_HOME/omp/profiles/<name>`), never the base app root,
so a profile's location is decided once at first activation and stays
stable even after `omp config init-xdg` materializes the base later.
The default profile keeps its existing base-app-root check.
`setProfile(undefined)` (and `setProfile("default")`) restores the
pre-profile `PI_CODING_AGENT_DIR` snapshot taken at first activation
instead of unconditionally deleting it. `setAgentDir` refreshes the
snapshot since that call is the user explicitly redefining the
baseline.
Validation rejects profile names that match `.`/`..`, fail
`/^[A-Za-z0-9][A-Za-z0-9._-]{0,63}$/`, or hit a Windows reserved
device name (`CON`, `PRN`, `AUX`, `NUL`, `COM0-9`, `LPT0-9`, including
dotted variants like `CON.txt`) — those would let `setProfile` accept
the input only for directory creation to fail later with confusing
errors on Windows.
98 lines
3.5 KiB
TypeScript
98 lines
3.5 KiB
TypeScript
import { afterEach, beforeEach, describe, expect, it, vi } from "bun:test";
|
|
import * as fs from "node:fs/promises";
|
|
import * as os from "node:os";
|
|
import * as path from "node:path";
|
|
import { getActiveProfile, getAgentDir, setAgentDir, setProfile } from "@oh-my-pi/pi-utils/dirs";
|
|
import { Snowflake } from "@oh-my-pi/pi-utils/snowflake";
|
|
import { runCli } from "../src/cli";
|
|
import * as profileAliasCli from "../src/cli/profile-alias";
|
|
|
|
describe("global --profile flag", () => {
|
|
let configDir = "";
|
|
let originalProfile: string | undefined;
|
|
let originalAgentDir = "";
|
|
let originalAgentDirEnv: string | undefined;
|
|
let originalConfigDir: string | undefined;
|
|
|
|
beforeEach(() => {
|
|
originalProfile = getActiveProfile();
|
|
originalAgentDir = getAgentDir();
|
|
originalAgentDirEnv = process.env.PI_CODING_AGENT_DIR;
|
|
originalConfigDir = process.env.PI_CONFIG_DIR;
|
|
configDir = `.omp-profile-cli-test-${Snowflake.next()}`;
|
|
process.env.PI_CONFIG_DIR = configDir;
|
|
process.exitCode = 0;
|
|
});
|
|
|
|
afterEach(async () => {
|
|
vi.restoreAllMocks();
|
|
setProfile(undefined);
|
|
if (originalConfigDir === undefined) {
|
|
delete process.env.PI_CONFIG_DIR;
|
|
} else {
|
|
process.env.PI_CONFIG_DIR = originalConfigDir;
|
|
}
|
|
if (originalProfile) {
|
|
setProfile(originalProfile);
|
|
} else if (originalAgentDirEnv !== undefined) {
|
|
setAgentDir(originalAgentDir);
|
|
} else {
|
|
setProfile(undefined);
|
|
}
|
|
process.exitCode = 0;
|
|
await fs.rm(path.join(os.homedir(), configDir), { recursive: true, force: true });
|
|
});
|
|
|
|
it("activates a profile before dispatching root flags", async () => {
|
|
const writeSpy = vi.spyOn(process.stdout, "write").mockImplementation(() => true);
|
|
|
|
await runCli(["--profile=work", "--version"]);
|
|
|
|
expect(process.exitCode).toBe(0);
|
|
expect(writeSpy).toHaveBeenCalled();
|
|
expect(getActiveProfile()).toBe("work");
|
|
expect(getAgentDir()).toBe(path.join(os.homedir(), configDir, "profiles", "work", "agent"));
|
|
});
|
|
|
|
it("accepts the profile flag after other root flags", async () => {
|
|
vi.spyOn(process.stdout, "write").mockImplementation(() => true);
|
|
|
|
await runCli(["--version", "--profile", "office"]);
|
|
|
|
expect(process.exitCode).toBe(0);
|
|
expect(getActiveProfile()).toBe("office");
|
|
expect(getAgentDir()).toBe(path.join(os.homedir(), configDir, "profiles", "office", "agent"));
|
|
});
|
|
|
|
it("installs a shell alias and exits before command dispatch", async () => {
|
|
const installSpy = vi.spyOn(profileAliasCli, "installProfileAlias").mockResolvedValue({
|
|
shell: "bash",
|
|
configPath: "/home/me/.bashrc",
|
|
aliasName: "omp-work",
|
|
profile: "work",
|
|
command: "omp --profile work",
|
|
reloadedWith: ". '/home/me/.bashrc'",
|
|
});
|
|
const outSpy = vi.spyOn(process.stdout, "write").mockImplementation(() => true);
|
|
|
|
await runCli(["--profile", "work", "--alias", "omp-work", "--version"]);
|
|
|
|
expect(process.exitCode).toBe(0);
|
|
expect(installSpy).toHaveBeenCalledWith({ profile: "work", aliasName: "omp-work" });
|
|
expect(outSpy.mock.calls.map(call => String(call[0] ?? "")).join("\n")).toContain("Created omp-work");
|
|
});
|
|
|
|
it("rejects missing profile values without dispatching", async () => {
|
|
const errSpy = vi.spyOn(process.stderr, "write").mockImplementation(() => true);
|
|
const outSpy = vi.spyOn(process.stdout, "write").mockImplementation(() => true);
|
|
|
|
await runCli(["--profile", "--version"]);
|
|
|
|
expect(process.exitCode).toBe(1);
|
|
expect(errSpy.mock.calls.map(call => String(call[0] ?? "")).join("\n")).toContain(
|
|
"--profile requires a profile name",
|
|
);
|
|
expect(outSpy).not.toHaveBeenCalled();
|
|
});
|
|
});
|