@fluixi/cli 0.1.0-alpha.82 → 0.1.0-alpha.83

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.
Files changed (57) hide show
  1. package/README.md +21 -3
  2. package/dist/add.cjs +590 -0
  3. package/dist/add.d.ts +10 -0
  4. package/dist/add.d.ts.map +1 -0
  5. package/dist/add.js +107 -0
  6. package/dist/add.mjs +562 -0
  7. package/dist/check.cjs +102 -0
  8. package/dist/check.d.ts +13 -0
  9. package/dist/check.d.ts.map +1 -0
  10. package/dist/check.js +88 -0
  11. package/dist/check.mjs +76 -0
  12. package/dist/cli.cjs +977 -78
  13. package/dist/cli.js +44 -2
  14. package/dist/cli.mjs +976 -77
  15. package/dist/doctor.cjs +119 -0
  16. package/dist/doctor.d.ts +13 -0
  17. package/dist/doctor.d.ts.map +1 -0
  18. package/dist/doctor.js +114 -0
  19. package/dist/doctor.mjs +92 -0
  20. package/dist/generate.cjs +614 -65
  21. package/dist/generate.d.ts.map +1 -1
  22. package/dist/generate.js +118 -48
  23. package/dist/generate.mjs +619 -64
  24. package/dist/index.cjs +1 -1
  25. package/dist/index.mjs +1 -1
  26. package/dist/report.cjs +54 -0
  27. package/dist/report.d.ts +5 -0
  28. package/dist/report.d.ts.map +1 -0
  29. package/dist/report.js +31 -0
  30. package/dist/report.mjs +28 -0
  31. package/dist/scaffold/auth.cjs +422 -0
  32. package/dist/scaffold/auth.d.ts +39 -0
  33. package/dist/scaffold/auth.d.ts.map +1 -0
  34. package/dist/scaffold/auth.js +137 -0
  35. package/dist/scaffold/auth.mjs +389 -0
  36. package/dist/scaffold/edit.cjs +153 -0
  37. package/dist/scaffold/edit.d.ts +35 -0
  38. package/dist/scaffold/edit.d.ts.map +1 -0
  39. package/dist/scaffold/edit.js +140 -0
  40. package/dist/scaffold/edit.mjs +124 -0
  41. package/dist/scaffold/index.cjs +610 -0
  42. package/dist/scaffold/index.d.ts +10 -0
  43. package/dist/scaffold/index.d.ts.map +1 -0
  44. package/dist/scaffold/index.js +9 -0
  45. package/dist/scaffold/index.mjs +569 -0
  46. package/dist/scaffold/prompt.cjs +108 -0
  47. package/dist/scaffold/prompt.d.ts +9 -0
  48. package/dist/scaffold/prompt.d.ts.map +1 -0
  49. package/dist/scaffold/prompt.js +95 -0
  50. package/dist/scaffold/prompt.mjs +81 -0
  51. package/dist/scaffold/templates.cjs +274 -0
  52. package/dist/scaffold/templates.d.ts +26 -0
  53. package/dist/scaffold/templates.d.ts.map +1 -0
  54. package/dist/scaffold/templates.js +216 -0
  55. package/dist/scaffold/templates.mjs +229 -0
  56. package/dist/tsconfig.lib.tsbuildinfo +1 -1
  57. package/package.json +19 -3
