@owlmeans/create-app 0.1.18-rc.13 → 0.1.18-rc.14
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/build/bin.js +0 -0
- package/package.json +2 -2
- package/template/_agents/skills/getting-started/SKILL.md +56 -15
- package/template/sources/api/src/app/session/list.ts +6 -4
- package/template/sources/api/src/app/session/remove.ts +1 -1
- package/template/sources/api/src/context.ts +0 -4
- package/template/sources/api/src/entrypoints.ts +11 -0
- package/template/sources/api/src/index.ts +2 -2
- package/template/sources/common/src/{modules.ts → entrypoints.ts} +1 -1
- package/template/sources/common/src/index.ts +1 -1
- package/template/sources/web/src/context.ts +0 -4
- package/template/sources/web/src/entrypoints.ts +22 -0
- package/template/sources/web/src/index.tsx +2 -2
- package/template/sources/web/src/screens/session.tsx +7 -7
- package/template/sources/api/src/modules.ts +0 -11
- package/template/sources/web/src/modules.ts +0 -22
package/build/bin.js
CHANGED
|
File without changes
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@owlmeans/create-app",
|
|
3
|
-
"version": "0.1.18-rc.
|
|
3
|
+
"version": "0.1.18-rc.14",
|
|
4
4
|
"license": "MIT",
|
|
5
5
|
"description": "Scaffold a minimal fullstack OwlMeans Common app — common + api + web workspaces, shadcn UI navigation/layout, no auth, and a session-scoped in-memory resource. Deploys agent skills via @owlmeans/agent-skills by default.",
|
|
6
6
|
"type": "module",
|
|
@@ -29,7 +29,7 @@
|
|
|
29
29
|
"template"
|
|
30
30
|
],
|
|
31
31
|
"dependencies": {
|
|
32
|
-
"@owlmeans/agent-skills": "^0.1.18-rc.
|
|
32
|
+
"@owlmeans/agent-skills": "^0.1.18-rc.11"
|
|
33
33
|
},
|
|
34
34
|
"devDependencies": {
|
|
35
35
|
"@owlmeans/dep-config": "workspace:*",
|
|
@@ -14,8 +14,11 @@ A minimal OwlMeans app is a **bun-workspace monorepo with three packages**:
|
|
|
14
14
|
```
|
|
15
15
|
sources/
|
|
16
16
|
├── common/ # shared entrypoints (routes), AJV schemas, types, config — the single source of truth
|
|
17
|
+
│ # entrypoints.ts
|
|
17
18
|
├── api/ # @owlmeans/server-app backend; handlers attached to the shared entrypoints
|
|
19
|
+
│ # context.ts, entrypoints.ts, app/<area>/*, index.ts
|
|
18
20
|
└── web/ # @owlmeans/web-panel + shadcn UI; screens attached to the same entrypoints
|
|
21
|
+
# context.ts, entrypoints.ts, nav.ts, layout/, screens/, index.tsx
|
|
19
22
|
```
|
|
20
23
|
|
|
21
24
|
The full, runnable walkthrough (scaffolded **and** manual) lives in
|
|
@@ -27,8 +30,8 @@ The full, runnable walkthrough (scaffolded **and** manual) lives in
|
|
|
27
30
|
Declare each route once in `common` as an **entrypoint**, then `elevate()` it on each side:
|
|
28
31
|
|
|
29
32
|
```ts
|
|
30
|
-
// common/
|
|
31
|
-
export const
|
|
33
|
+
// common/entrypoints.ts — declaration + validation, no implementation
|
|
34
|
+
export const sessionEntrypoints = [
|
|
32
35
|
entrypoint(route(session.base, '/session')),
|
|
33
36
|
entrypoint(route(session.list, '/:sid/items', { parent: session.base, method: RouteMethod.GET }),
|
|
34
37
|
filter(params<SessionParams>(SessionParamsSchema))),
|
|
@@ -38,17 +41,25 @@ export const sessionModules = [
|
|
|
38
41
|
```
|
|
39
42
|
|
|
40
43
|
```ts
|
|
41
|
-
// api/
|
|
42
|
-
elevate(
|
|
43
|
-
export const
|
|
44
|
+
// api/entrypoints.ts — attach handlers
|
|
45
|
+
elevate(sessionEntrypoints, session.list, handlers.list)
|
|
46
|
+
export const appEntrypoints = [...entrypoints, ...sessionEntrypoints] // `entrypoints` = framework defaults
|
|
44
47
|
```
|
|
45
48
|
|
|
46
49
|
```ts
|
|
47
|
-
// web/
|
|
48
|
-
|
|
49
|
-
|
|
50
|
+
// web/entrypoints.ts — attach screen components, plus call-only elevation for backend routes
|
|
51
|
+
const entrypoints = [...baseEntrypoints, ...sessionEntrypoints] // `baseEntrypoints` from web-panel
|
|
52
|
+
elevate(entrypoints, session.list) // callable from the client
|
|
53
|
+
entrypoints.push(entrypoint(route(web.session, '/session', frontend({ parent: BASE })), handler(SessionScreen)))
|
|
54
|
+
export const appEntrypoints = entrypoints
|
|
50
55
|
```
|
|
51
56
|
|
|
57
|
+
A route declaration is plain data: its `path` is the SEGMENT it contributes under its `parent`, and
|
|
58
|
+
nothing ever rewrites it. `session.list` reads `/:sid/items` under `session.base`'s `/session`,
|
|
59
|
+
under the api service's `base: 'api'` — so the address is `GET /api/session/:sid/items`, computed
|
|
60
|
+
on demand by whoever asks. `elevate` is idempotent, so re-elevating an alias is allowed and guards
|
|
61
|
+
given at elevation are added to the declared ones.
|
|
62
|
+
|
|
52
63
|
Change a route or schema in `common` and both sides stay in sync. See [[entrypoint]], [[route]],
|
|
53
64
|
[[server-app]], [[web-client]], [[web-panel]].
|
|
54
65
|
|
|
@@ -61,6 +72,8 @@ prefixes API routes with `/api`:
|
|
|
61
72
|
const cfg = service({ type: AppType.Frontend, service: APP_WEB, host: 'localhost', port: 3001 })
|
|
62
73
|
service({ type: AppType.Backend, service: APP_API, host: 'localhost', port: 3000, base: 'api' }, cfg)
|
|
63
74
|
cfg.debug = { all: true }
|
|
75
|
+
cfg.alias = APP
|
|
76
|
+
cfg.security = { unsecure: true } // local dev serves the API over plain HTTP
|
|
64
77
|
export const commonConfig = cfg
|
|
65
78
|
```
|
|
66
79
|
|
|
@@ -75,9 +88,20 @@ appendStaticResource(context, SESSION_ITEMS) // @owlmeans/static-resource —
|
|
|
75
88
|
```
|
|
76
89
|
|
|
77
90
|
Handlers use `handleRequest` / `handleBody` / `handleParams` (validated payload, then context, then
|
|
78
|
-
req). Read/write `ctx.getStaticResource<T>(alias)` — full
|
|
79
|
-
|
|
80
|
-
|
|
91
|
+
req). Read/write `ctx.getStaticResource<T>(alias)` — the full resource contract
|
|
92
|
+
(`get/load/list/count/create/update/save/delete/take/purge`), so the resource answers the whole
|
|
93
|
+
question rather than the handler filtering afterwards:
|
|
94
|
+
|
|
95
|
+
```ts
|
|
96
|
+
const { items } = await resource.list(
|
|
97
|
+
{ sessionId: params.sid },
|
|
98
|
+
{ sort: [{ field: 'createdAt', order: 'desc' }] }
|
|
99
|
+
)
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
`list` returns `{ items, total }`; the in-memory backends are unpaged unless a `size` is asked for.
|
|
103
|
+
`main(context, appEntrypoints)` starts the server. See [[static-resource]], [[server-app]],
|
|
104
|
+
[[resource]].
|
|
81
105
|
|
|
82
106
|
Swap `@owlmeans/static-resource` for [[mongo-resource]] / [[redis-resource]] when you need
|
|
83
107
|
persistence — the handler shape is identical.
|
|
@@ -86,19 +110,36 @@ persistence — the handler shape is identical.
|
|
|
86
110
|
|
|
87
111
|
`@owlmeans/web-panel`'s `PanelApp` is shadcn/Tailwind v4 (no MUI). The **app provides** the shadcn
|
|
88
112
|
primitives at the `@` alias — `web-panel` references `@/lib/utils` and
|
|
89
|
-
`@/components/ui/{alert,button,card,input,label,progress}`; copy those into `src/`.
|
|
90
|
-
`
|
|
113
|
+
`@/components/ui/{alert,button,card,input,label,navigation-menu,progress}`; copy those into `src/`.
|
|
114
|
+
Routing resolves itself from the active router plugin, so `PanelApp` takes no router prop:
|
|
91
115
|
|
|
92
116
|
```tsx
|
|
93
|
-
basicRender(<PanelApp context={context}
|
|
117
|
+
basicRender(<PanelApp context={context} />)
|
|
94
118
|
```
|
|
95
119
|
|
|
96
120
|
`vite.config.ts` sets `@`→`src`, `@tailwindcss/vite`, and dedupes the owlmeans/react singletons.
|
|
97
121
|
`index.css` is `@import "tailwindcss";` + a shadcn `@theme` token block (replaces `@owlmeans/owl-theme`).
|
|
98
122
|
A parent `BASE` route renders the layout via `handler(LayoutComponent)`; `HOME` is its default child.
|
|
99
|
-
|
|
123
|
+
`index.tsx` calls `context.registerEntrypoints(appEntrypoints)` and `context.serviceRoute(...)` for
|
|
124
|
+
each service, then renders.
|
|
125
|
+
|
|
126
|
+
Screens address the backend through three explicit verbs on the entrypoint:
|
|
127
|
+
|
|
128
|
+
```tsx
|
|
129
|
+
const items = await ctx.entrypoint<ClientEntrypoint<Item[]>>(session.list).call({ params: { sid } })
|
|
130
|
+
const { value, outcome } = await ctx.entrypoint<ClientEntrypoint<Item>>(session.add).invoke({ body })
|
|
131
|
+
const href = await ctx.entrypoint<ClientEntrypoint<string>>(web.about).url()
|
|
132
|
+
```
|
|
133
|
+
|
|
134
|
+
`call` resolves to the VALUE and throws the reply's error; `invoke` gives `{ value, outcome }` when
|
|
135
|
+
the outcome decides what happens next; `url` gives the address (`{ absolute: true }` forces a fully
|
|
136
|
+
qualified one). A screen entrypoint answers `url()` and refuses `call()` — a screen is navigated to.
|
|
100
137
|
See [[web-panel]], [[web-client]], [[shadcn-web]], [[client-entrypoint]].
|
|
101
138
|
|
|
139
|
+
The screen keeps nothing in component state: `makeContext` registers a `@owlmeans/state` resource,
|
|
140
|
+
the fetch writes what came back into it with `store.replace(items)`, and `useStoreList` renders the
|
|
141
|
+
live subscription. See [[state]].
|
|
142
|
+
|
|
102
143
|
## Authentication
|
|
103
144
|
|
|
104
145
|
This shape is intentionally **auth-free**. To add it: `@owlmeans/server-auth` + `@owlmeans/client-auth`
|
|
@@ -7,9 +7,11 @@ export const list = handleParams<SessionParams>(async (params, context) => {
|
|
|
7
7
|
const ctx = context as Context
|
|
8
8
|
const resource = ctx.getStaticResource<SessionItem>(SESSION_ITEMS)
|
|
9
9
|
|
|
10
|
-
// The
|
|
11
|
-
const { items } = await resource.list
|
|
10
|
+
// The resource answers the whole question — this session's items, newest first.
|
|
11
|
+
const { items } = await resource.list(
|
|
12
|
+
{ sessionId: params.sid },
|
|
13
|
+
{ sort: [{ field: 'createdAt', order: 'desc' }] }
|
|
14
|
+
)
|
|
15
|
+
|
|
12
16
|
return items
|
|
13
|
-
.filter(item => item.sessionId === params.sid)
|
|
14
|
-
.sort((a, b) => (a.createdAt < b.createdAt ? 1 : -1))
|
|
15
17
|
})
|
|
@@ -7,7 +7,7 @@ export const remove = handleParams<ItemParams>(async (params, context) => {
|
|
|
7
7
|
const ctx = context as Context
|
|
8
8
|
const resource = ctx.getStaticResource<SessionItem>(SESSION_ITEMS)
|
|
9
9
|
|
|
10
|
-
const existing = await resource.load
|
|
10
|
+
const existing = await resource.load(params.id)
|
|
11
11
|
// Only remove the item if it belongs to the requesting session.
|
|
12
12
|
if (existing == null || existing.sessionId !== params.sid) {
|
|
13
13
|
return { removed: false }
|
|
@@ -10,9 +10,5 @@ export const makeContext = <C extends Config, T extends Context<C>>(cfg: C): T =
|
|
|
10
10
|
// data lives in process memory and is cleared when the api restarts.
|
|
11
11
|
appendStaticResource<C, T>(context, SESSION_ITEMS)
|
|
12
12
|
|
|
13
|
-
// A child context has to inherit THIS factory, not the layer's — otherwise a derived context
|
|
14
|
-
// is built without the resource registered above and every lookup on it throws.
|
|
15
|
-
context.makeContext = makeContext as typeof context.makeContext
|
|
16
|
-
|
|
17
13
|
return context
|
|
18
14
|
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { elevate, entrypoints } from '@owlmeans/server-app'
|
|
2
|
+
import { session, sessionEntrypoints } from '__APP_SLUG__-common'
|
|
3
|
+
import * as handlers from './app/session/index.js'
|
|
4
|
+
|
|
5
|
+
// Attach handler implementations to the shared entrypoint declarations.
|
|
6
|
+
elevate(sessionEntrypoints, session.base)
|
|
7
|
+
elevate(sessionEntrypoints, session.list, handlers.list)
|
|
8
|
+
elevate(sessionEntrypoints, session.add, handlers.add)
|
|
9
|
+
elevate(sessionEntrypoints, session.remove, handlers.remove)
|
|
10
|
+
|
|
11
|
+
export const appEntrypoints = [...entrypoints, ...sessionEntrypoints]
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
import { main } from '@owlmeans/server-app'
|
|
2
2
|
import config from './config.js'
|
|
3
3
|
import { makeContext } from './context.js'
|
|
4
|
-
import {
|
|
4
|
+
import { appEntrypoints } from './entrypoints.js'
|
|
5
5
|
import type { Config, Context } from './types.js'
|
|
6
6
|
|
|
7
7
|
const context = makeContext<Config, Context>(config)
|
|
8
8
|
|
|
9
|
-
main<{}, Config, Context>(context,
|
|
9
|
+
main<{}, Config, Context>(context, appEntrypoints)
|
|
@@ -9,7 +9,7 @@ import type { AddItemPayload, ItemParams, SessionParams } from './types.js'
|
|
|
9
9
|
* elevates them with screen components and calls them. Routes resolve under the
|
|
10
10
|
* api service `base` (`/api`), so e.g. `session.list` → `GET /api/session/:sid/items`.
|
|
11
11
|
*/
|
|
12
|
-
export const
|
|
12
|
+
export const sessionEntrypoints = [
|
|
13
13
|
entrypoint(route(session.base, '/session')),
|
|
14
14
|
entrypoint(
|
|
15
15
|
route(session.list, '/:sid/items', { parent: session.base, method: RouteMethod.GET }),
|
|
@@ -15,9 +15,5 @@ export const makeContext = <C extends Config, T extends Context<C>>(cfg: C): T =
|
|
|
15
15
|
// the app in a module of its own.
|
|
16
16
|
appendStateResource<C, T>(context, SESSION_STATE)
|
|
17
17
|
|
|
18
|
-
// A child context has to inherit THIS factory, not the layer's. Without the line, a derived
|
|
19
|
-
// context is built without the resource registered above and every lookup on it throws.
|
|
20
|
-
context.makeContext = makeContext as typeof context.makeContext
|
|
21
|
-
|
|
22
18
|
return context
|
|
23
19
|
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { BASE, elevate, entrypoint, entrypoints as baseEntrypoints, frontend, handler, HOME, route } from '@owlmeans/web-panel'
|
|
2
|
+
import { session, sessionEntrypoints, web } from '__APP_SLUG__-common'
|
|
3
|
+
import { MainLayout } from './layout/main.js'
|
|
4
|
+
import { AboutScreen } from './screens/about.js'
|
|
5
|
+
import { HomeScreen } from './screens/home.js'
|
|
6
|
+
import { SessionScreen } from './screens/session.js'
|
|
7
|
+
|
|
8
|
+
const entrypoints = [...baseEntrypoints, ...sessionEntrypoints]
|
|
9
|
+
|
|
10
|
+
// Backend entrypoints — elevated without a component so the client can call them.
|
|
11
|
+
elevate(entrypoints, session.base)
|
|
12
|
+
elevate(entrypoints, session.list)
|
|
13
|
+
elevate(entrypoints, session.add)
|
|
14
|
+
elevate(entrypoints, session.remove)
|
|
15
|
+
|
|
16
|
+
// Frontend layout + screens. BASE renders the shared layout; HOME is its default child.
|
|
17
|
+
entrypoints.push(entrypoint(route(BASE, '/', frontend()), handler(MainLayout)))
|
|
18
|
+
entrypoints.push(entrypoint(route(HOME, '/', frontend({ default: true, parent: BASE })), handler(HomeScreen)))
|
|
19
|
+
entrypoints.push(entrypoint(route(web.session, '/session', frontend({ parent: BASE })), handler(SessionScreen)))
|
|
20
|
+
entrypoints.push(entrypoint(route(web.about, '/about', frontend({ parent: BASE })), handler(AboutScreen)))
|
|
21
|
+
|
|
22
|
+
export const appEntrypoints = entrypoints
|
|
@@ -2,12 +2,12 @@ import './index.css'
|
|
|
2
2
|
import { APP_API, APP_WEB } from '__APP_SLUG__-common'
|
|
3
3
|
import config from './config.js'
|
|
4
4
|
import { makeContext } from './context.js'
|
|
5
|
-
import {
|
|
5
|
+
import { appEntrypoints } from './entrypoints.js'
|
|
6
6
|
import { render } from './render.js'
|
|
7
7
|
import type { Config, Context } from './types.js'
|
|
8
8
|
|
|
9
9
|
const context = makeContext<Config, Context>(config)
|
|
10
|
-
context.registerEntrypoints(
|
|
10
|
+
context.registerEntrypoints(appEntrypoints)
|
|
11
11
|
|
|
12
12
|
context.serviceRoute(APP_WEB, true)
|
|
13
13
|
context.serviceRoute(APP_API, true)
|
|
@@ -33,15 +33,15 @@ export const SessionScreen: FC = () => {
|
|
|
33
33
|
*/
|
|
34
34
|
const items = useStoreList<SessionItem>({ query: {}, resource: SESSION_STATE })
|
|
35
35
|
|
|
36
|
-
// The server is the source of truth; the store is what the screen reads. Fetch once,
|
|
37
|
-
// what came back
|
|
36
|
+
// The server is the source of truth; the store is what the screen reads. Fetch once, install
|
|
37
|
+
// what came back, and let the subscription render it. `replace` rather than a save per item:
|
|
38
|
+
// the endpoint answers with the session's whole set, so an item removed on another tab has to
|
|
39
|
+
// leave the store too, and one write wakes the subscribers once instead of once per record.
|
|
38
40
|
const load = async () => {
|
|
39
|
-
const
|
|
41
|
+
const data = await ctx
|
|
40
42
|
.entrypoint<ClientEntrypoint<SessionItem[]>>(session.list)
|
|
41
43
|
.call({ params: { sid } })
|
|
42
|
-
|
|
43
|
-
await store.save(item)
|
|
44
|
-
}
|
|
44
|
+
await store.replace(data ?? [])
|
|
45
45
|
}
|
|
46
46
|
|
|
47
47
|
useEffect(() => { void load() }, [])
|
|
@@ -50,7 +50,7 @@ export const SessionScreen: FC = () => {
|
|
|
50
50
|
if (text.trim() === '') return
|
|
51
51
|
setBusy(true)
|
|
52
52
|
try {
|
|
53
|
-
const
|
|
53
|
+
const item = await ctx
|
|
54
54
|
.entrypoint<ClientEntrypoint<SessionItem>>(session.add)
|
|
55
55
|
.call({ params: { sid }, body: { text } })
|
|
56
56
|
setText('')
|
|
@@ -1,11 +0,0 @@
|
|
|
1
|
-
import { elevate, modules } from '@owlmeans/server-app'
|
|
2
|
-
import { session, sessionModules } from '__APP_SLUG__-common'
|
|
3
|
-
import * as handlers from './app/session/index.js'
|
|
4
|
-
|
|
5
|
-
// Attach handler implementations to the shared entrypoint declarations.
|
|
6
|
-
elevate(sessionModules, session.base)
|
|
7
|
-
elevate(sessionModules, session.list, handlers.list)
|
|
8
|
-
elevate(sessionModules, session.add, handlers.add)
|
|
9
|
-
elevate(sessionModules, session.remove, handlers.remove)
|
|
10
|
-
|
|
11
|
-
export const appModules = [...modules, ...sessionModules]
|
|
@@ -1,22 +0,0 @@
|
|
|
1
|
-
import { BASE, elevate, entrypoint, frontend, handler, HOME, modules as baseModules, route } from '@owlmeans/web-panel'
|
|
2
|
-
import { session, sessionModules, web } from '__APP_SLUG__-common'
|
|
3
|
-
import { MainLayout } from './layout/main.js'
|
|
4
|
-
import { AboutScreen } from './screens/about.js'
|
|
5
|
-
import { HomeScreen } from './screens/home.js'
|
|
6
|
-
import { SessionScreen } from './screens/session.js'
|
|
7
|
-
|
|
8
|
-
const modules = [...baseModules, ...sessionModules]
|
|
9
|
-
|
|
10
|
-
// Backend entrypoints — elevated without a component so the client can call them.
|
|
11
|
-
elevate(modules, session.base)
|
|
12
|
-
elevate(modules, session.list)
|
|
13
|
-
elevate(modules, session.add)
|
|
14
|
-
elevate(modules, session.remove)
|
|
15
|
-
|
|
16
|
-
// Frontend layout + screens. BASE renders the shared layout; HOME is its default child.
|
|
17
|
-
modules.push(entrypoint(route(BASE, '/', frontend()), handler(MainLayout)))
|
|
18
|
-
modules.push(entrypoint(route(HOME, '/', frontend({ default: true, parent: BASE })), handler(HomeScreen)))
|
|
19
|
-
modules.push(entrypoint(route(web.session, '/session', frontend({ parent: BASE })), handler(SessionScreen)))
|
|
20
|
-
modules.push(entrypoint(route(web.about, '/about', frontend({ parent: BASE })), handler(AboutScreen)))
|
|
21
|
-
|
|
22
|
-
export const appModules = modules
|