remote-codex 0.1.6 → 0.1.8

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.
Files changed (34) hide show
  1. package/apps/supervisor-api/dist/index.js +7515 -6185
  2. package/apps/supervisor-web/dist/assets/{highlighted-body-OFNGDK62-DvEvXPo5.js → highlighted-body-OFNGDK62-owvlMiML.js} +1 -1
  3. package/apps/supervisor-web/dist/assets/index-CbIt0KnL.css +32 -0
  4. package/apps/supervisor-web/dist/assets/index-CrcX157r.js +377 -0
  5. package/apps/supervisor-web/dist/assets/{xterm-CWQ1ih_R.js → xterm-BQ_J5An_.js} +1 -1
  6. package/apps/supervisor-web/dist/index.html +2 -2
  7. package/package.json +5 -1
  8. package/packages/agent-runtime/src/index.ts +2 -0
  9. package/packages/agent-runtime/src/registry.ts +44 -0
  10. package/packages/agent-runtime/src/types.ts +534 -0
  11. package/packages/codex/src/appServerManager.test.ts +328 -0
  12. package/packages/codex/src/appServerManager.ts +656 -0
  13. package/packages/codex/src/historyItems.ts +1256 -0
  14. package/packages/codex/src/hookHistory.ts +224 -0
  15. package/packages/codex/src/index.ts +6 -0
  16. package/packages/codex/src/jsonrpc.test.ts +58 -0
  17. package/packages/codex/src/jsonrpc.ts +198 -0
  18. package/packages/codex/src/requestMapper.test.ts +127 -0
  19. package/packages/codex/src/requestMapper.ts +511 -0
  20. package/packages/codex/src/runtimeAdapter.ts +743 -0
  21. package/packages/codex/src/types.ts +403 -0
  22. package/packages/db/migrations/0015_agent_provider_fields.sql +14 -0
  23. package/packages/db/migrations/0016_remove_codex_thread_goal_id.sql +46 -0
  24. package/packages/db/migrations/0017_remove_codex_thread_columns.sql +85 -0
  25. package/packages/db/src/client.ts +53 -0
  26. package/packages/db/src/index.ts +5 -0
  27. package/packages/db/src/migrate.test.ts +36 -0
  28. package/packages/db/src/migrate.ts +84 -0
  29. package/packages/db/src/repositories.ts +898 -0
  30. package/packages/db/src/schema.ts +177 -0
  31. package/packages/db/src/seed.ts +51 -0
  32. package/packages/shared/src/index.ts +880 -0
  33. package/apps/supervisor-web/dist/assets/index-CQu6sRq7.css +0 -32
  34. package/apps/supervisor-web/dist/assets/index-MELw9ga_.js +0 -377
