kitcn
Version:
kitcn - React Query integration and CLI tools for Convex
1,181 lines (1,176 loc) • 112 kB
JavaScript
#!/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