| author | |
| committer | |
| log | d15909af15602622c6bd1d10aa2e3e165117a1c8 |
| tree | 7a1749e425107f0d1d6bfc977f40d30267841cab |
| parent | 447236d51f4d6835482f6707d627dbc8a39eb553 |
| signature | Signed by PGP key B5690EEEBB952194 |
Co-authored-by: Cursor Agent <cursoragent@cursor.com>
Co-authored-by: Julius Marminge <juliusmarminge@users.noreply.github.com>
Co-authored-by: Julius Marminge <julius0216@outlook.com>12 files changed, 1205 insertions(+), 933 deletions(-)
apps/server/package.json+1-2| ... | @@ -31,8 +31,7 @@ | ... | @@ -31,8 +31,7 @@ |
| 31 | "@opencode-ai/sdk": "^1.3.15", | 31 | "@opencode-ai/sdk": "^1.3.15", |
| 32 | "@pierre/diffs": "catalog:", | 32 | "@pierre/diffs": "catalog:", |
| 33 | "effect": "catalog:", | 33 | "effect": "catalog:", |
| 34 | "node-pty": "^1.1.0", | 34 | "node-pty": "^1.1.0" |
| 35 | "open": "^10.1.0" | ||
| 36 | }, | 35 | }, |
| 37 | "devDependencies": { | 36 | "devDependencies": { |
| 38 | "@effect/language-service": "catalog:", | 37 | "@effect/language-service": "catalog:", |
apps/server/src/open.test.ts deleted-642| ... | @@ -1,642 +0,0 @@ | ||
| 1 | import * as NodeServices from "@effect/platform-node/NodeServices"; | ||
| 2 | import { assert, it } from "@effect/vitest"; | ||
| 3 | import { assertSuccess } from "@effect/vitest/utils"; | ||
| 4 | import * as Effect from "effect/Effect"; | ||
| 5 | import * as FileSystem from "effect/FileSystem"; | ||
| 6 | import * as Path from "effect/Path"; | ||
| 7 | import * as Random from "effect/Random"; | ||
| 8 | |||
| 9 | import { | ||
| 10 | isCommandAvailable, | ||
| 11 | launchDetached, | ||
| 12 | resolveAvailableEditors, | ||
| 13 | resolveEditorLaunch, | ||
| 14 | } from "./open.ts"; | ||
| 15 | |||
| 16 | it.layer(NodeServices.layer)("resolveEditorLaunch", (it) => { | ||
| 17 | it.effect("returns commands for command-based editors", () => | ||
| 18 | Effect.gen(function* () { | ||
| 19 | const antigravityLaunch = yield* resolveEditorLaunch( | ||
| 20 | { cwd: "/tmp/workspace", editor: "antigravity" }, | ||
| 21 | "darwin", | ||
| 22 | { PATH: "" }, | ||
| 23 | ); | ||
| 24 | assert.deepEqual(antigravityLaunch, { | ||
| 25 | command: "agy", | ||
| 26 | args: ["/tmp/workspace"], | ||
| 27 | }); | ||
| 28 | |||
| 29 | const cursorLaunch = yield* resolveEditorLaunch( | ||
| 30 | { cwd: "/tmp/workspace", editor: "cursor" }, | ||
| 31 | "darwin", | ||
| 32 | { PATH: "" }, | ||
| 33 | ); | ||
| 34 | assert.deepEqual(cursorLaunch, { | ||
| 35 | command: "cursor", | ||
| 36 | args: ["/tmp/workspace"], | ||
| 37 | }); | ||
| 38 | |||
| 39 | const traeLaunch = yield* resolveEditorLaunch( | ||
| 40 | { cwd: "/tmp/workspace", editor: "trae" }, | ||
| 41 | "darwin", | ||
| 42 | ); | ||
| 43 | assert.deepEqual(traeLaunch, { | ||
| 44 | command: "trae", | ||
| 45 | args: ["/tmp/workspace"], | ||
| 46 | }); | ||
| 47 | |||
| 48 | const kiroLaunch = yield* resolveEditorLaunch( | ||
| 49 | { cwd: "/tmp/workspace", editor: "kiro" }, | ||
| 50 | "darwin", | ||
| 51 | { PATH: "" }, | ||
| 52 | ); | ||
| 53 | assert.deepEqual(kiroLaunch, { | ||
| 54 | command: "kiro", | ||
| 55 | args: ["ide", "/tmp/workspace"], | ||
| 56 | }); | ||
| 57 | |||
| 58 | const vscodeLaunch = yield* resolveEditorLaunch( | ||
| 59 | { cwd: "/tmp/workspace", editor: "vscode" }, | ||
| 60 | "darwin", | ||
| 61 | { PATH: "" }, | ||
| 62 | ); | ||
| 63 | assert.deepEqual(vscodeLaunch, { | ||
| 64 | command: "code", | ||
| 65 | args: ["/tmp/workspace"], | ||
| 66 | }); | ||
| 67 | |||
| 68 | const vscodeInsidersLaunch = yield* resolveEditorLaunch( | ||
| 69 | { cwd: "/tmp/workspace", editor: "vscode-insiders" }, | ||
| 70 | "darwin", | ||
| 71 | ); | ||
| 72 | assert.deepEqual(vscodeInsidersLaunch, { | ||
| 73 | command: "code-insiders", | ||
| 74 | args: ["/tmp/workspace"], | ||
| 75 | }); | ||
| 76 | |||
| 77 | const vscodiumLaunch = yield* resolveEditorLaunch( | ||
| 78 | { cwd: "/tmp/workspace", editor: "vscodium" }, | ||
| 79 | "darwin", | ||
| 80 | ); | ||
| 81 | assert.deepEqual(vscodiumLaunch, { | ||
| 82 | command: "codium", | ||
| 83 | args: ["/tmp/workspace"], | ||
| 84 | }); | ||
| 85 | |||
| 86 | const zedLaunch = yield* resolveEditorLaunch( | ||
| 87 | { cwd: "/tmp/workspace", editor: "zed" }, | ||
| 88 | "darwin", | ||
| 89 | { PATH: "" }, | ||
| 90 | ); | ||
| 91 | assert.deepEqual(zedLaunch, { | ||
| 92 | command: "zed", | ||
| 93 | args: ["/tmp/workspace"], | ||
| 94 | }); | ||
| 95 | |||
| 96 | const ideaLaunch = yield* resolveEditorLaunch( | ||
| 97 | { cwd: "/tmp/workspace", editor: "idea" }, | ||
| 98 | "darwin", | ||
| 99 | ); | ||
| 100 | assert.deepEqual(ideaLaunch, { | ||
| 101 | command: "idea", | ||
| 102 | args: ["/tmp/workspace"], | ||
| 103 | }); | ||
| 104 | |||
| 105 | const aquaLaunch = yield* resolveEditorLaunch( | ||
| 106 | { cwd: "/tmp/workspace", editor: "aqua" }, | ||
| 107 | "darwin", | ||
| 108 | ); | ||
| 109 | assert.deepEqual(aquaLaunch, { | ||
| 110 | command: "aqua", | ||
| 111 | args: ["/tmp/workspace"], | ||
| 112 | }); | ||
| 113 | |||
| 114 | const clionLaunch = yield* resolveEditorLaunch( | ||
| 115 | { cwd: "/tmp/workspace", editor: "clion" }, | ||
| 116 | "darwin", | ||
| 117 | ); | ||
| 118 | assert.deepEqual(clionLaunch, { | ||
| 119 | command: "clion", | ||
| 120 | args: ["/tmp/workspace"], | ||
| 121 | }); | ||
| 122 | |||
| 123 | const datagripLaunch = yield* resolveEditorLaunch( | ||
| 124 | { cwd: "/tmp/workspace", editor: "datagrip" }, | ||
| 125 | "darwin", | ||
| 126 | ); | ||
| 127 | assert.deepEqual(datagripLaunch, { | ||
| 128 | command: "datagrip", | ||
| 129 | args: ["/tmp/workspace"], | ||
| 130 | }); | ||
| 131 | |||
| 132 | const dataspellLaunch = yield* resolveEditorLaunch( | ||
| 133 | { cwd: "/tmp/workspace", editor: "dataspell" }, | ||
| 134 | "darwin", | ||
| 135 | ); | ||
| 136 | assert.deepEqual(dataspellLaunch, { | ||
| 137 | command: "dataspell", | ||
| 138 | args: ["/tmp/workspace"], | ||
| 139 | }); | ||
| 140 | |||
| 141 | const golandLaunch = yield* resolveEditorLaunch( | ||
| 142 | { cwd: "/tmp/workspace", editor: "goland" }, | ||
| 143 | "darwin", | ||
| 144 | ); | ||
| 145 | assert.deepEqual(golandLaunch, { | ||
| 146 | command: "goland", | ||
| 147 | args: ["/tmp/workspace"], | ||
| 148 | }); | ||
| 149 | |||
| 150 | const phpstormLaunch = yield* resolveEditorLaunch( | ||
| 151 | { cwd: "/tmp/workspace", editor: "phpstorm" }, | ||
| 152 | "darwin", | ||
| 153 | ); | ||
| 154 | assert.deepEqual(phpstormLaunch, { | ||
| 155 | command: "phpstorm", | ||
| 156 | args: ["/tmp/workspace"], | ||
| 157 | }); | ||
| 158 | |||
| 159 | const pycharmLaunch = yield* resolveEditorLaunch( | ||
| 160 | { cwd: "/tmp/workspace", editor: "pycharm" }, | ||
| 161 | "darwin", | ||
| 162 | ); | ||
| 163 | assert.deepEqual(pycharmLaunch, { | ||
| 164 | command: "pycharm", | ||
| 165 | args: ["/tmp/workspace"], | ||
| 166 | }); | ||
| 167 | |||
| 168 | const riderLaunch = yield* resolveEditorLaunch( | ||
| 169 | { cwd: "/tmp/workspace", editor: "rider" }, | ||
| 170 | "darwin", | ||
| 171 | ); | ||
| 172 | assert.deepEqual(riderLaunch, { | ||
| 173 | command: "rider", | ||
| 174 | args: ["/tmp/workspace"], | ||
| 175 | }); | ||
| 176 | |||
| 177 | const rubymineLaunch = yield* resolveEditorLaunch( | ||
| 178 | { cwd: "/tmp/workspace", editor: "rubymine" }, | ||
| 179 | "darwin", | ||
| 180 | ); | ||
| 181 | assert.deepEqual(rubymineLaunch, { | ||
| 182 | command: "rubymine", | ||
| 183 | args: ["/tmp/workspace"], | ||
| 184 | }); | ||
| 185 | |||
| 186 | const rustroverLaunch = yield* resolveEditorLaunch( | ||
| 187 | { cwd: "/tmp/workspace", editor: "rustrover" }, | ||
| 188 | "darwin", | ||
| 189 | ); | ||
| 190 | assert.deepEqual(rustroverLaunch, { | ||
| 191 | command: "rustrover", | ||
| 192 | args: ["/tmp/workspace"], | ||
| 193 | }); | ||
| 194 | |||
| 195 | const webstormLaunch = yield* resolveEditorLaunch( | ||
| 196 | { cwd: "/tmp/workspace", editor: "webstorm" }, | ||
| 197 | "darwin", | ||
| 198 | ); | ||
| 199 | assert.deepEqual(webstormLaunch, { | ||
| 200 | command: "webstorm", | ||
| 201 | args: ["/tmp/workspace"], | ||
| 202 | }); | ||
| 203 | }), | ||
| 204 | ); | ||
| 205 | |||
| 206 | it.effect("applies launch-style-specific navigation arguments", () => | ||
| 207 | Effect.gen(function* () { | ||
| 208 | const lineOnly = yield* resolveEditorLaunch( | ||
| 209 | { cwd: "/tmp/workspace/AGENTS.md:48", editor: "cursor" }, | ||
| 210 | "darwin", | ||
| 211 | { PATH: "" }, | ||
| 212 | ); | ||
| 213 | assert.deepEqual(lineOnly, { | ||
| 214 | command: "cursor", | ||
| 215 | args: ["--goto", "/tmp/workspace/AGENTS.md:48"], | ||
| 216 | }); | ||
| 217 | |||
| 218 | const lineAndColumn = yield* resolveEditorLaunch( | ||
| 219 | { cwd: "/tmp/workspace/src/open.ts:71:5", editor: "cursor" }, | ||
| 220 | "darwin", | ||
| 221 | { PATH: "" }, | ||
| 222 | ); | ||
| 223 | assert.deepEqual(lineAndColumn, { | ||
| 224 | command: "cursor", | ||
| 225 | args: ["--goto", "/tmp/workspace/src/open.ts:71:5"], | ||
| 226 | }); | ||
| 227 | |||
| 228 | const traeLineAndColumn = yield* resolveEditorLaunch( | ||
| 229 | { cwd: "/tmp/workspace/src/open.ts:71:5", editor: "trae" }, | ||
| 230 | "darwin", | ||
| 231 | ); | ||
| 232 | assert.deepEqual(traeLineAndColumn, { | ||
| 233 | command: "trae", | ||
| 234 | args: ["--goto", "/tmp/workspace/src/open.ts:71:5"], | ||
| 235 | }); | ||
| 236 | |||
| 237 | const kiroLineAndColumn = yield* resolveEditorLaunch( | ||
| 238 | { cwd: "/tmp/workspace/src/open.ts:71:5", editor: "kiro" }, | ||
| 239 | "darwin", | ||
| 240 | { PATH: "" }, | ||
| 241 | ); | ||
| 242 | assert.deepEqual(kiroLineAndColumn, { | ||
| 243 | command: "kiro", | ||
| 244 | args: ["ide", "--goto", "/tmp/workspace/src/open.ts:71:5"], | ||
| 245 | }); | ||
| 246 | |||
| 247 | const vscodeLineAndColumn = yield* resolveEditorLaunch( | ||
| 248 | { cwd: "/tmp/workspace/src/open.ts:71:5", editor: "vscode" }, | ||
| 249 | "darwin", | ||
| 250 | { PATH: "" }, | ||
| 251 | ); | ||
| 252 | assert.deepEqual(vscodeLineAndColumn, { | ||
| 253 | command: "code", | ||
| 254 | args: ["--goto", "/tmp/workspace/src/open.ts:71:5"], | ||
| 255 | }); | ||
| 256 | |||
| 257 | const vscodeInsidersLineAndColumn = yield* resolveEditorLaunch( | ||
| 258 | { cwd: "/tmp/workspace/src/open.ts:71:5", editor: "vscode-insiders" }, | ||
| 259 | "darwin", | ||
| 260 | ); | ||
| 261 | assert.deepEqual(vscodeInsidersLineAndColumn, { | ||
| 262 | command: "code-insiders", | ||
| 263 | args: ["--goto", "/tmp/workspace/src/open.ts:71:5"], | ||
| 264 | }); | ||
| 265 | |||
| 266 | const vscodiumLineAndColumn = yield* resolveEditorLaunch( | ||
| 267 | { cwd: "/tmp/workspace/src/open.ts:71:5", editor: "vscodium" }, | ||
| 268 | "darwin", | ||
| 269 | ); | ||
| 270 | assert.deepEqual(vscodiumLineAndColumn, { | ||
| 271 | command: "codium", | ||
| 272 | args: ["--goto", "/tmp/workspace/src/open.ts:71:5"], | ||
| 273 | }); | ||
| 274 | |||
| 275 | const zedLineAndColumn = yield* resolveEditorLaunch( | ||
| 276 | { cwd: "/tmp/workspace/src/open.ts:71:5", editor: "zed" }, | ||
| 277 | "darwin", | ||
| 278 | { PATH: "" }, | ||
| 279 | ); | ||
| 280 | assert.deepEqual(zedLineAndColumn, { | ||
| 281 | command: "zed", | ||
| 282 | args: ["/tmp/workspace/src/open.ts:71:5"], | ||
| 283 | }); | ||
| 284 | |||
| 285 | const zedLineOnly = yield* resolveEditorLaunch( | ||
| 286 | { cwd: "/tmp/workspace/AGENTS.md:48", editor: "zed" }, | ||
| 287 | "darwin", | ||
| 288 | { PATH: "" }, | ||
| 289 | ); | ||
| 290 | assert.deepEqual(zedLineOnly, { | ||
| 291 | command: "zed", | ||
| 292 | args: ["/tmp/workspace/AGENTS.md:48"], | ||
| 293 | }); | ||
| 294 | |||
| 295 | const ideaLineOnly = yield* resolveEditorLaunch( | ||
| 296 | { cwd: "/tmp/workspace/AGENTS.md:48", editor: "idea" }, | ||
| 297 | "darwin", | ||
| 298 | ); | ||
| 299 | assert.deepEqual(ideaLineOnly, { | ||
| 300 | command: "idea", | ||
| 301 | args: ["--line", "48", "/tmp/workspace/AGENTS.md"], | ||
| 302 | }); | ||
| 303 | |||
| 304 | const ideaLineAndColumn = yield* resolveEditorLaunch( | ||
| 305 | { cwd: "/tmp/workspace/src/open.ts:71:5", editor: "idea" }, | ||
| 306 | "darwin", | ||
| 307 | ); | ||
| 308 | assert.deepEqual(ideaLineAndColumn, { | ||
| 309 | command: "idea", | ||
| 310 | args: ["--line", "71", "--column", "5", "/tmp/workspace/src/open.ts"], | ||
| 311 | }); | ||
| 312 | |||
| 313 | const aquaLineAndColumn = yield* resolveEditorLaunch( | ||
| 314 | { cwd: "/tmp/workspace/src/open.ts:71:5", editor: "aqua" }, | ||
| 315 | "darwin", | ||
| 316 | ); | ||
| 317 | assert.deepEqual(aquaLineAndColumn, { | ||
| 318 | command: "aqua", | ||
| 319 | args: ["--line", "71", "--column", "5", "/tmp/workspace/src/open.ts"], | ||
| 320 | }); | ||
| 321 | |||
| 322 | const clionLineAndColumn = yield* resolveEditorLaunch( | ||
| 323 | { cwd: "/tmp/workspace/src/open.ts:71:5", editor: "clion" }, | ||
| 324 | "darwin", | ||
| 325 | ); | ||
| 326 | assert.deepEqual(clionLineAndColumn, { | ||
| 327 | command: "clion", | ||
| 328 | args: ["--line", "71", "--column", "5", "/tmp/workspace/src/open.ts"], | ||
| 329 | }); | ||
| 330 | |||
| 331 | const datagripLineAndColumn = yield* resolveEditorLaunch( | ||
| 332 | { cwd: "/tmp/workspace/src/open.ts:71:5", editor: "datagrip" }, | ||
| 333 | "darwin", | ||
| 334 | ); | ||
| 335 | assert.deepEqual(datagripLineAndColumn, { | ||
| 336 | command: "datagrip", | ||
| 337 | args: ["--line", "71", "--column", "5", "/tmp/workspace/src/open.ts"], | ||
| 338 | }); | ||
| 339 | |||
| 340 | const dataspellLineAndColumn = yield* resolveEditorLaunch( | ||
| 341 | { cwd: "/tmp/workspace/src/open.ts:71:5", editor: "dataspell" }, | ||
| 342 | "darwin", | ||
| 343 | ); | ||
| 344 | assert.deepEqual(dataspellLineAndColumn, { | ||
| 345 | command: "dataspell", | ||
| 346 | args: ["--line", "71", "--column", "5", "/tmp/workspace/src/open.ts"], | ||
| 347 | }); | ||
| 348 | |||
| 349 | const golandLineAndColumn = yield* resolveEditorLaunch( | ||
| 350 | { cwd: "/tmp/workspace/src/open.ts:71:5", editor: "goland" }, | ||
| 351 | "darwin", | ||
| 352 | ); | ||
| 353 | assert.deepEqual(golandLineAndColumn, { | ||
| 354 | command: "goland", | ||
| 355 | args: ["--line", "71", "--column", "5", "/tmp/workspace/src/open.ts"], | ||
| 356 | }); | ||
| 357 | |||
| 358 | const phpstormLineAndColumn = yield* resolveEditorLaunch( | ||
| 359 | { cwd: "/tmp/workspace/src/open.ts:71:5", editor: "phpstorm" }, | ||
| 360 | "darwin", | ||
| 361 | ); | ||
| 362 | assert.deepEqual(phpstormLineAndColumn, { | ||
| 363 | command: "phpstorm", | ||
| 364 | args: ["--line", "71", "--column", "5", "/tmp/workspace/src/open.ts"], | ||
| 365 | }); | ||
| 366 | |||
| 367 | const pycharmLineAndColumn = yield* resolveEditorLaunch( | ||
| 368 | { cwd: "/tmp/workspace/src/open.ts:71:5", editor: "pycharm" }, | ||
| 369 | "darwin", | ||
| 370 | ); | ||
| 371 | assert.deepEqual(pycharmLineAndColumn, { | ||
| 372 | command: "pycharm", | ||
| 373 | args: ["--line", "71", "--column", "5", "/tmp/workspace/src/open.ts"], | ||
| 374 | }); | ||
| 375 | |||
| 376 | const riderLineAndColumn = yield* resolveEditorLaunch( | ||
| 377 | { cwd: "/tmp/workspace/src/open.ts:71:5", editor: "rider" }, | ||
| 378 | "darwin", | ||
| 379 | ); | ||
| 380 | assert.deepEqual(riderLineAndColumn, { | ||
| 381 | command: "rider", | ||
| 382 | args: ["--line", "71", "--column", "5", "/tmp/workspace/src/open.ts"], | ||
| 383 | }); | ||
| 384 | |||
| 385 | const rubymineLineAndColumn = yield* resolveEditorLaunch( | ||
| 386 | { cwd: "/tmp/workspace/src/open.ts:71:5", editor: "rubymine" }, | ||
| 387 | "darwin", | ||
| 388 | ); | ||
| 389 | assert.deepEqual(rubymineLineAndColumn, { | ||
| 390 | command: "rubymine", | ||
| 391 | args: ["--line", "71", "--column", "5", "/tmp/workspace/src/open.ts"], | ||
| 392 | }); | ||
| 393 | |||
| 394 | const rustroverLineAndColumn = yield* resolveEditorLaunch( | ||
| 395 | { cwd: "/tmp/workspace/src/open.ts:71:5", editor: "rustrover" }, | ||
| 396 | "darwin", | ||
| 397 | ); | ||
| 398 | assert.deepEqual(rustroverLineAndColumn, { | ||
| 399 | command: "rustrover", | ||
| 400 | args: ["--line", "71", "--column", "5", "/tmp/workspace/src/open.ts"], | ||
| 401 | }); | ||
| 402 | |||
| 403 | const webstormLineOnly = yield* resolveEditorLaunch( | ||
| 404 | { cwd: "/tmp/workspace/AGENTS.md:48", editor: "webstorm" }, | ||
| 405 | "darwin", | ||
| 406 | ); | ||
| 407 | assert.deepEqual(webstormLineOnly, { | ||
| 408 | command: "webstorm", | ||
| 409 | args: ["--line", "48", "/tmp/workspace/AGENTS.md"], | ||
| 410 | }); | ||
| 411 | }), | ||
| 412 | ); | ||
| 413 | |||
| 414 | it.effect("falls back to zeditor when zed is not installed", () => | ||
| 415 | Effect.gen(function* () { | ||
| 416 | const fs = yield* FileSystem.FileSystem; | ||
| 417 | const path = yield* Path.Path; | ||
| 418 | const dir = yield* fs.makeTempDirectoryScoped({ prefix: "t3-open-test-" }); | ||
| 419 | yield* fs.writeFileString(path.join(dir, "zeditor"), "#!/bin/sh\nexit 0\n"); | ||
| 420 | yield* fs.chmod(path.join(dir, "zeditor"), 0o755); | ||
| 421 | |||
| 422 | const result = yield* resolveEditorLaunch({ cwd: "/tmp/workspace", editor: "zed" }, "linux", { | ||
| 423 | PATH: dir, | ||
| 424 | }); | ||
| 425 | |||
| 426 | assert.deepEqual(result, { | ||
| 427 | command: "zeditor", | ||
| 428 | args: ["/tmp/workspace"], | ||
| 429 | }); | ||
| 430 | }), | ||
| 431 | ); | ||
| 432 | |||
| 433 | it.effect("falls back to the primary command when no alias is installed", () => | ||
| 434 | Effect.gen(function* () { | ||
| 435 | const result = yield* resolveEditorLaunch({ cwd: "/tmp/workspace", editor: "zed" }, "linux", { | ||
| 436 | PATH: "", | ||
| 437 | }); | ||
| 438 | assert.deepEqual(result, { | ||
| 439 | command: "zed", | ||
| 440 | args: ["/tmp/workspace"], | ||
| 441 | }); | ||
| 442 | }), | ||
| 443 | ); | ||
| 444 | |||
| 445 | it.effect("maps file-manager editor to OS open commands", () => | ||
| 446 | Effect.gen(function* () { | ||
| 447 | const launch1 = yield* resolveEditorLaunch( | ||
| 448 | { cwd: "/tmp/workspace", editor: "file-manager" }, | ||
| 449 | "darwin", | ||
| 450 | { PATH: "" }, | ||
| 451 | ); | ||
| 452 | assert.deepEqual(launch1, { | ||
| 453 | command: "open", | ||
| 454 | args: ["/tmp/workspace"], | ||
| 455 | }); | ||
| 456 | |||
| 457 | const launch2 = yield* resolveEditorLaunch( | ||
| 458 | { cwd: "C:\\workspace", editor: "file-manager" }, | ||
| 459 | "win32", | ||
| 460 | { PATH: "" }, | ||
| 461 | ); | ||
| 462 | assert.deepEqual(launch2, { | ||
| 463 | command: "explorer", | ||
| 464 | args: ["C:\\workspace"], | ||
| 465 | }); | ||
| 466 | |||
| 467 | const launch3 = yield* resolveEditorLaunch( | ||
| 468 | { cwd: "/tmp/workspace", editor: "file-manager" }, | ||
| 469 | "linux", | ||
| 470 | { PATH: "" }, | ||
| 471 | ); | ||
| 472 | assert.deepEqual(launch3, { | ||
| 473 | command: "xdg-open", | ||
| 474 | args: ["/tmp/workspace"], | ||
| 475 | }); | ||
| 476 | }), | ||
| 477 | ); | ||
| 478 | }); | ||
| 479 | |||
| 480 | it.layer(NodeServices.layer)("launchDetached", (it) => { | ||
| 481 | it.effect("resolves when command can be spawned", () => | ||
| 482 | Effect.gen(function* () { | ||
| 483 | const result = yield* launchDetached({ | ||
| 484 | command: process.execPath, | ||
| 485 | args: ["-e", "process.exit(0)"], | ||
| 486 | }).pipe(Effect.result); | ||
| 487 | assertSuccess(result, undefined); | ||
| 488 | }), | ||
| 489 | ); | ||
| 490 | |||
| 491 | it.effect("rejects when command does not exist", () => | ||
| 492 | Effect.gen(function* () { | ||
| 493 | const result = yield* launchDetached({ | ||
| 494 | command: `t3code-no-such-command-${yield* Random.nextUUIDv4}`, | ||
| 495 | args: [], | ||
| 496 | }).pipe(Effect.result); | ||
| 497 | assert.equal(result._tag, "Failure"); | ||
| 498 | }), | ||
| 499 | ); | ||
| 500 | }); | ||
| 501 | |||
| 502 | it.layer(NodeServices.layer)("isCommandAvailable", (it) => { | ||
| 503 | it.effect("resolves win32 commands with PATHEXT", () => | ||
| 504 | Effect.gen(function* () { | ||
| 505 | const fs = yield* FileSystem.FileSystem; | ||
| 506 | const path = yield* Path.Path; | ||
| 507 | const dir = yield* fs.makeTempDirectoryScoped({ prefix: "t3-open-test-" }); | ||
| 508 | yield* fs.writeFileString(path.join(dir, "code.CMD"), "@echo off\r\n"); | ||
| 509 | const env = { | ||
| 510 | PATH: dir, | ||
| 511 | PATHEXT: ".COM;.EXE;.BAT;.CMD", | ||
| 512 | } satisfies NodeJS.ProcessEnv; | ||
| 513 | assert.equal(isCommandAvailable("code", { platform: "win32", env }), true); | ||
| 514 | }), | ||
| 515 | ); | ||
| 516 | |||
| 517 | it("returns false when a command is not on PATH", () => { | ||
| 518 | const env = { | ||
| 519 | PATH: "", | ||
| 520 | PATHEXT: ".COM;.EXE;.BAT;.CMD", | ||
| 521 | } satisfies NodeJS.ProcessEnv; | ||
| 522 | assert.equal(isCommandAvailable("definitely-not-installed", { platform: "win32", env }), false); | ||
| 523 | }); | ||
| 524 | |||
| 525 | it.effect("does not treat bare files without executable extension as available on win32", () => | ||
| 526 | Effect.gen(function* () { | ||
| 527 | const fs = yield* FileSystem.FileSystem; | ||
| 528 | const path = yield* Path.Path; | ||
| 529 | const dir = yield* fs.makeTempDirectoryScoped({ prefix: "t3-open-test-" }); | ||
| 530 | yield* fs.writeFileString(path.join(dir, "npm"), "echo nope\r\n"); | ||
| 531 | const env = { | ||
| 532 | PATH: dir, | ||
| 533 | PATHEXT: ".COM;.EXE;.BAT;.CMD", | ||
| 534 | } satisfies NodeJS.ProcessEnv; | ||
| 535 | assert.equal(isCommandAvailable("npm", { platform: "win32", env }), false); | ||
| 536 | }), | ||
| 537 | ); | ||
| 538 | |||
| 539 | it.effect("appends PATHEXT for commands with non-executable extensions on win32", () => | ||
| 540 | Effect.gen(function* () { | ||
| 541 | const fs = yield* FileSystem.FileSystem; | ||
| 542 | const path = yield* Path.Path; | ||
| 543 | const dir = yield* fs.makeTempDirectoryScoped({ prefix: "t3-open-test-" }); | ||
| 544 | yield* fs.writeFileString(path.join(dir, "my.tool.CMD"), "@echo off\r\n"); | ||
| 545 | const env = { | ||
| 546 | PATH: dir, | ||
| 547 | PATHEXT: ".COM;.EXE;.BAT;.CMD", | ||
| 548 | } satisfies NodeJS.ProcessEnv; | ||
| 549 | assert.equal(isCommandAvailable("my.tool", { platform: "win32", env }), true); | ||
| 550 | }), | ||
| 551 | ); | ||
| 552 | |||
| 553 | it.effect("uses platform-specific PATH delimiter for platform overrides", () => | ||
| 554 | Effect.gen(function* () { | ||
| 555 | const fs = yield* FileSystem.FileSystem; | ||
| 556 | const path = yield* Path.Path; | ||
| 557 | const firstDir = yield* fs.makeTempDirectoryScoped({ prefix: "t3-open-test-" }); | ||
| 558 | const secondDir = yield* fs.makeTempDirectoryScoped({ prefix: "t3-open-test-" }); | ||
| 559 | yield* fs.writeFileString(path.join(firstDir, "code.CMD"), "@echo off\r\n"); | ||
| 560 | yield* fs.writeFileString(path.join(secondDir, "code.CMD"), "MZ"); | ||
| 561 | const env = { | ||
| 562 | PATH: `${firstDir};${secondDir}`, | ||
| 563 | PATHEXT: ".COM;.EXE;.BAT;.CMD", | ||
| 564 | } satisfies NodeJS.ProcessEnv; | ||
| 565 | assert.equal(isCommandAvailable("code", { platform: "win32", env }), true); | ||
| 566 | }), | ||
| 567 | ); | ||
| 568 | }); | ||
| 569 | |||
| 570 | it.layer(NodeServices.layer)("resolveAvailableEditors", (it) => { | ||
| 571 | it.effect("returns installed editors for command launches", () => | ||
| 572 | Effect.gen(function* () { | ||
| 573 | const fs = yield* FileSystem.FileSystem; | ||
| 574 | const path = yield* Path.Path; | ||
| 575 | const dir = yield* fs.makeTempDirectoryScoped({ prefix: "t3-editors-" }); | ||
| 576 | |||
| 577 | yield* fs.writeFileString(path.join(dir, "trae.CMD"), "@echo off\r\n"); | ||
| 578 | yield* fs.writeFileString(path.join(dir, "kiro.CMD"), "@echo off\r\n"); | ||
| 579 | yield* fs.writeFileString(path.join(dir, "code-insiders.CMD"), "@echo off\r\n"); | ||
| 580 | yield* fs.writeFileString(path.join(dir, "codium.CMD"), "@echo off\r\n"); | ||
| 581 | yield* fs.writeFileString(path.join(dir, "aqua.CMD"), "@echo off\r\n"); | ||
| 582 | yield* fs.writeFileString(path.join(dir, "clion.CMD"), "@echo off\r\n"); | ||
| 583 | yield* fs.writeFileString(path.join(dir, "datagrip.CMD"), "@echo off\r\n"); | ||
| 584 | yield* fs.writeFileString(path.join(dir, "dataspell.CMD"), "@echo off\r\n"); | ||
| 585 | yield* fs.writeFileString(path.join(dir, "goland.CMD"), "@echo off\r\n"); | ||
| 586 | yield* fs.writeFileString(path.join(dir, "phpstorm.CMD"), "@echo off\r\n"); | ||
| 587 | yield* fs.writeFileString(path.join(dir, "pycharm.CMD"), "@echo off\r\n"); | ||
| 588 | yield* fs.writeFileString(path.join(dir, "rider.CMD"), "@echo off\r\n"); | ||
| 589 | yield* fs.writeFileString(path.join(dir, "rubymine.CMD"), "@echo off\r\n"); | ||
| 590 | yield* fs.writeFileString(path.join(dir, "rustrover.CMD"), "@echo off\r\n"); | ||
| 591 | yield* fs.writeFileString(path.join(dir, "webstorm.CMD"), "@echo off\r\n"); | ||
| 592 | yield* fs.writeFileString(path.join(dir, "explorer.CMD"), "MZ"); | ||
| 593 | const editors = resolveAvailableEditors("win32", { | ||
| 594 | PATH: dir, | ||
| 595 | PATHEXT: ".COM;.EXE;.BAT;.CMD", | ||
| 596 | }); | ||
| 597 | assert.deepEqual(editors, [ | ||
| 598 | "trae", | ||
| 599 | "kiro", | ||
| 600 | "vscode-insiders", | ||
| 601 | "vscodium", | ||
| 602 | "aqua", | ||
| 603 | "clion", | ||
| 604 | "datagrip", | ||
| 605 | "dataspell", | ||
| 606 | "goland", | ||
| 607 | "phpstorm", | ||
| 608 | "pycharm", | ||
| 609 | "rider", | ||
| 610 | "rubymine", | ||
| 611 | "rustrover", | ||
| 612 | "webstorm", | ||
| 613 | "file-manager", | ||
| 614 | ]); | ||
| 615 | }), | ||
| 616 | ); | ||
| 617 | |||
| 618 | it.effect("includes zed when only the zeditor command is installed", () => | ||
| 619 | Effect.gen(function* () { | ||
| 620 | const fs = yield* FileSystem.FileSystem; | ||
| 621 | const path = yield* Path.Path; | ||
| 622 | const dir = yield* fs.makeTempDirectoryScoped({ prefix: "t3-editors-" }); | ||
| 623 | |||
| 624 | yield* fs.writeFileString(path.join(dir, "zeditor"), "#!/bin/sh\nexit 0\n"); | ||
| 625 | yield* fs.writeFileString(path.join(dir, "xdg-open"), "#!/bin/sh\nexit 0\n"); | ||
| 626 | yield* fs.chmod(path.join(dir, "zeditor"), 0o755); | ||
| 627 | yield* fs.chmod(path.join(dir, "xdg-open"), 0o755); | ||
| 628 | |||
| 629 | const editors = resolveAvailableEditors("linux", { | ||
| 630 | PATH: dir, | ||
| 631 | }); | ||
| 632 | assert.deepEqual(editors, ["zed", "file-manager"]); | ||
| 633 | }), | ||
| 634 | ); | ||
| 635 | |||
| 636 | it("omits file-manager when the platform opener is unavailable", () => { | ||
| 637 | const editors = resolveAvailableEditors("linux", { | ||
| 638 | PATH: "", | ||
| 639 | }); | ||
| 640 | assert.deepEqual(editors, []); | ||
| 641 | }); | ||
| 642 | }); | ||
apps/server/src/open.ts deleted-241| ... | @@ -1,241 +0,0 @@ | ||
| 1 | // @effect-diagnostics nodeBuiltinImport:off | ||
| 2 | /** | ||
| 3 | * Open - Browser/editor launch service interface. | ||
| 4 | * | ||
| 5 | * Owns process launch helpers for opening URLs in a browser and workspace | ||
| 6 | * paths in a configured editor. | ||
| 7 | * | ||
| 8 | * @module Open | ||
| 9 | */ | ||
| 10 | import { spawn } from "node:child_process"; | ||
| 11 | |||
| 12 | import { EDITORS, OpenError, type EditorId } from "@t3tools/contracts"; | ||
| 13 | import { isCommandAvailable, type CommandAvailabilityOptions } from "@t3tools/shared/shell"; | ||
| 14 | import * as Context from "effect/Context"; | ||
| 15 | import * as Effect from "effect/Effect"; | ||
| 16 | import * as Layer from "effect/Layer"; | ||
| 17 | |||
| 18 | // ============================== | ||
| 19 | // Definitions | ||
| 20 | // ============================== | ||
| 21 | |||
| 22 | export { OpenError }; | ||
| 23 | export { isCommandAvailable } from "@t3tools/shared/shell"; | ||
| 24 | |||
| 25 | export interface OpenInEditorInput { | ||
| 26 | readonly cwd: string; | ||
| 27 | readonly editor: EditorId; | ||
| 28 | } | ||
| 29 | |||
| 30 | interface EditorLaunch { | ||
| 31 | readonly command: string; | ||
| 32 | readonly args: ReadonlyArray<string>; | ||
| 33 | } | ||
| 34 | |||
| 35 | const TARGET_WITH_POSITION_PATTERN = /^(.*?):(\d+)(?::(\d+))?$/; | ||
| 36 | |||
| 37 | function parseTargetPathAndPosition(target: string): { | ||
| 38 | path: string; | ||
| 39 | line: string | undefined; | ||
| 40 | column: string | undefined; | ||
| 41 | } | null { | ||
| 42 | const match = TARGET_WITH_POSITION_PATTERN.exec(target); | ||
| 43 | if (!match?.[1] || !match[2]) { | ||
| 44 | return null; | ||
| 45 | } | ||
| 46 | |||
| 47 | return { | ||
| 48 | path: match[1], | ||
| 49 | line: match[2], | ||
| 50 | column: match[3], | ||
| 51 | }; | ||
| 52 | } | ||
| 53 | |||
| 54 | function resolveCommandEditorArgs( | ||
| 55 | editor: (typeof EDITORS)[number], | ||
| 56 | target: string, | ||
| 57 | ): ReadonlyArray<string> { | ||
| 58 | const parsedTarget = parseTargetPathAndPosition(target); | ||
| 59 | |||
| 60 | switch (editor.launchStyle) { | ||
| 61 | case "direct-path": | ||
| 62 | return [target]; | ||
| 63 | case "goto": | ||
| 64 | return parsedTarget ? ["--goto", target] : [target]; | ||
| 65 | case "line-column": { | ||
| 66 | if (!parsedTarget) { | ||
| 67 | return [target]; | ||
| 68 | } | ||
| 69 | |||
| 70 | const { path, line, column } = parsedTarget; | ||
| 71 | return [...(line ? ["--line", line] : []), ...(column ? ["--column", column] : []), path]; | ||
| 72 | } | ||
| 73 | } | ||
| 74 | } | ||
| 75 | |||
| 76 | function resolveEditorArgs( | ||
| 77 | editor: (typeof EDITORS)[number], | ||
| 78 | target: string, | ||
| 79 | ): ReadonlyArray<string> { | ||
| 80 | const baseArgs = "baseArgs" in editor ? editor.baseArgs : []; | ||
| 81 | return [...baseArgs, ...resolveCommandEditorArgs(editor, target)]; | ||
| 82 | } | ||
| 83 | |||
| 84 | function resolveAvailableCommand( | ||
| 85 | commands: ReadonlyArray<string>, | ||
| 86 | options: CommandAvailabilityOptions = {}, | ||
| 87 | ): string | null { | ||
| 88 | for (const command of commands) { | ||
| 89 | if (isCommandAvailable(command, options)) { | ||
| 90 | return command; | ||
| 91 | } | ||
| 92 | } | ||
| 93 | return null; | ||
| 94 | } | ||
| 95 | |||
| 96 | function fileManagerCommandForPlatform(platform: NodeJS.Platform): string { | ||
| 97 | switch (platform) { | ||
| 98 | case "darwin": | ||
| 99 | return "open"; | ||
| 100 | case "win32": | ||
| 101 | return "explorer"; | ||
| 102 | default: | ||
| 103 | return "xdg-open"; | ||
| 104 | } | ||
| 105 | } | ||
| 106 | |||
| 107 | export function resolveAvailableEditors( | ||
| 108 | platform: NodeJS.Platform = process.platform, | ||
| 109 | env: NodeJS.ProcessEnv = process.env, | ||
| 110 | ): ReadonlyArray<EditorId> { | ||
| 111 | const available: EditorId[] = []; | ||
| 112 | |||
| 113 | for (const editor of EDITORS) { | ||
| 114 | if (editor.commands === null) { | ||
| 115 | const command = fileManagerCommandForPlatform(platform); | ||
| 116 | if (isCommandAvailable(command, { platform, env })) { | ||
| 117 | available.push(editor.id); | ||
| 118 | } | ||
| 119 | continue; | ||
| 120 | } | ||
| 121 | |||
| 122 | const command = resolveAvailableCommand(editor.commands, { platform, env }); | ||
| 123 | if (command !== null) { | ||
| 124 | available.push(editor.id); | ||
| 125 | } | ||
| 126 | } | ||
| 127 | |||
| 128 | return available; | ||
| 129 | } | ||
| 130 | |||
| 131 | /** | ||
| 132 | * OpenShape - Service API for browser and editor launch actions. | ||
| 133 | */ | ||
| 134 | export interface OpenShape { | ||
| 135 | /** | ||
| 136 | * Open a URL target in the default browser. | ||
| 137 | */ | ||
| 138 | readonly openBrowser: (target: string) => Effect.Effect<void, OpenError>; | ||
| 139 | |||
| 140 | /** | ||
| 141 | * Open a workspace path in a selected editor integration. | ||
| 142 | * | ||
| 143 | * Launches the editor as a detached process so server startup is not blocked. | ||
| 144 | */ | ||
| 145 | readonly openInEditor: (input: OpenInEditorInput) => Effect.Effect<void, OpenError>; | ||
| 146 | } | ||
| 147 | |||
| 148 | /** | ||
| 149 | * Open - Service tag for browser/editor launch operations. | ||
| 150 | */ | ||
| 151 | export class Open extends Context.Service<Open, OpenShape>()("t3/open") {} | ||
| 152 | |||
| 153 | // ============================== | ||
| 154 | // Implementations | ||
| 155 | // ============================== | ||
| 156 | |||
| 157 | export const resolveEditorLaunch = Effect.fn("resolveEditorLaunch")(function* ( | ||
| 158 | input: OpenInEditorInput, | ||
| 159 | platform: NodeJS.Platform = process.platform, | ||
| 160 | env: NodeJS.ProcessEnv = process.env, | ||
| 161 | ): Effect.fn.Return<EditorLaunch, OpenError> { | ||
| 162 | yield* Effect.annotateCurrentSpan({ | ||
| 163 | "open.editor": input.editor, | ||
| 164 | "open.cwd": input.cwd, | ||
| 165 | "open.platform": platform, | ||
| 166 | }); | ||
| 167 | const editorDef = EDITORS.find((editor) => editor.id === input.editor); | ||
| 168 | if (!editorDef) { | ||
| 169 | return yield* new OpenError({ message: `Unknown editor: ${input.editor}` }); | ||
| 170 | } | ||
| 171 | |||
| 172 | if (editorDef.commands) { | ||
| 173 | const command = | ||
| 174 | resolveAvailableCommand(editorDef.commands, { platform, env }) ?? editorDef.commands[0]; | ||
| 175 | return { | ||
| 176 | command, | ||
| 177 | args: resolveEditorArgs(editorDef, input.cwd), | ||
| 178 | }; | ||
| 179 | } | ||
| 180 | |||
| 181 | if (editorDef.id !== "file-manager") { | ||
| 182 | return yield* new OpenError({ message: `Unsupported editor: ${input.editor}` }); | ||
| 183 | } | ||
| 184 | |||
| 185 | return { command: fileManagerCommandForPlatform(platform), args: [input.cwd] }; | ||
| 186 | }); | ||
| 187 | |||
| 188 | export const launchDetached = (launch: EditorLaunch) => | ||
| 189 | Effect.gen(function* () { | ||
| 190 | if (!isCommandAvailable(launch.command)) { | ||
| 191 | return yield* new OpenError({ message: `Editor command not found: ${launch.command}` }); | ||
| 192 | } | ||
| 193 | |||
| 194 | yield* Effect.callback<void, OpenError>((resume) => { | ||
| 195 | let child; | ||
| 196 | try { | ||
| 197 | const isWin32 = process.platform === "win32"; | ||
| 198 | child = spawn( | ||
| 199 | launch.command, | ||
| 200 | isWin32 ? launch.args.map((a) => `"${a}"`) : [...launch.args], | ||
| 201 | { | ||
| 202 | detached: true, | ||
| 203 | stdio: "ignore", | ||
| 204 | shell: isWin32, | ||
| 205 | }, | ||
| 206 | ); | ||
| 207 | } catch (error) { | ||
| 208 | return resume( | ||
| 209 | Effect.fail(new OpenError({ message: "failed to spawn detached process", cause: error })), | ||
| 210 | ); | ||
| 211 | } | ||
| 212 | |||
| 213 | const handleSpawn = () => { | ||
| 214 | child.unref(); | ||
| 215 | resume(Effect.void); | ||
| 216 | }; | ||
| 217 | |||
| 218 | child.once("spawn", handleSpawn); | ||
| 219 | child.once("error", (cause) => | ||
| 220 | resume(Effect.fail(new OpenError({ message: "failed to spawn detached process", cause }))), | ||
| 221 | ); | ||
| 222 | }); | ||
| 223 | }); | ||
| 224 | |||
| 225 | const make = Effect.gen(function* () { | ||
| 226 | const open = yield* Effect.tryPromise({ | ||
| 227 | try: () => import("open"), | ||
| 228 | catch: (cause) => new OpenError({ message: "failed to load browser opener", cause }), | ||
| 229 | }); | ||
| 230 | |||
| 231 | return { | ||
| 232 | openBrowser: (target) => | ||
| 233 | Effect.tryPromise({ | ||
| 234 | try: () => open.default(target), | ||
| 235 | catch: (cause) => new OpenError({ message: "Browser auto-open failed", cause }), | ||
| 236 | }), | ||
| 237 | openInEditor: (input) => Effect.flatMap(resolveEditorLaunch(input), launchDetached), | ||
| 238 | } satisfies OpenShape; | ||
| 239 | }); | ||
| 240 | |||
| 241 | export const OpenLive = Layer.effect(Open, make); | ||
apps/server/src/process/externalLauncher.test.ts created+802| ... | @@ -0,0 +1,802 @@ | ||
| 1 | import * as NodeServices from "@effect/platform-node/NodeServices"; | ||
| 2 | import { assert, it } from "@effect/vitest"; | ||
| 3 | import { assertSuccess } from "@effect/vitest/utils"; | ||
| 4 | import * as Effect from "effect/Effect"; | ||
| 5 | import * as Encoding from "effect/Encoding"; | ||
| 6 | import * as FileSystem from "effect/FileSystem"; | ||
| 7 | import * as Layer from "effect/Layer"; | ||
| 8 | import * as Path from "effect/Path"; | ||
| 9 | import * as Random from "effect/Random"; | ||
| 10 | import * as Sink from "effect/Sink"; | ||
| 11 | import * as Stream from "effect/Stream"; | ||
| 12 | import { ChildProcess, ChildProcessSpawner } from "effect/unstable/process"; | ||
| 13 | |||
| 14 | import { | ||
| 15 | isCommandAvailable, | ||
| 16 | launchBrowser, | ||
| 17 | launchEditorProcess, | ||
| 18 | resolveAvailableEditors, | ||
| 19 | resolveBrowserLaunch, | ||
| 20 | resolveEditorLaunch, | ||
| 21 | } from "./externalLauncher.ts"; | ||
| 22 | |||
| 23 | function encodeUtf16LeBase64(input: string): string { | ||
| 24 | const bytes = new Uint8Array(input.length * 2); | ||
| 25 | for (let index = 0; index < input.length; index += 1) { | ||
| 26 | const code = input.charCodeAt(index); | ||
| 27 | bytes[index * 2] = code & 0xff; | ||
| 28 | bytes[index * 2 + 1] = code >>> 8; | ||
| 29 | } | ||
| 30 | return Encoding.encodeBase64(bytes); | ||
| 31 | } | ||
| 32 | |||
| 33 | function makeMockDetachedHandle(onUnref: () => void = () => undefined) { | ||
| 34 | return ChildProcessSpawner.makeHandle({ | ||
| 35 | pid: ChildProcessSpawner.ProcessId(1), | ||
| 36 | exitCode: Effect.succeed(ChildProcessSpawner.ExitCode(0)), | ||
| 37 | isRunning: Effect.succeed(true), | ||
| 38 | kill: () => Effect.void, | ||
| 39 | unref: Effect.sync(() => { | ||
| 40 | onUnref(); | ||
| 41 | return Effect.void; | ||
| 42 | }), | ||
| 43 | stdin: Sink.drain, | ||
| 44 | stdout: Stream.empty, | ||
| 45 | stderr: Stream.empty, | ||
| 46 | all: Stream.empty, | ||
| 47 | getInputFd: () => Sink.drain, | ||
| 48 | getOutputFd: () => Stream.empty, | ||
| 49 | }); | ||
| 50 | } | ||
| 51 | |||
| 52 | it.layer(NodeServices.layer)("resolveEditorLaunch", (it) => { | ||
| 53 | it.effect("returns commands for command-based editors", () => | ||
| 54 | Effect.gen(function* () { | ||
| 55 | const antigravityLaunch = yield* resolveEditorLaunch( | ||
| 56 | { cwd: "/tmp/workspace", editor: "antigravity" }, | ||
| 57 | "darwin", | ||
| 58 | { PATH: "" }, | ||
| 59 | ); | ||
| 60 | assert.deepEqual(antigravityLaunch, { | ||
| 61 | command: "agy", | ||
| 62 | args: ["/tmp/workspace"], | ||
| 63 | }); | ||
| 64 | |||
| 65 | const cursorLaunch = yield* resolveEditorLaunch( | ||
| 66 | { cwd: "/tmp/workspace", editor: "cursor" }, | ||
| 67 | "darwin", | ||
| 68 | { PATH: "" }, | ||
| 69 | ); | ||
| 70 | assert.deepEqual(cursorLaunch, { | ||
| 71 | command: "cursor", | ||
| 72 | args: ["/tmp/workspace"], | ||
| 73 | }); | ||
| 74 | |||
| 75 | const traeLaunch = yield* resolveEditorLaunch( | ||
| 76 | { cwd: "/tmp/workspace", editor: "trae" }, | ||
| 77 | "darwin", | ||
| 78 | ); | ||
| 79 | assert.deepEqual(traeLaunch, { | ||
| 80 | command: "trae", | ||
| 81 | args: ["/tmp/workspace"], | ||
| 82 | }); | ||
| 83 | |||
| 84 | const kiroLaunch = yield* resolveEditorLaunch( | ||
| 85 | { cwd: "/tmp/workspace", editor: "kiro" }, | ||
| 86 | "darwin", | ||
| 87 | { PATH: "" }, | ||
| 88 | ); | ||
| 89 | assert.deepEqual(kiroLaunch, { | ||
| 90 | command: "kiro", | ||
| 91 | args: ["ide", "/tmp/workspace"], | ||
| 92 | }); | ||
| 93 | |||
| 94 | const vscodeLaunch = yield* resolveEditorLaunch( | ||
| 95 | { cwd: "/tmp/workspace", editor: "vscode" }, | ||
| 96 | "darwin", | ||
| 97 | { PATH: "" }, | ||
| 98 | ); | ||
| 99 | assert.deepEqual(vscodeLaunch, { | ||
| 100 | command: "code", | ||
| 101 | args: ["/tmp/workspace"], | ||
| 102 | }); | ||
| 103 | |||
| 104 | const vscodeInsidersLaunch = yield* resolveEditorLaunch( | ||
| 105 | { cwd: "/tmp/workspace", editor: "vscode-insiders" }, | ||
| 106 | "darwin", | ||
| 107 | ); | ||
| 108 | assert.deepEqual(vscodeInsidersLaunch, { | ||
| 109 | command: "code-insiders", | ||
| 110 | args: ["/tmp/workspace"], | ||
| 111 | }); | ||
| 112 | |||
| 113 | const vscodiumLaunch = yield* resolveEditorLaunch( | ||
| 114 | { cwd: "/tmp/workspace", editor: "vscodium" }, | ||
| 115 | "darwin", | ||
| 116 | ); | ||
| 117 | assert.deepEqual(vscodiumLaunch, { | ||
| 118 | command: "codium", | ||
| 119 | args: ["/tmp/workspace"], | ||
| 120 | }); | ||
| 121 | |||
| 122 | const zedLaunch = yield* resolveEditorLaunch( | ||
| 123 | { cwd: "/tmp/workspace", editor: "zed" }, | ||
| 124 | "darwin", | ||
| 125 | { PATH: "" }, | ||
| 126 | ); | ||
| 127 | assert.deepEqual(zedLaunch, { | ||
| 128 | command: "zed", | ||
| 129 | args: ["/tmp/workspace"], | ||
| 130 | }); | ||
| 131 | |||
| 132 | const ideaLaunch = yield* resolveEditorLaunch( | ||
| 133 | { cwd: "/tmp/workspace", editor: "idea" }, | ||
| 134 | "darwin", | ||
| 135 | ); | ||
| 136 | assert.deepEqual(ideaLaunch, { | ||
| 137 | command: "idea", | ||
| 138 | args: ["/tmp/workspace"], | ||
| 139 | }); | ||
| 140 | |||
| 141 | const aquaLaunch = yield* resolveEditorLaunch( | ||
| 142 | { cwd: "/tmp/workspace", editor: "aqua" }, | ||
| 143 | "darwin", | ||
| 144 | ); | ||
| 145 | assert.deepEqual(aquaLaunch, { | ||
| 146 | command: "aqua", | ||
| 147 | args: ["/tmp/workspace"], | ||
| 148 | }); | ||
| 149 | |||
| 150 | const clionLaunch = yield* resolveEditorLaunch( | ||
| 151 | { cwd: "/tmp/workspace", editor: "clion" }, | ||
| 152 | "darwin", | ||
| 153 | ); | ||
| 154 | assert.deepEqual(clionLaunch, { | ||
| 155 | command: "clion", | ||
| 156 | args: ["/tmp/workspace"], | ||
| 157 | }); | ||
| 158 | |||
| 159 | const datagripLaunch = yield* resolveEditorLaunch( | ||
| 160 | { cwd: "/tmp/workspace", editor: "datagrip" }, | ||
| 161 | "darwin", | ||
| 162 | ); | ||
| 163 | assert.deepEqual(datagripLaunch, { | ||
| 164 | command: "datagrip", | ||
| 165 | args: ["/tmp/workspace"], | ||
| 166 | }); | ||
| 167 | |||
| 168 | const dataspellLaunch = yield* resolveEditorLaunch( | ||
| 169 | { cwd: "/tmp/workspace", editor: "dataspell" }, | ||
| 170 | "darwin", | ||
| 171 | ); | ||
| 172 | assert.deepEqual(dataspellLaunch, { | ||
| 173 | command: "dataspell", | ||
| 174 | args: ["/tmp/workspace"], | ||
| 175 | }); | ||
| 176 | |||
| 177 | const golandLaunch = yield* resolveEditorLaunch( | ||
| 178 | { cwd: "/tmp/workspace", editor: "goland" }, | ||
| 179 | "darwin", | ||
| 180 | ); | ||
| 181 | assert.deepEqual(golandLaunch, { | ||
| 182 | command: "goland", | ||
| 183 | args: ["/tmp/workspace"], | ||
| 184 | }); | ||
| 185 | |||
| 186 | const phpstormLaunch = yield* resolveEditorLaunch( | ||
| 187 | { cwd: "/tmp/workspace", editor: "phpstorm" }, | ||
| 188 | "darwin", | ||
| 189 | ); | ||
| 190 | assert.deepEqual(phpstormLaunch, { | ||
| 191 | command: "phpstorm", | ||
| 192 | args: ["/tmp/workspace"], | ||
| 193 | }); | ||
| 194 | |||
| 195 | const pycharmLaunch = yield* resolveEditorLaunch( | ||
| 196 | { cwd: "/tmp/workspace", editor: "pycharm" }, | ||
| 197 | "darwin", | ||
| 198 | ); | ||
| 199 | assert.deepEqual(pycharmLaunch, { | ||
| 200 | command: "pycharm", | ||
| 201 | args: ["/tmp/workspace"], | ||
| 202 | }); | ||
| 203 | |||
| 204 | const riderLaunch = yield* resolveEditorLaunch( | ||
| 205 | { cwd: "/tmp/workspace", editor: "rider" }, | ||
| 206 | "darwin", | ||
| 207 | ); | ||
| 208 | assert.deepEqual(riderLaunch, { | ||
| 209 | command: "rider", | ||
| 210 | args: ["/tmp/workspace"], | ||
| 211 | }); | ||
| 212 | |||
| 213 | const rubymineLaunch = yield* resolveEditorLaunch( | ||
| 214 | { cwd: "/tmp/workspace", editor: "rubymine" }, | ||
| 215 | "darwin", | ||
| 216 | ); | ||
| 217 | assert.deepEqual(rubymineLaunch, { | ||
| 218 | command: "rubymine", | ||
| 219 | args: ["/tmp/workspace"], | ||
| 220 | }); | ||
| 221 | |||
| 222 | const rustroverLaunch = yield* resolveEditorLaunch( | ||
| 223 | { cwd: "/tmp/workspace", editor: "rustrover" }, | ||
| 224 | "darwin", | ||
| 225 | ); | ||
| 226 | assert.deepEqual(rustroverLaunch, { | ||
| 227 | command: "rustrover", | ||
| 228 | args: ["/tmp/workspace"], | ||
| 229 | }); | ||
| 230 | |||
| 231 | const webstormLaunch = yield* resolveEditorLaunch( | ||
| 232 | { cwd: "/tmp/workspace", editor: "webstorm" }, | ||
| 233 | "darwin", | ||
| 234 | ); | ||
| 235 | assert.deepEqual(webstormLaunch, { | ||
| 236 | command: "webstorm", | ||
| 237 | args: ["/tmp/workspace"], | ||
| 238 | }); | ||
| 239 | }), | ||
| 240 | ); | ||
| 241 | |||
| 242 | it.effect("applies launch-style-specific navigation arguments", () => | ||
| 243 | Effect.gen(function* () { | ||
| 244 | const lineOnly = yield* resolveEditorLaunch( | ||
| 245 | { cwd: "/tmp/workspace/AGENTS.md:48", editor: "cursor" }, | ||
| 246 | "darwin", | ||
| 247 | { PATH: "" }, | ||
| 248 | ); | ||
| 249 | assert.deepEqual(lineOnly, { | ||
| 250 | command: "cursor", | ||
| 251 | args: ["--goto", "/tmp/workspace/AGENTS.md:48"], | ||
| 252 | }); | ||
| 253 | |||
| 254 | const lineAndColumn = yield* resolveEditorLaunch( | ||
| 255 | { cwd: "/tmp/workspace/src/process/externalLauncher.ts:71:5", editor: "cursor" }, | ||
| 256 | "darwin", | ||
| 257 | { PATH: "" }, | ||
| 258 | ); | ||
| 259 | assert.deepEqual(lineAndColumn, { | ||
| 260 | command: "cursor", | ||
| 261 | args: ["--goto", "/tmp/workspace/src/process/externalLauncher.ts:71:5"], | ||
| 262 | }); | ||
| 263 | |||
| 264 | const traeLineAndColumn = yield* resolveEditorLaunch( | ||
| 265 | { cwd: "/tmp/workspace/src/process/externalLauncher.ts:71:5", editor: "trae" }, | ||
| 266 | "darwin", | ||
| 267 | ); | ||
| 268 | assert.deepEqual(traeLineAndColumn, { | ||
| 269 | command: "trae", | ||
| 270 | args: ["--goto", "/tmp/workspace/src/process/externalLauncher.ts:71:5"], | ||
| 271 | }); | ||
| 272 | |||
| 273 | const kiroLineAndColumn = yield* resolveEditorLaunch( | ||
| 274 | { cwd: "/tmp/workspace/src/process/externalLauncher.ts:71:5", editor: "kiro" }, | ||
| 275 | "darwin", | ||
| 276 | { PATH: "" }, | ||
| 277 | ); | ||
| 278 | assert.deepEqual(kiroLineAndColumn, { | ||
| 279 | command: "kiro", | ||
| 280 | args: ["ide", "--goto", "/tmp/workspace/src/process/externalLauncher.ts:71:5"], | ||
| 281 | }); | ||
| 282 | |||
| 283 | const vscodeLineAndColumn = yield* resolveEditorLaunch( | ||
| 284 | { cwd: "/tmp/workspace/src/process/externalLauncher.ts:71:5", editor: "vscode" }, | ||
| 285 | "darwin", | ||
| 286 | { PATH: "" }, | ||
| 287 | ); | ||
| 288 | assert.deepEqual(vscodeLineAndColumn, { | ||
| 289 | command: "code", | ||
| 290 | args: ["--goto", "/tmp/workspace/src/process/externalLauncher.ts:71:5"], | ||
| 291 | }); | ||
| 292 | |||
| 293 | const vscodeInsidersLineAndColumn = yield* resolveEditorLaunch( | ||
| 294 | { cwd: "/tmp/workspace/src/process/externalLauncher.ts:71:5", editor: "vscode-insiders" }, | ||
| 295 | "darwin", | ||
| 296 | ); | ||
| 297 | assert.deepEqual(vscodeInsidersLineAndColumn, { | ||
| 298 | command: "code-insiders", | ||
| 299 | args: ["--goto", "/tmp/workspace/src/process/externalLauncher.ts:71:5"], | ||
| 300 | }); | ||
| 301 | |||
| 302 | const vscodiumLineAndColumn = yield* resolveEditorLaunch( | ||
| 303 | { cwd: "/tmp/workspace/src/process/externalLauncher.ts:71:5", editor: "vscodium" }, | ||
| 304 | "darwin", | ||
| 305 | ); | ||
| 306 | assert.deepEqual(vscodiumLineAndColumn, { | ||
| 307 | command: "codium", | ||
| 308 | args: ["--goto", "/tmp/workspace/src/process/externalLauncher.ts:71:5"], | ||
| 309 | }); | ||
| 310 | |||
| 311 | const zedLineAndColumn = yield* resolveEditorLaunch( | ||
| 312 | { cwd: "/tmp/workspace/src/process/externalLauncher.ts:71:5", editor: "zed" }, | ||
| 313 | "darwin", | ||
| 314 | { PATH: "" }, | ||
| 315 | ); | ||
| 316 | assert.deepEqual(zedLineAndColumn, { | ||
| 317 | command: "zed", | ||
| 318 | args: ["/tmp/workspace/src/process/externalLauncher.ts:71:5"], | ||
| 319 | }); | ||
| 320 | |||
| 321 | const zedLineOnly = yield* resolveEditorLaunch( | ||
| 322 | { cwd: "/tmp/workspace/AGENTS.md:48", editor: "zed" }, | ||
| 323 | "darwin", | ||
| 324 | { PATH: "" }, | ||
| 325 | ); | ||
| 326 | assert.deepEqual(zedLineOnly, { | ||
| 327 | command: "zed", | ||
| 328 | args: ["/tmp/workspace/AGENTS.md:48"], | ||
| 329 | }); | ||
| 330 | |||
| 331 | const ideaLineOnly = yield* resolveEditorLaunch( | ||
| 332 | { cwd: "/tmp/workspace/AGENTS.md:48", editor: "idea" }, | ||
| 333 | "darwin", | ||
| 334 | ); | ||
| 335 | assert.deepEqual(ideaLineOnly, { | ||
| 336 | command: "idea", | ||
| 337 | args: ["--line", "48", "/tmp/workspace/AGENTS.md"], | ||
| 338 | }); | ||
| 339 | |||
| 340 | const ideaLineAndColumn = yield* resolveEditorLaunch( | ||
| 341 | { cwd: "/tmp/workspace/src/process/externalLauncher.ts:71:5", editor: "idea" }, | ||
| 342 | "darwin", | ||
| 343 | ); | ||
| 344 | assert.deepEqual(ideaLineAndColumn, { | ||
| 345 | command: "idea", | ||
| 346 | args: ["--line", "71", "--column", "5", "/tmp/workspace/src/process/externalLauncher.ts"], | ||
| 347 | }); | ||
| 348 | |||
| 349 | const aquaLineAndColumn = yield* resolveEditorLaunch( | ||
| 350 | { cwd: "/tmp/workspace/src/process/externalLauncher.ts:71:5", editor: "aqua" }, | ||
| 351 | "darwin", | ||
| 352 | ); | ||
| 353 | assert.deepEqual(aquaLineAndColumn, { | ||
| 354 | command: "aqua", | ||
| 355 | args: ["--line", "71", "--column", "5", "/tmp/workspace/src/process/externalLauncher.ts"], | ||
| 356 | }); | ||
| 357 | |||
| 358 | const clionLineAndColumn = yield* resolveEditorLaunch( | ||
| 359 | { cwd: "/tmp/workspace/src/process/externalLauncher.ts:71:5", editor: "clion" }, | ||
| 360 | "darwin", | ||
| 361 | ); | ||
| 362 | assert.deepEqual(clionLineAndColumn, { | ||
| 363 | command: "clion", | ||
| 364 | args: ["--line", "71", "--column", "5", "/tmp/workspace/src/process/externalLauncher.ts"], | ||
| 365 | }); | ||
| 366 | |||
| 367 | const datagripLineAndColumn = yield* resolveEditorLaunch( | ||
| 368 | { cwd: "/tmp/workspace/src/process/externalLauncher.ts:71:5", editor: "datagrip" }, | ||
| 369 | "darwin", | ||
| 370 | ); | ||
| 371 | assert.deepEqual(datagripLineAndColumn, { | ||
| 372 | command: "datagrip", | ||
| 373 | args: ["--line", "71", "--column", "5", "/tmp/workspace/src/process/externalLauncher.ts"], | ||
| 374 | }); | ||
| 375 | |||
| 376 | const dataspellLineAndColumn = yield* resolveEditorLaunch( | ||
| 377 | { cwd: "/tmp/workspace/src/process/externalLauncher.ts:71:5", editor: "dataspell" }, | ||
| 378 | "darwin", | ||
| 379 | ); | ||
| 380 | assert.deepEqual(dataspellLineAndColumn, { | ||
| 381 | command: "dataspell", | ||
| 382 | args: ["--line", "71", "--column", "5", "/tmp/workspace/src/process/externalLauncher.ts"], | ||
| 383 | }); | ||
| 384 | |||
| 385 | const golandLineAndColumn = yield* resolveEditorLaunch( | ||
| 386 | { cwd: "/tmp/workspace/src/process/externalLauncher.ts:71:5", editor: "goland" }, | ||
| 387 | "darwin", | ||
| 388 | ); | ||
| 389 | assert.deepEqual(golandLineAndColumn, { | ||
| 390 | command: "goland", | ||
| 391 | args: ["--line", "71", "--column", "5", "/tmp/workspace/src/process/externalLauncher.ts"], | ||
| 392 | }); | ||
| 393 | |||
| 394 | const phpstormLineAndColumn = yield* resolveEditorLaunch( | ||
| 395 | { cwd: "/tmp/workspace/src/process/externalLauncher.ts:71:5", editor: "phpstorm" }, | ||
| 396 | "darwin", | ||
| 397 | ); | ||
| 398 | assert.deepEqual(phpstormLineAndColumn, { | ||
| 399 | command: "phpstorm", | ||
| 400 | args: ["--line", "71", "--column", "5", "/tmp/workspace/src/process/externalLauncher.ts"], | ||
| 401 | }); | ||
| 402 | |||
| 403 | const pycharmLineAndColumn = yield* resolveEditorLaunch( | ||
| 404 | { cwd: "/tmp/workspace/src/process/externalLauncher.ts:71:5", editor: "pycharm" }, | ||
| 405 | "darwin", | ||
| 406 | ); | ||
| 407 | assert.deepEqual(pycharmLineAndColumn, { | ||
| 408 | command: "pycharm", | ||
| 409 | args: ["--line", "71", "--column", "5", "/tmp/workspace/src/process/externalLauncher.ts"], | ||
| 410 | }); | ||
| 411 | |||
| 412 | const riderLineAndColumn = yield* resolveEditorLaunch( | ||
| 413 | { cwd: "/tmp/workspace/src/process/externalLauncher.ts:71:5", editor: "rider" }, | ||
| 414 | "darwin", | ||
| 415 | ); | ||
| 416 | assert.deepEqual(riderLineAndColumn, { | ||
| 417 | command: "rider", | ||
| 418 | args: ["--line", "71", "--column", "5", "/tmp/workspace/src/process/externalLauncher.ts"], | ||
| 419 | }); | ||
| 420 | |||
| 421 | const rubymineLineAndColumn = yield* resolveEditorLaunch( | ||
| 422 | { cwd: "/tmp/workspace/src/process/externalLauncher.ts:71:5", editor: "rubymine" }, | ||
| 423 | "darwin", | ||
| 424 | ); | ||
| 425 | assert.deepEqual(rubymineLineAndColumn, { | ||
| 426 | command: "rubymine", | ||
| 427 | args: ["--line", "71", "--column", "5", "/tmp/workspace/src/process/externalLauncher.ts"], | ||
| 428 | }); | ||
| 429 | |||
| 430 | const rustroverLineAndColumn = yield* resolveEditorLaunch( | ||
| 431 | { cwd: "/tmp/workspace/src/process/externalLauncher.ts:71:5", editor: "rustrover" }, | ||
| 432 | "darwin", | ||
| 433 | ); | ||
| 434 | assert.deepEqual(rustroverLineAndColumn, { | ||
| 435 | command: "rustrover", | ||
| 436 | args: ["--line", "71", "--column", "5", "/tmp/workspace/src/process/externalLauncher.ts"], | ||
| 437 | }); | ||
| 438 | |||
| 439 | const webstormLineOnly = yield* resolveEditorLaunch( | ||
| 440 | { cwd: "/tmp/workspace/AGENTS.md:48", editor: "webstorm" }, | ||
| 441 | "darwin", | ||
| 442 | ); | ||
| 443 | assert.deepEqual(webstormLineOnly, { | ||
| 444 | command: "webstorm", | ||
| 445 | args: ["--line", "48", "/tmp/workspace/AGENTS.md"], | ||
| 446 | }); | ||
| 447 | }), | ||
| 448 | ); | ||
| 449 | |||
| 450 | it.effect("falls back to zeditor when zed is not installed", () => | ||
| 451 | Effect.gen(function* () { | ||
| 452 | const fs = yield* FileSystem.FileSystem; | ||
| 453 | const path = yield* Path.Path; | ||
| 454 | const dir = yield* fs.makeTempDirectoryScoped({ prefix: "t3-external-launcher-test-" }); | ||
| 455 | yield* fs.writeFileString(path.join(dir, "zeditor"), "#!/bin/sh\nexit 0\n"); | ||
| 456 | yield* fs.chmod(path.join(dir, "zeditor"), 0o755); | ||
| 457 | |||
| 458 | const result = yield* resolveEditorLaunch({ cwd: "/tmp/workspace", editor: "zed" }, "linux", { | ||
| 459 | PATH: dir, | ||
| 460 | }); | ||
| 461 | |||
| 462 | assert.deepEqual(result, { | ||
| 463 | command: "zeditor", | ||
| 464 | args: ["/tmp/workspace"], | ||
| 465 | }); | ||
| 466 | }), | ||
| 467 | ); | ||
| 468 | |||
| 469 | it.effect("falls back to the primary command when no alias is installed", () => | ||
| 470 | Effect.gen(function* () { | ||
| 471 | const result = yield* resolveEditorLaunch({ cwd: "/tmp/workspace", editor: "zed" }, "linux", { | ||
| 472 | PATH: "", | ||
| 473 | }); | ||
| 474 | assert.deepEqual(result, { | ||
| 475 | command: "zed", | ||
| 476 | args: ["/tmp/workspace"], | ||
| 477 | }); | ||
| 478 | }), | ||
| 479 | ); | ||
| 480 | |||
| 481 | it.effect("maps file-manager editor to OS open commands", () => | ||
| 482 | Effect.gen(function* () { | ||
| 483 | const launch1 = yield* resolveEditorLaunch( | ||
| 484 | { cwd: "/tmp/workspace", editor: "file-manager" }, | ||
| 485 | "darwin", | ||
| 486 | { PATH: "" }, | ||
| 487 | ); | ||
| 488 | assert.deepEqual(launch1, { | ||
| 489 | command: "open", | ||
| 490 | args: ["/tmp/workspace"], | ||
| 491 | }); | ||
| 492 | |||
| 493 | const launch2 = yield* resolveEditorLaunch( | ||
| 494 | { cwd: "C:\\workspace", editor: "file-manager" }, | ||
| 495 | "win32", | ||
| 496 | { PATH: "" }, | ||
| 497 | ); | ||
| 498 | assert.deepEqual(launch2, { | ||
| 499 | command: "explorer", | ||
| 500 | args: ["C:\\workspace"], | ||
| 501 | }); | ||
| 502 | |||
| 503 | const launch3 = yield* resolveEditorLaunch( | ||
| 504 | { cwd: "/tmp/workspace", editor: "file-manager" }, | ||
| 505 | "linux", | ||
| 506 | { PATH: "" }, | ||
| 507 | ); | ||
| 508 | assert.deepEqual(launch3, { | ||
| 509 | command: "xdg-open", | ||
| 510 | args: ["/tmp/workspace"], | ||
| 511 | }); | ||
| 512 | }), | ||
| 513 | ); | ||
| 514 | }); | ||
| 515 | |||
| 516 | it("resolveBrowserLaunch maps default browser launchers by platform", () => { | ||
| 517 | const target = "https://example.com/some path?name=o'hara"; | ||
| 518 | |||
| 519 | assert.deepEqual(resolveBrowserLaunch(target, "darwin").command, "open"); | ||
| 520 | assert.deepEqual(resolveBrowserLaunch(target, "darwin").args, [target]); | ||
| 521 | assert.deepEqual(resolveBrowserLaunch(target, "darwin").options, { | ||
| 522 | detached: true, | ||
| 523 | stdin: "ignore", | ||
| 524 | stdout: "ignore", | ||
| 525 | stderr: "ignore", | ||
| 526 | }); | ||
| 527 | |||
| 528 | assert.deepEqual(resolveBrowserLaunch(target, "linux", {}).command, "xdg-open"); | ||
| 529 | assert.deepEqual(resolveBrowserLaunch(target, "linux", {}).args, [target]); | ||
| 530 | |||
| 531 | const windows = resolveBrowserLaunch(target, "win32", { | ||
| 532 | SYSTEMROOT: "C:\\Windows", | ||
| 533 | }); | ||
| 534 | assert.equal(windows.command, "C:\\Windows\\System32\\WindowsPowerShell\\v1.0\\powershell.exe"); | ||
| 535 | assert.deepEqual(windows.args, [ | ||
| 536 | "-NoProfile", | ||
| 537 | "-NonInteractive", | ||
| 538 | "-ExecutionPolicy", | ||
| 539 | "Bypass", | ||
| 540 | "-EncodedCommand", | ||
| 541 | encodeUtf16LeBase64( | ||
| 542 | "$ProgressPreference = 'SilentlyContinue'; Start 'https://example.com/some path?name=o''hara'", | ||
| 543 | ), | ||
| 544 | ]); | ||
| 545 | assert.deepEqual(windows.options, { | ||
| 546 | detached: true, | ||
| 547 | shell: false, | ||
| 548 | stdin: "ignore", | ||
| 549 | stdout: "ignore", | ||
| 550 | stderr: "ignore", | ||
| 551 | }); | ||
| 552 | }); | ||
| 553 | |||
| 554 | it("resolveBrowserLaunch opens through Windows from WSL when not remote", () => { | ||
| 555 | const launch = resolveBrowserLaunch("https://example.com", "linux", { | ||
| 556 | WSL_DISTRO_NAME: "Ubuntu", | ||
| 557 | }); | ||
| 558 | assert.equal(launch.command, "/mnt/c/Windows/System32/WindowsPowerShell/v1.0/powershell.exe"); | ||
| 559 | assert.equal(launch.options.detached, true); | ||
| 560 | }); | ||
| 561 | |||
| 562 | it("resolveBrowserLaunch keeps xdg-open for WSL over SSH", () => { | ||
| 563 | const launch = resolveBrowserLaunch("https://example.com", "linux", { | ||
| 564 | WSL_DISTRO_NAME: "Ubuntu", | ||
| 565 | SSH_CONNECTION: "client server", | ||
| 566 | }); | ||
| 567 | assert.equal(launch.command, "xdg-open"); | ||
| 568 | }); | ||
| 569 | |||
| 570 | it.layer(NodeServices.layer)("launchBrowser", (it) => { | ||
| 571 | it.effect("spawns through the ChildProcessSpawner service and unrefs the handle", () => | ||
| 572 | Effect.gen(function* () { | ||
| 573 | let spawnedCommand: ChildProcess.StandardCommand | undefined; | ||
| 574 | let didUnref = false; | ||
| 575 | |||
| 576 | const spawnerLayer = Layer.mock(ChildProcessSpawner.ChildProcessSpawner, { | ||
| 577 | spawn: (command) => | ||
| 578 | Effect.sync(() => { | ||
| 579 | assert.equal(ChildProcess.isStandardCommand(command), true); | ||
| 580 | if (!ChildProcess.isStandardCommand(command)) { | ||
| 581 | throw new Error("Expected a standard command"); | ||
| 582 | } | ||
| 583 | spawnedCommand = command; | ||
| 584 | return makeMockDetachedHandle(() => { | ||
| 585 | didUnref = true; | ||
| 586 | }); | ||
| 587 | }), | ||
| 588 | }); | ||
| 589 | |||
| 590 | const result = yield* launchBrowser("https://example.com").pipe( | ||
| 591 | Effect.provide(spawnerLayer), | ||
| 592 | Effect.result, | ||
| 593 | ); | ||
| 594 | |||
| 595 | assertSuccess(result, undefined); | ||
| 596 | assert.ok(spawnedCommand); | ||
| 597 | const expectedLaunch = resolveBrowserLaunch("https://example.com"); | ||
| 598 | assert.equal(spawnedCommand.command, expectedLaunch.command); | ||
| 599 | assert.deepEqual(spawnedCommand.args, expectedLaunch.args); | ||
| 600 | assert.deepEqual(spawnedCommand.options, expectedLaunch.options); | ||
| 601 | assert.equal(didUnref, true); | ||
| 602 | }), | ||
| 603 | ); | ||
| 604 | }); | ||
| 605 | |||
| 606 | it.layer(NodeServices.layer)("launchEditorProcess", (it) => { | ||
| 607 | it.effect("spawns through the ChildProcessSpawner service and unrefs the handle", () => | ||
| 608 | Effect.gen(function* () { | ||
| 609 | let spawnedCommand: ChildProcess.StandardCommand | undefined; | ||
| 610 | let didUnref = false; | ||
| 611 | const expectedArgs = ["-e", "process.exit(0)"]; | ||
| 612 | |||
| 613 | const spawnerLayer = Layer.mock(ChildProcessSpawner.ChildProcessSpawner, { | ||
| 614 | spawn: (command) => | ||
| 615 | Effect.sync(() => { | ||
| 616 | assert.equal(ChildProcess.isStandardCommand(command), true); | ||
| 617 | if (!ChildProcess.isStandardCommand(command)) { | ||
| 618 | throw new Error("Expected a standard command"); | ||
| 619 | } | ||
| 620 | spawnedCommand = command; | ||
| 621 | return makeMockDetachedHandle(() => { | ||
| 622 | didUnref = true; | ||
| 623 | }); | ||
| 624 | }), | ||
| 625 | }); | ||
| 626 | |||
| 627 | const result = yield* launchEditorProcess({ | ||
| 628 | command: process.execPath, | ||
| 629 | args: expectedArgs, | ||
| 630 | }).pipe(Effect.provide(spawnerLayer), Effect.result); | ||
| 631 | |||
| 632 | assertSuccess(result, undefined); | ||
| 633 | assert.ok(spawnedCommand); | ||
| 634 | assert.equal(spawnedCommand.command, process.execPath); | ||
| 635 | assert.deepEqual( | ||
| 636 | spawnedCommand.args, | ||
| 637 | process.platform === "win32" ? expectedArgs.map((arg) => `"${arg}"`) : expectedArgs, | ||
| 638 | ); | ||
| 639 | assert.deepEqual(spawnedCommand.options, { | ||
| 640 | detached: true, | ||
| 641 | shell: process.platform === "win32", | ||
| 642 | stdin: "ignore", | ||
| 643 | stdout: "ignore", | ||
| 644 | stderr: "ignore", | ||
| 645 | }); | ||
| 646 | assert.equal(didUnref, true); | ||
| 647 | }), | ||
| 648 | ); | ||
| 649 | |||
| 650 | it.effect("rejects when command does not exist", () => | ||
| 651 | Effect.gen(function* () { | ||
| 652 | const spawnerLayer = Layer.mock(ChildProcessSpawner.ChildProcessSpawner, {}); | ||
| 653 | const result = yield* launchEditorProcess({ | ||
| 654 | command: `t3code-no-such-command-${yield* Random.nextUUIDv4}`, | ||
| 655 | args: [], | ||
| 656 | }).pipe(Effect.provide(spawnerLayer), Effect.result); | ||
| 657 | assert.equal(result._tag, "Failure"); | ||
| 658 | }), | ||
| 659 | ); | ||
| 660 | }); | ||
| 661 | |||
| 662 | it.layer(NodeServices.layer)("isCommandAvailable", (it) => { | ||
| 663 | it.effect("resolves win32 commands with PATHEXT", () => | ||
| 664 | Effect.gen(function* () { | ||
| 665 | const fs = yield* FileSystem.FileSystem; | ||
| 666 | const path = yield* Path.Path; | ||
| 667 | const dir = yield* fs.makeTempDirectoryScoped({ prefix: "t3-external-launcher-test-" }); | ||
| 668 | yield* fs.writeFileString(path.join(dir, "code.CMD"), "@echo off\r\n"); | ||
| 669 | const env = { | ||
| 670 | PATH: dir, | ||
| 671 | PATHEXT: ".COM;.EXE;.BAT;.CMD", | ||
| 672 | } satisfies NodeJS.ProcessEnv; | ||
| 673 | assert.equal(isCommandAvailable("code", { platform: "win32", env }), true); | ||
| 674 | }), | ||
| 675 | ); | ||
| 676 | |||
| 677 | it("returns false when a command is not on PATH", () => { | ||
| 678 | const env = { | ||
| 679 | PATH: "", | ||
| 680 | PATHEXT: ".COM;.EXE;.BAT;.CMD", | ||
| 681 | } satisfies NodeJS.ProcessEnv; | ||
| 682 | assert.equal(isCommandAvailable("definitely-not-installed", { platform: "win32", env }), false); | ||
| 683 | }); | ||
| 684 | |||
| 685 | it.effect("does not treat bare files without executable extension as available on win32", () => | ||
| 686 | Effect.gen(function* () { | ||
| 687 | const fs = yield* FileSystem.FileSystem; | ||
| 688 | const path = yield* Path.Path; | ||
| 689 | const dir = yield* fs.makeTempDirectoryScoped({ prefix: "t3-external-launcher-test-" }); | ||
| 690 | yield* fs.writeFileString(path.join(dir, "npm"), "echo nope\r\n"); | ||
| 691 | const env = { | ||
| 692 | PATH: dir, | ||
| 693 | PATHEXT: ".COM;.EXE;.BAT;.CMD", | ||
| 694 | } satisfies NodeJS.ProcessEnv; | ||
| 695 | assert.equal(isCommandAvailable("npm", { platform: "win32", env }), false); | ||
| 696 | }), | ||
| 697 | ); | ||
| 698 | |||
| 699 | it.effect("appends PATHEXT for commands with non-executable extensions on win32", () => | ||
| 700 | Effect.gen(function* () { | ||
| 701 | const fs = yield* FileSystem.FileSystem; | ||
| 702 | const path = yield* Path.Path; | ||
| 703 | const dir = yield* fs.makeTempDirectoryScoped({ prefix: "t3-external-launcher-test-" }); | ||
| 704 | yield* fs.writeFileString(path.join(dir, "my.tool.CMD"), "@echo off\r\n"); | ||
| 705 | const env = { | ||
| 706 | PATH: dir, | ||
| 707 | PATHEXT: ".COM;.EXE;.BAT;.CMD", | ||
| 708 | } satisfies NodeJS.ProcessEnv; | ||
| 709 | assert.equal(isCommandAvailable("my.tool", { platform: "win32", env }), true); | ||
| 710 | }), | ||
| 711 | ); | ||
| 712 | |||
| 713 | it.effect("uses platform-specific PATH delimiter for platform overrides", () => | ||
| 714 | Effect.gen(function* () { | ||
| 715 | const fs = yield* FileSystem.FileSystem; | ||
| 716 | const path = yield* Path.Path; | ||
| 717 | const firstDir = yield* fs.makeTempDirectoryScoped({ prefix: "t3-external-launcher-test-" }); | ||
| 718 | const secondDir = yield* fs.makeTempDirectoryScoped({ prefix: "t3-external-launcher-test-" }); | ||
| 719 | yield* fs.writeFileString(path.join(firstDir, "code.CMD"), "@echo off\r\n"); | ||
| 720 | yield* fs.writeFileString(path.join(secondDir, "code.CMD"), "MZ"); | ||
| 721 | const env = { | ||
| 722 | PATH: `${firstDir};${secondDir}`, | ||
| 723 | PATHEXT: ".COM;.EXE;.BAT;.CMD", | ||
| 724 | } satisfies NodeJS.ProcessEnv; | ||
| 725 | assert.equal(isCommandAvailable("code", { platform: "win32", env }), true); | ||
| 726 | }), | ||
| 727 | ); | ||
| 728 | }); | ||
| 729 | |||
| 730 | it.layer(NodeServices.layer)("resolveAvailableEditors", (it) => { | ||
| 731 | it.effect("returns installed editors for command launches", () => | ||
| 732 | Effect.gen(function* () { | ||
| 733 | const fs = yield* FileSystem.FileSystem; | ||
| 734 | const path = yield* Path.Path; | ||
| 735 | const dir = yield* fs.makeTempDirectoryScoped({ prefix: "t3-editors-" }); | ||
| 736 | |||
| 737 | yield* fs.writeFileString(path.join(dir, "trae.CMD"), "@echo off\r\n"); | ||
| 738 | yield* fs.writeFileString(path.join(dir, "kiro.CMD"), "@echo off\r\n"); | ||
| 739 | yield* fs.writeFileString(path.join(dir, "code-insiders.CMD"), "@echo off\r\n"); | ||
| 740 | yield* fs.writeFileString(path.join(dir, "codium.CMD"), "@echo off\r\n"); | ||
| 741 | yield* fs.writeFileString(path.join(dir, "aqua.CMD"), "@echo off\r\n"); | ||
| 742 | yield* fs.writeFileString(path.join(dir, "clion.CMD"), "@echo off\r\n"); | ||
| 743 | yield* fs.writeFileString(path.join(dir, "datagrip.CMD"), "@echo off\r\n"); | ||
| 744 | yield* fs.writeFileString(path.join(dir, "dataspell.CMD"), "@echo off\r\n"); | ||
| 745 | yield* fs.writeFileString(path.join(dir, "goland.CMD"), "@echo off\r\n"); | ||
| 746 | yield* fs.writeFileString(path.join(dir, "phpstorm.CMD"), "@echo off\r\n"); | ||
| 747 | yield* fs.writeFileString(path.join(dir, "pycharm.CMD"), "@echo off\r\n"); | ||
| 748 | yield* fs.writeFileString(path.join(dir, "rider.CMD"), "@echo off\r\n"); | ||
| 749 | yield* fs.writeFileString(path.join(dir, "rubymine.CMD"), "@echo off\r\n"); | ||
| 750 | yield* fs.writeFileString(path.join(dir, "rustrover.CMD"), "@echo off\r\n"); | ||
| 751 | yield* fs.writeFileString(path.join(dir, "webstorm.CMD"), "@echo off\r\n"); | ||
| 752 | yield* fs.writeFileString(path.join(dir, "explorer.CMD"), "MZ"); | ||
| 753 | const editors = resolveAvailableEditors("win32", { | ||
| 754 | PATH: dir, | ||
| 755 | PATHEXT: ".COM;.EXE;.BAT;.CMD", | ||
| 756 | }); | ||
| 757 | assert.deepEqual(editors, [ | ||
| 758 | "trae", | ||
| 759 | "kiro", | ||
| 760 | "vscode-insiders", | ||
| 761 | "vscodium", | ||
| 762 | "aqua", | ||
| 763 | "clion", | ||
| 764 | "datagrip", | ||
| 765 | "dataspell", | ||
| 766 | "goland", | ||
| 767 | "phpstorm", | ||
| 768 | "pycharm", | ||
| 769 | "rider", | ||
| 770 | "rubymine", | ||
| 771 | "rustrover", | ||
| 772 | "webstorm", | ||
| 773 | "file-manager", | ||
| 774 | ]); | ||
| 775 | }), | ||
| 776 | ); | ||
| 777 | |||
| 778 | it.effect("includes zed when only the zeditor command is installed", () => | ||
| 779 | Effect.gen(function* () { | ||
| 780 | const fs = yield* FileSystem.FileSystem; | ||
| 781 | const path = yield* Path.Path; | ||
| 782 | const dir = yield* fs.makeTempDirectoryScoped({ prefix: "t3-editors-" }); | ||
| 783 | |||
| 784 | yield* fs.writeFileString(path.join(dir, "zeditor"), "#!/bin/sh\nexit 0\n"); | ||
| 785 | yield* fs.writeFileString(path.join(dir, "xdg-open"), "#!/bin/sh\nexit 0\n"); | ||
| 786 | yield* fs.chmod(path.join(dir, "zeditor"), 0o755); | ||
| 787 | yield* fs.chmod(path.join(dir, "xdg-open"), 0o755); | ||
| 788 | |||
| 789 | const editors = resolveAvailableEditors("linux", { | ||
| 790 | PATH: dir, | ||
| 791 | }); | ||
| 792 | assert.deepEqual(editors, ["zed", "file-manager"]); | ||
| 793 | }), | ||
| 794 | ); | ||
| 795 | |||
| 796 | it("omits file-manager when the platform opener is unavailable", () => { | ||
| 797 | const editors = resolveAvailableEditors("linux", { | ||
| 798 | PATH: "", | ||
| 799 | }); | ||
| 800 | assert.deepEqual(editors, []); | ||
| 801 | }); | ||
| 802 | }); | ||
apps/server/src/process/externalLauncher.ts created+364| ... | @@ -0,0 +1,364 @@ | ||
| 1 | /** | ||
| 2 | * ExternalLauncher - external application launch service interface. | ||
| 3 | * | ||
| 4 | * Owns process launch helpers for browser URLs and workspace paths | ||
| 5 | * in configured editor integrations. | ||
| 6 | * | ||
| 7 | * @module ExternalLauncher | ||
| 8 | */ | ||
| 9 | import { | ||
| 10 | EDITORS, | ||
| 11 | ExternalLauncherError, | ||
| 12 | type EditorId, | ||
| 13 | type LaunchEditorInput, | ||
| 14 | } from "@t3tools/contracts"; | ||
| 15 | import { isCommandAvailable, type CommandAvailabilityOptions } from "@t3tools/shared/shell"; | ||
| 16 | import * as Context from "effect/Context"; | ||
| 17 | import * as Effect from "effect/Effect"; | ||
| 18 | import * as Encoding from "effect/Encoding"; | ||
| 19 | import * as Layer from "effect/Layer"; | ||
| 20 | import * as Option from "effect/Option"; | ||
| 21 | import { ChildProcess, ChildProcessSpawner } from "effect/unstable/process"; | ||
| 22 | |||
| 23 | // ============================== | ||
| 24 | // Definitions | ||
| 25 | // ============================== | ||
| 26 | |||
| 27 | export { ExternalLauncherError }; | ||
| 28 | export type { LaunchEditorInput }; | ||
| 29 | export { isCommandAvailable } from "@t3tools/shared/shell"; | ||
| 30 | |||
| 31 | interface EditorLaunch { | ||
| 32 | readonly command: string; | ||
| 33 | readonly args: ReadonlyArray<string>; | ||
| 34 | } | ||
| 35 | |||
| 36 | interface ProcessLaunch { | ||
| 37 | readonly command: string; | ||
| 38 | readonly args: ReadonlyArray<string>; | ||
| 39 | readonly options: ChildProcess.CommandOptions; | ||
| 40 | } | ||
| 41 | |||
| 42 | interface TargetPathAndPosition { | ||
| 43 | readonly path: string; | ||
| 44 | readonly line: string; | ||
| 45 | readonly column: Option.Option<string>; | ||
| 46 | } | ||
| 47 | |||
| 48 | const TARGET_WITH_POSITION_PATTERN = /^(.*?):(\d+)(?::(\d+))?$/; | ||
| 49 | const POWERSHELL_ARGUMENTS_PREFIX = [ | ||
| 50 | "-NoProfile", | ||
| 51 | "-NonInteractive", | ||
| 52 | "-ExecutionPolicy", | ||
| 53 | "Bypass", | ||
| 54 | "-EncodedCommand", | ||
| 55 | ] as const; | ||
| 56 | |||
| 57 | const DETACHED_IGNORE_STDIO_OPTIONS = { | ||
| 58 | detached: true, | ||
| 59 | stdin: "ignore", | ||
| 60 | stdout: "ignore", | ||
| 61 | stderr: "ignore", | ||
| 62 | } as const satisfies ChildProcess.CommandOptions; | ||
| 63 | |||
| 64 | function parseTargetPathAndPosition(target: string): Option.Option<TargetPathAndPosition> { | ||
| 65 | const match = TARGET_WITH_POSITION_PATTERN.exec(target); | ||
| 66 | if (!match?.[1] || !match[2]) { | ||
| 67 | return Option.none(); | ||
| 68 | } | ||
| 69 | |||
| 70 | return Option.some({ | ||
| 71 | path: match[1], | ||
| 72 | line: match[2], | ||
| 73 | column: Option.fromUndefinedOr(match[3]), | ||
| 74 | }); | ||
| 75 | } | ||
| 76 | |||
| 77 | function resolveCommandEditorArgs( | ||
| 78 | editor: (typeof EDITORS)[number], | ||
| 79 | target: string, | ||
| 80 | ): ReadonlyArray<string> { | ||
| 81 | const parsedTarget = parseTargetPathAndPosition(target); | ||
| 82 | |||
| 83 | switch (editor.launchStyle) { | ||
| 84 | case "direct-path": | ||
| 85 | return [target]; | ||
| 86 | case "goto": | ||
| 87 | return Option.isSome(parsedTarget) ? ["--goto", target] : [target]; | ||
| 88 | case "line-column": | ||
| 89 | return Option.match(parsedTarget, { | ||
| 90 | onNone: () => [target], | ||
| 91 | onSome: ({ path, line, column }) => [ | ||
| 92 | "--line", | ||
| 93 | line, | ||
| 94 | ...Option.match(column, { | ||
| 95 | onNone: () => [], | ||
| 96 | onSome: (value) => ["--column", value], | ||
| 97 | }), | ||
| 98 | path, | ||
| 99 | ], | ||
| 100 | }); | ||
| 101 | } | ||
| 102 | } | ||
| 103 | |||
| 104 | function resolveEditorArgs( | ||
| 105 | editor: (typeof EDITORS)[number], | ||
| 106 | target: string, | ||
| 107 | ): ReadonlyArray<string> { | ||
| 108 | const baseArgs = "baseArgs" in editor ? editor.baseArgs : []; | ||
| 109 | return [...baseArgs, ...resolveCommandEditorArgs(editor, target)]; | ||
| 110 | } | ||
| 111 | |||
| 112 | function resolveAvailableCommand( | ||
| 113 | commands: ReadonlyArray<string>, | ||
| 114 | options: CommandAvailabilityOptions = {}, | ||
| 115 | ): Option.Option<string> { | ||
| 116 | for (const command of commands) { | ||
| 117 | if (isCommandAvailable(command, options)) { | ||
| 118 | return Option.some(command); | ||
| 119 | } | ||
| 120 | } | ||
| 121 | return Option.none(); | ||
| 122 | } | ||
| 123 | |||
| 124 | function encodeUtf16LeBase64(input: string): string { | ||
| 125 | const bytes = new Uint8Array(input.length * 2); | ||
| 126 | for (let index = 0; index < input.length; index += 1) { | ||
| 127 | const code = input.charCodeAt(index); | ||
| 128 | bytes[index * 2] = code & 0xff; | ||
| 129 | bytes[index * 2 + 1] = code >>> 8; | ||
| 130 | } | ||
| 131 | return Encoding.encodeBase64(bytes); | ||
| 132 | } | ||
| 133 | |||
| 134 | function escapePowerShellStringLiteral(input: string): string { | ||
| 135 | return `'${input.replaceAll("'", "''")}'`; | ||
| 136 | } | ||
| 137 | |||
| 138 | function resolvePowerShellPath(env: NodeJS.ProcessEnv = process.env): string { | ||
| 139 | return `${env.SYSTEMROOT || env.windir || String.raw`C:\Windows`}\\System32\\WindowsPowerShell\\v1.0\\powershell.exe`; | ||
| 140 | } | ||
| 141 | |||
| 142 | function resolveWslPowerShellPath(): string { | ||
| 143 | return "/mnt/c/Windows/System32/WindowsPowerShell/v1.0/powershell.exe"; | ||
| 144 | } | ||
| 145 | |||
| 146 | function shouldUseWindowsBrowserFromWsl( | ||
| 147 | platform: NodeJS.Platform, | ||
| 148 | env: NodeJS.ProcessEnv = process.env, | ||
| 149 | ): boolean { | ||
| 150 | return ( | ||
| 151 | platform === "linux" && | ||
| 152 | (env.WSL_DISTRO_NAME !== undefined || env.WSL_INTEROP !== undefined) && | ||
| 153 | env.SSH_CONNECTION === undefined && | ||
| 154 | env.SSH_TTY === undefined && | ||
| 155 | env.container === undefined | ||
| 156 | ); | ||
| 157 | } | ||
| 158 | |||
| 159 | function resolveWindowsBrowserLaunch(target: string, command: string): ProcessLaunch { | ||
| 160 | const encodedCommand = encodeUtf16LeBase64( | ||
| 161 | `$ProgressPreference = 'SilentlyContinue'; Start ${escapePowerShellStringLiteral(target)}`, | ||
| 162 | ); | ||
| 163 | return { | ||
| 164 | command, | ||
| 165 | args: [...POWERSHELL_ARGUMENTS_PREFIX, encodedCommand], | ||
| 166 | options: { | ||
| 167 | detached: true, | ||
| 168 | shell: false, | ||
| 169 | stdin: "ignore", | ||
| 170 | stdout: "ignore", | ||
| 171 | stderr: "ignore", | ||
| 172 | }, | ||
| 173 | }; | ||
| 174 | } | ||
| 175 | |||
| 176 | function fileManagerCommandForPlatform(platform: NodeJS.Platform): string { | ||
| 177 | switch (platform) { | ||
| 178 | case "darwin": | ||
| 179 | return "open"; | ||
| 180 | case "win32": | ||
| 181 | return "explorer"; | ||
| 182 | default: | ||
| 183 | return "xdg-open"; | ||
| 184 | } | ||
| 185 | } | ||
| 186 | |||
| 187 | export function resolveBrowserLaunch( | ||
| 188 | target: string, | ||
| 189 | platform: NodeJS.Platform = process.platform, | ||
| 190 | env: NodeJS.ProcessEnv = process.env, | ||
| 191 | ): ProcessLaunch { | ||
| 192 | if (platform === "darwin") { | ||
| 193 | return { | ||
| 194 | command: "open", | ||
| 195 | args: [target], | ||
| 196 | options: DETACHED_IGNORE_STDIO_OPTIONS, | ||
| 197 | }; | ||
| 198 | } | ||
| 199 | |||
| 200 | if (platform === "win32") { | ||
| 201 | return resolveWindowsBrowserLaunch(target, resolvePowerShellPath(env)); | ||
| 202 | } | ||
| 203 | |||
| 204 | if (shouldUseWindowsBrowserFromWsl(platform, env)) { | ||
| 205 | return resolveWindowsBrowserLaunch(target, resolveWslPowerShellPath()); | ||
| 206 | } | ||
| 207 | |||
| 208 | return { | ||
| 209 | command: "xdg-open", | ||
| 210 | args: [target], | ||
| 211 | options: DETACHED_IGNORE_STDIO_OPTIONS, | ||
| 212 | }; | ||
| 213 | } | ||
| 214 | |||
| 215 | export function resolveAvailableEditors( | ||
| 216 | platform: NodeJS.Platform = process.platform, | ||
| 217 | env: NodeJS.ProcessEnv = process.env, | ||
| 218 | ): ReadonlyArray<EditorId> { | ||
| 219 | const available: EditorId[] = []; | ||
| 220 | |||
| 221 | for (const editor of EDITORS) { | ||
| 222 | if (editor.commands === null) { | ||
| 223 | const command = fileManagerCommandForPlatform(platform); | ||
| 224 | if (isCommandAvailable(command, { platform, env })) { | ||
| 225 | available.push(editor.id); | ||
| 226 | } | ||
| 227 | continue; | ||
| 228 | } | ||
| 229 | |||
| 230 | const command = resolveAvailableCommand(editor.commands, { platform, env }); | ||
| 231 | if (Option.isSome(command)) { | ||
| 232 | available.push(editor.id); | ||
| 233 | } | ||
| 234 | } | ||
| 235 | |||
| 236 | return available; | ||
| 237 | } | ||
| 238 | |||
| 239 | /** | ||
| 240 | * ExternalLauncherShape - Service API for browser and editor launch actions. | ||
| 241 | */ | ||
| 242 | export interface ExternalLauncherShape { | ||
| 243 | /** | ||
| 244 | * Launch a URL target in the default browser. | ||
| 245 | */ | ||
| 246 | readonly launchBrowser: (target: string) => Effect.Effect<void, ExternalLauncherError>; | ||
| 247 | |||
| 248 | /** | ||
| 249 | * Launch a workspace path in a selected editor integration. | ||
| 250 | * | ||
| 251 | * Launches the editor as a detached process so server startup is not blocked. | ||
| 252 | */ | ||
| 253 | readonly launchEditor: (input: LaunchEditorInput) => Effect.Effect<void, ExternalLauncherError>; | ||
| 254 | } | ||
| 255 | |||
| 256 | /** | ||
| 257 | * ExternalLauncher - Service tag for browser/editor launch operations. | ||
| 258 | */ | ||
| 259 | export class ExternalLauncher extends Context.Service<ExternalLauncher, ExternalLauncherShape>()( | ||
| 260 | "t3/process/ExternalLauncher", | ||
| 261 | ) {} | ||
| 262 | |||
| 263 | // ============================== | ||
| 264 | // Implementations | ||
| 265 | // ============================== | ||
| 266 | |||
| 267 | export const resolveEditorLaunch = Effect.fn("resolveEditorLaunch")(function* ( | ||
| 268 | input: LaunchEditorInput, | ||
| 269 | platform: NodeJS.Platform = process.platform, | ||
| 270 | env: NodeJS.ProcessEnv = process.env, | ||
| 271 | ): Effect.fn.Return<EditorLaunch, ExternalLauncherError> { | ||
| 272 | yield* Effect.annotateCurrentSpan({ | ||
| 273 | "externalLauncher.editor": input.editor, | ||
| 274 | "externalLauncher.cwd": input.cwd, | ||
| 275 | "externalLauncher.platform": platform, | ||
| 276 | }); | ||
| 277 | const editorDef = EDITORS.find((editor) => editor.id === input.editor); | ||
| 278 | if (!editorDef) { | ||
| 279 | return yield* new ExternalLauncherError({ message: `Unknown editor: ${input.editor}` }); | ||
| 280 | } | ||
| 281 | |||
| 282 | if (editorDef.commands) { | ||
| 283 | const command = Option.getOrElse( | ||
| 284 | resolveAvailableCommand(editorDef.commands, { platform, env }), | ||
| 285 | () => editorDef.commands[0], | ||
| 286 | ); | ||
| 287 | return { | ||
| 288 | command, | ||
| 289 | args: resolveEditorArgs(editorDef, input.cwd), | ||
| 290 | }; | ||
| 291 | } | ||
| 292 | |||
| 293 | if (editorDef.id !== "file-manager") { | ||
| 294 | return yield* new ExternalLauncherError({ message: `Unsupported editor: ${input.editor}` }); | ||
| 295 | } | ||
| 296 | |||
| 297 | return { command: fileManagerCommandForPlatform(platform), args: [input.cwd] }; | ||
| 298 | }); | ||
| 299 | |||
| 300 | const launchAndUnref = Effect.fn("externalLauncher.launchAndUnref")(function* ( | ||
| 301 | launch: ProcessLaunch, | ||
| 302 | errorMessage: string, | ||
| 303 | ): Effect.fn.Return<void, ExternalLauncherError, ChildProcessSpawner.ChildProcessSpawner> { | ||
| 304 | const spawner = yield* ChildProcessSpawner.ChildProcessSpawner; | ||
| 305 | const command = ChildProcess.make(launch.command, launch.args, launch.options); | ||
| 306 | |||
| 307 | yield* spawner.spawn(command).pipe( | ||
| 308 | Effect.flatMap((handle) => handle.unref), | ||
| 309 | Effect.asVoid, | ||
| 310 | Effect.scoped, | ||
| 311 | Effect.mapError((cause) => new ExternalLauncherError({ message: errorMessage, cause })), | ||
| 312 | ); | ||
| 313 | }); | ||
| 314 | |||
| 315 | export const launchBrowser = Effect.fn("externalLauncher.launchBrowser")(function* ( | ||
| 316 | target: string, | ||
| 317 | ): Effect.fn.Return<void, ExternalLauncherError, ChildProcessSpawner.ChildProcessSpawner> { | ||
| 318 | return yield* launchAndUnref(resolveBrowserLaunch(target), "Browser auto-open failed"); | ||
| 319 | }); | ||
| 320 | |||
| 321 | export const launchEditorProcess = Effect.fn("externalLauncher.launchEditorProcess")(function* ( | ||
| 322 | launch: EditorLaunch, | ||
| 323 | ): Effect.fn.Return<void, ExternalLauncherError, ChildProcessSpawner.ChildProcessSpawner> { | ||
| 324 | if (!isCommandAvailable(launch.command)) { | ||
| 325 | return yield* new ExternalLauncherError({ | ||
| 326 | message: `Editor command not found: ${launch.command}`, | ||
| 327 | }); | ||
| 328 | } | ||
| 329 | |||
| 330 | const isWin32 = process.platform === "win32"; | ||
| 331 | yield* launchAndUnref( | ||
| 332 | { | ||
| 333 | command: launch.command, | ||
| 334 | args: isWin32 ? launch.args.map((arg) => `"${arg}"`) : [...launch.args], | ||
| 335 | options: { | ||
| 336 | detached: true, | ||
| 337 | shell: isWin32, | ||
| 338 | stdin: "ignore", | ||
| 339 | stdout: "ignore", | ||
| 340 | stderr: "ignore", | ||
| 341 | }, | ||
| 342 | }, | ||
| 343 | "failed to spawn detached process", | ||
| 344 | ); | ||
| 345 | }); | ||
| 346 | |||
| 347 | const make = Effect.gen(function* () { | ||
| 348 | const spawner = yield* ChildProcessSpawner.ChildProcessSpawner; | ||
| 349 | |||
| 350 | return { | ||
| 351 | launchBrowser: (target) => | ||
| 352 | launchBrowser(target).pipe( | ||
| 353 | Effect.provideService(ChildProcessSpawner.ChildProcessSpawner, spawner), | ||
| 354 | ), | ||
| 355 | launchEditor: (input) => | ||
| 356 | Effect.flatMap(resolveEditorLaunch(input), (launch) => | ||
| 357 | launchEditorProcess(launch).pipe( | ||
| 358 | Effect.provideService(ChildProcessSpawner.ChildProcessSpawner, spawner), | ||
| 359 | ), | ||
| 360 | ), | ||
| 361 | } satisfies ExternalLauncherShape; | ||
| 362 | }); | ||
| 363 | |||
| 364 | export const layer = Layer.effect(ExternalLauncher, make); | ||
apps/server/src/server.test.ts+13-11| ... | @@ -10,7 +10,7 @@ import { | ... | @@ -10,7 +10,7 @@ import { |
| 10 | GitCommandError, | 10 | GitCommandError, |
| 11 | KeybindingRule, | 11 | KeybindingRule, |
| 12 | MessageId, | 12 | MessageId, |
| 13 | OpenError, | 13 | ExternalLauncherError, |
| 14 | type OrchestrationThreadShell, | 14 | type OrchestrationThreadShell, |
| 15 | TerminalNotRunningError, | 15 | TerminalNotRunningError, |
| 16 | type OrchestrationCommand, | 16 | type OrchestrationCommand, |
| ... | @@ -63,7 +63,7 @@ import { | ... | @@ -63,7 +63,7 @@ import { |
| 63 | } from "./checkpointing/Services/CheckpointDiffQuery.ts"; | 63 | } from "./checkpointing/Services/CheckpointDiffQuery.ts"; |
| 64 | import { GitManager, type GitManagerShape } from "./git/GitManager.ts"; | 64 | import { GitManager, type GitManagerShape } from "./git/GitManager.ts"; |
| 65 | import { Keybindings, type KeybindingsShape } from "./keybindings.ts"; | 65 | import { Keybindings, type KeybindingsShape } from "./keybindings.ts"; |
| 66 | import { Open, type OpenShape } from "./open.ts"; | 66 | import * as ExternalLauncher from "./process/externalLauncher.ts"; |
| 67 | import { | 67 | import { |
| 68 | OrchestrationEngineService, | 68 | OrchestrationEngineService, |
| 69 | type OrchestrationEngineShape, | 69 | type OrchestrationEngineShape, |
| ... | @@ -319,7 +319,7 @@ const buildAppUnderTest = (options?: { | ... | @@ -319,7 +319,7 @@ const buildAppUnderTest = (options?: { |
| 319 | keybindings?: Partial<KeybindingsShape>; | 319 | keybindings?: Partial<KeybindingsShape>; |
| 320 | providerRegistry?: Partial<ProviderRegistryShape>; | 320 | providerRegistry?: Partial<ProviderRegistryShape>; |
| 321 | serverSettings?: Partial<ServerSettingsShape>; | 321 | serverSettings?: Partial<ServerSettingsShape>; |
| 322 | open?: Partial<OpenShape>; | 322 | externalLauncher?: Partial<ExternalLauncher.ExternalLauncherShape>; |
| 323 | vcsDriver?: Partial<VcsDriver.VcsDriverShape>; | 323 | vcsDriver?: Partial<VcsDriver.VcsDriverShape>; |
| 324 | vcsDriverRegistry?: Partial<VcsDriverRegistry.VcsDriverRegistryShape>; | 324 | vcsDriverRegistry?: Partial<VcsDriverRegistry.VcsDriverRegistryShape>; |
| 325 | gitVcsDriver?: Partial<GitVcsDriver.GitVcsDriverShape>; | 325 | gitVcsDriver?: Partial<GitVcsDriver.GitVcsDriverShape>; |
| ... | @@ -540,8 +540,8 @@ const buildAppUnderTest = (options?: { | ... | @@ -540,8 +540,8 @@ const buildAppUnderTest = (options?: { |
| 540 | }), | 540 | }), |
| 541 | ), | 541 | ), |
| 542 | Layer.provide( | 542 | Layer.provide( |
| 543 | Layer.mock(Open)({ | 543 | Layer.mock(ExternalLauncher.ExternalLauncher)({ |
| 544 | ...options?.layers?.open, | 544 | ...options?.layers?.externalLauncher, |
| 545 | }), | 545 | }), |
| 546 | ), | 546 | ), |
| 547 | Layer.provide( | 547 | Layer.provide( |
| ... | @@ -2517,8 +2517,8 @@ it.layer(NodeServices.layer)("server router seam", (it) => { | ... | @@ -2517,8 +2517,8 @@ it.layer(NodeServices.layer)("server router seam", (it) => { |
| 2517 | let openedInput: { cwd: string; editor: EditorId } | null = null; | 2517 | let openedInput: { cwd: string; editor: EditorId } | null = null; |
| 2518 | yield* buildAppUnderTest({ | 2518 | yield* buildAppUnderTest({ |
| 2519 | layers: { | 2519 | layers: { |
| 2520 | open: { | 2520 | externalLauncher: { |
| 2521 | openInEditor: (input) => | 2521 | launchEditor: (input) => |
| 2522 | Effect.sync(() => { | 2522 | Effect.sync(() => { |
| 2523 | openedInput = input; | 2523 | openedInput = input; |
| 2524 | }), | 2524 | }), |
| ... | @@ -2542,11 +2542,13 @@ it.layer(NodeServices.layer)("server router seam", (it) => { | ... | @@ -2542,11 +2542,13 @@ it.layer(NodeServices.layer)("server router seam", (it) => { |
| 2542 | 2542 | ||
| 2543 | it.effect("routes websocket rpc shell.openInEditor errors", () => | 2543 | it.effect("routes websocket rpc shell.openInEditor errors", () => |
| 2544 | Effect.gen(function* () { | 2544 | Effect.gen(function* () { |
| 2545 | const openError = new OpenError({ message: "Editor command not found: cursor" }); | 2545 | const externalLauncherError = new ExternalLauncherError({ |
| 2546 | message: "Editor command not found: cursor", | ||
| 2547 | }); | ||
| 2546 | yield* buildAppUnderTest({ | 2548 | yield* buildAppUnderTest({ |
| 2547 | layers: { | 2549 | layers: { |
| 2548 | open: { | 2550 | externalLauncher: { |
| 2549 | openInEditor: () => Effect.fail(openError), | 2551 | launchEditor: () => Effect.fail(externalLauncherError), |
| 2550 | }, | 2552 | }, |
| 2551 | }, | 2553 | }, |
| 2552 | }); | 2554 | }); |
| ... | @@ -2561,7 +2563,7 @@ it.layer(NodeServices.layer)("server router seam", (it) => { | ... | @@ -2561,7 +2563,7 @@ it.layer(NodeServices.layer)("server router seam", (it) => { |
| 2561 | ).pipe(Effect.result), | 2563 | ).pipe(Effect.result), |
| 2562 | ); | 2564 | ); |
| 2563 | 2565 | ||
| 2564 | assertFailure(result, openError); | 2566 | assertFailure(result, externalLauncherError); |
| 2565 | }).pipe(Effect.provide(NodeHttpServer.layerTest)), | 2567 | }).pipe(Effect.provide(NodeHttpServer.layerTest)), |
| 2566 | ); | 2568 | ); |
| 2567 | 2569 |
apps/server/src/server.ts+2-2| ... | @@ -13,7 +13,7 @@ import { | ... | @@ -13,7 +13,7 @@ import { |
| 13 | } from "./http.ts"; | 13 | } from "./http.ts"; |
| 14 | import { fixPath } from "./os-jank.ts"; | 14 | import { fixPath } from "./os-jank.ts"; |
| 15 | import { websocketRpcRouteLayer } from "./ws.ts"; | 15 | import { websocketRpcRouteLayer } from "./ws.ts"; |
| 16 | import { OpenLive } from "./open.ts"; | 16 | import * as ExternalLauncher from "./process/externalLauncher.ts"; |
| 17 | import { layerConfig as SqlitePersistenceLayerLive } from "./persistence/Layers/Sqlite.ts"; | 17 | import { layerConfig as SqlitePersistenceLayerLive } from "./persistence/Layers/Sqlite.ts"; |
| 18 | import { ServerLifecycleEventsLive } from "./serverLifecycleEvents.ts"; | 18 | import { ServerLifecycleEventsLive } from "./serverLifecycleEvents.ts"; |
| 19 | import { AnalyticsServiceLayerLive } from "./telemetry/Layers/AnalyticsService.ts"; | 19 | import { AnalyticsServiceLayerLive } from "./telemetry/Layers/AnalyticsService.ts"; |
| ... | @@ -282,7 +282,7 @@ const RuntimeDependenciesLive = RuntimeCoreDependenciesLive.pipe( | ... | @@ -282,7 +282,7 @@ const RuntimeDependenciesLive = RuntimeCoreDependenciesLive.pipe( |
| 282 | Layer.provideMerge(ProcessDiagnostics.layer), | 282 | Layer.provideMerge(ProcessDiagnostics.layer), |
| 283 | Layer.provideMerge(TraceDiagnostics.layer), | 283 | Layer.provideMerge(TraceDiagnostics.layer), |
| 284 | Layer.provideMerge(AnalyticsServiceLayerLive), | 284 | Layer.provideMerge(AnalyticsServiceLayerLive), |
| 285 | Layer.provideMerge(OpenLive), | 285 | Layer.provideMerge(ExternalLauncher.layer), |
| 286 | Layer.provideMerge(ServerLifecycleEventsLive), | 286 | Layer.provideMerge(ServerLifecycleEventsLive), |
| 287 | Layer.provide(NetService.layer), | 287 | Layer.provide(NetService.layer), |
| 288 | ); | 288 | ); |
apps/server/src/serverRuntimeStartup.ts+3-3| ... | @@ -23,7 +23,7 @@ import * as DateTime from "effect/DateTime"; | ... | @@ -23,7 +23,7 @@ import * as DateTime from "effect/DateTime"; |
| 23 | 23 | ||
| 24 | import { ServerConfig } from "./config.ts"; | 24 | import { ServerConfig } from "./config.ts"; |
| 25 | import { Keybindings } from "./keybindings.ts"; | 25 | import { Keybindings } from "./keybindings.ts"; |
| 26 | import { Open } from "./open.ts"; | 26 | import * as ExternalLauncher from "./process/externalLauncher.ts"; |
| 27 | import { OrchestrationEngineService } from "./orchestration/Services/OrchestrationEngine.ts"; | 27 | import { OrchestrationEngineService } from "./orchestration/Services/OrchestrationEngine.ts"; |
| 28 | import { ProjectionSnapshotQuery } from "./orchestration/Services/ProjectionSnapshotQuery.ts"; | 28 | import { ProjectionSnapshotQuery } from "./orchestration/Services/ProjectionSnapshotQuery.ts"; |
| 29 | import { OrchestrationReactor } from "./orchestration/Services/OrchestrationReactor.ts"; | 29 | import { OrchestrationReactor } from "./orchestration/Services/OrchestrationReactor.ts"; |
| ... | @@ -261,9 +261,9 @@ const maybeOpenBrowser = (target: string) => | ... | @@ -261,9 +261,9 @@ const maybeOpenBrowser = (target: string) => |
| 261 | if (serverConfig.noBrowser) { | 261 | if (serverConfig.noBrowser) { |
| 262 | return; | 262 | return; |
| 263 | } | 263 | } |
| 264 | const { openBrowser } = yield* Open; | 264 | const externalLauncher = yield* ExternalLauncher.ExternalLauncher; |
| 265 | 265 | ||
| 266 | yield* openBrowser(target).pipe( | 266 | yield* externalLauncher.launchBrowser(target).pipe( |
| 267 | Effect.catch(() => | 267 | Effect.catch(() => |
| 268 | Effect.logInfo("browser auto-open unavailable", { | 268 | Effect.logInfo("browser auto-open unavailable", { |
| 269 | hint: `Open ${target} in your browser.`, | 269 | hint: `Open ${target} in your browser.`, |
apps/server/src/ws.ts+4-4| ... | @@ -40,7 +40,7 @@ import { RpcSerialization, RpcServer } from "effect/unstable/rpc"; | ... | @@ -40,7 +40,7 @@ import { RpcSerialization, RpcServer } from "effect/unstable/rpc"; |
| 40 | import { CheckpointDiffQuery } from "./checkpointing/Services/CheckpointDiffQuery.ts"; | 40 | import { CheckpointDiffQuery } from "./checkpointing/Services/CheckpointDiffQuery.ts"; |
| 41 | import { ServerConfig } from "./config.ts"; | 41 | import { ServerConfig } from "./config.ts"; |
| 42 | import { Keybindings } from "./keybindings.ts"; | 42 | import { Keybindings } from "./keybindings.ts"; |
| 43 | import { Open, resolveAvailableEditors } from "./open.ts"; | 43 | import * as ExternalLauncher from "./process/externalLauncher.ts"; |
| 44 | import { normalizeDispatchCommand } from "./orchestration/Normalizer.ts"; | 44 | import { normalizeDispatchCommand } from "./orchestration/Normalizer.ts"; |
| 45 | import { OrchestrationEngineService } from "./orchestration/Services/OrchestrationEngine.ts"; | 45 | import { OrchestrationEngineService } from "./orchestration/Services/OrchestrationEngine.ts"; |
| 46 | import { ProjectionSnapshotQuery } from "./orchestration/Services/ProjectionSnapshotQuery.ts"; | 46 | import { ProjectionSnapshotQuery } from "./orchestration/Services/ProjectionSnapshotQuery.ts"; |
| ... | @@ -163,7 +163,7 @@ const makeWsRpcLayer = (currentSessionId: AuthSessionId) => | ... | @@ -163,7 +163,7 @@ const makeWsRpcLayer = (currentSessionId: AuthSessionId) => |
| 163 | const orchestrationEngine = yield* OrchestrationEngineService; | 163 | const orchestrationEngine = yield* OrchestrationEngineService; |
| 164 | const checkpointDiffQuery = yield* CheckpointDiffQuery; | 164 | const checkpointDiffQuery = yield* CheckpointDiffQuery; |
| 165 | const keybindings = yield* Keybindings; | 165 | const keybindings = yield* Keybindings; |
| 166 | const open = yield* Open; | 166 | const externalLauncher = yield* ExternalLauncher.ExternalLauncher; |
| 167 | const gitWorkflow = yield* GitWorkflowService; | 167 | const gitWorkflow = yield* GitWorkflowService; |
| 168 | const vcsProvisioning = yield* VcsProvisioningService; | 168 | const vcsProvisioning = yield* VcsProvisioningService; |
| 169 | const vcsStatusBroadcaster = yield* VcsStatusBroadcaster; | 169 | const vcsStatusBroadcaster = yield* VcsStatusBroadcaster; |
| ... | @@ -583,7 +583,7 @@ const makeWsRpcLayer = (currentSessionId: AuthSessionId) => | ... | @@ -583,7 +583,7 @@ const makeWsRpcLayer = (currentSessionId: AuthSessionId) => |
| 583 | keybindings: keybindingsConfig.keybindings, | 583 | keybindings: keybindingsConfig.keybindings, |
| 584 | issues: keybindingsConfig.issues, | 584 | issues: keybindingsConfig.issues, |
| 585 | providers, | 585 | providers, |
| 586 | availableEditors: resolveAvailableEditors(), | 586 | availableEditors: ExternalLauncher.resolveAvailableEditors(), |
| 587 | observability: { | 587 | observability: { |
| 588 | logsDirectoryPath: config.logsDir, | 588 | logsDirectoryPath: config.logsDir, |
| 589 | localTracingEnabled: true, | 589 | localTracingEnabled: true, |
| ... | @@ -970,7 +970,7 @@ const makeWsRpcLayer = (currentSessionId: AuthSessionId) => | ... | @@ -970,7 +970,7 @@ const makeWsRpcLayer = (currentSessionId: AuthSessionId) => |
| 970 | { "rpc.aggregate": "workspace" }, | 970 | { "rpc.aggregate": "workspace" }, |
| 971 | ), | 971 | ), |
| 972 | [WS_METHODS.shellOpenInEditor]: (input) => | 972 | [WS_METHODS.shellOpenInEditor]: (input) => |
| 973 | observeRpcEffect(WS_METHODS.shellOpenInEditor, open.openInEditor(input), { | 973 | observeRpcEffect(WS_METHODS.shellOpenInEditor, externalLauncher.launchEditor(input), { |
| 974 | "rpc.aggregate": "workspace", | 974 | "rpc.aggregate": "workspace", |
| 975 | }), | 975 | }), |
| 976 | [WS_METHODS.filesystemBrowse]: (input) => | 976 | [WS_METHODS.filesystemBrowse]: (input) => |
bun.lock+4-19| ... | @@ -17,7 +17,7 @@ | ... | @@ -17,7 +17,7 @@ |
| 17 | }, | 17 | }, |
| 18 | "apps/desktop": { | 18 | "apps/desktop": { |
| 19 | "name": "@t3tools/desktop", | 19 | "name": "@t3tools/desktop", |
| 20 | "version": "0.0.22", | 20 | "version": "0.0.23", |
| 21 | "dependencies": { | 21 | "dependencies": { |
| 22 | "@effect/platform-node": "catalog:", | 22 | "@effect/platform-node": "catalog:", |
| 23 | "effect": "catalog:", | 23 | "effect": "catalog:", |
| ... | @@ -52,7 +52,7 @@ | ... | @@ -52,7 +52,7 @@ |
| 52 | }, | 52 | }, |
| 53 | "apps/server": { | 53 | "apps/server": { |
| 54 | "name": "t3", | 54 | "name": "t3", |
| 55 | "version": "0.0.22", | 55 | "version": "0.0.23", |
| 56 | "bin": { | 56 | "bin": { |
| 57 | "t3": "./dist/bin.mjs", | 57 | "t3": "./dist/bin.mjs", |
| 58 | }, | 58 | }, |
| ... | @@ -65,7 +65,6 @@ | ... | @@ -65,7 +65,6 @@ |
| 65 | "@pierre/diffs": "catalog:", | 65 | "@pierre/diffs": "catalog:", |
| 66 | "effect": "catalog:", | 66 | "effect": "catalog:", |
| 67 | "node-pty": "^1.1.0", | 67 | "node-pty": "^1.1.0", |
| 68 | "open": "^10.1.0", | ||
| 69 | }, | 68 | }, |
| 70 | "devDependencies": { | 69 | "devDependencies": { |
| 71 | "@effect/language-service": "catalog:", | 70 | "@effect/language-service": "catalog:", |
| ... | @@ -85,7 +84,7 @@ | ... | @@ -85,7 +84,7 @@ |
| 85 | }, | 84 | }, |
| 86 | "apps/web": { | 85 | "apps/web": { |
| 87 | "name": "@t3tools/web", | 86 | "name": "@t3tools/web", |
| 88 | "version": "0.0.22", | 87 | "version": "0.0.23", |
| 89 | "dependencies": { | 88 | "dependencies": { |
| 90 | "@base-ui/react": "^1.4.1", | 89 | "@base-ui/react": "^1.4.1", |
| 91 | "@dnd-kit/core": "^6.3.1", | 90 | "@dnd-kit/core": "^6.3.1", |
| ... | @@ -167,7 +166,7 @@ | ... | @@ -167,7 +166,7 @@ |
| 167 | }, | 166 | }, |
| 168 | "packages/contracts": { | 167 | "packages/contracts": { |
| 169 | "name": "@t3tools/contracts", | 168 | "name": "@t3tools/contracts", |
| 170 | "version": "0.0.22", | 169 | "version": "0.0.23", |
| 171 | "dependencies": { | 170 | "dependencies": { |
| 172 | "effect": "catalog:", | 171 | "effect": "catalog:", |
| 173 | }, | 172 | }, |
| ... | @@ -1038,8 +1037,6 @@ | ... | @@ -1038,8 +1037,6 @@ |
| 1038 | 1037 | ||
| 1039 | "bun-types": ["bun-types@1.3.11", "", { "dependencies": { "@types/node": "*" } }, "sha512-1KGPpoxQWl9f6wcZh57LvrPIInQMn2TQ7jsgxqpRzg+l0QPOFvJVH7HmvHo/AiPgwXy+/Thf6Ov3EdVn1vOabg=="], | 1038 | "bun-types": ["bun-types@1.3.11", "", { "dependencies": { "@types/node": "*" } }, "sha512-1KGPpoxQWl9f6wcZh57LvrPIInQMn2TQ7jsgxqpRzg+l0QPOFvJVH7HmvHo/AiPgwXy+/Thf6Ov3EdVn1vOabg=="], |
| 1040 | 1039 | ||
| 1041 | "bundle-name": ["bundle-name@4.1.0", "", { "dependencies": { "run-applescript": "^7.0.0" } }, "sha512-tjwM5exMg6BGRI+kNmTntNsvdZS1X8BFYS6tnJ2hdH0kVxM6/eVZ2xy+FqStSWvYmtfFMDLIxurorHwDKfDz5Q=="], | ||
| 1042 | |||
| 1043 | "bytes": ["bytes@3.1.2", "", {}, "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg=="], | 1040 | "bytes": ["bytes@3.1.2", "", {}, "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg=="], |
| 1044 | 1041 | ||
| 1045 | "cac": ["cac@6.7.14", "", {}, "sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ=="], | 1042 | "cac": ["cac@6.7.14", "", {}, "sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ=="], |
| ... | @@ -1126,16 +1123,10 @@ | ... | @@ -1126,16 +1123,10 @@ |
| 1126 | 1123 | ||
| 1127 | "decompress-response": ["decompress-response@6.0.0", "", { "dependencies": { "mimic-response": "^3.1.0" } }, "sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ=="], | 1124 | "decompress-response": ["decompress-response@6.0.0", "", { "dependencies": { "mimic-response": "^3.1.0" } }, "sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ=="], |
| 1128 | 1125 | ||
| 1129 | "default-browser": ["default-browser@5.5.0", "", { "dependencies": { "bundle-name": "^4.1.0", "default-browser-id": "^5.0.0" } }, "sha512-H9LMLr5zwIbSxrmvikGuI/5KGhZ8E2zH3stkMgM5LpOWDutGM2JZaj460Udnf1a+946zc7YBgrqEWwbk7zHvGw=="], | ||
| 1130 | |||
| 1131 | "default-browser-id": ["default-browser-id@5.0.1", "", {}, "sha512-x1VCxdX4t+8wVfd1so/9w+vQ4vx7lKd2Qp5tDRutErwmR85OgmfX7RlLRMWafRMY7hbEiXIbudNrjOAPa/hL8Q=="], | ||
| 1132 | |||
| 1133 | "defer-to-connect": ["defer-to-connect@2.0.1", "", {}, "sha512-4tvttepXG1VaYGrRibk5EwJd1t4udunSOVMdLSAL6mId1ix438oPwPZMALY41FCijukO1L0twNcGsdzS7dHgDg=="], | 1126 | "defer-to-connect": ["defer-to-connect@2.0.1", "", {}, "sha512-4tvttepXG1VaYGrRibk5EwJd1t4udunSOVMdLSAL6mId1ix438oPwPZMALY41FCijukO1L0twNcGsdzS7dHgDg=="], |
| 1134 | 1127 | ||
| 1135 | "define-data-property": ["define-data-property@1.1.4", "", { "dependencies": { "es-define-property": "^1.0.0", "es-errors": "^1.3.0", "gopd": "^1.0.1" } }, "sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A=="], | 1128 | "define-data-property": ["define-data-property@1.1.4", "", { "dependencies": { "es-define-property": "^1.0.0", "es-errors": "^1.3.0", "gopd": "^1.0.1" } }, "sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A=="], |
| 1136 | 1129 | ||
| 1137 | "define-lazy-prop": ["define-lazy-prop@3.0.0", "", {}, "sha512-N+MeXYoqr3pOgn8xfyRPREN7gHakLYjhsHhWGT3fWAiL4IkAt0iDw14QiiEm2bE30c5XX5q0FtAA3CK5f9/BUg=="], | ||
| 1138 | |||
| 1139 | "define-properties": ["define-properties@1.2.1", "", { "dependencies": { "define-data-property": "^1.0.1", "has-property-descriptors": "^1.0.0", "object-keys": "^1.1.1" } }, "sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg=="], | 1130 | "define-properties": ["define-properties@1.2.1", "", { "dependencies": { "define-data-property": "^1.0.1", "has-property-descriptors": "^1.0.0", "object-keys": "^1.1.1" } }, "sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg=="], |
| 1140 | 1131 | ||
| 1141 | "defu": ["defu@6.1.4", "", {}, "sha512-mEQCMmwJu317oSz8CwdIOdwf3xMif1ttiM8LTufzc3g6kR+9Pe236twL8j3IYT1F7GfRgGcW6MWxzZjLIkuHIg=="], | 1132 | "defu": ["defu@6.1.4", "", {}, "sha512-mEQCMmwJu317oSz8CwdIOdwf3xMif1ttiM8LTufzc3g6kR+9Pe236twL8j3IYT1F7GfRgGcW6MWxzZjLIkuHIg=="], |
| ... | @@ -1664,8 +1655,6 @@ | ... | @@ -1664,8 +1655,6 @@ |
| 1664 | 1655 | ||
| 1665 | "oniguruma-to-es": ["oniguruma-to-es@4.3.5", "", { "dependencies": { "oniguruma-parser": "^0.12.1", "regex": "^6.1.0", "regex-recursion": "^6.0.2" } }, "sha512-Zjygswjpsewa0NLTsiizVuMQZbp0MDyM6lIt66OxsF21npUDlzpHi1Mgb/qhQdkb+dWFTzJmFbEWdvZgRho8eQ=="], | 1656 | "oniguruma-to-es": ["oniguruma-to-es@4.3.5", "", { "dependencies": { "oniguruma-parser": "^0.12.1", "regex": "^6.1.0", "regex-recursion": "^6.0.2" } }, "sha512-Zjygswjpsewa0NLTsiizVuMQZbp0MDyM6lIt66OxsF21npUDlzpHi1Mgb/qhQdkb+dWFTzJmFbEWdvZgRho8eQ=="], |
| 1666 | 1657 | ||
| 1667 | "open": ["open@10.2.0", "", { "dependencies": { "default-browser": "^5.2.1", "define-lazy-prop": "^3.0.0", "is-inside-container": "^1.0.0", "wsl-utils": "^0.1.0" } }, "sha512-YgBpdJHPyQ2UE5x+hlSXcnejzAvD0b22U2OuAP+8OnlJT+PjWPxtgmGqKKc+RgTM63U9gN0YzrYc71R2WT/hTA=="], | ||
| 1668 | |||
| 1669 | "outvariant": ["outvariant@1.4.3", "", {}, "sha512-+Sl2UErvtsoajRDKCE5/dBz4DIvHXQQnAxtQTF04OJxY0+DyZXSo5P5Bb7XYWOh81syohlYL24hbDwxedPUJCA=="], | 1658 | "outvariant": ["outvariant@1.4.3", "", {}, "sha512-+Sl2UErvtsoajRDKCE5/dBz4DIvHXQQnAxtQTF04OJxY0+DyZXSo5P5Bb7XYWOh81syohlYL24hbDwxedPUJCA=="], |
| 1670 | 1659 | ||
| 1671 | "oxfmt": ["oxfmt@0.40.0", "", { "dependencies": { "tinypool": "2.1.0" }, "optionalDependencies": { "@oxfmt/binding-android-arm-eabi": "0.40.0", "@oxfmt/binding-android-arm64": "0.40.0", "@oxfmt/binding-darwin-arm64": "0.40.0", "@oxfmt/binding-darwin-x64": "0.40.0", "@oxfmt/binding-freebsd-x64": "0.40.0", "@oxfmt/binding-linux-arm-gnueabihf": "0.40.0", "@oxfmt/binding-linux-arm-musleabihf": "0.40.0", "@oxfmt/binding-linux-arm64-gnu": "0.40.0", "@oxfmt/binding-linux-arm64-musl": "0.40.0", "@oxfmt/binding-linux-ppc64-gnu": "0.40.0", "@oxfmt/binding-linux-riscv64-gnu": "0.40.0", "@oxfmt/binding-linux-riscv64-musl": "0.40.0", "@oxfmt/binding-linux-s390x-gnu": "0.40.0", "@oxfmt/binding-linux-x64-gnu": "0.40.0", "@oxfmt/binding-linux-x64-musl": "0.40.0", "@oxfmt/binding-openharmony-arm64": "0.40.0", "@oxfmt/binding-win32-arm64-msvc": "0.40.0", "@oxfmt/binding-win32-ia32-msvc": "0.40.0", "@oxfmt/binding-win32-x64-msvc": "0.40.0" }, "bin": { "oxfmt": "bin/oxfmt" } }, "sha512-g0C3I7xUj4b4DcagevM9kgH6+pUHytikxUcn3/VUkvzTNaaXBeyZqb7IBsHwojeXm4mTBEC/aBjBTMVUkZwWUQ=="], | 1660 | "oxfmt": ["oxfmt@0.40.0", "", { "dependencies": { "tinypool": "2.1.0" }, "optionalDependencies": { "@oxfmt/binding-android-arm-eabi": "0.40.0", "@oxfmt/binding-android-arm64": "0.40.0", "@oxfmt/binding-darwin-arm64": "0.40.0", "@oxfmt/binding-darwin-x64": "0.40.0", "@oxfmt/binding-freebsd-x64": "0.40.0", "@oxfmt/binding-linux-arm-gnueabihf": "0.40.0", "@oxfmt/binding-linux-arm-musleabihf": "0.40.0", "@oxfmt/binding-linux-arm64-gnu": "0.40.0", "@oxfmt/binding-linux-arm64-musl": "0.40.0", "@oxfmt/binding-linux-ppc64-gnu": "0.40.0", "@oxfmt/binding-linux-riscv64-gnu": "0.40.0", "@oxfmt/binding-linux-riscv64-musl": "0.40.0", "@oxfmt/binding-linux-s390x-gnu": "0.40.0", "@oxfmt/binding-linux-x64-gnu": "0.40.0", "@oxfmt/binding-linux-x64-musl": "0.40.0", "@oxfmt/binding-openharmony-arm64": "0.40.0", "@oxfmt/binding-win32-arm64-msvc": "0.40.0", "@oxfmt/binding-win32-ia32-msvc": "0.40.0", "@oxfmt/binding-win32-x64-msvc": "0.40.0" }, "bin": { "oxfmt": "bin/oxfmt" } }, "sha512-g0C3I7xUj4b4DcagevM9kgH6+pUHytikxUcn3/VUkvzTNaaXBeyZqb7IBsHwojeXm4mTBEC/aBjBTMVUkZwWUQ=="], |
| ... | @@ -1820,8 +1809,6 @@ | ... | @@ -1820,8 +1809,6 @@ |
| 1820 | 1809 | ||
| 1821 | "router": ["router@2.2.0", "", { "dependencies": { "debug": "^4.4.0", "depd": "^2.0.0", "is-promise": "^4.0.0", "parseurl": "^1.3.3", "path-to-regexp": "^8.0.0" } }, "sha512-nLTrUKm2UyiL7rlhapu/Zl45FwNgkZGaCpZbIHajDYgwlJCOzLSk+cIPAnsEqV955GjILJnKbdQC1nVPz+gAYQ=="], | 1810 | "router": ["router@2.2.0", "", { "dependencies": { "debug": "^4.4.0", "depd": "^2.0.0", "is-promise": "^4.0.0", "parseurl": "^1.3.3", "path-to-regexp": "^8.0.0" } }, "sha512-nLTrUKm2UyiL7rlhapu/Zl45FwNgkZGaCpZbIHajDYgwlJCOzLSk+cIPAnsEqV955GjILJnKbdQC1nVPz+gAYQ=="], |
| 1822 | 1811 | ||
| 1823 | "run-applescript": ["run-applescript@7.1.0", "", {}, "sha512-DPe5pVFaAsinSaV6QjQ6gdiedWDcRCbUuiQfQa2wmWV7+xC9bGulGI8+TdRmoFkAPaBXk8CrAbnlY2ISniJ47Q=="], | ||
| 1824 | |||
| 1825 | "safer-buffer": ["safer-buffer@2.1.2", "", {}, "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg=="], | 1812 | "safer-buffer": ["safer-buffer@2.1.2", "", {}, "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg=="], |
| 1826 | 1813 | ||
| 1827 | "sax": ["sax@1.5.0", "", {}, "sha512-21IYA3Q5cQf089Z6tgaUTr7lDAyzoTPx5HRtbhsME8Udispad8dC/+sziTNugOEx54ilvatQ9YCzl4KQLPcRHA=="], | 1814 | "sax": ["sax@1.5.0", "", {}, "sha512-21IYA3Q5cQf089Z6tgaUTr7lDAyzoTPx5HRtbhsME8Udispad8dC/+sziTNugOEx54ilvatQ9YCzl4KQLPcRHA=="], |
| ... | @@ -2110,8 +2097,6 @@ | ... | @@ -2110,8 +2097,6 @@ |
| 2110 | 2097 | ||
| 2111 | "ws": ["ws@8.20.0", "", { "peerDependencies": { "bufferutil": "^4.0.1", "utf-8-validate": ">=5.0.2" }, "optionalPeers": ["bufferutil", "utf-8-validate"] }, "sha512-sAt8BhgNbzCtgGbt2OxmpuryO63ZoDk/sqaB/znQm94T4fCEsy/yV+7CdC1kJhOU9lboAEU7R3kquuycDoibVA=="], | 2098 | "ws": ["ws@8.20.0", "", { "peerDependencies": { "bufferutil": "^4.0.1", "utf-8-validate": ">=5.0.2" }, "optionalPeers": ["bufferutil", "utf-8-validate"] }, "sha512-sAt8BhgNbzCtgGbt2OxmpuryO63ZoDk/sqaB/znQm94T4fCEsy/yV+7CdC1kJhOU9lboAEU7R3kquuycDoibVA=="], |
| 2112 | 2099 | ||
| 2113 | "wsl-utils": ["wsl-utils@0.1.0", "", { "dependencies": { "is-wsl": "^3.1.0" } }, "sha512-h3Fbisa2nKGPxCpm89Hk33lBLsnaGBvctQopaBSOW/uIs6FTe1ATyAnKFJrzVs9vpGdsTe73WF3V4lIsk4Gacw=="], | ||
| 2114 | |||
| 2115 | "xxhash-wasm": ["xxhash-wasm@1.1.0", "", {}, "sha512-147y/6YNh+tlp6nd/2pWq38i9h6mz/EuQ6njIrmW8D1BS5nCqs0P6DG+m6zTGnNz5I+uhZ0SHxBs9BsPrwcKDA=="], | 2100 | "xxhash-wasm": ["xxhash-wasm@1.1.0", "", {}, "sha512-147y/6YNh+tlp6nd/2pWq38i9h6mz/EuQ6njIrmW8D1BS5nCqs0P6DG+m6zTGnNz5I+uhZ0SHxBs9BsPrwcKDA=="], |
| 2116 | 2101 | ||
| 2117 | "y18n": ["y18n@5.0.8", "", {}, "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA=="], | 2102 | "y18n": ["y18n@5.0.8", "", {}, "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA=="], |
packages/contracts/src/editor.ts+9-6| ... | @@ -44,13 +44,16 @@ export const EDITORS = [ | ... | @@ -44,13 +44,16 @@ export const EDITORS = [ |
| 44 | export const EditorId = Schema.Literals(EDITORS.map((e) => e.id)); | 44 | export const EditorId = Schema.Literals(EDITORS.map((e) => e.id)); |
| 45 | export type EditorId = typeof EditorId.Type; | 45 | export type EditorId = typeof EditorId.Type; |
| 46 | 46 | ||
| 47 | export const OpenInEditorInput = Schema.Struct({ | 47 | export const LaunchEditorInput = Schema.Struct({ |
| 48 | cwd: TrimmedNonEmptyString, | 48 | cwd: TrimmedNonEmptyString, |
| 49 | editor: EditorId, | 49 | editor: EditorId, |
| 50 | }); | 50 | }); |
| 51 | export type OpenInEditorInput = typeof OpenInEditorInput.Type; | 51 | export type LaunchEditorInput = typeof LaunchEditorInput.Type; |
| 52 | 52 | ||
| 53 | export class OpenError extends Schema.TaggedErrorClass<OpenError>()("OpenError", { | 53 | export class ExternalLauncherError extends Schema.TaggedErrorClass<ExternalLauncherError>()( |
| 54 | message: Schema.String, | 54 | "ExternalLauncherError", |
| 55 | cause: Schema.optional(Schema.Defect), | 55 | { |
| 56 | }) {} | 56 | message: Schema.String, |
| 57 | cause: Schema.optional(Schema.Defect), | ||
| 58 | }, | ||
| 59 | ) {} |
packages/contracts/src/rpc.ts+3-3| ... | @@ -2,7 +2,7 @@ import * as Schema from "effect/Schema"; | ... | @@ -2,7 +2,7 @@ import * as Schema from "effect/Schema"; |
| 2 | import * as Rpc from "effect/unstable/rpc/Rpc"; | 2 | import * as Rpc from "effect/unstable/rpc/Rpc"; |
| 3 | import * as RpcGroup from "effect/unstable/rpc/RpcGroup"; | 3 | import * as RpcGroup from "effect/unstable/rpc/RpcGroup"; |
| 4 | 4 | ||
| 5 | import { OpenError, OpenInEditorInput } from "./editor.ts"; | 5 | import { ExternalLauncherError, LaunchEditorInput } from "./editor.ts"; |
| 6 | import { AuthAccessStreamEvent } from "./auth.ts"; | 6 | import { AuthAccessStreamEvent } from "./auth.ts"; |
| 7 | import { | 7 | import { |
| 8 | FilesystemBrowseInput, | 8 | FilesystemBrowseInput, |
| ... | @@ -266,8 +266,8 @@ export const WsProjectsWriteFileRpc = Rpc.make(WS_METHODS.projectsWriteFile, { | ... | @@ -266,8 +266,8 @@ export const WsProjectsWriteFileRpc = Rpc.make(WS_METHODS.projectsWriteFile, { |
| 266 | }); | 266 | }); |
| 267 | 267 | ||
| 268 | export const WsShellOpenInEditorRpc = Rpc.make(WS_METHODS.shellOpenInEditor, { | 268 | export const WsShellOpenInEditorRpc = Rpc.make(WS_METHODS.shellOpenInEditor, { |
| 269 | payload: OpenInEditorInput, | 269 | payload: LaunchEditorInput, |
| 270 | error: OpenError, | 270 | error: ExternalLauncherError, |
| 271 | }); | 271 | }); |
| 272 | 272 | ||
| 273 | export const WsFilesystemBrowseRpc = Rpc.make(WS_METHODS.filesystemBrowse, { | 273 | export const WsFilesystemBrowseRpc = Rpc.make(WS_METHODS.filesystemBrowse, { |