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

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 15 additions & 0 deletions .snyk
Original file line number Diff line number Diff line change
Expand Up @@ -76,3 +76,18 @@ ignore:
reason: 'Transitive dependency in Docusaurus; not exploitable in current usage.'
expires: '2026-06-28T00:00:00.000Z'
created: '2026-05-11T10:00:00.000Z'
'SNYK-JS-AI-16734889':
- '* > ai@5.0.105':
reason: 'Transitive dependency in @docusaurus/preset-classic; not exploitable in current usage.'
expires: '2026-06-18T00:00:00.000Z'
created: '2026-05-18T11:04:00.000Z'
'SNYK-JS-AISDKPROVIDERUTILS-16734888':
- '* > @ai-sdk/provider-utils@3.0.18':
reason: 'Transitive dependency in @docusaurus/preset-classic; not exploitable in current usage.'
expires: '2026-06-18T00:00:00.000Z'
created: '2026-05-18T11:04:00.000Z'
'SNYK-JS-AISDKPROVIDERUTILS-16735288':
- '* > @ai-sdk/provider-utils@3.0.18':
reason: 'Transitive dependency in @docusaurus/preset-classic; not exploitable in current usage.'
expires: '2026-06-18T00:00:00.000Z'
created: '2026-05-18T11:04:00.000Z'
7 changes: 5 additions & 2 deletions apps/api/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@
"build": "tsgo --build && rolldown -c rolldown.config.ts",
"predev": "pnpm run prepare:deploy && pnpm run sync-local-settings",
"dev": "pnpm exec portless data-access.ownercommunity.localhost --force node start-dev.mjs",
"predev:worktree": "pnpm run prepare:deploy && pnpm run sync-local-settings",
"dev:worktree": "pnpm exec portless data-access.ownercommunity.${WORKTREE_NAME}.localhost --force node start-dev.mjs",
"prepare:deploy": "cellix-prepare-func-deploy",
"watch": "tsgo --watch",
"test": "vitest run --silent --reporter=dot",
Expand All @@ -22,7 +24,7 @@
"prestart": "pnpm run prepare:deploy && pnpm run sync-local-settings",
"start": "func start --typescript --script-root deploy/",
"sync-local-settings": "node -e \"const fs=require('node:fs'); fs.mkdirSync('deploy',{recursive:true}); if (fs.existsSync('local.settings.json')) fs.copyFileSync('local.settings.json','deploy/local.settings.json');\"",
"azurite": "azurite-blob --silent --location ../../__blobstorage__ & azurite-queue --silent --location ../../__queuestorage__ & azurite-table --silent --location ../../__tablestorage__"
"azurite": "node start-azurite.mjs"
},
"dependencies": {
"@azure/functions": "catalog:",
Expand All @@ -31,8 +33,8 @@
"@ocom/application-services": "workspace:*",
"@ocom/context-spec": "workspace:*",
"@ocom/event-handler": "workspace:*",
"@ocom/graphql-handler": "workspace:*",
"@ocom/graphql": "workspace:*",
"@ocom/graphql-handler": "workspace:*",
"@ocom/persistence": "workspace:*",
"@ocom/rest": "workspace:*",
"@ocom/service-apollo-server": "workspace:*",
Expand All @@ -47,6 +49,7 @@
"@cellix/config-typescript": "workspace:*",
"@cellix/config-vitest": "workspace:*",
"@vitest/coverage-istanbul": "catalog:",
"azurite": "^3.35.0",
"rimraf": "catalog:",
"rolldown": "1.0.0-beta.55",
"typescript": "catalog:",
Expand Down
57 changes: 57 additions & 0 deletions apps/api/start-azurite.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
import { spawn } from 'node:child_process';
import net from 'node:net';
import { isGracefulInterruptExit } from '../../build-pipeline/scripts/dev-process-exit.mjs';
import { getAzuritePorts } from '../../build-pipeline/scripts/worktree-ports.mjs';

const ports = getAzuritePorts();
const worktreeName = process.env.WORKTREE_NAME ?? '';
const storageSuffix = worktreeName ? `-${worktreeName}` : '';

function isPortListening(port) {
return new Promise((resolve) => {
const socket = net.createConnection({ port, host: '127.0.0.1' });
socket.once('connect', () => {
socket.destroy();
resolve(true);
});
socket.once('error', () => {
socket.destroy();
resolve(false);
});
});
}

if (await isPortListening(ports.blob)) {
console.log(`[azurite] already running (blob port ${ports.blob}), skipping`);
process.exit(0);
}

const blobDir = `../../__blobstorage__${storageSuffix}`;
const queueDir = `../../__queuestorage__${storageSuffix}`;
const tableDir = `../../__tablestorage__${storageSuffix}`;

const procs = [
spawn('azurite-blob', ['--silent', '--blobPort', String(ports.blob), '--location', blobDir], { stdio: 'inherit' }),
spawn('azurite-queue', ['--silent', '--queuePort', String(ports.queue), '--location', queueDir], { stdio: 'inherit' }),
spawn('azurite-table', ['--silent', '--tablePort', String(ports.table), '--location', tableDir], { stdio: 'inherit' }),
];

let exited = 0;
for (const proc of procs) {
proc.on('exit', (code, signal) => {
if (isGracefulInterruptExit(signal, code)) {
if (++exited === procs.length) process.exit(0);
return;
}
console.error(`[azurite] process exited unexpectedly: code=${code} signal=${signal}`);
for (const p of procs) p.kill();
process.exit(code ?? 1);
});
}

process.on('SIGINT', () => {
for (const p of procs) p.kill('SIGINT');
});
process.on('SIGTERM', () => {
for (const p of procs) p.kill('SIGTERM');
});
21 changes: 20 additions & 1 deletion apps/api/start-dev.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@ import { spawn } from 'node:child_process';
import os from 'node:os';
import path from 'node:path';
import { isGracefulInterruptExit } from '../../build-pipeline/scripts/dev-process-exit.mjs';
import { buildPortlessUrl, getHostnames } from '../../build-pipeline/scripts/portless-hostnames.mjs';
import { getAzuriteConnectionString, getMongoConnectionString } from '../../build-pipeline/scripts/worktree-ports.mjs';

const envPort = process.env.PORT;

Expand All @@ -18,7 +20,24 @@ const childEnv = {
NODE_OPTIONS: `${process.env.NODE_OPTIONS ?? ''} --use-system-ca`.trim(),
};

const child = spawn('func', ['start', '--typescript', '--script-root', 'deploy/', '--port', envPort], {
// Only inject worktree-scoped overrides when running in worktree mode.
// When WORKTREE_NAME is absent, local.settings.json remains the source of truth.
// Use `??=` so callers (e.g. e2e harness with a MongoMemoryServer port) can override
// any individual value via process.env before invoking this script.
if (process.env.WORKTREE_NAME) {
const hostnames = getHostnames();
childEnv.ACCOUNT_PORTAL_OIDC_ISSUER ??= buildPortlessUrl(hostnames.mockAuth, '/community');
childEnv.ACCOUNT_PORTAL_OIDC_ENDPOINT ??= buildPortlessUrl(hostnames.mockAuth, '/community/.well-known/jwks.json');
childEnv.STAFF_PORTAL_OIDC_ISSUER ??= buildPortlessUrl(hostnames.mockAuth, '/staff');
childEnv.STAFF_PORTAL_OIDC_ENDPOINT ??= buildPortlessUrl(hostnames.mockAuth, '/staff/.well-known/jwks.json');
childEnv.COSMOSDB_CONNECTION_STRING ??= getMongoConnectionString();
childEnv.AZURE_STORAGE_CONNECTION_STRING ??= getAzuriteConnectionString();
childEnv.AzureWebJobsStorage ??= getAzuriteConnectionString();
// Disable the Node.js inspector — port 5858 is already used by the primary worktree.
childEnv.languageWorkers__node__arguments ??= '';
}

const child = spawn('func', ['start', '--typescript', '--script-root', 'deploy/', '--port', envPort, '--cors', '*'], {
stdio: 'inherit',
env: childEnv,
});
Expand Down
5 changes: 5 additions & 0 deletions apps/api/turbo.json
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,11 @@
"dependsOn": ["build"],
"interruptible": true,
"inputs": []
},
"dev:worktree": {
"dependsOn": ["build"],
"interruptible": true,
"inputs": []
}
}
}
1 change: 1 addition & 0 deletions apps/docs/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
"scripts": {
"docusaurus": "docusaurus",
"dev": "pnpm exec portless docs.ownercommunity.localhost --force node start-dev.mjs",
"dev:worktree": "pnpm exec portless docs.ownercommunity.${WORKTREE_NAME}.localhost --force node start-dev.mjs",
"start": "docusaurus start --port 3001",
"build": "docusaurus build",
"swizzle": "docusaurus swizzle",
Expand Down
6 changes: 6 additions & 0 deletions apps/docs/turbo.json
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,12 @@
"interruptible": false,
"inputs": [".env", "package.json", "start-dev.mjs", "docusaurus.config.ts", "sidebars.ts", "tsconfig.json"]
},
"dev:worktree": {
"dependsOn": [],
"persistent": true,
"interruptible": false,
"inputs": [".env", "package.json", "start-dev.mjs", "docusaurus.config.ts", "sidebars.ts", "tsconfig.json"]
},
"test": {
"inputs": ["$TURBO_EXTENDS$", "!docs/**", "!blog/**", "!static/**"]
},
Expand Down
3 changes: 2 additions & 1 deletion apps/server-mongodb-memory-mock/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,8 @@
"format": "biome format --write",
"format:check": "biome format .",
"start": "node dist/index.js",
"dev": "tsx src/index.ts"
"dev": "tsx src/index.ts",
"dev:worktree": "node start-mongo.mjs"
},
"dependencies": {
"@cellix/server-mongodb-memory-mock-seedwork": "workspace:*",
Expand Down
35 changes: 35 additions & 0 deletions apps/server-mongodb-memory-mock/start-mongo.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
import net from 'node:net';
import { getMongoPort } from '../../build-pipeline/scripts/worktree-ports.mjs';

const MONGO_PORT = getMongoPort();

function isPortListening(port) {
return new Promise((resolve) => {
const socket = net.createConnection({ port, host: '127.0.0.1' });
socket.once('connect', () => {
socket.destroy();
resolve(true);
});
socket.once('error', () => {
socket.destroy();
resolve(false);
});
});
}

if (await isPortListening(MONGO_PORT)) {
console.log(`[mongo-mock] already running on port ${MONGO_PORT}, skipping`);
process.exit(0);
}

// Not running — start it via tsx with the worktree-scoped port
const {
default: { spawn },
} = await import('node:child_process');
const child = spawn('tsx', ['src/index.ts'], {
stdio: 'inherit',
env: { ...process.env, PORT: String(MONGO_PORT) },
});
child.on('exit', (code) => {
process.exit(code ?? 1);
});
6 changes: 6 additions & 0 deletions apps/server-mongodb-memory-mock/turbo.json
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,12 @@
"persistent": true,
"interruptible": true,
"inputs": []
},
"dev:worktree": {
"dependsOn": ["build"],
"persistent": true,
"interruptible": true,
"inputs": []
}
}
}
1 change: 1 addition & 0 deletions apps/server-oauth2-mock/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
"format:check": "biome format .",
"start": "node dist/index.js",
"dev": "pnpm exec portless mock-auth.ownercommunity.localhost --force tsx src/index.ts",
"dev:worktree": "pnpm exec portless mock-auth.ownercommunity.${WORKTREE_NAME}.localhost --force node start-dev.mjs",
"test": "vitest run",
"test:coverage": "vitest run --coverage",
"test:watch": "vitest"
Expand Down
9 changes: 5 additions & 4 deletions apps/server-oauth2-mock/src/portal-discovery.ts
Original file line number Diff line number Diff line change
Expand Up @@ -119,16 +119,17 @@ function buildPortalFromConfig(config: MockOidcConfig, parsedEnv: Record<string,
const clientIdVar = config.envVars.clientId;
const redirectUriVar = config.envVars.redirectUri;

const clientId = parsedEnv[clientIdVar];
const redirectUri = parsedEnv[redirectUriVar];
// process.env takes precedence — allows worktree-scoped overrides injected at startup
const clientId = process.env[clientIdVar] ?? parsedEnv[clientIdVar];
const redirectUri = process.env[redirectUriVar] ?? parsedEnv[redirectUriVar];

if (!clientId) {
console.warn(`[server-oauth2-mock] Skipping ${entryName}: env var ${clientIdVar} not found in .env`);
console.warn(`[server-oauth2-mock] Skipping ${entryName}: env var ${clientIdVar} not found in .env or process.env`);
return null;
}

if (!redirectUri) {
console.warn(`[server-oauth2-mock] Skipping ${entryName}: env var ${redirectUriVar} not found in .env`);
console.warn(`[server-oauth2-mock] Skipping ${entryName}: env var ${redirectUriVar} not found in .env or process.env`);
return null;
}

Expand Down
26 changes: 26 additions & 0 deletions apps/server-oauth2-mock/start-dev.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import { spawn } from 'node:child_process';
import { isGracefulInterruptExit } from '../../build-pipeline/scripts/dev-process-exit.mjs';
import { buildPortlessUrl, getHostnames } from '../../build-pipeline/scripts/portless-hostnames.mjs';

const childEnv = { ...process.env };

if (process.env.WORKTREE_NAME) {
const hostnames = getHostnames();
childEnv.BASE_URL = buildPortlessUrl(hostnames.mockAuth);
// Override redirect URIs so portal-discovery picks up worktree-scoped URLs.
childEnv.VITE_APP_UI_COMMUNITY_B2C_REDIRECT_URI = buildPortlessUrl(hostnames.uiCommunity, '/auth-redirect');
childEnv.VITE_APP_UI_STAFF_AAD_REDIRECT_URI = buildPortlessUrl(hostnames.uiStaff, '/auth-redirect');
}

const child = spawn('tsx', ['src/index.ts'], {
stdio: 'inherit',
env: childEnv,
});

child.on('exit', (code, signal) => {
if (isGracefulInterruptExit(signal, code)) {
process.exitCode = 0;
return;
}
process.exitCode = code ?? 1;
});
6 changes: 6 additions & 0 deletions apps/server-oauth2-mock/turbo.json
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,12 @@
"interruptible": true,
"persistent": true,
"inputs": ["$TURBO_DEFAULT$", "../**/mock-oidc*.json"]
},
"dev:worktree": {
"dependsOn": ["build"],
"interruptible": true,
"persistent": true,
"inputs": ["$TURBO_DEFAULT$", "../**/mock-oidc*.json", "start-dev.mjs"]
}
}
}
11 changes: 11 additions & 0 deletions apps/ui-community/mock-oidc.users.json
Original file line number Diff line number Diff line change
Expand Up @@ -20,5 +20,16 @@
"family_name": "Doe",
"tid": "test-tenant-id"
}
},
{
"username": "owner@test.example",
"sub": "aaaaaaaa-bbbb-1ccc-9ddd-eeeeeeeeee01",
"password": "password",
"claims": {
"email": "owner@test.example",
"given_name": "Test",
"family_name": "Owner",
"tid": "test-tenant-id"
}
}
]
1 change: 1 addition & 0 deletions apps/ui-community/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
"prebuild": "pnpm run lint",
"build": "tsgo --build && vite build",
"dev": "pnpm exec portless ownercommunity.localhost --force vite",
"dev:worktree": "pnpm exec portless ownercommunity.${WORKTREE_NAME}.localhost --force node start-dev.mjs",
"start": "vite",
"preview": "vite preview",
"test": "vitest run --silent --reporter=dot",
Expand Down
27 changes: 27 additions & 0 deletions apps/ui-community/start-dev.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import { spawn } from 'node:child_process';
import { isGracefulInterruptExit } from '../../build-pipeline/scripts/dev-process-exit.mjs';
import { buildPortlessUrl, getHostnames } from '../../build-pipeline/scripts/portless-hostnames.mjs';

