feat: add devflow doctor cli
This commit is contained in:
@@ -2,3 +2,4 @@ DATABASE_URL=postgres://devflow:devflow@127.0.0.1:55432/devflow
|
|||||||
WORKSPACE_ROOT=./data/workspace
|
WORKSPACE_ROOT=./data/workspace
|
||||||
LOG_LEVEL=info
|
LOG_LEVEL=info
|
||||||
DEVFLOW_POSTGRES_PORT=55432
|
DEVFLOW_POSTGRES_PORT=55432
|
||||||
|
DEVFLOW_BACKENDS_JSON=[{"id":"fake","enabled":true}]
|
||||||
|
|||||||
20
apps/cli/package.json
Normal file
20
apps/cli/package.json
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
{
|
||||||
|
"name": "@devflow/cli",
|
||||||
|
"version": "0.0.0",
|
||||||
|
"private": true,
|
||||||
|
"type": "module",
|
||||||
|
"bin": {
|
||||||
|
"devflow": "./dist/index.js"
|
||||||
|
},
|
||||||
|
"scripts": {
|
||||||
|
"build": "tsup src/index.ts --format esm --clean",
|
||||||
|
"typecheck": "tsc -p ../../tsconfig.typecheck.json --noEmit",
|
||||||
|
"test": "vitest run"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"commander": "12.1.0",
|
||||||
|
"dotenv": "17.4.2",
|
||||||
|
"pg": "8.20.0",
|
||||||
|
"zod": "3.24.1"
|
||||||
|
}
|
||||||
|
}
|
||||||
138
apps/cli/src/doctor.test.ts
Normal file
138
apps/cli/src/doctor.test.ts
Normal file
@@ -0,0 +1,138 @@
|
|||||||
|
import { describe, expect, it } from "vitest";
|
||||||
|
|
||||||
|
import {
|
||||||
|
type DoctorCommandRunner,
|
||||||
|
doctorExitCode,
|
||||||
|
formatDoctorJson,
|
||||||
|
formatDoctorTable,
|
||||||
|
runDoctor,
|
||||||
|
} from "./doctor.js";
|
||||||
|
|
||||||
|
const passingRunner: DoctorCommandRunner = async (command, args = []) => {
|
||||||
|
if (command === "docker" && args.join(" ") === "compose ps postgres") {
|
||||||
|
return {
|
||||||
|
exitCode: 0,
|
||||||
|
stdout: "postgres running\n",
|
||||||
|
stderr: "",
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
if (command === "docker" && args.includes("pg_isready")) {
|
||||||
|
return {
|
||||||
|
exitCode: 0,
|
||||||
|
stdout: "/var/run/postgresql:5432 - accepting connections\n",
|
||||||
|
stderr: "",
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
if (command === "df") {
|
||||||
|
return {
|
||||||
|
exitCode: 0,
|
||||||
|
stdout:
|
||||||
|
"Filesystem 1024-blocks Used Available Capacity Mounted on\n/dev/disk 20000000 1 19999999 1% /\n",
|
||||||
|
stderr: "",
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const stdoutByCommand: Record<string, string> = {
|
||||||
|
pnpm: "9.15.9\n",
|
||||||
|
tmux: "tmux 3.4\n",
|
||||||
|
git: "git version 2.45.0\n",
|
||||||
|
docker: "29.3.0\n",
|
||||||
|
};
|
||||||
|
|
||||||
|
return {
|
||||||
|
exitCode: 0,
|
||||||
|
stdout: stdoutByCommand[command] ?? "",
|
||||||
|
stderr: "",
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
describe("doctor", () => {
|
||||||
|
it("emits the closed M1 check set in order", async () => {
|
||||||
|
const results = await runDoctor({
|
||||||
|
commandRunner: passingRunner,
|
||||||
|
connectDatabase: async () => undefined,
|
||||||
|
countAppliedMigrations: async () => 1,
|
||||||
|
countExpectedMigrations: () => 1,
|
||||||
|
dockerComposePath: "docker",
|
||||||
|
env: {
|
||||||
|
DATABASE_URL: "postgres://devflow:devflow@127.0.0.1:55432/devflow",
|
||||||
|
WORKSPACE_ROOT: process.cwd(),
|
||||||
|
LOG_LEVEL: "info",
|
||||||
|
},
|
||||||
|
nodeVersion: "22.11.0",
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(results.map((result) => result.name)).toEqual([
|
||||||
|
"node",
|
||||||
|
"pnpm",
|
||||||
|
"tmux",
|
||||||
|
"git",
|
||||||
|
"docker",
|
||||||
|
"postgres",
|
||||||
|
"drizzle_migrations",
|
||||||
|
"workspace_root",
|
||||||
|
"config",
|
||||||
|
"codex",
|
||||||
|
"claude",
|
||||||
|
"workspace_disk",
|
||||||
|
]);
|
||||||
|
expect(doctorExitCode(results)).toBe(0);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("returns exit code 1 when any hard check fails", () => {
|
||||||
|
expect(
|
||||||
|
doctorExitCode([
|
||||||
|
{
|
||||||
|
name: "node",
|
||||||
|
status: "fail",
|
||||||
|
detail: "Node 21 is unsupported",
|
||||||
|
remediation: "Install Node 22",
|
||||||
|
},
|
||||||
|
]),
|
||||||
|
).toBe(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("surfaces command failures before parsing version output", async () => {
|
||||||
|
const results = await runDoctor({
|
||||||
|
commandRunner: async (command, args) => {
|
||||||
|
if (command === "tmux") {
|
||||||
|
return {
|
||||||
|
exitCode: 127,
|
||||||
|
stdout: "",
|
||||||
|
stderr: "command not found: tmux",
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return passingRunner(command, args);
|
||||||
|
},
|
||||||
|
connectDatabase: async () => undefined,
|
||||||
|
countAppliedMigrations: async () => 1,
|
||||||
|
countExpectedMigrations: () => 1,
|
||||||
|
env: {
|
||||||
|
DATABASE_URL: "postgres://devflow:devflow@127.0.0.1:55432/devflow",
|
||||||
|
WORKSPACE_ROOT: process.cwd(),
|
||||||
|
LOG_LEVEL: "info",
|
||||||
|
},
|
||||||
|
nodeVersion: "22.11.0",
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(results.find((result) => result.name === "tmux")).toMatchObject({
|
||||||
|
status: "fail",
|
||||||
|
detail: "command not found: tmux",
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("formats human and JSON output", () => {
|
||||||
|
const result = {
|
||||||
|
name: "node",
|
||||||
|
status: "pass" as const,
|
||||||
|
detail: "22.11.0",
|
||||||
|
remediation: "",
|
||||||
|
};
|
||||||
|
|
||||||
|
expect(formatDoctorTable([result])).toContain("node");
|
||||||
|
expect(JSON.parse(formatDoctorJson([result]))).toEqual([result]);
|
||||||
|
});
|
||||||
|
});
|
||||||
458
apps/cli/src/doctor.ts
Normal file
458
apps/cli/src/doctor.ts
Normal file
@@ -0,0 +1,458 @@
|
|||||||
|
import { execFile } from "node:child_process";
|
||||||
|
import { constants } from "node:fs";
|
||||||
|
import { access, readFile } from "node:fs/promises";
|
||||||
|
import { resolve } from "node:path";
|
||||||
|
import { promisify } from "node:util";
|
||||||
|
import { Pool } from "pg";
|
||||||
|
|
||||||
|
import { type Config, loadConfigFromSources } from "../../../packages/core/src/config.js";
|
||||||
|
|
||||||
|
const execFileAsync = promisify(execFile);
|
||||||
|
|
||||||
|
export type DoctorStatus = "pass" | "fail" | "warn";
|
||||||
|
|
||||||
|
export interface DoctorResult {
|
||||||
|
name: string;
|
||||||
|
status: DoctorStatus;
|
||||||
|
detail: string;
|
||||||
|
remediation: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface CommandResult {
|
||||||
|
exitCode: number;
|
||||||
|
stdout: string;
|
||||||
|
stderr: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export type DoctorCommandRunner = (
|
||||||
|
command: string,
|
||||||
|
args?: string[],
|
||||||
|
options?: { cwd?: string; env?: NodeJS.ProcessEnv },
|
||||||
|
) => Promise<CommandResult>;
|
||||||
|
|
||||||
|
export interface DoctorOptions {
|
||||||
|
cwd?: string;
|
||||||
|
env?: Record<string, string | undefined>;
|
||||||
|
nodeVersion?: string;
|
||||||
|
commandRunner?: DoctorCommandRunner;
|
||||||
|
connectDatabase?: (databaseUrl: string) => Promise<void>;
|
||||||
|
countAppliedMigrations?: (databaseUrl: string) => Promise<number>;
|
||||||
|
countExpectedMigrations?: (cwd: string) => Promise<number> | number;
|
||||||
|
dockerComposePath?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const MIN_DISK_KB = 5 * 1024 * 1024;
|
||||||
|
const WARN_DISK_KB = 10 * 1024 * 1024;
|
||||||
|
const FAIL_DISK_KB = 2 * 1024 * 1024;
|
||||||
|
|
||||||
|
export async function runDoctor(options: DoctorOptions = {}): Promise<DoctorResult[]> {
|
||||||
|
const cwd = options.cwd ?? process.cwd();
|
||||||
|
const env = options.env ?? process.env;
|
||||||
|
const commandRunner = options.commandRunner ?? defaultCommandRunner;
|
||||||
|
const dockerCommand = options.dockerComposePath ?? "docker";
|
||||||
|
const results: DoctorResult[] = [];
|
||||||
|
const configResult = loadDoctorConfig(cwd, env);
|
||||||
|
|
||||||
|
results.push(checkNodeVersion(options.nodeVersion ?? process.versions.node));
|
||||||
|
results.push(await checkCommandVersion("pnpm", ["--version"], "9.0.0", commandRunner));
|
||||||
|
results.push(await checkCommandVersion("tmux", ["-V"], "3.3.0", commandRunner));
|
||||||
|
results.push(await checkCommandVersion("git", ["--version"], "2.40.0", commandRunner));
|
||||||
|
results.push(await checkDocker(commandRunner));
|
||||||
|
results.push(
|
||||||
|
await checkPostgres({
|
||||||
|
commandRunner,
|
||||||
|
config: configResult.config,
|
||||||
|
configError: configResult.error,
|
||||||
|
connectDatabase: options.connectDatabase ?? defaultConnectDatabase,
|
||||||
|
dockerCommand,
|
||||||
|
cwd,
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
results.push(
|
||||||
|
await checkMigrations({
|
||||||
|
config: configResult.config,
|
||||||
|
configError: configResult.error,
|
||||||
|
countAppliedMigrations: options.countAppliedMigrations ?? defaultCountAppliedMigrations,
|
||||||
|
countExpectedMigrations: options.countExpectedMigrations ?? defaultCountExpectedMigrations,
|
||||||
|
cwd,
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
results.push(await checkWorkspaceRoot(configResult.config, configResult.error));
|
||||||
|
results.push(checkConfig(configResult.config, configResult.error));
|
||||||
|
results.push(await checkOptionalBinary("codex", commandRunner));
|
||||||
|
results.push(await checkOptionalBinary("claude", commandRunner));
|
||||||
|
results.push(await checkWorkspaceDisk(configResult.config, commandRunner));
|
||||||
|
|
||||||
|
return results;
|
||||||
|
}
|
||||||
|
|
||||||
|
function loadDoctorConfig(
|
||||||
|
cwd: string,
|
||||||
|
env: Record<string, string | undefined>,
|
||||||
|
): { config?: Config; error?: unknown } {
|
||||||
|
try {
|
||||||
|
return { config: loadConfigFromSources({ cwd, env }) };
|
||||||
|
} catch (error) {
|
||||||
|
return { error };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function checkNodeVersion(version: string): DoctorResult {
|
||||||
|
if (satisfiesMin(version, "22.0.0") && compareVersions(version, "23.0.0") < 0) {
|
||||||
|
return pass("node", version, "Node is within >=22.0.0 <23");
|
||||||
|
}
|
||||||
|
|
||||||
|
return fail("node", version, "Install Node 22 LTS and rerun doctor");
|
||||||
|
}
|
||||||
|
|
||||||
|
async function checkCommandVersion(
|
||||||
|
name: string,
|
||||||
|
args: string[],
|
||||||
|
minimum: string,
|
||||||
|
commandRunner: DoctorCommandRunner,
|
||||||
|
): Promise<DoctorResult> {
|
||||||
|
const result = await safeRun(commandRunner, name, args);
|
||||||
|
|
||||||
|
if (!result.ok) {
|
||||||
|
return fail(name, result.error, `Install ${name} >= ${minimum}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (result.value.exitCode !== 0) {
|
||||||
|
return fail(
|
||||||
|
name,
|
||||||
|
(result.value.stderr || result.value.stdout).trim(),
|
||||||
|
`Install ${name} >= ${minimum}`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const version = extractVersion(result.value.stdout);
|
||||||
|
if (!version) {
|
||||||
|
return fail(name, result.value.stdout.trim(), `Ensure ${name} reports a version`);
|
||||||
|
}
|
||||||
|
|
||||||
|
return satisfiesMin(version, minimum)
|
||||||
|
? pass(name, version, `${name} satisfies >= ${minimum}`)
|
||||||
|
: fail(name, version, `Upgrade ${name} to >= ${minimum}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function checkDocker(commandRunner: DoctorCommandRunner): Promise<DoctorResult> {
|
||||||
|
const result = await safeRun(commandRunner, "docker", ["info", "--format", "{{.ServerVersion}}"]);
|
||||||
|
|
||||||
|
if (!result.ok) {
|
||||||
|
return fail("docker", result.error, "Start Docker and ensure docker is in PATH");
|
||||||
|
}
|
||||||
|
|
||||||
|
return result.value.exitCode === 0
|
||||||
|
? pass("docker", result.value.stdout.trim(), "Docker daemon is reachable")
|
||||||
|
: fail("docker", result.value.stderr.trim(), "Start Docker and rerun doctor");
|
||||||
|
}
|
||||||
|
|
||||||
|
async function checkPostgres(input: {
|
||||||
|
commandRunner: DoctorCommandRunner;
|
||||||
|
config: Config | undefined;
|
||||||
|
configError: unknown;
|
||||||
|
connectDatabase: (databaseUrl: string) => Promise<void>;
|
||||||
|
dockerCommand: string;
|
||||||
|
cwd: string;
|
||||||
|
}): Promise<DoctorResult> {
|
||||||
|
if (!input.config) {
|
||||||
|
return fail("postgres", errorDetail(input.configError), "Fix Config before checking Postgres");
|
||||||
|
}
|
||||||
|
|
||||||
|
const compose = await safeRun(
|
||||||
|
input.commandRunner,
|
||||||
|
input.dockerCommand,
|
||||||
|
["compose", "ps", "postgres"],
|
||||||
|
{
|
||||||
|
cwd: input.cwd,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!compose.ok || compose.value.exitCode !== 0 || !compose.value.stdout.includes("postgres")) {
|
||||||
|
return fail(
|
||||||
|
"postgres",
|
||||||
|
compose.ok ? compose.value.stderr : compose.error,
|
||||||
|
"Run docker compose up -d postgres",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const ready = await safeRun(input.commandRunner, input.dockerCommand, [
|
||||||
|
"compose",
|
||||||
|
"exec",
|
||||||
|
"-T",
|
||||||
|
"postgres",
|
||||||
|
"pg_isready",
|
||||||
|
"-U",
|
||||||
|
"devflow",
|
||||||
|
"-d",
|
||||||
|
"devflow",
|
||||||
|
]);
|
||||||
|
|
||||||
|
if (!ready.ok || ready.value.exitCode !== 0) {
|
||||||
|
return fail(
|
||||||
|
"postgres",
|
||||||
|
ready.ok ? ready.value.stderr : ready.error,
|
||||||
|
"Wait for Postgres healthcheck to pass",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
await input.connectDatabase(input.config.DATABASE_URL);
|
||||||
|
return pass("postgres", "connected", "Postgres is reachable and accepts DATABASE_URL");
|
||||||
|
} catch (error) {
|
||||||
|
return fail("postgres", errorDetail(error), "Check DATABASE_URL and container health");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function checkMigrations(input: {
|
||||||
|
config: Config | undefined;
|
||||||
|
configError: unknown;
|
||||||
|
countAppliedMigrations: (databaseUrl: string) => Promise<number>;
|
||||||
|
countExpectedMigrations: (cwd: string) => Promise<number> | number;
|
||||||
|
cwd: string;
|
||||||
|
}): Promise<DoctorResult> {
|
||||||
|
if (!input.config) {
|
||||||
|
return fail(
|
||||||
|
"drizzle_migrations",
|
||||||
|
errorDetail(input.configError),
|
||||||
|
"Fix Config before checking migrations",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const [applied, expected] = await Promise.all([
|
||||||
|
input.countAppliedMigrations(input.config.DATABASE_URL),
|
||||||
|
input.countExpectedMigrations(input.cwd),
|
||||||
|
]);
|
||||||
|
|
||||||
|
if (applied >= expected) {
|
||||||
|
return pass("drizzle_migrations", `${applied}/${expected}`, "No pending migrations");
|
||||||
|
}
|
||||||
|
|
||||||
|
return fail("drizzle_migrations", `${applied}/${expected}`, "Run pnpm db:migrate");
|
||||||
|
} catch (error) {
|
||||||
|
return fail(
|
||||||
|
"drizzle_migrations",
|
||||||
|
errorDetail(error),
|
||||||
|
"Run pnpm db:migrate after Postgres is healthy",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function checkWorkspaceRoot(config?: Config, configError?: unknown): Promise<DoctorResult> {
|
||||||
|
if (!config) {
|
||||||
|
return fail(
|
||||||
|
"workspace_root",
|
||||||
|
errorDetail(configError),
|
||||||
|
"Set WORKSPACE_ROOT to an existing writable path",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
await access(config.WORKSPACE_ROOT, constants.W_OK);
|
||||||
|
return pass("workspace_root", config.WORKSPACE_ROOT, "Workspace root is writable");
|
||||||
|
} catch (error) {
|
||||||
|
return fail("workspace_root", errorDetail(error), "Create WORKSPACE_ROOT and make it writable");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function checkConfig(config?: Config, configError?: unknown): DoctorResult {
|
||||||
|
return config
|
||||||
|
? pass("config", "valid", ".env resolved to a valid Config")
|
||||||
|
: fail("config", errorDetail(configError), "Set DATABASE_URL, WORKSPACE_ROOT, and LOG_LEVEL");
|
||||||
|
}
|
||||||
|
|
||||||
|
async function checkOptionalBinary(
|
||||||
|
name: "codex" | "claude",
|
||||||
|
commandRunner: DoctorCommandRunner,
|
||||||
|
): Promise<DoctorResult> {
|
||||||
|
const result = await safeRun(commandRunner, name, ["--version"]);
|
||||||
|
|
||||||
|
return result.ok && result.value.exitCode === 0
|
||||||
|
? pass(name, result.value.stdout.trim(), `${name} backend can be enabled`)
|
||||||
|
: warn(name, "not found", `${name} is only required for real backend opt-in`);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function checkWorkspaceDisk(
|
||||||
|
config: Config | undefined,
|
||||||
|
commandRunner: DoctorCommandRunner,
|
||||||
|
): Promise<DoctorResult> {
|
||||||
|
if (!config) {
|
||||||
|
return warn("workspace_disk", "unknown", "Fix Config before checking free disk");
|
||||||
|
}
|
||||||
|
|
||||||
|
const result = await safeRun(commandRunner, "df", ["-Pk", config.WORKSPACE_ROOT]);
|
||||||
|
if (!result.ok || result.value.exitCode !== 0) {
|
||||||
|
return warn(
|
||||||
|
"workspace_disk",
|
||||||
|
result.ok ? result.value.stderr : result.error,
|
||||||
|
"Ensure df is available",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const availableKb = Number.parseInt(result.value.stdout.trim().split(/\s+/).at(-3) ?? "", 10);
|
||||||
|
if (Number.isNaN(availableKb)) {
|
||||||
|
return warn("workspace_disk", result.value.stdout.trim(), "Unable to parse df output");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (availableKb < FAIL_DISK_KB) {
|
||||||
|
return fail(
|
||||||
|
"workspace_disk",
|
||||||
|
`${availableKb} KB free`,
|
||||||
|
"Free at least 5GB under WORKSPACE_ROOT",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (availableKb < WARN_DISK_KB || availableKb < MIN_DISK_KB) {
|
||||||
|
return warn(
|
||||||
|
"workspace_disk",
|
||||||
|
`${availableKb} KB free`,
|
||||||
|
"Free space is below the recommended 10GB",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return pass(
|
||||||
|
"workspace_disk",
|
||||||
|
`${availableKb} KB free`,
|
||||||
|
"Workspace partition has enough free space",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function doctorExitCode(results: DoctorResult[]): 0 | 1 {
|
||||||
|
return results.some((result) => result.status === "fail") ? 1 : 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function formatDoctorJson(results: DoctorResult[]): string {
|
||||||
|
return `${JSON.stringify(results, null, 2)}\n`;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function formatDoctorTable(results: DoctorResult[]): string {
|
||||||
|
const nameWidth = Math.max(...results.map((result) => result.name.length), "check".length);
|
||||||
|
const statusWidth = "status".length;
|
||||||
|
const lines = [
|
||||||
|
`${"check".padEnd(nameWidth)} ${"status".padEnd(statusWidth)} detail`,
|
||||||
|
`${"-".repeat(nameWidth)} ${"-".repeat(statusWidth)} ${"-".repeat(6)}`,
|
||||||
|
...results.map(
|
||||||
|
(result) =>
|
||||||
|
`${result.name.padEnd(nameWidth)} ${result.status.padEnd(statusWidth)} ${result.detail}${
|
||||||
|
result.remediation ? ` (${result.remediation})` : ""
|
||||||
|
}`,
|
||||||
|
),
|
||||||
|
];
|
||||||
|
|
||||||
|
return `${lines.join("\n")}\n`;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function defaultCommandRunner(
|
||||||
|
command: string,
|
||||||
|
args: string[] = [],
|
||||||
|
options: { cwd?: string; env?: NodeJS.ProcessEnv } = {},
|
||||||
|
): Promise<CommandResult> {
|
||||||
|
try {
|
||||||
|
const result = await execFileAsync(command, args, {
|
||||||
|
cwd: options.cwd,
|
||||||
|
env: options.env,
|
||||||
|
timeout: 15_000,
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
exitCode: 0,
|
||||||
|
stdout: result.stdout,
|
||||||
|
stderr: result.stderr,
|
||||||
|
};
|
||||||
|
} catch (error) {
|
||||||
|
const maybeError = error as {
|
||||||
|
code?: string | number;
|
||||||
|
stdout?: string;
|
||||||
|
stderr?: string;
|
||||||
|
message?: string;
|
||||||
|
};
|
||||||
|
return {
|
||||||
|
exitCode: typeof maybeError.code === "number" ? maybeError.code : 1,
|
||||||
|
stdout: maybeError.stdout ?? "",
|
||||||
|
stderr: maybeError.stderr || maybeError.message || String(error),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function defaultConnectDatabase(databaseUrl: string): Promise<void> {
|
||||||
|
const pool = new Pool({ connectionString: databaseUrl });
|
||||||
|
try {
|
||||||
|
await pool.query("select 1");
|
||||||
|
} finally {
|
||||||
|
await pool.end();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function defaultCountAppliedMigrations(databaseUrl: string): Promise<number> {
|
||||||
|
const pool = new Pool({ connectionString: databaseUrl });
|
||||||
|
try {
|
||||||
|
const result = await pool.query<{ count: string }>(
|
||||||
|
"select count(*)::text as count from drizzle.__drizzle_migrations",
|
||||||
|
);
|
||||||
|
|
||||||
|
return Number.parseInt(result.rows[0]?.count ?? "0", 10);
|
||||||
|
} catch {
|
||||||
|
return 0;
|
||||||
|
} finally {
|
||||||
|
await pool.end();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function defaultCountExpectedMigrations(cwd: string): Promise<number> {
|
||||||
|
const journalPath = resolve(cwd, "packages/db/src/migrations/meta/_journal.json");
|
||||||
|
const journal = JSON.parse(await readFile(journalPath, "utf8")) as { entries?: unknown[] };
|
||||||
|
|
||||||
|
return journal.entries?.length ?? 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function safeRun(
|
||||||
|
commandRunner: DoctorCommandRunner,
|
||||||
|
command: string,
|
||||||
|
args: string[] = [],
|
||||||
|
options?: { cwd?: string; env?: NodeJS.ProcessEnv },
|
||||||
|
): Promise<{ ok: true; value: CommandResult } | { ok: false; error: string }> {
|
||||||
|
try {
|
||||||
|
return { ok: true, value: await commandRunner(command, args, options) };
|
||||||
|
} catch (error) {
|
||||||
|
return { ok: false, error: errorDetail(error) };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function extractVersion(output: string): string | undefined {
|
||||||
|
return output.match(/\d+\.\d+\.\d+/)?.[0] ?? output.match(/\d+\.\d+/)?.[0];
|
||||||
|
}
|
||||||
|
|
||||||
|
function satisfiesMin(version: string, minimum: string): boolean {
|
||||||
|
return compareVersions(version, minimum) >= 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
function compareVersions(left: string, right: string): number {
|
||||||
|
const leftParts = left.split(".").map((part) => Number.parseInt(part, 10));
|
||||||
|
const rightParts = right.split(".").map((part) => Number.parseInt(part, 10));
|
||||||
|
|
||||||
|
for (let index = 0; index < 3; index += 1) {
|
||||||
|
const diff = (leftParts[index] ?? 0) - (rightParts[index] ?? 0);
|
||||||
|
if (diff !== 0) {
|
||||||
|
return diff;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
function pass(name: string, detail: string, remediation: string): DoctorResult {
|
||||||
|
return { name, status: "pass", detail, remediation };
|
||||||
|
}
|
||||||
|
|
||||||
|
function fail(name: string, detail: string, remediation: string): DoctorResult {
|
||||||
|
return { name, status: "fail", detail, remediation };
|
||||||
|
}
|
||||||
|
|
||||||
|
function warn(name: string, detail: string, remediation: string): DoctorResult {
|
||||||
|
return { name, status: "warn", detail, remediation };
|
||||||
|
}
|
||||||
|
|
||||||
|
function errorDetail(error: unknown): string {
|
||||||
|
return error instanceof Error ? error.message : String(error);
|
||||||
|
}
|
||||||
37
apps/cli/src/index.ts
Normal file
37
apps/cli/src/index.ts
Normal file
@@ -0,0 +1,37 @@
|
|||||||
|
#!/usr/bin/env node
|
||||||
|
|
||||||
|
import { Command } from "commander";
|
||||||
|
|
||||||
|
import { doctorExitCode, formatDoctorJson, formatDoctorTable, runDoctor } from "./doctor.js";
|
||||||
|
|
||||||
|
const program = new Command();
|
||||||
|
|
||||||
|
program.name("devflow").description("Local agentic engineering workflow runner");
|
||||||
|
|
||||||
|
program
|
||||||
|
.command("doctor")
|
||||||
|
.description("Check local Devflow prerequisites")
|
||||||
|
.option("--json", "print machine-readable JSON")
|
||||||
|
.option("--quiet", "print only non-passing checks")
|
||||||
|
.action(async (options: { json?: boolean; quiet?: boolean }) => {
|
||||||
|
try {
|
||||||
|
const results = await runDoctor();
|
||||||
|
const visibleResults = options.quiet
|
||||||
|
? results.filter((result) => result.status !== "pass")
|
||||||
|
: results;
|
||||||
|
|
||||||
|
if (options.json) {
|
||||||
|
process.stdout.write(formatDoctorJson(visibleResults));
|
||||||
|
} else if (!options.quiet || visibleResults.length > 0) {
|
||||||
|
process.stdout.write(formatDoctorTable(visibleResults));
|
||||||
|
}
|
||||||
|
|
||||||
|
process.exitCode = doctorExitCode(results);
|
||||||
|
} catch (error) {
|
||||||
|
const message = error instanceof Error ? error.message : String(error);
|
||||||
|
process.stderr.write(`${message}\n`);
|
||||||
|
process.exitCode = 2;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
await program.parseAsync(process.argv);
|
||||||
10
apps/cli/tsconfig.json
Normal file
10
apps/cli/tsconfig.json
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
{
|
||||||
|
"extends": "../../tsconfig.base.json",
|
||||||
|
"compilerOptions": {
|
||||||
|
"rootDir": "src",
|
||||||
|
"outDir": "dist",
|
||||||
|
"types": ["node", "vitest"]
|
||||||
|
},
|
||||||
|
"include": ["src/**/*.ts"],
|
||||||
|
"references": [{ "path": "../../packages/core" }]
|
||||||
|
}
|
||||||
@@ -12,7 +12,8 @@
|
|||||||
"build": "tsc -b",
|
"build": "tsc -b",
|
||||||
"db:generate": "drizzle-kit generate",
|
"db:generate": "drizzle-kit generate",
|
||||||
"db:migrate": "tsx scripts/migrate.ts",
|
"db:migrate": "tsx scripts/migrate.ts",
|
||||||
"typecheck": "tsc -b --noEmit",
|
"devflow": "tsx apps/cli/src/index.ts",
|
||||||
|
"typecheck": "tsc -p tsconfig.typecheck.json --noEmit",
|
||||||
"test": "vitest run",
|
"test": "vitest run",
|
||||||
"test:watch": "vitest",
|
"test:watch": "vitest",
|
||||||
"coverage": "vitest run --coverage",
|
"coverage": "vitest run --coverage",
|
||||||
@@ -33,8 +34,10 @@
|
|||||||
"vitest": "2.1.8"
|
"vitest": "2.1.8"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"commander": "12.1.0",
|
||||||
"dotenv": "17.4.2",
|
"dotenv": "17.4.2",
|
||||||
"drizzle-orm": "0.45.2",
|
"drizzle-orm": "0.45.2",
|
||||||
"pg": "8.20.0"
|
"pg": "8.20.0",
|
||||||
|
"zod": "3.24.1"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
18
packages/core/package.json
Normal file
18
packages/core/package.json
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
{
|
||||||
|
"name": "@devflow/core",
|
||||||
|
"version": "0.0.0",
|
||||||
|
"private": true,
|
||||||
|
"type": "module",
|
||||||
|
"main": "./dist/index.cjs",
|
||||||
|
"module": "./dist/index.js",
|
||||||
|
"types": "./dist/index.d.ts",
|
||||||
|
"scripts": {
|
||||||
|
"build": "tsup src/index.ts --format esm,cjs --dts",
|
||||||
|
"typecheck": "tsc -b --noEmit",
|
||||||
|
"test": "vitest run"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"dotenv": "17.4.2",
|
||||||
|
"zod": "3.24.1"
|
||||||
|
}
|
||||||
|
}
|
||||||
74
packages/core/src/config.test.ts
Normal file
74
packages/core/src/config.test.ts
Normal file
@@ -0,0 +1,74 @@
|
|||||||
|
import { mkdirSync, mkdtempSync, realpathSync, writeFileSync } from "node:fs";
|
||||||
|
import { tmpdir } from "node:os";
|
||||||
|
import { join } from "node:path";
|
||||||
|
import { describe, expect, it } from "vitest";
|
||||||
|
|
||||||
|
import { loadConfigFromSources } from "./config.js";
|
||||||
|
|
||||||
|
describe("config loader", () => {
|
||||||
|
it("loads .env, .env.local, then process env in descending precedence", () => {
|
||||||
|
const root = mkdtempSync(join(tmpdir(), "devflow-config-"));
|
||||||
|
const workspace = join(root, "workspace");
|
||||||
|
mkdirSync(workspace);
|
||||||
|
writeFileSync(
|
||||||
|
join(root, ".env"),
|
||||||
|
[
|
||||||
|
"DATABASE_URL=postgres://env:env@localhost:5432/env",
|
||||||
|
"WORKSPACE_ROOT=workspace",
|
||||||
|
"LOG_LEVEL=warn",
|
||||||
|
].join("\n"),
|
||||||
|
);
|
||||||
|
writeFileSync(join(root, ".env.local"), "LOG_LEVEL=debug\n");
|
||||||
|
|
||||||
|
const config = loadConfigFromSources({
|
||||||
|
cwd: root,
|
||||||
|
env: {
|
||||||
|
DATABASE_URL: "postgres://process:process@localhost:5432/process",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(config.DATABASE_URL).toBe("postgres://process:process@localhost:5432/process");
|
||||||
|
expect(config.LOG_LEVEL).toBe("debug");
|
||||||
|
expect(config.WORKSPACE_ROOT).toBe(realpathSync(workspace));
|
||||||
|
});
|
||||||
|
|
||||||
|
it("always exposes the fake backend as enabled", () => {
|
||||||
|
const root = mkdtempSync(join(tmpdir(), "devflow-config-"));
|
||||||
|
const workspace = join(root, "workspace");
|
||||||
|
mkdirSync(workspace);
|
||||||
|
|
||||||
|
const config = loadConfigFromSources({
|
||||||
|
cwd: root,
|
||||||
|
env: {
|
||||||
|
DATABASE_URL: "postgres://devflow:devflow@localhost:5432/devflow",
|
||||||
|
WORKSPACE_ROOT: workspace,
|
||||||
|
LOG_LEVEL: "info",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(config.backends).toContainEqual({ id: "fake", enabled: true });
|
||||||
|
});
|
||||||
|
|
||||||
|
it("parses backend registration from DEVFLOW_BACKENDS_JSON", () => {
|
||||||
|
const root = mkdtempSync(join(tmpdir(), "devflow-config-"));
|
||||||
|
const workspace = join(root, "workspace");
|
||||||
|
mkdirSync(workspace);
|
||||||
|
|
||||||
|
const config = loadConfigFromSources({
|
||||||
|
cwd: root,
|
||||||
|
env: {
|
||||||
|
DATABASE_URL: "postgres://devflow:devflow@localhost:5432/devflow",
|
||||||
|
WORKSPACE_ROOT: workspace,
|
||||||
|
LOG_LEVEL: "info",
|
||||||
|
DEVFLOW_BACKENDS_JSON: JSON.stringify([
|
||||||
|
{ id: "codex", enabled: true, binaryPath: "/usr/local/bin/codex" },
|
||||||
|
]),
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(config.backends).toEqual([
|
||||||
|
{ id: "fake", enabled: true },
|
||||||
|
{ id: "codex", enabled: true, binaryPath: "/usr/local/bin/codex" },
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
});
|
||||||
93
packages/core/src/config.ts
Normal file
93
packages/core/src/config.ts
Normal file
@@ -0,0 +1,93 @@
|
|||||||
|
import { existsSync, readFileSync, realpathSync } from "node:fs";
|
||||||
|
import { resolve } from "node:path";
|
||||||
|
import { parse } from "dotenv";
|
||||||
|
import { z } from "zod";
|
||||||
|
|
||||||
|
import { Backend } from "./enums.js";
|
||||||
|
|
||||||
|
const LogLevel = z.enum(["trace", "debug", "info", "warn", "error"]);
|
||||||
|
|
||||||
|
export const BackendConfig = z.object({
|
||||||
|
id: Backend,
|
||||||
|
enabled: z.boolean(),
|
||||||
|
binaryPath: z.string().optional(),
|
||||||
|
});
|
||||||
|
|
||||||
|
export const ConfigSchema = z
|
||||||
|
.object({
|
||||||
|
DATABASE_URL: z.string().min(1),
|
||||||
|
WORKSPACE_ROOT: z.string().min(1),
|
||||||
|
LOG_LEVEL: LogLevel.default("info"),
|
||||||
|
TEMPORAL_ADDRESS: z.string().optional(),
|
||||||
|
MAX_CONCURRENT_RUNS: z.coerce.number().int().positive().default(4),
|
||||||
|
backends: z.array(BackendConfig).default([{ id: "fake", enabled: true }]),
|
||||||
|
})
|
||||||
|
.transform((value) => {
|
||||||
|
const canonicalWorkspaceRoot = realpathSync(resolve(value.WORKSPACE_ROOT));
|
||||||
|
const hasFakeBackend = value.backends.some((backend) => backend.id === "fake");
|
||||||
|
|
||||||
|
return Object.freeze({
|
||||||
|
...value,
|
||||||
|
WORKSPACE_ROOT: canonicalWorkspaceRoot,
|
||||||
|
backends: Object.freeze(
|
||||||
|
hasFakeBackend
|
||||||
|
? value.backends
|
||||||
|
: [{ id: "fake" as const, enabled: true }, ...value.backends],
|
||||||
|
),
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
export type BackendConfig = z.infer<typeof BackendConfig>;
|
||||||
|
export type Config = z.infer<typeof ConfigSchema>;
|
||||||
|
|
||||||
|
export interface LoadConfigOptions {
|
||||||
|
cwd?: string;
|
||||||
|
env?: Record<string, string | undefined>;
|
||||||
|
}
|
||||||
|
|
||||||
|
function readEnvFile(cwd: string, fileName: string): Record<string, string> {
|
||||||
|
const path = resolve(cwd, fileName);
|
||||||
|
|
||||||
|
if (!existsSync(path)) {
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
|
||||||
|
return parse(readFileSync(path));
|
||||||
|
}
|
||||||
|
|
||||||
|
export function loadConfigFromSources(options: LoadConfigOptions = {}): Config {
|
||||||
|
const cwd = options.cwd ?? process.cwd();
|
||||||
|
const env = options.env ?? process.env;
|
||||||
|
const raw = {
|
||||||
|
...readEnvFile(cwd, ".env"),
|
||||||
|
...readEnvFile(cwd, ".env.local"),
|
||||||
|
...env,
|
||||||
|
};
|
||||||
|
const normalizedRaw = normalizeRawConfig(raw);
|
||||||
|
|
||||||
|
if (typeof normalizedRaw.WORKSPACE_ROOT === "string") {
|
||||||
|
normalizedRaw.WORKSPACE_ROOT = resolve(cwd, normalizedRaw.WORKSPACE_ROOT);
|
||||||
|
}
|
||||||
|
|
||||||
|
return ConfigSchema.parse(normalizedRaw);
|
||||||
|
}
|
||||||
|
|
||||||
|
function normalizeRawConfig(raw: Record<string, string | undefined>): Record<string, unknown> {
|
||||||
|
const backendsJson = raw.DEVFLOW_BACKENDS_JSON;
|
||||||
|
|
||||||
|
if (!backendsJson) {
|
||||||
|
return raw;
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
...raw,
|
||||||
|
backends: JSON.parse(backendsJson) as unknown,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
let cachedConfig: Config | undefined;
|
||||||
|
|
||||||
|
export function getConfig(): Config {
|
||||||
|
cachedConfig ??= loadConfigFromSources();
|
||||||
|
return cachedConfig;
|
||||||
|
}
|
||||||
5
packages/core/src/enums.ts
Normal file
5
packages/core/src/enums.ts
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
import { z } from "zod";
|
||||||
|
|
||||||
|
export const BackendValues = ["codex", "claude", "fake"] as const;
|
||||||
|
export const Backend = z.enum(BackendValues);
|
||||||
|
export type Backend = z.infer<typeof Backend>;
|
||||||
2
packages/core/src/index.ts
Normal file
2
packages/core/src/index.ts
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
export * from "./config.js";
|
||||||
|
export * from "./enums.js";
|
||||||
9
packages/core/tsconfig.json
Normal file
9
packages/core/tsconfig.json
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
{
|
||||||
|
"extends": "../../tsconfig.base.json",
|
||||||
|
"compilerOptions": {
|
||||||
|
"rootDir": "src",
|
||||||
|
"outDir": "dist",
|
||||||
|
"types": ["node", "vitest"]
|
||||||
|
},
|
||||||
|
"include": ["src/**/*.ts"]
|
||||||
|
}
|
||||||
36
pnpm-lock.yaml
generated
36
pnpm-lock.yaml
generated
@@ -8,15 +8,21 @@ importers:
|
|||||||
|
|
||||||
.:
|
.:
|
||||||
dependencies:
|
dependencies:
|
||||||
|
commander:
|
||||||
|
specifier: 12.1.0
|
||||||
|
version: 12.1.0
|
||||||
dotenv:
|
dotenv:
|
||||||
specifier: ^17.4.2
|
specifier: 17.4.2
|
||||||
version: 17.4.2
|
version: 17.4.2
|
||||||
drizzle-orm:
|
drizzle-orm:
|
||||||
specifier: ^0.45.2
|
specifier: 0.45.2
|
||||||
version: 0.45.2(@types/pg@8.20.0)(pg@8.20.0)
|
version: 0.45.2(@types/pg@8.20.0)(pg@8.20.0)
|
||||||
pg:
|
pg:
|
||||||
specifier: ^8.20.0
|
specifier: 8.20.0
|
||||||
version: 8.20.0
|
version: 8.20.0
|
||||||
|
zod:
|
||||||
|
specifier: 3.24.1
|
||||||
|
version: 3.24.1
|
||||||
devDependencies:
|
devDependencies:
|
||||||
'@biomejs/biome':
|
'@biomejs/biome':
|
||||||
specifier: 1.9.4
|
specifier: 1.9.4
|
||||||
@@ -25,13 +31,13 @@ importers:
|
|||||||
specifier: 22.10.2
|
specifier: 22.10.2
|
||||||
version: 22.10.2
|
version: 22.10.2
|
||||||
'@types/pg':
|
'@types/pg':
|
||||||
specifier: ^8.20.0
|
specifier: 8.20.0
|
||||||
version: 8.20.0
|
version: 8.20.0
|
||||||
'@vitest/coverage-v8':
|
'@vitest/coverage-v8':
|
||||||
specifier: 2.1.8
|
specifier: 2.1.8
|
||||||
version: 2.1.8(vitest@2.1.8(@types/node@22.10.2))
|
version: 2.1.8(vitest@2.1.8(@types/node@22.10.2))
|
||||||
drizzle-kit:
|
drizzle-kit:
|
||||||
specifier: ^0.31.10
|
specifier: 0.31.10
|
||||||
version: 0.31.10
|
version: 0.31.10
|
||||||
lefthook:
|
lefthook:
|
||||||
specifier: 2.1.6
|
specifier: 2.1.6
|
||||||
@@ -52,6 +58,15 @@ importers:
|
|||||||
specifier: 2.1.8
|
specifier: 2.1.8
|
||||||
version: 2.1.8(@types/node@22.10.2)
|
version: 2.1.8(@types/node@22.10.2)
|
||||||
|
|
||||||
|
packages/db:
|
||||||
|
dependencies:
|
||||||
|
drizzle-orm:
|
||||||
|
specifier: 0.45.2
|
||||||
|
version: 0.45.2(@types/pg@8.20.0)(pg@8.20.0)
|
||||||
|
pg:
|
||||||
|
specifier: 8.20.0
|
||||||
|
version: 8.20.0
|
||||||
|
|
||||||
packages:
|
packages:
|
||||||
|
|
||||||
'@ampproject/remapping@2.3.0':
|
'@ampproject/remapping@2.3.0':
|
||||||
@@ -1290,6 +1305,10 @@ packages:
|
|||||||
color-name@1.1.4:
|
color-name@1.1.4:
|
||||||
resolution: {integrity: sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==}
|
resolution: {integrity: sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==}
|
||||||
|
|
||||||
|
commander@12.1.0:
|
||||||
|
resolution: {integrity: sha512-Vw8qHK3bZM9y/P10u3Vib8o/DdkvA2OtPtZvD871QKjy74Wj1WSKFILMPRPSdUSx5RFK1arlJzEtA4PkFgnbuA==}
|
||||||
|
engines: {node: '>=18'}
|
||||||
|
|
||||||
commander@4.1.1:
|
commander@4.1.1:
|
||||||
resolution: {integrity: sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA==}
|
resolution: {integrity: sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA==}
|
||||||
engines: {node: '>= 6'}
|
engines: {node: '>= 6'}
|
||||||
@@ -2039,6 +2058,9 @@ packages:
|
|||||||
resolution: {integrity: sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==}
|
resolution: {integrity: sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==}
|
||||||
engines: {node: '>=0.4'}
|
engines: {node: '>=0.4'}
|
||||||
|
|
||||||
|
zod@3.24.1:
|
||||||
|
resolution: {integrity: sha512-muH7gBL9sI1nciMZV67X5fTKKBLtwpZ5VBp1vsOQzj1MhrBZ4wlVCm3gedKZWLp0Oyel8sIGfeiz54Su+OVT+A==}
|
||||||
|
|
||||||
snapshots:
|
snapshots:
|
||||||
|
|
||||||
'@ampproject/remapping@2.3.0':
|
'@ampproject/remapping@2.3.0':
|
||||||
@@ -2780,6 +2802,8 @@ snapshots:
|
|||||||
|
|
||||||
color-name@1.1.4: {}
|
color-name@1.1.4: {}
|
||||||
|
|
||||||
|
commander@12.1.0: {}
|
||||||
|
|
||||||
commander@4.1.1: {}
|
commander@4.1.1: {}
|
||||||
|
|
||||||
consola@3.4.2: {}
|
consola@3.4.2: {}
|
||||||
@@ -3506,3 +3530,5 @@ snapshots:
|
|||||||
strip-ansi: 7.2.0
|
strip-ansi: 7.2.0
|
||||||
|
|
||||||
xtend@4.0.2: {}
|
xtend@4.0.2: {}
|
||||||
|
|
||||||
|
zod@3.24.1: {}
|
||||||
|
|||||||
@@ -5,5 +5,9 @@
|
|||||||
"types": ["node", "vitest"]
|
"types": ["node", "vitest"]
|
||||||
},
|
},
|
||||||
"include": ["vitest.workspace.ts", "tests/**/*.ts"],
|
"include": ["vitest.workspace.ts", "tests/**/*.ts"],
|
||||||
"references": [{ "path": "./packages/db" }]
|
"references": [
|
||||||
|
{ "path": "./packages/core" },
|
||||||
|
{ "path": "./packages/db" },
|
||||||
|
{ "path": "./apps/cli" }
|
||||||
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
11
tsconfig.typecheck.json
Normal file
11
tsconfig.typecheck.json
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
{
|
||||||
|
"extends": "./tsconfig.base.json",
|
||||||
|
"compilerOptions": {
|
||||||
|
"composite": false,
|
||||||
|
"declaration": false,
|
||||||
|
"declarationMap": false,
|
||||||
|
"noEmit": true,
|
||||||
|
"types": ["node", "vitest"]
|
||||||
|
},
|
||||||
|
"include": ["apps/**/*.ts", "packages/**/*.ts", "scripts/**/*.ts", "tests/**/*.ts", "*.ts"]
|
||||||
|
}
|
||||||
@@ -15,4 +15,18 @@ export default defineWorkspace([
|
|||||||
environment: "node",
|
environment: "node",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
test: {
|
||||||
|
name: "packages/core",
|
||||||
|
include: ["packages/core/src/**/*.test.ts"],
|
||||||
|
environment: "node",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
test: {
|
||||||
|
name: "apps/cli",
|
||||||
|
include: ["apps/cli/src/**/*.test.ts"],
|
||||||
|
environment: "node",
|
||||||
|
},
|
||||||
|
},
|
||||||
]);
|
]);
|
||||||
|
|||||||
Reference in New Issue
Block a user