UNPKG

kitcn

Version:

kitcn - React Query integration and CLI tools for Convex

1,181 lines (1,176 loc) 112 kB
#!/usr/bin/env node import { $ as promptForScaffoldTemplateSelection, A as resolveCodegenTrimSegments, At as highlighter, B as runConfiguredCodegen, C as isEntryPoint, Ct as formatDependencyInstallCommand, D as parseInitCommandArgs, E as parseBackendRunJson, Et as stripConvexCommandNoise, F as resolveRunDeps, G as runMigrationFlow, H as runDevSchemaBackfillIfNeeded, I as runAfterScaffoldScript, J as withWorkingDirectory, K as trackProcess, L as runAggregateBackfillFlow, M as resolveDocTopic, N as resolveInitProjectDir, O as readPackageVersions, P as resolveMigrationConfig, Q as promptForPluginSelection, R as runAggregatePruneFlow, S as isConvexDevPreRunConflictFlag, St as detectPackageManager, T as parseArgs, Tt as serializeEnvValue, U as runInitCommandFlow, V as runConvexInitIfNeeded, W as runMigrationCreate, X as collectPluginScaffoldTemplates, Y as createSpinner, Z as filterScaffoldTemplatePathMap, _ as formatInfoOutput, _t as applyPlanningDependencyInstall, a as cleanup, at as getPluginCatalogEntry, b as getDevAggregateBackfillStatePath, bt as resolveSupportedDependencyWarnings, c as createCommandEnv, ct as buildPluginInstallPlan, d as extractBackfillCliOptions, dt as collectInstalledPluginKeys, et as resolveAddTemplateDefaults, f as extractConcaveRunTargetArgs, ft as getPluginLockfilePath, g as formatDocsOutput, gt as applyDependencyHintsInstall, h as extractResetCliOptions, ht as resolveSchemaInstalledPlugins, i as buildInitializationPlan, it as resolveTemplatesByIdOrThrow, j as resolveConfiguredBackend, k as resolveBackfillConfig, kt as logger, l as ensureConvexGitignoreEntry, lt as resolvePluginScaffoldRoots, m as extractMigrationDownOptions, mt as readPluginLockfile, n as applyPluginInstallPlanFiles, nt as resolvePresetScaffoldTemplates, o as createBackendAdapter, ot as getSupportedPluginKeys, p as extractMigrationCliOptions, pt as getSchemaFilePath, q as withLocalCodegenEnv, r as assertNoRemovedDevPreRunFlag, rt as resolveTemplateSelectionSource, s as createBackendCommandEnv, st as isSupportedPluginKey, t as applyDependencyInstallPlan, tt as resolvePluginPreset, u as extractBackendRunTargetArgs, ut as assertSchemaFileExists, v as getAggregateBackfillDeploymentKey, vt as applyPluginDependencyInstall, w as isInitialized, wt as resolveAuthEnvState, x as hasRemoteConvexDeploymentEnv, xt as resolveProjectScaffoldContext, y as getConvexDeploymentCommandEnv, yt as inspectPluginDependencyInstall, z as runBackendFunction } from "./backend-core-cwf87w_T.mjs"; import fs, { existsSync, readFileSync } from "node:fs"; import path, { delimiter, dirname, join, relative, resolve } from "node:path"; import { fileURLToPath } from "node:url"; import { diffWords, structuredPatch } from "diff"; import { parseEnv } from "node:util"; import { createServer } from "node:http"; //#region src/cli/utils/plan-formatter.ts const MAX_OVERVIEW_FILES = 5; const BOX_INNER_WIDTH = 46; const ANSI_RE = new RegExp(`${String.fromCharCode(27)}\\[[0-9;]*m`, "g"); const TRAILING_COMMA_RE = /,$/; const LEADING_WHITESPACE_RE = /^(\s*)/; const SINGLE_OR_DOUBLE_QUOTE_RE = /['"]/g; const TRAILING_SEMICOLON_RE = /;$/g; const normalizePath = (value) => value.replaceAll("\\", "/"); const visibleLength = (value) => value.replace(ANSI_RE, "").length; const padEnd = (value, width) => { const missing = Math.max(0, width - visibleLength(value)); return `${value}${" ".repeat(missing)}`; }; const actionGlyph = (action) => { if (action === "create") return highlighter.success("+"); if (action === "update") return highlighter.warn("~"); return highlighter.dim("="); }; const actionLabel = (action) => { if (action === "create") return highlighter.success("create"); if (action === "update") return highlighter.warn("update"); return highlighter.dim("skip"); }; const dimLine = (value) => highlighter.dim(value); const boldLine = (value) => highlighter.bold(value); const renderHeader = (title, subtitle) => `${boldLine("┌")} ${boldLine(title)}${subtitle ? ` ${dimLine(subtitle)}` : ""}`; const renderSectionTitle = (title, meta) => `${dimLine("├")} ${boldLine(title)}${meta ? ` ${dimLine(meta)}` : ""}`; const renderBoxLine = (value) => `${dimLine("│")} ${dimLine("│")} ${value}`; const renderContentBox = (contentLines, formatLine = (value) => value) => { const top = dimLine(`┌${"─".repeat(BOX_INNER_WIDTH)}`); const bottom = dimLine(`└${"─".repeat(BOX_INNER_WIDTH)}`); return [ `${dimLine("│")} ${top}`, ...contentLines.map((line) => renderBoxLine(formatLine(line))), `${dimLine("│")} ${bottom}` ]; }; const renderOperationTarget = (operation) => operation.path ?? operation.packageName ?? operation.command ?? operation.key; const renderSummaryFileLine = (file) => `${dimLine("│")} ${actionGlyph(file.action)} ${padEnd(highlighter.path(file.path), 44)} ${dimLine(`[${file.kind}]`)} ${actionLabel(file.action)}`; const renderSummaryOperationLine = (operation) => { const target = renderOperationTarget(operation); return `${dimLine("│")} ${dimLine("•")} ${padEnd(operation.kind, 18)} ${dimLine(operation.status)}${target ? ` ${dimLine(target)}` : ""}`; }; const normalizeLine = (line) => line.replace(/\s+/g, " ").trim().replace(SINGLE_OR_DOUBLE_QUOTE_RE, "'").replaceAll(";", "").replace(TRAILING_COMMA_RE, ""); const normalizeFileForDiff = (value) => value.split("\n").map((line) => { const indent = line.match(LEADING_WHITESPACE_RE)?.[1] ?? ""; return indent + line.slice(indent.length).replace(SINGLE_OR_DOUBLE_QUOTE_RE, "\"").replace(TRAILING_SEMICOLON_RE, ""); }).join("\n"); const isFormattingOnly = (oldValue, newValue) => { const normalize = (value) => value.split("\n").map(normalizeLine).filter((line) => line.length > 0).join(" "); return normalize(oldValue) === normalize(newValue); }; const isGroupFormattingOnly = (removed, added) => { const normalize = (lines) => lines.map(normalizeLine).filter((line) => line.length > 0).join(" "); return normalize(removed) === normalize(added); }; const collapseContinuationLines = (lines) => { const result = []; for (let index = 0; index < lines.length; index += 1) { let line = lines[index]; while (index + 1 < lines.length && line.trimEnd().endsWith(":")) { index += 1; line = `${line.trimEnd()} ${lines[index].trim()}`; } result.push(line); } return result; }; const highlightInlineChanges = (oldLine, newLine) => { const changes = diffWords(oldLine, newLine); let oldHighlighted = "-"; let newHighlighted = "+"; for (const change of changes) { if (change.added) { newHighlighted += highlighter.bold(highlighter.success(change.value)); continue; } if (change.removed) { oldHighlighted += highlighter.bold(highlighter.error(change.value)); continue; } oldHighlighted += highlighter.error(change.value); newHighlighted += highlighter.success(change.value); } return { oldHighlighted, newHighlighted }; }; const processChangeGroup = (removed, added, newLines, newLineIndex, entries) => { let nextLineIndex = newLineIndex; if (isGroupFormattingOnly(removed, added)) { for (const line of added) { const actual = newLines[nextLineIndex] ?? line; entries.push({ kind: "context", formatted: dimLine(` ${actual}`) }); nextLineIndex += 1; } return nextLineIndex; } const collapsedRemoved = collapseContinuationLines(removed); const normalizedCollapsed = collapsedRemoved.map(normalizeLine); const usedCollapsed = /* @__PURE__ */ new Set(); for (const line of added) { const actualNewLine = newLines[nextLineIndex] ?? line; const normalizedAdded = normalizeLine(line); const matchIndex = normalizedCollapsed.findIndex((normalized, collapsedIndex) => !usedCollapsed.has(collapsedIndex) && normalized === normalizedAdded); if (matchIndex !== -1) { usedCollapsed.add(matchIndex); entries.push({ kind: "context", formatted: dimLine(` ${actualNewLine}`) }); nextLineIndex += 1; continue; } const unmatchedIndex = normalizedCollapsed.findIndex((_, collapsedIndex) => !usedCollapsed.has(collapsedIndex)); if (unmatchedIndex !== -1) { usedCollapsed.add(unmatchedIndex); const { oldHighlighted, newHighlighted } = highlightInlineChanges(collapsedRemoved[unmatchedIndex], actualNewLine); entries.push({ kind: "removed", formatted: oldHighlighted }); entries.push({ kind: "added", formatted: newHighlighted }); } else entries.push({ kind: "added", formatted: highlighter.success(`+${actualNewLine}`) }); nextLineIndex += 1; } for (let index = 0; index < collapsedRemoved.length; index += 1) { if (usedCollapsed.has(index)) continue; entries.push({ kind: "removed", formatted: highlighter.error(`-${collapsedRemoved[index]}`) }); } return nextLineIndex; }; const processHunk = (hunk, newLines) => { const entries = []; let newLineIndex = hunk.newStart - 1; let index = 0; while (index < hunk.lines.length) { const line = hunk.lines[index]; if (line.startsWith("-")) { const removed = []; while (index < hunk.lines.length && hunk.lines[index].startsWith("-")) { removed.push(hunk.lines[index].slice(1)); index += 1; } while (index < hunk.lines.length && hunk.lines[index].startsWith("\\")) index += 1; const added = []; while (index < hunk.lines.length && hunk.lines[index].startsWith("+")) { added.push(hunk.lines[index].slice(1)); index += 1; } while (index < hunk.lines.length && hunk.lines[index].startsWith("\\")) index += 1; newLineIndex = processChangeGroup(removed, added, newLines, newLineIndex, entries); continue; } if (line.startsWith("+")) { const actual = newLines[newLineIndex] ?? line.slice(1); entries.push({ kind: "added", formatted: highlighter.success(`+${actual}`) }); newLineIndex += 1; index += 1; continue; } if (line.startsWith("\\")) { index += 1; continue; } const actual = newLines[newLineIndex] ?? line.slice(1); entries.push({ kind: "context", formatted: dimLine(` ${actual}`) }); newLineIndex += 1; index += 1; } return { entries, newLineIndex }; }; const computeUnifiedDiff = (oldValue, newValue, filePath) => { if (isFormattingOnly(oldValue, newValue)) return [dimLine(" Formatting-only changes (spacing, quotes, semicolons).")]; const normalizedOld = normalizeFileForDiff(oldValue); const normalizedNew = normalizeFileForDiff(newValue); const patch = structuredPatch(`a/${filePath}`, `b/${filePath}`, normalizedOld, normalizedNew, "", "", { context: 3 }); if (patch.hunks.length === 0) return [dimLine(" No changes.")]; const output = [dimLine(`--- a/${filePath}`), dimLine(`+++ b/${filePath}`)]; const newLines = newValue.split("\n"); for (const hunk of patch.hunks) { const { entries } = processHunk(hunk, newLines); if (!entries.some((entry) => entry.kind !== "context")) continue; const contextCount = entries.filter((entry) => entry.kind === "context").length; const removedCount = entries.filter((entry) => entry.kind === "removed").length; const addedCount = entries.filter((entry) => entry.kind === "added").length; output.push(highlighter.info(`@@ -${hunk.oldStart},${contextCount + removedCount} +${hunk.newStart},${contextCount + addedCount} @@`)); output.push(...entries.map((entry) => entry.formatted)); } return output.length > 2 ? output : [dimLine(" Formatting-only changes (spacing, quotes, semicolons).")]; }; const renderDiff = (file) => { if (file.action === "skip") return [dimLine(" No changes.")]; if (file.action === "create" || !file.existingContent) return file.content.trimEnd().split("\n").map((line) => highlighter.success(`+${line}`)); return computeUnifiedDiff(file.existingContent, file.content, file.path); }; const resolvePlanPathMatches = (files, filterPath) => { const normalizedFilter = normalizePath(filterPath); const exactMatches = files.filter((file) => file.path === normalizedFilter); if (exactMatches.length > 0) return exactMatches; const substringMatches = files.filter((file) => file.path.includes(normalizedFilter)); if (substringMatches.length > 0) return substringMatches; return files.filter((file) => file.path.endsWith(normalizedFilter)); }; const formatPlanSummary = (plan) => { const changedFiles = plan.files.filter((file) => file.action !== "skip"); const visibleFiles = plan.files.slice(0, MAX_OVERVIEW_FILES); const lines = [ renderHeader(`kitcn add ${plan.plugin}`, "(dry run)"), dimLine("│"), `${dimLine("│")} ${dimLine(`preset ${plan.preset} • selection ${plan.selectionSource}`)}`, renderSectionTitle("Files", `${changedFiles.length}/${plan.files.length} changed`), ...visibleFiles.map((file) => renderSummaryFileLine(file)) ]; if (plan.files.length > MAX_OVERVIEW_FILES) lines.push(`${dimLine("│")} ${dimLine(`... ${plan.files.length - MAX_OVERVIEW_FILES} more files; use --diff <path> or --view <path>.`)}`); lines.push(dimLine("│")); lines.push(renderSectionTitle("Operations", `${plan.operations.length} queued`)); lines.push(...plan.operations.map((operation) => renderSummaryOperationLine(operation))); if (plan.envReminders.length > 0) { lines.push(dimLine("│")); lines.push(renderSectionTitle("Environment")); lines.push(...plan.envReminders.map((reminder) => `${dimLine("│")} ${dimLine("•")} ${highlighter.path(reminder.key)} ${dimLine(`→ ${reminder.path}`)}${reminder.message ? ` ${dimLine(reminder.message)}` : ""}`)); } lines.push(dimLine("│")); lines.push(`${dimLine("│")} ${dimLine("Run with --diff to inspect patches.")}`); lines.push(`${dimLine("│")} ${dimLine("Run with --view to inspect rendered files.")}`); lines.push(`${dimLine("└")} ${dimLine("Run without --dry-run to apply. use --diff <path> or --view <path>.")}`); return lines.join("\n"); }; const formatPlanDiff = (plan, filterPath) => { const changedFiles = plan.files.filter((file) => file.action !== "skip"); const files = typeof filterPath === "string" ? resolvePlanPathMatches(plan.files, filterPath) : changedFiles.slice(0, MAX_OVERVIEW_FILES); if (files.length === 0) return typeof filterPath === "string" ? `No planned file matching "${filterPath}".` : "No planned file changes."; const lines = [renderHeader(`kitcn add ${plan.plugin}`, "(dry run)"), dimLine("│")]; for (const file of files) { lines.push(`${dimLine("├")} ${highlighter.path(file.path)} ${dimLine("(")}${actionLabel(file.action)}${dimLine(")")}`); lines.push(...renderContentBox(renderDiff(file))); lines.push(dimLine("│")); } if (typeof filterPath !== "string" && changedFiles.length > MAX_OVERVIEW_FILES) lines.push(`${dimLine("│")} ${dimLine(`Showing ${MAX_OVERVIEW_FILES} of ${changedFiles.length} files. Use --diff <path> to focus one file.`)}`); lines.push(`${dimLine("└")} ${dimLine("Run without --dry-run to apply.")}`); return lines.join("\n"); }; const formatPlanView = (plan, filterPath) => { const files = typeof filterPath === "string" ? resolvePlanPathMatches(plan.files, filterPath) : plan.files.slice(0, MAX_OVERVIEW_FILES); if (files.length === 0) return typeof filterPath === "string" ? `No planned file matching "${filterPath}".` : "No planned files."; const lines = [renderHeader(`kitcn add ${plan.plugin}`, "(dry run)"), dimLine("│")]; for (const file of files) { lines.push(`${dimLine("├")} ${highlighter.path(file.path)} ${dimLine("(")}${actionLabel(file.action)}${dimLine(")")} ${dimLine(`${file.content.split("\n").length} lines`)}`); lines.push(...renderContentBox(file.content.trimEnd().split("\n"))); lines.push(dimLine("│")); } if (typeof filterPath !== "string" && plan.files.length > MAX_OVERVIEW_FILES) lines.push(`${dimLine("│")} ${dimLine(`Showing ${MAX_OVERVIEW_FILES} of ${plan.files.length} files. Use --view <path> to focus one file.`)}`); lines.push(`${dimLine("└")} ${dimLine("Run without --dry-run to apply.")}`); return lines.join("\n"); }; const formatPluginView = (plan) => { return [ renderHeader(`kitcn view ${plan.plugin}`), dimLine("│"), `${dimLine("│")} ${dimLine(`preset ${plan.preset} • selection ${plan.selectionSource}`)}`, `${dimLine("│")} ${dimLine(`docs ${plan.docs.publicUrl}`)}`, dimLine("│"), renderSectionTitle("Files", `${plan.files.length} tracked`), ...plan.files.map((file) => renderSummaryFileLine(file)), dimLine("│"), renderSectionTitle("Operations", `${plan.operations.length} steps`), ...plan.operations.map((operation) => renderSummaryOperationLine(operation)), `${dimLine("└")} ${dimLine(plan.docs.localPath)}` ].join("\n"); }; //#endregion //#region src/cli/utils/dry-run.ts const serializeDryRunPlan = (plan) => { return { ...plan, dependency: { packageName: plan.dependency.packageName, packageSpec: plan.dependency.packageSpec, packageJsonPath: plan.dependency.packageJsonPath?.replaceAll("\\", "/"), installed: plan.dependency.installed, skipped: plan.dependency.skipped, reason: plan.dependency.reason } }; }; //#endregion //#region src/cli/commands/dev.ts const __filename$1 = fileURLToPath(import.meta.url); const __dirname = dirname(__filename$1); const HELP_FLAGS$11 = new Set(["--help", "-h"]); const LOCAL_CONCAVE_HOST = "127.0.0.1"; const LOCAL_CONCAVE_DEV_PORT = 3210; const LOCAL_CONCAVE_SITE_PORT = 3211; const LOCAL_CONCAVE_DEV_URL = `http://${LOCAL_CONCAVE_HOST}:${LOCAL_CONCAVE_DEV_PORT}`; const LOCAL_CONCAVE_SITE_URL = `http://${LOCAL_CONCAVE_HOST}:${LOCAL_CONCAVE_SITE_PORT}`; const CONCAVE_DEV_STARTUP_MAX_ATTEMPTS = 4; const DEV_STARTUP_RETRY_DELAY_CAP_MS = 3e4; const DEV_FILE_WATCH_DEBOUNCE_MS = 200; const DEV_BOOTSTRAP_FLAG = "--bootstrap"; const DEV_BOOTSTRAP_TYPECHECK_FLAG = "--typecheck"; const DEV_BOOTSTRAP_TYPECHECK_MODE = "disable"; const DEV_READY_LINE_RE = /(Convex|Concave) functions ready!/i; const DEV_SUPPRESSED_LINE_PATTERNS = [/WARN \[Better Auth\]: Rate limiting skipped: could not determine client IP address\./]; const SUPPORTED_LOCAL_CONVEX_NODE_MAJORS = new Set([ 18, 20, 22, 24 ]); const LINE_SPLIT_RE = /\r?\n/; function sleepWithAbort(ms, signal) { if (signal?.aborted) return Promise.resolve(); return new Promise((resolve) => { const timer = setTimeout(() => { signal?.removeEventListener("abort", onAbort); resolve(); }, ms); const onAbort = () => { clearTimeout(timer); signal?.removeEventListener("abort", onAbort); resolve(); }; signal?.addEventListener("abort", onAbort); }); } function readWatchedFileSnapshot(filePath) { if (!fs.existsSync(filePath)) return null; return fs.readFileSync(filePath, "utf8"); } function resolveDevStartupRetryDelayMs(retryAttempt) { return Math.min(1e3 * 2 ** Math.max(0, retryAttempt - 1), DEV_STARTUP_RETRY_DELAY_CAP_MS); } async function runDevAuthEnvSyncLoop({ runTask, signal, sleep = sleepWithAbort }) { for (let retryAttempt = 1; retryAttempt <= CONCAVE_DEV_STARTUP_MAX_ATTEMPTS; retryAttempt += 1) { if (signal?.aborted) return; try { await runTask(); return; } catch (error) { if (retryAttempt >= CONCAVE_DEV_STARTUP_MAX_ATTEMPTS) throw error; await sleep(resolveDevStartupRetryDelayMs(retryAttempt), signal); } } } function filterDevStartupLine(rawLine) { const line = stripConvexCommandNoise(rawLine).trim(); if (!line) return { kind: "skip" }; if (DEV_SUPPRESSED_LINE_PATTERNS.some((pattern) => pattern.test(line))) return { kind: "skip" }; if (line.includes("Finished running function \"init\"") || line.includes("Convex AI files are not installed.") || line.includes("Preparing Convex functions...") || line.includes("Bundling component schemas and implementations") || line.includes("Uploading functions to Convex")) return { kind: "skip" }; if (DEV_READY_LINE_RE.test(line)) return { kind: "ready", message: line.toLowerCase().includes("concave") ? "Concave ready" : "Convex ready" }; return { kind: "pass", line }; } function isDevOutputProcessLike(value) { return typeof value === "object" && value !== null; } function observeDevProcessOutput(child, mode) { if (!isDevOutputProcessLike(child)) return Promise.resolve(true); if (!child.stdout && !child.stderr) return Promise.resolve(true); let settled = false; let readyLogged = false; let processExited = false; let openStreamCount = 0; return new Promise((resolve) => { const settle = (ready) => { if (settled) return; settled = true; resolve(ready); }; const maybeSettleAfterExit = () => { if (processExited && openStreamCount === 0) settle(false); }; const attach = (stream, sink) => { if (!stream) return; openStreamCount += 1; let pending = ""; let closed = false; const flushLine = (line) => { if (mode === "raw") { const normalizedLine = line.replaceAll("\r", ""); if (DEV_SUPPRESSED_LINE_PATTERNS.some((pattern) => pattern.test(normalizedLine))) return; if (DEV_READY_LINE_RE.test(normalizedLine)) settle(true); sink.write(normalizedLine.endsWith("\n") ? normalizedLine : `${normalizedLine}\n`); return; } const filtered = filterDevStartupLine(line); if (filtered.kind === "skip") return; if (filtered.kind === "ready") { if (!readyLogged) { readyLogged = true; logger.success(filtered.message); } settle(true); return; } sink.write(filtered.line.endsWith("\n") ? filtered.line : `${filtered.line}\n`); }; const flushPendingAndClose = () => { if (closed) return; closed = true; if (pending.length > 0) { flushLine(pending); pending = ""; } openStreamCount -= 1; maybeSettleAfterExit(); }; stream.on("data", (chunk) => { pending += String(chunk).replaceAll("\r", ""); const lines = pending.split("\n"); pending = lines.pop() ?? ""; for (const line of lines) flushLine(line); }); stream.on("end", flushPendingAndClose); stream.on("close", flushPendingAndClose); }; attach(child.stdout, process.stdout); attach(child.stderr, process.stderr); if (typeof child.then === "function") { child.then(() => { processExited = true; maybeSettleAfterExit(); }, () => { processExited = true; maybeSettleAfterExit(); }); return; } processExited = true; maybeSettleAfterExit(); }); } function extractDevBootstrapCliFlag(args) { let bootstrap = false; const remainingArgs = []; for (const arg of args) { if (arg === DEV_BOOTSTRAP_FLAG) { bootstrap = true; continue; } remainingArgs.push(arg); } return { bootstrap, remainingArgs }; } function hasDevArg(args, flag) { return args.some((arg) => arg === flag || arg.startsWith(`${flag}=`)); } function applyConvexBootstrapDevArgs(args) { const nextArgs = [...args]; if (!hasDevArg(nextArgs, "--once")) nextArgs.push("--once"); if (!hasDevArg(nextArgs, DEV_BOOTSTRAP_TYPECHECK_FLAG)) nextArgs.push(DEV_BOOTSTRAP_TYPECHECK_FLAG, DEV_BOOTSTRAP_TYPECHECK_MODE); return nextArgs; } function killProcessIfRunning(process, signal = "SIGTERM") { if (typeof process === "object" && process !== null && "killed" in process && !process.killed && "kill" in process && typeof process.kill === "function") process.kill(signal); } async function runLocalConvexBootstrap({ authSyncMode = "complete", config, debug, devArgs = [], execaFn, generateMetaFn, realConcavePath, realConvexPath, sharedDir, skipGenerateMeta = false, syncEnvFn, targetArgs }) { if (!debug) logger.info("Bootstrapping local Convex..."); const backendAdapter = createBackendAdapter({ backend: "convex", realConvexPath, realConcavePath }); const trimSegments = resolveCodegenTrimSegments(config); const localConvexEnvPath = join(process.cwd(), config.paths.lib, "..", ".env"); const authEnvState = resolveAuthEnvState({ cwd: process.cwd(), sharedDir }); const localNodeEnvOverrides = await resolveSupportedLocalNodeEnvOverrides({ execaFn }); const convexInitResult = await runConvexInitIfNeeded({ execaFn, backendAdapter, echoOutput: false, env: localNodeEnvOverrides, targetArgs }); if (convexInitResult.exitCode !== 0) return convexInitResult.exitCode; if (fs.existsSync(localConvexEnvPath) || authEnvState.installed) await syncEnvFn({ authSyncMode: authEnvState.installed ? "prepare" : "skip", force: true, sharedDir, silent: true, targetArgs }); if (!skipGenerateMeta) await generateMetaFn(sharedDir, { debug, silent: true, scope: "all", trimSegments }); const bootstrapProcess = execaFn(backendAdapter.command, [ ...backendAdapter.argsPrefix, "dev", ...applyConvexBootstrapDevArgs(devArgs) ], { stdio: "pipe", cwd: process.cwd(), env: createBackendCommandEnv(localNodeEnvOverrides), reject: false }); const backendReadyPromise = observeDevProcessOutput(bootstrapProcess, debug ? "raw" : "filtered"); const abortController = new AbortController(); const authEnvSyncPromise = authEnvState.installed ? (async () => { if (!await backendReadyPromise || abortController.signal.aborted) return; await runDevAuthEnvSyncLoop({ signal: abortController.signal, runTask: () => syncEnvFn({ authSyncMode, force: true, sharedDir, silent: true, targetArgs }) }); })() : null; const migrationPromise = config.dev.migrations.enabled !== "off" ? (async () => { try { if (!await backendReadyPromise || abortController.signal.aborted) return; if (await runDevStartupRetryLoop({ backend: "convex", label: "migration up", signal: abortController.signal, runTask: () => runMigrationFlow({ execaFn, backendAdapter, migrationConfig: config.dev.migrations, targetArgs, signal: abortController.signal, context: "dev", direction: "up" }) }) !== 0 && !abortController.signal.aborted) logger.warn("⚠️ migration up failed in bootstrap (continuing without blocking)."); } catch (error) { if (!abortController.signal.aborted) logger.warn(`⚠️ migration up errored in bootstrap: ${error.message}`); } })() : null; const backfillPromise = config.dev.aggregateBackfill.enabled !== "off" ? (async () => { try { if (!await backendReadyPromise || abortController.signal.aborted) return; if (await runDevStartupRetryLoop({ backend: "convex", label: "aggregateBackfill kickoff", signal: abortController.signal, runTask: () => runAggregateBackfillFlow({ execaFn, backendAdapter, backfillConfig: config.dev.aggregateBackfill, mode: "resume", targetArgs, signal: abortController.signal, context: "dev" }) }) !== 0 && !abortController.signal.aborted) logger.warn("⚠️ aggregateBackfill kickoff failed in bootstrap (continuing without blocking)."); } catch (error) { if (!abortController.signal.aborted) logger.warn(`⚠️ aggregateBackfill kickoff errored in bootstrap: ${error.message}`); } })() : null; try { const result = await bootstrapProcess; await backendReadyPromise; if (authEnvSyncPromise) await authEnvSyncPromise; await migrationPromise; await backfillPromise; return result.exitCode ?? 0; } finally { abortController.abort(); killProcessIfRunning(bootstrapProcess); } } function applyConvexDevPreRunArgs(args, preRunFunction) { if (!preRunFunction) return args; return [ "--run", preRunFunction, ...args ]; } async function runDevStartupRetryLoop({ backend, label, runTask, signal, sleep = sleepWithAbort, logger: retryLogger = logger }) { const maxAttempts = backend === "concave" ? CONCAVE_DEV_STARTUP_MAX_ATTEMPTS : 1; let attempt = 1; while (true) { try { const exitCode = await runTask(); if (exitCode === 0 || attempt >= maxAttempts || signal?.aborted) return exitCode; } catch (error) { if (attempt >= maxAttempts || signal?.aborted) throw error; } attempt += 1; retryLogger.info(`↻ ${label} retry ${attempt}/${maxAttempts}`); await sleep(resolveDevStartupRetryDelayMs(attempt - 1), signal); } } function resolveConcaveLocalSiteUrl(cwd = process.cwd()) { const envLocalPath = join(cwd, ".env.local"); if (!fs.existsSync(envLocalPath)) return "http://localhost:3000"; const parsed = parseEnv(fs.readFileSync(envLocalPath, "utf8")); return parsed.NEXT_PUBLIC_SITE_URL ?? parsed.EXPO_PUBLIC_SITE_URL ?? parsed.VITE_SITE_URL ?? "http://localhost:3000"; } function resolveImplicitConvexRemoteDeploymentEnv(cwd = process.cwd()) { const envLocalPath = join(cwd, ".env.local"); if (!fs.existsSync(envLocalPath)) return null; const parsed = parseEnv(fs.readFileSync(envLocalPath, "utf8")); if (!hasRemoteConvexDeploymentEnv(parsed)) return null; return { CONVEX_DEPLOYMENT: parsed.CONVEX_DEPLOYMENT, CONVEX_DEPLOY_KEY: parsed.CONVEX_DEPLOY_KEY, CONVEX_SELF_HOSTED_URL: parsed.CONVEX_SELF_HOSTED_URL, CONVEX_SELF_HOSTED_ADMIN_KEY: parsed.CONVEX_SELF_HOSTED_ADMIN_KEY }; } function resolveConvexEnvFileCommandEnv(args, cwd = process.cwd()) { let envFilePath = null; for (let i = 0; i < args.length; i += 1) { const arg = args[i]; if (arg === "--env-file") { envFilePath = args[i + 1] ?? null; break; } if (arg.startsWith("--env-file=")) { envFilePath = arg.slice(11) || null; break; } } if (!envFilePath) return null; const resolvedPath = resolve(cwd, envFilePath); if (!fs.existsSync(resolvedPath)) return null; const parsed = parseEnv(fs.readFileSync(resolvedPath, "utf8")); return { CONVEX_DEPLOYMENT: parsed.CONVEX_DEPLOYMENT, CONVEX_DEPLOY_KEY: parsed.CONVEX_DEPLOY_KEY, CONVEX_SELF_HOSTED_URL: parsed.CONVEX_SELF_HOSTED_URL, CONVEX_SELF_HOSTED_ADMIN_KEY: parsed.CONVEX_SELF_HOSTED_ADMIN_KEY }; } function stripConvexEnvFileTargetArgs(args) { const nextArgs = []; let skipNext = false; for (const arg of args) { if (skipNext) { skipNext = false; continue; } if (arg === "--env-file") { skipNext = true; continue; } if (arg.startsWith("--env-file=")) continue; nextArgs.push(arg); } return nextArgs; } const resolveWatcherCommand = (currentFilename = __filename$1, currentDir = __dirname) => { const isTs = currentFilename.endsWith(".ts"); return { runtime: isTs ? "bun" : "node", watcherPath: isTs ? join(currentDir, "..", "watcher.ts") : join(currentDir, "watcher.mjs") }; }; const LEADING_NODE_VERSION_PREFIX_RE = /^v/; function parseNodeMajor(version) { if (!version) return null; const majorText = version.trim().replace(LEADING_NODE_VERSION_PREFIX_RE, "").split(".")[0]; const major = Number.parseInt(majorText, 10); return Number.isFinite(major) ? major : null; } function prependPathEntry(currentPath, entry) { const normalizedEntry = resolve(entry); return [entry, ...(currentPath ?? "").split(delimiter).filter((segment) => segment.length > 0).filter((segment) => resolve(segment) !== normalizedEntry)].join(delimiter); } async function resolveSupportedLocalNodeEnvOverrides({ cwd = process.cwd(), currentNodeVersion = process.version, env, execaFn, runtimeName = process.release?.name ?? "node" }) { if (runtimeName !== "node") return {}; const currentMajor = parseNodeMajor(currentNodeVersion); if (currentMajor !== null && SUPPORTED_LOCAL_CONVEX_NODE_MAJORS.has(currentMajor)) return {}; const baseEnv = createCommandEnv(env); const whichResult = await execaFn("which", ["-a", "node"], { cwd, env: baseEnv, reject: false, stdio: "pipe" }); if ((whichResult.exitCode ?? 0) !== 0) return {}; const candidates = [...new Set((whichResult.stdout ?? "").split(LINE_SPLIT_RE).map((line) => line.trim()).filter((line) => line.length > 0))]; for (const candidate of candidates) { const versionResult = await execaFn(candidate, ["-p", "process.versions.node"], { cwd, env: baseEnv, reject: false, stdio: "pipe" }); if ((versionResult.exitCode ?? 0) !== 0) continue; const candidateMajor = parseNodeMajor(versionResult.stdout ?? ""); if (candidateMajor !== null && SUPPORTED_LOCAL_CONVEX_NODE_MAJORS.has(candidateMajor)) return { PATH: prependPathEntry(baseEnv.PATH, dirname(candidate)) }; } return {}; } function readRequestBody(request) { return (async () => { const chunks = []; for await (const chunk of request) chunks.push(Buffer.from(chunk)); const body = Buffer.concat(chunks); return body.buffer.slice(body.byteOffset, body.byteOffset + body.byteLength); })(); } function normalizeConcaveDevUrlArg(args) { for (let i = 0; i < args.length; i += 1) { const arg = args[i]; const inlineValue = arg.startsWith("--url=") ? arg.slice(6) : void 0; const nextValue = arg === "--url" ? args[i + 1] : void 0; const urlValue = inlineValue ?? nextValue; if (!urlValue) continue; const url = new URL(urlValue); const port = url.port || (url.protocol === "https:" ? String(443) : String(80)); const backendArgs = [...args]; if (inlineValue !== void 0) backendArgs.splice(i, 1, `--port=${port}`); else backendArgs.splice(i, 2, "--port", port); return { backendArgs, targetArgs: extractConcaveRunTargetArgs(args) }; } return null; } function resolveConcaveLocalDevContract(args, frontendSiteUrl = "http://localhost:3000") { const explicitTargetArgs = extractConcaveRunTargetArgs(args); if (explicitTargetArgs.length > 0) return normalizeConcaveDevUrlArg(args) ?? { backendArgs: args, targetArgs: explicitTargetArgs }; return { backendArgs: args, targetArgs: ["--url", LOCAL_CONCAVE_DEV_URL], backendEnv: { CONVEX_SITE_URL: LOCAL_CONCAVE_SITE_URL, SITE_URL: frontendSiteUrl }, siteProxy: { listenHost: LOCAL_CONCAVE_HOST, listenPort: LOCAL_CONCAVE_SITE_PORT, targetOrigin: LOCAL_CONCAVE_DEV_URL } }; } async function startLocalSiteProxy(options) { const server = createServer(async (request, response) => { try { const targetUrl = new URL(request.url ?? "/", options.targetOrigin); const headers = new Headers(); for (const [key, value] of Object.entries(request.headers)) { if (value === void 0 || key === "host" || key === "connection" || key === "content-length") continue; if (Array.isArray(value)) { for (const entry of value) headers.append(key, entry); continue; } headers.set(key, value); } const body = request.method === "GET" || request.method === "HEAD" ? void 0 : await readRequestBody(request); const upstream = await fetch(targetUrl, { method: request.method, headers, body }); response.statusCode = upstream.status; for (const [key, value] of upstream.headers) { if (key === "connection" || key === "transfer-encoding") continue; response.setHeader(key, value); } response.end(Buffer.from(await upstream.arrayBuffer())); } catch (error) { response.statusCode = 502; response.end(`Local site proxy error: ${error.message}`); } }); await new Promise((resolve, reject) => { server.once("error", reject); server.listen(options.listenPort, options.listenHost, () => { server.off("error", reject); resolve(); }); }); logger.info(`concave site proxy ready at http://${options.listenHost}:${options.listenPort}`); const proxyHandle = { killed: false, kill() { if (proxyHandle.killed) return; proxyHandle.killed = true; new Promise((resolve) => { server.close(() => resolve(void 0)); }); } }; return proxyHandle; } const DEV_HELP_TEXT = `Usage: kitcn dev [options] Options: --api <dir> Output directory (default from config) --backend <convex|concave> Backend CLI to drive --bootstrap Run one-shot local Convex bootstrap and exit --config <path> Config path override --backfill=auto|on|off Dev aggregate backfill mode toggle --backfill-wait Wait for aggregate backfill completion --no-backfill-wait Skip waiting for aggregate backfill --migrations=auto|on|off Dev migration mode toggle --migrations-wait Wait for migration completion --no-migrations-wait Skip waiting for migration completion`; const handleDevCommand = async (argv, deps) => { const parsed = parseArgs(argv); if (HELP_FLAGS$11.has(argv[0] ?? "") || HELP_FLAGS$11.has(parsed.restArgs[0] ?? "")) { logger.write(DEV_HELP_TEXT); return 0; } if (parsed.scope) throw new Error("`--scope` is not supported for `kitcn dev`. Use `kitcn codegen --scope <all|auth|orm>` for scoped generation."); const { execa: execaFn, generateMeta: generateMetaFn, getConvexConfig: getConvexConfigFn, loadCliConfig: loadCliConfigFn, ensureConvexGitignoreEntry: ensureConvexGitignoreEntryFn, enableDevSchemaWatch, syncEnv: syncEnvFn, realConvex: realConvexPath, realConcave: realConcavePath } = resolveRunDeps(deps); const startLocalSiteProxyFn = deps?.startLocalSiteProxy ?? startLocalSiteProxy; const resolveSupportedLocalNodeEnvOverridesFn = deps?.resolveSupportedLocalNodeEnvOverrides ?? resolveSupportedLocalNodeEnvOverrides; assertNoRemovedDevPreRunFlag(argv); const config = loadCliConfigFn(parsed.configPath); const backend = resolveConfiguredBackend({ backendArg: parsed.backend, config }); const { remainingArgs: devArgsWithoutMigrationFlags, overrides: devMigrationOverrides } = extractMigrationCliOptions(parsed.convexArgs); const { remainingArgs: devCommandArgs, overrides: devBackfillOverrides } = extractBackfillCliOptions(devArgsWithoutMigrationFlags); const sharedDir = parsed.sharedDir ?? config.paths.shared; const debug = parsed.debug || config.dev.debug; assertNoRemovedDevPreRunFlag(config.dev.args); const { bootstrap, remainingArgs: convexDevArgs } = extractDevBootstrapCliFlag([...config.dev.args, ...devCommandArgs]); const explicitConvexTargetArgs = extractBackendRunTargetArgs("convex", convexDevArgs); const explicitConvexCommandEnv = resolveConvexEnvFileCommandEnv(convexDevArgs); const implicitConvexCommandEnv = backend === "convex" && explicitConvexTargetArgs.length === 0 && explicitConvexCommandEnv === null ? resolveImplicitConvexRemoteDeploymentEnv() : null; const effectiveConvexCommandEnv = explicitConvexCommandEnv ?? implicitConvexCommandEnv; const preRunFunction = config.dev.preRun; if (bootstrap && backend !== "convex") throw new Error("`kitcn dev --bootstrap` is only supported for backend convex."); if (preRunFunction && backend === "concave") throw new Error("`dev.preRun` is only supported for backend convex. Concave dev has no equivalent `--run` flow."); if (preRunFunction && convexDevArgs.some((arg) => isConvexDevPreRunConflictFlag(arg))) throw new Error("`dev.preRun` cannot be combined with Convex dev run flags (`--run`, `--start`, `--run-sh`, `--run-component`)."); const backendAdapter = createBackendAdapter({ backend, realConvexPath, realConcavePath }); const devBackfillConfig = resolveBackfillConfig(config.dev.aggregateBackfill, devBackfillOverrides); const devMigrationConfig = resolveMigrationConfig(config.dev.migrations, devMigrationOverrides); const { functionsDir } = getConvexConfigFn(sharedDir); const schemaPath = join(functionsDir, "schema.ts"); const concaveLocalDevContract = backend === "concave" ? resolveConcaveLocalDevContract(convexDevArgs, (deps?.resolveConcaveLocalSiteUrl ?? resolveConcaveLocalSiteUrl)(process.cwd())) : null; const backendDevArgs = concaveLocalDevContract?.backendArgs ?? applyConvexDevPreRunArgs(convexDevArgs, preRunFunction); const backendOutputMode = debug || backend === "convex" && !hasDevArg(backendDevArgs, "--once") ? "raw" : "filtered"; const targetArgs = concaveLocalDevContract?.targetArgs ?? stripConvexEnvFileTargetArgs(extractBackendRunTargetArgs(backend, convexDevArgs)); const trimSegments = resolveCodegenTrimSegments(config); const localNodeEnvOverrides = backend === "convex" ? await resolveSupportedLocalNodeEnvOverridesFn({ execaFn }) : {}; if (!bootstrap && backend === "convex" && !debug && getAggregateBackfillDeploymentKey(targetArgs, process.cwd(), effectiveConvexCommandEnv ?? void 0) === "local") logger.info("Bootstrapping local Convex..."); if (bootstrap) return runLocalConvexBootstrap({ authSyncMode: "complete", config, debug, devArgs: backendDevArgs, execaFn, generateMetaFn, realConvexPath, realConcavePath, sharedDir, syncEnvFn, targetArgs }); if (!deps) try { ensureConvexGitignoreEntryFn(process.cwd()); } catch (error) { logger.warn(`⚠️ Failed to ensure .convex/ and .concave/ are ignored in .gitignore: ${error.message}`); } const convexInitResult = await runConvexInitIfNeeded({ execaFn, backendAdapter, echoOutput: false, env: { ...localNodeEnvOverrides, ...effectiveConvexCommandEnv }, targetArgs }); if (convexInitResult.exitCode !== 0) return convexInitResult.exitCode; const localConvexEnvPath = join(process.cwd(), config.paths.lib, "..", ".env"); const authEnvState = resolveAuthEnvState({ cwd: process.cwd(), sharedDir }); if (backend === "convex" && (fs.existsSync(localConvexEnvPath) || authEnvState.installed)) await syncEnvFn({ authSyncMode: authEnvState.installed ? "prepare" : "skip", commandEnv: effectiveConvexCommandEnv ?? void 0, force: true, sharedDir, silent: true, targetArgs }); await withLocalCodegenEnv(sharedDir, backend, async () => { await generateMetaFn(sharedDir, { debug, silent: true, scope: "all", trimSegments }); }); const { runtime, watcherPath } = resolveWatcherCommand(); const watcherProcess = execaFn(runtime, [watcherPath], { stdio: "inherit", cwd: process.cwd(), env: { ...createCommandEnv(localNodeEnvOverrides), KITCN_BACKEND: backend, KITCN_API_OUTPUT_DIR: sharedDir || "", ...parsed.configPath ? { KITCN_CONFIG_PATH: parsed.configPath } : {}, KITCN_DEBUG: debug ? "1" : "", KITCN_CODEGEN_SCOPE: "all", KITCN_CODEGEN_TRIM_SEGMENTS: JSON.stringify(trimSegments) } }); trackProcess(watcherProcess); const siteProxy = concaveLocalDevContract?.siteProxy ? await startLocalSiteProxyFn(concaveLocalDevContract.siteProxy) : null; if (siteProxy) trackProcess(siteProxy); const backendProcess = execaFn(backendAdapter.command, [ ...backendAdapter.argsPrefix, "dev", ...backendDevArgs ], { stdio: "pipe", cwd: process.cwd(), env: createBackendCommandEnv({ ...localNodeEnvOverrides, ...effectiveConvexCommandEnv, ...concaveLocalDevContract?.backendEnv }), reject: false }); const backendReadyPromise = observeDevProcessOutput(backendProcess, backendOutputMode); trackProcess(backendProcess); const backfillAbortController = new AbortController(); const authEnvSyncPromise = backend === "convex" && authEnvState.installed ? (async () => { if (!await backendReadyPromise || backfillAbortController.signal.aborted) return; await runDevAuthEnvSyncLoop({ signal: backfillAbortController.signal, runTask: () => syncEnvFn({ authSyncMode: "complete", commandEnv: effectiveConvexCommandEnv ?? void 0, force: true, sharedDir, silent: true, targetArgs }) }); })() : null; const authEnvSyncFailurePromise = authEnvSyncPromise ? authEnvSyncPromise.then(() => new Promise(() => {})) : new Promise(() => {}); let envWatcher = null; let schemaWatcher = null; let envDebounceTimer = null; let schemaDebounceTimer = null; let schemaBackfillInFlight = null; let schemaBackfillQueued = false; let lastSyncedLocalEnvSnapshot = readWatchedFileSnapshot(localConvexEnvPath); const maybeRunSchemaBackfill = async () => { try { if (await runDevSchemaBackfillIfNeeded({ execaFn, backendAdapter, backfillConfig: devBackfillConfig, functionsDir, env: effectiveConvexCommandEnv ?? void 0, targetArgs, signal: backfillAbortController.signal }) !== 0 && !backfillAbortController.signal.aborted) logger.warn("⚠️ aggregateBackfill on schema update failed in dev (continuing without blocking)."); } catch (error) { if (!backfillAbortController.signal.aborted) logger.warn(`⚠️ aggregateBackfill on schema update errored in dev: ${error.message}`); } }; const queueSchemaBackfill = () => { if (backfillAbortController.signal.aborted) return; schemaBackfillQueued = true; if (schemaBackfillInFlight) return; schemaBackfillInFlight = (async () => { while (schemaBackfillQueued && !backfillAbortController.signal.aborted) { schemaBackfillQueued = false; await maybeRunSchemaBackfill(); } })().finally(() => { schemaBackfillInFlight = null; }); }; const syncWatchedLocalEnv = async () => { const currentSnapshot = readWatchedFileSnapshot(localConvexEnvPath); if (backfillAbortController.signal.aborted || currentSnapshot === lastSyncedLocalEnvSnapshot) return; try { await authEnvSyncPromise; await syncEnvFn({ authSyncMode: "auto", commandEnv: effectiveConvexCommandEnv ?? void 0, force: true, sharedDir, silent: true, targetArgs }); lastSyncedLocalEnvSnapshot = readWatchedFileSnapshot(localConvexEnvPath); } catch (error) { if (!backfillAbortController.signal.aborted) logger.warn(`⚠️ env push on convex/.env update failed in dev: ${error.message}`); } }; const queueLocalEnvSync = () => { if (backfillAbortController.signal.aborted) return; if (envDebounceTimer) clearTimeout(envDebounceTimer); envDebounceTimer = setTimeout(() => { syncWatchedLocalEnv(); }, DEV_FILE_WATCH_DEBOUNCE_MS); }; if (devMigrationConfig.enabled !== "off") (async () => { try { if (!await backendReadyPromise || backfillAbortController.signal.aborted) return; if (await runDevStartupRetryLoop({ backend, label: "migration up", signal: backfillAbortController.signal, runTask: () => runMigrationFlow({ execaFn, backendAdapter, env: effectiveConvexCommandEnv ?? void 0, migrationConfig: devMigrationConfig, targetArgs, signal: backfillAbortController.signal, context: "dev", direction: "up" }) }) !== 0 && !backfillAbortController.signal.aborted) logger.warn("⚠️ migration up failed in dev (continuing without blocking)."); } catch (error) { if (!backfillAbortController.signal.aborted) logger.warn(`⚠️ migration up errored in dev: ${error.message}`); } })(); if (devBackfillConfig.enabled !== "off") (async () => { try { if (!await backendReadyPromise || backfillAbortController.signal.aborted) return; if (await runDevStartupRetryLoop({ backend, label: "aggregateBackfill kickoff", signal: backfillAbortController.signal, runTask: () => runAggregateBackfillFlow({ execaFn, backendAdapter, backfillConfig: devBackfillConfig, env: effectiveConvexCommandEnv ?? void 0, mode: "resume", targetArgs, signal: backfillAbortController.signal, context: "dev" }) }) !== 0 && !backfillAbortController.signal.aborted) logger.warn("⚠️ aggregateBackfill kickoff failed in dev (continuing without blocking)."); } catch (error) { if (!backfillAbortController.signal.aborted) logger.warn(`⚠️ aggregateBackfill kickoff errored in dev: ${error.message}`); } })(); if (backend === "convex" && !convexDevArgs.includes("--once")) { const { watch } = await import("chokidar"); const watchedEnv = watch(localConvexEnvPath, { ignoreInitial: true }); envWatcher = watchedEnv; watchedEnv.on("add", queueLocalEnvSync).on("change", queueLocalEnvSync).on("error", (error) => { if (!backfillAbortController.signal.aborted) logger.warn(`⚠️ convex/.env watch error: ${error.message}`); }); } if (enableDevSchemaWatch && devBackfillConfig.enabled !== "off" && fs.existsSync(schemaPath)) { const { watch } = await import("chokidar"); const watchedSchema = watch(schemaPath, { ignoreInitial: true }); schemaWatcher = watchedSchema; watchedSchema.on("change", () => { if (schemaDebounceTimer) clearTimeout(schemaDebounceTimer); schemaDebounceTimer = setTimeout(() => { queueSchemaBackfill(); }, DEV_FILE_WATCH_DEBOUNCE_MS); }).on("error", (error) => { if (!backfillAbortController.signal.aborted) logger.warn(`⚠️ schema watch error (aggregate backfill): ${error.message}`); }); } process.on("exit", cleanup); process.on("SIGINT", () => { backfillAbortController.abort(); if (envDebounceTimer) clearTimeout(envDebounceTimer); if (schemaDebounceTimer) clearTimeout(schemaDebounceTimer); envWatcher?.close(); schemaWatcher?.close(); cleanup(); process.exit(0); }); process.on("SIGTERM", () => { backfillAbortController.abort(); if (envDebounceTimer) clearTimeout(envDebounceTimer); if (schemaDebounceTimer) clearTimeout(schemaDebounceTimer); envWatcher?.close(); schemaWatcher?.close(); cleanup(); process.exit(0); }); try { const result = await Promise.race([ watcherProcess.then((value) => ({ source: "watcher", exitCode: value.exitCode ?? 0 }), () => ({ source: "watcher", exitCode: 1 })), backendProcess.then((value) => ({ source: "backend", exitCode: value.exitCode ?? 0 }), () => ({ source: "backend", exitCode: 1 })), authEnvSyncFailurePromise ]); if (authEnvSyncPromise && convexDevArgs.includes("--once")) await authEnvSyncPromise; if (result.source === "backend") await backendReadyPromise; return result.exitCode ?? 0; } finally { backfillAbortController.abort(); if (envDebounceTimer) clearTimeout(envDebounceTimer); if (schemaDebounceTimer) clearTimeout(schemaDebounceTimer); await envWatcher?.close(); await schemaWatcher?.close(); cleanup(); } }; //#endregion //#region src/cli/commands/add.ts const HELP_FLAGS$10 = new Set(["--help", "-h"]); const RAW_CONVEX_AUTH_PRESET = "convex"; const AUTH_SCHEMA_ONLY_SCOPE = "schema"; const AUTH_SCHEMA_FLAG = "--schema"; const RAW_CONVEX_AUTH_DEPLOYMENT_ERROR = "Raw Convex auth adoption requires an initialized Convex deployment. Run `convex init` first, then re-run `kitcn add auth --preset convex`."; const AUTH_SCHEMA_ONLY_MISSING_ERROR = "Auth schema sync requires the default kitcn auth scaffold to already exist. If auth is not scaffolded yet, run `kitcn add auth --yes` once."; const AUTH_SCHEMA_ONLY_RAW_CONVEX_ERROR = "Auth schema sync is only supported for the default kitcn auth scaffold. Re-run `kitcn add auth --preset convex --yes` for raw Convex auth."; const AUTH_SCHEMA_OVERWRITE_ERROR = "Auth schema sync is additive. Do not pass `--overwrite`; use `kitcn add auth --sche