@base44-preview/cli 0.1.15-pr.630.434be14 → 0.1.15-pr.630.51361fe

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1224,7 +1224,7 @@
1224
1224
  "import { useContext } from 'react';\nimport FocusContext from '../components/FocusContext.js';\n/**\n * This hook exposes methods to enable or disable focus management for all\n * components or manually switch focus to next or previous components.\n */\nconst useFocusManager = () => {\n const focusContext = useContext(FocusContext);\n return {\n enableFocus: focusContext.enableFocus,\n disableFocus: focusContext.disableFocus,\n focusNext: focusContext.focusNext,\n focusPrevious: focusContext.focusPrevious,\n focus: focusContext.focus,\n };\n};\nexport default useFocusManager;\n//# sourceMappingURL=use-focus-manager.js.map",
1225
1225
  "import React, { useState, useEffect } from 'react';\nimport { Text, useInput } from 'ink';\nimport chalk from 'chalk';\nfunction TextInput({ value: originalValue, placeholder = '', focus = true, mask, highlightPastedText = false, showCursor = true, onChange, onSubmit, }) {\n const [state, setState] = useState({\n cursorOffset: (originalValue || '').length,\n cursorWidth: 0,\n });\n const { cursorOffset, cursorWidth } = state;\n useEffect(() => {\n setState(previousState => {\n if (!focus || !showCursor) {\n return previousState;\n }\n const newValue = originalValue || '';\n if (previousState.cursorOffset > newValue.length - 1) {\n return {\n cursorOffset: newValue.length,\n cursorWidth: 0,\n };\n }\n return previousState;\n });\n }, [originalValue, focus, showCursor]);\n const cursorActualWidth = highlightPastedText ? cursorWidth : 0;\n const value = mask ? mask.repeat(originalValue.length) : originalValue;\n let renderedValue = value;\n let renderedPlaceholder = placeholder ? chalk.grey(placeholder) : undefined;\n // Fake mouse cursor, because it's too inconvenient to deal with actual cursor and ansi escapes\n if (showCursor && focus) {\n renderedPlaceholder =\n placeholder.length > 0\n ? chalk.inverse(placeholder[0]) + chalk.grey(placeholder.slice(1))\n : chalk.inverse(' ');\n renderedValue = value.length > 0 ? '' : chalk.inverse(' ');\n let i = 0;\n for (const char of value) {\n renderedValue +=\n i >= cursorOffset - cursorActualWidth && i <= cursorOffset\n ? chalk.inverse(char)\n : char;\n i++;\n }\n if (value.length > 0 && cursorOffset === value.length) {\n renderedValue += chalk.inverse(' ');\n }\n }\n useInput((input, key) => {\n if (key.upArrow ||\n key.downArrow ||\n (key.ctrl && input === 'c') ||\n key.tab ||\n (key.shift && key.tab)) {\n return;\n }\n if (key.return) {\n if (onSubmit) {\n onSubmit(originalValue);\n }\n return;\n }\n let nextCursorOffset = cursorOffset;\n let nextValue = originalValue;\n let nextCursorWidth = 0;\n if (key.leftArrow) {\n if (showCursor) {\n nextCursorOffset--;\n }\n }\n else if (key.rightArrow) {\n if (showCursor) {\n nextCursorOffset++;\n }\n }\n else if (key.backspace || key.delete) {\n if (cursorOffset > 0) {\n nextValue =\n originalValue.slice(0, cursorOffset - 1) +\n originalValue.slice(cursorOffset, originalValue.length);\n nextCursorOffset--;\n }\n }\n else {\n nextValue =\n originalValue.slice(0, cursorOffset) +\n input +\n originalValue.slice(cursorOffset, originalValue.length);\n nextCursorOffset += input.length;\n if (input.length > 1) {\n nextCursorWidth = input.length;\n }\n }\n if (cursorOffset < 0) {\n nextCursorOffset = 0;\n }\n if (cursorOffset > originalValue.length) {\n nextCursorOffset = originalValue.length;\n }\n setState({\n cursorOffset: nextCursorOffset,\n cursorWidth: nextCursorWidth,\n });\n if (nextValue !== originalValue) {\n onChange(nextValue);\n }\n }, { isActive: focus });\n return (React.createElement(Text, null, placeholder\n ? value.length > 0\n ? renderedValue\n : renderedPlaceholder\n : renderedValue));\n}\nexport default TextInput;\nexport function UncontrolledTextInput({ initialValue = '', ...props }) {\n const [value, setValue] = useState(initialValue);\n return React.createElement(TextInput, { ...props, value: value, onChange: setValue });\n}\n//# sourceMappingURL=index.js.map",
