@owlmeans/create-app 0.1.18-rc.58 → 0.1.18-rc.59

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 CHANGED
@@ -9,7 +9,7 @@ bun create @owlmeans/app my-app
9
9
  # or
10
10
  yarn create @owlmeans/app my-app
11
11
  # or
12
- npx @owlmeans/create-app@^0.1.18-rc.58 my-app
12
+ npx @owlmeans/create-app@^0.1.18-rc.59 my-app
13
13
  ```
14
14
 
15
15
  ## What it generates
@@ -1,5 +1,5 @@
1
1
  import type { PackageManager } from './types.js';
2
- export declare const HELP = "@owlmeans/create-app \u2014 scaffold a fullstack OwlMeans Common app\n\nUsage:\n npm create @owlmeans/app@latest <dir> [options]\n bun create @owlmeans/app <dir> [options]\n yarn create @owlmeans/app <dir> [options]\n npx @owlmeans/create-app@^0.1.18-rc.58 <dir> [options]\n\nGenerates three workspaces (common + api + web) with shadcn UI navigation and\nlayout, no authentication, and a session-scoped in-memory resource on the\nbackend. With --bare the demo is left out and only the working shell remains.\nAgent skills are deployed into the project by default.\n\nOptions:\n --name <name> human-readable app name (default: derived from <dir>)\n --slug <slug> package slug (default: derived from <dir>)\n --lang <code> language of the generated UI text (default: en)\n --description <t> one-line project description for the generated docs\n --bare scaffold the shell only \u2014 no example/demo code\n --pm <bun|npm|yarn> package manager (default: bun)\n --no-install do not install dependencies\n --no-skills do not deploy agent skills via @owlmeans/agent-skills\n --no-git do not run \"git init\"\n --yes, -y proceed without prompts / into a non-empty directory\n --help, -h show this help\n";
2
+ export declare const HELP = "@owlmeans/create-app \u2014 scaffold a fullstack OwlMeans Common app\n\nUsage:\n npm create @owlmeans/app@latest <dir> [options]\n bun create @owlmeans/app <dir> [options]\n yarn create @owlmeans/app <dir> [options]\n npx @owlmeans/create-app@^0.1.18-rc.59 <dir> [options]\n\nGenerates three workspaces (common + api + web) with shadcn UI navigation and\nlayout, no authentication, and a session-scoped in-memory resource on the\nbackend. With --bare the demo is left out and only the working shell remains.\nAgent skills are deployed into the project by default.\n\nOptions:\n --name <name> human-readable app name (default: derived from <dir>)\n --slug <slug> package slug (default: derived from <dir>)\n --lang <code> language of the generated UI text (default: en)\n --description <t> one-line project description for the generated docs\n --bare scaffold the shell only \u2014 no example/demo code\n --pm <bun|npm|yarn> package manager (default: bun)\n --no-install do not install dependencies\n --no-skills do not deploy agent skills via @owlmeans/agent-skills\n --no-git do not run \"git init\"\n --yes, -y proceed without prompts / into a non-empty directory\n --help, -h show this help\n";
3
3
  export declare const PMS: PackageManager[];
4
4
  /** Dotfiles/dirs are shipped with an underscore prefix so npm does not strip them from the tarball. */
5
5
  export declare const DOTFILE_RENAMES: Record<string, string>;
@@ -5,7 +5,7 @@ Usage:
5
5
  npm create @owlmeans/app@latest <dir> [options]
6
6
  bun create @owlmeans/app <dir> [options]
7
7
  yarn create @owlmeans/app <dir> [options]
8
- npx @owlmeans/create-app@^0.1.18-rc.58 <dir> [options]
8
+ npx @owlmeans/create-app@^0.1.18-rc.59 <dir> [options]
9
9
 
10
10
  Generates three workspaces (common + api + web) with shadcn UI navigation and
11
11
  layout, no authentication, and a session-scoped in-memory resource on the
package/build/run.js CHANGED
@@ -50,7 +50,7 @@ export const run = async (args) => {
50
50
  log(args.install
51
51
  ? '\nDeploying agent skills via @owlmeans/agent-skills…'
52
52
  : '\nDeploying harness guidance via @owlmeans/agent-skills (general skills only — re-run'
53
- + '\n`npx @owlmeans/agent-skills@^0.1.18-rc.49` after installing to add the package-specific ones)…');
53
+ + '\n`npx @owlmeans/agent-skills@^0.1.18-rc.50` after installing to add the package-specific ones)…');
54
54
  try {
55
55
  const result = await installSkills({
56
56
  dir: dest,
@@ -62,7 +62,7 @@ export const run = async (args) => {
62
62
  help: false,
63
63
  });
64
64
  if (result.code !== 0) {
65
- process.stderr.write(` agent-skills exited with code ${result.code} — you can re-run \`npx @owlmeans/agent-skills@^0.1.18-rc.49\` later.\n`);
65
+ process.stderr.write(` agent-skills exited with code ${result.code} — you can re-run \`npx @owlmeans/agent-skills@^0.1.18-rc.50\` later.\n`);
66
66
  }
