diff --git a/.env.secrets.example b/.env.secrets.example
index 4f095e2..6512009 100644
--- a/.env.secrets.example
+++ b/.env.secrets.example
@@ -1,12 +1,5 @@
-# Required for Namecheap API DNS automation script.
-# Keep real values in .env.secrets (already gitignored).
+# Local-only secrets (kept out of git).
+# Copy to `.env.secrets` and add only what you need for your environment.
-NAMECHEAP_API_KEY=replace-me
-NAMECHEAP_API_USER=replace-me
-NAMECHEAP_USERNAME=replace-me
-NAMECHEAP_CLIENT_IP=replace-me
-NAMECHEAP_DOMAIN=yappybara.dev
-
-# Optional: defaults to obsecurus.github.io
-GITHUB_PAGES_USER=obsecurus
-# GITHUB_PAGES_CNAME_TARGET=obsecurus.github.io
+# Example:
+# ANTHROPIC_API_KEY=sk-ant-...
diff --git a/.gitignore b/.gitignore
index b6c44b4..72b0a3a 100644
--- a/.gitignore
+++ b/.gitignore
@@ -106,3 +106,6 @@ archive/
# Devcontainer local cert material
.devcontainer/certs/
+
+# Local-only helper scripts and secrets
+.local/
diff --git a/CLAUDE.md b/CLAUDE.md
index 3798e0f..73cfb35 100644
--- a/CLAUDE.md
+++ b/CLAUDE.md
@@ -41,7 +41,6 @@ make interview-test
- `site/` is deployed by `.github/workflows/pages.yml`
- custom domain target: `yappybara.dev`
-- DNS automation script: `scripts/configure-namecheap-github-pages.mjs`
## Release posture
diff --git a/README.md b/README.md
index 36fa269..9ba631e 100644
--- a/README.md
+++ b/README.md
@@ -65,17 +65,6 @@ The public site is in `site/` and deploys via `.github/workflows/pages.yml`.
Setup guide:
- `docs/github-pages.md`
-## Namecheap DNS automation
-
-Use the existing script to configure apex + `www` for GitHub Pages:
-
-```bash
-node scripts/configure-namecheap-github-pages.mjs
-node scripts/configure-namecheap-github-pages.mjs --apply
-```
-
-Required secrets are documented in `.env.secrets.example`.
-
## Repository layout
```text
diff --git a/docs/github-pages.md b/docs/github-pages.md
index 8790dd6..ef3b146 100644
--- a/docs/github-pages.md
+++ b/docs/github-pages.md
@@ -24,7 +24,7 @@ After Pages is enabled:
1. In `Settings -> Pages`, set custom domain to `yappybara.dev`
2. Enable HTTPS once DNS has propagated
-## 3) Namecheap DNS records
+## 3) DNS records
For GitHub Pages custom domain support:
@@ -34,39 +34,11 @@ For GitHub Pages custom domain support:
- `@ A 185.199.111.153`
- `www CNAME obsecurus.github.io`
-## 4) DNS automation script
-
-Dry run:
-
-```bash
-node scripts/configure-namecheap-github-pages.mjs
-```
-
-Apply:
-
-```bash
-node scripts/configure-namecheap-github-pages.mjs --apply
-```
-
-Required `.env.secrets` values:
-
-- `NAMECHEAP_API_KEY`
-- `NAMECHEAP_API_USER`
-- `NAMECHEAP_USERNAME`
-- `NAMECHEAP_CLIENT_IP`
-- `NAMECHEAP_DOMAIN` (defaults to `yappybara.dev`)
-
-Optional:
-
-- `GITHUB_PAGES_USER` (defaults to `obsecurus`)
-- `GITHUB_PAGES_CNAME_TARGET` (defaults to `${GITHUB_PAGES_USER}.github.io`)
-
-## 5) Verify
+## 4) Verify
- Pages URL: `https://obsecurus.github.io/yappybara/`
- Custom domain: `https://yappybara.dev`
If custom domain is still pending:
- verify DNS records,
-- verify Namecheap API whitelist for `NAMECHEAP_CLIENT_IP`,
- wait for DNS propagation.
diff --git a/interview-app/public/index.html b/interview-app/public/index.html
index 444d517..c9af34a 100644
--- a/interview-app/public/index.html
+++ b/interview-app/public/index.html
@@ -2249,6 +2249,10 @@
Parent Supervision Required
lastSpokenEndedAt: 0,
};
+const PREVIEW_BLOCK_MATCH_RE = /---\s*PREVIEW\s*---([\s\S]*?)---\s*END_PREVIEW\s*---/i;
+const PREVIEW_BLOCK_STRIP_RE = /---\s*PREVIEW\s*---[\s\S]*?---\s*END_PREVIEW\s*---/gi;
+const BUILD_PHASE_BLOCK_STRIP_RE = /---\s*BUILD(?:_|[\s-]+)PHASE\s*---[\s\S]*?---\s*END(?:_|[\s-]+)BUILD(?:_|[\s-]+)PHASE\s*---/gi;
+
// ===== DOM refs =====
const $chat = document.getElementById('chat');
const $input = document.getElementById('text-input');
@@ -3388,9 +3392,9 @@ Parent Supervision Required
if (buildPollTimer) return Promise.resolve();
// Strip markdown + emoji for speech (emoji still shows in chat, just not spoken)
const clean = text
- .replace(/---BUILD_PHASE---[\s\S]*?---END_BUILD_PHASE---/g, '')
+ .replace(BUILD_PHASE_BLOCK_STRIP_RE, '')
// Avoid speaking preview boilerplate; STT can hear it and create loops.
- .replace(/---PREVIEW---[\s\S]*?---END_PREVIEW---/g, '')
+ .replace(PREVIEW_BLOCK_STRIP_RE, '')
.replace(/---INTERVIEW_COMPLETE---[\s\S]*---END_INTERVIEW---/g, 'I\'ve put together a summary of your app idea!')
.replace(/[#*_`\[\]()>]/g, '')
.replace(/[\p{Emoji_Presentation}\p{Extended_Pictographic}]/gu, '')
@@ -3635,10 +3639,10 @@ Parent Supervision Required
function sanitizeAiDisplayText(text) {
if (!text) return text;
// Never show build internals in chat bubbles.
- let cleaned = text.replace(/---BUILD_PHASE---[\s\S]*?---END_BUILD_PHASE---/g, '');
+ let cleaned = text.replace(BUILD_PHASE_BLOCK_STRIP_RE, '');
// Protect preview HTML from text sanitization so tags remain valid.
const previewBlocks = [];
- cleaned = cleaned.replace(/---PREVIEW---[\s\S]*?---END_PREVIEW---/g, (match) => {
+ cleaned = cleaned.replace(PREVIEW_BLOCK_STRIP_RE, (match) => {
previewBlocks.push(match);
return `__PREVIEW_${previewBlocks.length - 1}__`;
});
@@ -3703,9 +3707,9 @@ Parent Supervision Required
} else if (role === 'ai') {
// Extract and render preview blocks
let displayText = text;
- const previewMatch = text.match(/---PREVIEW---([\s\S]*?)---END_PREVIEW---/);
+ const previewMatch = text.match(PREVIEW_BLOCK_MATCH_RE);
if (previewMatch) {
- displayText = text.replace(/---PREVIEW---[\s\S]*?---END_PREVIEW---/, '').trim();
+ displayText = text.replace(PREVIEW_BLOCK_STRIP_RE, '').trim();
}
// Highlight the question so the kid knows what to answer
@@ -3853,6 +3857,27 @@ Meet Your Build Crew!
}
// ===== API Communication =====
+async function syncBuildProgressFromServer() {
+ try {
+ const resp = await fetch('/api/build-status');
+ if (!resp.ok) return false;
+ const data = await resp.json();
+
+ if (data.status === 'building') {
+ switchTab('dash');
+ startBuildPolling();
+ return true;
+ }
+
+ if (data.status === 'done' && data.previewUrl) {
+ updateBuildState('done', data.previewUrl.replace(/^\//, ''));
+ }
+ } catch (err) {
+ // Ignore transient polling errors and keep chat flow moving.
+ }
+ return false;
+}
+
async function sendMessage(text, imageInfo) {
if (state.isWaiting) return;
if (!text && !imageInfo) return;
@@ -3930,15 +3955,19 @@ Meet Your Build Crew!
setBartMood('excited');
setTimeout(() => { if (bartMood === 'excited') setBartMood('idle'); }, 500);
addMessage('ai', aiText);
- await speak(aiText);
- // If build phase was triggered, switch to Dash tab
+ let buildStarted = false;
if (data.buildPhase) {
- addMessage('ai', 'Dash is on it! Switch to the Dash tab to watch the progress and have fun while you wait!');
- setTimeout(() => {
- switchTab('dash');
- startBuildPolling();
- }, 1500);
+ addSystemMessage('Dash is on it! Opening Dash so you can watch the build.');
+ switchTab('dash');
+ startBuildPolling();
+ buildStarted = true;
+ } else {
+ buildStarted = await syncBuildProgressFromServer();
+ }
+
+ if (!buildStarted) {
+ await speak(aiText);
}
} catch (err) {
hideTyping();
@@ -5447,8 +5476,22 @@ What You Built
const sysMsg = document.querySelector('#chat .message.system');
if (sysMsg) sysMsg.remove();
addMessage('ai', chatData.response);
- await speak(chatData.response);
- if (state.recognition) {
+
+ let buildStarted = false;
+ if (chatData.buildPhase) {
+ addSystemMessage('Dash is on it! Opening Dash so you can watch the build.');
+ switchTab('dash');
+ startBuildPolling();
+ buildStarted = true;
+ } else {
+ buildStarted = await syncBuildProgressFromServer();
+ }
+
+ if (!buildStarted) {
+ await speak(chatData.response);
+ }
+
+ if (state.recognition && !buildPollTimer) {
state.micActive = true;
startListening();
}
@@ -5475,6 +5518,10 @@ What You Built
// Update build state
const buildStatus = data.buildStatus || 'idle';
updateBuildState(buildStatus, data.buildPath);
+ if (buildStatus === 'building') {
+ switchTab('dash');
+ startBuildPolling();
+ }
renderDeployStatus(data.deployRequest || null);
enterAppMode();
@@ -5819,7 +5866,20 @@ What You Built
refreshParentGuide();
state.messages.push({ role: 'assistant', content: data.response });
addMessage('ai', data.response);
- await speak(data.response);
+
+ let buildStarted = false;
+ if (data.buildPhase) {
+ addSystemMessage('Dash is on it! Opening Dash so you can watch the build.');
+ switchTab('dash');
+ startBuildPolling();
+ buildStarted = true;
+ } else {
+ buildStarted = await syncBuildProgressFromServer();
+ }
+
+ if (!buildStarted) {
+ await speak(data.response);
+ }
} catch(e) { hideTyping(); }
state.isWaiting = false;
setBartMood('idle');
diff --git a/interview-app/safety.js b/interview-app/safety.js
index d9622f4..d7ad070 100644
--- a/interview-app/safety.js
+++ b/interview-app/safety.js
@@ -105,6 +105,9 @@ export function sanitizePreviewHtml(html) {
return cleaned;
}
+const PREVIEW_BLOCK_RE = /---\s*PREVIEW\s*---([\s\S]*?)---\s*END_PREVIEW\s*---/gi;
+const BUILD_PHASE_BLOCK_RE = /---\s*BUILD(?:_|[\s-]+)PHASE\s*---([\s\S]*?)---\s*END(?:_|[\s-]+)BUILD(?:_|[\s-]+)PHASE\s*---/gi;
+
// Hard guardrail — runs on every Bart response before it reaches the kid
export function sanitizeBartResponse(text) {
if (!text) return text;
@@ -115,11 +118,11 @@ export function sanitizeBartResponse(text) {
// (they contain HTML/spec content which has legit tech words)
const previewBlocks = [];
const buildBlocks = [];
- let cleaned = text.replace(/---PREVIEW---([\s\S]*?)---END_PREVIEW---/g, (match) => {
+ let cleaned = text.replace(PREVIEW_BLOCK_RE, (match) => {
previewBlocks.push(match);
return `__PREVIEW_${previewBlocks.length - 1}__`;
});
- cleaned = cleaned.replace(/---BUILD_PHASE---([\s\S]*?)---END_BUILD_PHASE---/g, (match) => {
+ cleaned = cleaned.replace(BUILD_PHASE_BLOCK_RE, (match) => {
buildBlocks.push(match);
return `__BUILD_${buildBlocks.length - 1}__`;
});
diff --git a/interview-app/server.js b/interview-app/server.js
index 1c4b0ae..629f983 100644
--- a/interview-app/server.js
+++ b/interview-app/server.js
@@ -31,6 +31,9 @@ if (BUILDS_DIR !== LEGACY_BUILDS_DIR) {
}
const BUILD_NUDGE_MINUTES = 1; // After this many minutes, nudge Bart to build
+const PREVIEW_BLOCK_RE = /---\s*PREVIEW\s*---[\s\S]*?---\s*END_PREVIEW\s*---/gi;
+const BUILD_PHASE_BLOCK_RE = /---\s*BUILD(?:_|[\s-]+)PHASE\s*---([\s\S]*?)---\s*END(?:_|[\s-]+)BUILD(?:_|[\s-]+)PHASE\s*---/i;
+const BUILD_PHASE_BLOCK_RE_GLOBAL = /---\s*BUILD(?:_|[\s-]+)PHASE\s*---[\s\S]*?---\s*END(?:_|[\s-]+)BUILD(?:_|[\s-]+)PHASE\s*---/gi;
const REQUESTED_ASSIST_MODE = (process.env.YAPPY_MODE || 'auto').toLowerCase();
const VALID_ASSIST_MODES = new Set(['auto', 'claude', 'demo']);
const REQUESTED_BILLING_MODE = (process.env.YAPPY_BILLING_MODE || 'subscription').toLowerCase();
@@ -672,8 +675,8 @@ function userMessageTexts(messages) {
function cleanIdeaText(raw) {
return String(raw || '')
// Remove preview/build payloads if they somehow appear in message history.
- .replace(/---PREVIEW---[\s\S]*?---END_PREVIEW---/g, ' ')
- .replace(/---BUILD_PHASE---[\s\S]*?---END_BUILD_PHASE---/g, ' ')
+ .replace(PREVIEW_BLOCK_RE, ' ')
+ .replace(BUILD_PHASE_BLOCK_RE_GLOBAL, ' ')
// Remove html/xml tags to avoid parsing attribute values as app names.
.replace(/<[^>]*>/g, ' ')
.replace(/\s+/g, ' ')
@@ -1224,11 +1227,11 @@ app.post('/api/chat', async (req, res) => {
});
// Detect build phase trigger
- const buildMatch = responseText.match(/---BUILD_PHASE---([\s\S]*?)---END_BUILD_PHASE---/);
+ const buildMatch = responseText.match(BUILD_PHASE_BLOCK_RE);
const currentBuildStatus = activeApp.buildStatus || 'idle';
if (buildMatch && (currentBuildStatus === 'idle' || currentBuildStatus === 'done' || currentBuildStatus === 'failed')) {
const spec = buildMatch[1].trim();
- const displayText = responseText.replace(/---BUILD_PHASE---[\s\S]*?---END_BUILD_PHASE---/, '').trim();
+ const displayText = responseText.replace(BUILD_PHASE_BLOCK_RE_GLOBAL, '').trim();
logAction('build_phase', { spec: spec.slice(0, 500), appId: activeApp.id });
updateActiveApp({ buildStatus: 'building', spec });
diff --git a/interview-app/test.js b/interview-app/test.js
index 8975f42..0a884ce 100644
--- a/interview-app/test.js
+++ b/interview-app/test.js
@@ -582,6 +582,19 @@ Idea: Build with HTML and API calls.
assert.ok(cleaned.includes('---END_BUILD_PHASE---'));
});
+ it('restores flexible build phase marker variants', () => {
+ const raw = `Ready to build!
+
+--- build-phase ---
+App Name: Rocket Painter
+Idea: Paint stars and rockets.
+--- end build-phase ---`;
+ const cleaned = sanitizeBartResponse(raw);
+ assert.ok(cleaned.includes('--- build-phase ---'));
+ assert.ok(cleaned.includes('App Name: Rocket Painter'));
+ assert.ok(cleaned.includes('--- end build-phase ---'));
+ });
+
it('does not alter interview complete payload', () => {
const raw = `---INTERVIEW_COMPLETE---
# Dream App
@@ -766,6 +779,54 @@ describe('API Endpoints (integration — requires server)', () => {
assert.equal(r.status, 400);
});
+ it('POST /api/chat triggers build phase and publishes build status', async (t) => {
+ if (skipIfNoServer()) return t.skip('Server not available');
+
+ const createResp = await fetch(`${BASE}/api/apps`, {
+ method: 'POST',
+ headers: { 'Content-Type': 'application/json' },
+ body: JSON.stringify({ name: 'Build Trigger Test' }),
+ });
+ const { id } = await createResp.json();
+ await fetch(`${BASE}/api/apps/${id}/activate`, { method: 'PUT' });
+
+ const messages = [];
+ let buildTriggered = false;
+
+ for (let i = 1; i <= 7; i++) {
+ messages.push({ role: 'user', content: `Turn ${i}: I want a fun drawing app for kids.` });
+ const chatResp = await fetch(`${BASE}/api/chat`, {
+ method: 'POST',
+ headers: { 'Content-Type': 'application/json' },
+ body: JSON.stringify({ messages }),
+ });
+ assert.equal(chatResp.status, 200);
+ const chatData = await chatResp.json();
+ messages.push({ role: 'assistant', content: chatData.response });
+ if (chatData.buildPhase) {
+ buildTriggered = true;
+ break;
+ }
+ }
+
+ assert.equal(buildTriggered, true, 'Expected build phase to trigger in demo mode');
+
+ let buildStatus = null;
+ let previewUrl = null;
+ for (let i = 0; i < 15; i++) {
+ const statusResp = await fetch(`${BASE}/api/build-status`);
+ assert.equal(statusResp.status, 200);
+ const statusData = await statusResp.json();
+ buildStatus = statusData.status;
+ previewUrl = statusData.previewUrl || null;
+ if (buildStatus === 'done' || buildStatus === 'failed') break;
+ await new Promise(r => setTimeout(r, 200));
+ }
+
+ assert.equal(buildStatus, 'done');
+ assert.ok(previewUrl && previewUrl.startsWith('/builds/'));
+ });
+
it('GET /api/build-status returns idle when no active app', async (t) => {
if (skipIfNoServer()) return t.skip('Server not available');
await fetch(`${BASE}/api/start-over`, { method: 'POST' });
diff --git a/scripts/configure-namecheap-github-pages.mjs b/scripts/configure-namecheap-github-pages.mjs
deleted file mode 100755
index 2ba98d9..0000000
--- a/scripts/configure-namecheap-github-pages.mjs
+++ /dev/null
@@ -1,187 +0,0 @@
-#!/usr/bin/env node
-
-import fs from 'fs';
-import path from 'path';
-import process from 'process';
-
-const args = new Set(process.argv.slice(2));
-const shouldApply = args.has('--apply');
-
-const envFromFile = readEnvFile(path.resolve(process.cwd(), '.env.secrets'));
-const env = { ...envFromFile, ...process.env };
-
-const apiKey = normalizeSecret(env.NAMECHEAP_API_KEY || '');
-const apiUser = normalizeSecret(env.NAMECHEAP_API_USER || env.NAMECHEAP_USERNAME || '');
-const userName = normalizeSecret(env.NAMECHEAP_USERNAME || apiUser);
-const clientIp = normalizeSecret(env.NAMECHEAP_CLIENT_IP || '');
-const domain = normalizeSecret(env.NAMECHEAP_DOMAIN || env.DOMAIN || 'yappybara.dev');
-const githubPagesTarget = normalizeSecret(
- env.GITHUB_PAGES_CNAME_TARGET || `${normalizeSecret(env.GITHUB_PAGES_USER || 'obsecurus')}.github.io`
-);
-
-const missing = [];
-if (!apiKey) missing.push('NAMECHEAP_API_KEY');
-if (!apiUser) missing.push('NAMECHEAP_API_USER or NAMECHEAP_USERNAME');
-if (!userName) missing.push('NAMECHEAP_USERNAME');
-if (!clientIp) missing.push('NAMECHEAP_CLIENT_IP');
-
-if (missing.length > 0) {
- console.error('Missing required env values:');
- for (const key of missing) console.error(`- ${key}`);
- console.error('');
- console.error('Add them to .env.secrets or export them in your shell, then rerun.');
- process.exit(1);
-}
-
-const domainParts = splitDomain(domain);
-if (!domainParts) {
- console.error(`Could not parse domain: ${domain}`);
- process.exit(1);
-}
-
-const desiredRootRecords = [
- { Name: '@', Type: 'A', Address: '185.199.108.153', TTL: '1800' },
- { Name: '@', Type: 'A', Address: '185.199.109.153', TTL: '1800' },
- { Name: '@', Type: 'A', Address: '185.199.110.153', TTL: '1800' },
- { Name: '@', Type: 'A', Address: '185.199.111.153', TTL: '1800' },
- { Name: 'www', Type: 'CNAME', Address: githubPagesTarget, TTL: '1800' },
-];
-
-const apiBase = 'https://api.namecheap.com/xml.response';
-
-try {
- const currentHostsXml = await callNamecheap('namecheap.domains.dns.getHosts', {
- SLD: domainParts.sld,
- TLD: domainParts.tld,
- });
-
- const currentHosts = parseHostsFromXml(currentHostsXml);
-
- const preservedHosts = currentHosts.filter((record) => {
- const name = record.Name.toLowerCase();
- return name !== '@' && name !== 'www';
- });
-
- const finalHosts = [...desiredRootRecords, ...preservedHosts];
-
- console.log(`Planned DNS records for ${domain}:`);
- for (const record of finalHosts) {
- console.log(`- ${record.Name} ${record.Type} ${record.Address} (TTL ${record.TTL || '1800'})`);
- }
-
- if (!shouldApply) {
- console.log('');
- console.log('Dry run only. Re-run with --apply to push these records to Namecheap.');
- process.exit(0);
- }
-
- const setParams = {
- SLD: domainParts.sld,
- TLD: domainParts.tld,
- };
-
- finalHosts.forEach((record, index) => {
- const i = index + 1;
- setParams[`HostName${i}`] = record.Name;
- setParams[`RecordType${i}`] = record.Type;
- setParams[`Address${i}`] = record.Address;
- setParams[`TTL${i}`] = record.TTL || '1800';
- if (record.Type === 'MX') {
- setParams[`MXPref${i}`] = record.MXPref || '10';
- }
- });
-
- const setHostsXml = await callNamecheap('namecheap.domains.dns.setHosts', setParams);
- const ok = /IsSuccess="true"/i.test(setHostsXml) || /]*Status="OK"/i.test(setHostsXml);
-
- if (!ok) {
- console.error('Namecheap responded, but success could not be confirmed.');
- process.exit(1);
- }
-
- console.log('');
- console.log(`DNS update pushed for ${domain}.`);
- console.log('Next: verify the custom domain in GitHub Pages settings.');
-} catch (error) {
- console.error(`Namecheap API request failed: ${error.message}`);
- process.exit(1);
-}
-
-async function callNamecheap(command, extraParams) {
- const query = new URLSearchParams({
- ApiUser: apiUser,
- ApiKey: apiKey,
- UserName: userName,
- ClientIp: clientIp,
- Command: command,
- ...extraParams,
- });
-
- const response = await fetch(`${apiBase}?${query.toString()}`);
- const text = await response.text();
-
- if (!response.ok) {
- throw new Error(`HTTP ${response.status}`);
- }
-
- const apiError = text.match(/]*>([\s\S]*?)<\/Error>/i);
- if (apiError) {
- throw new Error(apiError[1].replace(/\s+/g, ' ').trim());
- }
-
- return text;
-}
-
-function splitDomain(raw) {
- const parts = raw.trim().toLowerCase().split('.').filter(Boolean);
- if (parts.length < 2) return null;
- return {
- sld: parts[0],
- tld: parts.slice(1).join('.'),
- };
-}
-
-function parseHostsFromXml(xml) {
- const hostTags = [...xml.matchAll(/]+?)\/?\s*>/gi)];
- const records = [];
-
- for (const [, attrBlob] of hostTags) {
- const attrs = Object.fromEntries([...attrBlob.matchAll(/(\w+)="([^"]*)"/g)].map((m) => [m[1], m[2]]));
-
- if (!attrs.Name || !attrs.Type || !attrs.Address) continue;
- records.push({
- Name: attrs.Name,
- Type: attrs.Type,
- Address: attrs.Address,
- TTL: attrs.TTL || '1800',
- MXPref: attrs.MXPref || '10',
- });
- }
-
- return records;
-}
-
-function readEnvFile(filePath) {
- const result = {};
- if (!fs.existsSync(filePath)) return result;
-
- const content = fs.readFileSync(filePath, 'utf8');
- for (const line of content.split(/\r?\n/)) {
- const trimmed = line.trim();
- if (!trimmed || trimmed.startsWith('#')) continue;
-
- const eqIndex = trimmed.indexOf('=');
- if (eqIndex <= 0) continue;
-
- const key = trimmed.slice(0, eqIndex).trim();
- const value = trimmed.slice(eqIndex + 1).trim();
- result[key] = normalizeSecret(value);
- }
- return result;
-}
-
-function normalizeSecret(value) {
- let output = String(value || '').trim();
- output = output.replace(/^["'“”‘’]+/, '').replace(/["'“”‘’]+$/, '');
- return output;
-}
diff --git a/site/index.html b/site/index.html
index d8ea410..abcea29 100644
--- a/site/index.html
+++ b/site/index.html
@@ -81,7 +81,7 @@ Public release posture
- MIT licensed
- CI-verified test suite
- - Domain automation script for Namecheap
+ - Registrar DNS setup docs included