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.
108 lines
3.1 KiB
TypeScript
108 lines
3.1 KiB
TypeScript
import { describe, expect, it } from "bun:test";
|
|
import { installProfileAlias } from "../src/cli/profile-alias";
|
|
|
|
describe("profile alias installer", () => {
|
|
it("writes a bash-compatible alias that forwards subcommands through omp", async () => {
|
|
const files = new Map<string, string>();
|
|
|
|
const result = await installProfileAlias({
|
|
profile: "work",
|
|
aliasName: "omp-work",
|
|
shellPath: "/bin/bash",
|
|
platform: "linux",
|
|
homeDir: "/home/me",
|
|
readFile: async filePath => files.get(filePath) ?? "",
|
|
writeFile: async (filePath, content) => {
|
|
files.set(filePath, content);
|
|
},
|
|
});
|
|
|
|
expect(result.configPath).toBe("/home/me/.bashrc");
|
|
expect(files.get("/home/me/.bashrc")).toContain("alias omp-work='command omp --profile work'");
|
|
});
|
|
|
|
it("writes a fish function that forwards argv", async () => {
|
|
const files = new Map<string, string>();
|
|
|
|
await installProfileAlias({
|
|
profile: "work",
|
|
aliasName: "omp-work",
|
|
shellPath: "/opt/homebrew/bin/fish",
|
|
platform: "darwin",
|
|
homeDir: "/Users/me",
|
|
readFile: async filePath => files.get(filePath) ?? "",
|
|
writeFile: async (filePath, content) => {
|
|
files.set(filePath, content);
|
|
},
|
|
});
|
|
|
|
const content = files.get("/Users/me/.config/fish/conf.d/omp-profiles.fish") ?? "";
|
|
expect(content).toContain("function omp-work --wraps omp");
|
|
expect(content).toContain("command omp --profile work $argv");
|
|
});
|
|
|
|
it("writes a PowerShell function because aliases cannot carry arguments", async () => {
|
|
const files = new Map<string, string>();
|
|
|
|
await installProfileAlias({
|
|
profile: "work",
|
|
aliasName: "omp-work",
|
|
shellPath: "pwsh.exe",
|
|
platform: "win32",
|
|
homeDir: "C:\\Users\\me",
|
|
readFile: async filePath => files.get(filePath) ?? "",
|
|
writeFile: async (filePath, content) => {
|
|
files.set(filePath, content);
|
|
},
|
|
});
|
|
|
|
const content = files.get("C:\\Users\\me/Documents/PowerShell/Microsoft.PowerShell_profile.ps1") ?? "";
|
|
expect(content).toContain("function omp-work");
|
|
expect(content).toContain("& omp --profile work @args");
|
|
});
|
|
|
|
it("replaces a previous block for the same alias", async () => {
|
|
const files = new Map<string, string>([
|
|
[
|
|
"/home/me/.zshrc",
|
|
[
|
|
"before",
|
|
"# >>> omp profile alias: omp-work >>>",
|
|
"alias omp-work='command omp --profile old'",
|
|
"# <<< omp profile alias: omp-work <<<",
|
|
"after",
|
|
].join("\n"),
|
|
],
|
|
]);
|
|
|
|
await installProfileAlias({
|
|
profile: "work",
|
|
aliasName: "omp-work",
|
|
shellPath: "/bin/zsh",
|
|
platform: "darwin",
|
|
homeDir: "/home/me",
|
|
readFile: async filePath => files.get(filePath) ?? "",
|
|
writeFile: async (filePath, content) => {
|
|
files.set(filePath, content);
|
|
},
|
|
});
|
|
|
|
const content = files.get("/home/me/.zshrc") ?? "";
|
|
expect(content).toContain("before");
|
|
expect(content).toContain("after");
|
|
expect(content).toContain("alias omp-work='command omp --profile work'");
|
|
expect(content).not.toContain("--profile old");
|
|
});
|
|
|
|
it("refuses to shadow the base omp command", () => {
|
|
expect(
|
|
installProfileAlias({
|
|
profile: "work",
|
|
aliasName: "omp",
|
|
shellPath: "/bin/bash",
|
|
homeDir: "/home/me",
|
|
}),
|
|
).rejects.toThrow("Refusing to shadow");
|
|
});
|
|
});
|