attaché api draft
This commit is contained in:
parent
0f3bc47164
commit
c3f32120fc
43
packages/attache/.cursorrules
Normal file
43
packages/attache/.cursorrules
Normal file
|
|
@ -0,0 +1,43 @@
|
|||
## 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.
|
||||
34
packages/attache/.gitignore
vendored
Normal file
34
packages/attache/.gitignore
vendored
Normal file
|
|
@ -0,0 +1,34 @@
|
|||
# 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
|
||||
8
packages/attache/README.md
Normal file
8
packages/attache/README.md
Normal file
|
|
@ -0,0 +1,8 @@
|
|||
# attaché 💼
|
||||
|
||||
Stores files in folders, with style.
|
||||
|
||||
## Quickstart
|
||||
|
||||
bun install
|
||||
bun dev
|
||||
19
packages/attache/package.json
Normal file
19
packages/attache/package.json
Normal file
|
|
@ -0,0 +1,19 @@
|
|||
{
|
||||
"name": "attache",
|
||||
"module": "index.ts",
|
||||
"type": "module",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"dev": "bun run --watch src/server.ts"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/bun": "latest"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"typescript": "^5"
|
||||
},
|
||||
"dependencies": {
|
||||
"hono": "^4.8.0",
|
||||
"nanoid": "^5.1.5"
|
||||
}
|
||||
}
|
||||
128
packages/attache/src/server.ts
Normal file
128
packages/attache/src/server.ts
Normal file
|
|
@ -0,0 +1,128 @@
|
|||
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'
|
||||
|
||||
const app = new Hono()
|
||||
const PROJECTS_DIR = './projects'
|
||||
const PROJECTS_FILE = join(PROJECTS_DIR, 'projects.json')
|
||||
|
||||
type Project = {
|
||||
id: string
|
||||
name: string
|
||||
createdAt: string
|
||||
}
|
||||
|
||||
app.get('/projects', async c => {
|
||||
const projects = await loadProjects()
|
||||
return c.json(projects)
|
||||
})
|
||||
|
||||
app.post('/projects', async c => {
|
||||
const { name } = await c.req.json()
|
||||
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])
|
||||
})
|
||||
|
||||
app.patch('/projects/:id', async c => {
|
||||
const id = c.req.param('id')
|
||||
const { name } = await c.req.json()
|
||||
const projects = await loadProjects()
|
||||
const proj = projects.find(p => p.id === id)
|
||||
if (!proj) return c.json({ error: 'Not found' }, 404)
|
||||
proj.name = name
|
||||
await saveProjects(projects)
|
||||
return c.json(proj)
|
||||
})
|
||||
|
||||
app.get('/project/:id', async c => {
|
||||
const id = c.req.param('id')
|
||||
const folder = join(PROJECTS_DIR, `project_${id}`)
|
||||
try {
|
||||
return c.json(await readdir(folder, { withFileTypes: true }))
|
||||
} catch {
|
||||
return c.json({ error: 'Not found' }, 404)
|
||||
}
|
||||
})
|
||||
|
||||
app.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)
|
||||
}
|
||||
})
|
||||
|
||||
app.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',
|
||||
},
|
||||
})
|
||||
})
|
||||
|
||||
app.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)
|
||||
}
|
||||
})
|
||||
|
||||
app.use('/public/*', serveStatic({ root: './' }))
|
||||
|
||||
async function loadProjects(): Promise<Project[]> {
|
||||
try {
|
||||
const file = Bun.file(PROJECTS_FILE)
|
||||
const text = await file.text()
|
||||
return JSON.parse(text)
|
||||
} catch {
|
||||
return []
|
||||
}
|
||||
}
|
||||
|
||||
async function saveProjects(projects: Project[]) {
|
||||
await Bun.write(PROJECTS_FILE, JSON.stringify(projects, null, 2))
|
||||
}
|
||||
|
||||
export default {
|
||||
port: 3000,
|
||||
fetch: app.fetch
|
||||
}
|
||||
28
packages/attache/tsconfig.json
Normal file
28
packages/attache/tsconfig.json
Normal file
|
|
@ -0,0 +1,28 @@
|
|||
{
|
||||
"compilerOptions": {
|
||||
// Environment setup & latest features
|
||||
"lib": ["ESNext"],
|
||||
"target": "ESNext",
|
||||
"module": "ESNext",
|
||||
"moduleDetection": "force",
|
||||
"jsx": "react-jsx",
|
||||
"allowJs": true,
|
||||
|
||||
// Bundler mode
|
||||
"moduleResolution": "bundler",
|
||||
"allowImportingTsExtensions": true,
|
||||
"verbatimModuleSyntax": true,
|
||||
"noEmit": true,
|
||||
|
||||
// Best practices
|
||||
"strict": true,
|
||||
"skipLibCheck": true,
|
||||
"noFallthroughCasesInSwitch": true,
|
||||
"noUncheckedIndexedAccess": true,
|
||||
|
||||
// Some stricter flags (disabled by default)
|
||||
"noUnusedLocals": false,
|
||||
"noUnusedParameters": false,
|
||||
"noPropertyAccessFromIndexSignature": false
|
||||
}
|
||||
}
|
||||
Loading…
Reference in New Issue
Block a user