import * as NodeHttp from "node:http";import { mkdtempSync } from "node:fs";import { tmpdir } from "node:os";import { join } from "node:path"; import * as NodeHttpServer from "@effect/platform-node/NodeHttpServer";import * as NodeServices from "@effect/platform-node/NodeServices";import { NetService } from "@t3tools/shared/Net";import { assert, it } from "@effect/vitest";import * as Effect from "effect/Effect";import * as Layer from "effect/Layer";import * as HttpRouter from "effect/unstable/http/HttpRouter";import * as HttpServer from "effect/unstable/http/HttpServer";import * as CliError from "effect/unstable/cli/CliError";import * as TestConsole from "effect/testing/TestConsole";import { Command } from "effect/unstable/cli"; import { cli } from "./cli.ts";import { deriveServerPaths, ServerConfig, type ServerConfigShape } from "./config.ts";import { OrchestrationEngineService } from "./orchestration/Services/OrchestrationEngine.ts";import { ProjectionSnapshotQuery } from "./orchestration/Services/ProjectionSnapshotQuery.ts";import { OrchestrationLayerLive } from "./orchestration/runtimeLayer.ts";import { orchestrationDispatchRouteLayer, orchestrationSnapshotRouteLayer,} from "./orchestration/http.ts";import { layerConfig as SqlitePersistenceLayerLive } from "./persistence/Layers/Sqlite.ts";import { RepositoryIdentityResolverLive } from "./project/Layers/RepositoryIdentityResolver.ts";import { makePersistedServerRuntimeState, persistServerRuntimeState,} from "./serverRuntimeState.ts";import { WorkspacePathsLive } from "./workspace/Layers/WorkspacePaths.ts";import { ServerSecretStoreLive } from "./auth/Layers/ServerSecretStore.ts";import { ServerAuthLive } from "./auth/Layers/ServerAuth.ts"; const CliRuntimeLayer = Layer.mergeAll(NodeServices.layer, NetService.layer); const runCli = (args: ReadonlyArray<string>) => Command.runWith(cli, { version: "0.0.0" })(args);const runCliWithRuntime = (args: ReadonlyArray<string>) => runCli(args).pipe(Effect.provide(CliRuntimeLayer)); const captureStdout = <A, E, R>(effect: Effect.Effect<A, E, R>) => Effect.gen(function* () { const result = yield* effect; const output = (yield* TestConsole.logLines).findLast((line): line is string => typeof line === "string") ?? ""; return { result, output }; }).pipe(Effect.provide(Layer.mergeAll(CliRuntimeLayer, TestConsole.layer))); const makeCliTestServerConfig = (baseDir: string) => Effect.gen(function* () { const derivedPaths = yield* deriveServerPaths(baseDir, undefined); return { logLevel: "Info", traceMinLevel: "Info", traceTimingEnabled: true, traceBatchWindowMs: 200, traceMaxBytes: 10 * 1024 * 1024, traceMaxFiles: 10, otlpTracesUrl: undefined, otlpMetricsUrl: undefined, otlpExportIntervalMs: 10_000, otlpServiceName: "t3-server", mode: "web", port: 0, host: "127.0.0.1", cwd: process.cwd(), baseDir, ...derivedPaths, staticDir: undefined, devUrl: undefined, noBrowser: true, startupPresentation: "browser", desktopBootstrapToken: undefined, autoBootstrapProjectFromCwd: false, logWebSocketEvents: false, } satisfies ServerConfigShape; }); const makeProjectPersistenceLayer = (config: ServerConfigShape) => Layer.mergeAll( OrchestrationLayerLive.pipe( Layer.provideMerge(RepositoryIdentityResolverLive), Layer.provideMerge(SqlitePersistenceLayerLive), ), WorkspacePathsLive, ).pipe( Layer.provideMerge(NodeServices.layer), Layer.provide(Layer.succeed(ServerConfig, config)), ); const readPersistedSnapshot = (baseDir: string) => Effect.gen(function* () { const config = yield* makeCliTestServerConfig(baseDir); return yield* Effect.gen(function* () { const projectionSnapshotQuery = yield* ProjectionSnapshotQuery; return yield* projectionSnapshotQuery.getSnapshot(); }).pipe(Effect.provide(makeProjectPersistenceLayer(config))); }); const withLiveProjectCliServer = <A, E, R>(baseDir: string, run: () => Effect.Effect<A, E, R>) => Effect.gen(function* () { const config = yield* makeCliTestServerConfig(baseDir); const routesLayer = Layer.mergeAll( orchestrationSnapshotRouteLayer, orchestrationDispatchRouteLayer, ); const appLayer = HttpRouter.serve(routesLayer, { disableListenLog: true, disableLogger: true, }).pipe( Layer.provideMerge( ServerAuthLive.pipe( Layer.provideMerge(SqlitePersistenceLayerLive), Layer.provide(ServerSecretStoreLive), ), ), Layer.provideMerge(makeProjectPersistenceLayer(config)), Layer.provideMerge( NodeHttpServer.layer(NodeHttp.createServer, { host: "127.0.0.1", port: 0, }), ), Layer.provideMerge(NodeServices.layer), Layer.provide(Layer.succeed(ServerConfig, config)), ); return yield* Effect.scoped( Effect.gen(function* () { const server = yield* HttpServer.HttpServer; const address = server.address; if (typeof address === "string" || !("port" in address)) { assert.fail(`Expected TCP address, got ${address}`); } yield* persistServerRuntimeState({ path: config.serverRuntimeStatePath, state: makePersistedServerRuntimeState({ config, port: address.port, }), }); return yield* run(); }).pipe(Effect.provide(Layer.mergeAll(appLayer, NodeServices.layer))), ); }); it.layer(NodeServices.layer)("cli log-level parsing", (it) => { it.effect("accepts the built-in lowercase log-level flag values", () => runCliWithRuntime(["--log-level", "debug", "--version"]), ); it.effect("accepts canonical --no-<flag> boolean negation", () => runCliWithRuntime(["--no-log-websocket-events", "--version"]), ); it.effect("rejects invalid log-level casing before launching the server", () => Effect.gen(function* () { const error = yield* runCliWithRuntime(["--log-level", "Debug"]).pipe(Effect.flip); if (!CliError.isCliError(error)) { assert.fail(`Expected CliError, got ${String(error)}`); } if (error._tag !== "InvalidValue") { assert.fail(`Expected InvalidValue, got ${error._tag}`); } assert.equal(error.option, "log-level"); assert.equal(error.value, "Debug"); }), ); it.effect("executes auth pairing subcommands and redacts secrets from list output", () => Effect.gen(function* () { const baseDir = mkdtempSync(join(tmpdir(), "t3-cli-auth-pairing-test-")); const createdOutput = yield* captureStdout( runCli(["auth", "pairing", "create", "--base-dir", baseDir, "--json"]), ); const created = JSON.parse(createdOutput.output) as { readonly id: string; readonly credential: string; }; const listedOutput = yield* captureStdout( runCli(["auth", "pairing", "list", "--base-dir", baseDir, "--json"]), ); const listed = JSON.parse(listedOutput.output) as ReadonlyArray<{ readonly id: string; readonly credential?: string; }>; assert.equal(typeof created.id, "string"); assert.equal(typeof created.credential, "string"); assert.equal(created.credential.length > 0, true); assert.equal(listed.length, 1); assert.equal(listed[0]?.id, created.id); assert.equal("credential" in (listed[0] ?? {}), false); }), ); it.effect("executes auth session subcommands and redacts secrets from list output", () => Effect.gen(function* () { const baseDir = mkdtempSync(join(tmpdir(), "t3-cli-auth-session-test-")); const issuedOutput = yield* captureStdout( runCli(["auth", "session", "issue", "--base-dir", baseDir, "--json"]), ); const issued = JSON.parse(issuedOutput.output) as { readonly sessionId: string; readonly token: string; readonly role: string; }; const listedOutput = yield* captureStdout( runCli(["auth", "session", "list", "--base-dir", baseDir, "--json"]), ); const listed = JSON.parse(listedOutput.output) as ReadonlyArray<{ readonly sessionId: string; readonly token?: string; readonly role: string; }>; assert.equal(typeof issued.sessionId, "string"); assert.equal(typeof issued.token, "string"); assert.equal(issued.role, "owner"); assert.equal(listed.length, 1); assert.equal(listed[0]?.sessionId, issued.sessionId); assert.equal(listed[0]?.role, "owner"); assert.equal("token" in (listed[0] ?? {}), false); }), ); it.effect("rejects invalid ttl values before running auth commands", () => Effect.gen(function* () { const error = yield* runCliWithRuntime(["auth", "pairing", "create", "--ttl", "soon"]).pipe( Effect.flip, ); if (!CliError.isCliError(error)) { assert.fail(`Expected CliError, got ${String(error)}`); } if (error._tag !== "ShowHelp") { assert.fail(`Expected ShowHelp, got ${error._tag}`); } assert.deepEqual(error.commandPath, ["t3", "auth", "pairing", "create"]); const ttlError = error.errors[0] as CliError.CliError | undefined; if (!ttlError || ttlError._tag !== "InvalidValue") { assert.fail(`Expected InvalidValue, got ${String(ttlError?._tag)}`); } assert.equal(ttlError.option, "ttl"); assert.equal(ttlError.value, "soon"); assert.isTrue(ttlError.message.includes("Invalid duration")); assert.isTrue(ttlError.message.includes("5m, 1h, 30d, or 15 minutes")); }), ); it.effect("adds, renames, and removes projects offline through the orchestration engine", () => Effect.gen(function* () { const baseDir = mkdtempSync(join(tmpdir(), "t3-cli-projects-offline-test-")); const workspaceRoot = mkdtempSync(join(tmpdir(), "t3-cli-projects-workspace-")); yield* runCliWithRuntime([ "project", "add", workspaceRoot, "--title", "Alpha", "--base-dir", baseDir, ]); const afterAdd = yield* readPersistedSnapshot(baseDir); const addedProject = afterAdd.projects.find( (project) => project.workspaceRoot === workspaceRoot && project.deletedAt === null, ); assert.isTrue(addedProject !== undefined); assert.equal(addedProject?.title, "Alpha"); yield* runCliWithRuntime(["project", "rename", workspaceRoot, "Beta", "--base-dir", baseDir]); const afterRename = yield* readPersistedSnapshot(baseDir); const renamedProject = afterRename.projects.find( (project) => project.id === addedProject?.id, ); assert.equal(renamedProject?.title, "Beta"); assert.equal(renamedProject?.deletedAt, null); yield* runCliWithRuntime([ "project", "remove", addedProject?.id ?? "", "--base-dir", baseDir, ]); const afterRemove = yield* readPersistedSnapshot(baseDir); const removedProject = afterRemove.projects.find( (project) => project.id === addedProject?.id, ); assert.isTrue((removedProject?.deletedAt ?? null) !== null); }), ); it.effect("routes project commands through a running server when runtime state is present", () => Effect.gen(function* () { const baseDir = mkdtempSync(join(tmpdir(), "t3-cli-projects-live-test-")); const workspaceRoot = mkdtempSync(join(tmpdir(), "t3-cli-projects-live-workspace-")); yield* withLiveProjectCliServer(baseDir, () => Effect.gen(function* () { yield* runCliWithRuntime([ "project", "add", workspaceRoot, "--title", "Live Project", "--base-dir", baseDir, ]); const orchestrationEngine = yield* OrchestrationEngineService; const readModel = yield* orchestrationEngine.getReadModel(); const addedProject = readModel.projects.find( (project) => project.workspaceRoot === workspaceRoot && project.deletedAt === null, ); assert.isTrue(addedProject !== undefined); assert.equal(addedProject?.title, "Live Project"); }), ); }), ); it.effect("rejects dev-url on project commands", () => Effect.gen(function* () { const workspaceRoot = mkdtempSync( join(tmpdir(), "t3-cli-projects-unknown-option-workspace-"), ); const error = yield* runCliWithRuntime([ "project", "add", workspaceRoot, "--dev-url", "http://127.0.0.1:5173", ]).pipe(Effect.flip); if (!CliError.isCliError(error)) { assert.fail(`Expected CliError, got ${String(error)}`); } if (error._tag !== "ShowHelp") { assert.fail(`Expected ShowHelp, got ${error._tag}`); } assert.deepEqual(error.commandPath, ["t3", "project", "add"]); const optionError = error.errors[0] as CliError.CliError | undefined; if (!optionError || optionError._tag !== "UnrecognizedOption") { assert.fail(`Expected UnrecognizedOption, got ${String(optionError?._tag)}`); } assert.equal(optionError.option, "--dev-url"); }), );});
import * as NodeHttp from "node:http";
import { mkdtempSync } from "node:fs";
import { tmpdir } from "node:os";
import { join } from "node:path";
import * as NodeHttpServer from "@effect/platform-node/NodeHttpServer";
import * as NodeServices from "@effect/platform-node/NodeServices";
import { NetService } from "@t3tools/shared/Net";
import { assert, it } from "@effect/vitest";
import * as Effect from "effect/Effect";
import * as Layer from "effect/Layer";
import * as HttpRouter from "effect/unstable/http/HttpRouter";
import * as HttpServer from "effect/unstable/http/HttpServer";
import * as CliError from "effect/unstable/cli/CliError";
import * as TestConsole from "effect/testing/TestConsole";
import { Command } from "effect/unstable/cli";
import { cli } from "./cli.ts";
import { deriveServerPaths, ServerConfig, type ServerConfigShape } from "./config.ts";
import { OrchestrationEngineService } from "./orchestration/Services/OrchestrationEngine.ts";
import { ProjectionSnapshotQuery } from "./orchestration/Services/ProjectionSnapshotQuery.ts";
import { OrchestrationLayerLive } from "./orchestration/runtimeLayer.ts";
import {
orchestrationDispatchRouteLayer,
orchestrationSnapshotRouteLayer,
} from "./orchestration/http.ts";
import { layerConfig as SqlitePersistenceLayerLive } from "./persistence/Layers/Sqlite.ts";
import { RepositoryIdentityResolverLive } from "./project/Layers/RepositoryIdentityResolver.ts";
import {
makePersistedServerRuntimeState,
persistServerRuntimeState,
} from "./serverRuntimeState.ts";
import { WorkspacePathsLive } from "./workspace/Layers/WorkspacePaths.ts";
import { ServerSecretStoreLive } from "./auth/Layers/ServerSecretStore.ts";
import { ServerAuthLive } from "./auth/Layers/ServerAuth.ts";
const CliRuntimeLayer = Layer.mergeAll(NodeServices.layer, NetService.layer);
const runCli = (args: ReadonlyArray<string>) => Command.runWith(cli, { version: "0.0.0" })(args);
const runCliWithRuntime = (args: ReadonlyArray<string>) =>
runCli(args).pipe(Effect.provide(CliRuntimeLayer));
const captureStdout = <A, E, R>(effect: Effect.Effect<A, E, R>) =>
Effect.gen(function* () {
const result = yield* effect;
const output =
(yield* TestConsole.logLines).findLast((line): line is string => typeof line === "string") ??
"";
return { result, output };
}).pipe(Effect.provide(Layer.mergeAll(CliRuntimeLayer, TestConsole.layer)));
const makeCliTestServerConfig = (baseDir: string) =>
Effect.gen(function* () {
const derivedPaths = yield* deriveServerPaths(baseDir, undefined);
return {
logLevel: "Info",
traceMinLevel: "Info",
traceTimingEnabled: true,
traceBatchWindowMs: 200,
traceMaxBytes: 10 * 1024 * 1024,
traceMaxFiles: 10,
otlpTracesUrl: undefined,
otlpMetricsUrl: undefined,
otlpExportIntervalMs: 10_000,
otlpServiceName: "t3-server",
mode: "web",
port: 0,
host: "127.0.0.1",
cwd: process.cwd(),
baseDir,
...derivedPaths,
staticDir: undefined,
devUrl: undefined,
noBrowser: true,
startupPresentation: "browser",
desktopBootstrapToken: undefined,
autoBootstrapProjectFromCwd: false,
logWebSocketEvents: false,
} satisfies ServerConfigShape;
});
const makeProjectPersistenceLayer = (config: ServerConfigShape) =>
Layer.mergeAll(
OrchestrationLayerLive.pipe(
Layer.provideMerge(RepositoryIdentityResolverLive),
Layer.provideMerge(SqlitePersistenceLayerLive),
),
WorkspacePathsLive,
).pipe(
Layer.provideMerge(NodeServices.layer),
Layer.provide(Layer.succeed(ServerConfig, config)),
);
const readPersistedSnapshot = (baseDir: string) =>
Effect.gen(function* () {
const config = yield* makeCliTestServerConfig(baseDir);
return yield* Effect.gen(function* () {
const projectionSnapshotQuery = yield* ProjectionSnapshotQuery;
return yield* projectionSnapshotQuery.getSnapshot();
}).pipe(Effect.provide(makeProjectPersistenceLayer(config)));
});
const withLiveProjectCliServer = <A, E, R>(baseDir: string, run: () => Effect.Effect<A, E, R>) =>
Effect.gen(function* () {
const config = yield* makeCliTestServerConfig(baseDir);
const routesLayer = Layer.mergeAll(
orchestrationSnapshotRouteLayer,
orchestrationDispatchRouteLayer,
);
const appLayer = HttpRouter.serve(routesLayer, {
disableListenLog: true,
disableLogger: true,
}).pipe(
Layer.provideMerge(
ServerAuthLive.pipe(
Layer.provideMerge(SqlitePersistenceLayerLive),
Layer.provide(ServerSecretStoreLive),
),
),
Layer.provideMerge(makeProjectPersistenceLayer(config)),
Layer.provideMerge(
NodeHttpServer.layer(NodeHttp.createServer, {
host: "127.0.0.1",
port: 0,
}),
),
Layer.provideMerge(NodeServices.layer),
Layer.provide(Layer.succeed(ServerConfig, config)),
);
return yield* Effect.scoped(
Effect.gen(function* () {
const server = yield* HttpServer.HttpServer;
const address = server.address;
if (typeof address === "string" || !("port" in address)) {
assert.fail(`Expected TCP address, got ${address}`);
}
yield* persistServerRuntimeState({
path: config.serverRuntimeStatePath,
state: makePersistedServerRuntimeState({
config,
port: address.port,
}),
});
return yield* run();
}).pipe(Effect.provide(Layer.mergeAll(appLayer, NodeServices.layer))),
);
});
it.layer(NodeServices.layer)("cli log-level parsing", (it) => {
it.effect("accepts the built-in lowercase log-level flag values", () =>
runCliWithRuntime(["--log-level", "debug", "--version"]),
);
it.effect("accepts canonical --no-<flag> boolean negation", () =>
runCliWithRuntime(["--no-log-websocket-events", "--version"]),
);
it.effect("rejects invalid log-level casing before launching the server", () =>
Effect.gen(function* () {
const error = yield* runCliWithRuntime(["--log-level", "Debug"]).pipe(Effect.flip);
if (!CliError.isCliError(error)) {
assert.fail(`Expected CliError, got ${String(error)}`);
}
if (error._tag !== "InvalidValue") {
assert.fail(`Expected InvalidValue, got ${error._tag}`);
}
assert.equal(error.option, "log-level");
assert.equal(error.value, "Debug");
}),
);
it.effect("executes auth pairing subcommands and redacts secrets from list output", () =>
Effect.gen(function* () {
const baseDir = mkdtempSync(join(tmpdir(), "t3-cli-auth-pairing-test-"));
const createdOutput = yield* captureStdout(
runCli(["auth", "pairing", "create", "--base-dir", baseDir, "--json"]),
);
const created = JSON.parse(createdOutput.output) as {
readonly id: string;
readonly credential: string;
};
const listedOutput = yield* captureStdout(
runCli(["auth", "pairing", "list", "--base-dir", baseDir, "--json"]),
);
const listed = JSON.parse(listedOutput.output) as ReadonlyArray<{
readonly id: string;
readonly credential?: string;
}>;
assert.equal(typeof created.id, "string");
assert.equal(typeof created.credential, "string");
assert.equal(created.credential.length > 0, true);
assert.equal(listed.length, 1);
assert.equal(listed[0]?.id, created.id);
assert.equal("credential" in (listed[0] ?? {}), false);
}),
);
it.effect("executes auth session subcommands and redacts secrets from list output", () =>
Effect.gen(function* () {
const baseDir = mkdtempSync(join(tmpdir(), "t3-cli-auth-session-test-"));
const issuedOutput = yield* captureStdout(
runCli(["auth", "session", "issue", "--base-dir", baseDir, "--json"]),
);
const issued = JSON.parse(issuedOutput.output) as {
readonly sessionId: string;
readonly token: string;
readonly role: string;
};
const listedOutput = yield* captureStdout(
runCli(["auth", "session", "list", "--base-dir", baseDir, "--json"]),
);
const listed = JSON.parse(listedOutput.output) as ReadonlyArray<{
readonly sessionId: string;
readonly token?: string;
readonly role: string;
}>;
assert.equal(typeof issued.sessionId, "string");
assert.equal(typeof issued.token, "string");
assert.equal(issued.role, "owner");
assert.equal(listed.length, 1);
assert.equal(listed[0]?.sessionId, issued.sessionId);
assert.equal(listed[0]?.role, "owner");
assert.equal("token" in (listed[0] ?? {}), false);
}),
);
it.effect("rejects invalid ttl values before running auth commands", () =>
Effect.gen(function* () {
const error = yield* runCliWithRuntime(["auth", "pairing", "create", "--ttl", "soon"]).pipe(
Effect.flip,
);
if (!CliError.isCliError(error)) {
assert.fail(`Expected CliError, got ${String(error)}`);
}
if (error._tag !== "ShowHelp") {
assert.fail(`Expected ShowHelp, got ${error._tag}`);
}
assert.deepEqual(error.commandPath, ["t3", "auth", "pairing", "create"]);
const ttlError = error.errors[0] as CliError.CliError | undefined;
if (!ttlError || ttlError._tag !== "InvalidValue") {
assert.fail(`Expected InvalidValue, got ${String(ttlError?._tag)}`);
}
assert.equal(ttlError.option, "ttl");
assert.equal(ttlError.value, "soon");
assert.isTrue(ttlError.message.includes("Invalid duration"));
assert.isTrue(ttlError.message.includes("5m, 1h, 30d, or 15 minutes"));
}),
);
it.effect("adds, renames, and removes projects offline through the orchestration engine", () =>
Effect.gen(function* () {
const baseDir = mkdtempSync(join(tmpdir(), "t3-cli-projects-offline-test-"));
const workspaceRoot = mkdtempSync(join(tmpdir(), "t3-cli-projects-workspace-"));
yield* runCliWithRuntime([
"project",
"add",
workspaceRoot,
"--title",
"Alpha",
"--base-dir",
baseDir,
]);
const afterAdd = yield* readPersistedSnapshot(baseDir);
const addedProject = afterAdd.projects.find(
(project) => project.workspaceRoot === workspaceRoot && project.deletedAt === null,
);
assert.isTrue(addedProject !== undefined);
assert.equal(addedProject?.title, "Alpha");
yield* runCliWithRuntime(["project", "rename", workspaceRoot, "Beta", "--base-dir", baseDir]);
const afterRename = yield* readPersistedSnapshot(baseDir);
const renamedProject = afterRename.projects.find(
(project) => project.id === addedProject?.id,
);
assert.equal(renamedProject?.title, "Beta");
assert.equal(renamedProject?.deletedAt, null);
yield* runCliWithRuntime([
"project",
"remove",
addedProject?.id ?? "",
"--base-dir",
baseDir,
]);
const afterRemove = yield* readPersistedSnapshot(baseDir);
const removedProject = afterRemove.projects.find(
(project) => project.id === addedProject?.id,
);
assert.isTrue((removedProject?.deletedAt ?? null) !== null);
}),
);
it.effect("routes project commands through a running server when runtime state is present", () =>
Effect.gen(function* () {
const baseDir = mkdtempSync(join(tmpdir(), "t3-cli-projects-live-test-"));
const workspaceRoot = mkdtempSync(join(tmpdir(), "t3-cli-projects-live-workspace-"));
yield* withLiveProjectCliServer(baseDir, () =>
Effect.gen(function* () {
yield* runCliWithRuntime([
"project",
"add",
workspaceRoot,
"--title",
"Live Project",
"--base-dir",
baseDir,
]);
const orchestrationEngine = yield* OrchestrationEngineService;
const readModel = yield* orchestrationEngine.getReadModel();
const addedProject = readModel.projects.find(
(project) => project.workspaceRoot === workspaceRoot && project.deletedAt === null,
);
assert.isTrue(addedProject !== undefined);
assert.equal(addedProject?.title, "Live Project");
}),
);
}),
);
it.effect("rejects dev-url on project commands", () =>
Effect.gen(function* () {
const workspaceRoot = mkdtempSync(
join(tmpdir(), "t3-cli-projects-unknown-option-workspace-"),
);
const error = yield* runCliWithRuntime([
"project",
"add",
workspaceRoot,
"--dev-url",
"http://127.0.0.1:5173",
]).pipe(Effect.flip);
if (!CliError.isCliError(error)) {
assert.fail(`Expected CliError, got ${String(error)}`);
}
if (error._tag !== "ShowHelp") {
assert.fail(`Expected ShowHelp, got ${error._tag}`);
}
assert.deepEqual(error.commandPath, ["t3", "project", "add"]);
const optionError = error.errors[0] as CliError.CliError | undefined;
if (!optionError || optionError._tag !== "UnrecognizedOption") {
assert.fail(`Expected UnrecognizedOption, got ${String(optionError?._tag)}`);
}
assert.equal(optionError.option, "--dev-url");
}),
);
});