diff --git a/packages/attache/.cursorrules b/packages/attache/.cursorrules deleted file mode 100644 index 35bb9fa..0000000 --- a/packages/attache/.cursorrules +++ /dev/null @@ -1,43 +0,0 @@ -## Style - -- No semicolons — ever. -- No comments — ever. -- 2‑space indentation. -- Double quotes for strings. -- Trailing commas where ES5 allows (objects, arrays, imports). -- Keep lines <= 100 characters. -- End every file with a single newline. - -## TypeScript - -- This project runs on Bun. -- Assume `strict` mode is on (no implicit `any`). -- Prefer `const`; use `let` only when reassignment is required. -- Avoid the `any` type unless unavoidable. -- Use `import type { … }` when importing only types. -- In TypeScript files put npm imports first, then std imports, then library imports. - -## Tests - -Tests should use the toplevel `test()` and the `assert` library. - -Test files should always live in `test/` off the project root and be named `THING.test.ts` - -### Example Test - -``` -import { test } from "bun:test" -import assert from 'assert' - -test("1 + 2 = 3", () => { - assert.equal(1 + 2, 3) - assert.ok(true) -}) -``` - -## Assistant behaviour - -- Respond in a concise, direct tone. -- Do not ask follow‑up questions unless clarification is essential. - -Stay simple, readable, and stick to these rules. diff --git a/packages/attache/.gitignore b/packages/attache/.gitignore deleted file mode 100644 index b3526ad..0000000 --- a/packages/attache/.gitignore +++ /dev/null @@ -1,37 +0,0 @@ -# DATA! -projects/ - -# dependencies (bun install) -node_modules - -# output -out -dist -*.tgz - -# code coverage -coverage -*.lcov - -# logs -logs -_.log -report.[0-9]_.[0-9]_.[0-9]_.[0-9]_.json - -# dotenv environment variable files -.env -.env.development.local -.env.test.local -.env.production.local -.env.local - -# caches -.eslintcache -.cache -*.tsbuildinfo - -# IntelliJ based IDEs -.idea - -# Finder (MacOS) folder config -.DS_Store diff --git a/packages/attache/README.md b/packages/attache/README.md deleted file mode 100644 index ad6da1f..0000000 --- a/packages/attache/README.md +++ /dev/null @@ -1,10 +0,0 @@ -# 💼 Attaché - -Attaché provides a JSON API and web UI for creating and viewing Projects. - -Each Project is essentially a folder of files that you can upload, download, and preview. - -## Quickstart - - bun install - bun dev diff --git a/packages/attache/package.json b/packages/attache/package.json deleted file mode 100644 index 977152e..0000000 --- a/packages/attache/package.json +++ /dev/null @@ -1,21 +0,0 @@ -{ - "name": "attache", - "module": "src/server.tsx", - "type": "module", - "private": true, - "scripts": { - "dev": "bun run --hot src/server.tsx", - "start": "bun run src/server.tsx" - }, - "devDependencies": { - "@types/bun": "latest" - }, - "peerDependencies": { - "typescript": "^5" - }, - "dependencies": { - "hono": "catalog:", - "nanoid": "^5.1.5", - "@workshop/shared": "workspace:*" - } -} \ No newline at end of file diff --git a/packages/attache/src/server.tsx b/packages/attache/src/server.tsx deleted file mode 100644 index a107017..0000000 --- a/packages/attache/src/server.tsx +++ /dev/null @@ -1,503 +0,0 @@ -import { Hono } from 'hono' -import { serveStatic } from 'hono/bun' -import { basename, join } from 'path' -import { mkdir, readdir } from 'node:fs/promises' -import { nanoid } from 'nanoid' -import KV, { type Keys } from '@workshop/shared/kv' -import { Dirent } from 'node:fs' -import { html } from 'hono/html' - -declare global { - interface Window { - location: Location - } - var location: Location -} - -type Project = { - id: string - name: string - createdAt: string -} - -declare module "@workshop/shared/kv" { - interface Keys { - projects: Project[] - } -} - - -const app = new Hono() -const PROJECTS_DIR = './projects' - -app.get('/projects', async c => { - const projects = await loadProjects() - return c.html( - - - - ) -}) - -app.get('/project/:id', async c => { - const id = c.req.param('id') - const { project, files } = await loadProject(id, { files: true }) - if (!project) return c.json({ error: 'Not found' }, 404) - return c.html( - - - - ) -}) - -app.get('/new', async c => { - return c.html( - - - - ) -}) - -const api = new Hono() - -api.get('/projects', async c => { - const projects = await loadProjects() - return c.json(projects) -}) - -api.post('/projects', async c => { - const form = await c.req.formData() - const name = form.get('name') - if (!name) return c.json({ error: "Name is required" }, 400) - if (typeof name !== 'string') return c.json({ error: "Name must be a string" }, 400) - - const projects = await loadProjects() - - if (projects.some(p => p.name === name)) - return c.json({ error: "A project with this name already exists" }, 409) - - const id = nanoid(6) - projects.push({ id, name, createdAt: new Date().toISOString() }) - await mkdir(join(PROJECTS_DIR, `project_${id}`), { recursive: true }) - await saveProjects(projects) - return c.json(projects[projects.length - 1]) -}) - -api.patch('/projects/:id', async c => { - const id = c.req.param('id') - const { name } = await c.req.json() - const { project } = await loadProject(id) - if (!project) return c.json({ error: 'Not found' }, 404) - project.name = name - await saveProject(project) - return c.json(project) -}) - -api.get('/project/:id', async c => { - const id = c.req.param('id') - try { - const { project, files } = await loadProject(id, { files: true }) - return c.json({ files, project }) - } catch { - return c.json({ error: 'Not found' }, 404) - } -}) - -api.post('/project/:id/upload', async c => { - const id = c.req.param('id') - - if (id !== basename(id)) - return c.json({ error: 'Invalid project id' }, 400) - - const folder = join(PROJECTS_DIR, `project_${id}`) - try { - await mkdir(folder, { recursive: true }) - - const form = await c.req.formData() - const file = form.get('file') - if (!(file instanceof File)) - return c.json({ error: 'Invalid file' }, 400) - - - // strip any directory parts from the uploaded filename - const filename = basename(file.name) - const filepath = join(folder, filename) - await Bun.write(filepath, await file.arrayBuffer()) - - return c.json({ status: 'ok' }) - } catch (err) { - console.error('Upload error:', err) - return c.json({ error: 'Upload failed' }, 500) - } -}) - -api.get('/project/:id/file/:filename', async c => { - const { id, filename } = c.req.param() - const path = join(PROJECTS_DIR, `project_${id}`, filename) - const file = Bun.file(path) - if (!(await file.exists())) return c.json({ error: 'Not found' }, 404) - - return new Response(file, { - headers: { - 'Content-Type': file.type || 'application/octet-stream', - }, - }) -}) - -api.delete('/project/:id/file/:filename', async c => { - const { id, filename } = c.req.param() - const path = join(PROJECTS_DIR, `project_${id}`, filename) - try { - await Bun.file(path).delete() - return c.text('Deleted') - } catch { - return c.json({ error: 'Not found' }, 404) - } -}) - -api.delete("/project/:id", async c => { - const id = c.req.param("id") - const { project } = await loadProject(id) - if (!project) return c.json({ error: "Not found" }, 404) - - const projects = await loadProjects() - const index = projects.findIndex(p => p.id === id) - if (index !== -1) { - projects.splice(index, 1) - await saveProjects(projects) - } - return c.json({ status: "ok" }) -}) - -app.route('/api', api) -app.use('/public/*', serveStatic({ root: './' })) - -async function loadProject(id: string, { files }: { files?: boolean } = {}): Promise<{ project: Project | null, files?: string[] }> { - const projects = await loadProjects() - const project = projects.find(p => p.id === id) - if (!project) return { project: null } - - if (files) { - const folder = join(PROJECTS_DIR, `project_${id}`) - const files = (await readdir(folder, { withFileTypes: true })).filter(f => f.isFile()).map(f => f.name) - return { project, files } - } - - return { project } -} - -async function loadProjects(): Promise { - return (await KV.get("projects")) ?? [] -} - -async function saveProject(project: Project) { - const projects = await loadProjects() - const index = projects.findIndex(p => p.id === project.id) - if (index !== -1) { - projects[index] = project - } else { - projects.push(project) - } - await saveProjects(projects) -} - -async function saveProjects(projects: Project[]) { - await KV.set("projects", projects) -} - -const Projects = ({ projects }: { projects: Project[] }) => { - return ( -
-

