1import type {
2 DesktopAppActivationFailure,
3 DesktopAppActivationRequest,
4 DesktopAppActivationResponse,
5 EnvironmentId,
6 ExecutionEnvironmentPlatformOs,
7 ProjectId,
8 ScopedProjectRef,
9 ThreadId,
10} from "@t3tools/contracts";
11
12export interface DesktopAppActivationProject {
13 readonly id: ProjectId;
14 readonly environmentId: EnvironmentId;
15 readonly workspaceRoot: string;
16}
17
18export interface DesktopAppActivationTarget {
19 readonly environmentId: EnvironmentId;
20 readonly platform: ExecutionEnvironmentPlatformOs;
21}
22
23export interface DesktopAppActivationDependencies {
24 readonly getTarget: () => DesktopAppActivationTarget | null;
25 readonly findProject: (
26 environmentId: EnvironmentId,
27 workspaceRoot: string,
28 ) => DesktopAppActivationProject | null;
29 readonly createProject: (
30 environmentId: EnvironmentId,
31 workspaceRoot: string,
32 ) => Promise<ProjectId>;
33 readonly waitForProject: (projectRef: ScopedProjectRef) => Promise<void>;
34 readonly openThread: (
35 projectRef: ScopedProjectRef,
36 ) => Promise<{ readonly threadId: ThreadId } | null>;
37}
38
39function failure(
40 requestId: string,
41 code: DesktopAppActivationFailure["code"],
42 message: string,
43): DesktopAppActivationFailure {
44 return { version: 1, requestId, ok: false, code, message };
45}
46
47function desktopPlatformToEnvironmentOs(
48 platform: DesktopAppActivationRequest["platform"],
49): ExecutionEnvironmentPlatformOs {
50 return platform === "win32" ? "windows" : platform;
51}
52
53function errorMessage(error: unknown, fallback: string): string {
54 return error instanceof Error && error.message.trim().length > 0 ? error.message : fallback;
55}
56
57export async function handleDesktopAppActivationRequest(
58 request: DesktopAppActivationRequest,
59 dependencies: DesktopAppActivationDependencies,
60): Promise<DesktopAppActivationResponse> {
61 const target = dependencies.getTarget();
62 if (target === null) {
63 return failure(
64 request.requestId,
65 "environment-unavailable",
66 "The desktop app's primary local environment is not connected.",
67 );
68 }
69
70 const requestPlatform = desktopPlatformToEnvironmentOs(request.platform);
71 if (requestPlatform !== target.platform) {
72 return failure(
73 request.requestId,
74 "platform-mismatch",
75 `The command path is for ${requestPlatform}, but the desktop app's primary environment uses ${target.platform}. Cross-platform path mapping is not supported.`,
76 );
77 }
78
79 let projectId = dependencies.findProject(target.environmentId, request.workspaceRoot)?.id ?? null;
80 if (projectId === null) {
81 try {
82 projectId = await dependencies.createProject(target.environmentId, request.workspaceRoot);
83 await dependencies.waitForProject({ environmentId: target.environmentId, projectId });
84 } catch (error) {
85 return failure(
86 request.requestId,
87 "project-create-failed",
88 errorMessage(error, "The Knife could not add the project."),
89 );
90 }
91 }
92
93 try {
94 const opened = await dependencies.openThread({
95 environmentId: target.environmentId,
96 projectId,
97 });
98 if (opened === null) {
99 return failure(
100 request.requestId,
101 "thread-open-failed",
102 "The Knife could not open a new thread for the project.",
103 );
104 }
105 return {
106 version: 1,
107 requestId: request.requestId,
108 ok: true,
109 projectId,
110 threadId: opened.threadId,
111 };
112 } catch (error) {
113 return failure(
114 request.requestId,
115 "thread-open-failed",
116 errorMessage(error, "The Knife could not open a new thread for the project."),
117 );
118 }
119}