Compare commits

..

7 Commits

Author SHA1 Message Date
830cd4e45d info command 2026-01-29 13:01:50 -08:00
0f5fd50fec colors 2026-01-29 11:34:56 -08:00
84a341ebf9 log -f 2026-01-29 11:33:21 -08:00
e1b53fc54d toes logs 2026-01-29 11:28:22 -08:00
7763dc6314 toes open <app> 2026-01-29 11:24:09 -08:00
95d4348560 start adding cli 2026-01-28 22:36:23 -08:00
2d544e9bd3 new coding guidelines 2026-01-28 22:21:03 -08:00
9 changed files with 478 additions and 107 deletions

View File

@ -1,15 +1,18 @@
# Toes - Claude Code Guide # Toes - Claude Code Guide
## What It Is ## What It Is
Personal web server framework that auto-discovers and runs multiple web apps on your home network. "Set it up, turn it on, forget about the cloud." Personal web server framework that auto-discovers and runs multiple web apps on your home network. "Set it up, turn it on, forget about the cloud."
## How It Works ## How It Works
1. Host server scans `/apps` directory for valid apps 1. Host server scans `/apps` directory for valid apps
2. Valid app = has `package.json` with `scripts.toes` entry 2. Valid app = has `package.json` with `scripts.toes` entry
3. Each app spawned as child process with unique port (3001+) 3. Each app spawned as child process with unique port (3001+)
4. Dashboard UI shows all apps with current status, logs, and links 4. Dashboard UI shows all apps with current status, logs, and links
## Key Files ## Key Files
- `src/server/apps.ts` - **The heart**: app discovery, process management, lifecycle - `src/server/apps.ts` - **The heart**: app discovery, process management, lifecycle
- `src/server/index.tsx` - Entry point (minimal, just initializes Hype) - `src/server/index.tsx` - Entry point (minimal, just initializes Hype)
- `src/pages/index.tsx` - Dashboard UI - `src/pages/index.tsx` - Dashboard UI
@ -17,32 +20,107 @@ Personal web server framework that auto-discovers and runs multiple web apps on
- `TODO.md` - User-maintained task list (read this!) - `TODO.md` - User-maintained task list (read this!)
## Tech Stack ## Tech Stack
- **Bun** runtime (not Node) - **Bun** runtime (not Node)
- **Hype** (custom HTTP framework wrapping Hono) from git+https://git.nose.space/defunkt/hype - **Hype** (custom HTTP framework wrapping Hono) from git+https://git.nose.space/defunkt/hype
- **Forge** (typed CSS-in-JS) from git+https://git.nose.space/defunkt/forge - **Forge** (typed CSS-in-JS) from git+https://git.nose.space/defunkt/forge
- TypeScript + Hono JSX - TypeScript + Hono JSX
## Running ## Running
```bash ```bash
bun run --hot src/server/index.tsx # Dev mode with hot reload bun run --hot src/server/index.tsx # Dev mode with hot reload
``` ```
## App Structure ## App Structure
```tsx ```tsx
// apps/example/index.tsx // apps/example/index.tsx
import { Hype } from 'hype' import { Hype } from "hype"
const app = new Hype() const app = new Hype()
app.get('/', c => c.html(<h1>Content</h1>)) app.get("/", (c) => c.html(<h1>Content</h1>))
export default app.defaults export default app.defaults
``` ```
## Conventions ## Conventions
- Apps get `PORT` env var from host - Apps get `PORT` env var from host
- Each app is isolated process with own dependencies - Each app is isolated process with own dependencies
- No path-based routing - apps run on separate ports - No path-based routing - apps run on separate ports
- `DATA_DIR` env controls where apps are discovered - `DATA_DIR` env controls where apps are discovered
## Current State ## Current State
- Core infrastructure: ✓ Complete (discovery, spawn, watch, ports, UI) - Core infrastructure: ✓ Complete (discovery, spawn, watch, ports, UI)
- Apps: basic, profile (working); risk, tictactoe (empty) - Apps: basic, profile (working); risk, tictactoe (empty)
- Check TODO.md for planned features - Check TODO.md for planned features
## Coding Guidelines
TS files should be organized in the following way:
- imports
- re-exports
- const/lets
- enums
- interfaces
- types
- classes
- functions
- module init (top level function calls)
In each section, put the `export`s first, in alphabetical order.
Then, after the `export`s (if there were any), put everything else,
also in alphabetical order.
For single-line functions, use `const fn = () => {}` and put them in the
"functions" section of the file.
All other functions use the `function blah(){}` format.
Example:
```ts
import { code } from "coders"
import { something } from "somewhere"
export type { SomeType }
const RETRY_TIMES = 5
const WIDTH = 480
enum State {
Stopped,
Starting,
Running,
}
interface Config {
name: string
port: number
}
type Handler = (req: Request) => Response
class App {
config: Config
constructor(config: Config) {
this.config = config
}
}
const isApp = (name: string) =>
apps.has(name)
function createApp(name: string): App {
const app = new App({ name, port: 3000 })
apps.set(name, app)
return app
}
function start(app: App): void {
console.log(`Starting ${app.config.name}`)
}
```

