diff --git a/packages/software-factory/scripts/boxel-search.ts b/packages/software-factory/scripts/boxel-search.ts index 86030a0919b..7467cf0364e 100644 --- a/packages/software-factory/scripts/boxel-search.ts +++ b/packages/software-factory/scripts/boxel-search.ts @@ -1,3 +1,6 @@ +// This should be first +import '../src/setup-logger'; + import { fieldPairs, forceArray, @@ -9,6 +12,9 @@ import { type SearchQuery, type SearchSort, } from './lib/boxel'; +import { logger } from '../src/logger'; + +let log = logger('boxel-search'); async function main(): Promise { let args = parseArgs(process.argv.slice(2)); @@ -89,6 +95,6 @@ async function main(): Promise { main().catch((error: unknown) => { let message = error instanceof Error ? (error.stack ?? error.message) : String(error); - console.error(message); + log.error(message); process.exit(1); }); diff --git a/packages/software-factory/scripts/boxel-session.ts b/packages/software-factory/scripts/boxel-session.ts index c9473c8ab61..fc770ddcaac 100644 --- a/packages/software-factory/scripts/boxel-session.ts +++ b/packages/software-factory/scripts/boxel-session.ts @@ -1,3 +1,6 @@ +// This should be first +import '../src/setup-logger'; + import { buildBrowserAuth, buildBrowserSession, @@ -6,6 +9,9 @@ import { parseArgs, printJson, } from './lib/boxel'; +import { logger } from '../src/logger'; + +let log = logger('boxel-session'); async function main(): Promise { let args = parseArgs(process.argv.slice(2)); @@ -30,6 +36,6 @@ async function main(): Promise { main().catch((error: unknown) => { let message = error instanceof Error ? (error.stack ?? error.message) : String(error); - console.error(message); + log.error(message); process.exit(1); }); diff --git a/packages/software-factory/scripts/lib/boxel.ts b/packages/software-factory/scripts/lib/boxel.ts index ae7f0b5470e..02bdac1b9da 100644 --- a/packages/software-factory/scripts/lib/boxel.ts +++ b/packages/software-factory/scripts/lib/boxel.ts @@ -354,5 +354,5 @@ export function fieldPairs( } export function printJson(value: unknown): void { - console.log(JSON.stringify(value, null, 2)); + process.stdout.write(`${JSON.stringify(value, null, 2)}\n`); } diff --git a/packages/software-factory/scripts/lib/darkfactory-schemas.ts b/packages/software-factory/scripts/lib/darkfactory-schemas.ts index 4a7ce59b1d4..72bc637e018 100644 --- a/packages/software-factory/scripts/lib/darkfactory-schemas.ts +++ b/packages/software-factory/scripts/lib/darkfactory-schemas.ts @@ -7,6 +7,7 @@ * are always derived from the actual card definitions. */ +import { logger } from '../../src/logger'; import type { ResolvedCodeRef, LooseSingleCardDocument, @@ -23,6 +24,8 @@ import { // Runtime schema fetching // --------------------------------------------------------------------------- +let log = logger('darkfactory-schemas'); + const GET_CARD_TYPE_SCHEMA_COMMAND = '@cardstack/boxel-host/commands/get-card-type-schema/default'; @@ -68,7 +71,7 @@ export async function fetchCardTypeSchema( ); if (response.status !== 'ready' || !response.result) { - console.warn( + log.warn( `[darkfactory-schemas] Failed to fetch schema for ${cacheKey}: ${response.error ?? response.status}`, ); return undefined; @@ -85,9 +88,7 @@ export async function fetchCardTypeSchema( schemaCache.set(cacheKey, result); return result; } catch { - console.warn( - `[darkfactory-schemas] Failed to parse schema for ${cacheKey}`, - ); + log.warn(`Failed to parse schema for ${cacheKey}`); return undefined; } } diff --git a/packages/software-factory/scripts/lib/factory-implement.ts b/packages/software-factory/scripts/lib/factory-implement.ts index 39f43847508..ea5d1db7717 100644 --- a/packages/software-factory/scripts/lib/factory-implement.ts +++ b/packages/software-factory/scripts/lib/factory-implement.ts @@ -15,6 +15,8 @@ import { resolve } from 'node:path'; +import { logger } from '../../src/logger'; + import type { KnowledgeArticle, ProjectCard, @@ -71,6 +73,8 @@ import type { FactoryBootstrapResult } from '../../src/factory-bootstrap'; // Constants // --------------------------------------------------------------------------- +let log = logger('factory-implement'); + const PACKAGE_ROOT = resolve(__dirname, '../..'); // --------------------------------------------------------------------------- @@ -248,10 +252,10 @@ export async function runFactoryImplement( if (loopResult.outcome === 'tests_passed' || loopResult.outcome === 'done') { try { await updateTicketStatus(targetRealmUrl, ticket.id, 'done', fetchOptions); - console.error('[factory-implement] Updated ticket status to done'); + log.info('Updated ticket status to done'); } catch (error) { - console.warn( - `[factory-implement] Could not update ticket status: ${ + log.warn( + `Could not update ticket status: ${ error instanceof Error ? error.message : String(error) }`, ); @@ -405,9 +409,7 @@ async function fetchCardData( knowledge.push(card); } catch { // Non-fatal: knowledge articles are supplementary - console.warn( - `[factory-implement] Could not fetch knowledge article: ${ka.id}`, - ); + log.warn(`Could not fetch knowledge article: ${ka.id}`); } } @@ -512,9 +514,7 @@ function buildTestRunner( let start = Date.now(); try { - console.error( - `[factory-implement] Running test file(s) for ticket: ${slug}`, - ); + log.info(`Running test file(s) for ticket: ${slug}`); let handle = await executeTestRunFromRealm({ targetRealmUrl, @@ -529,9 +529,7 @@ function buildTestRunner( }); let durationMs = Date.now() - start; - console.error( - `[factory-implement] Test run complete: status=${handle.status} (${durationMs}ms)`, - ); + log.info(`Test run complete: status=${handle.status} (${durationMs}ms)`); if (handle.status === 'passed') { return { @@ -579,8 +577,8 @@ function buildTestRunner( } } catch (error) { let durationMs = Date.now() - start; - console.error( - `[factory-implement] Test execution error: ${ + log.error( + `Test execution error: ${ error instanceof Error ? error.message : String(error) }`, ); @@ -771,8 +769,8 @@ async function loadDarkFactorySchemas( schemas.set(cardName, schema); } } catch (error) { - console.warn( - `[factory-implement] Could not fetch schema for ${cardName}: ${ + log.warn( + `Could not fetch schema for ${cardName}: ${ error instanceof Error ? error.message : String(error) }`, ); @@ -792,8 +790,8 @@ async function loadDarkFactorySchemas( schemas.set(name, schema); } } catch (error) { - console.warn( - `[factory-implement] Could not fetch schema for ${name}: ${ + log.warn( + `Could not fetch schema for ${name}: ${ error instanceof Error ? error.message : String(error) }`, ); diff --git a/packages/software-factory/scripts/lib/factory-skill-loader.ts b/packages/software-factory/scripts/lib/factory-skill-loader.ts index 82d74bdc7b9..f316c300538 100644 --- a/packages/software-factory/scripts/lib/factory-skill-loader.ts +++ b/packages/software-factory/scripts/lib/factory-skill-loader.ts @@ -231,7 +231,7 @@ export class SkillLoader implements SkillLoaderInterface { results.push(skill); } catch (error) { console.warn( - `[SkillLoader] Skipping unavailable skill "${name}": ${ + `Skipping unavailable skill "${name}": ${ error instanceof Error ? error.message : String(error) }`, ); @@ -379,7 +379,7 @@ export function enforceSkillBudget( if (usedTokens + skillTokens > maxTokens) { console.warn( - `[SkillBudget] Dropping skill "${skill.name}" (${skillTokens} tokens) — ` + + `Dropping skill "${skill.name}" (${skillTokens} tokens) — ` + `would exceed budget of ${maxTokens} (used: ${usedTokens})`, ); continue; diff --git a/packages/software-factory/scripts/lib/factory-tool-builder.ts b/packages/software-factory/scripts/lib/factory-tool-builder.ts index a990364100b..fdb9112a794 100644 --- a/packages/software-factory/scripts/lib/factory-tool-builder.ts +++ b/packages/software-factory/scripts/lib/factory-tool-builder.ts @@ -7,6 +7,7 @@ * (realm protection, per-realm JWT auth, logging). */ +import { logger } from '../../src/logger'; import type { LooseSingleCardDocument, Relationship, @@ -31,6 +32,8 @@ import { // Constants // --------------------------------------------------------------------------- +let log = logger('factory-tool-builder'); + // --------------------------------------------------------------------------- // Types // --------------------------------------------------------------------------- @@ -130,7 +133,7 @@ export function buildFactoryTools( if (schemas?.has(cardName)) { tools.push(buildFn()); } else { - console.warn( + log.warn( `[factory-tool-builder] Omitting ${toolName} tool: no schema for ${cardName}`, ); } diff --git a/packages/software-factory/scripts/lib/realm-operations.ts b/packages/software-factory/scripts/lib/realm-operations.ts index 8797475113b..8506588b635 100644 --- a/packages/software-factory/scripts/lib/realm-operations.ts +++ b/packages/software-factory/scripts/lib/realm-operations.ts @@ -8,6 +8,7 @@ import { mkdirSync, writeFileSync } from 'node:fs'; import { dirname, join } from 'node:path'; +import { logger } from '../../src/logger'; import type { LooseSingleCardDocument } from '@cardstack/runtime-common'; import { APP_BOXEL_REALMS_EVENT_TYPE } from '@cardstack/runtime-common/matrix-constants'; import { @@ -19,6 +20,8 @@ import { SupportedMimeType } from '@cardstack/runtime-common/supported-mime-type export { SupportedMimeType }; +let log = logger('realm-operations'); + export function ensureTrailingSlash(url: string): string { return url.endsWith('/') ? url : `${url}/`; } @@ -777,12 +780,12 @@ async function addRealmToMatrixAccountData( body: JSON.stringify({ realms: existingRealms }), }); if (!putResponse.ok) { - console.warn( + log.warn( `Warning: failed to update Matrix account data for realm ${realmUrl}: HTTP ${putResponse.status}`, ); } } catch (err) { - console.warn( + log.warn( `Warning: failed to update Matrix account data for realm ${realmUrl}: ${err instanceof Error ? err.message : String(err)}`, ); } diff --git a/packages/software-factory/scripts/lib/test-run-execution.ts b/packages/software-factory/scripts/lib/test-run-execution.ts index b891c76427f..742112f96a9 100644 --- a/packages/software-factory/scripts/lib/test-run-execution.ts +++ b/packages/software-factory/scripts/lib/test-run-execution.ts @@ -2,6 +2,8 @@ import { createServer, type Server } from 'node:http'; import { readFileSync } from 'node:fs'; import { normalize, resolve } from 'node:path'; +import { logger } from '../../src/logger'; + import { chromium } from '@playwright/test'; import { ensureTrailingSlash, searchRealm } from './realm-operations'; @@ -14,6 +16,8 @@ import type { TestRunRealmOptions, } from './test-run-types'; +let log = logger('test-run-execution'); + // --------------------------------------------------------------------------- // Resume Logic // --------------------------------------------------------------------------- @@ -449,8 +453,8 @@ export async function executeTestRunFromRealm( }); setHtml(html); - console.error( - `[test-run-execution] Serving QUnit page at ${testPageUrl} for realm ${options.targetRealmUrl}`, + log.info( + `Serving QUnit page at ${testPageUrl} for realm ${options.targetRealmUrl}`, ); browser = await chromium.launch({ headless: true }); @@ -459,10 +463,10 @@ export async function executeTestRunFromRealm( // Forward browser console when debug is enabled if (options.debug) { page.on('console', (msg) => { - console.error(`[browser] ${msg.type()}: ${msg.text()}`); + log.debug(`[browser] ${msg.type()}: ${msg.text()}`); }); page.on('pageerror', (err) => { - console.error(`[browser] PAGE ERROR: ${err.message}`); + log.debug(`[browser] PAGE ERROR: ${err.message}`); }); } @@ -497,8 +501,8 @@ export async function executeTestRunFromRealm( ); let durationMs = Date.now() - start; - console.error( - `[test-run-execution] QUnit completed in ${durationMs}ms: ${qunitResults.runEnd?.testCounts?.total ?? 0} test(s)`, + log.info( + `QUnit completed in ${durationMs}ms: ${qunitResults.runEnd?.testCounts?.total ?? 0} test(s)`, ); // Step 3: Parse results and complete the TestRun card. @@ -520,9 +524,7 @@ export async function executeTestRunFromRealm( } catch (err) { let durationMs = Date.now() - start; let errorMessage = err instanceof Error ? err.message : String(err); - console.error( - `[test-run-execution] Error: ${errorMessage} (${durationMs}ms)`, - ); + log.error(`Error: ${errorMessage} (${durationMs}ms)`); try { await completeTestRun( testRunId, diff --git a/packages/software-factory/scripts/pick-ticket.ts b/packages/software-factory/scripts/pick-ticket.ts index 7892b347ca6..b003f6cb706 100644 --- a/packages/software-factory/scripts/pick-ticket.ts +++ b/packages/software-factory/scripts/pick-ticket.ts @@ -1,3 +1,6 @@ +// This should be first +import '../src/setup-logger'; + import { getAccessibleRealmTokens, matrixLogin, @@ -7,6 +10,9 @@ import { type SearchResultCard, type SearchSort, } from './lib/boxel'; +import { logger } from '../src/logger'; + +let log = logger('pick-ticket'); type TicketStatus = 'backlog' | 'in_progress' | 'blocked' | 'review' | 'done'; type TicketPriority = 'critical' | 'high' | 'medium' | 'low'; @@ -160,6 +166,6 @@ async function main(): Promise { main().catch((error: unknown) => { let message = error instanceof Error ? (error.stack ?? error.message) : String(error); - console.error(message); + log.error(message); process.exit(1); }); diff --git a/packages/software-factory/scripts/run-realm-tests.ts b/packages/software-factory/scripts/run-realm-tests.ts index 8493a2fb3d2..9fdc716d4d9 100644 --- a/packages/software-factory/scripts/run-realm-tests.ts +++ b/packages/software-factory/scripts/run-realm-tests.ts @@ -1,3 +1,6 @@ +// This should be first +import '../src/setup-logger'; + import { spawnSync, type SpawnSyncOptionsWithStringEncoding, @@ -310,7 +313,7 @@ let summary = { failures, }; -console.log(JSON.stringify(summary, null, 2)); +process.stdout.write(`${JSON.stringify(summary, null, 2)}\n`); if (testRun.status !== 0) { process.exit(testRun.status ?? 1); diff --git a/packages/software-factory/scripts/smoke-tests/factory-agent-smoke.ts b/packages/software-factory/scripts/smoke-tests/factory-agent-smoke.ts index 92edfd83ac7..c381842d2e9 100644 --- a/packages/software-factory/scripts/smoke-tests/factory-agent-smoke.ts +++ b/packages/software-factory/scripts/smoke-tests/factory-agent-smoke.ts @@ -47,7 +47,11 @@ * Falls back to FACTORY_DEFAULT_MODEL (anthropic/claude-sonnet-4) */ +// This should be first +import '../../src/setup-logger'; + import { parseArgs } from 'node:util'; +import { logger } from '../../src/logger'; import { OpenRouterFactoryAgent, @@ -55,6 +59,8 @@ import { type AgentContext, } from '../lib/factory-agent'; +let log = logger('factory-agent-smoke'); + async function main(): Promise { let argv = process.argv.slice(2); // Strip leading '--' that pnpm/ts-node passes through @@ -73,7 +79,7 @@ async function main(): Promise { let realmServerUrl = values['realm-server-url']; if (!realmServerUrl) { - console.error( + log.error( 'Usage: pnpm smoke:agent --realm-server-url [--model ]', ); process.exit(1); @@ -85,10 +91,8 @@ async function main(): Promise { } let model = resolveFactoryModel(values.model); - console.log(`Model: ${model}`); - console.log(`Realm server: ${realmServerUrl}`); - console.log(); - + log.info(`Model: ${model}`); + log.info(`Realm server: ${realmServerUrl}`); let agent = new OpenRouterFactoryAgent({ model, realmServerUrl, @@ -113,18 +117,16 @@ async function main(): Promise { testRealmUrl: 'https://example.test/user/target-tests/', }; - console.log('Sending plan() request...'); - console.log(); + log.info('Sending plan() request...'); let actions = await agent.plan(context); - console.log(`Received ${actions.length} action(s):`); - console.log(JSON.stringify(actions, null, 2)); - console.log(); - console.log('Smoke test passed.'); + log.info(`Received ${actions.length} action(s):`); + log.info(JSON.stringify(actions, null, 2)); + log.info('Smoke test passed.'); } main().catch((err) => { - console.error('Smoke test failed:', err); + log.error('Smoke test failed:', err); process.exit(1); }); diff --git a/packages/software-factory/scripts/smoke-tests/factory-context-smoke.ts b/packages/software-factory/scripts/smoke-tests/factory-context-smoke.ts index 963ee61a305..fffa74c03f4 100644 --- a/packages/software-factory/scripts/smoke-tests/factory-context-smoke.ts +++ b/packages/software-factory/scripts/smoke-tests/factory-context-smoke.ts @@ -9,7 +9,11 @@ * pnpm smoke:context --max-tokens 8000 */ +// This should be first +import '../../src/setup-logger'; + import { parseArgs } from 'node:util'; +import { logger } from '../../src/logger'; import type { KnowledgeArticle, @@ -27,16 +31,18 @@ import { // Helpers // --------------------------------------------------------------------------- +let log = logger('factory-context-smoke'); + let passed = 0; let failed = 0; function check(label: string, ok: boolean, detail?: string): void { if (ok) { passed++; - console.log(` \u2713 ${label}`); + log.info(` \u2713 ${label}`); } else { failed++; - console.log(` \u2717 ${label}${detail ? ` -- ${detail}` : ''}`); + log.info(` \u2717 ${label}${detail ? ` -- ${detail}` : ''}`); } } @@ -115,7 +121,7 @@ async function main(): Promise { !Number.isFinite(maxSkillTokens) || maxSkillTokens <= 0) ) { - console.error( + log.error( `Invalid value for --max-tokens: "${values['max-tokens']}". ` + 'Please provide a positive numeric value.', ); @@ -128,14 +134,14 @@ async function main(): Promise { maxSkillTokens, }); - console.log(''); - console.log('=== Context Builder Smoke Test ==='); - console.log(''); + log.info(''); + log.info('=== Context Builder Smoke Test ==='); + log.info(''); for (let { label, ticket } of SAMPLE_TICKETS) { - console.log(`--- ${label} ---`); - console.log(` Ticket: ${ticket.title}`); - console.log(''); + log.info(`--- ${label} ---`); + log.info(` Ticket: ${ticket.title}`); + log.info(''); // ------------------------------------------------------------------- // First pass (no test results) @@ -149,7 +155,7 @@ async function main(): Promise { testRealmUrl: 'https://example.test/user/target-test-artifacts/', }); - console.log(' First pass (no test results):'); + log.info(' First pass (no test results):'); check('project.id set', ctx.project.id === SAMPLE_PROJECT.id); check('ticket.id set', ctx.ticket.id === ticket.id); check( @@ -172,12 +178,12 @@ async function main(): Promise { ); let totalTokens = ctx.skills.reduce((s, sk) => s + estimateTokens(sk), 0); - console.log(` Skill breakdown (~${totalTokens} total tokens):`); + log.info(` Skill breakdown (~${totalTokens} total tokens):`); for (let skill of ctx.skills) { let tokens = estimateTokens(skill); let refCount = skill.references?.length ?? 0; let refNote = refCount > 0 ? ` + ${refCount} ref(s)` : ''; - console.log(` - ${skill.name}: ~${tokens} tokens${refNote}`); + log.info(` - ${skill.name}: ~${tokens} tokens${refNote}`); } // ------------------------------------------------------------------- @@ -205,8 +211,8 @@ async function main(): Promise { }, }); - console.log(''); - console.log(' Iteration pass (with failed test results):'); + log.info(''); + log.info(' Iteration pass (with failed test results):'); check( 'testResults.status = failed', ctxWithResults.testResults?.status === 'failed', @@ -232,7 +238,7 @@ async function main(): Promise { ctxWithResults.iteration === undefined, ); - console.log(''); + log.info(''); } // ----------------------------------------------------------------------- @@ -240,8 +246,8 @@ async function main(): Promise { // ----------------------------------------------------------------------- if (maxSkillTokens) { - console.log(`--- Budget enforcement (${maxSkillTokens} tokens) ---`); - console.log(''); + log.info(`--- Budget enforcement (${maxSkillTokens} tokens) ---`); + log.info(''); let ctx = await builder.build({ project: SAMPLE_PROJECT, @@ -257,19 +263,19 @@ async function main(): Promise { totalTokens <= maxSkillTokens, ); for (let skill of ctx.skills) { - console.log(` - ${skill.name}: ~${estimateTokens(skill)} tokens`); + log.info(` - ${skill.name}: ~${estimateTokens(skill)} tokens`); } - console.log(''); + log.info(''); } // ----------------------------------------------------------------------- // Summary // ----------------------------------------------------------------------- - console.log('==========================='); - console.log(` ${passed} passed, ${failed} failed`); - console.log('==========================='); - console.log(''); + log.info('==========================='); + log.info(` ${passed} passed, ${failed} failed`); + log.info('==========================='); + log.info(''); if (failed > 0) { process.exit(1); @@ -277,7 +283,7 @@ async function main(): Promise { } main().catch((err: unknown) => { - console.error( + log.error( 'Smoke test failed:', err instanceof Error ? err.message : String(err), ); diff --git a/packages/software-factory/scripts/smoke-tests/factory-loop-smoke.ts b/packages/software-factory/scripts/smoke-tests/factory-loop-smoke.ts index d62810d9b5f..942f4ad73b8 100644 --- a/packages/software-factory/scripts/smoke-tests/factory-loop-smoke.ts +++ b/packages/software-factory/scripts/smoke-tests/factory-loop-smoke.ts @@ -9,7 +9,11 @@ * pnpm smoke:loop --max-iterations 3 */ +// This should be first +import '../../src/setup-logger'; + import { parseArgs } from 'node:util'; +import { logger } from '../../src/logger'; import type { AgentContext, @@ -36,16 +40,18 @@ import { // Helpers // --------------------------------------------------------------------------- +let log = logger('factory-loop-smoke'); + let passed = 0; let failed = 0; function check(label: string, ok: boolean, detail?: string): void { if (ok) { passed++; - console.log(` \u2713 ${label}`); + log.info(` \u2713 ${label}`); } else { failed++; - console.log(` \u2717 ${label}${detail ? ` -- ${detail}` : ''}`); + log.info(` \u2717 ${label}${detail ? ` -- ${detail}` : ''}`); } } @@ -195,16 +201,16 @@ function makeBaseConfig( } function printResult(result: FactoryLoopResult): void { - console.log(` outcome: ${result.outcome}`); - console.log(` iterations: ${result.iterations}`); - console.log(` tool calls: ${result.toolCallLog.length}`); + log.info(` outcome: ${result.outcome}`); + log.info(` iterations: ${result.iterations}`); + log.info(` tool calls: ${result.toolCallLog.length}`); if (result.testResults) { - console.log( + log.info( ` tests: ${result.testResults.passedCount} passed, ${result.testResults.failedCount} failed`, ); } if (result.message) { - console.log(` message: ${result.message}`); + log.info(` message: ${result.message}`); } } @@ -213,8 +219,8 @@ function printResult(result: FactoryLoopResult): void { // --------------------------------------------------------------------------- async function scenarioHappyPath(maxIterations: number): Promise { - console.log('--- Scenario 1: Happy path (implement + test pass) ---'); - console.log(''); + log.info('--- Scenario 1: Happy path (implement + test pass) ---'); + log.info(''); let agent = new MockLoopAgent([ { @@ -264,12 +270,12 @@ async function scenarioHappyPath(maxIterations: number): Promise { check('completed in 1 iteration', result.iterations === 1); check('4 tool calls logged', result.toolCallLog.length === 4); check('tests passed', result.testResults?.status === 'passed'); - console.log(''); + log.info(''); } async function scenarioIterationPath(maxIterations: number): Promise { - console.log('--- Scenario 2: Iteration path (fail then fix) ---'); - console.log(''); + log.info('--- Scenario 2: Iteration path (fail then fix) ---'); + log.info(''); let agent = new MockLoopAgent([ { @@ -326,12 +332,12 @@ async function scenarioIterationPath(maxIterations: number): Promise { 'context threading: second call had failing testResults', agent.receivedContexts[1].testResults?.status === 'failed', ); - console.log(''); + log.info(''); } async function scenarioMaxIterations(maxIterations: number): Promise { - console.log(`--- Scenario 3: Max iterations (${maxIterations} failures) ---`); - console.log(''); + log.info(`--- Scenario 3: Max iterations (${maxIterations} failures) ---`); + log.info(''); let configs: MockRunConfig[] = []; let testResults: TestResult[] = []; @@ -369,12 +375,12 @@ async function scenarioMaxIterations(maxIterations: number): Promise { result.iterations === maxIterations, ); check('last test result was failed', result.testResults?.status === 'failed'); - console.log(''); + log.info(''); } async function scenarioDoneSignal(): Promise { - console.log('--- Scenario 4: Bare done signal (no work) ---'); - console.log(''); + log.info('--- Scenario 4: Bare done signal (no work) ---'); + log.info(''); let result = await runFactoryLoop( makeBaseConfig({ @@ -387,12 +393,12 @@ async function scenarioDoneSignal(): Promise { check('1 iteration', result.iterations === 1); check('no tool calls', result.toolCallLog.length === 0); check('no test results', result.testResults === undefined); - console.log(''); + log.info(''); } async function scenarioClarification(): Promise { - console.log('--- Scenario 5: Clarification needed (blocked) ---'); - console.log(''); + log.info('--- Scenario 5: Clarification needed (blocked) ---'); + log.info(''); let result = await runFactoryLoop( makeBaseConfig({ @@ -413,12 +419,12 @@ async function scenarioClarification(): Promise { ); check('message preserved', result.message?.includes('color scheme') ?? false); check('tool calls before block recorded', result.toolCallLog.length === 1); - console.log(''); + log.info(''); } async function scenarioToolOnlyRound(): Promise { - console.log('--- Scenario 6: Tool-only round (read then write) ---'); - console.log(''); + log.info('--- Scenario 6: Tool-only round (read then write) ---'); + log.info(''); let agent = new MockLoopAgent([ { @@ -465,7 +471,7 @@ async function scenarioToolOnlyRound(): Promise { 'all 4 tool calls across both rounds logged', result.toolCallLog.length === 4, ); - console.log(''); + log.info(''); } // --------------------------------------------------------------------------- @@ -487,17 +493,17 @@ async function main(): Promise { : 5; if (!Number.isFinite(maxIterations) || maxIterations <= 0) { - console.error( + log.error( `Invalid --max-iterations: "${values['max-iterations']}". Must be a positive number.`, ); process.exit(1); } - console.log(''); - console.log('=== Factory Loop Smoke Test ==='); - console.log(''); - console.log(`maxIterations: ${maxIterations}`); - console.log(''); + log.info(''); + log.info('=== Factory Loop Smoke Test ==='); + log.info(''); + log.info(`maxIterations: ${maxIterations}`); + log.info(''); await scenarioHappyPath(maxIterations); await scenarioIterationPath(maxIterations); @@ -506,10 +512,10 @@ async function main(): Promise { await scenarioClarification(); await scenarioToolOnlyRound(); - console.log('==========================='); - console.log(` ${passed} passed, ${failed} failed`); - console.log('==========================='); - console.log(''); + log.info('==========================='); + log.info(` ${passed} passed, ${failed} failed`); + log.info('==========================='); + log.info(''); if (failed > 0) { process.exit(1); @@ -517,7 +523,7 @@ async function main(): Promise { } main().catch((err: unknown) => { - console.error( + log.error( 'Smoke test failed:', err instanceof Error ? err.message : String(err), ); diff --git a/packages/software-factory/scripts/smoke-tests/factory-prompt-smoke.ts b/packages/software-factory/scripts/smoke-tests/factory-prompt-smoke.ts index d81a41b8283..e16db7520e6 100644 --- a/packages/software-factory/scripts/smoke-tests/factory-prompt-smoke.ts +++ b/packages/software-factory/scripts/smoke-tests/factory-prompt-smoke.ts @@ -12,7 +12,11 @@ * pnpm smoke:prompt -- --stage all (default) */ +// This should be first +import '../../src/setup-logger'; + import { parseArgs } from 'node:util'; +import { logger } from '../../src/logger'; import type { AgentAction, AgentContext } from '../lib/factory-agent'; @@ -136,18 +140,19 @@ const SAMPLE_PREVIOUS_ACTIONS: AgentAction[] = [ // Helpers // --------------------------------------------------------------------------- +let log = logger('factory-prompt-smoke'); + function separator(label: string): void { let line = '═'.repeat(72); - console.log(`\n${line}`); - console.log(` ${label}`); - console.log(`${line}\n`); + log.info(`\n${line}`); + log.info(` ${label}`); + log.info(`${line}\n`); } function printMessages(messages: { role: string; content: string }[]): void { for (let msg of messages) { - console.log(`── [${msg.role.toUpperCase()}] ──────────────────────────`); - console.log(msg.content); - console.log(); + log.info(`── [${msg.role.toUpperCase()}] ──────────────────────────`); + log.info(msg.content); } } @@ -169,7 +174,7 @@ function main(): void { let stage = (values.stage ?? 'all') as Stage; if (!['all', 'implement', 'iterate', 'test'].includes(stage)) { - console.error( + log.error( `Invalid stage: "${stage}". Must be one of: all, implement, iterate, test`, ); process.exit(1); @@ -186,7 +191,7 @@ function main(): void { let userPrompt = assembleImplementPrompt({ context: ctx, loader }); let messages = buildOneShotMessages(systemPrompt, userPrompt); printMessages(messages); - console.log( + log.info( `📊 System: ${systemPrompt.length} chars | User: ${userPrompt.length} chars`, ); } @@ -221,7 +226,7 @@ function main(): void { }); let messages = buildOneShotMessages(systemPrompt, userPrompt); printMessages(messages); - console.log( + log.info( `📊 System: ${systemPrompt.length} chars | User: ${userPrompt.length} chars`, ); } @@ -242,14 +247,14 @@ function main(): void { }); let messages = buildOneShotMessages(systemPrompt, userPrompt); printMessages(messages); - console.log( + log.info( `📊 System: ${systemPrompt.length} chars | User: ${userPrompt.length} chars`, ); } separator('DONE'); - console.log('All prompt templates assembled successfully.'); - console.log( + log.info('All prompt templates assembled successfully.'); + log.info( 'The prompts above are exactly what the LLM would receive in a one-shot call.', ); } diff --git a/packages/software-factory/scripts/smoke-tests/factory-skill-smoke.ts b/packages/software-factory/scripts/smoke-tests/factory-skill-smoke.ts index bd58a9c30be..e9238da5048 100644 --- a/packages/software-factory/scripts/smoke-tests/factory-skill-smoke.ts +++ b/packages/software-factory/scripts/smoke-tests/factory-skill-smoke.ts @@ -9,7 +9,11 @@ * pnpm smoke:skill --ticket-text "Create a .gts component with styling" */ +// This should be first +import '../../src/setup-logger'; + import { parseArgs } from 'node:util'; +import { logger } from '../../src/logger'; import { DefaultSkillResolver, @@ -58,6 +62,8 @@ const SAMPLE_TICKETS: { label: string; ticket: TicketCard }[] = [ }, ]; +let log = logger('factory-skill-smoke'); + async function main(): Promise { let { values } = parseArgs({ args: process.argv.slice(2), @@ -77,7 +83,7 @@ async function main(): Promise { values['max-tokens'] !== undefined && (maxTokens === undefined || !Number.isFinite(maxTokens) || maxTokens <= 0) ) { - console.error( + log.error( `Invalid value for --max-tokens: "${values['max-tokens']}". ` + 'Please provide a positive numeric value.', ); @@ -90,7 +96,7 @@ async function main(): Promise { let loader = new SkillLoader(); let project: ProjectCard = { id: 'Projects/smoke-test' }; - console.log('=== Skill Loader & Resolver Smoke Test ===\n'); + log.info('=== Skill Loader & Resolver Smoke Test ===\n'); // If custom ticket text is provided, use only that let tickets = customTicketText @@ -107,22 +113,22 @@ async function main(): Promise { : SAMPLE_TICKETS; for (let { label, ticket } of tickets) { - console.log(`--- ${label} ---`); - console.log(` Ticket: ${ticket.title}`); + log.info(`--- ${label} ---`); + log.info(` Ticket: ${ticket.title}`); // 1. Resolve let skillNames = resolver.resolve(ticket, project); - console.log(` Resolved skills: [${skillNames.join(', ')}]`); + log.info(` Resolved skills: [${skillNames.join(', ')}]`); // 2. Load (with ticket context for reference filtering) let skills = await loader.loadAll(skillNames, ticket); - console.log(` Loaded: ${skills.length}/${skillNames.length} skills`); + log.info(` Loaded: ${skills.length}/${skillNames.length} skills`); for (let skill of skills) { let tokens = estimateTokens(skill); let refCount = skill.references?.length ?? 0; let refNote = refCount > 0 ? ` + ${refCount} reference(s)` : ''; - console.log(` - ${skill.name}: ~${tokens} tokens${refNote}`); + log.info(` - ${skill.name}: ~${tokens} tokens${refNote}`); } // 3. Budget enforcement @@ -130,7 +136,7 @@ async function main(): Promise { let budgeted = enforceSkillBudget(skills, maxTokens); let totalBefore = skills.reduce((s, sk) => s + estimateTokens(sk), 0); let totalAfter = budgeted.reduce((s, sk) => s + estimateTokens(sk), 0); - console.log( + log.info( ` Budget (${maxTokens} tokens): ` + `${budgeted.length}/${skills.length} skills kept ` + `(${totalAfter}/${totalBefore} tokens)`, @@ -139,15 +145,13 @@ async function main(): Promise { let dropped = skills.filter( (s) => !budgeted.find((b) => b.name === s.name), ); - console.log(` Dropped: [${dropped.map((d) => d.name).join(', ')}]`); + log.info(` Dropped: [${dropped.map((d) => d.name).join(', ')}]`); } } - - console.log(); } // Summary: list all discoverable skills - console.log('--- All discoverable skills ---'); + log.info('--- All discoverable skills ---'); let allSkillNames = [ 'boxel-development', 'boxel-file-structure', @@ -168,22 +172,20 @@ async function main(): Promise { grandTotal += tokens; let refCount = skill.references?.length ?? 0; let refNote = refCount > 0 ? ` (${refCount} refs)` : ''; - console.log(` ${skill.name}: ~${tokens} tokens${refNote}`); + log.info(` ${skill.name}: ~${tokens} tokens${refNote}`); } let missing = allSkillNames.filter( (n) => !allSkills.find((s) => s.name === n), ); if (missing.length > 0) { - console.log(` Not found: [${missing.join(', ')}]`); + log.info(` Not found: [${missing.join(', ')}]`); } - console.log( - ` Total: ~${grandTotal} tokens across ${allSkills.length} skills`, - ); + log.info(` Total: ~${grandTotal} tokens across ${allSkills.length} skills`); - console.log('\nSmoke test passed.'); + log.info('\nSmoke test passed.'); } main().catch((err) => { - console.error('Smoke test failed:', err); + log.error('Smoke test failed:', err); process.exit(1); }); diff --git a/packages/software-factory/scripts/smoke-tests/factory-tools-smoke.ts b/packages/software-factory/scripts/smoke-tests/factory-tools-smoke.ts index e56a50cf70f..d7c813ff67a 100644 --- a/packages/software-factory/scripts/smoke-tests/factory-tools-smoke.ts +++ b/packages/software-factory/scripts/smoke-tests/factory-tools-smoke.ts @@ -9,7 +9,11 @@ * pnpm smoke:tools */ +// This should be first +import '../../src/setup-logger'; + import { SupportedMimeType } from '@cardstack/runtime-common/supported-mime-type'; +import { logger } from '../../src/logger'; import { ToolExecutor, @@ -29,16 +33,18 @@ import { ToolRegistry } from '../lib/factory-tool-registry'; // Helpers // --------------------------------------------------------------------------- +let log = logger('factory-tools-smoke'); + let passed = 0; let failed = 0; function check(label: string, ok: boolean, detail?: string): void { if (ok) { passed++; - console.log(` \u2713 ${label}`); + log.info(` \u2713 ${label}`); } else { failed++; - console.log(` \u2717 ${label}${detail ? ` -- ${detail}` : ''}`); + log.info(` \u2717 ${label}${detail ? ` -- ${detail}` : ''}`); } } @@ -51,15 +57,15 @@ async function main(): Promise { // 1. Registry // ----------------------------------------------------------------------- - console.log(''); - console.log('=== Tool Registry ==='); - console.log(''); + log.info(''); + log.info('=== Tool Registry ==='); + log.info(''); let registry = new ToolRegistry(); let manifests = registry.getManifests(); - console.log(`Registered tools: ${manifests.length}`); - console.log(''); + log.info(`Registered tools: ${manifests.length}`); + log.info(''); let byCategory: Record = {}; for (let m of manifests) { @@ -67,7 +73,7 @@ async function main(): Promise { } for (let [category, names] of Object.entries(byCategory)) { - console.log(` ${category} (${names.length}):`); + log.info(` ${category} (${names.length}):`); for (let name of names) { let manifest = registry.getManifest(name)!; let requiredArgs = manifest.args @@ -76,7 +82,7 @@ async function main(): Promise { let optionalArgs = manifest.args .filter((a) => !a.required) .map((a) => a.name); - console.log( + log.info( ` - ${name} [${manifest.outputFormat}]` + (requiredArgs.length ? ` required: ${requiredArgs.join(', ')}` @@ -84,7 +90,7 @@ async function main(): Promise { (optionalArgs.length ? ` optional: ${optionalArgs.join(', ')}` : ''), ); } - console.log(''); + log.info(''); } check('has script tools', byCategory['script']?.length === 4); @@ -99,9 +105,9 @@ async function main(): Promise { // 2. Argument validation // ----------------------------------------------------------------------- - console.log(''); - console.log('=== Argument Validation ==='); - console.log(''); + log.info(''); + log.info('=== Argument Validation ==='); + log.info(''); let validErrors = registry.validateArgs('search-realm', { realm: 'http://example.test/', @@ -124,9 +130,9 @@ async function main(): Promise { // 3. Safety constraints // ----------------------------------------------------------------------- - console.log(''); - console.log('=== Safety Constraints ==='); - console.log(''); + log.info(''); + log.info('=== Safety Constraints ==='); + log.info(''); let executor = new ToolExecutor(registry, { packageRoot: process.cwd(), @@ -169,9 +175,9 @@ async function main(): Promise { // 4. Mocked realm-api round-trip // ----------------------------------------------------------------------- - console.log(''); - console.log('=== Realm API Round-Trip (mock) ==='); - console.log(''); + log.info(''); + log.info('=== Realm API Round-Trip (mock) ==='); + log.info(''); let mockCallCount = 0; @@ -183,7 +189,7 @@ async function main(): Promise { mockCallCount++; let url = String(input); let method = init?.method ?? 'GET'; - console.log(` -> ${method} ${url}`); + log.info(` -> ${method} ${url}`); return new Response( JSON.stringify({ data: [{ id: 'CardDef/hello', type: 'card' }], @@ -223,9 +229,9 @@ async function main(): Promise { // 5. FactoryTool builder // ----------------------------------------------------------------------- - console.log(''); - console.log('=== Factory Tool Builder ==='); - console.log(''); + log.info(''); + log.info('=== Factory Tool Builder ==='); + log.info(''); let toolBuilderFetchCount = 0; let toolBuilderFetch = (async ( @@ -235,7 +241,7 @@ async function main(): Promise { toolBuilderFetchCount++; let url = String(input); let method = init?.method ?? 'GET'; - console.log(` -> ${method} ${url}`); + log.info(` -> ${method} ${url}`); return new Response(JSON.stringify({ ok: true }), { status: 200, headers: { 'Content-Type': SupportedMimeType.JSON }, @@ -264,14 +270,14 @@ async function main(): Promise { ); let toolNames = factoryTools.map((t) => t.name); - console.log(` Built ${factoryTools.length} tools:`); - console.log( + log.info(` Built ${factoryTools.length} tools:`); + log.info( ` factory: ${toolNames.filter((n) => ['write_file', 'read_file', 'search_realm', 'run_command', 'signal_done', 'request_clarification', 'update_project', 'update_ticket', 'create_knowledge'].includes(n)).join(', ')}`, ); - console.log( + log.info( ` registered: ${toolNames.filter((n) => !['write_file', 'read_file', 'search_realm', 'run_command', 'signal_done', 'request_clarification', 'update_project', 'update_ticket', 'create_knowledge'].includes(n)).join(', ')}`, ); - console.log(''); + log.info(''); check('has write_file tool', toolNames.includes('write_file')); check('has read_file tool', toolNames.includes('read_file')); @@ -340,11 +346,11 @@ async function main(): Promise { // Summary // ----------------------------------------------------------------------- - console.log(''); - console.log('==========================='); - console.log(` ${passed} passed, ${failed} failed`); - console.log('==========================='); - console.log(''); + log.info(''); + log.info('==========================='); + log.info(` ${passed} passed, ${failed} failed`); + log.info('==========================='); + log.info(''); if (failed > 0) { process.exit(1); @@ -352,7 +358,7 @@ async function main(): Promise { } main().catch((err: unknown) => { - console.error( + log.error( 'Smoke test failed:', err instanceof Error ? err.message : String(err), ); diff --git a/packages/software-factory/scripts/test.ts b/packages/software-factory/scripts/test.ts index f6692c84ede..d4b44fd25f9 100644 --- a/packages/software-factory/scripts/test.ts +++ b/packages/software-factory/scripts/test.ts @@ -1,5 +1,10 @@ import { spawn } from 'node:child_process'; import { resolve } from 'node:path'; +import { configureLogger, logger } from '../src/logger'; + +configureLogger(process.env.LOG_LEVELS || '*=error'); + +let log = logger('test'); const packageRoot = resolve(__dirname, '..'); @@ -9,7 +14,7 @@ type TestRunnerOptions = { headed: boolean; }; -const defaultNodeTestLogLevels = '*=info,prerenderer-chrome=none'; +const defaultNodeTestLogLevels = '*=error'; function parseArgs(argv: string[]): TestRunnerOptions { let options: TestRunnerOptions = { @@ -89,6 +94,6 @@ async function main(): Promise { main().catch((error: unknown) => { let message = error instanceof Error ? error.message : String(error); - console.error(message); + log.error(message); process.exit(1); }); diff --git a/packages/software-factory/src/cli/cache-realm.ts b/packages/software-factory/src/cli/cache-realm.ts index a290d3b6283..913450adf55 100644 --- a/packages/software-factory/src/cli/cache-realm.ts +++ b/packages/software-factory/src/cli/cache-realm.ts @@ -1,3 +1,6 @@ +// This should be first +import '../setup-logger'; + import { mkdirSync, writeFileSync } from 'node:fs'; import { basename, dirname, resolve } from 'node:path'; @@ -7,6 +10,9 @@ import { } from '../harness'; import { isFactorySupportContext } from '../harness/shared'; import { readSupportContext } from '../runtime-metadata'; +import { logger } from '../logger'; + +let log = logger('cache-realm'); async function main(): Promise { let args = process.argv.slice(2).filter((arg) => !arg.startsWith('--')); @@ -38,13 +44,13 @@ async function main(): Promise { try { let response = await fetch(hostURL); if (!response.ok) { - console.warn( + log.warn( `Stale support context: hostURL ${hostURL} returned ${response.status}, ignoring cached context`, ); supportContext = undefined; } } catch { - console.warn( + log.warn( `Stale support context: hostURL ${hostURL} is not reachable, ignoring cached context`, ); supportContext = undefined; @@ -60,13 +66,13 @@ async function main(): Promise { try { let response = await fetch(`${matrixURL}/_matrix/client/versions`); if (!response.ok) { - console.warn( + log.warn( `Stale support context: matrixURL ${matrixURL} returned ${response.status}, ignoring cached context`, ); supportContext = undefined; } } catch { - console.warn( + log.warn( `Stale support context: matrixURL ${matrixURL} is not reachable, ignoring cached context`, ); supportContext = undefined; @@ -147,10 +153,10 @@ async function main(): Promise { JSON.stringify(payload, null, 2), ); } - console.log(JSON.stringify(payload, null, 2)); + process.stdout.write(JSON.stringify(payload, null, 2) + '\n'); } main().catch((error: unknown) => { - console.error(error); + log.error(String(error)); process.exitCode = 1; }); diff --git a/packages/software-factory/src/cli/factory-entrypoint.ts b/packages/software-factory/src/cli/factory-entrypoint.ts index 0bf0291cabb..9063a0b02d6 100644 --- a/packages/software-factory/src/cli/factory-entrypoint.ts +++ b/packages/software-factory/src/cli/factory-entrypoint.ts @@ -1,3 +1,6 @@ +// This should be first +import '../setup-logger'; + import { FactoryEntrypointUsageError, getFactoryEntrypointUsage, @@ -6,10 +9,9 @@ import { wantsFactoryEntrypointHelp, } from '../factory-entrypoint'; import { FactoryBriefError } from '../factory-brief'; +import { logger } from '../logger'; -function log(message: string): void { - process.stderr.write(`[factory:go] ${message}\n`); -} +let log = logger('factory-entrypoint'); async function main(): Promise { try { @@ -19,16 +21,16 @@ async function main(): Promise { } let options = parseFactoryEntrypointArgs(process.argv.slice(2)); - log(`mode=${options.mode} brief=${options.briefUrl}`); + log.info(`mode=${options.mode} brief=${options.briefUrl}`); if (options.mode === 'implement') { - log('Starting bootstrap + implement flow...'); + log.info('Starting bootstrap + implement flow...'); } let summary = await runFactoryEntrypoint(options); if (summary.implement) { - log( + log.info( `Implement complete: outcome=${summary.implement.outcome} ` + `iterations=${summary.implement.iterations} ` + `toolCalls=${summary.implement.toolCallCount}`, @@ -43,13 +45,15 @@ async function main(): Promise { } } catch (error) { if (error instanceof FactoryEntrypointUsageError) { - console.error(error.message); - console.error(''); - console.error(getFactoryEntrypointUsage()); + log.error(error.message); + log.error(''); + log.error(getFactoryEntrypointUsage()); } else if (error instanceof FactoryBriefError) { - console.error(error.message); + log.error(error.message); + } else if (error instanceof Error) { + log.error(error.stack ?? error.message); } else { - console.error(error); + log.error(String(error)); } process.exit(1); diff --git a/packages/software-factory/src/cli/serve-realm.ts b/packages/software-factory/src/cli/serve-realm.ts index 803801f90c0..d6f07230e28 100644 --- a/packages/software-factory/src/cli/serve-realm.ts +++ b/packages/software-factory/src/cli/serve-realm.ts @@ -1,4 +1,8 @@ +// This should be first +import '../setup-logger'; + import { readSupportContext } from '../runtime-metadata'; +import { logger } from '../logger'; import { writeFileSync } from 'node:fs'; import { resolve } from 'node:path'; @@ -6,6 +10,8 @@ import type { RealmPermissions } from '@cardstack/runtime-common'; import { startFactoryRealmServer } from '../harness'; +let log = logger('serve-realm'); + async function main(): Promise { let realmDir = resolve( process.cwd(), @@ -57,7 +63,7 @@ async function main(): Promise { ); } - console.log(JSON.stringify(payload, null, 2)); + process.stdout.write(JSON.stringify(payload, null, 2) + '\n'); let cleanExit = false; process.on('exit', () => { @@ -91,6 +97,10 @@ async function main(): Promise { } main().catch((error: unknown) => { - console.error(error); + if (error instanceof Error) { + log.error(error.stack ?? error.message); + } else { + log.error(String(error)); + } process.exitCode = 1; }); diff --git a/packages/software-factory/src/cli/serve-support.ts b/packages/software-factory/src/cli/serve-support.ts index e75522d7dd7..b389afa7435 100644 --- a/packages/software-factory/src/cli/serve-support.ts +++ b/packages/software-factory/src/cli/serve-support.ts @@ -1,8 +1,14 @@ +// This should be first +import '../setup-logger'; + import { mkdirSync } from 'node:fs'; import { resolve } from 'node:path'; import { startFactorySupportServices } from '../harness'; import { sharedRuntimeDir, writeSupportMetadata } from '../runtime-metadata'; +import { logger } from '../logger'; + +let log = logger('serve-support'); async function main(): Promise { let realmDir = resolve( @@ -20,7 +26,7 @@ async function main(): Promise { mkdirSync(sharedRuntimeDir, { recursive: true }); writeSupportMetadata(payload); - console.log(JSON.stringify(payload, null, 2)); + process.stdout.write(`${JSON.stringify(payload, null, 2)}\n`); let stop = async () => { await support.stop(); @@ -40,6 +46,6 @@ async function main(): Promise { } main().catch((error: unknown) => { - console.error(error); + log.error(String(error)); process.exitCode = 1; }); diff --git a/packages/software-factory/src/cli/smoke-realm.ts b/packages/software-factory/src/cli/smoke-realm.ts index 3ccd9f62275..7fce7c2682b 100644 --- a/packages/software-factory/src/cli/smoke-realm.ts +++ b/packages/software-factory/src/cli/smoke-realm.ts @@ -1,7 +1,13 @@ +// This should be first +import '../setup-logger'; + import { resolve } from 'node:path'; import { fetchRealmCardJson } from '../harness'; import { readSupportContext } from '../runtime-metadata'; +import { logger } from '../logger'; + +let log = logger('smoke-realm'); async function main(): Promise { let realmDir = resolve( @@ -18,7 +24,7 @@ async function main(): Promise { } let response = await fetchRealmCardJson(cardPath, { realmDir }); - console.log( + log.info( JSON.stringify( { realmDir, @@ -34,6 +40,6 @@ async function main(): Promise { } main().catch((error: unknown) => { - console.error(error); + log.error(String(error)); process.exit(1); }); diff --git a/packages/software-factory/src/cli/smoke-test-realm.ts b/packages/software-factory/src/cli/smoke-test-realm.ts index f9680888f18..2dc595864b0 100644 --- a/packages/software-factory/src/cli/smoke-test-realm.ts +++ b/packages/software-factory/src/cli/smoke-test-realm.ts @@ -34,11 +34,15 @@ * --target-realm-url */ +// This should be first +import '../setup-logger'; + import { getRealmServerToken, matrixLogin, parseArgs, } from '../../scripts/lib/boxel'; +import { logger } from '../logger'; import { executeTestRunFromRealm } from '../../scripts/lib/test-run-execution'; import { createRealm, @@ -144,6 +148,8 @@ export function runTests() { // Main // --------------------------------------------------------------------------- +let log = logger('smoke-test-realm'); + async function main() { let args = parseArgs(process.argv.slice(2)); let targetRealmUrl = (args['target-realm-url'] as string) ?? ''; @@ -151,14 +157,14 @@ async function main() { if (!targetRealmUrl) { let username = process.env.MATRIX_USERNAME; if (!username) { - console.error('Usage: pnpm smoke:test-realm -- --target-realm-url '); - console.error( + log.error('Usage: pnpm smoke:test-realm -- --target-realm-url '); + log.error( '\nRequires MATRIX_USERNAME and MATRIX_PASSWORD environment variables.', ); process.exit(1); } targetRealmUrl = `http://localhost:4201/${username}/smoke-test-realm/`; - console.log( + log.info( `No --target-realm-url specified, using default: ${targetRealmUrl}\n`, ); } @@ -188,15 +194,15 @@ async function main() { process.env.REALM_SERVER_URL = realmServerUrl; } - console.log('=== Factory Test Realm Smoke Test (QUnit) ===\n'); - console.log(`Target realm: ${targetRealmUrl}`); - console.log(`Realm server: ${realmServerUrl}`); - console.log(`Test results module: ${testResultsModuleUrl}`); + log.info('=== Factory Test Realm Smoke Test (QUnit) ===\n'); + log.info(`Target realm: ${targetRealmUrl}`); + log.info(`Realm server: ${realmServerUrl}`); + log.info(`Test results module: ${testResultsModuleUrl}`); // Authenticate via Matrix to get a realm server JWT for realm creation let matrixAuth = await matrixLogin(); let serverToken = await getRealmServerToken(matrixAuth); - console.log(`Auth: server token obtained\n`); + log.info(`Auth: server token obtained\n`); let fetchImpl = globalThis.fetch; let authorization: string | undefined = serverToken; @@ -205,10 +211,10 @@ async function main() { // Phase 0: Ensure the target realm exists // ------------------------------------------------------------------------- - console.log('--- Phase 0: Ensuring target realm exists ---\n'); + log.info('--- Phase 0: Ensuring target realm exists ---\n'); let realmDisplayName = realmEndpoint.replace(/-/g, ' '); - console.log(` Creating realm: ${realmEndpoint}...`); + log.info(` Creating realm: ${realmEndpoint}...`); let createResult = await createRealm(realmServerUrl, { name: realmDisplayName, endpoint: realmEndpoint, @@ -221,29 +227,27 @@ async function main() { }); if (createResult.created) { - console.log(` Created: ${createResult.realmUrl}\n`); + log.info(` Created: ${createResult.realmUrl}\n`); } else if (createResult.error?.includes('already exists')) { - console.log(` Realm already exists.\n`); + log.info(` Realm already exists.\n`); } else { - console.error(` Failed to create realm: ${createResult.error}`); + log.error(` Failed to create realm: ${createResult.error}`); process.exit(1); } // Get realm-scoped JWT now that the realm exists - console.log(' Authenticating with new realm...'); + log.info(' Authenticating with new realm...'); let realmAuth = await getRealmScopedAuth(realmServerUrl, serverToken); if (realmAuth.error) { - console.warn( - ` Warning: could not get realm-scoped auth: ${realmAuth.error}`, - ); + log.warn(` Warning: could not get realm-scoped auth: ${realmAuth.error}`); } else { // Find the token for our target realm let realmToken = realmAuth.tokens[targetRealmUrl]; if (realmToken) { authorization = realmToken; - console.log(' Realm-scoped JWT obtained.\n'); + log.info(' Realm-scoped JWT obtained.\n'); } else { - console.warn( + log.warn( ` Warning: no token for ${targetRealmUrl} in realm-auth response\n`, ); } @@ -258,52 +262,52 @@ async function main() { // Phase 1: Simulate LLM implementation output // ------------------------------------------------------------------------- - console.log( + log.info( '--- Phase 1: Writing LLM implementation output to target realm ---\n', ); // 1. Card definition - console.log(' Writing hello.gts (HelloCard definition)...'); + log.info(' Writing hello.gts (HelloCard definition)...'); let defResult = await writeFile( targetRealmUrl, 'hello.gts', HELLO_CARD_GTS, fetchOptions, ); - console.log( + log.info( defResult.ok ? ' ✓ hello.gts' : ` ✗ hello.gts: ${defResult.error}`, ); // 2. Spec card instance pointing to the card definition - console.log(' Writing Spec/hello-card.json (Spec card for HelloCard)...'); + log.info(' Writing Spec/hello-card.json (Spec card for HelloCard)...'); let specCardResult = await writeFile( targetRealmUrl, 'Spec/hello-card.json', JSON.stringify(HELLO_SPEC_CARD, null, 2), fetchOptions, ); - console.log( + log.info( specCardResult.ok ? ' ✓ Spec/hello-card.json' : ` ✗ Spec/hello-card.json: ${specCardResult.error}`, ); // 3. QUnit passing test (imports HelloCard from the realm) - console.log(' Writing hello.test.gts (QUnit passing test)...'); + log.info(' Writing hello.test.gts (QUnit passing test)...'); let testResult = await writeFile( targetRealmUrl, 'hello.test.gts', HELLO_TEST_GTS, fetchOptions, ); - console.log( + log.info( testResult.ok ? ' ✓ hello.test.gts' : ` ✗ hello.test.gts: ${testResult.error}`, ); // 4. QUnit deliberately failing test (imports HelloCard from the realm) - console.log( + log.info( ' Writing hello-fail.test.gts (QUnit deliberately failing test)...', ); let failTestResult = await writeFile( @@ -312,7 +316,7 @@ async function main() { HELLO_FAILING_TEST_GTS, fetchOptions, ); - console.log( + log.info( failTestResult.ok ? ' ✓ hello-fail.test.gts' : ` ✗ hello-fail.test.gts: ${failTestResult.error}`, @@ -322,7 +326,7 @@ async function main() { // Phase 2: Run QUnit tests via executeTestRunFromRealm // ------------------------------------------------------------------------- - console.log( + log.info( '\n--- Phase 2: Running QUnit tests via executeTestRunFromRealm ---\n', ); @@ -338,13 +342,13 @@ async function main() { hostAppUrl: realmServerUrl, }); - console.log(` TestRun ID: ${handle.testRunId}`); - console.log(` Status: ${handle.status}`); + log.info(` TestRun ID: ${handle.testRunId}`); + log.info(` Status: ${handle.status}`); if (handle.errorMessage) { - console.log(` Error: ${handle.errorMessage}`); + log.info(` Error: ${handle.errorMessage}`); } if ((handle as unknown as Record).error) { - console.log( + log.info( ` Complete error: ${(handle as unknown as Record).error}`, ); } @@ -353,25 +357,25 @@ async function main() { // Results // ------------------------------------------------------------------------- - console.log('\n--- Results ---\n'); + log.info('\n--- Results ---\n'); // The TestRun should have status 'failed' because it contains both a // passing and a deliberately failing QUnit test. The module results inside // should show one test passed and one test failed. let expectedStatus = handle.status === 'failed'; - console.log( + log.info( ` TestRun status: ${expectedStatus ? '✓ failed (as expected -- one test passes, one fails)' : `✗ expected failed, got ${handle.status}`}`, ); - console.log(`\n View in Boxel: ${targetRealmUrl}${handle.testRunId}`); + log.info(`\n View in Boxel: ${targetRealmUrl}${handle.testRunId}`); if (expectedStatus) { - console.log( + log.info( '\n✓ Smoke test passed! TestRun contains both pass and fail QUnit results.', ); } else { - console.log('\n✗ Smoke test had unexpected results.'); - console.log( + log.info('\n✗ Smoke test had unexpected results.'); + log.info( ` Expected "failed" (mixed pass/fail QUnit tests) but got "${handle.status}"`, ); process.exit(1); @@ -379,6 +383,6 @@ async function main() { } main().catch((err) => { - console.error(err); + log.error(String(err)); process.exit(1); }); diff --git a/packages/software-factory/src/harness/support-services.ts b/packages/software-factory/src/harness/support-services.ts index c4d925ea836..a0319973a78 100644 --- a/packages/software-factory/src/harness/support-services.ts +++ b/packages/software-factory/src/harness/support-services.ts @@ -2,6 +2,8 @@ import { spawn, spawnSync, type ChildProcess } from 'node:child_process'; import { existsSync, readFileSync, rmSync, symlinkSync } from 'node:fs'; import { join } from 'node:path'; +import { logger } from '../logger'; + import { boxelIconsDir, browserPassword, @@ -30,6 +32,7 @@ import { } from './shared'; import { canConnectToPg } from './database'; +let log = logger('support-services'); let preparePgPromise: Promise | undefined; function hostStartupLooksLikePortContention(logs: string): boolean { @@ -411,10 +414,10 @@ export async function startHarnessPrerenderServer(options: { ); child.stdout?.on('data', (data: Buffer) => { - console.log(`prerender: ${data.toString()}`); + log.info(`prerender: ${data.toString()}`); }); child.stderr?.on('data', (data: Buffer) => { - console.error(`prerender: ${data.toString()}`); + log.error(`prerender: ${data.toString()}`); }); let exitPromise = new Promise((_, reject) => {