Projects

- -
- ) -} - -const Project = ({ project, files }: { project: Project, files: string[] }) => { - return ( -
-
-

{project.name}

- -
-
-

Files

- -
    - {files.map(file => ( -
  • - {file} -
    - - Download - - -
    -
  • - ))} -
-
- {html` - - `} -
- ) -} - -const NewProject = () => { - return ( -
-

New Project

-
-
- - -
-
- {html` - - `} -
- ) -} - -const Layout = ({ children, title }: { children: any, title: string }) => { - return ( - - - {title} - 💼 Attaché - - - - - -
- -
{children}
-
- {html` - - `} - - - ) -} - -export default { - port: 3000, - fetch: app.fetch, - maxRequestBodySize: 1024 * 1024 * 1024, // 1GB -} \ No newline at end of file diff --git a/packages/attache/tsconfig.json b/packages/attache/tsconfig.json deleted file mode 100644 index 2d958c5..0000000 --- a/packages/attache/tsconfig.json +++ /dev/null @@ -1,30 +0,0 @@ -{ - "compilerOptions": { - // Environment setup & latest features - "lib": ["ESNext", "DOM", "DOM.Iterable"], - "target": "ESNext", - "module": "Preserve", - "moduleDetection": "force", - "jsx": "react-jsx", - "jsxImportSource": "hono/jsx", - "allowJs": true, - - // Bundler mode - "moduleResolution": "bundler", - "allowImportingTsExtensions": true, - "verbatimModuleSyntax": true, - "noEmit": true, - - // Best practices - "strict": true, - "skipLibCheck": true, - "noFallthroughCasesInSwitch": true, - "noUncheckedIndexedAccess": true, - "noImplicitOverride": true, - - // Some stricter flags (disabled by default) - "noUnusedLocals": false, - "noUnusedParameters": false, - "noPropertyAccessFromIndexSignature": false - } -}