cortex-agents 2.2.0 → 2.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,627 @@
1
+ /**
2
+ * Terminal Driver System — Strategy pattern for cross-platform terminal tab management.
3
+ *
4
+ * Each supported terminal emulator implements the TerminalDriver interface for:
5
+ * - detect() — check if this is the active terminal
6
+ * - openTab() — open a new tab and return identifiers
7
+ * - closeTab() — close a tab by its identifiers (idempotent, never throws)
8
+ *
9
+ * Session data is persisted to `.cortex/.terminal-session` inside each worktree
10
+ * so tabs can be closed when the worktree is removed.
11
+ */
12
+ import * as fs from "fs";
13
+ import * as path from "path";
14
+ import { exec, shellEscape, spawn, which } from "./shell.js";
15
+ // ─── Session I/O ─────────────────────────────────────────────────────────────
16
+ const SESSION_FILE = ".terminal-session";
17
+ /** Persist terminal session data to the worktree's .cortex directory. */
18
+ export function writeSession(worktreePath, session) {
19
+ const cortexDir = path.join(worktreePath, ".cortex");
20
+ if (!fs.existsSync(cortexDir)) {
21
+ fs.mkdirSync(cortexDir, { recursive: true });
22
+ }
23
+ fs.writeFileSync(path.join(cortexDir, SESSION_FILE), JSON.stringify(session, null, 2));
24
+ }
25
+ /** Read terminal session data from the worktree's .cortex directory. */
26
+ export function readSession(worktreePath) {
27
+ const sessionPath = path.join(worktreePath, ".cortex", SESSION_FILE);
28
+ if (!fs.existsSync(sessionPath))
29
+ return null;
30
+ try {
31
+ return JSON.parse(fs.readFileSync(sessionPath, "utf-8"));
32
+ }
33
+ catch {
34
+ return null;
35
+ }
36
+ }
37
+ // ─── Helper: build the shell command for the new tab ─────────────────────────
38
+ function buildTabCommand(opts) {
39
+ return `cd "${opts.worktreePath}" && "${opts.opencodeBin}" --agent ${opts.agent}`;
40
+ }
41
+ // ─── Helper: safe process kill ───────────────────────────────────────────────
42
+ function killPid(pid) {
43
+ try {
44
+ process.kill(pid, "SIGTERM");
45
+ return true;
46
+ }
47
+ catch {
48
+ return false; // Process already dead or permission denied
49
+ }
50
+ }
51
+ // ═════════════════════════════════════════════════════════════════════════════
52
+ // Driver Implementations
53
+ // ═════════════════════════════════════════════════════════════════════════════
54
+ // ─── tmux (multiplexer — highest priority) ───────────────────────────────────
55
+ class TmuxDriver {
56
+ name = "tmux";
57
+ detect() {
58
+ return !!process.env.TMUX;
59
+ }
60
+ async openTab(opts) {
61
+ const cmd = buildTabCommand(opts);
62
+ try {
63
+ // -P prints info about the new window, -F formats it
64
+ const result = await exec("tmux", [
65
+ "new-window",
66
+ "-P",
67
+ "-F",
68
+ "#{pane_id}",
69
+ "-c",
70
+ opts.worktreePath,
71
+ cmd,
72
+ ]);
73
+ const paneId = result.stdout.trim();
74
+ return { paneId: paneId || undefined };
75
+ }
76
+ catch {
77
+ // Fallback: try without -P (older tmux)
78
+ try {
79
+ await exec("tmux", ["new-window", "-c", opts.worktreePath, cmd]);
80
+ return {};
81
+ }
82
+ catch {
83
+ throw new Error("Failed to open tmux window");
84
+ }
85
+ }
86
+ }
87
+ async closeTab(session) {
88
+ if (session.paneId) {
89
+ try {
90
+ await exec("tmux", ["kill-pane", "-t", session.paneId], { nothrow: true });
91
+ return true;
92
+ }
93
+ catch {
94
+ // Pane already closed
95
+ }
96
+ }
97
+ // Fallback: kill PID
98
+ if (session.pid)
99
+ return killPid(session.pid);
100
+ return false;
101
+ }
102
+ }
103
+ // ─── iTerm2 (macOS) ──────────────────────────────────────────────────────────
104
+ class ITerm2Driver {
105
+ name = "iterm2";
106
+ detect() {
107
+ if (process.platform !== "darwin")
108
+ return false;
109
+ if (process.env.ITERM_SESSION_ID)
110
+ return true;
111
+ if (process.env.TERM_PROGRAM === "iTerm.app")
112
+ return true;
113
+ const bundleId = process.env.__CFBundleIdentifier;
114
+ if (bundleId?.includes("iterm2") || bundleId?.includes("iTerm"))
115
+ return true;
116
+ return false;
117
+ }
118
+ async openTab(opts) {
119
+ const safePath = shellEscape(opts.worktreePath);
120
+ const safeBin = shellEscape(opts.opencodeBin);
121
+ const safeAgent = shellEscape(opts.agent);
122
+ // Create tab, write command, capture session ID
123
+ const script = `tell application "iTerm2"
124
+ tell current window
125
+ create tab with default profile
126
+ tell current session of current tab
127
+ write text "cd \\"${safePath}\\" && \\"${safeBin}\\" --agent ${safeAgent}"
128
+ return id
129
+ end tell
130
+ end tell
131
+ end tell`;
132
+ try {
133
+ const result = await exec("osascript", ["-e", script]);
134
+ const sessionId = result.stdout.trim();
135
+ return { sessionId: sessionId || undefined };
136
+ }
137
+ catch {
138
+ // Fallback: try without capturing ID
139
+ const fallbackScript = `tell application "iTerm2"
140
+ tell current window
141
+ create tab with default profile
142
+ tell current session of current tab
143
+ write text "cd \\"${safePath}\\" && \\"${safeBin}\\" --agent ${safeAgent}"
144
+ end tell
145
+ end tell
146
+ end tell`;
147
+ try {
148
+ await exec("osascript", ["-e", fallbackScript]);
149
+ return {};
150
+ }
151
+ catch {
152
+ throw new Error("Failed to open iTerm2 tab");
153
+ }
154
+ }
155
+ }
156
+ async closeTab(session) {
157
+ if (!session.sessionId) {
158
+ if (session.pid)
159
+ return killPid(session.pid);
160
+ return false;
161
+ }
162
+ const script = `tell application "iTerm2"
163
+ repeat with w in windows
164
+ repeat with t in tabs of w
165
+ repeat with s in sessions of t
166
+ if id of s is "${shellEscape(session.sessionId)}" then
167
+ close s
168
+ return "closed"
169
+ end if
170
+ end repeat
171
+ end repeat
172
+ end repeat
173
+ return "not_found"
174
+ end tell`;
175
+ try {
176
+ const result = await exec("osascript", ["-e", script], { nothrow: true });
177
+ return result.stdout.trim() === "closed";
178
+ }
179
+ catch {
180
+ return false;
181
+ }
182
+ }
183
+ }
184
+ // ─── Terminal.app (macOS) ────────────────────────────────────────────────────
185
+ class TerminalAppDriver {
186
+ name = "terminal.app";
187
+ detect() {
188
+ if (process.platform !== "darwin")
189
+ return false;
190
+ if (process.env.TERM_PROGRAM === "Apple_Terminal")
191
+ return true;
192
+ const bundleId = process.env.__CFBundleIdentifier;
193
+ if (bundleId?.includes("Terminal") || bundleId?.includes("apple.Terminal"))
194
+ return true;
195
+ return false;
196
+ }
197
+ async openTab(opts) {
198
+ const safePath = shellEscape(opts.worktreePath);
199
+ const safeBin = shellEscape(opts.opencodeBin);
200
+ const safeAgent = shellEscape(opts.agent);
201
+ // do script returns tab reference; we capture the window ID
202
+ const script = `tell application "Terminal"
203
+ activate
204
+ set newTab to do script "cd \\"${safePath}\\" && \\"${safeBin}\\" --agent ${safeAgent}"
205
+ return id of window of newTab
206
+ end tell`;
207
+ try {
208
+ const result = await exec("osascript", ["-e", script]);
209
+ const windowId = result.stdout.trim();
210
+ return { windowId: windowId || undefined };
211
+ }
212
+ catch {
213
+ // Fallback: basic open without capturing ID
214
+ try {
215
+ await exec("open", ["-a", "Terminal", opts.worktreePath]);
216
+ return {};
217
+ }
218
+ catch {
219
+ throw new Error("Failed to open Terminal.app");
220
+ }
221
+ }
222
+ }
223
+ async closeTab(session) {
224
+ if (!session.windowId) {
225
+ if (session.pid)
226
+ return killPid(session.pid);
227
+ return false;
228
+ }
229
+ const script = `tell application "Terminal"
230
+ try
231
+ close window id ${session.windowId}
232
+ return "closed"
233
+ on error
234
+ return "not_found"
235
+ end try
236
+ end tell`;
237
+ try {
238
+ const result = await exec("osascript", ["-e", script], { nothrow: true });
239
+ return result.stdout.trim() === "closed";
240
+ }
241
+ catch {
242
+ return false;
243
+ }
244
+ }
245
+ }
246
+ // ─── kitty (Linux/macOS) ─────────────────────────────────────────────────────
247
+ class KittyDriver {
248
+ name = "kitty";
249
+ detect() {
250
+ return !!process.env.KITTY_WINDOW_ID || process.env.TERM_PROGRAM === "kitty";
251
+ }
252
+ /**
253
+ * Check if kitty remote control is available.
254
+ * Requires `allow_remote_control yes` in kitty.conf.
255
+ */
256
+ async hasRemoteControl() {
257
+ try {
258
+ await exec("kitty", ["@", "ls"], { timeout: 3000, nothrow: true });
259
+ return true;
260
+ }
261
+ catch {
262
+ return false;
263
+ }
264
+ }
265
+ async openTab(opts) {
266
+ const cmd = buildTabCommand(opts);
267
+ // Prefer IPC tab creation if remote control is enabled
268
+ if (await this.hasRemoteControl()) {
269
+ try {
270
+ const result = await exec("kitty", [
271
+ "@",
272
+ "launch",
273
+ "--type=tab",
274
+ `--cwd=${opts.worktreePath}`,
275
+ `--title=Worktree: ${opts.branchName}`,
276
+ "bash",
277
+ "-c",
278
+ cmd,
279
+ ]);
280
+ const tabId = result.stdout.trim();
281
+ return { tabId: tabId || undefined };
282
+ }
283
+ catch {
284
+ // Fall through to new-window approach
285
+ }
286
+ }
287
+ // Fallback: open a new kitty window (captures PID)
288
+ const child = spawn("kitty", [
289
+ "--directory",
290
+ opts.worktreePath,
291
+ "--title",
292
+ `Worktree: ${opts.branchName}`,
293
+ "--",
294
+ "bash",
295
+ "-c",
296
+ cmd,
297
+ ], { cwd: opts.worktreePath });
298
+ return { pid: child.pid ?? undefined };
299
+ }
300
+ async closeTab(session) {
301
+ // Try IPC close by tab ID
302
+ if (session.tabId) {
303
+ try {
304
+ await exec("kitty", ["@", "close-tab", `--match=id:${session.tabId}`], { nothrow: true });
305
+ return true;
306
+ }
307
+ catch {
308
+ // Tab may not exist
309
+ }
310
+ }
311
+ // Try IPC close by PID
312
+ if (session.pid) {
313
+ try {
314
+ await exec("kitty", ["@", "close-window", `--match=pid:${session.pid}`], { nothrow: true });
315
+ return true;
316
+ }
317
+ catch {
318
+ // Fall through to kill
319
+ }
320
+ }
321
+ // Fallback: kill PID
322
+ if (session.pid)
323
+ return killPid(session.pid);
324
+ return false;
325
+ }
326
+ }
327
+ // ─── wezterm ─────────────────────────────────────────────────────────────────
328
+ class WeztermDriver {
329
+ name = "wezterm";
330
+ detect() {
331
+ return !!process.env.WEZTERM_PANE || process.env.TERM_PROGRAM === "WezTerm";
332
+ }
333
+ async openTab(opts) {
334
+ const cmd = buildTabCommand(opts);
335
+ // wezterm cli spawn opens a tab in the current window
336
+ try {
337
+ const result = await exec("wezterm", [
338
+ "cli",
339
+ "spawn",
340
+ "--cwd",
341
+ opts.worktreePath,
342
+ "--",
343
+ "bash",
344
+ "-c",
345
+ cmd,
346
+ ]);
347
+ const paneId = result.stdout.trim();
348
+ return { paneId: paneId || undefined };
349
+ }
350
+ catch {
351
+ // Fallback: wezterm start opens a new window
352
+ const child = spawn("wezterm", [
353
+ "start",
354
+ "--cwd",
355
+ opts.worktreePath,
356
+ "--",
357
+ "bash",
358
+ "-c",
359
+ cmd,
360
+ ], { cwd: opts.worktreePath });
361
+ return { pid: child.pid ?? undefined };
362
+ }
363
+ }
364
+ async closeTab(session) {
365
+ if (session.paneId) {
366
+ try {
367
+ await exec("wezterm", ["cli", "kill-pane", "--pane-id", session.paneId], { nothrow: true });
368
+ return true;
369
+ }
370
+ catch {
371
+ // Pane may not exist
372
+ }
373
+ }
374
+ if (session.pid)
375
+ return killPid(session.pid);
376
+ return false;
377
+ }
378
+ }
379
+ // ─── Konsole (KDE) ───────────────────────────────────────────────────────────
380
+ class KonsoleDriver {
381
+ name = "konsole";
382
+ detect() {
383
+ return !!process.env.KONSOLE_VERSION;
384
+ }
385
+ /** Find the qdbus binary (qdbus or qdbus6 on newer KDE). */
386
+ async findQdbus() {
387
+ const bin = await which("qdbus");
388
+ if (bin)
389
+ return "qdbus";
390
+ const bin6 = await which("qdbus6");
391
+ if (bin6)
392
+ return "qdbus6";
393
+ return null;
394
+ }
395
+ async openTab(opts) {
396
+ const cmd = buildTabCommand(opts);
397
+ const qdbus = await this.findQdbus();
398
+ const service = process.env.KONSOLE_DBUS_SERVICE
399
+ ? `org.kde.konsole-${process.env.KONSOLE_DBUS_SERVICE}`
400
+ : "org.kde.konsole";
401
+ // Try D-Bus new session
402
+ if (qdbus) {
403
+ try {
404
+ const result = await exec(qdbus, [
405
+ service,
406
+ "/Windows/1",
407
+ "newSession",
408
+ ]);
409
+ const sessionNum = result.stdout.trim();
410
+ const dbusPath = `/Sessions/${sessionNum}`;
411
+ // Set working directory and run command
412
+ await exec(qdbus, [service, dbusPath, "setProfile", "Default"]);
413
+ await exec(qdbus, [service, dbusPath, "runCommand", cmd]);
414
+ return { dbusPath };
415
+ }
416
+ catch {
417
+ // Fall through
418
+ }
419
+ }
420
+ // Fallback: launch konsole with --new-tab
421
+ try {
422
+ const child = spawn("konsole", [
423
+ "--new-tab",
424
+ "--workdir",
425
+ opts.worktreePath,
426
+ "-e",
427
+ "bash",
428
+ "-c",
429
+ cmd,
430
+ ], { cwd: opts.worktreePath });
431
+ return { pid: child.pid ?? undefined };
432
+ }
433
+ catch {
434
+ throw new Error("Failed to open Konsole tab");
435
+ }
436
+ }
437
+ async closeTab(session) {
438
+ if (session.dbusPath) {
439
+ const qdbus = await this.findQdbus();
440
+ const service = process.env.KONSOLE_DBUS_SERVICE
441
+ ? `org.kde.konsole-${process.env.KONSOLE_DBUS_SERVICE}`
442
+ : "org.kde.konsole";
443
+ if (qdbus) {
444
+ try {
445
+ await exec(qdbus, [service, session.dbusPath, "close"], { nothrow: true });
446
+ return true;
447
+ }
448
+ catch {
449
+ // Session may not exist
450
+ }
451
+ }
452
+ }
453
+ if (session.pid)
454
+ return killPid(session.pid);
455
+ return false;
456
+ }
457
+ }
458
+ // ─── GNOME Terminal ──────────────────────────────────────────────────────────
459
+ class GnomeTerminalDriver {
460
+ name = "gnome-terminal";
461
+ detect() {
462
+ return !!process.env.GNOME_TERMINAL_SERVICE;
463
+ }
464
+ async openTab(opts) {
465
+ const cmd = buildTabCommand(opts);
466
+ // gnome-terminal --tab opens in the current window
467
+ try {
468
+ const child = spawn("gnome-terminal", [
469
+ "--tab",
470
+ `--working-directory=${opts.worktreePath}`,
471
+ "--",
472
+ "bash",
473
+ "-c",
474
+ cmd,
475
+ ], { cwd: opts.worktreePath });
476
+ return { pid: child.pid ?? undefined };
477
+ }
478
+ catch {
479
+ // Fallback: open a new window
480
+ const child = spawn("gnome-terminal", [
481
+ `--working-directory=${opts.worktreePath}`,
482
+ "--",
483
+ "bash",
484
+ "-c",
485
+ cmd,
486
+ ], { cwd: opts.worktreePath });
487
+ return { pid: child.pid ?? undefined };
488
+ }
489
+ }
490
+ async closeTab(session) {
491
+ // GNOME Terminal has no reliable tab-close API — kill PID
492
+ if (session.pid)
493
+ return killPid(session.pid);
494
+ return false;
495
+ }
496
+ }
497
+ // ─── Fallback (PID-based, always matches) ────────────────────────────────────
498
+ class FallbackDriver {
499
+ name = "fallback";
500
+ detect() {
501
+ return true; // Always matches — catch-all
502
+ }
503
+ async openTab(opts) {
504
+ const cmd = buildTabCommand(opts);
505
+ const platform = process.platform;
506
+ // macOS: try generic open
507
+ if (platform === "darwin") {
508
+ try {
509
+ await exec("open", ["-a", "Terminal", opts.worktreePath]);
510
+ return {};
511
+ }
512
+ catch {
513
+ // Fall through
514
+ }
515
+ }
516
+ // Linux: try common terminals in order
517
+ if (platform === "linux") {
518
+ const terminals = [
519
+ { name: "xterm", args: ["-e", "bash", "-c", cmd] },
520
+ { name: "x-terminal-emulator", args: ["-e", "bash", "-c", cmd] },
521
+ ];
522
+ for (const t of terminals) {
523
+ try {
524
+ const child = spawn(t.name, t.args, { cwd: opts.worktreePath });
525
+ return { pid: child.pid ?? undefined };
526
+ }
527
+ catch {
528
+ continue;
529
+ }
530
+ }
531
+ }
532
+ // Windows: try Windows Terminal, then cmd
533
+ if (platform === "win32") {
534
+ // Try Windows Terminal first
535
+ const wt = await which("wt.exe");
536
+ if (wt) {
537
+ try {
538
+ const child = spawn("wt.exe", [
539
+ "new-tab",
540
+ "--startingDirectory",
541
+ opts.worktreePath,
542
+ "cmd",
543
+ "/k",
544
+ cmd,
545
+ ], { cwd: opts.worktreePath });
546
+ return { pid: child.pid ?? undefined };
547
+ }
548
+ catch {
549
+ // Fall through to cmd
550
+ }
551
+ }
552
+ try {
553
+ await exec("cmd", ["/k", cmd], { cwd: opts.worktreePath });
554
+ return {};
555
+ }
556
+ catch {
557
+ // Nothing worked
558
+ }
559
+ }
560
+ throw new Error(`Could not open terminal on ${platform}`);
561
+ }
562
+ async closeTab(session) {
563
+ if (session.pid)
564
+ return killPid(session.pid);
565
+ return false;
566
+ }
567
+ }
568
+ // ═════════════════════════════════════════════════════════════════════════════
569
+ // Detection Registry
570
+ // ═════════════════════════════════════════════════════════════════════════════
571
+ /**
572
+ * Ordered list of terminal drivers. Detection runs first-to-last.
573
+ *
574
+ * Priority: multiplexers first (tmux), then terminal emulators, then fallback.
575
+ * This ensures that if the user is in tmux inside iTerm2, we open a tmux window.
576
+ */
577
+ const DRIVERS = [
578
+ new TmuxDriver(),
579
+ new ITerm2Driver(),
580
+ new TerminalAppDriver(),
581
+ new KittyDriver(),
582
+ new WeztermDriver(),
583
+ new KonsoleDriver(),
584
+ new GnomeTerminalDriver(),
585
+ new FallbackDriver(),
586
+ ];
587
+ /** Map of driver name → driver instance for reverse lookup. */
588
+ const DRIVER_MAP = new Map(DRIVERS.map((d) => [d.name, d]));
589
+ /**
590
+ * Detect the active terminal emulator and return the matching driver.
591
+ * Falls back to FallbackDriver if no specific terminal is detected.
592
+ */
593
+ export function detectDriver() {
594
+ for (const driver of DRIVERS) {
595
+ if (driver.detect())
596
+ return driver;
597
+ }
598
+ // Should never reach here (FallbackDriver always matches)
599
+ return new FallbackDriver();
600
+ }
601
+ /**
602
+ * Get a driver by name (used when closing a tab from a persisted session).
603
+ * Returns null if the driver name is unknown.
604
+ */
605
+ export function getDriverByName(name) {
606
+ return DRIVER_MAP.get(name) ?? null;
607
+ }
608
+ /**
609
+ * Close a terminal session using the appropriate driver.
610
+ * Handles all modes (terminal, pty, background) with PID fallback.
611
+ *
612
+ * This is the main entry point for worktree_remove cleanup.
613
+ */
614
+ export async function closeSession(session) {
615
+ if (session.mode === "terminal") {
616
+ const driver = getDriverByName(session.terminal);
617
+ if (driver) {
618
+ const closed = await driver.closeTab(session);
619
+ if (closed)
620
+ return true;
621
+ }
622
+ }
623
+ // Universal fallback: kill PID
624
+ if (session.pid)
625
+ return killPid(session.pid);
626
+ return false;
627
+ }
@@ -1 +1 @@
1
- {"version":3,"file":"worktree-detect.d.ts","sourceRoot":"","sources":["../../src/utils/worktree-detect.ts"],"names":[],"mappings":"AAEA;;GAEG;AACH,MAAM,WAAW,YAAY;IAC3B,qFAAqF;IACrF,UAAU,EAAE,OAAO,CAAC;IACpB,8BAA8B;IAC9B,aAAa,EAAE,MAAM,CAAC;IACtB,2EAA2E;IAC3E,gBAAgB,EAAE,MAAM,GAAG,IAAI,CAAC;CACjC;AAED;;;;;;GAMG;AACH,wBAAsB,kBAAkB,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,YAAY,CAAC,CAqC3E"}
1
+ {"version":3,"file":"worktree-detect.d.ts","sourceRoot":"","sources":["../../src/utils/worktree-detect.ts"],"names":[],"mappings":"AAGA;;GAEG;AACH,MAAM,WAAW,YAAY;IAC3B,qFAAqF;IACrF,UAAU,EAAE,OAAO,CAAC;IACpB,8BAA8B;IAC9B,aAAa,EAAE,MAAM,CAAC;IACtB,2EAA2E;IAC3E,gBAAgB,EAAE,MAAM,GAAG,IAAI,CAAC;CACjC;AAED;;;;;;GAMG;AACH,wBAAsB,kBAAkB,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,YAAY,CAAC,CAqC3E"}
@@ -1,4 +1,5 @@
1
1
  import * as path from "path";
