From 433f656a44d66d15a24e8b4ec75e36eb761d00ca Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 29 Jul 2026 01:48:17 +0000 Subject: [PATCH 01/11] feat: move the wizard-internal source into this package MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Move the Ink-based setup wizard implementation from seamapi/wizard-internal into src/lib verbatim, so the wizard lives in the published package instead of a separate internal repository. This commit is a pure move: no source edits. The only change is that wizard-internal's src/index.tsx becomes src/lib/render.tsx, since src/lib/index.ts is already this package's library entrypoint. Nothing here is wired up yet — the stubbed entrypoint still runs, and the following commits adapt the moved code to this repository's conventions. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01W1qhuuZQZnA1FB1fBSxrqx --- scripts/smoke.tsx | 24 + src/lib/app.tsx | 827 +++++++++++++++++++++++++++ src/lib/components/checkbox-list.tsx | 67 +++ src/lib/render.tsx | 36 ++ src/lib/steps/analyze-project.ts | 313 ++++++++++ src/lib/steps/authenticate.ts | 45 ++ src/lib/steps/build-plan.ts | 160 ++++++ src/lib/steps/connect-web.ts | 122 ++++ src/lib/steps/detect-project.ts | 69 +++ src/lib/steps/install-seam-plugin.ts | 38 ++ src/lib/steps/integrate.ts | 188 ++++++ src/lib/util/env-file.ts | 67 +++ src/lib/util/run-install.ts | 35 ++ src/lib/util/seam-api.ts | 187 ++++++ 14 files changed, 2178 insertions(+) create mode 100644 scripts/smoke.tsx create mode 100644 src/lib/app.tsx create mode 100644 src/lib/components/checkbox-list.tsx create mode 100644 src/lib/render.tsx create mode 100644 src/lib/steps/analyze-project.ts create mode 100644 src/lib/steps/authenticate.ts create mode 100644 src/lib/steps/build-plan.ts create mode 100644 src/lib/steps/connect-web.ts create mode 100644 src/lib/steps/detect-project.ts create mode 100644 src/lib/steps/install-seam-plugin.ts create mode 100644 src/lib/steps/integrate.ts create mode 100644 src/lib/util/env-file.ts create mode 100644 src/lib/util/run-install.ts create mode 100644 src/lib/util/seam-api.ts diff --git a/scripts/smoke.tsx b/scripts/smoke.tsx new file mode 100644 index 0000000..942d602 --- /dev/null +++ b/scripts/smoke.tsx @@ -0,0 +1,24 @@ +// Minimal render smoke test: mount and assert the first frame renders +// without throwing. Uses a nonexistent root with no SEAM_API_KEY so no network +// call happens (findExistingApiKey returns null before any API request). +// A full interactive run can't be exercised headlessly. + +import React from "react" +import { render } from "ink-testing-library" +import { App } from "../src/app.js" + +delete process.env.SEAM_API_KEY + +const { lastFrame, unmount } = render( + +) +const frame = lastFrame() ?? "" +unmount() + +if (!frame.includes("Checking")) { + console.error(`SMOKE FAIL — unexpected first frame:\n${frame}`) + process.exit(1) +} + +console.log("SMOKE OK — initial frame rendered") +process.exit(0) diff --git a/src/lib/app.tsx b/src/lib/app.tsx new file mode 100644 index 0000000..79c8389 --- /dev/null +++ b/src/lib/app.tsx @@ -0,0 +1,827 @@ +import React, { useEffect, useRef, useState } from "react" +import { Box, Text, useApp, useStdout } from "ink" +import Spinner from "ink-spinner" +import SelectInput from "ink-select-input" +import TextInput from "ink-text-input" +import { + detectProject, + installSeamSdkCommand, + type ProjectInfo, + type Sdk, +} from "./steps/detect-project.js" +import { + findVerifiedExistingKey, + verifyAndSaveKey, +} from "./steps/authenticate.js" +import { connectViaWeb } from "./steps/connect-web.js" +import { + detectPluginTarget, + SEAM_PLUGIN_NPX_COMMAND, + CLAUDE_CODE_COMMANDS, +} from "./steps/install-seam-plugin.js" +import { runIntegration, type IntegrateEvent } from "./steps/integrate.js" +import { analyzeProject, type ProjectAnalysis } from "./steps/analyze-project.js" +import { + CORE_BLOCKS, + COMMON_BLOCKS, + composeGoal, + writeOnboardingRecord, + type BuildMode, + type OnboardingRecord, +} from "./steps/build-plan.js" +import { CheckboxList } from "./components/checkbox-list.js" +import { runInstall } from "./util/run-install.js" +import { findExistingApiKey } from "./util/env-file.js" +import { + ApiKeyError, + exchangeWizardInferenceToken, + looksLikeSeamApiKey, + SEAM_INFERENCE_BASE_URL, + type SeamWorkspace, + type WizardInferenceSession, +} from "./util/seam-api.js" + +const MAX_ATTEMPTS = 3 + +type Tone = "ok" | "info" | "warn" | "plain" +interface Msg { + tone: Tone + text: string +} + +type Phase = + | { t: "init" } + | { t: "method" } + | { t: "browser" } + | { t: "paste" } + | { t: "verify-paste"; api_key: string } + | { t: "sdk" } + | { t: "install-sdk" } + | { t: "install-plugin" } + | { t: "offer-integrate" } + | { t: "analyze" } + | { t: "integrate-mode" } + | { t: "checklist" } + | { t: "note" } + | { t: "integrate"; goal: string } + | { t: "done" } + | { t: "error"; message: string } + +export function App({ + root, + onExit, +}: { + root: string + onExit?: (lines: string[]) => void +}): React.ReactElement { + const { exit } = useApp() + const { stdout } = useStdout() + const project_ref = useRef(detectProject(root)) + const attempt_ref = useRef(0) + + const [dimensions, setDimensions] = useState({ + rows: stdout?.rows ?? 24, + columns: stdout?.columns ?? 80, + }) + + const [messages, setMessages] = useState([]) + const [phase, setPhase] = useState({ t: "init" }) + const [sdk, setSdk] = useState(null) + const [workspace, setWorkspace] = useState(null) + + const [browser, setBrowser] = useState<{ url: string | null; received: boolean }>({ + url: null, + received: false, + }) + const [paste_value, setPasteValue] = useState("") + const [paste_error, setPasteError] = useState(null) + const [install_lines, setInstallLines] = useState([]) + const [note_value, setNoteValue] = useState("") + const [agent_lines, setAgentLines] = useState([]) + const [session, setSession] = useState(null) + const [analysis, setAnalysis] = useState(null) + const [mode, setMode] = useState(null) + const [selections, setSelections] = useState([]) + + // The plan record (written to .seam/onboarding.json before the agent runs); + // the done handler rewrites it with the run result. + const plan_ref = useRef | null>(null) + + // Kept in sync with `messages` so the exit handler can hand the full + // transcript to index.tsx to reprint after leaving the alt screen. + const messages_ref = useRef([]) + const addMessage = (message: Msg): void => + setMessages((previous) => { + const next = [...previous, message] + messages_ref.current = next + return next + }) + + const finishWithNextSteps = (): void => { + pushNextSteps(addMessage, sdk, workspace?.name ?? "your workspace") + setPhase({ t: "done" }) + } + + // Compose the goal from the chosen mode + building blocks + optional note, + // persist the plan to .seam/onboarding.json, then hand off to the agent. + const startIntegration = (note_input: string): void => { + const note = note_input.trim().length > 0 ? note_input.trim() : null + const effective_mode: BuildMode = mode ?? "full_api" + const effective_selections = + effective_mode === "customer_portal" ? [] : selections + const goal = composeGoal({ + mode: effective_mode, + selections: effective_selections, + note, + framework: analysis?.signals.framework ?? null, + }) + const record: Omit = { + created_at: new Date().toISOString(), + mode: effective_mode, + selections: effective_selections, + note, + goal, + analysis: { + sdk: analysis?.signals.sdk ?? null, + framework: analysis?.signals.framework ?? null, + app_type_guess: analysis?.recommendation.app_type_guess ?? null, + seam_already_setup: analysis?.signals.seam_already_setup ?? false, + used_onboarding: analysis?.used_onboarding ?? false, + recommendation_source: analysis?.recommendation.source ?? "heuristic", + }, + } + plan_ref.current = record + try { + writeOnboardingRecord(root, record) + addMessage({ tone: "info", text: "Saved your plan to .seam/onboarding.json" }) + } catch { + // Writing the record is best-effort; proceed with the integration. + } + setPhase({ t: "integrate", goal }) + } + + const handleIntegrateEvent = (event: IntegrateEvent): void => { + if (event.kind === "text") { + setAgentLines((previous) => [...previous.slice(-5), truncate(event.text, 100)]) + } else if (event.kind === "tool") { + setAgentLines((previous) => [ + ...previous.slice(-5), + formatTool(event.name, event.detail), + ]) + } else { + setAgentLines([]) + if (plan_ref.current != null) { + try { + writeOnboardingRecord(root, { + ...plan_ref.current, + result: { + ok: event.ok, + files_summary: event.summary.trim().slice(0, 4000), + cost_usd: event.cost_usd, + }, + }) + } catch { + // Recording the result is best-effort; never block finishing. + } + } + addMessage( + event.ok + ? { tone: "ok", text: "Integration written — review it with `git diff`" } + : { tone: "warn", text: "Agent stopped early — review what it changed with `git diff`" } + ) + for (const line of event.summary.trim().split("\n").slice(0, 15)) { + if (line.trim().length > 0) addMessage({ tone: "plain", text: ` ${line}` }) + } + if (event.cost_usd != null) { + addMessage({ tone: "info", text: `Model cost: $${event.cost_usd.toFixed(2)}` }) + } + } + } + + // Once a workspace is known, choose SDK (or skip if detected) then install. + const advanceAfterAuth = (): void => { + const detected = project_ref.current.detected_sdk + if (detected != null) { + setSdk(detected) + addMessage({ tone: "info", text: `Detected ${detected} project` }) + setPhase({ t: "install-sdk" }) + } else { + setPhase({ t: "sdk" }) + } + } + + // init: reuse an existing key if valid, else ask how to connect. + useEffect(() => { + if (phase.t !== "init") return + let cancelled = false + void (async () => { + const existing = await findVerifiedExistingKey(root) + if (cancelled) return + if (existing != null) { + setWorkspace(existing.workspace) + addMessage({ + tone: "ok", + text: `Using existing key from ${existing.source} · workspace ${existing.workspace.name}`, + }) + advanceAfterAuth() + } else { + setPhase({ t: "method" }) + } + })() + return () => { + cancelled = true + } + }, [phase.t]) + + // browser handoff + useEffect(() => { + if (phase.t !== "browser") return + let cancelled = false + void (async () => { + try { + const result = await connectViaWeb(root, { + onUrl: (url) => !cancelled && setBrowser((b) => ({ ...b, url })), + onReceived: () => !cancelled && setBrowser((b) => ({ ...b, received: true })), + }) + if (cancelled) return + setWorkspace(result.workspace) + addMessage({ tone: "ok", text: `Connected · workspace ${result.workspace.name}` }) + advanceAfterAuth() + } catch (error) { + if (!cancelled) { + setPhase({ + t: "error", + message: error instanceof Error ? error.message : "Browser connection failed.", + }) + } + } + })() + return () => { + cancelled = true + } + }, [phase.t]) + + // verify a pasted key + useEffect(() => { + if (phase.t !== "verify-paste") return + const { api_key } = phase + let cancelled = false + void (async () => { + try { + const result = await verifyAndSaveKey(root, api_key) + if (cancelled) return + setWorkspace(result.workspace) + addMessage({ tone: "ok", text: `Workspace: ${result.workspace.name}` }) + advanceAfterAuth() + } catch (error) { + if (cancelled) return + const message = + error instanceof ApiKeyError ? error.message : "Couldn't verify the key." + if (attempt_ref.current >= MAX_ATTEMPTS) { + setPhase({ t: "error", message: "Too many attempts. Re-run with a valid key." }) + } else { + setPasteError(message) + setPasteValue("") + setPhase({ t: "paste" }) + } + } + })() + return () => { + cancelled = true + } + }, [phase.t]) + + // install the Seam SDK (streamed), then install the plugin + useEffect(() => { + if (phase.t !== "install-sdk" || sdk == null) return + let cancelled = false + const command = installSeamSdkCommand(sdk, project_ref.current) + void (async () => { + try { + await runInstall(command, root, (line) => { + if (!cancelled) setInstallLines((previous) => [...previous.slice(-3), line]) + }) + if (!cancelled) addMessage({ tone: "ok", text: "Seam SDK installed" }) + } catch { + if (!cancelled) { + addMessage({ + tone: "warn", + text: `Couldn't finish installing — run it yourself: ${command.join(" ")}`, + }) + } + } + if (!cancelled) { + setInstallLines([]) + setPhase({ t: "install-plugin" }) + } + })() + return () => { + cancelled = true + } + }, [phase.t, sdk]) + + // install the official Seam plugin skills, then finish. We always run the + // universal installer (works everywhere); for Claude Code we additionally + // point at the native /plugin path, which also wires up the seam-docs MCP. + useEffect(() => { + if (phase.t !== "install-plugin") return + const workspace_name = workspace?.name ?? "your workspace" + const target = detectPluginTarget(root) + + let cancelled = false + void (async () => { + try { + await runInstall(SEAM_PLUGIN_NPX_COMMAND, root, (line) => { + if (!cancelled) setInstallLines((previous) => [...previous.slice(-3), line]) + }) + if (!cancelled) addMessage({ tone: "ok", text: "Installed the Seam plugin skills" }) + } catch { + if (!cancelled) { + addMessage({ + tone: "warn", + text: `Couldn't install the plugin — run it yourself: ${SEAM_PLUGIN_NPX_COMMAND.join(" ")}`, + }) + } + } + if (!cancelled) { + if (target === "claude-code") { + addMessage({ + tone: "info", + text: "Claude Code: for the native plugin + seam-docs MCP, you can also run:", + }) + for (const command of CLAUDE_CODE_COMMANDS) { + addMessage({ tone: "plain", text: ` ${command}` }) + } + } + setPhase({ t: "offer-integrate" }) + } + })() + return () => { + cancelled = true + } + }, [phase.t]) + + // analyze: exchange the API key for a scoped wizard token (which also returns + // the Console onboarding answers), scan the project, and get a mode/checklist + // recommendation to pre-fill the next steps. The token is reused by the + // integration agent below, so it is minted once. + useEffect(() => { + if (phase.t !== "analyze") return + let cancelled = false + void (async () => { + const found = findExistingApiKey(root) + if (found == null) { + addMessage({ + tone: "warn", + text: "Couldn't find your Seam API key to plan the integration.", + }) + if (!cancelled) finishWithNextSteps() + return + } + + let current_session: WizardInferenceSession + try { + current_session = await exchangeWizardInferenceToken(found.api_key) + } catch (error) { + if (!cancelled) { + addMessage({ + tone: "warn", + text: `Couldn't start the AI session: ${ + error instanceof Error ? error.message : "unknown error" + }`, + }) + finishWithNextSteps() + } + return + } + if (cancelled) return + setSession(current_session) + + const result = await analyzeProject({ + root, + project: project_ref.current, + onboarding: current_session.onboarding, + inference: { + base_url: SEAM_INFERENCE_BASE_URL, + token: current_session.token, + }, + }) + if (cancelled) return + setAnalysis(result) + setMode(result.recommendation.mode) + setSelections(result.recommendation.selections) + + const detail = [ + result.signals.framework ?? result.signals.sdk, + result.recommendation.app_type_guess, + ] + .filter((value): value is string => value != null && value.length > 0) + .join(" · ") + addMessage({ + tone: "info", + text: `Analyzed your project${detail.length > 0 ? `: ${detail}` : ""}`, + }) + if (current_session.onboarding != null) { + addMessage({ tone: "plain", text: " Used your Console onboarding answers" }) + } + setPhase({ t: "integrate-mode" }) + })() + return () => { + cancelled = true + } + }, [phase.t]) + + // run the embedded integration agent (Claude Agent SDK), routed through + // Seam-hosted inference using the token minted during the analyze step — no + // developer Anthropic key. + useEffect(() => { + if (phase.t !== "integrate") return + const { goal } = phase + const controller = new AbortController() + void (async () => { + if (session == null) { + addMessage({ + tone: "warn", + text: "Lost the AI session — re-run the wizard to try again.", + }) + if (!controller.signal.aborted) finishWithNextSteps() + return + } + addMessage({ tone: "info", text: "Starting the Seam integration agent…" }) + try { + await runIntegration({ + root, + sdk: sdk ?? "javascript", + workspace_name: workspace?.name ?? "your workspace", + goal, + inference: { + base_url: SEAM_INFERENCE_BASE_URL, + token: session.token, + }, + framework: analysis?.signals.framework ?? null, + mode: mode ?? "full_api", + signal: controller.signal, + onEvent: (event) => { + if (!controller.signal.aborted) handleIntegrateEvent(event) + }, + }) + } catch (error) { + if (!controller.signal.aborted) { + addMessage({ + tone: "warn", + text: `Couldn't run the integration agent: ${ + error instanceof Error ? error.message : "unknown error" + }`, + }) + } + } + if (!controller.signal.aborted) finishWithNextSteps() + })() + return () => controller.abort() + }, [phase.t]) + + // keep the full-screen layout sized to the terminal + useEffect(() => { + if (stdout == null) return + const onResize = (): void => + setDimensions({ rows: stdout.rows ?? 24, columns: stdout.columns ?? 80 }) + stdout.on("resize", onResize) + return () => { + stdout.off("resize", onResize) + } + }, [stdout]) + + // exit when finished + useEffect(() => { + if (phase.t !== "done" && phase.t !== "error") return + if (phase.t === "error") { + addMessage({ tone: "warn", text: phase.message }) + process.exitCode = 1 + } + // Hand the transcript back so index.tsx can reprint it once the alt screen + // is torn down (its contents are otherwise discarded). Small delay lets the + // final addMessage above flush into messages_ref. + const id = setTimeout(() => { + onExit?.(messages_ref.current.map(formatMessageLine)) + exit() + }, 40) + return () => clearTimeout(id) + }, [phase.t]) + + // Full-screen: a header, the transcript (bounded to what fits), then the + // active step. The outer box is sized to the terminal so it fills the alt + // screen; older transcript lines scroll off the top and are reprinted on exit. + const transcript_capacity = Math.max(1, dimensions.rows - 8) + const visible_messages = messages.slice(-transcript_capacity) + + return ( + +
+ + {visible_messages.map((message, index) => ( + + ))} + + {renderActive()} + + ) + + function renderActive(): React.ReactElement | null { + switch (phase.t) { + case "init": + return + case "method": + return ( + + { + if (item.value === "paste") { + attempt_ref.current = 0 + setPhase({ t: "paste" }) + } else { + setPhase({ t: "browser" }) + } + }} + /> + + ) + case "browser": + return ( + + + {browser.url != null && {browser.url}} + + ) + case "paste": + return ( + + + {"› "} + { + if (!looksLikeSeamApiKey(value)) { + setPasteError("That doesn't look like a Seam key (expected seam_…).") + return + } + setPasteError(null) + attempt_ref.current += 1 + setPhase({ t: "verify-paste", api_key: value }) + }} + /> + + {paste_error != null && {paste_error}} + + ) + case "verify-paste": + return + case "sdk": + return ( + + { + const chosen: Sdk = item.value === "python" ? "python" : "javascript" + setSdk(chosen) + addMessage({ tone: "info", text: `SDK: ${chosen}` }) + setPhase({ t: "install-sdk" }) + }} + /> + + ) + case "install-sdk": + return ( + + + {install_lines.map((line, index) => ( + {line} + ))} + + ) + case "install-plugin": + return ( + + + {install_lines.map((line, index) => ( + {line} + ))} + + ) + case "offer-integrate": + return ( + + { + if (item.value === "yes") setPhase({ t: "analyze" }) + else finishWithNextSteps() + }} + /> + + ) + case "analyze": + return + case "integrate-mode": { + const recommended: BuildMode = mode ?? "full_api" + const portal_item = { + label: "Customer Portal — Seam hosts the UI (you call ~2 endpoints)", + value: "customer_portal", + } + const api_item = { + label: "Full API control — you build the UI, wire up the API", + value: "full_api", + } + const items = + recommended === "customer_portal" + ? [portal_item, api_item] + : [api_item, portal_item] + return ( + + {analysis?.recommendation.rationale != null && + analysis.recommendation.rationale.length > 0 && ( + {` ${analysis.recommendation.rationale}`} + )} + { + const chosen: BuildMode = + item.value === "customer_portal" ? "customer_portal" : "full_api" + setMode(chosen) + addMessage({ + tone: "info", + text: `Mode: ${ + chosen === "customer_portal" ? "Customer Portal" : "Full API" + }`, + }) + setPhase(chosen === "customer_portal" ? { t: "note" } : { t: "checklist" }) + }} + /> + + ) + } + case "checklist": + return ( + + ({ + id: block.id, + label: block.label, + group: block.group, + }))} + initial_selected={selections} + onSubmit={(chosen) => { + setSelections(chosen) + setPhase({ t: "note" }) + }} + /> + + ) + case "note": + return ( + + + {"› "} + startIntegration(value)} + /> + + + ) + case "integrate": + return ( + + + {agent_lines.map((line, index) => ( + {line} + ))} + + ) + case "done": + case "error": + return null + } + } +} + +function pushNextSteps( + addMessage: (message: Msg) => void, + sdk: Sdk | null, + workspace_name: string +): void { + const env_hint = + sdk === "python" + ? "Make sure SEAM_API_KEY is exported (it's in .env)." + : "Add .env to .gitignore — it holds your API key." + addMessage({ tone: "plain", text: "" }) + addMessage({ tone: "ok", text: `You're set up in ${workspace_name}` }) + addMessage({ tone: "plain", text: "Next steps:" }) + addMessage({ + tone: "plain", + text: ' 1. Describe your integration to your AI assistant — e.g. "add Seam access grants". The Seam skill will guide it.', + }) + addMessage({ tone: "plain", text: ` 2. ${env_hint}` }) + addMessage({ tone: "plain", text: " 3. Docs: https://docs.seam.co" }) +} + +function truncate(text: string, max_length: number): string { + const collapsed = text.replace(/\s+/g, " ").trim() + return collapsed.length > max_length + ? `${collapsed.slice(0, max_length - 1)}…` + : collapsed +} + +// A compact one-line label for a tool the agent just invoked. +function formatTool(name: string, detail: string): string { + const verb = + name === "Write" + ? "write" + : name === "Edit" + ? "edit" + : name === "Read" + ? "read" + : name === "Glob" || name === "Grep" + ? "search" + : name === "WebFetch" + ? "fetch" + : name.startsWith("mcp__seam-docs__") + ? "docs" + : name + return detail.length > 0 ? `${verb} ${truncate(detail, 60)}` : verb +} + +function Header(): React.ReactElement { + return ( + + + {" Seam setup wizard "} + + + ) +} + +// Plain-text rendering of a message, matching MessageLine's symbols — used to +// reprint the transcript into the normal terminal after the alt screen closes. +function formatMessageLine(message: Msg): string { + if (message.tone === "plain") return message.text + const symbol = + message.tone === "ok" ? "✔" : message.tone === "warn" ? "▲" : "•" + return `${symbol} ${message.text}` +} + +function MessageLine({ message }: { message: Msg }): React.ReactElement { + if (message.tone === "plain") return {message.text} + const symbol = message.tone === "ok" ? "✔" : message.tone === "warn" ? "▲" : "•" + const color = message.tone === "ok" ? "green" : message.tone === "warn" ? "yellow" : "cyan" + return ( + + {symbol} {message.text} + + ) +} + +function Pending({ label }: { label: string }): React.ReactElement { + return ( + + + + {" "} + {label} + + ) +} + +function Prompt({ + title, + children, +}: { + title: string + children: React.ReactNode +}): React.ReactElement { + return ( + + {title} + {children} + + ) +} diff --git a/src/lib/components/checkbox-list.tsx b/src/lib/components/checkbox-list.tsx new file mode 100644 index 0000000..238a68f --- /dev/null +++ b/src/lib/components/checkbox-list.tsx @@ -0,0 +1,67 @@ +import React, { useState } from "react" +import { Box, Text, useInput } from "ink" + +export interface CheckboxItem { + id: string + label: string + group?: string +} + +// A minimal multi-select: ↑/↓ to move, space to toggle, enter to confirm. Ink +// ships single-select (ink-select-input) but no checkbox list, so we hand-roll +// one over useInput. Only mounted during the checklist phase. +export function CheckboxList({ + items, + initial_selected, + onSubmit, +}: { + items: CheckboxItem[] + initial_selected: string[] + onSubmit: (selected: string[]) => void +}): React.ReactElement { + const [cursor, setCursor] = useState(0) + const [selected, setSelected] = useState>( + () => new Set(initial_selected) + ) + + useInput((input, key) => { + if (key.upArrow || input === "k") { + setCursor((current) => (current - 1 + items.length) % items.length) + } else if (key.downArrow || input === "j") { + setCursor((current) => (current + 1) % items.length) + } else if (input === " ") { + const id = items[cursor]?.id + if (id == null) return + setSelected((previous) => { + const next = new Set(previous) + if (next.has(id)) next.delete(id) + else next.add(id) + return next + }) + } else if (key.return) { + onSubmit(items.filter((item) => selected.has(item.id)).map((item) => item.id)) + } + }) + + return ( + + {items.map((item, index) => { + const is_cursor = index === cursor + const is_checked = selected.has(item.id) + const show_group = + item.group != null && item.group !== items[index - 1]?.group + return ( + + {show_group && {` ${item.group ?? ""}`}} + + {is_cursor ? "❯ " : " "} + {is_checked ? "◉ " : "◯ "} + {item.label} + + + ) + })} + {" ↑/↓ move · space toggle · enter confirm"} + + ) +} diff --git a/src/lib/render.tsx b/src/lib/render.tsx new file mode 100644 index 0000000..49a27ea --- /dev/null +++ b/src/lib/render.tsx @@ -0,0 +1,36 @@ +#!/usr/bin/env node +import React from "react" +import { render } from "ink" +import { App } from "./app.js" + +const ESC = String.fromCharCode(27) +// Enter alt screen + clear + home / leave alt screen (restores normal buffer). +const ENTER_ALT_SCREEN = `${ESC}[?1049h${ESC}[2J${ESC}[H` +const LEAVE_ALT_SCREEN = `${ESC}[?1049l` + +const is_tty = Boolean(process.stdout.isTTY) + +// Run full-screen in the alternate screen buffer (like less/vim). Enter before +// the first render to avoid a flash of the initial frame in the normal buffer; +// leave on exit and reprint the transcript, since the alternate buffer is +// discarded when we leave it. +if (is_tty) process.stdout.write(ENTER_ALT_SCREEN) + +let transcript: string[] = [] + +const app = render( + (transcript = lines)} /> +) + +app + .waitUntilExit() + .then(() => { + if (is_tty) process.stdout.write(LEAVE_ALT_SCREEN) + if (transcript.length > 0) { + process.stdout.write(`\n${transcript.join("\n")}\n`) + } + }) + .catch(() => { + if (is_tty) process.stdout.write(LEAVE_ALT_SCREEN) + process.exitCode = 1 + }) diff --git a/src/lib/steps/analyze-project.ts b/src/lib/steps/analyze-project.ts new file mode 100644 index 0000000..51e5855 --- /dev/null +++ b/src/lib/steps/analyze-project.ts @@ -0,0 +1,313 @@ +import { existsSync, readFileSync } from "node:fs" +import { join } from "node:path" +import type { ProjectInfo, Sdk } from "./detect-project.js" +import { ALL_BLOCKS, type BuildMode } from "./build-plan.js" +import { findExistingApiKey } from "../util/env-file.js" +import { + callInferenceForText, + type WizardOnboarding, +} from "../util/seam-api.js" + +// A cheap classification model — the deep work happens in the integration +// agent, so the pre-analysis only needs a good recommendation, not deep +// reasoning. +const RECOMMENDATION_MODEL = "claude-haiku-4-5" + +export interface ProjectSignals { + sdk: Sdk | null + framework: string | null + package_name: string | null + description: string | null + keywords: string[] + dependency_names: string[] + readme_excerpt: string | null + seam_already_setup: boolean +} + +export interface Recommendation { + mode: BuildMode + selections: string[] + app_type_guess: string | null + rationale: string + source: "llm" | "heuristic" +} + +export interface ProjectAnalysis { + signals: ProjectSignals + recommendation: Recommendation + used_onboarding: boolean +} + +const VALID_BLOCK_IDS = new Set(ALL_BLOCKS.map((block) => block.id)) + +// Gather what the project reveals about itself, then recommend a mode + build +// blocks by combining those signals with the Console onboarding answers. The +// recommendation comes from a short LLM call, falling back to a deterministic +// heuristic if that call fails or returns something unusable. +export async function analyzeProject(args: { + root: string + project: ProjectInfo + onboarding: WizardOnboarding | null + inference: { base_url: string; token: string } +}): Promise { + const { root, project, onboarding, inference } = args + const signals = gatherProjectSignals(root, project) + + let recommendation: Recommendation + try { + recommendation = await recommendViaLlm(signals, onboarding, inference) + } catch { + recommendation = heuristicRecommendation(signals, onboarding) + } + + return { + signals, + recommendation, + used_onboarding: onboarding != null, + } +} + +function gatherProjectSignals( + root: string, + project: ProjectInfo +): ProjectSignals { + const package_json = readJsonIfExists(join(root, "package.json")) as { + name?: string + description?: string + keywords?: string[] + dependencies?: Record + devDependencies?: Record + } | null + + const dependency_names = [ + ...Object.keys(package_json?.dependencies ?? {}), + ...Object.keys(package_json?.devDependencies ?? {}), + ] + + return { + sdk: project.detected_sdk, + framework: detectFramework(root, project.detected_sdk, dependency_names), + package_name: package_json?.name ?? null, + description: package_json?.description ?? null, + keywords: package_json?.keywords ?? [], + dependency_names: dependency_names.slice(0, 40), + readme_excerpt: readReadmeExcerpt(root), + seam_already_setup: + dependency_names.includes("seam") || findExistingApiKey(root) != null, + } +} + +function detectFramework( + root: string, + sdk: Sdk | null, + dependency_names: string[] +): string | null { + const has = (name: string): boolean => dependency_names.includes(name) + + if (sdk === "javascript") { + if (has("next")) return "Next.js" + if (has("@remix-run/react") || has("@remix-run/node")) return "Remix" + if (has("nuxt")) return "Nuxt" + if (has("@nestjs/core")) return "NestJS" + if (has("fastify")) return "Fastify" + if (has("express")) return "Express" + if (has("react")) return "React" + return null + } + + if (sdk === "python") { + if (existsSync(join(root, "manage.py")) || hasPythonDep(root, "django")) { + return "Django" + } + if (hasPythonDep(root, "fastapi")) return "FastAPI" + if (hasPythonDep(root, "flask")) return "Flask" + return null + } + + return null +} + +function hasPythonDep(root: string, name: string): boolean { + for (const marker of ["requirements.txt", "pyproject.toml", "Pipfile"]) { + const path = join(root, marker) + if (!existsSync(path)) continue + try { + if (readFileSync(path, "utf8").toLowerCase().includes(name)) return true + } catch { + // Unreadable dependency file — treat as absent. + } + } + return false +} + +function readReadmeExcerpt(root: string): string | null { + for (const name of ["README.md", "README.MD", "readme.md", "README"]) { + const path = join(root, name) + if (!existsSync(path)) continue + try { + return readFileSync(path, "utf8").slice(0, 1200) + } catch { + return null + } + } + return null +} + +function readJsonIfExists(path: string): unknown { + if (!existsSync(path)) return null + try { + return JSON.parse(readFileSync(path, "utf8")) + } catch { + return null + } +} + +async function recommendViaLlm( + signals: ProjectSignals, + onboarding: WizardOnboarding | null, + inference: { base_url: string; token: string } +): Promise { + const block_menu = ALL_BLOCKS.map( + (block) => ` "${block.id}" — ${block.label}` + ).join("\n") + + const system = + "You help developers integrate Seam, an API for smart locks, access " + + "control, and physical access. Given signals about a project, recommend " + + "how to integrate Seam. Reply with ONLY a JSON object, no prose." + + const user = [ + "Project signals:", + `- SDK: ${signals.sdk ?? "unknown"}`, + `- Framework: ${signals.framework ?? "unknown"}`, + `- Package: ${signals.package_name ?? "unknown"} — ${signals.description ?? ""}`, + `- Keywords: ${signals.keywords.join(", ") || "none"}`, + `- Dependencies: ${signals.dependency_names.join(", ") || "none"}`, + `- Seam already set up: ${signals.seam_already_setup}`, + `- README excerpt: ${signals.readme_excerpt ?? "none"}`, + "", + "Console onboarding answers (any may be null):", + `- use_case: ${onboarding?.use_case ?? "null"}`, + `- primary_goal: ${onboarding?.primary_goal ?? "null"}`, + `- build_target: ${onboarding?.build_target ?? "null"}`, + `- embed_customer_portal: ${onboarding?.embed_customer_portal ?? "null"}`, + `- device_categories: ${onboarding?.device_categories?.join(", ") ?? "null"}`, + "", + "Decide:", + '1) mode: "customer_portal" (Seam hosts the UI; the app calls ~2 endpoints)', + ' or "full_api" (control everything via the API, build your own UI).', + " If embed_customer_portal is true, strongly prefer customer_portal.", + '2) selections: building-block ids to scaffold — only for "full_api" (use', + " [] for customer_portal). Always include \"access_grants\" for full_api.", + " Valid ids:", + block_menu, + '3) app_type_guess: short phrase (e.g. "vacation rental", "coworking",', + ' "property management", "unknown").', + "4) rationale: one short sentence.", + "", + 'Return exactly: {"mode":"...","selections":["..."],"app_type_guess":"...","rationale":"..."}', + ].join("\n") + + const text = await callInferenceForText(inference, { + model: RECOMMENDATION_MODEL, + max_tokens: 400, + system, + user, + }) + + const parsed = parseRecommendationJson(text) + if (parsed == null) return heuristicRecommendation(signals, onboarding) + + const mode: BuildMode = + parsed.mode === "customer_portal" ? "customer_portal" : "full_api" + const selections = + mode === "customer_portal" + ? [] + : normalizeSelections(parsed.selections ?? []) + + return { + mode, + selections, + app_type_guess: parsed.app_type_guess ?? null, + rationale: parsed.rationale ?? "", + source: "llm", + } +} + +function parseRecommendationJson(text: string): { + mode?: string + selections?: unknown + app_type_guess?: string + rationale?: string +} | null { + const match = /\{[\S\s]*\}/.exec(text) + if (match == null) return null + try { + return JSON.parse(match[0]) + } catch { + return null + } +} + +// Keep only known ids, always guarantee access_grants for the full-API path. +function normalizeSelections(raw: unknown): string[] { + const ids = Array.isArray(raw) + ? raw.filter( + (id): id is string => typeof id === "string" && VALID_BLOCK_IDS.has(id) + ) + : [] + const unique = [...new Set(ids)] + if (!unique.includes("access_grants")) unique.unshift("access_grants") + return unique +} + +function heuristicRecommendation( + signals: ProjectSignals, + onboarding: WizardOnboarding | null +): Recommendation { + if (onboarding?.embed_customer_portal === true) { + return { + mode: "customer_portal", + selections: [], + app_type_guess: onboarding.use_case ?? onboarding.org_type ?? null, + rationale: "You chose to embed the Customer Portal during onboarding.", + source: "heuristic", + } + } + + const haystack = [ + onboarding?.use_case, + onboarding?.primary_goal, + onboarding?.org_type, + signals.description, + signals.package_name, + ...signals.keywords, + ] + .filter((value): value is string => value != null) + .join(" ") + .toLowerCase() + + const mentions = (...terms: string[]): boolean => + terms.some((term) => haystack.includes(term)) + + const selections = new Set(["access_grants", "connect_device"]) + let app_type_guess: string | null = null + + if ( + mentions("hotel", "booking", "reservation", "vacation", "rental", "pms", "guest", "hospitality") + ) { + selections.add("reservations") + app_type_guess = "hospitality / short-term rental" + } else if (mentions("coworking", "tenant", "member", "property", "apartment", "resident")) { + selections.add("user_identities") + app_type_guess = "property / coworking" + } + + return { + mode: "full_api", + selections: [...selections], + app_type_guess, + rationale: "Recommended from your project and onboarding answers.", + source: "heuristic", + } +} diff --git a/src/lib/steps/authenticate.ts b/src/lib/steps/authenticate.ts new file mode 100644 index 0000000..e18f1af --- /dev/null +++ b/src/lib/steps/authenticate.ts @@ -0,0 +1,45 @@ +import { join } from "node:path" +import { + getWorkspaceForApiKey, + type SeamWorkspace, +} from "../util/seam-api.js" +import { upsertEnvVar, findExistingApiKey } from "../util/env-file.js" + +export interface AuthResult { + workspace: SeamWorkspace +} + +export interface ExistingKeyResult { + workspace: SeamWorkspace + source: string +} + +// Pure auth logic (no UI). The Ink app drives the prompts and renders progress. +// The wizard never handles credentials — keys are created in the browser or +// pasted, never minted from a password here. + +// Return the workspace for an already-present SEAM_API_KEY (env or .env*), or +// null when there is none / it doesn't verify. +export async function findVerifiedExistingKey( + root: string +): Promise { + const existing = findExistingApiKey(root) + if (existing == null) return null + try { + const workspace = await getWorkspaceForApiKey(existing.api_key) + return { workspace, source: existing.source } + } catch { + return null + } +} + +// Verify a pasted key and save it to .env. Throws (ApiKeyError) if invalid. +export async function verifyAndSaveKey( + root: string, + api_key: string +): Promise { + const trimmed = api_key.trim() + const workspace = await getWorkspaceForApiKey(trimmed) + upsertEnvVar(join(root, ".env"), "SEAM_API_KEY", trimmed) + return { workspace } +} diff --git a/src/lib/steps/build-plan.ts b/src/lib/steps/build-plan.ts new file mode 100644 index 0000000..ef7c7bf --- /dev/null +++ b/src/lib/steps/build-plan.ts @@ -0,0 +1,160 @@ +import { mkdirSync, writeFileSync } from "node:fs" +import { join } from "node:path" + +// Top-level fork: let Seam host the UI (the app calls ~2 endpoints), or drive +// everything through the API and build the UI yourself. +export type BuildMode = "customer_portal" | "full_api" + +export interface BuildBlock { + id: string + group: "Core" | "Common" + label: string + // Appended (as an instruction line) to the agent goal when selected. + agent_hint: string +} + +// Core building blocks — the foundation almost every integration needs. +// `access_grants` is Seam's recommended way to grant a person access, so it is +// the default-checked item. +export const CORE_BLOCKS: BuildBlock[] = [ + { + id: "connect_device", + group: "Core", + label: "Connect a device (Connect Webview + connected accounts)", + agent_hint: + "Set up a Connect Webview so an end user can connect their account and devices, then list the connected devices.", + }, + { + id: "access_grants", + group: "Core", + label: "Access Grants — grant a person access", + agent_hint: + "Create an Access Grant to give a person access to a space or device (Seam's recommended API for granting access).", + }, + { + id: "user_identities", + group: "Core", + label: "User Identities (the people who receive access)", + agent_hint: + "Create and manage User Identities representing the people who receive access.", + }, +] + +// Common building blocks — frequent next steps beyond the core. +export const COMMON_BLOCKS: BuildBlock[] = [ + { + id: "access_codes", + group: "Common", + label: "Access codes (PIN codes on locks)", + agent_hint: "Program PIN access codes on smart locks.", + }, + { + id: "reservations", + group: "Common", + label: "Reservations → access (PMS / booking flow)", + agent_hint: + "Wire a booking/reservation flow so each reservation automatically provisions and revokes access.", + }, + { + id: "mobile_keys", + group: "Common", + label: "Mobile keys / credentials", + agent_hint: "Issue mobile-key credentials to users.", + }, + { + id: "webhooks", + group: "Common", + label: "Webhooks & events", + agent_hint: + "Subscribe to Seam webhooks and handle the events (e.g. access granted, device connected).", + }, +] + +export const ALL_BLOCKS: BuildBlock[] = [...CORE_BLOCKS, ...COMMON_BLOCKS] + +export function blockById(id: string): BuildBlock | undefined { + return ALL_BLOCKS.find((block) => block.id === id) +} + +// Turn the chosen mode + building blocks + free-text note into the goal string +// that drives the embedded integration agent. +export function composeGoal(args: { + mode: BuildMode + selections: string[] + note: string | null + framework: string | null +}): string { + const { mode, selections, note, framework } = args + const target = framework ?? "the project" + const note_suffix = + note != null && note.trim().length > 0 + ? ` Additional context from the developer: ${note.trim()}` + : "" + + if (mode === "customer_portal") { + return ( + "Integrate Seam using the Customer Portal so the UI is Seam-hosted and " + + "this app only calls a couple of endpoints. Create a Customer Portal for " + + "a customer (it returns a hosted URL), embed it in the app (iframe or a " + + "magic link), and regenerate the portal per visit since the session is " + + `short-lived. Wire it into ${target}'s conventions, load SEAM_API_KEY from ` + + "the existing .env, and add a short runnable example." + + note_suffix + ) + } + + const hints = selections + .map((id) => blockById(id)?.agent_hint) + .filter((hint): hint is string => hint != null) + .map((hint) => `- ${hint}`) + .join("\n") + + return ( + "Set up a Seam integration that controls everything through the Seam API. " + + `Implement the following, wired into ${target}'s conventions:\n${hints}\n` + + "Load SEAM_API_KEY from the existing .env, keep changes minimal and " + + "idiomatic, and add a short runnable example." + + note_suffix + ) +} + +// Schema version for the on-disk record; bump on any breaking shape change. +const RECORD_SCHEMA_VERSION = 1 + +export interface OnboardingRecord { + schema_version: number + created_at: string + mode: BuildMode + selections: string[] + note: string | null + goal: string + analysis: { + sdk: string | null + framework: string | null + app_type_guess: string | null + seam_already_setup: boolean + used_onboarding: boolean + recommendation_source: "llm" | "heuristic" + } + result?: { + ok: boolean + files_summary: string + cost_usd: number | null + } +} + +// Write the wizard's run record to /.seam/onboarding.json. Holds no +// secrets (the API key stays in .env), so it is safe to commit as a record of +// what was set up, and the embedded agent / a later editor agent can read it. +export function writeOnboardingRecord( + root: string, + record: Omit +): void { + const dir = join(root, ".seam") + mkdirSync(dir, { recursive: true }) + const full: OnboardingRecord = { + schema_version: RECORD_SCHEMA_VERSION, + ...record, + } + writeFileSync(join(dir, "onboarding.json"), `${JSON.stringify(full, null, 2)}\n`) +} diff --git a/src/lib/steps/connect-web.ts b/src/lib/steps/connect-web.ts new file mode 100644 index 0000000..59e3be5 --- /dev/null +++ b/src/lib/steps/connect-web.ts @@ -0,0 +1,122 @@ +import { createServer, type ServerResponse } from "node:http" +import { randomBytes } from "node:crypto" +import { join } from "node:path" +import open from "open" +import { getWorkspaceForApiKey, type SeamWorkspace } from "../util/seam-api.js" +import { upsertEnvVar } from "../util/env-file.js" + +// The dashboard "wizard" page mints a key and posts it back to the local +// callback. Override the console host with SEAM_CONSOLE_URL for dev. +const CONSOLE_URL = process.env.SEAM_CONSOLE_URL ?? "https://console.seam.co" +const CONSOLE_WIZARD_PATH = "/dashboard/wizard" +const CALLBACK_TIMEOUT_MS = 5 * 60 * 1000 + +export interface WebConnectResult { + workspace: SeamWorkspace +} + +// Progress callbacks so the Ink UI can render the handoff without any logging +// living in this module. +export interface WebConnectEvents { + onUrl?: (url: string) => void + onWaiting?: () => void + onReceived?: () => void +} + +interface CallbackPayload { + state: string + api_key: string +} + +// Browser → CLI handoff: start a localhost callback, open the dashboard wizard +// with the port + a random state, and wait for the page to post the freshly +// created key back. Throws on error/timeout; resolves with the workspace. +// +// Wire protocol (must match the dashboard page): the page is opened at +// {CONSOLE_URL}/dashboard/wizard?cli_connect=1&cli_port=&cli_state= +// and POSTs JSON { state, api_key } to http://127.0.0.1:/ (CORS *). +export async function connectViaWeb( + root: string, + events: WebConnectEvents = {} +): Promise { + const state = randomBytes(16).toString("hex") + + const payload = await new Promise((resolve, reject) => { + const server = createServer((request, response) => { + response.setHeader("Access-Control-Allow-Origin", "*") + response.setHeader("Access-Control-Allow-Methods", "POST, OPTIONS") + response.setHeader("Access-Control-Allow-Headers", "content-type") + + if (request.method === "OPTIONS") { + response.writeHead(204) + response.end() + return + } + if (request.method !== "POST") { + response.writeHead(405) + response.end() + return + } + + let body = "" + request.on("data", (chunk) => { + body += chunk + }) + request.on("end", () => { + let parsed: Partial = {} + try { + parsed = JSON.parse(body) as Partial + } catch { + respondJson(response, 400, { ok: false, error: "invalid_json" }) + return + } + if (parsed.state !== state) { + respondJson(response, 403, { ok: false, error: "state_mismatch" }) + return + } + if (parsed.api_key == null || parsed.api_key.length === 0) { + respondJson(response, 400, { ok: false, error: "missing_api_key" }) + return + } + respondJson(response, 200, { ok: true }) + events.onReceived?.() + server.close() + resolve({ state, api_key: parsed.api_key }) + }) + }) + + server.on("error", reject) + server.listen(0, "127.0.0.1", () => { + const address = server.address() + if (address == null || typeof address === "string") { + reject(new Error("Could not start the local callback server.")) + return + } + const url = `${CONSOLE_URL}${CONSOLE_WIZARD_PATH}?cli_connect=1&cli_port=${address.port}&cli_state=${state}` + events.onUrl?.(url) + void open(url).catch(() => { + // Browser may not open (headless/SSH) — the UI shows the URL to visit. + }) + events.onWaiting?.() + }) + + const timeout = setTimeout(() => { + server.close() + reject(new Error("Timed out waiting for the browser.")) + }, CALLBACK_TIMEOUT_MS) + timeout.unref() + }) + + const workspace = await getWorkspaceForApiKey(payload.api_key) + upsertEnvVar(join(root, ".env"), "SEAM_API_KEY", payload.api_key) + return { workspace } +} + +function respondJson( + response: ServerResponse, + status: number, + body: unknown +): void { + response.writeHead(status, { "content-type": "application/json" }) + response.end(JSON.stringify(body)) +} diff --git a/src/lib/steps/detect-project.ts b/src/lib/steps/detect-project.ts new file mode 100644 index 0000000..15a2680 --- /dev/null +++ b/src/lib/steps/detect-project.ts @@ -0,0 +1,69 @@ +import { existsSync } from "node:fs" +import { join } from "node:path" + +export type Sdk = "javascript" | "python" +export type JsPackageManager = "npm" | "pnpm" | "yarn" | "bun" +export type PythonInstaller = "pip" | "poetry" | "uv" + +export interface ProjectInfo { + root: string + detected_sdk: Sdk | null + js_package_manager: JsPackageManager + python_installer: PythonInstaller +} + +export function detectProject(cwd: string): ProjectInfo { + return { + root: cwd, + detected_sdk: detectSdk(cwd), + js_package_manager: detectJsPackageManager(cwd), + python_installer: detectPythonInstaller(cwd), + } +} + +// null when the project is ambiguous (both or neither) — the wizard then asks. +function detectSdk(cwd: string): Sdk | null { + const is_javascript = existsSync(join(cwd, "package.json")) + const is_python = ["pyproject.toml", "requirements.txt", "setup.py", "Pipfile"].some( + (marker) => existsSync(join(cwd, marker)) + ) + if (is_javascript && !is_python) return "javascript" + if (is_python && !is_javascript) return "python" + return null +} + +function detectJsPackageManager(cwd: string): JsPackageManager { + if (existsSync(join(cwd, "pnpm-lock.yaml"))) return "pnpm" + if (existsSync(join(cwd, "yarn.lock"))) return "yarn" + if (existsSync(join(cwd, "bun.lockb"))) return "bun" + return "npm" +} + +function detectPythonInstaller(cwd: string): PythonInstaller { + if (existsSync(join(cwd, "poetry.lock"))) return "poetry" + if (existsSync(join(cwd, "uv.lock"))) return "uv" + return "pip" +} + +export function installSeamSdkCommand(sdk: Sdk, project: ProjectInfo): string[] { + if (sdk === "python") { + switch (project.python_installer) { + case "poetry": + return ["poetry", "add", "seam"] + case "uv": + return ["uv", "add", "seam"] + default: + return ["pip", "install", "seam"] + } + } + switch (project.js_package_manager) { + case "pnpm": + return ["pnpm", "add", "seam"] + case "yarn": + return ["yarn", "add", "seam"] + case "bun": + return ["bun", "add", "seam"] + default: + return ["npm", "install", "seam"] + } +} diff --git a/src/lib/steps/install-seam-plugin.ts b/src/lib/steps/install-seam-plugin.ts new file mode 100644 index 0000000..b068b47 --- /dev/null +++ b/src/lib/steps/install-seam-plugin.ts @@ -0,0 +1,38 @@ +import { existsSync } from "node:fs" +import { join } from "node:path" + +export type PluginTarget = "claude-code" | "universal" + +// The official Seam plugin: 3 integration skills + the seam-docs MCP. +const SEAM_PLUGIN = "seamapi/seam-plugin" + +// Install the plugin's three skills into .claude/skills. We must be +// non-interactive (`-y`) — the wizard spawns this with stdin ignored, and +// without a target the `skills` CLI prompts for an agent and installs nothing. +// We target `claude-code` explicitly so the skills land in .claude/skills, +// which is exactly where the embedded agent reads them (settingSources: +// ["project"]) and where Claude Code picks them up too. +export const SEAM_PLUGIN_NPX_COMMAND = [ + "npx", + "skills", + "add", + SEAM_PLUGIN, + "-y", + "-a", + "claude-code", +] + +// Claude Code installs plugins via in-app slash commands, which an external CLI +// can't drive — so we print these for the user to run. This path also wires up +// the seam-docs MCP. +export const CLAUDE_CODE_COMMANDS = [ + "/plugin marketplace add seamapi/seam-plugin", + "/plugin install seam@seamapi", +] + +// Detect Claude Code so we print its slash commands instead of running npx. +export function detectPluginTarget(root: string): PluginTarget { + const has_claude_code = + existsSync(join(root, ".claude")) || existsSync(join(root, "CLAUDE.md")) + return has_claude_code ? "claude-code" : "universal" +} diff --git a/src/lib/steps/integrate.ts b/src/lib/steps/integrate.ts new file mode 100644 index 0000000..56a0c5e --- /dev/null +++ b/src/lib/steps/integrate.ts @@ -0,0 +1,188 @@ +import { query } from "@anthropic-ai/claude-agent-sdk" +import type { Sdk } from "./detect-project.js" + +// The official Seam MCP (same server the seam-plugin wires up). +const SEAM_MCP_URL = "https://mcp.seam.co/mcp" + +// Read/search/write + the docs MCP. Deliberately no Bash, no subagents, no task +// tools: the agent writes integration code, it does not run the developer's +// shell. `mcp__seam-docs__*` grants every seam-docs tool. +const ALLOWED_TOOLS = [ + "Read", + "Glob", + "Grep", + "Edit", + "Write", + "WebFetch", + "mcp__seam-docs__*", +] + +export type IntegrateEvent = + | { kind: "text"; text: string } + | { kind: "tool"; name: string; detail: string } + | { kind: "done"; ok: boolean; summary: string; cost_usd: number | null } + +export interface RunIntegrationArgs { + root: string + sdk: Sdk + workspace_name: string + goal: string + inference: { base_url: string; token: string } + // The detected framework + chosen mode, so the agent fetches the matching + // reference app from the Seam MCP (get_example_app) to model its work on. + framework?: string | null + mode?: "full_api" | "customer_portal" + signal: AbortSignal + onEvent: (event: IntegrateEvent) => void +} + +// Drive the Claude Agent SDK to write the integration into the developer's +// project, routed through Seam-hosted inference. Streams progress via onEvent; +// resolves when the agent finishes or the signal aborts. +export async function runIntegration(args: RunIntegrationArgs): Promise { + const { root, sdk, workspace_name, goal, inference, framework, mode, signal, onEvent } = + args + const abort_controller = new AbortController() + const forward_abort = (): void => abort_controller.abort() + signal.addEventListener("abort", forward_abort, { once: true }) + + try { + for await (const message of query({ + prompt: goal, + options: { + cwd: root, + // Cost-tuned for a starter integration (Seam pays for this): Sonnet 5 is + // near-Opus on coding at ~half the token price, medium effort trims the + // thinking spend, and maxBudgetUsd is a hard per-run dollar ceiling. + model: "claude-sonnet-5", + effort: "medium", + maxBudgetUsd: 2, + env: buildAgentEnv(inference), + allowedTools: ALLOWED_TOOLS, + // Auto-apply file edits so the agent runs uninterrupted; the developer + // reviews the result as a git diff afterward. Read/search tools and the + // docs MCP are read-only, so nothing destructive runs unattended. + permissionMode: "acceptEdits", + mcpServers: { + // Wired exactly like the seam-plugin: mcp-remote bridges to the hosted + // Seam MCP and runs the OAuth browser flow on first use, caching the + // token in ~/.mcp-auth. The developer's Claude Code (also using + // mcp-remote to the same server) then reuses it, already authenticated. + "seam-docs": { + command: "npx", + args: ["-y", "mcp-remote", SEAM_MCP_URL], + }, + }, + // Pick up any Seam skill installed into the project's .claude/skills. + settingSources: ["project"], + skills: "all", + systemPrompt: { + type: "preset", + preset: "claude_code", + append: buildSystemAppend(sdk, workspace_name, framework, mode), + }, + maxTurns: 40, + abortController: abort_controller, + }, + })) { + if (signal.aborted) break + + if (message.type === "assistant") { + for (const block of message.message.content) { + if (block.type === "text") { + const text = block.text.trim() + if (text.length > 0) onEvent({ kind: "text", text }) + } else if (block.type === "tool_use") { + onEvent({ + kind: "tool", + name: block.name, + detail: describeToolInput(block.input), + }) + } + } + } else if (message.type === "result") { + const ok = message.subtype === "success" + onEvent({ + kind: "done", + ok, + summary: ok ? message.result : "", + cost_usd: + typeof message.total_cost_usd === "number" + ? message.total_cost_usd + : null, + }) + } + } + } finally { + signal.removeEventListener("abort", forward_abort) + } +} + +function buildSystemAppend( + sdk: Sdk, + workspace_name: string, + framework?: string | null, + mode?: "full_api" | "customer_portal" +): string { + const language = sdk === "python" ? "Python" : "JavaScript/TypeScript" + const framework_label = framework ?? "this project's framework" + const mode_label = mode === "customer_portal" ? "Customer Portal" : "full-API" + return [ + `You are the Seam integration agent, embedded in the seam-wizard CLI.`, + `The developer has just connected their Seam account (workspace "${workspace_name}"),`, + `and their SEAM_API_KEY is already saved in a local .env file. This is a ${language} project.`, + ``, + `Before writing any code:`, + `- Fetch the reference integration: call mcp__seam-docs__list_example_apps, then`, + ` mcp__seam-docs__get_example_app to pull the example matching ${framework_label} and the`, + ` ${mode_label} approach. Model your integration on it — match its structure and Seam API`, + ` usage — but ADAPT it to this project's actual framework version, conventions, and file`, + ` layout. Do not copy it verbatim, and don't add files this project doesn't need.`, + `- Find and read the installed Seam skill. Glob for a directory named like "*seam*" under`, + ` .claude/skills and .agents/skills, and read its SKILL.md and any referenced files.`, + `- Use the seam-docs MCP tools (prefixed mcp__seam-docs__) to confirm current Seam API usage.`, + ` Prefer Access Grants for granting a person access — they are Seam's recommended API.`, + `- Read the surrounding project files first and match its language, framework, and conventions.`, + ``, + `Then implement exactly what the developer asked for — nothing more. Load SEAM_API_KEY from the`, + `existing .env; never hardcode or print it. Keep changes minimal and idiomatic. When finished,`, + `give a short summary of the files you changed and how to run the result.`, + ].join("\n") +} + +// Route the embedded agent through Seam-hosted inference: point the SDK at the +// Seam proxy with the scoped wizard token, and drop any developer Anthropic key +// so it can't override the proxy routing. The SDK reads credentials from the +// child-process env (ANTHROPIC_AUTH_TOKEN is sent as a Bearer, which is what the +// proxy authenticates). +function buildAgentEnv(inference: { + base_url: string + token: string +}): Record { + const env: Record = {} + for (const [key, value] of Object.entries(process.env)) { + if (value != null) env[key] = value + } + delete env.ANTHROPIC_API_KEY + env.ANTHROPIC_BASE_URL = inference.base_url + env.ANTHROPIC_AUTH_TOKEN = inference.token + return env +} + +// Pull the most useful identifier out of a tool's input for a one-line UI label, +// without asserting the input's shape. +function describeToolInput(input: unknown): string { + return ( + stringField(input, "file_path") ?? + stringField(input, "pattern") ?? + stringField(input, "query") ?? + stringField(input, "url") ?? + "" + ) +} + +function stringField(input: unknown, key: string): string | null { + if (typeof input !== "object" || input === null || !(key in input)) return null + const value = (input as Record)[key] + return typeof value === "string" ? value : null +} diff --git a/src/lib/util/env-file.ts b/src/lib/util/env-file.ts new file mode 100644 index 0000000..2872b7b --- /dev/null +++ b/src/lib/util/env-file.ts @@ -0,0 +1,67 @@ +import { existsSync, readFileSync, writeFileSync } from "node:fs" +import { join } from "node:path" + +export type EnvWriteResult = "created" | "updated" | "added" + +// dotenv files we look at for an existing key, in priority order. +const ENV_FILE_NAMES = [ + ".env.local", + ".env", + ".env.development", + ".env.development.local", +] + +export interface FoundApiKey { + api_key: string + source: string // e.g. ".env.local" or "environment" +} + +// Look for an existing SEAM_API_KEY: first the process environment, then the +// project's dotenv files. Returns the first hit so the wizard can skip auth. +export function findExistingApiKey(root: string): FoundApiKey | null { + const from_process = process.env.SEAM_API_KEY?.trim() + if (from_process != null && from_process.length > 0) { + return { api_key: from_process, source: "environment" } + } + + for (const file_name of ENV_FILE_NAMES) { + const file_path = join(root, file_name) + if (!existsSync(file_path)) continue + const match = readFileSync(file_path, "utf8").match( + /^\s*SEAM_API_KEY\s*=\s*(.+?)\s*$/m + ) + const value = match?.[1]?.replace(/^["']|["']$/g, "").trim() + if (value != null && value.length > 0) { + return { api_key: value, source: file_name } + } + } + + return null +} + +// Upsert a KEY=value line into a dotenv file without disturbing other entries. +// Returns what happened so the wizard can report it accurately. +export function upsertEnvVar( + file_path: string, + key: string, + value: string +): EnvWriteResult { + const line = `${key}=${value}` + + if (!existsSync(file_path)) { + writeFileSync(file_path, `${line}\n`) + return "created" + } + + const content = readFileSync(file_path, "utf8") + const existing_line = new RegExp(`^${key}=.*$`, "m") + + if (existing_line.test(content)) { + writeFileSync(file_path, content.replace(existing_line, line)) + return "updated" + } + + const separator = content.length === 0 || content.endsWith("\n") ? "" : "\n" + writeFileSync(file_path, `${content}${separator}${line}\n`) + return "added" +} diff --git a/src/lib/util/run-install.ts b/src/lib/util/run-install.ts new file mode 100644 index 0000000..97433ff --- /dev/null +++ b/src/lib/util/run-install.ts @@ -0,0 +1,35 @@ +import { spawn } from "node:child_process" + +// Run a command with piped output, streaming each non-empty line to `onLine` +// so the Ink UI can render progress instead of clobbering the frame with +// inherited stdio. +export function runInstall( + command: string[], + cwd: string, + onLine: (line: string) => void +): Promise { + const [binary, ...args] = command + if (binary == null) throw new Error("runInstall: empty command") + + return new Promise((resolve, reject) => { + const child = spawn(binary, args, { + cwd, + stdio: ["ignore", "pipe", "pipe"], + shell: false, + }) + + const handle = (data: Buffer): void => { + for (const line of data.toString().split("\n")) { + if (line.trim().length > 0) onLine(line.trimEnd()) + } + } + child.stdout?.on("data", handle) + child.stderr?.on("data", handle) + + child.on("error", reject) + child.on("close", (code) => { + if (code === 0) resolve() + else reject(new Error(`${binary} exited with code ${code ?? "unknown"}`)) + }) + }) +} diff --git a/src/lib/util/seam-api.ts b/src/lib/util/seam-api.ts new file mode 100644 index 0000000..127d2e8 --- /dev/null +++ b/src/lib/util/seam-api.ts @@ -0,0 +1,187 @@ +// Minimal Seam API access used to validate a pasted API key. We deliberately +// avoid pulling in the full SDK just for a health check — a single fetch keeps +// the wizard's install footprint tiny. + +const SEAM_API_BASE = "https://connect.getseam.com" + +export interface SeamWorkspace { + workspace_id: string + name: string + is_sandbox: boolean +} + +export class ApiKeyError extends Error {} + +// Validates the key by fetching the workspace it belongs to. Returns the +// workspace so the wizard can show which workspace the key is for. +export async function getWorkspaceForApiKey( + api_key: string +): Promise { + let response: Response + try { + response = await fetch(`${SEAM_API_BASE}/workspaces/get`, { + method: "POST", + headers: { + authorization: `Bearer ${api_key}`, + "content-type": "application/json", + }, + body: "{}", + }) + } catch { + throw new ApiKeyError( + "Could not reach the Seam API. Check your network connection and try again." + ) + } + + if (response.status === 401) { + throw new ApiKeyError( + "That key was rejected (401). Make sure you copied the full key, including the seam_ prefix." + ) + } + if (!response.ok) { + throw new ApiKeyError( + `The Seam API returned ${response.status}. Please try again in a moment.` + ) + } + + const body = (await response.json()) as { workspace?: SeamWorkspace } + if (body.workspace == null) { + throw new ApiKeyError("Unexpected response from the Seam API.") + } + return body.workspace +} + +export function looksLikeSeamApiKey(value: string): boolean { + return /^seam_[A-Za-z0-9]/.test(value.trim()) +} + +// Base URL for Seam-hosted inference. The embedded agent's SDK appends +// /v1/messages; the exchange endpoint below lives at /session. +export const SEAM_INFERENCE_BASE_URL = `${SEAM_API_BASE}/internal/wizard_inference` + +// The Console-collected onboarding answers Seam returns alongside the token, so +// the wizard can pre-fill its plan instead of re-asking (null if none recorded). +export interface WizardOnboarding { + org_type: string | null + primary_goal: string | null + use_case: string | null + build_target: string | null + embed_customer_portal: boolean | null + device_categories: string[] | null +} + +export interface WizardInferenceSession { + token: string + expires_at: string + onboarding: WizardOnboarding | null +} + +// Exchange a Seam API key for a short-lived wizard inference token. The token — +// not the API key — is what the embedded agent sends to Seam-hosted inference, +// so the long-lived key stays off the repeated inference path. +export async function exchangeWizardInferenceToken( + api_key: string +): Promise { + let response: Response + try { + response = await fetch(`${SEAM_INFERENCE_BASE_URL}/session`, { + method: "POST", + headers: { + authorization: `Bearer ${api_key}`, + "content-type": "application/json", + }, + body: "{}", + }) + } catch { + throw new ApiKeyError( + "Could not reach Seam to start the AI session. Check your network connection and try again." + ) + } + + if (!response.ok) { + throw new ApiKeyError( + `Seam couldn't start the AI session (${response.status}). Please try again in a moment.` + ) + } + + const body = (await response.json()) as { + wizard_session?: { token: string; expires_at: string } + onboarding?: WizardOnboarding | null + } + if (body.wizard_session == null) { + throw new ApiKeyError("Unexpected response from Seam starting the AI session.") + } + return { + token: body.wizard_session.token, + expires_at: body.wizard_session.expires_at, + onboarding: body.onboarding ?? null, + } +} + +// One-shot call to Seam-hosted inference (Anthropic Messages API shape). Sent +// streamed so the proxy meters usage the same way it does for the agent; the +// text deltas are concatenated and returned. Used for the cheap project-analysis +// recommendation — not the full integration (that goes through the agent SDK). +export async function callInferenceForText( + inference: { base_url: string; token: string }, + args: { model: string; max_tokens: number; system: string; user: string } +): Promise { + const response = await fetch(`${inference.base_url}/v1/messages`, { + method: "POST", + headers: { + authorization: `Bearer ${inference.token}`, + "content-type": "application/json", + "anthropic-version": "2023-06-01", + }, + body: JSON.stringify({ + model: args.model, + max_tokens: args.max_tokens, + system: args.system, + stream: true, + messages: [{ role: "user", content: args.user }], + }), + }) + + if (!response.ok || response.body == null) { + throw new Error(`Seam inference returned ${response.status}`) + } + + return await readTextDeltas(response.body) +} + +async function readTextDeltas( + body: ReadableStream +): Promise { + const reader = body.getReader() + const decoder = new TextDecoder() + let buffer = "" + let text = "" + for (;;) { + const { value, done } = await reader.read() + if (done) break + buffer += decoder.decode(value, { stream: true }) + const lines = buffer.split("\n") + buffer = lines.pop() ?? "" + for (const line of lines) { + const trimmed = line.trim() + if (!trimmed.startsWith("data:")) continue + const payload = trimmed.slice("data:".length).trim() + if (payload.length === 0 || payload === "[DONE]") continue + try { + const event = JSON.parse(payload) as { + type?: string + delta?: { type?: string; text?: string } + } + if ( + event.type === "content_block_delta" && + event.delta?.type === "text_delta" + ) { + text += event.delta.text ?? "" + } + } catch { + // Ignore keep-alive pings and any non-JSON SSE lines. + } + } + } + return text +} From a2a90cdb3abba58f7ffbf9ccd078ae3bb7cdd23d Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 29 Jul 2026 01:49:42 +0000 Subject: [PATCH 02/11] build: add the wizard runtime dependencies and TSX build configuration The moved source is an Ink application, so add its runtime dependencies (Ink and its input components, React, open, and the Claude Agent SDK) alongside the React types and Ink testing library used for development. Teach the build, packaging, and test configuration about .tsx: exclude .test.tsx from the published build and include it in the Vitest run. The tsconfig already sets jsx: react-jsx, so no compiler option changes are needed. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01W1qhuuZQZnA1FB1fBSxrqx --- package-lock.json | 2130 ++++++++++++++++++++++++++++++++++++++++++- package.json | 14 +- tsconfig.build.json | 2 +- vitest.config.ts | 8 +- 4 files changed, 2106 insertions(+), 48 deletions(-) diff --git a/package-lock.json b/package-lock.json index 6e4d64d..bcd91a0 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,17 +9,26 @@ "version": "0.0.1", "license": "MIT", "dependencies": { - "minimist": "^1.2.8" + "@anthropic-ai/claude-agent-sdk": "^0.3.216", + "ink": "^5.2.1", + "ink-select-input": "^6.2.0", + "ink-spinner": "^5.0.0", + "ink-text-input": "^6.0.0", + "minimist": "^1.2.8", + "open": "^10.1.0", + "react": "^18.3.1" }, "devDependencies": { "@types/minimist": "^1.2.5", "@types/node": "^24.10.9", + "@types/react": "^18.3.27", "@vitest/coverage-v8": "^4.1.10", "del-cli": "^7.0.0", "eslint": "^9.31.0", "eslint-plugin-import": "^2.32.0", "eslint-plugin-simple-import-sort": "^12.1.1", "eslint-plugin-unused-imports": "^4.1.4", + "ink-testing-library": "^4.0.0", "jiti": "^2.4.2", "landlubber": "^2.0.0", "neostandard": "^0.13.0", @@ -34,6 +43,193 @@ "npm": ">=10.0.0" } }, + "node_modules/@alcalzone/ansi-tokenize": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/@alcalzone/ansi-tokenize/-/ansi-tokenize-0.1.3.tgz", + "integrity": "sha512-3yWxPTq3UQ/FY9p1ErPxIyfT64elWaMvM9lIHnaqpyft63tkxodF5aUElYHrdisWve5cETkh1+KBw1yJuW0aRw==", + "license": "MIT", + "dependencies": { + "ansi-styles": "^6.2.1", + "is-fullwidth-code-point": "^4.0.0" + }, + "engines": { + "node": ">=14.13.1" + } + }, + "node_modules/@alcalzone/ansi-tokenize/node_modules/ansi-styles": { + "version": "6.2.3", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.3.tgz", + "integrity": "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/@alcalzone/ansi-tokenize/node_modules/is-fullwidth-code-point": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-4.0.0.tgz", + "integrity": "sha512-O4L094N2/dZ7xqVdrXhh9r1KODPJpFms8B5sGdJLPy664AgvXsreZUyCQQNItZRDlYug4xStLjNp/sz3HvBowQ==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@anthropic-ai/claude-agent-sdk": { + "version": "0.3.220", + "resolved": "https://registry.npmjs.org/@anthropic-ai/claude-agent-sdk/-/claude-agent-sdk-0.3.220.tgz", + "integrity": "sha512-glc7SdwPkOkLw8oxwLo9PKTdLJGqW/PIR4urWXFoRtX9YllwozsEVc5Tc1+EvLSkfrsxPJqQWqOgpjUOQXf1oA==", + "license": "SEE LICENSE IN README.md", + "engines": { + "node": ">=18.0.0" + }, + "optionalDependencies": { + "@anthropic-ai/claude-agent-sdk-darwin-arm64": "0.3.220", + "@anthropic-ai/claude-agent-sdk-darwin-x64": "0.3.220", + "@anthropic-ai/claude-agent-sdk-linux-arm64": "0.3.220", + "@anthropic-ai/claude-agent-sdk-linux-arm64-musl": "0.3.220", + "@anthropic-ai/claude-agent-sdk-linux-x64": "0.3.220", + "@anthropic-ai/claude-agent-sdk-linux-x64-musl": "0.3.220", + "@anthropic-ai/claude-agent-sdk-win32-arm64": "0.3.220", + "@anthropic-ai/claude-agent-sdk-win32-x64": "0.3.220" + }, + "peerDependencies": { + "@anthropic-ai/sdk": ">=0.93.0", + "@modelcontextprotocol/sdk": "^1.29.0", + "zod": "^4.0.0" + } + }, + "node_modules/@anthropic-ai/claude-agent-sdk-darwin-arm64": { + "version": "0.3.220", + "resolved": "https://registry.npmjs.org/@anthropic-ai/claude-agent-sdk-darwin-arm64/-/claude-agent-sdk-darwin-arm64-0.3.220.tgz", + "integrity": "sha512-7VxlbEosK7DODiOnsjoVd0DSJzbnaPrM2jelMHI0y8zx1UnLS3WC6EFUXbvy74F2sXqEznh2tzn7EKWInaRN6Q==", + "cpu": [ + "arm64" + ], + "license": "SEE LICENSE IN LICENSE.md", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@anthropic-ai/claude-agent-sdk-darwin-x64": { + "version": "0.3.220", + "resolved": "https://registry.npmjs.org/@anthropic-ai/claude-agent-sdk-darwin-x64/-/claude-agent-sdk-darwin-x64-0.3.220.tgz", + "integrity": "sha512-X9RwDsSmbF6ultKZroaip+DL8WRgC64gHbrAwrRlAFSPNZV7zmJyP2ur8rW7KrxqmtuehdMMkw8+SAC/6hD2PA==", + "cpu": [ + "x64" + ], + "license": "SEE LICENSE IN LICENSE.md", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@anthropic-ai/claude-agent-sdk-linux-arm64": { + "version": "0.3.220", + "resolved": "https://registry.npmjs.org/@anthropic-ai/claude-agent-sdk-linux-arm64/-/claude-agent-sdk-linux-arm64-0.3.220.tgz", + "integrity": "sha512-WkROPwWskqhKR9XgnmseHQ6rLi9zM9qt57IWoToIjL/eXOqDWipp7JXZ1L5ud+LrA42dunHPZfBwD/vXZ+A7LA==", + "cpu": [ + "arm64" + ], + "license": "SEE LICENSE IN LICENSE.md", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@anthropic-ai/claude-agent-sdk-linux-arm64-musl": { + "version": "0.3.220", + "resolved": "https://registry.npmjs.org/@anthropic-ai/claude-agent-sdk-linux-arm64-musl/-/claude-agent-sdk-linux-arm64-musl-0.3.220.tgz", + "integrity": "sha512-OHoZOZ8Cf2TBr6oXIXPwyvUxj9jrq2w8E4poA8dMpacXszcPSPiCQCMuuOh4aWJzfeJE1+TtWxhKMVb2csXyZQ==", + "cpu": [ + "arm64" + ], + "license": "SEE LICENSE IN LICENSE.md", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@anthropic-ai/claude-agent-sdk-linux-x64": { + "version": "0.3.220", + "resolved": "https://registry.npmjs.org/@anthropic-ai/claude-agent-sdk-linux-x64/-/claude-agent-sdk-linux-x64-0.3.220.tgz", + "integrity": "sha512-tkTJFnpR9VifvWX2fmkCAPkT6+8Wk/gVu8B5jsVekKZPiZoWRHmMXO30BnZn+f0TZhgYP+82PSX3S8crH1kn+w==", + "cpu": [ + "x64" + ], + "license": "SEE LICENSE IN LICENSE.md", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@anthropic-ai/claude-agent-sdk-linux-x64-musl": { + "version": "0.3.220", + "resolved": "https://registry.npmjs.org/@anthropic-ai/claude-agent-sdk-linux-x64-musl/-/claude-agent-sdk-linux-x64-musl-0.3.220.tgz", + "integrity": "sha512-K+FWj+LcGhC1Z7wqeWoLxm1iemcba5xKpLLFVwYm4V6HyMx3ruYd/2r2TiQtjT+JWeNFWIys0ScHiItR6vWAiA==", + "cpu": [ + "x64" + ], + "license": "SEE LICENSE IN LICENSE.md", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@anthropic-ai/claude-agent-sdk-win32-arm64": { + "version": "0.3.220", + "resolved": "https://registry.npmjs.org/@anthropic-ai/claude-agent-sdk-win32-arm64/-/claude-agent-sdk-win32-arm64-0.3.220.tgz", + "integrity": "sha512-rIwgq0UwQExWl6KrHUyC4w5KwpL9l6nd95aUTx6RitexaAuEw//xtfTVLnuE4hDDQZFkzEwpdKc3nxDWoGcUbA==", + "cpu": [ + "arm64" + ], + "license": "SEE LICENSE IN LICENSE.md", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@anthropic-ai/claude-agent-sdk-win32-x64": { + "version": "0.3.220", + "resolved": "https://registry.npmjs.org/@anthropic-ai/claude-agent-sdk-win32-x64/-/claude-agent-sdk-win32-x64-0.3.220.tgz", + "integrity": "sha512-MuOuXhbr66HlGaWXD2f3w0k2PsvmnbkwcUZ0dAe2poFLdl72GC2dapwwOBefxm9QmoNqk9+jmv/dSKGOVWyvLw==", + "cpu": [ + "x64" + ], + "license": "SEE LICENSE IN LICENSE.md", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@anthropic-ai/sdk": { + "version": "0.115.0", + "resolved": "https://registry.npmjs.org/@anthropic-ai/sdk/-/sdk-0.115.0.tgz", + "integrity": "sha512-BJrFIVyjNuU8lfDyIJTvlRYzgQg+zEl78BxE7fq8esULsGz9IRQvGtW5spq3tydmtjQb/GFdooKGdGsetpx+lQ==", + "license": "MIT", + "peer": true, + "dependencies": { + "json-schema-to-ts": "^3.1.1", + "standardwebhooks": "^1.0.0" + }, + "bin": { + "anthropic-ai-sdk": "bin/cli" + }, + "peerDependencies": { + "zod": "^3.25.0 || ^4.0.0" + }, + "peerDependenciesMeta": { + "zod": { + "optional": true + } + } + }, "node_modules/@babel/helper-string-parser": { "version": "7.29.7", "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.29.7.tgz", @@ -70,6 +266,16 @@ "node": ">=6.0.0" } }, + "node_modules/@babel/runtime": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.29.7.tgz", + "integrity": "sha512-Nq8OhGWiZIZGV6hLHoyAKLLcJihP/xFeBMGJoUrxTX2psI8dCifzLhZISFb+VWS3wFMRDmCGw5R+dOySCqPLhw==", + "license": "MIT", + "peer": true, + "engines": { + "node": ">=6.9.0" + } + }, "node_modules/@babel/types": { "version": "7.29.7", "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.7.tgz", @@ -717,6 +923,19 @@ "node": "^18.18.0 || ^20.9.0 || >=21.1.0" } }, + "node_modules/@hono/node-server": { + "version": "2.0.12", + "resolved": "https://registry.npmjs.org/@hono/node-server/-/node-server-2.0.12.tgz", + "integrity": "sha512-eWpQYr67tqJLeaSUl0Q+TquuYfUdTibpOJlUMV2FfUP7+KqCC5TufnwnlXL6mobZBJbGAYRd7ZvEBDCbLInjhg==", + "license": "MIT", + "peer": true, + "engines": { + "node": ">=20" + }, + "peerDependencies": { + "hono": "^4" + } + }, "node_modules/@humanfs/core": { "version": "0.19.2", "resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.2.tgz", @@ -822,6 +1041,71 @@ "@jridgewell/sourcemap-codec": "^1.4.14" } }, + "node_modules/@modelcontextprotocol/sdk": { + "version": "1.30.0", + "resolved": "https://registry.npmjs.org/@modelcontextprotocol/sdk/-/sdk-1.30.0.tgz", + "integrity": "sha512-xKd8OIzlqNzcqcNumGAa6g+PW2kjD5vrpcKOnfldAUPP3j7lnqMPwlTXQm8gF+UwH72z0lqaRbjr9hqGz0eITA==", + "license": "MIT", + "peer": true, + "dependencies": { + "@hono/node-server": "^1.19.9 || ^2.0.5", + "ajv": "^8.17.1", + "ajv-formats": "^3.0.1", + "content-type": "^1.0.5", + "cors": "^2.8.5", + "cross-spawn": "^7.0.5", + "eventsource": "^3.0.2", + "eventsource-parser": "^3.0.0", + "express": "^5.2.1", + "express-rate-limit": "^8.2.1", + "hono": "^4.11.4", + "jose": "^6.1.3", + "json-schema-typed": "^8.0.2", + "pkce-challenge": "^5.0.0", + "raw-body": "^3.0.0", + "zod": "^3.25 || ^4.0", + "zod-to-json-schema": "^3.25.1" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@cfworker/json-schema": "^4.1.1", + "zod": "^3.25 || ^4.0" + }, + "peerDependenciesMeta": { + "@cfworker/json-schema": { + "optional": true + }, + "zod": { + "optional": false + } + } + }, + "node_modules/@modelcontextprotocol/sdk/node_modules/ajv": { + "version": "8.20.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.20.0.tgz", + "integrity": "sha512-Thbli+OlOj+iMPYFBVBfJ3OmCAnaSyNn4M1vz9T6Gka5Jt9ba/HIR56joy65tY6kx/FCF5VXNB819Y7/GUrBGA==", + "license": "MIT", + "peer": true, + "dependencies": { + "fast-deep-equal": "^3.1.3", + "fast-uri": "^3.0.1", + "json-schema-traverse": "^1.0.0", + "require-from-string": "^2.0.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/@modelcontextprotocol/sdk/node_modules/json-schema-traverse": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", + "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", + "license": "MIT", + "peer": true + }, "node_modules/@napi-rs/wasm-runtime": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-1.2.0.tgz", @@ -1210,6 +1494,13 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/@stablelib/base64": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@stablelib/base64/-/base64-1.0.1.tgz", + "integrity": "sha512-1bnPQqSxSuc3Ii6MhBysoWCg58j97aUjuCSZrGSmDxNqtytIi0k8utUenAwTZN4V5mXXYGsVUI9zeBqy+jBOSQ==", + "license": "MIT", + "peer": true + }, "node_modules/@standard-schema/spec": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/@standard-schema/spec/-/spec-1.1.0.tgz", @@ -1317,6 +1608,24 @@ "undici-types": "~7.18.0" } }, + "node_modules/@types/prop-types": { + "version": "15.7.15", + "resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.15.tgz", + "integrity": "sha512-F6bEyamV9jKGAFBEmlQnesRPGOQqS2+Uwi0Em15xenOxHaf2hv6L8YCVn3rPdPJOiJfPiCnLIRyvwVaqMY3MIw==", + "devOptional": true, + "license": "MIT" + }, + "node_modules/@types/react": { + "version": "18.3.31", + "resolved": "https://registry.npmjs.org/@types/react/-/react-18.3.31.tgz", + "integrity": "sha512-vfEqpXTvwT91yhmwdfouStN2hSKwTvyRs8qpLfADyrq/kxDw0hZM7Wk9Ug1FELj8hIby+S/+kQCSRFF32nv2Qw==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "@types/prop-types": "*", + "csstype": "^3.2.2" + } + }, "node_modules/@types/yargs": { "version": "17.0.35", "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-17.0.35.tgz", @@ -1786,6 +2095,20 @@ "node": ">=6.5" } }, + "node_modules/accepts": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/accepts/-/accepts-2.0.0.tgz", + "integrity": "sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng==", + "license": "MIT", + "peer": true, + "dependencies": { + "mime-types": "^3.0.0", + "negotiator": "^1.0.0" + }, + "engines": { + "node": ">= 0.6" + } + }, "node_modules/acorn": { "version": "8.18.0", "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.18.0.tgz", @@ -1826,6 +2149,63 @@ "url": "https://github.com/sponsors/epoberezkin" } }, + "node_modules/ajv-formats": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/ajv-formats/-/ajv-formats-3.0.1.tgz", + "integrity": "sha512-8iUql50EUR+uUcdRQ3HDqa6EVyo3docL8g5WJ3FNcWmu62IbkGUue/pEyLBW8VGKKucTPgqeks4fIU1DA4yowQ==", + "license": "MIT", + "peer": true, + "dependencies": { + "ajv": "^8.0.0" + }, + "peerDependencies": { + "ajv": "^8.0.0" + }, + "peerDependenciesMeta": { + "ajv": { + "optional": true + } + } + }, + "node_modules/ajv-formats/node_modules/ajv": { + "version": "8.20.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.20.0.tgz", + "integrity": "sha512-Thbli+OlOj+iMPYFBVBfJ3OmCAnaSyNn4M1vz9T6Gka5Jt9ba/HIR56joy65tY6kx/FCF5VXNB819Y7/GUrBGA==", + "license": "MIT", + "peer": true, + "dependencies": { + "fast-deep-equal": "^3.1.3", + "fast-uri": "^3.0.1", + "json-schema-traverse": "^1.0.0", + "require-from-string": "^2.0.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/ajv-formats/node_modules/json-schema-traverse": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", + "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", + "license": "MIT", + "peer": true + }, + "node_modules/ansi-escapes": { + "version": "7.3.0", + "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-7.3.0.tgz", + "integrity": "sha512-BvU8nYgGQBxcmMuEeUEmNTvrMVjJNSH7RgW24vXexN4Ven6qCvy4TntnvlnwnMLTVlcRQQdbRY8NKnaIoeWDNg==", + "license": "MIT", + "dependencies": { + "environment": "^1.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/ansi-regex": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", @@ -2085,6 +2465,18 @@ "node": ">=8.0.0" } }, + "node_modules/auto-bind": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/auto-bind/-/auto-bind-5.0.1.tgz", + "integrity": "sha512-ooviqdwwgfIfNmDwo94wlshcdzfO64XV0Cg6oDsDYBJfITDz1EngD2z7DkbvCWn+XIMsIqW27sEVF6qcpJrRcg==", + "license": "MIT", + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/available-typed-arrays": { "version": "1.0.7", "resolved": "https://registry.npmjs.org/available-typed-arrays/-/available-typed-arrays-1.0.7.tgz", @@ -2142,6 +2534,45 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/body-parser": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-2.3.0.tgz", + "integrity": "sha512-2cGmJupaNgg+QUwVLAucDuWuoMZ6EX9iHDRswZ5lsNYEmwPaRknMPCLZz07yTzVq/83p4o/wzbDZbBrTvGGTIw==", + "license": "MIT", + "peer": true, + "dependencies": { + "bytes": "^3.1.2", + "content-type": "^2.0.0", + "debug": "^4.4.3", + "http-errors": "^2.0.1", + "iconv-lite": "^0.7.2", + "on-finished": "^2.4.1", + "qs": "^6.15.2", + "raw-body": "^3.0.2", + "type-is": "^2.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/body-parser/node_modules/content-type": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/content-type/-/content-type-2.0.0.tgz", + "integrity": "sha512-j/O/d7GcZCyNl7/hwZAb606rzqkyvaDctLmckbxLzHvFBzTJHuGEdodATcP3yIRoDrLHkIATJuvzbFlp/ki2cQ==", + "license": "MIT", + "peer": true, + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, "node_modules/brace-expansion": { "version": "1.1.16", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.16.tgz", @@ -2191,6 +2622,31 @@ "ieee754": "^1.2.1" } }, + "node_modules/bundle-name": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/bundle-name/-/bundle-name-4.1.0.tgz", + "integrity": "sha512-tjwM5exMg6BGRI+kNmTntNsvdZS1X8BFYS6tnJ2hdH0kVxM6/eVZ2xy+FqStSWvYmtfFMDLIxurorHwDKfDz5Q==", + "license": "MIT", + "dependencies": { + "run-applescript": "^7.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/bytes": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", + "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==", + "license": "MIT", + "peer": true, + "engines": { + "node": ">= 0.8" + } + }, "node_modules/call-bind": { "version": "1.0.9", "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.9.tgz", @@ -2214,7 +2670,6 @@ "version": "1.0.2", "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", - "dev": true, "license": "MIT", "dependencies": { "es-errors": "^1.3.0", @@ -2228,7 +2683,6 @@ "version": "1.0.4", "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz", "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==", - "dev": true, "license": "MIT", "dependencies": { "call-bind-apply-helpers": "^1.0.2", @@ -2316,21 +2770,178 @@ "node": ">= 6" } }, - "node_modules/cliui": { - "version": "8.0.1", - "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz", - "integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==", - "dev": true, - "license": "ISC", - "dependencies": { - "string-width": "^4.2.0", - "strip-ansi": "^6.0.1", + "node_modules/cli-boxes": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/cli-boxes/-/cli-boxes-3.0.0.tgz", + "integrity": "sha512-/lzGpEWL/8PfI0BmBOPRwp0c/wFNX1RdUML3jK/RcSBA9T8mZDdQpqYBKtCFTOfQbwPqWEOpjqW+Fnayc0969g==", + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/cli-cursor": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-4.0.0.tgz", + "integrity": "sha512-VGtlMu3x/4DOtIUwEkRezxUZ2lBacNJCHash0N0WeZDBS+7Ux1dm3XWAgWYxLJFMMdOeXMHXorshEFhbMSGelg==", + "license": "MIT", + "dependencies": { + "restore-cursor": "^4.0.0" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/cli-spinners": { + "version": "2.9.2", + "resolved": "https://registry.npmjs.org/cli-spinners/-/cli-spinners-2.9.2.tgz", + "integrity": "sha512-ywqV+5MmyL4E7ybXgKys4DugZbX0FC6LnwrhjuykIjnK9k8OQacQ7axGKnjDXWNhns0xot3bZI5h55H8yo9cJg==", + "license": "MIT", + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/cli-truncate": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/cli-truncate/-/cli-truncate-4.0.0.tgz", + "integrity": "sha512-nPdaFdQ0h/GEigbPClz11D0v/ZJEwxmeVZGeMo3Z5StPtUTkA9o1lD6QwoirYiSDzbcwn2XcjwmCp68W1IS4TA==", + "license": "MIT", + "dependencies": { + "slice-ansi": "^5.0.0", + "string-width": "^7.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/cli-truncate/node_modules/ansi-regex": { + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.2.2.tgz", + "integrity": "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-regex?sponsor=1" + } + }, + "node_modules/cli-truncate/node_modules/ansi-styles": { + "version": "6.2.3", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.3.tgz", + "integrity": "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/cli-truncate/node_modules/emoji-regex": { + "version": "10.6.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-10.6.0.tgz", + "integrity": "sha512-toUI84YS5YmxW219erniWD0CIVOo46xGKColeNQRgOzDorgBi1v4D71/OFzgD9GO2UGKIv1C3Sp8DAn0+j5w7A==", + "license": "MIT" + }, + "node_modules/cli-truncate/node_modules/is-fullwidth-code-point": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-4.0.0.tgz", + "integrity": "sha512-O4L094N2/dZ7xqVdrXhh9r1KODPJpFms8B5sGdJLPy664AgvXsreZUyCQQNItZRDlYug4xStLjNp/sz3HvBowQ==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/cli-truncate/node_modules/slice-ansi": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-5.0.0.tgz", + "integrity": "sha512-FC+lgizVPfie0kkhqUScwRu1O/lF6NOgJmlCgK+/LYxDCTk8sGelYaHDhFcDN+Sn3Cv+3VSa4Byeo+IMCzpMgQ==", + "license": "MIT", + "dependencies": { + "ansi-styles": "^6.0.0", + "is-fullwidth-code-point": "^4.0.0" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/slice-ansi?sponsor=1" + } + }, + "node_modules/cli-truncate/node_modules/string-width": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-7.2.0.tgz", + "integrity": "sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ==", + "license": "MIT", + "dependencies": { + "emoji-regex": "^10.3.0", + "get-east-asian-width": "^1.0.0", + "strip-ansi": "^7.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/cli-truncate/node_modules/strip-ansi": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.2.0.tgz", + "integrity": "sha512-yDPMNjp4WyfYBkHnjIRLfca1i6KMyGCtsVgoKe/z1+6vukgaENdgGBZt+ZmKPc4gavvEZ5OgHfHdrazhgNyG7w==", + "license": "MIT", + "dependencies": { + "ansi-regex": "^6.2.2" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/strip-ansi?sponsor=1" + } + }, + "node_modules/cliui": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz", + "integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.1", "wrap-ansi": "^7.0.0" }, "engines": { "node": ">=12" } }, + "node_modules/code-excerpt": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/code-excerpt/-/code-excerpt-4.0.0.tgz", + "integrity": "sha512-xxodCmBen3iy2i0WtAK8FlFNrRzjUqjRsMfho58xT/wvZU1YTM3fCnRjcy1gJPMepaRlgm/0e6w8SpWHpn3/cA==", + "license": "MIT", + "dependencies": { + "convert-to-spaces": "^2.0.1" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + } + }, "node_modules/color-convert": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", @@ -2375,6 +2986,30 @@ "dev": true, "license": "MIT" }, + "node_modules/content-disposition": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-1.1.0.tgz", + "integrity": "sha512-5jRCH9Z/+DRP7rkvY83B+yGIGX96OYdJmzngqnw2SBSxqCFPd0w2km3s5iawpGX8krnwSGmF0FW5Nhr0Hfai3g==", + "license": "MIT", + "peer": true, + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/content-type": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz", + "integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==", + "license": "MIT", + "peer": true, + "engines": { + "node": ">= 0.6" + } + }, "node_modules/convert-source-map": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", @@ -2382,11 +3017,57 @@ "dev": true, "license": "MIT" }, + "node_modules/convert-to-spaces": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/convert-to-spaces/-/convert-to-spaces-2.0.1.tgz", + "integrity": "sha512-rcQ1bsQO9799wq24uE5AM2tAILy4gXGIK/njFWcVQkGNZ96edlpY+A7bjwvzjYvLDyzmG1MmMLZhpcsb+klNMQ==", + "license": "MIT", + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + } + }, + "node_modules/cookie": { + "version": "0.7.2", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.2.tgz", + "integrity": "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==", + "license": "MIT", + "peer": true, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/cookie-signature": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.2.2.tgz", + "integrity": "sha512-D76uU73ulSXrD1UXF4KE2TMxVVwhsnCgfAyTg9k8P6KGZjlXKrOLe4dJQKI3Bxi5wjesZoFXJWElNWBjPZMbhg==", + "license": "MIT", + "peer": true, + "engines": { + "node": ">=6.6.0" + } + }, + "node_modules/cors": { + "version": "2.8.6", + "resolved": "https://registry.npmjs.org/cors/-/cors-2.8.6.tgz", + "integrity": "sha512-tJtZBBHA6vjIAaF6EnIaq6laBBP9aq/Y3ouVJjEfoHbRBcHBAHYcMh/w8LDrk2PvIMMq8gmopa5D4V8RmbrxGw==", + "license": "MIT", + "peer": true, + "dependencies": { + "object-assign": "^4", + "vary": "^1" + }, + "engines": { + "node": ">= 0.10" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, "node_modules/cross-spawn": { "version": "7.0.6", "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", - "dev": true, "license": "MIT", "dependencies": { "path-key": "^3.1.0", @@ -2397,6 +3078,13 @@ "node": ">= 8" } }, + "node_modules/csstype": { + "version": "3.2.3", + "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz", + "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==", + "devOptional": true, + "license": "MIT" + }, "node_modules/data-view-buffer": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/data-view-buffer/-/data-view-buffer-1.0.2.tgz", @@ -2465,7 +3153,6 @@ "version": "4.4.3", "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", - "dev": true, "license": "MIT", "dependencies": { "ms": "^2.1.3" @@ -2486,6 +3173,34 @@ "dev": true, "license": "MIT" }, + "node_modules/default-browser": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/default-browser/-/default-browser-5.5.0.tgz", + "integrity": "sha512-H9LMLr5zwIbSxrmvikGuI/5KGhZ8E2zH3stkMgM5LpOWDutGM2JZaj460Udnf1a+946zc7YBgrqEWwbk7zHvGw==", + "license": "MIT", + "dependencies": { + "bundle-name": "^4.1.0", + "default-browser-id": "^5.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/default-browser-id": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/default-browser-id/-/default-browser-id-5.0.1.tgz", + "integrity": "sha512-x1VCxdX4t+8wVfd1so/9w+vQ4vx7lKd2Qp5tDRutErwmR85OgmfX7RlLRMWafRMY7hbEiXIbudNrjOAPa/hL8Q==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/define-data-property": { "version": "1.1.4", "resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.4.tgz", @@ -2504,6 +3219,18 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/define-lazy-prop": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/define-lazy-prop/-/define-lazy-prop-3.0.0.tgz", + "integrity": "sha512-N+MeXYoqr3pOgn8xfyRPREN7gHakLYjhsHhWGT3fWAiL4IkAt0iDw14QiiEm2bE30c5XX5q0FtAA3CK5f9/BUg==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/define-properties": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.2.1.tgz", @@ -2566,6 +3293,16 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/depd": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", + "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==", + "license": "MIT", + "peer": true, + "engines": { + "node": ">= 0.8" + } + }, "node_modules/detect-libc": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz", @@ -2616,7 +3353,6 @@ "version": "1.0.1", "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", - "dev": true, "license": "MIT", "dependencies": { "call-bind-apply-helpers": "^1.0.1", @@ -2627,6 +3363,13 @@ "node": ">= 0.4" } }, + "node_modules/ee-first": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", + "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==", + "license": "MIT", + "peer": true + }, "node_modules/emoji-regex": { "version": "8.0.0", "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", @@ -2634,6 +3377,16 @@ "dev": true, "license": "MIT" }, + "node_modules/encodeurl": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz", + "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==", + "license": "MIT", + "peer": true, + "engines": { + "node": ">= 0.8" + } + }, "node_modules/end-of-stream": { "version": "1.4.5", "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.5.tgz", @@ -2658,6 +3411,18 @@ "node": ">=10.13.0" } }, + "node_modules/environment": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/environment/-/environment-1.1.0.tgz", + "integrity": "sha512-xUtoPkMggbz0MPyPiIWr1Kp4aeWJjDZ6SMvURhimjdZgsRuDplF5/s9hcgGhyXMhs+6vpnuoiZ2kFiu3FMnS8Q==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/es-abstract": { "version": "1.24.2", "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.24.2.tgz", @@ -2750,7 +3515,6 @@ "version": "1.0.1", "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", - "dev": true, "license": "MIT", "engines": { "node": ">= 0.4" @@ -2760,7 +3524,6 @@ "version": "1.3.0", "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", - "dev": true, "license": "MIT", "engines": { "node": ">= 0.4" @@ -2805,7 +3568,6 @@ "version": "1.1.2", "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.2.tgz", "integrity": "sha512-HWcBoN6NileqtSydK2FqHbS/LoDd2pqrnQHLyJzBj4kOp/ky2MWMN694xOfkK8/SnUsW2DH7EfyVlydKCsm1Zw==", - "dev": true, "license": "MIT", "dependencies": { "es-errors": "^1.3.0" @@ -2864,6 +3626,17 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/es-toolkit": { + "version": "1.50.0", + "resolved": "https://registry.npmjs.org/es-toolkit/-/es-toolkit-1.50.0.tgz", + "integrity": "sha512-OyZKhUVvEep9ITEiwHn8GKnMRQIVqoSIX7WnRbkWgJkllCujilqP2rD0u979tkl8wqyc8ICwlc1UBVv/Sl1G6w==", + "license": "MIT", + "workspaces": [ + "docs", + "benchmarks", + "tests/types" + ] + }, "node_modules/esbuild": { "version": "0.28.1", "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.28.1.tgz", @@ -2916,6 +3689,13 @@ "node": ">=6" } }, + "node_modules/escape-html": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", + "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==", + "license": "MIT", + "peer": true + }, "node_modules/escape-string-regexp": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", @@ -3369,6 +4149,16 @@ "node": ">=0.10.0" } }, + "node_modules/etag": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", + "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==", + "license": "MIT", + "peer": true, + "engines": { + "node": ">= 0.6" + } + }, "node_modules/event-target-shim": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/event-target-shim/-/event-target-shim-5.0.1.tgz", @@ -3389,6 +4179,29 @@ "node": ">=0.8.x" } }, + "node_modules/eventsource": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/eventsource/-/eventsource-3.0.7.tgz", + "integrity": "sha512-CRT1WTyuQoD771GW56XEZFQ/ZoSfWid1alKGDYMmkt2yl8UXrVR4pspqWNEcqKvVIzg6PAltWjxcSSPrboA4iA==", + "license": "MIT", + "peer": true, + "dependencies": { + "eventsource-parser": "^3.0.1" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/eventsource-parser": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/eventsource-parser/-/eventsource-parser-3.1.0.tgz", + "integrity": "sha512-kJezFj9YFAMLeORyi7aCLxLbD5/qWMQnoMVlVPyHIll7lgRJCc3JVln9Vgl9nwQi0YkMnhdGTMNn7CkRRAptMg==", + "license": "MIT", + "peer": true, + "engines": { + "node": ">=18.0.0" + } + }, "node_modules/expect-type": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/expect-type/-/expect-type-1.4.0.tgz", @@ -3399,6 +4212,70 @@ "node": ">=12.0.0" } }, + "node_modules/express": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/express/-/express-5.2.1.tgz", + "integrity": "sha512-hIS4idWWai69NezIdRt2xFVofaF4j+6INOpJlVOLDO8zXGpUVEVzIYk12UUi2JzjEzWL3IOAxcTubgz9Po0yXw==", + "license": "MIT", + "peer": true, + "dependencies": { + "accepts": "^2.0.0", + "body-parser": "^2.2.1", + "content-disposition": "^1.0.0", + "content-type": "^1.0.5", + "cookie": "^0.7.1", + "cookie-signature": "^1.2.1", + "debug": "^4.4.0", + "depd": "^2.0.0", + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "etag": "^1.8.1", + "finalhandler": "^2.1.0", + "fresh": "^2.0.0", + "http-errors": "^2.0.0", + "merge-descriptors": "^2.0.0", + "mime-types": "^3.0.0", + "on-finished": "^2.4.1", + "once": "^1.4.0", + "parseurl": "^1.3.3", + "proxy-addr": "^2.0.7", + "qs": "^6.14.0", + "range-parser": "^1.2.1", + "router": "^2.2.0", + "send": "^1.1.0", + "serve-static": "^2.2.0", + "statuses": "^2.0.1", + "type-is": "^2.0.1", + "vary": "^1.1.2" + }, + "engines": { + "node": ">= 18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/express-rate-limit": { + "version": "8.6.1", + "resolved": "https://registry.npmjs.org/express-rate-limit/-/express-rate-limit-8.6.1.tgz", + "integrity": "sha512-0D493aP61w0TJ2A0wy27riRsO7FMQ7FK+KUHOKCSfPvYo0R55aiC6emCVgFUeShH0fq0ICPVzNcgoS+BsbXQCA==", + "license": "MIT", + "peer": true, + "dependencies": { + "debug": "^4.4.3", + "ip-address": "^10.2.0" + }, + "engines": { + "node": ">= 16" + }, + "funding": { + "url": "https://github.com/sponsors/express-rate-limit" + }, + "peerDependencies": { + "express": ">= 4.11" + } + }, "node_modules/fast-copy": { "version": "3.0.2", "resolved": "https://registry.npmjs.org/fast-copy/-/fast-copy-3.0.2.tgz", @@ -3410,7 +4287,6 @@ "version": "3.1.3", "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", - "dev": true, "license": "MIT" }, "node_modules/fast-glob": { @@ -3474,6 +4350,30 @@ "dev": true, "license": "MIT" }, + "node_modules/fast-sha256": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/fast-sha256/-/fast-sha256-1.3.0.tgz", + "integrity": "sha512-n11RGP/lrWEFI/bWdygLxhI+pVeo1ZYIVwvvPkW7azl/rOy+F3HYRZ2K5zeE9mmkhQppyv9sQFx0JM9UabnpPQ==", + "license": "Unlicense", + "peer": true + }, + "node_modules/fast-uri": { + "version": "3.1.4", + "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.4.tgz", + "integrity": "sha512-8JnbkQ4juDyvYs4mgFGQqg4yCYtFDtUtmp2QIQq11ZZe5CFQ5wcqm1rqDgAh/QdMySuBnPzMUiJUNZG5N/AiQw==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "BSD-3-Clause", + "peer": true + }, "node_modules/fastq": { "version": "1.20.1", "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.20.1.tgz", @@ -3484,6 +4384,21 @@ "reusify": "^1.0.4" } }, + "node_modules/figures": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/figures/-/figures-6.1.0.tgz", + "integrity": "sha512-d+l3qxjSesT4V7v2fh+QnmFnUWv9lSpjarhShNTgBOfA0ttejbQUAlHLitbjkoRiDulW0OPoQPYIGhIC8ohejg==", + "license": "MIT", + "dependencies": { + "is-unicode-supported": "^2.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/file-entry-cache": { "version": "8.0.0", "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-8.0.0.tgz", @@ -3510,6 +4425,28 @@ "node": ">=8" } }, + "node_modules/finalhandler": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-2.1.1.tgz", + "integrity": "sha512-S8KoZgRZN+a5rNwqTxlZZePjT/4cnm0ROV70LedRHZ0p8u9fRID0hJUZQpkKLzro8LfmC8sx23bY6tVNxv8pQA==", + "license": "MIT", + "peer": true, + "dependencies": { + "debug": "^4.4.0", + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "on-finished": "^2.4.1", + "parseurl": "^1.3.3", + "statuses": "^2.0.1" + }, + "engines": { + "node": ">= 18.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, "node_modules/find-up": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", @@ -3564,6 +4501,26 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/forwarded": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", + "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==", + "license": "MIT", + "peer": true, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/fresh": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/fresh/-/fresh-2.0.0.tgz", + "integrity": "sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A==", + "license": "MIT", + "peer": true, + "engines": { + "node": ">= 0.8" + } + }, "node_modules/fs.realpath": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", @@ -3590,7 +4547,6 @@ "version": "1.1.2", "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", - "dev": true, "license": "MIT", "funding": { "url": "https://github.com/sponsors/ljharb" @@ -3650,11 +4606,22 @@ "node": "6.* || 8.* || >= 10.*" } }, + "node_modules/get-east-asian-width": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/get-east-asian-width/-/get-east-asian-width-1.6.0.tgz", + "integrity": "sha512-QRbvDIbx6YklUe6RxeTeleMR0yv3cYH6PsPZHcnVn7xv7zO1BHN8r0XETu8n6Ye3Q+ahtSarc3WgtNWmehIBfA==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/get-intrinsic": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", - "dev": true, "license": "MIT", "dependencies": { "call-bind-apply-helpers": "^1.0.2", @@ -3679,7 +4646,6 @@ "version": "1.0.1", "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", - "dev": true, "license": "MIT", "dependencies": { "dunder-proto": "^1.0.1", @@ -3849,7 +4815,6 @@ "version": "1.2.0", "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", - "dev": true, "license": "MIT", "engines": { "node": ">= 0.4" @@ -3921,7 +4886,6 @@ "version": "1.1.0", "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", - "dev": true, "license": "MIT", "engines": { "node": ">= 0.4" @@ -3950,7 +4914,6 @@ "version": "2.0.4", "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.4.tgz", "integrity": "sha512-T2UbfbBEF32wiepXIsMlTW9+dDYC6wMh/t/vYA4tuOMKqWz/n3vr1NFSxQiyP+zk2mXsoMA/i/7qV6LKut1t1A==", - "dev": true, "license": "MIT", "dependencies": { "function-bind": "^1.1.2" @@ -3985,6 +4948,16 @@ "node": ">= 6" } }, + "node_modules/hono": { + "version": "4.12.32", + "resolved": "https://registry.npmjs.org/hono/-/hono-4.12.32.tgz", + "integrity": "sha512-XcuyW9qE2kJn07PkecMOBd5Vq/hMy7mmGw+idz1yblbg9N17ijJODrvPkn7/dwL3Kulj8LcRJ69DLOWf91dRUg==", + "license": "MIT", + "peer": true, + "engines": { + "node": ">=16.9.0" + } + }, "node_modules/html-escaper": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/html-escaper/-/html-escaper-2.0.2.tgz", @@ -3992,6 +4965,44 @@ "dev": true, "license": "MIT" }, + "node_modules/http-errors": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.1.tgz", + "integrity": "sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ==", + "license": "MIT", + "peer": true, + "dependencies": { + "depd": "~2.0.0", + "inherits": "~2.0.4", + "setprototypeof": "~1.2.0", + "statuses": "~2.0.2", + "toidentifier": "~1.0.1" + }, + "engines": { + "node": ">= 0.8" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/iconv-lite": { + "version": "0.7.3", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.7.3.tgz", + "integrity": "sha512-IKXpvIzjnC9XTAUbVBcMfGS0EPaIXtW6v+zr+RRp+hqULEpo0owZax6wyRwPOJbWbzjYspQwusTsfVr0ifh4uQ==", + "license": "MIT", + "peer": true, + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + }, + "engines": { + "node": ">=0.10.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, "node_modules/ieee754": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", @@ -4050,6 +5061,18 @@ "node": ">=0.8.19" } }, + "node_modules/indent-string": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/indent-string/-/indent-string-5.0.0.tgz", + "integrity": "sha512-m6FAo/spmsW2Ab2fU35JTYwtOKa2yAwXSwgjSv1TJzh4Mh7mC3lzAOVLBprb72XsTrgkEIsl7YrFNAiDiRhIGg==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/inflight": { "version": "1.0.6", "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", @@ -4066,9 +5089,227 @@ "version": "2.0.4", "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", - "dev": true, "license": "ISC" }, + "node_modules/ink": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/ink/-/ink-5.2.1.tgz", + "integrity": "sha512-BqcUyWrG9zq5HIwW6JcfFHsIYebJkWWb4fczNah1goUO0vv5vneIlfwuS85twyJ5hYR/y18FlAYUxrO9ChIWVg==", + "license": "MIT", + "dependencies": { + "@alcalzone/ansi-tokenize": "^0.1.3", + "ansi-escapes": "^7.0.0", + "ansi-styles": "^6.2.1", + "auto-bind": "^5.0.1", + "chalk": "^5.3.0", + "cli-boxes": "^3.0.0", + "cli-cursor": "^4.0.0", + "cli-truncate": "^4.0.0", + "code-excerpt": "^4.0.0", + "es-toolkit": "^1.22.0", + "indent-string": "^5.0.0", + "is-in-ci": "^1.0.0", + "patch-console": "^2.0.0", + "react-reconciler": "^0.29.0", + "scheduler": "^0.23.0", + "signal-exit": "^3.0.7", + "slice-ansi": "^7.1.0", + "stack-utils": "^2.0.6", + "string-width": "^7.2.0", + "type-fest": "^4.27.0", + "widest-line": "^5.0.0", + "wrap-ansi": "^9.0.0", + "ws": "^8.18.0", + "yoga-layout": "~3.2.1" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@types/react": ">=18.0.0", + "react": ">=18.0.0", + "react-devtools-core": "^4.19.1" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "react-devtools-core": { + "optional": true + } + } + }, + "node_modules/ink-select-input": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/ink-select-input/-/ink-select-input-6.2.0.tgz", + "integrity": "sha512-304fZXxkpYxJ9si5lxRCaX01GNlmPBgOZumXXRnPYbHW/iI31cgQynqk2tRypGLOF1cMIwPUzL2LSm6q4I5rQQ==", + "license": "MIT", + "dependencies": { + "figures": "^6.1.0", + "to-rotated": "^1.0.0" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "ink": ">=5.0.0", + "react": ">=18.0.0" + } + }, + "node_modules/ink-spinner": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/ink-spinner/-/ink-spinner-5.0.0.tgz", + "integrity": "sha512-EYEasbEjkqLGyPOUc8hBJZNuC5GvXGMLu0w5gdTNskPc7Izc5vO3tdQEYnzvshucyGCBXc86ig0ujXPMWaQCdA==", + "license": "MIT", + "dependencies": { + "cli-spinners": "^2.7.0" + }, + "engines": { + "node": ">=14.16" + }, + "peerDependencies": { + "ink": ">=4.0.0", + "react": ">=18.0.0" + } + }, + "node_modules/ink-testing-library": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/ink-testing-library/-/ink-testing-library-4.0.0.tgz", + "integrity": "sha512-yF92kj3pmBvk7oKbSq5vEALO//o7Z9Ck/OaLNlkzXNeYdwfpxMQkSowGTFUCS5MSu9bWfSZMewGpp7bFc66D7Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@types/react": ">=18.0.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/ink-text-input": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/ink-text-input/-/ink-text-input-6.0.0.tgz", + "integrity": "sha512-Fw64n7Yha5deb1rHY137zHTAbSTNelUKuB5Kkk2HACXEtwIHBCf9OH2tP/LQ9fRYTl1F0dZgbW0zPnZk6FA9Lw==", + "license": "MIT", + "dependencies": { + "chalk": "^5.3.0", + "type-fest": "^4.18.2" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "ink": ">=5", + "react": ">=18" + } + }, + "node_modules/ink-text-input/node_modules/chalk": { + "version": "5.6.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-5.6.2.tgz", + "integrity": "sha512-7NzBL0rN6fMUW+f7A6Io4h40qQlG+xGmtMxfbnH/K7TAtt8JQWVQK+6g0UXKMeVJoyV5EkkNsErQ8pVD3bLHbA==", + "license": "MIT", + "engines": { + "node": "^12.17.0 || ^14.13 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/ink/node_modules/ansi-regex": { + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.2.2.tgz", + "integrity": "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-regex?sponsor=1" + } + }, + "node_modules/ink/node_modules/ansi-styles": { + "version": "6.2.3", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.3.tgz", + "integrity": "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/ink/node_modules/chalk": { + "version": "5.6.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-5.6.2.tgz", + "integrity": "sha512-7NzBL0rN6fMUW+f7A6Io4h40qQlG+xGmtMxfbnH/K7TAtt8JQWVQK+6g0UXKMeVJoyV5EkkNsErQ8pVD3bLHbA==", + "license": "MIT", + "engines": { + "node": "^12.17.0 || ^14.13 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/ink/node_modules/emoji-regex": { + "version": "10.6.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-10.6.0.tgz", + "integrity": "sha512-toUI84YS5YmxW219erniWD0CIVOo46xGKColeNQRgOzDorgBi1v4D71/OFzgD9GO2UGKIv1C3Sp8DAn0+j5w7A==", + "license": "MIT" + }, + "node_modules/ink/node_modules/string-width": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-7.2.0.tgz", + "integrity": "sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ==", + "license": "MIT", + "dependencies": { + "emoji-regex": "^10.3.0", + "get-east-asian-width": "^1.0.0", + "strip-ansi": "^7.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/ink/node_modules/strip-ansi": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.2.0.tgz", + "integrity": "sha512-yDPMNjp4WyfYBkHnjIRLfca1i6KMyGCtsVgoKe/z1+6vukgaENdgGBZt+ZmKPc4gavvEZ5OgHfHdrazhgNyG7w==", + "license": "MIT", + "dependencies": { + "ansi-regex": "^6.2.2" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/strip-ansi?sponsor=1" + } + }, + "node_modules/ink/node_modules/wrap-ansi": { + "version": "9.0.2", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-9.0.2.tgz", + "integrity": "sha512-42AtmgqjV+X1VpdOfyTGOYRi0/zsoLqtXQckTmqTeybT+BDIbM/Guxo7x3pE2vtpr1ok6xRqM9OpBe+Jyoqyww==", + "license": "MIT", + "dependencies": { + "ansi-styles": "^6.2.1", + "string-width": "^7.0.0", + "strip-ansi": "^7.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, "node_modules/internal-slot": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/internal-slot/-/internal-slot-1.1.0.tgz", @@ -4084,6 +5325,26 @@ "node": ">= 0.4" } }, + "node_modules/ip-address": { + "version": "10.3.1", + "resolved": "https://registry.npmjs.org/ip-address/-/ip-address-10.3.1.tgz", + "integrity": "sha512-1e9d3kb97NHJTIJDZW9rKqW2h6+dFa50Dy0fpPSMQp2ADje5gvKsXmdiK6dwY5t76TaTt5+P5N1Y/LoToIxP6g==", + "license": "MIT", + "peer": true, + "engines": { + "node": ">= 12" + } + }, + "node_modules/ipaddr.js": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", + "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==", + "license": "MIT", + "peer": true, + "engines": { + "node": ">= 0.10" + } + }, "node_modules/is-array-buffer": { "version": "3.0.5", "resolved": "https://registry.npmjs.org/is-array-buffer/-/is-array-buffer-3.0.5.tgz", @@ -4232,6 +5493,21 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/is-docker": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-docker/-/is-docker-3.0.0.tgz", + "integrity": "sha512-eljcgEDlEns/7AXFosB5K/2nCM4P7FQPkGc/DWLy5rmFEWvZayGrik1d9/QIY5nJ4f9YsVvBkA6kJpHn9rISdQ==", + "license": "MIT", + "bin": { + "is-docker": "cli.js" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/is-document.all": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/is-document.all/-/is-document.all-1.0.0.tgz", @@ -4317,6 +5593,39 @@ "node": ">=0.10.0" } }, + "node_modules/is-in-ci": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-in-ci/-/is-in-ci-1.0.0.tgz", + "integrity": "sha512-eUuAjybVTHMYWm/U+vBO1sY/JOCgoPCXRxzdju0K+K0BiGW0SChEL1MLC0PoCIR1OlPo5YAp8HuQoUlsWEICwg==", + "license": "MIT", + "bin": { + "is-in-ci": "cli.js" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-inside-container": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-inside-container/-/is-inside-container-1.0.0.tgz", + "integrity": "sha512-KIYLCCJghfHZxqjYBE7rEy0OBuTd5xCHS7tHVgvCLkx7StIoaxwNW3hCALgEUjFfeRk+MG/Qxmp/vtETEF3tRA==", + "license": "MIT", + "dependencies": { + "is-docker": "^3.0.0" + }, + "bin": { + "is-inside-container": "cli.js" + }, + "engines": { + "node": ">=14.16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/is-map": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/is-map/-/is-map-2.0.3.tgz", @@ -4396,6 +5705,13 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/is-promise": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/is-promise/-/is-promise-4.0.0.tgz", + "integrity": "sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ==", + "license": "MIT", + "peer": true + }, "node_modules/is-regex": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.2.1.tgz", @@ -4495,6 +5811,18 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/is-unicode-supported": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-unicode-supported/-/is-unicode-supported-2.1.0.tgz", + "integrity": "sha512-mE00Gnza5EEB3Ds0HfMyllZzbBrmLOX3vfWoj9A9PEnTfratQ/BcaJOuMhnkhjXvb2+FkY3VuHqtAGpTPmglFQ==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/is-weakmap": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/is-weakmap/-/is-weakmap-2.0.2.tgz", @@ -4541,6 +5869,21 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/is-wsl": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/is-wsl/-/is-wsl-3.1.1.tgz", + "integrity": "sha512-e6rvdUCiQCAuumZslxRJWR/Doq4VpPR82kqclvcS0efgt430SlGIk05vdCN58+VrzgtIcfNODjozVielycD4Sw==", + "license": "MIT", + "dependencies": { + "is-inside-container": "^1.0.0" + }, + "engines": { + "node": ">=16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/isarray": { "version": "2.0.5", "resolved": "https://registry.npmjs.org/isarray/-/isarray-2.0.5.tgz", @@ -4552,7 +5895,6 @@ "version": "2.0.0", "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", - "dev": true, "license": "ISC" }, "node_modules/istanbul-lib-coverage": { @@ -4622,6 +5964,16 @@ "jiti": "lib/jiti-cli.mjs" } }, + "node_modules/jose": { + "version": "6.2.4", + "resolved": "https://registry.npmjs.org/jose/-/jose-6.2.4.tgz", + "integrity": "sha512-N8acGzVsQy6M/fjFcxtysNc4Q379TcM5dM/qKkNtsHFji88yANnXTr7BLeP75iPnFwBfQzM/jg2BZ9+HZrHCZA==", + "license": "MIT", + "peer": true, + "funding": { + "url": "https://github.com/sponsors/panva" + } + }, "node_modules/joycon": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/joycon/-/joycon-3.1.1.tgz", @@ -4669,6 +6021,20 @@ "dev": true, "license": "MIT" }, + "node_modules/json-schema-to-ts": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/json-schema-to-ts/-/json-schema-to-ts-3.1.1.tgz", + "integrity": "sha512-+DWg8jCJG2TEnpy7kOm/7/AxaYoaRbjVB4LFZLySZlWn8exGs3A4OLJR966cVvU26N7X9TWxl+Jsw7dzAqKT6g==", + "license": "MIT", + "peer": true, + "dependencies": { + "@babel/runtime": "^7.18.3", + "ts-algebra": "^2.0.0" + }, + "engines": { + "node": ">=16" + } + }, "node_modules/json-schema-traverse": { "version": "0.4.1", "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", @@ -4676,6 +6042,13 @@ "dev": true, "license": "MIT" }, + "node_modules/json-schema-typed": { + "version": "8.0.2", + "resolved": "https://registry.npmjs.org/json-schema-typed/-/json-schema-typed-8.0.2.tgz", + "integrity": "sha512-fQhoXdcvc3V28x7C7BMs4P5+kNlgUURe2jmUT1T//oBRMDrqy1QPelJimwZGo7Hg9VPV3EQV5Bnq4hbFy2vetA==", + "license": "BSD-2-Clause", + "peer": true + }, "node_modules/json-stable-stringify-without-jsonify": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz", @@ -5041,7 +6414,6 @@ "version": "1.4.0", "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz", "integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==", - "dev": true, "license": "MIT", "dependencies": { "js-tokens": "^3.0.0 || ^4.0.0" @@ -5054,7 +6426,6 @@ "version": "4.0.0", "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", - "dev": true, "license": "MIT" }, "node_modules/magic-string": { @@ -5112,12 +6483,25 @@ "version": "1.1.0", "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", - "dev": true, "license": "MIT", "engines": { "node": ">= 0.4" } }, + "node_modules/media-typer": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-1.1.1.tgz", + "integrity": "sha512-yz3xRaG20c6/BOzvYoDaGtPmGscs7YivItZEEqe6GbwNfHuxu9YNmvnEkMzKldAGY4/80pRcQRZSEnhquk9XuQ==", + "license": "MIT", + "peer": true, + "engines": { + "node": ">= 0.8" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, "node_modules/meow": { "version": "14.1.0", "resolved": "https://registry.npmjs.org/meow/-/meow-14.1.0.tgz", @@ -5131,6 +6515,19 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/merge-descriptors": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-2.0.0.tgz", + "integrity": "sha512-Snk314V5ayFLhp3fkUREub6WtjBfPdCPY1Ln8/8munuLuiYhsABgBVWsozAG+MWMbVEvcdcpbi9R7ww22l9Q3g==", + "license": "MIT", + "peer": true, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/merge2": { "version": "1.4.1", "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", @@ -5155,6 +6552,42 @@ "node": ">=8.6" } }, + "node_modules/mime-db": { + "version": "1.54.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.54.0.tgz", + "integrity": "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==", + "license": "MIT", + "peer": true, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-3.0.2.tgz", + "integrity": "sha512-Lbgzdk0h4juoQ9fCKXW4by0UJqj+nOOrI9MJ1sSj4nI8aI2eo1qmvQEie4VD1glsS250n15LsWsYtCugiStS5A==", + "license": "MIT", + "peer": true, + "dependencies": { + "mime-db": "^1.54.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/mimic-fn": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-2.1.0.tgz", + "integrity": "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, "node_modules/minimatch": { "version": "3.1.5", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", @@ -5181,7 +6614,6 @@ "version": "2.1.3", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", - "dev": true, "license": "MIT" }, "node_modules/mylas": { @@ -5224,6 +6656,16 @@ "dev": true, "license": "MIT" }, + "node_modules/negotiator": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-1.0.0.tgz", + "integrity": "sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg==", + "license": "MIT", + "peer": true, + "engines": { + "node": ">= 0.6" + } + }, "node_modules/neostandard": { "version": "0.13.0", "resolved": "https://registry.npmjs.org/neostandard/-/neostandard-0.13.0.tgz", @@ -5375,7 +6817,6 @@ "version": "4.1.1", "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", - "dev": true, "license": "MIT", "engines": { "node": ">=0.10.0" @@ -5385,7 +6826,6 @@ "version": "1.13.4", "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz", "integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==", - "dev": true, "license": "MIT", "engines": { "node": ">= 0.4" @@ -5518,16 +6958,61 @@ "node": ">=14.0.0" } }, + "node_modules/on-finished": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz", + "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==", + "license": "MIT", + "peer": true, + "dependencies": { + "ee-first": "1.1.1" + }, + "engines": { + "node": ">= 0.8" + } + }, "node_modules/once": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", - "dev": true, "license": "ISC", "dependencies": { "wrappy": "1" } }, + "node_modules/onetime": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/onetime/-/onetime-5.1.2.tgz", + "integrity": "sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==", + "license": "MIT", + "dependencies": { + "mimic-fn": "^2.1.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/open": { + "version": "10.2.0", + "resolved": "https://registry.npmjs.org/open/-/open-10.2.0.tgz", + "integrity": "sha512-YgBpdJHPyQ2UE5x+hlSXcnejzAvD0b22U2OuAP+8OnlJT+PjWPxtgmGqKKc+RgTM63U9gN0YzrYc71R2WT/hTA==", + "license": "MIT", + "dependencies": { + "default-browser": "^5.2.1", + "define-lazy-prop": "^3.0.0", + "is-inside-container": "^1.0.0", + "wsl-utils": "^0.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/optionator": { "version": "0.9.4", "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz", @@ -5623,6 +7108,25 @@ "node": ">=6" } }, + "node_modules/parseurl": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", + "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==", + "license": "MIT", + "peer": true, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/patch-console": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/patch-console/-/patch-console-2.0.0.tgz", + "integrity": "sha512-0YNdUceMdaQwoKce1gatDScmMo5pu/tfABfnzEqeG0gtTmd7mh/WcwgUjtAeOU7N8nFFlbQBnFK2gXW5fGvmMA==", + "license": "MIT", + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + } + }, "node_modules/path-exists": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", @@ -5637,7 +7141,6 @@ "version": "3.1.1", "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", - "dev": true, "license": "MIT", "engines": { "node": ">=8" @@ -5650,6 +7153,17 @@ "dev": true, "license": "MIT" }, + "node_modules/path-to-regexp": { + "version": "8.4.2", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-8.4.2.tgz", + "integrity": "sha512-qRcuIdP69NPm4qbACK+aDogI5CBDMi1jKe0ry5rSQJz8JVLsC7jV8XpiJjGRLLol3N+R5ihGYcrPLTno6pAdBA==", + "license": "MIT", + "peer": true, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, "node_modules/path-type": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/path-type/-/path-type-6.0.0.tgz", @@ -5768,6 +7282,16 @@ "dev": true, "license": "MIT" }, + "node_modules/pkce-challenge": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/pkce-challenge/-/pkce-challenge-5.0.1.tgz", + "integrity": "sha512-wQ0b/W4Fr01qtpHlqSqspcj3EhBvimsdh0KlHhH8HRZnMsEa0ea2fTULOXOS9ccQr3om+GcGRk4e+isrZWV8qQ==", + "license": "MIT", + "peer": true, + "engines": { + "node": ">=16.20.0" + } + }, "node_modules/plimit-lit": { "version": "1.6.1", "resolved": "https://registry.npmjs.org/plimit-lit/-/plimit-lit-1.6.1.tgz", @@ -5888,6 +7412,20 @@ "react-is": "^16.13.1" } }, + "node_modules/proxy-addr": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", + "integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==", + "license": "MIT", + "peer": true, + "dependencies": { + "forwarded": "0.2.0", + "ipaddr.js": "1.9.1" + }, + "engines": { + "node": ">= 0.10" + } + }, "node_modules/pump": { "version": "3.0.4", "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.4.tgz", @@ -5909,6 +7447,23 @@ "node": ">=6" } }, + "node_modules/qs": { + "version": "6.15.3", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.15.3.tgz", + "integrity": "sha512-O9gl3zCl5h5blw1KGUzQKhA5oUXSl8rwUIM5o0S3nCXMliSvy5Dzx7/DJcI+SwgICv+IneSZwhBh1oSyEHA71A==", + "license": "BSD-3-Clause", + "peer": true, + "dependencies": { + "es-define-property": "^1.0.1", + "side-channel": "^1.1.1" + }, + "engines": { + "node": ">=0.6" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/queue-lit": { "version": "1.5.2", "resolved": "https://registry.npmjs.org/queue-lit/-/queue-lit-1.5.2.tgz", @@ -5947,6 +7502,48 @@ "dev": true, "license": "MIT" }, + "node_modules/range-parser": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.3.0.tgz", + "integrity": "sha512-hek2mFQpPuI4E1BBKrSto+BU3e3x4xuarsbiwr3+lf7p44juvFMV0XFWQAP3xUyqXA4RrXLIoaSUGbSt056ZMw==", + "license": "MIT", + "peer": true, + "engines": { + "node": ">= 0.6" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/raw-body": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-3.0.2.tgz", + "integrity": "sha512-K5zQjDllxWkf7Z5xJdV0/B0WTNqx6vxG70zJE4N0kBs4LovmEYWJzQGxC9bS9RAKu3bgM40lrd5zoLJ12MQ5BA==", + "license": "MIT", + "peer": true, + "dependencies": { + "bytes": "~3.1.2", + "http-errors": "~2.0.1", + "iconv-lite": "~0.7.0", + "unpipe": "~1.0.0" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/react": { + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react/-/react-18.3.1.tgz", + "integrity": "sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==", + "license": "MIT", + "dependencies": { + "loose-envify": "^1.1.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/react-is": { "version": "16.13.1", "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", @@ -5954,6 +7551,22 @@ "dev": true, "license": "MIT" }, + "node_modules/react-reconciler": { + "version": "0.29.2", + "resolved": "https://registry.npmjs.org/react-reconciler/-/react-reconciler-0.29.2.tgz", + "integrity": "sha512-zZQqIiYgDCTP/f1N/mAR10nJGrPD2ZR+jDSEsKWJHYC7Cm2wodlwbR3upZRdC3cjIjSlTLNVyO7Iu0Yy7t2AYg==", + "license": "MIT", + "dependencies": { + "loose-envify": "^1.1.0", + "scheduler": "^0.23.2" + }, + "engines": { + "node": ">=0.10.0" + }, + "peerDependencies": { + "react": "^18.3.1" + } + }, "node_modules/readable-stream": { "version": "4.7.0", "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-4.7.0.tgz", @@ -6048,6 +7661,16 @@ "node": ">=0.10.0" } }, + "node_modules/require-from-string": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz", + "integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==", + "license": "MIT", + "peer": true, + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/resolve": { "version": "2.0.0-next.7", "resolved": "https://registry.npmjs.org/resolve/-/resolve-2.0.0-next.7.tgz", @@ -6089,7 +7712,23 @@ "dev": true, "license": "MIT", "funding": { - "url": "https://github.com/privatenumber/resolve-pkg-maps?sponsor=1" + "url": "https://github.com/privatenumber/resolve-pkg-maps?sponsor=1" + } + }, + "node_modules/restore-cursor": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-4.0.0.tgz", + "integrity": "sha512-I9fPXU9geO9bHOt9pHHOhOkYerIMsmVaWB0rA2AI9ERh/+x/i7MV5HKBNrg+ljO5eoPVgCcnFuRjJ9uH6I/3eg==", + "license": "MIT", + "dependencies": { + "onetime": "^5.1.0", + "signal-exit": "^3.0.2" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, "node_modules/reusify": { @@ -6137,6 +7776,35 @@ "@rolldown/binding-win32-x64-msvc": "1.1.5" } }, + "node_modules/router": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/router/-/router-2.2.0.tgz", + "integrity": "sha512-nLTrUKm2UyiL7rlhapu/Zl45FwNgkZGaCpZbIHajDYgwlJCOzLSk+cIPAnsEqV955GjILJnKbdQC1nVPz+gAYQ==", + "license": "MIT", + "peer": true, + "dependencies": { + "debug": "^4.4.0", + "depd": "^2.0.0", + "is-promise": "^4.0.0", + "parseurl": "^1.3.3", + "path-to-regexp": "^8.0.0" + }, + "engines": { + "node": ">= 18" + } + }, + "node_modules/run-applescript": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/run-applescript/-/run-applescript-7.1.0.tgz", + "integrity": "sha512-DPe5pVFaAsinSaV6QjQ6gdiedWDcRCbUuiQfQa2wmWV7+xC9bGulGI8+TdRmoFkAPaBXk8CrAbnlY2ISniJ47Q==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/run-parallel": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", @@ -6247,6 +7915,22 @@ "node": ">=10" } }, + "node_modules/safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", + "license": "MIT", + "peer": true + }, + "node_modules/scheduler": { + "version": "0.23.2", + "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.23.2.tgz", + "integrity": "sha512-UOShsPwz7NrMUqhR6t0hWjFduvOzbtv7toDH1/hIrfRNIDBnnBWd0CwJTGvTpngVlmwGCdP9/Zl/tVrDqcuYzQ==", + "license": "MIT", + "dependencies": { + "loose-envify": "^1.1.0" + } + }, "node_modules/secure-json-parse": { "version": "2.7.0", "resolved": "https://registry.npmjs.org/secure-json-parse/-/secure-json-parse-2.7.0.tgz", @@ -6264,6 +7948,53 @@ "semver": "bin/semver.js" } }, + "node_modules/send": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/send/-/send-1.2.1.tgz", + "integrity": "sha512-1gnZf7DFcoIcajTjTwjwuDjzuz4PPcY2StKPlsGAQ1+YH20IRVrBaXSWmdjowTJ6u8Rc01PoYOGHXfP1mYcZNQ==", + "license": "MIT", + "peer": true, + "dependencies": { + "debug": "^4.4.3", + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "etag": "^1.8.1", + "fresh": "^2.0.0", + "http-errors": "^2.0.1", + "mime-types": "^3.0.2", + "ms": "^2.1.3", + "on-finished": "^2.4.1", + "range-parser": "^1.2.1", + "statuses": "^2.0.2" + }, + "engines": { + "node": ">= 18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/serve-static": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-2.2.1.tgz", + "integrity": "sha512-xRXBn0pPqQTVQiC8wyQrKs2MOlX24zQ0POGaj0kultvoOCstBQM5yvOhAVSUwOMjQtTvsPWoNCHfPGwaaQJhTw==", + "license": "MIT", + "peer": true, + "dependencies": { + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "parseurl": "^1.3.3", + "send": "^1.2.0" + }, + "engines": { + "node": ">= 18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, "node_modules/set-function-length": { "version": "1.2.2", "resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.2.2.tgz", @@ -6313,11 +8044,17 @@ "node": ">= 0.4" } }, + "node_modules/setprototypeof": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", + "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==", + "license": "ISC", + "peer": true + }, "node_modules/shebang-command": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", - "dev": true, "license": "MIT", "dependencies": { "shebang-regex": "^3.0.0" @@ -6330,7 +8067,6 @@ "version": "3.0.0", "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", - "dev": true, "license": "MIT", "engines": { "node": ">=8" @@ -6340,7 +8076,6 @@ "version": "1.1.1", "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.1.tgz", "integrity": "sha512-6x6dK6zJdpTzF4sQeNYxwtvBzf6Eg4GtlesS94HOvTudUeyK2WXAaIfmDgsyslYrRBeFIlsi54AYsFGUuhmvrQ==", - "dev": true, "license": "MIT", "dependencies": { "es-errors": "^1.3.0", @@ -6360,7 +8095,6 @@ "version": "1.0.1", "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.1.tgz", "integrity": "sha512-mjn/0bi/oUURjc5Xl7IaWi/OJJJumuoJFQJfDDyO46+hBWsfaVM65TBHq2eoZBhzl9EchxOijpkbRC8SVBQU0w==", - "dev": true, "license": "MIT", "dependencies": { "es-errors": "^1.3.0", @@ -6377,7 +8111,6 @@ "version": "1.0.1", "resolved": "https://registry.npmjs.org/side-channel-map/-/side-channel-map-1.0.1.tgz", "integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==", - "dev": true, "license": "MIT", "dependencies": { "call-bound": "^1.0.2", @@ -6396,7 +8129,6 @@ "version": "1.0.2", "resolved": "https://registry.npmjs.org/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz", "integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==", - "dev": true, "license": "MIT", "dependencies": { "call-bound": "^1.0.2", @@ -6419,6 +8151,12 @@ "dev": true, "license": "ISC" }, + "node_modules/signal-exit": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", + "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==", + "license": "ISC" + }, "node_modules/slash": { "version": "5.1.0", "resolved": "https://registry.npmjs.org/slash/-/slash-5.1.0.tgz", @@ -6432,6 +8170,49 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/slice-ansi": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-7.1.2.tgz", + "integrity": "sha512-iOBWFgUX7caIZiuutICxVgX1SdxwAVFFKwt1EvMYYec/NWO5meOJ6K5uQxhrYBdQJne4KxiqZc+KptFOWFSI9w==", + "license": "MIT", + "dependencies": { + "ansi-styles": "^6.2.1", + "is-fullwidth-code-point": "^5.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/chalk/slice-ansi?sponsor=1" + } + }, + "node_modules/slice-ansi/node_modules/ansi-styles": { + "version": "6.2.3", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.3.tgz", + "integrity": "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/slice-ansi/node_modules/is-fullwidth-code-point": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-5.1.0.tgz", + "integrity": "sha512-5XHYaSyiqADb4RnZ1Bdad6cPp8Toise4TzEjcOYDHZkTCbKgiUl7WTUCpNWHuxmDt91wnsZBc9xinNzopv3JMQ==", + "license": "MIT", + "dependencies": { + "get-east-asian-width": "^1.3.1" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/sonic-boom": { "version": "3.8.1", "resolved": "https://registry.npmjs.org/sonic-boom/-/sonic-boom-3.8.1.tgz", @@ -6462,6 +8243,27 @@ "node": ">= 10.x" } }, + "node_modules/stack-utils": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/stack-utils/-/stack-utils-2.0.6.tgz", + "integrity": "sha512-XlkWvfIm6RmsWtNJx+uqtKLS8eqFbxUg0ZzLXqY0caEy9l7hruX8IpiDnjsLavoBgqCCR71TqWO8MaXYheJ3RQ==", + "license": "MIT", + "dependencies": { + "escape-string-regexp": "^2.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/stack-utils/node_modules/escape-string-regexp": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-2.0.0.tgz", + "integrity": "sha512-UpzcLCXolUWcNu5HtVMHYdXJjArjsF9C0aNnquZYY4uW/Vu0miy5YoWvbV345HauVvcAUnpRuhMMcqTcGOY2+w==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, "node_modules/stackback": { "version": "0.0.2", "resolved": "https://registry.npmjs.org/stackback/-/stackback-0.0.2.tgz", @@ -6469,6 +8271,27 @@ "dev": true, "license": "MIT" }, + "node_modules/standardwebhooks": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/standardwebhooks/-/standardwebhooks-1.0.0.tgz", + "integrity": "sha512-BbHGOQK9olHPMvQNHWul6MYlrRTAOKn03rOe4A8O3CLWhNf4YHBqq2HJKKC+sfqpxiBY52pNeesD6jIiLDz8jg==", + "license": "MIT", + "peer": true, + "dependencies": { + "@stablelib/base64": "^1.0.0", + "fast-sha256": "^1.3.0" + } + }, + "node_modules/statuses": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.2.tgz", + "integrity": "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==", + "license": "MIT", + "peer": true, + "engines": { + "node": ">= 0.8" + } + }, "node_modules/std-env": { "version": "4.2.0", "resolved": "https://registry.npmjs.org/std-env/-/std-env-4.2.0.tgz", @@ -6788,6 +8611,35 @@ "node": ">=8.0" } }, + "node_modules/to-rotated": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/to-rotated/-/to-rotated-1.0.0.tgz", + "integrity": "sha512-KsEID8AfgUy+pxVRLsWp0VzCa69wxzUDZnzGbyIST/bcgcrMvTYoFBX/QORH4YApoD89EDuUovx4BTdpOn319Q==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/toidentifier": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz", + "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==", + "license": "MIT", + "peer": true, + "engines": { + "node": ">=0.6" + } + }, + "node_modules/ts-algebra": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ts-algebra/-/ts-algebra-2.0.0.tgz", + "integrity": "sha512-FPAhNPFMrkwz76P7cdjdmiShwMynZYN6SgOujD1urY4oNm80Ou9oMdmbR45LotcKOXoy7wSmHkRFE6Mxbrhefw==", + "license": "MIT", + "peer": true + }, "node_modules/ts-api-utils": { "version": "2.5.0", "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-2.5.0.tgz", @@ -6943,6 +8795,51 @@ "node": ">= 0.8.0" } }, + "node_modules/type-fest": { + "version": "4.41.0", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-4.41.0.tgz", + "integrity": "sha512-TeTSQ6H5YHvpqVwBRcnLDCBnDOHWYu7IvGbHT6N8AOymcr9PJGjc1GTtiWZTYg0NCgYwvnYWEkVChQAr9bjfwA==", + "license": "(MIT OR CC0-1.0)", + "engines": { + "node": ">=16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/type-is": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/type-is/-/type-is-2.1.0.tgz", + "integrity": "sha512-faYHw0anBbc/kWF3zFTEnxSFOAGUX9GFbOBthvDdLsIlEoWOFOtS0zgCiQYwIskL9iGXZL3kAXD8OoZ4GmMATA==", + "license": "MIT", + "peer": true, + "dependencies": { + "content-type": "^2.0.0", + "media-typer": "^1.1.0", + "mime-types": "^3.0.0" + }, + "engines": { + "node": ">= 18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/type-is/node_modules/content-type": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/content-type/-/content-type-2.0.0.tgz", + "integrity": "sha512-j/O/d7GcZCyNl7/hwZAb606rzqkyvaDctLmckbxLzHvFBzTJHuGEdodATcP3yIRoDrLHkIATJuvzbFlp/ki2cQ==", + "license": "MIT", + "peer": true, + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, "node_modules/typed-array-buffer": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/typed-array-buffer/-/typed-array-buffer-1.0.3.tgz", @@ -7098,6 +8995,16 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/unpipe": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", + "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==", + "license": "MIT", + "peer": true, + "engines": { + "node": ">= 0.8" + } + }, "node_modules/uri-js": { "version": "4.4.1", "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", @@ -7115,6 +9022,16 @@ "dev": true, "license": "MIT" }, + "node_modules/vary": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", + "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==", + "license": "MIT", + "peer": true, + "engines": { + "node": ">= 0.8" + } + }, "node_modules/vite": { "version": "8.1.5", "resolved": "https://registry.npmjs.org/vite/-/vite-8.1.5.tgz", @@ -7313,7 +9230,6 @@ "version": "2.0.2", "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", - "dev": true, "license": "ISC", "dependencies": { "isexe": "^2.0.0" @@ -7431,6 +9347,71 @@ "node": ">=8" } }, + "node_modules/widest-line": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/widest-line/-/widest-line-5.0.0.tgz", + "integrity": "sha512-c9bZp7b5YtRj2wOe6dlj32MK+Bx/M/d+9VB2SHM1OtsUHR0aV0tdP6DWh/iMt0kWi1t5g1Iudu6hQRNd1A4PVA==", + "license": "MIT", + "dependencies": { + "string-width": "^7.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/widest-line/node_modules/ansi-regex": { + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.2.2.tgz", + "integrity": "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-regex?sponsor=1" + } + }, + "node_modules/widest-line/node_modules/emoji-regex": { + "version": "10.6.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-10.6.0.tgz", + "integrity": "sha512-toUI84YS5YmxW219erniWD0CIVOo46xGKColeNQRgOzDorgBi1v4D71/OFzgD9GO2UGKIv1C3Sp8DAn0+j5w7A==", + "license": "MIT" + }, + "node_modules/widest-line/node_modules/string-width": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-7.2.0.tgz", + "integrity": "sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ==", + "license": "MIT", + "dependencies": { + "emoji-regex": "^10.3.0", + "get-east-asian-width": "^1.0.0", + "strip-ansi": "^7.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/widest-line/node_modules/strip-ansi": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.2.0.tgz", + "integrity": "sha512-yDPMNjp4WyfYBkHnjIRLfca1i6KMyGCtsVgoKe/z1+6vukgaENdgGBZt+ZmKPc4gavvEZ5OgHfHdrazhgNyG7w==", + "license": "MIT", + "dependencies": { + "ansi-regex": "^6.2.2" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/strip-ansi?sponsor=1" + } + }, "node_modules/word-wrap": { "version": "1.2.5", "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz", @@ -7463,9 +9444,44 @@ "version": "1.0.2", "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", - "dev": true, "license": "ISC" }, + "node_modules/ws": { + "version": "8.21.1", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.21.1.tgz", + "integrity": "sha512-+0NTnW77fFN/DjQi6k/Sq/Yvk4Sgajw7urW8V+asjXnRgDs9gyGkdb7EzgfhA4goXsRIZKE28fzIXBHEzhuiWw==", + "license": "MIT", + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": ">=5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + }, + "node_modules/wsl-utils": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/wsl-utils/-/wsl-utils-0.1.0.tgz", + "integrity": "sha512-h3Fbisa2nKGPxCpm89Hk33lBLsnaGBvctQopaBSOW/uIs6FTe1ATyAnKFJrzVs9vpGdsTe73WF3V4lIsk4Gacw==", + "license": "MIT", + "dependencies": { + "is-wsl": "^3.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/y18n": { "version": "5.0.8", "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", @@ -7517,6 +9533,32 @@ "funding": { "url": "https://github.com/sponsors/sindresorhus" } + }, + "node_modules/yoga-layout": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/yoga-layout/-/yoga-layout-3.2.1.tgz", + "integrity": "sha512-0LPOt3AxKqMdFBZA3HBAt/t/8vIKq7VaQYbuA8WxCgung+p9TVyKRYdpvCb80HcdTN2NkbIKbhNwKUfm3tQywQ==", + "license": "MIT" + }, + "node_modules/zod": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/zod/-/zod-4.4.3.tgz", + "integrity": "sha512-ytENFjIJFl2UwYglde2jchW2Hwm4GJFLDiSXWdTrJQBIN9Fcyp7n4DhxJEiWNAJMV1/BqWfW/kkg71UDcHJyTQ==", + "license": "MIT", + "peer": true, + "funding": { + "url": "https://github.com/sponsors/colinhacks" + } + }, + "node_modules/zod-to-json-schema": { + "version": "3.25.2", + "resolved": "https://registry.npmjs.org/zod-to-json-schema/-/zod-to-json-schema-3.25.2.tgz", + "integrity": "sha512-O/PgfnpT1xKSDeQYSCfRI5Gy3hPf91mKVDuYLUHZJMiDFptvP41MSnWofm8dnCm0256ZNfZIM7DSzuSMAFnjHA==", + "license": "ISC", + "peer": true, + "peerDependencies": { + "zod": "^3.25.28 || ^4" + } } } } diff --git a/package.json b/package.json index 38ded7b..b2ee5b2 100644 --- a/package.json +++ b/package.json @@ -34,7 +34,8 @@ "src", "!src/bin", "!test", - "!**/*.test.ts" + "!**/*.test.ts", + "!**/*.test.tsx" ], "scripts": { "build": "npm run build:entrypoints", @@ -76,17 +77,26 @@ } }, "dependencies": { - "minimist": "^1.2.8" + "@anthropic-ai/claude-agent-sdk": "^0.3.216", + "ink": "^5.2.1", + "ink-select-input": "^6.2.0", + "ink-spinner": "^5.0.0", + "ink-text-input": "^6.0.0", + "minimist": "^1.2.8", + "open": "^10.1.0", + "react": "^18.3.1" }, "devDependencies": { "@types/minimist": "^1.2.5", "@types/node": "^24.10.9", + "@types/react": "^18.3.27", "@vitest/coverage-v8": "^4.1.10", "del-cli": "^7.0.0", "eslint": "^9.31.0", "eslint-plugin-import": "^2.32.0", "eslint-plugin-simple-import-sort": "^12.1.1", "eslint-plugin-unused-imports": "^4.1.4", + "ink-testing-library": "^4.0.0", "jiti": "^2.4.2", "landlubber": "^2.0.0", "neostandard": "^0.13.0", diff --git a/tsconfig.build.json b/tsconfig.build.json index 01d2f6d..45cd5ca 100644 --- a/tsconfig.build.json +++ b/tsconfig.build.json @@ -11,5 +11,5 @@ }, "files": ["src/index.ts"], "include": ["src/**/*"], - "exclude": ["**/*.test.ts", "src/bin/**/*"] + "exclude": ["**/*.test.ts", "**/*.test.tsx", "src/bin/**/*"] } diff --git a/vitest.config.ts b/vitest.config.ts index 01a4447..f54f1c0 100644 --- a/vitest.config.ts +++ b/vitest.config.ts @@ -15,10 +15,16 @@ export default defineConfig({ 'package/**/*.ts', 'examples/**/*.ts', '**/*.test.ts', + '**/*.test.tsx', ], provider: 'v8', reporter: ['html', 'lcov', 'text'], }, - include: ['src/**/*.test.ts', 'test/**/*.test.ts'], + include: [ + 'src/**/*.test.ts', + 'src/**/*.test.tsx', + 'test/**/*.test.ts', + 'test/**/*.test.tsx', + ], }, }) From 1b9c399a09db34300c3ed8d1175629ac06ee1479 Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 29 Jul 2026 01:51:11 +0000 Subject: [PATCH 03/11] refactor: import shared wizard modules through the lib alias This repository addresses modules under src/lib through the `lib/*` path alias rather than parent-relative specifiers: the alias is declared in tsconfig.json, mapped for tests in vitest.config.ts, rewritten at build time by tsc-alias, given its own import-sort group in eslint.config.ts, and enforced by import/no-relative-parent-imports. Point the step modules at lib/util/* instead of ../util/*. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01W1qhuuZQZnA1FB1fBSxrqx --- scripts/smoke.tsx | 2 +- src/lib/steps/analyze-project.ts | 4 ++-- src/lib/steps/authenticate.ts | 4 ++-- src/lib/steps/connect-web.ts | 4 ++-- 4 files changed, 7 insertions(+), 7 deletions(-) diff --git a/scripts/smoke.tsx b/scripts/smoke.tsx index 942d602..7ff7a65 100644 --- a/scripts/smoke.tsx +++ b/scripts/smoke.tsx @@ -5,7 +5,7 @@ import React from "react" import { render } from "ink-testing-library" -import { App } from "../src/app.js" +import { App } from "lib/app.js" delete process.env.SEAM_API_KEY diff --git a/src/lib/steps/analyze-project.ts b/src/lib/steps/analyze-project.ts index 51e5855..eb3c828 100644 --- a/src/lib/steps/analyze-project.ts +++ b/src/lib/steps/analyze-project.ts @@ -2,11 +2,11 @@ import { existsSync, readFileSync } from "node:fs" import { join } from "node:path" import type { ProjectInfo, Sdk } from "./detect-project.js" import { ALL_BLOCKS, type BuildMode } from "./build-plan.js" -import { findExistingApiKey } from "../util/env-file.js" +import { findExistingApiKey } from "lib/util/env-file.js" import { callInferenceForText, type WizardOnboarding, -} from "../util/seam-api.js" +} from "lib/util/seam-api.js" // A cheap classification model — the deep work happens in the integration // agent, so the pre-analysis only needs a good recommendation, not deep diff --git a/src/lib/steps/authenticate.ts b/src/lib/steps/authenticate.ts index e18f1af..43ed69d 100644 --- a/src/lib/steps/authenticate.ts +++ b/src/lib/steps/authenticate.ts @@ -2,8 +2,8 @@ import { join } from "node:path" import { getWorkspaceForApiKey, type SeamWorkspace, -} from "../util/seam-api.js" -import { upsertEnvVar, findExistingApiKey } from "../util/env-file.js" +} from "lib/util/seam-api.js" +import { upsertEnvVar, findExistingApiKey } from "lib/util/env-file.js" export interface AuthResult { workspace: SeamWorkspace diff --git a/src/lib/steps/connect-web.ts b/src/lib/steps/connect-web.ts index 59e3be5..a8e9a7c 100644 --- a/src/lib/steps/connect-web.ts +++ b/src/lib/steps/connect-web.ts @@ -2,8 +2,8 @@ import { createServer, type ServerResponse } from "node:http" import { randomBytes } from "node:crypto" import { join } from "node:path" import open from "open" -import { getWorkspaceForApiKey, type SeamWorkspace } from "../util/seam-api.js" -import { upsertEnvVar } from "../util/env-file.js" +import { getWorkspaceForApiKey, type SeamWorkspace } from "lib/util/seam-api.js" +import { upsertEnvVar } from "lib/util/env-file.js" // The dashboard "wizard" page mints a key and posts it back to the local // callback. Override the console host with SEAM_CONSOLE_URL for dev. From b10a4b1c058b8d563b8b07a00dcc412c3bd25086 Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 29 Jul 2026 01:53:54 +0000 Subject: [PATCH 04/11] feat: mount and run the Ink wizard from the package entrypoint MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Wire the moved Ink application up to the package's public API, replacing the stub that printed "not implemented yet". The API is unchanged for consumers: the default export still takes `argv` and `commandName`, still handles `--help` itself, and now renders the wizard instead of returning. Turn the moved src/lib/render.tsx from a bin script with top-level side effects into an exported `renderApp` function, so importing the package never touches the terminal. Rendering full-screen and reprinting the transcript on the way out is unchanged, except that the transcript is now also reprinted when the app fails, and failures reject instead of quietly setting an exit code — the caller, e.g., src/bin/cli.ts, reports them. Add a `cwd` option for the project root the wizard sets up, defaulting to process.cwd() as the bin script previously hardcoded. The Seam CLI can now mount the wizard against an explicit directory, and it makes the entrypoint testable without touching the working tree. Replace the scaffold `todo` placeholder, its tests, and its example with the wizard itself: `npm run example -- wizard` mounts the package the way a consumer does. Switch help output from console.log to process.stdout, matching the renderer and dropping the no-console suppression. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01W1qhuuZQZnA1FB1fBSxrqx --- examples/index.ts | 4 +-- examples/todo.ts | 24 ----------------- examples/wizard.ts | 23 ++++++++++++++++ src/lib/index.ts | 1 - src/lib/render.tsx | 61 +++++++++++++++++++++++++----------------- src/lib/todo.test.ts | 7 ----- src/lib/todo.ts | 1 - src/lib/wizard.test.ts | 32 +++++++++++++++------- src/lib/wizard.ts | 34 ++++++++++++++--------- test/todo.test.ts | 7 ----- 10 files changed, 105 insertions(+), 89 deletions(-) delete mode 100644 examples/todo.ts create mode 100644 examples/wizard.ts delete mode 100644 src/lib/todo.test.ts delete mode 100644 src/lib/todo.ts delete mode 100644 test/todo.test.ts diff --git a/examples/index.ts b/examples/index.ts index 7a1cc11..ebb1f98 100755 --- a/examples/index.ts +++ b/examples/index.ts @@ -2,8 +2,8 @@ import landlubber from 'landlubber' -import * as todo from './todo.js' +import * as wizard from './wizard.js' -const commands = [todo] +const commands = [wizard] await landlubber(commands).parse() diff --git a/examples/todo.ts b/examples/todo.ts deleted file mode 100644 index e97cd8a..0000000 --- a/examples/todo.ts +++ /dev/null @@ -1,24 +0,0 @@ -import type { Builder, Command, Describe, Handler } from 'landlubber' - -import { todo } from '@seamapi/wizard' - -interface Options { - x: string -} - -export const command: Command = 'todo x' - -export const describe: Describe = 'TODO' - -export const builder: Builder = { - x: { - type: 'string', - default: 'TODO', - describe: 'TODO', - }, -} - -export const handler: Handler = async ({ x, logger }) => { - await Promise.resolve() - logger.info({ data: todo(x) }, 'TODO') -} diff --git a/examples/wizard.ts b/examples/wizard.ts new file mode 100644 index 0000000..92bb85f --- /dev/null +++ b/examples/wizard.ts @@ -0,0 +1,23 @@ +import type { Builder, Command, Describe, Handler } from 'landlubber' + +import wizard from '@seamapi/wizard' + +interface Options { + cwd: string +} + +export const command: Command = 'wizard' + +export const describe: Describe = 'Mount and run the Seam setup wizard' + +export const builder: Builder = { + cwd: { + type: 'string', + default: '.', + describe: 'The project root the wizard should set up', + }, +} + +export const handler: Handler = async ({ cwd }) => { + await wizard({ argv: [], commandName: 'example wizard', cwd }) +} diff --git a/src/lib/index.ts b/src/lib/index.ts index f3b05e0..3069cf6 100644 --- a/src/lib/index.ts +++ b/src/lib/index.ts @@ -1,2 +1 @@ -export { todo } from './todo.js' export { default, type WizardOptions } from './wizard.js' diff --git a/src/lib/render.tsx b/src/lib/render.tsx index 49a27ea..6b79ac1 100644 --- a/src/lib/render.tsx +++ b/src/lib/render.tsx @@ -1,36 +1,49 @@ -#!/usr/bin/env node -import React from "react" -import { render } from "ink" -import { App } from "./app.js" +import { render } from 'ink' + +import { App } from './app.js' const ESC = String.fromCharCode(27) // Enter alt screen + clear + home / leave alt screen (restores normal buffer). const ENTER_ALT_SCREEN = `${ESC}[?1049h${ESC}[2J${ESC}[H` const LEAVE_ALT_SCREEN = `${ESC}[?1049l` -const is_tty = Boolean(process.stdout.isTTY) +export interface RenderAppOptions { + /** The project root the wizard sets up. */ + root: string +} -// Run full-screen in the alternate screen buffer (like less/vim). Enter before -// the first render to avoid a flash of the initial frame in the normal buffer; -// leave on exit and reprint the transcript, since the alternate buffer is -// discarded when we leave it. -if (is_tty) process.stdout.write(ENTER_ALT_SCREEN) +/** + * Render the wizard full-screen and resolve once the user has finished. + * + * The app runs in the alternate screen buffer (like less or vim). Entering it + * before the first render avoids a flash of the initial frame in the normal + * buffer. The alternate buffer is discarded on the way out, so the transcript + * the app hands back is reprinted afterwards, leaving the usual record of the + * run behind in the terminal's scrollback. + * + * Rejects if the app fails to run, leaving the terminal restored either way. + */ +export const renderApp = async ({ root }: RenderAppOptions): Promise => { + const isTty = Boolean(process.stdout.isTTY) + if (isTty) process.stdout.write(ENTER_ALT_SCREEN) -let transcript: string[] = [] + let transcript: readonly string[] = [] -const app = render( - (transcript = lines)} /> -) + const app = render( + { + transcript = lines + }} + />, + ) -app - .waitUntilExit() - .then(() => { - if (is_tty) process.stdout.write(LEAVE_ALT_SCREEN) + try { + await app.waitUntilExit() + } finally { + if (isTty) process.stdout.write(LEAVE_ALT_SCREEN) if (transcript.length > 0) { - process.stdout.write(`\n${transcript.join("\n")}\n`) + process.stdout.write(`\n${transcript.join('\n')}\n`) } - }) - .catch(() => { - if (is_tty) process.stdout.write(LEAVE_ALT_SCREEN) - process.exitCode = 1 - }) + } +} diff --git a/src/lib/todo.test.ts b/src/lib/todo.test.ts deleted file mode 100644 index c90fd94..0000000 --- a/src/lib/todo.test.ts +++ /dev/null @@ -1,7 +0,0 @@ -import { expect, test } from 'vitest' - -import { todo } from './todo.js' - -test('todo: returns argument', () => { - expect(todo('todo')).toBe('todo') -}) diff --git a/src/lib/todo.ts b/src/lib/todo.ts deleted file mode 100644 index 5633fe7..0000000 --- a/src/lib/todo.ts +++ /dev/null @@ -1 +0,0 @@ -export const todo = (x: string): string => x diff --git a/src/lib/wizard.test.ts b/src/lib/wizard.test.ts index 5b083fe..4ea9a89 100644 --- a/src/lib/wizard.test.ts +++ b/src/lib/wizard.test.ts @@ -1,16 +1,23 @@ -import { expect, test, vi } from 'vitest' +import { beforeEach, expect, test, vi } from 'vitest' +import { renderApp } from './render.js' import wizard from './wizard.js' +vi.mock('./render.js', () => ({ renderApp: vi.fn() })) + +beforeEach(() => { + vi.mocked(renderApp).mockClear() +}) + const captureOutput = async ( options: Parameters[0], ): Promise => { - const log = vi.spyOn(console, 'log').mockImplementation(() => {}) + const write = vi.spyOn(process.stdout, 'write').mockImplementation(() => true) try { await wizard(options) - return log.mock.calls.map(([message]) => String(message)).join('\n') + return write.mock.calls.map(([chunk]) => String(chunk)).join('') } finally { - log.mockRestore() + write.mockRestore() } } @@ -33,12 +40,17 @@ test('wizard: uses the given command name in usage', async () => { expect(output).toContain('$ seam wizard [options]') }) -test('wizard: runs with no arguments', async () => { - const output = await captureOutput({}) - expect(output).toContain('not implemented yet') +test('wizard: does not run the app when displaying usage', async () => { + await captureOutput({ argv: ['--help'] }) + expect(renderApp).not.toHaveBeenCalled() +}) + +test('wizard: runs the app in the working directory by default', async () => { + await wizard() + expect(renderApp).toHaveBeenCalledWith({ root: process.cwd() }) }) -test('wizard: reports forwarded arguments', async () => { - const output = await captureOutput({ argv: ['setup', 'devices'] }) - expect(output).toContain('setup devices') +test('wizard: runs the app in the given directory', async () => { + await wizard({ argv: [], cwd: '/tmp/example-project' }) + expect(renderApp).toHaveBeenCalledWith({ root: '/tmp/example-project' }) }) diff --git a/src/lib/wizard.ts b/src/lib/wizard.ts index f461577..8d1820f 100644 --- a/src/lib/wizard.ts +++ b/src/lib/wizard.ts @@ -1,5 +1,7 @@ import parseArgs from 'minimist' +import { renderApp } from './render.js' + export interface WizardOptions { /** * Command line arguments for the wizard, e.g., `process.argv.slice(2)`. @@ -17,6 +19,15 @@ export interface WizardOptions { * The Seam CLI mounts this wizard and passes `seam wizard`. */ commandName?: string + + /** + * The project root the wizard sets up. + * + * Defaults to `process.cwd()`, which is the project the developer ran the + * command in. The wizard reads and writes files here, e.g., `.env` and + * `.seam/onboarding.json`. + */ + cwd?: string } /** @@ -24,9 +35,13 @@ export interface WizardOptions { * * This is the entrypoint used by the Seam CLI to mount the entire wizard * as a subcommand. It is also used by the development CLI in `src/bin/cli.ts`. + * + * Resolves once the wizard has finished, having taken over the terminal for + * the duration of the run. Rejects only if the wizard could not be run at + * all: a step that fails reports itself to the user and does not reject. */ const wizard = async (options: WizardOptions = {}): Promise => { - const { argv = [], commandName = 'wizard' } = options + const { argv = [], commandName = 'wizard', cwd = process.cwd() } = options const args = parseArgs([...argv], { boolean: ['help'], @@ -38,16 +53,7 @@ const wizard = async (options: WizardOptions = {}): Promise => { return } - // TODO: Implement the wizard. - await Promise.resolve() - - write( - [ - `The ${commandName} is not implemented yet.`, - `Run '${commandName} --help' for usage.`, - ...(args._.length > 0 ? [`Received arguments: ${args._.join(' ')}`] : []), - ].join('\n'), - ) + await renderApp({ root: cwd }) } export default wizard @@ -58,6 +64,9 @@ const usage = (commandName: string): string => '', ' The AI powered Seam setup wizard.', '', + ' Connects your Seam account, installs the Seam SDK and the Seam plugin', + ' skills, and optionally writes a Seam integration into your project.', + '', 'Usage', '', ` $ ${commandName} [options]`, @@ -70,6 +79,5 @@ const usage = (commandName: string): string => // TODO: Replace this with a logger wrapper. const write = (message: string): void => { - // eslint-disable-next-line no-console - console.log(message) + process.stdout.write(`${message}\n`) } diff --git a/test/todo.test.ts b/test/todo.test.ts deleted file mode 100644 index b9908e0..0000000 --- a/test/todo.test.ts +++ /dev/null @@ -1,7 +0,0 @@ -import { expect, test } from 'vitest' - -import { todo } from '@seamapi/wizard' - -test('todo: returns argument', () => { - expect(todo('todo')).toBe('todo') -}) From 1ae0e4a67df44121fa7b0a774e38e886bb50add3 Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 29 Jul 2026 01:55:07 +0000 Subject: [PATCH 05/11] style: format the migrated source MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Apply this repository's Prettier configuration and the autofixable ESLint rules to the moved source: single quotes, no semicolons, and sorted import groups. No behavior changes — this is the mechanical part of the migration, kept separate so the substantive conformance changes that follow are reviewable. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01W1qhuuZQZnA1FB1fBSxrqx --- scripts/smoke.tsx | 15 +- src/lib/app.tsx | 539 ++++++++++++++++----------- src/lib/components/checkbox-list.tsx | 28 +- src/lib/steps/analyze-project.ts | 178 +++++---- src/lib/steps/authenticate.ts | 16 +- src/lib/steps/build-plan.ts | 97 ++--- src/lib/steps/connect-web.ts | 60 +-- src/lib/steps/detect-project.ts | 70 ++-- src/lib/steps/install-seam-plugin.ts | 28 +- src/lib/steps/integrate.ts | 109 +++--- src/lib/util/env-file.ts | 36 +- src/lib/util/run-install.ts | 20 +- src/lib/util/seam-api.ts | 66 ++-- 13 files changed, 695 insertions(+), 567 deletions(-) diff --git a/scripts/smoke.tsx b/scripts/smoke.tsx index 7ff7a65..3854964 100644 --- a/scripts/smoke.tsx +++ b/scripts/smoke.tsx @@ -3,22 +3,23 @@ // call happens (findExistingApiKey returns null before any API request). // A full interactive run can't be exercised headlessly. -import React from "react" -import { render } from "ink-testing-library" -import { App } from "lib/app.js" +import { render } from 'ink-testing-library' +import React from 'react' + +import { App } from 'lib/app.js' delete process.env.SEAM_API_KEY const { lastFrame, unmount } = render( - + , ) -const frame = lastFrame() ?? "" +const frame = lastFrame() ?? '' unmount() -if (!frame.includes("Checking")) { +if (!frame.includes('Checking')) { console.error(`SMOKE FAIL — unexpected first frame:\n${frame}`) process.exit(1) } -console.log("SMOKE OK — initial frame rendered") +console.log('SMOKE OK — initial frame rendered') process.exit(0) diff --git a/src/lib/app.tsx b/src/lib/app.tsx index 79c8389..a42549c 100644 --- a/src/lib/app.tsx +++ b/src/lib/app.tsx @@ -1,37 +1,41 @@ -import React, { useEffect, useRef, useState } from "react" -import { Box, Text, useApp, useStdout } from "ink" -import Spinner from "ink-spinner" -import SelectInput from "ink-select-input" -import TextInput from "ink-text-input" +import { Box, Text, useApp, useStdout } from 'ink' +import SelectInput from 'ink-select-input' +import Spinner from 'ink-spinner' +import TextInput from 'ink-text-input' +import React, { useEffect, useRef, useState } from 'react' + +import { CheckboxList } from './components/checkbox-list.js' import { - detectProject, - installSeamSdkCommand, - type ProjectInfo, - type Sdk, -} from "./steps/detect-project.js" + analyzeProject, + type ProjectAnalysis, +} from './steps/analyze-project.js' import { findVerifiedExistingKey, verifyAndSaveKey, -} from "./steps/authenticate.js" -import { connectViaWeb } from "./steps/connect-web.js" +} from './steps/authenticate.js' import { - detectPluginTarget, - SEAM_PLUGIN_NPX_COMMAND, - CLAUDE_CODE_COMMANDS, -} from "./steps/install-seam-plugin.js" -import { runIntegration, type IntegrateEvent } from "./steps/integrate.js" -import { analyzeProject, type ProjectAnalysis } from "./steps/analyze-project.js" -import { - CORE_BLOCKS, + type BuildMode, COMMON_BLOCKS, composeGoal, - writeOnboardingRecord, - type BuildMode, + CORE_BLOCKS, type OnboardingRecord, -} from "./steps/build-plan.js" -import { CheckboxList } from "./components/checkbox-list.js" -import { runInstall } from "./util/run-install.js" -import { findExistingApiKey } from "./util/env-file.js" + writeOnboardingRecord, +} from './steps/build-plan.js' +import { connectViaWeb } from './steps/connect-web.js' +import { + detectProject, + installSeamSdkCommand, + type ProjectInfo, + type Sdk, +} from './steps/detect-project.js' +import { + CLAUDE_CODE_COMMANDS, + detectPluginTarget, + SEAM_PLUGIN_NPX_COMMAND, +} from './steps/install-seam-plugin.js' +import { type IntegrateEvent, runIntegration } from './steps/integrate.js' +import { findExistingApiKey } from './util/env-file.js' +import { runInstall } from './util/run-install.js' import { ApiKeyError, exchangeWizardInferenceToken, @@ -39,33 +43,33 @@ import { SEAM_INFERENCE_BASE_URL, type SeamWorkspace, type WizardInferenceSession, -} from "./util/seam-api.js" +} from './util/seam-api.js' const MAX_ATTEMPTS = 3 -type Tone = "ok" | "info" | "warn" | "plain" +type Tone = 'ok' | 'info' | 'warn' | 'plain' interface Msg { tone: Tone text: string } type Phase = - | { t: "init" } - | { t: "method" } - | { t: "browser" } - | { t: "paste" } - | { t: "verify-paste"; api_key: string } - | { t: "sdk" } - | { t: "install-sdk" } - | { t: "install-plugin" } - | { t: "offer-integrate" } - | { t: "analyze" } - | { t: "integrate-mode" } - | { t: "checklist" } - | { t: "note" } - | { t: "integrate"; goal: string } - | { t: "done" } - | { t: "error"; message: string } + | { t: 'init' } + | { t: 'method' } + | { t: 'browser' } + | { t: 'paste' } + | { t: 'verify-paste'; api_key: string } + | { t: 'sdk' } + | { t: 'install-sdk' } + | { t: 'install-plugin' } + | { t: 'offer-integrate' } + | { t: 'analyze' } + | { t: 'integrate-mode' } + | { t: 'checklist' } + | { t: 'note' } + | { t: 'integrate'; goal: string } + | { t: 'done' } + | { t: 'error'; message: string } export function App({ root, @@ -85,18 +89,21 @@ export function App({ }) const [messages, setMessages] = useState([]) - const [phase, setPhase] = useState({ t: "init" }) + const [phase, setPhase] = useState({ t: 'init' }) const [sdk, setSdk] = useState(null) const [workspace, setWorkspace] = useState(null) - const [browser, setBrowser] = useState<{ url: string | null; received: boolean }>({ + const [browser, setBrowser] = useState<{ + url: string | null + received: boolean + }>({ url: null, received: false, }) - const [paste_value, setPasteValue] = useState("") + const [paste_value, setPasteValue] = useState('') const [paste_error, setPasteError] = useState(null) const [install_lines, setInstallLines] = useState([]) - const [note_value, setNoteValue] = useState("") + const [note_value, setNoteValue] = useState('') const [agent_lines, setAgentLines] = useState([]) const [session, setSession] = useState(null) const [analysis, setAnalysis] = useState(null) @@ -105,7 +112,7 @@ export function App({ // The plan record (written to .seam/onboarding.json before the agent runs); // the done handler rewrites it with the run result. - const plan_ref = useRef | null>(null) + const plan_ref = useRef | null>(null) // Kept in sync with `messages` so the exit handler can hand the full // transcript to index.tsx to reprint after leaving the alt screen. @@ -118,24 +125,24 @@ export function App({ }) const finishWithNextSteps = (): void => { - pushNextSteps(addMessage, sdk, workspace?.name ?? "your workspace") - setPhase({ t: "done" }) + pushNextSteps(addMessage, sdk, workspace?.name ?? 'your workspace') + setPhase({ t: 'done' }) } // Compose the goal from the chosen mode + building blocks + optional note, // persist the plan to .seam/onboarding.json, then hand off to the agent. const startIntegration = (note_input: string): void => { const note = note_input.trim().length > 0 ? note_input.trim() : null - const effective_mode: BuildMode = mode ?? "full_api" + const effective_mode: BuildMode = mode ?? 'full_api' const effective_selections = - effective_mode === "customer_portal" ? [] : selections + effective_mode === 'customer_portal' ? [] : selections const goal = composeGoal({ mode: effective_mode, selections: effective_selections, note, framework: analysis?.signals.framework ?? null, }) - const record: Omit = { + const record: Omit = { created_at: new Date().toISOString(), mode: effective_mode, selections: effective_selections, @@ -147,23 +154,29 @@ export function App({ app_type_guess: analysis?.recommendation.app_type_guess ?? null, seam_already_setup: analysis?.signals.seam_already_setup ?? false, used_onboarding: analysis?.used_onboarding ?? false, - recommendation_source: analysis?.recommendation.source ?? "heuristic", + recommendation_source: analysis?.recommendation.source ?? 'heuristic', }, } plan_ref.current = record try { writeOnboardingRecord(root, record) - addMessage({ tone: "info", text: "Saved your plan to .seam/onboarding.json" }) + addMessage({ + tone: 'info', + text: 'Saved your plan to .seam/onboarding.json', + }) } catch { // Writing the record is best-effort; proceed with the integration. } - setPhase({ t: "integrate", goal }) + setPhase({ t: 'integrate', goal }) } const handleIntegrateEvent = (event: IntegrateEvent): void => { - if (event.kind === "text") { - setAgentLines((previous) => [...previous.slice(-5), truncate(event.text, 100)]) - } else if (event.kind === "tool") { + if (event.kind === 'text') { + setAgentLines((previous) => [ + ...previous.slice(-5), + truncate(event.text, 100), + ]) + } else if (event.kind === 'tool') { setAgentLines((previous) => [ ...previous.slice(-5), formatTool(event.name, event.detail), @@ -186,14 +199,24 @@ export function App({ } addMessage( event.ok - ? { tone: "ok", text: "Integration written — review it with `git diff`" } - : { tone: "warn", text: "Agent stopped early — review what it changed with `git diff`" } + ? { + tone: 'ok', + text: 'Integration written — review it with `git diff`', + } + : { + tone: 'warn', + text: 'Agent stopped early — review what it changed with `git diff`', + }, ) - for (const line of event.summary.trim().split("\n").slice(0, 15)) { - if (line.trim().length > 0) addMessage({ tone: "plain", text: ` ${line}` }) + for (const line of event.summary.trim().split('\n').slice(0, 15)) { + if (line.trim().length > 0) + addMessage({ tone: 'plain', text: ` ${line}` }) } if (event.cost_usd != null) { - addMessage({ tone: "info", text: `Model cost: $${event.cost_usd.toFixed(2)}` }) + addMessage({ + tone: 'info', + text: `Model cost: $${event.cost_usd.toFixed(2)}`, + }) } } } @@ -203,16 +226,16 @@ export function App({ const detected = project_ref.current.detected_sdk if (detected != null) { setSdk(detected) - addMessage({ tone: "info", text: `Detected ${detected} project` }) - setPhase({ t: "install-sdk" }) + addMessage({ tone: 'info', text: `Detected ${detected} project` }) + setPhase({ t: 'install-sdk' }) } else { - setPhase({ t: "sdk" }) + setPhase({ t: 'sdk' }) } } // init: reuse an existing key if valid, else ask how to connect. useEffect(() => { - if (phase.t !== "init") return + if (phase.t !== 'init') return let cancelled = false void (async () => { const existing = await findVerifiedExistingKey(root) @@ -220,12 +243,12 @@ export function App({ if (existing != null) { setWorkspace(existing.workspace) addMessage({ - tone: "ok", + tone: 'ok', text: `Using existing key from ${existing.source} · workspace ${existing.workspace.name}`, }) advanceAfterAuth() } else { - setPhase({ t: "method" }) + setPhase({ t: 'method' }) } })() return () => { @@ -235,23 +258,30 @@ export function App({ // browser handoff useEffect(() => { - if (phase.t !== "browser") return + if (phase.t !== 'browser') return let cancelled = false void (async () => { try { const result = await connectViaWeb(root, { onUrl: (url) => !cancelled && setBrowser((b) => ({ ...b, url })), - onReceived: () => !cancelled && setBrowser((b) => ({ ...b, received: true })), + onReceived: () => + !cancelled && setBrowser((b) => ({ ...b, received: true })), }) if (cancelled) return setWorkspace(result.workspace) - addMessage({ tone: "ok", text: `Connected · workspace ${result.workspace.name}` }) + addMessage({ + tone: 'ok', + text: `Connected · workspace ${result.workspace.name}`, + }) advanceAfterAuth() } catch (error) { if (!cancelled) { setPhase({ - t: "error", - message: error instanceof Error ? error.message : "Browser connection failed.", + t: 'error', + message: + error instanceof Error + ? error.message + : 'Browser connection failed.', }) } } @@ -263,7 +293,7 @@ export function App({ // verify a pasted key useEffect(() => { - if (phase.t !== "verify-paste") return + if (phase.t !== 'verify-paste') return const { api_key } = phase let cancelled = false void (async () => { @@ -271,18 +301,23 @@ export function App({ const result = await verifyAndSaveKey(root, api_key) if (cancelled) return setWorkspace(result.workspace) - addMessage({ tone: "ok", text: `Workspace: ${result.workspace.name}` }) + addMessage({ tone: 'ok', text: `Workspace: ${result.workspace.name}` }) advanceAfterAuth() } catch (error) { if (cancelled) return const message = - error instanceof ApiKeyError ? error.message : "Couldn't verify the key." + error instanceof ApiKeyError + ? error.message + : "Couldn't verify the key." if (attempt_ref.current >= MAX_ATTEMPTS) { - setPhase({ t: "error", message: "Too many attempts. Re-run with a valid key." }) + setPhase({ + t: 'error', + message: 'Too many attempts. Re-run with a valid key.', + }) } else { setPasteError(message) - setPasteValue("") - setPhase({ t: "paste" }) + setPasteValue('') + setPhase({ t: 'paste' }) } } })() @@ -293,26 +328,27 @@ export function App({ // install the Seam SDK (streamed), then install the plugin useEffect(() => { - if (phase.t !== "install-sdk" || sdk == null) return + if (phase.t !== 'install-sdk' || sdk == null) return let cancelled = false const command = installSeamSdkCommand(sdk, project_ref.current) void (async () => { try { await runInstall(command, root, (line) => { - if (!cancelled) setInstallLines((previous) => [...previous.slice(-3), line]) + if (!cancelled) + setInstallLines((previous) => [...previous.slice(-3), line]) }) - if (!cancelled) addMessage({ tone: "ok", text: "Seam SDK installed" }) + if (!cancelled) addMessage({ tone: 'ok', text: 'Seam SDK installed' }) } catch { if (!cancelled) { addMessage({ - tone: "warn", - text: `Couldn't finish installing — run it yourself: ${command.join(" ")}`, + tone: 'warn', + text: `Couldn't finish installing — run it yourself: ${command.join(' ')}`, }) } } if (!cancelled) { setInstallLines([]) - setPhase({ t: "install-plugin" }) + setPhase({ t: 'install-plugin' }) } })() return () => { @@ -324,36 +360,38 @@ export function App({ // universal installer (works everywhere); for Claude Code we additionally // point at the native /plugin path, which also wires up the seam-docs MCP. useEffect(() => { - if (phase.t !== "install-plugin") return - const workspace_name = workspace?.name ?? "your workspace" + if (phase.t !== 'install-plugin') return + const workspace_name = workspace?.name ?? 'your workspace' const target = detectPluginTarget(root) let cancelled = false void (async () => { try { await runInstall(SEAM_PLUGIN_NPX_COMMAND, root, (line) => { - if (!cancelled) setInstallLines((previous) => [...previous.slice(-3), line]) + if (!cancelled) + setInstallLines((previous) => [...previous.slice(-3), line]) }) - if (!cancelled) addMessage({ tone: "ok", text: "Installed the Seam plugin skills" }) + if (!cancelled) + addMessage({ tone: 'ok', text: 'Installed the Seam plugin skills' }) } catch { if (!cancelled) { addMessage({ - tone: "warn", - text: `Couldn't install the plugin — run it yourself: ${SEAM_PLUGIN_NPX_COMMAND.join(" ")}`, + tone: 'warn', + text: `Couldn't install the plugin — run it yourself: ${SEAM_PLUGIN_NPX_COMMAND.join(' ')}`, }) } } if (!cancelled) { - if (target === "claude-code") { + if (target === 'claude-code') { addMessage({ - tone: "info", - text: "Claude Code: for the native plugin + seam-docs MCP, you can also run:", + tone: 'info', + text: 'Claude Code: for the native plugin + seam-docs MCP, you can also run:', }) for (const command of CLAUDE_CODE_COMMANDS) { - addMessage({ tone: "plain", text: ` ${command}` }) + addMessage({ tone: 'plain', text: ` ${command}` }) } } - setPhase({ t: "offer-integrate" }) + setPhase({ t: 'offer-integrate' }) } })() return () => { @@ -366,13 +404,13 @@ export function App({ // recommendation to pre-fill the next steps. The token is reused by the // integration agent below, so it is minted once. useEffect(() => { - if (phase.t !== "analyze") return + if (phase.t !== 'analyze') return let cancelled = false void (async () => { const found = findExistingApiKey(root) if (found == null) { addMessage({ - tone: "warn", + tone: 'warn', text: "Couldn't find your Seam API key to plan the integration.", }) if (!cancelled) finishWithNextSteps() @@ -385,9 +423,9 @@ export function App({ } catch (error) { if (!cancelled) { addMessage({ - tone: "warn", + tone: 'warn', text: `Couldn't start the AI session: ${ - error instanceof Error ? error.message : "unknown error" + error instanceof Error ? error.message : 'unknown error' }`, }) finishWithNextSteps() @@ -416,15 +454,18 @@ export function App({ result.recommendation.app_type_guess, ] .filter((value): value is string => value != null && value.length > 0) - .join(" · ") + .join(' · ') addMessage({ - tone: "info", - text: `Analyzed your project${detail.length > 0 ? `: ${detail}` : ""}`, + tone: 'info', + text: `Analyzed your project${detail.length > 0 ? `: ${detail}` : ''}`, }) if (current_session.onboarding != null) { - addMessage({ tone: "plain", text: " Used your Console onboarding answers" }) + addMessage({ + tone: 'plain', + text: ' Used your Console onboarding answers', + }) } - setPhase({ t: "integrate-mode" }) + setPhase({ t: 'integrate-mode' }) })() return () => { cancelled = true @@ -435,31 +476,31 @@ export function App({ // Seam-hosted inference using the token minted during the analyze step — no // developer Anthropic key. useEffect(() => { - if (phase.t !== "integrate") return + if (phase.t !== 'integrate') return const { goal } = phase const controller = new AbortController() void (async () => { if (session == null) { addMessage({ - tone: "warn", - text: "Lost the AI session — re-run the wizard to try again.", + tone: 'warn', + text: 'Lost the AI session — re-run the wizard to try again.', }) if (!controller.signal.aborted) finishWithNextSteps() return } - addMessage({ tone: "info", text: "Starting the Seam integration agent…" }) + addMessage({ tone: 'info', text: 'Starting the Seam integration agent…' }) try { await runIntegration({ root, - sdk: sdk ?? "javascript", - workspace_name: workspace?.name ?? "your workspace", + sdk: sdk ?? 'javascript', + workspace_name: workspace?.name ?? 'your workspace', goal, inference: { base_url: SEAM_INFERENCE_BASE_URL, token: session.token, }, framework: analysis?.signals.framework ?? null, - mode: mode ?? "full_api", + mode: mode ?? 'full_api', signal: controller.signal, onEvent: (event) => { if (!controller.signal.aborted) handleIntegrateEvent(event) @@ -468,9 +509,9 @@ export function App({ } catch (error) { if (!controller.signal.aborted) { addMessage({ - tone: "warn", + tone: 'warn', text: `Couldn't run the integration agent: ${ - error instanceof Error ? error.message : "unknown error" + error instanceof Error ? error.message : 'unknown error' }`, }) } @@ -485,17 +526,17 @@ export function App({ if (stdout == null) return const onResize = (): void => setDimensions({ rows: stdout.rows ?? 24, columns: stdout.columns ?? 80 }) - stdout.on("resize", onResize) + stdout.on('resize', onResize) return () => { - stdout.off("resize", onResize) + stdout.off('resize', onResize) } }, [stdout]) // exit when finished useEffect(() => { - if (phase.t !== "done" && phase.t !== "error") return - if (phase.t === "error") { - addMessage({ tone: "warn", text: phase.message }) + if (phase.t !== 'done' && phase.t !== 'error') return + if (phase.t === 'error') { + addMessage({ tone: 'warn', text: phase.message }) process.exitCode = 1 } // Hand the transcript back so index.tsx can reprint it once the alt screen @@ -516,13 +557,13 @@ export function App({ return (
- + {visible_messages.map((message, index) => ( ))} @@ -533,153 +574,186 @@ export function App({ function renderActive(): React.ReactElement | null { switch (phase.t) { - case "init": - return - case "method": + case 'init': + return + case 'method': return ( - + { - if (item.value === "paste") { + if (item.value === 'paste') { attempt_ref.current = 0 - setPhase({ t: "paste" }) + setPhase({ t: 'paste' }) } else { - setPhase({ t: "browser" }) + setPhase({ t: 'browser' }) } }} /> ) - case "browser": + case 'browser': return ( - - - {browser.url != null && {browser.url}} + + + {browser.url != null && {browser.url}} ) - case "paste": + case 'paste': return ( - + - {"› "} + {'› '} { if (!looksLikeSeamApiKey(value)) { - setPasteError("That doesn't look like a Seam key (expected seam_…).") + setPasteError( + "That doesn't look like a Seam key (expected seam_…).", + ) return } setPasteError(null) attempt_ref.current += 1 - setPhase({ t: "verify-paste", api_key: value }) + setPhase({ t: 'verify-paste', api_key: value }) }} /> - {paste_error != null && {paste_error}} + {paste_error != null && {paste_error}} ) - case "verify-paste": - return - case "sdk": + case 'verify-paste': + return + case 'sdk': return ( - + { - const chosen: Sdk = item.value === "python" ? "python" : "javascript" + const chosen: Sdk = + item.value === 'python' ? 'python' : 'javascript' setSdk(chosen) - addMessage({ tone: "info", text: `SDK: ${chosen}` }) - setPhase({ t: "install-sdk" }) + addMessage({ tone: 'info', text: `SDK: ${chosen}` }) + setPhase({ t: 'install-sdk' }) }} /> ) - case "install-sdk": + case 'install-sdk': return ( - - + + {install_lines.map((line, index) => ( - {line} + + {' '} + {line} + ))} ) - case "install-plugin": + case 'install-plugin': return ( - - + + {install_lines.map((line, index) => ( - {line} + + {' '} + {line} + ))} ) - case "offer-integrate": + case 'offer-integrate': return ( - + { - if (item.value === "yes") setPhase({ t: "analyze" }) + if (item.value === 'yes') setPhase({ t: 'analyze' }) else finishWithNextSteps() }} /> ) - case "analyze": - return - case "integrate-mode": { - const recommended: BuildMode = mode ?? "full_api" + case 'analyze': + return + case 'integrate-mode': { + const recommended: BuildMode = mode ?? 'full_api' const portal_item = { - label: "Customer Portal — Seam hosts the UI (you call ~2 endpoints)", - value: "customer_portal", + label: 'Customer Portal — Seam hosts the UI (you call ~2 endpoints)', + value: 'customer_portal', } const api_item = { - label: "Full API control — you build the UI, wire up the API", - value: "full_api", + label: 'Full API control — you build the UI, wire up the API', + value: 'full_api', } const items = - recommended === "customer_portal" + recommended === 'customer_portal' ? [portal_item, api_item] : [api_item, portal_item] return ( - + {analysis?.recommendation.rationale != null && analysis.recommendation.rationale.length > 0 && ( - {` ${analysis.recommendation.rationale}`} + {` ${analysis.recommendation.rationale}`} )} { const chosen: BuildMode = - item.value === "customer_portal" ? "customer_portal" : "full_api" + item.value === 'customer_portal' + ? 'customer_portal' + : 'full_api' setMode(chosen) addMessage({ - tone: "info", + tone: 'info', text: `Mode: ${ - chosen === "customer_portal" ? "Customer Portal" : "Full API" + chosen === 'customer_portal' + ? 'Customer Portal' + : 'Full API' }`, }) - setPhase(chosen === "customer_portal" ? { t: "note" } : { t: "checklist" }) + setPhase( + chosen === 'customer_portal' + ? { t: 'note' } + : { t: 'checklist' }, + ) }} /> ) } - case "checklist": + case 'checklist': return ( - + ({ id: block.id, @@ -689,36 +763,39 @@ export function App({ initial_selected={selections} onSubmit={(chosen) => { setSelections(chosen) - setPhase({ t: "note" }) + setPhase({ t: 'note' }) }} /> ) - case "note": + case 'note': return ( - + - {"› "} + {'› '} startIntegration(value)} /> ) - case "integrate": + case 'integrate': return ( - - + + {agent_lines.map((line, index) => ( - {line} + + {' '} + {line} + ))} ) - case "done": - case "error": + case 'done': + case 'error': return null } } @@ -727,25 +804,25 @@ export function App({ function pushNextSteps( addMessage: (message: Msg) => void, sdk: Sdk | null, - workspace_name: string + workspace_name: string, ): void { const env_hint = - sdk === "python" + sdk === 'python' ? "Make sure SEAM_API_KEY is exported (it's in .env)." - : "Add .env to .gitignore — it holds your API key." - addMessage({ tone: "plain", text: "" }) - addMessage({ tone: "ok", text: `You're set up in ${workspace_name}` }) - addMessage({ tone: "plain", text: "Next steps:" }) + : 'Add .env to .gitignore — it holds your API key.' + addMessage({ tone: 'plain', text: '' }) + addMessage({ tone: 'ok', text: `You're set up in ${workspace_name}` }) + addMessage({ tone: 'plain', text: 'Next steps:' }) addMessage({ - tone: "plain", + tone: 'plain', text: ' 1. Describe your integration to your AI assistant — e.g. "add Seam access grants". The Seam skill will guide it.', }) - addMessage({ tone: "plain", text: ` 2. ${env_hint}` }) - addMessage({ tone: "plain", text: " 3. Docs: https://docs.seam.co" }) + addMessage({ tone: 'plain', text: ` 2. ${env_hint}` }) + addMessage({ tone: 'plain', text: ' 3. Docs: https://docs.seam.co' }) } function truncate(text: string, max_length: number): string { - const collapsed = text.replace(/\s+/g, " ").trim() + const collapsed = text.replace(/\s+/g, ' ').trim() return collapsed.length > max_length ? `${collapsed.slice(0, max_length - 1)}…` : collapsed @@ -754,18 +831,18 @@ function truncate(text: string, max_length: number): string { // A compact one-line label for a tool the agent just invoked. function formatTool(name: string, detail: string): string { const verb = - name === "Write" - ? "write" - : name === "Edit" - ? "edit" - : name === "Read" - ? "read" - : name === "Glob" || name === "Grep" - ? "search" - : name === "WebFetch" - ? "fetch" - : name.startsWith("mcp__seam-docs__") - ? "docs" + name === 'Write' + ? 'write' + : name === 'Edit' + ? 'edit' + : name === 'Read' + ? 'read' + : name === 'Glob' || name === 'Grep' + ? 'search' + : name === 'WebFetch' + ? 'fetch' + : name.startsWith('mcp__seam-docs__') + ? 'docs' : name return detail.length > 0 ? `${verb} ${truncate(detail, 60)}` : verb } @@ -773,8 +850,8 @@ function formatTool(name: string, detail: string): string { function Header(): React.ReactElement { return ( - - {" Seam setup wizard "} + + {' Seam setup wizard '} ) @@ -783,16 +860,22 @@ function Header(): React.ReactElement { // Plain-text rendering of a message, matching MessageLine's symbols — used to // reprint the transcript into the normal terminal after the alt screen closes. function formatMessageLine(message: Msg): string { - if (message.tone === "plain") return message.text + if (message.tone === 'plain') return message.text const symbol = - message.tone === "ok" ? "✔" : message.tone === "warn" ? "▲" : "•" + message.tone === 'ok' ? '✔' : message.tone === 'warn' ? '▲' : '•' return `${symbol} ${message.text}` } function MessageLine({ message }: { message: Msg }): React.ReactElement { - if (message.tone === "plain") return {message.text} - const symbol = message.tone === "ok" ? "✔" : message.tone === "warn" ? "▲" : "•" - const color = message.tone === "ok" ? "green" : message.tone === "warn" ? "yellow" : "cyan" + if (message.tone === 'plain') return {message.text} + const symbol = + message.tone === 'ok' ? '✔' : message.tone === 'warn' ? '▲' : '•' + const color = + message.tone === 'ok' + ? 'green' + : message.tone === 'warn' + ? 'yellow' + : 'cyan' return ( {symbol} {message.text} @@ -803,9 +886,9 @@ function MessageLine({ message }: { message: Msg }): React.ReactElement { function Pending({ label }: { label: string }): React.ReactElement { return ( - - - {" "} + + + {' '} {label} ) @@ -819,7 +902,7 @@ function Prompt({ children: React.ReactNode }): React.ReactElement { return ( - + {title} {children} diff --git a/src/lib/components/checkbox-list.tsx b/src/lib/components/checkbox-list.tsx index 238a68f..08c9958 100644 --- a/src/lib/components/checkbox-list.tsx +++ b/src/lib/components/checkbox-list.tsx @@ -1,5 +1,5 @@ -import React, { useState } from "react" -import { Box, Text, useInput } from "ink" +import { Box, Text, useInput } from 'ink' +import React, { useState } from 'react' export interface CheckboxItem { id: string @@ -21,15 +21,15 @@ export function CheckboxList({ }): React.ReactElement { const [cursor, setCursor] = useState(0) const [selected, setSelected] = useState>( - () => new Set(initial_selected) + () => new Set(initial_selected), ) useInput((input, key) => { - if (key.upArrow || input === "k") { + if (key.upArrow || input === 'k') { setCursor((current) => (current - 1 + items.length) % items.length) - } else if (key.downArrow || input === "j") { + } else if (key.downArrow || input === 'j') { setCursor((current) => (current + 1) % items.length) - } else if (input === " ") { + } else if (input === ' ') { const id = items[cursor]?.id if (id == null) return setSelected((previous) => { @@ -39,12 +39,14 @@ export function CheckboxList({ return next }) } else if (key.return) { - onSubmit(items.filter((item) => selected.has(item.id)).map((item) => item.id)) + onSubmit( + items.filter((item) => selected.has(item.id)).map((item) => item.id), + ) } }) return ( - + {items.map((item, index) => { const is_cursor = index === cursor const is_checked = selected.has(item.id) @@ -52,16 +54,16 @@ export function CheckboxList({ item.group != null && item.group !== items[index - 1]?.group return ( - {show_group && {` ${item.group ?? ""}`}} - - {is_cursor ? "❯ " : " "} - {is_checked ? "◉ " : "◯ "} + {show_group && {` ${item.group ?? ''}`}} + + {is_cursor ? '❯ ' : ' '} + {is_checked ? '◉ ' : '◯ '} {item.label} ) })} - {" ↑/↓ move · space toggle · enter confirm"} + {' ↑/↓ move · space toggle · enter confirm'} ) } diff --git a/src/lib/steps/analyze-project.ts b/src/lib/steps/analyze-project.ts index eb3c828..3ccb87a 100644 --- a/src/lib/steps/analyze-project.ts +++ b/src/lib/steps/analyze-project.ts @@ -1,17 +1,19 @@ -import { existsSync, readFileSync } from "node:fs" -import { join } from "node:path" -import type { ProjectInfo, Sdk } from "./detect-project.js" -import { ALL_BLOCKS, type BuildMode } from "./build-plan.js" -import { findExistingApiKey } from "lib/util/env-file.js" +import { existsSync, readFileSync } from 'node:fs' +import { join } from 'node:path' + +import { findExistingApiKey } from 'lib/util/env-file.js' import { callInferenceForText, type WizardOnboarding, -} from "lib/util/seam-api.js" +} from 'lib/util/seam-api.js' + +import { ALL_BLOCKS, type BuildMode } from './build-plan.js' +import type { ProjectInfo, Sdk } from './detect-project.js' // A cheap classification model — the deep work happens in the integration // agent, so the pre-analysis only needs a good recommendation, not deep // reasoning. -const RECOMMENDATION_MODEL = "claude-haiku-4-5" +const RECOMMENDATION_MODEL = 'claude-haiku-4-5' export interface ProjectSignals { sdk: Sdk | null @@ -29,7 +31,7 @@ export interface Recommendation { selections: string[] app_type_guess: string | null rationale: string - source: "llm" | "heuristic" + source: 'llm' | 'heuristic' } export interface ProjectAnalysis { @@ -69,9 +71,9 @@ export async function analyzeProject(args: { function gatherProjectSignals( root: string, - project: ProjectInfo + project: ProjectInfo, ): ProjectSignals { - const package_json = readJsonIfExists(join(root, "package.json")) as { + const package_json = readJsonIfExists(join(root, 'package.json')) as { name?: string description?: string keywords?: string[] @@ -93,34 +95,34 @@ function gatherProjectSignals( dependency_names: dependency_names.slice(0, 40), readme_excerpt: readReadmeExcerpt(root), seam_already_setup: - dependency_names.includes("seam") || findExistingApiKey(root) != null, + dependency_names.includes('seam') || findExistingApiKey(root) != null, } } function detectFramework( root: string, sdk: Sdk | null, - dependency_names: string[] + dependency_names: string[], ): string | null { const has = (name: string): boolean => dependency_names.includes(name) - if (sdk === "javascript") { - if (has("next")) return "Next.js" - if (has("@remix-run/react") || has("@remix-run/node")) return "Remix" - if (has("nuxt")) return "Nuxt" - if (has("@nestjs/core")) return "NestJS" - if (has("fastify")) return "Fastify" - if (has("express")) return "Express" - if (has("react")) return "React" + if (sdk === 'javascript') { + if (has('next')) return 'Next.js' + if (has('@remix-run/react') || has('@remix-run/node')) return 'Remix' + if (has('nuxt')) return 'Nuxt' + if (has('@nestjs/core')) return 'NestJS' + if (has('fastify')) return 'Fastify' + if (has('express')) return 'Express' + if (has('react')) return 'React' return null } - if (sdk === "python") { - if (existsSync(join(root, "manage.py")) || hasPythonDep(root, "django")) { - return "Django" + if (sdk === 'python') { + if (existsSync(join(root, 'manage.py')) || hasPythonDep(root, 'django')) { + return 'Django' } - if (hasPythonDep(root, "fastapi")) return "FastAPI" - if (hasPythonDep(root, "flask")) return "Flask" + if (hasPythonDep(root, 'fastapi')) return 'FastAPI' + if (hasPythonDep(root, 'flask')) return 'Flask' return null } @@ -128,11 +130,11 @@ function detectFramework( } function hasPythonDep(root: string, name: string): boolean { - for (const marker of ["requirements.txt", "pyproject.toml", "Pipfile"]) { + for (const marker of ['requirements.txt', 'pyproject.toml', 'Pipfile']) { const path = join(root, marker) if (!existsSync(path)) continue try { - if (readFileSync(path, "utf8").toLowerCase().includes(name)) return true + if (readFileSync(path, 'utf8').toLowerCase().includes(name)) return true } catch { // Unreadable dependency file — treat as absent. } @@ -141,11 +143,11 @@ function hasPythonDep(root: string, name: string): boolean { } function readReadmeExcerpt(root: string): string | null { - for (const name of ["README.md", "README.MD", "readme.md", "README"]) { + for (const name of ['README.md', 'README.MD', 'readme.md', 'README']) { const path = join(root, name) if (!existsSync(path)) continue try { - return readFileSync(path, "utf8").slice(0, 1200) + return readFileSync(path, 'utf8').slice(0, 1200) } catch { return null } @@ -156,7 +158,7 @@ function readReadmeExcerpt(root: string): string | null { function readJsonIfExists(path: string): unknown { if (!existsSync(path)) return null try { - return JSON.parse(readFileSync(path, "utf8")) + return JSON.parse(readFileSync(path, 'utf8')) } catch { return null } @@ -165,48 +167,48 @@ function readJsonIfExists(path: string): unknown { async function recommendViaLlm( signals: ProjectSignals, onboarding: WizardOnboarding | null, - inference: { base_url: string; token: string } + inference: { base_url: string; token: string }, ): Promise { const block_menu = ALL_BLOCKS.map( - (block) => ` "${block.id}" — ${block.label}` - ).join("\n") + (block) => ` "${block.id}" — ${block.label}`, + ).join('\n') const system = - "You help developers integrate Seam, an API for smart locks, access " + - "control, and physical access. Given signals about a project, recommend " + - "how to integrate Seam. Reply with ONLY a JSON object, no prose." + 'You help developers integrate Seam, an API for smart locks, access ' + + 'control, and physical access. Given signals about a project, recommend ' + + 'how to integrate Seam. Reply with ONLY a JSON object, no prose.' const user = [ - "Project signals:", - `- SDK: ${signals.sdk ?? "unknown"}`, - `- Framework: ${signals.framework ?? "unknown"}`, - `- Package: ${signals.package_name ?? "unknown"} — ${signals.description ?? ""}`, - `- Keywords: ${signals.keywords.join(", ") || "none"}`, - `- Dependencies: ${signals.dependency_names.join(", ") || "none"}`, + 'Project signals:', + `- SDK: ${signals.sdk ?? 'unknown'}`, + `- Framework: ${signals.framework ?? 'unknown'}`, + `- Package: ${signals.package_name ?? 'unknown'} — ${signals.description ?? ''}`, + `- Keywords: ${signals.keywords.join(', ') || 'none'}`, + `- Dependencies: ${signals.dependency_names.join(', ') || 'none'}`, `- Seam already set up: ${signals.seam_already_setup}`, - `- README excerpt: ${signals.readme_excerpt ?? "none"}`, - "", - "Console onboarding answers (any may be null):", - `- use_case: ${onboarding?.use_case ?? "null"}`, - `- primary_goal: ${onboarding?.primary_goal ?? "null"}`, - `- build_target: ${onboarding?.build_target ?? "null"}`, - `- embed_customer_portal: ${onboarding?.embed_customer_portal ?? "null"}`, - `- device_categories: ${onboarding?.device_categories?.join(", ") ?? "null"}`, - "", - "Decide:", + `- README excerpt: ${signals.readme_excerpt ?? 'none'}`, + '', + 'Console onboarding answers (any may be null):', + `- use_case: ${onboarding?.use_case ?? 'null'}`, + `- primary_goal: ${onboarding?.primary_goal ?? 'null'}`, + `- build_target: ${onboarding?.build_target ?? 'null'}`, + `- embed_customer_portal: ${onboarding?.embed_customer_portal ?? 'null'}`, + `- device_categories: ${onboarding?.device_categories?.join(', ') ?? 'null'}`, + '', + 'Decide:', '1) mode: "customer_portal" (Seam hosts the UI; the app calls ~2 endpoints)', ' or "full_api" (control everything via the API, build your own UI).', - " If embed_customer_portal is true, strongly prefer customer_portal.", + ' If embed_customer_portal is true, strongly prefer customer_portal.', '2) selections: building-block ids to scaffold — only for "full_api" (use', - " [] for customer_portal). Always include \"access_grants\" for full_api.", - " Valid ids:", + ' [] for customer_portal). Always include "access_grants" for full_api.', + ' Valid ids:', block_menu, '3) app_type_guess: short phrase (e.g. "vacation rental", "coworking",', ' "property management", "unknown").', - "4) rationale: one short sentence.", - "", + '4) rationale: one short sentence.', + '', 'Return exactly: {"mode":"...","selections":["..."],"app_type_guess":"...","rationale":"..."}', - ].join("\n") + ].join('\n') const text = await callInferenceForText(inference, { model: RECOMMENDATION_MODEL, @@ -219,9 +221,9 @@ async function recommendViaLlm( if (parsed == null) return heuristicRecommendation(signals, onboarding) const mode: BuildMode = - parsed.mode === "customer_portal" ? "customer_portal" : "full_api" + parsed.mode === 'customer_portal' ? 'customer_portal' : 'full_api' const selections = - mode === "customer_portal" + mode === 'customer_portal' ? [] : normalizeSelections(parsed.selections ?? []) @@ -229,8 +231,8 @@ async function recommendViaLlm( mode, selections, app_type_guess: parsed.app_type_guess ?? null, - rationale: parsed.rationale ?? "", - source: "llm", + rationale: parsed.rationale ?? '', + source: 'llm', } } @@ -253,25 +255,25 @@ function parseRecommendationJson(text: string): { function normalizeSelections(raw: unknown): string[] { const ids = Array.isArray(raw) ? raw.filter( - (id): id is string => typeof id === "string" && VALID_BLOCK_IDS.has(id) + (id): id is string => typeof id === 'string' && VALID_BLOCK_IDS.has(id), ) : [] const unique = [...new Set(ids)] - if (!unique.includes("access_grants")) unique.unshift("access_grants") + if (!unique.includes('access_grants')) unique.unshift('access_grants') return unique } function heuristicRecommendation( signals: ProjectSignals, - onboarding: WizardOnboarding | null + onboarding: WizardOnboarding | null, ): Recommendation { if (onboarding?.embed_customer_portal === true) { return { - mode: "customer_portal", + mode: 'customer_portal', selections: [], app_type_guess: onboarding.use_case ?? onboarding.org_type ?? null, - rationale: "You chose to embed the Customer Portal during onboarding.", - source: "heuristic", + rationale: 'You chose to embed the Customer Portal during onboarding.', + source: 'heuristic', } } @@ -284,30 +286,48 @@ function heuristicRecommendation( ...signals.keywords, ] .filter((value): value is string => value != null) - .join(" ") + .join(' ') .toLowerCase() const mentions = (...terms: string[]): boolean => terms.some((term) => haystack.includes(term)) - const selections = new Set(["access_grants", "connect_device"]) + const selections = new Set(['access_grants', 'connect_device']) let app_type_guess: string | null = null if ( - mentions("hotel", "booking", "reservation", "vacation", "rental", "pms", "guest", "hospitality") + mentions( + 'hotel', + 'booking', + 'reservation', + 'vacation', + 'rental', + 'pms', + 'guest', + 'hospitality', + ) + ) { + selections.add('reservations') + app_type_guess = 'hospitality / short-term rental' + } else if ( + mentions( + 'coworking', + 'tenant', + 'member', + 'property', + 'apartment', + 'resident', + ) ) { - selections.add("reservations") - app_type_guess = "hospitality / short-term rental" - } else if (mentions("coworking", "tenant", "member", "property", "apartment", "resident")) { - selections.add("user_identities") - app_type_guess = "property / coworking" + selections.add('user_identities') + app_type_guess = 'property / coworking' } return { - mode: "full_api", + mode: 'full_api', selections: [...selections], app_type_guess, - rationale: "Recommended from your project and onboarding answers.", - source: "heuristic", + rationale: 'Recommended from your project and onboarding answers.', + source: 'heuristic', } } diff --git a/src/lib/steps/authenticate.ts b/src/lib/steps/authenticate.ts index 43ed69d..32025f5 100644 --- a/src/lib/steps/authenticate.ts +++ b/src/lib/steps/authenticate.ts @@ -1,9 +1,7 @@ -import { join } from "node:path" -import { - getWorkspaceForApiKey, - type SeamWorkspace, -} from "lib/util/seam-api.js" -import { upsertEnvVar, findExistingApiKey } from "lib/util/env-file.js" +import { join } from 'node:path' + +import { findExistingApiKey, upsertEnvVar } from 'lib/util/env-file.js' +import { getWorkspaceForApiKey, type SeamWorkspace } from 'lib/util/seam-api.js' export interface AuthResult { workspace: SeamWorkspace @@ -21,7 +19,7 @@ export interface ExistingKeyResult { // Return the workspace for an already-present SEAM_API_KEY (env or .env*), or // null when there is none / it doesn't verify. export async function findVerifiedExistingKey( - root: string + root: string, ): Promise { const existing = findExistingApiKey(root) if (existing == null) return null @@ -36,10 +34,10 @@ export async function findVerifiedExistingKey( // Verify a pasted key and save it to .env. Throws (ApiKeyError) if invalid. export async function verifyAndSaveKey( root: string, - api_key: string + api_key: string, ): Promise { const trimmed = api_key.trim() const workspace = await getWorkspaceForApiKey(trimmed) - upsertEnvVar(join(root, ".env"), "SEAM_API_KEY", trimmed) + upsertEnvVar(join(root, '.env'), 'SEAM_API_KEY', trimmed) return { workspace } } diff --git a/src/lib/steps/build-plan.ts b/src/lib/steps/build-plan.ts index ef7c7bf..98d8b7d 100644 --- a/src/lib/steps/build-plan.ts +++ b/src/lib/steps/build-plan.ts @@ -1,13 +1,13 @@ -import { mkdirSync, writeFileSync } from "node:fs" -import { join } from "node:path" +import { mkdirSync, writeFileSync } from 'node:fs' +import { join } from 'node:path' // Top-level fork: let Seam host the UI (the app calls ~2 endpoints), or drive // everything through the API and build the UI yourself. -export type BuildMode = "customer_portal" | "full_api" +export type BuildMode = 'customer_portal' | 'full_api' export interface BuildBlock { id: string - group: "Core" | "Common" + group: 'Core' | 'Common' label: string // Appended (as an instruction line) to the agent goal when selected. agent_hint: string @@ -18,55 +18,55 @@ export interface BuildBlock { // the default-checked item. export const CORE_BLOCKS: BuildBlock[] = [ { - id: "connect_device", - group: "Core", - label: "Connect a device (Connect Webview + connected accounts)", + id: 'connect_device', + group: 'Core', + label: 'Connect a device (Connect Webview + connected accounts)', agent_hint: - "Set up a Connect Webview so an end user can connect their account and devices, then list the connected devices.", + 'Set up a Connect Webview so an end user can connect their account and devices, then list the connected devices.', }, { - id: "access_grants", - group: "Core", - label: "Access Grants — grant a person access", + id: 'access_grants', + group: 'Core', + label: 'Access Grants — grant a person access', agent_hint: "Create an Access Grant to give a person access to a space or device (Seam's recommended API for granting access).", }, { - id: "user_identities", - group: "Core", - label: "User Identities (the people who receive access)", + id: 'user_identities', + group: 'Core', + label: 'User Identities (the people who receive access)', agent_hint: - "Create and manage User Identities representing the people who receive access.", + 'Create and manage User Identities representing the people who receive access.', }, ] // Common building blocks — frequent next steps beyond the core. export const COMMON_BLOCKS: BuildBlock[] = [ { - id: "access_codes", - group: "Common", - label: "Access codes (PIN codes on locks)", - agent_hint: "Program PIN access codes on smart locks.", + id: 'access_codes', + group: 'Common', + label: 'Access codes (PIN codes on locks)', + agent_hint: 'Program PIN access codes on smart locks.', }, { - id: "reservations", - group: "Common", - label: "Reservations → access (PMS / booking flow)", + id: 'reservations', + group: 'Common', + label: 'Reservations → access (PMS / booking flow)', agent_hint: - "Wire a booking/reservation flow so each reservation automatically provisions and revokes access.", + 'Wire a booking/reservation flow so each reservation automatically provisions and revokes access.', }, { - id: "mobile_keys", - group: "Common", - label: "Mobile keys / credentials", - agent_hint: "Issue mobile-key credentials to users.", + id: 'mobile_keys', + group: 'Common', + label: 'Mobile keys / credentials', + agent_hint: 'Issue mobile-key credentials to users.', }, { - id: "webhooks", - group: "Common", - label: "Webhooks & events", + id: 'webhooks', + group: 'Common', + label: 'Webhooks & events', agent_hint: - "Subscribe to Seam webhooks and handle the events (e.g. access granted, device connected).", + 'Subscribe to Seam webhooks and handle the events (e.g. access granted, device connected).', }, ] @@ -85,20 +85,20 @@ export function composeGoal(args: { framework: string | null }): string { const { mode, selections, note, framework } = args - const target = framework ?? "the project" + const target = framework ?? 'the project' const note_suffix = note != null && note.trim().length > 0 ? ` Additional context from the developer: ${note.trim()}` - : "" + : '' - if (mode === "customer_portal") { + if (mode === 'customer_portal') { return ( - "Integrate Seam using the Customer Portal so the UI is Seam-hosted and " + - "this app only calls a couple of endpoints. Create a Customer Portal for " + - "a customer (it returns a hosted URL), embed it in the app (iframe or a " + - "magic link), and regenerate the portal per visit since the session is " + + 'Integrate Seam using the Customer Portal so the UI is Seam-hosted and ' + + 'this app only calls a couple of endpoints. Create a Customer Portal for ' + + 'a customer (it returns a hosted URL), embed it in the app (iframe or a ' + + 'magic link), and regenerate the portal per visit since the session is ' + `short-lived. Wire it into ${target}'s conventions, load SEAM_API_KEY from ` + - "the existing .env, and add a short runnable example." + + 'the existing .env, and add a short runnable example.' + note_suffix ) } @@ -107,13 +107,13 @@ export function composeGoal(args: { .map((id) => blockById(id)?.agent_hint) .filter((hint): hint is string => hint != null) .map((hint) => `- ${hint}`) - .join("\n") + .join('\n') return ( - "Set up a Seam integration that controls everything through the Seam API. " + + 'Set up a Seam integration that controls everything through the Seam API. ' + `Implement the following, wired into ${target}'s conventions:\n${hints}\n` + - "Load SEAM_API_KEY from the existing .env, keep changes minimal and " + - "idiomatic, and add a short runnable example." + + 'Load SEAM_API_KEY from the existing .env, keep changes minimal and ' + + 'idiomatic, and add a short runnable example.' + note_suffix ) } @@ -134,7 +134,7 @@ export interface OnboardingRecord { app_type_guess: string | null seam_already_setup: boolean used_onboarding: boolean - recommendation_source: "llm" | "heuristic" + recommendation_source: 'llm' | 'heuristic' } result?: { ok: boolean @@ -148,13 +148,16 @@ export interface OnboardingRecord { // what was set up, and the embedded agent / a later editor agent can read it. export function writeOnboardingRecord( root: string, - record: Omit + record: Omit, ): void { - const dir = join(root, ".seam") + const dir = join(root, '.seam') mkdirSync(dir, { recursive: true }) const full: OnboardingRecord = { schema_version: RECORD_SCHEMA_VERSION, ...record, } - writeFileSync(join(dir, "onboarding.json"), `${JSON.stringify(full, null, 2)}\n`) + writeFileSync( + join(dir, 'onboarding.json'), + `${JSON.stringify(full, null, 2)}\n`, + ) } diff --git a/src/lib/steps/connect-web.ts b/src/lib/steps/connect-web.ts index a8e9a7c..87b0b75 100644 --- a/src/lib/steps/connect-web.ts +++ b/src/lib/steps/connect-web.ts @@ -1,14 +1,16 @@ -import { createServer, type ServerResponse } from "node:http" -import { randomBytes } from "node:crypto" -import { join } from "node:path" -import open from "open" -import { getWorkspaceForApiKey, type SeamWorkspace } from "lib/util/seam-api.js" -import { upsertEnvVar } from "lib/util/env-file.js" +import { randomBytes } from 'node:crypto' +import { createServer, type ServerResponse } from 'node:http' +import { join } from 'node:path' + +import open from 'open' + +import { upsertEnvVar } from 'lib/util/env-file.js' +import { getWorkspaceForApiKey, type SeamWorkspace } from 'lib/util/seam-api.js' // The dashboard "wizard" page mints a key and posts it back to the local // callback. Override the console host with SEAM_CONSOLE_URL for dev. -const CONSOLE_URL = process.env.SEAM_CONSOLE_URL ?? "https://console.seam.co" -const CONSOLE_WIZARD_PATH = "/dashboard/wizard" +const CONSOLE_URL = process.env.SEAM_CONSOLE_URL ?? 'https://console.seam.co' +const CONSOLE_WIZARD_PATH = '/dashboard/wizard' const CALLBACK_TIMEOUT_MS = 5 * 60 * 1000 export interface WebConnectResult { @@ -37,45 +39,45 @@ interface CallbackPayload { // and POSTs JSON { state, api_key } to http://127.0.0.1:/ (CORS *). export async function connectViaWeb( root: string, - events: WebConnectEvents = {} + events: WebConnectEvents = {}, ): Promise { - const state = randomBytes(16).toString("hex") + const state = randomBytes(16).toString('hex') const payload = await new Promise((resolve, reject) => { const server = createServer((request, response) => { - response.setHeader("Access-Control-Allow-Origin", "*") - response.setHeader("Access-Control-Allow-Methods", "POST, OPTIONS") - response.setHeader("Access-Control-Allow-Headers", "content-type") + response.setHeader('Access-Control-Allow-Origin', '*') + response.setHeader('Access-Control-Allow-Methods', 'POST, OPTIONS') + response.setHeader('Access-Control-Allow-Headers', 'content-type') - if (request.method === "OPTIONS") { + if (request.method === 'OPTIONS') { response.writeHead(204) response.end() return } - if (request.method !== "POST") { + if (request.method !== 'POST') { response.writeHead(405) response.end() return } - let body = "" - request.on("data", (chunk) => { + let body = '' + request.on('data', (chunk) => { body += chunk }) - request.on("end", () => { + request.on('end', () => { let parsed: Partial = {} try { parsed = JSON.parse(body) as Partial } catch { - respondJson(response, 400, { ok: false, error: "invalid_json" }) + respondJson(response, 400, { ok: false, error: 'invalid_json' }) return } if (parsed.state !== state) { - respondJson(response, 403, { ok: false, error: "state_mismatch" }) + respondJson(response, 403, { ok: false, error: 'state_mismatch' }) return } if (parsed.api_key == null || parsed.api_key.length === 0) { - respondJson(response, 400, { ok: false, error: "missing_api_key" }) + respondJson(response, 400, { ok: false, error: 'missing_api_key' }) return } respondJson(response, 200, { ok: true }) @@ -85,11 +87,11 @@ export async function connectViaWeb( }) }) - server.on("error", reject) - server.listen(0, "127.0.0.1", () => { + server.on('error', reject) + server.listen(0, '127.0.0.1', () => { const address = server.address() - if (address == null || typeof address === "string") { - reject(new Error("Could not start the local callback server.")) + if (address == null || typeof address === 'string') { + reject(new Error('Could not start the local callback server.')) return } const url = `${CONSOLE_URL}${CONSOLE_WIZARD_PATH}?cli_connect=1&cli_port=${address.port}&cli_state=${state}` @@ -102,21 +104,21 @@ export async function connectViaWeb( const timeout = setTimeout(() => { server.close() - reject(new Error("Timed out waiting for the browser.")) + reject(new Error('Timed out waiting for the browser.')) }, CALLBACK_TIMEOUT_MS) timeout.unref() }) const workspace = await getWorkspaceForApiKey(payload.api_key) - upsertEnvVar(join(root, ".env"), "SEAM_API_KEY", payload.api_key) + upsertEnvVar(join(root, '.env'), 'SEAM_API_KEY', payload.api_key) return { workspace } } function respondJson( response: ServerResponse, status: number, - body: unknown + body: unknown, ): void { - response.writeHead(status, { "content-type": "application/json" }) + response.writeHead(status, { 'content-type': 'application/json' }) response.end(JSON.stringify(body)) } diff --git a/src/lib/steps/detect-project.ts b/src/lib/steps/detect-project.ts index 15a2680..dc8c4ba 100644 --- a/src/lib/steps/detect-project.ts +++ b/src/lib/steps/detect-project.ts @@ -1,9 +1,9 @@ -import { existsSync } from "node:fs" -import { join } from "node:path" +import { existsSync } from 'node:fs' +import { join } from 'node:path' -export type Sdk = "javascript" | "python" -export type JsPackageManager = "npm" | "pnpm" | "yarn" | "bun" -export type PythonInstaller = "pip" | "poetry" | "uv" +export type Sdk = 'javascript' | 'python' +export type JsPackageManager = 'npm' | 'pnpm' | 'yarn' | 'bun' +export type PythonInstaller = 'pip' | 'poetry' | 'uv' export interface ProjectInfo { root: string @@ -23,47 +23,53 @@ export function detectProject(cwd: string): ProjectInfo { // null when the project is ambiguous (both or neither) — the wizard then asks. function detectSdk(cwd: string): Sdk | null { - const is_javascript = existsSync(join(cwd, "package.json")) - const is_python = ["pyproject.toml", "requirements.txt", "setup.py", "Pipfile"].some( - (marker) => existsSync(join(cwd, marker)) - ) - if (is_javascript && !is_python) return "javascript" - if (is_python && !is_javascript) return "python" + const is_javascript = existsSync(join(cwd, 'package.json')) + const is_python = [ + 'pyproject.toml', + 'requirements.txt', + 'setup.py', + 'Pipfile', + ].some((marker) => existsSync(join(cwd, marker))) + if (is_javascript && !is_python) return 'javascript' + if (is_python && !is_javascript) return 'python' return null } function detectJsPackageManager(cwd: string): JsPackageManager { - if (existsSync(join(cwd, "pnpm-lock.yaml"))) return "pnpm" - if (existsSync(join(cwd, "yarn.lock"))) return "yarn" - if (existsSync(join(cwd, "bun.lockb"))) return "bun" - return "npm" + if (existsSync(join(cwd, 'pnpm-lock.yaml'))) return 'pnpm' + if (existsSync(join(cwd, 'yarn.lock'))) return 'yarn' + if (existsSync(join(cwd, 'bun.lockb'))) return 'bun' + return 'npm' } function detectPythonInstaller(cwd: string): PythonInstaller { - if (existsSync(join(cwd, "poetry.lock"))) return "poetry" - if (existsSync(join(cwd, "uv.lock"))) return "uv" - return "pip" + if (existsSync(join(cwd, 'poetry.lock'))) return 'poetry' + if (existsSync(join(cwd, 'uv.lock'))) return 'uv' + return 'pip' } -export function installSeamSdkCommand(sdk: Sdk, project: ProjectInfo): string[] { - if (sdk === "python") { +export function installSeamSdkCommand( + sdk: Sdk, + project: ProjectInfo, +): string[] { + if (sdk === 'python') { switch (project.python_installer) { - case "poetry": - return ["poetry", "add", "seam"] - case "uv": - return ["uv", "add", "seam"] + case 'poetry': + return ['poetry', 'add', 'seam'] + case 'uv': + return ['uv', 'add', 'seam'] default: - return ["pip", "install", "seam"] + return ['pip', 'install', 'seam'] } } switch (project.js_package_manager) { - case "pnpm": - return ["pnpm", "add", "seam"] - case "yarn": - return ["yarn", "add", "seam"] - case "bun": - return ["bun", "add", "seam"] + case 'pnpm': + return ['pnpm', 'add', 'seam'] + case 'yarn': + return ['yarn', 'add', 'seam'] + case 'bun': + return ['bun', 'add', 'seam'] default: - return ["npm", "install", "seam"] + return ['npm', 'install', 'seam'] } } diff --git a/src/lib/steps/install-seam-plugin.ts b/src/lib/steps/install-seam-plugin.ts index b068b47..5f5d972 100644 --- a/src/lib/steps/install-seam-plugin.ts +++ b/src/lib/steps/install-seam-plugin.ts @@ -1,10 +1,10 @@ -import { existsSync } from "node:fs" -import { join } from "node:path" +import { existsSync } from 'node:fs' +import { join } from 'node:path' -export type PluginTarget = "claude-code" | "universal" +export type PluginTarget = 'claude-code' | 'universal' // The official Seam plugin: 3 integration skills + the seam-docs MCP. -const SEAM_PLUGIN = "seamapi/seam-plugin" +const SEAM_PLUGIN = 'seamapi/seam-plugin' // Install the plugin's three skills into .claude/skills. We must be // non-interactive (`-y`) — the wizard spawns this with stdin ignored, and @@ -13,26 +13,26 @@ const SEAM_PLUGIN = "seamapi/seam-plugin" // which is exactly where the embedded agent reads them (settingSources: // ["project"]) and where Claude Code picks them up too. export const SEAM_PLUGIN_NPX_COMMAND = [ - "npx", - "skills", - "add", + 'npx', + 'skills', + 'add', SEAM_PLUGIN, - "-y", - "-a", - "claude-code", + '-y', + '-a', + 'claude-code', ] // Claude Code installs plugins via in-app slash commands, which an external CLI // can't drive — so we print these for the user to run. This path also wires up // the seam-docs MCP. export const CLAUDE_CODE_COMMANDS = [ - "/plugin marketplace add seamapi/seam-plugin", - "/plugin install seam@seamapi", + '/plugin marketplace add seamapi/seam-plugin', + '/plugin install seam@seamapi', ] // Detect Claude Code so we print its slash commands instead of running npx. export function detectPluginTarget(root: string): PluginTarget { const has_claude_code = - existsSync(join(root, ".claude")) || existsSync(join(root, "CLAUDE.md")) - return has_claude_code ? "claude-code" : "universal" + existsSync(join(root, '.claude')) || existsSync(join(root, 'CLAUDE.md')) + return has_claude_code ? 'claude-code' : 'universal' } diff --git a/src/lib/steps/integrate.ts b/src/lib/steps/integrate.ts index 56a0c5e..24cfea7 100644 --- a/src/lib/steps/integrate.ts +++ b/src/lib/steps/integrate.ts @@ -1,26 +1,27 @@ -import { query } from "@anthropic-ai/claude-agent-sdk" -import type { Sdk } from "./detect-project.js" +import { query } from '@anthropic-ai/claude-agent-sdk' + +import type { Sdk } from './detect-project.js' // The official Seam MCP (same server the seam-plugin wires up). -const SEAM_MCP_URL = "https://mcp.seam.co/mcp" +const SEAM_MCP_URL = 'https://mcp.seam.co/mcp' // Read/search/write + the docs MCP. Deliberately no Bash, no subagents, no task // tools: the agent writes integration code, it does not run the developer's // shell. `mcp__seam-docs__*` grants every seam-docs tool. const ALLOWED_TOOLS = [ - "Read", - "Glob", - "Grep", - "Edit", - "Write", - "WebFetch", - "mcp__seam-docs__*", + 'Read', + 'Glob', + 'Grep', + 'Edit', + 'Write', + 'WebFetch', + 'mcp__seam-docs__*', ] export type IntegrateEvent = - | { kind: "text"; text: string } - | { kind: "tool"; name: string; detail: string } - | { kind: "done"; ok: boolean; summary: string; cost_usd: number | null } + | { kind: 'text'; text: string } + | { kind: 'tool'; name: string; detail: string } + | { kind: 'done'; ok: boolean; summary: string; cost_usd: number | null } export interface RunIntegrationArgs { root: string @@ -31,7 +32,7 @@ export interface RunIntegrationArgs { // The detected framework + chosen mode, so the agent fetches the matching // reference app from the Seam MCP (get_example_app) to model its work on. framework?: string | null - mode?: "full_api" | "customer_portal" + mode?: 'full_api' | 'customer_portal' signal: AbortSignal onEvent: (event: IntegrateEvent) => void } @@ -40,11 +41,20 @@ export interface RunIntegrationArgs { // project, routed through Seam-hosted inference. Streams progress via onEvent; // resolves when the agent finishes or the signal aborts. export async function runIntegration(args: RunIntegrationArgs): Promise { - const { root, sdk, workspace_name, goal, inference, framework, mode, signal, onEvent } = - args + const { + root, + sdk, + workspace_name, + goal, + inference, + framework, + mode, + signal, + onEvent, + } = args const abort_controller = new AbortController() const forward_abort = (): void => abort_controller.abort() - signal.addEventListener("abort", forward_abort, { once: true }) + signal.addEventListener('abort', forward_abort, { once: true }) try { for await (const message of query({ @@ -54,31 +64,31 @@ export async function runIntegration(args: RunIntegrationArgs): Promise { // Cost-tuned for a starter integration (Seam pays for this): Sonnet 5 is // near-Opus on coding at ~half the token price, medium effort trims the // thinking spend, and maxBudgetUsd is a hard per-run dollar ceiling. - model: "claude-sonnet-5", - effort: "medium", + model: 'claude-sonnet-5', + effort: 'medium', maxBudgetUsd: 2, env: buildAgentEnv(inference), allowedTools: ALLOWED_TOOLS, // Auto-apply file edits so the agent runs uninterrupted; the developer // reviews the result as a git diff afterward. Read/search tools and the // docs MCP are read-only, so nothing destructive runs unattended. - permissionMode: "acceptEdits", + permissionMode: 'acceptEdits', mcpServers: { // Wired exactly like the seam-plugin: mcp-remote bridges to the hosted // Seam MCP and runs the OAuth browser flow on first use, caching the // token in ~/.mcp-auth. The developer's Claude Code (also using // mcp-remote to the same server) then reuses it, already authenticated. - "seam-docs": { - command: "npx", - args: ["-y", "mcp-remote", SEAM_MCP_URL], + 'seam-docs': { + command: 'npx', + args: ['-y', 'mcp-remote', SEAM_MCP_URL], }, }, // Pick up any Seam skill installed into the project's .claude/skills. - settingSources: ["project"], - skills: "all", + settingSources: ['project'], + skills: 'all', systemPrompt: { - type: "preset", - preset: "claude_code", + type: 'preset', + preset: 'claude_code', append: buildSystemAppend(sdk, workspace_name, framework, mode), }, maxTurns: 40, @@ -87,34 +97,34 @@ export async function runIntegration(args: RunIntegrationArgs): Promise { })) { if (signal.aborted) break - if (message.type === "assistant") { + if (message.type === 'assistant') { for (const block of message.message.content) { - if (block.type === "text") { + if (block.type === 'text') { const text = block.text.trim() - if (text.length > 0) onEvent({ kind: "text", text }) - } else if (block.type === "tool_use") { + if (text.length > 0) onEvent({ kind: 'text', text }) + } else if (block.type === 'tool_use') { onEvent({ - kind: "tool", + kind: 'tool', name: block.name, detail: describeToolInput(block.input), }) } } - } else if (message.type === "result") { - const ok = message.subtype === "success" + } else if (message.type === 'result') { + const ok = message.subtype === 'success' onEvent({ - kind: "done", + kind: 'done', ok, - summary: ok ? message.result : "", + summary: ok ? message.result : '', cost_usd: - typeof message.total_cost_usd === "number" + typeof message.total_cost_usd === 'number' ? message.total_cost_usd : null, }) } } } finally { - signal.removeEventListener("abort", forward_abort) + signal.removeEventListener('abort', forward_abort) } } @@ -122,11 +132,11 @@ function buildSystemAppend( sdk: Sdk, workspace_name: string, framework?: string | null, - mode?: "full_api" | "customer_portal" + mode?: 'full_api' | 'customer_portal', ): string { - const language = sdk === "python" ? "Python" : "JavaScript/TypeScript" + const language = sdk === 'python' ? 'Python' : 'JavaScript/TypeScript' const framework_label = framework ?? "this project's framework" - const mode_label = mode === "customer_portal" ? "Customer Portal" : "full-API" + const mode_label = mode === 'customer_portal' ? 'Customer Portal' : 'full-API' return [ `You are the Seam integration agent, embedded in the seam-wizard CLI.`, `The developer has just connected their Seam account (workspace "${workspace_name}"),`, @@ -147,7 +157,7 @@ function buildSystemAppend( `Then implement exactly what the developer asked for — nothing more. Load SEAM_API_KEY from the`, `existing .env; never hardcode or print it. Keep changes minimal and idiomatic. When finished,`, `give a short summary of the files you changed and how to run the result.`, - ].join("\n") + ].join('\n') } // Route the embedded agent through Seam-hosted inference: point the SDK at the @@ -173,16 +183,17 @@ function buildAgentEnv(inference: { // without asserting the input's shape. function describeToolInput(input: unknown): string { return ( - stringField(input, "file_path") ?? - stringField(input, "pattern") ?? - stringField(input, "query") ?? - stringField(input, "url") ?? - "" + stringField(input, 'file_path') ?? + stringField(input, 'pattern') ?? + stringField(input, 'query') ?? + stringField(input, 'url') ?? + '' ) } function stringField(input: unknown, key: string): string | null { - if (typeof input !== "object" || input === null || !(key in input)) return null + if (typeof input !== 'object' || input === null || !(key in input)) + return null const value = (input as Record)[key] - return typeof value === "string" ? value : null + return typeof value === 'string' ? value : null } diff --git a/src/lib/util/env-file.ts b/src/lib/util/env-file.ts index 2872b7b..cd7e193 100644 --- a/src/lib/util/env-file.ts +++ b/src/lib/util/env-file.ts @@ -1,14 +1,14 @@ -import { existsSync, readFileSync, writeFileSync } from "node:fs" -import { join } from "node:path" +import { existsSync, readFileSync, writeFileSync } from 'node:fs' +import { join } from 'node:path' -export type EnvWriteResult = "created" | "updated" | "added" +export type EnvWriteResult = 'created' | 'updated' | 'added' // dotenv files we look at for an existing key, in priority order. const ENV_FILE_NAMES = [ - ".env.local", - ".env", - ".env.development", - ".env.development.local", + '.env.local', + '.env', + '.env.development', + '.env.development.local', ] export interface FoundApiKey { @@ -21,16 +21,16 @@ export interface FoundApiKey { export function findExistingApiKey(root: string): FoundApiKey | null { const from_process = process.env.SEAM_API_KEY?.trim() if (from_process != null && from_process.length > 0) { - return { api_key: from_process, source: "environment" } + return { api_key: from_process, source: 'environment' } } for (const file_name of ENV_FILE_NAMES) { const file_path = join(root, file_name) if (!existsSync(file_path)) continue - const match = readFileSync(file_path, "utf8").match( - /^\s*SEAM_API_KEY\s*=\s*(.+?)\s*$/m + const match = readFileSync(file_path, 'utf8').match( + /^\s*SEAM_API_KEY\s*=\s*(.+?)\s*$/m, ) - const value = match?.[1]?.replace(/^["']|["']$/g, "").trim() + const value = match?.[1]?.replace(/^["']|["']$/g, '').trim() if (value != null && value.length > 0) { return { api_key: value, source: file_name } } @@ -44,24 +44,24 @@ export function findExistingApiKey(root: string): FoundApiKey | null { export function upsertEnvVar( file_path: string, key: string, - value: string + value: string, ): EnvWriteResult { const line = `${key}=${value}` if (!existsSync(file_path)) { writeFileSync(file_path, `${line}\n`) - return "created" + return 'created' } - const content = readFileSync(file_path, "utf8") - const existing_line = new RegExp(`^${key}=.*$`, "m") + const content = readFileSync(file_path, 'utf8') + const existing_line = new RegExp(`^${key}=.*$`, 'm') if (existing_line.test(content)) { writeFileSync(file_path, content.replace(existing_line, line)) - return "updated" + return 'updated' } - const separator = content.length === 0 || content.endsWith("\n") ? "" : "\n" + const separator = content.length === 0 || content.endsWith('\n') ? '' : '\n' writeFileSync(file_path, `${content}${separator}${line}\n`) - return "added" + return 'added' } diff --git a/src/lib/util/run-install.ts b/src/lib/util/run-install.ts index 97433ff..0d8db48 100644 --- a/src/lib/util/run-install.ts +++ b/src/lib/util/run-install.ts @@ -1,4 +1,4 @@ -import { spawn } from "node:child_process" +import { spawn } from 'node:child_process' // Run a command with piped output, streaming each non-empty line to `onLine` // so the Ink UI can render progress instead of clobbering the frame with @@ -6,30 +6,30 @@ import { spawn } from "node:child_process" export function runInstall( command: string[], cwd: string, - onLine: (line: string) => void + onLine: (line: string) => void, ): Promise { const [binary, ...args] = command - if (binary == null) throw new Error("runInstall: empty command") + if (binary == null) throw new Error('runInstall: empty command') return new Promise((resolve, reject) => { const child = spawn(binary, args, { cwd, - stdio: ["ignore", "pipe", "pipe"], + stdio: ['ignore', 'pipe', 'pipe'], shell: false, }) const handle = (data: Buffer): void => { - for (const line of data.toString().split("\n")) { + for (const line of data.toString().split('\n')) { if (line.trim().length > 0) onLine(line.trimEnd()) } } - child.stdout?.on("data", handle) - child.stderr?.on("data", handle) + child.stdout?.on('data', handle) + child.stderr?.on('data', handle) - child.on("error", reject) - child.on("close", (code) => { + child.on('error', reject) + child.on('close', (code) => { if (code === 0) resolve() - else reject(new Error(`${binary} exited with code ${code ?? "unknown"}`)) + else reject(new Error(`${binary} exited with code ${code ?? 'unknown'}`)) }) }) } diff --git a/src/lib/util/seam-api.ts b/src/lib/util/seam-api.ts index 127d2e8..34e6af3 100644 --- a/src/lib/util/seam-api.ts +++ b/src/lib/util/seam-api.ts @@ -2,7 +2,7 @@ // avoid pulling in the full SDK just for a health check — a single fetch keeps // the wizard's install footprint tiny. -const SEAM_API_BASE = "https://connect.getseam.com" +const SEAM_API_BASE = 'https://connect.getseam.com' export interface SeamWorkspace { workspace_id: string @@ -15,38 +15,38 @@ export class ApiKeyError extends Error {} // Validates the key by fetching the workspace it belongs to. Returns the // workspace so the wizard can show which workspace the key is for. export async function getWorkspaceForApiKey( - api_key: string + api_key: string, ): Promise { let response: Response try { response = await fetch(`${SEAM_API_BASE}/workspaces/get`, { - method: "POST", + method: 'POST', headers: { authorization: `Bearer ${api_key}`, - "content-type": "application/json", + 'content-type': 'application/json', }, - body: "{}", + body: '{}', }) } catch { throw new ApiKeyError( - "Could not reach the Seam API. Check your network connection and try again." + 'Could not reach the Seam API. Check your network connection and try again.', ) } if (response.status === 401) { throw new ApiKeyError( - "That key was rejected (401). Make sure you copied the full key, including the seam_ prefix." + 'That key was rejected (401). Make sure you copied the full key, including the seam_ prefix.', ) } if (!response.ok) { throw new ApiKeyError( - `The Seam API returned ${response.status}. Please try again in a moment.` + `The Seam API returned ${response.status}. Please try again in a moment.`, ) } const body = (await response.json()) as { workspace?: SeamWorkspace } if (body.workspace == null) { - throw new ApiKeyError("Unexpected response from the Seam API.") + throw new ApiKeyError('Unexpected response from the Seam API.') } return body.workspace } @@ -80,27 +80,27 @@ export interface WizardInferenceSession { // not the API key — is what the embedded agent sends to Seam-hosted inference, // so the long-lived key stays off the repeated inference path. export async function exchangeWizardInferenceToken( - api_key: string + api_key: string, ): Promise { let response: Response try { response = await fetch(`${SEAM_INFERENCE_BASE_URL}/session`, { - method: "POST", + method: 'POST', headers: { authorization: `Bearer ${api_key}`, - "content-type": "application/json", + 'content-type': 'application/json', }, - body: "{}", + body: '{}', }) } catch { throw new ApiKeyError( - "Could not reach Seam to start the AI session. Check your network connection and try again." + 'Could not reach Seam to start the AI session. Check your network connection and try again.', ) } if (!response.ok) { throw new ApiKeyError( - `Seam couldn't start the AI session (${response.status}). Please try again in a moment.` + `Seam couldn't start the AI session (${response.status}). Please try again in a moment.`, ) } @@ -109,7 +109,9 @@ export async function exchangeWizardInferenceToken( onboarding?: WizardOnboarding | null } if (body.wizard_session == null) { - throw new ApiKeyError("Unexpected response from Seam starting the AI session.") + throw new ApiKeyError( + 'Unexpected response from Seam starting the AI session.', + ) } return { token: body.wizard_session.token, @@ -124,21 +126,21 @@ export async function exchangeWizardInferenceToken( // recommendation — not the full integration (that goes through the agent SDK). export async function callInferenceForText( inference: { base_url: string; token: string }, - args: { model: string; max_tokens: number; system: string; user: string } + args: { model: string; max_tokens: number; system: string; user: string }, ): Promise { const response = await fetch(`${inference.base_url}/v1/messages`, { - method: "POST", + method: 'POST', headers: { authorization: `Bearer ${inference.token}`, - "content-type": "application/json", - "anthropic-version": "2023-06-01", + 'content-type': 'application/json', + 'anthropic-version': '2023-06-01', }, body: JSON.stringify({ model: args.model, max_tokens: args.max_tokens, system: args.system, stream: true, - messages: [{ role: "user", content: args.user }], + messages: [{ role: 'user', content: args.user }], }), }) @@ -150,33 +152,33 @@ export async function callInferenceForText( } async function readTextDeltas( - body: ReadableStream + body: ReadableStream, ): Promise { const reader = body.getReader() const decoder = new TextDecoder() - let buffer = "" - let text = "" + let buffer = '' + let text = '' for (;;) { const { value, done } = await reader.read() if (done) break buffer += decoder.decode(value, { stream: true }) - const lines = buffer.split("\n") - buffer = lines.pop() ?? "" + const lines = buffer.split('\n') + buffer = lines.pop() ?? '' for (const line of lines) { const trimmed = line.trim() - if (!trimmed.startsWith("data:")) continue - const payload = trimmed.slice("data:".length).trim() - if (payload.length === 0 || payload === "[DONE]") continue + if (!trimmed.startsWith('data:')) continue + const payload = trimmed.slice('data:'.length).trim() + if (payload.length === 0 || payload === '[DONE]') continue try { const event = JSON.parse(payload) as { type?: string delta?: { type?: string; text?: string } } if ( - event.type === "content_block_delta" && - event.delta?.type === "text_delta" + event.type === 'content_block_delta' && + event.delta?.type === 'text_delta' ) { - text += event.delta.text ?? "" + text += event.delta.text ?? '' } } catch { // Ignore keep-alive pings and any non-JSON SSE lines. From 4b2fdf758da2bfaf32a42899c6d0507fda13a508 Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 29 Jul 2026 02:00:13 +0000 Subject: [PATCH 06/11] refactor: conform the migrated wizard source to the repo lint and type rules MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The migrated source was written against a looser configuration than this repository's. Bring it into line, with no behavior changes: - camelCase for local variables, parameters, and destructuring bindings. Property names are left alone: the repo's camelcase rule sets `properties: 'never'`, and these types mirror the Seam API wire format and the on-disk .seam/onboarding.json record, so renaming them would change the protocol. Snake_case properties destructured into locals are aliased, e.g. `const { api_key: apiKey } = phase`. - Replace `void (async () => {…})()` in the app's effects, which the no-void rule rejects, with a named async function and an explicit rejection handler that routes to the app's existing error phase. `void` had turned an unexpected rejection into an unhandled rejection, so failures that previously vanished are now shown to the user. - Access environment variables and Record members by index, as noPropertyAccessFromIndexSignature requires. - Drop the unneeded default React imports in favor of named imports, since the repo compiles with jsx: react-jsx and verbatimModuleSyntax. - Omit Ink's `color` prop on non-cursor checkbox rows rather than passing undefined, which exactOptionalPropertyTypes rejects. The rows still inherit the terminal default. - Brace multi-line conditionals for the curly rule, and drop a dead local. `npm run lint` and `npm run typecheck` now pass over src. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01W1qhuuZQZnA1FB1fBSxrqx --- src/lib/app.tsx | 249 ++++++++++++++++++--------- src/lib/components/checkbox-list.tsx | 29 ++-- src/lib/steps/analyze-project.ts | 36 ++-- src/lib/steps/authenticate.ts | 4 +- src/lib/steps/build-plan.ts | 6 +- src/lib/steps/connect-web.ts | 4 +- src/lib/steps/detect-project.ts | 8 +- src/lib/steps/install-seam-plugin.ts | 4 +- src/lib/steps/integrate.ts | 35 ++-- src/lib/util/env-file.ts | 32 ++-- src/lib/util/seam-api.ts | 8 +- 11 files changed, 249 insertions(+), 166 deletions(-) diff --git a/src/lib/app.tsx b/src/lib/app.tsx index a42549c..9962243 100644 --- a/src/lib/app.tsx +++ b/src/lib/app.tsx @@ -2,7 +2,13 @@ import { Box, Text, useApp, useStdout } from 'ink' import SelectInput from 'ink-select-input' import Spinner from 'ink-spinner' import TextInput from 'ink-text-input' -import React, { useEffect, useRef, useState } from 'react' +import { + type ReactElement, + type ReactNode, + useEffect, + useRef, + useState, +} from 'react' import { CheckboxList } from './components/checkbox-list.js' import { @@ -77,11 +83,11 @@ export function App({ }: { root: string onExit?: (lines: string[]) => void -}): React.ReactElement { +}): ReactElement { const { exit } = useApp() const { stdout } = useStdout() - const project_ref = useRef(detectProject(root)) - const attempt_ref = useRef(0) + const projectRef = useRef(detectProject(root)) + const attemptRef = useRef(0) const [dimensions, setDimensions] = useState({ rows: stdout?.rows ?? 24, @@ -100,11 +106,11 @@ export function App({ url: null, received: false, }) - const [paste_value, setPasteValue] = useState('') - const [paste_error, setPasteError] = useState(null) - const [install_lines, setInstallLines] = useState([]) - const [note_value, setNoteValue] = useState('') - const [agent_lines, setAgentLines] = useState([]) + const [pasteValue, setPasteValue] = useState('') + const [pasteError, setPasteError] = useState(null) + const [installLines, setInstallLines] = useState([]) + const [noteValue, setNoteValue] = useState('') + const [agentLines, setAgentLines] = useState([]) const [session, setSession] = useState(null) const [analysis, setAnalysis] = useState(null) const [mode, setMode] = useState(null) @@ -112,15 +118,15 @@ export function App({ // The plan record (written to .seam/onboarding.json before the agent runs); // the done handler rewrites it with the run result. - const plan_ref = useRef | null>(null) + const planRef = useRef | null>(null) // Kept in sync with `messages` so the exit handler can hand the full // transcript to index.tsx to reprint after leaving the alt screen. - const messages_ref = useRef([]) + const messagesRef = useRef([]) const addMessage = (message: Msg): void => setMessages((previous) => { const next = [...previous, message] - messages_ref.current = next + messagesRef.current = next return next }) @@ -131,21 +137,21 @@ export function App({ // Compose the goal from the chosen mode + building blocks + optional note, // persist the plan to .seam/onboarding.json, then hand off to the agent. - const startIntegration = (note_input: string): void => { - const note = note_input.trim().length > 0 ? note_input.trim() : null - const effective_mode: BuildMode = mode ?? 'full_api' - const effective_selections = - effective_mode === 'customer_portal' ? [] : selections + const startIntegration = (noteInput: string): void => { + const note = noteInput.trim().length > 0 ? noteInput.trim() : null + const effectiveMode: BuildMode = mode ?? 'full_api' + const effectiveSelections = + effectiveMode === 'customer_portal' ? [] : selections const goal = composeGoal({ - mode: effective_mode, - selections: effective_selections, + mode: effectiveMode, + selections: effectiveSelections, note, framework: analysis?.signals.framework ?? null, }) const record: Omit = { created_at: new Date().toISOString(), - mode: effective_mode, - selections: effective_selections, + mode: effectiveMode, + selections: effectiveSelections, note, goal, analysis: { @@ -157,7 +163,7 @@ export function App({ recommendation_source: analysis?.recommendation.source ?? 'heuristic', }, } - plan_ref.current = record + planRef.current = record try { writeOnboardingRecord(root, record) addMessage({ @@ -183,10 +189,10 @@ export function App({ ]) } else { setAgentLines([]) - if (plan_ref.current != null) { + if (planRef.current != null) { try { writeOnboardingRecord(root, { - ...plan_ref.current, + ...planRef.current, result: { ok: event.ok, files_summary: event.summary.trim().slice(0, 4000), @@ -209,8 +215,9 @@ export function App({ }, ) for (const line of event.summary.trim().split('\n').slice(0, 15)) { - if (line.trim().length > 0) + if (line.trim().length > 0) { addMessage({ tone: 'plain', text: ` ${line}` }) + } } if (event.cost_usd != null) { addMessage({ @@ -223,7 +230,7 @@ export function App({ // Once a workspace is known, choose SDK (or skip if detected) then install. const advanceAfterAuth = (): void => { - const detected = project_ref.current.detected_sdk + const detected = projectRef.current.detected_sdk if (detected != null) { setSdk(detected) addMessage({ tone: 'info', text: `Detected ${detected} project` }) @@ -237,7 +244,7 @@ export function App({ useEffect(() => { if (phase.t !== 'init') return let cancelled = false - void (async () => { + const run = async (): Promise => { const existing = await findVerifiedExistingKey(root) if (cancelled) return if (existing != null) { @@ -250,7 +257,17 @@ export function App({ } else { setPhase({ t: 'method' }) } - })() + } + run().catch((error: unknown) => { + if (cancelled) return + setPhase({ + t: 'error', + message: + error instanceof Error + ? error.message + : 'The wizard hit an unexpected error.', + }) + }) return () => { cancelled = true } @@ -260,7 +277,7 @@ export function App({ useEffect(() => { if (phase.t !== 'browser') return let cancelled = false - void (async () => { + const run = async (): Promise => { try { const result = await connectViaWeb(root, { onUrl: (url) => !cancelled && setBrowser((b) => ({ ...b, url })), @@ -285,7 +302,17 @@ export function App({ }) } } - })() + } + run().catch((error: unknown) => { + if (cancelled) return + setPhase({ + t: 'error', + message: + error instanceof Error + ? error.message + : 'The wizard hit an unexpected error.', + }) + }) return () => { cancelled = true } @@ -294,11 +321,11 @@ export function App({ // verify a pasted key useEffect(() => { if (phase.t !== 'verify-paste') return - const { api_key } = phase + const { api_key: apiKey } = phase let cancelled = false - void (async () => { + const run = async (): Promise => { try { - const result = await verifyAndSaveKey(root, api_key) + const result = await verifyAndSaveKey(root, apiKey) if (cancelled) return setWorkspace(result.workspace) addMessage({ tone: 'ok', text: `Workspace: ${result.workspace.name}` }) @@ -309,7 +336,7 @@ export function App({ error instanceof ApiKeyError ? error.message : "Couldn't verify the key." - if (attempt_ref.current >= MAX_ATTEMPTS) { + if (attemptRef.current >= MAX_ATTEMPTS) { setPhase({ t: 'error', message: 'Too many attempts. Re-run with a valid key.', @@ -320,7 +347,17 @@ export function App({ setPhase({ t: 'paste' }) } } - })() + } + run().catch((error: unknown) => { + if (cancelled) return + setPhase({ + t: 'error', + message: + error instanceof Error + ? error.message + : 'The wizard hit an unexpected error.', + }) + }) return () => { cancelled = true } @@ -330,12 +367,13 @@ export function App({ useEffect(() => { if (phase.t !== 'install-sdk' || sdk == null) return let cancelled = false - const command = installSeamSdkCommand(sdk, project_ref.current) - void (async () => { + const command = installSeamSdkCommand(sdk, projectRef.current) + const run = async (): Promise => { try { await runInstall(command, root, (line) => { - if (!cancelled) + if (!cancelled) { setInstallLines((previous) => [...previous.slice(-3), line]) + } }) if (!cancelled) addMessage({ tone: 'ok', text: 'Seam SDK installed' }) } catch { @@ -350,7 +388,17 @@ export function App({ setInstallLines([]) setPhase({ t: 'install-plugin' }) } - })() + } + run().catch((error: unknown) => { + if (cancelled) return + setPhase({ + t: 'error', + message: + error instanceof Error + ? error.message + : 'The wizard hit an unexpected error.', + }) + }) return () => { cancelled = true } @@ -361,18 +409,19 @@ export function App({ // point at the native /plugin path, which also wires up the seam-docs MCP. useEffect(() => { if (phase.t !== 'install-plugin') return - const workspace_name = workspace?.name ?? 'your workspace' const target = detectPluginTarget(root) let cancelled = false - void (async () => { + const run = async (): Promise => { try { await runInstall(SEAM_PLUGIN_NPX_COMMAND, root, (line) => { - if (!cancelled) + if (!cancelled) { setInstallLines((previous) => [...previous.slice(-3), line]) + } }) - if (!cancelled) + if (!cancelled) { addMessage({ tone: 'ok', text: 'Installed the Seam plugin skills' }) + } } catch { if (!cancelled) { addMessage({ @@ -393,7 +442,17 @@ export function App({ } setPhase({ t: 'offer-integrate' }) } - })() + } + run().catch((error: unknown) => { + if (cancelled) return + setPhase({ + t: 'error', + message: + error instanceof Error + ? error.message + : 'The wizard hit an unexpected error.', + }) + }) return () => { cancelled = true } @@ -406,7 +465,7 @@ export function App({ useEffect(() => { if (phase.t !== 'analyze') return let cancelled = false - void (async () => { + const run = async (): Promise => { const found = findExistingApiKey(root) if (found == null) { addMessage({ @@ -417,9 +476,9 @@ export function App({ return } - let current_session: WizardInferenceSession + let currentSession: WizardInferenceSession try { - current_session = await exchangeWizardInferenceToken(found.api_key) + currentSession = await exchangeWizardInferenceToken(found.api_key) } catch (error) { if (!cancelled) { addMessage({ @@ -433,15 +492,15 @@ export function App({ return } if (cancelled) return - setSession(current_session) + setSession(currentSession) const result = await analyzeProject({ root, - project: project_ref.current, - onboarding: current_session.onboarding, + project: projectRef.current, + onboarding: currentSession.onboarding, inference: { base_url: SEAM_INFERENCE_BASE_URL, - token: current_session.token, + token: currentSession.token, }, }) if (cancelled) return @@ -459,14 +518,24 @@ export function App({ tone: 'info', text: `Analyzed your project${detail.length > 0 ? `: ${detail}` : ''}`, }) - if (current_session.onboarding != null) { + if (currentSession.onboarding != null) { addMessage({ tone: 'plain', text: ' Used your Console onboarding answers', }) } setPhase({ t: 'integrate-mode' }) - })() + } + run().catch((error: unknown) => { + if (cancelled) return + setPhase({ + t: 'error', + message: + error instanceof Error + ? error.message + : 'The wizard hit an unexpected error.', + }) + }) return () => { cancelled = true } @@ -479,7 +548,7 @@ export function App({ if (phase.t !== 'integrate') return const { goal } = phase const controller = new AbortController() - void (async () => { + const run = async (): Promise => { if (session == null) { addMessage({ tone: 'warn', @@ -517,7 +586,17 @@ export function App({ } } if (!controller.signal.aborted) finishWithNextSteps() - })() + } + run().catch((error: unknown) => { + if (controller.signal.aborted) return + setPhase({ + t: 'error', + message: + error instanceof Error + ? error.message + : 'The wizard hit an unexpected error.', + }) + }) return () => controller.abort() }, [phase.t]) @@ -541,9 +620,9 @@ export function App({ } // Hand the transcript back so index.tsx can reprint it once the alt screen // is torn down (its contents are otherwise discarded). Small delay lets the - // final addMessage above flush into messages_ref. + // final addMessage above flush into messagesRef. const id = setTimeout(() => { - onExit?.(messages_ref.current.map(formatMessageLine)) + onExit?.(messagesRef.current.map(formatMessageLine)) exit() }, 40) return () => clearTimeout(id) @@ -552,8 +631,8 @@ export function App({ // Full-screen: a header, the transcript (bounded to what fits), then the // active step. The outer box is sized to the terminal so it fills the alt // screen; older transcript lines scroll off the top and are reprinted on exit. - const transcript_capacity = Math.max(1, dimensions.rows - 8) - const visible_messages = messages.slice(-transcript_capacity) + const transcriptCapacity = Math.max(1, dimensions.rows - 8) + const visibleMessages = messages.slice(-transcriptCapacity) return (
- {visible_messages.map((message, index) => ( + {visibleMessages.map((message, index) => ( ))} @@ -572,7 +651,7 @@ export function App({ ) - function renderActive(): React.ReactElement | null { + function renderActive(): ReactElement | null { switch (phase.t) { case 'init': return @@ -593,7 +672,7 @@ export function App({ ]} onSelect={(item) => { if (item.value === 'paste') { - attempt_ref.current = 0 + attemptRef.current = 0 setPhase({ t: 'paste' }) } else { setPhase({ t: 'browser' }) @@ -621,7 +700,7 @@ export function App({ {'› '} - {paste_error != null && {paste_error}} + {pasteError != null && {pasteError}} ) case 'verify-paste': @@ -665,7 +744,7 @@ export function App({ return ( - {install_lines.map((line, index) => ( + {installLines.map((line, index) => ( {' '} {line} @@ -677,7 +756,7 @@ export function App({ return ( - {install_lines.map((line, index) => ( + {installLines.map((line, index) => ( {' '} {line} @@ -707,18 +786,18 @@ export function App({ return case 'integrate-mode': { const recommended: BuildMode = mode ?? 'full_api' - const portal_item = { + const portalItem = { label: 'Customer Portal — Seam hosts the UI (you call ~2 endpoints)', value: 'customer_portal', } - const api_item = { + const apiItem = { label: 'Full API control — you build the UI, wire up the API', value: 'full_api', } const items = recommended === 'customer_portal' - ? [portal_item, api_item] - : [api_item, portal_item] + ? [portalItem, apiItem] + : [apiItem, portalItem] return ( {analysis?.recommendation.rationale != null && @@ -774,7 +853,7 @@ export function App({ {'› '} startIntegration(value)} @@ -786,7 +865,7 @@ export function App({ return ( - {agent_lines.map((line, index) => ( + {agentLines.map((line, index) => ( {' '} {line} @@ -804,27 +883,27 @@ export function App({ function pushNextSteps( addMessage: (message: Msg) => void, sdk: Sdk | null, - workspace_name: string, + workspaceName: string, ): void { - const env_hint = + const envHint = sdk === 'python' ? "Make sure SEAM_API_KEY is exported (it's in .env)." : 'Add .env to .gitignore — it holds your API key.' addMessage({ tone: 'plain', text: '' }) - addMessage({ tone: 'ok', text: `You're set up in ${workspace_name}` }) + addMessage({ tone: 'ok', text: `You're set up in ${workspaceName}` }) addMessage({ tone: 'plain', text: 'Next steps:' }) addMessage({ tone: 'plain', text: ' 1. Describe your integration to your AI assistant — e.g. "add Seam access grants". The Seam skill will guide it.', }) - addMessage({ tone: 'plain', text: ` 2. ${env_hint}` }) + addMessage({ tone: 'plain', text: ` 2. ${envHint}` }) addMessage({ tone: 'plain', text: ' 3. Docs: https://docs.seam.co' }) } -function truncate(text: string, max_length: number): string { +function truncate(text: string, maxLength: number): string { const collapsed = text.replace(/\s+/g, ' ').trim() - return collapsed.length > max_length - ? `${collapsed.slice(0, max_length - 1)}…` + return collapsed.length > maxLength + ? `${collapsed.slice(0, maxLength - 1)}…` : collapsed } @@ -847,7 +926,7 @@ function formatTool(name: string, detail: string): string { return detail.length > 0 ? `${verb} ${truncate(detail, 60)}` : verb } -function Header(): React.ReactElement { +function Header(): ReactElement { return ( @@ -866,7 +945,7 @@ function formatMessageLine(message: Msg): string { return `${symbol} ${message.text}` } -function MessageLine({ message }: { message: Msg }): React.ReactElement { +function MessageLine({ message }: { message: Msg }): ReactElement { if (message.tone === 'plain') return {message.text} const symbol = message.tone === 'ok' ? '✔' : message.tone === 'warn' ? '▲' : '•' @@ -883,7 +962,7 @@ function MessageLine({ message }: { message: Msg }): React.ReactElement { ) } -function Pending({ label }: { label: string }): React.ReactElement { +function Pending({ label }: { label: string }): ReactElement { return ( @@ -899,8 +978,8 @@ function Prompt({ children, }: { title: string - children: React.ReactNode -}): React.ReactElement { + children: ReactNode +}): ReactElement { return ( {title} diff --git a/src/lib/components/checkbox-list.tsx b/src/lib/components/checkbox-list.tsx index 08c9958..f8d7596 100644 --- a/src/lib/components/checkbox-list.tsx +++ b/src/lib/components/checkbox-list.tsx @@ -1,5 +1,5 @@ import { Box, Text, useInput } from 'ink' -import React, { useState } from 'react' +import { Fragment, type ReactElement, useState } from 'react' export interface CheckboxItem { id: string @@ -12,16 +12,16 @@ export interface CheckboxItem { // one over useInput. Only mounted during the checklist phase. export function CheckboxList({ items, - initial_selected, + initial_selected: initialSelected, onSubmit, }: { items: CheckboxItem[] initial_selected: string[] onSubmit: (selected: string[]) => void -}): React.ReactElement { +}): ReactElement { const [cursor, setCursor] = useState(0) const [selected, setSelected] = useState>( - () => new Set(initial_selected), + () => new Set(initialSelected), ) useInput((input, key) => { @@ -48,19 +48,22 @@ export function CheckboxList({ return ( {items.map((item, index) => { - const is_cursor = index === cursor - const is_checked = selected.has(item.id) - const show_group = + const isCursor = index === cursor + const isChecked = selected.has(item.id) + const showGroup = item.group != null && item.group !== items[index - 1]?.group return ( - - {show_group && {` ${item.group ?? ''}`}} - - {is_cursor ? '❯ ' : ' '} - {is_checked ? '◉ ' : '◯ '} + + {showGroup && {` ${item.group ?? ''}`}} + {/* Non-cursor rows omit `color` entirely (rather than passing + undefined, which exactOptionalPropertyTypes rejects) so they + inherit the terminal's default color. */} + + {isCursor ? '❯ ' : ' '} + {isChecked ? '◉ ' : '◯ '} {item.label} - + ) })} {' ↑/↓ move · space toggle · enter confirm'} diff --git a/src/lib/steps/analyze-project.ts b/src/lib/steps/analyze-project.ts index 3ccb87a..8f7d642 100644 --- a/src/lib/steps/analyze-project.ts +++ b/src/lib/steps/analyze-project.ts @@ -73,7 +73,7 @@ function gatherProjectSignals( root: string, project: ProjectInfo, ): ProjectSignals { - const package_json = readJsonIfExists(join(root, 'package.json')) as { + const packageJson = readJsonIfExists(join(root, 'package.json')) as { name?: string description?: string keywords?: string[] @@ -81,30 +81,30 @@ function gatherProjectSignals( devDependencies?: Record } | null - const dependency_names = [ - ...Object.keys(package_json?.dependencies ?? {}), - ...Object.keys(package_json?.devDependencies ?? {}), + const dependencyNames = [ + ...Object.keys(packageJson?.dependencies ?? {}), + ...Object.keys(packageJson?.devDependencies ?? {}), ] return { sdk: project.detected_sdk, - framework: detectFramework(root, project.detected_sdk, dependency_names), - package_name: package_json?.name ?? null, - description: package_json?.description ?? null, - keywords: package_json?.keywords ?? [], - dependency_names: dependency_names.slice(0, 40), + framework: detectFramework(root, project.detected_sdk, dependencyNames), + package_name: packageJson?.name ?? null, + description: packageJson?.description ?? null, + keywords: packageJson?.keywords ?? [], + dependency_names: dependencyNames.slice(0, 40), readme_excerpt: readReadmeExcerpt(root), seam_already_setup: - dependency_names.includes('seam') || findExistingApiKey(root) != null, + dependencyNames.includes('seam') || findExistingApiKey(root) != null, } } function detectFramework( root: string, sdk: Sdk | null, - dependency_names: string[], + dependencyNames: string[], ): string | null { - const has = (name: string): boolean => dependency_names.includes(name) + const has = (name: string): boolean => dependencyNames.includes(name) if (sdk === 'javascript') { if (has('next')) return 'Next.js' @@ -169,7 +169,7 @@ async function recommendViaLlm( onboarding: WizardOnboarding | null, inference: { base_url: string; token: string }, ): Promise { - const block_menu = ALL_BLOCKS.map( + const blockMenu = ALL_BLOCKS.map( (block) => ` "${block.id}" — ${block.label}`, ).join('\n') @@ -202,7 +202,7 @@ async function recommendViaLlm( '2) selections: building-block ids to scaffold — only for "full_api" (use', ' [] for customer_portal). Always include "access_grants" for full_api.', ' Valid ids:', - block_menu, + blockMenu, '3) app_type_guess: short phrase (e.g. "vacation rental", "coworking",', ' "property management", "unknown").', '4) rationale: one short sentence.', @@ -293,7 +293,7 @@ function heuristicRecommendation( terms.some((term) => haystack.includes(term)) const selections = new Set(['access_grants', 'connect_device']) - let app_type_guess: string | null = null + let appTypeGuess: string | null = null if ( mentions( @@ -308,7 +308,7 @@ function heuristicRecommendation( ) ) { selections.add('reservations') - app_type_guess = 'hospitality / short-term rental' + appTypeGuess = 'hospitality / short-term rental' } else if ( mentions( 'coworking', @@ -320,13 +320,13 @@ function heuristicRecommendation( ) ) { selections.add('user_identities') - app_type_guess = 'property / coworking' + appTypeGuess = 'property / coworking' } return { mode: 'full_api', selections: [...selections], - app_type_guess, + app_type_guess: appTypeGuess, rationale: 'Recommended from your project and onboarding answers.', source: 'heuristic', } diff --git a/src/lib/steps/authenticate.ts b/src/lib/steps/authenticate.ts index 32025f5..9b08aa5 100644 --- a/src/lib/steps/authenticate.ts +++ b/src/lib/steps/authenticate.ts @@ -34,9 +34,9 @@ export async function findVerifiedExistingKey( // Verify a pasted key and save it to .env. Throws (ApiKeyError) if invalid. export async function verifyAndSaveKey( root: string, - api_key: string, + apiKey: string, ): Promise { - const trimmed = api_key.trim() + const trimmed = apiKey.trim() const workspace = await getWorkspaceForApiKey(trimmed) upsertEnvVar(join(root, '.env'), 'SEAM_API_KEY', trimmed) return { workspace } diff --git a/src/lib/steps/build-plan.ts b/src/lib/steps/build-plan.ts index 98d8b7d..4b097a6 100644 --- a/src/lib/steps/build-plan.ts +++ b/src/lib/steps/build-plan.ts @@ -86,7 +86,7 @@ export function composeGoal(args: { }): string { const { mode, selections, note, framework } = args const target = framework ?? 'the project' - const note_suffix = + const noteSuffix = note != null && note.trim().length > 0 ? ` Additional context from the developer: ${note.trim()}` : '' @@ -99,7 +99,7 @@ export function composeGoal(args: { 'magic link), and regenerate the portal per visit since the session is ' + `short-lived. Wire it into ${target}'s conventions, load SEAM_API_KEY from ` + 'the existing .env, and add a short runnable example.' + - note_suffix + noteSuffix ) } @@ -114,7 +114,7 @@ export function composeGoal(args: { `Implement the following, wired into ${target}'s conventions:\n${hints}\n` + 'Load SEAM_API_KEY from the existing .env, keep changes minimal and ' + 'idiomatic, and add a short runnable example.' + - note_suffix + noteSuffix ) } diff --git a/src/lib/steps/connect-web.ts b/src/lib/steps/connect-web.ts index 87b0b75..0f5dc6a 100644 --- a/src/lib/steps/connect-web.ts +++ b/src/lib/steps/connect-web.ts @@ -9,7 +9,7 @@ import { getWorkspaceForApiKey, type SeamWorkspace } from 'lib/util/seam-api.js' // The dashboard "wizard" page mints a key and posts it back to the local // callback. Override the console host with SEAM_CONSOLE_URL for dev. -const CONSOLE_URL = process.env.SEAM_CONSOLE_URL ?? 'https://console.seam.co' +const CONSOLE_URL = process.env['SEAM_CONSOLE_URL'] ?? 'https://console.seam.co' const CONSOLE_WIZARD_PATH = '/dashboard/wizard' const CALLBACK_TIMEOUT_MS = 5 * 60 * 1000 @@ -96,7 +96,7 @@ export async function connectViaWeb( } const url = `${CONSOLE_URL}${CONSOLE_WIZARD_PATH}?cli_connect=1&cli_port=${address.port}&cli_state=${state}` events.onUrl?.(url) - void open(url).catch(() => { + open(url).catch(() => { // Browser may not open (headless/SSH) — the UI shows the URL to visit. }) events.onWaiting?.() diff --git a/src/lib/steps/detect-project.ts b/src/lib/steps/detect-project.ts index dc8c4ba..43fa4a0 100644 --- a/src/lib/steps/detect-project.ts +++ b/src/lib/steps/detect-project.ts @@ -23,15 +23,15 @@ export function detectProject(cwd: string): ProjectInfo { // null when the project is ambiguous (both or neither) — the wizard then asks. function detectSdk(cwd: string): Sdk | null { - const is_javascript = existsSync(join(cwd, 'package.json')) - const is_python = [ + const isJavascript = existsSync(join(cwd, 'package.json')) + const isPython = [ 'pyproject.toml', 'requirements.txt', 'setup.py', 'Pipfile', ].some((marker) => existsSync(join(cwd, marker))) - if (is_javascript && !is_python) return 'javascript' - if (is_python && !is_javascript) return 'python' + if (isJavascript && !isPython) return 'javascript' + if (isPython && !isJavascript) return 'python' return null } diff --git a/src/lib/steps/install-seam-plugin.ts b/src/lib/steps/install-seam-plugin.ts index 5f5d972..eafd3f5 100644 --- a/src/lib/steps/install-seam-plugin.ts +++ b/src/lib/steps/install-seam-plugin.ts @@ -32,7 +32,7 @@ export const CLAUDE_CODE_COMMANDS = [ // Detect Claude Code so we print its slash commands instead of running npx. export function detectPluginTarget(root: string): PluginTarget { - const has_claude_code = + const hasClaudeCode = existsSync(join(root, '.claude')) || existsSync(join(root, 'CLAUDE.md')) - return has_claude_code ? 'claude-code' : 'universal' + return hasClaudeCode ? 'claude-code' : 'universal' } diff --git a/src/lib/steps/integrate.ts b/src/lib/steps/integrate.ts index 24cfea7..5381ddc 100644 --- a/src/lib/steps/integrate.ts +++ b/src/lib/steps/integrate.ts @@ -44,7 +44,7 @@ export async function runIntegration(args: RunIntegrationArgs): Promise { const { root, sdk, - workspace_name, + workspace_name: workspaceName, goal, inference, framework, @@ -52,9 +52,9 @@ export async function runIntegration(args: RunIntegrationArgs): Promise { signal, onEvent, } = args - const abort_controller = new AbortController() - const forward_abort = (): void => abort_controller.abort() - signal.addEventListener('abort', forward_abort, { once: true }) + const abortController = new AbortController() + const forwardAbort = (): void => abortController.abort() + signal.addEventListener('abort', forwardAbort, { once: true }) try { for await (const message of query({ @@ -89,10 +89,10 @@ export async function runIntegration(args: RunIntegrationArgs): Promise { systemPrompt: { type: 'preset', preset: 'claude_code', - append: buildSystemAppend(sdk, workspace_name, framework, mode), + append: buildSystemAppend(sdk, workspaceName, framework, mode), }, maxTurns: 40, - abortController: abort_controller, + abortController, }, })) { if (signal.aborted) break @@ -124,28 +124,28 @@ export async function runIntegration(args: RunIntegrationArgs): Promise { } } } finally { - signal.removeEventListener('abort', forward_abort) + signal.removeEventListener('abort', forwardAbort) } } function buildSystemAppend( sdk: Sdk, - workspace_name: string, + workspaceName: string, framework?: string | null, mode?: 'full_api' | 'customer_portal', ): string { const language = sdk === 'python' ? 'Python' : 'JavaScript/TypeScript' - const framework_label = framework ?? "this project's framework" - const mode_label = mode === 'customer_portal' ? 'Customer Portal' : 'full-API' + const frameworkLabel = framework ?? "this project's framework" + const modeLabel = mode === 'customer_portal' ? 'Customer Portal' : 'full-API' return [ `You are the Seam integration agent, embedded in the seam-wizard CLI.`, - `The developer has just connected their Seam account (workspace "${workspace_name}"),`, + `The developer has just connected their Seam account (workspace "${workspaceName}"),`, `and their SEAM_API_KEY is already saved in a local .env file. This is a ${language} project.`, ``, `Before writing any code:`, `- Fetch the reference integration: call mcp__seam-docs__list_example_apps, then`, - ` mcp__seam-docs__get_example_app to pull the example matching ${framework_label} and the`, - ` ${mode_label} approach. Model your integration on it — match its structure and Seam API`, + ` mcp__seam-docs__get_example_app to pull the example matching ${frameworkLabel} and the`, + ` ${modeLabel} approach. Model your integration on it — match its structure and Seam API`, ` usage — but ADAPT it to this project's actual framework version, conventions, and file`, ` layout. Do not copy it verbatim, and don't add files this project doesn't need.`, `- Find and read the installed Seam skill. Glob for a directory named like "*seam*" under`, @@ -173,9 +173,9 @@ function buildAgentEnv(inference: { for (const [key, value] of Object.entries(process.env)) { if (value != null) env[key] = value } - delete env.ANTHROPIC_API_KEY - env.ANTHROPIC_BASE_URL = inference.base_url - env.ANTHROPIC_AUTH_TOKEN = inference.token + delete env['ANTHROPIC_API_KEY'] + env['ANTHROPIC_BASE_URL'] = inference.base_url + env['ANTHROPIC_AUTH_TOKEN'] = inference.token return env } @@ -192,8 +192,9 @@ function describeToolInput(input: unknown): string { } function stringField(input: unknown, key: string): string | null { - if (typeof input !== 'object' || input === null || !(key in input)) + if (typeof input !== 'object' || input === null || !(key in input)) { return null + } const value = (input as Record)[key] return typeof value === 'string' ? value : null } diff --git a/src/lib/util/env-file.ts b/src/lib/util/env-file.ts index cd7e193..874d4c5 100644 --- a/src/lib/util/env-file.ts +++ b/src/lib/util/env-file.ts @@ -19,20 +19,20 @@ export interface FoundApiKey { // Look for an existing SEAM_API_KEY: first the process environment, then the // project's dotenv files. Returns the first hit so the wizard can skip auth. export function findExistingApiKey(root: string): FoundApiKey | null { - const from_process = process.env.SEAM_API_KEY?.trim() - if (from_process != null && from_process.length > 0) { - return { api_key: from_process, source: 'environment' } + const fromProcess = process.env['SEAM_API_KEY']?.trim() + if (fromProcess != null && fromProcess.length > 0) { + return { api_key: fromProcess, source: 'environment' } } - for (const file_name of ENV_FILE_NAMES) { - const file_path = join(root, file_name) - if (!existsSync(file_path)) continue - const match = readFileSync(file_path, 'utf8').match( + for (const fileName of ENV_FILE_NAMES) { + const filePath = join(root, fileName) + if (!existsSync(filePath)) continue + const match = readFileSync(filePath, 'utf8').match( /^\s*SEAM_API_KEY\s*=\s*(.+?)\s*$/m, ) const value = match?.[1]?.replace(/^["']|["']$/g, '').trim() if (value != null && value.length > 0) { - return { api_key: value, source: file_name } + return { api_key: value, source: fileName } } } @@ -42,26 +42,26 @@ export function findExistingApiKey(root: string): FoundApiKey | null { // Upsert a KEY=value line into a dotenv file without disturbing other entries. // Returns what happened so the wizard can report it accurately. export function upsertEnvVar( - file_path: string, + filePath: string, key: string, value: string, ): EnvWriteResult { const line = `${key}=${value}` - if (!existsSync(file_path)) { - writeFileSync(file_path, `${line}\n`) + if (!existsSync(filePath)) { + writeFileSync(filePath, `${line}\n`) return 'created' } - const content = readFileSync(file_path, 'utf8') - const existing_line = new RegExp(`^${key}=.*$`, 'm') + const content = readFileSync(filePath, 'utf8') + const existingLine = new RegExp(`^${key}=.*$`, 'm') - if (existing_line.test(content)) { - writeFileSync(file_path, content.replace(existing_line, line)) + if (existingLine.test(content)) { + writeFileSync(filePath, content.replace(existingLine, line)) return 'updated' } const separator = content.length === 0 || content.endsWith('\n') ? '' : '\n' - writeFileSync(file_path, `${content}${separator}${line}\n`) + writeFileSync(filePath, `${content}${separator}${line}\n`) return 'added' } diff --git a/src/lib/util/seam-api.ts b/src/lib/util/seam-api.ts index 34e6af3..0f55ed3 100644 --- a/src/lib/util/seam-api.ts +++ b/src/lib/util/seam-api.ts @@ -15,14 +15,14 @@ export class ApiKeyError extends Error {} // Validates the key by fetching the workspace it belongs to. Returns the // workspace so the wizard can show which workspace the key is for. export async function getWorkspaceForApiKey( - api_key: string, + apiKey: string, ): Promise { let response: Response try { response = await fetch(`${SEAM_API_BASE}/workspaces/get`, { method: 'POST', headers: { - authorization: `Bearer ${api_key}`, + authorization: `Bearer ${apiKey}`, 'content-type': 'application/json', }, body: '{}', @@ -80,14 +80,14 @@ export interface WizardInferenceSession { // not the API key — is what the embedded agent sends to Seam-hosted inference, // so the long-lived key stays off the repeated inference path. export async function exchangeWizardInferenceToken( - api_key: string, + apiKey: string, ): Promise { let response: Response try { response = await fetch(`${SEAM_INFERENCE_BASE_URL}/session`, { method: 'POST', headers: { - authorization: `Bearer ${api_key}`, + authorization: `Bearer ${apiKey}`, 'content-type': 'application/json', }, body: '{}', From db1419e3b78122d9565b2e250490d3f3c1b6f241 Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 29 Jul 2026 02:01:40 +0000 Subject: [PATCH 07/11] test: replace the migrated smoke script with a render test The migrated source checked that the app mounts via scripts/smoke.tsx, a standalone script run by its own npm script. It reported results with console calls, which the no-console rule rejects, and it sat outside the tsconfig, so it was never typechecked. Move that check into the test suite as src/lib/app.test.tsx, where it runs under `npm test` with the rest of the suite and is covered by lint and typecheck. The assertion is unchanged: render the app against a project root that does not exist with no key in the environment, which keeps the render offline, and confirm the first frame. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01W1qhuuZQZnA1FB1fBSxrqx --- scripts/smoke.tsx | 25 ------------------------- src/lib/app.test.tsx | 27 +++++++++++++++++++++++++++ 2 files changed, 27 insertions(+), 25 deletions(-) delete mode 100644 scripts/smoke.tsx create mode 100644 src/lib/app.test.tsx diff --git a/scripts/smoke.tsx b/scripts/smoke.tsx deleted file mode 100644 index 3854964..0000000 --- a/scripts/smoke.tsx +++ /dev/null @@ -1,25 +0,0 @@ -// Minimal render smoke test: mount and assert the first frame renders -// without throwing. Uses a nonexistent root with no SEAM_API_KEY so no network -// call happens (findExistingApiKey returns null before any API request). -// A full interactive run can't be exercised headlessly. - -import { render } from 'ink-testing-library' -import React from 'react' - -import { App } from 'lib/app.js' - -delete process.env.SEAM_API_KEY - -const { lastFrame, unmount } = render( - , -) -const frame = lastFrame() ?? '' -unmount() - -if (!frame.includes('Checking')) { - console.error(`SMOKE FAIL — unexpected first frame:\n${frame}`) - process.exit(1) -} - -console.log('SMOKE OK — initial frame rendered') -process.exit(0) diff --git a/src/lib/app.test.tsx b/src/lib/app.test.tsx new file mode 100644 index 0000000..0697ecf --- /dev/null +++ b/src/lib/app.test.tsx @@ -0,0 +1,27 @@ +import { render } from 'ink-testing-library' +import { afterEach, expect, test, vi } from 'vitest' + +import { App } from './app.js' + +// A project root that does not exist, with no key in the environment, keeps the +// render offline: the first phase looks for an existing key, finds none, and +// returns before making a request. A full interactive run of the wizard cannot +// be exercised headlessly, so this covers the mount and the first frame only. +const NONEXISTENT_ROOT = '/nonexistent/seam-wizard-test-project' + +afterEach(() => { + vi.unstubAllEnvs() +}) + +test('App: renders the first frame while looking for an existing key', () => { + vi.stubEnv('SEAM_API_KEY', '') + + const { lastFrame, unmount } = render() + try { + const frame = lastFrame() ?? '' + expect(frame).toContain('Seam setup wizard') + expect(frame).toContain('Checking for an existing key') + } finally { + unmount() + } +}) From 71f5f1c8cd3efd238411f23304d16b8ba5bea126 Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 29 Jul 2026 02:05:56 +0000 Subject: [PATCH 08/11] test: cover the migrated wizard modules The migrated source arrived with no unit tests. Cover the pure, non-UI modules that the wizard's behavior depends on, so the coverage report this repository already produces means something: - env-file: which key wins between the environment and each dotenv file, quote stripping, and the created/added/updated outcomes of upserting a variable without disturbing neighboring entries. - detect-project: SDK detection from project markers, including the ambiguous cases, package manager and installer detection, and the install command produced for each. - build-plan: goal composition for both modes, the agent hints contributed by each selected building block, and the on-disk onboarding record. Every test that touches the filesystem uses its own temporary directory, and those reading SEAM_API_KEY stub it, so the suite neither depends on nor leaks ambient state. The networked modules and the Ink components are left out: the app's render is covered by src/lib/app.test.tsx. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01W1qhuuZQZnA1FB1fBSxrqx --- src/lib/steps/build-plan.test.ts | 211 +++++++++++++++++++++++++++ src/lib/steps/detect-project.test.ts | 186 +++++++++++++++++++++++ src/lib/util/env-file.test.ts | 162 ++++++++++++++++++++ 3 files changed, 559 insertions(+) create mode 100644 src/lib/steps/build-plan.test.ts create mode 100644 src/lib/steps/detect-project.test.ts create mode 100644 src/lib/util/env-file.test.ts diff --git a/src/lib/steps/build-plan.test.ts b/src/lib/steps/build-plan.test.ts new file mode 100644 index 0000000..139b7f5 --- /dev/null +++ b/src/lib/steps/build-plan.test.ts @@ -0,0 +1,211 @@ +import { mkdtempSync, readFileSync, rmSync } from 'node:fs' +import { tmpdir } from 'node:os' +import { join } from 'node:path' + +import { afterEach, beforeEach, expect, test } from 'vitest' + +import { + blockById, + composeGoal, + type OnboardingRecord, + writeOnboardingRecord, +} from './build-plan.js' + +let dir = '' + +beforeEach(() => { + dir = mkdtempSync(join(tmpdir(), 'seam-wizard-')) +}) + +afterEach(() => { + rmSync(dir, { recursive: true, force: true }) +}) + +const hintFor = (id: string): string => { + const block = blockById(id) + if (block == null) throw new Error(`Missing build block: ${id}`) + return block.agent_hint +} + +const hintLines = (goal: string): string[] => + goal.split('\n').filter((line) => line.startsWith('- ')) + +test('blockById: finds a core block', () => { + expect(blockById('access_grants')).toMatchObject({ + id: 'access_grants', + group: 'Core', + }) +}) + +test('blockById: finds a common block', () => { + expect(blockById('webhooks')).toMatchObject({ + id: 'webhooks', + group: 'Common', + }) +}) + +test('blockById: returns undefined for an unknown id', () => { + expect(blockById('not_a_block')).toBeUndefined() +}) + +test('composeGoal: describes the Customer Portal for customer_portal', () => { + const goal = composeGoal({ + mode: 'customer_portal', + selections: [], + note: null, + framework: null, + }) + + expect(goal).toContain('Customer Portal') +}) + +test('composeGoal: ignores selections for customer_portal', () => { + const goal = composeGoal({ + mode: 'customer_portal', + selections: ['access_codes', 'webhooks'], + note: null, + framework: null, + }) + + expect(goal).not.toContain(hintFor('access_codes')) + expect(hintLines(goal)).toEqual([]) +}) + +test('composeGoal: includes the agent hint of each selected block for full_api', () => { + const goal = composeGoal({ + mode: 'full_api', + selections: ['connect_device', 'webhooks'], + note: null, + framework: null, + }) + + expect(hintLines(goal)).toEqual([ + `- ${hintFor('connect_device')}`, + `- ${hintFor('webhooks')}`, + ]) +}) + +test('composeGoal: skips unknown block ids for full_api', () => { + const goal = composeGoal({ + mode: 'full_api', + selections: ['not_a_block', 'access_grants'], + note: null, + framework: null, + }) + + expect(hintLines(goal)).toEqual([`- ${hintFor('access_grants')}`]) +}) + +test('composeGoal: names the framework when given', () => { + const goal = composeGoal({ + mode: 'full_api', + selections: ['access_grants'], + note: null, + framework: 'Next.js', + }) + + expect(goal).toContain("Next.js's conventions") +}) + +test('composeGoal: falls back to a generic phrase without a framework', () => { + const goal = composeGoal({ + mode: 'full_api', + selections: ['access_grants'], + note: null, + framework: null, + }) + + expect(goal).toContain("the project's conventions") +}) + +test('composeGoal: names the framework for customer_portal too', () => { + const goal = composeGoal({ + mode: 'customer_portal', + selections: [], + note: null, + framework: 'Django', + }) + + expect(goal).toContain("Django's conventions") +}) + +test('composeGoal: appends the developer note', () => { + const goal = composeGoal({ + mode: 'full_api', + selections: ['access_grants'], + note: ' Use the existing service layer. ', + framework: null, + }) + + expect(goal).toContain( + 'Additional context from the developer: Use the existing service layer.', + ) +}) + +test('composeGoal: omits the note when it is null', () => { + const goal = composeGoal({ + mode: 'full_api', + selections: ['access_grants'], + note: null, + framework: null, + }) + + expect(goal).not.toContain('Additional context from the developer') +}) + +test('composeGoal: omits the note when it is only whitespace', () => { + const goal = composeGoal({ + mode: 'customer_portal', + selections: [], + note: ' \n ', + framework: null, + }) + + expect(goal).not.toContain('Additional context from the developer') +}) + +const exampleRecord: Omit = { + created_at: '2026-07-29T00:00:00.000Z', + mode: 'full_api', + selections: ['access_grants'], + note: null, + goal: 'Set up a Seam integration.', + analysis: { + sdk: 'javascript', + framework: 'Next.js', + app_type_guess: 'property management', + seam_already_setup: false, + used_onboarding: true, + recommendation_source: 'llm', + }, +} + +test('writeOnboardingRecord: writes the record to .seam/onboarding.json', () => { + writeOnboardingRecord(dir, exampleRecord) + + const contents = readFileSync(join(dir, '.seam', 'onboarding.json'), 'utf8') + expect(JSON.parse(contents)).toEqual({ + schema_version: 1, + ...exampleRecord, + }) +}) + +test('writeOnboardingRecord: ends the file with a trailing newline', () => { + writeOnboardingRecord(dir, exampleRecord) + + const contents = readFileSync(join(dir, '.seam', 'onboarding.json'), 'utf8') + expect(contents.endsWith('}\n')).toBe(true) +}) + +test('writeOnboardingRecord: keeps an optional result in the record', () => { + writeOnboardingRecord(dir, { + ...exampleRecord, + result: { ok: true, files_summary: 'Added src/seam.ts', cost_usd: 0.42 }, + }) + + const contents = readFileSync(join(dir, '.seam', 'onboarding.json'), 'utf8') + expect(JSON.parse(contents)).toMatchObject({ + schema_version: 1, + result: { ok: true, files_summary: 'Added src/seam.ts', cost_usd: 0.42 }, + }) +}) diff --git a/src/lib/steps/detect-project.test.ts b/src/lib/steps/detect-project.test.ts new file mode 100644 index 0000000..ba699d7 --- /dev/null +++ b/src/lib/steps/detect-project.test.ts @@ -0,0 +1,186 @@ +import { mkdtempSync, rmSync, writeFileSync } from 'node:fs' +import { tmpdir } from 'node:os' +import { join } from 'node:path' + +import { afterEach, beforeEach, expect, test } from 'vitest' + +import { + detectProject, + installSeamSdkCommand, + type JsPackageManager, + type ProjectInfo, + type PythonInstaller, +} from './detect-project.js' + +let dir = '' + +beforeEach(() => { + dir = mkdtempSync(join(tmpdir(), 'seam-wizard-')) +}) + +afterEach(() => { + rmSync(dir, { recursive: true, force: true }) +}) + +const touch = (fileName: string): void => { + writeFileSync(join(dir, fileName), '') +} + +const jsProject = (packageManager: JsPackageManager): ProjectInfo => ({ + root: '/example', + detected_sdk: 'javascript', + js_package_manager: packageManager, + python_installer: 'pip', +}) + +const pythonProject = (installer: PythonInstaller): ProjectInfo => ({ + root: '/example', + detected_sdk: 'python', + js_package_manager: 'npm', + python_installer: installer, +}) + +test('detectProject: reports the given directory as the root', () => { + expect(detectProject(dir).root).toBe(dir) +}) + +test('detectProject: detects javascript from package.json', () => { + touch('package.json') + + expect(detectProject(dir).detected_sdk).toBe('javascript') +}) + +test.each(['pyproject.toml', 'requirements.txt', 'setup.py', 'Pipfile'])( + 'detectProject: detects python from %s', + (marker) => { + touch(marker) + + expect(detectProject(dir).detected_sdk).toBe('python') + }, +) + +test('detectProject: detects no sdk when javascript and python markers are both present', () => { + touch('package.json') + touch('requirements.txt') + + expect(detectProject(dir).detected_sdk).toBeNull() +}) + +test('detectProject: detects no sdk when neither marker is present', () => { + expect(detectProject(dir).detected_sdk).toBeNull() +}) + +test.each([ + ['pnpm-lock.yaml', 'pnpm'], + ['yarn.lock', 'yarn'], + ['bun.lockb', 'bun'], +])( + 'detectProject: detects the %s package manager as %s', + (lockfile, expected) => { + touch(lockfile) + + expect(detectProject(dir).js_package_manager).toBe(expected) + }, +) + +test('detectProject: defaults to the npm package manager', () => { + touch('package.json') + + expect(detectProject(dir).js_package_manager).toBe('npm') +}) + +test.each([ + ['poetry.lock', 'poetry'], + ['uv.lock', 'uv'], +])( + 'detectProject: detects the %s python installer as %s', + (lockfile, expected) => { + touch(lockfile) + + expect(detectProject(dir).python_installer).toBe(expected) + }, +) + +test('detectProject: defaults to the pip python installer', () => { + touch('pyproject.toml') + + expect(detectProject(dir).python_installer).toBe('pip') +}) + +test('detectProject: prefers pnpm over yarn when both lockfiles exist', () => { + touch('pnpm-lock.yaml') + touch('yarn.lock') + + expect(detectProject(dir).js_package_manager).toBe('pnpm') +}) + +test('installSeamSdkCommand: installs the javascript sdk with npm', () => { + expect(installSeamSdkCommand('javascript', jsProject('npm'))).toEqual([ + 'npm', + 'install', + 'seam', + ]) +}) + +test('installSeamSdkCommand: installs the javascript sdk with pnpm', () => { + expect(installSeamSdkCommand('javascript', jsProject('pnpm'))).toEqual([ + 'pnpm', + 'add', + 'seam', + ]) +}) + +test('installSeamSdkCommand: installs the javascript sdk with yarn', () => { + expect(installSeamSdkCommand('javascript', jsProject('yarn'))).toEqual([ + 'yarn', + 'add', + 'seam', + ]) +}) + +test('installSeamSdkCommand: installs the javascript sdk with bun', () => { + expect(installSeamSdkCommand('javascript', jsProject('bun'))).toEqual([ + 'bun', + 'add', + 'seam', + ]) +}) + +test('installSeamSdkCommand: installs the python sdk with pip', () => { + expect(installSeamSdkCommand('python', pythonProject('pip'))).toEqual([ + 'pip', + 'install', + 'seam', + ]) +}) + +test('installSeamSdkCommand: installs the python sdk with poetry', () => { + expect(installSeamSdkCommand('python', pythonProject('poetry'))).toEqual([ + 'poetry', + 'add', + 'seam', + ]) +}) + +test('installSeamSdkCommand: installs the python sdk with uv', () => { + expect(installSeamSdkCommand('python', pythonProject('uv'))).toEqual([ + 'uv', + 'add', + 'seam', + ]) +}) + +test('installSeamSdkCommand: ignores the python installer for the javascript sdk', () => { + const project: ProjectInfo = { + root: '/example', + detected_sdk: 'javascript', + js_package_manager: 'yarn', + python_installer: 'poetry', + } + + expect(installSeamSdkCommand('javascript', project)).toEqual([ + 'yarn', + 'add', + 'seam', + ]) +}) diff --git a/src/lib/util/env-file.test.ts b/src/lib/util/env-file.test.ts new file mode 100644 index 0000000..6f35e05 --- /dev/null +++ b/src/lib/util/env-file.test.ts @@ -0,0 +1,162 @@ +import { mkdtempSync, readFileSync, rmSync, writeFileSync } from 'node:fs' +import { tmpdir } from 'node:os' +import { join } from 'node:path' + +import { afterEach, beforeEach, expect, test, vi } from 'vitest' + +import { findExistingApiKey, upsertEnvVar } from './env-file.js' + +let dir = '' + +beforeEach(() => { + dir = mkdtempSync(join(tmpdir(), 'seam-wizard-')) + // The module reads process.env['SEAM_API_KEY']; start every test without it. + vi.stubEnv('SEAM_API_KEY', undefined) +}) + +afterEach(() => { + rmSync(dir, { recursive: true, force: true }) + vi.unstubAllEnvs() +}) + +const writeEnvFile = (fileName: string, contents: string): void => { + writeFileSync(join(dir, fileName), contents) +} + +test('findExistingApiKey: prefers the process environment', () => { + vi.stubEnv('SEAM_API_KEY', 'seam_from_environment') + writeEnvFile('.env.local', 'SEAM_API_KEY=seam_from_file\n') + + expect(findExistingApiKey(dir)).toEqual({ + api_key: 'seam_from_environment', + source: 'environment', + }) +}) + +test('findExistingApiKey: trims the process environment value', () => { + vi.stubEnv('SEAM_API_KEY', ' seam_padded ') + + expect(findExistingApiKey(dir)).toEqual({ + api_key: 'seam_padded', + source: 'environment', + }) +}) + +test('findExistingApiKey: reads .env.local before .env', () => { + writeEnvFile('.env.local', 'SEAM_API_KEY=seam_local\n') + writeEnvFile('.env', 'SEAM_API_KEY=seam_plain\n') + + expect(findExistingApiKey(dir)).toEqual({ + api_key: 'seam_local', + source: '.env.local', + }) +}) + +test('findExistingApiKey: falls back to .env when .env.local is missing', () => { + writeEnvFile('.env', 'OTHER=1\nSEAM_API_KEY=seam_plain\n') + + expect(findExistingApiKey(dir)).toEqual({ + api_key: 'seam_plain', + source: '.env', + }) +}) + +test('findExistingApiKey: falls back to the development dotenv files', () => { + writeEnvFile('.env.development', 'SEAM_API_KEY=seam_dev\n') + + expect(findExistingApiKey(dir)).toEqual({ + api_key: 'seam_dev', + source: '.env.development', + }) +}) + +test('findExistingApiKey: strips surrounding double quotes', () => { + writeEnvFile('.env', 'SEAM_API_KEY="seam_quoted"\n') + + expect(findExistingApiKey(dir)?.api_key).toBe('seam_quoted') +}) + +test('findExistingApiKey: strips surrounding single quotes', () => { + writeEnvFile('.env', "SEAM_API_KEY = 'seam_quoted'\n") + + expect(findExistingApiKey(dir)?.api_key).toBe('seam_quoted') +}) + +test('findExistingApiKey: returns null when there is no key anywhere', () => { + writeEnvFile('.env', 'OTHER=1\n') + + expect(findExistingApiKey(dir)).toBeNull() +}) + +test('findExistingApiKey: returns null when the project has no dotenv files', () => { + expect(findExistingApiKey(dir)).toBeNull() +}) + +test('findExistingApiKey: treats a whitespace-only environment value as absent', () => { + vi.stubEnv('SEAM_API_KEY', ' ') + writeEnvFile('.env', 'SEAM_API_KEY=seam_plain\n') + + expect(findExistingApiKey(dir)).toEqual({ + api_key: 'seam_plain', + source: '.env', + }) +}) + +test('findExistingApiKey: treats an empty environment value as absent', () => { + vi.stubEnv('SEAM_API_KEY', '') + + expect(findExistingApiKey(dir)).toBeNull() +}) + +test('upsertEnvVar: creates a missing file', () => { + const filePath = join(dir, '.env') + + expect(upsertEnvVar(filePath, 'SEAM_API_KEY', 'seam_new')).toBe('created') + expect(readFileSync(filePath, 'utf8')).toBe('SEAM_API_KEY=seam_new\n') +}) + +test('upsertEnvVar: adds the key to an existing file', () => { + const filePath = join(dir, '.env') + writeFileSync(filePath, 'OTHER=1\n') + + expect(upsertEnvVar(filePath, 'SEAM_API_KEY', 'seam_new')).toBe('added') + expect(readFileSync(filePath, 'utf8')).toBe( + 'OTHER=1\nSEAM_API_KEY=seam_new\n', + ) +}) + +test('upsertEnvVar: appends a newline separator when the file does not end in one', () => { + const filePath = join(dir, '.env') + writeFileSync(filePath, 'OTHER=1') + + expect(upsertEnvVar(filePath, 'SEAM_API_KEY', 'seam_new')).toBe('added') + expect(readFileSync(filePath, 'utf8')).toBe( + 'OTHER=1\nSEAM_API_KEY=seam_new\n', + ) +}) + +test('upsertEnvVar: updates an existing value', () => { + const filePath = join(dir, '.env') + writeFileSync(filePath, 'SEAM_API_KEY=seam_old\n') + + expect(upsertEnvVar(filePath, 'SEAM_API_KEY', 'seam_new')).toBe('updated') + expect(readFileSync(filePath, 'utf8')).toBe('SEAM_API_KEY=seam_new\n') +}) + +test('upsertEnvVar: leaves other entries intact when updating', () => { + const filePath = join(dir, '.env') + writeFileSync(filePath, 'BEFORE=1\nSEAM_API_KEY=seam_old\nAFTER=2\n') + + expect(upsertEnvVar(filePath, 'SEAM_API_KEY', 'seam_new')).toBe('updated') + expect(readFileSync(filePath, 'utf8')).toBe( + 'BEFORE=1\nSEAM_API_KEY=seam_new\nAFTER=2\n', + ) +}) + +test('upsertEnvVar: writes into an existing empty file without a leading newline', () => { + const filePath = join(dir, '.env') + writeFileSync(filePath, '') + + expect(upsertEnvVar(filePath, 'SEAM_API_KEY', 'seam_new')).toBe('added') + expect(readFileSync(filePath, 'utf8')).toBe('SEAM_API_KEY=seam_new\n') +}) From c2f59954e396092fb4ed5814359c2fd9d5230980 Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 29 Jul 2026 02:05:56 +0000 Subject: [PATCH 09/11] docs: document what the wizard does and how to run it MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Replace the placeholder description with what the wizard actually does, now that it is implemented: the steps it runs, that keys are never minted here, and that inference is routed through Seam so no developer Anthropic key is needed. Document the `cwd` option and what the entrypoint's promise means, the two environment variables that affect a run, and the src/lib layout including the lib/* alias and why the wire-format types keep snake_case properties. Note that `npm run wizard` sets up whichever project it runs in — that is this repository, which is rarely what you want — and show how to run it against a scratch project instead. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01W1qhuuZQZnA1FB1fBSxrqx --- README.md | 110 +++++++++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 109 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index bc496a5..0561afd 100644 --- a/README.md +++ b/README.md @@ -7,13 +7,47 @@ The AI powered Seam setup wizard. ## Description -TODO +An interactive terminal wizard that takes a project from zero to a working +Seam integration. Run inside a project, it will: + +1. **Detect the project** — JavaScript/TypeScript or Python, + and the package manager or installer in use. +2. **Connect a Seam account** — an existing `SEAM_API_KEY` + in the environment or a `.env*` file is verified and reused. + Otherwise the developer can continue in the browser, where the + Console mints a fresh key and hands it back over a localhost + callback, or paste a key by hand. + Either way the key is verified and saved to `.env`. +3. **Install the Seam SDK** — `seam` via npm, pnpm, yarn, or bun + for JavaScript, or pip, poetry, or uv for Python. +4. **Install the Seam plugin** — + [`seamapi/seam-plugin`](https://github.com/seamapi/seam-plugin), + which provides the Seam integration skills and the `seam-docs` MCP + for the developer's AI coding assistant. + Claude Code is detected via `.claude` or `CLAUDE.md`, + in which case the wizard prints the slash commands to run, + since an external program cannot drive them. +5. **Write the integration** — optionally. The wizard analyzes the + project, recommends an approach, confirms it with the developer, + records the plan in `.seam/onboarding.json`, and then runs an + embedded agent that writes the integration for review as a diff. + +The wizard never asks for a password: keys are created in the browser +or pasted, never minted here. Inference for the analysis and the +embedded agent is routed through Seam, so no developer Anthropic API +key is needed. + +The wizard is built with [Ink] and takes over the terminal for the +duration of a run, restoring it and reprinting a transcript on the way +out. This package is not a standalone command line program: it deliberately publishes no `bin`. The wizard is distributed as a library and mounted by the [Seam CLI](https://github.com/seamapi/cli) under `seam wizard`. +[Ink]: https://github.com/vadimdemedes/ink + ## Installation Add this as a dependency to your project using [npm] with @@ -43,6 +77,37 @@ The `commandName` option is only used in help output so that the wizard describes itself using the command that was actually run. +The wizard handles `--help` itself and otherwise takes over the terminal +until the developer finishes, then restores it and prints a transcript of +the run. It resolves when the wizard is done, and only rejects if the +wizard could not run at all: a step that fails reports itself to the +developer and does not reject. + +Pass `cwd` to set the project root the wizard sets up. +It defaults to `process.cwd()`, +which is the project the command was run in. +The wizard reads and writes files there, +e.g., `.env` and `.seam/onboarding.json`. + +```ts +await wizard({ + argv: process.argv.slice(3), + commandName: 'seam wizard', + cwd: '/path/to/project', +}) +``` + +### Environment variables + +- `SEAM_API_KEY`: An existing Seam API key. + When set, the wizard verifies and reuses it instead of asking + the developer to connect an account. + The wizard also looks for this in the project's `.env*` files, + and writes the key it obtains back to `.env`. +- `SEAM_CONSOLE_URL`: Points the browser connection flow at a + non-production Console. + Defaults to `https://console.seam.co`. + ## Development and Testing ### Quickstart @@ -66,6 +131,49 @@ which simply calls the wizard with the arguments given. That file exists for local development only: it is excluded from the build and from the published package. +Because the wizard sets up whichever project it is run in, +`npm run wizard` sets up this repository itself, +which is rarely what you want. +Run it against a scratch project instead with + +``` +$ mkdir -p /tmp/scratch && cd /tmp/scratch +$ npm --prefix /path/to/wizard run wizard +``` + +or mount it with an explicit `cwd` through the example with + +``` +$ npm run example -- wizard --cwd /tmp/scratch +``` + +Pass arguments through to the wizard with + +``` +$ npm run wizard -- --help +``` + +### Source layout + +The wizard is an [Ink] application under `src/lib`: + +- `wizard.ts` is the entrypoint the package exports. + It parses arguments, handles `--help`, and hands off to the renderer. +- `render.tsx` runs the app full-screen + and reprints its transcript on exit. +- `app.tsx` is the state machine driving the run, + one phase per step, and holds all of the rendering. +- `steps/` holds the logic for each step, with no UI in it. +- `util/` holds the Seam API client, dotenv handling, + and the subprocess runner. + +Modules under `src/lib` are imported through the `lib/*` path alias +rather than parent-relative specifiers. + +Types that mirror the Seam API wire format +or the on-disk `.seam/onboarding.json` record keep their `snake_case` +property names, which the lint configuration allows. + Primary development tasks are defined under `scripts` in `package.json` and available via `npm run`. View them with From 689c9d9e7c020bbd2998cb9e6024b54870f949a4 Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 29 Jul 2026 02:12:34 +0000 Subject: [PATCH 10/11] refactor: drop the examples directory and landlubber The scaffold's landlubber examples harness does not fit this package. The wizard is a single interactive entrypoint that takes over the terminal, not a set of commands worth demonstrating one by one, and the README already shows the two lines a consumer needs to mount it. Remove the examples directory, the landlubber dependency, the example scripts, and the examples entries in the tsconfig include and the coverage exclude list. The examples were the only way to run the wizard against a directory other than this repository, so move that onto the development CLI as a '--cwd ' flag. Running the CLI from another directory is not an option: tsx resolves the tsconfig from the working directory, so the lib/* alias fails to resolve. The flag is consumed by the CLI and becomes the wizard's `cwd` option; every other argument is still forwarded untouched. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01W1qhuuZQZnA1FB1fBSxrqx --- README.md | 15 +- examples/index.ts | 9 - examples/wizard.ts | 23 -- package-lock.json | 683 +-------------------------------------------- package.json | 3 - src/bin/cli.ts | 11 +- tsconfig.json | 8 +- vitest.config.ts | 1 - 8 files changed, 22 insertions(+), 731 deletions(-) delete mode 100755 examples/index.ts delete mode 100644 examples/wizard.ts diff --git a/README.md b/README.md index 0561afd..5bea07d 100644 --- a/README.md +++ b/README.md @@ -137,22 +137,19 @@ which is rarely what you want. Run it against a scratch project instead with ``` -$ mkdir -p /tmp/scratch && cd /tmp/scratch -$ npm --prefix /path/to/wizard run wizard +$ npm run wizard -- --cwd /tmp/scratch ``` -or mount it with an explicit `cwd` through the example with - -``` -$ npm run example -- wizard --cwd /tmp/scratch -``` - -Pass arguments through to the wizard with +`--cwd` is a development convenience of that CLI, not an argument of the +wizard: it becomes the `cwd` option. +Every other argument is forwarded to the wizard untouched, so ``` $ npm run wizard -- --help ``` +reaches the wizard as `--help`. + ### Source layout The wizard is an [Ink] application under `src/lib`: diff --git a/examples/index.ts b/examples/index.ts deleted file mode 100755 index ebb1f98..0000000 --- a/examples/index.ts +++ /dev/null @@ -1,9 +0,0 @@ -#!/usr/bin/env tsx - -import landlubber from 'landlubber' - -import * as wizard from './wizard.js' - -const commands = [wizard] - -await landlubber(commands).parse() diff --git a/examples/wizard.ts b/examples/wizard.ts deleted file mode 100644 index 92bb85f..0000000 --- a/examples/wizard.ts +++ /dev/null @@ -1,23 +0,0 @@ -import type { Builder, Command, Describe, Handler } from 'landlubber' - -import wizard from '@seamapi/wizard' - -interface Options { - cwd: string -} - -export const command: Command = 'wizard' - -export const describe: Describe = 'Mount and run the Seam setup wizard' - -export const builder: Builder = { - cwd: { - type: 'string', - default: '.', - describe: 'The project root the wizard should set up', - }, -} - -export const handler: Handler = async ({ cwd }) => { - await wizard({ argv: [], commandName: 'example wizard', cwd }) -} diff --git a/package-lock.json b/package-lock.json index bcd91a0..d955af7 100644 --- a/package-lock.json +++ b/package-lock.json @@ -30,7 +30,6 @@ "eslint-plugin-unused-imports": "^4.1.4", "ink-testing-library": "^4.0.0", "jiti": "^2.4.2", - "landlubber": "^2.0.0", "neostandard": "^0.13.0", "prettier": "^3.0.0", "tsc-alias": "^1.8.2", @@ -1626,23 +1625,6 @@ "csstype": "^3.2.2" } }, - "node_modules/@types/yargs": { - "version": "17.0.35", - "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-17.0.35.tgz", - "integrity": "sha512-qUHkeCyQFxMXg79wQfTtfndEC+N9ZZg76HJftDJp+qH2tV7Gj4OJi7l+PiWwJ+pWtW8GwSmqsDj/oymhrTWXjg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/yargs-parser": "*" - } - }, - "node_modules/@types/yargs-parser": { - "version": "21.0.3", - "resolved": "https://registry.npmjs.org/@types/yargs-parser/-/yargs-parser-21.0.3.tgz", - "integrity": "sha512-I4q9QU9MQv4oEOz4tAHJtNz1cwuLxn2F3xcc2iV5WdqLPpUnj30aUuxt1mAxYTG+oe8CZMV/+6rU4S4gRDzqtQ==", - "dev": true, - "license": "MIT" - }, "node_modules/@typescript-eslint/eslint-plugin": { "version": "8.65.0", "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.65.0.tgz", @@ -2082,19 +2064,6 @@ "url": "https://opencollective.com/vitest" } }, - "node_modules/abort-controller": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/abort-controller/-/abort-controller-3.0.0.tgz", - "integrity": "sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg==", - "dev": true, - "license": "MIT", - "dependencies": { - "event-target-shim": "^5.0.0" - }, - "engines": { - "node": ">=6.5" - } - }, "node_modules/accepts": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/accepts/-/accepts-2.0.0.tgz", @@ -2206,16 +2175,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/ansi-regex": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", - "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, "node_modules/ansi-styles": { "version": "4.3.0", "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", @@ -2455,16 +2414,6 @@ "node": ">= 0.4" } }, - "node_modules/atomic-sleep": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/atomic-sleep/-/atomic-sleep-1.0.0.tgz", - "integrity": "sha512-kNOjDqAh7px0XWNI+4QbzoiR/nTkHAWNud2uvnJquD1/x5a7EQZMJT0AczqK0Qn67oY/TTQ1LbUKajZpp3I9tQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8.0.0" - } - }, "node_modules/auto-bind": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/auto-bind/-/auto-bind-5.0.1.tgz", @@ -2500,27 +2449,6 @@ "dev": true, "license": "MIT" }, - "node_modules/base64-js": { - "version": "1.5.1", - "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", - "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==", - "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ], - "license": "MIT" - }, "node_modules/binary-extensions": { "version": "2.3.0", "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz", @@ -2597,31 +2525,6 @@ "node": ">=8" } }, - "node_modules/buffer": { - "version": "6.0.3", - "resolved": "https://registry.npmjs.org/buffer/-/buffer-6.0.3.tgz", - "integrity": "sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==", - "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ], - "license": "MIT", - "dependencies": { - "base64-js": "^1.3.1", - "ieee754": "^1.2.1" - } - }, "node_modules/bundle-name": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/bundle-name/-/bundle-name-4.1.0.tgz", @@ -2915,21 +2818,6 @@ "url": "https://github.com/chalk/strip-ansi?sponsor=1" } }, - "node_modules/cliui": { - "version": "8.0.1", - "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz", - "integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==", - "dev": true, - "license": "ISC", - "dependencies": { - "string-width": "^4.2.0", - "strip-ansi": "^6.0.1", - "wrap-ansi": "^7.0.0" - }, - "engines": { - "node": ">=12" - } - }, "node_modules/code-excerpt": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/code-excerpt/-/code-excerpt-4.0.0.tgz", @@ -2962,13 +2850,6 @@ "dev": true, "license": "MIT" }, - "node_modules/colorette": { - "version": "2.0.20", - "resolved": "https://registry.npmjs.org/colorette/-/colorette-2.0.20.tgz", - "integrity": "sha512-IfEDxwoWIjkeXL1eXcDiow4UbKjhLdq6/EuSVR9GMN7KVH3r9gQ83e73hsz1Nd1T3ijd5xv1wcWRYO+D6kCI2w==", - "dev": true, - "license": "MIT" - }, "node_modules/commander": { "version": "9.5.0", "resolved": "https://registry.npmjs.org/commander/-/commander-9.5.0.tgz", @@ -3139,16 +3020,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/dateformat": { - "version": "4.6.3", - "resolved": "https://registry.npmjs.org/dateformat/-/dateformat-4.6.3.tgz", - "integrity": "sha512-2P0p0pFGzHS5EMnhdxQi7aJN+iMheud0UhG4dlE1DLAlvL8JHjJJTX/CSm4JXwV0Ka5nGk3zC5mcb5bUQUxxMA==", - "dev": true, - "license": "MIT", - "engines": { - "node": "*" - } - }, "node_modules/debug": { "version": "4.4.3", "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", @@ -3370,13 +3241,6 @@ "license": "MIT", "peer": true }, - "node_modules/emoji-regex": { - "version": "8.0.0", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", - "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", - "dev": true, - "license": "MIT" - }, "node_modules/encodeurl": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz", @@ -3387,16 +3251,6 @@ "node": ">= 0.8" } }, - "node_modules/end-of-stream": { - "version": "1.4.5", - "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.5.tgz", - "integrity": "sha512-ooEGc6HP26xXq/N+GCGOT0JKCLDGrq2bQUZrQ7gyrJiZANJ/8YDTxTpQBXGMn+WbIQXNVpyWymm7KYVICQnyOg==", - "dev": true, - "license": "MIT", - "dependencies": { - "once": "^1.4.0" - } - }, "node_modules/enhanced-resolve": { "version": "5.24.4", "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.24.4.tgz", @@ -3679,16 +3533,6 @@ "@esbuild/win32-x64": "0.28.1" } }, - "node_modules/escalade": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", - "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6" - } - }, "node_modules/escape-html": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", @@ -4159,26 +4003,6 @@ "node": ">= 0.6" } }, - "node_modules/event-target-shim": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/event-target-shim/-/event-target-shim-5.0.1.tgz", - "integrity": "sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6" - } - }, - "node_modules/events": { - "version": "3.3.0", - "resolved": "https://registry.npmjs.org/events/-/events-3.3.0.tgz", - "integrity": "sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.8.x" - } - }, "node_modules/eventsource": { "version": "3.0.7", "resolved": "https://registry.npmjs.org/eventsource/-/eventsource-3.0.7.tgz", @@ -4276,13 +4100,6 @@ "express": ">= 4.11" } }, - "node_modules/fast-copy": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/fast-copy/-/fast-copy-3.0.2.tgz", - "integrity": "sha512-dl0O9Vhju8IrcLndv2eU4ldt1ftXMqqfgN4H1cpmGV7P6jeB9FwpN9a2c8DPGE1Ys88rNUJVYDHq73CGAGOPfQ==", - "dev": true, - "license": "MIT" - }, "node_modules/fast-deep-equal": { "version": "3.1.3", "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", @@ -4333,23 +4150,6 @@ "dev": true, "license": "MIT" }, - "node_modules/fast-redact": { - "version": "3.5.0", - "resolved": "https://registry.npmjs.org/fast-redact/-/fast-redact-3.5.0.tgz", - "integrity": "sha512-dwsoQlS7h9hMeYUq1W++23NDcBLV4KqONnITDV9DjfS3q1SgDGVrBdvvTLUotWtPSD7asWDV9/CmsZPy8Hf70A==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6" - } - }, - "node_modules/fast-safe-stringify": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/fast-safe-stringify/-/fast-safe-stringify-2.1.1.tgz", - "integrity": "sha512-W+KJc2dmILlPplD/H4K9l9LcAHAfPtP6BY84uVLXQ6Evcz9Lcg33Y2z1IVblT6xdY54PXYVHEv+0Wpq8Io6zkA==", - "dev": true, - "license": "MIT" - }, "node_modules/fast-sha256": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/fast-sha256/-/fast-sha256-1.3.0.tgz", @@ -4521,13 +4321,6 @@ "node": ">= 0.8" } }, - "node_modules/fs.realpath": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", - "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==", - "dev": true, - "license": "ISC" - }, "node_modules/fsevents": { "version": "2.3.3", "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", @@ -4596,16 +4389,6 @@ "node": ">= 0.4" } }, - "node_modules/get-caller-file": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", - "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", - "dev": true, - "license": "ISC", - "engines": { - "node": "6.* || 8.* || >= 10.*" - } - }, "node_modules/get-east-asian-width": { "version": "1.6.0", "resolved": "https://registry.npmjs.org/get-east-asian-width/-/get-east-asian-width-1.6.0.tgz", @@ -4686,27 +4469,6 @@ "url": "https://github.com/privatenumber/get-tsconfig?sponsor=1" } }, - "node_modules/glob": { - "version": "8.1.0", - "resolved": "https://registry.npmjs.org/glob/-/glob-8.1.0.tgz", - "integrity": "sha512-r8hpEjiQEYlF2QU0df3dS+nxxSIreXQS1qRhMJM0Q5NDdR386C7jb7Hwwod8Fgiuex+k0GFjgft18yvxm5XoCQ==", - "deprecated": "Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me", - "dev": true, - "license": "ISC", - "dependencies": { - "fs.realpath": "^1.0.0", - "inflight": "^1.0.4", - "inherits": "2", - "minimatch": "^5.0.1", - "once": "^1.3.0" - }, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, "node_modules/glob-parent": { "version": "6.0.2", "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", @@ -4720,29 +4482,6 @@ "node": ">=10.13.0" } }, - "node_modules/glob/node_modules/brace-expansion": { - "version": "2.1.3", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.1.3.tgz", - "integrity": "sha512-DRdx5neNsG/QXbniLFWi2YmC/68oeOOmKz6zOjVk6ZS1ZLXgLIKqVEc6hWsmkjBbgii0SwaBTcJ5XKj5gzY/4A==", - "dev": true, - "license": "MIT", - "dependencies": { - "balanced-match": "^1.0.0" - } - }, - "node_modules/glob/node_modules/minimatch": { - "version": "5.1.9", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.9.tgz", - "integrity": "sha512-7o1wEA2RyMP7Iu7GNba9vc0RWWGACJOCZBJX2GJWip0ikV+wcOsgVuY9uE8CPiyQhkGFSlhuSkZPavN7u1c2Fw==", - "dev": true, - "license": "ISC", - "dependencies": { - "brace-expansion": "^2.0.1" - }, - "engines": { - "node": ">=10" - } - }, "node_modules/globals": { "version": "14.0.0", "resolved": "https://registry.npmjs.org/globals/-/globals-14.0.0.tgz", @@ -4922,32 +4661,6 @@ "node": ">= 0.4" } }, - "node_modules/help-me": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/help-me/-/help-me-4.2.0.tgz", - "integrity": "sha512-TAOnTB8Tz5Dw8penUuzHVrKNKlCIbwwbHnXraNJxPwf8LRtE2HlM84RYuezMFcwOJmoYOCWVDyJ8TQGxn9PgxA==", - "dev": true, - "license": "MIT", - "dependencies": { - "glob": "^8.0.0", - "readable-stream": "^3.6.0" - } - }, - "node_modules/help-me/node_modules/readable-stream": { - "version": "3.6.2", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", - "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", - "dev": true, - "license": "MIT", - "dependencies": { - "inherits": "^2.0.3", - "string_decoder": "^1.1.1", - "util-deprecate": "^1.0.1" - }, - "engines": { - "node": ">= 6" - } - }, "node_modules/hono": { "version": "4.12.32", "resolved": "https://registry.npmjs.org/hono/-/hono-4.12.32.tgz", @@ -5003,27 +4716,6 @@ "url": "https://opencollective.com/express" } }, - "node_modules/ieee754": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", - "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==", - "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ], - "license": "BSD-3-Clause" - }, "node_modules/ignore": { "version": "5.3.2", "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", @@ -5073,23 +4765,12 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/inflight": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", - "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==", - "deprecated": "This module is not supported, and leaks memory. Do not use it. Check out lru-cache if you want a good and tested way to coalesce async requests by a key value, which is much more comprehensive and powerful.", - "dev": true, - "license": "ISC", - "dependencies": { - "once": "^1.3.0", - "wrappy": "1" - } - }, "node_modules/inherits": { "version": "2.0.4", "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", - "license": "ISC" + "license": "ISC", + "peer": true }, "node_modules/ink": { "version": "5.2.1", @@ -5550,16 +5231,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/is-fullwidth-code-point": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", - "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, "node_modules/is-generator-function": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/is-generator-function/-/is-generator-function-1.1.2.tgz", @@ -5974,16 +5645,6 @@ "url": "https://github.com/sponsors/panva" } }, - "node_modules/joycon": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/joycon/-/joycon-3.1.1.tgz", - "integrity": "sha512-34wB/Y7MW7bzjKRjUKTa46I2Z7eV62Rkhva+KkopW7Qvv/OSWBqvkSY7vusOPrNuZcUG3tApvdVgNB8POj3SPw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=10" - } - }, "node_modules/js-tokens": { "version": "10.0.0", "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-10.0.0.tgz", @@ -6095,23 +5756,6 @@ "json-buffer": "3.0.1" } }, - "node_modules/landlubber": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/landlubber/-/landlubber-2.0.0.tgz", - "integrity": "sha512-BhqvT4eCGY78NothDI7H/rW5ZrEurXS4nK8iCetoSyvjBfzuD8DikJhpAig9nwRBKOqv7mnD1qbxJIap/XMv+A==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/yargs": "^17.0.20", - "pino": "^8.8.0", - "pino-pretty": "^9.1.1", - "yargs": "^17.6.2" - }, - "engines": { - "node": ">=18.12.0", - "npm": ">= 9.0.0" - } - }, "node_modules/levn": { "version": "0.4.1", "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", @@ -6948,16 +6592,6 @@ "node": ">=12.20.0" } }, - "node_modules/on-exit-leak-free": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/on-exit-leak-free/-/on-exit-leak-free-2.1.2.tgz", - "integrity": "sha512-0eJJY6hXLGf1udHwfNftBqH+g73EU4B504nZeKpz1sYRKafAghwxEJunB2O7rDZkL4PGfsMVnTXZ2EjibbqcsA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=14.0.0" - } - }, "node_modules/on-finished": { "version": "2.4.1", "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz", @@ -6976,6 +6610,7 @@ "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", "license": "ISC", + "peer": true, "dependencies": { "wrappy": "1" } @@ -7215,73 +6850,6 @@ "url": "https://github.com/sponsors/jonschlinkert" } }, - "node_modules/pino": { - "version": "8.21.0", - "resolved": "https://registry.npmjs.org/pino/-/pino-8.21.0.tgz", - "integrity": "sha512-ip4qdzjkAyDDZklUaZkcRFb2iA118H9SgRh8yzTkSQK8HilsOJF7rSY8HoW5+I0M46AZgX/pxbprf2vvzQCE0Q==", - "dev": true, - "license": "MIT", - "dependencies": { - "atomic-sleep": "^1.0.0", - "fast-redact": "^3.1.1", - "on-exit-leak-free": "^2.1.0", - "pino-abstract-transport": "^1.2.0", - "pino-std-serializers": "^6.0.0", - "process-warning": "^3.0.0", - "quick-format-unescaped": "^4.0.3", - "real-require": "^0.2.0", - "safe-stable-stringify": "^2.3.1", - "sonic-boom": "^3.7.0", - "thread-stream": "^2.6.0" - }, - "bin": { - "pino": "bin.js" - } - }, - "node_modules/pino-abstract-transport": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/pino-abstract-transport/-/pino-abstract-transport-1.2.0.tgz", - "integrity": "sha512-Guhh8EZfPCfH+PMXAb6rKOjGQEoy0xlAIn+irODG5kgfYV+BQ0rGYYWTIel3P5mmyXqkYkPmdIkywsn6QKUR1Q==", - "dev": true, - "license": "MIT", - "dependencies": { - "readable-stream": "^4.0.0", - "split2": "^4.0.0" - } - }, - "node_modules/pino-pretty": { - "version": "9.4.1", - "resolved": "https://registry.npmjs.org/pino-pretty/-/pino-pretty-9.4.1.tgz", - "integrity": "sha512-loWr5SNawVycvY//hamIzyz3Fh5OSpvkcO13MwdDW+eKIGylobPLqnVGTDwDXkdmpJd1BhEG+qhDw09h6SqJiQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "colorette": "^2.0.7", - "dateformat": "^4.6.3", - "fast-copy": "^3.0.0", - "fast-safe-stringify": "^2.1.1", - "help-me": "^4.0.1", - "joycon": "^3.1.1", - "minimist": "^1.2.6", - "on-exit-leak-free": "^2.1.0", - "pino-abstract-transport": "^1.0.0", - "pump": "^3.0.0", - "readable-stream": "^4.0.0", - "secure-json-parse": "^2.4.0", - "sonic-boom": "^3.0.0", - "strip-json-comments": "^3.1.1" - }, - "bin": { - "pino-pretty": "bin.js" - } - }, - "node_modules/pino-std-serializers": { - "version": "6.2.2", - "resolved": "https://registry.npmjs.org/pino-std-serializers/-/pino-std-serializers-6.2.2.tgz", - "integrity": "sha512-cHjPPsE+vhj/tnhCy/wiMh3M3z3h/j15zHQX+S9GkTBgqJuTuJzYJ4gUyACLhDaJ7kk9ba9iRDmbH2tJU03OiA==", - "dev": true, - "license": "MIT" - }, "node_modules/pkce-challenge": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/pkce-challenge/-/pkce-challenge-5.0.1.tgz", @@ -7383,23 +6951,6 @@ "url": "https://github.com/prettier/prettier?sponsor=1" } }, - "node_modules/process": { - "version": "0.11.10", - "resolved": "https://registry.npmjs.org/process/-/process-0.11.10.tgz", - "integrity": "sha512-cdGef/drWFoydD1JsMzuFf8100nZl+GT+yacc2bEced5f9Rjk4z+WtFUTBu9PhOi9j/jfmBPu0mMEY4wIdAF8A==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.6.0" - } - }, - "node_modules/process-warning": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/process-warning/-/process-warning-3.0.0.tgz", - "integrity": "sha512-mqn0kFRl0EoqhnL0GQ0veqFHyIN1yig9RHh/InzORTUiZHFRAur+aMtRkELNwGs9aNwKS6tg/An4NYBPGwvtzQ==", - "dev": true, - "license": "MIT" - }, "node_modules/prop-types": { "version": "15.8.1", "resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz", @@ -7426,17 +6977,6 @@ "node": ">= 0.10" } }, - "node_modules/pump": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.4.tgz", - "integrity": "sha512-VS7sjc6KR7e1ukRFhQSY5LM2uBWAUPiOPa/A3mkKmiMwSmRFUITt0xuj+/lesgnCv+dPIEYlkzrcyXgquIHMcA==", - "dev": true, - "license": "MIT", - "dependencies": { - "end-of-stream": "^1.1.0", - "once": "^1.3.1" - } - }, "node_modules/punycode": { "version": "2.3.1", "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", @@ -7495,13 +7035,6 @@ ], "license": "MIT" }, - "node_modules/quick-format-unescaped": { - "version": "4.0.4", - "resolved": "https://registry.npmjs.org/quick-format-unescaped/-/quick-format-unescaped-4.0.4.tgz", - "integrity": "sha512-tYC1Q1hgyRuHgloV/YXs2w15unPVh8qfu/qCTfhTYamaw7fyhumKa2yGpdSo87vY32rIclj+4fWYQXUMs9EHvg==", - "dev": true, - "license": "MIT" - }, "node_modules/range-parser": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.3.0.tgz", @@ -7567,23 +7100,6 @@ "react": "^18.3.1" } }, - "node_modules/readable-stream": { - "version": "4.7.0", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-4.7.0.tgz", - "integrity": "sha512-oIGGmcpTLwPga8Bn6/Z75SVaH1z5dUut2ibSyAMVhmUggWpmDn2dapB0n7f8nwaSiRtepAsfJyfXIO5DCVAODg==", - "dev": true, - "license": "MIT", - "dependencies": { - "abort-controller": "^3.0.0", - "buffer": "^6.0.3", - "events": "^3.3.0", - "process": "^0.11.10", - "string_decoder": "^1.3.0" - }, - "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" - } - }, "node_modules/readdirp": { "version": "3.6.0", "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", @@ -7597,16 +7113,6 @@ "node": ">=8.10.0" } }, - "node_modules/real-require": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/real-require/-/real-require-0.2.0.tgz", - "integrity": "sha512-57frrGM/OCTLqLOAh0mhVA9VBMHd+9U7Zb2THMGdBUoZVOtGbJzjxsYGDJ3A9AYYCP4hn6y1TVbaOfzWtm5GFg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 12.13.0" - } - }, "node_modules/reflect.getprototypeof": { "version": "1.0.10", "resolved": "https://registry.npmjs.org/reflect.getprototypeof/-/reflect.getprototypeof-1.0.10.tgz", @@ -7651,16 +7157,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/require-directory": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", - "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/require-from-string": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz", @@ -7849,27 +7345,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/safe-buffer": { - "version": "5.2.1", - "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", - "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", - "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ], - "license": "MIT" - }, "node_modules/safe-push-apply": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/safe-push-apply/-/safe-push-apply-1.0.0.tgz", @@ -7905,16 +7380,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/safe-stable-stringify": { - "version": "2.5.0", - "resolved": "https://registry.npmjs.org/safe-stable-stringify/-/safe-stable-stringify-2.5.0.tgz", - "integrity": "sha512-b3rppTKm9T+PsVCBEOUR46GWI7fdOs00VKZ1+9c1EWDaDMvjQc6tUwuFyIprgGgTcWoVHSKrU8H31ZHA2e0RHA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=10" - } - }, "node_modules/safer-buffer": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", @@ -7931,13 +7396,6 @@ "loose-envify": "^1.1.0" } }, - "node_modules/secure-json-parse": { - "version": "2.7.0", - "resolved": "https://registry.npmjs.org/secure-json-parse/-/secure-json-parse-2.7.0.tgz", - "integrity": "sha512-6aU+Rwsezw7VR8/nyvKTx8QpWH9FrcYiXXlqC4z5d5XQBDRqtbfsRjnwGyqbi3gddNtWHuEk9OANUotL26qKUw==", - "dev": true, - "license": "BSD-3-Clause" - }, "node_modules/semver": { "version": "6.3.1", "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", @@ -8213,16 +7671,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/sonic-boom": { - "version": "3.8.1", - "resolved": "https://registry.npmjs.org/sonic-boom/-/sonic-boom-3.8.1.tgz", - "integrity": "sha512-y4Z8LCDBuum+PBP3lSV7RHrXscqksve/bi0as7mhwVnBW+/wUqKT/2Kb7um8yqcFy0duYbbPxzt89Zy2nOCaxg==", - "dev": true, - "license": "MIT", - "dependencies": { - "atomic-sleep": "^1.0.0" - } - }, "node_modules/source-map-js": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", @@ -8233,16 +7681,6 @@ "node": ">=0.10.0" } }, - "node_modules/split2": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/split2/-/split2-4.2.0.tgz", - "integrity": "sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg==", - "dev": true, - "license": "ISC", - "engines": { - "node": ">= 10.x" - } - }, "node_modules/stack-utils": { "version": "2.0.6", "resolved": "https://registry.npmjs.org/stack-utils/-/stack-utils-2.0.6.tgz", @@ -8313,31 +7751,6 @@ "node": ">= 0.4" } }, - "node_modules/string_decoder": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", - "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", - "dev": true, - "license": "MIT", - "dependencies": { - "safe-buffer": "~5.2.0" - } - }, - "node_modules/string-width": { - "version": "4.2.3", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", - "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", - "dev": true, - "license": "MIT", - "dependencies": { - "emoji-regex": "^8.0.0", - "is-fullwidth-code-point": "^3.0.0", - "strip-ansi": "^6.0.1" - }, - "engines": { - "node": ">=8" - } - }, "node_modules/string.prototype.matchall": { "version": "4.0.12", "resolved": "https://registry.npmjs.org/string.prototype.matchall/-/string.prototype.matchall-4.0.12.tgz", @@ -8437,19 +7850,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/strip-ansi": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", - "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-regex": "^5.0.1" - }, - "engines": { - "node": ">=8" - } - }, "node_modules/strip-bom": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-3.0.0.tgz", @@ -8513,16 +7913,6 @@ "url": "https://opencollective.com/webpack" } }, - "node_modules/thread-stream": { - "version": "2.7.0", - "resolved": "https://registry.npmjs.org/thread-stream/-/thread-stream-2.7.0.tgz", - "integrity": "sha512-qQiRWsU/wvNolI6tbbCKd9iKaTnCXsTwVxhhKM6nctPdujTyztjlbUkUTUymidWcMnZ5pWR0ej4a0tjsW021vw==", - "dev": true, - "license": "MIT", - "dependencies": { - "real-require": "^0.2.0" - } - }, "node_modules/tinybench": { "version": "2.9.0", "resolved": "https://registry.npmjs.org/tinybench/-/tinybench-2.9.0.tgz", @@ -9015,13 +8405,6 @@ "punycode": "^2.1.0" } }, - "node_modules/util-deprecate": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", - "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", - "dev": true, - "license": "MIT" - }, "node_modules/vary": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", @@ -9422,29 +8805,12 @@ "node": ">=0.10.0" } }, - "node_modules/wrap-ansi": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", - "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-styles": "^4.0.0", - "string-width": "^4.1.0", - "strip-ansi": "^6.0.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/wrap-ansi?sponsor=1" - } - }, "node_modules/wrappy": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", - "license": "ISC" + "license": "ISC", + "peer": true }, "node_modules/ws": { "version": "8.21.1", @@ -9482,45 +8848,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/y18n": { - "version": "5.0.8", - "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", - "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", - "dev": true, - "license": "ISC", - "engines": { - "node": ">=10" - } - }, - "node_modules/yargs": { - "version": "17.7.3", - "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.3.tgz", - "integrity": "sha512-GZtjxm/J/4TSxuL3FNYjCmLktBTnIw/rVmKSIyKeYAZpmJB2ig9VauCC5xsa82GNKVKDAqpOn3KVzNt0zmrU0g==", - "dev": true, - "license": "MIT", - "dependencies": { - "cliui": "^8.0.1", - "escalade": "^3.1.1", - "get-caller-file": "^2.0.5", - "require-directory": "^2.1.1", - "string-width": "^4.2.3", - "y18n": "^5.0.5", - "yargs-parser": "^21.1.1" - }, - "engines": { - "node": ">=12" - } - }, - "node_modules/yargs-parser": { - "version": "21.1.1", - "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz", - "integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==", - "dev": true, - "license": "ISC", - "engines": { - "node": ">=12" - } - }, "node_modules/yocto-queue": { "version": "0.1.0", "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", diff --git a/package.json b/package.json index b2ee5b2..5d1da93 100644 --- a/package.json +++ b/package.json @@ -56,8 +56,6 @@ "postversion": "git push --follow-tags", "wizard": "tsx src/bin/cli.ts", "inspect": "tsx --inspect src/bin/cli.ts", - "example": "tsx examples", - "example:inspect": "tsx --inspect examples", "format": "prettier --write --ignore-path .gitignore .", "preformat": "eslint --fix .", "report": "vitest run --coverage" @@ -98,7 +96,6 @@ "eslint-plugin-unused-imports": "^4.1.4", "ink-testing-library": "^4.0.0", "jiti": "^2.4.2", - "landlubber": "^2.0.0", "neostandard": "^0.13.0", "prettier": "^3.0.0", "tsc-alias": "^1.8.2", diff --git a/src/bin/cli.ts b/src/bin/cli.ts index f6af9e1..4adb58d 100644 --- a/src/bin/cli.ts +++ b/src/bin/cli.ts @@ -7,7 +7,16 @@ import wizard from 'lib/wizard.js' -wizard({ argv: process.argv.slice(2) }).catch((err: unknown) => { +// The wizard sets up whichever project it is run in, which here is this +// repository. '--cwd ' points it at a scratch project instead. It is +// consumed here rather than forwarded, since it is an option of the wizard +// rather than one of its arguments. Every other argument is passed through +// untouched, so 'npm run wizard -- --help' reaches the wizard as '--help'. +const argv = process.argv.slice(2) +const cwdIndex = argv.indexOf('--cwd') +const cwd = cwdIndex === -1 ? undefined : argv.splice(cwdIndex, 2)[1] + +wizard({ argv, ...(cwd == null ? {} : { cwd }) }).catch((err: unknown) => { const { message, stack } = err instanceof Error ? err : new Error(String(err)) // eslint-disable-next-line no-console console.error(`Wizard Error: ${message}\n${stack ?? ''}`) diff --git a/tsconfig.json b/tsconfig.json index cdfc697..b47625e 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -30,11 +30,5 @@ } }, "files": ["src/index.ts", "src/bin/cli.ts"], - "include": [ - "src/**/*", - "test/**/*", - "examples/**/*", - "eslint.config.ts", - "vitest.config.ts" - ] + "include": ["src/**/*", "test/**/*", "eslint.config.ts", "vitest.config.ts"] } diff --git a/vitest.config.ts b/vitest.config.ts index f54f1c0..ac14356 100644 --- a/vitest.config.ts +++ b/vitest.config.ts @@ -13,7 +13,6 @@ export default defineConfig({ '**/index.ts', 'src/bin/cli.ts', 'package/**/*.ts', - 'examples/**/*.ts', '**/*.test.ts', '**/*.test.tsx', ], From 9ccd0eb7a1d0e5a3dea6fa68d41534a9eb737cbf Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 29 Jul 2026 02:15:20 +0000 Subject: [PATCH 11/11] docs: simplify how to run the wizard in development Document running it against a project with '--cwd' directly, rather than explaining the default of setting up this repository and working up to the flag from there. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01W1qhuuZQZnA1FB1fBSxrqx --- README.md | 24 ++++-------------------- 1 file changed, 4 insertions(+), 20 deletions(-) diff --git a/README.md b/README.md index 5bea07d..7a8d508 100644 --- a/README.md +++ b/README.md @@ -120,10 +120,10 @@ $ npm install $ npm run test:watch ``` -Run the wizard locally with +Run the wizard locally against a project with ``` -$ npm run wizard +$ npm run wizard -- --cwd ``` This runs the development CLI in `src/bin/cli.ts`, @@ -131,24 +131,8 @@ which simply calls the wizard with the arguments given. That file exists for local development only: it is excluded from the build and from the published package. -Because the wizard sets up whichever project it is run in, -`npm run wizard` sets up this repository itself, -which is rarely what you want. -Run it against a scratch project instead with - -``` -$ npm run wizard -- --cwd /tmp/scratch -``` - -`--cwd` is a development convenience of that CLI, not an argument of the -wizard: it becomes the `cwd` option. -Every other argument is forwarded to the wizard untouched, so - -``` -$ npm run wizard -- --help -``` - -reaches the wizard as `--help`. +`--cwd` is an option of that CLI, which becomes the wizard's `cwd`. +Every other argument is forwarded to the wizard untouched. ### Source layout