From cd1a1cdbe51da7c2ccc55d15c4e5be75db8a5044 Mon Sep 17 00:00:00 2001 From: Chris Wanstrath Date: Fri, 30 Jan 2026 19:48:07 -0800 Subject: [PATCH] v0.0.4 --- ISSUES.md | 194 ------------------ apps/code/20260130-000000/bun.lock | 4 +- apps/code/20260130-000000/package.json | 2 +- .../code/20260130-000000/src/server/index.tsx | 112 ++-------- apps/todo/20260130-181927/bun.lock | 11 +- apps/todo/20260130-181927/package.json | 3 +- .../todo/20260130-181927/src/server/index.tsx | 118 +++-------- apps/versions/20260130-000000/bun.lock | 6 +- apps/versions/20260130-000000/index.tsx | 91 ++------ apps/versions/20260130-000000/package.json | 2 +- package.json | 9 +- src/cli/setup.ts | 2 +- src/client/themes/dark.ts | 4 + src/client/themes/light.ts | 4 + src/index.ts | 1 + src/tools/index.ts | 3 + src/tools/scripts.ts | 31 +++ 17 files changed, 130 insertions(+), 467 deletions(-) delete mode 100644 ISSUES.md create mode 100644 src/index.ts create mode 100644 src/tools/index.ts create mode 100644 src/tools/scripts.ts diff --git a/ISSUES.md b/ISSUES.md deleted file mode 100644 index 089b3fd..0000000 --- a/ISSUES.md +++ /dev/null @@ -1,194 +0,0 @@ -# Issues - Versioned Deployment Implementation - -## Critical Issues - -### 1. Watch Filter Breaks File Change Detection - -**Location**: `src/server/apps.ts:589-593` - -**Problem**: The watch logic ignores all changes deeper than 2 levels: - -```ts -// Ignore changes deeper than 2 levels (inside timestamp dirs) -if (parts.length > 2) return - -// For versioned apps, only care about changes to "current" directory -if (parts.length === 2 && parts[1] !== 'current' && parts[1] !== 'package.json') return -``` - -Files inside `current/` are 3 levels deep: `appname/current/somefile.ts` - -This **ignores all file changes** inside the current directory, breaking hot reload and auto-restart. - -**Fix**: -```ts -// Ignore changes inside old timestamp dirs (but allow current/) -if (parts.length > 2 && parts[1] !== 'current') return -``` - ---- - -### 2. App Restart Race Condition - -**Location**: `src/server/api/sync.ts:145-148` - -**Problem**: Activation uses arbitrary 1 second delay without confirming stop completed: - -```ts -// Restart app to use new version -const app = allApps().find(a => a.name === appName) -if (app?.state === 'running') { - stopApp(appName) - setTimeout(() => startApp(appName), 1000) // ⚠️ Arbitrary 1s delay -} -``` - -**Issues**: -- 1 second may not be enough for app to fully stop -- No confirmation that stop completed before start -- If app crashes during startup, activation still returns success - -**Fix**: Make activation async and wait for stop to complete, or move restart logic to after symlink succeeds and poll for stop completion. - ---- - -## Major Issues - -### 3. No Version Cleanup - -**Problem**: Old timestamp directories accumulate forever with no cleanup mechanism. - -**Impact**: Disk space grows indefinitely as deployments pile up. - -**Recommendation**: Add cleanup logic: -- Keep last N versions (e.g., 5-10) -- Delete versions older than X days -- Expose as `toes clean ` command or automatic post-activation - ---- - -### 4. safePath Behavior Change - -**Location**: `src/server/api/sync.ts:14-21` - -**Problem**: Security model changed by resolving symlinks in base path: - -```ts -const canonicalBase = existsSync(base) ? realpathSync(base) : base -``` - -Previously paths were checked against literal base, now against resolved base. This changes behavior if someone creates a symlink attack. - -**Recommendation**: Document this intentional change, or keep original check and add separate symlink resolution only where needed. - ---- - -### 5. Non-Atomic Deploy Copy - -**Location**: `src/server/api/sync.ts:133-141` - -**Problem**: Race condition possible during deploy: - -```ts -if (existsSync(currentLink)) { - const currentReal = realpathSync(currentLink) - cpSync(currentReal, newVersion, { recursive: true }) -} -``` - -If `current` changes between `existsSync` and `cpSync`, stale code might be copied. - -**Impact**: Low probability for single-user system, but possible during concurrent deploys. - -**Fix**: Read symlink once and reuse: `const currentReal = existsSync(currentLink) ? realpathSync(currentLink) : null` - ---- - -### 6. Upload Error Handling Inconsistency - -**Location**: `src/cli/commands/sync.ts:113-115` - -**Problem**: Upload continues if a file fails but doesn't track failures: - -```ts -if (success) { - console.log(` ${color.green('↑')} ${file}`) -} else { - console.log(` ${color.red('✗')} ${file}`) - // Continue even if one file fails -} -``` - -If any file fails to upload, deployment still activates with incomplete files. - -**Fix**: Either: -- Abort entire deployment on first failure -- Track failures and warn/abort before activating - ---- - -## Minor Issues - -### 7. POST Body Check Too Loose - -**Location**: `src/cli/http.ts:42-44` - -**Problem**: Falsy values like `0`, `false`, or `""` would incorrectly skip body: - -```ts -headers: body ? { 'Content-Type': 'application/json' } : undefined, -body: body ? JSON.stringify(body) : undefined, -``` - -**Fix**: -```ts -headers: body !== undefined ? { 'Content-Type': 'application/json' } : undefined, -body: body !== undefined ? JSON.stringify(body) : undefined, -``` - ---- - -### 8. Unconventional Timestamp Format - -**Location**: `src/server/api/sync.ts:115-123` - -**Problem**: Unusual array construction with separator as element: - -```ts -const timestamp = [ - now.getFullYear(), - String(now.getMonth() + 1).padStart(2, '0'), - String(now.getDate()).padStart(2, '0'), - '-', // Unusual to put separator as array element - String(now.getHours()).padStart(2, '0'), - String(now.getMinutes()).padStart(2, '0'), - String(now.getSeconds()).padStart(2, '0'), -].join('') -``` - -**Recommendation**: Use template literal: -```ts -const pad = (n: number) => String(n).padStart(2, '0') -const timestamp = `${now.getFullYear()}${pad(now.getMonth()+1)}${pad(now.getDate())}-${pad(now.getHours())}${pad(now.getMinutes())}${pad(now.getSeconds())}` -``` - -Or ISO format: `now.toISOString().replace(/[:.]/g, '-').slice(0, -5)` - ---- - -## Positive Points - -✓ Atomic symlink swapping with temp file pattern is correct -✓ Clear 3-step deployment protocol (deploy, upload, activate) -✓ Proper use of `realpathSync` to resolve canonical paths for running apps -✓ Good separation of concerns in API routes - ---- - -## Priority - -1. **BLOCKER**: Fix watch filter (#1) - breaks hot reload -2. **HIGH**: Fix restart race condition (#2) - affects deployment reliability -3. **HIGH**: Add version cleanup (#3) - disk space concern -4. **MEDIUM**: Fix upload error handling (#6) - data integrity -5. **LOW**: Everything else diff --git a/apps/code/20260130-000000/bun.lock b/apps/code/20260130-000000/bun.lock index fbb9f3f..8c6f095 100644 --- a/apps/code/20260130-000000/bun.lock +++ b/apps/code/20260130-000000/bun.lock @@ -5,9 +5,9 @@ "": { "name": "code", "dependencies": { - "@because/forge": "*", "@because/howl": "*", "@because/hype": "*", + "@because/toes": "link:../../..", }, "devDependencies": { "@types/bun": "latest", @@ -24,6 +24,8 @@ "@because/hype": ["@because/hype@0.0.1", "https://npm.nose.space/@because/hype/-/hype-0.0.1.tgz", { "dependencies": { "hono": "^4.10.4", "kleur": "^4.1.5" }, "peerDependencies": { "typescript": "^5" } }, "sha512-i92DNUXJOwt3J8dN1x8sh7i86blelcTCk8XDpwD839Ic8oe710lkDSVXJ7xYZb/i8YtzGhRg+L6eXDhaRiU2Pw=="], + "@because/toes": ["@because/toes@link:../../..", { "bin": { "toes": "src/cli/index.ts" } }], + "@types/bun": ["@types/bun@1.3.8", "https://npm.nose.space/@types/bun/-/bun-1.3.8.tgz", { "dependencies": { "bun-types": "1.3.8" } }, "sha512-3LvWJ2q5GerAXYxO2mffLTqOzEu5qnhEAlh48Vnu8WQfnmSwbgagjGZV6BoHKJztENYEDn6QmVd949W4uESRJA=="], "@types/node": ["@types/node@25.1.0", "https://npm.nose.space/@types/node/-/node-25.1.0.tgz", { "dependencies": { "undici-types": "~7.16.0" } }, "sha512-t7frlewr6+cbx+9Ohpl0NOTKXZNV9xHRmNOvql47BFJKcEG1CxtxlPEEe+gR9uhVWM4DwhnvTF110mIL4yP9RA=="], diff --git a/apps/code/20260130-000000/package.json b/apps/code/20260130-000000/package.json index 53d2806..c31c8d4 100644 --- a/apps/code/20260130-000000/package.json +++ b/apps/code/20260130-000000/package.json @@ -20,7 +20,7 @@ }, "dependencies": { "@because/hype": "*", - "@because/forge": "*", + "@because/toes": "link:../../..", "@because/howl": "*" } } diff --git a/apps/code/20260130-000000/src/server/index.tsx b/apps/code/20260130-000000/src/server/index.tsx index edafa96..e214233 100644 --- a/apps/code/20260130-000000/src/server/index.tsx +++ b/apps/code/20260130-000000/src/server/index.tsx @@ -1,5 +1,5 @@ import { Hype } from '@because/hype' -import { createThemes, define, stylesToCSS } from '@because/forge' +import { theme, define, stylesToCSS, initScript, baseStyles } from '@because/toes/tools' import { readdir, stat } from 'fs/promises' import { readFileSync } from 'fs' import { join, extname, basename } from 'path' @@ -9,53 +9,18 @@ const APPS_DIR = process.env.APPS_DIR! const app = new Hype({ prettyHTML: false }) -// Theme -const theme = createThemes({ - light: { - bg: '#ffffff', - text: '#1a1a1a', - textMuted: '#666666', - border: '#dddddd', - borderSubtle: '#eeeeee', - borderStrong: '#333333', - hover: '#f5f5f5', - surface: '#f5f5f5', - link: '#0066cc', - icon: '#666666', - codeBg: '#fafafa', - error: '#d32f2f', - errorBg: '#ffebee', - }, - dark: { - bg: '#1a1a1a', - text: '#e5e5e5', - textMuted: '#999999', - border: '#404040', - borderSubtle: '#333333', - borderStrong: '#555555', - hover: '#2a2a2a', - surface: '#252525', - link: '#5c9eff', - icon: '#888888', - codeBg: '#1e1e1e', - error: '#ff6b6b', - errorBg: '#3d1f1f', - }, -}) - -// Styles const Container = define('Container', { - fontFamily: '-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif', + fontFamily: theme('fonts-sans'), padding: '20px', maxWidth: '1200px', margin: '0 auto', - color: theme('text'), + color: theme('colors-text'), }) const Header = define('Header', { marginBottom: '20px', paddingBottom: '10px', - borderBottom: `2px solid ${theme('borderStrong')}`, + borderBottom: `2px solid ${theme('colors-border')}`, }) const Title = define('Title', { @@ -65,7 +30,7 @@ const Title = define('Title', { }) const Subtitle = define('Subtitle', { - color: theme('textMuted'), + color: theme('colors-textMuted'), fontSize: '18px', marginTop: '5px', }) @@ -74,20 +39,20 @@ const FileList = define('FileList', { listStyle: 'none', padding: 0, margin: '20px 0', - border: `1px solid ${theme('border')}`, - borderRadius: '4px', + border: `1px solid ${theme('colors-border')}`, + borderRadius: theme('radius-md'), overflow: 'hidden', }) const FileItem = define('FileItem', { padding: '10px 15px', - borderBottom: `1px solid ${theme('borderSubtle')}`, + borderBottom: `1px solid ${theme('colors-border')}`, states: { ':last-child': { borderBottom: 'none', }, ':hover': { - backgroundColor: theme('hover'), + backgroundColor: theme('colors-bgHover'), }, } }) @@ -95,7 +60,7 @@ const FileItem = define('FileItem', { const FileLink = define('FileLink', { base: 'a', textDecoration: 'none', - color: theme('link'), + color: theme('colors-link'), display: 'flex', alignItems: 'center', gap: '8px', @@ -111,48 +76,48 @@ const FileIcon = define('FileIcon', { width: '18px', height: '18px', flexShrink: 0, - fill: theme('icon'), + fill: theme('colors-textMuted'), }) const CodeBlock = define('CodeBlock', { margin: '20px 0', - border: `1px solid ${theme('border')}`, - borderRadius: '4px', + border: `1px solid ${theme('colors-border')}`, + borderRadius: theme('radius-md'), overflowX: 'auto', selectors: { '& pre': { margin: 0, padding: '15px', whiteSpace: 'pre', - backgroundColor: theme('codeBg'), + backgroundColor: theme('colors-bgSubtle'), }, '& pre code': { whiteSpace: 'pre', - fontFamily: 'monospace', + fontFamily: theme('fonts-mono'), }, }, }) const CodeHeader = define('CodeHeader', { padding: '10px 15px', - backgroundColor: theme('surface'), - borderBottom: `1px solid ${theme('border')}`, + backgroundColor: theme('colors-bgElement'), + borderBottom: `1px solid ${theme('colors-border')}`, fontWeight: 'bold', fontSize: '14px', }) const ErrorBox = define('ErrorBox', { - color: theme('error'), + color: theme('colors-error'), padding: '20px', - backgroundColor: theme('errorBg'), - borderRadius: '4px', + backgroundColor: theme('colors-bgElement'), + borderRadius: theme('radius-md'), margin: '20px 0', }) const BackLink = define('BackLink', { base: 'a', textDecoration: 'none', - color: theme('link'), + color: theme('colors-link'), display: 'inline-flex', alignItems: 'center', gap: '5px', @@ -176,37 +141,6 @@ const FileIconSvg = () => ( ) -const initScript = ` -(function() { - var theme = window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light'; - document.body.setAttribute('data-theme', theme); - window.matchMedia('(prefers-color-scheme: dark)').addEventListener('change', function(e) { - document.body.setAttribute('data-theme', e.matches ? 'dark' : 'light'); - }); - function sendHeight() { - // Measure the actual content container (skip script tags) - var container = document.querySelector('.Container'); - if (!container) return; - var rect = container.getBoundingClientRect(); - var height = rect.bottom + 20; // Add some padding - window.parent.postMessage({ type: 'resize-iframe', height: height }, '*'); - } - sendHeight(); - setTimeout(sendHeight, 50); - setTimeout(sendHeight, 200); - setTimeout(sendHeight, 500); - new ResizeObserver(sendHeight).observe(document.body); - window.addEventListener('load', sendHeight); -})(); -` - -const baseStyles = ` -body { - background: ${theme('bg')}; - margin: 0; -} -` - interface LayoutProps { title: string subtitle?: string @@ -233,10 +167,6 @@ function Layout({ title, subtitle, children, highlight }: LayoutProps) {