// @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<string>) => 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)),);
// @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<string>) =>
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)),
);