{"version":3,"sources":["../src/cli/commands/login.ts"],"sourcesContent":["import * as fs from \"fs\";\nimport * as path from \"path\";\nimport chalk from \"chalk\";\nimport prompts from \"prompts\";\nimport { formatApiErrorMessage } from \"@/client-sdk/services/_shared/format-api-error\";\nimport {\n  runDeviceFlowLogin,\n  runUnifiedLoginFlow,\n} from \"@/cli/utils/governance/login-flow\";\nimport {\n  loadConfig,\n  saveConfig,\n} from \"@/cli/utils/governance/config\";\nimport { resolveControlPlaneEndpoint } from \"@/cli/utils/governance/resolveEndpoint\";\nimport { DEFAULT_ENDPOINT } from \"@/internal/constants\";\n\n/**\n * Always-on agent-hint banner shown above the interactive prompts on\n * `langwatch login` (no flags). Some agent harnesses fake a TTY but can't\n * actually answer prompts — this banner names the escape-hatch flags so\n * the agent (or the human staring at the stuck prompt) can re-invoke\n * with the right flag and proceed.\n *\n * Spec: specs/ai-governance/cli-onboarding/login-unified.feature\n */\nfunction printAgentHintBanner(): void {\n  console.log(\n    chalk.gray(\n      \"Running interactively. To skip these prompts (CI / agents that already have a credential):\",\n    ),\n  );\n  console.log(\n    chalk.gray(\n      \"  --device                   AI tools / SSO (claude, codex, gemini, opencode)\",\n    ),\n  );\n  console.log(\n    chalk.gray(\n      \"  --project                  project SDK key into .env via browser (SDK, evals, prompts)\",\n    ),\n  );\n  console.log(\n    chalk.gray(\n      \"  --api-key <KEY>            project SDK key you already have, into .env\",\n    ),\n  );\n  console.log(\n    chalk.gray(\n      \"  --token <TOKEN>            pre-minted device session (writes ~/.langwatch/config.json)\",\n    ),\n  );\n  console.log(\n    chalk.gray(\n      \"  --endpoint <URL>           self-hosted instance URL\",\n    ),\n  );\n  console.log();\n}\n\nconst updateEnvFile = (\n  apiKey: string,\n): { created: boolean; updated: boolean; path: string } => {\n  const envPath = path.join(process.cwd(), \".env\");\n\n  // Check if .env exists\n  if (!fs.existsSync(envPath)) {\n    // Create new .env file\n    fs.writeFileSync(envPath, `LANGWATCH_API_KEY=${apiKey}\\n`);\n    return { created: true, updated: false, path: envPath };\n  }\n\n  // Read existing .env file\n  const content = fs.readFileSync(envPath, \"utf-8\");\n  const lines = content.split(\"\\n\");\n\n  // Check if LANGWATCH_API_KEY already exists and update it\n  let found = false;\n  const updatedLines = lines.map((line) => {\n    if (line.startsWith(\"LANGWATCH_API_KEY=\")) {\n      found = true;\n      return `LANGWATCH_API_KEY=${apiKey}`;\n    }\n    return line;\n  });\n\n  if (!found) {\n    // Add to end of file\n    if (content.endsWith(\"\\n\") || content === \"\") {\n      updatedLines.push(`LANGWATCH_API_KEY=${apiKey}`);\n    } else {\n      updatedLines.push(\"\", `LANGWATCH_API_KEY=${apiKey}`);\n    }\n  }\n\n  fs.writeFileSync(envPath, updatedLines.join(\"\\n\"));\n  return { created: false, updated: found, path: envPath };\n};\n\nexport const loginCommand = async (\n  options?: {\n    apiKey?: string;\n    device?: boolean;\n    project?: boolean;\n    browser?: string;\n    endpoint?: string;\n    token?: string;\n  },\n): Promise<void> => {\n  try {\n    // Honor `--endpoint` flag OR `LANGWATCH_ENDPOINT` env. Persist the\n    // resolved value BEFORE the chosen flow runs so subsequent reads\n    // (in the device flow, the API-key flow, any sub-command spawned\n    // later) see the right control-plane URL. The 4-source resolver\n    // (flag > env > config > default) honors this value via the\n    // persisted-config layer for any flow that doesn't explicitly take\n    // a flag. The env var has the same precedence as the flag for the\n    // login flow itself so users running `LANGWATCH_ENDPOINT=... langwatch\n    // login` skip the cloud/self-hosted picker.\n    const endpointFromEnv = process.env.LANGWATCH_ENDPOINT?.trim();\n    const presetEndpoint = options?.endpoint ?? endpointFromEnv;\n    if (presetEndpoint) {\n      const trimmed = presetEndpoint.replace(/\\/+$/, \"\");\n      const cfg = loadConfig();\n      cfg.control_plane_url = trimmed;\n      saveConfig(cfg);\n    }\n\n    // --token: pre-minted device-session escape hatch (CI / agent contexts\n    // where the token was minted via the dashboard 'Personal Access Tokens'\n    // surface). No browser, no prompts — just persist the token so\n    // subsequent `langwatch claude/codex/...` invocations can use it.\n    if (options?.token) {\n      const token = options.token.trim();\n      if (token.length < 10) {\n        console.error(chalk.red(\"Error: token seems too short. Please check and try again.\"));\n        process.exit(1);\n      }\n      const cfg = loadConfig();\n      cfg.access_token = token;\n      // No refresh_token / expires_at — that's the trade-off of bypassing\n      // the device flow. The wrapper auto-login will mint a real session\n      // if this token expires, since loadConfig+isLoggedIn only checks\n      // access_token presence.\n      saveConfig(cfg);\n      console.log(chalk.green(\"✓ device-session token saved\"));\n      console.log(chalk.gray(\"  ~/.langwatch/config.json\"));\n      return;\n    }\n\n    // Device-flow mode: SSO via the control plane's RFC 8628 endpoints,\n    // mints a personal virtual key bound to the user. This is the\n    // governance-plane onboarding for enterprise users, distinct from the\n    // single-user API-key flow below.\n    if (options?.device) {\n      await runDeviceFlowLogin({ browser: options.browser });\n      return;\n    }\n\n    // --project: force PROJECT login (mint a project SDK key via the browser,\n    // write it to $CWD/.env). Symmetric to --device, and the explicit form of\n    // what a non-TTY context already defaults to. Use this when you want a\n    // real project's key for the SDK / `langwatch eval` / prompts rather than\n    // a personal device session. No \"where\"/\"how\" prompts; the project is\n    // picked in the browser.\n    if (options?.project) {\n      await runUnifiedLoginFlow({\n        kind: \"project_api_key\",\n        browser: options.browser,\n      });\n      return;\n    }\n\n    // Non-interactive mode: --api-key flag provided\n    if (options?.apiKey) {\n      const apiKey = options.apiKey.trim();\n      if (apiKey.length < 10) {\n        console.error(chalk.red(\"Error: API key seems too short. Please check and try again.\"));\n        process.exit(1);\n      }\n\n      const envResult = updateEnvFile(apiKey);\n      console.log(chalk.green(\"API key saved successfully.\"));\n      if (envResult.created) {\n        console.log(chalk.gray(`Created .env file at ${envResult.path}`));\n      } else if (envResult.updated) {\n        console.log(chalk.gray(`Updated existing API key in ${envResult.path}`));\n      } else {\n        console.log(chalk.gray(`Added API key to ${envResult.path}`));\n      }\n      return;\n    }\n\n    // Interactive mode (no flags). On a non-TTY context (CI, an agent's\n    // piped stdin) we cannot prompt. Erroring here used to nudge agents\n    // toward `--device`, which signs them into a personal device-session and\n    // silently routed their evaluations to a personal project. Default to\n    // PROJECT login instead: it writes a real project's key to `.env`, which\n    // is what the SDK, `langwatch eval`, and the skills expect. AI-tools\n    // login stays explicit behind `--device`.\n    if (!process.stdin.isTTY) {\n      console.log(\n        chalk.gray(\n          \"No login mode given. Defaulting to project login (writes LANGWATCH_API_KEY to .env). Force it with --project.\",\n        ),\n      );\n      console.log(\n        chalk.gray(\n          \"For AI-tools login (claude, codex, gemini, opencode), re-run: langwatch login --device\",\n        ),\n      );\n      console.log();\n      await runUnifiedLoginFlow({\n        kind: \"project_api_key\",\n        browser: options?.browser,\n      });\n      return;\n    }\n\n    // Always-on agent-hint banner — fake-TTY agents see this BEFORE the\n    // prompt block so they (or the human watching) can re-invoke with the\n    // right flag instead of staring at a stuck prompt.\n    printAgentHintBanner();\n\n    console.log(chalk.blue(\"🔐 LangWatch Login\"));\n    console.log();\n\n    // Q1 — endpoint (cloud vs self-hosted). Skipped if --endpoint was\n    // passed (already persisted above). The chosen endpoint is persisted to\n    // ~/.langwatch/config.json on EVERY branch so the subsequent\n    // `runUnifiedLoginFlow`/`runDeviceFlowLogin` call (which reads\n    // control_plane_url directly) and every later CLI command's resolver read\n    // target the right host with no env-vs-config ambiguity.\n    if (!options?.endpoint) {\n      // When the user already resolved a non-cloud endpoint (local dev or a\n      // self-hosted deployment via LANGWATCH_ENDPOINT or persisted config),\n      // default to keeping it but still offer to switch endpoint or jump to\n      // Cloud. On a fresh install Cloud stays the priority default.\n      const current = resolveControlPlaneEndpoint();\n      const hasCustomEndpoint = current.url !== DEFAULT_ENDPOINT;\n\n      const cloudChoice = {\n        title: \"LangWatch Cloud\",\n        description: \"app.langwatch.ai\",\n        value: \"cloud\",\n      };\n      const choices = hasCustomEndpoint\n        ? [\n            {\n              title: `Self-hosted instance (${current.url})`,\n              description: \"Keep using your current endpoint\",\n              value: \"keep\",\n            },\n            {\n              title: \"Self-hosted instance (different endpoint)\",\n              description: \"Point at another LangWatch deployment\",\n              value: \"self-hosted\",\n            },\n            cloudChoice,\n          ]\n        : [\n            cloudChoice,\n            {\n              title: \"Self-hosted instance\",\n              description: \"Your company's LangWatch deployment (custom URL)\",\n              value: \"self-hosted\",\n            },\n          ];\n\n      const where = await prompts({\n        type: \"select\",\n        name: \"where\",\n        message: \"Where do you want to log in?\",\n        choices,\n        initial: 0,\n      });\n      if (!where.where) {\n        console.log(chalk.yellow(\"Login cancelled\"));\n        process.exit(0);\n      }\n\n      const cfg = loadConfig();\n      if (where.where === \"cloud\") {\n        // Always repoint to cloud, overriding any stale local control_plane_url.\n        // Otherwise the device flow would dial the old localhost host and fail\n        // with ECONNREFUSED.\n        cfg.control_plane_url = DEFAULT_ENDPOINT;\n        saveConfig(cfg);\n      } else if (where.where === \"keep\") {\n        cfg.control_plane_url = current.url;\n        saveConfig(cfg);\n      } else if (where.where === \"self-hosted\") {\n        const url = await prompts({\n          type: \"text\",\n          name: \"url\",\n          message: \"Self-hosted LangWatch URL (e.g. https://lw.acme.internal):\",\n          validate: (v: string) => {\n            try {\n              const parsed = new URL(v);\n              if (parsed.protocol !== \"http:\" && parsed.protocol !== \"https:\") {\n                return \"URL must start with http(s)://\";\n              }\n              return true;\n            } catch {\n              return \"URL must be absolute (https://...)\";\n            }\n          },\n        });\n        if (!url.url) {\n          console.log(chalk.yellow(\"Login cancelled\"));\n          process.exit(0);\n        }\n        cfg.control_plane_url = (url.url as string).replace(/\\/+$/, \"\");\n        saveConfig(cfg);\n      }\n    }\n\n    // Q2 — auth mode (AI tools = device-flow vs Project SDK = API key)\n    const mode = await prompts({\n      type: \"select\",\n      name: \"mode\",\n      message: \"How do you want to use LangWatch?\",\n      choices: [\n        {\n          title: \"AI tools / agentic flows\",\n          description: \"claude, codex, cursor, gemini, opencode - device-flow SSO\",\n          value: \"device\",\n        },\n        {\n          title: \"Project / SDK API key\",\n          description: \"langwatch eval, sync, prompts, SDK auto-instrumentation - writes .env\",\n          value: \"api-key\",\n        },\n        {\n          title: \"Both\",\n          description: \"Run both flows in sequence\",\n          value: \"both\",\n        },\n      ],\n      initial: 0,\n    });\n    if (!mode.mode) {\n      console.log(chalk.yellow(\"Login cancelled\"));\n      process.exit(0);\n    }\n\n    if (mode.mode === \"device\" || mode.mode === \"both\") {\n      await runDeviceFlowLogin({ browser: options?.browser });\n    }\n    if (mode.mode === \"api-key\" || mode.mode === \"both\") {\n      // No-paste convergence (sergey f9fcc3927 + alexis bfef4ebab):\n      // /authorize page shows a project-picker + 'Generate API key'\n      // button; the freshly-minted key flows back to the CLI over the\n      // same RFC 8628 poll endpoint as the device-session flow. No\n      // copy-paste of the credential ever.\n      await runUnifiedLoginFlow({\n        kind: \"project_api_key\",\n        browser: options?.browser,\n      });\n    }\n    return;\n  } catch (error) {\n    console.error(\n      chalk.red(\n        `Error during login: ${\n          formatApiErrorMessage({ error })\n        }`,\n      ),\n    );\n    process.exit(1);\n  }\n};\n\n\n"],"mappings":"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","names":[]}