feat: implement two-phase review flow for config updates

- Introduced a new API route for committing changes after user review, allowing the agent to keep the sandbox alive for inspection before finalizing updates.
- Enhanced the existing applyConfigUpdate function to transition to an awaiting review state, storing the diff for user visibility.
- Added progress tracking and stage reporting for the config agent run, improving user feedback during the update process.
- Updated the dashboard to reflect the new review stages and provide a more interactive experience for managing configuration changes.

Co-Authored-By: mantra <mantra@stack-auth.com>
This commit is contained in:
mantrakp04 2026-06-25 17:12:42 -07:00
parent f6e121f816
commit 2558a63a81
16 changed files with 1190 additions and 570 deletions

View File

@ -2,9 +2,10 @@
* End-to-end smoke test for the config-update repo agent against a REAL Vercel
* Sandbox and a REAL GitHub repo. This is a scratch script, not a unit test.
*
* It runs the single-phase apply flow: boot a sandbox (warm from the shared base
* It runs the two-phase review flow: boot a sandbox (warm from the shared base
* snapshot if STACK_CONFIG_AGENT_BASE_SNAPSHOT_ID is set, else cold-install the
* agent SDK), clone the repo, agent edits the config, COMMIT + PUSH to the branch.
* agent SDK), clone the repo, agent edits the config, print the generated diff,
* then explicitly COMMIT + PUSH to the branch.
*
* WARNING: this pushes a commit to the target repo. Point SPIKE_OWNER,
* SPIKE_REPO, and SPIKE_BRANCH at a throwaway repo/branch.
@ -18,6 +19,7 @@
import { execFileSync } from "child_process";
import {
applyConfigUpdate,
commitConfigUpdate,
type GithubRepoRef,
} from "../src/lib/config/repo-agent";
@ -53,21 +55,27 @@ async function main() {
const getGithubToken = async () => githubToken();
log(`Target: ${REF.owner}/${REF.repo}@${REF.branch}`);
log("applyConfigUpdate (boot + clone + agent edit + push)…");
const t2 = Date.now();
log("applyConfigUpdate (boot + clone + agent edit)…");
const t2 = performance.now();
const result = await applyConfigUpdate({
getGithubToken,
ref: REF,
completeConfig: COMPLETE_CONFIG,
commitMessage: "chore(hexclave): e2e smoke — set auth.allowSignUp=false",
});
log(`Done in ${((Date.now() - t2) / 1000).toFixed(0)}s`);
log(`Done in ${((performance.now() - t2) / 1000).toFixed(0)}s`);
log(`Result: ${JSON.stringify(result)}`);
if (result.mode === "commit-to-branch") {
log(`✅ Pushed: ${result.commitUrl}`);
} else {
if (result.mode === "no-change") {
log("⚠️ Agent produced no change (config already matched).");
} else {
log(`Review diff has ${result.diff.length} characters. Committing reviewed changes…`);
const commit = await commitConfigUpdate({
sandboxId: result.sandboxId,
getGithubToken,
ref: REF,
commitMessage: "chore(hexclave): e2e smoke — set auth.allowSignUp=false",
});
log(`✅ Pushed: ${commit.commitUrl}`);
}
}

View File

@ -4,9 +4,11 @@ import {
recordConfigAgentRunProgress,
recordConfigAgentRunResult,
recordConfigAgentRunSandbox,
recordConfigAgentRunStage,
setConfigAgentRunAwaitingReview,
tryStartConfigAgentRun,
} from "@/lib/config";
import { applyConfigUpdate, type GithubRepoRef } from "@/lib/config/repo-agent";
import { applyConfigUpdate, stopConfigAgentSandbox, type GithubRepoRef } from "@/lib/config/repo-agent";
import { createSmartRouteHandler } from "@/route-handlers/smart-route-handler";
import { runAsynchronouslyAndWaitUntil } from "@/utils/background-tasks";
import type { EnvironmentConfigOverrideOverride } from "@hexclave/shared/dist/config/schema";
@ -107,6 +109,10 @@ export const POST = createSmartRouteHandler({
const onProgress = async (activity: string) => {
await recordConfigAgentRunProgress({ projectId, branchId, progress: activity });
};
// Stage updates drive the dashboard progress bar.
const onStage = async (stage: "initializing_sandbox" | "cloning_repo" | "agent_making_changes") => {
await recordConfigAgentRunStage({ projectId, branchId, stage });
};
runAsynchronouslyAndWaitUntil(async () => {
try {
@ -115,25 +121,32 @@ export const POST = createSmartRouteHandler({
// computed from the current branch override merged with this change.
const completeConfig = await getCompleteBranchConfigForFile({ projectId, branchId, configUpdate });
// Boot a sandbox (warm from the shared base snapshot if configured, else
// cold-install the agent SDK), clone the repo fresh, edit + commit + push.
// Boot a sandbox, clone the repo, run the agent. The sandbox stays alive
// in `awaiting_review` so the user can inspect the diff before committing.
const result = await applyConfigUpdate({
getGithubToken,
ref,
completeConfig,
commitMessage,
onSandboxId,
onStage,
onProgress,
});
await recordConfigAgentRunResult({
projectId,
branchId,
nowMs: Date.now(),
outcome: result.mode === "commit-to-branch"
? { status: "success", commitUrl: result.commitUrl, newCommitHash: result.commitSha }
: { status: "no-change" },
});
if (result.mode === "no-change") {
await recordConfigAgentRunResult({
projectId,
branchId,
nowMs: Date.now(),
outcome: { status: "no-change" },
});
} else {
// Transition to awaiting_review — sandbox stays alive.
await setConfigAgentRunAwaitingReview({
projectId,
branchId,
diff: result.diff,
});
}
} catch (error) {
captureError("config-github-apply", error);
await recordConfigAgentRunResult({

View File

@ -0,0 +1,99 @@
import {
getBranchConfigOverrideSource,
recordConfigAgentRunResult,
} from "@/lib/config";
import { commitConfigUpdate, stopConfigAgentSandbox, type GithubRepoRef } from "@/lib/config/repo-agent";
import { createSmartRouteHandler } from "@/route-handlers/smart-route-handler";
import { runAsynchronouslyAndWaitUntil } from "@/utils/background-tasks";
import { adaptSchema, adminAuthTypeSchema, yupNumber, yupObject, yupString } from "@hexclave/shared/dist/schema-fields";
import { StatusError, captureError } from "@hexclave/shared/dist/utils/errors";
// The commit+push itself is fast (~10 s) but we give generous room for sandbox
// reconnect latency and slow GitHub API responses.
export const maxDuration = 120;
/**
* Commits and pushes the agent's already-applied changes from an `awaiting_review`
* sandbox. The user explicitly triggered this after reviewing the diff in the
* dashboard. Returns immediately and does the work in the background.
*/
export const POST = createSmartRouteHandler({
metadata: {
summary: "Commit config agent changes to GitHub",
description: "Commits and pushes the agent's already-applied config changes after user review.",
tags: ["Config"],
hidden: true,
},
request: yupObject({
auth: yupObject({
type: adminAuthTypeSchema,
tenancy: adaptSchema,
}).defined(),
body: yupObject({
github_access_token: yupString().defined(),
commit_message: yupString().optional(),
}).defined(),
method: yupString().oneOf(["POST"]).defined(),
}),
response: yupObject({
statusCode: yupNumber().oneOf([200]).defined(),
bodyType: yupString().oneOf(["json"]).defined(),
body: yupObject({
status: yupString().oneOf(["committing", "not-awaiting-review", "sandbox-expired"]).defined(),
}).defined(),
}),
handler: async (req) => {
const projectId = req.auth.tenancy.project.id;
const branchId = req.auth.tenancy.branchId;
const source = await getBranchConfigOverrideSource({ projectId, branchId });
if (source.type !== "pushed-from-github") {
throw new StatusError(StatusError.BadRequest, "This project's configuration is not linked to a GitHub repository.");
}
const run = source.agent_run;
if (!run || run.status !== "awaiting_review") {
return { statusCode: 200, bodyType: "json", body: { status: "not-awaiting-review" } };
}
const sandboxId = run.sandbox_id;
if (!sandboxId) {
// Sandbox id was never recorded — can't commit. Treat as an error and clean up.
await recordConfigAgentRunResult({
projectId,
branchId,
nowMs: Date.now(),
outcome: { status: "error", error: "Sandbox session expired. Please retry the update." },
});
return { statusCode: 200, bodyType: "json", body: { status: "sandbox-expired" } };
}
const githubToken = req.body.github_access_token;
const commitMessage = req.body.commit_message?.trim() || "chore(hexclave): update config from dashboard";
const ref: GithubRepoRef = { owner: source.owner, repo: source.repo, branch: source.branch };
const getGithubToken = async () => githubToken;
runAsynchronouslyAndWaitUntil(async () => {
try {
const result = await commitConfigUpdate({ sandboxId, getGithubToken, ref, commitMessage });
await recordConfigAgentRunResult({
projectId,
branchId,
nowMs: Date.now(),
outcome: { status: "success", commitUrl: result.commitUrl, newCommitHash: result.commitSha },
});
} catch (error) {
captureError("config-github-commit", error);
await stopConfigAgentSandbox(sandboxId);
await recordConfigAgentRunResult({
projectId,
branchId,
nowMs: Date.now(),
outcome: { status: "error", error: "Failed to commit and push the config changes." },
}).catch((e) => captureError("config-github-commit-record-error", e));
}
});
return { statusCode: 200, bodyType: "json", body: { status: "committing" } };
},
});

View File

@ -494,7 +494,10 @@ export async function getCompleteBranchConfigForFile(options: {
}): Promise<Record<string, unknown>> {
const current = await rawQuery(globalPrismaClient, getBranchConfigOverrideQuery({ projectId: options.projectId, branchId: options.branchId }));
const merged = override(current as Config, options.configUpdate as Config);
return normalize(merged, { onDotIntoNonObject: "ignore" }) as Record<string, unknown>;
// Dashboard saves usually arrive as dot-notation deltas (for example
// `auth.allowSignUp: false`). The branch override can be empty, so missing
// parents must be materialized instead of silently dropping the pending edit.
return normalize(merged, { onDotIntoNonObject: "ignore", onDotIntoNull: "empty-object" }) as Record<string, unknown>;
}
/**
@ -587,6 +590,10 @@ export async function recordConfigAgentRunProgress(options: {
if (!source || source.type !== "pushed-from-github") return;
if (source.agent_run?.status !== "running") return;
const next: GithubConfigSource = {
// Stored cap (2000 chars): a coarse DB-size guard on the persisted feed.
// The runner already trims each line to 100 chars and keeps only the last
// ~6 lines (see buildRunnerScript), so this only bites pathological input;
// the two limits are independent on purpose (storage vs. in-sandbox feed).
...source,
agent_run: { ...source.agent_run, progress: options.progress.slice(0, 2000) },
};
@ -597,13 +604,80 @@ export async function recordConfigAgentRunProgress(options: {
});
}
/**
* Records the current stage of an in-flight run for the dashboard progress bar.
* No-ops unless a run is still `running`.
*/
export async function recordConfigAgentRunStage(options: {
projectId: string,
branchId: string,
stage: "initializing_sandbox" | "cloning_repo" | "agent_making_changes",
}): Promise<void> {
await retryTransaction(globalPrismaClient, async (tx) => {
const rows = await tx.$queryRaw<{ source: any }[]>`
SELECT "source" FROM "BranchConfigOverride"
WHERE "projectId" = ${options.projectId} AND "branchId" = ${options.branchId}
FOR UPDATE
`;
const source = rows[0]?.source;
if (!source || source.type !== "pushed-from-github") return;
if (source.agent_run?.status !== "running") return;
const next: GithubConfigSource = {
...source,
agent_run: { ...source.agent_run, stage: options.stage },
};
await tx.branchConfigOverride.update({
where: { projectId_branchId: { projectId: options.projectId, branchId: options.branchId } },
data: { source: next as any },
});
});
}
/**
* Transitions a `running` agent run to `awaiting_review`: the agent has finished
* editing the config but has not yet committed or pushed. The diff is stored so
* the dashboard can display it. The sandbox_id must still be set (the sandbox
* stays alive it will be used to commit+push when the user confirms, or
* hard-stopped on cancel).
*/
export async function setConfigAgentRunAwaitingReview(options: {
projectId: string,
branchId: string,
diff: string,
}): Promise<{ sandboxId: string | undefined }> {
return await retryTransaction(globalPrismaClient, async (tx) => {
const rows = await tx.$queryRaw<{ source: any }[]>`
SELECT "source" FROM "BranchConfigOverride"
WHERE "projectId" = ${options.projectId} AND "branchId" = ${options.branchId}
FOR UPDATE
`;
const source = rows[0]?.source;
if (!source || source.type !== "pushed-from-github") return { sandboxId: undefined };
if (source.agent_run?.status !== "running") return { sandboxId: undefined };
const next: GithubConfigSource = {
...source,
agent_run: {
...source.agent_run,
status: "awaiting_review",
stage: "awaiting_review",
diff: options.diff.slice(0, 100_000),
},
};
await tx.branchConfigOverride.update({
where: { projectId_branchId: { projectId: options.projectId, branchId: options.branchId } },
data: { source: next as any },
});
return { sandboxId: source.agent_run?.sandbox_id };
});
}
/**
* Requests cancellation of the in-flight config agent run. Atomically flips a
* `running` run to the terminal `cancelled` status (so the original run's late
* result is ignored see {@link recordConfigAgentRunResult}) and returns the
* sandbox id (if recorded) so the caller can hard-stop the sandbox. Returns
* `{ cancelled: false }` when no fresh run is in flight. (No revert: stopping the
* sandbox before the push undoes the change; a commit that already landed stays.)
* `running` or `awaiting_review` run to the terminal `cancelled` status and
* returns the sandbox id (if recorded) so the caller can hard-stop the sandbox.
* Returns `{ cancelled: false }` when no fresh run is in flight. (No revert:
* stopping the sandbox before the push undoes the change; a commit that already
* landed stays.)
*/
export async function cancelConfigAgentRun(options: {
projectId: string,
@ -619,7 +693,7 @@ export async function cancelConfigAgentRun(options: {
const source = rows[0]?.source;
if (!source || source.type !== "pushed-from-github") return { cancelled: false };
const run = source.agent_run;
if (!run || run.status !== "running") return { cancelled: false };
if (!run || (run.status !== "running" && run.status !== "awaiting_review")) return { cancelled: false };
const sandboxId: string | undefined = run.sandbox_id;
const next: GithubConfigSource = {
...source,

View File

@ -18,8 +18,9 @@
*
* Each update then does a FRESH shallow clone of the target branch inside the
* sandbox (we take that clone cost per write instead of caching it), runs the
* agent, commits, and pushes. The sandbox is destroyed afterwards and is NEVER
* snapshotted, so the token that lives in `origin` for the clone/push dies with it.
* agent, keeps the sandbox alive for review, then either commits/pushes or
* stops it on discard. The sandbox is NEVER snapshotted, so the token that lives
* in `origin` for the clone/push dies with it.
*
* Phases:
* buildConfigAgentBaseSnapshot one-off (build script): node24 + agent SDK + git
@ -49,6 +50,7 @@ const TOOLS_DIR = BASE; // agent SDK + runner live here, separate from the repo
const DEFAULT_AGENT_MODEL = "anthropic/claude-haiku-4.5";
const DEFAULT_PROXY_URL = "https://api.hexclave.com/api/latest/integrations/ai-proxy";
const SANDBOX_TIMEOUT_MS = 900_000;
const REVIEW_SANDBOX_KEEPALIVE_MS = 5 * 60_000;
const GIT_BOT_NAME = "Hexclave Config Bot";
const GIT_BOT_EMAIL = "config-bot@hexclave.com";
@ -63,9 +65,7 @@ export type GithubRepoRef = { owner: string, repo: string, branch: string };
*/
export type GithubTokenProvider = () => Promise<string>;
export type ConfigUpdatePushResult =
| { mode: "commit-to-branch", branch: string, commitUrl: string, commitSha: string }
| { mode: "no-change" };
export type ConfigUpdateCommitResult = { mode: "commit-to-branch", branch: string, commitUrl: string, commitSha: string };
export class ConfigRepoAgentError extends Error {
constructor(message: string, options?: { cause?: unknown }) {
@ -92,6 +92,51 @@ function sandboxCreds(): SandboxCreds {
};
}
async function getConfigAgentSandbox(sandboxId: string): Promise<Sandbox> {
const creds = sandboxCreds();
return await Sandbox.get({ sandboxId, token: creds.token, teamId: creds.teamId, projectId: creds.projectId });
}
async function stopSandboxWithContext(sandboxId: string, context: string): Promise<void> {
try {
const sandbox = await getConfigAgentSandbox(sandboxId);
await sandbox.stop();
} catch (error) {
captureError(context, error);
}
}
async function keepSandboxAliveForReview(sandbox: Sandbox): Promise<void> {
// The review UI needs the live sandbox because commit runs from the edited
// working tree. Top it back up to a five-minute review window once the diff is
// ready instead of relying on whatever time is left after agent execution.
const timeoutRemainingMs = sandbox.timeout;
if (timeoutRemainingMs < REVIEW_SANDBOX_KEEPALIVE_MS) {
await sandbox.extendTimeout(REVIEW_SANDBOX_KEEPALIVE_MS - timeoutRemainingMs);
}
}
async function reportConfigAgentProgress(onProgress: AgentProgressSink | undefined, progress: string, context: string): Promise<void> {
if (!onProgress) return;
try {
await onProgress(progress);
} catch (error) {
captureError(context, error);
}
}
async function reportConfigAgentStage(
onStage: ((stage: "initializing_sandbox" | "cloning_repo" | "agent_making_changes") => Promise<void>) | undefined,
stage: "initializing_sandbox" | "cloning_repo" | "agent_making_changes",
): Promise<void> {
if (!onStage) return;
try {
await onStage(stage);
} catch (error) {
captureError("config-repo-agent-stage", error);
}
}
/**
* Strip any tokenized remote URL (`https://x-access-token:<token>@github.com/...`)
* out of a string before it can be thrown, captured, persisted, or logged. The
@ -183,6 +228,9 @@ process.on("unhandledRejection", (e) => { status({ ok: false, error: "unhandledR
const PROGRESS = ${JSON.stringify(`${TOOLS_DIR}/progress.json`)};
const recent = [];
const base = (p) => (typeof p === "string" ? (p.split("/").pop() || p) : "");
// In-sandbox feed cap: 100 chars/line, last 6 lines. Separate from the coarser
// 2000-char storage cap in recordConfigAgentRunProgress (config/index.tsx) —
// this one shapes what the user sees live; that one guards DB size.
const emit = (s) => { recent.push(String(s).replace(/[\\r\\n]+/g, " ").slice(0, 100)); while (recent.length > 6) recent.shift(); try { writeFileSync(PROGRESS, JSON.stringify(recent)); } catch {} };
const describeTool = (name, inp) => {
inp = inp || {};
@ -268,7 +316,7 @@ async function pollAgentProgress(
const text = redactTokens(lines.map((l) => String(l)).join("\n")).trim();
if (text && text !== last) {
last = text;
await onProgress(text).catch(() => {});
await reportConfigAgentProgress(onProgress, text, "config-repo-agent-progress-record");
}
};
while (!finished) {
@ -393,55 +441,102 @@ export async function buildConfigAgentBaseSnapshot(onProgress?: (msg: string) =>
// Apply update (on save)
// ---------------------------------------------------------------------------
/**
* The result of `applyConfigUpdate`. When the agent found and edited the config
* file, the sandbox is left alive (status `awaiting_review`) and the caller is
* responsible for either calling `commitConfigUpdate` or `stopConfigAgentSandbox`.
*/
export type ConfigUpdateApplyResult =
| {
mode: "awaiting_review",
sandboxId: string,
/** Unified git diff of the agent's changes; never contains secrets. */
diff: string,
}
| { mode: "no-change" };
export async function applyConfigUpdate(options: {
getGithubToken: GithubTokenProvider,
ref: GithubRepoRef,
completeConfig: Record<string, unknown>,
commitMessage?: string,
onSandboxId?: (sandboxId: string) => Promise<void>,
onStage?: (stage: "initializing_sandbox" | "cloning_repo" | "agent_making_changes") => Promise<void>,
onProgress?: AgentProgressSink,
}): Promise<ConfigUpdatePushResult> {
const { getGithubToken, ref, completeConfig, onSandboxId, onProgress } = options;
}): Promise<ConfigUpdateApplyResult> {
const { getGithubToken, ref, completeConfig, onSandboxId, onStage, onProgress } = options;
const creds = sandboxCreds();
const commitMessage = options.commitMessage?.trim() || "chore(hexclave): update config from dashboard";
const step = async (msg: string) => {
if (onProgress) await onProgress(msg).catch(() => {});
await reportConfigAgentProgress(onProgress, msg, "config-repo-agent-step-record");
};
const githubToken = await getGithubToken(); // fresh token for this boot
await step("Starting the config agent…");
await reportConfigAgentStage(onStage, "initializing_sandbox");
await step("Initializing the sandbox…");
const sandbox = await bootAgentSandbox(creds);
// Do NOT stop the sandbox in a finally block — when changes are found, we leave
// it alive for the user to review and commit. The caller must stop it.
await onSandboxId?.(sandbox.sandboxId);
// Configure the bot identity for the commit (idempotent; cheap on a warm boot).
await run(sandbox, "git", ["config", "--global", "user.email", GIT_BOT_EMAIL]);
await run(sandbox, "git", ["config", "--global", "user.name", GIT_BOT_NAME]);
// Fresh shallow clone of just the target branch. The tokenized URL is used
// only for the clone; immediately after, we reset `origin` to a tokenless URL
// so the agent (which has Bash access) cannot read the token from `.git/config`
// or `git remote -v`. The token is re-injected only for our own push command.
await reportConfigAgentStage(onStage, "cloning_repo");
await step(`Cloning ${ref.owner}/${ref.repo}@${ref.branch}`);
await run(sandbox, "git", ["clone", "--depth", "1", "--single-branch", "--branch", ref.branch, tokenUrl(githubToken, ref), REPO_DIR]);
await run(sandbox, "git", ["-C", REPO_DIR, "remote", "set-url", "origin", tokenlessUrl(ref)]);
// Agent writes the COMPLETE config to the file — no dependency install, no
// typecheck (the linked repo's CI validates the committed change). See buildUpdatePrompt.
await reportConfigAgentStage(onStage, "agent_making_changes");
await step("Agent editing config…");
await runAgent(sandbox, buildUpdatePrompt(completeConfig), onProgress);
const dirty = (await runRaw(sandbox, "git", ["-C", REPO_DIR, "status", "--porcelain"])).stdout.trim();
if (dirty === "") {
// No changes — stop the sandbox immediately (nothing to review).
await stopSandboxWithContext(sandbox.sandboxId, "config-repo-agent-no-change-stop");
return { mode: "no-change" };
}
// Capture the diff before staging so the user can review it. Redact tokens
// (defensive; the diff shouldn't contain any, but be safe).
const diff = redactTokens(
(await runRaw(sandbox, "git", ["-C", REPO_DIR, "diff"])).stdout,
);
await keepSandboxAliveForReview(sandbox);
// Leave the sandbox alive — the user must confirm before we commit+push.
return { mode: "awaiting_review", sandboxId: sandbox.sandboxId, diff };
}
/**
* Commits and pushes the agent's already-applied changes from an existing sandbox
* that is currently in `awaiting_review` state. The sandbox is stopped afterwards.
* The GitHub token is obtained freshly the user may have been reviewing for a
* while so we don't reuse the token from the original `applyConfigUpdate` call.
*/
export async function commitConfigUpdate(options: {
sandboxId: string,
getGithubToken: GithubTokenProvider,
ref: GithubRepoRef,
commitMessage: string,
}): Promise<ConfigUpdateCommitResult> {
const { sandboxId, ref, commitMessage } = options;
const githubToken = await options.getGithubToken();
const sandbox = await getConfigAgentSandbox(sandboxId);
try {
await onSandboxId?.(sandbox.sandboxId);
// Configure the bot identity for the commit (idempotent; cheap on a warm boot).
await run(sandbox, "git", ["config", "--global", "user.email", GIT_BOT_EMAIL]);
await run(sandbox, "git", ["config", "--global", "user.name", GIT_BOT_NAME]);
// Fresh shallow clone of just the target branch. The tokenized URL is used
// only for the clone; immediately after, we reset `origin` to a tokenless URL
// so the agent (which has Bash access) cannot read the token from `.git/config`
// or `git remote -v`. The token is re-injected only for our own push command.
await step(`Cloning ${ref.owner}/${ref.repo}@${ref.branch}`);
await run(sandbox, "git", ["clone", "--depth", "1", "--single-branch", "--branch", ref.branch, tokenUrl(githubToken, ref), REPO_DIR]);
await run(sandbox, "git", ["-C", REPO_DIR, "remote", "set-url", "origin", tokenlessUrl(ref)]);
// Agent writes the COMPLETE config to the file — no dependency install, no
// typecheck (the linked repo's CI validates the committed change). See buildUpdatePrompt.
await runAgent(sandbox, buildUpdatePrompt(completeConfig), onProgress);
const dirty = (await runRaw(sandbox, "git", ["-C", REPO_DIR, "status", "--porcelain"])).stdout.trim();
if (dirty === "") {
return { mode: "no-change" };
}
await run(sandbox, "git", ["-C", REPO_DIR, "add", "-A"]);
await run(sandbox, "git", ["-C", REPO_DIR, "commit", "-m", commitMessage]);
const commitSha = await gitHead(sandbox);
await step("Pushing the commit…");
// Re-inject the token for the push only (origin was reset to tokenless after clone).
// Re-inject the token for the push only.
await run(sandbox, "git", ["-C", REPO_DIR, "push", tokenUrl(githubToken, ref), `HEAD:refs/heads/${ref.branch}`]);
return { mode: "commit-to-branch", branch: ref.branch, commitUrl: `https://github.com/${ref.owner}/${ref.repo}/commit/${commitSha}`, commitSha };
} finally {
await sandbox.stop().catch(() => {});
await stopSandboxWithContext(sandboxId, "config-repo-agent-commit-stop");
}
}
@ -456,11 +551,5 @@ export async function applyConfigUpdate(options: {
* undoes the change; if a commit already landed, it stays (no revert).
*/
export async function stopConfigAgentSandbox(sandboxId: string): Promise<void> {
const creds = sandboxCreds();
try {
const sandbox = await Sandbox.get({ sandboxId, token: creds.token, teamId: creds.teamId, projectId: creds.projectId });
await sandbox.stop();
} catch (error) {
captureError("config-repo-agent-cancel-stop", error);
}
await stopSandboxWithContext(sandboxId, "config-repo-agent-cancel-stop");
}

View File

@ -1,7 +1,7 @@
import { globalPrismaClient } from "@/prisma-client";
import { showOnboardingHexclaveConfigValue } from "@hexclave/shared/dist/config-authoring";
import { renderConfigFileContent } from "@hexclave/shared/dist/config-rendering";
import { detectImportPackageFromDir, evalConfigFileContent } from "@hexclave/shared/dist/config-eval";
import { detectImportPackageFromDir, evalConfigFileContent, type ParsedConfigValue } from "@hexclave/shared/dist/config-eval";
import { isValidConfig } from "@hexclave/shared/dist/config/format";
import { LOCAL_EMULATOR_ADMIN_EMAIL, LOCAL_EMULATOR_ADMIN_PASSWORD } from "@hexclave/shared/dist/local-emulator";
import { getEnvVariable } from "@hexclave/shared/dist/utils/env";
@ -18,7 +18,7 @@ export const LOCAL_EMULATOR_ONLY_ENDPOINT_MESSAGE =
export const LOCAL_EMULATOR_HOST_MOUNT_ROOT_ENV = "STACK_LOCAL_EMULATOR_HOST_MOUNT_ROOT";
export const LOCAL_EMULATOR_SHOW_ONBOARDING_VALUE = showOnboardingHexclaveConfigValue;
type LocalEmulatorConfigValue = Record<string, unknown> | typeof LOCAL_EMULATOR_SHOW_ONBOARDING_VALUE;
type LocalEmulatorConfigValue = ParsedConfigValue;
export function isLocalEmulatorEnabled() {
return getEnvVariable("NEXT_PUBLIC_STACK_IS_LOCAL_EMULATOR", "") === "true";
@ -76,14 +76,10 @@ async function readConfigContent(filePath: string): Promise<string> {
async function readConfigValueFromFile(filePath: string): Promise<LocalEmulatorConfigValue> {
const content = await readConfigContent(filePath);
try {
const result = evalConfigFileContent(content, filePath);
if (typeof result === "string") {
if (result !== LOCAL_EMULATOR_SHOW_ONBOARDING_VALUE) {
throw new Error(`Unexpected string config value: ${JSON.stringify(result)}`);
}
return result;
}
return result;
// `evalConfigFileContent` already guarantees the value is a config object or
// the `"show-onboarding"` sentinel (it throws otherwise), so no further
// shape check is needed here.
return evalConfigFileContent(content, filePath);
} catch (e) {
const message = e instanceof Error ? e.message : String(e);
throw new StatusError(StatusError.BadRequest, `Error evaluating config in ${filePath}: ${message}`);

View File

@ -22,6 +22,7 @@
},
"dependencies": {
"@ai-sdk/react": "^3.0.72",
"@pierre/diffs": "^1.2.11",
"@assistant-ui/react": "^0.10.24",
"@assistant-ui/react-ai-sdk": "^0.10.14",
"@assistant-ui/react-markdown": "^0.10.5",

View File

@ -2,17 +2,18 @@
import { useAdminAppIfExists } from "@/app/(main)/(protected)/projects/[projectId]/use-admin-app";
import { ActionDialog } from "@/components/ui/action-dialog";
import { GitBranch } from "@phosphor-icons/react";
import type { StackAdminApp } from "@hexclave/next";
import { captureError } from "@hexclave/shared/dist/utils/errors";
import React, { useCallback, useEffect, useRef, useState } from "react";
import { ConfigAgentActivityFeed, useGithubRunActive } from "./config-update";
import { ConfigAgentRunProgressContent, type AgentStage, getAdminInterface, isGithubPushedSourceWithAgentRun, useGithubRunActive, type GithubPushedSourceWithAgentRun } from "./config-update";
/**
* Watches the linked-GitHub config source for an in-flight agent run and, when
* one is running, pops a NON-DISMISSIBLE progress modal so opening the project
* (in any tab, or after a reload) while a run is going surfaces it and prevents
* starting a conflicting edit. The only way out is Cancel (hard-stops the
* sandbox) or the run reaching a terminal status.
* one is running, pops a NON-DISMISSIBLE progress modal so opening the project
* in another tab surfaces it and prevents starting a conflicting edit. Review
* and commit stay owned by the push dialog that started the run.
*
* Mounted once per project (inside `AdminAppProvider`). It deliberately stays
* silent for runs THIS tab started via the push dialog that dialog owns the
@ -25,11 +26,12 @@ const ACTIVE_POLL_MS = 3_000; // a run is on screen — poll tightly
const LINKED_IDLE_POLL_MS = 10_000; // linked repo, no run — watch for new runs
const UNLINKED_POLL_MS = 30_000; // not a GitHub-linked project — back off
async function readPushedConfigSource(adminApp: unknown): Promise<any | null> {
const iface = (adminApp as any)?._interface;
async function readPushedConfigSource(adminApp: StackAdminApp<false> | null): Promise<GithubPushedSourceWithAgentRun | null> {
const iface = getAdminInterface(adminApp);
if (iface == null || typeof iface.getPushedConfigSource !== "function") return null;
try {
return await iface.getPushedConfigSource();
const source: unknown = await iface.getPushedConfigSource();
return isGithubPushedSourceWithAgentRun(source) ? source : null;
} catch {
return null; // transient — try again next tick
}
@ -43,13 +45,14 @@ export function ConfigAgentRunWatcher() {
const [sourceInfo, setSourceInfo] = useState<SourceInfo | null>(null);
const [errorMessage, setErrorMessage] = useState<string | null>(null);
const [activity, setActivity] = useState<string | null>(null);
const [stage, setStage] = useState<AgentStage | null>(null);
const [startedAt, setStartedAt] = useState<number>(0);
// Keep the latest phase readable inside the polling loop without retriggering it.
const phaseRef = useRef(phase);
phaseRef.current = phase;
const handleCancel = useCallback(async (): Promise<"prevent-close" | undefined> => {
const iface = (adminApp as any)?._interface;
const iface = getAdminInterface(adminApp);
if (iface == null || typeof iface.cancelConfigAgentRun !== "function") {
setErrorMessage("This dashboard build can't cancel a config run. Please refresh and try again.");
setPhase("error");
@ -60,7 +63,6 @@ export function ConfigAgentRunWatcher() {
try {
await iface.cancelConfigAgentRun();
} catch (error) {
// Best-effort: the poll loop still settles if the cancel landed.
captureError("config-agent-watcher-cancel", error);
}
return "prevent-close";
@ -68,13 +70,11 @@ export function ConfigAgentRunWatcher() {
useEffect(() => {
if (!adminApp) return;
// Mutable holder (not a `let`) so eslint's flow analysis doesn't treat the
// post-await `stopped` check as always-false — the flag is flipped in cleanup.
const loop = { stopped: false };
let timer: ReturnType<typeof setTimeout> | undefined;
const apply = (source: any): number => {
if (!source || source.type !== "pushed-from-github") {
const apply = (source: GithubPushedSourceWithAgentRun | null): number => {
if (source == null) {
if (phaseRef.current !== "error") setPhase("hidden");
return UNLINKED_POLL_MS;
}
@ -88,11 +88,11 @@ export function ConfigAgentRunWatcher() {
}
if (status === "running") {
setActivity(typeof source.agent_run?.progress === "string" ? source.agent_run.progress : null);
if (source.agent_run?.stage != null) setStage(source.agent_run.stage);
if (typeof source.agent_run?.started_at === "number") setStartedAt(source.agent_run.started_at);
if (phaseRef.current !== "cancelling") setPhase("running");
return ACTIVE_POLL_MS;
}
// Terminal (or no run). Only surface an error if we were actively showing
// this run — a stale error from before the page loaded shouldn't pop a modal.
if (status === "error" && (phaseRef.current === "running" || phaseRef.current === "cancelling")) {
setErrorMessage(source.agent_run?.error ?? "The config agent failed to apply the change.");
setPhase("error");
@ -104,7 +104,7 @@ export function ConfigAgentRunWatcher() {
const tick = async () => {
const source = await readPushedConfigSource(adminApp);
if (loop.stopped) return; // unmounted while the request was in flight
if (loop.stopped) return;
const delay = apply(source);
timer = setTimeout(() => void tick(), delay);
};
@ -139,27 +139,22 @@ export function ConfigAgentRunWatcher() {
<ActionDialog
open
preventClose
title="Configuration update in progress"
description={`A configuration change is being applied to ${linked} via the config agent.`}
titleIcon={GitBranch}
title="Push configuration to GitHub"
description={`Applying your change in a sandbox — ${linked}`}
cancelButton={{
label: phase === "cancelling" ? "Cancelling…" : "Cancel update",
label: phase === "cancelling" ? "Cancelling…" : "Cancel",
onClick: handleCancel,
props: { disabled: phase === "cancelling" },
props: { disabled: phase === "cancelling", variant: "outline" },
}}
>
<div className="space-y-3">
<p className="text-sm text-muted-foreground">
{phase === "cancelling"
? "Cancelling the update and stopping the agent…"
: "Applying your change in a sandbox and committing it to GitHub. This can take a couple of minutes."}
</p>
{phase !== "cancelling" && activity != null && activity.trim().length > 0 && (
<ConfigAgentActivityFeed activity={activity} />
)}
{errorMessage != null && (
<p className="text-sm text-destructive">{errorMessage}</p>
)}
</div>
<ConfigAgentRunProgressContent
isCancelling={phase === "cancelling"}
stage={stage}
startedAt={startedAt}
activity={activity}
errorMessage={errorMessage}
/>
</ActionDialog>
);
}

File diff suppressed because it is too large Load Diff

View File

@ -421,7 +421,7 @@ describe("oauth config", () => {
expect(invalidTypeResponse).toMatchInlineSnapshot(`
NiceResponse {
"status": 400,
"body": "auth.oauth.providers.invalid.type must be one of the following values: google, github, microsoft, spotify, facebook, discord, gitlab, bitbucket, linkedin, apple, x, twitch",
"body": "auth.oauth.providers.invalid.type must be one of the following values: google, github, microsoft, spotify, facebook, discord, gitlab, bitbucket, linkedin, apple, x, twitch, custom_oidc",
"headers": Headers { <some fields may have been hidden> },
}
`);
@ -1765,6 +1765,18 @@ describe("branch config source", () => {
branch: string,
commit_hash: string,
config_file_path: string,
agent_run: {
status: "running" | "awaiting_review" | "success" | "no-change" | "error" | "cancelled",
started_at: number,
finished_at?: number,
progress?: string,
sandbox_id?: string,
commit_url?: string,
new_commit_hash?: string,
error?: string,
stage?: "initializing_sandbox" | "cloning_repo" | "agent_making_changes" | "awaiting_review",
diff?: string,
},
}>) => ({
type: "pushed-from-github" as const,
owner: overrides?.owner ?? "myorg",
@ -1772,6 +1784,7 @@ describe("branch config source", () => {
branch: overrides?.branch ?? "main",
commit_hash: overrides?.commit_hash ?? "abc123def456",
config_file_path: overrides?.config_file_path ?? "stack.config.ts",
...(overrides?.agent_run != null ? { agent_run: overrides.agent_run } : {}),
});
const createUnknownSource = () => ({
@ -2364,6 +2377,26 @@ describe("branch config source", () => {
expect(source).toEqual(sourceWithEmptyStrings);
});
it("preserves config agent review state on github source fields", async ({ expect }) => {
await Project.createAndSwitch();
const sourceWithReviewRun = createGitHubSource({
agent_run: {
status: "awaiting_review" as const,
started_at: 1_777_000_000_000,
sandbox_id: "sbx_review_123",
stage: "awaiting_review" as const,
progress: "Editing hexclave.config.ts",
diff: "diff --git a/hexclave.config.ts b/hexclave.config.ts\n",
},
});
await Project.pushConfig({ 'teams.allowClientTeamCreation': true }, sourceWithReviewRun);
const source = await Project.getConfigSource();
expect(source).toEqual(sourceWithReviewRun);
});
it("handles source fields at string length boundaries", async ({ expect }) => {
await Project.createAndSwitch();

View File

@ -56,7 +56,7 @@ export async function readConfigFile(configFilePath: string): Promise<{ config:
// user-facing message (the raw jiti/framework error is captured for diagnostics
// but not attached as `cause` — dashboard error formatting renders causes
// recursively and would leak framework internals).
let parsed: Record<string, unknown> | string;
let parsed: ReturnType<typeof evalConfigFileContent>;
try {
parsed = evalConfigFileContent(content, configFilePath);
} catch (error) {

View File

@ -1,6 +1,7 @@
import { existsSync, readFileSync } from "fs";
import { createJiti } from "jiti";
import path from "path";
import { showOnboardingHexclaveConfigValue } from "./config-authoring";
import { detectConfigImportPackage } from "./config-rendering";
const jiti = createJiti(import.meta.url, { moduleCache: false });
@ -49,11 +50,16 @@ function isRecord(value: unknown): value is Record<string, unknown> {
return value != null && typeof value === "object" && !Array.isArray(value);
}
type ParsedConfigValue = Record<string, unknown> | string;
/** A config object, or the `"show-onboarding"` sentinel that stands in for one. */
export type ParsedConfigValue = Record<string, unknown> | typeof showOnboardingHexclaveConfigValue;
function invalidConfigShape(filePath: string): ConfigFileEvalError {
return new ConfigFileEvalError(`Invalid config in ${filePath}. The file must export a plain \`config\` object or "${showOnboardingHexclaveConfigValue}".`);
}
/**
* Evaluates config file content using jiti and returns the exported `config`
* value. Replaces the old Babel AST-based `parseHexclaveConfigFileContent`.
* value.
*
* WARNING: This executes arbitrary code via `jiti.evalModule` only use on
* content that is fully operator-controlled (local filesystem). Never call
@ -64,32 +70,20 @@ export function evalConfigFileContent(content: string, filePath: string): Parsed
const resolvedPath = path.isAbsolute(filePath) ? filePath : path.resolve(filePath);
const mod: unknown = jiti.evalModule(content, { filename: resolvedPath });
if (!isRecord(mod)) {
throw new ConfigFileEvalError(`Invalid config in ${filePath}. The file must export a plain \`config\` object or "show-onboarding".`);
throw invalidConfigShape(filePath);
}
const config = mod.config;
if (config === undefined) {
throw new ConfigFileEvalError(`Invalid config in ${filePath}. The file must export a plain \`config\` object or "show-onboarding".`);
throw invalidConfigShape(filePath);
}
if (typeof config === "string") {
if (config !== "show-onboarding") {
throw new ConfigFileEvalError(`Invalid config in ${filePath}. String config values must be "show-onboarding", got "${config}".`);
if (config !== showOnboardingHexclaveConfigValue) {
throw new ConfigFileEvalError(`Invalid config in ${filePath}. String config values must be "${showOnboardingHexclaveConfigValue}", got "${config}".`);
}
return config;
}
if (isRecord(config)) return config;
throw new ConfigFileEvalError(`Invalid config in ${filePath}. The file must export a plain \`config\` object or "show-onboarding".`);
}
/**
* Like {@link evalConfigFileContent}, but returns `null` instead of throwing
* when the content cannot be evaluated.
*/
export function tryEvalConfigFileContent(content: string, filePath: string): ParsedConfigValue | null {
try {
return evalConfigFileContent(content, filePath);
} catch {
return null;
}
throw invalidConfigShape(filePath);
}
// --- inline vitest tests ---
@ -124,21 +118,3 @@ import.meta.vitest?.test("evalConfigFileContent rejects content without config e
import.meta.vitest?.test("evalConfigFileContent rejects arbitrary string config values", ({ expect }) => {
expect(() => evalConfigFileContent('export const config = "arbitrary-string";', "stack.config.ts")).toThrow(/must be "show-onboarding"/);
});
import.meta.vitest?.test("tryEvalConfigFileContent returns the config for valid exports", ({ expect }) => {
expect(tryEvalConfigFileContent("export const config = { auth: { allowSignUp: true } };", "stack.config.ts")).toEqual({
auth: { allowSignUp: true },
});
});
import.meta.vitest?.test("tryEvalConfigFileContent returns null on unresolvable function call", ({ expect }) => {
expect(tryEvalConfigFileContent("export const config = someUndefinedFunction();", "stack.config.ts")).toBeNull();
});
import.meta.vitest?.test("tryEvalConfigFileContent returns null on unresolvable import", ({ expect }) => {
expect(tryEvalConfigFileContent('import x from "./nonexistent-file";\nexport const config = { a: x };', "stack.config.ts")).toBeNull();
});
import.meta.vitest?.test("tryEvalConfigFileContent returns null on syntax error", ({ expect }) => {
expect(tryEvalConfigFileContent("export const config = {", "stack.config.ts")).toBeNull();
});

View File

@ -375,6 +375,24 @@ import.meta.vitest?.test("normalize(...)", ({ expect }) => {
expect(normalize({
"b.c": 2,
}, { onDotIntoNonObject: "ignore" })).toEqual({});
expect(normalize({
"auth.allowSignUp": false,
"apps.installed.payments.enabled": false,
}, {
onDotIntoNonObject: "ignore",
onDotIntoNull: "empty-object",
})).toEqual({
auth: {
allowSignUp: false,
},
apps: {
installed: {
payments: {
enabled: false,
},
},
},
});
// dotting into non-object
expect(() => normalize({

View File

@ -859,8 +859,9 @@ export class HexclaveAdminInterface extends HexclaveServerInterface {
/**
* Cancels the in-flight agent-driven config write: hard-stops the sandbox so
* the agent stops mid-work. No revert if the agent already pushed, the commit
* stays. Returns `not-running` if no run is in flight.
* the agent stops mid-work. Also cancels runs in `awaiting_review`. No revert
* if the agent already pushed, the commit stays. Returns `not-running` if
* no run is in flight.
*/
async cancelConfigAgentRun(): Promise<{ status: "cancelling" | "not-running" }> {
const response = await this.sendAdminRequest(
@ -875,6 +876,28 @@ export class HexclaveAdminInterface extends HexclaveServerInterface {
return await response.json();
}
/**
* Commits and pushes the agent's already-applied changes after the user has
* reviewed the diff. Only valid when a run is in `awaiting_review` status.
* Returns `sandbox-expired` if the review state exists but its sandbox id is
* missing, which means the user needs to rerun the agent.
*/
async commitConfigAgentRun(options: { githubAccessToken: string, commitMessage?: string }): Promise<{ status: "committing" | "not-awaiting-review" | "sandbox-expired" }> {
const response = await this.sendAdminRequest(
`/internal/config/github/commit`,
{
method: "POST",
headers: { "content-type": "application/json" },
body: JSON.stringify({
github_access_token: options.githubAccessToken,
...(options.commitMessage ? { commit_message: options.commitMessage } : {}),
}),
},
null,
);
return await response.json();
}
async resetConfigOverrideKeys(level: "branch" | "environment", keys: string[]): Promise<void> {
await this.sendAdminRequest(
`/internal/config/override/${level}/reset-keys`,

View File

@ -942,12 +942,14 @@ export const branchConfigSourceSchema = yupUnion(
// Status of the most recent agent-driven config write, so the dashboard can
// poll for progress and surface the resulting commit (or error) across tabs.
agent_run: yupObject({
status: yupString().oneOf(["running", "success", "no-change", "error", "cancelled"]).defined(),
// "running": agent is working; "awaiting_review": agent done, diff ready, waiting for user to commit;
// "success" | "no-change" | "error" | "cancelled": terminal.
status: yupString().oneOf(["running", "awaiting_review", "success", "no-change", "error", "cancelled"]).defined(),
started_at: yupNumber().defined(),
finished_at: yupNumber().optional(),
commit_url: urlSchema.optional(),
error: yupString().optional(),
// Vercel Sandbox id of the in-flight run, recorded while `status === "running"`
// Vercel Sandbox id of the in-flight run, recorded while `status === "running"` or `"awaiting_review"`
// so a cancel request (a different invocation) can hard-stop the sandbox.
// Cleared/ignored once the run reaches a terminal status.
sandbox_id: yupString().optional(),
@ -955,6 +957,12 @@ export const branchConfigSourceSchema = yupUnion(
// "Editing hexclave.config.ts", "Running: git push") so the dashboard can
// show what's happening. Never contains file contents, tool inputs, or tokens.
progress: yupString().optional(),
// Current stage of the run, for showing a progress bar in the dashboard.
// Cleared on terminal status.
stage: yupString().oneOf(["initializing_sandbox", "cloning_repo", "agent_making_changes", "awaiting_review"]).optional(),
// The git unified diff produced by the agent, set when status becomes "awaiting_review".
// Displayed in the dashboard for review before the user commits.
diff: yupString().optional(),
}).optional(),
}),
yupObject({

View File

@ -404,6 +404,9 @@ importers:
'@phosphor-icons/react':
specifier: ^2.1.10
version: 2.1.10(react-dom@19.2.3(react@19.2.3))(react@19.2.3)
'@pierre/diffs':
specifier: ^1.2.11
version: 1.2.11(@shikijs/themes@3.23.0)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)
'@radix-ui/react-accordion':
specifier: ^1.2.1
version: 1.2.12(@types/react-dom@19.2.3(@types/react@19.2.7))(@types/react@19.2.7)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)
@ -798,7 +801,7 @@ importers:
version: 1.166.6(react-dom@19.2.1(react@19.2.1))(react@19.2.1)
'@tanstack/react-start':
specifier: ^1.121.3
version: 1.166.6(crossws@0.4.4(srvx@0.8.16))(react-dom@19.2.1(react@19.2.1))(react@19.2.1)(vite@7.3.1(@types/node@22.19.0)(jiti@1.21.7)(lightningcss@1.32.0)(terser@5.44.0)(tsx@4.21.0)(yaml@2.8.0))(webpack@5.92.0(esbuild@0.24.2))
version: 1.166.6(crossws@0.4.4(srvx@0.8.16))(react-dom@19.2.1(react@19.2.1))(react@19.2.1)(vite@7.3.1(@types/node@22.19.0)(jiti@2.6.1)(lightningcss@1.32.0)(terser@5.44.0)(tsx@4.21.0)(yaml@2.8.0))(webpack@5.92.0(esbuild@0.24.2))
'@tanstack/react-start-client':
specifier: ^1.121.3
version: 1.166.6(react-dom@19.2.1(react@19.2.1))(react@19.2.1)
@ -813,7 +816,7 @@ importers:
version: 1.166.6
'@tanstack/start-plugin-core':
specifier: ^1.121.3
version: 1.166.6(@tanstack/react-router@1.166.6(react-dom@19.2.1(react@19.2.1))(react@19.2.1))(crossws@0.4.4(srvx@0.8.16))(vite@7.3.1(@types/node@22.19.0)(jiti@1.21.7)(lightningcss@1.32.0)(terser@5.44.0)(tsx@4.21.0)(yaml@2.8.0))(webpack@5.92.0(esbuild@0.24.2))
version: 1.166.6(@tanstack/react-router@1.166.6(react-dom@19.2.1(react@19.2.1))(react@19.2.1))(crossws@0.4.4(srvx@0.8.16))(vite@7.3.1(@types/node@22.19.0)(jiti@2.6.1)(lightningcss@1.32.0)(terser@5.44.0)(tsx@4.21.0)(yaml@2.8.0))(webpack@5.92.0(esbuild@0.24.2))
'@tanstack/start-server-core':
specifier: ^1.121.3
version: 1.166.6(crossws@0.4.4(srvx@0.8.16))
@ -837,7 +840,7 @@ importers:
version: 0.468.0(react@19.2.1)
nitro:
specifier: ^3.0.0
version: 3.0.0(@electric-sql/pglite@0.3.2)(chokidar@4.0.3)(lru-cache@11.2.2)(mysql2@3.15.3)(rolldown@1.0.0-rc.3)(vite@7.3.1(@types/node@22.19.0)(jiti@1.21.7)(lightningcss@1.32.0)(terser@5.44.0)(tsx@4.21.0)(yaml@2.8.0))(xml2js@0.6.2)
version: 3.0.0(@electric-sql/pglite@0.3.2)(chokidar@4.0.3)(lru-cache@11.2.2)(mysql2@3.15.3)(rolldown@1.0.0-rc.3)(vite@7.3.1(@types/node@22.19.0)(jiti@2.6.1)(lightningcss@1.32.0)(terser@5.44.0)(tsx@4.21.0)(yaml@2.8.0))(xml2js@0.6.2)
qrcode:
specifier: ^1.5.4
version: 1.5.4
@ -874,7 +877,7 @@ importers:
version: 19.2.3(@types/react@19.2.7)
'@vitejs/plugin-react':
specifier: ^5.0.0
version: 5.1.4(vite@7.3.1(@types/node@22.19.0)(jiti@1.21.7)(lightningcss@1.32.0)(terser@5.44.0)(tsx@4.21.0)(yaml@2.8.0))
version: 5.1.4(vite@7.3.1(@types/node@22.19.0)(jiti@2.6.1)(lightningcss@1.32.0)(terser@5.44.0)(tsx@4.21.0)(yaml@2.8.0))
autoprefixer:
specifier: ^10.4.20
version: 10.4.21(postcss@8.5.6)
@ -892,10 +895,10 @@ importers:
version: 6.0.3
vite:
specifier: ^7.0.0
version: 7.3.1(@types/node@22.19.0)(jiti@1.21.7)(lightningcss@1.32.0)(terser@5.44.0)(tsx@4.21.0)(yaml@2.8.0)
version: 7.3.1(@types/node@22.19.0)(jiti@2.6.1)(lightningcss@1.32.0)(terser@5.44.0)(tsx@4.21.0)(yaml@2.8.0)
vite-tsconfig-paths:
specifier: ^4.3.2
version: 4.3.2(typescript@6.0.3)(vite@7.3.1(@types/node@22.19.0)(jiti@1.21.7)(lightningcss@1.32.0)(terser@5.44.0)(tsx@4.21.0)(yaml@2.8.0))
version: 4.3.2(typescript@6.0.3)(vite@7.3.1(@types/node@22.19.0)(jiti@2.6.1)(lightningcss@1.32.0)(terser@5.44.0)(tsx@4.21.0)(yaml@2.8.0))
apps/internal-tool:
dependencies:
@ -2545,7 +2548,7 @@ importers:
devDependencies:
'@quetzallabs/i18n':
specifier: ^0.1.19
version: 0.1.19(next@16.2.9(@babel/core@7.26.0)(@opentelemetry/api@1.9.0)(react-dom@19.2.3(react@19.2.3))(react@19.2.3))
version: 0.1.19(next@16.2.9(@babel/core@7.29.0)(@opentelemetry/api@1.9.0)(react-dom@19.2.3(react@19.2.3))(react@19.2.3))
'@tanstack/react-router':
specifier: ^1.167.4
version: 1.169.1(react-dom@19.2.3(react@19.2.3))(react@19.2.3)
@ -6853,6 +6856,36 @@ packages:
react: '>= 16.8'
react-dom: '>= 16.8'
'@pierre/diffs@1.2.11':
resolution: {integrity: sha512-lSkl5C7eb8Zq7Ote0+J5ZdVOlI72r2EU3vW4+06wULSQqkIMP8mkxG70lVj593b1XYlsM2hCvuyt0cKTA96plQ==}
peerDependencies:
react: ^18.3.1 || ^19.0.0
react-dom: ^18.3.1 || ^19.0.0
'@pierre/theme@1.0.3':
resolution: {integrity: sha512-sWHv11TMoqKxKDgTIk5VbhQjdPhs8DCcBxbjh3mRlS3YOM/OcrWoGX6MM8eBGn9cUu3M46Py0JnxsG2nJaFTuA==}
engines: {vscode: ^1.0.0}
'@pierre/theming@0.0.1':
resolution: {integrity: sha512-1thlEtJbqdyLzc1ZS2KQa1q7FzDGHT4dTEdKHoyQjOMeWWOmbVG5/ndEfOKfAb5Fzkz8cNJrOjFLiZoDH/A03A==}
peerDependencies:
'@pierre/theme': ^1.0.0
'@shikijs/themes': ^3.0.0 || ^4.0.0
react: ^18.3.1 || ^19.0.0
react-dom: ^18.3.1 || ^19.0.0
shiki: ^3.0.0 || ^4.0.0
peerDependenciesMeta:
'@pierre/theme':
optional: true
'@shikijs/themes':
optional: true
react:
optional: true
react-dom:
optional: true
shiki:
optional: true
'@pkgjs/parseargs@0.11.0':
resolution: {integrity: sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==}
engines: {node: '>=14'}
@ -14603,6 +14636,9 @@ packages:
resolution: {integrity: sha512-Lkk/vx6ak3rYkRR0Nhu4lFUT2VDnQSxBe8Hbl7f36358p6ow8Bnvr8lrLt98H8J1aGxfhbX4Fs5tYg2+FTwr5Q==}
engines: {bun: '>=1.0.0', deno: '>=1.30.0', node: '>=8.0.0'}
lru_map@0.4.1:
resolution: {integrity: sha512-I+lBvqMMFfqaV8CJCISjI3wbjmwVu/VyOoU7+qtu9d7ioW5klMgsTTiUOUp+DJvfTTzKXoPbyC6YfgkNcyPSOg==}
lucide-react@0.378.0:
resolution: {integrity: sha512-u6EPU8juLUk9ytRcyapkWI18epAv3RU+6+TC23ivjR0e+glWKBobFeSgRwOIJihzktILQuy6E0E80P2jVTDR5g==}
peerDependencies:
@ -22417,7 +22453,7 @@ snapshots:
remark-gfm: 4.0.1
remark-math: 6.0.0
remark-smartypants: 3.0.2
shiki: 3.14.0
shiki: 3.23.0
unified: 11.0.5
unist-util-visit: 5.0.0
transitivePeerDependencies:
@ -22443,7 +22479,7 @@ snapshots:
remark-gfm: 4.0.1
remark-math: 6.0.0
remark-smartypants: 3.0.2
shiki: 3.14.0
shiki: 3.23.0
unified: 11.0.5
unist-util-visit: 5.0.0
transitivePeerDependencies:
@ -24193,6 +24229,30 @@ snapshots:
react: 19.2.3
react-dom: 19.2.3(react@19.2.3)
'@pierre/diffs@1.2.11(@shikijs/themes@3.23.0)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)':
dependencies:
'@pierre/theme': 1.0.3
'@pierre/theming': 0.0.1(@pierre/theme@1.0.3)(@shikijs/themes@3.23.0)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(shiki@3.23.0)
'@shikijs/transformers': 3.14.0
diff: 8.0.3
hast-util-to-html: 9.0.5
lru_map: 0.4.1
react: 19.2.3
react-dom: 19.2.3(react@19.2.3)
shiki: 3.23.0
transitivePeerDependencies:
- '@shikijs/themes'
'@pierre/theme@1.0.3': {}
'@pierre/theming@0.0.1(@pierre/theme@1.0.3)(@shikijs/themes@3.23.0)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(shiki@3.23.0)':
optionalDependencies:
'@pierre/theme': 1.0.3
'@shikijs/themes': 3.23.0
react: 19.2.3
react-dom: 19.2.3(react@19.2.3)
shiki: 3.23.0
'@pkgjs/parseargs@0.11.0':
optional: true
@ -24438,7 +24498,7 @@ snapshots:
- next
- supports-color
'@quetzallabs/i18n@0.1.19(next@16.2.9(@babel/core@7.26.0)(@opentelemetry/api@1.9.0)(react-dom@19.2.3(react@19.2.3))(react@19.2.3))':
'@quetzallabs/i18n@0.1.19(next@16.2.9(@babel/core@7.29.0)(@opentelemetry/api@1.9.0)(react-dom@19.2.3(react@19.2.3))(react@19.2.3))':
dependencies:
'@babel/parser': 7.29.0
'@babel/traverse': 7.29.0
@ -24446,7 +24506,7 @@ snapshots:
dotenv: 10.0.0
i18next: 21.10.0
i18next-parser: 9.0.2
next-intl: 3.19.1(next@16.2.9(@babel/core@7.26.0)(@opentelemetry/api@1.9.0)(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react@18.3.1)
next-intl: 3.19.1(next@16.2.9(@babel/core@7.29.0)(@opentelemetry/api@1.9.0)(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react@18.3.1)
path: 0.12.7
react: 18.3.1
react-dom: 18.3.1(react@18.3.1)
@ -28510,19 +28570,19 @@ snapshots:
transitivePeerDependencies:
- crossws
'@tanstack/react-start@1.166.6(crossws@0.4.4(srvx@0.8.16))(react-dom@19.2.1(react@19.2.1))(react@19.2.1)(vite@7.3.1(@types/node@22.19.0)(jiti@1.21.7)(lightningcss@1.32.0)(terser@5.44.0)(tsx@4.21.0)(yaml@2.8.0))(webpack@5.92.0(esbuild@0.24.2))':
'@tanstack/react-start@1.166.6(crossws@0.4.4(srvx@0.8.16))(react-dom@19.2.1(react@19.2.1))(react@19.2.1)(vite@7.3.1(@types/node@22.19.0)(jiti@2.6.1)(lightningcss@1.32.0)(terser@5.44.0)(tsx@4.21.0)(yaml@2.8.0))(webpack@5.92.0(esbuild@0.24.2))':
dependencies:
'@tanstack/react-router': 1.166.6(react-dom@19.2.1(react@19.2.1))(react@19.2.1)
'@tanstack/react-start-client': 1.166.6(react-dom@19.2.1(react@19.2.1))(react@19.2.1)
'@tanstack/react-start-server': 1.166.6(crossws@0.4.4(srvx@0.8.16))(react-dom@19.2.1(react@19.2.1))(react@19.2.1)
'@tanstack/router-utils': 1.161.4
'@tanstack/start-client-core': 1.166.6
'@tanstack/start-plugin-core': 1.166.6(@tanstack/react-router@1.166.6(react-dom@19.2.1(react@19.2.1))(react@19.2.1))(crossws@0.4.4(srvx@0.8.16))(vite@7.3.1(@types/node@22.19.0)(jiti@1.21.7)(lightningcss@1.32.0)(terser@5.44.0)(tsx@4.21.0)(yaml@2.8.0))(webpack@5.92.0(esbuild@0.24.2))
'@tanstack/start-plugin-core': 1.166.6(@tanstack/react-router@1.166.6(react-dom@19.2.1(react@19.2.1))(react@19.2.1))(crossws@0.4.4(srvx@0.8.16))(vite@7.3.1(@types/node@22.19.0)(jiti@2.6.1)(lightningcss@1.32.0)(terser@5.44.0)(tsx@4.21.0)(yaml@2.8.0))(webpack@5.92.0(esbuild@0.24.2))
'@tanstack/start-server-core': 1.166.6(crossws@0.4.4(srvx@0.8.16))
pathe: 2.0.3
react: 19.2.1
react-dom: 19.2.1(react@19.2.1)
vite: 7.3.1(@types/node@22.19.0)(jiti@1.21.7)(lightningcss@1.32.0)(terser@5.44.0)(tsx@4.21.0)(yaml@2.8.0)
vite: 7.3.1(@types/node@22.19.0)(jiti@2.6.1)(lightningcss@1.32.0)(terser@5.44.0)(tsx@4.21.0)(yaml@2.8.0)
transitivePeerDependencies:
- '@rsbuild/core'
- crossws
@ -28687,7 +28747,7 @@ snapshots:
transitivePeerDependencies:
- supports-color
'@tanstack/router-plugin@1.166.6(@tanstack/react-router@1.166.6(react-dom@19.2.1(react@19.2.1))(react@19.2.1))(vite@7.3.1(@types/node@22.19.0)(jiti@1.21.7)(lightningcss@1.32.0)(terser@5.44.0)(tsx@4.21.0)(yaml@2.8.0))(webpack@5.92.0(esbuild@0.24.2))':
'@tanstack/router-plugin@1.166.6(@tanstack/react-router@1.166.6(react-dom@19.2.1(react@19.2.1))(react@19.2.1))(vite@7.3.1(@types/node@22.19.0)(jiti@2.6.1)(lightningcss@1.32.0)(terser@5.44.0)(tsx@4.21.0)(yaml@2.8.0))(webpack@5.92.0(esbuild@0.24.2))':
dependencies:
'@babel/core': 7.29.0
'@babel/plugin-syntax-jsx': 7.28.6(@babel/core@7.29.0)
@ -28704,7 +28764,7 @@ snapshots:
zod: 3.25.76
optionalDependencies:
'@tanstack/react-router': 1.166.6(react-dom@19.2.1(react@19.2.1))(react@19.2.1)
vite: 7.3.1(@types/node@22.19.0)(jiti@1.21.7)(lightningcss@1.32.0)(terser@5.44.0)(tsx@4.21.0)(yaml@2.8.0)
vite: 7.3.1(@types/node@22.19.0)(jiti@2.6.1)(lightningcss@1.32.0)(terser@5.44.0)(tsx@4.21.0)(yaml@2.8.0)
webpack: 5.92.0(esbuild@0.24.2)
transitivePeerDependencies:
- supports-color
@ -28823,7 +28883,7 @@ snapshots:
'@tanstack/start-fn-stubs@1.161.6': {}
'@tanstack/start-plugin-core@1.166.6(@tanstack/react-router@1.166.6(react-dom@19.2.1(react@19.2.1))(react@19.2.1))(crossws@0.4.4(srvx@0.8.16))(vite@7.3.1(@types/node@22.19.0)(jiti@1.21.7)(lightningcss@1.32.0)(terser@5.44.0)(tsx@4.21.0)(yaml@2.8.0))(webpack@5.92.0(esbuild@0.24.2))':
'@tanstack/start-plugin-core@1.166.6(@tanstack/react-router@1.166.6(react-dom@19.2.1(react@19.2.1))(react@19.2.1))(crossws@0.4.4(srvx@0.8.16))(vite@7.3.1(@types/node@22.19.0)(jiti@2.6.1)(lightningcss@1.32.0)(terser@5.44.0)(tsx@4.21.0)(yaml@2.8.0))(webpack@5.92.0(esbuild@0.24.2))':
dependencies:
'@babel/code-frame': 7.27.1
'@babel/core': 7.28.5
@ -28831,7 +28891,7 @@ snapshots:
'@rolldown/pluginutils': 1.0.0-beta.40
'@tanstack/router-core': 1.166.6
'@tanstack/router-generator': 1.166.6
'@tanstack/router-plugin': 1.166.6(@tanstack/react-router@1.166.6(react-dom@19.2.1(react@19.2.1))(react@19.2.1))(vite@7.3.1(@types/node@22.19.0)(jiti@1.21.7)(lightningcss@1.32.0)(terser@5.44.0)(tsx@4.21.0)(yaml@2.8.0))(webpack@5.92.0(esbuild@0.24.2))
'@tanstack/router-plugin': 1.166.6(@tanstack/react-router@1.166.6(react-dom@19.2.1(react@19.2.1))(react@19.2.1))(vite@7.3.1(@types/node@22.19.0)(jiti@2.6.1)(lightningcss@1.32.0)(terser@5.44.0)(tsx@4.21.0)(yaml@2.8.0))(webpack@5.92.0(esbuild@0.24.2))
'@tanstack/router-utils': 1.161.4
'@tanstack/start-client-core': 1.166.6
'@tanstack/start-server-core': 1.166.6(crossws@0.4.4(srvx@0.8.16))
@ -28843,8 +28903,8 @@ snapshots:
srvx: 0.11.9
tinyglobby: 0.2.15
ufo: 1.5.4
vite: 7.3.1(@types/node@22.19.0)(jiti@1.21.7)(lightningcss@1.32.0)(terser@5.44.0)(tsx@4.21.0)(yaml@2.8.0)
vitefu: 1.1.2(vite@7.3.1(@types/node@22.19.0)(jiti@1.21.7)(lightningcss@1.32.0)(terser@5.44.0)(tsx@4.21.0)(yaml@2.8.0))
vite: 7.3.1(@types/node@22.19.0)(jiti@2.6.1)(lightningcss@1.32.0)(terser@5.44.0)(tsx@4.21.0)(yaml@2.8.0)
vitefu: 1.1.2(vite@7.3.1(@types/node@22.19.0)(jiti@2.6.1)(lightningcss@1.32.0)(terser@5.44.0)(tsx@4.21.0)(yaml@2.8.0))
xmlbuilder2: 4.0.3
zod: 3.25.76
transitivePeerDependencies:
@ -29841,18 +29901,6 @@ snapshots:
transitivePeerDependencies:
- supports-color
'@vitejs/plugin-react@5.1.4(vite@7.3.1(@types/node@22.19.0)(jiti@1.21.7)(lightningcss@1.32.0)(terser@5.44.0)(tsx@4.21.0)(yaml@2.8.0))':
dependencies:
'@babel/core': 7.29.0
'@babel/plugin-transform-react-jsx-self': 7.27.1(@babel/core@7.29.0)
'@babel/plugin-transform-react-jsx-source': 7.27.1(@babel/core@7.29.0)
'@rolldown/pluginutils': 1.0.0-rc.3
'@types/babel__core': 7.20.5
react-refresh: 0.18.0
vite: 7.3.1(@types/node@22.19.0)(jiti@1.21.7)(lightningcss@1.32.0)(terser@5.44.0)(tsx@4.21.0)(yaml@2.8.0)
transitivePeerDependencies:
- supports-color
'@vitejs/plugin-react@5.1.4(vite@7.3.1(@types/node@22.19.0)(jiti@2.6.1)(lightningcss@1.32.0)(terser@5.44.0)(tsx@4.21.0)(yaml@2.8.0))':
dependencies:
'@babel/core': 7.29.0
@ -33471,7 +33519,7 @@ snapshots:
remark: 15.0.1
remark-gfm: 4.0.1
scroll-into-view-if-needed: 3.1.0
shiki: 3.14.0
shiki: 3.23.0
unist-util-visit: 5.0.0
optionalDependencies:
next: 15.5.19(@babel/core@7.26.0)(@opentelemetry/api@1.9.0)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)
@ -33909,15 +33957,6 @@ snapshots:
optionalDependencies:
crossws: 0.4.4(srvx@0.8.16)
h3@2.0.1-rc.2(crossws@0.4.4(srvx@0.11.15)):
dependencies:
cookie-es: 2.0.0
fetchdts: 0.1.7
rou3: 0.7.12
srvx: 0.8.16
optionalDependencies:
crossws: 0.4.4(srvx@0.11.15)
h3@2.0.1-rc.2(crossws@0.4.4(srvx@0.8.16)):
dependencies:
cookie-es: 2.0.0
@ -35245,6 +35284,8 @@ snapshots:
lru.min@1.1.3: {}
lru_map@0.4.1: {}
lucide-react@0.378.0(react@19.2.1):
dependencies:
react: 19.2.1
@ -36105,11 +36146,11 @@ snapshots:
react: 18.3.1
use-intl: 3.19.1(react@18.3.1)
next-intl@3.19.1(next@16.2.9(@babel/core@7.26.0)(@opentelemetry/api@1.9.0)(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react@18.3.1):
next-intl@3.19.1(next@16.2.9(@babel/core@7.29.0)(@opentelemetry/api@1.9.0)(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react@18.3.1):
dependencies:
'@formatjs/intl-localematcher': 0.5.4
negotiator: 0.6.4
next: 16.2.9(@babel/core@7.26.0)(@opentelemetry/api@1.9.0)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)
next: 16.2.9(@babel/core@7.29.0)(@opentelemetry/api@1.9.0)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)
react: 18.3.1
use-intl: 3.19.1(react@18.3.1)
@ -36436,58 +36477,6 @@ snapshots:
jsonpath-plus: 10.4.0
lodash.topath: 4.5.2
nitro@3.0.0(@electric-sql/pglite@0.3.2)(chokidar@4.0.3)(lru-cache@11.2.2)(mysql2@3.15.3)(rolldown@1.0.0-rc.3)(vite@7.3.1(@types/node@22.19.0)(jiti@1.21.7)(lightningcss@1.32.0)(terser@5.44.0)(tsx@4.21.0)(yaml@2.8.0))(xml2js@0.6.2):
dependencies:
consola: 3.4.2
cookie-es: 2.0.0
crossws: 0.4.4(srvx@0.8.16)
db0: 0.3.4(@electric-sql/pglite@0.3.2)(mysql2@3.15.3)
esbuild: 0.25.11
fetchdts: 0.1.7
h3: 2.0.1-rc.2(crossws@0.4.4(srvx@0.8.16))
jiti: 2.6.1
nf3: 0.1.12
ofetch: 1.5.1
ohash: 2.0.11
rendu: 0.0.6
rollup: 4.57.1
srvx: 0.8.16
undici: 7.18.2
unenv: 2.0.0-rc.21
unstorage: 2.0.0-alpha.3(chokidar@4.0.3)(db0@0.3.4(@electric-sql/pglite@0.3.2)(mysql2@3.15.3))(lru-cache@11.2.2)(ofetch@1.5.1)
optionalDependencies:
rolldown: 1.0.0-rc.3
vite: 7.3.1(@types/node@22.19.0)(jiti@1.21.7)(lightningcss@1.32.0)(terser@5.44.0)(tsx@4.21.0)(yaml@2.8.0)
xml2js: 0.6.2
transitivePeerDependencies:
- '@azure/app-configuration'
- '@azure/cosmos'
- '@azure/data-tables'
- '@azure/identity'
- '@azure/keyvault-secrets'
- '@azure/storage-blob'
- '@capacitor/preferences'
- '@deno/kv'
- '@electric-sql/pglite'
- '@libsql/client'
- '@netlify/blobs'
- '@planetscale/database'
- '@upstash/redis'
- '@vercel/blob'
- '@vercel/functions'
- '@vercel/kv'
- aws4fetch
- better-sqlite3
- chokidar
- drizzle-orm
- idb-keyval
- ioredis
- lru-cache
- mongodb
- mysql2
- sqlite3
- uploadthing
nitro@3.0.0(@electric-sql/pglite@0.3.2)(chokidar@4.0.3)(lru-cache@11.2.2)(mysql2@3.15.3)(rolldown@1.0.0-rc.3)(vite@7.3.1(@types/node@22.19.0)(jiti@2.6.1)(lightningcss@1.32.0)(terser@5.44.0)(tsx@4.21.0)(yaml@2.8.0))(xml2js@0.6.2):
dependencies:
consola: 3.4.2
@ -36496,7 +36485,7 @@ snapshots:
db0: 0.3.4(@electric-sql/pglite@0.3.2)(mysql2@3.15.3)
esbuild: 0.25.11
fetchdts: 0.1.7
h3: 2.0.1-rc.2(crossws@0.4.4(srvx@0.11.15))
h3: 2.0.1-rc.2(crossws@0.4.4(srvx@0.8.16))
jiti: 2.6.1
nf3: 0.1.12
ofetch: 1.5.1
@ -40542,17 +40531,6 @@ snapshots:
- supports-color
- typescript
vite-tsconfig-paths@4.3.2(typescript@6.0.3)(vite@7.3.1(@types/node@22.19.0)(jiti@1.21.7)(lightningcss@1.32.0)(terser@5.44.0)(tsx@4.21.0)(yaml@2.8.0)):
dependencies:
debug: 4.4.3
globrex: 0.1.2
tsconfck: 3.1.5(typescript@6.0.3)
optionalDependencies:
vite: 7.3.1(@types/node@22.19.0)(jiti@1.21.7)(lightningcss@1.32.0)(terser@5.44.0)(tsx@4.21.0)(yaml@2.8.0)
transitivePeerDependencies:
- supports-color
- typescript
vite-tsconfig-paths@4.3.2(typescript@6.0.3)(vite@7.3.1(@types/node@22.19.0)(jiti@2.6.1)(lightningcss@1.32.0)(terser@5.44.0)(tsx@4.21.0)(yaml@2.8.0)):
dependencies:
debug: 4.4.3
@ -40642,23 +40620,6 @@ snapshots:
tsx: 4.19.3
yaml: 2.6.0
vite@7.3.1(@types/node@22.19.0)(jiti@1.21.7)(lightningcss@1.32.0)(terser@5.44.0)(tsx@4.21.0)(yaml@2.8.0):
dependencies:
esbuild: 0.27.1
fdir: 6.5.0(picomatch@4.0.3)
picomatch: 4.0.3
postcss: 8.5.6
rollup: 4.57.1
tinyglobby: 0.2.15
optionalDependencies:
'@types/node': 22.19.0
fsevents: 2.3.3
jiti: 1.21.7
lightningcss: 1.32.0
terser: 5.44.0
tsx: 4.21.0
yaml: 2.8.0
vite@7.3.1(@types/node@22.19.0)(jiti@2.6.1)(lightningcss@1.32.0)(terser@5.44.0)(tsx@4.21.0)(yaml@2.8.0):
dependencies:
esbuild: 0.27.1
@ -40676,10 +40637,6 @@ snapshots:
tsx: 4.21.0
yaml: 2.8.0
vitefu@1.1.2(vite@7.3.1(@types/node@22.19.0)(jiti@1.21.7)(lightningcss@1.32.0)(terser@5.44.0)(tsx@4.21.0)(yaml@2.8.0)):
optionalDependencies:
vite: 7.3.1(@types/node@22.19.0)(jiti@1.21.7)(lightningcss@1.32.0)(terser@5.44.0)(tsx@4.21.0)(yaml@2.8.0)
vitefu@1.1.2(vite@7.3.1(@types/node@22.19.0)(jiti@2.6.1)(lightningcss@1.32.0)(terser@5.44.0)(tsx@4.21.0)(yaml@2.8.0)):
optionalDependencies:
vite: 7.3.1(@types/node@22.19.0)(jiti@2.6.1)(lightningcss@1.32.0)(terser@5.44.0)(tsx@4.21.0)(yaml@2.8.0)