@webjsdev/cli 0.8.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/README.md ADDED
@@ -0,0 +1,71 @@
1
+ # @webjsdev/cli
2
+
3
+ CLI for [webjs](https://github.com/vivek7405/webjs): scaffold, develop,
4
+ build, and run webjs apps.
5
+
6
+ Installing this package gives you the `webjs` command.
7
+
8
+ ## Install
9
+
10
+ Install once, globally:
11
+
12
+ ```sh
13
+ npm i -g @webjsdev/cli
14
+ ```
15
+
16
+ Then scaffold a new app anywhere:
17
+
18
+ ```sh
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 @webjsdev/cli create my-app
28
+ cd my-app && npm install && npm run dev
29
+ ```
30
+
31
+ ## Commands
32
+
33
+ ```sh
34
+ webjs create <name> # scaffold a full-stack app (default)
35
+ webjs create <name> --template api # backend-only API app
36
+ webjs create <name> --template saas # auth + dashboard + Prisma User model
37
+
38
+ webjs dev # dev server with live reload
39
+ webjs start # production server (no build step, serves source directly)
40
+ webjs check # validate project conventions
41
+ webjs test # run server + browser tests
42
+ webjs db <prisma-subcommand> # prisma passthrough (saas template)
43
+
44
+ webjs ui init # initialise @webjsdev/ui in this project
45
+ webjs ui add <names...> # copy components from the registry (https://ui.webjs.dev/registry/<name>.json)
46
+ webjs ui list # list every component available in the registry
47
+ ```
48
+
49
+ `webjs ui` proxies to [`@webjsdev/ui`](https://www.npmjs.com/package/@webjsdev/ui),
50
+ an AI-first component library + CLI that copies sources into your project: class
51
+ helpers (`buttonClass`, `cardClass`, …) for the visual primitives and a small set
52
+ of stateful custom elements (`<ui-dialog>`, `<ui-tabs>`, `<ui-popover>`) where
53
+ state matters. The package is a hard dependency of `@webjsdev/cli`, so installing
54
+ the CLI gives you `webjs ui` automatically. See
55
+ [https://ui.webjs.dev](https://ui.webjs.dev) for the catalogue.
56
+
57
+ ## Scaffolded templates
58
+
59
+ The scaffold seeds opinionated defaults so AI agents produce consistent code:
60
+
61
+ - `AGENTS.md` + `CONVENTIONS.md` (the machine-readable contract)
62
+ - `.claude/`, `.cursorrules`, `.windsurfrules`, `.github/copilot-instructions.md`
63
+ - `test/<feature>/` (with optional `browser/` / `e2e/` subfolders per kind) with example tests
64
+ - Tailwind CSS via CLI (no browser runtime at build time)
65
+ - TypeScript, `.editorconfig`, `.gitignore`
66
+
67
+ See the full framework docs at https://github.com/vivek7405/webjs.
68
+
69
+ ## License
70
+
71
+ MIT
package/bin/webjs.js ADDED
@@ -0,0 +1,279 @@
1
+ #!/usr/bin/env node
2
+ import { resolve, join, dirname } from 'node:path';
3
+ import { spawn } from 'node:child_process';
4
+ import { fileURLToPath } from 'node:url';
5
+
6
+ const __dirname = dirname(fileURLToPath(import.meta.url));
7
+ const [cmd, ...rest] = process.argv.slice(2);
8
+
9
+ // Exactly three scaffolds exist. Keep this list as the single source of
10
+ // truth. AI-agent docs in README.md / AGENTS.md / .cursorrules /
11
+ // .windsurfrules / .github/copilot-instructions.md mirror it.
12
+ const TEMPLATES = ['full-stack', 'api', 'saas'];
13
+
14
+ const USAGE = `webjs commands:
15
+ webjs dev [--port 3000] Start dev server with live reload
16
+ webjs start [--port 3000] Start production server (serves source directly, no build step)
17
+ webjs test [--server|--browser] Run server + browser tests
18
+ webjs check Validate app against conventions
19
+ webjs create <name> [--template full-stack|api|saas] Scaffold a new webjs app
20
+ (only 3 templates exist. default: full-stack with Prisma+SQLite)
21
+ webjs db generate Run \`prisma generate\`
22
+ webjs db migrate [name] Run \`prisma migrate dev\`
23
+ webjs db studio Run \`prisma studio\`
24
+ webjs ui <subcmd> AI-first component library CLI
25
+ (init / add / list / view / diff / info)
26
+ Requires @webjsdev/ui installed in the project
27
+ webjs help Show this help`;
28
+
29
+ /** @param {string[]} args */
30
+ function flag(args, name, def) {
31
+ const i = args.indexOf(name);
32
+ if (i === -1) return def;
33
+ return args[i + 1];
34
+ }
35
+
36
+ async function main() {
37
+ switch (cmd) {
38
+ case 'dev': {
39
+ // If we're already inside the --watch child, start the server directly.
40
+ if (process.env.__WEBJS_DEV_CHILD === '1') {
41
+ const { startServer } = await import('@webjsdev/server');
42
+ const port = Number(flag(rest, '--port', process.env.PORT || 3000));
43
+ await startServer({ appDir: process.cwd(), port, dev: true });
44
+ break;
45
+ }
46
+
47
+ // Otherwise, spawn ourselves as a child with node --watch.
48
+ // This restarts the process on file changes, guaranteeing a fresh
49
+ // Node ESM module cache. Without this, edits to transitively-imported
50
+ // modules (actions, queries, components, utils) don't take effect
51
+ // because Node caches ESM by URL with no public invalidation API.
52
+ // Build watch paths from directories that exist in the project.
53
+ const { existsSync } = await import('node:fs');
54
+ const watchPaths = [];
55
+ for (const dir of ['app', 'components', 'modules', 'lib', 'actions']) {
56
+ if (existsSync(dir)) watchPaths.push('--watch-path', dir);
57
+ }
58
+ // Watch root middleware/config if present
59
+ for (const f of ['middleware.ts', 'middleware.js']) {
60
+ if (existsSync(f)) watchPaths.push('--watch-path', f);
61
+ }
62
+
63
+ const child = spawn(
64
+ process.execPath,
65
+ [
66
+ '--watch',
67
+ '--watch-preserve-output',
68
+ ...watchPaths,
69
+ ...process.argv.slice(1),
70
+ ],
71
+ {
72
+ stdio: 'inherit',
73
+ cwd: process.cwd(),
74
+ env: { ...process.env, __WEBJS_DEV_CHILD: '1' },
75
+ },
76
+ );
77
+ child.on('exit', (code) => process.exit(code ?? 0));
78
+ break;
79
+ }
80
+ case 'start': {
81
+ const { startServer } = await import('@webjsdev/server');
82
+ const port = Number(flag(rest, '--port', process.env.PORT || 3000));
83
+ await startServer({ appDir: process.cwd(), port, dev: false });
84
+ break;
85
+ }
86
+ case 'db': {
87
+ const sub = rest[0];
88
+ const args = rest.slice(1);
89
+ const map = { generate: ['generate'], migrate: ['migrate', 'dev', ...args], studio: ['studio'] };
90
+ const prismaArgs = map[sub];
91
+ if (!prismaArgs) { console.error('Unknown db subcommand.\n' + USAGE); process.exit(1); }
92
+ const child = spawn('npx', ['prisma', ...prismaArgs], { stdio: 'inherit', cwd: process.cwd() });
93
+ child.on('exit', (code) => process.exit(code ?? 0));
94
+ break;
95
+ }
96
+ case 'ui': {
97
+ // Delegate to @webjsdev/ui. Bundled as a hard dependency of
98
+ // @webjsdev/cli, so `npm install -g @webjsdev/cli` pulls it in
99
+ // automatically, and `webjs ui add button` works out of the box
100
+ // without an extra install in user projects.
101
+ const { createRequire } = await import('node:module');
102
+ const req = createRequire(import.meta.url);
103
+ let entry;
104
+ try {
105
+ entry = req.resolve('@webjsdev/ui/bin/webjsui.js');
106
+ } catch {
107
+ // Fallback: try resolving from the user's cwd in case of weird
108
+ // workspace setups.
109
+ try {
110
+ const userReq = createRequire(join(process.cwd(), 'package.json'));
111
+ entry = userReq.resolve('@webjsdev/ui/bin/webjsui.js');
112
+ } catch {
113
+ console.error('@webjsdev/ui could not be resolved.');
114
+ console.error('Reinstall the CLI: npm install -g @webjsdev/cli');
115
+ process.exit(1);
116
+ }
117
+ }
118
+ const child = spawn('node', [entry, ...rest], { stdio: 'inherit', cwd: process.cwd() });
119
+ child.on('exit', (code) => process.exit(code ?? 0));
120
+ break;
121
+ }
122
+ case 'test': {
123
+ const cwd = process.cwd();
124
+ const { existsSync } = await import('node:fs');
125
+
126
+ // Two test runners:
127
+ // 1. node:test for server-side tests (test/server/*.test.ts, test/unit/*.test.ts)
128
+ // 2. WTR + Playwright for browser tests (test/browser/*.test.js)
129
+ //
130
+ // `webjs test` → runs both
131
+ // `webjs test --server` → server tests only (node:test)
132
+ // `webjs test --browser` → browser tests only (WTR + Playwright)
133
+
134
+ const serverOnly = rest.includes('--server');
135
+ const browserOnly = rest.includes('--browser');
136
+ const runServer = !browserOnly;
137
+ const runBrowser = !serverOnly;
138
+
139
+ // --- Server tests (node:test) ---
140
+ if (runServer) {
141
+ const { readdir } = await import('node:fs/promises');
142
+ const testFiles = [];
143
+
144
+ for (const dir of ['test/server', 'test/unit', 'test']) {
145
+ const fullDir = join(cwd, dir);
146
+ if (!existsSync(fullDir)) continue;
147
+ const files = await readdir(fullDir);
148
+ for (const f of files) {
149
+ if (/\.test\.(js|ts|mjs|mts)$/.test(f)) {
150
+ const full = join(fullDir, f);
151
+ if (!testFiles.includes(full)) testFiles.push(full);
152
+ }
153
+ }
154
+ }
155
+
156
+ if (testFiles.length > 0) {
157
+ console.log(`webjs test: running ${testFiles.length} server test file(s)…\n`);
158
+ const child = spawn(process.execPath, ['--test', ...testFiles], {
159
+ stdio: 'inherit', cwd, env: { ...process.env },
160
+ });
161
+ const code = await new Promise(r => child.on('exit', r));
162
+ if (code !== 0) process.exit(code ?? 1);
163
+ }
164
+ }
165
+
166
+ // --- Browser tests (WTR + Playwright) ---
167
+ if (runBrowser) {
168
+ const wtrConfig = join(cwd, 'web-test-runner.config.js');
169
+ if (existsSync(wtrConfig) || existsSync(join(cwd, 'web-test-runner.config.mjs'))) {
170
+ console.log(`\nwebjs test: running browser tests (WTR + Playwright)…\n`);
171
+ const child = spawn('npx', ['wtr'], {
172
+ stdio: 'inherit', cwd, env: { ...process.env },
173
+ });
174
+ const code = await new Promise(r => child.on('exit', r));
175
+ if (code !== 0) process.exit(code ?? 1);
176
+ } else if (!serverOnly) {
177
+ // No WTR config, check for test/browser directory
178
+ const browserDir = join(cwd, 'test', 'browser');
179
+ if (existsSync(browserDir)) {
180
+ console.log(`\nwebjs test: running browser tests (WTR + Playwright)…\n`);
181
+ const child = spawn('npx', ['wtr', '--files', 'test/browser/**/*.test.js'], {
182
+ stdio: 'inherit', cwd, env: { ...process.env },
183
+ });
184
+ const code = await new Promise(r => child.on('exit', r));
185
+ if (code !== 0) process.exit(code ?? 1);
186
+ }
187
+ }
188
+ }
189
+
190
+ console.log('\nwebjs test: done ✓');
191
+ break;
192
+ }
193
+ case 'check': {
194
+ const { checkConventions, RULES, loadConventionOverrides } = await import('@webjsdev/server/check');
195
+
196
+ if (rest.includes('--rules')) {
197
+ const overrides = await loadConventionOverrides(process.cwd());
198
+ const anyOverride = Object.keys(overrides).length > 0;
199
+ console.log('webjs check, available rules:');
200
+ console.log(' All rules are ENABLED by default. A rule is only off when');
201
+ console.log(' package.json "webjs": { "conventions": { ... } } sets it');
202
+ console.log(' to false.\n');
203
+ for (const r of RULES) {
204
+ const off = overrides[r.name] === false;
205
+ const status = off ? '[disabled by override]' : '[enabled]';
206
+ console.log(` ${r.name.padEnd(30)} ${status.padEnd(24)} ${r.description}`);
207
+ }
208
+ if (!anyOverride) {
209
+ console.log('\n (no overrides found; every rule above is active in this project)');
210
+ }
211
+ break;
212
+ }
213
+
214
+ const violations = await checkConventions(process.cwd());
215
+
216
+ if (violations.length === 0) {
217
+ console.log('webjs check: all conventions pass ✓');
218
+ } else {
219
+ console.log(`webjs check: ${violations.length} violation(s) found\n`);
220
+ for (const v of violations) {
221
+ console.log(` ✗ [${v.rule}] ${v.file}`);
222
+ console.log(` ${v.message}`);
223
+ if (v.fix) console.log(` Fix: ${v.fix}`);
224
+ console.log();
225
+ }
226
+ process.exit(1);
227
+ }
228
+ break;
229
+ }
230
+ case 'create': {
231
+ const name = rest[0];
232
+ if (!name || name.startsWith('-')) {
233
+ console.error('Usage: webjs create <app-name> [--template full-stack|api|saas]');
234
+ process.exit(1);
235
+ }
236
+ const template = flag(rest, '--template', 'full-stack');
237
+ if (!TEMPLATES.includes(template)) {
238
+ // AI agents sometimes hallucinate template names ("blog", "todo",
239
+ // "ecommerce"). Reject early with the canonical list + guidance
240
+ // on which scaffold to pick for which kind of app.
241
+ console.error(`Error: unknown template '${template}'.
242
+
243
+ Only three scaffolds exist:
244
+ full-stack (default): pages + components + API + Prisma/SQLite.
245
+ Pick this for any app the user describes in product terms
246
+ (todo app, blog, dashboard, marketplace, social feed, …).
247
+ api backend-only: route handlers + modules, no pages/SSR.
248
+ Pick this only if the user explicitly asks for an HTTP/JSON
249
+ API with no UI.
250
+ saas auth + login/signup + protected dashboard + Prisma User
251
+ model. Pick this only if the user explicitly asks for auth
252
+ or a SaaS-shaped product.
253
+
254
+ The scaffold is a starting point. Replace the example layout/page/
255
+ components/schema with the actual app the user requested. Use Prisma +
256
+ SQLite for persistence (already wired up). Never store app data in JSON
257
+ files.
258
+
259
+ Full docs: https://docs.webjs.com`);
260
+ process.exit(1);
261
+ }
262
+ const { scaffoldApp } = await import('../lib/create.js');
263
+ await scaffoldApp(name, process.cwd(), { template });
264
+ break;
265
+ }
266
+ case 'help':
267
+ case undefined:
268
+ console.log(USAGE);
269
+ break;
270
+ default:
271
+ console.error(`Unknown command: ${cmd}\n` + USAGE);
272
+ process.exit(1);
273
+ }
274
+ }
275
+
276
+ main().catch((e) => {
277
+ console.error(e);
278
+ process.exit(1);
279
+ });