View File

@ -18,9 +18,15 @@
## cli ## cli
[ ] `toes --help` [x] `toes --help`
[ ] `toes --version` [x] `toes --version`
[ ] `toes list` [x] `toes list`
[x] `toes start <app>`
[x] `toes stop <app>`
[x] `toes restart <app>`
[x] `toes open <app>`
[x] `toes logs <app>`
[x] `toes logs -f <app>`
[ ] `toes new` [ ] `toes new`
[ ] `toes pull` [ ] `toes pull`
[ ] `toes push` [ ] `toes push`

3
bin/toes Executable file
View File

@ -0,0 +1,3 @@
#!/usr/bin/env bun
import '../src/cli/index.ts'

View File

@ -5,8 +5,10 @@
"": { "": {
"name": "toes", "name": "toes",
"dependencies": { "dependencies": {
"commander": "^14.0.2",
"forge": "git+https://git.nose.space/defunkt/forge", "forge": "git+https://git.nose.space/defunkt/forge",
"hype": "git+https://git.nose.space/defunkt/hype", "hype": "git+https://git.nose.space/defunkt/hype",
"kleur": "^4.1.5",
}, },
"devDependencies": { "devDependencies": {
"@types/bun": "latest", "@types/bun": "latest",
@ -23,6 +25,8 @@
"bun-types": ["bun-types@1.3.7", "", { "dependencies": { "@types/node": "*" } }, "sha512-qyschsA03Qz+gou+apt6HNl6HnI+sJJLL4wLDke4iugsE6584CMupOtTY1n+2YC9nGVrEKUlTs99jjRLKgWnjQ=="], "bun-types": ["bun-types@1.3.7", "", { "dependencies": { "@types/node": "*" } }, "sha512-qyschsA03Qz+gou+apt6HNl6HnI+sJJLL4wLDke4iugsE6584CMupOtTY1n+2YC9nGVrEKUlTs99jjRLKgWnjQ=="],
"commander": ["commander@14.0.2", "", {}, "sha512-TywoWNNRbhoD0BXs1P3ZEScW8W5iKrnbithIl0YH+uCmBd0QpPOA8yc82DS3BIE5Ma6FnBVUsJ7wVUDz4dvOWQ=="],
"forge": ["forge@git+https://git.nose.space/defunkt/forge#debfd73ab22c50f66ccc93cb41164c234f78a920", { "peerDependencies": { "typescript": "^5" } }, "debfd73ab22c50f66ccc93cb41164c234f78a920"], "forge": ["forge@git+https://git.nose.space/defunkt/forge#debfd73ab22c50f66ccc93cb41164c234f78a920", { "peerDependencies": { "typescript": "^5" } }, "debfd73ab22c50f66ccc93cb41164c234f78a920"],
"hono": ["hono@4.11.7", "", {}, "sha512-l7qMiNee7t82bH3SeyUCt9UF15EVmaBvsppY2zQtrbIhl/yzBTny+YUxsVjSjQ6gaqaeVtZmGocom8TzBlA4Yw=="], "hono": ["hono@4.11.7", "", {}, "sha512-l7qMiNee7t82bH3SeyUCt9UF15EVmaBvsppY2zQtrbIhl/yzBTny+YUxsVjSjQ6gaqaeVtZmGocom8TzBlA4Yw=="],

View File

@ -14,7 +14,9 @@
"typescript": "^5.9.2" "typescript": "^5.9.2"
}, },
"dependencies": { "dependencies": {
"commander": "^14.0.2",
"forge": "git+https://git.nose.space/defunkt/forge",
"hype": "git+https://git.nose.space/defunkt/hype", "hype": "git+https://git.nose.space/defunkt/hype",
"forge": "git+https://git.nose.space/defunkt/forge" "kleur": "^4.1.5"
} }
} }

