create-seiro 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md ADDED
@@ -0,0 +1,50 @@
1
+ # create-seiro
2
+
3
+ Scaffold a new Seiro project.
4
+
5
+ ## Usage
6
+
7
+ ```bash
8
+ bunx create-seiro my-app
9
+ cd my-app
10
+ docker compose up -d
11
+ bun run dev
12
+ ```
13
+
14
+ ## What's Included
15
+
16
+ The scaffolded project includes:
17
+
18
+ - **Authentication** - User registration and login with JWT tokens
19
+ - **Database** - PostgreSQL with Docker Compose setup
20
+ - **Server** - WebSocket server with CQRS pattern
21
+ - **Client** - Web Components with Preact Signals
22
+ - **Skills** - Claude skills for adding new entities
23
+
24
+ ## Project Structure
25
+
26
+ ```
27
+ my-app/
28
+ ├── auth/ # Authentication types and handlers
29
+ ├── components/ # Web components
30
+ │ ├── auth.ts # Login/register form
31
+ │ └── shared/ # Shared utilities (modal, router)
32
+ ├── init_db/ # SQL initialization scripts
33
+ ├── .claude/skills/ # Claude skills for development
34
+ ├── server.ts # Server entry point
35
+ ├── app.ts # Client entry point
36
+ ├── types.ts # Combined types
37
+ └── compose.yml # Docker Compose for PostgreSQL
38
+ ```
39
+
40
+ ## Adding Features
41
+
42
+ Use the Claude skills to add new entities:
43
+
44
+ 1. Ask Claude to use the `new-entity` skill
45
+ 2. Provide entity name and fields
46
+ 3. Claude generates types, SQL, and handlers
47
+
48
+ ## License
49
+
50
+ MIT
package/index.ts ADDED
@@ -0,0 +1,36 @@
1
+ #!/usr/bin/env bun
2
+ import { $ } from "bun";
3
+ import { cp, readFile, writeFile } from "fs/promises";
4
+ import { join, resolve } from "path";
5
+
6
+ const projectName = process.argv[2];
7
+ if (!projectName) {
8
+ console.error("Usage: bunx create-seiro <project-name>");
9
+ process.exit(1);
10
+ }
11
+
12
+ const targetDir = resolve(projectName);
13
+ const templateDir = join(import.meta.dir, "template");
14
+
15
+ console.log(`Creating ${projectName}...`);
16
+
17
+ // Copy template
18
+ await cp(templateDir, targetDir, { recursive: true });
19
+
20
+ // Update package.json name
21
+ const pkgPath = join(targetDir, "package.json");
22
+ const pkg = JSON.parse(await readFile(pkgPath, "utf-8"));
23
+ pkg.name = projectName;
24
+ await writeFile(pkgPath, JSON.stringify(pkg, null, 2));
25
+
26
+ // Install dependencies
27
+ console.log("Installing dependencies...");
28
+ await $`cd ${targetDir} && bun install`.quiet();
29
+
30
+ console.log(`
31
+ Done! Next steps:
32
+
33
+ cd ${projectName}
34
+ docker compose up -d
35
+ bun run dev
36
+ `);
package/package.json ADDED
@@ -0,0 +1,26 @@
1
+ {
2
+ "name": "create-seiro",
3
+ "version": "0.1.0",
4
+ "description": "Scaffold a new Seiro project",
5
+ "type": "module",
6
+ "bin": {
7
+ "create-seiro": "./index.ts"
8
+ },
9
+ "files": [
10
+ "index.ts",
11
+ "template"
12
+ ],
13
+ "keywords": [
14
+ "seiro",
15
+ "scaffold",
16
+ "cli",
17
+ "cqrs",
18
+ "websocket"
19
+ ],
20
+ "license": "MIT",
21
+ "repository": {
22
+ "type": "git",
23
+ "url": "https://github.com/blueshed/seiro.git",
24
+ "directory": "packages/create-seiro"
25
+ }
26
+ }
@@ -0,0 +1,169 @@
1
+ # Entity UI Skill
2
+
3
+ Create UI components for entities using Web Components and Preact Signals.
4
+
5
+ ## Critical: Research First
6
+
7
+ 1. **Read existing components** - Look at `components/auth.ts` for the pattern
8
+ 2. **Check app.ts** - See how views are switched
9
+ 3. **Use shared utilities** - Don't reinvent modals, routing
10
+
11
+ ## Component Architecture
12
+
13
+ ### Initialization Pattern
14
+
15
+ Components are initialized with the client after connection:
16
+
17
+ ```typescript
18
+ // component file
19
+ import { signal, effect } from "seiro/client";
20
+ import type { Client } from "seiro";
21
+ import type { Commands, Queries, Events } from "../types";
22
+
23
+ type AppClient = Client<Commands, Queries, Events>;
24
+
25
+ let client: AppClient;
26
+
27
+ export function initMyEntity(c: AppClient) {
28
+ client = c;
29
+
30
+ // Subscribe to events
31
+ client.on("entity_created", handleCreated);
32
+ client.on("entity_updated", handleUpdated);
33
+ }
34
+
35
+ // app.ts - initialize after connect
36
+ const profile = await client.connect<User>();
37
+ initMyEntity(client);
38
+ initAuth(client, profile); // Auth last - triggers rendering
39
+ client.subscribe();
40
+ ```
41
+
42
+ ### State with Signals
43
+
44
+ Use signals for reactive state that drives UI updates:
45
+
46
+ ```typescript
47
+ import { signal, effect } from "seiro/client";
48
+
49
+ // Module-level state
50
+ const entities = signal<Map<number, Entity>>(new Map());
51
+
52
+ // Update by creating new Map (immutable pattern)
53
+ function addEntity(entity: Entity) {
54
+ const next = new Map(entities.value);
55
+ next.set(entity.id, entity);
56
+ entities.value = next;
57
+ }
58
+
59
+ // Load from query
60
+ async function loadEntities() {
61
+ const next = new Map<number, Entity>();
62
+ for await (const row of client.query("entities.all")) {
63
+ next.set(row.id, row);
64
+ }
65
+ entities.value = next;
66
+ }
67
+ ```
68
+
69
+ ### Web Component Structure
70
+
71
+ ```typescript
72
+ class EntityList extends HTMLElement {
73
+ private disposeEffects: (() => void)[] = [];
74
+
75
+ connectedCallback() {
76
+ this.innerHTML = `<div data-list></div>`;
77
+ const list = this.querySelector("[data-list]")!;
78
+
79
+ // React to signal changes
80
+ this.disposeEffects.push(
81
+ effect(() => {
82
+ const items = Array.from(entities.value.values());
83
+ list.innerHTML = items
84
+ .map(e => `<div>${e.name}</div>`)
85
+ .join("") || "<p>None yet</p>";
86
+ })
87
+ );
88
+ }
89
+
90
+ disconnectedCallback() {
91
+ // Clean up effects to prevent memory leaks
92
+ for (const dispose of this.disposeEffects) dispose();
93
+ this.disposeEffects = [];
94
+ }
95
+ }
96
+
97
+ customElements.define("entity-list", EntityList);
98
+ ```
99
+
100
+ ## Routing
101
+
102
+ ### Adding a Route
103
+
104
+ Routes are handled in `app.ts` with hash-based navigation:
105
+
106
+ ```typescript
107
+ import { route, navigate } from "./components/shared/router";
108
+
109
+ effect(() => {
110
+ const currentRoute = route.value;
111
+
112
+ if (currentRoute.startsWith("#/myview")) {
113
+ main.innerHTML = `<my-component></my-component>`;
114
+ }
115
+ });
116
+ ```
117
+
118
+ ### Navigation Links
119
+
120
+ ```html
121
+ <a href="#/myview" class="text-zinc-500 hover:text-zinc-300">My View</a>
122
+ ```
123
+
124
+ ## Shared Utilities
125
+
126
+ ### Modals (components/shared/modal.ts)
127
+
128
+ ```typescript
129
+ import { showToast, showInputModal, showConfirmModal, showFormModal } from "./shared/modal";
130
+
131
+ // Toast notification
132
+ showToast("Saved successfully");
133
+
134
+ // Input modal - returns string or null
135
+ const name = await showInputModal("Enter name", "Default value");
136
+
137
+ // Confirm modal - returns boolean
138
+ const confirmed = await showConfirmModal("Delete?", "This cannot be undone.");
139
+
140
+ // Form modal - returns object or null
141
+ const result = await showFormModal("Edit User", [
142
+ { name: "name", label: "Name", required: true },
143
+ { name: "email", label: "Email" },
144
+ ]);
145
+ ```
146
+
147
+ ### Router (components/shared/router.ts)
148
+
149
+ ```typescript
150
+ import { route, navigate } from "./shared/router";
151
+
152
+ // Read current route (reactive)
153
+ effect(() => {
154
+ if (route.value === "#/myview") {
155
+ // show my view
156
+ }
157
+ });
158
+
159
+ // Navigate programmatically
160
+ navigate("#/myview/123");
161
+ ```
162
+
163
+ ## Checklist
164
+
165
+ - [ ] Component has `initX(client)` function called from app.ts
166
+ - [ ] Event subscriptions set up in init function
167
+ - [ ] Signal state with immutable updates (new Map)
168
+ - [ ] Effects cleaned up in disconnectedCallback
169
+ - [ ] Routes added to app.ts effect
@@ -0,0 +1,198 @@
1
+ # New Entity Skill
2
+
3
+ Create a new entity with database, server handlers, and client types.
4
+
5
+ ## Critical: Research First, Copy Patterns
6
+
7
+ **Before writing any code:**
8
+
9
+ 1. **Read existing similar code** - Look at `auth/` for the pattern
10
+ 2. **Copy patterns exactly** - Don't invent new approaches, follow what already works
11
+ 3. **Check the wire protocol** - Review `CLAUDE.md` for message formats
12
+
13
+ ## Type Definitions
14
+
15
+ Use `Command<D, R>` and `Query<P, R>` helpers for type-safe definitions:
16
+
17
+ ```typescript
18
+ // {entity}/types.ts
19
+ import type { Command, Query } from "seiro";
20
+
21
+ export type {Entity} = {
22
+ id: number;
23
+ name: string;
24
+ // fields...
25
+ };
26
+
27
+ export type {Entity}Commands = {
28
+ "{entity}.create": Command<{ name: string }, { id: number }>;
29
+ "{entity}.save": Command<{ id: number; name: string }, { id: number }>;
30
+ "{entity}.delete": Command<{ id: number }, void>;
31
+ };
32
+
33
+ export type {Entity}Queries = {
34
+ "{entity}s.all": Query<void, {Entity}>;
35
+ "{entity}.detail": Query<{ id: number }, {Entity}Detail>;
36
+ };
37
+
38
+ export type {Entity}Events = {
39
+ {entity}_created: {Entity};
40
+ {entity}_updated: {Entity};
41
+ };
42
+ ```
43
+
44
+ ## Typed SQL Pattern
45
+
46
+ Use postgres library's type parameter:
47
+
48
+ ```typescript
49
+ // Query returning multiple rows
50
+ server.query("{entity}s.all", async function* (_params, ctx) {
51
+ if (!ctx.userId) throw new Error("Not authenticated");
52
+ const rows = await sql<{ query_{entity}s_all: {Entity} }[]>`
53
+ SELECT query_{entity}s_all(${ctx.userId})
54
+ `;
55
+ for (const row of rows) {
56
+ yield row.query_{entity}s_all;
57
+ }
58
+ });
59
+
60
+ // Command returning result
61
+ server.command("{entity}.save", async (data, ctx) => {
62
+ if (!ctx.userId) throw new Error("Not authenticated");
63
+ const [row] = await sql<[{ result: { id: number } }]>`
64
+ SELECT cmd_{entity}_save(${ctx.userId}, ${sql.json(data)}) as result
65
+ `;
66
+ return row?.result;
67
+ });
68
+ ```
69
+
70
+ ## Steps
71
+
72
+ ### 1. Types ({entity}/types.ts)
73
+
74
+ ```typescript
75
+ import type { Command, Query } from "seiro";
76
+
77
+ export type {Entity} = {
78
+ id: number;
79
+ name: string;
80
+ };
81
+
82
+ export type {Entity}Commands = {
83
+ "{entity}.create": Command<{ name: string }, { id: number }>;
84
+ "{entity}.save": Command<{ id?: number; name: string }, { id: number }>;
85
+ "{entity}.delete": Command<{ id: number }, void>;
86
+ };
87
+
88
+ export type {Entity}Queries = {
89
+ "{entity}s.all": Query<void, {Entity}>;
90
+ };
91
+
92
+ export type {Entity}Events = {
93
+ {entity}_created: {Entity};
94
+ {entity}_updated: {Entity};
95
+ };
96
+ ```
97
+
98
+ ### 2. Database (init_db/)
99
+
100
+ Create a new numbered SQL file (e.g., `04_{entity}_tables.sql`):
101
+
102
+ ```sql
103
+ -- Table
104
+ CREATE TABLE {entity}s (
105
+ id SERIAL PRIMARY KEY,
106
+ user_id INTEGER NOT NULL REFERENCES users(id),
107
+ name TEXT NOT NULL,
108
+ created_at TIMESTAMPTZ DEFAULT now()
109
+ );
110
+
111
+ -- Command: create
112
+ CREATE OR REPLACE FUNCTION cmd_{entity}_create(p_user_id INTEGER, data JSONB)
113
+ RETURNS JSONB AS $$
114
+ DECLARE
115
+ v_result {entity}s;
116
+ BEGIN
117
+ INSERT INTO {entity}s (user_id, name)
118
+ VALUES (p_user_id, data->>'name')
119
+ RETURNING * INTO v_result;
120
+
121
+ RETURN jsonb_build_object('id', v_result.id);
122
+ END;
123
+ $$ LANGUAGE plpgsql;
124
+
125
+ -- Query: all for user
126
+ CREATE OR REPLACE FUNCTION query_{entity}s_all(p_user_id INTEGER)
127
+ RETURNS SETOF JSONB AS $$
128
+ SELECT jsonb_build_object('id', id, 'name', name)
129
+ FROM {entity}s
130
+ WHERE user_id = p_user_id
131
+ ORDER BY created_at DESC;
132
+ $$ LANGUAGE sql;
133
+ ```
134
+
135
+ ### 3. Server Handlers ({entity}/server.ts)
136
+
137
+ ```typescript
138
+ import type { Sql } from "postgres";
139
+ import type { Server } from "seiro";
140
+ import type {
141
+ {Entity},
142
+ {Entity}Commands,
143
+ {Entity}Queries,
144
+ {Entity}Events,
145
+ } from "./types";
146
+
147
+ export function register<
148
+ C extends {Entity}Commands,
149
+ Q extends {Entity}Queries,
150
+ E extends {Entity}Events,
151
+ >(server: Server<C, Q, E>, sql: Sql) {
152
+ server.command("{entity}.create", async (data, ctx) => {
153
+ if (!ctx.userId) throw new Error("Not authenticated");
154
+ const [row] = await sql<[{ result: { id: number } }]>`
155
+ SELECT cmd_{entity}_create(${ctx.userId}, ${sql.json(data)}) as result
156
+ `;
157
+ return row?.result;
158
+ });
159
+
160
+ server.query("{entity}s.all", async function* (_params, ctx) {
161
+ if (!ctx.userId) throw new Error("Not authenticated");
162
+ const rows = await sql<{ query_{entity}s_all: {Entity} }[]>`
163
+ SELECT query_{entity}s_all(${ctx.userId})
164
+ `;
165
+ for (const row of rows) {
166
+ yield row.query_{entity}s_all;
167
+ }
168
+ });
169
+ }
170
+
171
+ export const channels = ["{entity}_created", "{entity}_updated"] as const;
172
+ ```
173
+
174
+ ### 4. Register Types (types.ts)
175
+
176
+ ```typescript
177
+ import type { {Entity}Commands, {Entity}Queries, {Entity}Events } from "./{entity}/types";
178
+ export type { {Entity} } from "./{entity}/types";
179
+
180
+ export type Commands = AuthCommands & {Entity}Commands;
181
+ export type Queries = AuthQueries & {Entity}Queries;
182
+ export type Events = AuthEvents & {Entity}Events;
183
+ ```
184
+
185
+ ### 5. Register in Server (server.ts)
186
+
187
+ ```typescript
188
+ import * as {entity} from "./{entity}/server";
189
+
190
+ {entity}.register(server, sql);
191
+ ```
192
+
193
+ ## Checklist
194
+
195
+ - [ ] Types use `Command<D, R>` and `Query<P, R>` helpers
196
+ - [ ] Server handlers use typed SQL: `sql<[{ result: Type }]>`
197
+ - [ ] Database functions return JSONB with `jsonb_build_object()`
198
+ - [ ] Tests written and passing
@@ -0,0 +1,49 @@
1
+ # Project Dev Context
2
+
3
+ CQRS over WebSocket. Bun + Preact Signals + Web Components.
4
+
5
+ ## Commands
6
+
7
+ ```bash
8
+ docker compose up -d # start database
9
+ bun run dev # server on :3000
10
+ bun run check # typecheck
11
+ bun test # run tests
12
+ ```
13
+
14
+ ## Adding Entities
15
+
16
+ Use the new-entity skill to add new entities with proper types and handlers.
17
+
18
+ ## Type Patterns
19
+
20
+ Use `Command<D, R>` and `Query<P, R>` from seiro:
21
+
22
+ ```typescript
23
+ import type { Command, Query } from "seiro";
24
+
25
+ export type MyCommands = {
26
+ "thing.create": Command<{ name: string }, { id: number }>;
27
+ };
28
+
29
+ export type MyQueries = {
30
+ "things.all": Query<void, Thing>;
31
+ };
32
+ ```
33
+
34
+ ## Typed SQL
35
+
36
+ ```typescript
37
+ const rows = await sql<{ query_things_all: Thing }[]>`
38
+ SELECT query_things_all(${ctx.userId})
39
+ `;
40
+ for (const row of rows) {
41
+ yield row.query_things_all;
42
+ }
43
+ ```
44
+
45
+ ## Wire Protocol
46
+
47
+ Commands: `{ cmd: "name", cid: "abc123", data: {...} }`
48
+ Queries: `{ q: "name", id: 1, params: {...} }`
49
+ Events: `{ ev: "name", data: {...} }`
@@ -0,0 +1,40 @@
1
+ import { createClient, effect } from "seiro/client";
2
+ import type { Commands, Queries, Events, User } from "./types";
3
+ import { initAuth, user } from "./components/auth";
4
+ import "./components/auth";
5
+
6
+ // Create client
7
+ const wsUrl = `ws://${window.location.host}/ws`;
8
+ const client = createClient<Commands, Queries, Events>(wsUrl);
9
+
10
+ // Connect and initialize
11
+ async function main() {
12
+ const profile = await client.connect<User>();
13
+ initAuth(client, profile);
14
+ client.subscribe();
15
+
16
+ // Show/hide content based on auth state
17
+ effect(() => {
18
+ const main = document.querySelector("main");
19
+ if (main) {
20
+ if (user.value) {
21
+ main.innerHTML = `
22
+ <div class="text-center py-12">
23
+ <h2 class="text-2xl font-bold text-white mb-4">Welcome!</h2>
24
+ <p class="text-zinc-400">You are logged in as ${user.value.email}</p>
25
+ <p class="text-zinc-500 mt-8">Start building your app by adding entities.</p>
26
+ </div>
27
+ `;
28
+ } else {
29
+ main.innerHTML = `
30
+ <div class="text-center py-12">
31
+ <h2 class="text-2xl font-bold text-white mb-4">Welcome to Seiro</h2>
32
+ <p class="text-zinc-400">Please login or register to continue.</p>
33
+ </div>
34
+ `;
35
+ }
36
+ }
37
+ });
38
+ }
39
+
40
+ main().catch(console.error);