const childEnv = { ...process.env };

// Worktree-scoped overrides; plain `dev` leaves .env as the source of truth.
if (process.env.WORKTREE_NAME) {
const hostnames = getHostnames();
childEnv.VITE_APP_UI_COMMUNITY_B2C_AUTHORITY = buildPortlessUrl(hostnames.mockAuth, '/community');
childEnv.VITE_APP_UI_COMMUNITY_B2C_REDIRECT_URI = buildPortlessUrl(hostnames.uiCommunity, '/auth-redirect');
childEnv.VITE_COMMON_API_ENDPOINT = buildPortlessUrl(hostnames.api, '/api/graphql');
childEnv.VITE_APP_UI_COMMUNITY_BASE_URL = buildPortlessUrl(hostnames.uiCommunity);
}

const child = spawn('vite', ['--port', process.env.PORT, '--host', process.env.HOST ?? '127.0.0.1'], {
stdio: 'inherit',
env: childEnv,
});

child.on('exit', (code, signal) => {
if (isGracefulInterruptExit(signal, code)) {
process.exitCode = 0;
return;
}
process.exitCode = code ?? 1;
});
6 changes: 6 additions & 0 deletions apps/ui-community/turbo.json
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,12 @@
"persistent": true,
"interruptible": false,
"inputs": [".env", "package.json", "vite.config.ts", "tsconfig.json", "tsconfig.app.json", "tsconfig.node.json"]
},
"dev:worktree": {
"dependsOn": ["^build"],
"persistent": true,
"interruptible": false,
"inputs": [".env", "package.json", "start-dev.mjs", "vite.config.ts", "tsconfig.json", "tsconfig.app.json", "tsconfig.node.json"]
}
}
}
1 change: 1 addition & 0 deletions apps/ui-staff/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
"prebuild": "pnpm run lint",
"build": "tsgo --build && vite build",
"dev": "pnpm exec portless staff.ownercommunity.localhost --force vite",
"dev:worktree": "pnpm exec portless staff.ownercommunity.${WORKTREE_NAME}.localhost --force node start-dev.mjs",
"start": "vite",
"preview": "vite preview",
"test": "vitest run --silent --reporter=dot",
Expand Down
Loading