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
28 changes: 28 additions & 0 deletions .claude/rules/env-vars.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
# Environment Variables

Environment variables are defined in `.env` (committed defaults) at the repo root.
For demo services, port/URL overrides go in `demo/.env.local` (gitignored), which is symlinked into `demo/api/`, `demo/admin/`, and `demo/site/`.
A root-level `.env.local` (also gitignored) can hold non-demo overrides (e.g. `MUI_LICENSE_KEY`).

## Adding a new environment variable

When adding a variable to `.env`, also update `set-ports.js` if applicable:

### Add to `PORT_VARS` in `set-ports.js` if:
- The variable holds a plain port number (e.g. `MY_SERVICE_PORT=1234`)

### Add to `URL_VARS` in `set-ports.js` if:
- The variable holds a URL, address, or any value that references a port variable
(e.g. `MY_SERVICE_URL=http://localhost:${MY_SERVICE_PORT}`)
- This includes HTTP/HTTPS URLs, host:port strings, comma-separated upstream lists, redirect URLs, etc.

### Do NOT add to `set-ports.js` if:
- The variable is a secret, credential, feature flag, or non-URL string
- The value does not contain or reference a port number

## Checklist when adding a new service

1. Add `MY_SERVICE_PORT=<default>` to `.env`
2. Add `'MY_SERVICE_PORT'` to `PORT_VARS` in `set-ports.js`
3. If you also add URL/address variables referencing that port, add them to `URL_VARS` in `set-ports.js`
4. Keep array ordering consistent with `.env` for readability
11 changes: 11 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -131,6 +131,17 @@ It is also possible to start specific microservices
pnpm exec dev-pm start @demo-api # (@demo-api|@demo-admin|@demo-site)
```

#### Port Offset (running multiple apps simultaneously)

All Comet DXP demo services share the same default ports (API: 4000, Admin: 8001, Site: 3000, etc.).
To run two instances at the same time, shift one app's ports by an integer offset:
Comment on lines +136 to +137
Copy link

Copilot AI Mar 11, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This sentence says “Admin: 8001”, but elsewhere in the README the Demo Admin is documented as available at http://localhost:8000/ (auth-proxy). In .env, AUTHPROXY_PORT is 8000 and ADMIN_PORT is 8001; the user-facing Admin URL is typically 8000. Consider clarifying which port is which (e.g., Admin via auth-proxy 8000 vs internal admin 8001) to avoid confusion.

Copilot uses AI. Check for mistakes.

pnpm run set-ports -- 100 # shift all ports by +100 (writes demo/.env.local)
pnpm run set-ports -- 0 # reset to default port values

Comment on lines +139 to +141
Copy link

Copilot AI Mar 11, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The command examples are formatted as an indented code block, while surrounding sections use fenced ```bash blocks. Consider switching these lines to a fenced block for consistency and proper syntax highlighting.

Copilot uses AI. Check for mistakes.
The script rewrites `demo/.env.local` with the new port and URL values.
Since `demo/.env.local` is symlinked into `demo/api/`, `demo/admin/`, and `demo/site/`, one command covers the whole demo application.

Copy link

Copilot AI Mar 11, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Port offset docs currently suggest you can run two demo stacks side-by-side after running set-ports, but docker-compose.yml still maps Mailpit to fixed host ports 1025/8025. Starting a second instance will fail unless those ports are also made configurable (and included in the offset) or the docs mention how to disable/adjust Mailpit for one instance.

Suggested change
**Mailpit and Docker note:** The `set-ports` script only affects services that read their configuration from `demo/.env.local` (API, Admin, Site, etc.). Mailpit, which is started via Docker, is still mapped to fixed host ports (typically `1025` for SMTP and `8025` for the web UI) in `docker-compose.yml`. When running multiple demo stacks at the same time, you must either adjust the Mailpit port mappings for one stack (e.g. by editing `docker-compose.yml`) or disable Mailpit for that stack to avoid host port conflicts.

Copilot uses AI. Check for mistakes.
#### Start Storybook

