authorgravatar for julius0216@outlook.comJulius Marminge <julius0216@outlook.com> 2026-06-19 22:21:58-07:00
committergravatar for noreply@github.comGitHub <noreply@github.com> 2026-06-19 22:21:58-07:00
log2f8d3baa88cd137b11083942a4961ca79a9f99be
treed31dab932ae981bfbe6f192a5d62f61076c938a9
parent6b7447632292f54dc98244320a7f47d7d3858ad4
signature Signed by PGP key B5690EEEBB952194

[codex] finish server process and preview Effect cleanup (#3209)

Co-authored-by: codex <codex@users.noreply.github.com>

5 files changed, 139 insertions(+), 29 deletions(-)

apps/server/src/preview/PortScanner.test.ts+2-2
......@@ -6,9 +6,9 @@ import * as Net from "@t3tools/shared/Net";
66import { Effect, Layer } from "effect";
77import { expect } from "vite-plus/test";
88
9import { ProcessRunner } from "../processRunner.ts";
9import * as ProcessRunner from "../processRunner.ts";
1010import * as PortScanner from "./PortScanner.ts";
11const TestProcessRunner = Layer.succeed(ProcessRunner, {
11const TestProcessRunner = Layer.succeed(ProcessRunner.ProcessRunner, {
1212 run: () => Effect.die("ProcessRunner should not be used by Windows TCP probe tests"),
1313});
1414const TestPortDiscoveryLive = PortScanner.layer.pipe(
apps/server/src/process/externalLauncher.test.ts+11-9
......@@ -11,7 +11,7 @@ import { ChildProcess, ChildProcessSpawner } from "effect/unstable/process";
1111
1212import { HostProcessPlatform } from "@t3tools/shared/hostProcess";
1313import { SpawnExecutableResolution } from "@t3tools/shared/shell";
14import { ExternalLauncher, layer as ExternalLauncherLive } from "./externalLauncher.ts";
14import * as ExternalLauncher from "./externalLauncher.ts";
1515
1616function makeMockDetachedHandle(onUnref: () => void = () => undefined) {
1717 return ChildProcessSpawner.makeHandle({
......@@ -54,7 +54,7 @@ const testLayer = (input: {
5454 );
5555
5656 return Layer.mergeAll(
57 ExternalLauncherLive.pipe(Layer.provide(Layer.merge(NodeServices.layer, spawnerLayer))),
57 ExternalLauncher.layer.pipe(Layer.provide(Layer.merge(NodeServices.layer, spawnerLayer))),
5858 Layer.succeed(HostProcessPlatform, input.platform),
5959 Layer.succeed(
6060 SpawnExecutableResolution,
......@@ -68,7 +68,7 @@ it.effect("launches the default browser through the platform command", () => {
6868 let spawned: ChildProcess.StandardCommand | undefined;
6969 let didUnref = false;
7070 return Effect.gen(function* () {
71 const launcher = yield* ExternalLauncher;
71 const launcher = yield* ExternalLauncher.ExternalLauncher;
7272
7373 yield* launcher.launchBrowser("https://example.com/some path");
7474
......@@ -101,7 +101,7 @@ it.effect("launches an installed editor with platform-safe arguments", () =>
101101
102102 let spawned: ChildProcess.StandardCommand | undefined;
103103 yield* Effect.gen(function* () {
104 const launcher = yield* ExternalLauncher;
104 const launcher = yield* ExternalLauncher.ExternalLauncher;
105105 yield* launcher.launchEditor({
106106 editor: "vscode",
107107 cwd: "C:\\workspace with spaces\\src\\index.ts:12:4",
......@@ -139,7 +139,7 @@ it.effect("discovers editors through the service API", () =>
139139 yield* fileSystem.writeFileString(path.join(binDir, "explorer.CMD"), "@echo off\r\n");
140140
141141 const editors = yield* Effect.gen(function* () {
142 const launcher = yield* ExternalLauncher;
142 const launcher = yield* ExternalLauncher.ExternalLauncher;
143143 return yield* launcher.resolveAvailableEditors();
144144 }).pipe(
145145 Effect.provide(
......@@ -157,10 +157,12 @@ it.effect("discovers editors through the service API", () =>
157157
158158it.effect("rejects unknown editors through the service API", () =>
159159 Effect.gen(function* () {
160 const launcher = yield* ExternalLauncher;
161 const result = yield* launcher
160 const launcher = yield* ExternalLauncher.ExternalLauncher;
161 const error = yield* launcher
162162 .launchEditor({ editor: "missing-editor" as never, cwd: "/tmp/workspace" })
163 .pipe(Effect.result);
164 assert.equal(result._tag, "Failure");
163 .pipe(Effect.flip);
164 assert.instanceOf(error, ExternalLauncher.ExternalLauncherUnknownEditorError);
165 assert.equal(error.editor, "missing-editor");
166 assert.equal(error.message, "Unknown editor: missing-editor");
165167 }).pipe(Effect.provide(testLayer({ platform: "linux", env: { PATH: "" } }))),
166168);
apps/server/src/process/externalLauncher.ts+49-10
......@@ -9,6 +9,11 @@
99import {
1010 EDITORS,
1111 ExternalLauncherError,
12 ExternalLauncherBrowserSpawnError,
13 ExternalLauncherCommandNotFoundError,
14 ExternalLauncherEditorSpawnError,
15 ExternalLauncherUnknownEditorError,
16 ExternalLauncherUnsupportedEditorError,
1217 type EditorId,
1318 type LaunchEditorInput,
1419} from "@t3tools/contracts";
......@@ -29,9 +34,19 @@ import * as ChildProcessSpawner from "effect/unstable/process/ChildProcessSpawne
2934// Definitions
3035// ==============================
3136
32export { ExternalLauncherError };
37export {
38 ExternalLauncherError,
39 ExternalLauncherBrowserSpawnError,
40 ExternalLauncherCommandNotFoundError,
41 ExternalLauncherEditorSpawnError,
42 ExternalLauncherUnknownEditorError,
43 ExternalLauncherUnsupportedEditorError,
44 isExternalLauncherError,
45} from "@t3tools/contracts";
3346export type { LaunchEditorInput };
3447interface EditorLaunch {
48 readonly editor: EditorId;
49 readonly target: string;
3550 readonly command: string;
3651 readonly args: ReadonlyArray<string>;
3752}
......@@ -317,7 +332,7 @@ const resolveEditorLaunch = Effect.fn("resolveEditorLaunch")(function* (
317332 });
318333 const editorDef = EDITORS.find((editor) => editor.id === input.editor);
319334 if (!editorDef) {
320 return yield* new ExternalLauncherError({ message: `Unknown editor: ${input.editor}` });
335 return yield* new ExternalLauncherUnknownEditorError({ editor: input.editor });
321336 }
322337
323338 if (editorDef.commands) {
......@@ -326,21 +341,28 @@ const resolveEditorLaunch = Effect.fn("resolveEditorLaunch")(function* (
326341 () => editorDef.commands[0],
327342 );
328343 return {
344 editor: editorDef.id,
345 target: input.cwd,
329346 command,
330347 args: resolveEditorArgs(editorDef, input.cwd),
331348 };
332349 }
333350
334351 if (editorDef.id !== "file-manager") {
335 return yield* new ExternalLauncherError({ message: `Unsupported editor: ${input.editor}` });
352 return yield* new ExternalLauncherUnsupportedEditorError({ editor: input.editor });
336353 }
337354
338 return { command: fileManagerCommandForPlatform(platform), args: [input.cwd] };
355 return {
356 editor: editorDef.id,
357 target: input.cwd,
358 command: fileManagerCommandForPlatform(platform),
359 args: [input.cwd],
360 };
339361});
340362
341363const launchAndUnref = Effect.fn("externalLauncher.launchAndUnref")(function* (
342364 launch: ProcessLaunch,
343 errorMessage: string,
365 onError: (cause: unknown) => ExternalLauncherError,
344366): Effect.fn.Return<void, ExternalLauncherError, ChildProcessSpawner.ChildProcessSpawner> {
345367 const spawner = yield* ChildProcessSpawner.ChildProcessSpawner;
346368 const command = ChildProcess.make(launch.command, launch.args, launch.options);
......@@ -349,7 +371,7 @@ const launchAndUnref = Effect.fn("externalLauncher.launchAndUnref")(function* (
349371 Effect.flatMap((handle) => handle.unref),
350372 Effect.asVoid,
351373 Effect.scoped,
352 Effect.mapError((cause) => new ExternalLauncherError({ message: errorMessage, cause })),
374 Effect.mapError(onError),
353375 );
354376});
355377
......@@ -357,7 +379,16 @@ const launchBrowser = Effect.fn("externalLauncher.launchBrowser")(function* (
357379 target: string,
358380): Effect.fn.Return<void, ExternalLauncherError, ChildProcessSpawner.ChildProcessSpawner> {
359381 const launch = yield* resolveBrowserLaunch(target);
360 return yield* launchAndUnref(launch, "Browser auto-open failed");
382 return yield* launchAndUnref(
383 launch,
384 (cause) =>
385 new ExternalLauncherBrowserSpawnError({
386 target,
387 command: launch.command,
388 args: launch.args,
389 cause,
390 }),
391 );
361392});
362393
363394const launchEditorProcess = Effect.fn("externalLauncher.launchEditorProcess")(function* (
......@@ -369,8 +400,9 @@ const launchEditorProcess = Effect.fn("externalLauncher.launchEditorProcess")(fu
369400> {
370401 const env = yield* readCommandLookupEnv;
371402 if (!(yield* isCommandAvailable(launch.command, { env }))) {
372 return yield* new ExternalLauncherError({
373 message: `Editor command not found: ${launch.command}`,
403 return yield* new ExternalLauncherCommandNotFoundError({
404 editor: launch.editor,
405 command: launch.command,
374406 });
375407 }
376408
......@@ -387,7 +419,14 @@ const launchEditorProcess = Effect.fn("externalLauncher.launchEditorProcess")(fu
387419 stderr: "ignore",
388420 },
389421 },
390 "failed to spawn detached process",
422 (cause) =>
423 new ExternalLauncherEditorSpawnError({
424 editor: launch.editor,
425 target: launch.target,
426 command: spawnCommand.command,
427 args: spawnCommand.args,
428 cause,
429 }),
391430 );
392431});
393432
apps/server/src/server.test.ts+4-3
......@@ -14,7 +14,7 @@ import {
1414 GitCommandError,
1515 KeybindingRule,
1616 MessageId,
17 ExternalLauncherError,
17 ExternalLauncherCommandNotFoundError,
1818 type OrchestrationThreadShell,
1919 TerminalNotRunningError,
2020 type OrchestrationCommand,
......@@ -4570,8 +4570,9 @@ it.layer(NodeServices.layer)("server router seam", (it) => {
45704570
45714571 it.effect("routes websocket rpc shell.openInEditor errors", () =>
45724572 Effect.gen(function* () {
4573 const externalLauncherError = new ExternalLauncherError({
4574 message: "Editor command not found: cursor",
4573 const externalLauncherError = new ExternalLauncherCommandNotFoundError({
4574 editor: "cursor",
4575 command: "cursor",
45754576 });
45764577 yield* buildAppUnderTest({
45774578 layers: {
packages/contracts/src/editor.ts+73-5
......@@ -50,10 +50,78 @@ export const LaunchEditorInput = Schema.Struct({
5050});
5151export type LaunchEditorInput = typeof LaunchEditorInput.Type;
5252
53export class ExternalLauncherError extends Schema.TaggedErrorClass<ExternalLauncherError>()(
54 "ExternalLauncherError",
53export class ExternalLauncherUnknownEditorError extends Schema.TaggedErrorClass<ExternalLauncherUnknownEditorError>()(
54 "ExternalLauncherUnknownEditorError",
5555 {
56 message: Schema.String,
57 cause: Schema.optional(Schema.Defect()),
56 editor: Schema.String,
5857 },
59) {}
58) {
59 override get message(): string {
60 return `Unknown editor: ${this.editor}`;
61 }
62}
63
64export class ExternalLauncherUnsupportedEditorError extends Schema.TaggedErrorClass<ExternalLauncherUnsupportedEditorError>()(
65 "ExternalLauncherUnsupportedEditorError",
66 {
67 editor: EditorId,
68 },
69) {
70 override get message(): string {
71 return `Unsupported editor: ${this.editor}`;
72 }
73}
74
75export class ExternalLauncherCommandNotFoundError extends Schema.TaggedErrorClass<ExternalLauncherCommandNotFoundError>()(
76 "ExternalLauncherCommandNotFoundError",
77 {
78 editor: EditorId,
79 command: Schema.String,
80 },
81) {
82 override get message(): string {
83 return `Editor command not found: ${this.command}`;
84 }
85}
86
87const ExternalLauncherSpawnFields = {
88 command: Schema.String,
89 args: Schema.Array(Schema.String),
90 cause: Schema.Defect(),
91};
92
93export class ExternalLauncherBrowserSpawnError extends Schema.TaggedErrorClass<ExternalLauncherBrowserSpawnError>()(
94 "ExternalLauncherBrowserSpawnError",
95 {
96 ...ExternalLauncherSpawnFields,
97 target: Schema.String,
98 },
99) {
100 override get message(): string {
101 return `Failed to launch browser target '${this.target}' with '${[this.command, ...this.args].join(" ")}'`;
102 }
103}
104
105export class ExternalLauncherEditorSpawnError extends Schema.TaggedErrorClass<ExternalLauncherEditorSpawnError>()(
106 "ExternalLauncherEditorSpawnError",
107 {
108 ...ExternalLauncherSpawnFields,
109 editor: EditorId,
110 target: Schema.String,
111 },
112) {
113 override get message(): string {
114 return `Failed to launch '${this.target}' in ${this.editor} with '${[this.command, ...this.args].join(" ")}'`;
115 }
116}
117
118export const ExternalLauncherError = Schema.Union([
119 ExternalLauncherUnknownEditorError,
120 ExternalLauncherUnsupportedEditorError,
121 ExternalLauncherCommandNotFoundError,
122 ExternalLauncherBrowserSpawnError,
123 ExternalLauncherEditorSpawnError,
124]);
125export type ExternalLauncherError = typeof ExternalLauncherError.Type;
126
127export const isExternalLauncherError = Schema.is(ExternalLauncherError);