67
67
  }
68
68
  catch (err) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@owlmeans/create-app",
3
- "version": "0.1.18-rc.58",
3
+ "version": "0.1.18-rc.59",
4
4
  "license": "MIT",
5
5
  "description": "Scaffold a fullstack OwlMeans Common app — common + api + web workspaces, shadcn UI navigation/layout, no auth, and a session-scoped in-memory resource, or --bare for the demo-free shell. Also usable programmatically via scaffold(). Deploys agent skills via @owlmeans/agent-skills by default.",
6
6
  "type": "module",
@@ -30,7 +30,7 @@
30
30
  "template"
31
31
  ],
32
32
  "dependencies": {
33
- "@owlmeans/agent-skills": "^0.1.18-rc.49"
33
+ "@owlmeans/agent-skills": "^0.1.18-rc.50"
34
34
  },
35
35
  "devDependencies": {
36
36
  "@owlmeans/dep-config": "workspace:*",
@@ -114,7 +114,7 @@ from the installed `@owlmeans/*` packages. Agents load a skill by topic, or you
114
114
  explicitly. Copilot and Codex read `.agents/skills/` directly; Claude Code reads the generated
115
115
  symlinks in `.claude/skills/` (see "Claude Code" below).
116
116
 
117
- - After adding or updating any `@owlmeans/*` dependency, run `npx @owlmeans/agent-skills@^0.1.18-rc.49` to refresh
117
+ - After adding or updating any `@owlmeans/*` dependency, run `npx @owlmeans/agent-skills@^0.1.18-rc.50` to refresh
118
118
  the deployed skills.
119
119
  - `/logging` — before adding any log line, catch block or `console` call (mechanics: `/log`).
120
120
  - Deployed files carry an `AUTO-GENERATED` banner and are refreshed in place — never hand-edit them.
@@ -66,5 +66,5 @@ them; write your own guidance as separate, un-bannered files. Refresh after addi
66
66
  `@owlmeans/*` packages:
67
67
 
68
68
  ```sh
69
- npx @owlmeans/agent-skills@^0.1.18-rc.49
69
+ npx @owlmeans/agent-skills@^0.1.18-rc.50
70
70
  ```
@@ -140,16 +140,21 @@ scripts, error classes and schema files.
140
140
  Applications the viable agent generates follow the same rules, in these fixed shapes:
141
141
 
142
142
  - **Domain model per entity.** `backend/src/models/<entity>/types.ts` declares
143
- `interface <Entity>Model` first; `models/<entity>/<entity>.ts` exports only
144
- `make<Entity>Model = (ctx: Context): <Entity>Model => { const op = async (actor: Actor, …) => …; return { op, … } }`.
143
+ `interface <Base>Model` first; `models/<entity>/<base>.ts` exports only
144
+ `make<Base>Model = (ctx: Context): <Base>Model => { const op = async (actor: Actor, …) => …; return { op, … } }`.
145
145
  It is built where it is used — `await makeTaskModel(ctx).complete(actorOf(request), id)` — and is
146
146
  never registered as a context service. Its member names are the endpoint keys of the entity.
147
147
  - **Handlers and job processors**: one plain exported function per file
148
- (`api/src/app/<entity>/<action>.ts`, `worker/src/jobs/<job>.ts`); a handler's body is one call into
149
- the model.
150
- - **Seed helpers are objects**: `actorOf(request): Actor` with `actor.inOrganization(…)`,
151
- `actor.assertOrganization(…)`, `actor.grantedIds(…)`, `actor.organizationScope(…)`; the
152
- record-owner, visit, planning-access, landing hand-off and llm helpers likewise.
148
+ (`api/src/app/<entity>/<action>.ts` beside a GENERATED `index.ts` barrel, `worker/src/jobs/<job>.ts`);
149
+ a handler's body is one call into the model.
150
+ - **Seed helpers are objects** whose members keep the old function names:
151
+ `actorOf(request): Actor` with `actor.inOrganization(…)`, `actor.assertOrganization(…)`,
152
+ `actor.grantedIds(…)`, `actor.organizationScope(…)`; `recordOwnerOf(request)`, `visitOf(request)`,
153
+ `planningAccessOf(request, ctx)`, `landingHandoff`, `visitKey`. Their types live in the module's
154
+ same-named folder (`lib/actor/types.ts`); the old free functions stay as
155
+ `@deprecated generated-app:factory-objects` wrappers so a project generated before still compiles.
156
+ - A model module generated before this shape keeps its plain functions: its callers import them by
157
+ name, so it is extended in that shape and never converted in place.
153
158
  - **Resources** keep their maker and their `xResource(ctx)` accessor; the resource's interface lives
154
159
  in `resources/<entity>/types.ts`.
155
160
  - Shared types in `common` are interfaces.
@@ -47,7 +47,7 @@ How you research the repo depends on whether `@owlmeans/*` is linked locally:
47
47
  **https://github.com/owlmeans/common** — `tree.md` and package READMEs — to find the right package.
48
48
 
49
49
  This is the same dev-linked detection `@owlmeans/agent-skills` uses (see its `detectLinked`). After
50
- adding an `@owlmeans/*` dependency, run `npx @owlmeans/agent-skills@^0.1.18-rc.49` to deploy its
50
+ adding an `@owlmeans/*` dependency, run `npx @owlmeans/agent-skills@^0.1.18-rc.50` to deploy its
51
51
  skill. Prefer an `@owlmeans/*` package over a third-party library or bespoke code whenever one fits.
52
52
 
53
53
  ### Never add an OwlMeans dependency without an explicit range
@@ -4,6 +4,7 @@
4
4
  "sources/common/src/schemas.ts",
5
5
  "sources/api/src/consts.ts",
6
6
  "sources/api/src/app",
7
+ "sources/api/src/models",
7
8
  "sources/web/src/screens/about.tsx",
8
9
  "sources/web/src/screens/session.tsx"
9
10
  ],
