Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
62 changes: 62 additions & 0 deletions apps/cli/lib/debug-log.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
import { appendFileSync } from 'fs';
import { tmpdir } from 'os';
import { join } from 'path';

export interface DebugLogOptions {
defaultFilename: string;
enabledEnvVar: string;
logFileEnvVar?: string;
scope?: string;
}

export interface DebugLogEntry {
event: string;
payload?: unknown;
scope?: string;
timestamp: string;
}

export interface DebugLogger {
enabled: boolean;
log: ( event: string, payload?: unknown ) => void;
path: string;
}

function isEnabled( value: string | undefined ): boolean {
if ( ! value ) {
return false;
}

const normalized = value.trim().toLowerCase();
return normalized === '1' || normalized === 'true';
}

export function createDebugLogger( options: DebugLogOptions ): DebugLogger {
const enabled = isEnabled( process.env[ options.enabledEnvVar ] );
const path =
process.env[ options.logFileEnvVar ?? `${ options.enabledEnvVar }_FILE` ] ??
join( tmpdir(), options.defaultFilename );

return {
enabled,
path,
log: ( event, payload ) => {
if ( ! enabled ) {
return;
}

const entry: DebugLogEntry = {
timestamp: new Date().toISOString(),
scope: options.scope,
event,
payload,
};

try {
appendFileSync( path, JSON.stringify( entry ) + '\n' );
} catch {
return;
}
},
};
}
121 changes: 121 additions & 0 deletions apps/cli/lib/tests/debug-log.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,121 @@
import { appendFileSync } from 'fs';
import { tmpdir } from 'os';
import { join } from 'path';
import { beforeEach, describe, expect, it, vi } from 'vitest';

vi.mock( 'fs', () => {
const mockedModule = {
appendFileSync: vi.fn(),
};
return {
...mockedModule,
default: mockedModule,
};
} );

vi.mock( 'os', () => {
const mockedModule = {
tmpdir: vi.fn(),
};
return {
...mockedModule,
default: mockedModule,
};
} );

describe( 'createDebugLogger', () => {
const originalEnv = { ...process.env };

beforeEach( () => {
vi.resetModules();
vi.clearAllMocks();
process.env = { ...originalEnv };
vi.mocked( tmpdir ).mockReturnValue( '/tmp/studio-tests' );
vi.useFakeTimers();
vi.setSystemTime( new Date( '2026-03-12T10:00:00.000Z' ) );
} );

afterEach( () => {
process.env = { ...originalEnv };
vi.useRealTimers();
} );

it( 'uses the default tmpdir path and skips writes when disabled', async () => {
delete process.env.STUDIO_AI_DEBUG;

const { createDebugLogger } = await import( '../debug-log' );
const logger = createDebugLogger( {
enabledEnvVar: 'STUDIO_AI_DEBUG',
defaultFilename: 'studio-ai-debug.log',
scope: 'todo-rendering',
} );

logger.log( 'todo_tool_use_received', { input: 'test' } );

expect( logger.enabled ).toBe( false );
expect( logger.path ).toBe( join( '/tmp/studio-tests', 'studio-ai-debug.log' ) );
expect( appendFileSync ).not.toHaveBeenCalled();
} );

it( 'writes JSONL entries when enabled', async () => {
process.env.STUDIO_AI_DEBUG = ' TRUE ';

const { createDebugLogger } = await import( '../debug-log' );
const logger = createDebugLogger( {
enabledEnvVar: 'STUDIO_AI_DEBUG',
defaultFilename: 'studio-ai-debug.log',
scope: 'todo-rendering',
} );

logger.log( 'todo_tool_use_received', { input: 'test' } );

expect( logger.enabled ).toBe( true );
expect( appendFileSync ).toHaveBeenCalledWith(
join( '/tmp/studio-tests', 'studio-ai-debug.log' ),
JSON.stringify( {
timestamp: '2026-03-12T10:00:00.000Z',
scope: 'todo-rendering',
event: 'todo_tool_use_received',
payload: { input: 'test' },
} ) + '\n'
);
} );

it( 'uses the derived file override env var by default', async () => {
process.env.STUDIO_AI_DEBUG = '1';
process.env.STUDIO_AI_DEBUG_FILE = '/tmp/custom-debug.log';

const { createDebugLogger } = await import( '../debug-log' );
const logger = createDebugLogger( {
enabledEnvVar: 'STUDIO_AI_DEBUG',
defaultFilename: 'studio-ai-debug.log',
} );

logger.log( 'todo_tool_use_received' );

expect( logger.path ).toBe( '/tmp/custom-debug.log' );
expect( appendFileSync ).toHaveBeenCalledWith(
'/tmp/custom-debug.log',
JSON.stringify( {
timestamp: '2026-03-12T10:00:00.000Z',
scope: undefined,
event: 'todo_tool_use_received',
payload: undefined,
} ) + '\n'
);
} );

it( 'uses an explicit log file env var override when provided', async () => {
process.env.STUDIO_AI_DEBUG = '1';
process.env.STUDIO_AI_LOG_PATH = '/tmp/explicit-debug.log';

const { createDebugLogger } = await import( '../debug-log' );
const logger = createDebugLogger( {
enabledEnvVar: 'STUDIO_AI_DEBUG',
defaultFilename: 'studio-ai-debug.log',
logFileEnvVar: 'STUDIO_AI_LOG_PATH',
} );

expect( logger.path ).toBe( '/tmp/explicit-debug.log' );
} );
} );
2 changes: 1 addition & 1 deletion apps/studio/src/stores/provider-constants-slice.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { createSlice, PayloadAction } from '@reduxjs/toolkit';
import { createSlice } from '@reduxjs/toolkit';
import {
DEFAULT_PHP_VERSION,
DEFAULT_WORDPRESS_VERSION,
Expand Down
Loading