Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
66 changes: 66 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,72 @@ app.start();
// Open http://localhost:8088/test
```

## AI Skills

`@axiosleo/koapp` ships a bundle of AI Agent Skills so tools like Cursor and
Claude Code can generate framework-correct code for you. Each skill is a
self-contained `SKILL.md` with YAML frontmatter, bundled under
`node_modules/@axiosleo/koapp/assets/skills/` after installation.

### Install into a project

```bash
# After: npm install @axiosleo/koapp
npx @axiosleo/koapp skills --install=cursor
npx @axiosleo/koapp skills --install=claude
```

This copies the skills into `./.cursor/skills/` or `./.claude/skills/` in the
current project, making them visible to the matching AI tool.

### Install for the current user

```bash
npx @axiosleo/koapp skills --install=cursor --scope=user
npx @axiosleo/koapp skills --install=claude --scope=user
```

Writes to `~/.cursor/skills/` or `~/.claude/skills/`, shared across every
project on this machine.

### Options

| Flag | Values | Default | Description |
| --- | --- | --- | --- |
| `--install`, `-i` | `cursor`, `claude` | required | Which tool's skills directory to target |
| `--scope`, `-s` | `project`, `user` | `project` | Where to write the skills |
| `--force`, `-f` | boolean | `false` | Overwrite existing skill directories without prompting |

### Bundled skills

| Skill | Purpose |
| --- | --- |
| `koapp` | Framework overview + navigation to other skills |
| `koapp-apps` | Choose and configure `KoaApplication` (HTTP), `SocketApplication` (TCP), or `WebSocketApplication` |
| `koapp-router` | Define routes, path params, validators, nested routers |
| `koapp-response` | Send responses via `success` / `failed` / `result` / `response` / `error` |
| `koapp-controller` | Organize handlers into classes by extending `Controller` |
| `koapp-model` | Validate and serialize structured data with `Model` |
| `koapp-sse` | Stream Server-Sent Events with `KoaSSEMiddleware` |

### How the installer picks the source

1. If `@axiosleo/koapp` is installed in the current project, skills are
copied from `node_modules/@axiosleo/koapp/assets/skills/`.
2. If the project does not depend on `@axiosleo/koapp`, the CLI prompts to
install it first.
3. If the local install is an older version without the skills assets, the
CLI falls back to the skills shipped inside the `npx`-executed copy and
reminds you to run `npm install @axiosleo/koapp@latest`.

### Uninstall

```bash
rm -rf ./.cursor/skills/koapp* # or ./.claude/skills/koapp*
# user scope
rm -rf ~/.cursor/skills/koapp* # or ~/.claude/skills/koapp*
```

## More Examples

- Request Validation
Expand Down
125 changes: 125 additions & 0 deletions assets/skills/koapp-apps/SKILL.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,125 @@
---
name: koapp-apps
description: Choose and configure the right @axiosleo/koapp Application class - KoaApplication for HTTP, SocketApplication for TCP sockets, WebSocketApplication for WebSocket. Use when building a server with koapp, configuring ports, listen host, session, static files, body parser, ping heartbeat, managing socket connections, or deciding between HTTP/TCP/WS transport.
---

# @axiosleo/koapp Application Classes

`@axiosleo/koapp` exposes three runtime application classes, all extending a
shared `Application` base. Pick one based on the transport you need.

## Which class do I need?

| Class | Transport | Use when |
| --- | --- | --- |
| `KoaApplication` | HTTP(S) via Koa | REST APIs, file uploads, SSR, Server-Sent Events |
| `SocketApplication` | Raw TCP via Node `net` | Custom TCP protocol, IoT gateways, line-delimited services |
| `WebSocketApplication` | WebSocket via `ws` | Real-time browser apps, chat, live dashboards |

All three accept `{ port, routers, debug, app_id }` at minimum; see the
per-class docs for full options:

- [http-server.md](http-server.md) - `KoaApplication`
- [socket-server.md](socket-server.md) - `SocketApplication`
- [websocket-server.md](websocket-server.md) - `WebSocketApplication`
- [examples.md](examples.md) - full server examples

## Shared config keys

All three apps normalize config through `Configuration` from
`@axiosleo/cli-tool`:

```javascript
{
port: 8080, // Port to listen on
listen_host: 'localhost', // '0.0.0.0' for public access (Koa only)
routers: [], // Array of Router instances
app_id: '', // Optional stable ID; auto uuid-v4 if empty
debug: false // Verbose logging
}
```

## Shared events

All apps extend `EventEmitter` and emit:

- `starting` - before the server binds
- `response` - after each response is produced (framework uses this internally to write the response)

Socket apps also expose a separate `app.event` EventEmitter emitting:

- `connection` - new client connected
- `listen` - server bound to the port

## Shared lifecycle

```javascript
const app = new KoaApplication({ ... });
app.on('starting', () => console.log('about to listen'));
await app.start(); // all three classes implement .start()
```

## Connection management (Socket + WebSocket)

`SocketApplication` implements the following methods, inherited by
`WebSocketApplication`:

- `broadcast(data, msg, code, connections)` - send to many; pass `null` for all
- `send(connection, data, msg, code)` - send to one raw connection
- `close(connection)` - close one raw connection
- `sendByConnectionId(id, data, msg, code)` - send by tracked connection ID
- `closeByConnectionId(id)` - close by tracked connection ID
- `getConnection(id)` - returns the raw connection or `null`
- `ping(id)` - send a ping payload to one connection

Connections are tracked in `app.connections` keyed by an auto-generated
`connection_id` (`_uuid_salt('connect:' + app_id)`).

## Ping heartbeat

Socket apps support opt-in periodic ping:

```javascript
new SocketApplication({
port: 8081,
routers: [root],
ping: {
open: true, // default false
interval: 1000 * 60 * 5, // default 5min
data: 'this is a ping'
}
});
```

When `ping.open` is `true`, the app broadcasts `data` to all active
connections every `interval` ms.

## Protocol differences

| Aspect | Socket (TCP) | WebSocket |
| --- | --- | --- |
| Request framing | `{...json}@@@@@@` delimiter | Plain JSON string |
| `send(conn, data)` | `conn.write(data + '@@@@@@')` | `conn.send(data)` |
| `close(conn)` | `conn.end()` | `conn.close()` |
| headers in context | none | `context.headers` (from upgrade request) |

## Common pitfalls

- Calling `new KoaApplication({ static: false })` **disables** the built-in
static server. Set `static: { rootDir: './public' }` to enable it.
- `SocketApplication` requires every inbound message to end with `@@@@@@`.
Clients must append that delimiter.
- `WebSocketApplication.ping.open = true` triggers `broadcast` every
`interval` even when there are zero connections - the call is a no-op but
still schedules.
- Do **not** call `app.start()` inside a route handler - the app is already
running at that point.

## Quick jump

If you just need to build one server, start with the matching doc:

- Building an HTTP API → [http-server.md](http-server.md)
- Building a TCP service → [socket-server.md](socket-server.md)
- Building a WebSocket service → [websocket-server.md](websocket-server.md)
- Copy-paste-ready examples → [examples.md](examples.md)
188 changes: 188 additions & 0 deletions assets/skills/koapp-apps/examples.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,188 @@
# Application Examples

Copy-paste-ready boilerplate for each transport.

## HTTP server

```javascript
'use strict';