```bash
Expand Down
3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,8 @@
"install-agent-skills": "pnpm exec comet install-agent-skills --config agent-skills.json",
"setup:ci": "pnpm run copy-project-files",
"setup:download-oauth2-proxy": "dotenv -- sh -c 'pnpm exec comet download-oauth2-proxy -v $OAUTH2_PROXY_VERSION'",
"setup:download-mitmproxy": "dotenv -- sh -c 'pnpm exec comet download-mitmproxy -v $MITMPROXY_VERSION'"
"setup:download-mitmproxy": "dotenv -- sh -c 'pnpm exec comet download-mitmproxy -v $MITMPROXY_VERSION'",
"set-ports": "node set-ports.js"
Copy link

Copilot AI Mar 11, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The PR description mentions “npm run set-ports …”, but this repo is configured to use pnpm (packageManager: pnpm@…) and the docs use pnpm run set-ports. Consider updating the PR description (or any related documentation) so the advertised command matches the actual package manager used in this repo.

Copilot uses AI. Check for mistakes.
},
"devDependencies": {
"@changesets/cli": "^2.29.8",
Expand Down
194 changes: 194 additions & 0 deletions set-ports.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,194 @@
#!/usr/bin/env node
'use strict';

const fs = require('node:fs');
const path = require('node:path');

const ENV_FILE = path.join(__dirname, '.env');
const ENV_LOCAL_FILE = path.join(__dirname, 'demo', '.env.local');
Copy link

Copilot AI Mar 11, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The script writes the offset ports to demo/.env.local, but docker compose up (used by the demo-docker dev process) reads variable substitutions from the root .env by default. With a non-zero offset this can desync container ports (postgres/imgproxy/jaeger/valkey) from the app’s connection settings and prevent the demo from starting. Consider either generating an env file that docker compose actually consumes (e.g. root .env/.env.local) or adjusting the docker-compose startup command to load demo/.env.local (via --env-file or dotenv -e ... -- docker compose ...).

Suggested change
const ENV_LOCAL_FILE = path.join(__dirname, 'demo', '.env.local');
const ENV_LOCAL_FILE = path.join(__dirname, '.env');

Copilot uses AI. Check for mistakes.

// Port variables to offset
const PORT_VARS = [
'API_PORT',
'ADMIN_PORT',
'AUTHPROXY_PORT',
'SITE_PORT',
'IDP_PORT',
'POSTGRESQL_PORT',
'IMGPROXY_PORT',
'JAEGER_UI_PORT',
'JAEGER_OLTP_PORT',
];
Comment on lines +11 to +21
Copy link

Copilot AI Mar 11, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

VALKEY_PORT is used by docker-compose (valkey service binds 127.0.0.1:${VALKEY_PORT}:6379) and by the demo site at runtime, but it isn’t included in PORT_VARS. With a non-zero offset, two demo instances will still conflict on 6379 and the generated .env.local won’t keep valkey aligned with the container port. Add VALKEY_PORT to PORT_VARS (and ensure it’s also reflected wherever docker compose gets its env from).

Copilot uses AI. Check for mistakes.

// URL/address variables whose values reference port variables and must be rewritten
const URL_VARS = [
'IMGPROXY_URL',
'AUTHPROXY_URL',
'ADMIN_URL',
'ADMIN_URL_INTERNAL',
'API_URL',
'POST_LOGOUT_REDIRECT_URI',
'IDP_SSO_URL',
'IDP_JWKS_URI',
'IDP_END_SESSION_ENDPOINT',
'SITE_URL',
'OAUTH2_PROXY_OIDC_ISSUER_URL',
'OAUTH2_PROXY_UPSTREAMS',
'OAUTH2_PROXY_REDIRECT_URL',
'OAUTH2_PROXY_HTTP_ADDRESS',
'BREVO_REDIRECT_URL_FOR_IMPORT',
];
Comment on lines +10 to +40
Copy link

Copilot AI Mar 11, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

.claude/rules/env-vars.md asks to “Keep array ordering consistent with .env for readability”, but PORT_VARS/URL_VARS ordering doesn’t currently match the order in .env (e.g. postgres/imgproxy appear earlier in .env but later here). Reordering these arrays to follow .env would make future updates less error-prone.

Copilot uses AI. Check for mistakes.

/**
* Minimal .env parser: handles quoted values and strips inline comments.
* Does not expand variable references — that is done separately.
*/
function parseEnvFile(filePath) {
if (!fs.existsSync(filePath)) return {};
const env = {};
for (const line of fs.readFileSync(filePath, 'utf-8').split('\n')) {
const trimmed = line.trim();
if (!trimmed || trimmed.startsWith('#')) continue;
const eqIdx = trimmed.indexOf('=');
if (eqIdx === -1) continue;
const key = trimmed.slice(0, eqIdx).trim();
let value = trimmed.slice(eqIdx + 1);
if (value.startsWith('"') || value.startsWith("'")) {
const quote = value[0];
const endIdx = value.lastIndexOf(quote);
value = endIdx > 0 ? value.slice(1, endIdx) : value.slice(1);
} else {
// Strip trailing inline comment (space + #)
const commentIdx = value.indexOf(' #');
if (commentIdx !== -1) value = value.slice(0, commentIdx);
value = value.trim();
}
env[key] = value;
}
return env;
}

/**
* Expand $VAR, ${VAR}, and ${VAR:-default} references using the given map.
*/
function expandValue(value, env) {
return value.replace(/\$\{([^}]+)\}|\$([A-Za-z_][A-Za-z0-9_]*)/g, (match, braced, simple) => {
if (braced) {
const sepIdx = braced.indexOf(':-');
if (sepIdx !== -1) {
const name = braced.slice(0, sepIdx);
const def = braced.slice(sepIdx + 2);
return name in env ? String(env[name]) : def;
}
return braced in env ? String(env[braced]) : match;
}
return simple in env ? String(env[simple]) : match;
});
}

/**
* Iteratively expand all variable references in the map until stable.
* Handles chains like A=$B, B=$C, C=value.
*/
function expandAll(envMap) {
const result = Object.fromEntries(Object.entries(envMap).map(([k, v]) => [k, String(v)]));
let changed = true;
while (changed) {
changed = false;
for (const key of Object.keys(result)) {
if (!result[key].includes('$')) continue;
const expanded = expandValue(result[key], result);
if (expanded !== result[key]) {
result[key] = expanded;
changed = true;
}
}
}
return result;
}

/**
* Format a value for writing to a .env file.
* Values containing backslashes, quotes, or shell-special characters are double-quoted.
* Backslashes inside quoted values are escaped so dotenv round-trips them correctly.
*/
function formatValue(value) {
if (/[\\"|'`$^!;&<>(){}*?\s]/.test(value)) {
return `"${value.replace(/\\/g, '\\\\').replace(/"/g, '\\"')}"`;
}
return value;
}

