From eb2f4ffe53943aea6a7588d87de955770a372156 Mon Sep 17 00:00:00 2001 From: Kamal Fariz Mahyuddin Date: Sun, 8 Feb 2026 15:34:44 -0800 Subject: [PATCH 1/3] Support persisting custom env vars in session defaults --- .../__tests__/session_set_defaults.test.ts | 42 +++++++++++++++++++ src/utils/session-defaults-schema.ts | 5 +++ src/utils/session-store.ts | 1 + src/version.ts | 2 +- 4 files changed, 49 insertions(+), 1 deletion(-) diff --git a/src/mcp/tools/session-management/__tests__/session_set_defaults.test.ts b/src/mcp/tools/session-management/__tests__/session_set_defaults.test.ts index f1cb8c3b..f37435bd 100644 --- a/src/mcp/tools/session-management/__tests__/session_set_defaults.test.ts +++ b/src/mcp/tools/session-management/__tests__/session_set_defaults.test.ts @@ -258,6 +258,48 @@ describe('session-set-defaults tool', () => { expect(parsed.sessionDefaults?.simulatorName).toBeUndefined(); }); + it('should store env as a Record default', async () => { + const envVars = { STAGING_ENABLED: '1', DEBUG: 'true' }; + const result = await sessionSetDefaultsLogic({ env: envVars }, createContext()); + + expect(result.isError).toBe(false); + expect(sessionStore.getAll().env).toEqual(envVars); + }); + + it('should persist env to config when persist is true', async () => { + const yaml = ['schemaVersion: 1', 'sessionDefaults: {}', ''].join('\n'); + + const writes: { path: string; content: string }[] = []; + const fs = createMockFileSystemExecutor({ + existsSync: (targetPath: string) => targetPath === configPath, + readFile: async (targetPath: string) => { + if (targetPath !== configPath) { + throw new Error(`Unexpected readFile path: ${targetPath}`); + } + return yaml; + }, + writeFile: async (targetPath: string, content: string) => { + writes.push({ path: targetPath, content }); + }, + }); + + await initConfigStore({ cwd, fs }); + + const envVars = { API_URL: 'https://staging.example.com' }; + const result = await sessionSetDefaultsLogic( + { env: envVars, persist: true }, + createContext(), + ); + + expect(result.content[0].text).toContain('Persisted defaults to'); + expect(writes.length).toBe(1); + + const parsed = parseYaml(writes[0].content) as { + sessionDefaults?: Record; + }; + expect(parsed.sessionDefaults?.env).toEqual(envVars); + }); + it('should not persist when persist is true but no defaults were provided', async () => { const result = await sessionSetDefaultsLogic({ persist: true }, createContext()); diff --git a/src/utils/session-defaults-schema.ts b/src/utils/session-defaults-schema.ts index 0383e86e..253c16f6 100644 --- a/src/utils/session-defaults-schema.ts +++ b/src/utils/session-defaults-schema.ts @@ -16,6 +16,7 @@ export const sessionDefaultKeys = [ 'preferXcodebuild', 'platform', 'bundleId', + 'env', ] as const; export type SessionDefaultKey = (typeof sessionDefaultKeys)[number]; @@ -54,4 +55,8 @@ export const sessionDefaultsSchema = z.object({ .string() .optional() .describe('Default bundle ID for launch/stop/log tools when working on a single app.'), + env: z + .record(z.string(), z.string()) + .optional() + .describe('Default environment variables to pass to launched apps.'), }); diff --git a/src/utils/session-store.ts b/src/utils/session-store.ts index 85ddf05c..548797d4 100644 --- a/src/utils/session-store.ts +++ b/src/utils/session-store.ts @@ -20,6 +20,7 @@ export type SessionDefaults = { preferXcodebuild?: boolean; platform?: string; bundleId?: string; + env?: Record; }; class SessionStore { diff --git a/src/version.ts b/src/version.ts index 35404a97..255d996f 100644 --- a/src/version.ts +++ b/src/version.ts @@ -1,3 +1,3 @@ -export const version = '2.0.0-beta.1'; +export const version = '2.0.0'; export const iOSTemplateVersion = 'v1.0.8'; export const macOSTemplateVersion = 'v1.0.5'; From 6110405ad28452af00052dcd19a3bbd9da97cd0e Mon Sep 17 00:00:00 2001 From: Kamal Fariz Mahyuddin Date: Sun, 8 Feb 2026 20:00:04 -0800 Subject: [PATCH 2/3] deep merge env --- .../session-aware-tool-factory.test.ts | 31 +++++++++++++++++++ src/utils/typed-tool-factory.ts | 9 +++++- 2 files changed, 39 insertions(+), 1 deletion(-) diff --git a/src/utils/__tests__/session-aware-tool-factory.test.ts b/src/utils/__tests__/session-aware-tool-factory.test.ts index feed93e5..ea9a4b91 100644 --- a/src/utils/__tests__/session-aware-tool-factory.test.ts +++ b/src/utils/__tests__/session-aware-tool-factory.test.ts @@ -258,4 +258,35 @@ describe('createSessionAwareTool', () => { expect(parsed.simulatorId).toBe('SIM-123'); expect(parsed.simulatorName).toBeUndefined(); }); + + it('deep-merges env so user-provided env vars are additive with session defaults', async () => { + const envSchema = z.object({ + scheme: z.string(), + projectPath: z.string().optional(), + env: z.record(z.string(), z.string()).optional(), + }); + + const envHandler = createSessionAwareTool>({ + internalSchema: envSchema, + logicFunction: async (params) => ({ + content: [{ type: 'text', text: JSON.stringify(params.env) }], + isError: false, + }), + getExecutor: () => createMockExecutor({ success: true }), + requirements: [{ allOf: ['scheme'] }], + }); + + sessionStore.setDefaults({ + scheme: 'App', + projectPath: '/a.xcodeproj', + env: { API_KEY: 'abc123', VERBOSE: '1' }, + }); + + // User provides additional env var; session default env vars should be preserved + const result = await envHandler({ env: { DEBUG: 'true', VERBOSE: '0' } }); + expect(result.isError).toBe(false); + + const parsed = JSON.parse(result.content[0].text); + expect(parsed).toEqual({ API_KEY: 'abc123', DEBUG: 'true', VERBOSE: '0' }); + }); }); diff --git a/src/utils/typed-tool-factory.ts b/src/utils/typed-tool-factory.ts index f6ddfe95..0c9593d7 100644 --- a/src/utils/typed-tool-factory.ts +++ b/src/utils/typed-tool-factory.ts @@ -163,7 +163,14 @@ function createSessionAwareHandler(opts: { } // Start with session defaults merged with explicit args (args override session) - const merged: Record = { ...sessionStore.getAll(), ...sanitizedArgs }; + const sessionDefaults = sessionStore.getAll(); + const merged: Record = { ...sessionDefaults, ...sanitizedArgs }; + + // Deep-merge env: combine session-default env vars with user-provided ones + // (user-provided keys take precedence on conflict) + if (sessionDefaults.env && typeof sanitizedArgs.env === 'object' && sanitizedArgs.env) { + merged.env = { ...sessionDefaults.env, ...(sanitizedArgs.env as Record) }; + } // Apply exclusive pair pruning: only when caller provided a concrete (non-null/undefined) value // for any key in the pair. When activated, drop other keys in the pair coming from session defaults. From 3a47f1f87ef8aa343ba8ed06240e418cba7694aa Mon Sep 17 00:00:00 2001 From: Kamal Fariz Mahyuddin Date: Mon, 9 Feb 2026 10:12:22 -0800 Subject: [PATCH 3/3] fix typecheck --- src/utils/__tests__/session-aware-tool-factory.test.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/utils/__tests__/session-aware-tool-factory.test.ts b/src/utils/__tests__/session-aware-tool-factory.test.ts index ea9a4b91..dc8161ad 100644 --- a/src/utils/__tests__/session-aware-tool-factory.test.ts +++ b/src/utils/__tests__/session-aware-tool-factory.test.ts @@ -286,7 +286,8 @@ describe('createSessionAwareTool', () => { const result = await envHandler({ env: { DEBUG: 'true', VERBOSE: '0' } }); expect(result.isError).toBe(false); - const parsed = JSON.parse(result.content[0].text); + const envContent = result.content[0] as { type: 'text'; text: string }; + const parsed = JSON.parse(envContent.text); expect(parsed).toEqual({ API_KEY: 'abc123', DEBUG: 'true', VERBOSE: '0' }); }); });