1226
1226
  "import chalk from \"chalk\";\nimport { Box, render, Text, useApp, useInput } from \"ink\";\nimport TextInput from \"ink-text-input\";\nimport { useEffect, useReducer, useRef, useState } from \"react\";\nimport { LOGO_COLS, logoRows } from \"@/cli/commands/code/logo.js\";\nimport { createPasteFriendlyStdin } from \"@/cli/commands/code/paste.js\";\nimport {\n formatDuration,\n hardWrapAnsi,\n idleMusing,\n terminalLink,\n} from \"@/cli/commands/code/render.js\";\nimport type {\n SessionEngine,\n SessionStatus,\n TurnSettleInfo,\n} from \"@/cli/commands/code/session-engine.js\";\nimport { createSessionEngine } from \"@/cli/commands/code/session-engine.js\";\nimport { readAuth } from \"@/core/auth/config.js\";\nimport { getBase44ApiUrl } from \"@/core/config.js\";\nimport {\n displayName,\n getMe,\n MODELS,\n resolvePick,\n saveBuilderModel,\n} from \"@/core/model.js\";\nimport {\n isGithubUserTokenError,\n startGithubReauth,\n} from \"@/core/resources/apps/api.js\";\nimport packageJson from \"../../../../package.json\";\n\nconst FRAMES = [\"⠋\", \"⠙\", \"⠹\", \"⠸\", \"⠼\", \"⠴\", \"⠦\", \"⠧\", \"⠇\", \"⠏\"];\nconst BRAND_ORANGE = \"#E86B3C\";\n\n// Alternate screen (Claude Code model): the session owns the viewport with\n// its own internal scroll; the shell screen is restored untouched on exit.\n// Mode 1007 makes the mouse wheel send arrow keys, which drive the scroll.\nlet altScreenActive = false;\nlet altExitHooked = false;\nfunction enterAltScreen(): void {\n process.stdout.write(\"\\x1b[?1049h\\x1b[?1007h\\x1b[2J\\x1b[H\");\n altScreenActive = true;\n if (!altExitHooked) {\n altExitHooked = true;\n process.on(\"exit\", () => {\n if (altScreenActive) process.stdout.write(\"\\x1b[?1007l\\x1b[?1049l\");\n });\n }\n}\nfunction exitAltScreen(): void {\n if (!altScreenActive) return;\n altScreenActive = false;\n process.stdout.write(\"\\x1b[?1007l\\x1b[?1049l\");\n}\n\ninterface SessionOptions {\n branchId?: string;\n /** Live footer lines (repo/editor/preview) — pushing appends to the block. */\n footer: string[];\n /** Swallow whatever the conversation already holds before showing anything —\n * false for a fresh create, whose kickoff turn IS the history. */\n primeFirstPoll: boolean;\n /** Sent as the first turn right after priming (the `chat` argument). */\n initialMessage?: string;\n /** A turn is already starting server-side (the create kickoff): show this\n * as the busy label until its user message appears, instead of \"ready\". */\n awaitingTurnLabel?: string;\n /** Shown next to \"ready\" when nothing is running. */\n idleHint?: string;\n onTurnSettled?: (info: TurnSettleInfo) => void | Promise<void>;\n}\n\nfunction statusText(status: SessionStatus, musingSeed: number): string {\n const frame = FRAMES[Math.floor(Date.now() / 120) % FRAMES.length];\n switch (status.phase) {\n case \"awaiting\":\n return chalk.dim(\n `${frame} ${status.awaitingLabel} (${formatDuration(Date.now() - status.awaitingSince)})`,\n );\n case \"running\": {\n const turnFor = formatDuration(\n Date.now() - (status.turnStartedAt ?? Date.now()),\n );\n const tool = status.runningTool;\n if (tool) {\n const toolFor = Math.round((Date.now() - tool.startedAt) / 1000);\n const others = tool.others > 0 ? ` (+${tool.others})` : \"\";\n const what =\n tool.label ||\n `${tool.alias}${tool.summary ? ` ${tool.summary}` : \"\"}`;\n return chalk.dim(\n `${frame} ${what}${others} · ${toolFor}s (turn ${turnFor})`,\n );\n }\n // Long silent stretch: some arms (plan/design) run minutes-long model\n // calls whose UI renders only in the editor — say so instead of\n // looking frozen.\n const quiet =\n status.quietForMs > 60_000\n ? \" · a long private step — details render in the editor\"\n : \"\";\n return `${chalk.magenta(\"✻\")} ${chalk.dim(`${idleMusing(musingSeed)} (${turnFor})${quiet}`)}`;\n }\n case \"sending\":\n return chalk.dim(`${frame} sending…`);\n case \"idle\": {\n const hint = status.idleHint ? ` — ${status.idleHint}` : \"\";\n const last =\n status.lastTurnMs != null\n ? ` · last turn ${formatDuration(status.lastTurnMs)}${status.lastTurnOk ? \"\" : \" (failed)\"}`\n : \"\";\n return chalk.dim(`ready${hint}${last}`);\n }\n }\n}\n\ninterface ViewProps {\n engine: SessionEngine;\n footer: string[];\n subscribe: (listener: (line: string) => void) => () => void;\n}\n\nfunction SessionView({ engine, footer, subscribe }: ViewProps) {\n const { exit } = useApp();\n const [items, setItems] = useState<string[]>([]);\n const [input, setInput] = useState(\"\");\n const [scroll, setScroll] = useState(0); // lines up from the live bottom\n const [, tick] = useReducer((x: number) => x + 1, 0);\n const [musingSeed] = useState(() => Math.floor(Math.random() * 97));\n const [currentModel, setCurrentModel] = useState<string | null>(null);\n const [pickerIndex, setPickerIndex] = useState<number | null>(null); // null = closed\n const maxScrollRef = useRef(0);\n const meIdRef = useRef<string | null>(null);\n\n // Append a line straight into the transcript (for /command output that isn't\n // an engine event).\n const emit = (line: string) => setItems((h) => [...h, `${line}\\n`]);\n\n useEffect(\n // The trailing newline gives every stream item a blank line after it.\n () => subscribe((line) => setItems((h) => [...h, `${line}\\n`])),\n [subscribe],\n );\n useEffect(() => {\n const timer = setInterval(tick, 120);\n return () => clearInterval(timer);\n }, []);\n // Load the account's current builder-model pick for the footer (non-blocking).\n useEffect(() => {\n getMe()\n .then((me) => {\n meIdRef.current = me.id;\n setCurrentModel(me.builder_model ?? null);\n })\n .catch(() => {});\n }, []);\n\n // Persist a pick and reflect it in the footer.\n const applyModel = async (pick: (typeof MODELS)[number]) => {\n const orange = chalk.hex(BRAND_ORANGE);\n try {\n if ((pick.id ?? null) === currentModel) {\n emit(chalk.dim(` already on ${pick.name}`));\n return;\n }\n let id = meIdRef.current;\n if (!id) {\n id = (await getMe()).id;\n meIdRef.current = id;\n }\n await saveBuilderModel(id, pick.id);\n setCurrentModel(pick.id);\n emit(\n pick.id === null\n ? chalk.dim(\" model reset — Base44 chooses per app\")\n : ` ${orange(\"●\")} model set to ${chalk.bold(pick.name)}`,\n );\n } catch (error) {\n emit(\n chalk.red(\n ` /model: ${error instanceof Error ? error.message : String(error)}`,\n ),\n );\n }\n };\n\n // `/model` alone opens the arrow-navigable picker; `/model <name>` switches\n // straight away.\n const runModelSlash = (arg: string) => {\n if (!arg) {\n const cur = MODELS.findIndex((m) => (m.id ?? null) === currentModel);\n setPickerIndex(cur >= 0 ? cur : 0);\n return;\n }\n try {\n void applyModel(resolvePick(arg));\n } catch (error) {\n emit(\n chalk.red(\n ` /model: ${error instanceof Error ? error.message : String(error)}`,\n ),\n );\n }\n };\n\n useInput((char, key) => {\n // Model picker owns the keyboard while open: arrows move the selection,\n // Enter commits, Esc/Ctrl-C cancels. Swallow everything else so it doesn't\n // scroll the transcript or type into the (hidden) input.\n if (pickerIndex !== null) {\n if (key.upArrow)\n setPickerIndex((i) => ((i ?? 0) - 1 + MODELS.length) % MODELS.length);\n else if (key.downArrow)\n setPickerIndex((i) => ((i ?? 0) + 1) % MODELS.length);\n else if (key.return) {\n const pick = MODELS[pickerIndex];\n setPickerIndex(null);\n void applyModel(pick);\n } else if (key.escape || (key.ctrl && char === \"c\")) {\n setPickerIndex(null);\n }\n return;\n }\n if (key.ctrl && char === \"c\") {\n if (input) setInput(\"\");\n else exit();\n return;\n }\n if (key.ctrl && char === \"d\") {\n exit();\n return;\n }\n // Wheel scrolling: alternate-scroll mode turns it into arrow keys.\n if (key.upArrow) {\n setScroll((s) => Math.min(s + 3, maxScrollRef.current));\n return;\n }\n if (key.downArrow) {\n setScroll((s) => Math.max(0, s - 3));\n return;\n }\n if (key.pageUp) {\n setScroll((s) => Math.min(s + 20, maxScrollRef.current));\n return;\n }\n if (key.pageDown) {\n setScroll((s) => Math.max(0, s - 20));\n return;\n }\n // Esc stops the running turn (like the editor's stop button); when nothing\n // is running it snaps the transcript back to live.\n if (key.escape) {\n if (engine.turnRunning()) engine.stopTurn();\n else setScroll(0);\n }\n });\n\n const columns = process.stdout.columns || 80;\n const rows = process.stdout.rows || 24;\n const width = Math.min(columns, 100);\n const innerWidth = Math.max(10, width - 4); // input border + padding\n const inputRows = Math.max(1, Math.ceil((input.length + 3) / innerWidth)); // +cursor cell\n const pickerOpen = pickerIndex !== null;\n // The bottom block is either the input box (inputRows + 2 border) or the model\n // picker (title + one row per model + 2 border). +3 = status + model line +\n // hint; +1 more for the footer links when present.\n const inputBlockHeight = pickerOpen ? MODELS.length + 3 : inputRows + 2;\n const widgetHeight = inputBlockHeight + 3 + (footer.length ? 1 : 0);\n const viewHeight = Math.max(3, rows - widgetHeight - 1);\n\n // Hard-wrapped physical lines of the whole transcript; the view is a\n // window over them, pinned to the bottom unless the user scrolled.\n const lines = items.flatMap((item) => hardWrapAnsi(item, columns));\n const maxScroll = Math.max(0, lines.length - viewHeight);\n maxScrollRef.current = maxScroll;\n const clamped = Math.min(scroll, maxScroll);\n const end = lines.length - clamped;\n const visible = lines.slice(Math.max(0, end - viewHeight), end);\n\n const scrollNote =\n clamped > 0 ? chalk.yellow(` ↑ ${clamped} lines — Esc for live`) : \"\";\n // The status row must stay EXACTLY one row or the whole widget bounces —\n // truncate it (and every transcript row) instead of letting them wrap.\n const statusLine = hardWrapAnsi(\n `${statusText(engine.status(), musingSeed)}${scrollNote}`,\n Math.max(10, columns - 1),\n )[0];\n\n return (\n <Box flexDirection=\"column\">\n <Box flexDirection=\"column\" height={viewHeight}>\n {visible.map((line, index) => (\n // biome-ignore lint/suspicious/noArrayIndexKey: windowed slice re-renders wholesale each frame; position is the identity\n <Text key={`${index}-${line.length}`} wrap=\"truncate-end\">\n {line || \" \"}\n </Text>\n ))}\n </Box>\n <Text wrap=\"truncate-end\">{statusLine}</Text>\n {pickerOpen ? (\n <Box\n flexDirection=\"column\"\n borderStyle=\"round\"\n borderColor=\"cyan\"\n paddingX={1}\n width={width}\n >\n <Text>\n {chalk.bold(\"Pick a model\")}\n {chalk.dim(\" ↑↓ move · Enter select · Esc cancel\")}\n </Text>\n {MODELS.map((m, i) => {\n const selected = i === pickerIndex;\n const isCurrent = (m.id ?? null) === currentModel;\n const label = `${selected ? \"▸\" : \" \"} ${isCurrent ? \"●\" : \"○\"} ${m.name}${m.note ? ` (${m.note})` : \"\"}`;\n return (\n <Text\n key={m.name}\n color={selected ? \"cyan\" : undefined}\n wrap=\"truncate-end\"\n >\n {selected ? label : chalk.dim(label)}\n </Text>\n );\n })}\n </Box>\n ) : (\n <Box borderStyle=\"round\" borderColor=\"gray\" paddingX={1} width={width}>\n <Text color=\"cyan\">{\"❯ \"}</Text>\n {/* TODO: ink-text-input only handles ←/→, backspace and Enter. Ink\n already decodes Alt+←/→ (word jump), Alt+Backspace / Ctrl+W (delete\n word), Ctrl+A/E (line start/end) and Ctrl+U/K (kill line) — replace\n this with a small in-house line editor that honours them. */}\n <TextInput\n value={input}\n onChange={setInput}\n onSubmit={(value) => {\n const trimmed = value.trim();\n if (trimmed === \"/model\" || trimmed.startsWith(\"/model \")) {\n runModelSlash(trimmed.slice(\"/model\".length).trim());\n } else if (trimmed) {\n engine.submit(value);\n }\n setInput(\"\");\n }}\n />\n </Box>\n )}\n {footer.length > 0 && (\n <Text wrap=\"truncate-end\">{` ${footer.join(chalk.dim(\" · \"))}`}</Text>\n )}\n <Text wrap=\"truncate-end\">\n {` ${chalk.dim(\"model\")} ${chalk.hex(BRAND_ORANGE)(displayName(currentModel))}`}\n </Text>\n <Text dimColor wrap=\"truncate-end\">\n {pickerOpen\n ? \" ↑↓ to move · Enter to select · Esc to cancel\"\n : engine.turnRunning()\n ? \" Esc to stop · type to queue · scroll to read · Ctrl+C to exit\"\n : \" Enter to send · /model to switch model · Esc for live · Ctrl+C to exit\"}\n </Text>\n </Box>\n );\n}\n\n/**\n * The Claude-Code-style interactive session, rendered with Ink: history goes\n * permanently into scrollback via <Static>, while the bottom region — rule,\n * footer links, status line with the live turn timer, input, hints — re-renders\n * in place. Typing works mid-turn (the backend queues the message); Ctrl+C\n * clears the input, then exits; turns keep running server-side after exit.\n * TTY only — callers gate on interactivity.\n */\n\n/** The welcome header, Claude-Code style: the sun mark on the left, the title /\n * account / cwd lines stacked to its right. No box. */\nfunction renderHeader(who: string, mode?: string): string {\n const orange = chalk.hex(BRAND_ORANGE);\n const cwd = process.cwd().replace(process.env.HOME ?? \"\", \"~\");\n const logo = logoRows(BRAND_ORANGE);\n const logoW = LOGO_COLS;\n const text = [\n `${orange.bold(\"Base44 Code\")} ${chalk.dim(`v${packageJson.version}`)}`,\n chalk.bold(who ? `Welcome back, ${who}!` : \"Welcome!\"),\n chalk.dim(getBase44ApiUrl().replace(/^https:\\/\\//, \"\")),\n chalk.dim(cwd),\n ...(mode ? [`${orange(\"●\")} ${chalk.dim(mode)}`] : []),\n ];\n const height = Math.max(logo.length, text.length);\n const textTop = Math.max(0, Math.floor((logo.length - text.length) / 2));\n const out: string[] = [];\n for (let i = 0; i < height; i++) {\n const left = i < logo.length ? logo[i] : \" \".repeat(logoW);\n const right = text[i - textTop] ?? \"\";\n out.push(` ${left} ${right}`.trimEnd());\n }\n return out.join(\"\\n\");\n}\n\nasync function currentUserName(): Promise<string> {\n try {\n const auth = await readAuth();\n return auth.name || auth.email || \"\";\n } catch {\n return \"\"; // Not logged in yet — the welcome stays generic.\n }\n}\n\n/** The Base44 Code welcome box — the session's first history item, so it\n * scrolls away naturally like Claude Code's header does. */\nasync function buildHeader(mode?: string): Promise<string> {\n return renderHeader(await currentUserName(), mode);\n}\n\nexport async function runInteractiveSession(\n options: SessionOptions,\n): Promise<void> {\n const sessionStartedAt = Date.now();\n const listeners = new Set<(line: string) => void>();\n const buffered: string[] = [];\n const onLine = (line: string) => {\n if (listeners.size === 0) {\n buffered.push(line);\n return;\n }\n for (const listener of listeners) listener(line);\n };\n const subscribe = (listener: (line: string) => void) => {\n listeners.add(listener);\n if (buffered.length) {\n for (const line of buffered.splice(0)) listener(line);\n }\n return () => listeners.delete(listener);\n };\n\n const engine = createSessionEngine({\n branchId: options.branchId,\n awaitingTurnLabel: options.awaitingTurnLabel,\n idleHint: options.idleHint,\n onLine,\n onTurnSettled: options.onTurnSettled,\n });\n\n // Fresh viewport, Claude-Code style: clear the visible screen (shell history\n // stays in scrollback) and start at the TOP — the header renders first, and\n // the dynamic region's fixed height bottom-justifies the input widget at the\n // terminal's bottom, with the conversation filling the space between.\n enterAltScreen();\n onLine(await buildHeader());\n\n // Bracketed paste: the terminal wraps pastes in markers (and drops its\n // multi-line paste warning); the stdin proxy flattens them to one line.\n process.stdout.write(\"\\x1b[?2004h\");\n const stdinProxy = createPasteFriendlyStdin(process.stdin);\n const app = render(\n <SessionView\n engine={engine}\n footer={options.footer}\n subscribe={subscribe}\n />,\n { exitOnCtrlC: false, stdin: stdinProxy },\n );\n\n try {\n await engine.start(options.primeFirstPoll);\n if (options.initialMessage) engine.submit(options.initialMessage);\n await app.waitUntilExit();\n } finally {\n process.stdout.write(\"\\x1b[?2004l\");\n stdinProxy.cleanup();\n engine.stop();\n exitAltScreen();\n if (options.footer.length) {\n process.stdout.write(`${options.footer.join(chalk.dim(\" · \"))}\\n`);\n }\n const note = engine.turnRunning()\n ? \" — the running turn continues server-side (watch it in the editor)\"\n : \"\";\n process.stdout.write(\n `${chalk.dim(`session ended · ${formatDuration(Date.now() - sessionStartedAt)}${note}`)}\\n`,\n );\n }\n}\n\ninterface GenesisAppConfig {\n branchId?: string;\n awaitingTurnLabel?: string;\n onTurnSettled?: (info: TurnSettleInfo) => void | Promise<void>;\n}\n\ninterface GenesisOptions {\n /** Shown next to \"ready\" before the first prompt. */\n idleHint: string;\n /** Busy label while `createApp` runs. */\n creatingLabel: string;\n /** Live footer array — `createApp` pushes the links as they exist. */\n footer: string[];\n /** Short mode label rendered in the header (e.g. \"Builder\" / \"Import\"). */\n modeLabel?: string;\n /** Turn the first prompt into an app; returns the wiring for the real\n * engine, which takes over every later prompt. */\n createApp: (\n prompt: string,\n emit: (line: string) => void,\n ) => Promise<GenesisAppConfig>;\n}\n\n/**\n * A session that starts BEFORE any app exists: the Base44 Code page opens\n * with just the header and the input, and the first prompt creates the app\n * (repo, directory, kickoff build) — then a real engine takes over, exactly\n * as if the session had been opened on it.\n */\nexport async function runGenesisSession(\n options: GenesisOptions,\n): Promise<void> {\n const sessionStartedAt = Date.now();\n const listeners = new Set<(line: string) => void>();\n const buffered: string[] = [];\n const onLine = (line: string) => {\n if (listeners.size === 0) {\n buffered.push(line);\n return;\n }\n for (const listener of listeners) listener(line);\n };\n const subscribe = (listener: (line: string) => void) => {\n listeners.add(listener);\n if (buffered.length) {\n for (const line of buffered.splice(0)) listener(line);\n }\n return () => listeners.delete(listener);\n };\n\n let inner: SessionEngine | null = null;\n let creating = false;\n let creatingSince = 0;\n const IDLE_STATUS: SessionStatus = {\n phase: \"idle\",\n idleHint: options.idleHint,\n awaitingSince: 0,\n turnStartedAt: null,\n runningTool: null,\n lastTurnMs: null,\n lastTurnOk: true,\n quietForMs: 0,\n };\n const genesis: SessionEngine = {\n async start() {},\n stop() {\n inner?.stop();\n },\n stopTurn() {\n // Only a real engine can stop a server turn; app creation isn't stoppable.\n inner?.stopTurn();\n },\n submit(text: string) {\n if (inner) {\n inner.submit(text);\n return;\n }\n if (creating) {\n onLine(chalk.dim(\"· hold on — still creating the app\"));\n return;\n }\n creating = true;\n creatingSince = Date.now();\n onLine(`${chalk.cyan(\"❯\")} ${chalk.bold(text)}`);\n options\n .createApp(text, onLine)\n .then(async (config) => {\n const engine = createSessionEngine({\n branchId: config.branchId,\n awaitingTurnLabel: config.awaitingTurnLabel,\n onLine,\n onTurnSettled: config.onTurnSettled,\n });\n await engine.start(false);\n inner = engine;\n })\n .catch(async (error: unknown) => {\n creating = false;\n const message =\n error instanceof Error ? error.message : String(error);\n onLine(chalk.red(`✗ create failed: ${message}`));\n // A stale GitHub connection 401s while the create verifies repo\n // access. Hand back a reconnect link — a plain retry just 401s again.\n if (isGithubUserTokenError(error)) {\n const link = await startGithubReauth().catch(() => null);\n onLine(\n chalk.yellow(\n \"GitHub authorization expired — reconnect, then try again:\",\n ),\n );\n onLine(\n link\n ? terminalLink(\"Reconnect GitHub\", link)\n : \"Open Base44 → GitHub settings to reconnect your account.\",\n );\n }\n });\n },\n status(): SessionStatus {\n if (inner) return inner.status();\n if (creating) {\n return {\n ...IDLE_STATUS,\n phase: \"awaiting\",\n awaitingLabel: options.creatingLabel,\n awaitingSince: creatingSince,\n };\n }\n return IDLE_STATUS;\n },\n turnRunning() {\n return inner?.turnRunning() ?? creating;\n },\n };\n\n enterAltScreen();\n onLine(await buildHeader(options.modeLabel));\n\n process.stdout.write(\"\\x1b[?2004h\");\n const stdinProxy = createPasteFriendlyStdin(process.stdin);\n const app = render(\n <SessionView\n engine={genesis}\n footer={options.footer}\n subscribe={subscribe}\n />,\n { exitOnCtrlC: false, stdin: stdinProxy },\n );\n\n try {\n await app.waitUntilExit();\n } finally {\n process.stdout.write(\"\\x1b[?2004l\");\n stdinProxy.cleanup();\n genesis.stop();\n exitAltScreen();\n if (options.footer.length) {\n process.stdout.write(`${options.footer.join(chalk.dim(\" · \"))}\\n`);\n }\n const note = genesis.turnRunning()\n ? \" — the running turn continues server-side (watch it in the editor)\"\n : \"\";\n process.stdout.write(\n `${chalk.dim(`session ended · ${formatDuration(Date.now() - sessionStartedAt)}${note}`)}\\n`,\n );\n }\n}\n",
