@phren/cli
Version:
Knowledge layer for AI agents - CLI, MCP server, and data layer
114 lines (113 loc) • 4.09 kB
JavaScript
/**
* Deep links into the shell: `phren shell --view tasks --here` opens straight
* on a project's task list instead of the Projects home screen.
*
* This exists so an outside launcher — the Herdr plugin, a tmux binding, an
* editor task — can put the user where they meant to be in one keypress.
*/
import { detectProject } from "../shared/index.js";
/** Spelling of a view a person would actually type, mapped to the canonical name. */
const VIEW_ALIASES = {
projects: "Projects",
project: "Projects",
tasks: "Tasks",
task: "Tasks",
todo: "Tasks",
todos: "Tasks",
findings: "Findings",
finding: "Findings",
memory: "Findings",
review: "Review Queue",
"review queue": "Review Queue",
"review-queue": "Review Queue",
queue: "Review Queue",
skills: "Skills",
skill: "Skills",
hooks: "Hooks",
hook: "Hooks",
health: "Health",
doctor: "Health",
graph: "Graph",
map: "Graph",
network: "Graph",
machines: "Machines/Profiles",
profiles: "Machines/Profiles",
"machines/profiles": "Machines/Profiles",
};
export function normalizeShellView(raw) {
if (!raw)
return undefined;
return VIEW_ALIASES[raw.trim().toLowerCase()];
}
export const SHELL_VIEW_ALIASES = Object.keys(VIEW_ALIASES);
/** Views that render nothing useful until a project is in context. */
const PROJECT_SCOPED_VIEWS = new Set(["Tasks", "Findings", "Review Queue", "Skills"]);
/** Parse `--view`/`--project`/`--here`, in both `--flag value` and `--flag=value` form. */
export function parseShellArgs(args) {
const parsed = {};
for (let i = 0; i < args.length; i++) {
const arg = args[i];
const eq = arg.indexOf("=");
const flag = eq === -1 ? arg : arg.slice(0, eq);
const inline = eq === -1 ? undefined : arg.slice(eq + 1);
// `--view tasks` consumes the next argv entry; `--view=tasks` does not.
const take = () => (inline !== undefined ? inline : args[++i]);
switch (flag) {
case "--view":
parsed.view = take();
break;
case "--project":
parsed.project = take();
break;
case "--here":
parsed.here = true;
break;
case "--live":
parsed.live = true;
break;
case "--no-live":
parsed.live = false;
break;
default:
if (flag.startsWith("-") && parsed.unknown === undefined)
parsed.unknown = flag;
}
}
return parsed;
}
/**
* Turn parsed flags into the state the shell should open in. An unresolvable
* view or project is dropped rather than fatal — a deep link that misses should
* still land you in the shell, on the home screen, with everything else intact.
*/
export function resolveShellStartup(parsed, opts) {
const warnings = [];
const startup = {};
if (parsed.live !== undefined)
startup.live = parsed.live;
if (parsed.unknown)
warnings.push(`Unknown flag ${parsed.unknown} — ignored.`);
if (parsed.view) {
const view = normalizeShellView(parsed.view);
if (view)
startup.view = view;
else
warnings.push(`Unknown view "${parsed.view}" — opening on Projects.`);
}
if (parsed.project) {
startup.project = parsed.project;
}
else if (parsed.here) {
const detected = detectProject(opts.phrenPath, opts.cwd ?? process.cwd(), opts.profile);
if (detected)
startup.project = detected;
else
warnings.push("No phren project for this directory — opening without a project.");
}
// These views render an empty "no project selected" screen without a project,
// which is a worse landing spot than the project list the user came from.
if (!startup.project && startup.view && PROJECT_SCOPED_VIEWS.has(startup.view)) {
startup.view = undefined;
}
return { startup, warnings };
}