Compare commits

..

3 Commits

Author SHA1 Message Date
Jesse Vincent
9171439d2b Bump version to 6.0.0 2026-06-15 21:19:07 -07:00
Drew Ritter
529e192c32 Add visual companion Prime Radiant branding 2026-06-15 20:56:31 -07:00
Jesse Vincent
985434ddb0 Draft Superpowers 6 release notes 2026-06-15 20:27:43 -07:00
15 changed files with 424 additions and 67 deletions

View File

@@ -9,7 +9,7 @@
{
"name": "superpowers",
"description": "Core skills library for Claude Code: TDD, debugging, collaboration patterns, and proven techniques",
"version": "5.1.0",
"version": "6.0.0",
"source": "./",
"author": {
"name": "Jesse Vincent",

View File

@@ -1,7 +1,7 @@
{
"name": "superpowers",
"description": "Core skills library for Claude Code: TDD, debugging, collaboration patterns, and proven techniques",
"version": "5.1.0",
"version": "6.0.0",
"author": {
"name": "Jesse Vincent",
"email": "jesse@fsck.com"

View File

@@ -1,6 +1,6 @@
{
"name": "superpowers",
"version": "5.1.0",
"version": "6.0.0",
"description": "An agentic skills framework & software development methodology that works: planning, TDD, debugging, and collaboration workflows.",
"author": {
"name": "Jesse Vincent",

View File

@@ -2,7 +2,7 @@
"name": "superpowers",
"displayName": "Superpowers",
"description": "Core skills library: TDD, debugging, collaboration patterns, and proven techniques",
"version": "5.1.0",
"version": "6.0.0",
"author": {
"name": "Jesse Vincent",
"email": "jesse@fsck.com"

View File

@@ -1,6 +1,6 @@
{
"name": "superpowers",
"version": "5.1.0",
"version": "6.0.0",
"description": "An agentic skills framework and software development methodology.",
"author": {
"name": "Jesse Vincent",

View File

@@ -25,15 +25,9 @@ Next up, once you say "go", it launches a *subagent-driven-development* process,
There's a bunch more to it, but that's the core of the system. And because the skills trigger automatically, you don't need to do anything special. Your coding agent just has Superpowers.
## Commercial Services
## Sponsorship
If Superpowers has helped you do stuff that makes money and you are so inclined, I'd greatly appreciate it if you'd consider [sponsoring my opensource work](https://github.com/sponsors/obra).
Thanks!
\- Jesse
If you're using Superpowers in enterprise and could benefit from commercial support, additional tooling, or managed spending, please don't hesitate to drop us a line at sales@primeradiant.com.
## Installation
@@ -280,6 +274,10 @@ Superpowers updates are somewhat coding-agent dependent, but are often automatic
MIT License - see LICENSE file for details
## Visual companion telemetry
Because skills and plugins don't provide any feedback to creators, we have no idea how many of you are using Superpowers. By default, the Prime Radiant logo on brainstorming's optional visual companion feature is loaded from our website. It includes the version of Superpowers in use. It does not include any details about your project, prompt, or coding agent. We don't see your clicks or anything about what you're building. This helps us have a rough idea of how many folks are using Superpowers and which version of Superpowers they're using. It's 100% optional. To disable this, set the environment variable `SUPERPOWERS_DISABLE_TELEMETRY` to any true value. Superpowers also honors Claude Code's `DISABLE_TELEMETRY` and `CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC` opt-outs.
## Community
Superpowers is built by [Jesse Vincent](https://blog.fsck.com) and the rest of the folks at [Prime Radiant](https://primeradiant.com).

View File

@@ -1,6 +1,6 @@
{
"name": "superpowers",
"description": "Core skills library: TDD, debugging, collaboration patterns, and proven techniques",
"version": "5.1.0",
"version": "6.0.0",
"contextFileName": "GEMINI.md"
}

View File

@@ -1,6 +1,6 @@
{
"name": "superpowers",
"version": "5.1.0",
"version": "6.0.0",
"description": "Superpowers skills and runtime bootstrap for coding agents",
"type": "module",
"main": ".opencode/plugins/superpowers.js",

View File

@@ -9,7 +9,7 @@
*
* This template provides a consistent frame with:
* - OS-aware light/dark theming
* - Fixed header and selection indicator bar
* - Header branding and connection status
* - Scrollable main content area
* - CSS helpers for common UI patterns
*
@@ -63,34 +63,37 @@
}
/* ===== FRAME STRUCTURE ===== */
.header {
background: var(--bg-secondary);
padding: 0.5rem 1.5rem;
display: flex;
justify-content: space-between;
align-items: center;
border-bottom: 1px solid var(--border);
flex-shrink: 0;
.brand { display: flex; align-items: center; min-width: 0; overflow: hidden; color: var(--text-secondary); line-height: 1; }
.brand a { color: inherit; text-decoration: none; display: flex; align-items: center; gap: 0.5rem; min-width: 0; max-width: 100%; line-height: 1; }
.brand-copy { display: block; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; line-height: 1; transform: translateY(-1px); }
.brand-logo { display: block; height: 1em; width: auto; max-width: 180px; flex-shrink: 0; filter: invert(1); }
@media (prefers-color-scheme: dark) {
.brand-logo { filter: none; }
}
.header h1 { font-size: 0.85rem; font-weight: 500; color: var(--text-secondary); }
.header .status { font-size: 0.7rem; color: var(--status-color, var(--success)); display: flex; align-items: center; gap: 0.4rem; }
.header .status::before { content: ''; width: 6px; height: 6px; background: var(--status-color, var(--success)); border-radius: 50%; }
.status { font-size: 0.7rem; color: var(--status-color, var(--success)); display: flex; align-items: center; gap: 0.4rem; justify-self: end; white-space: nowrap; line-height: 1; }
.status::before { content: ''; width: 6px; height: 6px; background: var(--status-color, var(--success)); border-radius: 50%; }
.main { flex: 1; overflow-y: auto; }
#frame-content { padding: 2rem; min-height: 100%; }
.indicator-bar {
.header {
background: var(--bg-secondary);
border-top: 1px solid var(--border);
border-bottom: 1px solid var(--border);
padding: 0.5rem 1.5rem;
flex-shrink: 0;
text-align: center;
display: grid;
grid-template-columns: minmax(0, 1fr) auto;
align-items: center;
gap: 1rem;
min-height: 42px;
}
.indicator-bar span {
.header .brand { justify-self: start; width: 100%; font-size: 0.75rem; line-height: 1; }
.header .status { grid-column: 2; line-height: 1; }
.header span {
font-size: 0.75rem;
color: var(--text-secondary);
}
.indicator-bar .selected-text {
.header .selected-text {
color: var(--accent);
font-weight: 500;
}
@@ -196,7 +199,7 @@
</head>
<body>
<div class="header">
<h1><a href="https://github.com/obra/superpowers" style="color: inherit; text-decoration: none;">Superpowers Brainstorming</a></h1>
<!-- BRANDING -->
<div class="status">Connecting…</div>
</div>
@@ -206,9 +209,5 @@
</div>
</div>
<div class="indicator-bar">
<span id="indicator-text">Click an option above, then return to the terminal</span>
</div>
</body>
</html>

View File

@@ -138,21 +138,6 @@
id: target.id || null
});
// Update indicator bar (defer so toggleSelect runs first)
setTimeout(() => {
const indicator = document.getElementById('indicator-text');
if (!indicator) return;
const container = target.closest('.options') || target.closest('.cards');
const selected = container ? container.querySelectorAll('.selected') : [];
if (selected.length === 0) {
indicator.textContent = 'Click an option above, then return to the terminal';
} else if (selected.length === 1) {
const label = selected[0].querySelector('h3, .content h3, .card-body h3')?.textContent?.trim() || selected[0].dataset.choice;
indicator.innerHTML = '<span class="selected-text">' + label + ' selected</span> — return to terminal to continue';
} else {
indicator.innerHTML = '<span class="selected-text">' + selected.length + ' selected</span> — return to terminal to continue';
}
}, 0);
});
// Frame UI: selection tracking

View File

@@ -102,6 +102,14 @@ const URL_HOST = process.env.BRAINSTORM_URL_HOST || (HOST === '127.0.0.1' ? 'loc
const SESSION_DIR = process.env.BRAINSTORM_DIR || '/tmp/brainstorm';
const CONTENT_DIR = path.join(SESSION_DIR, 'content');
const STATE_DIR = path.join(SESSION_DIR, 'state');
const SUPERPOWERS_VERSION = readSuperpowersVersion();
const SUPERPOWERS_BRAND_IMAGE_URL = 'https://primeradiant.com/brand/superpowers-visual-brainstorming-logo.png';
const TELEMETRY_DISABLE_ENV_VARS = [
'SUPERPOWERS_DISABLE_TELEMETRY',
'DISABLE_TELEMETRY',
'CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC'
];
const SUPERPOWERS_TELEMETRY_DISABLED = TELEMETRY_DISABLE_ENV_VARS.some(name => isTruthyEnv(process.env[name]));
let ownerPid = process.env.BRAINSTORM_OWNER_PID ? Number(process.env.BRAINSTORM_OWNER_PID) : null;
// Per-session secret key. The companion is reachable by any local browser tab
@@ -150,14 +158,22 @@ const MIME_TYPES = {
// ========== Templates and Constants ==========
const WAITING_PAGE = `<!DOCTYPE html>
function waitingPage() {
return renderBranding(`<!DOCTYPE html>
<html>
<head><meta charset="utf-8"><title>Brainstorm Companion</title>
<style>body { font-family: system-ui, sans-serif; padding: 2rem; max-width: 800px; margin: 0 auto; }
h1 { color: #333; } p { color: #666; }</style>
<style>
body { font-family: system-ui, sans-serif; padding: 2rem; max-width: 800px; margin: 0 auto; }
h1 { color: #333; } p { color: #666; }
.brand { display: flex; align-items: center; min-width: 0; overflow: hidden; margin-bottom: 1.5rem; color: #666; font-size: 0.9rem; line-height: 1; }
.brand a { color: inherit; text-decoration: none; display: flex; align-items: center; gap: 0.5rem; min-width: 0; max-width: 100%; line-height: 1; }
.brand-copy { display: block; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; line-height: 1; transform: translateY(-1px); }
.brand-logo { display: block; height: 1em; width: auto; max-width: 180px; filter: invert(1); }
</style>
</head>
<body><h1>Brainstorm Companion</h1>
<p>Waiting for the agent to push a screen...</p></body></html>`;
<body><!-- BRANDING --><h1>Brainstorm Companion</h1>
<p>Waiting for the agent to push a screen...</p></body></html>`);
}
const FORBIDDEN_PAGE = `<!DOCTYPE html>
<html>
@@ -189,13 +205,55 @@ const helperInjection = '<script>\n' + helperScript + '\n</script>';
// ========== Helper Functions ==========
function readSuperpowersVersion() {
try {
const packageJson = JSON.parse(
fs.readFileSync(path.join(__dirname, '../../..', 'package.json'), 'utf-8')
);
return String(packageJson.version || 'unknown');
} catch (e) {
return 'unknown';
}
}
function isTruthyEnv(value) {
if (!value) return false;
const normalized = String(value).trim().toLowerCase();
if (!normalized) return false;
return !['0', 'false', 'no', 'off'].includes(normalized);
}
function escapeHtmlText(value) {
return String(value)
.replace(/&/g, '&amp;')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;')
.replace(/"/g, '&quot;');
}
function brandMarkup() {
const version = escapeHtmlText(SUPERPOWERS_VERSION);
const text = SUPERPOWERS_TELEMETRY_DISABLED
? 'Prime Radiant Superpowers v' + version
: 'Superpowers v' + version;
const logo = SUPERPOWERS_TELEMETRY_DISABLED
? ''
: '<img class="brand-logo" src="' + SUPERPOWERS_BRAND_IMAGE_URL + '?v=' + encodeURIComponent(SUPERPOWERS_VERSION) + '" alt="Prime Radiant" referrerpolicy="no-referrer" decoding="async">';
return '<div class="brand"><a href="https://github.com/obra/superpowers">' + logo + '<span class="brand-copy">' + text + '</span></a></div>';
}
function renderBranding(html) {
return html.split('<!-- BRANDING -->').join(brandMarkup());
}
function isFullDocument(html) {
const trimmed = html.trimStart().toLowerCase();
return trimmed.startsWith('<!doctype') || trimmed.startsWith('<html');
}
function wrapInFrame(content) {
return frameTemplate.replace('<!-- CONTENT -->', content);
return renderBranding(frameTemplate).replace('<!-- CONTENT -->', content);
}
function getNewestScreen() {
@@ -341,7 +399,7 @@ function handleRequest(req, res) {
const screenFile = getNewestScreen();
let html = screenFile
? (raw => isFullDocument(raw) ? raw : wrapInFrame(raw))(fs.readFileSync(screenFile, 'utf-8'))
: WAITING_PAGE;
: waitingPage();
if (html.includes('</body>')) {
html = html.replace('</body>', helperInjection + '\n</body>');

View File

@@ -28,7 +28,7 @@ A question *about* a UI topic is not automatically a visual question. "What kind
The server watches a directory for HTML files and serves the newest one to the browser. You write HTML content to `screen_dir`, the user sees it in their browser and can click to select options. Selections are recorded to `state_dir/events` that you read on your next turn.
**Content fragments vs full documents:** If your HTML file starts with `<!DOCTYPE` or `<html`, the server serves it as-is (just injects the helper script). Otherwise, the server automatically wraps your content in the frame template — adding the header, CSS theme, selection indicator, and all interactive infrastructure. **Write content fragments by default.** Only write full documents when you need complete control over the page.
**Content fragments vs full documents:** If your HTML file starts with `<!DOCTYPE` or `<html`, the server serves it as-is (just injects the helper script). Otherwise, the server automatically wraps your content in the frame template — adding the header, CSS theme, connection status, and all interactive infrastructure. **Write content fragments by default.** Only write full documents when you need complete control over the page.
## Starting a Session
@@ -138,7 +138,7 @@ Use `--url-host` to control what hostname is printed in the returned URL JSON.
## Writing Content Fragments
Write just the content that goes inside the page. The server wraps it in the frame template automatically (header, theme CSS, selection indicator, and all interactive infrastructure).
Write just the content that goes inside the page. The server wraps it in the frame template automatically (header, theme CSS, connection status, and all interactive infrastructure).
**Minimal example:**
@@ -184,7 +184,7 @@ The frame template provides these CSS classes for your content:
</div>
```
**Multi-select:** Add `data-multiselect` to the container to let users select multiple options. Each click toggles the item. The indicator bar shows the count.
**Multi-select:** Add `data-multiselect` to the container to let users select multiple options. Each click toggles the item's selected styling.
```html
<div class="options" data-multiselect>

View File

@@ -0,0 +1,309 @@
/**
* Tests for the visual companion's Superpowers/Prime Radiant branding.
*/
const { spawn } = require('child_process');
const http = require('http');
const fs = require('fs');
const path = require('path');
const assert = require('assert');
const REPO_ROOT = path.join(__dirname, '../..');
const SERVER_PATH = path.join(REPO_ROOT, 'skills/brainstorming/scripts/server.cjs');
const PACKAGE_VERSION = JSON.parse(
fs.readFileSync(path.join(REPO_ROOT, 'package.json'), 'utf-8')
).version;
const TOKEN = 'testtoken-branding-0123456789abcdef';
const ASSET_URL = 'https://primeradiant.com/brand/superpowers-visual-brainstorming-logo.png';
function cleanup(dir) {
if (fs.existsSync(dir)) {
fs.rmSync(dir, { recursive: true });
}
}
function sleep(ms) {
return new Promise(resolve => setTimeout(resolve, ms));
}
function startServer({ port, dir, env = {} }) {
cleanup(dir);
return spawn('node', [SERVER_PATH], {
env: {
...process.env,
BRAINSTORM_PORT: String(port),
BRAINSTORM_DIR: dir,
BRAINSTORM_TOKEN: TOKEN,
...env
}
});
}
function waitForServer(server) {
let stdout = '';
let stderr = '';
return new Promise((resolve, reject) => {
const timeout = setTimeout(() => reject(new Error(`Server did not start. stderr: ${stderr}`)), 5000);
server.stdout.on('data', (data) => {
stdout += data.toString();
if (stdout.includes('server-started')) {
clearTimeout(timeout);
resolve();
}
});
server.stderr.on('data', (data) => { stderr += data.toString(); });
server.on('error', reject);
});
}
function fetchHtml(port) {
return new Promise((resolve, reject) => {
const headers = { Cookie: `brainstorm-key-${port}=${TOKEN}` };
http.get(`http://localhost:${port}/`, { headers }, (res) => {
let body = '';
res.on('data', chunk => { body += chunk; });
res.on('end', () => resolve(body));
}).on('error', reject);
});
}
function writeFragment(dir) {
const contentDir = path.join(dir, 'content');
fs.mkdirSync(contentDir, { recursive: true });
fs.writeFileSync(path.join(contentDir, 'screen.html'), '<h2>Pick a layout</h2>');
}
async function withServer(options, fn) {
const server = startServer(options);
try {
await waitForServer(server);
await fn();
} finally {
if (server.exitCode === null && server.signalCode === null) {
server.kill();
await new Promise(resolve => server.once('exit', resolve));
}
await sleep(100);
cleanup(options.dir);
}
}
let passed = 0;
let failed = 0;
async function test(name, fn) {
try {
await fn();
console.log(` PASS: ${name}`);
passed++;
} catch (e) {
console.log(` FAIL: ${name}`);
console.log(` ${e.message}`);
failed++;
}
}
function assertBrandedWithLogo(html) {
assert(
html.includes(`Superpowers v${PACKAGE_VERSION}`),
'branding text should include dynamic package version'
);
assert(
!html.includes(`Superpowers v${PACKAGE_VERSION} by`),
'branding text should not include "by" when the logo is visible'
);
assert(
/<img class="brand-logo"[^>]*>\s*<span class="brand-copy">Superpowers v/.test(html),
'visible logo should appear before the Superpowers version text'
);
assert(
/\.brand a\s*\{[^}]*line-height:\s*1/i.test(html),
'brand row should align the logo and version text by their visual height'
);
assert(
/\.brand a\s*\{[^}]*gap:\s*0\.5rem/i.test(html),
'brand row should keep the logo and version text close together'
);
assert(
/\.brand a\s*\{[^}]*max-width:\s*100%/i.test(html),
'brand link should be constrained so it cannot overlap the status column'
);
assert(
/\.brand\s*\{[^}]*line-height:\s*1/i.test(html),
'brand wrapper should not inherit the page line height'
);
assert(
/\.brand\s*\{[^}]*overflow:\s*hidden/i.test(html),
'brand wrapper should clip before it reaches the status column'
);
}
function assertBrandedFallbackText(html) {
assert(
html.includes(`Prime Radiant Superpowers v${PACKAGE_VERSION}`),
'disabled telemetry should keep plain text Prime Radiant/Superpowers branding'
);
}
function assertTelemetryImage(html) {
const expectedUrl = `${ASSET_URL}?v=${encodeURIComponent(PACKAGE_VERSION)}`;
assert(html.includes(`src="${expectedUrl}"`), 'remote image should use the dedicated main-domain asset with only v=');
assert(!html.includes('event='), 'remote image URL must not include event=');
assert(!html.includes('surface='), 'remote image URL must not include surface=');
assert(!html.includes('launch_id='), 'remote image URL must not include launch_id=');
assert(!html.includes('lid='), 'remote image URL must not include lid=');
}
function assertLogoKeepsTransparentBackground(html) {
assert(
/\.brand-logo\s*\{[^}]*height:\s*1em/i.test(html),
'logo should match the surrounding brand text size'
);
assert(
/\.brand-logo\s*\{[^}]*display:\s*block/i.test(html),
'logo should not reserve inline-image descender space'
);
assert(
/\.brand-copy\s*\{[^}]*line-height:\s*1/i.test(html),
'version text should use the same compact line height as the logo'
);
assert(
/\.brand-copy\s*\{[^}]*min-width:\s*0/i.test(html),
'version text should be allowed to shrink inside the brand row'
);
assert(
/\.brand-copy\s*\{[^}]*transform:\s*translateY\(-1px\)/i.test(html),
'version text should compensate for bottom padding inside the logo asset'
);
assert(
/\.brand-logo\s*\{[^}]*filter:\s*invert\(1\)/i.test(html),
'white logo asset should invert on light backgrounds'
);
assert(
!/\.brand-logo\s*\{[^}]*background:/i.test(html),
'logo should keep its transparent background'
);
assert(
!/\.brand-logo\s*\{[^}]*padding:/i.test(html),
'logo should not rely on a padded backing'
);
}
function assertFramedLogoSupportsDarkTheme(html) {
assert(
/@media\s*\(prefers-color-scheme:\s*dark\)[\s\S]*\.brand-logo\s*\{[^}]*filter:\s*none/i.test(html),
'framed screens should leave the white logo unfiltered in dark mode'
);
}
function assertFramedScreenUsesBrandHeader(html) {
const logoCount = (html.match(/class="brand-logo"/g) || []).length;
assert.strictEqual(logoCount, 1, 'framed screens should render the logo only in the header');
assert(!html.includes('<div class="indicator-bar">'), 'framed screens should not render footer chrome');
assert(
/<div class="header">[\s\S]*<div class="brand">[\s\S]*<div class="status">Connecting…<\/div>/.test(html),
'header should contain branding and connection status'
);
assert(!html.includes('id="indicator-text"'), 'header should not render the selection indicator text');
assert(!html.includes('Click an option above'), 'header should not render the selection instruction');
}
function assertHeaderAvoidsNarrowOverlap(html) {
assert(
/grid-template-columns:\s*minmax\(0,\s*1fr\)\s*auto/i.test(html),
'header should allocate shrinkable space to branding before the status column'
);
assert(
/\.header \.status\s*\{[^}]*grid-column:\s*2/i.test(html),
'status should live in the final fixed-width grid column'
);
assert(
/\.header \.brand\s*\{[^}]*width:\s*100%/i.test(html),
'header brand should fill its grid track so overflow clipping prevents overlap'
);
}
async function main() {
console.log('\n--- Visual Companion Branding ---');
await test('framed screens render versioned Prime Radiant logo by default', async () => {
const port = 3451;
const dir = '/tmp/brainstorm-branding-default';
await withServer({ port, dir }, async () => {
writeFragment(dir);
await sleep(300);
const html = await fetchHtml(port);
assertBrandedWithLogo(html);
assertTelemetryImage(html);
assertLogoKeepsTransparentBackground(html);
assertFramedLogoSupportsDarkTheme(html);
assertFramedScreenUsesBrandHeader(html);
assertHeaderAvoidsNarrowOverlap(html);
});
});
await test('waiting screen renders versioned Prime Radiant logo by default', async () => {
const port = 3452;
const dir = '/tmp/brainstorm-branding-waiting';
await withServer({ port, dir }, async () => {
const html = await fetchHtml(port);
assert(html.includes('Waiting for the agent'), 'waiting page should still render');
assertBrandedWithLogo(html);
assertTelemetryImage(html);
assertLogoKeepsTransparentBackground(html);
});
});
await test('SUPERPOWERS_DISABLE_TELEMETRY=true omits remote image but keeps local branding', async () => {
const port = 3453;
const dir = '/tmp/brainstorm-branding-disabled';
await withServer({ port, dir, env: { SUPERPOWERS_DISABLE_TELEMETRY: 'true' } }, async () => {
writeFragment(dir);
await sleep(300);
const html = await fetchHtml(port);
assertBrandedFallbackText(html);
assert(!html.includes(ASSET_URL), 'disabled telemetry should omit the remote image');
});
});
await test('SUPERPOWERS_DISABLE_TELEMETRY=yes also omits the remote image on the waiting screen', async () => {
const port = 3454;
const dir = '/tmp/brainstorm-branding-disabled-waiting';
await withServer({ port, dir, env: { SUPERPOWERS_DISABLE_TELEMETRY: 'yes' } }, async () => {
const html = await fetchHtml(port);
assertBrandedFallbackText(html);
assert(!html.includes(ASSET_URL), 'disabled telemetry should omit the remote image');
});
});
await test('DISABLE_TELEMETRY=true omits remote image for Claude Code telemetry opt-out', async () => {
const port = 3455;
const dir = '/tmp/brainstorm-branding-claude-disable-telemetry';
await withServer({ port, dir, env: { DISABLE_TELEMETRY: 'true' } }, async () => {
writeFragment(dir);
await sleep(300);
const html = await fetchHtml(port);
assertBrandedFallbackText(html);
assert(!html.includes(ASSET_URL), 'Claude Code telemetry opt-out should omit the remote image');
});
});
await test('CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC=1 omits remote image for Claude Code traffic opt-out', async () => {
const port = 3456;
const dir = '/tmp/brainstorm-branding-claude-disable-nonessential';
await withServer({ port, dir, env: { CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC: '1' } }, async () => {
const html = await fetchHtml(port);
assertBrandedFallbackText(html);
assert(!html.includes(ASSET_URL), 'Claude Code non-essential traffic opt-out should omit the remote image');
});
});
console.log(`\n--- Results: ${passed} passed, ${failed} failed ---`);
if (failed > 0) process.exitCode = 1;
}
main().catch((err) => {
console.error('Test failed:', err);
process.exit(1);
});

View File

@@ -2,7 +2,7 @@
"name": "brainstorm-server-tests",
"version": "1.0.0",
"scripts": {
"test": "node ws-protocol.test.js && node helper.test.js && node browser-launcher.test.js && node auth.test.js && node server.test.js && node lifecycle.test.js && bash start-server.test.sh && bash stop-server.test.sh"
"test": "node ws-protocol.test.js && node helper.test.js && node browser-launcher.test.js && node auth.test.js && node branding.test.js && node server.test.js && node lifecycle.test.js && bash start-server.test.sh && bash stop-server.test.sh"
},
"dependencies": {
"ws": "^8.19.0"

View File

@@ -196,7 +196,7 @@ async function runTests() {
const res = await fetch(`http://localhost:${TEST_PORT}/`);
assert(res.body.includes('<h1>Custom Page</h1>'), 'Should contain original content');
assert(res.body.includes('WebSocket'), 'Should still inject helper.js');
assert(!res.body.includes('indicator-bar'), 'Should NOT wrap in frame template');
assert(!res.body.includes('<div class="header">'), 'Should NOT wrap in frame template');
});
await test('wraps content fragments in frame template', async () => {
@@ -205,7 +205,7 @@ async function runTests() {
await sleep(300);
const res = await fetch(`http://localhost:${TEST_PORT}/`);
assert(res.body.includes('indicator-bar'), 'Fragment should get indicator bar');
assert(res.body.includes('<div class="header">'), 'Fragment should get header chrome');
assert(!res.body.includes('<!-- CONTENT -->'), 'Placeholder should be replaced');
assert(res.body.includes('Pick a layout'), 'Fragment content should be present');
assert(res.body.includes('data-choice="a"'), 'Fragment interactive elements intact');
@@ -560,8 +560,16 @@ async function runTests() {
const template = fs.readFileSync(
path.join(__dirname, '../../skills/brainstorming/scripts/frame-template.html'), 'utf-8'
);
assert(template.includes('indicator-bar'), 'Should have indicator bar');
assert(template.includes('indicator-text'), 'Should have indicator text');
assert(template.includes('<div class="header">'), 'Should have top header markup');
assert(!template.includes('indicator-bar'), 'Should not have footer chrome');
assert(!template.includes('indicator-text'), 'Header should not render selection indicator text');
assert(template.includes('<!-- BRANDING -->'), 'Should have branding placeholder');
assert(template.includes('<div class="status">Connecting…</div>'), 'Header should include connection status');
assert(template.includes('grid-template-columns: minmax(0, 1fr) auto;'), 'Header should let brand text shrink before the status column');
assert(template.includes('padding: 0.5rem 1.5rem;'), 'Header should keep equal left and right edge padding');
assert(template.includes('.header .brand { justify-self: start; width: 100%; font-size: 0.75rem; line-height: 1; }'), 'Header brand should align left, fill its grid track, and match header text size');
assert(template.includes('.header .status { grid-column: 2; line-height: 1; }'), 'Header status should sit in the right column');
assert(!template.includes('<div></div>'), 'Header should not use an empty spacer before branding');
assert(template.includes('<!-- CONTENT -->'), 'Should have content placeholder');
assert(template.includes('frame-content'), 'Should have content container');
return Promise.resolve();