@@ -1,21 +1,9 @@
1
- import { randomUUID } from 'node:crypto'
2
1
  import { handlers } from '@owlmeans/server-app'
3
- import { session, type SessionItem } from '__APP_SLUG__-common'
4
- import { SESSION_ITEMS } from '../../consts.js'
2
+ import { session } from '__APP_SLUG__-common'
3
+ import { makeSessionModel } from '../../models/session.js'
5
4
  import type { Context } from '../../types.js'
6
5
 
7
6
  const handle = handlers<Context>()
8
7
 
9
- export const add = handle.body(session.add, async (payload, context, request) => {
10
- const { sid } = request.params
11
- const resource = context.getStaticResource<SessionItem>(SESSION_ITEMS)
12
-
13
- const item: SessionItem = {
14
- id: randomUUID(),
15
- sessionId: sid,
16
- text: payload.text,
17
- createdAt: new Date().toISOString(),
18
- }
19
-
20
- return await resource.create(item)
21
- })
8
+ export const add = handle.body(session.add, async (payload, context, request) =>
9
+ await makeSessionModel(context, request.params.sid).add(payload.text))
@@ -1,18 +1,9 @@
1
1
  import { handlers } from '@owlmeans/server-app'
2
- import { session, type SessionItem } from '__APP_SLUG__-common'
3
- import { SESSION_ITEMS } from '../../consts.js'
2
+ import { session } from '__APP_SLUG__-common'
3
+ import { makeSessionModel } from '../../models/session.js'
4
4
  import type { Context } from '../../types.js'
5
5
 
6
6
  const handle = handlers<Context>()
7
7
 
8
- export const list = handle.params(session.list, async (params, context) => {
9
- const resource = context.getStaticResource<SessionItem>(SESSION_ITEMS)
10
-
11
- // The resource answers the whole question — this session's items, newest first.
12
- const { items } = await resource.list(
13
- { sessionId: params.sid },
14
- { sort: [{ field: 'createdAt', order: 'desc' }] }
15
- )
16
-
17
- return items
18
- })
8
+ export const list = handle.params(session.list, async (params, context) =>
9
+ await makeSessionModel(context, params.sid).list())
@@ -1,19 +1,9 @@
1
1
  import { handlers } from '@owlmeans/server-app'
2
- import { session, type SessionItem } from '__APP_SLUG__-common'
3
- import { SESSION_ITEMS } from '../../consts.js'
2
+ import { session } from '__APP_SLUG__-common'
3
+ import { makeSessionModel } from '../../models/session.js'
4
4
  import type { Context } from '../../types.js'
5
5
 
6
6
  const handle = handlers<Context>()
7
7
 
