From a58c42e0d4d31929feaaaaf74f2fba085bfa4340 Mon Sep 17 00:00:00 2001 From: Chris Wanstrath Date: Sun, 1 Feb 2026 23:04:08 -0800 Subject: [PATCH] dotenv support --- CLAUDE.md | 12 + apps/env/20260130-000000/.npmrc | 1 + apps/env/20260130-000000/bun.lock | 47 ++++ apps/env/20260130-000000/index.tsx | 332 +++++++++++++++++++++++++ apps/env/20260130-000000/package.json | 26 ++ apps/env/20260130-000000/tsconfig.json | 30 +++ apps/env/current | 1 + docs/ENV.md | 41 +++ src/cli/commands/env.ts | 87 +++++++ src/cli/commands/index.ts | 1 + src/cli/setup.ts | 24 ++ src/server/api/apps.ts | 109 +++++++- src/server/apps.ts | 34 ++- 13 files changed, 742 insertions(+), 3 deletions(-) create mode 100644 apps/env/20260130-000000/.npmrc create mode 100644 apps/env/20260130-000000/bun.lock create mode 100644 apps/env/20260130-000000/index.tsx create mode 100644 apps/env/20260130-000000/package.json create mode 100644 apps/env/20260130-000000/tsconfig.json create mode 120000 apps/env/current create mode 100644 docs/ENV.md create mode 100644 src/cli/commands/env.ts diff --git a/CLAUDE.md b/CLAUDE.md index 88e252a..e1fc55f 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -126,6 +126,18 @@ export default app.defaults - `DATA_DIR` env controls where apps are discovered - Path aliases: `$` → server, `@` → shared, `%` → lib +## Environment Variables + +Env vars are stored per-app in `TOES_DIR/env/`: + +``` +${DATA_DIR}/toes/env/ + clock.env # env vars for clock app + todo.env # env vars for todo app +``` + +`TOES_DIR` defaults to `${DATA_DIR}/toes`. Apps cannot access this directory directly. + ## Current State ### Infrastructure (Complete) diff --git a/apps/env/20260130-000000/.npmrc b/apps/env/20260130-000000/.npmrc new file mode 100644 index 0000000..6c57d5c --- /dev/null +++ b/apps/env/20260130-000000/.npmrc @@ -0,0 +1 @@ +registry=https://npm.nose.space diff --git a/apps/env/20260130-000000/bun.lock b/apps/env/20260130-000000/bun.lock new file mode 100644 index 0000000..57e3f6f --- /dev/null +++ b/apps/env/20260130-000000/bun.lock @@ -0,0 +1,47 @@ +{ + "lockfileVersion": 1, + "configVersion": 1, + "workspaces": { + "": { + "name": "env", + "dependencies": { + "@because/forge": "*", + "@because/hype": "*", + "@because/toes": "*", + }, + "devDependencies": { + "@types/bun": "latest", + }, + "peerDependencies": { + "typescript": "^5.9.2", + }, + }, + }, + "packages": { + "@because/forge": ["@because/forge@0.0.1", "https://npm.nose.space/@because/forge/-/forge-0.0.1.tgz", { "peerDependencies": { "typescript": "^5" } }, "sha512-QS5CK51gcWma91i4uECWe4HPJeNHcE+Af4SQHOcfEovyzOEa7VOTAjei+jIWr2i+abGWqQCEC9wIuFgPgyr2Bg=="], + + "@because/hype": ["@because/hype@0.0.2", "https://npm.nose.space/@because/hype/-/hype-0.0.2.tgz", { "dependencies": { "hono": "^4.10.4", "kleur": "^4.1.5" }, "peerDependencies": { "typescript": "^5" } }, "sha512-fdKeII6USGC1loVVj+tPz086cKz+Bm+XozNee3NOnK4VP+q4yNPP2Fq1Yujw5xeDYE+ZvJn40gKwlngRvmX2hA=="], + + "@because/toes": ["@because/toes@0.0.4", "https://npm.nose.space/@because/toes/-/toes-0.0.4.tgz", { "dependencies": { "@because/forge": "^0.0.1", "@because/hype": "^0.0.1", "commander": "^14.0.2", "diff": "^8.0.3", "kleur": "^4.1.5" }, "peerDependencies": { "typescript": "^5.9.2" }, "bin": { "toes": "src/cli/index.ts" } }, "sha512-/eZB84VoARYzSBtwJe00dV7Ilgqq7DRFj3vJlWhCHg87Jx5Yr2nTqPnzclLmiZ55XvWNogXqGTzyW8hApzXnJw=="], + + "@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.2.0", "https://npm.nose.space/@types/node/-/node-25.2.0.tgz", { "dependencies": { "undici-types": "~7.16.0" } }, "sha512-DZ8VwRFUNzuqJ5khrvwMXHmvPe+zGayJhr2CDNiKB1WBE1ST8Djl00D0IC4vvNmHMdj6DlbYRIaFE7WHjlDl5w=="], + + "bun-types": ["bun-types@1.3.8", "https://npm.nose.space/bun-types/-/bun-types-1.3.8.tgz", { "dependencies": { "@types/node": "*" } }, "sha512-fL99nxdOWvV4LqjmC+8Q9kW3M4QTtTR1eePs94v5ctGqU8OeceWrSUaRw3JYb7tU3FkMIAjkueehrHPPPGKi5Q=="], + + "commander": ["commander@14.0.3", "https://npm.nose.space/commander/-/commander-14.0.3.tgz", {}, "sha512-H+y0Jo/T1RZ9qPP4Eh1pkcQcLRglraJaSLoyOtHxu6AapkjWVCy2Sit1QQ4x3Dng8qDlSsZEet7g5Pq06MvTgw=="], + + "diff": ["diff@8.0.3", "https://npm.nose.space/diff/-/diff-8.0.3.tgz", {}, "sha512-qejHi7bcSD4hQAZE0tNAawRK1ZtafHDmMTMkrrIGgSLl7hTnQHmKCeB45xAcbfTqK2zowkM3j3bHt/4b/ARbYQ=="], + + "hono": ["hono@4.11.7", "https://npm.nose.space/hono/-/hono-4.11.7.tgz", {}, "sha512-l7qMiNee7t82bH3SeyUCt9UF15EVmaBvsppY2zQtrbIhl/yzBTny+YUxsVjSjQ6gaqaeVtZmGocom8TzBlA4Yw=="], + + "kleur": ["kleur@4.1.5", "https://npm.nose.space/kleur/-/kleur-4.1.5.tgz", {}, "sha512-o+NO+8WrRiQEE4/7nwRJhN1HWpVmJm511pBHUxPLtp0BUISzlBplORYSmTclCnJvQq2tKu/sgl3xVpkc7ZWuQQ=="], + + "typescript": ["typescript@5.9.3", "https://npm.nose.space/typescript/-/typescript-5.9.3.tgz", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw=="], + + "undici-types": ["undici-types@7.16.0", "https://npm.nose.space/undici-types/-/undici-types-7.16.0.tgz", {}, "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw=="], + + "@because/toes/@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=="], + } +} diff --git a/apps/env/20260130-000000/index.tsx b/apps/env/20260130-000000/index.tsx new file mode 100644 index 0000000..75da411 --- /dev/null +++ b/apps/env/20260130-000000/index.tsx @@ -0,0 +1,332 @@ +import { Hype } from '@because/hype' +import { define, stylesToCSS } from '@because/forge' +import { baseStyles, initScript, theme } from '@because/toes/tools' +import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'fs' +import { join } from 'path' +import type { Child } from 'hono/jsx' + +const TOES_DIR = process.env.TOES_DIR ?? join(process.env.HOME!, '.toes') +const ENV_DIR = join(TOES_DIR, 'env') + +const app = new Hype({ prettyHTML: false }) + +const Container = define('Container', { + fontFamily: theme('fonts-sans'), + padding: '20px', + paddingTop: 0, + maxWidth: '800px', + margin: '0 auto', + color: theme('colors-text'), +}) + +const Header = define('Header', { + marginBottom: '20px', + paddingBottom: '10px', + borderBottom: `2px solid ${theme('colors-border')}`, +}) + +const Title = define('Title', { + margin: 0, + fontSize: '24px', + fontWeight: 'bold', +}) + +const EnvList = define('EnvList', { + listStyle: 'none', + padding: 0, + margin: 0, + border: `1px solid ${theme('colors-border')}`, + borderRadius: theme('radius-md'), + overflow: 'hidden', +}) + +const EnvItem = define('EnvItem', { + padding: '12px 15px', + borderBottom: `1px solid ${theme('colors-border')}`, + display: 'flex', + alignItems: 'center', + justifyContent: 'space-between', + gap: '10px', + states: { + ':last-child': { + borderBottom: 'none', + }, + }, +}) + +const EnvKey = define('EnvKey', { + fontFamily: theme('fonts-mono'), + fontSize: '14px', + fontWeight: 'bold', + color: theme('colors-text'), +}) + +const EnvValue = define('EnvValue', { + fontFamily: theme('fonts-mono'), + fontSize: '14px', + color: theme('colors-textMuted'), + flex: 1, + overflow: 'hidden', + textOverflow: 'ellipsis', + whiteSpace: 'nowrap', +}) + +const EnvActions = define('EnvActions', { + display: 'flex', + gap: '8px', + flexShrink: 0, +}) + +const Button = define('Button', { + base: 'button', + padding: '6px 12px', + fontSize: '13px', + borderRadius: theme('radius-md'), + border: `1px solid ${theme('colors-border')}`, + backgroundColor: theme('colors-bgElement'), + color: theme('colors-text'), + cursor: 'pointer', + states: { + ':hover': { + backgroundColor: theme('colors-bgHover'), + }, + }, +}) + +const DangerButton = define('DangerButton', { + base: 'button', + padding: '6px 12px', + fontSize: '13px', + borderRadius: theme('radius-md'), + border: 'none', + backgroundColor: theme('colors-error'), + color: 'white', + cursor: 'pointer', + states: { + ':hover': { + opacity: 0.9, + }, + }, +}) + +const Form = define('Form', { + base: 'form', + display: 'flex', + gap: '10px', + marginTop: '15px', + padding: '15px', + backgroundColor: theme('colors-bgSubtle'), + borderRadius: theme('radius-md'), +}) + +const Input = define('Input', { + base: 'input', + flex: 1, + padding: '8px 12px', + fontSize: '14px', + fontFamily: theme('fonts-mono'), + borderRadius: theme('radius-md'), + border: `1px solid ${theme('colors-border')}`, + backgroundColor: theme('colors-bg'), + color: theme('colors-text'), + states: { + ':focus': { + outline: 'none', + borderColor: theme('colors-primary'), + }, + }, +}) + +const ErrorBox = define('ErrorBox', { + color: theme('colors-error'), + padding: '20px', + backgroundColor: theme('colors-bgElement'), + borderRadius: theme('radius-md'), + margin: '20px 0', +}) + +const EmptyState = define('EmptyState', { + padding: '30px', + textAlign: 'center', + color: theme('colors-textMuted'), + backgroundColor: theme('colors-bgSubtle'), + borderRadius: theme('radius-md'), +}) + +interface LayoutProps { + title: string + children: Child +} + +function Layout({ title, children }: LayoutProps) { + return ( + + + + + {title} + + + +