@webjskit/cli 0.1.0 → 0.1.2

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
@@ -7,20 +7,25 @@ Installing this package gives you the `webjs` command.
7
7
 
8
8
  ## Install
9
9
 
10
- Most users won't install globally. Scaffold a new app instead:
10
+ Install once, globally:
11
11
 
12
12
  ```sh
13
- npx @webjskit/cli create my-app
14
- cd my-app
15
- npm install
16
- npm run dev
13
+ npm i -g @webjskit/cli
17
14
  ```
18
15
 
19
- Or globally:
16
+ Then scaffold a new app anywhere:
20
17
 
21
18
  ```sh
22
- npm install -g @webjskit/cli
23
19
  webjs create my-app
20
+ cd my-app && npm install && npm run dev
21
+ # → http://localhost:3000
22
+ ```
23
+
24
+ One-shot without global install:
25
+
26
+ ```sh
27
+ npx @webjskit/cli create my-app
28
+ cd my-app && npm install && npm run dev
24
29
  ```
25
30
 
26
31
  ## Commands
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
 
@@ -437,18 +498,26 @@ export class ThemeToggle extends WebComponent {
437
498
  }
438
499
 
439
500
  render() {
501
+ const t = this.state.theme;
502
+ const label = t === 'system' ? 'AUTO' : t === 'light' ? 'LIGHT' : 'DARK';
503
+ const icon = t === 'light' ? ICONS.sun : t === 'dark' ? ICONS.moon : ICONS.system;
440
504
  return html\`
441
505
  <button
442
- class="inline-flex items-center px-3 py-1.5 rounded-full border border-border bg-bg-elev text-fg-muted font-mono text-[11px] leading-none tracking-wider uppercase duration-fast hover:text-fg hover:border-border-strong"
506
+ class="inline-flex items-center justify-center w-9 h-9 p-0 border border-border rounded-full bg-bg-elev text-fg-muted cursor-pointer transition-all duration-150 hover:text-fg hover:border-border-strong active:scale-[0.94] focus-visible:outline-none focus-visible:border-accent focus-visible:ring-[3px] focus-visible:ring-accent-tint"
443
507
  @click=\${() => this.cycle()}
444
- >
445
- \${this.state.theme === 'system' ? 'Auto'
446
- : this.state.theme === 'light' ? 'Light' : 'Dark'}
447
- </button>
508
+ aria-label="Cycle theme (currently \${label})"
509
+ title="Theme: \${label.toLowerCase()}"
510
+ >\${icon}</button>
448
511
  \`;
449
512
  }
450
513
  }
451
514
 
515
+ const ICONS = {
516
+ sun: html\`<svg class="w-4 h-4 stroke-current fill-none" style="stroke-width:1.8;stroke-linecap:round;stroke-linejoin:round" viewBox="0 0 24 24"><circle cx="12" cy="12" r="4"/><path d="M12 3v2M12 19v2M4.93 4.93l1.41 1.41M17.66 17.66l1.41 1.41M3 12h2M19 12h2M4.93 19.07l1.41-1.41M17.66 6.34l1.41-1.41"/></svg>\`,
517
+ moon: html\`<svg class="w-4 h-4 stroke-current fill-none" style="stroke-width:1.8;stroke-linecap:round;stroke-linejoin:round" viewBox="0 0 24 24"><path d="M21 12.8A9 9 0 1 1 11.2 3a7 7 0 0 0 9.8 9.8Z"/></svg>\`,
518
+ system: html\`<svg class="w-4 h-4 stroke-current fill-none" style="stroke-width:1.8;stroke-linecap:round;stroke-linejoin:round" viewBox="0 0 24 24"><path d="M3 5h18v11H3zM8 20h8M12 16v4"/></svg>\`,
519
+ };
520
+
452
521
  ThemeToggle.register('theme-toggle');
453
522
  `);
454
523
  } // end if (!isApi)
@@ -459,11 +528,8 @@ ThemeToggle.register('theme-toggle');
459
528
  await writeSaasFiles(appDir);
460
529
  }
461
530
 
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
- }
531
+ // AGENTS.md is already in place via the shared `templateFiles` loop
532
+ // earlier in this function no framework-root fallback needed.
467
533
 
468
534
  // --- Git init + configure hooks directory ---
469
535
  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.2",
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