2
+ import { git } from "./shell.js";
2
3
  /**
3
4
  * Detect whether the current git directory is a linked worktree.
4
5
  *
@@ -14,16 +15,16 @@ export async function detectWorktreeInfo(cwd) {
14
15
  };
15
16
  // Get current branch
16
17
  try {
17
- const branch = await Bun.$ `git -C ${cwd} branch --show-current`.quiet().text();
18
- result.currentBranch = branch.trim();
18
+ const { stdout } = await git(cwd, "branch", "--show-current");
19
+ result.currentBranch = stdout.trim();
19
20
  }
20
21
  catch {
21
22
  result.currentBranch = "(unknown)";
22
23
  }
23
24
  // Compare git-dir and git-common-dir
24
25
  try {
25
- const gitDirRaw = await Bun.$ `git -C ${cwd} rev-parse --git-dir`.quiet().text();
26
- const commonDirRaw = await Bun.$ `git -C ${cwd} rev-parse --git-common-dir`.quiet().text();
26
+ const { stdout: gitDirRaw } = await git(cwd, "rev-parse", "--git-dir");
27
+ const { stdout: commonDirRaw } = await git(cwd, "rev-parse", "--git-common-dir");
27
28
  // Resolve both to absolute paths for reliable comparison
28
29
  const absGitDir = path.resolve(cwd, gitDirRaw.trim());
29
30
  const absCommonDir = path.resolve(cwd, commonDirRaw.trim());
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "cortex-agents",
3
- "version": "2.2.0",
3
+ "version": "2.3.0",
4
4
  "description": "Supercharge OpenCode with structured workflows, intelligent agents, and automated development practices",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
@@ -14,6 +14,8 @@
14
14
  ],
15
15
  "scripts": {
16
16
  "build": "tsc",
17
+ "test": "vitest run",
18
+ "test:watch": "vitest",
17
19
  "prepare": "npm run build",
18
20
  "postinstall": "echo 'Run: npx cortex-agents install && npx cortex-agents configure'"
19
21
  },
@@ -53,11 +55,10 @@
53
55
  },
54
56
  "devDependencies": {
55
57
  "@opencode-ai/plugin": "^1.0.0",
56
- "@types/bun": "^1.3.9",
57
58
  "@types/node": "^20.0.0",
58
59
  "@types/prompts": "^2.4.9",
59
- "bun-types": "^1.0.0",
60
- "typescript": "^5.0.0"
60
+ "typescript": "^5.0.0",
61
+ "vitest": "^3.0.0"
61
62
  },
62
63
  "dependencies": {
63
64
  "prompts": "^2.4.2"