/** * Copyright (c) 2026, Salesforce, Inc., * All rights reserved. * For full license text, see the LICENSE.txt file */ import { COMMANDS, type CommandDef } from "./command-registry.js"; import type { QuerySession } from "./session.js"; import type { WalkerResult } from "./walker.js"; // ── Interactive mode flag ───────────────────────────────────────────────────── let _interactiveMode = false; export function setInteractiveMode(on: boolean): void { _interactiveMode = on; } export function isInteractiveMode(): boolean { return _interactiveMode; } // ── Backward-compatible re-export ───────────────────────────────────────────── // QUERY_COMMANDS was the original registry. It now delegates to the canonical // COMMANDS array in command-registry.ts so there is a single source of truth. export { COMMANDS as QUERY_COMMANDS }; // ── Formatting helpers ─────────────────────────────────────────────────────── export function formatHelp(topic?: string): string { const lines: string[] = []; if (topic) { const spec: CommandDef | undefined = COMMANDS.find((c) => c.name === topic); if (!spec) { return `Unknown help topic: "${topic}". Run \`help\` with no arguments for a full list.`; } lines.push(`${spec.name} — ${spec.summary}`); lines.push(""); const usagePrefix = _interactiveMode ? "" : "graphiti "; const usageStr = spec.usage.startsWith("graphiti ") ? spec.usage : `${usagePrefix}${spec.usage}`; lines.push(`Usage: ${usageStr}`); if (spec.subcommands && spec.subcommands.length > 0) { lines.push(""); lines.push("Subcommands:"); for (const sub of spec.subcommands) { lines.push(` ${sub.usage.padEnd(32)} ${sub.description}`); } } if (topic === "ls") { lines.push(""); lines.push("Search behavior (--search):"); lines.push(' Multiple terms are OR-matched: --search "Name Id" matches fields'); lines.push(' containing either "Name" or "Id". Each term is prefix-matched'); lines.push(' against CamelCase word segments, so "Id" matches "AccountId"'); lines.push(' but not "Hide".'); } if (topic === "alias" || topic === "mkdir") { lines.push(""); lines.push("Multi-query composition:"); lines.push(" Use alias to create multiple named instances of the same field,"); lines.push(" each with different arguments. This lets you combine several queries"); lines.push(" into a single GraphQL request (e.g. dashboard tiles + lists)."); lines.push(""); lines.push(" # Full end-to-end example (from /query/uiapi/query):"); lines.push(" alias newCases Case"); lines.push(' set newCases/@args/where \'{"Status":{"eq":"New"}}\''); lines.push(" set newCases/@args/first 10"); lines.push(" select newCases/edges/node/Id newCases/edges/node/Subject/value"); lines.push(""); lines.push(" alias myCases Case"); lines.push(" set myCases/@args/scope MINE"); lines.push(" set myCases/@args/first 20"); lines.push(" select myCases/edges/node/Id myCases/edges/node/Status/value"); lines.push(""); lines.push(" show # see the combined query"); } if (topic === "chain") { lines.push(""); lines.push("Note: session management commands (new, use, connect, describe,"); lines.push("interactive) must be run as separate commands before chaining."); lines.push("Only session-scoped commands (cd, ls, select, set, var, etc.)"); lines.push("can be used inside a chain."); } if (topic === "set" || topic === "assign") { lines.push(""); lines.push("Inline key=value syntax (preferred):"); lines.push(" set first=10 # set on current field"); lines.push(" set uiapi/query/Case first=10 scope=MINE # with absolute field path"); lines.push(' set where=\'{"Status":{"eq":"New"}}\' # JSON value'); lines.push(""); lines.push("Legacy pair syntax:"); lines.push(" set @args/first 10"); lines.push(' set @args/where/Name/like "Acme%"'); } lines.push(""); lines.push("Examples:"); for (const ex of spec.examples) { const exPrefix = _interactiveMode ? " " : " "; lines.push(`${exPrefix}${ex}`); } return lines.join("\n"); } lines.push("Graphiti CLI — Progressive GraphQL query builder for Salesforce"); lines.push(""); lines.push( "Session resolution: --session / -s flag > GRAPHITI_SESSION env var > ~/.graphiti/active", ); lines.push("JSON output: --json flag or GRAPHITI_AGENT=1 env var"); lines.push(""); lines.push("Setup:"); for (const name of ["new", "use", "connect"]) { const spec = COMMANDS.find((c) => c.name === name); if (spec) lines.push(` ${spec.name.padEnd(12)} ${spec.summary}`); } lines.push(""); lines.push("Navigation:"); for (const name of ["pwd", "ls", "cd"]) { const spec = COMMANDS.find((c) => c.name === name)!; lines.push(` ${spec.name.padEnd(12)} ${spec.summary}`); } lines.push(""); lines.push("Query Building:"); for (const name of ["select", "drop", "alias", "optional", "undo"]) { const spec = COMMANDS.find((c) => c.name === name)!; lines.push(` ${spec.name.padEnd(12)} ${spec.summary}`); } lines.push(""); lines.push("Arguments & Variables:"); for (const name of ["set", "unset", "var"]) { const spec = COMMANDS.find((c) => c.name === name)!; lines.push(` ${spec.name.padEnd(12)} ${spec.summary}`); } lines.push(""); lines.push("Review & Execute:"); for (const name of ["show", "check", "run", "describe", "codegen"]) { const spec = COMMANDS.find((c) => c.name === name)!; lines.push(` ${spec.name.padEnd(12)} ${spec.summary}`); } lines.push(""); lines.push("Session Management:"); for (const name of ["sessions", "clone", "reset"]) { const spec = COMMANDS.find((c) => c.name === name)!; lines.push(` ${spec.name.padEnd(12)} ${spec.summary}`); } lines.push(""); lines.push("Other:"); const otherCommands = _interactiveMode ? ["chain", "help"] : ["chain", "help", "interactive"]; for (const name of otherCommands) { const spec = COMMANDS.find((c) => c.name === name); if (spec) lines.push(` ${spec.name.padEnd(12)} ${spec.summary}`); } lines.push(""); lines.push("Run `help ` for detailed usage and examples."); return lines.join("\n"); } // ── Next-steps hints ───────────────────────────────────────────────────────── export interface NextStepsContext { sessionId: string; command: string; walkerResult?: WalkerResult; session?: QuerySession; } export function formatNextSteps(ctx: NextStepsContext): string { const { sessionId: sid, command, walkerResult: wr, session } = ctx; const lines: string[] = []; const p = (cmd: string, desc: string) => { if (_interactiveMode) { lines.push(` ${cmd.padEnd(36)} # ${desc}`); } else { lines.push(` graphiti query ${sid} ${cmd.padEnd(36)} # ${desc}`); } }; const isRoot = !session || session.navigationPath.length === 0; const hasSelections = session ? session.nodes.length > 0 : false; const hasArgs = wr && wr.args.length > 0; const isLeaf = wr?.isLeaf ?? false; const _hasFragments = wr && wr.possibleTypes.length > 0; const hasFields = wr && !wr.isLeaf && wr.fields.length > 0; lines.push(""); lines.push("Next steps:"); switch (command) { case "new": { p("cd query", "navigate into the query tree"); p("ls", "see available root fields"); p("interactive", "start an interactive session"); break; } case "ls": { if (isLeaf) { if (!isRoot && session) { const leaf = session.navigationPath[session.navigationPath.length - 1]; p(`cd ..`, "go up to parent directory"); p(`select ${leaf}`, "select this leaf field"); } } else { if (hasFields) { const firstDir = wr!.fields.find((f) => f.typeKind !== "SCALAR" && f.typeKind !== "ENUM"); const firstLeaf = wr!.fields.find( (f) => f.typeKind === "SCALAR" || f.typeKind === "ENUM", ); if (firstDir) p(`cd ${firstDir.name}`, `navigate into ${firstDir.name}/`); if (firstLeaf) p(`select ${firstLeaf.name}`, `add ${firstLeaf.name} to projection`); const hasConnections = wr!.fields.some((f) => f.typeName.includes("Connection")); if (hasConnections && firstDir) p(`alias myAlias ${firstDir.name}`, "compose multiple queries via aliases"); } if (hasArgs) p(`cd @args`, "navigate into arguments"); if (!isRoot) p(`cd ..`, "go up one level"); } break; } case "cd": { p(`ls`, "see contents at this path"); if (hasArgs) p(`cd @args`, "configure field arguments"); if (hasFields) { const firstLeaf = wr!.fields.find((f) => f.typeKind === "SCALAR" || f.typeKind === "ENUM"); if (firstLeaf) p(`select ${firstLeaf.name}`, `add ${firstLeaf.name} to projection`); } break; } case "select": { p(`ls`, "see more available fields"); if (!isRoot) p(`cd ..`, "go up to parent"); if (hasSelections) { p(`show`, "preview the current query string"); p(`check`, "validate the query"); } break; } case "assign": case "set": { p(`show`, "preview query with arguments"); p(`ls`, "see other fields to set"); if (hasSelections) p(`check`, "validate the query"); break; } case "define": case "var": { p(`cd /variables/$varName`, "navigate into the variable to set values"); p(`show`, "preview the query signature"); break; } case "show": { p(`check`, "validate the query"); if (hasSelections) p(`run`, "execute the query against the org"); break; } case "validate": case "check": { if (hasSelections) p(`run`, "execute the validated query"); p(`show`, "preview the query string"); break; } default: { p(`ls`, "list contents at current path"); p(`show`, "preview the current query"); if (hasSelections) p(`check`, "validate the query"); break; } } if (lines.length <= 2) { p(`help`, "show all available commands"); } return lines.join("\n"); }