8
- export const remove = handle.params(session.remove, async (params, context) => {
9
- const resource = context.getStaticResource<SessionItem>(SESSION_ITEMS)
10
-
11
- const existing = await resource.load(params.id)
12
- // Only remove the item if it belongs to the requesting session.
13
- if (existing == null || existing.sessionId !== params.sid) {
14
- return { removed: false }
15
- }
16
-
17
- await resource.delete(params.id)
18
- return { removed: true }
19
- })
8
+ export const remove = handle.params(session.remove, async (params, context) =>
9
+ await makeSessionModel(context, params.sid).remove(params.id))
@@ -0,0 +1,14 @@
1
+ import type { SessionItem } from '__APP_SLUG__-common'
2
+
3
+ /**
4
+ * The items of ONE browser session. The session id is bound when the model is built, so no
5
+ * member can reach another session's items; the member names are the session endpoints' keys.
6
+ */
7
+ export interface SessionModel {
8
+ /** This session's items, newest first. */
9
+ list: () => Promise<SessionItem[]>
10
+ /** Store a new item under this session. */
11
+ add: (text: string) => Promise<SessionItem>
12
+ /** Remove an item — only when it belongs to this session. */
13
+ remove: (id: string) => Promise<{ removed: boolean }>
14
+ }
@@ -0,0 +1,43 @@
1
+ import { randomUUID } from 'node:crypto'
2
+ import type { SessionItem } from '__APP_SLUG__-common'
3
+ import { SESSION_ITEMS } from '../consts.js'
4
+ import type { Context } from '../types.js'
5
+ import type { SessionModel } from './session/types.js'
6
+
7
+ /**
8
+ * The session domain, bound to the context that holds its resource and to the session it serves.
9
+ * Built where it is used — `makeSessionModel(context, sid).list()` — never stored.
10
+ */
11
+ export const makeSessionModel = (context: Context, sid: string): SessionModel => {
12
+ const resource = context.getStaticResource<SessionItem>(SESSION_ITEMS)
13
+
14
+ const list: SessionModel['list'] = async () => {
15
+ // The resource answers the whole question — this session's items, newest first.
16
+ const { items } = await resource.list(
17
+ { sessionId: sid },
18
+ { sort: [{ field: 'createdAt', order: 'desc' }] }
19
+ )
20
+
21
+ return items
22
+ }
23
+
24
+ const add: SessionModel['add'] = async text => await resource.create({
25
+ id: randomUUID(),
26
+ sessionId: sid,
27
+ text,
28
+ createdAt: new Date().toISOString(),
29
+ })
30
+
31
+ const remove: SessionModel['remove'] = async id => {
32
+ const existing = await resource.load(id)
33
+ // Only remove the item if it belongs to this session.
34
+ if (existing == null || existing.sessionId !== sid) {
35
+ return { removed: false }
36
+ }
37
+
38
+ await resource.delete(id)
39
+ return { removed: true }
40
+ }
41
+
42
+ return { list, add, remove }
43
+ }
@@ -3,6 +3,9 @@ import { type PanelNavLink, HOME } from '@owlmeans/web-panel'
3
3
  /** The alias the session items are stored under. Screens address the store by it, never by path. */
4
4
  export const SESSION_STATE = 'session-items'
5
5
 
6
+ /** The `localStorage` key that keeps this browser's session id across reloads. */
7
+ export const SESSION_ID_KEY = '__APP_SLUG__-sid'
8
+
6
9
  // The platform/owner credit — "Powered by OwlMeans" and the copyright the platform delivers — is
7
10
  // rendered by the shell itself (`NavLayout`'s `Footer`) and is never a footer link: an app link
8
11
  // list is places IN the app, and the credit is not one of those.
@@ -2,18 +2,16 @@ import { useEffect, useState, type FC } from 'react'
2
2
  import { useStoreList } from '@owlmeans/client'
3
3
  import { session, type SessionItem } from '__APP_SLUG__-common'
4
4
  import { useContext } from '../context.js'
5
- import { SESSION_STATE } from '../consts.js'
5
+ import { SESSION_ID_KEY, SESSION_STATE } from '../consts.js'
6
6
  import { Button } from '@/components/ui/button'
7
7
  import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'
8
8
  import { Input } from '@/components/ui/input'
9
9
 
10
- const SID_KEY = '__APP_SLUG__-sid'
11
-
12
10
  const sessionId = (): string => {
13
- let sid = localStorage.getItem(SID_KEY)
11
+ let sid = localStorage.getItem(SESSION_ID_KEY)
14
12
  if (sid == null) {
15
13
  sid = crypto.randomUUID()
16
- localStorage.setItem(SID_KEY, sid)
14
+ localStorage.setItem(SESSION_ID_KEY, sid)
17
15
  }
18
16
  return sid
19
17
  }