// @effect-diagnostics nodeBuiltinImport:off - CLI integration uses temporary Node paths. import * as NodeFS from "node:fs"; import * as NodeOS from "node:os"; import * as NodePath from "node:path"; import * as NodeServices from "@effect/platform-node/NodeServices"; import { assert, it } from "@effect/vitest"; import { EnvironmentInternalError } from "@t3tools/contracts"; import * as NetService from "@t3tools/shared/Net"; import * as Effect from "effect/Effect"; import * as Layer from "effect/Layer"; import * as References from "effect/References"; import { Command } from "effect/unstable/cli"; import { cli } from "../bin.ts"; import * as ServerConfig from "../config.ts"; import { ProjectServiceLayerLive } from "../orchestration-v2/runtimeLayer.ts"; import { layerConfig as SqlitePersistenceLayerLive } from "../persistence/Layers/Sqlite.ts"; import * as ProjectEnrichmentService from "../project/ProjectEnrichmentService.ts"; import * as ProjectFaviconResolver from "../project/ProjectFaviconResolver.ts"; import * as ProjectService from "../project/ProjectService.ts"; import * as RepositoryIdentityResolver from "../project/RepositoryIdentityResolver.ts"; import * as T3ProjectFileLoader from "../project/T3ProjectFileLoader.ts"; import * as WorkspacePaths from "../workspace/WorkspacePaths.ts"; import { ProjectLiveServerDeclaredResponseError, ProjectLiveServerRequestError, projectCommandErrorFromLiveServerRequest, } from "./project.ts"; const CliRuntimeLayer = Layer.mergeAll(NodeServices.layer, NetService.layer); const runCli = (args: ReadonlyArray) => Command.runWith(cli, { version: "0.0.0" })(args).pipe(Effect.provide(CliRuntimeLayer)); const makeConfig = (baseDir: string) => Effect.gen(function* () { const derivedPaths = yield* ServerConfig.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, devAllowedOrigins: [], noBrowser: true, startupPresentation: "browser", desktopBootstrapToken: undefined, autoBootstrapProjectFromCwd: false, logWebSocketEvents: false, tailscaleServeEnabled: false, tailscaleServePort: 443, } satisfies ServerConfig.ServerConfig["Service"]; }); const readProjects = (baseDir: string) => Effect.gen(function* () { const config = yield* makeConfig(baseDir); const layer = ProjectServiceLayerLive.pipe( Layer.provideMerge(ProjectEnrichmentService.layer), Layer.provideMerge(RepositoryIdentityResolver.layer), Layer.provideMerge(ProjectFaviconResolver.layer), Layer.provideMerge(T3ProjectFileLoader.layer), Layer.provideMerge(WorkspacePaths.layer), Layer.provideMerge(SqlitePersistenceLayerLive), Layer.provideMerge(NodeServices.layer), Layer.provide(ServerConfig.layer(config)), Layer.provide(Layer.succeed(References.MinimumLogLevel, config.logLevel)), ); return yield* ProjectService.ProjectService.pipe( Effect.flatMap((projects) => projects.snapshot), Effect.provide(layer), ); }); it("maps declared server failures into structural project command errors", () => { const cause = new EnvironmentInternalError({ code: "internal_error", reason: "access_token_issuance_failed", traceId: "trace-123", }); const error = projectCommandErrorFromLiveServerRequest(cause); assert.instanceOf(error, ProjectLiveServerDeclaredResponseError); assert.strictEqual(error.operation, "callLiveServer"); assert.strictEqual(error.code, "internal_error"); assert.strictEqual(error.traceId, "trace-123"); assert.strictEqual(error.message, "Server request failed (internal_error, trace trace-123)."); assert.strictEqual(error.cause, cause); }); it("preserves unexpected server failures without deriving the message from them", () => { const cause = new Error("credential abc123 was rejected"); const error = projectCommandErrorFromLiveServerRequest(cause); assert.instanceOf(error, ProjectLiveServerRequestError); assert.strictEqual(error.operation, "callLiveServer"); assert.strictEqual(error.message, "Failed to call the running server."); assert.strictEqual(error.cause, cause); }); it.effect("adds, renames, and removes projects through the V2 project CLI domain", () => Effect.gen(function* () { const baseDir = NodeFS.mkdtempSync(NodePath.join(NodeOS.tmpdir(), "t3-v2-project-cli-")); const workspaceRoot = NodeFS.mkdtempSync( NodePath.join(NodeOS.tmpdir(), "t3-v2-project-workspace-"), ); yield* runCli(["project", "add", workspaceRoot, "--title", "Alpha", "--base-dir", baseDir]); const added = (yield* readProjects(baseDir)).projects[0]; assert.equal(added?.title, "Alpha"); assert.equal(added?.workspaceRoot, workspaceRoot); yield* runCli(["project", "rename", workspaceRoot, "Beta", "--base-dir", baseDir]); assert.equal((yield* readProjects(baseDir)).projects[0]?.title, "Beta"); yield* runCli(["project", "remove", added?.id ?? "", "--base-dir", baseDir]); assert.deepEqual((yield* readProjects(baseDir)).projects, []); }).pipe(Effect.provide(NodeServices.layer)), );