1227
- "import chalk from \"chalk\";\n\n/**\n * The Base44 mark, rendered the way `circle.py -d 6 --gap 3.5r --gap-height=0.8r`\n * draws it: an anti-aliased disc built from sub-cell block glyphs, corrected\n * for ~2:1 terminal cells, with one thin slot cut across it. Terminals known to\n * rasterise the Unicode 16 octants get the 2x4 grid; everything else gets the\n * 2x2 quadrant blocks every font has. Fully covered cells are painted as\n * background rather than a block glyph, so fonts whose blocks stop short of the\n * line height don't stripe the fill.\n */\nconst ROWS = 6;\nconst ASPECT = 0.44; // cell width / cell height\nconst SAMPLES = 4; // supersamples per axis on edge pixels\nconst FILL = 0.6; // coverage that turns a pixel on\n// `--gap 3.5r` names row 3.5 and aims at its middle; `--gap-height 0.8r`.\nconst GAP_ROW = 3.5 + 0.5;\nconst GAP_HEIGHT_ROWS = 0.8;\nexport const LOGO_COLS = Math.max(2, Math.floor(ROWS / ASPECT + 0.5));\n\ninterface Tier {\n rx: number;\n ry: number;\n /** Sub-cell mask → glyph; bit i = row i / rx, column i % rx. */\n table: string[];\n}\n\n// The octant tier as circle.py fits it: the sixteen \"quarter\" glyphs are not\n// in the tier, so their masks are replaced by the nearest drawable shape,\n// preferring to drop a pixel over adding one.\nconst OCTANT: Tier = {\n rx: 2,\n ry: 4,\n table: Array.from(\n \" \\u{1FB82}\\u{1CD00}\\u{2598}\\u{1CD01}\\u{1CD02}\\u{1CD03}\\u{1CD04}\\u{259D}\\u{1CD05}\\u{1CD06}\\u{1CD07}\\u{1CD08}\\u{2580}\\u{1CD09}\\u{1CD0A}\\u{1CD0B}\\u{1CD0C}\\u{1CD00}\\u{1CD0D}\\u{1CD0E}\\u{1CD0F}\\u{1CD10}\\u{1CD11}\\u{1CD12}\\u{1CD13}\\u{1CD14}\\u{1CD15}\\u{1CD16}\\u{1CD17}\\u{1CD18}\\u{1CD19}\\u{1CD1A}\\u{1CD1B}\\u{1CD1C}\\u{1CD1D}\\u{1CD1E}\\u{1CD1F}\\u{1CD03}\\u{1CD20}\\u{1CD21}\\u{1CD22}\\u{1CD23}\\u{1CD24}\\u{1CD25}\\u{1CD26}\\u{1CD27}\\u{1CD28}\\u{1CD29}\\u{1CD2A}\\u{1CD2B}\\u{1CD2C}\\u{1CD2D}\\u{1CD2E}\\u{1CD2F}\\u{1CD30}\\u{1CD31}\\u{1CD32}\\u{1CD33}\\u{1CD34}\\u{1CD35}\\u{1FB85} \\u{1CD36}\\u{1CD37}\\u{1CD38}\\u{1CD39}\\u{1CD3A}\\u{1CD3B}\\u{1CD3C}\\u{1CD3D}\\u{1CD3E}\\u{1CD3F}\\u{1CD40}\\u{1CD41}\\u{1CD42}\\u{1CD43}\\u{1CD44}\\u{2596}\\u{1CD45}\\u{1CD46}\\u{1CD47}\\u{1CD48}\\u{258C}\\u{1CD49}\\u{1CD4A}\\u{1CD4B}\\u{1CD4C}\\u{259E}\\u{1CD4D}\\u{1CD4E}\\u{1CD4F}\\u{1CD50}\\u{259B}\\u{1CD51}\\u{1CD52}\\u{1CD53}\\u{1CD54}\\u{1CD55}\\u{1CD56}\\u{1CD57}\\u{1CD58}\\u{1CD59}\\u{1CD5A}\\u{1CD5B}\\u{1CD5C}\\u{1CD5D}\\u{1CD5E}\\u{1CD5F}\\u{1CD60}\\u{1CD61}\\u{1CD62}\\u{1CD63}\\u{1CD64}\\u{1CD65}\\u{1CD66}\\u{1CD67}\\u{1CD68}\\u{1CD69}\\u{1CD6A}\\u{1CD6B}\\u{1CD6C}\\u{1CD6D}\\u{1CD6E}\\u{1CD6F}\\u{1CD70} \\u{1CD71}\\u{1CD72}\\u{1CD73}\\u{1CD74}\\u{1CD75}\\u{1CD76}\\u{1CD77}\\u{1CD78}\\u{1CD79}\\u{1CD7A}\\u{1CD7B}\\u{1CD7C}\\u{1CD7D}\\u{1CD7E}\\u{1CD7F}\\u{1CD80}\\u{1CD81}\\u{1CD82}\\u{1CD83}\\u{1CD84}\\u{1CD85}\\u{1CD86}\\u{1CD87}\\u{1CD88}\\u{1CD89}\\u{1CD8A}\\u{1CD8B}\\u{1CD8C}\\u{1CD8D}\\u{1CD8E}\\u{1CD8F}\\u{2597}\\u{1CD90}\\u{1CD91}\\u{1CD92}\\u{1CD93}\\u{259A}\\u{1CD94}\\u{1CD95}\\u{1CD96}\\u{1CD97}\\u{2590}\\u{1CD98}\\u{1CD99}\\u{1CD9A}\\u{1CD9B}\\u{259C}\\u{1CD9C}\\u{1CD9D}\\u{1CD9E}\\u{1CD9F}\\u{1CDA0}\\u{1CDA1}\\u{1CDA2}\\u{1CDA3}\\u{1CDA4}\\u{1CDA5}\\u{1CDA6}\\u{1CDA7}\\u{1CDA8}\\u{1CDA9}\\u{1CDAA}\\u{1CDAB}\\u{2582}\\u{1CDAC}\\u{1CDAD}\\u{1CDAE}\\u{1CDAF}\\u{1CDB0}\\u{1CDB1}\\u{1CDB2}\\u{1CDB3}\\u{1CDB4}\\u{1CDB5}\\u{1CDB6}\\u{1CDB7}\\u{1CDB8}\\u{1CDB9}\\u{1CDBA}\\u{1CDBB}\\u{1CDBC}\\u{1CDBD}\\u{1CDBE}\\u{1CDBF}\\u{1CDC0}\\u{1CDC1}\\u{1CDC2}\\u{1CDC3}\\u{1CDC4}\\u{1CDC5}\\u{1CDC6}\\u{1CDC7}\\u{1CDC8}\\u{1CDC9}\\u{1CDCA}\\u{1CDCB}\\u{1CDCC}\\u{1CDCD}\\u{1CDCE}\\u{1CDCF}\\u{1CDD0}\\u{1CDD1}\\u{1CDD2}\\u{1CDD3}\\u{1CDD4}\\u{1CDD5}\\u{1CDD6}\\u{1CDD7}\\u{1CDD8}\\u{1CDD9}\\u{1CDDA}\\u{2584}\\u{1CDDB}\\u{1CDDC}\\u{1CDDD}\\u{1CDDE}\\u{2599}\\u{1CDDF}\\u{1CDE0}\\u{1CDE1}\\u{1CDE2}\\u{259F}\\u{1CDE3}\\u{2586}\\u{1CDE4}\\u{1CDE5}\\u{2588}\",\n ),\n};\nconst QUAD: Tier = {\n rx: 2,\n ry: 2,\n table: Array.from(\n \" \\u{2598}\\u{259D}\\u{2580}\\u{2596}\\u{258C}\\u{259E}\\u{259B}\\u{2597}\\u{259A}\\u{2590}\\u{259C}\\u{2584}\\u{2599}\\u{259F}\\u{2588}\",\n ),\n};\n\nexport type LogoTier = \"octant\" | \"quad\";\n\n/** Same rule as circle.py: only terminals that draw octants themselves. */\nfunction detectTier(env: NodeJS.ProcessEnv = process.env): LogoTier {\n const term = env.TERM ?? \"\";\n const prog = env.TERM_PROGRAM ?? \"\";\n if (prog === \"ghostty\" || term.includes(\"ghostty\")) return \"octant\";\n if (env.KITTY_WINDOW_ID || term.includes(\"kitty\")) return \"octant\";\n if (env.WEZTERM_PANE || env.WEZTERM_EXECUTABLE) return \"octant\";\n if (term.startsWith(\"foot\") || prog === \"contour\") return \"octant\";\n return \"quad\";\n}\n\n/** Coverage in [0,1] per pixel. Pixels wholly inside or outside are settled by\n * two corner tests; only the outline is supersampled. */\nfunction coverageGrid(\n rows: number,\n cols: number,\n aspect: number,\n pixelH: number,\n): number[][] {\n const worldH = rows * pixelH;\n const worldW = cols * aspect;\n const radius = Math.min(worldH, worldW) / 2;\n const cx = worldW / 2;\n const cy = worldH / 2;\n const step = 1 / SAMPLES;\n const grid: number[][] = [];\n for (let py = 0; py < rows; py++) {\n const y0 = py * pixelH;\n const y1 = y0 + pixelH;\n const dyLo =\n y0 <= cy && cy <= y1 ? 0 : Math.min(Math.abs(y0 - cy), Math.abs(y1 - cy));\n const dyHi = Math.max(Math.abs(y0 - cy), Math.abs(y1 - cy));\n const line: number[] = [];\n for (let px = 0; px < cols; px++) {\n const x0 = px * aspect;\n const x1 = x0 + aspect;\n const dxLo =\n x0 <= cx && cx <= x1\n ? 0\n : Math.min(Math.abs(x0 - cx), Math.abs(x1 - cx));\n const dxHi = Math.max(Math.abs(x0 - cx), Math.abs(x1 - cx));\n if (Math.hypot(dxLo, dyLo) >= radius) {\n line.push(0);\n continue;\n }\n if (Math.hypot(dxHi, dyHi) <= radius) {\n line.push(1);\n continue;\n }\n let hits = 0;\n for (let j = 0; j < SAMPLES; j++) {\n const dy = y0 + (j + 0.5) * step * pixelH - cy;\n for (let i = 0; i < SAMPLES; i++) {\n const dx = x0 + (i + 0.5) * step * aspect - cx;\n if (Math.hypot(dx, dy) <= radius) hits++;\n }\n }\n line.push(hits / (SAMPLES * SAMPLES));\n }\n grid.push(line);\n }\n return grid;\n}\n\n/** Clear a band of pixel rows; GAP_ROW (text rows) marks the slot's middle, so\n * the same numbers land in the same place whatever the tier's row density. */\nfunction carveGap(grid: number[][], ry: number): void {\n const n = grid.length;\n const thick = Math.max(1, Math.round(GAP_HEIGHT_ROWS * ry));\n const centre = GAP_ROW >= 0 ? GAP_ROW * ry : n + GAP_ROW * ry;\n const start = Math.max(0, Math.min(n - thick, centre - thick / 2));\n for (let i = Math.trunc(start); i < Math.trunc(start + thick); i++) {\n grid[i].fill(0);\n }\n}\n\n/** The mark as LOGO_COLS-wide rows. With a colour, glyphs are painted in it and\n * full cells become background-coloured spaces; without, plain glyphs. */\nexport function logoRows(\n color?: string,\n tier: LogoTier = detectTier(),\n): string[] {\n const { rx, ry, table } = tier === \"octant\" ? OCTANT : QUAD;\n const grid = coverageGrid(ROWS * ry, LOGO_COLS * rx, ASPECT / rx, 1 / ry);\n carveGap(grid, ry);\n const fg = color ? chalk.hex(color) : (s: string) => s;\n const bg = color ? chalk.bgHex(color) : (s: string) => s;\n const full = (1 << (rx * ry)) - 1;\n const out: string[] = [];\n for (let r = 0; r < ROWS; r++) {\n let row = \"\";\n for (let c = 0; c < LOGO_COLS; c++) {\n let mask = 0;\n for (let sr = 0; sr < ry; sr++) {\n for (let sc = 0; sc < rx; sc++) {\n if (grid[r * ry + sr][c * rx + sc] >= FILL)\n mask |= 1 << (sr * rx + sc);\n }\n }\n const glyph = table[mask];\n if (glyph === \" \") row += \" \";\n else if (mask === full && color) row += bg(\" \");\n else row += fg(glyph);\n }\n out.push(row);\n }\n return out;\n}\n",
1227
+ "import chalk from \"chalk\";\n\n/**\n * The Base44 mark, rendered the way `circle.py -d 6 --gap 3.5r --gap-height=0.8r`\n * draws it: an anti-aliased disc built from sub-cell block glyphs, corrected\n * for ~2:1 terminal cells, with one thin slot cut across it. Terminals known to\n * rasterise the Unicode 16 octants get the 2x4 grid; everything else gets the\n * 2x2 quadrant blocks every font has. Fully covered cells are painted as\n * background rather than a block glyph, so fonts whose blocks stop short of the\n * line height don't stripe the fill.\n */\nconst ROWS = 6;\nconst ASPECT = 0.44; // cell width / cell height\nconst SAMPLES = 4; // supersamples per axis on edge pixels\nconst FILL = 0.6; // coverage that turns a pixel on\n// `--gap 3.5r` names row 3.5 and aims at its middle; `--gap-height 0.8r`.\nconst GAP_ROW = 3.5 + 0.5;\nconst GAP_HEIGHT_ROWS = 0.8;\nexport const LOGO_COLS = Math.max(2, Math.floor(ROWS / ASPECT + 0.5));\n\ninterface Tier {\n rx: number;\n ry: number;\n /** Sub-cell mask → glyph; bit i = row i / rx, column i % rx. */\n table: string[];\n}\n\n// The octant tier as circle.py fits it: the sixteen \"quarter\" glyphs are not\n// in the tier, so their masks are replaced by the nearest drawable shape,\n// preferring to drop a pixel over adding one.\nconst OCTANT: Tier = {\n rx: 2,\n ry: 4,\n table: Array.from(\n \" \\u{1FB82}\\u{1CD00}\\u{2598}\\u{1CD01}\\u{1CD02}\\u{1CD03}\\u{1CD04}\\u{259D}\\u{1CD05}\\u{1CD06}\\u{1CD07}\\u{1CD08}\\u{2580}\\u{1CD09}\\u{1CD0A}\\u{1CD0B}\\u{1CD0C}\\u{1CD00}\\u{1CD0D}\\u{1CD0E}\\u{1CD0F}\\u{1CD10}\\u{1CD11}\\u{1CD12}\\u{1CD13}\\u{1CD14}\\u{1CD15}\\u{1CD16}\\u{1CD17}\\u{1CD18}\\u{1CD19}\\u{1CD1A}\\u{1CD1B}\\u{1CD1C}\\u{1CD1D}\\u{1CD1E}\\u{1CD1F}\\u{1CD03}\\u{1CD20}\\u{1CD21}\\u{1CD22}\\u{1CD23}\\u{1CD24}\\u{1CD25}\\u{1CD26}\\u{1CD27}\\u{1CD28}\\u{1CD29}\\u{1CD2A}\\u{1CD2B}\\u{1CD2C}\\u{1CD2D}\\u{1CD2E}\\u{1CD2F}\\u{1CD30}\\u{1CD31}\\u{1CD32}\\u{1CD33}\\u{1CD34}\\u{1CD35}\\u{1FB85} \\u{1CD36}\\u{1CD37}\\u{1CD38}\\u{1CD39}\\u{1CD3A}\\u{1CD3B}\\u{1CD3C}\\u{1CD3D}\\u{1CD3E}\\u{1CD3F}\\u{1CD40}\\u{1CD41}\\u{1CD42}\\u{1CD43}\\u{1CD44}\\u{2596}\\u{1CD45}\\u{1CD46}\\u{1CD47}\\u{1CD48}\\u{258C}\\u{1CD49}\\u{1CD4A}\\u{1CD4B}\\u{1CD4C}\\u{259E}\\u{1CD4D}\\u{1CD4E}\\u{1CD4F}\\u{1CD50}\\u{259B}\\u{1CD51}\\u{1CD52}\\u{1CD53}\\u{1CD54}\\u{1CD55}\\u{1CD56}\\u{1CD57}\\u{1CD58}\\u{1CD59}\\u{1CD5A}\\u{1CD5B}\\u{1CD5C}\\u{1CD5D}\\u{1CD5E}\\u{1CD5F}\\u{1CD60}\\u{1CD61}\\u{1CD62}\\u{1CD63}\\u{1CD64}\\u{1CD65}\\u{1CD66}\\u{1CD67}\\u{1CD68}\\u{1CD69}\\u{1CD6A}\\u{1CD6B}\\u{1CD6C}\\u{1CD6D}\\u{1CD6E}\\u{1CD6F}\\u{1CD70} \\u{1CD71}\\u{1CD72}\\u{1CD73}\\u{1CD74}\\u{1CD75}\\u{1CD76}\\u{1CD77}\\u{1CD78}\\u{1CD79}\\u{1CD7A}\\u{1CD7B}\\u{1CD7C}\\u{1CD7D}\\u{1CD7E}\\u{1CD7F}\\u{1CD80}\\u{1CD81}\\u{1CD82}\\u{1CD83}\\u{1CD84}\\u{1CD85}\\u{1CD86}\\u{1CD87}\\u{1CD88}\\u{1CD89}\\u{1CD8A}\\u{1CD8B}\\u{1CD8C}\\u{1CD8D}\\u{1CD8E}\\u{1CD8F}\\u{2597}\\u{1CD90}\\u{1CD91}\\u{1CD92}\\u{1CD93}\\u{259A}\\u{1CD94}\\u{1CD95}\\u{1CD96}\\u{1CD97}\\u{2590}\\u{1CD98}\\u{1CD99}\\u{1CD9A}\\u{1CD9B}\\u{259C}\\u{1CD9C}\\u{1CD9D}\\u{1CD9E}\\u{1CD9F}\\u{1CDA0}\\u{1CDA1}\\u{1CDA2}\\u{1CDA3}\\u{1CDA4}\\u{1CDA5}\\u{1CDA6}\\u{1CDA7}\\u{1CDA8}\\u{1CDA9}\\u{1CDAA}\\u{1CDAB}\\u{2582}\\u{1CDAC}\\u{1CDAD}\\u{1CDAE}\\u{1CDAF}\\u{1CDB0}\\u{1CDB1}\\u{1CDB2}\\u{1CDB3}\\u{1CDB4}\\u{1CDB5}\\u{1CDB6}\\u{1CDB7}\\u{1CDB8}\\u{1CDB9}\\u{1CDBA}\\u{1CDBB}\\u{1CDBC}\\u{1CDBD}\\u{1CDBE}\\u{1CDBF}\\u{1CDC0}\\u{1CDC1}\\u{1CDC2}\\u{1CDC3}\\u{1CDC4}\\u{1CDC5}\\u{1CDC6}\\u{1CDC7}\\u{1CDC8}\\u{1CDC9}\\u{1CDCA}\\u{1CDCB}\\u{1CDCC}\\u{1CDCD}\\u{1CDCE}\\u{1CDCF}\\u{1CDD0}\\u{1CDD1}\\u{1CDD2}\\u{1CDD3}\\u{1CDD4}\\u{1CDD5}\\u{1CDD6}\\u{1CDD7}\\u{1CDD8}\\u{1CDD9}\\u{1CDDA}\\u{2584}\\u{1CDDB}\\u{1CDDC}\\u{1CDDD}\\u{1CDDE}\\u{2599}\\u{1CDDF}\\u{1CDE0}\\u{1CDE1}\\u{1CDE2}\\u{259F}\\u{1CDE3}\\u{2586}\\u{1CDE4}\\u{1CDE5}\\u{2588}\",\n ),\n};\nconst QUAD: Tier = {\n rx: 2,\n ry: 2,\n table: Array.from(\n \" \\u{2598}\\u{259D}\\u{2580}\\u{2596}\\u{258C}\\u{259E}\\u{259B}\\u{2597}\\u{259A}\\u{2590}\\u{259C}\\u{2584}\\u{2599}\\u{259F}\\u{2588}\",\n ),\n};\n\ntype LogoTier = \"octant\" | \"quad\";\n\n/** Same rule as circle.py: only terminals that draw octants themselves. */\nfunction detectTier(env: NodeJS.ProcessEnv = process.env): LogoTier {\n const term = env.TERM ?? \"\";\n const prog = env.TERM_PROGRAM ?? \"\";\n if (prog === \"ghostty\" || term.includes(\"ghostty\")) return \"octant\";\n if (env.KITTY_WINDOW_ID || term.includes(\"kitty\")) return \"octant\";\n if (env.WEZTERM_PANE || env.WEZTERM_EXECUTABLE) return \"octant\";\n if (term.startsWith(\"foot\") || prog === \"contour\") return \"octant\";\n return \"quad\";\n}\n\n/** Coverage in [0,1] per pixel. Pixels wholly inside or outside are settled by\n * two corner tests; only the outline is supersampled. */\nfunction coverageGrid(\n rows: number,\n cols: number,\n aspect: number,\n pixelH: number,\n): number[][] {\n const worldH = rows * pixelH;\n const worldW = cols * aspect;\n const radius = Math.min(worldH, worldW) / 2;\n const cx = worldW / 2;\n const cy = worldH / 2;\n const step = 1 / SAMPLES;\n const grid: number[][] = [];\n for (let py = 0; py < rows; py++) {\n const y0 = py * pixelH;\n const y1 = y0 + pixelH;\n const dyLo =\n y0 <= cy && cy <= y1 ? 0 : Math.min(Math.abs(y0 - cy), Math.abs(y1 - cy));\n const dyHi = Math.max(Math.abs(y0 - cy), Math.abs(y1 - cy));\n const line: number[] = [];\n for (let px = 0; px < cols; px++) {\n const x0 = px * aspect;\n const x1 = x0 + aspect;\n const dxLo =\n x0 <= cx && cx <= x1\n ? 0\n : Math.min(Math.abs(x0 - cx), Math.abs(x1 - cx));\n const dxHi = Math.max(Math.abs(x0 - cx), Math.abs(x1 - cx));\n if (Math.hypot(dxLo, dyLo) >= radius) {\n line.push(0);\n continue;\n }\n if (Math.hypot(dxHi, dyHi) <= radius) {\n line.push(1);\n continue;\n }\n let hits = 0;\n for (let j = 0; j < SAMPLES; j++) {\n const dy = y0 + (j + 0.5) * step * pixelH - cy;\n for (let i = 0; i < SAMPLES; i++) {\n const dx = x0 + (i + 0.5) * step * aspect - cx;\n if (Math.hypot(dx, dy) <= radius) hits++;\n }\n }\n line.push(hits / (SAMPLES * SAMPLES));\n }\n grid.push(line);\n }\n return grid;\n}\n\n/** Clear a band of pixel rows; GAP_ROW (text rows) marks the slot's middle, so\n * the same numbers land in the same place whatever the tier's row density. */\nfunction carveGap(grid: number[][], ry: number): void {\n const n = grid.length;\n const thick = Math.max(1, Math.round(GAP_HEIGHT_ROWS * ry));\n const centre = GAP_ROW >= 0 ? GAP_ROW * ry : n + GAP_ROW * ry;\n const start = Math.max(0, Math.min(n - thick, centre - thick / 2));\n for (let i = Math.trunc(start); i < Math.trunc(start + thick); i++) {\n grid[i].fill(0);\n }\n}\n\n/** The mark as LOGO_COLS-wide rows. With a colour, glyphs are painted in it and\n * full cells become background-coloured spaces; without, plain glyphs. */\nexport function logoRows(\n color?: string,\n tier: LogoTier = detectTier(),\n): string[] {\n const { rx, ry, table } = tier === \"octant\" ? OCTANT : QUAD;\n const grid = coverageGrid(ROWS * ry, LOGO_COLS * rx, ASPECT / rx, 1 / ry);\n carveGap(grid, ry);\n const fg = color ? chalk.hex(color) : (s: string) => s;\n const bg = color ? chalk.bgHex(color) : (s: string) => s;\n const full = (1 << (rx * ry)) - 1;\n const out: string[] = [];\n for (let r = 0; r < ROWS; r++) {\n let row = \"\";\n for (let c = 0; c < LOGO_COLS; c++) {\n let mask = 0;\n for (let sr = 0; sr < ry; sr++) {\n for (let sc = 0; sc < rx; sc++) {\n if (grid[r * ry + sr][c * rx + sc] >= FILL)\n mask |= 1 << (sr * rx + sc);\n }\n }\n const glyph = table[mask];\n if (glyph === \" \") row += \" \";\n else if (mask === full && color) row += bg(\" \");\n else row += fg(glyph);\n }\n out.push(row);\n }\n return out;\n}\n",
1228
1228
  "import { PassThrough } from \"node:stream\";\n\nconst START = \"\\x1b[200~\";\nconst END = \"\\x1b[201~\";\n\n/** Longest suffix of `s` that is a prefix of `marker` — a paste marker can\n * arrive split across stdin chunks. */\nfunction partialSuffix(s: string, marker: string): string {\n for (let n = Math.min(marker.length - 1, s.length); n > 0; n--) {\n if (marker.startsWith(s.slice(-n))) return s.slice(-n);\n }\n return \"\";\n}\n\n/** Stateful chunk sanitizer for bracketed paste: strips the markers and\n * flattens pasted newlines/tabs to spaces so a multi-line paste lands in the\n * input as ONE line instead of a submit per line. Pure — unit-testable. */\nexport function makePasteSanitizer(): (chunk: string) => string {\n let inPaste = false;\n let carry = \"\";\n const clean = (t: string) =>\n t.replace(/\\r\\n|\\r|\\n/g, \" \").replace(/\\t/g, \" \");\n return (chunk: string): string => {\n let s = carry + chunk;\n carry = \"\";\n let out = \"\";\n while (s.length > 0) {\n if (!inPaste) {\n const i = s.indexOf(START);\n if (i === -1) {\n const tail = partialSuffix(s, START);\n out += s.slice(0, s.length - tail.length);\n carry = tail;\n s = \"\";\n } else {\n out += s.slice(0, i);\n s = s.slice(i + START.length);\n inPaste = true;\n }\n } else {\n const j = s.indexOf(END);\n if (j === -1) {\n const tail = partialSuffix(s, END);\n out += clean(s.slice(0, s.length - tail.length));\n carry = tail;\n s = \"\";\n } else {\n out += clean(s.slice(0, j));\n s = s.slice(j + END.length);\n inPaste = false;\n }\n }\n }\n return out;\n };\n}\n\ninterface PasteFriendlyStdin extends NodeJS.ReadStream {\n cleanup(): void;\n}\n\n/**\n * A stdin for Ink that understands bracketed paste. The caller enables mode\n * 2004 on the terminal (which also silences iTerm's multi-line paste warning);\n * this proxy strips the markers and flattens the pasted text before Ink or\n * ink-text-input ever see it.\n */\nexport function createPasteFriendlyStdin(\n real: NodeJS.ReadStream,\n): PasteFriendlyStdin {\n const out = new PassThrough();\n const sanitize = makePasteSanitizer();\n const onData = (buf: Buffer) => {\n const text = sanitize(buf.toString(\"utf8\"));\n if (text) out.write(text);\n };\n real.on(\"data\", onData);\n\n // biome-ignore lint/suspicious/noExplicitAny: decorating a stream into Ink's expected stdin shape\n const proxy = out as any;\n proxy.isTTY = true;\n proxy.setRawMode = (mode: boolean) => {\n real.setRawMode?.(mode);\n return proxy;\n };\n proxy.ref = () => real.ref?.();\n proxy.unref = () => real.unref?.();\n proxy.cleanup = () => {\n real.off(\"data\", onData);\n real.pause();\n };\n return proxy as PasteFriendlyStdin;\n}\n",