225
src/cli/index.ts Normal file
View File

@ -0,0 +1,225 @@
import { program } from 'commander'
import { join } from 'path'
import type { App, LogLine } from '@types'
import color from 'kleur'
import { APPS_DIR } from '$apps'
const HOST = `http://localhost:${process.env.PORT ?? 3000}`
const STATE_ICONS: Record<string, string> = {
running: color.green('●'),
starting: color.yellow('◎'),
stopped: color.gray('◯'),
invalid: color.red('◌'),
}
async function get<T>(url: string): Promise<T | undefined> {
try {
const res = await fetch(join(HOST, url))
if (!res.ok) throw new Error(`${res.status} ${res.statusText}`)
return await res.json()
} catch (error) {
console.error(error)
}
}
async function post<T, B = unknown>(url: string, body?: B): Promise<T | undefined> {
try {
const res = await fetch(join(HOST, url), {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(body),
})
if (!res.ok) throw new Error(`${res.status} ${res.statusText}`)
return await res.json()
} catch (error) {
console.error(error)
}
}
async function infoApp(name: string) {
const app: App | undefined = await get(`/api/apps/${name}`)
if (!app) {
console.error(`App not found: ${name}`)
return
}
const icon = STATE_ICONS[app.state] ?? '◯'
console.log(`${icon} ${color.bold(app.name)}`)
console.log(` State: ${app.state}`)
if (app.port) {
console.log(` Port: ${app.port}`)
console.log(` URL: http://localhost:${app.port}`)
}
if (app.started) {
const uptime = Date.now() - app.started
const seconds = Math.floor(uptime / 1000) % 60
const minutes = Math.floor(uptime / 60000) % 60
const hours = Math.floor(uptime / 3600000)
const parts = []
if (hours) parts.push(`${hours}h`)
if (minutes) parts.push(`${minutes}m`)
parts.push(`${seconds}s`)
console.log(` Uptime: ${parts.join(' ')}`)
}
if (app.error) console.log(` Error: ${color.red(app.error)}`)
}
async function listApps() {
const apps: App[] | undefined = await get('/api/apps')
if (!apps) return
for (const app of apps) {
console.log(`${STATE_ICONS[app.state] ?? '◯'} ${app.name}`)
}
}
const startApp = async (app: string) => {
await post(`/api/apps/${app}/start`)
}
const stopApp = async (app: string) => {
await post(`/api/apps/${app}/stop`)
}
const restartApp = async (app: string) => {
await post(`/api/apps/${app}/restart`)
}
const printLog = (line: LogLine) =>
console.log(`${new Date(line.time).toLocaleTimeString()} ${line.text}`)
async function logApp(name: string, options: { follow?: boolean }) {
if (options.follow) {
await tailLogs(name)
return
}
const logs: LogLine[] | undefined = await get(`/api/apps/${name}/logs`)
if (!logs) {
console.error(`App not found: ${name}`)
return
}
if (logs.length === 0) {
console.log('No logs yet')
return
}
for (const line of logs) {
printLog(line)
}
}
async function tailLogs(name: string) {
const url = join(HOST, `/api/apps/${name}/logs/stream`)
const res = await fetch(url)
if (!res.ok) {
console.error(`App not found: ${name}`)
return
}
if (!res.body) return
const reader = res.body.getReader()
const decoder = new TextDecoder()
let buffer = ''
while (true) {
const { done, value } = await reader.read()
if (done) break
buffer += decoder.decode(value, { stream: true })
const lines = buffer.split('\n\n')
buffer = lines.pop() ?? ''
for (const line of lines) {
if (line.startsWith('data: ')) {
const data = JSON.parse(line.slice(6)) as LogLine
printLog(data)
}
}
}
}
async function openApp(name: string) {
const app: App | undefined = await get(`/api/apps/${name}`)
if (!app) {
console.error(`App not found: ${name}`)
return
}
if (app.state !== 'running') {
console.error(`App is not running: ${name}`)
return
}
const url = `http://localhost:${app.port}`
console.log(`Opening ${url}`)
Bun.spawn(['open', url])
}
program
.name('toes')
.version('0.0.1', '-v, --version')
program
.command('info')
.description('Show info for an app')
.argument('<name>', 'app name')
.action(infoApp)
program
.command('list')
.description('List all apps')
.action(listApps)
program
.command('start')
.description('Start an app')
.argument('<name>', 'app name')
.action(startApp)
program
.command('stop')
.description('Stop an app')
.argument('<name>', 'app name')
.action(stopApp)
program
.command('restart')
.description('Restart an app')
.argument('<name>', 'app name')
.action(restartApp)
program
.command('logs')
.description('Show logs for an app')
.argument('<name>', 'app name')
.option('-f, --follow', 'follow log output')
.action(logApp)
program
.command('log', { hidden: true })
.argument('<name>', 'app name')
.option('-f, --follow', 'follow log output')
.action(logApp)
program
.command('open')
.description('Open an app in browser')
.argument('<name>', 'app name')
.action(openApp)
program
.command('new')
.description('Create a new app')
.argument('<name>', 'app name')
.action(name => {
// ...
})
program
.command('push')
.description('Push app to server')
.option('-f, --force', 'force overwrite')
.action(options => {
// ...
})
program.parse()