package/dist/generate.mjs CHANGED
@@ -1,15 +1,522 @@
1
+ var __defProp = Object.defineProperty;
2
+ var __export = (target, all) => {
3
+ for (var name in all)
4
+ __defProp(target, name, { get: all[name], enumerable: true });
5
+ };
6
+
7
+ // src/generate.ts
8
+ import { writeFileSync as writeFileSync3, readFileSync as readFileSync3, existsSync as existsSync3, mkdirSync as mkdirSync3 } from "node:fs";
9
+ import { resolve, dirname as dirname3, relative as relative3, basename } from "node:path";
10
+ import { c as c3 } from "@fluixi/start/ui";
11
+
12
+ // src/scaffold/auth.ts
13
+ import { existsSync as existsSync2, mkdirSync as mkdirSync2, readFileSync as readFileSync2, writeFileSync as writeFileSync2 } from "node:fs";
14
+ import { dirname as dirname2, join, relative } from "node:path";
15
+
16
+ // src/scaffold/edit.ts
17
+ import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
18
+ import { dirname } from "node:path";
19
+ function closing(text, open) {
20
+ let depth = 0;
21
+ for (let i = open; i < text.length; i++) {
22
+ const ch = text[i];
23
+ if (ch === "/" && text[i + 1] === "/") i = text.indexOf("\n", i) === -1 ? text.length : text.indexOf("\n", i);
24
+ else if (ch === "/" && text[i + 1] === "*") i = text.indexOf("*/", i + 2) + 1;
25
+ else if (ch === "'" || ch === '"' || ch === "`") {
26
+ for (i++; i < text.length && text[i] !== ch; i++) if (text[i] === "\\") i++;
27
+ } else if ("([{".includes(ch)) depth++;
28
+ else if (")]}".includes(ch) && --depth === 0) return i;
29
+ }
30
+ return -1;
31
+ }
32
+ function appendToArray(text, open, items) {
33
+ const close = closing(text, open);
34
+ const inner = text.slice(open + 1, close);
35
+ if (!inner.trim()) return text.slice(0, open + 1) + items.join(", ") + text.slice(close);
36
+ if (inner.includes("\n")) {
37
+ const last = inner.trimEnd();
38
+ const indent = last.slice(last.lastIndexOf("\n") + 1).match(/^\s*/)[0];
39
+ const comma = last.endsWith(",") ? "" : ",";
40
+ const tail = inner.slice(last.length);
41
+ return text.slice(0, open + 1) + last + comma + items.map((i) => `
42
+ ${indent}${i},`).join("") + tail + text.slice(close);
43
+ }
44
+ const body = inner.replace(/,?\s*$/, "");
45
+ const trailing = inner.slice(body.length).replace(/^,/, "");
46
+ return text.slice(0, open + 1) + body + ", " + items.join(", ") + trailing + text.slice(close);
47
+ }
48
+ function addImport(text, line) {
49
+ if (text.includes(line)) return text;
50
+ const imports = [...text.matchAll(/^import[\s\S]*?from\s+['"][^'"]+['"];?[^\n]*\n/gm)];
51
+ const at = imports.length ? imports[imports.length - 1].index + imports[imports.length - 1][0].length : 0;
52
+ return text.slice(0, at) + line + "\n" + text.slice(at);
53
+ }
54
+ function register(log, file, header, r) {
55
+ const imports = [...r.extraImports ?? [], r.importLine];
56
+ if (!existsSync(file)) {
57
+ mkdirSync(dirname(file), { recursive: true });
58
+ writeFileSync(
59
+ file,
60
+ `${header}import { defineApp } from '@fluixi/start/app';
61
+ ${imports.join("\n")}
62
+
63
+ export default defineApp({ imports: [${r.entry}] });
64
+ `
65
+ );
66
+ log.push({ kind: "created", file });
67
+ return true;
68
+ }
69
+ let text = readFileSync(file, "utf8");
70
+ if (text.includes(r.importLine)) {
71
+ log.push({ kind: "listed", file, detail: r.entry });
72
+ return true;
73
+ }
74
+ const m = text.match(/\bimports\s*:\s*\[/);
75
+ if (!m) {
76
+ log.push({ kind: "hint", file, lines: [...imports, `imports: [${r.entry}]`] });
77
+ return false;
78
+ }
79
+ text = appendToArray(text, m.index + m[0].length - 1, [r.entry]);
80
+ for (const line of imports) text = addImport(text, line);
81
+ writeFileSync(file, text);
82
+ log.push({ kind: "updated", file, detail: `imports ${r.entry}` });
83
+ return true;
84
+ }
85
+ function envPublicNames(configFile) {
86
+ if (!existsSync(configFile)) return null;
87
+ const text = readFileSync(configFile, "utf8");
88
+ const m = text.match(/\bpublic\s*:\s*\[/);
89
+ if (!m) return null;
90
+ const open = m.index + m[0].length - 1;
91
+ const list = text.slice(open, closing(text, open) + 1);
92
+ return [...list.matchAll(/['"]([A-Za-z_][A-Za-z0-9_]*)['"]/g)].map((x) => x[1]);
93
+ }
94
+ function addEnvPublic(log, configFile, names) {
95
+ const list = (indent) => `${indent}public: [
96
+ ${names.map((n) => `${indent} '${n}',
97
+ `).join("")}${indent}],
98
+ `;
99
+ const have = envPublicNames(configFile);
100
+ if (have === null) {
101
+ const text2 = existsSync(configFile) ? readFileSync(configFile, "utf8") : "";
102
+ const call = text2.match(/defineConfig\(\{\n/);
103
+ if (call && !/\benv\s*:/.test(text2)) {
104
+ const at = call.index + call[0].length;
105
+ writeFileSync(configFile, text2.slice(0, at) + ` env: {
106
+ ${list(" ")} },
107
+ ` + text2.slice(at));
108
+ log.push({ kind: "updated", file: configFile, detail: `env.public ${names.join(", ")}` });
109
+ return;
110
+ }
111
+ log.push({ kind: "hint", file: configFile, lines: [`env: { public: [${names.map((n) => `'${n}'`).join(", ")}] },`] });
112
+ return;
113
+ }
114
+ const missing = names.filter((n) => !have.includes(n));
115
+ if (!missing.length) return;
116
+ const text = readFileSync(configFile, "utf8");
117
+ const m = text.match(/\bpublic\s*:\s*\[/);
118
+ writeFileSync(configFile, appendToArray(text, m.index + m[0].length - 1, missing.map((n) => `'${n}'`)));
119
+ log.push({ kind: "updated", file: configFile, detail: `env.public + ${missing.join(", ")}` });
120
+ }
121
+ function addEnvExample(log, file, comment, vars) {
122
+ const text = existsSync(file) ? readFileSync(file, "utf8") : "";
123
+ const have = new Set([...text.matchAll(/^\s*(?:export\s+)?([A-Za-z_][A-Za-z0-9_]*)\s*=/gm)].map((m) => m[1]));
124
+ const add = Object.entries(vars).filter(([k]) => !have.has(k) && !have.has(`VITE_${k}`));
125
+ if (!add.length) return;
126
+ const block = `${comment.split("\n").map((l) => `# ${l}`).join("\n")}
127
+ ${add.map(([k, v]) => `${k}=${v}`).join("\n")}
128
+ `;
129
+ writeFileSync(file, text ? `${text.replace(/\n*$/, "\n")}
130
+ ${block}` : block);
131
+ log.push({ kind: text ? "updated" : "created", file, detail: add.map(([k]) => k).join(", ") });
132
+ }
133
+
134
+ // src/scaffold/templates.ts
135
+ var templates_exports = {};
136
+ __export(templates_exports, {
137
+ AUTH_READER: () => AUTH_READER,
138
+ PROVIDERS: () => PROVIDERS,
139
+ PROVIDER_VARS: () => PROVIDER_VARS,
140
+ SERVER_ONLY: () => SERVER_ONLY,
141
+ apiRoute: () => apiRoute,
142
+ authEnvConfig: () => authEnvConfig,
143
+ authServerFile: () => authServerFile,
144
+ authServiceFile: () => authServiceFile,
145
+ betterAuthClient: () => betterAuthClient,
146
+ betterAuthConfig: () => betterAuthConfig,
147
+ betterAuthMiddleware: () => betterAuthMiddleware,
148
+ betterAuthRoute: () => betterAuthRoute,
149
+ callbackRoute: () => callbackRoute,
150
+ camel: () => camel,
151
+ envConfig: () => envConfig,
152
+ guardedApiRoute: () => guardedApiRoute,
153
+ loginRoute: () => loginRoute,
154
+ moduleFile: () => moduleFile,
155
+ serverFn: () => serverFn,
156
+ serviceFile: () => serviceFile,
157
+ verifyServer: () => verifyServer
158
+ });
159
+ var camel = (name) => name.toLowerCase().replace(/_+([a-z0-9])/g, (_, ch) => ch.toUpperCase());
160
+ var envConfig = (names) => {
161
+ const fields = names.length ? names.map((n) => ` ${camel(n)}: '${n}',`).join("\n") : ` // apiUrl: 'API_URL',`;
162
+ return `import { pickEnv } from '@fluixi/start/env';
163
+
164
+ // Every missing variable is reported at once, when this module loads. A field that may be
165
+ // unset takes { key, optional: true } or { key, default }; parse: asNumber and friends convert.
166
+ export const config = pickEnv({
167
+ ${fields}
168
+ });
169
+ `;
170
+ };
171
+ var moduleFile = (Name, name) => `import { defineModule } from '@fluixi/start/app';
172
+
173
+ export const ${Name}Module = defineModule({
174
+ name: '${name}',
175
+ imports: [],
176
+ providers: [],
177
+ });
178
+ `;
179
+ var serviceFile = (Name, name, server) => `${server ? `// Server only: the browser build refuses a page that imports this file.
180
+ ` : ""}import { injectionToken } from '@fluixi/start/di';
181
+ import { defineModule } from '@fluixi/start/app';
182
+
183
+ export class ${Name}Service {}
184
+
185
+ export const ${Name}Token = injectionToken<${Name}Service>('${Name}');
186
+
187
+ export const ${Name}Module = defineModule({
188
+ name: '${name}',
189
+ providers: [{ provide: ${Name}Token, useFactory: () => new ${Name}Service() }],
190
+ });
191
+ `;
192
+ var apiRoute = () => `export function GET(request: Request) {
193
+ return Response.json({ path: new URL(request.url).pathname });
194
+ }
195
+ `;
196
+ var guardedApiRoute = (guardImport, role) => `import { guard } from '${guardImport}';
197
+
198
+ export const GET = guard(
199
+ async (request, claims) => {
200
+ return Response.json({ sub: claims.sub });
201
+ },
202
+ { role: '${role}' },
203
+ );
204
+ `;
205
+ var verifyServer = () => `import { createTokenVerifier, createApiGuard } from '@fluixi/oauth2/verify';
206
+ import { auth } from '../config/env.js';
207
+
208
+ // Checks a bearer token's signature, issuer, audience and expiry against the provider.
209
+ export const verify = createTokenVerifier(auth.provider.verifier(auth.clientId));
210
+
211
+ // Answers 401 without a valid token and 403 without the role.
212
+ export const guard = createApiGuard({ verify, roles: auth.provider.roles });
213
+ `;
214
+ var serverFn = (name) => `export async function ${name}(input: { id: string }) {
215
+ 'use server';
216
+ // Runs on the server only. The browser calls it over RPC, so input and result are plain data.
217
+ return { id: input.id };
218
+ }
219
+ `;
220
+ var PROVIDERS = ["oidc", "keycloak", "auth0", "okta", "google", "dex", "oauth2"];
221
+ var SERVER_ONLY = {
222
+ google: "Google answers a token request without a client secret with `client_secret is missing`",
223
+ dex: "Dex sends no CORS headers on discovery or the token endpoint"
224
+ };
225
+ var PROVIDER_VARS = {
226
+ oidc: { AUTH_ISSUER: "https://id.example.com" },
227
+ keycloak: { AUTH_BASE_URL: "http://localhost:8080", AUTH_REALM: "master" },
228
+ auth0: { AUTH_DOMAIN: "example.eu.auth0.com", AUTH_AUDIENCE: "https://api.example.com" },
229
+ okta: { AUTH_DOMAIN: "example.okta.com" },
230
+ google: {},
231
+ dex: { AUTH_BASE_URL: "http://localhost:5556/dex" },
232
+ oauth2: {
233
+ AUTH_ISSUER: "https://id.example.com",
234
+ AUTH_AUTHORIZATION_ENDPOINT: "https://id.example.com/authorize",
235
+ AUTH_TOKEN_ENDPOINT: "https://id.example.com/token"
236
+ }
237
+ };
238
+ var AUTH_READER = `// Provider, issuer, client and token mode from the AUTH_* variables in env.public. Moving to
239
+ // another provider is a change to .env.
240
+ export const auth = authFromEnv(publicEnv(), { clientPrefix: '' });
241
+ `;
242
+ var authEnvConfig = () => `import { publicEnv } from '@fluixi/start/env';
243
+ import { authFromEnv } from '@fluixi/oauth2/env';
244
+
245
+ ${AUTH_READER}`;
246
+ var authServiceFile = (tokens) => `import { createAuthService, authModule } from '@fluixi/oauth2';
247
+ import { env } from '@fluixi/start/env';
248
+ ${tokens === "browser" ? `import { auth as authEnv } from '../config/env.js';
249
+ ` : ""}
250
+ export interface User {
251
+ sub: string;
252
+ email?: string;
253
+ name?: string;
254
+ }
255
+
256
+ const appUrl = env('APP_URL') ?? 'http://localhost:3000';
257
+
258
+ export const auth = createAuthService<User>({
259
+ ${tokens === "browser" ? ` config: {
260
+ ...authEnv.client,
261
+ tokens: 'browser',
262
+ redirectUri: \`\${appUrl}/callback\`,
263
+ postLogoutRedirectUri: \`\${appUrl}/login\`,
264
+ },` : ` // The tokens stay on the server behind an httpOnly cookie. The browser calls the routes
265
+ // auth.server.ts mounts under basePath and never sees a token.
266
+ config: {
267
+ tokens: 'server',
268
+ redirectUri: \`\${appUrl}/auth/callback\`,
269
+ postLogoutRedirectUri: \`\${appUrl}/login\`,
270
+ basePath: '/auth',
271
+ },`}
272
+ });
273
+
274
+ export const AuthModule = authModule(auth);
275
+ export const { protectedRoute, Protect, Can, useUser } = auth;
276
+ `;
277
+ var authServerFile = () => `import { authServerModule } from '@fluixi/oauth2/server';
278
+ import { publicEnv, serverEnv } from '@fluixi/start/env';
279
+ import { auth } from '../config/env.js';
280
+
281
+ // A function: serverEnv with required throws when a secret is unset, and it is only read
282
+ // when app.config.server.ts registers this.
283
+ export function authServer() {
284
+ const appUrl = publicEnv().APP_URL ?? 'http://localhost:3000';
285
+ return authServerModule({
286
+ ...auth.client,
287
+ clientSecret: serverEnv('OIDC_CLIENT_SECRET', { required: true }),
288
+ cookieSecret: serverEnv('SESSION_SECRET', { required: true }),
289
+ redirectUri: \`\${appUrl}/auth/callback\`,
290
+ defaultReturnTo: '/',
291
+ secureCookie: appUrl.startsWith('https://'),
292
+ });
293
+ }
294
+ `;
295
+ var callbackRoute = (html) => html ? `import { html } from '@fluixi/core';
296
+ import { OAuthCallback } from '@fluixi/oauth2/callback';
297
+
298
+ export default OAuthCallback({
299
+ pending: () => html\`<p>Signing you in</p>\`,
300
+ failed: (reason) => html\`<p>\${reason}</p><a href="/login">Try again</a>\`,
301
+ });
302
+ ` : `import { OAuthCallback } from '@fluixi/oauth2/callback';
303
+
304
+ export default OAuthCallback({
305
+ pending: () => <p>Signing you in</p>,
306
+ failed: (reason) => (
307
+ <>
308
+ <p>{reason}</p>
309
+ <a href="/login">Try again</a>
310
+ </>
311
+ ),
312
+ });
313
+ `;
314
+ var loginRoute = (html) => html ? `import { html } from '@fluixi/core';
315
+ import { auth } from '../services/auth.js';
316
+
317
+ export default function Login() {
318
+ return html\`<button @click=\${() => auth.signIn({ returnTo: '/' })}>Sign in</button>\`;
319
+ }
320
+ ` : `import { auth } from '../services/auth.js';
321
+
322
+ export default function Login() {
323
+ return <button onClick={() => auth.signIn({ returnTo: '/' })}>Sign in</button>;
324
+ }
325
+ `;
326
+ var betterAuthConfig = () => `import { betterAuth } from 'better-auth';
327
+ import { defineAuth } from '@fluixi/auth/server';
328
+
329
+ export type AppUser = { id: string; email: string; name?: string; role?: string };
330
+
331
+ // Better Auth owns the audited primitives; defineAuth registers it with @fluixi/auth.
332
+ export const auth = defineAuth<AppUser>(
333
+ betterAuth({
334
+ // database: <your adapter>,
335
+ emailAndPassword: { enabled: true },
336
+ // socialProviders: { google: { … }, github: { … } },
337
+ } as Parameters<typeof betterAuth>[0]),
338
+ );
339
+ `;
340
+ var betterAuthClient = () => `import { createAuthClient } from 'better-auth/client';
341
+ import { createAuthHooks } from '@fluixi/auth/client';
342
+ import type { AppUser } from './auth.config.js';
343
+
344
+ const client = createAuthClient({ baseURL: '/api/auth' });
345
+
346
+ // Or, to reuse an existing reactive session: createAuthHooks({ session: yourSessionStore }).
347
+ export const { useUser, useSession, useAuth, useSignIn, useSignOut, protectedRoute } =
348
+ createAuthHooks<AppUser>({ client, loginPath: '/login' });
349
+ `;
350
+ var betterAuthMiddleware = () => `import { defineMiddleware } from '@fluixi/start';
351
+ import { authMiddleware } from '@fluixi/auth/server';
352
+ import './auth.config.js';
353
+
354
+ export default defineMiddleware([authMiddleware()]);
355
+ `;
356
+ var betterAuthRoute = () => `import { mountAuthRoutes } from '@fluixi/auth/server';
357
+ import '../../auth.config.js';
358
+
359
+ const handler = mountAuthRoutes();
360
+ export const GET = handler;
361
+ export const POST = handler;
362
+ export default handler;
363
+ `;
364
+
365
+ // src/scaffold/auth.ts
366
+ var ScaffoldError = class extends Error {
367
+ };
368
+ function importPath(from, to) {
369
+ const p = relative(dirname2(from), to).replace(/\\/g, "/").replace(/\.tsx?$/, ".js");
370
+ return p.startsWith(".") ? p : `./${p}`;
371
+ }
372
+ function writeOnce(log, file, content) {
373
+ if (existsSync2(file)) {
374
+ log.push({ kind: "kept", file });
375
+ return false;
376
+ }
377
+ mkdirSync2(dirname2(file), { recursive: true });
378
+ writeFileSync2(file, content);
379
+ log.push({ kind: "created", file });
380
+ return true;
381
+ }
382
+ var APP_HEADER = "// Registered by @fluixi/start ahead of both entries.\n";
383
+ var APP_SERVER_HEADER = "// Only the server build imports this file, so what it lists never reaches the browser.\n";
384
+ function registerModule(log, root, server, file, name, entry = name) {
385
+ const config = join(root, server ? "src/app.config.server.ts" : "src/app.config.ts");
386
+ register(log, config, server ? APP_SERVER_HEADER : APP_HEADER, {
387
+ importLine: `import { ${name} } from '${importPath(config, file)}';`,
388
+ entry
389
+ });
390
+ }
391
+ function ensureAuthEnv(log, root) {
392
+ const file = join(root, "src/config/env.ts");
393
+ if (writeOnce(log, file, authEnvConfig())) return;
394
+ let text = readFileSync2(file, "utf8");
395
+ if (/export\s+const\s+auth\b/.test(text)) return;
396
+ if (!/\bpublicEnv\b/.test(text)) text = addImport(text, `import { publicEnv } from '@fluixi/start/env';`);
397
+ text = addImport(text, `import { authFromEnv } from '@fluixi/oauth2/env';`);
398
+ writeFileSync2(file, `${text.replace(/\n*$/, "\n")}
399
+ ${AUTH_READER}`);
400
+ log.push({ kind: "updated", file, detail: "auth" });
401
+ }
402
+ function resolveTokens(provider, tokens) {
403
+ const refusal = SERVER_ONLY[provider];
404
+ if (refusal && tokens === "browser") throw new ScaffoldError(`${provider} needs server tokens: ${refusal}`);
405
+ return { tokens: tokens ?? (refusal ? "server" : "browser"), forced: !!refusal && !tokens };
406
+ }
407
+ function setupOAuth2(root, options, dependencySpec = "alpha") {
408
+ const provider = options.provider ?? "oidc";
409
+ if (!PROVIDERS.includes(provider)) throw new ScaffoldError(`unknown provider ${provider}: ${PROVIDERS.join(", ")}`);
410
+ const { tokens, forced } = resolveTokens(provider, options.tokens);
411
+ const html = options.format === "html";
412
+ const ext = html ? "ts" : "tsx";
413
+ const steps = [];
414
+ const at = (rel2) => join(root, rel2);
415
+ ensureAuthEnv(steps, root);
416
+ const service = at("src/services/auth.ts");
417
+ if (writeOnce(steps, service, authServiceFile(tokens))) registerModule(steps, root, false, service, "AuthModule");
418
+ if (tokens === "browser") writeOnce(steps, at(`src/routes/callback.${ext}`), callbackRoute(html));
419
+ writeOnce(steps, at(`src/routes/login.${ext}`), loginRoute(html));
420
+ if (tokens === "server") {
421
+ const server = at("src/services/auth.server.ts");
422
+ if (writeOnce(steps, server, authServerFile())) registerModule(steps, root, true, server, "authServer", "authServer()");
423
+ }
424
+ const vars = {
425
+ AUTH_PROVIDER: provider,
426
+ AUTH_CLIENT_ID: "my-app",
427
+ AUTH_TOKENS: tokens,
428
+ ...PROVIDER_VARS[provider],
429
+ APP_URL: "http://localhost:3000"
430
+ };
431
+ addEnvPublic(steps, at("fluixi.config.ts"), Object.keys(vars));
432
+ addEnvExample(steps, at(".env.example"), `@fluixi/oauth2 (${provider}). Listed in env.public: the browser sees them.`, vars);
433
+ if (tokens === "server") {
434
+ addEnvExample(steps, at(".env.example"), "Server only, never in env.public. SESSION_SECRET: 32 bytes or more.", {
435
+ OIDC_CLIENT_SECRET: "",
436
+ SESSION_SECRET: ""
437
+ });
438
+ }
439
+ const redirect = tokens === "browser" ? "/callback" : "/auth/callback";
440
+ return {
441
+ steps,
442
+ dependencies: { "@fluixi/oauth2": dependencySpec },
443
+ next: ["copy .env.example to .env and fill it in", `register <APP_URL>${redirect} as the redirect URI at the provider`],
444
+ notes: forced ? [`${provider} refuses a public client, so the tokens stay on the server`] : []
445
+ };
446
+ }
447
+ function setupBetterAuth(root, dependencySpec = "alpha") {
448
+ const steps = [];
449
+ const at = (rel2) => join(root, rel2);
450
+ writeOnce(steps, at("src/auth.config.ts"), betterAuthConfig());
451
+ writeOnce(steps, at("src/auth.client.ts"), betterAuthClient());
452
+ if (!writeOnce(steps, at("src/middleware.ts"), betterAuthMiddleware())) {
453
+ steps.push({
454
+ kind: "hint",
455
+ file: at("src/middleware.ts"),
456
+ lines: [`import { authMiddleware } from '@fluixi/auth/server';`, `import './auth.config.js';`, "authMiddleware() in the middleware list"]
457
+ });
458
+ }
459
+ writeOnce(steps, at("src/api/auth/[...all].ts"), betterAuthRoute());
460
+ return {
461
+ steps,
462
+ dependencies: { "@fluixi/auth": dependencySpec, "better-auth": "^1" },
463
+ next: ["set your database adapter in src/auth.config.ts"],
464
+ notes: []
465
+ };
466
+ }
467
+
468
+ // src/scaffold/prompt.ts
469
+ import { createInterface } from "node:readline/promises";
470
+ import { stdin as input, stdout as output, exit, env } from "node:process";
471
+ var COLOR = env.NO_COLOR || env.FORCE_COLOR === "0" ? false : (!!output.isTTY || !!env.FORCE_COLOR) && env.TERM !== "dumb";
472
+ var sgr = (o, x) => (s) => COLOR ? `\x1B[${o}m${s}\x1B[${x}m` : `${s}`;
473
+ var c = { bold: sgr(1, 22), green: sgr(32, 39), cyan: sgr(36, 39), gray: sgr(90, 39) };
474
+ var BAR = c.gray("│");
475
+
476
+ // src/report.ts
477
+ import { relative as relative2 } from "node:path";
478
+ import { c as c2 } from "@fluixi/start/ui";
479
+ var rel = (file) => relative2(process.cwd(), file) || ".";
480
+ function report(steps) {
481
+ for (const s of steps) {
482
+ if (s.kind === "hint") {
483
+ console.log(` ${c2.yellow("!")} ${c2.gray("add to")} ${c2.cyan(rel(s.file))}${c2.gray(":")}`);
484
+ for (const l of s.lines) console.log(` ${l}`);
485
+ } else if (s.kind === "kept") {
486
+ console.log(` ${c2.gray("-")} ${c2.cyan(rel(s.file))} ${c2.gray("exists, kept")}`);
487
+ } else if (s.kind === "listed") {
488
+ console.log(` ${c2.gray("-")} ${c2.cyan(rel(s.file))} ${c2.gray("already lists it")}`);
489
+ } else {
490
+ console.log(` ${c2.green("✓")} ${c2.gray(s.kind)} ${c2.cyan(rel(s.file))}${s.detail ? c2.gray(`: ${s.detail}`) : ""}`);
491
+ }
492
+ }
493
+ }
494
+ function reportAuth(result, installHint) {
495
+ for (const n of result.notes) console.log(` ${c2.gray(n)}`);
496
+ report(result.steps);
497
+ const next = [...installHint ? [installHint] : [], ...result.next];
498
+ if (next.length) console.log(` ${c2.gray("next:")} ${next.map((n) => c2.gray(n)).join(c2.gray("; "))}`);
499
+ }
500
+
1
501
  // src/generate.ts
2
- import { writeFileSync, readFileSync, existsSync, mkdirSync } from "node:fs";
3
- import { resolve, dirname, relative, basename } from "node:path";
4
- import { c } from "@fluixi/start/ui";
5
- var GEN_USAGE = `Usage: fluixi generate <kind> <name>
502
+ var GEN_USAGE = `Usage: fluixi generate <kind> [name] [options]
6
503
 
7
504
  Kinds:
8
- component <Name> a reusable component → src/components/<Name>.{tsx,ts}
9
- route <path> a file-based route page → src/routes/<path>.{tsx,ts}
10
- middleware the request middleware file → src/middleware.ts
11
- auth @fluixi/auth wiring → auth.config + auth.client + middleware + /api/auth route
505
+ component <Name> a reusable component → src/components/<Name>.{tsx,ts}
506
+ route <path> a file-based route page → src/routes/<path>.{tsx,ts}
507
+ middleware the request middleware file → src/middleware.ts
508
+ env typed config from env.public → src/config/env.ts
509
+ module <name> an app module → src/services/<name>/<name>.module.ts
510
+ service <Name> [--server] a service, token and module → src/services/<name>.ts (.server.ts)
511
+ api <path> [--role <role>] an API route, guarded by role → src/api/<path>.ts
512
+ server-fn <name> a "use server" function → src/server/<name>.ts
513
+ auth oauth2 [--provider <p>] [--tokens browser|server]
514
+ @fluixi/oauth2 wiring: env config, service, routes, .env.example
515
+ (fluixi add auth does the same, asks, and installs)
516
+ auth better-auth @fluixi/auth wiring → auth.config + auth.client + middleware + /api/auth
12
517
 
518
+ Modules and services are listed in src/app.config.ts, or app.config.server.ts for --server,
519
+ which is created when missing. Providers: ${templates_exports.PROVIDERS.join(", ")}.
13
520
  Component/route syntax follows the project (JSX → .tsx, html\`\` → .ts); override with --jsx / --html.
14
521
  `;
15
522
  function detectFormat() {
@@ -17,7 +524,7 @@ function detectFormat() {
17
524
  if (args.includes("--html")) return "html";
18
525
  if (args.includes("--jsx")) return "jsx";
19
526
  try {
20
- const ts = JSON.parse(readFileSync(resolve("tsconfig.json"), "utf8"));
527
+ const ts = JSON.parse(readFileSync3(resolve("tsconfig.json"), "utf8"));
21
528
  if (ts?.compilerOptions && !ts.compilerOptions.jsx) return "html";
22
529
  } catch {
23
530
  }
@@ -28,28 +535,46 @@ function pascal(s) {
28
535
  }
29
536
  function resolveHint(Name, ext) {
30
537
  const configs = ["vite.config.ts", "vite.config.js", "fluixi.config.ts", "fluixi.config.js"];
31
- const found = configs.map((f) => resolve(f)).find((f) => existsSync(f));
538
+ const found = configs.map((f) => resolve(f)).find((f) => existsSync3(f));
32
539
  if (!found) return;
33
540
  try {
34
- if (/\bresolve\s*:\s*\[/.test(readFileSync(found, "utf8"))) return;
541
+ if (/\bresolve\s*:\s*\[/.test(readFileSync3(found, "utf8"))) return;
35
542
  } catch {
36
543
  return;
37
544
  }
38
545
  console.log(
39
- ` ${c.gray("to use")} ${c.cyan(`<${Name} />`)} ${c.gray("without importing it, add to")} ${c.cyan(relative(process.cwd(), found))}:`
546
+ ` ${c3.gray("to use")} ${c3.cyan(`<${Name} />`)} ${c3.gray("without importing it, add to")} ${c3.cyan(relative3(process.cwd(), found))}:`
40
547
  );
41
- console.log(` ${c.gray(`resolve: [{ match: /^([A-Z]\\w+)$/, module: '/src/components/$1.${ext}' }]`)}`);
42
- console.log(` ${c.gray("one pattern covers the whole folder. Importing it works too — including with")} ${c.cyan("load:")}`);
548
+ console.log(` ${c3.gray(`resolve: [{ match: /^([A-Z]\\w+)$/, module: '/src/components/$1.${ext}' }]`)}`);
549
+ console.log(` ${c3.gray("one pattern covers the whole folder. Importing it works too — including with")} ${c3.cyan("load:")}`);
550
+ }
551
+ function option(name) {
552
+ const args = process.argv;
553
+ const i = args.indexOf(`--${name}`);
554
+ if (i !== -1 && args[i + 1] && !args[i + 1].startsWith("--")) return args[i + 1];
555
+ return args.find((a) => a.startsWith(`--${name}=`))?.slice(name.length + 3);
556
+ }
557
+ var kebab = (s) => s.replace(/([a-z0-9])([A-Z])/g, "$1-$2").toLowerCase();
558
+ function writeOnce2(file, content) {
559
+ const steps = [];
560
+ const wrote = writeOnce(steps, file, content);
561
+ report(steps);
562
+ return wrote;
563
+ }
564
+ function registerIn(server, file, name, entry = name) {
565
+ const steps = [];
566
+ registerModule(steps, process.cwd(), server, file, name, entry);
567
+ report(steps);
43
568
  }
44
569
  function write(file, content) {
45
- const rel = relative(process.cwd(), file);
46
- if (existsSync(file)) {
47
- console.error(` ${c.red("✗")} ${rel} ${c.gray("already exists")}`);
570
+ const rel2 = relative3(process.cwd(), file);
571
+ if (existsSync3(file)) {
572
+ console.error(` ${c3.red("✗")} ${rel2} ${c3.gray("already exists")}`);
48
573
  process.exit(1);
49
574
  }
50
- mkdirSync(dirname(file), { recursive: true });
51
- writeFileSync(file, content);
52
- console.log(` ${c.green("✓")} ${c.gray("created")} ${c.cyan(rel)}`);
575
+ mkdirSync3(dirname3(file), { recursive: true });
576
+ writeFileSync3(file, content);
577
+ console.log(` ${c3.green("✓")} ${c3.gray("created")} ${c3.cyan(rel2)}`);
53
578
  }
54
579
  var component = (Name) => `import type { Component, ComponentChild } from '@fluixi/core';
55
580
 
@@ -103,44 +628,6 @@ export default defineMiddleware([
103
628
  },
104
629
  ]);
105
630
  `;
106
- var authConfig = () => `import { betterAuth } from 'better-auth';
107
- import { defineAuth } from '@fluixi/auth/server';
108
-
109
- export type AppUser = { id: string; email: string; name?: string; role?: string };
110
-
111
- // Better Auth owns the audited primitives; defineAuth registers it with @fluixi/auth.
112
- export const auth = defineAuth<AppUser>(
113
- betterAuth({
114
- // database: <your adapter>,
115
- emailAndPassword: { enabled: true },
116
- // socialProviders: { google: { … }, github: { … } },
117
- } as Parameters<typeof betterAuth>[0]),
118
- );
119
- `;
120
- var authClient = () => `import { createAuthClient } from 'better-auth/client';
121
- import { createAuthHooks } from '@fluixi/auth/client';
122
- import type { AppUser } from './auth.config.js';
123
-
124
- const client = createAuthClient({ baseURL: '/api/auth' });
125
-
126
- // Or, to reuse an existing reactive session: createAuthHooks({ session: yourSessionStore }).
127
- export const { useUser, useSession, useAuth, useSignIn, useSignOut, protectedRoute } =
128
- createAuthHooks<AppUser>({ client, loginPath: '/login' });
129
- `;
130
- var authMiddleware = () => `import { defineMiddleware } from '@fluixi/start';
131
- import { authMiddleware } from '@fluixi/auth/server';
132
- import './auth.config.js';
133
-
134
- export default defineMiddleware([authMiddleware()]);
135
- `;
136
- var authRoute = () => `import { mountAuthRoutes } from '@fluixi/auth/server';
137
- import '../../../auth.config.js';
138
-
139
- const handler = mountAuthRoutes();
140
- export const GET = handler;
141
- export const POST = handler;
142
- export default handler;
143
- `;
144
631
  function generate(kind, name) {
145
632
  switch (kind) {
146
633
  case "component":
@@ -165,20 +652,88 @@ function generate(kind, name) {
165
652
  case "m":
166
653
  write(resolve("src/middleware.ts"), middleware());
167
654
  break;
655
+ case "env": {
656
+ const names = (envPublicNames(resolve("fluixi.config.ts")) ?? []).filter((n) => !n.startsWith("AUTH_"));
657
+ write(resolve("src/config/env.ts"), templates_exports.envConfig(names));
658
+ break;
659
+ }
660
+ case "module": {
661
+ if (!name) return fail("module name required, e.g. `fluixi g module billing`");
662
+ const slug = kebab(name);
663
+ const file = resolve("src/services", slug, `${slug}.module.ts`);
664
+ write(file, templates_exports.moduleFile(pascal(name), slug));
665
+ registerIn(false, file, `${pascal(name)}Module`);
666
+ break;
667
+ }
668
+ case "service":
669
+ case "s": {
670
+ if (!name) return fail("service name required, e.g. `fluixi g service Orders`");
671
+ const Name = pascal(name).replace(/Service$/, "") || "App";
672
+ const server = process.argv.includes("--server");
673
+ const file = resolve("src/services", `${kebab(Name)}${server ? ".server" : ""}.ts`);
674
+ write(file, templates_exports.serviceFile(Name, kebab(Name), server));
675
+ registerIn(server, file, `${Name}Module`);
676
+ break;
677
+ }
678
+ case "api": {
679
+ if (!name) return fail("api path required, e.g. `fluixi g api users/[id]`");
680
+ const file = resolve("src/api", `${name.replace(/\.ts$/, "")}.ts`);
681
+ const role = option("role");
682
+ if (!role) {
683
+ write(file, templates_exports.apiRoute());
684
+ break;
685
+ }
686
+ const verify = resolve("src/services/verify.server.ts");
687
+ write(file, templates_exports.guardedApiRoute(importPath(file, verify), role));
688
+ writeOnce2(verify, templates_exports.verifyServer());
689
+ {
690
+ const steps = [];
691
+ ensureAuthEnv(steps, process.cwd());
692
+ report(steps);
693
+ }
694
+ break;
695
+ }
696
+ case "server-fn": {
697
+ if (!name) return fail("function name required, e.g. `fluixi g server-fn getUser`");
698
+ const fn = name.replace(/[^a-zA-Z0-9]+(.)/g, (_, ch) => ch.toUpperCase());
699
+ write(resolve("src/server", `${fn}.ts`), templates_exports.serverFn(fn));
700
+ break;
701
+ }
168
702
  case "auth":
169
- write(resolve("src/auth.config.ts"), authConfig());
170
- write(resolve("src/auth.client.ts"), authClient());
171
- write(resolve("src/middleware.ts"), authMiddleware());
172
- write(resolve("src/routes/api/auth/[...all].ts"), authRoute());
173
- console.log(` ${c.gray("next: install")} ${c.cyan("better-auth")}${c.gray(", set your DB adapter in auth.config.ts, and forward `request` in entry-server")}`);
703
+ try {
704
+ if (name === "oauth2") {
705
+ reportAuth(
706
+ setupOAuth2(process.cwd(), {
707
+ provider: option("provider"),
708
+ tokens: tokensOption(),
709
+ format: detectFormat()
710
+ }),
711
+ "install @fluixi/oauth2"
712
+ );
713
+ } else if (!name || name === "better-auth") {
714
+ reportAuth(setupBetterAuth(process.cwd()), "install @fluixi/auth and better-auth");
715
+ } else {
716
+ return fail(`unknown auth kind ${name}: oauth2 or better-auth`);
717
+ }
718
+ } catch (error) {
719
+ if (error instanceof ScaffoldError) return fail(error.message);
720
+ throw error;
721
+ }
174
722
  break;
175
723
  default:
176
724
  console.error(GEN_USAGE);
177
725
  process.exit(1);
178
726
  }
179
727
  }
728
+ function tokensOption() {
729
+ const tokens = option("tokens");
730
+ if (tokens !== void 0 && tokens !== "browser" && tokens !== "server") {
731
+ fail("--tokens is browser or server");
732
+ }
733
+ return tokens;
734
+ }
180
735
  function fail(msg) {
181
- console.error(` ${c.red("✗")} ${msg}`);
736
+ console.error(` ${c3.red("✗")} ${msg}`);
182
737
  process.exit(1);
183
738
  }
184
739
  export {