@webjskit/cli 0.1.0 → 0.1.1

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/lib/create.js CHANGED
@@ -42,6 +42,7 @@ export async function scaffoldApp(name, cwd, opts = {}) {
42
42
  'modules',
43
43
  'lib',
44
44
  'public',
45
+ 'prisma',
45
46
  'test/unit',
46
47
  'test/e2e',
47
48
  ];
@@ -55,6 +56,8 @@ export async function scaffoldApp(name, cwd, opts = {}) {
55
56
  type: 'module',
56
57
  private: true,
57
58
  scripts: {
59
+ predev: 'prisma generate',
60
+ prestart: 'prisma migrate deploy',
58
61
  dev: 'webjs dev',
59
62
  build: 'webjs build',
60
63
  start: 'webjs start',
@@ -62,19 +65,22 @@ export async function scaffoldApp(name, cwd, opts = {}) {
62
65
  'test:server': 'webjs test --server',
63
66
  'test:browser': 'webjs test --browser',
64
67
  check: 'webjs check',
68
+ 'db:migrate': 'prisma migrate dev',
69
+ 'db:generate': 'prisma generate',
70
+ 'db:studio': 'prisma studio',
65
71
  },
66
72
  dependencies: {
73
+ '@prisma/client': '^6.0.0',
67
74
  '@webjskit/cli': 'latest',
68
75
  '@webjskit/core': 'latest',
69
76
  '@webjskit/server': 'latest',
70
- ...(isSaas ? { '@prisma/client': '^6.0.0' } : {}),
71
77
  },
72
78
  devDependencies: {
73
79
  esbuild: '^0.28.0',
80
+ prisma: '^6.0.0',
74
81
  '@web/test-runner': '^0.20.0',
75
82
  '@web/test-runner-playwright': '^0.11.0',
76
83
  'playwright': '^1.59.0',
77
- ...(isSaas ? { prisma: '^6.0.0' } : {}),
78
84
  },
79
85
  }, null, 2) + '\n');
80
86
 
@@ -95,9 +101,10 @@ export async function scaffoldApp(name, cwd, opts = {}) {
95
101
  },
96
102
  }, null, 2) + '\n');
97
103
 
98
- // --- Templates (CONVENTIONS.md, CLAUDE.md, test files, Claude hooks) ---
104
+ // --- Templates (AGENTS.md, CONVENTIONS.md, CLAUDE.md, test files, Claude hooks) ---
99
105
 