const { KoaApplication, Router, success, failed } = require('@axiosleo/koapp');

const router = new Router('/api', {
middlewares: [
async (context) => {
console.log(`[${context.method}] ${context.pathinfo}`);
}
]
});

router.get('/health', async () => {
success({ status: 'ok' });
});

router.post('/echo', async (context) => {
success({ received: context.body });
}, {
body: {
rules: { text: 'required|string' }
}
});

router.get('/users/{:id}', async (context) => {
const id = Number(context.params.id);
if (!Number.isInteger(id)) {
failed({ id }, '400;Invalid id', 400);
}
success({ id, name: `User ${id}` });
});

const app = new KoaApplication({
port: 8088,
listen_host: '0.0.0.0',
routers: [router]
});

app.start();
```

## TCP socket server

```javascript
'use strict';

const { SocketApplication, Router, success } = require('@axiosleo/koapp');

const router = new Router('/', {
middlewares: [
async (context) => {
console.log(`[tcp:${context.connection_id}] ${context.method} ${context.pathinfo}`);
}
]
});

router.any('/ping', async (context) => {
success({ pong: true, connection_id: context.connection_id });
});

router.any('/chat/{:room}', async (context) => {
// Broadcast the received message to everyone in the server
context.app.broadcast({
from: context.connection_id,
room: context.params.room,
body: context.body
}, 'chat', 0, null);
success({ delivered: true });
});

const app = new SocketApplication({
port: 8081,
routers: [router],
ping: {
open: true,
interval: 1000 * 10,
data: 'keep-alive'
}
});

app.event.on('connection', (socket) => {
console.log('client from', socket.remoteAddress);
});

app.start();
```

Matching TCP client:

```javascript
const net = require('net');

const client = net.createConnection({ port: 8081 });
client.write(JSON.stringify({
path: '/chat/general',
method: 'POST',
body: { text: 'hi everyone' }
}) + '@@@@@@');

client.on('data', (buf) => {
buf.toString()
.split('@@@@@@')
.filter(Boolean)
.forEach((frame) => console.log(JSON.parse(frame)));
});
```

## WebSocket server

```javascript
'use strict';

const { WebSocketApplication, Router, success } = require('@axiosleo/koapp');

const router = new Router('/', {
middlewares: [
async (context) => {
console.log(`[ws:${context.connection_id}] ${context.method} ${context.pathinfo}`);
}
]
});

router.any('/chat/{:id}', async (context) => {
context.app.broadcast({
from: context.connection_id,
chatId: context.params.id,
body: context.body
}, 'chat', 0, null);
success({ sent: true });
});

const app = new WebSocketApplication({
port: 8082,
routers: [router],
ping: { open: false }
});

setInterval(() => {
app.broadcast({ tick: Date.now() }, 'tick', 0, null);
}, 5000);

app.start();
```

Matching browser client:

```javascript
const ws = new WebSocket('ws://localhost:8082/chat/42?token=abc');
ws.onopen = () => ws.send(JSON.stringify({ body: { text: 'hi' } }));
ws.onmessage = (e) => console.log(JSON.parse(e.data));
```

## Shared HTTP + WebSocket

```javascript
'use strict';

const http = require('http');
const { KoaApplication, WebSocketApplication, Router, success } = require('@axiosleo/koapp');

const httpRouter = new Router('/api');
httpRouter.get('/health', async () => success({ status: 'ok' }));

const koaApp = new KoaApplication({ port: 0, routers: [httpRouter] });
const server = http.createServer(koaApp.koa.callback());

const wsRouter = new Router('/');
wsRouter.any('/ws/{:id}', async (context) => {
success({ echo: context.body, connectionId: context.connection_id });
});

const wsApp = new WebSocketApplication({
server,
routers: [wsRouter]
});

(async () => {
await wsApp.start(); // binds the ws listener
server.listen(3000, () => console.log('listening on 3000'));
})();
```
Loading
Loading