@@ -0,0 +1,403 @@
1
+ export type JsonRpcId = number;
2
+
3
+ export interface JsonRpcRequest<TParams = unknown> {
4
+ jsonrpc: '2.0';
5
+ id: JsonRpcId;
6
+ method: string;
7
+ params?: TParams;
8
+ }
9
+
10
+ export interface JsonRpcSuccess<TResult = unknown> {
11
+ jsonrpc?: '2.0';
12
+ id: JsonRpcId;
13
+ result: TResult;
14
+ }
15
+
16
+ export interface JsonRpcFailure {
17
+ jsonrpc?: '2.0';
18
+ id: JsonRpcId;
19
+ error: {
20
+ code: number;
21
+ message: string;
22
+ data?: unknown;
23
+ };
24
+ }
25
+
26
+ export interface JsonRpcNotification<TParams = unknown> {
27
+ jsonrpc?: '2.0';
28
+ method: string;
29
+ params: TParams;
30
+ }
31
+
32
+ export interface CodexClientInfo {
33
+ name: string;
34
+ title: string;
35
+ version: string;
36
+ }
37
+
38
+ export interface AppServerStatusSnapshot {
39
+ state: 'starting' | 'ready' | 'degraded' | 'stopped' | 'failed';
40
+ transport: 'stdio';
41
+ lastStartedAt: string | null;
42
+ lastError: string | null;
43
+ restartCount: number;
44
+ }
45
+
46
+ export interface CodexModelRecord {
47
+ id: string;
48
+ model: string;
49
+ displayName: string;
50
+ description: string;
51
+ hidden: boolean;
52
+ isDefault: boolean;
53
+ supportedReasoningEfforts: Array<{
54
+ reasoningEffort: ReasoningEffort;
55
+ description: string;
56
+ }>;
57
+ defaultReasoningEffort: ReasoningEffort;
58
+ }
59
+
60
+ export type ReasoningEffort =
61
+ | 'none'
62
+ | 'minimal'
63
+ | 'low'
64
+ | 'medium'
65
+ | 'high'
66
+ | 'xhigh';
67
+
68
+ export type CollaborationModeKind = 'default' | 'plan';
69
+ export type SandboxMode = 'read-only' | 'workspace-write' | 'danger-full-access';
70
+ export type ServiceTier = 'fast' | 'flex';
71
+ export type SkillScope = 'user' | 'repo' | 'system' | 'admin';
72
+ export type McpAuthStatus = 'unsupported' | 'notLoggedIn' | 'bearerToken' | 'oAuth';
73
+ export type ThreadGoalStatus = 'active' | 'paused' | 'budgetLimited' | 'complete';
74
+ export type NetworkAccess = 'restricted' | 'enabled';
75
+ export type ReadOnlyAccess =
76
+ | {
77
+ type: 'restricted';
78
+ includePlatformDefaults: boolean;
79
+ readableRoots: string[];
80
+ }
81
+ | {
82
+ type: 'fullAccess';
83
+ };
84
+ export type SandboxPolicy =
85
+ | {
86
+ type: 'dangerFullAccess';
87
+ }
88
+ | {
89
+ type: 'readOnly';
90
+ access: ReadOnlyAccess;
91
+ networkAccess: boolean;
92
+ }
93
+ | {
94
+ type: 'externalSandbox';
95
+ networkAccess: NetworkAccess;
96
+ }
97
+ | {
98
+ type: 'workspaceWrite';
99
+ writableRoots: string[];
100
+ readOnlyAccess: ReadOnlyAccess;
101
+ networkAccess: boolean;
102
+ excludeTmpdirEnvVar: boolean;
103
+ excludeSlashTmp: boolean;
104
+ };
105
+
106
+ export type CodexThreadStatus =
107
+ | { type: 'notLoaded' }
108
+ | { type: 'idle' }
109
+ | { type: 'systemError' }
110
+ | { type: 'active'; activeFlags: string[] };
111
+
112
+ export type CodexTurnStatus = 'completed' | 'interrupted' | 'failed' | 'inProgress';
113
+
114
+ export interface CodexTurnItem {
115
+ type: string;
116
+ id: string;
117
+ text?: string;
118
+ phase?: string | null;
119
+ content?: Array<{ type: string; text?: string }>;
120
+ summary?: string[];
121
+ command?: string;
122
+ aggregatedOutput?: string | null;
123
+ query?: string;
124
+ queries?: string[];
125
+ action?: unknown;
126
+ result?: unknown;
127
+ sources?: unknown;
128
+ status?: string | null;
129
+ changes?: unknown[];
130
+ [key: string]: unknown;
131
+ }
132
+
133
+ export interface CodexTurnRecord {
134
+ id: string;
135
+ status: CodexTurnStatus;
136
+ error: { message?: string } | null;
137
+ items: CodexTurnItem[];
138
+ }
139
+
140
+ export interface CodexThreadRecord {
141
+ id: string;
142
+ preview: string;
143
+ createdAt: number;
144
+ updatedAt: number;
145
+ status: CodexThreadStatus;
146
+ cwd: string;
147
+ name: string | null;
148
+ turns: CodexTurnRecord[];
149
+ }
150
+
151
+ export interface CodexSkillRecord {
152
+ name: string;
153
+ description: string;
154
+ shortDescription: string | null;
155
+ interface: {
156
+ displayName: string | null;
157
+ shortDescription: string | null;
158
+ brandColor: string | null;
159
+ defaultPrompt: string | null;
160
+ } | null;
161
+ path: string;
162
+ scope: SkillScope;
163
+ enabled: boolean;
164
+ }
165
+
166
+ export interface CodexSkillErrorRecord {
167
+ path: string;
168
+ message: string;
169
+ }
170
+
171
+ export interface CodexSkillsListEntry {
172
+ cwd: string;
173
+ skills: CodexSkillRecord[];
174
+ errors: CodexSkillErrorRecord[];
175
+ }
176
+
177
+ export interface CodexMcpToolRecord {
178
+ name: string;
179
+ title: string | null;
180
+ description: string | null;
181
+ }
182
+
183
+ export interface CodexMcpServerRecord {
184
+ name: string;
185
+ authStatus: McpAuthStatus;
186
+ tools: CodexMcpToolRecord[];
187
+ resourceCount: number;
188
+ resourceTemplateCount: number;
189
+ }
190
+
191
+ export type CodexHookEventName =
192
+ | 'preToolUse'
193
+ | 'permissionRequest'
194
+ | 'postToolUse'
195
+ | 'preCompact'
196
+ | 'postCompact'
197
+ | 'sessionStart'
198
+ | 'userPromptSubmit'
199
+ | 'stop';
200
+ export type CodexHookHandlerType = 'command' | 'prompt' | 'agent';
201
+ export type CodexHookSource =
202
+ | 'system'
203
+ | 'user'
204
+ | 'project'
205
+ | 'mdm'
206
+ | 'sessionFlags'
207
+ | 'plugin'
208
+ | 'cloudRequirements'
209
+ | 'legacyManagedConfigFile'
210
+ | 'legacyManagedConfigMdm'
211
+ | 'unknown';
212
+ export type CodexHookTrustStatus = 'managed' | 'untrusted' | 'trusted' | 'modified';
213
+ export type CodexHookRunStatus = 'running' | 'completed' | 'failed' | 'blocked' | 'stopped';
214
+ export type CodexHookExecutionMode = 'sync' | 'async';
215
+ export type CodexHookScope = 'thread' | 'turn';
216
+ export type CodexHookOutputEntryKind = 'warning' | 'stop' | 'feedback' | 'context' | 'error';
217
+
218
+ export interface CodexHookRecord {
219
+ key: string;
220
+ eventName: CodexHookEventName;
221
+ handlerType: CodexHookHandlerType;
222
+ matcher: string | null;
223
+ command: string | null;
224
+ timeoutSec: number;
225
+ statusMessage: string | null;
226
+ sourcePath: string;
227
+ source: CodexHookSource;
228
+ pluginId: string | null;
229
+ displayOrder: number;
230
+ enabled: boolean;
231
+ isManaged: boolean;
232
+ currentHash: string;
233
+ trustStatus: CodexHookTrustStatus;
234
+ }
235
+
236
+ export interface CodexHookErrorRecord {
237
+ path: string;
238
+ message: string;
239
+ }
240
+
241
+ export interface CodexHooksListEntry {
242
+ cwd: string;
243
+ hooks: CodexHookRecord[];
244
+ warnings: string[];
245
+ errors: CodexHookErrorRecord[];
246
+ }
247
+
248
+ export interface CodexHookTrustInput {
249
+ key: string;
250
+ trustedHash: string | null;
251
+ }
252
+
253
+ export interface CodexHookOutputEntry {
254
+ kind: CodexHookOutputEntryKind;
255
+ text: string;
256
+ }
257
+
258
+ export interface CodexHookRunSummary {
259
+ id: string;
260
+ eventName: CodexHookEventName;
261
+ handlerType: CodexHookHandlerType;
262
+ executionMode: CodexHookExecutionMode;
263
+ scope: CodexHookScope;
264
+ sourcePath: string;
265
+ source: CodexHookSource;
266
+ displayOrder: number;
267
+ status: CodexHookRunStatus;
268
+ statusMessage: string | null;
269
+ startedAt: number;
270
+ completedAt: number | null;
271
+ durationMs: number | null;
272
+ entries: CodexHookOutputEntry[];
273
+ stdout?: string | null;
274
+ stderr?: string | null;
275
+ output?: string | null;
276
+ text?: string | null;
277
+ systemMessage?: string | null;
278
+ stopReason?: string | null;
279
+ reason?: string | null;
280
+ }
281
+
282
+ export interface CodexThreadGoalRecord {
283
+ threadId: string;
284
+ objective: string;
285
+ status: ThreadGoalStatus;
286
+ tokenBudget: number | null;
287
+ tokensUsed: number;
288
+ timeUsedSeconds: number;
289
+ createdAt: number;
290
+ updatedAt: number;
291
+ }
292
+
293
+ export interface ThreadStartInput {
294
+ cwd: string;
295
+ model: string;
296
+ approvalPolicy: 'never' | 'on-request';
297
+ sandbox?: SandboxMode | null;
298
+ serviceTier?: ServiceTier | null;
299
+ }
300
+
301
+ export interface ThreadResumeInput {
302
+ threadId: string;
303
+ model?: string | null;
304
+ sandbox?: SandboxMode | null;
305
+ serviceTier?: ServiceTier | null;
306
+ }
307
+
308
+ export interface ThreadForkInput {
309
+ threadId: string;
310
+ }
311
+
312
+ export interface ThreadRollbackInput {
313
+ threadId: string;
314
+ count: number;
315
+ }
316
+
317
+ export interface TurnStartInput {
318
+ threadId: string;
319
+ prompt: string;
320
+ model?: string | null;
321
+ effort?: ReasoningEffort | null;
322
+ collaborationMode?: CollaborationModeKind | null;
323
+ sandboxPolicy?: SandboxPolicy | null;
324
+ serviceTier?: ServiceTier | null;
325
+ }
326
+
327
+ export interface TurnSteerInput {
328
+ threadId: string;
329
+ turnId: string;
330
+ prompt: string;
331
+ }
332
+
333
+ export interface ThreadCompactInput {
334
+ threadId: string;
335
+ }
336
+
337
+ export interface ThreadGoalSetInput {
338
+ threadId: string;
339
+ objective?: string | null;
340
+ status?: ThreadGoalStatus | null;
341
+ tokenBudget?: number | null;
342
+ }
343
+
344
+ export interface CodexServerRequest {
345
+ method: string;
346
+ id: number;
347
+ params: Record<string, unknown>;
348
+ }
349
+
350
+ export interface CodexTurnStartedEvent {
351
+ threadId: string;
352
+ turnId: string;
353
+ }
354
+
355
+ export interface CodexOutputDeltaEvent {
356
+ threadId: string;
357
+ turnId: string;
358
+ itemId: string;
359
+ delta: string;
360
+ }
361
+
362
+ export interface CodexTokenUsageBreakdown {
363
+ totalTokens: number;
364
+ inputTokens: number;
365
+ cachedInputTokens: number;
366
+ outputTokens: number;
367
+ reasoningOutputTokens: number;
368
+ }
369
+
370
+ export interface CodexThreadTokenUsageEvent {
371
+ threadId: string;
372
+ turnId: string;
373
+ tokenUsage: {
374
+ total: CodexTokenUsageBreakdown;
375
+ last: CodexTokenUsageBreakdown;
376
+ modelContextWindow: number | null;
377
+ };
378
+ }
379
+
380
+ export interface CodexErrorEvent {
381
+ threadId: string;
382
+ turnId: string;
383
+ message: string;
384
+ willRetry: boolean;
385
+ }
386
+
387
+ export type CodexServerEvent =
388
+ | { method: 'thread/started'; params: { thread: CodexThreadRecord } }
389
+ | { method: 'thread/status/changed'; params: { threadId: string; status: CodexThreadStatus } }
390
+ | { method: 'thread/name/updated'; params: { threadId: string; threadName?: string } }
391
+ | { method: 'thread/goal/updated'; params: { threadId: string; turnId: string | null; goal: CodexThreadGoalRecord } }
392
+ | { method: 'thread/goal/cleared'; params: { threadId: string } }
393
+ | { method: 'thread/tokenUsage/updated'; params: CodexThreadTokenUsageEvent }
394
+ | { method: 'turn/started'; params: { threadId: string; turn: CodexTurnRecord } }
395
+ | { method: 'hook/started'; params: { threadId: string; turnId: string | null; run: CodexHookRunSummary } }
396
+ | { method: 'hook/completed'; params: { threadId: string; turnId: string | null; run: CodexHookRunSummary } }
397
+ | { method: 'item/started'; params: { threadId: string; turnId: string; item: CodexTurnItem } }
398
+ | { method: 'item/completed'; params: { threadId: string; turnId: string; item: CodexTurnItem } }
399
+ | { method: 'turn/plan/updated'; params: { threadId: string; turnId: string; explanation: string | null; plan: Array<{ step: string; status: string }> } }
400
+ | { method: 'turn/completed'; params: { threadId: string; turn: CodexTurnRecord } }
401
+ | { method: 'item/agentMessage/delta'; params: CodexOutputDeltaEvent }
402
+ | { method: 'error'; params: { error: { message?: string }; willRetry: boolean; threadId: string; turnId: string } }
403
+ | { method: string; params: Record<string, unknown> };
@@ -0,0 +1,14 @@
1
+ ALTER TABLE threads
2
+ ADD COLUMN provider TEXT NOT NULL DEFAULT 'codex';
3
+
4
+ ALTER TABLE threads
5
+ ADD COLUMN provider_session_id TEXT;
6
+
7
+ ALTER TABLE threads
8
+ ADD COLUMN provider_turn_id TEXT;
9
+
10
+ UPDATE threads
11
+ SET provider = 'codex',
12
+ provider_session_id = codex_thread_id,
13
+ provider_turn_id = codex_turn_id
14
+ WHERE provider_session_id IS NULL;
@@ -0,0 +1,46 @@
1
+ CREATE TABLE `thread_goals_new` (
2
+ `id` text PRIMARY KEY NOT NULL,
3
+ `thread_id` text NOT NULL,
4
+ `provider_session_id` text NOT NULL,
5
+ `objective` text NOT NULL,
6
+ `status` text NOT NULL,
7
+ `token_budget` integer,
8
+ `tokens_used` integer DEFAULT 0 NOT NULL,
9
+ `time_used_seconds` integer DEFAULT 0 NOT NULL,
10
+ `started_at` text NOT NULL,
11
+ `completed_at` text,
12
+ `created_at` text NOT NULL,
13
+ `updated_at` text NOT NULL
14
+ );
15
+
16
+ INSERT INTO `thread_goals_new` (
17
+ `id`,
18
+ `thread_id`,
19
+ `provider_session_id`,
20
+ `objective`,
21
+ `status`,
22
+ `token_budget`,
23
+ `tokens_used`,
24
+ `time_used_seconds`,
25
+ `started_at`,
26
+ `completed_at`,
27
+ `created_at`,
28
+ `updated_at`
29
+ )
30
+ SELECT
31
+ `id`,
32
+ `thread_id`,
33
+ `codex_thread_id`,
34
+ `objective`,
35
+ `status`,
36
+ `token_budget`,
37
+ `tokens_used`,
38
+ `time_used_seconds`,
39
+ `started_at`,
40
+ `completed_at`,
41
+ `created_at`,
42
+ `updated_at`
43
+ FROM `thread_goals`;
44
+
45
+ DROP TABLE `thread_goals`;
46
+ ALTER TABLE `thread_goals_new` RENAME TO `thread_goals`;
@@ -0,0 +1,85 @@
1
+ CREATE TABLE `threads_new` (
2
+ `id` text PRIMARY KEY NOT NULL,
3
+ `workspace_id` text NOT NULL,
4
+ `provider` text DEFAULT 'codex' NOT NULL,
5
+ `provider_session_id` text,
6
+ `provider_turn_id` text,
7
+ `source` text DEFAULT 'supervisor' NOT NULL,
8
+ `title` text NOT NULL,
9
+ `model` text,
10
+ `reasoning_effort` text,
11
+ `fast_mode` integer DEFAULT false NOT NULL,
12
+ `fast_base_model` text,
13
+ `fast_base_reasoning_effort` text,
14
+ `collaboration_mode` text DEFAULT 'default' NOT NULL,
15
+ `approval_mode` text,
16
+ `sandbox_mode` text,
17
+ `status` text,
18
+ `summary_text` text,
19
+ `last_error` text,
20
+ `created_at` text NOT NULL,
21
+ `updated_at` text NOT NULL,
22
+ `last_turn_started_at` text,
23
+ `last_turn_completed_at` text,
24
+ `last_viewed_at` text,
25
+ `is_pinned` integer DEFAULT false NOT NULL,
26
+ `is_connected` integer DEFAULT true NOT NULL
27
+ );
28
+
29
+ INSERT INTO `threads_new` (
30
+ `id`,
31
+ `workspace_id`,
32
+ `provider`,
33
+ `provider_session_id`,
34
+ `provider_turn_id`,
35
+ `source`,
36
+ `title`,
37
+ `model`,
38
+ `reasoning_effort`,
39
+ `fast_mode`,
40
+ `fast_base_model`,
41
+ `fast_base_reasoning_effort`,
42
+ `collaboration_mode`,
43
+ `approval_mode`,
44
+ `sandbox_mode`,
45
+ `status`,
46
+ `summary_text`,
47
+ `last_error`,
48
+ `created_at`,
49
+ `updated_at`,
50
+ `last_turn_started_at`,
51
+ `last_turn_completed_at`,
52
+ `last_viewed_at`,
53
+ `is_pinned`,
54
+ `is_connected`
55
+ )
56
+ SELECT
57
+ `id`,
58
+ `workspace_id`,
59
+ COALESCE(`provider`, 'codex'),
60
+ COALESCE(`provider_session_id`, `codex_thread_id`),
61
+ COALESCE(`provider_turn_id`, `codex_turn_id`),
62
+ `source`,
63
+ `title`,
64
+ `model`,
65
+ `reasoning_effort`,
66
+ `fast_mode`,
67
+ `fast_base_model`,
68
+ `fast_base_reasoning_effort`,
69
+ `collaboration_mode`,
70
+ `approval_mode`,
71
+ `sandbox_mode`,
72
+ `status`,
73
+ `summary_text`,
74
+ `last_error`,
75
+ `created_at`,
76
+ `updated_at`,
77
+ `last_turn_started_at`,
78
+ `last_turn_completed_at`,
79
+ `last_viewed_at`,
80
+ `is_pinned`,
81
+ `is_connected`
82
+ FROM `threads`;
83
+
84
+ DROP TABLE `threads`;
85
+ ALTER TABLE `threads_new` RENAME TO `threads`;
@@ -0,0 +1,53 @@
1
+ import fs from 'node:fs';
2
+ import os from 'node:os';
3
+ import path from 'node:path';
4
+
5
+ import Database from 'better-sqlite3';
6
+ import { drizzle } from 'drizzle-orm/better-sqlite3';
7
+
8
+ import * as schema from './schema';
9
+
10
+ export type DatabaseClient = ReturnType<typeof drizzle<typeof schema>>;
11
+ export type SqliteDatabase = Database.Database;
12
+
13
+ export interface DatabaseContext {
14
+ sqlite: SqliteDatabase;
15
+ db: DatabaseClient;
16
+ }
17
+
18
+ function resolvePlatform(): string {
19
+ if (process.platform === 'darwin') {
20
+ return 'macos';
21
+ }
22
+
23
+ if (process.platform === 'linux') {
24
+ return os.release().toLowerCase().includes('microsoft') ? 'wsl-ubuntu' : 'linux';
25
+ }
26
+
27
+ return process.platform;
28
+ }
29
+
30
+ export function createDatabase(databaseUrl: string): DatabaseContext {
31
+ fs.mkdirSync(path.dirname(databaseUrl), { recursive: true });
32
+
33
+ const sqlite = new Database(databaseUrl);
34
+ sqlite.pragma('journal_mode = WAL');
35
+
36
+ return {
37
+ sqlite,
38
+ db: drizzle(sqlite, { schema })
39
+ };
40
+ }
41
+
42
+ export function getDefaultHostRecord() {
43
+ const now = new Date().toISOString();
44
+
45
+ return {
46
+ id: 'local-host',
47
+ hostname: os.hostname(),
48
+ platform: resolvePlatform(),
49
+ tailscaleName: null as string | null,
50
+ createdAt: now,
51
+ lastSeenAt: now
52
+ };
53
+ }
@@ -0,0 +1,5 @@
1
+ export * from './client';
2
+ export * from './migrate';
3
+ export * from './repositories';
4
+ export * from './schema';
5
+ export * from './seed';
@@ -0,0 +1,36 @@
1
+ import fs from 'node:fs/promises';
2
+ import os from 'node:os';
3
+ import path from 'node:path';
4
+
5
+ import { afterEach, describe, expect, it, vi } from 'vitest';
6
+
7
+ import { getMigrationsDir } from './migrate';
8
+
9
+ describe('migrations', () => {
10
+ afterEach(() => {
11
+ vi.unstubAllEnvs();
12
+ });
13
+
14
+ it('prefers the active workspace migrations over an installed package root', async () => {
15
+ const installedRoot = await fs.mkdtemp(path.join(os.tmpdir(), 'remote-codex-installed-root-'));
16
+ await fs.mkdir(path.join(installedRoot, 'packages', 'db', 'migrations'), { recursive: true });
17
+ vi.stubEnv('REMOTE_CODEX_PACKAGE_ROOT', installedRoot);
18
+
19
+ const workspaceDir = await fs.mkdtemp(path.join(os.tmpdir(), 'remote-codex-workspace-root-'));
20
+ await fs.writeFile(path.join(workspaceDir, 'pnpm-workspace.yaml'), 'packages: []\n');
21
+ await fs.mkdir(path.join(workspaceDir, 'packages', 'db', 'migrations'), { recursive: true });
22
+
23
+ expect(getMigrationsDir(workspaceDir)).toBe(
24
+ path.join(workspaceDir, 'packages', 'db', 'migrations'),
25
+ );
26
+ });
27
+
28
+ it('resolves migrations from the installed package root outside a workspace', async () => {
29
+ const tempDir = await fs.mkdtemp(path.join(os.tmpdir(), 'remote-codex-package-root-'));
30
+ await fs.mkdir(path.join(tempDir, 'packages', 'db', 'migrations'), { recursive: true });
31
+ const outsideWorkspace = await fs.mkdtemp(path.join(os.tmpdir(), 'remote-codex-no-workspace-'));
32
+ vi.stubEnv('REMOTE_CODEX_PACKAGE_ROOT', tempDir);
33
+
34
+ expect(getMigrationsDir(outsideWorkspace)).toBe(path.join(tempDir, 'packages', 'db', 'migrations'));
35
+ });
36
+ });