100
106
  const templateFiles = [
107
+ 'AGENTS.md',
101
108
  'CONVENTIONS.md',
102
109
  'CLAUDE.md',
103
110
  'test/unit/example.test.ts',
@@ -139,6 +146,64 @@ export async function scaffoldApp(name, cwd, opts = {}) {
139
146
  const preCommitPath = join(appDir, '.hooks', 'pre-commit');
140
147
  if (existsSync(preCommitPath)) await chmod(preCommitPath, 0o755);
141
148
 
149
+ // --- Prisma schema + client singleton (all templates) ---
150
+
151
+ await writeFile(join(appDir, 'prisma', 'schema.prisma'), `generator client {
152
+ provider = "prisma-client-js"
153
+ }
154
+
155
+ datasource db {
156
+ // Defaults to SQLite at ./prisma/dev.db. Switch to postgresql / mysql
157
+ // by changing the provider + DATABASE_URL in .env.
158
+ provider = "sqlite"
159
+ url = env("DATABASE_URL")
160
+ }
161
+
162
+ // Example model — feel free to delete or extend.
163
+ model User {
164
+ id Int @id @default(autoincrement())
165
+ email String @unique
166
+ name String?
167
+ createdAt DateTime @default(now())
168
+ }
169
+ `);
170
+
171
+ await writeFile(join(appDir, 'lib', 'prisma.ts'), `/**
172
+ * Prisma client singleton. The \`globalThis\` trick keeps a single
173
+ * instance across dev-server module reloads, so we don't open a new
174
+ * DB connection on every file change.
175
+ */
176
+ import { PrismaClient } from '@prisma/client';
177
+
178
+ const g = globalThis as unknown as { __prisma?: PrismaClient };
179
+
180
+ export const prisma = g.__prisma ?? new PrismaClient();
181
+ if (process.env.NODE_ENV !== 'production') g.__prisma = prisma;
182
+ `);
183
+
184
+ // Env vars: append DATABASE_URL to the .env.example the template
185
+ // already copied (if present). The scaffold's root .env.example
186
+ // lists auth secrets etc.; we just add the DB line idempotently.
187
+ const envExample = join(appDir, '.env.example');
188
+ if (existsSync(envExample)) {
189
+ const cur = await readFile(envExample, 'utf8');
190
+ if (!cur.includes('DATABASE_URL')) {
191
+ await writeFile(envExample, cur.replace(/\n?$/, '\n') + '\nDATABASE_URL=file:./prisma/dev.db\n');
192
+ }
193
+ } else {
194
+ await writeFile(envExample, 'DATABASE_URL=file:./prisma/dev.db\n');
195
+ }
196
+
197
+ // .gitignore the generated SQLite file.
198
+ const gitignore = join(appDir, '.gitignore');
199
+ const gitignoreExtra = '\n# SQLite dev database\nprisma/dev.db\nprisma/dev.db-journal\n';
200
+ if (existsSync(gitignore)) {
201
+ const cur = await readFile(gitignore, 'utf8');
202
+ if (!cur.includes('prisma/dev.db')) await writeFile(gitignore, cur + gitignoreExtra);
203
+ } else {
204
+ await writeFile(gitignore, 'node_modules\n.webjs\n' + gitignoreExtra);
205
+ }
206
+
142
207
  // --- App files (template-specific) ---
143
208
 
144
209
  if (isApi) {
@@ -386,12 +451,8 @@ export default function Home() {
386
451
  }
387
452
  `);
388
453
 
389
- // --- AGENTS.md (copy from framework root) ---
390
-
391
- const agentsSrc = resolve(__dirname, '..', '..', '..', 'AGENTS.md');
392
- if (existsSync(agentsSrc)) {
393
- await cp(agentsSrc, join(appDir, 'AGENTS.md'));
394
- }
454
+ // AGENTS.md is copied via the `templateFiles` loop above, from
455
+ // `packages/cli/templates/AGENTS.md` with `{{APP_NAME}}` substitution.
395
456
 
396
457
  // --- Theme toggle component ---
397
458
 
@@ -459,11 +520,8 @@ ThemeToggle.register('theme-toggle');
459
520
  await writeSaasFiles(appDir);
460
521
  }
461
522
 
462
- // --- AGENTS.md (always copy) ---
463
- const agentsSrc2 = resolve(__dirname, '..', '..', '..', 'AGENTS.md');
464
- if (!existsSync(join(appDir, 'AGENTS.md')) && existsSync(agentsSrc2)) {
465
- await cp(agentsSrc2, join(appDir, 'AGENTS.md'));
466
- }
523
+ // AGENTS.md is already in place via the shared `templateFiles` loop
524
+ // earlier in this function no framework-root fallback needed.
467
525
 
468
526
  // --- Git init + configure hooks directory ---
469
527
  const { execSync } = await import('node:child_process');
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@webjskit/cli",
3
- "version": "0.1.0",
3
+ "version": "0.1.1",
4
4
  "type": "module",
5
5
  "description": "webjs CLI — dev, start, create, db",
6
6
  "bin": {
@@ -0,0 +1,195 @@
1
+ # AGENTS.md — {{APP_NAME}}
2
+
3
+ Read this before editing any file. This is a webjs app: AI-first, web-
4
+ components-first, no build step. The framework's own full API reference
5
+ lives at https://github.com/vivek7405/webjs/blob/main/AGENTS.md — treat
6
+ this file as the app-scoped companion.
7
+
8
+ ## Framework source is in `node_modules/`
9
+
10
+ No build step, no bundler, no minification — what you read is what
11
+ runs. When in doubt, grep the framework:
12
+
13
+ ```
14
+ node_modules/@webjskit/
15
+ core/ renderer, WebComponent, directives, client router,
16
+ Task, context, testing helpers
17
+ src/component.js ← lifecycle, properties, light vs shadow DOM
18
+ src/render-client.js ← client-side DOM patching + hydration
19
+ src/render-server.js ← renderToString / renderToStream
20
+ src/router-client.js ← Turbo-Drive-style client navigation
21
+ src/directives.js ← unsafeHTML, live
22
+ src/context.js ← Context Protocol
23
+ src/task.js ← async data with states
24
+ server/ dev + prod server, SSR, file router, actions,
25
+ auth, sessions, cache, rate-limit, WebSocket
26
+ src/ssr.js ← how metadata becomes <head> tags
27
+ src/router.js ← file convention → route table
28
+ src/actions.js ← .server.ts scanner, RPC, expose()
29
+ src/auth.js, session.js, cache.js, rate-limit.js, csrf.js
30
+ cli/ webjs CLI (dev / start / build / test / check / create / db)
31
+ ts-plugin/ tsserver go-to-definition for custom-element tag names
32
+ ```
33
+
34
+ Reaching straight for the source is the fastest way to resolve "why
35
+ doesn't X work?" — no documentation guesswork, no stale blog posts.
36
+
37
+ ## File conventions
38
+
39
+ ```
40
+ app/ thin route adapters — import from modules/
41
+ page.ts → /
42
+ layout.ts root layout, wraps every page
43
+ error.ts error boundary (render failures → user-friendly)
44
+ loading.ts Suspense fallback for sibling page
45
+ not-found.ts custom 404 page
46
+ middleware.ts global request middleware
47
+ [slug]/page.ts dynamic route segment
48
+ [...rest]/page.ts catch-all
49
+ (group)/ route group (parens not in URL)
50
+ _private/ underscore = not routable
51
+ api/
52
+ <path>/route.ts GET / POST / PUT / DELETE / WS handlers
53
+ sitemap.ts metadata route → /sitemap.xml
54
+ robots.ts metadata route → /robots.txt
55
+ opengraph-image.ts metadata route → /opengraph-image
56
+ components/ web components — extend WebComponent, call .register()
57
+ modules/<feature>/
58
+ actions/*.server.ts server actions (one function per file)
59
+ queries/*.server.ts data reads (one function per file)
60
+ components/*.ts feature-scoped components
61
+ utils/*.ts feature-scoped helpers
62
+ types.ts feature types
63
+ lib/
64
+ prisma.ts PrismaClient singleton (import from here, never `new PrismaClient()`)
65
+ ... other cross-cutting infra (session, auth config, etc.)
66
+ prisma/
67
+ schema.prisma Prisma schema — SQLite by default, switch provider for Postgres/MySQL
68
+ dev.db SQLite file (gitignored); run `npm run db:migrate` to create
69
+ migrations/ generated migration SQL
70
+ public/ static assets, served at /public/*
71
+ test/unit/*.test.ts unit tests (node --test)
72
+ test/browser/*.test.ts browser tests (web-test-runner)
73
+ middleware.ts root middleware (optional, outermost)
74
+ ```
75
+
76
+ ## Database (Prisma + SQLite by default)
77
+
78
+ Every scaffold includes a Prisma setup pointed at a local SQLite file.
79
+ First-run workflow:
80
+
81
+ ```sh
82
+ cp .env.example .env # DATABASE_URL is pre-filled for SQLite
83
+ npm run db:migrate # creates prisma/dev.db + migration
84
+ npm run dev # webjs dev + prisma generate via predev
85
+ ```
86
+
87
+ Scripts:
88
+
89
+ - `npm run db:migrate` — `prisma migrate dev` (dev-time schema changes + migration + generate)
90
+ - `npm run db:generate` — `prisma generate` (regenerate client only)
91
+ - `npm run db:studio` — `prisma studio` (GUI)
92
+ - `predev` hook auto-runs `prisma generate` before `npm run dev`
93
+ - `prestart` hook runs `prisma migrate deploy` before `npm start` (idempotent in prod)
94
+
95
+ Always import the client from `lib/prisma.ts` (never `new PrismaClient()` directly —
96
+ the singleton avoids opening a new connection on every dev-server reload):
97
+
98
+ ```ts
99
+ import { prisma } from '../../../lib/prisma.ts';
100
+ const users = await prisma.user.findMany();
101
+ ```
102
+
103
+ To switch to Postgres or MySQL: change `provider` in `prisma/schema.prisma`
104
+ and the `DATABASE_URL` in `.env`.
105
+
106
+ ## Imports
107
+
108
+ ```ts
109
+ import { html, css, WebComponent } from '@webjskit/core';
110
+ import '@webjskit/core/client-router'; // enable SPA nav
111
+ import { unsafeHTML, live } from '@webjskit/core/directives';
112
+ import { createContext } from '@webjskit/core/context';
113
+ import { Task } from '@webjskit/core/task';
114
+ import { fixture, waitForUpdate } from '@webjskit/core/testing';
115
+
116
+ import { rateLimit, cache, createAuth, Credentials, Session } from '@webjskit/server';
117
+ ```
118
+
119
+ ## Component pattern
120
+
121
+ ```ts
122
+ import { WebComponent, html, css } from '@webjskit/core';
123
+
124
+ export class Counter extends WebComponent {
125
+ static tag = 'my-counter'; // required, must contain a hyphen
126
+ static properties = { count: { type: Number } };
127
+ static styles = css`button { padding: 8px 12px; }`;
128
+ // static shadow = true; // opt into shadow DOM (default: light DOM)
129
+ // static lazy = true; // download JS only when scrolled into view
130
+
131
+ render() {
132
+ return html`
133
+ <button @click=${() => this.setState({ count: this.count + 1 })}>
134
+ ${this.count}
135
+ </button>
136
+ `;
137
+ }
138
+ }
139
+ Counter.register('my-counter');
140
+ ```
141
+
142
+ ## Server action pattern
143
+
144
+ ```ts
145
+ // modules/posts/actions/create-post.server.ts
146
+ 'use server';
147
+ import { prisma } from '../../../lib/prisma.ts';
148
+
149
+ export async function createPost(input: { title: string; body: string }) {
150
+ if (!input.title) return { success: false, error: 'title required', status: 400 };
151
+ const post = await prisma.post.create({ data: input });
152
+ return { success: true, data: post };
153
+ }
154
+ ```
155
+
156
+ Import it from a client component — the framework rewrites it into a
157
+ type-safe RPC stub automatically.
158
+
159
+ ## Metadata (per-page)
160
+
161
+ ```ts
162
+ export const metadata = {
163
+ title: 'My page',
164
+ description: 'A page in {{APP_NAME}}',
165
+ openGraph: { type: 'website', image: 'https://...' },
166
+ twitter: { card: 'summary_large_image' },
167
+ cacheControl: 'public, max-age=60', // opt into caching (default: no-store)
168
+ };
169
+ ```
170
+
171
+ Use `generateMetadata(ctx)` when you need request-scoped values (e.g.
172
+ absolute URLs from `ctx.url`).
173
+
174
+ ## Invariants (do not violate)
175
+
176
+ 1. Custom element tags must contain a hyphen. Set `static tag`, call `.register()`.
177
+ 2. Never import `@prisma/client` or `node:*` from client-reachable files —
178
+ only from `.server.ts` modules or `lib/*.ts`.
179
+ 3. Event / property / boolean holes in `` html`` `` are unquoted:
180
+ `@click=${fn}`, not `@click="${fn}"`.
181
+ 4. Use `setState()` — never mutate `this.state` directly.
182
+ 5. Pages / layouts / metadata routes default-export a server-only function.
183
+ 6. One exported function per action / query file. Name the file after it.
184
+
185
+ ## Workflow expectations for AI agents
186
+
187
+ 1. Branch before editing — never push to `main` directly.
188
+ 2. Every code change comes with: unit test(s), AGENTS.md / docs updates if
189
+ the feature surface changed, `npx webjs check` passing.
190
+ 3. Commit and push after each logical unit. No AI attribution trailers.
191
+ 4. When unsure how a framework feature works, `grep` or `cat` the
192
+ relevant `node_modules/@webjskit/*/src/` file before asking the user.
193
+
194
+ Project-specific conventions and overrides live in
195
+ [CONVENTIONS.md](./CONVENTIONS.md).
@@ -82,14 +82,19 @@ variables control infrastructure — no config files needed:
82
82
 
83
83
  | Environment variable | Effect |
84
84
  |---|---|
85
- | `REDIS_URL` | Cache, sessions, rate limiting, and pub/sub all use Redis |
85
+ | `REDIS_URL` | Connection string consumed by `redisStore({ url: process.env.REDIS_URL })`. Not auto-wired — call `setStore(redisStore())` once at app startup to put cache / sessions / rate-limit on Redis. |
86
86
  | `AUTH_SECRET` | Required for auth JWT signing (32+ random chars) |
87
87
  | `AUTH_GOOGLE_ID` | Google OAuth client ID (optional) |
88
88
  | `AUTH_GITHUB_ID` | GitHub OAuth client ID (optional) |
89
89
  | `PORT` | Server port (default: 3000) |
90
90
 
91
91
  **Development:** zero env vars needed. Everything works with memory/cookie/disk.
92
- **Production:** set `REDIS_URL` + `SESSION_SECRET`. That's it.
92
+ **Production:** set `AUTH_SECRET` + `SESSION_SECRET`. For horizontal scaling, also set `REDIS_URL` and add one line at app startup:
93
+
94
+ ```js
95
+ import { setStore, redisStore } from '@webjskit/server';
96
+ setStore(redisStore({ url: process.env.REDIS_URL }));
97
+ ```
93
98
 
94
99
  ---
95
100