1229
1229
  "import chalk from \"chalk\";\nimport {\n eventLine,\n formatDuration,\n toolAlias,\n} from \"@/cli/commands/code/render.js\";\nimport { ApiError } from \"@/core/errors.js\";\nimport {\n getFullConversation,\n sendTurn,\n stopTurn,\n} from \"@/core/resources/apps/api.js\";\nimport {\n diffConversation,\n newestUserTurn,\n newStreamState,\n} from \"@/core/resources/apps/stream.js\";\n\nconst POLL_MS = 1_000;\n\ninterface RunningTool {\n alias: string;\n label: string;\n summary: string;\n startedAt: number;\n}\n\nexport interface TurnSettleInfo {\n turnIndex: number;\n ok: boolean;\n backendStatus?: string;\n durationMs: number;\n}\n\ntype SessionPhase = \"awaiting\" | \"running\" | \"sending\" | \"idle\";\n\nexport interface SessionStatus {\n phase: SessionPhase;\n awaitingLabel?: string;\n idleHint?: string;\n awaitingSince: number;\n turnStartedAt: number | null;\n runningTool: {\n label: string;\n alias: string;\n summary: string;\n startedAt: number;\n others: number;\n } | null;\n lastTurnMs: number | null;\n lastTurnOk: boolean;\n /** ms since the running turn last produced a visible event. */\n quietForMs: number;\n}\n\ninterface EngineOptions {\n branchId?: string;\n awaitingTurnLabel?: string;\n idleHint?: string;\n onLine: (line: string) => void;\n onTurnSettled?: (info: TurnSettleInfo) => void | Promise<void>;\n}\n\nexport interface SessionEngine {\n start(primeFirstPoll: boolean): Promise<void>;\n stop(): void;\n submit(text: string): void;\n /** Stop the running turn server-side (like the editor's stop button). */\n stopTurn(): void;\n status(): SessionStatus;\n turnRunning(): boolean;\n}\n\n/**\n * Everything about a session except pixels: the persistent conversation\n * watcher, turn-state derivation from the newest user message's outcome\n * stamp, and message submission (including mid-turn sends the backend\n * queues). Emits already-styled scrollback lines through `onLine`; the UI\n * layer renders them plus a status snapshot.\n */\nexport function createSessionEngine(options: EngineOptions): SessionEngine {\n const running = new Map<string, RunningTool>();\n const diffState = newStreamState();\n\n let stopped = false;\n let polling = false;\n let pollStartedAt = 0;\n let timer: ReturnType<typeof setInterval> | null = null;\n let sendsInFlight = 0;\n let activeTurnId: string | null = null;\n let turnStartedAt: number | null = null;\n let pendingSubmitAt: number | null = null;\n let lastTurnMs: number | null = null;\n let lastTurnOk = true;\n let settledCount = 0;\n let awaitingTurn = options.awaitingTurnLabel ?? null;\n const awaitingSince = Date.now();\n let lastEventAt = Date.now();\n\n const submit = (raw: string) => {\n const typed = raw.trim();\n if (!typed) return;\n const text = typed;\n options.onLine(`${chalk.cyan(\"❯\")} ${chalk.bold(typed)}`);\n pendingSubmitAt = Date.now();\n const submitTurnId = activeTurnId;\n sendsInFlight++;\n sendTurn(text, options.branchId)\n .then((turn) => {\n if (turn.queued) {\n options.onLine(chalk.dim(\"· queued — runs after the current turn\"));\n }\n })\n .catch((error: unknown) => {\n // The chat request stays open for the whole turn, so a long turn trips\n // the edge's request timeout (Cloudflare ~100s) with a 5xx even though\n // the message reached the backend and the turn is running. If the\n // poller has since picked up a new turn (activeTurnId advanced, or the\n // submit marker was consumed), the send was delivered — not a failure.\n const delivered =\n activeTurnId !== submitTurnId ||\n turnStartedAt != null ||\n pendingSubmitAt == null;\n const status = error instanceof ApiError ? error.statusCode : undefined;\n const edgeDrop =\n status === 502 ||\n status === 503 ||\n status === 504 ||\n /timeout|gateway/i.test(error instanceof Error ? error.message : \"\");\n if (edgeDrop && delivered) return; // Running — the stream shows it.\n pendingSubmitAt = null;\n const message = error instanceof Error ? error.message : String(error);\n options.onLine(chalk.red(`✗ send failed: ${message}`));\n })\n .finally(() => {\n sendsInFlight--;\n });\n };\n\n const poll = async (prime: boolean) => {\n // Re-entrancy guard, but time-bounded: if a previous poll's request wedged\n // (a hung fetch that never resolves or rejects), a plain boolean would block\n // every future poll forever — the turn settles server-side but the UI stays\n // stuck on \"running\" with the timer ticking. After STUCK_POLL_MS, let a new\n // poll through so settle is still detected.\n const STUCK_POLL_MS = 45_000;\n if (polling && Date.now() - pollStartedAt < STUCK_POLL_MS) return;\n polling = true;\n pollStartedAt = Date.now();\n try {\n let messages: Awaited<ReturnType<typeof getFullConversation>>;\n try {\n messages = await getFullConversation(30, options.branchId);\n } catch {\n return; // Transient — next tick retries.\n }\n const events = diffConversation(diffState, messages);\n if (!prime) {\n for (const event of events) {\n if (event.kind === \"tool_start\") {\n running.set(event.id, {\n alias: toolAlias(event.name),\n label: event.label,\n summary: event.summary,\n startedAt: Date.now(),\n });\n continue;\n }\n let elapsedMs: number | undefined;\n if (event.kind === \"tool_end\") {\n const started = running.get(event.id)?.startedAt;\n if (started != null) elapsedMs = Date.now() - started;\n running.delete(event.id);\n }\n const line = eventLine(event, elapsedMs);\n if (line != null) {\n lastEventAt = Date.now();\n options.onLine(line);\n }\n }\n }\n\n const turn = newestUserTurn(messages);\n if (!turn) return;\n const kickoffDetection = awaitingTurn != null && activeTurnId === null;\n awaitingTurn = null;\n if (turn.id !== activeTurnId) {\n activeTurnId = turn.id;\n if (!turn.settled) {\n // A kickoff was already running before this session opened — count\n // its time from session start. Later turns count from their submit.\n turnStartedAt =\n pendingSubmitAt ?? (kickoffDetection ? awaitingSince : Date.now());\n pendingSubmitAt = null;\n running.clear();\n } else if (prime) {\n // Session opened onto an already-finished turn — nothing to track.\n turnStartedAt = null;\n }\n }\n if (turn.settled && turnStartedAt != null && turn.id === activeTurnId) {\n const durationMs = Date.now() - turnStartedAt;\n turnStartedAt = null;\n running.clear();\n lastTurnMs = durationMs;\n const ok = !turn.backendStatus?.startsWith(\"error\");\n lastTurnOk = ok;\n options.onLine(\n ok\n ? chalk.dim(`— turn finished · ${formatDuration(durationMs)}`)\n : chalk.red(\n `— turn failed (${turn.backendStatus ?? \"unknown\"}) · ${formatDuration(durationMs)}`,\n ),\n );\n const info: TurnSettleInfo = {\n turnIndex: settledCount++,\n ok,\n backendStatus: turn.backendStatus,\n durationMs,\n };\n try {\n await options.onTurnSettled?.(info);\n } catch {\n // A settle hook failure must not kill the session.\n }\n }\n } finally {\n polling = false;\n }\n };\n\n return {\n async start(primeFirstPoll: boolean) {\n await poll(primeFirstPoll);\n timer = setInterval(() => {\n if (!stopped) void poll(false);\n }, POLL_MS);\n timer.unref?.();\n },\n stop() {\n stopped = true;\n if (timer) clearInterval(timer);\n },\n stopTurn() {\n // Nothing running (or already sending nothing) — no-op so Esc stays free\n // for scroll-to-live when idle.\n if (\n turnStartedAt == null &&\n sendsInFlight === 0 &&\n pendingSubmitAt == null\n )\n return;\n options.onLine(chalk.dim(\"· stopping…\"));\n // Fire-and-forget: the backend persists the stopped status, and the poller\n // settles the turn from the transcript — same path as a natural finish.\n stopTurn(options.branchId).catch((error: unknown) => {\n options.onLine(\n chalk.red(\n ` stop failed: ${error instanceof Error ? error.message : String(error)}`,\n ),\n );\n });\n },\n submit,\n status(): SessionStatus {\n let phase: SessionPhase = \"idle\";\n if (awaitingTurn != null) phase = \"awaiting\";\n else if (turnStartedAt != null) phase = \"running\";\n else if (sendsInFlight > 0 || pendingSubmitAt != null) phase = \"sending\";\n let runningTool: SessionStatus[\"runningTool\"] = null;\n if (running.size > 0) {\n const newest = [...running.values()].at(-1) as RunningTool;\n runningTool = { ...newest, others: running.size - 1 };\n }\n return {\n phase,\n awaitingLabel: awaitingTurn ?? undefined,\n idleHint: options.idleHint,\n quietForMs: Date.now() - lastEventAt,\n awaitingSince,\n turnStartedAt,\n runningTool,\n lastTurnMs,\n lastTurnOk,\n };\n },\n turnRunning() {\n return turnStartedAt != null;\n },\n };\n}\n",