View File

@ -1,52 +1,99 @@
import type { App as SharedApp, AppState, LogLine } from '@types'
import type { Subprocess } from 'bun' import type { Subprocess } from 'bun'
import { import { existsSync, readdirSync, readFileSync, statSync, watch, writeFileSync } from 'fs'
existsSync, readdirSync, readFileSync, writeFileSync,
statSync, watch
} from 'fs'
import { join } from 'path' import { join } from 'path'
import type { App as SharedApp, AppState, LogLine } from '../shared/types'
export type { AppState } from '../shared/types' export type { AppState } from '@types'
export const APPS_DIR = join(process.env.DATA_DIR ?? '.', 'apps')
const DEFAULT_EMOJI = '🖥️' const DEFAULT_EMOJI = '🖥️'
const APPS_DIR = join(process.env.DATA_DIR ?? '.', 'apps')
const MAX_LOGS = 100 const MAX_LOGS = 100
const _apps = new Map<string, App>()
const _listeners = new Set<() => void>()
let NEXT_PORT = 3001
export type App = SharedApp & { export type App = SharedApp & {
proc?: Subprocess proc?: Subprocess
} }
const _apps = new Map<string, App>() type LoadResult = { pkg: any; error?: string }
// Change notification system export const allApps = (): App[] =>
const _listeners = new Set<() => void>() Array.from(_apps.values())
export const onChange = (cb: () => void) => { .sort((a, b) => a.name.localeCompare(b.name))
export const getApp = (dir: string): App | undefined =>
_apps.get(dir)
export const runApps = () =>
allAppDirs().filter(isApp).forEach(startApp)
export const runningApps = (): App[] =>
allApps().filter(a => a.state === 'running')
export function initApps() {
discoverApps()
runApps()
watchAppsDir()
}
export function onChange(cb: () => void) {
_listeners.add(cb) _listeners.add(cb)
return () => _listeners.delete(cb) return () => _listeners.delete(cb)
} }
const update = () => _listeners.forEach(cb => cb())
export function startApp(dir: string) {
const app = _apps.get(dir)
if (!app || app.state !== 'stopped') return
if (!isApp(dir)) return
runApp(dir, getPort())
}
export function stopApp(dir: string) {
const app = _apps.get(dir)
if (!app || app.state !== 'running') return
info(dir, 'Stopping...')
app.state = 'stopping'
update()
app.proc?.kill()
}
export function updateAppIcon(dir: string, icon: string) {
const { pkg, error } = loadApp(dir)
if (error) throw new Error(error)
pkg.toes ??= {}
pkg.toes.icon = icon
saveApp(dir, pkg)
}
const err = (app: string, ...msg: string[]) => const err = (app: string, ...msg: string[]) =>
console.error('🐾', `${app}:`, ...msg) console.error('🐾', `${app}:`, ...msg)
const getPort = () => NEXT_PORT++
const info = (app: string, ...msg: string[]) => const info = (app: string, ...msg: string[]) =>
console.log('🐾', `${app}:`, ...msg) console.log('🐾', `${app}:`, ...msg)
const isApp = (dir: string): boolean =>
!loadApp(dir).error
const log = (app: string, ...msg: string[]) => const log = (app: string, ...msg: string[]) =>
console.log(`<${app}>`, ...msg) console.log(`<${app}>`, ...msg)
/** Returns all directory names in APPS_DIR */ const update = () => _listeners.forEach(cb => cb())
const allAppDirs = () => {
function allAppDirs() {
return readdirSync(APPS_DIR, { withFileTypes: true }) return readdirSync(APPS_DIR, { withFileTypes: true })
.filter(e => e.isDirectory()) .filter(e => e.isDirectory())
.map(e => e.name) .map(e => e.name)
.sort() .sort()
} }
let NEXT_PORT = 3001 function discoverApps() {
const getPort = () => NEXT_PORT++
const discoverApps = () => {
for (const dir of allAppDirs()) { for (const dir of allAppDirs()) {
const { pkg, error } = loadApp(dir) const { pkg, error } = loadApp(dir)
const state: AppState = error ? 'invalid' : 'stopped' const state: AppState = error ? 'invalid' : 'stopped'
@ -55,12 +102,15 @@ const discoverApps = () => {
} }
} }
export const runApps = () => function isDir(path: string): boolean {
allAppDirs().filter(isApp).forEach(startApp) try {
return statSync(path).isDirectory()
} catch {
return false
}
}
type LoadResult = { pkg: any; error?: string } function loadApp(dir: string): LoadResult {
const loadApp = (dir: string): LoadResult => {
try { try {
const file = readFileSync(join(APPS_DIR, dir, 'package.json'), 'utf-8') const file = readFileSync(join(APPS_DIR, dir, 'package.json'), 'utf-8')
@ -86,24 +136,7 @@ const loadApp = (dir: string): LoadResult => {
} }
} }
const saveApp = (dir: string, pkg: any) => { async function runApp(dir: string, port: number) {
const path = join(APPS_DIR, dir, 'package.json')
writeFileSync(path, JSON.stringify(pkg, null, 2) + '\n')
}
export const updateAppIcon = (dir: string, icon: string) => {
const { pkg, error } = loadApp(dir)
if (error) throw new Error(error)
pkg.toes ??= {}
pkg.toes.icon = icon
saveApp(dir, pkg)
}
const isApp = (dir: string): boolean =>
!loadApp(dir).error
const runApp = async (dir: string, port: number) => {
const { pkg, error } = loadApp(dir) const { pkg, error } = loadApp(dir)
if (error) return if (error) return
@ -175,35 +208,12 @@ const runApp = async (dir: string, port: number) => {
}) })
} }
/** Returns all apps */ function saveApp(dir: string, pkg: any) {
export const allApps = (): App[] => const path = join(APPS_DIR, dir, 'package.json')
Array.from(_apps.values()) writeFileSync(path, JSON.stringify(pkg, null, 2) + '\n')
.sort((a, b) => a.name.localeCompare(b.name))
/** Returns only running apps (for backwards compatibility) */
export const runningApps = (): App[] =>
allApps().filter(a => a.state === 'running')
export const getApp = (dir: string): App | undefined => _apps.get(dir)
export const startApp = (dir: string) => {
const app = _apps.get(dir)
if (!app || app.state !== 'stopped') return
if (!isApp(dir)) return
runApp(dir, getPort())
} }
export const stopApp = (dir: string) => { function watchAppsDir() {
const app = _apps.get(dir)
if (!app || app.state !== 'running') return
info(dir, 'Stopping...')
app.state = 'stopping'
update()
app.proc?.kill()
}
const watchAppsDir = () => {
watch(APPS_DIR, { recursive: true }, (event, filename) => { watch(APPS_DIR, { recursive: true }, (event, filename) => {
if (!filename) return if (!filename) return
@ -265,17 +275,3 @@ const watchAppsDir = () => {
} }
}) })
} }
function isDir(path: string): boolean {
try {
return statSync(path).isDirectory()
} catch {
return false
}
}
export const initApps = () => {
discoverApps()
runApps()
watchAppsDir()
}

