History

fix(cli): support force removing projects (#3922)

Andrew Barnes committed 3 months ago

Commit 5fcfe241 parent df65a6c2 files · +70 −1Browse files

Unified · Split

2 files
  1. apps/server/src/bin.test.ts +65 −1
  2. apps/server/src/cli/project.ts +5 −0
apps/server/src/bin.test.ts+65−1
@@ -6,10 +6,16 @@ import * as NodePath from "node:path"; · 5 unmodified lines
66
77 import * as NodeHttpServer from "@effect/platform-node/NodeHttpServer";
88 import * as NodeServices from "@effect/platform-node/NodeServices";
9−import { EnvironmentOrchestrationHttpApi } from "@t3tools/contracts";
9+import {
10+ CommandId,
11+ EnvironmentOrchestrationHttpApi,
12+ ProviderInstanceId,
13+ ThreadId,
14+} from "@t3tools/contracts";
1015 import * as NetService from "@t3tools/shared/Net";
1116 import { assert, it } from "@effect/vitest";
1217 import * as Effect from "effect/Effect";
18+import * as DateTime from "effect/DateTime";
1319 import * as Layer from "effect/Layer";
1420 import * as HttpRouter from "effect/unstable/http/HttpRouter";
1521 import * as HttpServer from "effect/unstable/http/HttpServer";
@@ -22,6 +28,7 @@ import { Command } from "effect/unstable/cli"; · 6 unmodified lines
2228 import { cli, makeCli } from "./bin.ts";
2329 import * as ServerConfig from "./config.ts";
2430 import * as ProjectionSnapshotQuery from "./orchestration/Services/ProjectionSnapshotQuery.ts";
31+import * as OrchestrationEngine from "./orchestration/Services/OrchestrationEngine.ts";
2532 import { OrchestrationLayerLive } from "./orchestration/runtimeLayer.ts";
2633 import { orchestrationHttpApiLayer } from "./orchestration/http.ts";
2734 import { layerConfig as SqlitePersistenceLayerLive } from "./persistence/Layers/Sqlite.ts";
@@ -460,6 +467,63 @@ it.layer(NodeServices.layer)("bin cli parsing", (it) => { · 432 unmodified lines
460467 }),
461468 );
462469
470+ it.effect("force removes projects that still contain threads", () =>
471+ Effect.gen(function* () {
472+ const baseDir = NodeFS.mkdtempSync(
473+ NodePath.join(NodeOS.tmpdir(), "t3-cli-projects-force-remove-test-"),
474+ );
475+ const workspaceRoot = NodeFS.mkdtempSync(
476+ NodePath.join(NodeOS.tmpdir(), "t3-cli-projects-force-remove-workspace-"),
477+ );
478+
479+ yield* runCliWithRuntime(["project", "add", workspaceRoot, "--base-dir", baseDir]);
480+ const afterAdd = yield* readPersistedSnapshot(baseDir);
481+ const project = afterAdd.projects.find(
482+ (candidate) => candidate.workspaceRoot === workspaceRoot && candidate.deletedAt === null,
483+ );
484+ assert.isTrue(project !== undefined);
485+
486+ const config = yield* makeCliTestServerConfig(baseDir);
487+ yield* Effect.gen(function* () {
488+ const engine = yield* OrchestrationEngine.OrchestrationEngineService;
489+ yield* engine.dispatch({
490+ type: "thread.create",
491+ commandId: CommandId.make("cmd-cli-force-remove-thread"),
492+ threadId: ThreadId.make("thread-cli-force-remove"),
493+ projectId: project!.id,
494+ title: "Thread",
495+ modelSelection: {
496+ instanceId: ProviderInstanceId.make("codex"),
497+ model: "gpt-5-codex",
498+ },
499+ interactionMode: "default",
500+ runtimeMode: "approval-required",
501+ branch: null,
502+ worktreePath: null,
503+ createdAt: DateTime.formatIso(yield* DateTime.now),
504+ });
505+ }).pipe(Effect.provide(makeProjectPersistenceLayer(config)));
506+
507+ yield* runCliWithRuntime([
508+ "project",
509+ "remove",
510+ project!.id,
511+ "--force",
512+ "--base-dir",
513+ baseDir,
514+ ]);
515+ const afterRemove = yield* readPersistedSnapshot(baseDir);
516+ assert.isTrue(
517+ (afterRemove.projects.find((candidate) => candidate.id === project!.id)?.deletedAt ??
518+ null) !== null,
519+ );
520+ assert.isTrue(
521+ (afterRemove.threads.find((thread) => thread.id === "thread-cli-force-remove")?.deletedAt ??
522+ null) !== null,
523+ );
524+ }),
525+ );
526+
463527 it.effect("routes project commands through a running server when runtime state is present", () =>
464528 Effect.gen(function* () {
465529 const baseDir = NodeFS.mkdtempSync(
apps/server/src/cli/project.ts+5−0
@@ -493,6 +493,10 @@ const projectRemoveCommand = Command.make("remove", { · 492 unmodified lines
493493 project: Argument.string("project").pipe(
494494 Argument.withDescription("Project id or workspace root to remove."),
495495 ),
496+ force: Flag.boolean("force").pipe(
497+ Flag.withDescription("Delete the project and all of its threads."),
498+ Flag.withDefault(false),
499+ ),
496500 }).pipe(
497501 Command.withDescription("Remove a project."),
498502 Command.withHandler((flags) =>
@@ -515,6 +519,7 @@ const projectRemoveCommand = Command.make("remove", { · 16 unmodified lines
515519 type: "project.delete",
516520 commandId: CommandId.make(yield* projectCommandUuid),
517521 projectId: project.id,
522+ force: flags.force,
518523 });
519524 return `Removed project ${project.id} (${project.title}).`;
520525 }),