1230
1230
  "import chalk from \"chalk\";\nimport {\n appTypeChip,\n assertBuilderApp,\n createAndLinkApp,\n githubReauthLines,\n nextStepsLines,\n repoLabel,\n} from \"@/cli/commands/builder/shared.js\";\nimport { terminalLink } from \"@/cli/commands/code/render.js\";\nimport {\n runGenesisSession,\n runInteractiveSession,\n} from \"@/cli/commands/code/session.js\";\nimport type { CLIContext, RunCommandResult } from \"@/cli/types.js\";\nimport { type AppIdOptions, Base44Command } from \"@/cli/utils/index.js\";\nimport { InvalidInputError } from \"@/core/errors.js\";\nimport { getAppContext, initAppContext } from \"@/core/project/app-config.js\";\nimport {\n getPreviewUrl,\n resolveActiveBranchId,\n} from \"@/core/resources/apps/api.js\";\n\nconst BRAND_ORANGE = \"#E86B3C\";\n\ntype LinkedApp = Awaited<ReturnType<typeof createAndLinkApp>>;\n\ninterface CodeOptions {\n import?: string;\n path?: string;\n}\n\n/** Turn the session's first prompt into an app and hand back the engine\n * wiring. Same create + link as `base44 builder new`. */\nasync function bootstrapApp(\n prompt: string,\n footer: string[],\n emit: (line: string) => void,\n onCreated: (app: LinkedApp) => void,\n importRepo?: string,\n path?: string,\n) {\n let app: LinkedApp;\n try {\n app = await createAndLinkApp({ prompt, importRepo, path });\n } catch (error) {\n for (const line of (await githubReauthLines(error)) ?? []) emit(line);\n throw error;\n }\n onCreated(app);\n // The directory stays visible for the whole session, next to the links.\n footer.push(chalk.dim(`dir ${app.here ? \"./\" : `./${app.dirName}`}`));\n if (app.repoUrl) footer.push(terminalLink(\"repo\", app.repoUrl));\n footer.push(terminalLink(\"editor\", app.editorUrl));\n emit(\n chalk.dim(\n app.here\n ? \"linked ./ (this directory)\"\n : `linked ./${app.dirName} (cd ${app.dirName} after the session)`,\n ),\n );\n\n const branchId = await resolveActiveBranchId().catch(() => undefined);\n let previewPushed = false;\n return {\n branchId,\n awaitingTurnLabel: \"provisioning the sandbox and starting the build\",\n onTurnSettled: async ({\n turnIndex,\n ok,\n }: {\n turnIndex: number;\n ok: boolean;\n }) => {\n if (turnIndex !== 0 || !ok || previewPushed) return;\n const url = await getPreviewUrl().catch(() => undefined);\n if (url) {\n previewPushed = true;\n footer.push(terminalLink(\"preview\", url));\n }\n },\n };\n}\n\nasync function codeAction(\n { log }: CLIContext,\n options: CodeOptions,\n appId?: string,\n): Promise<RunCommandResult> {\n const orange = chalk.hex(BRAND_ORANGE);\n const chip = (label: string) => chalk.dim(`${orange(\"●\")} ${label}`);\n if (process.stdout.isTTY !== true) {\n throw new InvalidInputError(\n \"base44 code is an interactive session and needs a terminal.\",\n );\n }\n\n // Inside a linked app, open its session; anywhere else the first prompt\n // creates the app. Must be the real context lookup — an existence glob is\n // recursive and would match apps in subdirectories of an unlinked cwd.\n // --app-id opens that app from anywhere; otherwise a linked directory opens\n // its app, and anywhere else the first prompt creates one.\n let linked = false;\n try {\n await initAppContext(appId ? { appId } : {});\n linked = true;\n } catch {\n // Not linked — genesis below.\n }\n if (linked) {\n if (options.import || options.path) {\n throw new InvalidInputError(\n \"--import and --path create a new app; run them outside a linked project, without --app-id.\",\n );\n }\n const { id, projectRoot } = getAppContext();\n const state = await assertBuilderApp(id);\n const branchId = await resolveActiveBranchId().catch(() => undefined);\n await runInteractiveSession({\n branchId,\n footer: [chip(appTypeChip(state))],\n primeFirstPoll: true,\n idleHint: \"what should the agent do next?\",\n });\n if (projectRoot) {\n log.message(chalk.dim(`app dir ${projectRoot}`));\n return {\n outroMessage: \"Session closed. Run `base44 code` here to resume.\",\n };\n }\n return {\n outroMessage: `Session closed. Resume with \\`base44 code --app-id ${id}\\`.`,\n };\n }\n\n let created: LinkedApp | undefined;\n const footer = [chip(options.import ? repoLabel(options.import) : \"web app\")];\n await runGenesisSession({\n idleHint: options.import\n ? \"describe what to build over the repository\"\n : \"describe the app you want to build · have one already? base44 code --app-id <id>, or base44 link\",\n creatingLabel: options.import\n ? \"importing the repository\"\n : \"creating your app\",\n modeLabel: options.import\n ? `Repository — ${repoLabel(options.import)}`\n : \"Web app — Base44 template + builder agent\",\n footer,\n createApp: (prompt, emit) =>\n bootstrapApp(\n prompt,\n footer,\n emit,\n (app) => {\n created = app;\n },\n options.import,\n options.path,\n ),\n });\n if (!created) {\n return { outroMessage: \"Session closed. No app was created.\" };\n }\n for (const line of nextStepsLines(created)) log.message(line);\n log.message(chalk.dim(` editor ${created.editorUrl}`));\n return { outroMessage: \"Session closed.\" };\n}\n\nexport function getCodeCommand(): Base44Command {\n const command = new Base44Command(\"code\", { requireAppContext: false });\n command\n .description(\n \"Open Base44 Code, an interactive builder session. In a linked directory (or with --app-id <id>) it opens that app; anywhere else your first prompt creates one (--import <repo> to build over your own repository). Attach a directory to an existing app with base44 link.\",\n )\n .option(\n \"--import <repo>\",\n \"Build over an existing GitHub repository instead of the Base44 template\",\n )\n .option(\n \"--path <dir>\",\n \"Directory to link the new app to (default: the current directory when empty, else ./<name>)\",\n )\n .action((ctx: CLIContext, options: CodeOptions) =>\n codeAction(ctx, options, command.optsWithGlobals<AppIdOptions>().appId),\n );\n return command;\n}\n",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@base44-preview/cli",
3
- "version": "0.1.15-pr.630.434be14",
3
+ "version": "0.1.15-pr.630.51361fe",
4
4
  "description": "Base44 CLI - Unified interface for managing Base44 applications",
5
5
  "type": "module",
6
6
  "bin": {