View File

@ -1,12 +1,18 @@
import { allApps, initApps, onChange, startApp, stopApp, updateAppIcon } from '$apps'
import type { App as SharedApp } from '@types'
import type { App as BackendApp } from '$apps'
import { Hype } from 'hype' import { Hype } from 'hype'
import { allApps, initApps, onChange, startApp, stopApp, updateAppIcon } from './apps'
import type { App as SharedApp } from '../shared/types' // BackendApp -> SharedApp
function convert(app: BackendApp): SharedApp {
const clone = { ...app }
delete clone.proc
delete clone.logs
return clone
}
const app = new Hype({ layout: false }) const app = new Hype({ layout: false })
console.log('🐾 Toes!')
initApps()
// SSE endpoint for real-time app state updates // SSE endpoint for real-time app state updates
app.get('/api/apps/stream', c => { app.get('/api/apps/stream', c => {
const encoder = new TextEncoder() const encoder = new TextEncoder()
@ -50,14 +56,53 @@ app.get('/api/apps/stream', c => {
}) })
}) })
app.get('/api/apps', c => { app.get('/api/apps', c =>
const apps = allApps().map(app => { c.json(allApps().map(convert))
const clone = { ...app } )
delete clone.proc
delete clone.logs app.get('/api/apps/:app', c => {
return clone const appName = c.req.param('app')
if (!appName) return c.json({ error: 'App not found' }, 404)
const app = allApps().find(a => a.name === appName)
if (!app) return c.json({ error: 'App not found' }, 404)
return c.json(convert(app))
}) })
return c.json(apps)
app.get('/api/apps/:app/logs', c => {
const appName = c.req.param('app')
if (!appName) return c.json({ error: 'App not found' }, 404)
const app = allApps().find(a => a.name === appName)
if (!app) return c.json({ error: 'App not found' }, 404)
return c.json(app.logs ?? [])
})
app.sse('/api/apps/:app/logs/stream', (send, c) => {
const appName = c.req.param('app')
const targetApp = allApps().find(a => a.name === appName)
if (!targetApp) return
let lastLogCount = 0
const sendNewLogs = () => {
const currentApp = allApps().find(a => a.name === appName)
if (!currentApp) return
const logs = currentApp.logs ?? []
const newLogs = logs.slice(lastLogCount)
lastLogCount = logs.length
for (const line of newLogs) {
send(line)
}
}
sendNewLogs()
const unsub = onChange(sendNewLogs)
return () => unsub()
}) })
app.post('/api/apps/:app/start', c => { app.post('/api/apps/:app/start', c => {
@ -98,4 +143,7 @@ app.post('/api/apps/:app/icon', c => {
} }
}) })
console.log('🐾 Toes!')
initApps()
export default app.defaults export default app.defaults