// --- Main ---

const offset = parseInt(process.argv[2], 10);
if (isNaN(offset)) {
console.error('Usage: pnpm run set-ports -- <offset>');
console.error(' offset Integer added to every port number');
console.error('');
console.error('Examples:');
console.error(' pnpm run set-ports -- 100 # shift all ports by +100');
console.error(' pnpm run set-ports -- 0 # reset to values from .env');
process.exit(1);
}

const rawEnv = parseEnvFile(ENV_FILE);

// Read existing .env.local and preserve user-managed keys
const MANAGED_KEYS = new Set([...PORT_VARS, ...URL_VARS]);
const existingLocal = parseEnvFile(ENV_LOCAL_FILE);
const userEntries = Object.entries(existingLocal).filter(([k]) => !MANAGED_KEYS.has(k));

// Compute new port values
const newPorts = {};
for (const portVar of PORT_VARS) {
if (!(portVar in rawEnv)) {
console.error(`Error: ${portVar} is not defined in .env`);
process.exit(1);
}
const base = parseInt(rawEnv[portVar], 10);
if (isNaN(base)) {
console.error(`Error: ${portVar} in .env is not a valid integer ("${rawEnv[portVar]}")`);
process.exit(1);
}
newPorts[portVar] = base + offset;
Copy link

Copilot AI Mar 11, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ports are computed as base + offset without validating the resulting range. Offsets that produce ports <= 0 or > 65535 will generate an unusable .env.local and potentially confusing runtime failures. Consider validating the computed ports and exiting with a clear error when an invalid range is produced.

Suggested change
newPorts[portVar] = base + offset;
const computedPort = base + offset;
if (computedPort <= 0 || computedPort > 65535) {
console.error(
`Error: computed port for ${portVar} (${computedPort}) is outside the valid range 1–65535. ` +
'Check your base port and port offset configuration.',
);
process.exit(1);
}
newPorts[portVar] = computedPort;

Copilot uses AI. Check for mistakes.
}

// Resolve all URL/address variables using the new port values
const expanded = expandAll({ ...rawEnv, ...newPorts });

// Build .env.local content
const lines = ['# override for local env', ''];
if (userEntries.length > 0) {
lines.push('# Custom settings (preserved by set-ports.js — edit freely)', '');
for (const [k, v] of userEntries) lines.push(`${k}=${formatValue(v)}`);
lines.push('');
}
if (offset !== 0) {
lines.push(
`# Generated by set-ports.js (offset: ${offset >= 0 ? '+' : ''}${offset}) — do not edit manually`,
'',
'# Ports',
...PORT_VARS.map((v) => `${v}=${newPorts[v]}`),
'',
'# URLs',
...URL_VARS.filter((v) => v in expanded).map((v) => `${v}=${formatValue(expanded[v])}`),
'',
);
}

fs.writeFileSync(ENV_LOCAL_FILE, lines.join('\n'));

// Print summary
const maxLen = Math.max(...PORT_VARS.map((v) => v.length));
console.log(`\nPort offset: ${offset >= 0 ? '+' : ''}${offset}\n`);
for (const portVar of PORT_VARS) {
if (!(portVar in newPorts)) continue;
const base = parseInt(rawEnv[portVar], 10);
console.log(` ${portVar.padEnd(maxLen)} ${String(base).padStart(5)} → ${newPorts[portVar]}`);
}
if (offset === 0) {
console.log('\nPorts reset to .env defaults — generated section removed from demo/.env.local.');
} else {
console.log('\nWritten to demo/.env.local');
}
Loading