From ffdd8883265903d4c3227003db1decd17bee9c71 Mon Sep 17 00:00:00 2001 From: Matic Jurglic Date: Thu, 16 Apr 2026 16:56:58 +0200 Subject: [PATCH 01/10] Remove MATRIX_URL/USERNAME/PASSWORD env var auth from software-factory Replace the dual auth path (env vars vs Boxel profile) with profile-only auth. The factory now relies solely on the active Boxel profile configured via `boxel profile add`. This eliminates the confusing requirement to set MATRIX_USERNAME even when a profile is configured. Also makes --realm-server-url fall back to the active profile's realmServerUrl instead of hardcoding localhost:4201, so staging and production environments work without extra flags. Co-Authored-By: Claude Opus 4.6 (1M context) --- packages/software-factory/README.md | 8 +- .../software-factory/docs/phase-1-plan.md | 2 +- .../software-factory/docs/testing-strategy.md | 2 +- .../smoke-test-factory-scenarios.ts | 20 ++--- .../scripts/smoke-tests/smoke-test-realm.ts | 37 ++++---- packages/software-factory/src/boxel.ts | 29 ++----- .../src/factory-entrypoint.ts | 15 ++-- .../src/factory-issue-loop-wiring.ts | 29 +------ .../src/factory-target-realm.ts | 84 ++++--------------- packages/software-factory/src/realm-auth.ts | 42 +--------- .../factory-entrypoint.integration.test.ts | 52 ++++++++++-- .../tests/factory-entrypoint.test.ts | 24 ++++-- .../tests/factory-target-realm.spec.ts | 33 ++++++-- .../tests/factory-target-realm.test.ts | 79 +++++++++-------- .../tests/helpers/browser-auth.ts | 1 - .../tests/helpers/test-profile.ts | 50 +++++++++++ .../software-factory/tests/realm-auth.test.ts | 65 ++------------ 17 files changed, 257 insertions(+), 315 deletions(-) create mode 100644 packages/software-factory/tests/helpers/test-profile.ts diff --git a/packages/software-factory/README.md b/packages/software-factory/README.md index c315ea9bcc8..158e6af3355 100644 --- a/packages/software-factory/README.md +++ b/packages/software-factory/README.md @@ -39,19 +39,17 @@ The orchestrator (`runIssueLoop`) is a thin scheduler that picks the next unbloc - Docker running - `mise run dev-all` (starts realm server, host app, icons server, Postgres, Synapse) -- Matrix credentials (username/password) for realm creation and auth +- Active Boxel CLI profile (`boxel profile add`) - An [OpenRouter API key](https://openrouter.ai/keys) for the LLM agent (when running the full factory) ## Running the Factory Make sure the prerequisites above are met, and that you have a brief card published in the software-factory realm (e.g., `http://localhost:4201/software-factory/Wiki/sticky-note`). -Set up credentials first (these persist in your shell session): +Set up your profile and API key first: ```bash -export MATRIX_URL=http://localhost:8008/ -export MATRIX_USERNAME=your-username -read -s 'MATRIX_PASSWORD?Matrix password: ' && export MATRIX_PASSWORD +boxel profile add # Interactive wizard — choose your environment, enter credentials export OPENROUTER_API_KEY=sk-or-v1-your-key-here ``` diff --git a/packages/software-factory/docs/phase-1-plan.md b/packages/software-factory/docs/phase-1-plan.md index babf239dfb7..b1086757de9 100644 --- a/packages/software-factory/docs/phase-1-plan.md +++ b/packages/software-factory/docs/phase-1-plan.md @@ -127,7 +127,7 @@ Required behavior: Required behavior: -- require `MATRIX_USERNAME` so the target realm owner is explicit before bootstrap starts +- require an active Boxel profile so the target realm owner is explicit before bootstrap starts - infer the target realm server URL from the target realm URL by default, but allow an explicit override when the realm server lives under a subdirectory and the URL shape is ambiguous - create missing target realms through the realm server `/_create-realm` API rather than by creating local directories directly - treat the successful `/_create-realm` response as the readiness boundary diff --git a/packages/software-factory/docs/testing-strategy.md b/packages/software-factory/docs/testing-strategy.md index f82fd633db7..3cae336d8a4 100644 --- a/packages/software-factory/docs/testing-strategy.md +++ b/packages/software-factory/docs/testing-strategy.md @@ -356,7 +356,7 @@ Use: - temporary-directory integration tests - bootstrap tests that cover missing-realm creation through `/_create-realm` - readiness checks that treat a successful `/_create-realm` response as the readiness boundary -- tests that require `MATRIX_USERNAME` instead of an explicit brief JWT flag +- tests that require an active Boxel profile instead of an explicit brief JWT flag ### Project Artifact Bootstrap diff --git a/packages/software-factory/scripts/smoke-tests/smoke-test-factory-scenarios.ts b/packages/software-factory/scripts/smoke-tests/smoke-test-factory-scenarios.ts index f761ddc41f7..601353e8066 100644 --- a/packages/software-factory/scripts/smoke-tests/smoke-test-factory-scenarios.ts +++ b/packages/software-factory/scripts/smoke-tests/smoke-test-factory-scenarios.ts @@ -12,7 +12,7 @@ * * Prerequisites: * - Docker running, `mise run dev-all` (realm server, host app, Synapse, etc.) - * - MATRIX_URL, MATRIX_USERNAME, MATRIX_PASSWORD environment variables + * - Active Boxel profile (`boxel profile add`) * - OPENROUTER_API_KEY for the LLM agent * * Usage: @@ -28,7 +28,7 @@ import '../../src/setup-logger'; import { spawn } from 'node:child_process'; import { resolve } from 'node:path'; -import { getRealmServerToken, matrixLogin } from '../../src/boxel'; +import { getActiveProfile, getRealmServerToken, matrixLogin } from '../../src/boxel'; import { inferDarkfactoryModuleUrl } from '../../src/factory-seed'; import { logger } from '../../src/logger'; import { @@ -671,25 +671,25 @@ async function scenario3( async function main(): Promise { let { scenario, debug, briefUrl } = parseArgs(); - let username = process.env.MATRIX_USERNAME; - if (!username) { + let profile; + try { + profile = getActiveProfile(); + } catch { log.error( - 'MATRIX_USERNAME is required. Set MATRIX_URL, MATRIX_USERNAME, and MATRIX_PASSWORD.', + 'No active Boxel profile found. Run `boxel profile add` to configure one.', ); process.exit(1); } + let username = profile.username; + if (!process.env.OPENROUTER_API_KEY) { log.error('OPENROUTER_API_KEY is required for the LLM agent.'); process.exit(1); } - if (!process.env.MATRIX_URL) { - process.env.MATRIX_URL = 'http://localhost:8008'; - } - - let realmServerUrl = process.env.REALM_SERVER_URL ?? 'http://localhost:4201/'; + let realmServerUrl = profile.realmServerUrl; if (!realmServerUrl.endsWith('/')) { realmServerUrl += '/'; } diff --git a/packages/software-factory/scripts/smoke-tests/smoke-test-realm.ts b/packages/software-factory/scripts/smoke-tests/smoke-test-realm.ts index 8ba06c7887b..b2cf14a6745 100644 --- a/packages/software-factory/scripts/smoke-tests/smoke-test-realm.ts +++ b/packages/software-factory/scripts/smoke-tests/smoke-test-realm.ts @@ -17,20 +17,16 @@ * * Prerequisites: * - * Realm server authentication -- one of: - * a. Active Boxel CLI profile (`boxel profile add` then `boxel profile switch`) - * b. Environment variables: MATRIX_URL, MATRIX_USERNAME, MATRIX_PASSWORD + * Active Boxel CLI profile (`boxel profile add`) * * Usage: - * MATRIX_URL=http://localhost:8008 MATRIX_USERNAME= MATRIX_PASSWORD= \ - * pnpm smoke:test-realm -- \ - * --target-realm-url + * pnpm smoke:test-realm -- --target-realm-url */ // This should be first import '../../src/setup-logger'; -import { getRealmServerToken, matrixLogin, parseArgs } from '../../src/boxel'; +import { getActiveProfile, getRealmServerToken, matrixLogin, parseArgs } from '../../src/boxel'; import { logger } from '../../src/logger'; import { createRealm, @@ -144,15 +140,18 @@ async function main() { let args = parseArgs(process.argv.slice(2)); let targetRealmUrl = (args['target-realm-url'] as string) ?? ''; + let profile; + try { + profile = getActiveProfile(); + } catch { + log.error( + 'No active Boxel profile found. Run `boxel profile add` to configure one.', + ); + process.exit(1); + } + if (!targetRealmUrl) { - let username = process.env.MATRIX_USERNAME; - if (!username) { - log.error('Usage: pnpm smoke:test-realm -- --target-realm-url '); - log.error( - '\nRequires MATRIX_USERNAME and MATRIX_PASSWORD environment variables.', - ); - process.exit(1); - } + let username = profile.username; targetRealmUrl = `http://localhost:4201/${username}/smoke-test-realm/`; log.info( `No --target-realm-url specified, using default: ${targetRealmUrl}\n`, @@ -176,14 +175,6 @@ async function main() { // The username is determined from the JWT. Extract just the last segment. let realmEndpoint = realmPath.split('/').pop() ?? realmPath; - // Set defaults for the auth chain - if (!process.env.MATRIX_URL) { - process.env.MATRIX_URL = 'http://localhost:8008'; - } - if (!process.env.REALM_SERVER_URL) { - process.env.REALM_SERVER_URL = realmServerUrl; - } - log.info('=== Factory Test Realm Smoke Test (QUnit) ===\n'); log.info(`Target realm: ${targetRealmUrl}`); log.info(`Realm server: ${realmServerUrl}`); diff --git a/packages/software-factory/src/boxel.ts b/packages/software-factory/src/boxel.ts index 4a713813aad..fed242bbc1d 100644 --- a/packages/software-factory/src/boxel.ts +++ b/packages/software-factory/src/boxel.ts @@ -5,7 +5,9 @@ import { join } from 'node:path'; import { formatErrorResponse } from './error-format'; import { ensureTrailingSlash, SupportedMimeType } from './realm-operations'; -const PROFILES_FILE = join(homedir(), '.boxel-cli', 'profiles.json'); +function getProfilesFile(): string { + return join(homedir(), '.boxel-cli', 'profiles.json'); +} type BoxelStoredProfile = { matrixUrl: string; @@ -85,11 +87,12 @@ export type ParsedArgs = Record & { }; function parseProfilesConfig(): BoxelProfilesConfig { - if (!existsSync(PROFILES_FILE)) { + let profilesFile = getProfilesFile(); + if (!existsSync(profilesFile)) { return { profiles: {}, activeProfile: null }; } - return JSON.parse(readFileSync(PROFILES_FILE, 'utf8')) as BoxelProfilesConfig; + return JSON.parse(readFileSync(profilesFile, 'utf8')) as BoxelProfilesConfig; } export function getActiveProfile(): ActiveBoxelProfile { @@ -106,23 +109,9 @@ export function getActiveProfile(): ActiveBoxelProfile { }; } - let matrixUrl = process.env.MATRIX_URL; - let username = process.env.MATRIX_USERNAME; - let password = process.env.MATRIX_PASSWORD; - let realmServerUrl = process.env.REALM_SERVER_URL; - if (!matrixUrl || !username || !password || !realmServerUrl) { - throw new Error( - 'No active Boxel profile found and MATRIX_URL/MATRIX_USERNAME/MATRIX_PASSWORD/REALM_SERVER_URL are not fully set', - ); - } - - return { - profileId: null, - username, - matrixUrl, - realmServerUrl: ensureTrailingSlash(realmServerUrl), - password, - }; + throw new Error( + 'No active Boxel profile found. Run `boxel profile add` to configure one.', + ); } export async function matrixLogin( diff --git a/packages/software-factory/src/factory-entrypoint.ts b/packages/software-factory/src/factory-entrypoint.ts index 2833c95249e..27a7c215caa 100644 --- a/packages/software-factory/src/factory-entrypoint.ts +++ b/packages/software-factory/src/factory-entrypoint.ts @@ -96,20 +96,19 @@ export function getFactoryEntrypointUsage(): string { ' --target-realm-url Absolute URL for the target realm', '', 'Options:', - ' --realm-server-url Realm server URL (default: http://localhost:4201/)', + ' --realm-server-url Realm server URL (default: from active Boxel profile)', ' --no-retry-blocked Skip retrying blocked issues (by default, blocked issues are reset to backlog)', ' --model OpenRouter model ID (e.g., anthropic/claude-sonnet-4)', ' --debug Log LLM prompts and responses to stderr', ' --help Show this usage information', '', 'Auth:', - ' MATRIX_USERNAME is required and determines the target realm owner.', - ' For public briefs, no auth setup is needed.', - ' For private briefs, factory:go can authenticate via:', - ' 1. the active Boxel profile, or', - ' 2. MATRIX_URL + MATRIX_USERNAME + MATRIX_PASSWORD environment variables', - ' The realm server URL comes from --realm-server-url (default: http://localhost:4201/).', - ' It is never inferred from --target-realm-url or read from an environment variable.', + ' Authentication uses the active Boxel profile (see: boxel profile add).', + ' The target realm owner is determined from the active profile username.', + ' For public briefs, no further auth setup is needed.', + ' For private briefs, factory:go authenticates via the active Boxel profile.', + ' The realm server URL comes from --realm-server-url, or the active Boxel profile.', + ' It is never inferred from --target-realm-url.', ].join('\n'); } diff --git a/packages/software-factory/src/factory-issue-loop-wiring.ts b/packages/software-factory/src/factory-issue-loop-wiring.ts index b3cfb836110..d252c349fa0 100644 --- a/packages/software-factory/src/factory-issue-loop-wiring.ts +++ b/packages/software-factory/src/factory-issue-loop-wiring.ts @@ -314,8 +314,7 @@ async function resolveAuth(config: IssueLoopWiringConfig): Promise<{ } } catch (error) { throw new Error( - `Matrix login failed. Ensure MATRIX_URL, MATRIX_USERNAME, and MATRIX_PASSWORD are set, ` + - `and pass --realm-server-url on the CLI.\n${ + `Matrix login failed. Ensure an active Boxel profile is configured (run \`boxel profile add\`).\n${ error instanceof Error ? error.message : String(error) }`, ); @@ -349,30 +348,8 @@ async function resolveAuth(config: IssueLoopWiringConfig): Promise<{ function buildProfileWithCliRealmServer( realmServerUrl: string, ): ActiveBoxelProfile { - try { - let profile = getActiveProfile(); - return { ...profile, realmServerUrl }; - } catch { - // No active profile — fall back to env vars - } - - let matrixUrl = process.env.MATRIX_URL?.trim(); - let username = process.env.MATRIX_USERNAME?.trim(); - let password = process.env.MATRIX_PASSWORD?.trim(); - - if (!matrixUrl || !username || !password) { - throw new Error( - 'No active Boxel profile found and MATRIX_URL/MATRIX_USERNAME/MATRIX_PASSWORD are not fully set.', - ); - } - - return { - profileId: null, - username, - matrixUrl, - realmServerUrl, - password, - }; + let profile = getActiveProfile(); + return { ...profile, realmServerUrl }; } // --------------------------------------------------------------------------- diff --git a/packages/software-factory/src/factory-target-realm.ts b/packages/software-factory/src/factory-target-realm.ts index bde40dc6fe2..9822685ec7b 100644 --- a/packages/software-factory/src/factory-target-realm.ts +++ b/packages/software-factory/src/factory-target-realm.ts @@ -220,24 +220,19 @@ function resolveRealmServerProfile( ownerUsername: string, serverUrl: string, ): ActiveBoxelProfile { - let envProfile = buildEnvRealmServerProfile(ownerUsername, serverUrl); - if (envProfile) { - return envProfile; - } - let profile: ActiveBoxelProfile; try { profile = getActiveProfile(); } catch { throw new FactoryEntrypointUsageError( - `Target realm bootstrap needs Matrix auth for ${serverUrl}. Configure MATRIX_URL, MATRIX_USERNAME, and MATRIX_PASSWORD or use a matching active Boxel profile.`, + `Target realm bootstrap needs Matrix auth for ${serverUrl}. Run \`boxel profile add\` to configure a profile.`, ); } if (getMatrixUsername(profile.username) !== ownerUsername) { throw new FactoryEntrypointUsageError( - `Active Boxel profile user "${getMatrixUsername(profile.username)}" does not match MATRIX_USERNAME "${ownerUsername}"`, + `Active Boxel profile user "${getMatrixUsername(profile.username)}" does not match target realm owner "${ownerUsername}"`, ); } @@ -252,57 +247,15 @@ function resolveRealmServerProfile( return profile; } -function buildEnvRealmServerProfile( - ownerUsername: string, - serverUrl: string, -): ActiveBoxelProfile | undefined { - let matrixUrl = normalizeOptionalString(process.env.MATRIX_URL); - let envUsername = normalizeOptionalString(process.env.MATRIX_USERNAME); - let matrixPassword = normalizeOptionalString(process.env.MATRIX_PASSWORD); - - if (!matrixPassword) { - return undefined; - } - - if (!matrixUrl) { - throw new FactoryEntrypointUsageError( - 'MATRIX_URL is required for target realm creation when using environment auth', - ); - } - - if (!envUsername) { - throw new FactoryEntrypointUsageError( - 'MATRIX_USERNAME is required for target realm creation when using environment auth', - ); - } - - let normalizedUsername = getMatrixUsername(envUsername); - - if (normalizedUsername !== ownerUsername) { - throw new FactoryEntrypointUsageError( - `MATRIX_USERNAME "${normalizedUsername}" does not match target realm owner "${ownerUsername}"`, - ); - } - - return { - profileId: null, - username: normalizedUsername, - matrixUrl: ensureTrailingSlash(matrixUrl), - realmServerUrl: serverUrl, - password: matrixPassword, - }; -} - function resolveTargetRealmOwner(): string { - let envUsername = normalizeOptionalString(process.env.MATRIX_USERNAME); - - if (!envUsername) { + try { + let profile = getActiveProfile(); + return getMatrixUsername(profile.username); + } catch { throw new FactoryEntrypointUsageError( - 'Cannot determine the target realm owner. Set MATRIX_USERNAME before running factory:go.', + 'Cannot determine the target realm owner. Run `boxel profile add` to configure a profile.', ); } - - return getMatrixUsername(envUsername); } function resolveTargetRealmUrl(explicitTargetRealmUrl: string | null): string { @@ -315,8 +268,6 @@ function resolveTargetRealmUrl(explicitTargetRealmUrl: string | null): string { return normalizeUrl(explicitTargetRealmUrl, '--target-realm-url'); } -const DEFAULT_REALM_SERVER_URL = 'http://localhost:4201/'; - function resolveRealmServerUrl( explicitRealmServerUrl: string | null, _targetRealmUrl: string, @@ -325,7 +276,16 @@ function resolveRealmServerUrl( return normalizeUrl(explicitRealmServerUrl, '--realm-server-url'); } - return DEFAULT_REALM_SERVER_URL; + try { + let profile = getActiveProfile(); + return ensureTrailingSlash(profile.realmServerUrl); + } catch { + // No profile — fall through to error + } + + throw new FactoryEntrypointUsageError( + 'Cannot determine the realm server URL. Pass --realm-server-url or configure an active Boxel profile.', + ); } function extractEndpointFromRealmUrl(targetRealmUrl: string): string { @@ -366,13 +326,3 @@ function normalizeCreatedRealmUrl( return normalizeUrl(createdRealmId, 'realm server response data.id'); } -function normalizeOptionalString( - value: string | undefined, -): string | undefined { - if (typeof value !== 'string') { - return undefined; - } - - let trimmed = value.trim(); - return trimmed === '' ? undefined : trimmed; -} diff --git a/packages/software-factory/src/realm-auth.ts b/packages/software-factory/src/realm-auth.ts index 38f5d09e4c0..816a38b427b 100644 --- a/packages/software-factory/src/realm-auth.ts +++ b/packages/software-factory/src/realm-auth.ts @@ -58,7 +58,7 @@ export function createBoxelRealmFetch( let profile = options && 'profile' in options ? (options.profile ?? undefined) - : getOptionalActiveProfile(resourceUrl); + : getOptionalActiveProfile(); if (!profile || !sharesOrigin(resourceUrl, profile.realmServerUrl)) { return fetchImpl; @@ -96,15 +96,7 @@ export function createBoxelRealmFetch( }; } -function getOptionalActiveProfile( - resourceUrl: string, -): ActiveBoxelProfile | undefined { - let envProfile = buildEnvProfile(resourceUrl); - - if (envProfile) { - return envProfile; - } - +function getOptionalActiveProfile(): ActiveBoxelProfile | undefined { let config = parseProfilesConfig(); if (config.activeProfile && config.profiles[config.activeProfile]) { @@ -140,7 +132,7 @@ function parseProfilesConfig(): BoxelProfilesConfig { throw new Error( `Failed to parse Boxel profiles config at ${profilesFile}: ${ error instanceof Error ? error.message : String(error) - }. Fix or remove the file, or provide auth via environment variables.`, + }. Fix or remove the file.`, ); } } @@ -187,34 +179,6 @@ function normalizeProfileUrl(value: string, label: string): string { } } -function buildEnvProfile(resourceUrl: string): ActiveBoxelProfile | undefined { - let matrixUrl = normalizeOptionalString(process.env.MATRIX_URL); - let username = normalizeOptionalString(process.env.MATRIX_USERNAME); - let password = normalizeOptionalString(process.env.MATRIX_PASSWORD); - let realmServerUrl = normalizeOptionalString(process.env.REALM_SERVER_URL); - - if (!matrixUrl) { - return undefined; - } - - let normalizedMatrixUrl = normalizeProfileUrl(matrixUrl, 'MATRIX_URL'); - let normalizedRealmServerUrl = realmServerUrl - ? normalizeProfileUrl(realmServerUrl, 'REALM_SERVER_URL') - : normalizeProfileUrl(new URL('/', resourceUrl).href, 'resourceUrl origin'); - - if (!username || !password) { - return undefined; - } - - return { - profileId: null, - username: getMatrixUsername(username), - matrixUrl: normalizedMatrixUrl, - realmServerUrl: normalizedRealmServerUrl, - password, - }; -} - function normalizeOptionalString( value: string | undefined, ): string | undefined { diff --git a/packages/software-factory/tests/factory-entrypoint.integration.test.ts b/packages/software-factory/tests/factory-entrypoint.integration.test.ts index 753f0344688..f1eb2d726c0 100644 --- a/packages/software-factory/tests/factory-entrypoint.integration.test.ts +++ b/packages/software-factory/tests/factory-entrypoint.integration.test.ts @@ -1,7 +1,8 @@ -import { readFileSync } from 'node:fs'; +import { mkdirSync, mkdtempSync, readFileSync, writeFileSync } from 'node:fs'; import { spawn, spawnSync } from 'node:child_process'; import { createServer } from 'node:http'; -import { resolve } from 'node:path'; +import { tmpdir } from 'node:os'; +import { join, resolve } from 'node:path'; import { module, test } from 'qunit'; import { SupportedMimeType } from '@cardstack/runtime-common/supported-mime-type'; @@ -43,6 +44,36 @@ interface RunCommandResult { stderr: string; } +function createTempProfileHome(options: { + username: string; + matrixUrl: string; + realmServerUrl: string; + password: string; +}): string { + let tempHome = mkdtempSync(join(tmpdir(), 'boxel-test-')); + let boxelCliDir = join(tempHome, '.boxel-cli'); + mkdirSync(boxelCliDir, { recursive: true }); + + let profileId = `@${options.username}:localhost`; + let config = { + profiles: { + [profileId]: { + matrixUrl: options.matrixUrl, + realmServerUrl: options.realmServerUrl, + password: options.password, + }, + }, + activeProfile: profileId, + }; + + writeFileSync( + join(boxelCliDir, 'profiles.json'), + JSON.stringify(config, null, 2), + ); + + return tempHome; +} + module('factory-entrypoint integration', function () { test('factory:go package script prints a structured JSON summary', async function (assert) { let canonicalTargetRealmUrl: string; @@ -222,6 +253,13 @@ module('factory-entrypoint integration', function () { targetRealmUrl = `${origin}/typed-by-user/personal/`; canonicalTargetRealmUrl = `${origin}/hassan/personal/`; + let tempHome = createTempProfileHome({ + username: 'hassan', + matrixUrl: `${origin}/`, + realmServerUrl: `${origin}/`, + password: 'secret', + }); + try { let result = await runCommand( 'pnpm', @@ -241,9 +279,7 @@ module('factory-entrypoint integration', function () { encoding: 'utf8', env: { ...process.env, - MATRIX_USERNAME: 'hassan', - MATRIX_PASSWORD: 'secret', - MATRIX_URL: origin, + HOME: tempHome, }, }, ); @@ -318,7 +354,7 @@ module('factory-entrypoint integration', function () { assert.true(/--no-retry-blocked/.test(result.stdout)); }); - test('factory:go fails clearly when MATRIX_USERNAME is missing', async function (assert) { + test('factory:go fails clearly when no profile is configured', async function (assert) { let server = createServer((_request, response) => { response.writeHead(200, { 'content-type': SupportedMimeType.JSON }); response.end(stickyNoteFixture); @@ -352,14 +388,14 @@ module('factory-entrypoint integration', function () { encoding: 'utf8', env: { ...process.env, - MATRIX_USERNAME: '', + HOME: '/tmp/no-boxel-cli-here', }, }, ); assert.strictEqual(result.status, 1); assert.true( - /Set MATRIX_USERNAME before running factory:go/.test(result.stderr), + /boxel profile add/.test(result.stderr), ); } finally { await new Promise((resolvePromise, reject) => diff --git a/packages/software-factory/tests/factory-entrypoint.test.ts b/packages/software-factory/tests/factory-entrypoint.test.ts index 2aa5485ab6c..590b200f500 100644 --- a/packages/software-factory/tests/factory-entrypoint.test.ts +++ b/packages/software-factory/tests/factory-entrypoint.test.ts @@ -13,6 +13,7 @@ import { import type { FactoryBrief } from '../src/factory-brief'; import type { FactoryTargetRealmBootstrapResult } from '../src/factory-target-realm'; import type { SeedIssueResult } from '../src/factory-seed'; +import { installTestProfile } from './helpers/test-profile'; const briefUrl = 'https://briefs.example.test/software-factory/Wiki/sticky-note'; @@ -39,12 +40,22 @@ const mockSeedResult: SeedIssueResult = { }; module('factory-entrypoint', function (hooks) { - let originalMatrixUsername = process.env.MATRIX_USERNAME; + let cleanupProfile: (() => void) | undefined; hooks.afterEach(function () { - process.env.MATRIX_USERNAME = originalMatrixUsername; + cleanupProfile?.(); + cleanupProfile = undefined; }); + function setupHassanProfile() { + cleanupProfile = installTestProfile({ + username: 'hassan', + matrixUrl: 'https://matrix.example.test/', + realmServerUrl: 'https://realms.example.test/', + password: 'secret', + }); + } + test('parseFactoryEntrypointArgs accepts required inputs', function (assert) { let options = parseFactoryEntrypointArgs([ '--brief-url', @@ -142,14 +153,13 @@ module('factory-entrypoint', function (hooks) { assert.true(/--realm-server-url /.test(usage)); assert.true(/--no-retry-blocked/.test(usage)); assert.true(/--help/.test(usage)); - assert.true(/MATRIX_USERNAME is required/.test(usage)); - assert.true(/For public briefs, no auth setup is needed./.test(usage)); - assert.true(/MATRIX_URL \+ MATRIX_USERNAME \+ MATRIX_PASSWORD/.test(usage)); + assert.true(/active Boxel profile/.test(usage)); + assert.true(/For public briefs, no further auth setup is needed./.test(usage)); assert.false(/REALM_SECRET_SEED/.test(usage)); }); test('runFactoryEntrypoint creates seed issue and loads brief data', async function (assert) { - process.env.MATRIX_USERNAME = 'hassan'; + setupHassanProfile(); let summary = await runFactoryEntrypoint( { @@ -219,7 +229,7 @@ module('factory-entrypoint', function (hooks) { }); test('runFactoryEntrypoint uses the resolved realm server URL for darkfactory module', async function (assert) { - process.env.MATRIX_USERNAME = 'hassan'; + setupHassanProfile(); let capturedDarkfactoryModuleUrl: string | undefined; diff --git a/packages/software-factory/tests/factory-target-realm.spec.ts b/packages/software-factory/tests/factory-target-realm.spec.ts index fb15fa9ac30..18d00be03aa 100644 --- a/packages/software-factory/tests/factory-target-realm.spec.ts +++ b/packages/software-factory/tests/factory-target-realm.spec.ts @@ -1,6 +1,7 @@ import { createServer } from 'node:http'; -import { readFileSync } from 'node:fs'; -import { resolve } from 'node:path'; +import { mkdirSync, mkdtempSync, readFileSync, writeFileSync } from 'node:fs'; +import { tmpdir } from 'node:os'; +import { join, resolve } from 'node:path'; import type { AddressInfo } from 'node:net'; import { SupportedMimeType } from '@cardstack/runtime-common/supported-mime-type'; @@ -90,9 +91,7 @@ test('factory:go creates a target realm and bootstraps project artifacts end-to- cwd: packageRoot, env: { ...process.env, - MATRIX_USERNAME: targetUsername, - MATRIX_PASSWORD: targetPassword, - MATRIX_URL: matrixURL, + HOME: createTempProfileHome(targetUsername, targetPassword, matrixURL, realmServerURL), }, timeoutMs: 120_000, }, @@ -145,3 +144,27 @@ test('factory:go creates a target realm and bootstraps project artifacts end-to- ); } }); + +function createTempProfileHome( + username: string, + password: string, + matrixUrl: string, + realmServerUrl: string, +): string { + let tempHome = mkdtempSync(join(tmpdir(), 'boxel-test-')); + let boxelCliDir = join(tempHome, '.boxel-cli'); + mkdirSync(boxelCliDir, { recursive: true }); + + let profileId = `@${username}:localhost`; + writeFileSync( + join(boxelCliDir, 'profiles.json'), + JSON.stringify({ + profiles: { + [profileId]: { matrixUrl, realmServerUrl, password }, + }, + activeProfile: profileId, + }), + ); + + return tempHome; +} diff --git a/packages/software-factory/tests/factory-target-realm.test.ts b/packages/software-factory/tests/factory-target-realm.test.ts index f851c76576e..d9b043338dd 100644 --- a/packages/software-factory/tests/factory-target-realm.test.ts +++ b/packages/software-factory/tests/factory-target-realm.test.ts @@ -7,28 +7,31 @@ import { bootstrapFactoryTargetRealm, resolveFactoryTargetRealm, } from '../src/factory-target-realm'; +import { installTestProfile } from './helpers/test-profile'; const targetRealmUrl = 'https://realms.example.test/hassan/personal/'; module('factory-target-realm', function (hooks) { - let originalHome = process.env.HOME; - let originalMatrixUsername = process.env.MATRIX_USERNAME; - let originalMatrixUrl = process.env.MATRIX_URL; - let originalMatrixPassword = process.env.MATRIX_PASSWORD; - let originalRealmServerUrl = process.env.REALM_SERVER_URL; + let cleanupProfile: (() => void) | undefined; let originalFetch = globalThis.fetch; hooks.afterEach(function () { - restoreEnv('HOME', originalHome); - restoreEnv('MATRIX_USERNAME', originalMatrixUsername); - restoreEnv('MATRIX_URL', originalMatrixUrl); - restoreEnv('MATRIX_PASSWORD', originalMatrixPassword); - restoreEnv('REALM_SERVER_URL', originalRealmServerUrl); + cleanupProfile?.(); + cleanupProfile = undefined; globalThis.fetch = originalFetch; }); - test('resolveFactoryTargetRealm uses MATRIX_USERNAME and explicit target URL', function (assert) { - process.env.MATRIX_USERNAME = 'hassan'; + function setupHassanProfile() { + cleanupProfile = installTestProfile({ + username: 'hassan', + matrixUrl: 'https://matrix.example.test/', + realmServerUrl: 'https://realms.example.test/', + password: 'secret', + }); + } + + test('resolveFactoryTargetRealm resolves owner from active profile', function (assert) { + setupHassanProfile(); let resolution = resolveFactoryTargetRealm({ targetRealmUrl, @@ -38,14 +41,14 @@ module('factory-target-realm', function (hooks) { assert.strictEqual(resolution.url, targetRealmUrl); assert.strictEqual( resolution.serverUrl, - 'http://localhost:4201/', - 'defaults to localhost when --realm-server-url is not provided', + 'https://realms.example.test/', + 'defaults to active profile realmServerUrl when --realm-server-url is not provided', ); assert.strictEqual(resolution.ownerUsername, 'hassan'); }); test('resolveFactoryTargetRealm accepts an explicit realm server URL override', function (assert) { - process.env.MATRIX_USERNAME = 'hassan'; + setupHassanProfile(); let resolution = resolveFactoryTargetRealm({ targetRealmUrl: 'https://realms.example.test/boxel/hassan/personal/', @@ -59,7 +62,7 @@ module('factory-target-realm', function (hooks) { }); test('resolveFactoryTargetRealm rejects when target realm URL is missing', function (assert) { - process.env.MATRIX_USERNAME = 'hassan'; + setupHassanProfile(); assert.throws( () => @@ -73,8 +76,20 @@ module('factory-target-realm', function (hooks) { ); }); - test('resolveFactoryTargetRealm rejects when MATRIX_USERNAME is missing', function (assert) { - delete process.env.MATRIX_USERNAME; + test('resolveFactoryTargetRealm rejects when no active profile is configured', function (assert) { + cleanupProfile = installTestProfile({ + username: 'nobody', + matrixUrl: 'https://matrix.example.test/', + realmServerUrl: 'https://realms.example.test/', + password: 'secret', + }); + // Overwrite with an empty profiles file to simulate no profile + let { writeFileSync } = require('node:fs'); + let { join } = require('node:path'); + writeFileSync( + join(process.env.HOME!, '.boxel-cli', 'profiles.json'), + JSON.stringify({ profiles: {}, activeProfile: null }), + ); assert.throws( () => @@ -84,12 +99,14 @@ module('factory-target-realm', function (hooks) { }), (error: unknown) => error instanceof FactoryEntrypointUsageError && - error.message.includes('Set MATRIX_USERNAME'), + (error.message.includes('boxel profile add') || + error.message.includes('active Boxel profile')), ); }); test('bootstrapFactoryTargetRealm creates the realm through the API', async function (assert) { - process.env.MATRIX_USERNAME = 'hassan'; + setupHassanProfile(); + let resolution = resolveFactoryTargetRealm({ targetRealmUrl, realmServerUrl: null, @@ -113,7 +130,8 @@ module('factory-target-realm', function (hooks) { }); test('bootstrapFactoryTargetRealm reports when the realm already exists', async function (assert) { - process.env.MATRIX_USERNAME = 'hassan'; + setupHassanProfile(); + let resolution = resolveFactoryTargetRealm({ targetRealmUrl, realmServerUrl: null, @@ -132,7 +150,8 @@ module('factory-target-realm', function (hooks) { }); test('bootstrapFactoryTargetRealm uses the canonical realm URL returned by create-realm', async function (assert) { - process.env.MATRIX_USERNAME = 'hassan'; + setupHassanProfile(); + let resolution = resolveFactoryTargetRealm({ targetRealmUrl: 'https://realms.example.test/typed-by-user/personal/', realmServerUrl: null, @@ -156,9 +175,7 @@ module('factory-target-realm', function (hooks) { test('bootstrapFactoryTargetRealm sends the realm-server JWT to create-realm', async function (assert) { assert.expect(17); - process.env.MATRIX_URL = 'https://matrix.example.test/'; - process.env.MATRIX_USERNAME = 'hassan'; - process.env.MATRIX_PASSWORD = 'secret'; + setupHassanProfile(); let resolution = resolveFactoryTargetRealm({ targetRealmUrl, @@ -327,9 +344,7 @@ module('factory-target-realm', function (hooks) { test('bootstrapFactoryTargetRealm does not surface non-serialized response objects as [object Object]', async function (assert) { assert.expect(2); - process.env.MATRIX_URL = 'https://matrix.example.test/'; - process.env.MATRIX_USERNAME = 'hassan'; - process.env.MATRIX_PASSWORD = 'secret'; + setupHassanProfile(); let resolution = resolveFactoryTargetRealm({ targetRealmUrl, @@ -404,11 +419,3 @@ module('factory-target-realm', function (hooks) { ); }); }); - -function restoreEnv(name: string, value: string | undefined): void { - if (value === undefined) { - delete process.env[name]; - } else { - process.env[name] = value; - } -} diff --git a/packages/software-factory/tests/helpers/browser-auth.ts b/packages/software-factory/tests/helpers/browser-auth.ts index 0cdd724b8a8..ed421345329 100644 --- a/packages/software-factory/tests/helpers/browser-auth.ts +++ b/packages/software-factory/tests/helpers/browser-auth.ts @@ -41,7 +41,6 @@ function getSupportRealmServerURL(): string | undefined { const defaultMatrixUrl = ensureTrailingSlash( process.env.SOFTWARE_FACTORY_BROWSER_MATRIX_URL ?? getSupportMatrixURL() ?? - process.env.MATRIX_URL ?? 'http://localhost:8008/', ); const defaultUsername = diff --git a/packages/software-factory/tests/helpers/test-profile.ts b/packages/software-factory/tests/helpers/test-profile.ts new file mode 100644 index 00000000000..c1d815e5c63 --- /dev/null +++ b/packages/software-factory/tests/helpers/test-profile.ts @@ -0,0 +1,50 @@ +import { mkdirSync, writeFileSync } from 'node:fs'; +import { mkdtempSync } from 'node:fs'; +import { tmpdir } from 'node:os'; +import { join } from 'node:path'; + +export interface TestProfileOptions { + username: string; + matrixUrl: string; + realmServerUrl: string; + password: string; +} + +/** + * Creates a temporary HOME directory with a fake ~/.boxel-cli/profiles.json. + * Sets process.env.HOME to the temp dir so getActiveProfile() reads from it. + * Returns a cleanup function that restores the original HOME. + */ +export function installTestProfile(options: TestProfileOptions): () => void { + let originalHome = process.env.HOME; + + let tempHome = mkdtempSync(join(tmpdir(), 'boxel-test-')); + let boxelCliDir = join(tempHome, '.boxel-cli'); + mkdirSync(boxelCliDir, { recursive: true }); + + let profileId = `@${options.username}:localhost`; + let config = { + profiles: { + [profileId]: { + matrixUrl: options.matrixUrl, + realmServerUrl: options.realmServerUrl, + password: options.password, + }, + }, + activeProfile: profileId, + }; + + writeFileSync( + join(boxelCliDir, 'profiles.json'), + JSON.stringify(config, null, 2), + ); + process.env.HOME = tempHome; + + return () => { + if (originalHome === undefined) { + delete process.env.HOME; + } else { + process.env.HOME = originalHome; + } + }; +} diff --git a/packages/software-factory/tests/realm-auth.test.ts b/packages/software-factory/tests/realm-auth.test.ts index 26a64e130c0..8aac4e9a0f4 100644 --- a/packages/software-factory/tests/realm-auth.test.ts +++ b/packages/software-factory/tests/realm-auth.test.ts @@ -230,79 +230,32 @@ module('realm-auth', function () { } }); - test('createBoxelRealmFetch uses MATRIX_USERNAME and MATRIX_PASSWORD env auth for a private brief', async function (assert) { + test('createBoxelRealmFetch uses active Boxel profile auth for a private brief', async function (assert) { let tempHome = mkdtempSync(join(tmpdir(), 'software-factory-realm-auth-')); let originalHome = process.env.HOME; - let originalMatrixUrl = process.env.MATRIX_URL; - let originalMatrixUsername = process.env.MATRIX_USERNAME; - let originalMatrixPassword = process.env.MATRIX_PASSWORD; - let originalRealmServerUrl = process.env.REALM_SERVER_URL; let username = 'software-factory-browser'; let password = browserPassword(username); let servers = await startServers({ username, password }); let briefUrl = `${servers.realmServer.realmUrl}Wiki/brief-card`; - try { - process.env.HOME = tempHome; - process.env.MATRIX_URL = servers.matrixServer.url; - process.env.MATRIX_USERNAME = username; - process.env.MATRIX_PASSWORD = password; - delete process.env.REALM_SERVER_URL; - - let brief = await loadFactoryBrief(briefUrl, { - fetch: createBoxelRealmFetch(briefUrl), - }); - - assert.strictEqual(brief.title, 'Private Brief'); - assert.strictEqual( - brief.contentSummary, - 'Private brief content for testing realm auth.', - ); - } finally { - restoreEnv('HOME', originalHome); - restoreEnv('MATRIX_URL', originalMatrixUrl); - restoreEnv('MATRIX_USERNAME', originalMatrixUsername); - restoreEnv('MATRIX_PASSWORD', originalMatrixPassword); - restoreEnv('REALM_SERVER_URL', originalRealmServerUrl); - await servers.stop(); - rmSync(tempHome, { recursive: true, force: true }); - } - }); - - test('createBoxelRealmFetch prefers env auth over an unrelated active profile', async function (assert) { - let tempHome = mkdtempSync(join(tmpdir(), 'software-factory-realm-auth-')); - let originalHome = process.env.HOME; - let originalMatrixUrl = process.env.MATRIX_URL; - let originalMatrixUsername = process.env.MATRIX_USERNAME; - let originalMatrixPassword = process.env.MATRIX_PASSWORD; - let originalRealmServerUrl = process.env.REALM_SERVER_URL; let profilesDir = join(tempHome, '.boxel-cli'); mkdirSync(profilesDir, { recursive: true }); writeFileSync( join(profilesDir, 'profiles.json'), JSON.stringify({ - activeProfile: '@someone-else:localhost', + activeProfile: `@${username}:localhost`, profiles: { - '@someone-else:localhost': { - matrixUrl: 'https://unrelated-matrix.example.test/', - realmServerUrl: 'https://unrelated-realm-server.example.test/', - password: 'wrong-password', + [`@${username}:localhost`]: { + matrixUrl: servers.matrixServer.url, + realmServerUrl: servers.realmServer.origin, + password, }, }, }), ); - let username = 'software-factory-browser'; - let password = browserPassword(username); - let servers = await startServers({ username, password }); - let briefUrl = `${servers.realmServer.realmUrl}Wiki/brief-card`; - try { process.env.HOME = tempHome; - process.env.MATRIX_URL = servers.matrixServer.url; - process.env.MATRIX_USERNAME = username; - process.env.MATRIX_PASSWORD = password; - delete process.env.REALM_SERVER_URL; let brief = await loadFactoryBrief(briefUrl, { fetch: createBoxelRealmFetch(briefUrl), @@ -315,16 +268,12 @@ module('realm-auth', function () { ); } finally { restoreEnv('HOME', originalHome); - restoreEnv('MATRIX_URL', originalMatrixUrl); - restoreEnv('MATRIX_USERNAME', originalMatrixUsername); - restoreEnv('MATRIX_PASSWORD', originalMatrixPassword); - restoreEnv('REALM_SERVER_URL', originalRealmServerUrl); await servers.stop(); rmSync(tempHome, { recursive: true, force: true }); } }); - test('matrixLogin rejects when MATRIX_USERNAME and MATRIX_PASSWORD are invalid', async function (assert) { + test('matrixLogin rejects when credentials are invalid', async function (assert) { let username = 'software-factory-browser'; let servers = await startServers({ username }); From 8b66ac4e5aae2beaf2645f4b3cd083b77c012535 Mon Sep 17 00:00:00 2001 From: Matic Jurglic Date: Thu, 16 Apr 2026 17:16:12 +0200 Subject: [PATCH 02/10] Lint fix --- .../scripts/smoke-tests/smoke-test-factory-scenarios.ts | 6 +++++- .../scripts/smoke-tests/smoke-test-realm.ts | 7 ++++++- packages/software-factory/src/factory-issue-loop-wiring.ts | 4 ++-- packages/software-factory/src/factory-target-realm.ts | 1 - .../tests/factory-entrypoint.integration.test.ts | 4 +--- packages/software-factory/tests/factory-entrypoint.test.ts | 4 +++- .../software-factory/tests/factory-target-realm.spec.ts | 7 ++++++- 7 files changed, 23 insertions(+), 10 deletions(-) diff --git a/packages/software-factory/scripts/smoke-tests/smoke-test-factory-scenarios.ts b/packages/software-factory/scripts/smoke-tests/smoke-test-factory-scenarios.ts index 601353e8066..fe8e9ed862b 100644 --- a/packages/software-factory/scripts/smoke-tests/smoke-test-factory-scenarios.ts +++ b/packages/software-factory/scripts/smoke-tests/smoke-test-factory-scenarios.ts @@ -28,7 +28,11 @@ import '../../src/setup-logger'; import { spawn } from 'node:child_process'; import { resolve } from 'node:path'; -import { getActiveProfile, getRealmServerToken, matrixLogin } from '../../src/boxel'; +import { + getActiveProfile, + getRealmServerToken, + matrixLogin, +} from '../../src/boxel'; import { inferDarkfactoryModuleUrl } from '../../src/factory-seed'; import { logger } from '../../src/logger'; import { diff --git a/packages/software-factory/scripts/smoke-tests/smoke-test-realm.ts b/packages/software-factory/scripts/smoke-tests/smoke-test-realm.ts index b2cf14a6745..52450590d8e 100644 --- a/packages/software-factory/scripts/smoke-tests/smoke-test-realm.ts +++ b/packages/software-factory/scripts/smoke-tests/smoke-test-realm.ts @@ -26,7 +26,12 @@ // This should be first import '../../src/setup-logger'; -import { getActiveProfile, getRealmServerToken, matrixLogin, parseArgs } from '../../src/boxel'; +import { + getActiveProfile, + getRealmServerToken, + matrixLogin, + parseArgs, +} from '../../src/boxel'; import { logger } from '../../src/logger'; import { createRealm, diff --git a/packages/software-factory/src/factory-issue-loop-wiring.ts b/packages/software-factory/src/factory-issue-loop-wiring.ts index d252c349fa0..4f8a936e3dc 100644 --- a/packages/software-factory/src/factory-issue-loop-wiring.ts +++ b/packages/software-factory/src/factory-issue-loop-wiring.ts @@ -315,8 +315,8 @@ async function resolveAuth(config: IssueLoopWiringConfig): Promise<{ } catch (error) { throw new Error( `Matrix login failed. Ensure an active Boxel profile is configured (run \`boxel profile add\`).\n${ - error instanceof Error ? error.message : String(error) - }`, + error instanceof Error ? error.message : String(error) + }`, ); } diff --git a/packages/software-factory/src/factory-target-realm.ts b/packages/software-factory/src/factory-target-realm.ts index 9822685ec7b..b394f3ae4f1 100644 --- a/packages/software-factory/src/factory-target-realm.ts +++ b/packages/software-factory/src/factory-target-realm.ts @@ -325,4 +325,3 @@ function normalizeCreatedRealmUrl( return normalizeUrl(createdRealmId, 'realm server response data.id'); } - diff --git a/packages/software-factory/tests/factory-entrypoint.integration.test.ts b/packages/software-factory/tests/factory-entrypoint.integration.test.ts index f1eb2d726c0..47a220f4f67 100644 --- a/packages/software-factory/tests/factory-entrypoint.integration.test.ts +++ b/packages/software-factory/tests/factory-entrypoint.integration.test.ts @@ -394,9 +394,7 @@ module('factory-entrypoint integration', function () { ); assert.strictEqual(result.status, 1); - assert.true( - /boxel profile add/.test(result.stderr), - ); + assert.true(/boxel profile add/.test(result.stderr)); } finally { await new Promise((resolvePromise, reject) => server.close((error) => (error ? reject(error) : resolvePromise())), diff --git a/packages/software-factory/tests/factory-entrypoint.test.ts b/packages/software-factory/tests/factory-entrypoint.test.ts index 590b200f500..b129f8d213f 100644 --- a/packages/software-factory/tests/factory-entrypoint.test.ts +++ b/packages/software-factory/tests/factory-entrypoint.test.ts @@ -154,7 +154,9 @@ module('factory-entrypoint', function (hooks) { assert.true(/--no-retry-blocked/.test(usage)); assert.true(/--help/.test(usage)); assert.true(/active Boxel profile/.test(usage)); - assert.true(/For public briefs, no further auth setup is needed./.test(usage)); + assert.true( + /For public briefs, no further auth setup is needed./.test(usage), + ); assert.false(/REALM_SECRET_SEED/.test(usage)); }); diff --git a/packages/software-factory/tests/factory-target-realm.spec.ts b/packages/software-factory/tests/factory-target-realm.spec.ts index 18d00be03aa..90aafd2290e 100644 --- a/packages/software-factory/tests/factory-target-realm.spec.ts +++ b/packages/software-factory/tests/factory-target-realm.spec.ts @@ -91,7 +91,12 @@ test('factory:go creates a target realm and bootstraps project artifacts end-to- cwd: packageRoot, env: { ...process.env, - HOME: createTempProfileHome(targetUsername, targetPassword, matrixURL, realmServerURL), + HOME: createTempProfileHome( + targetUsername, + targetPassword, + matrixURL, + realmServerURL, + ), }, timeoutMs: 120_000, }, From b7688af88fd199c94bdd9308e7b5db944e20bc11 Mon Sep 17 00:00:00 2001 From: Matic Jurglic Date: Thu, 16 Apr 2026 17:27:31 +0200 Subject: [PATCH 03/10] Add early origin mismatch check between target realm URL and active profile When the target realm URL origin doesn't match the realm server URL (derived from the active profile), fail early with a clear error explaining the mismatch and suggesting `boxel profile switch`. Co-Authored-By: Claude Opus 4.6 (1M context) --- .../src/factory-target-realm.ts | 11 ++++++++++ .../tests/factory-target-realm.test.ts | 22 +++++++++++++++++++ 2 files changed, 33 insertions(+) diff --git a/packages/software-factory/src/factory-target-realm.ts b/packages/software-factory/src/factory-target-realm.ts index b394f3ae4f1..3601e10303e 100644 --- a/packages/software-factory/src/factory-target-realm.ts +++ b/packages/software-factory/src/factory-target-realm.ts @@ -59,6 +59,17 @@ export function resolveFactoryTargetRealm( let serverUrl = resolveRealmServerUrl(options.realmServerUrl, url); let ownerUsername = resolveTargetRealmOwner(); + let targetOrigin = new URL(url).origin; + let serverOrigin = new URL(serverUrl).origin; + if (targetOrigin !== serverOrigin) { + let profile = getActiveProfile(); + throw new FactoryEntrypointUsageError( + `Target realm URL "${url}" (origin: ${targetOrigin}) does not match the realm server "${serverUrl}" (origin: ${serverOrigin}).\n` + + `Your active Boxel profile "${profile.profileId}" points to ${ensureTrailingSlash(profile.realmServerUrl)}.\n` + + `Either switch to a profile that matches the target realm (boxel profile switch), or pass --realm-server-url explicitly.`, + ); + } + return { url, serverUrl, diff --git a/packages/software-factory/tests/factory-target-realm.test.ts b/packages/software-factory/tests/factory-target-realm.test.ts index d9b043338dd..f9ea8aa8093 100644 --- a/packages/software-factory/tests/factory-target-realm.test.ts +++ b/packages/software-factory/tests/factory-target-realm.test.ts @@ -76,6 +76,28 @@ module('factory-target-realm', function (hooks) { ); }); + test('resolveFactoryTargetRealm rejects when target realm origin does not match profile', function (assert) { + // Profile points to staging, but target realm is localhost + cleanupProfile = installTestProfile({ + username: 'hassan', + matrixUrl: 'https://matrix-staging.stack.cards/', + realmServerUrl: 'https://realms-staging.stack.cards/', + password: 'secret', + }); + + assert.throws( + () => + resolveFactoryTargetRealm({ + targetRealmUrl: 'http://localhost:4201/hassan/my-realm/', + realmServerUrl: null, + }), + (error: unknown) => + error instanceof FactoryEntrypointUsageError && + error.message.includes('does not match the realm server') && + error.message.includes('boxel profile switch'), + ); + }); + test('resolveFactoryTargetRealm rejects when no active profile is configured', function (assert) { cleanupProfile = installTestProfile({ username: 'nobody', From 734342930672eaf07935fe188fad1ab7141dc918 Mon Sep 17 00:00:00 2001 From: Matic Jurglic Date: Thu, 16 Apr 2026 17:31:06 +0200 Subject: [PATCH 04/10] Improve error message when no profile is configured Tell the user to run `boxel profile add` instead of the generic "configure an active Boxel profile" message. Co-Authored-By: Claude Opus 4.6 (1M context) --- packages/software-factory/src/factory-target-realm.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/software-factory/src/factory-target-realm.ts b/packages/software-factory/src/factory-target-realm.ts index 3601e10303e..ed6baf32d27 100644 --- a/packages/software-factory/src/factory-target-realm.ts +++ b/packages/software-factory/src/factory-target-realm.ts @@ -295,7 +295,7 @@ function resolveRealmServerUrl( } throw new FactoryEntrypointUsageError( - 'Cannot determine the realm server URL. Pass --realm-server-url or configure an active Boxel profile.', + 'No active Boxel profile found. Run `boxel profile add` to configure one, or pass --realm-server-url explicitly.', ); } From 6b3cb9ce6f02c605b60870ef2098f4e7018c849e Mon Sep 17 00:00:00 2001 From: Matic Jurglic Date: Thu, 16 Apr 2026 17:55:37 +0200 Subject: [PATCH 05/10] Fix lint: replace require() with top-level imports in test Co-Authored-By: Claude Opus 4.6 (1M context) --- .../tests/factory-target-realm.test.ts | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/packages/software-factory/tests/factory-target-realm.test.ts b/packages/software-factory/tests/factory-target-realm.test.ts index 0bcdbab997c..7e290462a93 100644 --- a/packages/software-factory/tests/factory-target-realm.test.ts +++ b/packages/software-factory/tests/factory-target-realm.test.ts @@ -1,5 +1,10 @@ +import { existsSync, readFileSync, writeFileSync } from 'node:fs'; +import { homedir } from 'node:os'; +import { join } from 'node:path'; import { module, test } from 'qunit'; +import { resetProfileManager } from '@cardstack/boxel-cli/api'; + import { FactoryEntrypointUsageError } from '../src/factory-entrypoint-errors'; import { bootstrapFactoryTargetRealm, @@ -95,11 +100,6 @@ module('factory-target-realm', function (hooks) { }); test('resolveFactoryTargetRealm rejects when no active profile is configured', function (assert) { - let { existsSync, readFileSync, writeFileSync } = require('node:fs'); - let { homedir } = require('node:os'); - let { join } = require('node:path'); - let { resetProfileManager } = require('@cardstack/boxel-cli/api'); - let profilesFile = join(homedir(), '.boxel-cli', 'profiles.json'); let backup = existsSync(profilesFile) ? readFileSync(profilesFile, 'utf8') From d7536c42e5a943052ca17615e137cb0edc4d2d0d Mon Sep 17 00:00:00 2001 From: Matic Jurglic Date: Thu, 16 Apr 2026 18:05:08 +0200 Subject: [PATCH 06/10] Make test profiles hermetic with isolated temp config dirs Address Copilot review: installTestProfile was writing to the real ~/.boxel-cli/profiles.json. Now uses setProfileManager() to point the singleton at a temp directory, so no real user files are touched. Co-Authored-By: Claude Opus 4.6 (1M context) --- packages/boxel-cli/api.ts | 5 +- packages/boxel-cli/src/lib/profile-manager.ts | 9 +++ .../tests/factory-target-realm.test.ts | 55 ++++++++----------- .../tests/helpers/test-profile.ts | 45 ++++++--------- 4 files changed, 53 insertions(+), 61 deletions(-) diff --git a/packages/boxel-cli/api.ts b/packages/boxel-cli/api.ts index 6c8237292bf..61a7ff77ef1 100644 --- a/packages/boxel-cli/api.ts +++ b/packages/boxel-cli/api.ts @@ -4,4 +4,7 @@ export { type CreateRealmResult, } from './src/lib/boxel-cli-client'; -export { resetProfileManager } from './src/lib/profile-manager'; +export { + resetProfileManager, + setProfileManager, +} from './src/lib/profile-manager'; diff --git a/packages/boxel-cli/src/lib/profile-manager.ts b/packages/boxel-cli/src/lib/profile-manager.ts index 16db86c52b9..2bc462d73ea 100644 --- a/packages/boxel-cli/src/lib/profile-manager.ts +++ b/packages/boxel-cli/src/lib/profile-manager.ts @@ -572,3 +572,12 @@ export function getProfileManager(): ProfileManager { export function resetProfileManager(): void { _instance = null; } + +/** + * Replace the singleton with a ProfileManager using a custom config directory. + * Useful for tests that need an isolated profile without touching the real + * ~/.boxel-cli/profiles.json. + */ +export function setProfileManager(configDir: string): void { + _instance = new ProfileManager(configDir); +} diff --git a/packages/software-factory/tests/factory-target-realm.test.ts b/packages/software-factory/tests/factory-target-realm.test.ts index 7e290462a93..7d8f2de957b 100644 --- a/packages/software-factory/tests/factory-target-realm.test.ts +++ b/packages/software-factory/tests/factory-target-realm.test.ts @@ -1,9 +1,9 @@ -import { existsSync, readFileSync, writeFileSync } from 'node:fs'; -import { homedir } from 'node:os'; +import { mkdtempSync, writeFileSync } from 'node:fs'; +import { tmpdir } from 'node:os'; import { join } from 'node:path'; import { module, test } from 'qunit'; -import { resetProfileManager } from '@cardstack/boxel-cli/api'; +import { setProfileManager, resetProfileManager } from '@cardstack/boxel-cli/api'; import { FactoryEntrypointUsageError } from '../src/factory-entrypoint-errors'; import { @@ -100,35 +100,26 @@ module('factory-target-realm', function (hooks) { }); test('resolveFactoryTargetRealm rejects when no active profile is configured', function (assert) { - let profilesFile = join(homedir(), '.boxel-cli', 'profiles.json'); - let backup = existsSync(profilesFile) - ? readFileSync(profilesFile, 'utf8') - : undefined; - - try { - writeFileSync( - profilesFile, - JSON.stringify({ profiles: {}, activeProfile: null }), - ); - resetProfileManager(); - - assert.throws( - () => - resolveFactoryTargetRealm({ - targetRealmUrl, - realmServerUrl: null, - }), - (error: unknown) => - error instanceof FactoryEntrypointUsageError && - (error.message.includes('boxel profile add') || - error.message.includes('active Boxel profile')), - ); - } finally { - if (backup !== undefined) { - writeFileSync(profilesFile, backup); - } - resetProfileManager(); - } + // Point the singleton at a temp dir with an empty profiles file + let tempConfigDir = mkdtempSync(join(tmpdir(), 'boxel-test-empty-')); + writeFileSync( + join(tempConfigDir, 'profiles.json'), + JSON.stringify({ profiles: {}, activeProfile: null }), + ); + setProfileManager(tempConfigDir); + cleanupProfile = () => resetProfileManager(); + + assert.throws( + () => + resolveFactoryTargetRealm({ + targetRealmUrl, + realmServerUrl: null, + }), + (error: unknown) => + error instanceof FactoryEntrypointUsageError && + (error.message.includes('boxel profile add') || + error.message.includes('active Boxel profile')), + ); }); test('bootstrapFactoryTargetRealm creates the realm through the API', async function (assert) { diff --git a/packages/software-factory/tests/helpers/test-profile.ts b/packages/software-factory/tests/helpers/test-profile.ts index 91fc33ece01..0e1d33511fa 100644 --- a/packages/software-factory/tests/helpers/test-profile.ts +++ b/packages/software-factory/tests/helpers/test-profile.ts @@ -1,8 +1,11 @@ -import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs'; -import { homedir } from 'node:os'; +import { mkdirSync, mkdtempSync, writeFileSync } from 'node:fs'; +import { tmpdir } from 'node:os'; import { join } from 'node:path'; -import { resetProfileManager } from '@cardstack/boxel-cli/api'; +import { + resetProfileManager, + setProfileManager, +} from '@cardstack/boxel-cli/api'; export interface TestProfileOptions { username: string; @@ -12,24 +15,15 @@ export interface TestProfileOptions { } /** - * Installs a fake Boxel CLI profile into the real ~/.boxel-cli/profiles.json. - * Backs up any existing file and resets the ProfileManager singleton so - * BoxelCLIClient picks up the test profile. + * Installs a fake Boxel CLI profile into an isolated temp directory. + * Replaces the ProfileManager singleton so BoxelCLIClient picks up the + * test profile without touching the real ~/.boxel-cli/profiles.json. * - * Returns a cleanup function that restores the original file and resets again. + * Returns a cleanup function that resets the singleton back to default. */ export function installTestProfile(options: TestProfileOptions): () => void { - let configDir = join(homedir(), '.boxel-cli'); - let profilesFile = join(configDir, 'profiles.json'); - - let backup: string | undefined; - if (existsSync(profilesFile)) { - backup = readFileSync(profilesFile, 'utf8'); - } - - if (!existsSync(configDir)) { - mkdirSync(configDir, { recursive: true }); - } + let tempConfigDir = mkdtempSync(join(tmpdir(), 'boxel-test-config-')); + mkdirSync(tempConfigDir, { recursive: true }); let profileId = `@${options.username}:localhost`; let config = { @@ -43,18 +37,13 @@ export function installTestProfile(options: TestProfileOptions): () => void { activeProfile: profileId, }; - writeFileSync(profilesFile, JSON.stringify(config, null, 2)); - resetProfileManager(); + writeFileSync( + join(tempConfigDir, 'profiles.json'), + JSON.stringify(config, null, 2), + ); + setProfileManager(tempConfigDir); return () => { - if (backup !== undefined) { - writeFileSync(profilesFile, backup); - } else { - writeFileSync( - profilesFile, - JSON.stringify({ profiles: {}, activeProfile: null }), - ); - } resetProfileManager(); }; } From 5b0b124edc1f16239bab80bd2fd2913767e6c157 Mon Sep 17 00:00:00 2001 From: Matic Jurglic Date: Thu, 16 Apr 2026 18:09:01 +0200 Subject: [PATCH 07/10] Rename setupHassanProfile to useTestProfile Co-Authored-By: Claude Opus 4.6 (1M context) --- .../tests/factory-entrypoint.test.ts | 6 +++--- .../tests/factory-target-realm.test.ts | 14 +++++++------- 2 files changed, 10 insertions(+), 10 deletions(-) diff --git a/packages/software-factory/tests/factory-entrypoint.test.ts b/packages/software-factory/tests/factory-entrypoint.test.ts index b129f8d213f..2d12a892c3c 100644 --- a/packages/software-factory/tests/factory-entrypoint.test.ts +++ b/packages/software-factory/tests/factory-entrypoint.test.ts @@ -47,7 +47,7 @@ module('factory-entrypoint', function (hooks) { cleanupProfile = undefined; }); - function setupHassanProfile() { + function useTestProfile() { cleanupProfile = installTestProfile({ username: 'hassan', matrixUrl: 'https://matrix.example.test/', @@ -161,7 +161,7 @@ module('factory-entrypoint', function (hooks) { }); test('runFactoryEntrypoint creates seed issue and loads brief data', async function (assert) { - setupHassanProfile(); + useTestProfile(); let summary = await runFactoryEntrypoint( { @@ -231,7 +231,7 @@ module('factory-entrypoint', function (hooks) { }); test('runFactoryEntrypoint uses the resolved realm server URL for darkfactory module', async function (assert) { - setupHassanProfile(); + useTestProfile(); let capturedDarkfactoryModuleUrl: string | undefined; diff --git a/packages/software-factory/tests/factory-target-realm.test.ts b/packages/software-factory/tests/factory-target-realm.test.ts index 7d8f2de957b..8fcb858d444 100644 --- a/packages/software-factory/tests/factory-target-realm.test.ts +++ b/packages/software-factory/tests/factory-target-realm.test.ts @@ -22,7 +22,7 @@ module('factory-target-realm', function (hooks) { cleanupProfile = undefined; }); - function setupHassanProfile() { + function useTestProfile() { cleanupProfile = installTestProfile({ username: 'hassan', matrixUrl: 'https://matrix.example.test/', @@ -32,7 +32,7 @@ module('factory-target-realm', function (hooks) { } test('resolveFactoryTargetRealm resolves owner from active profile', function (assert) { - setupHassanProfile(); + useTestProfile(); let resolution = resolveFactoryTargetRealm({ targetRealmUrl, @@ -49,7 +49,7 @@ module('factory-target-realm', function (hooks) { }); test('resolveFactoryTargetRealm accepts an explicit realm server URL override', function (assert) { - setupHassanProfile(); + useTestProfile(); let resolution = resolveFactoryTargetRealm({ targetRealmUrl: 'https://realms.example.test/boxel/hassan/personal/', @@ -63,7 +63,7 @@ module('factory-target-realm', function (hooks) { }); test('resolveFactoryTargetRealm rejects when target realm URL is missing', function (assert) { - setupHassanProfile(); + useTestProfile(); assert.throws( () => @@ -123,7 +123,7 @@ module('factory-target-realm', function (hooks) { }); test('bootstrapFactoryTargetRealm creates the realm through the API', async function (assert) { - setupHassanProfile(); + useTestProfile(); let resolution = resolveFactoryTargetRealm({ targetRealmUrl, @@ -148,7 +148,7 @@ module('factory-target-realm', function (hooks) { }); test('bootstrapFactoryTargetRealm reports when the realm already exists', async function (assert) { - setupHassanProfile(); + useTestProfile(); let resolution = resolveFactoryTargetRealm({ targetRealmUrl, @@ -168,7 +168,7 @@ module('factory-target-realm', function (hooks) { }); test('bootstrapFactoryTargetRealm uses the canonical realm URL returned by create-realm', async function (assert) { - setupHassanProfile(); + useTestProfile(); let resolution = resolveFactoryTargetRealm({ targetRealmUrl: 'https://realms.example.test/typed-by-user/personal/', From 4f01141869df6ecf646c28001561c8d01069c5f6 Mon Sep 17 00:00:00 2001 From: Matic Jurglic Date: Thu, 16 Apr 2026 18:26:59 +0200 Subject: [PATCH 08/10] Clean up temp dirs in tests, fix stale docs - installTestProfile cleanup now removes the temp config dir - Integration and spec tests clean up their temp HOME dirs in finally blocks - Update phase-1-plan.md to reflect profile-based realm server URL resolution Co-Authored-By: Claude Opus 4.6 (1M context) --- packages/software-factory/docs/phase-1-plan.md | 2 +- .../factory-entrypoint.integration.test.ts | 3 ++- .../tests/factory-target-realm.spec.ts | 17 ++++++++++------- .../tests/factory-target-realm.test.ts | 7 +++++-- .../tests/helpers/test-profile.ts | 5 +++-- 5 files changed, 21 insertions(+), 13 deletions(-) diff --git a/packages/software-factory/docs/phase-1-plan.md b/packages/software-factory/docs/phase-1-plan.md index b1086757de9..0fda08c26b7 100644 --- a/packages/software-factory/docs/phase-1-plan.md +++ b/packages/software-factory/docs/phase-1-plan.md @@ -128,7 +128,7 @@ Required behavior: Required behavior: - require an active Boxel profile so the target realm owner is explicit before bootstrap starts -- infer the target realm server URL from the target realm URL by default, but allow an explicit override when the realm server lives under a subdirectory and the URL shape is ambiguous +- use `--realm-server-url` when explicitly provided; otherwise take the realm server URL from the active Boxel profile rather than inferring it from the target realm URL - create missing target realms through the realm server `/_create-realm` API rather than by creating local directories directly - treat the successful `/_create-realm` response as the readiness boundary diff --git a/packages/software-factory/tests/factory-entrypoint.integration.test.ts b/packages/software-factory/tests/factory-entrypoint.integration.test.ts index 47a220f4f67..8a56c6bbf74 100644 --- a/packages/software-factory/tests/factory-entrypoint.integration.test.ts +++ b/packages/software-factory/tests/factory-entrypoint.integration.test.ts @@ -1,4 +1,4 @@ -import { mkdirSync, mkdtempSync, readFileSync, writeFileSync } from 'node:fs'; +import { mkdirSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from 'node:fs'; import { spawn, spawnSync } from 'node:child_process'; import { createServer } from 'node:http'; import { tmpdir } from 'node:os'; @@ -314,6 +314,7 @@ module('factory-entrypoint integration', function () { nextStep: 'all-issues-completed', }); } finally { + rmSync(tempHome, { recursive: true, force: true }); await new Promise((resolvePromise, reject) => server.close((error) => (error ? reject(error) : resolvePromise())), ); diff --git a/packages/software-factory/tests/factory-target-realm.spec.ts b/packages/software-factory/tests/factory-target-realm.spec.ts index 90aafd2290e..8598cfa1f4d 100644 --- a/packages/software-factory/tests/factory-target-realm.spec.ts +++ b/packages/software-factory/tests/factory-target-realm.spec.ts @@ -1,5 +1,5 @@ import { createServer } from 'node:http'; -import { mkdirSync, mkdtempSync, readFileSync, writeFileSync } from 'node:fs'; +import { mkdirSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from 'node:fs'; import { tmpdir } from 'node:os'; import { join, resolve } from 'node:path'; import type { AddressInfo } from 'node:net'; @@ -67,6 +67,13 @@ test('factory:go creates a target realm and bootstraps project artifacts end-to- realmServerURL, ).href; + let tempProfileHome = createTempProfileHome( + targetUsername, + targetPassword, + matrixURL, + realmServerURL, + ); + try { // The factory always runs the issue loop after seed creation. Without // OPENROUTER_API_KEY the loop will fail when it tries to invoke the LLM @@ -91,12 +98,7 @@ test('factory:go creates a target realm and bootstraps project artifacts end-to- cwd: packageRoot, env: { ...process.env, - HOME: createTempProfileHome( - targetUsername, - targetPassword, - matrixURL, - realmServerURL, - ), + HOME: tempProfileHome, }, timeoutMs: 120_000, }, @@ -144,6 +146,7 @@ test('factory:go creates a target realm and bootstraps project artifacts end-to- 'Process brief and create project artifacts', ); } finally { + rmSync(tempProfileHome, { recursive: true, force: true }); await new Promise((r, reject) => briefServer.close((err) => (err ? reject(err) : r())), ); diff --git a/packages/software-factory/tests/factory-target-realm.test.ts b/packages/software-factory/tests/factory-target-realm.test.ts index 8fcb858d444..113e716a4dc 100644 --- a/packages/software-factory/tests/factory-target-realm.test.ts +++ b/packages/software-factory/tests/factory-target-realm.test.ts @@ -1,4 +1,4 @@ -import { mkdtempSync, writeFileSync } from 'node:fs'; +import { mkdtempSync, rmSync, writeFileSync } from 'node:fs'; import { tmpdir } from 'node:os'; import { join } from 'node:path'; import { module, test } from 'qunit'; @@ -107,7 +107,10 @@ module('factory-target-realm', function (hooks) { JSON.stringify({ profiles: {}, activeProfile: null }), ); setProfileManager(tempConfigDir); - cleanupProfile = () => resetProfileManager(); + cleanupProfile = () => { + resetProfileManager(); + rmSync(tempConfigDir, { recursive: true, force: true }); + }; assert.throws( () => diff --git a/packages/software-factory/tests/helpers/test-profile.ts b/packages/software-factory/tests/helpers/test-profile.ts index 0e1d33511fa..84ab7a1d9a4 100644 --- a/packages/software-factory/tests/helpers/test-profile.ts +++ b/packages/software-factory/tests/helpers/test-profile.ts @@ -1,4 +1,4 @@ -import { mkdirSync, mkdtempSync, writeFileSync } from 'node:fs'; +import { mkdirSync, mkdtempSync, rmSync, writeFileSync } from 'node:fs'; import { tmpdir } from 'node:os'; import { join } from 'node:path'; @@ -19,7 +19,7 @@ export interface TestProfileOptions { * Replaces the ProfileManager singleton so BoxelCLIClient picks up the * test profile without touching the real ~/.boxel-cli/profiles.json. * - * Returns a cleanup function that resets the singleton back to default. + * Returns a cleanup function that resets the singleton and removes the temp dir. */ export function installTestProfile(options: TestProfileOptions): () => void { let tempConfigDir = mkdtempSync(join(tmpdir(), 'boxel-test-config-')); @@ -45,5 +45,6 @@ export function installTestProfile(options: TestProfileOptions): () => void { return () => { resetProfileManager(); + rmSync(tempConfigDir, { recursive: true, force: true }); }; } From 849cd92e849153a296896bd07921e7d2a1e4dad5 Mon Sep 17 00:00:00 2001 From: Matic Jurglic Date: Fri, 17 Apr 2026 09:12:08 +0200 Subject: [PATCH 09/10] Lint fix --- .../tests/factory-entrypoint.integration.test.ts | 8 +++++++- .../software-factory/tests/factory-target-realm.spec.ts | 8 +++++++- .../software-factory/tests/factory-target-realm.test.ts | 5 ++++- 3 files changed, 18 insertions(+), 3 deletions(-) diff --git a/packages/software-factory/tests/factory-entrypoint.integration.test.ts b/packages/software-factory/tests/factory-entrypoint.integration.test.ts index 8a56c6bbf74..8675653ac91 100644 --- a/packages/software-factory/tests/factory-entrypoint.integration.test.ts +++ b/packages/software-factory/tests/factory-entrypoint.integration.test.ts @@ -1,4 +1,10 @@ -import { mkdirSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from 'node:fs'; +import { + mkdirSync, + mkdtempSync, + readFileSync, + rmSync, + writeFileSync, +} from 'node:fs'; import { spawn, spawnSync } from 'node:child_process'; import { createServer } from 'node:http'; import { tmpdir } from 'node:os'; diff --git a/packages/software-factory/tests/factory-target-realm.spec.ts b/packages/software-factory/tests/factory-target-realm.spec.ts index 8598cfa1f4d..3986489da34 100644 --- a/packages/software-factory/tests/factory-target-realm.spec.ts +++ b/packages/software-factory/tests/factory-target-realm.spec.ts @@ -1,5 +1,11 @@ import { createServer } from 'node:http'; -import { mkdirSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from 'node:fs'; +import { + mkdirSync, + mkdtempSync, + readFileSync, + rmSync, + writeFileSync, +} from 'node:fs'; import { tmpdir } from 'node:os'; import { join, resolve } from 'node:path'; import type { AddressInfo } from 'node:net'; diff --git a/packages/software-factory/tests/factory-target-realm.test.ts b/packages/software-factory/tests/factory-target-realm.test.ts index 113e716a4dc..e672286f696 100644 --- a/packages/software-factory/tests/factory-target-realm.test.ts +++ b/packages/software-factory/tests/factory-target-realm.test.ts @@ -3,7 +3,10 @@ import { tmpdir } from 'node:os'; import { join } from 'node:path'; import { module, test } from 'qunit'; -import { setProfileManager, resetProfileManager } from '@cardstack/boxel-cli/api'; +import { + setProfileManager, + resetProfileManager, +} from '@cardstack/boxel-cli/api'; import { FactoryEntrypointUsageError } from '../src/factory-entrypoint-errors'; import { From c92c920bdf217680f5219ec7e3a56eb745a316c3 Mon Sep 17 00:00:00 2001 From: Matic Jurglic Date: Fri, 17 Apr 2026 09:36:16 +0200 Subject: [PATCH 10/10] Revert changes to phase-1-plan.md Co-Authored-By: Claude Opus 4.6 (1M context) --- packages/software-factory/docs/phase-1-plan.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/software-factory/docs/phase-1-plan.md b/packages/software-factory/docs/phase-1-plan.md index 0fda08c26b7..babf239dfb7 100644 --- a/packages/software-factory/docs/phase-1-plan.md +++ b/packages/software-factory/docs/phase-1-plan.md @@ -127,8 +127,8 @@ Required behavior: Required behavior: -- require an active Boxel profile so the target realm owner is explicit before bootstrap starts -- use `--realm-server-url` when explicitly provided; otherwise take the realm server URL from the active Boxel profile rather than inferring it from the target realm URL +- require `MATRIX_USERNAME` so the target realm owner is explicit before bootstrap starts +- infer the target realm server URL from the target realm URL by default, but allow an explicit override when the realm server lives under a subdirectory and the URL shape is ambiguous - create missing target realms through the realm server `/_create-realm` API rather than by creating local directories directly - treat the successful `/_create-realm` response as the readiness boundary