{"version":3,"file":"index.mjs","names":[],"sources":["../../../../src/services/workflowLaunchCommand/index.ts"],"sourcesContent":["/**\n * The `/workflow-launch` line, parsed.\n *\n * WHY A VERB AT ALL:\n * A browser can only send a session a prompt frame, so every cockpit action\n * that has to run inside the session travels as a slash line, the way the\n * agent catalog sends `/run`. The workflow tools answer to the agent, not to a\n * page, and the leader board answers to a keyboard, so neither of them can be\n * what the launch dialog sends.\n *\n * THE SHAPE:\n *   /workflow-launch <workflow> [key=value ...] [prompt words]\n *\n * Key-value pairs come first and stop at the first token that is not one, so\n * everything after them is the prompt exactly as it was typed, including any\n * `=` inside it. `runner` is the one reserved key; the rest are the workflow's\n * own `workflow_dispatch` inputs. Values may be double quoted to hold spaces.\n */\n\n/** The reserved key: which runner map entry the run executes with. */\nconst RUNNER_KEY = 'runner';\nconst KEY_VALUE = /^([A-Za-z_][\\w.-]*)=(.*)$/s;\n\nexport interface ParsedWorkflowLaunch {\n  /** The workflow's name or its path, as typed; resolution belongs to the caller. */\n  workflow: string;\n  runner?: string;\n  inputs: Record<string, string>;\n  prompt?: string;\n}\n\nexport interface WorkflowLaunchParseFailure {\n  error: string;\n}\n\nexport function isLaunchParseFailure(\n  value: ParsedWorkflowLaunch | WorkflowLaunchParseFailure,\n): value is WorkflowLaunchParseFailure {\n  return 'error' in value;\n}\n\n/** Strips one layer of surrounding double quotes, which is how a value holds spaces. */\nfunction unquote(value: string): string {\n  return value.length > 1 && value.startsWith('\"') && value.endsWith('\"') ? value.slice(1, -1) : value;\n}\n\n/**\n * Splits on whitespace, keeping double-quoted runs together.\n *\n * Returns each token with the offset it started at, so the caller can take the\n * rest of the line verbatim rather than rebuilding it from tokens and losing\n * the spacing a prompt was written with.\n */\nfunction tokenize(line: string): { text: string; start: number }[] {\n  const tokens: { text: string; start: number }[] = [];\n  let index = 0;\n  while (index < line.length) {\n    while (index < line.length && /\\s/.test(line[index] as string)) index += 1;\n    if (index >= line.length) break;\n    const start = index;\n    let quoted = false;\n    while (index < line.length && (quoted || !/\\s/.test(line[index] as string))) {\n      if (line[index] === '\"') quoted = !quoted;\n      index += 1;\n    }\n    tokens.push({ text: line.slice(start, index), start });\n  }\n  return tokens;\n}\n\nexport function parseWorkflowLaunchCommand(args: string): ParsedWorkflowLaunch | WorkflowLaunchParseFailure {\n  const tokens = tokenize(args.trim());\n  const first = tokens[0];\n  if (first === undefined) return { error: 'Usage: /workflow-launch <workflow> [key=value …] [prompt]' };\n\n  const inputs: Record<string, string> = {};\n  let runner: string | undefined;\n  let index = 1;\n  for (; index < tokens.length; index += 1) {\n    const token = tokens[index] as { text: string; start: number };\n    const match = KEY_VALUE.exec(token.text);\n    if (!match) break;\n    const key = match[1] as string;\n    const value = unquote(match[2] as string);\n    if (key === RUNNER_KEY) runner = value;\n    else inputs[key] = value;\n  }\n\n  const rest = tokens[index];\n  const prompt = rest === undefined ? undefined : args.trim().slice(rest.start).trim();\n  return {\n    workflow: unquote(first.text),\n    ...(runner === undefined ? {} : { runner }),\n    inputs,\n    ...(prompt === undefined || prompt === '' ? {} : { prompt }),\n  };\n}\n\n/** The line the cockpit sends, built from what its dialog collected. */\nexport function workflowLaunchCommand(request: ParsedWorkflowLaunch): string {\n  const quote = (value: string): string => (/\\s/.test(value) ? `\"${value}\"` : value);\n  const pairs = [\n    ...(request.runner === undefined ? [] : [`${RUNNER_KEY}=${quote(request.runner)}`]),\n    ...Object.entries(request.inputs).map(([key, value]) => `${key}=${quote(value)}`),\n  ];\n  const prompt = request.prompt?.trim();\n  return ['/workflow-launch', quote(request.workflow), ...pairs, ...(prompt ? [prompt] : [])].join(' ');\n}\n\n/** One catalog row, as much of it as resolution needs. */\nexport interface LaunchResolvableWorkflow {\n  name: string;\n  path: string;\n  relativePath: string;\n}\n\n/**\n * The workflow a token names: its name, its path in the repository, or the\n * tail of that path. Case-insensitive on the name, because the catalog shows\n * names as their author capitalised them and nobody types that back exactly.\n */\nexport function resolveWorkflowEntry<T extends LaunchResolvableWorkflow>(\n  entries: readonly T[],\n  token: string,\n): T | undefined {\n  const needle = token.trim();\n  const lower = needle.toLocaleLowerCase();\n  return (\n    entries.find((entry) => entry.name.toLocaleLowerCase() === lower) ??\n    entries.find((entry) => entry.relativePath === needle || entry.path === needle) ??\n    entries.find((entry) => entry.relativePath.endsWith(`/${needle}`) || entry.path.endsWith(`/${needle}`))\n  );\n}\n\n/** What a workflow declares, as much of it as a launch has to satisfy. */\nexport interface LaunchRequirements {\n  triggers: readonly string[];\n  inputs: readonly { name: string; required?: boolean; options?: readonly string[] }[];\n  /** Absent means the workflow names no runner map, so any runner will do. */\n  runners?: readonly string[];\n}\n\n/** The trigger that makes a prompt mandatory: the run waits for one otherwise. */\nconst USER_PROMPT_TRIGGER = 'user_prompt';\n\n/**\n * Everything wrong with a launch, before it is started.\n *\n * Checked here rather than left to the engine because the failure would\n * otherwise be a run that starts and then waits forever for terminal input, or\n * one that dies on a runner its steps never declared. Both read as the launch\n * having worked.\n */\nexport function validateWorkflowLaunch(\n  requirements: LaunchRequirements,\n  parsed: Pick<ParsedWorkflowLaunch, 'inputs' | 'prompt' | 'runner'>,\n): string[] {\n  const problems: string[] = [];\n  if (requirements.triggers.includes(USER_PROMPT_TRIGGER) && (parsed.prompt ?? '') === '') {\n    problems.push('This workflow is triggered by a prompt, so it needs one.');\n  }\n  const missing = requirements.inputs\n    .filter((input) => input.required === true && (parsed.inputs[input.name] ?? '') === '')\n    .map((input) => input.name);\n  if (missing.length > 0)\n    problems.push(`Missing required input${missing.length > 1 ? 's' : ''}: ${missing.join(', ')}.`);\n  for (const input of requirements.inputs) {\n    const value = parsed.inputs[input.name];\n    if (value === undefined || input.options === undefined || input.options.includes(value)) continue;\n    problems.push(`Input ${input.name} must be one of: ${input.options.join(', ')}.`);\n  }\n  const declared = requirements.runners;\n  if (parsed.runner !== undefined && declared !== undefined && !declared.includes(parsed.runner)) {\n    problems.push(\n      declared.length === 0\n        ? 'This workflow declares no runner its steps agree on.'\n        : `Runner ${parsed.runner} is not one this workflow declares: ${declared.join(', ')}.`,\n    );\n  }\n  return problems;\n}\n"],"mappings":"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"}