View File

@ -1,30 +1,39 @@
{ {
"compilerOptions": { "compilerOptions": {
// Environment setup & latest features // Environment setup & latest features
"lib": ["ESNext", "DOM"], "lib": [
"ESNext",
"DOM"
],
"target": "ESNext", "target": "ESNext",
"module": "Preserve", "module": "Preserve",
"moduleDetection": "force", "moduleDetection": "force",
"jsx": "react-jsx", "jsx": "react-jsx",
"jsxImportSource": "hono/jsx", "jsxImportSource": "hono/jsx",
"allowJs": true, "allowJs": true,
// Bundler mode // Bundler mode
"moduleResolution": "bundler", "moduleResolution": "bundler",
"allowImportingTsExtensions": true, "allowImportingTsExtensions": true,
"verbatimModuleSyntax": true, "verbatimModuleSyntax": true,
"noEmit": true, "noEmit": true,
// Best practices // Best practices
"strict": true, "strict": true,
"skipLibCheck": true, "skipLibCheck": true,
"noFallthroughCasesInSwitch": true, "noFallthroughCasesInSwitch": true,
"noUncheckedIndexedAccess": true, "noUncheckedIndexedAccess": true,
"noImplicitOverride": true, "noImplicitOverride": true,
// Some stricter flags (disabled by default) // Some stricter flags (disabled by default)
"noUnusedLocals": false, "noUnusedLocals": false,
"noUnusedParameters": false, "noUnusedParameters": false,
"noPropertyAccessFromIndexSignature": false "noPropertyAccessFromIndexSignature": false,
"baseUrl": ".",
"paths": {
"$*": [
"./src/server/*"
],
"@*": [
"./src/shared/*"
]
}
} }
} }