kreftjs 0.0.0-stage → 0.0.2-beta

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 (184) hide show
  1. package/README.md +103 -2
  2. package/bin/add.js +1074 -0
  3. package/bin/artisan-more.js +994 -0
  4. package/bin/artisan.js +1877 -0
  5. package/bin/backend/app.js +161 -0
  6. package/bin/backend/cache.js +196 -0
  7. package/bin/backend/collection.js +117 -0
  8. package/bin/backend/command.js +45 -0
  9. package/bin/backend/config.js +234 -0
  10. package/bin/backend/error-page.js +773 -0
  11. package/bin/backend/events.js +49 -0
  12. package/bin/backend/factory.js +223 -0
  13. package/bin/backend/fake.js +180 -0
  14. package/bin/backend/form.js +102 -0
  15. package/bin/backend/gate.js +79 -0
  16. package/bin/backend/gc.js +69 -0
  17. package/bin/backend/html.js +322 -0
  18. package/bin/backend/http.js +652 -0
  19. package/bin/backend/index.js +452 -0
  20. package/bin/backend/mail.js +77 -0
  21. package/bin/backend/mcp.js +433 -0
  22. package/bin/backend/migrator.js +370 -0
  23. package/bin/backend/model.js +781 -0
  24. package/bin/backend/namespace.js +213 -0
  25. package/bin/backend/notifications.js +68 -0
  26. package/bin/backend/qr.js +164 -0
  27. package/bin/backend/query.js +387 -0
  28. package/bin/backend/queue.js +412 -0
  29. package/bin/backend/redis.js +479 -0
  30. package/bin/backend/schedule.js +112 -0
  31. package/bin/backend/schema.js +321 -0
  32. package/bin/backend/seo-pages.js +100 -0
  33. package/bin/backend/storage.js +153 -0
  34. package/bin/backend/support.js +567 -0
  35. package/bin/backend/typescript.js +166 -0
  36. package/bin/build.js +540 -0
  37. package/bin/db-api.js +249 -0
  38. package/bin/db.js +275 -0
  39. package/bin/deps.js +138 -0
  40. package/bin/electron.js +509 -0
  41. package/bin/env.js +97 -0
  42. package/bin/guard.js +80 -0
  43. package/bin/kreftjs-config.js +40 -0
  44. package/bin/kreftjs.js +1373 -0
  45. package/bin/lang.js +241 -0
  46. package/bin/layout.js +74 -0
  47. package/bin/no-auth.js +162 -0
  48. package/bin/prerender.js +258 -0
  49. package/bin/safe-json.js +24 -0
  50. package/bin/server-routes.js +154 -0
  51. package/bin/ssr.js +133 -0
  52. package/bin/terminal.js +195 -0
  53. package/bin/to-ts.js +36 -0
  54. package/bin/update.js +237 -0
  55. package/bin/urls-file.js +27 -0
  56. package/bin/vite.js +228 -0
  57. package/docs/Docs.kreftjs +557 -0
  58. package/docs/README.md +2302 -0
  59. package/docs/markdown.js +186 -0
  60. package/editors/vscode/README.md +13 -0
  61. package/editors/vscode/extension.js +183 -0
  62. package/editors/vscode/language-configuration.json +17 -0
  63. package/editors/vscode/package.json +57 -0
  64. package/editors/vscode/snippets/kreftjs.json +21 -0
  65. package/editors/vscode/syntaxes/kreftjs.tmLanguage.json +207 -0
  66. package/kreftjs.d.ts +579 -0
  67. package/kreftjs.js +5108 -0
  68. package/package.json +97 -4
  69. package/server.d.ts +1028 -0
  70. package/server.js +8 -0
  71. package/templates/app/README.md +127 -0
  72. package/templates/app/_env.example +64 -0
  73. package/templates/app/_gitignore +5 -0
  74. package/templates/app/_vscode/settings.json +9 -0
  75. package/templates/app/backend/app/Auth/Session.js +177 -0
  76. package/templates/app/backend/app/Auth/Throttle.js +29 -0
  77. package/templates/app/backend/app/Console/Commands/.gitkeep +0 -0
  78. package/templates/app/backend/app/Events/.gitkeep +0 -0
  79. package/templates/app/backend/app/Http/Controllers/Admin/DashboardController.js +34 -0
  80. package/templates/app/backend/app/Http/Controllers/Admin/UserController.js +53 -0
  81. package/templates/app/backend/app/Http/Controllers/Auth/ApiTokenController.js +47 -0
  82. package/templates/app/backend/app/Http/Controllers/Auth/AuthenticatedSessionController.js +41 -0
  83. package/templates/app/backend/app/Http/Controllers/Auth/ConfirmablePasswordController.js +14 -0
  84. package/templates/app/backend/app/Http/Controllers/Auth/EmailVerificationController.js +23 -0
  85. package/templates/app/backend/app/Http/Controllers/Auth/NewPasswordController.js +29 -0
  86. package/templates/app/backend/app/Http/Controllers/Auth/PasswordResetLinkController.js +20 -0
  87. package/templates/app/backend/app/Http/Controllers/Auth/RegisteredUserController.js +19 -0
  88. package/templates/app/backend/app/Http/Controllers/Auth/SessionController.js +36 -0
  89. package/templates/app/backend/app/Http/Controllers/Auth/TwoFactorChallengeController.js +41 -0
  90. package/templates/app/backend/app/Http/Controllers/Auth/TwoFactorController.js +63 -0
  91. package/templates/app/backend/app/Http/Controllers/ProfileController.js +66 -0
  92. package/templates/app/backend/app/Http/Kernel.js +8 -0
  93. package/templates/app/backend/app/Http/Middleware/Authenticate.js +84 -0
  94. package/templates/app/backend/app/Http/Middleware/CheckAbilities.js +17 -0
  95. package/templates/app/backend/app/Http/Middleware/CheckForAnyAbility.js +12 -0
  96. package/templates/app/backend/app/Http/Middleware/EnsureEmailIsVerified.js +11 -0
  97. package/templates/app/backend/app/Http/Middleware/EnsureHasRole.js +11 -0
  98. package/templates/app/backend/app/Http/Middleware/EnsureIsAdmin.js +11 -0
  99. package/templates/app/backend/app/Http/Middleware/RequirePassword.js +15 -0
  100. package/templates/app/backend/app/Http/Requests/Auth/LoginRequest.js +45 -0
  101. package/templates/app/backend/app/Http/Resources/.gitkeep +1 -0
  102. package/templates/app/backend/app/Jobs/.gitkeep +0 -0
  103. package/templates/app/backend/app/Listeners/.gitkeep +0 -0
  104. package/templates/app/backend/app/Mail/.gitkeep +0 -0
  105. package/templates/app/backend/app/Models/PersonalAccessToken.js +21 -0
  106. package/templates/app/backend/app/Models/User.js +64 -0
  107. package/templates/app/backend/app/Notifications/.gitkeep +0 -0
  108. package/templates/app/backend/app/Observers/.gitkeep +0 -0
  109. package/templates/app/backend/app/Policies/.gitkeep +0 -0
  110. package/templates/app/backend/app/Providers/AppServiceProvider.js +12 -0
  111. package/templates/app/backend/app/Rules/.gitkeep +1 -0
  112. package/templates/app/backend/config/app.js +16 -0
  113. package/templates/app/backend/config/auth.js +34 -0
  114. package/templates/app/backend/config/database.js +38 -0
  115. package/templates/app/backend/config/redis.js +28 -0
  116. package/templates/app/backend/config/session.js +36 -0
  117. package/templates/app/backend/database/migrations/0001_01_01_000000_create_users_table.js +49 -0
  118. package/templates/app/backend/database/migrations/0001_01_01_000001_create_cache_table.js +13 -0
  119. package/templates/app/backend/database/migrations/0001_01_01_000002_create_jobs_table.js +26 -0
  120. package/templates/app/backend/database/migrations/0001_01_01_000003_create_notifications_table.js +18 -0
  121. package/templates/app/backend/database/seeders/DatabaseSeeder.js +15 -0
  122. package/templates/app/backend/lang/ar/messages.json +10 -0
  123. package/templates/app/backend/lang/ar/validation.json +21 -0
  124. package/templates/app/backend/lang/en/messages.json +10 -0
  125. package/templates/app/backend/lang/en/validation.json +19 -0
  126. package/templates/app/backend/routes/api.js +25 -0
  127. package/templates/app/backend/routes/auth.js +52 -0
  128. package/templates/app/backend/routes/console.js +15 -0
  129. package/templates/app/frontend/App.kreftjs +4 -0
  130. package/templates/app/frontend/components/Field.kreftjs +19 -0
  131. package/templates/app/frontend/components/StatCard.kreftjs +47 -0
  132. package/templates/app/frontend/components/profile/ApiTokens.kreftjs +70 -0
  133. package/templates/app/frontend/components/profile/Sessions.kreftjs +42 -0
  134. package/templates/app/frontend/components/profile/TwoFactor.kreftjs +90 -0
  135. package/templates/app/frontend/errors/ErrorMonitor.kreftjs +264 -0
  136. package/templates/app/frontend/index.html +79 -0
  137. package/templates/app/frontend/kreftjs.d.ts +579 -0
  138. package/templates/app/frontend/kreftjs.js +5108 -0
  139. package/templates/app/frontend/layouts/app.kreftjs +718 -0
  140. package/templates/app/frontend/layouts/docs.kreftjs +37 -0
  141. package/templates/app/frontend/layouts/guest.kreftjs +18 -0
  142. package/templates/app/frontend/pages/Dashboard.kreftjs +273 -0
  143. package/templates/app/frontend/pages/ErrorMonitor.kreftjs +96 -0
  144. package/templates/app/frontend/pages/Home.kreftjs +617 -0
  145. package/templates/app/frontend/pages/Profile.kreftjs +104 -0
  146. package/templates/app/frontend/pages/Users.kreftjs +240 -0
  147. package/templates/app/frontend/pages/auth/ConfirmPassword.kreftjs +33 -0
  148. package/templates/app/frontend/pages/auth/ForgotPassword.kreftjs +36 -0
  149. package/templates/app/frontend/pages/auth/Login.kreftjs +89 -0
  150. package/templates/app/frontend/pages/auth/Register.kreftjs +48 -0
  151. package/templates/app/frontend/pages/auth/ResetPassword.kreftjs +45 -0
  152. package/templates/app/frontend/pages/auth/VerifyEmail.kreftjs +59 -0
  153. package/templates/app/frontend/routes/web.kreftjs +34 -0
  154. package/templates/app/frontend/tests/components/Counter.kreftjs +15 -0
  155. package/templates/app/frontend/tests/pages/About.kreftjs +12 -0
  156. package/templates/app/frontend/tests/pages/Test.kreftjs +31 -0
  157. package/templates/app/frontend/ui.css +517 -0
  158. package/templates/app/lang/ar/auth.json +6 -0
  159. package/templates/app/lang/ar/messages.json +6 -0
  160. package/templates/app/lang/ar/pagination.json +5 -0
  161. package/templates/app/lang/ar/passwords.json +8 -0
  162. package/templates/app/lang/ar/validation.json +22 -0
  163. package/templates/app/lang/ar.json +10 -0
  164. package/templates/app/lang/en/auth.json +6 -0
  165. package/templates/app/lang/en/messages.json +6 -0
  166. package/templates/app/lang/en/pagination.json +5 -0
  167. package/templates/app/lang/en/passwords.json +8 -0
  168. package/templates/app/lang/en/validation.json +22 -0
  169. package/templates/app/lang/en.json +10 -0
  170. package/templates/errors/401.kreftjs +11 -0
  171. package/templates/errors/402.kreftjs +11 -0
  172. package/templates/errors/403.kreftjs +11 -0
  173. package/templates/errors/404.kreftjs +11 -0
  174. package/templates/errors/419.kreftjs +11 -0
  175. package/templates/errors/429.kreftjs +11 -0
  176. package/templates/errors/500.kreftjs +11 -0
  177. package/templates/errors/503.kreftjs +11 -0
  178. package/templates/errors/ErrorMonitor.kreftjs +502 -0
  179. package/templates/errors/error.kreftjs +11 -0
  180. package/templates/errors/minimal.kreftjs +25 -0
  181. package/tools/dev-client.js +125 -0
  182. package/tools/devtools.js +172 -0
  183. package/tools/vite-plugin-kreftjs.d.ts +52 -0
  184. package/tools/vite-plugin-kreftjs.js +466 -0
package/bin/artisan.js ADDED
@@ -0,0 +1,1877 @@
1
+ /*
2
+ * The backend commands:
3
+ *
4
+ * kreftjs migrate run new migrations
5
+ * kreftjs migrate:rollback [--step 2] undo the last batch (or the last 2 migrations)
6
+ * kreftjs migrate:fresh [--seed] drop every table, migrate again (and seed)
7
+ * kreftjs migrate:status which migrations ran
8
+ * kreftjs db:seed [--class PostSeeder] run database/seeders/DatabaseSeeder.js (or another seeder)
9
+ * kreftjs route:list the routes in routes/api.js
10
+ * kreftjs make:model Post [-m -f -s -c -p -r] [--route=file] [--auth] [--all]
11
+ * kreftjs make:controller PostController [--api] [--model Post] [--route=file] [--auth]
12
+ * kreftjs make:page Post [--model Post] [--route=file] [--auth]
13
+ * kreftjs make:migration create_posts_table
14
+ * kreftjs make:seeder PostSeeder
15
+ * kreftjs make:factory PostFactory
16
+ * kreftjs make:middleware EnsureIsAdmin
17
+ *
18
+ * Every command takes --db <url> (or DATABASE_URL in .env) and --backend <folder> (default: the project).
19
+ */
20
+ const fs = require('fs');
21
+ const path = require('path');
22
+
23
+ function options(args) {
24
+ const flatArgs = (Array.isArray(args) ? args.flat(Infinity) : []).map((x) => (typeof x === 'string' ? x : String(x ?? '')));
25
+ const o = { _: [], flags: new Set() };
26
+ const valKeys = /^(db|backend|migrations|step|class|model|dir|event|route|routes|route-file|page|folder|subfolder|group|in|path|file|url|urls)$/;
27
+ for (let i = 0; i < flatArgs.length; i++) {
28
+ const a = flatArgs[i];
29
+ if (a.startsWith('--') && a.includes('=')) {
30
+ const idx = a.indexOf('=');
31
+ const k = a.slice(2, idx);
32
+ if (k === '__proto__' || k === 'constructor' || k === 'prototype') continue;
33
+ const v = a.slice(idx + 1);
34
+ o[k] = v;
35
+ if (k === 'route' || k === 'routes' || k === 'route-file') o.route = v;
36
+ if (k === 'folder' || k === 'subfolder' || k === 'in' || k === 'group' || k === 'path') o.folder = v;
37
+ if (k === 'file' || k === 'path') o.file = v;
38
+ o.flags.add(k);
39
+ } else if (a.startsWith('--')) {
40
+ const k = a.slice(2);
41
+ if (k === '__proto__' || k === 'constructor' || k === 'prototype') continue;
42
+ if (valKeys.test(k) && args[i + 1] && !args[i + 1].startsWith('-')) {
43
+ const v = args[++i];
44
+ o[k] = v;
45
+ if (k === 'route' || k === 'routes' || k === 'route-file') o.route = v;
46
+ if (k === 'folder' || k === 'subfolder' || k === 'in' || k === 'group' || k === 'path') o.folder = v;
47
+ if (k === 'file' || k === 'path') o.file = v;
48
+ }
49
+ o.flags.add(k);
50
+ } else if (/^-[a-z]+$/i.test(a)) {
51
+ a.slice(1).split('').forEach((f) => o.flags.add(f.toLowerCase()));
52
+ } else {
53
+ o._.push(a);
54
+ }
55
+ }
56
+ return o;
57
+ }
58
+ function context(o) {
59
+ const deps = require('./deps.js');
60
+ const start = path.resolve(o.dir || '.');
61
+ const lay = require('./layout.js').layout(start);
62
+ const project = lay.split ? lay.project : deps.projectDir(start);
63
+ require('./env.js').loadEnv(project);
64
+ const base = path.resolve(o.backend || (lay.split ? lay.backend : project)); // backend/ in new projects
65
+ return { project, base, frontend: lay.split ? lay.frontend : project };
66
+ }
67
+ async function withDb(o, ctx, fn) {
68
+ const { readConfig } = require('./kreftjs-config.js');
69
+ const config = readConfig(ctx.base);
70
+ const url = o.db || process.env.DATABASE_URL || config.db;
71
+ if (!url) { console.error(' Which database? Set DATABASE_URL in .env (for example sqlite:database/app.db), or add --db.'); process.exit(1); }
72
+ const { connect } = require('./db.js');
73
+ const backend = require('./backend/index.js');
74
+ backend.installAlias();
75
+ let db;
76
+ try {
77
+ db = await connect(url, { root: config.dir || ctx.project });
78
+ backend.setConnection(db);
79
+ require('./backend/model.js').setBaseDir(ctx.base);
80
+ await fn(db, backend);
81
+ } catch (e) {
82
+ console.error(' ' + String(e.message || e).replace(/\n/g, '\n '));
83
+ process.exitCode = 1;
84
+ } finally { if (db) await db.close(); }
85
+ }
86
+ const migrationsOf = (o, ctx) => (o.migrations ? path.resolve(o.migrations) : path.join(ctx.base, 'database', 'migrations'));
87
+ const green = (s) => (process.stdout.isTTY ? `\x1b[32m${s}\x1b[0m` : s);
88
+ const red = (s) => (process.stdout.isTTY ? `\x1b[31m${s}\x1b[0m` : s);
89
+ const yellow = (s) => (process.stdout.isTTY ? `\x1b[33m${s}\x1b[0m` : s);
90
+ const dim = (s) => (process.stdout.isTTY ? `\x1b[2m${s}\x1b[0m` : s);
91
+
92
+ /* ------------------------------------------------------------------ database commands */
93
+ const commands = {
94
+ async migrate(args) {
95
+ const o = options(args), ctx = context(o);
96
+ const targetFile = o.file || (o._.length && !o._[0].startsWith('-') ? o._[0] : null);
97
+ await withDb(o, ctx, async (db, b) => {
98
+ const ran = await b.migrator.migrate(db, migrationsOf(o, ctx), (f) => console.log(` ${green('DONE')} ${f}`), { file: targetFile });
99
+ console.log(ran.length ? `\n ${ran.length} migration${ran.length === 1 ? '' : 's'} ran.` : ' Nothing to migrate.');
100
+ if (o.flags.has('seed')) await runSeed(db, b, ctx, o.class);
101
+ });
102
+ },
103
+ async 'migrate:rollback'(args) {
104
+ const o = options(args), ctx = context(o);
105
+ const targetFile = o.file || (o._.length && !o._[0].startsWith('-') ? o._[0] : null);
106
+ await withDb(o, ctx, async (db, b) => {
107
+ const undone = await b.migrator.rollback(db, migrationsOf(o, ctx), (f) => console.log(` ${green('UNDONE')} ${f}`), {
108
+ steps: parseInt(o.step, 10) || 0,
109
+ file: targetFile,
110
+ });
111
+ if (!undone.length) console.log(' Nothing to roll back.');
112
+ });
113
+ },
114
+ async 'migrate:fresh'(args) {
115
+ const o = options(args), ctx = context(o);
116
+ await withDb(o, ctx, async (db, b) => {
117
+ const dropped = await b.migrator.dropAll(db);
118
+ console.log(` Dropped ${dropped.length} table${dropped.length === 1 ? '' : 's'}.`);
119
+ await b.migrator.migrate(db, migrationsOf(o, ctx), (f) => console.log(` ${green('DONE')} ${f}`));
120
+ if (o.flags.has('seed')) await runSeed(db, b, ctx, o.class);
121
+ });
122
+ },
123
+ async 'migrate:status'(args) {
124
+ const o = options(args), ctx = context(o);
125
+ await withDb(o, ctx, async (db, b) => {
126
+ const list = await b.migrator.status(db, migrationsOf(o, ctx));
127
+ if (!list.length) return console.log(' No migrations in database/migrations.');
128
+ list.forEach((m) => console.log(` ${m.ran ? green('Ran ') : 'Pending'} ${m.name}${m.ran ? dim(' batch ' + m.batch) : ''}`));
129
+ });
130
+ },
131
+ async 'db:seed'(args) {
132
+ const o = options(args), ctx = context(o);
133
+ await withDb(o, ctx, (db, b) => runSeed(db, b, ctx, o.class));
134
+ },
135
+ 'migrate:file': (args) => commands.migrate(args),
136
+ 'migrate:undo': (args) => commands['migrate:rollback'](args),
137
+ async 'route:list'(args) {
138
+ const o = options(args), ctx = context(o);
139
+ const b = require('./backend/index.js');
140
+ const handler = b.createBackend(ctx.base, { log: console });
141
+ if (!handler) return console.log(' No routes/api.js in ' + ctx.base);
142
+ if (typeof handler.ready === 'function') await handler.ready();
143
+ const rows = handler.routes().map((r) => ({
144
+ method: r.methods.filter((m) => m !== 'HEAD').join('|'),
145
+ uri: ('api' + r.uri).replace(/\/$/, ''),
146
+ name: r.routeName || '',
147
+ action: actionName(r.action),
148
+ middleware: r.middlewares.map((m) => (typeof m === 'string' ? m : m.name || 'fn')).join(', '),
149
+ }));
150
+ const w = (k) => Math.max(k.length, ...rows.map((r) => r[k].length));
151
+ console.log('');
152
+ rows.forEach((r) => console.log(` ${r.method.padEnd(w('method'))} ${r.uri.padEnd(w('uri'))} ${dim(r.name.padEnd(w('name')))} ${r.action}${r.middleware ? dim(' [' + r.middleware + ']') : ''}`));
153
+ console.log(`\n ${rows.length} route${rows.length === 1 ? '' : 's'}\n`);
154
+ },
155
+ update: (args) => require('./update.js')(args),
156
+ add: (args) => require('./add.js')(args),
157
+ install: (args) => require('./add.js')(args),
158
+ i: (args) => require('./add.js')(args),
159
+ };
160
+ function actionName(a) {
161
+ if (Array.isArray(a)) return `${(typeof a[0] === 'string' ? a[0] : a[0].name) || 'Controller'}@${a[1]}`;
162
+ if (typeof a === 'string') return a;
163
+ return a.name ? a.name + '()' : 'Closure';
164
+ }
165
+ async function runSeed(db, b, ctx, className) {
166
+ const ok = await b.migrator.seed(db, ctx.base, { className: className || 'DatabaseSeeder', log: (n) => console.log(` ${green('SEEDING')} ${n}`) });
167
+ if (ok) return console.log(' Seeded.');
168
+ // an older project: database/seed.sql
169
+ const sql = path.join(ctx.base, 'database', 'seed.sql');
170
+ if (!className && fs.existsSync(sql)) { await db.exec(fs.readFileSync(sql, 'utf8')); return console.log(' Seeded from database/seed.sql'); }
171
+ console.log(` No database/seeders/${className || 'DatabaseSeeder'}.js. Make one with: kreftjs make:seeder ${className || 'DatabaseSeeder'}`);
172
+ }
173
+
174
+ /* ------------------------------------------------------------------ make:… */
175
+ const pascal = (s) => s.replace(/(^|[-_\s])(\w)/g, (_, __, c) => c.toUpperCase()).replace(/[^\w]/g, '');
176
+ // 'User/ProfileController', 'user\\profile-controller' or 'User.ProfileController' → { dirs: ['User'], name: 'ProfileController' }
177
+ // (folders are made when they don't exist yet, e.g. kreftjs make:controller User/ProfileController)
178
+ function nested(raw) {
179
+ const parts = String(raw || '').split(/[\\/]+/).map((p) => pascal(p)).filter(Boolean);
180
+ const name = parts.pop() || '';
181
+ return { dirs: parts, name, rel: [...parts, name].join('/') };
182
+ }
183
+ const up = (n) => '../'.repeat(n);
184
+ const snake = (s) => s.replace(/([a-z0-9])([A-Z])/g, '$1_$2').toLowerCase();
185
+ const { plural } = require('./backend/schema.js');
186
+ // a time stamp later than every migration already there, so files keep their order even within one second
187
+ function nextStamp(dir) {
188
+ let t = new Date();
189
+ const last = (fs.existsSync(dir) ? fs.readdirSync(dir) : []).map((f) => f.match(/^(\d{4})_(\d\d)_(\d\d)_(\d\d)(\d\d)(\d\d)_/)).filter(Boolean)
190
+ .map((m) => new Date(+m[1], m[2] - 1, +m[3], +m[4], +m[5], +m[6]).getTime()).sort((a, b) => b - a)[0];
191
+ if (last != null && last >= t.getTime() - (t.getTime() % 1000)) t = new Date(last + 1000);
192
+ return stamp(t);
193
+ }
194
+ const stamp = (d = new Date()) => { const p = (n) => String(n).padStart(2, '0'); return `${d.getFullYear()}_${p(d.getMonth() + 1)}_${p(d.getDate())}_${p(d.getHours())}${p(d.getMinutes())}${p(d.getSeconds())}`; };
195
+
196
+ // a project with a tsconfig.json (or --ts flag) gets TypeScript files from kreftjs make:* (import/export instead of require)
197
+ function isTsProject(file) {
198
+ if (process.argv.includes('--ts') || process.argv.includes('--typescript')) return true;
199
+ if (process.argv.includes('--js') || process.argv.includes('--javascript')) return false;
200
+ for (let dir = path.dirname(file); ; dir = path.dirname(dir)) {
201
+ if (fs.existsSync(path.join(dir, 'tsconfig.json'))) return true;
202
+ if (fs.existsSync(path.join(dir, 'package.json'))) return fs.existsSync(path.join(dir, 'tsconfig.json'));
203
+ if (dir === path.dirname(dir)) return false;
204
+ }
205
+ }
206
+ function write(file, content, base) {
207
+ if (file.endsWith('.js') && isTsProject(file)) { file = file.replace(/\.js$/, '.ts'); content = require('./to-ts.js').toTs(content); }
208
+ const rel = path.relative(process.cwd(), file) || file;
209
+ if (fs.existsSync(file)) { console.error(` ${rel} already exists.`); process.exitCode = 1; return false; }
210
+ fs.mkdirSync(path.dirname(file), { recursive: true });
211
+ fs.writeFileSync(file, content);
212
+ console.log(` ${green('CREATED')} ${rel}`);
213
+ return true;
214
+ }
215
+
216
+ const stubs = {
217
+ webRoutes: (name) => `// The ${name} pages. index.html loads it: Kreftjs.router({ routes: ['routes/web.kreftjs', 'routes/${name}.kreftjs'] })
218
+ // Route.view(address, page) · .name() · .title() · .layout() · .middleware() — see routes/web.kreftjs
219
+
220
+ Route.prefix('/${name}').name('${name}.').middleware('auth').group(() => {
221
+ // Route.view('/', 'pages/${name}/Index.kreftjs').name('index').title('${name[0].toUpperCase() + name.slice(1)}')
222
+ })
223
+ `,
224
+ apiRoutes: (name) => `// The ${name} API, a route file of its own. routes/api.js loads it with require('./${name}'), so every address starts with /api.
225
+ const { Route } = require('kreftjs/server')
226
+
227
+ Route.prefix('/${name}').middleware('auth').group(() => {
228
+ // Route.get('/', [${name[0].toUpperCase() + name.slice(1)}Controller, 'index'])
229
+ })
230
+ `,
231
+ model: (name) => `const { Model } = require('kreftjs/server')
232
+
233
+ // The ${plural(snake(name))} table. ${name}.all(), ${name}.find(id), ${name}.where(…).get(), ${name}.create({…})
234
+ class ${name} extends Model {
235
+ // static table = '${plural(snake(name))}'
236
+ // static fillable = ['title', 'body'] // only these columns may be filled by create() and update()
237
+ static hidden = [] // never sent back as JSON (like passwords)
238
+ static casts = {} // { published: 'boolean', meta: 'json' }
239
+
240
+ // relations:
241
+ // user() { return this.belongsTo(require('./User')) }
242
+ // comments() { return this.hasMany(require('./Comment')) }
243
+ }
244
+
245
+ module.exports = ${name}
246
+ `,
247
+ controller: (name, model, modelReq) => model ? `const { abort, response, slug } = require('kreftjs/server')
248
+ const ${model} = require('${modelReq || '../../Models/' + model}')
249
+
250
+ // Route.apiResource('${plural(snake(model)).replace(/_/g, '-')}', ${name}) in routes/api.js
251
+ class ${name} {
252
+ // GET /api/${plural(snake(model))}
253
+ async index(req) {
254
+ return ${model}.latest('id').get()
255
+ }
256
+
257
+ // POST /api/${plural(snake(model))}
258
+ async store(req) {
259
+ // const data = await req.validate({ title: 'required|max:200' })
260
+ const data = req.all()
261
+ // if (data.title && !data.slug) data.slug = await slug.unique(${model}, 'slug', data.title)
262
+ return response(await ${model}.create(data), 201)
263
+ }
264
+
265
+ // GET /api/${plural(snake(model))}/{id}
266
+ async show(req, id) {
267
+ return ${model}.findOrFail(id)
268
+ }
269
+
270
+ // PUT or PATCH /api/${plural(snake(model))}/{id}
271
+ async update(req, id) {
272
+ const ${snake(model)} = await ${model}.findOrFail(id)
273
+ // const data = await req.validate({ title: 'sometimes|required|max:200' })
274
+ const data = req.all()
275
+ return ${snake(model)}.update(data)
276
+ }
277
+
278
+ // DELETE /api/${plural(snake(model))}/{id}
279
+ async destroy(req, id) {
280
+ const ${snake(model)} = await ${model}.findOrFail(id)
281
+ await ${snake(model)}.delete()
282
+ // nothing returned: 204 No Content
283
+ }
284
+ }
285
+
286
+ module.exports = ${name}
287
+ ` : `const { abort, response } = require('kreftjs/server')
288
+
289
+ // In routes/api.js: Route.get('/something', [${name}, 'index'])
290
+ class ${name} {
291
+ async index(req) {
292
+ return { message: 'Hello from ${name}' }
293
+ }
294
+ }
295
+
296
+ module.exports = ${name}
297
+ `,
298
+ migration: (name) => {
299
+ const create = name.match(/^create_(\w+?)_table$/);
300
+ const change = name.match(/^\w+_(?:to|from|in)_(\w+?)_table$/);
301
+ if (create) return `// kreftjs migrate runs up(); kreftjs migrate:rollback runs down()
302
+ module.exports = {
303
+ up(schema) {
304
+ schema.create('${create[1]}', (table) => {
305
+ table.id()
306
+ table.timestamps()
307
+ })
308
+ },
309
+
310
+ down(schema) {
311
+ schema.dropIfExists('${create[1]}')
312
+ },
313
+ }
314
+ `;
315
+ const t = change ? change[1] : 'table_name';
316
+ return `// kreftjs migrate runs up(); kreftjs migrate:rollback runs down()
317
+ module.exports = {
318
+ up(schema) {
319
+ schema.table('${t}', (table) => {
320
+ // table.string('column').nullable()
321
+ })
322
+ },
323
+
324
+ down(schema) {
325
+ schema.table('${t}', (table) => {
326
+ // table.dropColumn('column')
327
+ })
328
+ },
329
+ }
330
+ `;
331
+ },
332
+ seeder: (name, model, modelReq) => `const { Seeder, DB } = require('kreftjs/server')
333
+ ${model ? `const ${model} = require('${modelReq || '../../app/Models/' + model}')\n` : ''}
334
+ // kreftjs db:seed --class ${name} (or this.call(${name}) from DatabaseSeeder)
335
+ class ${name} extends Seeder {
336
+ async run() {
337
+ ${model ? ` // await ${model}.factory().count(10).create()\n // await ${model}.create({ … })\n` : name === 'DatabaseSeeder' ? ' // await this.call([UserSeeder, PostSeeder])\n' : " // await DB.table('posts').insert({ … })\n"} }
338
+ }
339
+
340
+ module.exports = ${name}
341
+ `,
342
+ middleware: (name) => `const { abort } = require('kreftjs/server')
343
+
344
+ // Use it in routes/api.js: Route.middleware('${name.replace(/Middleware$/, '').replace(/^[A-Z]/, (c) => c.toLowerCase())}').group(() => { … })
345
+ // Arguments after a colon arrive after next: 'role:admin' → handle(req, next, 'admin')
346
+ class ${name} {
347
+ async handle(req, next) {
348
+ // before the controller: check something, or stop with abort(403)
349
+ const response = await next(req)
350
+ // after the controller: response.header('X-Something', 'yes')
351
+ return response
352
+ }
353
+ }
354
+
355
+ module.exports = ${name}
356
+ `,
357
+ request: (name, rulesObj, isTs = false) => {
358
+ let rulesStr = '';
359
+ if (rulesObj && Object.keys(rulesObj).length) {
360
+ rulesStr = Object.entries(rulesObj)
361
+ .map(([k, v]) => ` ${k}: '${v}',`)
362
+ .join('\n');
363
+ } else {
364
+ rulesStr = ` // title: 'required|string|max:255',\n // body: 'required|string',`;
365
+ }
366
+ if (isTs) {
367
+ return `import { FormRequest } from 'kreftjs/server'
368
+
369
+ export default class ${name} extends FormRequest {
370
+ /**
371
+ * Determine if the user is authorized to make this request.
372
+ */
373
+ authorize(): boolean {
374
+ return true
375
+ }
376
+
377
+ /**
378
+ * Get the validation rules that apply to the request.
379
+ */
380
+ rules(): Record<string, string | string[]> {
381
+ return {
382
+ ${rulesStr}
383
+ }
384
+ }
385
+
386
+ /**
387
+ * Custom error messages.
388
+ */
389
+ messages(): Record<string, string> {
390
+ return {}
391
+ }
392
+ }
393
+ `;
394
+ }
395
+ return `const { FormRequest } = require('kreftjs/server')
396
+
397
+ class ${name} extends FormRequest {
398
+ /**
399
+ * Determine if the user is authorized to make this request.
400
+ */
401
+ authorize() {
402
+ return true
403
+ }
404
+
405
+ /**
406
+ * Get the validation rules that apply to the request.
407
+ */
408
+ rules() {
409
+ return {
410
+ ${rulesStr}
411
+ }
412
+ }
413
+
414
+ /**
415
+ * Custom error messages.
416
+ */
417
+ messages() {
418
+ return {}
419
+ }
420
+ }
421
+
422
+ module.exports = ${name}
423
+ `;
424
+ },
425
+ resource: (name, isTs = false) => {
426
+ if (isTs) {
427
+ return `import { JsonResource } from 'kreftjs/server'
428
+
429
+ export default class ${name} extends JsonResource {
430
+ toArray(req: any) {
431
+ return {
432
+ id: this.id,
433
+ created_at: this.created_at,
434
+ updated_at: this.updated_at,
435
+ }
436
+ }
437
+ }
438
+ `;
439
+ }
440
+ return `const { JsonResource } = require('kreftjs/server')
441
+
442
+ class ${name} extends JsonResource {
443
+ toArray(req) {
444
+ return {
445
+ id: this.id,
446
+ created_at: this.created_at,
447
+ updated_at: this.updated_at,
448
+ }
449
+ }
450
+ }
451
+
452
+ module.exports = ${name}
453
+ `;
454
+ },
455
+ rule: (name, isTs = false) => {
456
+ if (isTs) {
457
+ return `// In rules: { code: ['required', new ${name}()] }
458
+ export default class ${name} {
459
+ passes(attribute: string, value: any, data: any): boolean {
460
+ return true
461
+ }
462
+
463
+ message(): string {
464
+ return 'The :attribute is invalid.'
465
+ }
466
+ }
467
+ `;
468
+ }
469
+ return `// In rules: { code: ['required', new ${name}()] }
470
+ class ${name} {
471
+ passes(attribute, value, data) {
472
+ return true
473
+ }
474
+
475
+ message() {
476
+ return 'The :attribute is invalid.'
477
+ }
478
+ }
479
+
480
+ module.exports = ${name}
481
+ `;
482
+ },
483
+ factory: (name, model, modelReq) => {
484
+ const m = (model || '').toLowerCase();
485
+ let fields = '';
486
+ if (m.includes('category') || m.includes('section') || m.includes('department')) {
487
+ fields = ` name: fake.unique().category(),
488
+ slug: (attrs) => fake.slug(attrs.name),
489
+ description: fake.sentence(),
490
+ is_active: true,`;
491
+ } else if (m.includes('tag')) {
492
+ fields = ` name: fake.unique().tag(),
493
+ slug: (attrs) => fake.slug(attrs.name),`;
494
+ } else if (m.includes('product') || m.includes('item')) {
495
+ fields = ` name: fake.product(),
496
+ slug: (attrs) => fake.slug(attrs.name),
497
+ sku: fake.sku('PRD'),
498
+ price: fake.price(10, 499),
499
+ description: fake.paragraph(),
500
+ is_active: true,`;
501
+ } else if (m.includes('post') || m.includes('article') || m.includes('blog') || m.includes('news')) {
502
+ fields = ` title: fake.sentence(),
503
+ slug: (attrs) => fake.slug(attrs.title),
504
+ content: fake.paragraphs(3),
505
+ is_published: true,`;
506
+ } else if (m.includes('brand') || m.includes('company')) {
507
+ fields = ` name: fake.unique().brand(),
508
+ slug: (attrs) => fake.slug(attrs.name),
509
+ is_active: true,`;
510
+ } else if (m.includes('user') || m.includes('customer') || m.includes('author') || m.includes('member')) {
511
+ fields = ` name: fake.name(),
512
+ email: fake.unique().email(),
513
+ password: 'password',`;
514
+ } else {
515
+ fields = ` name: fake.name(),
516
+ slug: (attrs) => fake.slug(attrs.name),
517
+ is_active: true,`;
518
+ }
519
+
520
+ return `const { Factory } = require('kreftjs/server')
521
+ const ${model} = require('${modelReq || '../../app/Models/' + model}')
522
+
523
+ // await ${model}.factory().count(10).create()
524
+ module.exports = Factory.define(${model}, (fake) => ({
525
+ ${fields}
526
+ }))
527
+ `;
528
+ },
529
+ page: (name, endpoint, dirs = [], isTs = false) => `<!-- frontend/pages/${[...dirs, name].join('/')}.kreftjs -->
530
+ layout 'layouts/app.kreftjs'
531
+ ${isTs ? `
532
+ interface ${name}Item {
533
+ id: number
534
+ title?: string
535
+ name?: string
536
+ body?: string
537
+ description?: string
538
+ created_at?: string
539
+ updated_at?: string
540
+ [key: string]: any
541
+ }
542
+ ` : ''}
543
+ state items${isTs ? `: ${name}Item[]` : ''} = []
544
+ state loading${isTs ? ': boolean' : ''} = true
545
+ state error${isTs ? ': string' : ''} = ''
546
+ state notice${isTs ? ': string' : ''} = ''
547
+ state search${isTs ? ': string' : ''} = ''
548
+ state sortBy${isTs ? ': string' : ''} = 'newest'
549
+ state selected${isTs ? `: ${name}Item | null` : ''} = null
550
+ state showCreate${isTs ? ': boolean' : ''} = false
551
+ state showEdit${isTs ? ': boolean' : ''} = false
552
+ state form = { title: '', body: '' }
553
+ state editForm${isTs ? ': { id: number | null; title: string; body: string }' : ''} = { id: null, title: '', body: '' }
554
+
555
+ derive filteredItems${isTs ? `: ${name}Item[]` : ''} = items.filter(i => {
556
+ if (!search) return true
557
+ const q = search.toLowerCase()
558
+ return (i.title || i.name || '').toLowerCase().includes(q) || (i.body || i.description || '').toLowerCase().includes(q) || String(i.id).includes(q)
559
+ }).sort((a, b) => {
560
+ if (sortBy === 'oldest') return a.id - b.id
561
+ if (sortBy === 'az') return (a.title || a.name || '').localeCompare(b.title || b.name || '')
562
+ if (sortBy === 'za') return (b.title || b.name || '').localeCompare(a.title || a.name || '')
563
+ return b.id - a.id
564
+ })
565
+
566
+ on start {
567
+ await loadData()
568
+ }
569
+
570
+ action loadData() {
571
+ loading = true
572
+ error = ''
573
+ try {
574
+ const res = await api.get('/api/${endpoint}')
575
+ items = Array.isArray(res) ? res : (res && Array.isArray(res.data) ? res.data : [])
576
+ } catch (e) {
577
+ error = e.message || 'Failed to load records'
578
+ } finally {
579
+ loading = false
580
+ }
581
+ }
582
+
583
+ action selectItem(item) {
584
+ selected = item
585
+ }
586
+
587
+ action closeDetail() {
588
+ selected = null
589
+ }
590
+
591
+ action openCreate() {
592
+ form = { title: '', body: '' }
593
+ showCreate = true
594
+ }
595
+
596
+ action closeCreate() {
597
+ showCreate = false
598
+ }
599
+
600
+ action createItem() {
601
+ error = ''
602
+ try {
603
+ const created = await api.post('/api/${endpoint}', form)
604
+ items = [created, ...items]
605
+ notice = 'Created successfully'
606
+ showCreate = false
607
+ } catch (e) {
608
+ error = e.message || 'Failed to create'
609
+ }
610
+ }
611
+
612
+ action openEdit(item) {
613
+ editForm = { id: item.id, title: item.title || item.name || '', body: item.body || item.description || '' }
614
+ showEdit = true
615
+ }
616
+
617
+ action closeEdit() {
618
+ showEdit = false
619
+ }
620
+
621
+ action updateItem() {
622
+ error = ''
623
+ try {
624
+ const updated = await api.patch('/api/${endpoint}/' + editForm.id, editForm)
625
+ items = items.map(x => x.id === editForm.id ? { ...x, ...updated } : x)
626
+ if (selected && selected.id === editForm.id) selected = { ...selected, ...updated }
627
+ notice = 'Updated successfully'
628
+ showEdit = false
629
+ } catch (e) {
630
+ error = e.message || 'Failed to update'
631
+ }
632
+ }
633
+
634
+ action removeItem(item) {
635
+ if (!confirm('Are you sure you want to delete this record?')) return
636
+ error = ''
637
+ try {
638
+ await api.delete('/api/${endpoint}/' + item.id)
639
+ items = items.filter(x => x.id !== item.id)
640
+ if (selected && selected.id === item.id) selected = null
641
+ notice = 'Deleted successfully'
642
+ } catch (e) {
643
+ error = e.message || 'Failed to delete'
644
+ }
645
+ }
646
+
647
+ <view>
648
+ <div class="page p-6 max-w-7xl mx-auto space-y-6">
649
+ <header class="flex flex-col sm:flex-row justify-between items-start sm:items-center gap-4 pb-4 border-b border-border">
650
+ <div>
651
+ <h1 class="text-2xl font-bold tracking-tight">${name}</h1>
652
+ <p class="text-xs text-muted mt-1">Search, filter, view details, create and edit ${plural(snake(name)).replace(/_/g, ' ')}</p>
653
+ </div>
654
+ <div class="flex items-center gap-2">
655
+ <button class="btn btn-secondary text-xs" @click="loadData">Refresh</button>
656
+ <button class="btn btn-primary text-xs" @click="openCreate">+ New ${name}</button>
657
+ </div>
658
+ </header>
659
+
660
+ <p k-if="notice" class="p-3 bg-subtle border border-border text-sm flex justify-between items-center">
661
+ <span>{notice}</span>
662
+ <button class="text-muted hover:text-foreground text-xs" @click="notice = ''">✕</button>
663
+ </p>
664
+
665
+ <p k-if="error" class="p-3 bg-red-500/10 border border-red-500/30 text-red-500 text-sm flex justify-between items-center">
666
+ <span>{error}</span>
667
+ <button class="text-xs" @click="error = ''">✕</button>
668
+ </p>
669
+
670
+ <div class="flex flex-col sm:flex-row gap-3 items-stretch sm:items-center justify-between">
671
+ <div class="relative flex-1">
672
+ <input type="search" k-model="search" placeholder="Search records by title, body, ID…" class="w-full px-3 py-2 bg-subtle border border-border text-sm outline-none focus:border-foreground" />
673
+ </div>
674
+ <div class="flex items-center gap-2">
675
+ <label class="text-xs text-muted whitespace-nowrap">Sort:</label>
676
+ <select k-model="sortBy" class="px-3 py-2 bg-subtle border border-border text-sm outline-none">
677
+ <option value="newest">Newest first</option>
678
+ <option value="oldest">Oldest first</option>
679
+ <option value="az">A-Z</option>
680
+ <option value="za">Z-A</option>
681
+ </select>
682
+ </div>
683
+ </div>
684
+
685
+ <div class="grid grid-cols-1 lg:grid-cols-3 gap-6">
686
+ <div class="lg:col-span-2 border border-border bg-subtle overflow-hidden">
687
+ <div k-if="loading" class="p-8 text-center text-muted text-sm">
688
+ Loading ${plural(snake(name)).replace(/_/g, ' ')}…
689
+ </div>
690
+
691
+ <div k-elif="!filteredItems.length" class="p-8 text-center text-muted text-sm">
692
+ No records match the filter criteria.
693
+ </div>
694
+
695
+ <table k-else class="w-full text-left text-sm border-collapse">
696
+ <thead>
697
+ <tr class="border-b border-border bg-card text-xs text-muted">
698
+ <th class="p-3">ID</th>
699
+ <th class="p-3">Title / Record</th>
700
+ <th class="p-3">Date</th>
701
+ <th class="p-3 text-right">Actions</th>
702
+ </tr>
703
+ </thead>
704
+ <tbody>
705
+ <tr k-each="item in filteredItems" class="border-b border-border/50 hover:bg-card/50 cursor-pointer" @click="selectItem(item)">
706
+ <td class="p-3 font-mono text-xs">{item.id}</td>
707
+ <td class="p-3">
708
+ <div class="font-medium">{item.title || item.name || 'Untitled'}</div>
709
+ <div k-if="item.body || item.description" class="text-xs text-muted line-clamp-1">
710
+ {item.body || item.description}
711
+ </div>
712
+ </td>
713
+ <td class="p-3 text-xs text-muted whitespace-nowrap">
714
+ {item.created_at ? item.created_at.slice(0, 10) : '-'}
715
+ </td>
716
+ <td class="p-3 text-right whitespace-nowrap space-x-1" @click.stop="">
717
+ <button class="btn btn-xs btn-ghost" @click="selectItem(item)">View</button>
718
+ <button class="btn btn-xs btn-ghost" @click="openEdit(item)">Edit</button>
719
+ <button class="btn btn-xs btn-ghost text-red-500" @click="removeItem(item)">Delete</button>
720
+ </td>
721
+ </tr>
722
+ </tbody>
723
+ </table>
724
+ </div>
725
+
726
+ <div class="border border-border bg-card p-5 h-fit">
727
+ <div k-if="!selected" class="text-center py-12 text-muted text-sm">
728
+ <p class="font-medium text-foreground mb-1">No Record Selected</p>
729
+ <p class="text-xs">Select any row from the table to view or inspect its full data.</p>
730
+ </div>
731
+
732
+ <div k-else class="space-y-4">
733
+ <div class="flex justify-between items-start pb-3 border-b border-border">
734
+ <div>
735
+ <span class="font-mono text-xs text-muted">ID: {selected.id}</span>
736
+ <h2 class="text-lg font-bold">{selected.title || selected.name || 'Record'}</h2>
737
+ </div>
738
+ <button class="text-muted hover:text-foreground text-xs" @click="closeDetail">✕</button>
739
+ </div>
740
+
741
+ <div class="space-y-2 text-xs">
742
+ <div>
743
+ <span class="text-muted block mb-1">Full Content / Details:</span>
744
+ <div class="p-3 bg-subtle border border-border whitespace-pre-wrap font-sans">
745
+ {selected.body || selected.description || JSON.stringify(selected, null, 2)}
746
+ </div>
747
+ </div>
748
+ <div k-if="selected.created_at" class="text-muted">
749
+ Created: <code>{selected.created_at}</code>
750
+ </div>
751
+ <div k-if="selected.updated_at" class="text-muted">
752
+ Updated: <code>{selected.updated_at}</code>
753
+ </div>
754
+ </div>
755
+
756
+ <div class="flex gap-2 pt-3 border-t border-border">
757
+ <button class="btn btn-secondary text-xs flex-1" @click="openEdit(selected)">Edit</button>
758
+ <button class="btn btn-ghost text-red-500 text-xs" @click="removeItem(selected)">Delete</button>
759
+ </div>
760
+ </div>
761
+ </div>
762
+ </div>
763
+
764
+ <!-- Create Modal -->
765
+ <div k-if="showCreate" class="fixed inset-0 bg-black/60 z-50 flex items-center justify-center p-4">
766
+ <div class="bg-card border border-border w-full max-w-lg p-6 space-y-4">
767
+ <div class="flex justify-between items-center pb-2 border-b border-border">
768
+ <h3 class="text-lg font-bold">Create New ${name}</h3>
769
+ <button class="text-muted hover:text-foreground text-sm" @click="closeCreate">✕</button>
770
+ </div>
771
+
772
+ <div class="space-y-3 text-sm">
773
+ <div>
774
+ <label class="block text-xs text-muted mb-1">Title / Name</label>
775
+ <input type="text" k-model="form.title" placeholder="Enter title…" class="w-full px-3 py-2 bg-subtle border border-border outline-none" />
776
+ </div>
777
+ <div>
778
+ <label class="block text-xs text-muted mb-1">Content / Description</label>
779
+ <textarea k-model="form.body" rows="4" placeholder="Enter description…" class="w-full px-3 py-2 bg-subtle border border-border outline-none"></textarea>
780
+ </div>
781
+ </div>
782
+
783
+ <div class="flex justify-end gap-2 pt-2 border-t border-border">
784
+ <button class="btn btn-secondary text-xs" @click="closeCreate">Cancel</button>
785
+ <button class="btn btn-primary text-xs" @click="createItem">Save</button>
786
+ </div>
787
+ </div>
788
+ </div>
789
+
790
+ <!-- Edit Modal -->
791
+ <div k-if="showEdit" class="fixed inset-0 bg-black/60 z-50 flex items-center justify-center p-4">
792
+ <div class="bg-card border border-border w-full max-w-lg p-6 space-y-4">
793
+ <div class="flex justify-between items-center pb-2 border-b border-border">
794
+ <h3 class="text-lg font-bold">Edit ${name} #{editForm.id}</h3>
795
+ <button class="text-muted hover:text-foreground text-sm" @click="closeEdit">✕</button>
796
+ </div>
797
+
798
+ <div class="space-y-3 text-sm">
799
+ <div>
800
+ <label class="block text-xs text-muted mb-1">Title / Name</label>
801
+ <input type="text" k-model="editForm.title" class="w-full px-3 py-2 bg-subtle border border-border outline-none" />
802
+ </div>
803
+ <div>
804
+ <label class="block text-xs text-muted mb-1">Content / Description</label>
805
+ <textarea k-model="editForm.body" rows="4" class="w-full px-3 py-2 bg-subtle border border-border outline-none"></textarea>
806
+ </div>
807
+ </div>
808
+
809
+ <div class="flex justify-end gap-2 pt-2 border-t border-border">
810
+ <button class="btn btn-secondary text-xs" @click="closeEdit">Cancel</button>
811
+ <button class="btn btn-primary text-xs" @click="updateItem">Update</button>
812
+ </div>
813
+ </div>
814
+ </div>
815
+ </div>
816
+ </view>
817
+ `,
818
+ };
819
+
820
+ function addApiRoute(ctx, name, dirs, { route, auth }) {
821
+ const routeName = route ? String(route).replace(/\.(js|ts)$/, '').replace(/^[\\/]+/, '') : 'api';
822
+ const routeBaseName = routeName.split(/[\\/]/).pop();
823
+ let routeFile = path.join(ctx.base, 'routes', routeBaseName + '.js');
824
+ if (isTsProject(routeFile)) routeFile = routeFile.replace(/\.js$/, '.ts');
825
+
826
+ const isTs = isTsProject(routeFile);
827
+ if (!fs.existsSync(routeFile)) {
828
+ if (routeBaseName !== 'api') {
829
+ write(routeFile, stubs.apiRoutes(routeBaseName));
830
+ const mainApi = path.join(ctx.base, 'routes', isTs ? 'api.ts' : 'api.js');
831
+ if (fs.existsSync(mainApi)) {
832
+ const mainText = fs.readFileSync(mainApi, 'utf8');
833
+ const loadStmt = isTs ? `import './${routeBaseName}'\n` : `require('./${routeBaseName}')\n`;
834
+ if (!mainText.includes(routeBaseName)) {
835
+ fs.writeFileSync(mainApi, mainText + `\n// ${routeBaseName} routes\n${loadStmt}`);
836
+ console.log(` ${green('UPDATED')} ${path.relative(process.cwd(), mainApi)} (${loadStmt.trim()})`);
837
+ }
838
+ }
839
+ } else {
840
+ write(routeFile, isTs ? "import { Route } from 'kreftjs/server'\n\n" : "const { Route } = require('kreftjs/server')\n\n");
841
+ }
842
+ }
843
+
844
+ const endpoint = [...(dirs.length ? dirs.map((d) => snake(d).replace(/_/g, '-')) : []), plural(snake(name)).replace(/_/g, '-')].join('/');
845
+ const controllerName = `${name}Controller`;
846
+ const controllerRel = [...dirs, controllerName].join('/');
847
+ let text = fs.readFileSync(routeFile, 'utf8');
848
+
849
+ if (text.includes(`apiResource('${endpoint}'`) || text.includes(`apiResource("${endpoint}"`)) {
850
+ return;
851
+ }
852
+
853
+ const requireLine = isTs
854
+ ? `import ${controllerName} from '../app/Http/Controllers/${controllerRel}'\n`
855
+ : `const ${controllerName} = require('../app/Http/Controllers/${controllerRel}')\n`;
856
+
857
+ if (!text.includes(controllerName)) {
858
+ if (text.includes("require('kreftjs/server')") || text.includes("from 'kreftjs/server'")) {
859
+ text = text.replace(/((?:const \{[^}]+\} = require\('(?:kreftjs|kreft|somjs|som)\/server'\)|import \{[^}]+\} from '(?:kreftjs|kreft|somjs|som)\/server')[^\n]*\n)/, `$1${requireLine}`);
860
+ } else {
861
+ text = requireLine + text;
862
+ }
863
+ }
864
+
865
+ const routeLine = `Route.apiResource('${endpoint}', ${controllerName})${auth ? ".middleware('auth')" : ''}\n`;
866
+ text = text.trimEnd() + '\n' + routeLine;
867
+ fs.writeFileSync(routeFile, text);
868
+ console.log(` ${green('UPDATED')} ${path.relative(process.cwd(), routeFile)} (Route.apiResource('${endpoint}', ${controllerName})${auth ? ".middleware('auth')" : ''})`);
869
+ }
870
+
871
+ function addSidebarLink(ctx, routeNameTag, label) {
872
+ const layoutFile = path.join(ctx.frontend, 'layouts', 'app.kreftjs');
873
+ if (!fs.existsSync(layoutFile)) return;
874
+ let text = fs.readFileSync(layoutFile, 'utf8');
875
+ if (text.includes(`'${routeNameTag}'`) || text.includes(`"${routeNameTag}"`)) return;
876
+
877
+ const newLink = ` { to: '${routeNameTag}', label: '${label}', icon: 'panel' },\n`;
878
+ const mgmtMatch = text.match(/(label:\s*['"]Management['"][^[]*links:\s*\[[\s\S]*?)(\n\s*\]\s*\},?)/);
879
+ if (mgmtMatch) {
880
+ text = text.replace(mgmtMatch[0], mgmtMatch[1].trimEnd() + '\n' + newLink + mgmtMatch[2].trimStart());
881
+ fs.writeFileSync(layoutFile, text);
882
+ console.log(` ${green('UPDATED')} ${path.relative(process.cwd(), layoutFile)} (Sidebar: { to: '${routeNameTag}', label: '${label}' })`);
883
+ }
884
+ }
885
+
886
+ function addWebRoute(ctx, name, dirs, { route, auth }) {
887
+ const routeName = route ? String(route).replace(/\.(kreftjs|kreft|som|js)$/, '').replace(/^[\\/]+/, '') : 'web';
888
+ const routeBaseName = routeName.split(/[\\/]/).pop();
889
+ const fileTargetName = routeBaseName === 'api' ? 'web' : routeBaseName;
890
+ const routeFile = path.join(ctx.frontend, 'routes', fileTargetName + '.kreftjs');
891
+
892
+ if (!fs.existsSync(routeFile)) {
893
+ write(routeFile, stubs.webRoutes(fileTargetName));
894
+ }
895
+
896
+ const endpoint = '/' + [...(dirs.length ? dirs.map((d) => snake(d).replace(/_/g, '-')) : []), plural(snake(name)).replace(/_/g, '-')].join('/');
897
+ const pageRel = `pages/${[...dirs, name].join('/')}.kreftjs`;
898
+ let text = fs.readFileSync(routeFile, 'utf8');
899
+
900
+ if (text.includes(`'${endpoint}'`) || text.includes(`"${endpoint}"`)) {
901
+ return;
902
+ }
903
+
904
+ const routeNameTag = [...dirs.map(snake), plural(snake(name)).replace(/_/g, '-')].join('.');
905
+ const routeLine = `Route.view('${endpoint}', '${pageRel}').name('${routeNameTag}')${auth ? ".middleware('auth')" : ''}\n`;
906
+ text = text.trimEnd() + '\n' + routeLine;
907
+ fs.writeFileSync(routeFile, text);
908
+ console.log(` ${green('UPDATED')} ${path.relative(process.cwd(), routeFile)} (Route.view('${endpoint}', '${pageRel}')${auth ? ".middleware('auth')" : ''})`);
909
+ addSidebarLink(ctx, routeNameTag, plural(name));
910
+ }
911
+
912
+ function cleanUpEmptyDirs(filePath, stopDirs) {
913
+ let dir = path.dirname(filePath);
914
+ while (dir && dir !== '.' && dir !== '/' && dir !== path.dirname(dir)) {
915
+ const base = path.basename(dir);
916
+ if (stopDirs.has(base)) break;
917
+ try {
918
+ const items = fs.readdirSync(dir);
919
+ if (items.length === 0) {
920
+ fs.rmdirSync(dir);
921
+ dir = path.dirname(dir);
922
+ } else {
923
+ break;
924
+ }
925
+ } catch (_) {
926
+ break;
927
+ }
928
+ }
929
+ }
930
+
931
+ function removeApiRoute(ctx, name, dirs, { route, dryRun = false }) {
932
+ const routeName = route ? String(route).replace(/\.(js|ts)$/, '').replace(/^[\\/]+/, '') : 'api';
933
+ const routeBaseName = routeName.split(/[\\/]/).pop();
934
+ const routeFiles = [
935
+ path.join(ctx.base, 'routes', routeBaseName + '.js'),
936
+ path.join(ctx.base, 'routes', routeBaseName + '.ts'),
937
+ path.join(ctx.project, 'routes', routeBaseName + '.js'),
938
+ path.join(ctx.project, 'routes', routeBaseName + '.ts')
939
+ ];
940
+ if (routeBaseName !== 'api') {
941
+ routeFiles.push(
942
+ path.join(ctx.base, 'routes', 'api.js'),
943
+ path.join(ctx.base, 'routes', 'api.ts')
944
+ );
945
+ }
946
+ const seenFiles = new Set();
947
+ const removals = [];
948
+
949
+ const endpoints = new Set([
950
+ [...(dirs.length ? dirs.map((d) => snake(d).replace(/_/g, '-')) : []), plural(snake(name)).replace(/_/g, '-')].join('/'),
951
+ plural(snake(name)).replace(/_/g, '-')
952
+ ]);
953
+ const controllerName = `${name}Controller`;
954
+
955
+ for (const f of routeFiles) {
956
+ if (!f || seenFiles.has(f) || !fs.existsSync(f)) continue;
957
+ seenFiles.add(f);
958
+
959
+ const origText = fs.readFileSync(f, 'utf8');
960
+ const lines = origText.split('\n');
961
+
962
+ const filteredLines = lines.filter((line) => {
963
+ const trimmed = line.trim();
964
+ const isRoute = trimmed.startsWith('Route.') || trimmed.includes('Route.apiResource(');
965
+ if (isRoute) {
966
+ for (const ep of endpoints) {
967
+ if (line.includes(`'${ep}'`) || line.includes(`"${ep}"`)) {
968
+ return false;
969
+ }
970
+ }
971
+ if (line.includes(controllerName)) {
972
+ return false;
973
+ }
974
+ }
975
+ return true;
976
+ });
977
+
978
+ const controllerUsedElsewhere = new RegExp(`\\b${controllerName}\\b`).test(
979
+ filteredLines.filter((l) => !l.includes('require(') && !l.includes('import ')).join('\n')
980
+ );
981
+
982
+ let finalLines = filteredLines;
983
+ if (!controllerUsedElsewhere) {
984
+ finalLines = filteredLines.filter((line) => {
985
+ const trimmed = line.trim();
986
+ if ((trimmed.startsWith('const ') || trimmed.startsWith('let ') || trimmed.startsWith('var ') || trimmed.startsWith('import ')) &&
987
+ line.includes(controllerName)) {
988
+ return false;
989
+ }
990
+ return true;
991
+ });
992
+ }
993
+
994
+ const newText = finalLines.join('\n').replace(/\n{3,}/g, '\n\n');
995
+ if (newText !== origText) {
996
+ const epStr = Array.from(endpoints)[0];
997
+ const desc = `Route.apiResource('${epStr}', ${controllerName})`;
998
+ removals.push({
999
+ file: f,
1000
+ desc,
1001
+ apply: () => fs.writeFileSync(f, newText)
1002
+ });
1003
+ }
1004
+ }
1005
+
1006
+ return removals;
1007
+ }
1008
+
1009
+ function removeWebRoute(ctx, name, dirs, { route, dryRun = false }) {
1010
+ const routeName = route ? String(route).replace(/\.(kreftjs|kreft|som|js)$/, '').replace(/^[\\/]+/, '') : 'web';
1011
+ const routeBaseName = routeName.split(/[\\/]/).pop();
1012
+ const fileTargetName = routeBaseName === 'api' ? 'web' : routeBaseName;
1013
+ const routeFiles = [
1014
+ path.join(ctx.frontend, 'routes', fileTargetName + '.kreftjs'),
1015
+ path.join(ctx.project, 'frontend', 'routes', fileTargetName + '.kreftjs'),
1016
+ path.join(ctx.frontend, 'routes', 'web.kreftjs'),
1017
+ path.join(ctx.project, 'frontend', 'routes', 'web.kreftjs'),
1018
+ path.join(ctx.project, 'routes', fileTargetName + '.kreftjs'),
1019
+ path.join(ctx.project, 'routes', 'web.kreftjs')
1020
+ ];
1021
+
1022
+ const seenFiles = new Set();
1023
+ const removals = [];
1024
+
1025
+ const ep1 = '/' + [...(dirs.length ? dirs.map((d) => snake(d).replace(/_/g, '-')) : []), plural(snake(name)).replace(/_/g, '-')].join('/');
1026
+ const ep2 = '/' + plural(snake(name)).replace(/_/g, '-');
1027
+ const pageRel1 = `pages/${[...dirs, name].join('/')}.kreftjs`;
1028
+ const pageRel2 = `pages/${name}.kreftjs`;
1029
+ const routeNameTag1 = [...dirs.map(snake), plural(snake(name)).replace(/_/g, '-')].join('.');
1030
+ const routeNameTag2 = plural(snake(name)).replace(/_/g, '-');
1031
+
1032
+ const matchTargets = [ep1, ep2, ep1.replace(/^\//, ''), ep2.replace(/^\//, ''), pageRel1, pageRel2, routeNameTag1, routeNameTag2];
1033
+
1034
+ for (const f of routeFiles) {
1035
+ if (!f || seenFiles.has(f) || !fs.existsSync(f)) continue;
1036
+ seenFiles.add(f);
1037
+
1038
+ const origText = fs.readFileSync(f, 'utf8');
1039
+ const lines = origText.split('\n');
1040
+
1041
+ const filteredLines = lines.filter((line) => {
1042
+ if (!line.includes('Route.view(')) return true;
1043
+ for (const target of matchTargets) {
1044
+ if (line.includes(`'${target}'`) || line.includes(`"${target}"`)) {
1045
+ return false;
1046
+ }
1047
+ }
1048
+ return true;
1049
+ });
1050
+
1051
+ const newText = filteredLines.join('\n').replace(/\n{3,}/g, '\n\n');
1052
+ if (newText !== origText) {
1053
+ const desc = `Route.view('${ep1}', '${pageRel1}')`;
1054
+ removals.push({
1055
+ file: f,
1056
+ desc,
1057
+ apply: () => fs.writeFileSync(f, newText)
1058
+ });
1059
+ }
1060
+ }
1061
+
1062
+ return removals;
1063
+ }
1064
+
1065
+ function removeSidebarLink(ctx, name, dirs, { dryRun = false }) {
1066
+ const layoutFiles = [
1067
+ path.join(ctx.frontend, 'layouts', 'app.kreftjs'),
1068
+ path.join(ctx.project, 'frontend', 'layouts', 'app.kreftjs'),
1069
+ path.join(ctx.project, 'layouts', 'app.kreftjs')
1070
+ ];
1071
+
1072
+ const seenFiles = new Set();
1073
+ const removals = [];
1074
+
1075
+ const routeNameTag1 = [...dirs.map(snake), plural(snake(name)).replace(/_/g, '-')].join('.');
1076
+ const routeNameTag2 = plural(snake(name)).replace(/_/g, '-');
1077
+ const ep1 = '/' + [...(dirs.length ? dirs.map((d) => snake(d).replace(/_/g, '-')) : []), plural(snake(name)).replace(/_/g, '-')].join('/');
1078
+ const ep2 = '/' + plural(snake(name)).replace(/_/g, '-');
1079
+
1080
+ const matchTags = [routeNameTag1, routeNameTag2, ep1, ep2];
1081
+
1082
+ for (const f of layoutFiles) {
1083
+ if (!f || seenFiles.has(f) || !fs.existsSync(f)) continue;
1084
+ seenFiles.add(f);
1085
+
1086
+ const origText = fs.readFileSync(f, 'utf8');
1087
+ const lines = origText.split('\n');
1088
+
1089
+ const filteredLines = lines.filter((line) => {
1090
+ if (!line.includes('to:')) return true;
1091
+ for (const tag of matchTags) {
1092
+ if (line.includes(`'${tag}'`) || line.includes(`"${tag}"`)) {
1093
+ return false;
1094
+ }
1095
+ }
1096
+ return true;
1097
+ });
1098
+
1099
+ const newText = filteredLines.join('\n');
1100
+ if (newText !== origText) {
1101
+ removals.push({
1102
+ file: f,
1103
+ desc: `Sidebar: { to: '${routeNameTag1}' }`,
1104
+ apply: () => fs.writeFileSync(f, newText)
1105
+ });
1106
+ }
1107
+ }
1108
+
1109
+ return removals;
1110
+ }
1111
+
1112
+ const pass = (o) => [
1113
+ ...(o.backend ? ['--backend', o.backend] : []),
1114
+ ...(o.dir ? ['--dir', o.dir] : []),
1115
+ ...(o.route ? ['--route', o.route] : []),
1116
+ ...(o.flags.has('auth') ? ['--auth'] : []),
1117
+ ];
1118
+
1119
+ function introspectModelRules(ctx, modelName, requestName = '') {
1120
+ if (!modelName) return null;
1121
+ const { dirs, name } = nested(modelName);
1122
+ const candidates = [
1123
+ ctx && ctx.base ? path.join(ctx.base, 'app', 'Models', ...dirs, name + '.js') : null,
1124
+ ctx && ctx.base ? path.join(ctx.base, 'app', 'Models', ...dirs, name + '.ts') : null,
1125
+ ctx && ctx.base ? path.join(ctx.base, 'app', ...dirs, name + '.js') : null,
1126
+ ctx && ctx.base ? path.join(ctx.base, 'app', ...dirs, name + '.ts') : null,
1127
+ path.join(process.cwd(), 'app', 'Models', ...dirs, name + '.js'),
1128
+ path.join(process.cwd(), 'app', 'Models', ...dirs, name + '.ts'),
1129
+ path.join(process.cwd(), 'demo', 'app', 'Models', ...dirs, name + '.js'),
1130
+ ].filter(Boolean);
1131
+ let modelFile = candidates.find((f) => fs.existsSync(f));
1132
+ if (!modelFile) return null;
1133
+
1134
+ try {
1135
+ const text = fs.readFileSync(modelFile, 'utf8');
1136
+ const fillableMatch = text.match(/fillable\s*=\s*\[([\s\S]*?)\]/);
1137
+ if (!fillableMatch) return null;
1138
+ const rawItems = fillableMatch[1].match(/['"`]([\w-]+)['"`]/g);
1139
+ if (!rawItems || !rawItems.length) return null;
1140
+ const fields = rawItems.map((s) => s.replace(/['"`]/g, ''));
1141
+
1142
+ const casts = {};
1143
+ const castsMatch = text.match(/casts\s*=\s*\{([\s\S]*?)\}/);
1144
+ if (castsMatch) {
1145
+ const castPairs = castsMatch[1].matchAll(/['"`]?(\w+)['"`]?\s*:\s*['"`](\w+)['"`]/g);
1146
+ for (const m of castPairs) casts[m[1]] = m[2].toLowerCase();
1147
+ }
1148
+
1149
+ const isUpdate = /Update/i.test(requestName);
1150
+ const prefix = isUpdate ? 'sometimes|' : 'required|';
1151
+ const rules = {};
1152
+
1153
+ for (const field of fields) {
1154
+ const cast = casts[field] || '';
1155
+ if (cast === 'boolean' || cast === 'bool') {
1156
+ rules[field] = `${prefix}boolean`;
1157
+ } else if (cast === 'integer' || cast === 'int') {
1158
+ rules[field] = `${prefix}integer`;
1159
+ } else if (cast === 'float' || cast === 'decimal' || cast === 'real' || cast === 'double') {
1160
+ rules[field] = `${prefix}numeric`;
1161
+ } else if (cast === 'json' || cast === 'array') {
1162
+ rules[field] = `${prefix}array`;
1163
+ } else if (cast === 'date' || cast === 'datetime') {
1164
+ rules[field] = `${prefix}date`;
1165
+ } else if (/email/i.test(field)) {
1166
+ rules[field] = `${prefix}string|email|max:255`;
1167
+ } else if (/password/i.test(field)) {
1168
+ rules[field] = `${prefix}string|min:8`;
1169
+ } else if (/url/i.test(field)) {
1170
+ rules[field] = `${prefix}url`;
1171
+ } else if (/phone|mobile/i.test(field)) {
1172
+ rules[field] = `${prefix}string|max:30`;
1173
+ } else if (/body|description|content|notes|text/i.test(field)) {
1174
+ rules[field] = `${prefix}string`;
1175
+ } else {
1176
+ rules[field] = `${prefix}string|max:255`;
1177
+ }
1178
+ }
1179
+ return rules;
1180
+ } catch (_) {
1181
+ return null;
1182
+ }
1183
+ }
1184
+
1185
+ const make = {
1186
+ model(args) {
1187
+ const o = options(args), ctx = context(o);
1188
+ let { dirs, name, rel } = nested(o._[0]);
1189
+ if (!name) return console.error(' kreftjs make:model Post [-m -f -s -c -p -r] [--route=file] [--auth] [--all] [--folder=Admin] (Shop/Product: app/Models/Shop/Product.js)');
1190
+ const all = o.flags.has('a') || o.flags.has('all');
1191
+ const auth = o.flags.has('auth');
1192
+ const hasRoute = all || o.flags.has('r') || o.flags.has('route') || Boolean(o.route);
1193
+
1194
+ // Support placing all auxiliary files (controllers, pages, routes) in a specific folder [except model]:
1195
+ // e.g. kreftjs make:model Post --all --folder=Admin
1196
+ // kreftjs make:model Post --all Admin
1197
+ // kreftjs make:model Admin/Post --all --flat-model
1198
+ const targetFolder = o.folder || o.subfolder || o['in'] || o.group || o.path || (o._[1] && !o._[1].startsWith('-') ? o._[1] : null);
1199
+ const flatModel = o.flags.has('flat-model') || o.flags.has('except-model') || o.flags.has('no-model-dir') || Boolean(targetFolder);
1200
+
1201
+ const modelDirs = flatModel ? [] : dirs;
1202
+ const modelRel = [...modelDirs, name].join('/');
1203
+ const otherDirs = targetFolder ? targetFolder.split(/[\\/]/).filter(Boolean) : dirs;
1204
+
1205
+ write(path.join(ctx.base, 'app', 'Models', ...modelDirs, name + '.js'), stubs.model(name));
1206
+ if (all || o.flags.has('m') || o.flags.has('migration')) make.migration([`create_${plural(snake(name))}_table`, ...pass(o)]);
1207
+ if (all || o.flags.has('f') || o.flags.has('factory')) make.factory([`${name}Factory`, '--model', modelRel, ...pass(o)]);
1208
+ if (all || o.flags.has('s') || o.flags.has('seed') || o.flags.has('seeder')) make.seeder([`${name}Seeder`, '--model', modelRel, ...pass(o)]);
1209
+ if (all || o.flags.has('req') || o.flags.has('request') || o.flags.has('requests')) {
1210
+ make.request([[...otherDirs, `Store${name}Request`].join('/'), '--model', modelRel, ...pass(o)]);
1211
+ make.request([[...otherDirs, `Update${name}Request`].join('/'), '--model', modelRel, ...pass(o)]);
1212
+ }
1213
+ if (all || o.flags.has('c') || o.flags.has('controller') || o.flags.has('api')) {
1214
+ make.controller([[...otherDirs, `${name}Controller`].join('/'), '--model', modelRel, ...(hasRoute ? ['-r'] : []), ...pass(o)]);
1215
+ } else if (hasRoute) {
1216
+ addApiRoute(ctx, name, otherDirs, { route: o.route, auth });
1217
+ }
1218
+ if (all || o.flags.has('p') || o.flags.has('page')) {
1219
+ make.page([[...otherDirs, name].join('/'), '--model', modelRel, ...(hasRoute ? ['-r'] : []), ...pass(o)]);
1220
+ }
1221
+ },
1222
+ controller(args) {
1223
+ const o = options(args), ctx = context(o);
1224
+ let { dirs, name } = nested(o._[0]);
1225
+ if (!name) return console.error(' kreftjs make:controller PostController [--api] [--model Post] [--route=file] [--auth]');
1226
+ if (!/Controller$/.test(name)) name += 'Controller';
1227
+ const m = o.model ? nested(o.model) : o.flags.has('api') || o.flags.has('resource') ? { dirs: [], name: name.replace(/Controller$/, '') } : null;
1228
+ const model = m ? m.name : null;
1229
+ const modelReq = m ? up(2 + dirs.length) + ['Models', ...m.dirs, m.name].join('/') : null;
1230
+ const rel = [...dirs, name].join('/');
1231
+ const auth = o.flags.has('auth');
1232
+ const hasRoute = o.flags.has('r') || o.flags.has('route') || Boolean(o.route);
1233
+
1234
+ if (write(path.join(ctx.base, 'app', 'Http', 'Controllers', ...dirs, name + '.js'), stubs.controller(name, model, modelReq))) {
1235
+ if (hasRoute && model) {
1236
+ addApiRoute(ctx, model, dirs, { route: o.route, auth });
1237
+ } else {
1238
+ const isTs = isTsProject(path.join(ctx.base, 'app'));
1239
+ const routeExt = isTs ? 'routes/api.ts' : 'routes/api.js';
1240
+ const importHint = isTs ? `import ${name} from '../app/Http/Controllers/${rel}'` : `const ${name} = require('../app/Http/Controllers/${rel}')`;
1241
+ const resHint = model ? (isTs
1242
+ ? `Route.apiResource('${plural(snake(model)).replace(/_/g, '-')}', ${name})${auth ? ".middleware('auth')" : ''}`
1243
+ : `Route.apiResource('${plural(snake(model)).replace(/_/g, '-')}', require('../app/Http/Controllers/${rel}'))${auth ? ".middleware('auth')" : ''}`) : '';
1244
+ console.log(dim(model
1245
+ ? ` Add to ${routeExt}: ${isTs ? importHint + '\n ' : ''}${resHint}`
1246
+ : ` In ${routeExt}: ${importHint}`));
1247
+ }
1248
+ }
1249
+ },
1250
+ page(args) {
1251
+ const o = options(args), ctx = context(o);
1252
+ let { dirs, name } = nested(o._[0]);
1253
+ if (!name) return console.error(' kreftjs make:page Post [--model Post] [--route=file] [--auth]');
1254
+ const m = o.model ? nested(o.model) : { dirs, name };
1255
+ const modelDirs = m.dirs && m.dirs.length ? m.dirs : dirs;
1256
+ const endpoint = [...(modelDirs.length ? modelDirs.map((d) => snake(d).replace(/_/g, '-')) : []), plural(snake(m.name)).replace(/_/g, '-')].join('/');
1257
+ const auth = o.flags.has('auth');
1258
+ const hasRoute = o.flags.has('r') || o.flags.has('route') || Boolean(o.route);
1259
+
1260
+ const pageFile = path.join(ctx.frontend, 'pages', ...dirs, name + '.kreftjs');
1261
+ const isTs = isTsProject(pageFile);
1262
+ if (write(pageFile, stubs.page(name, endpoint, dirs, isTs))) {
1263
+ if (hasRoute) {
1264
+ addWebRoute(ctx, name, dirs, { route: o.route, auth });
1265
+ } else {
1266
+ console.log(dim(` Add to frontend/routes/web.kreftjs: Route.view('/${endpoint}', 'pages/${[...dirs, name].join('/')}.kreftjs')${auth ? ".middleware('auth')" : ''}`));
1267
+ }
1268
+ }
1269
+ },
1270
+ factory(args) {
1271
+ const o = options(args), ctx = context(o);
1272
+ let { dirs, name } = nested(o._[0]);
1273
+ if (!name) return console.error(' kreftjs make:factory PostFactory [--model Post]');
1274
+ if (!name.endsWith('Factory')) name += 'Factory';
1275
+ const m = o.model ? nested(o.model) : { dirs: [], name: name.replace(/Factory$/, '') };
1276
+ const modelReq = '../../app/Models/' + [...m.dirs, m.name].join('/');
1277
+ write(path.join(ctx.base, 'database', 'factories', ...dirs, name + '.js'), stubs.factory(name, m.name, modelReq));
1278
+ },
1279
+ migration(args) {
1280
+ const o = options(args), ctx = context(o);
1281
+ const name = snake(o._[0] || '').replace(/[^a-z0-9_]/g, '_');
1282
+ if (!name) return console.error(' kreftjs make:migration create_posts_table');
1283
+ const dir = migrationsOf(o, ctx);
1284
+ write(path.join(dir, `${nextStamp(dir)}_${name}.js`), stubs.migration(name));
1285
+ },
1286
+ seeder(args) {
1287
+ const o = options(args), ctx = context(o);
1288
+ let { dirs, name } = nested(o._[0]);
1289
+ if (!name) return console.error(' kreftjs make:seeder PostSeeder');
1290
+ if (!/Seeder$/.test(name)) name += 'Seeder';
1291
+ const m = o.model ? nested(o.model) : null;
1292
+ const modelReq = m ? up(2 + dirs.length) + ['app', 'Models', ...m.dirs, m.name].join('/') : null;
1293
+ write(path.join(ctx.base, 'database', 'seeders', ...dirs, name + '.js'), stubs.seeder(name, m ? m.name : null, modelReq));
1294
+ },
1295
+ middleware(args) {
1296
+ const o = options(args), ctx = context(o);
1297
+ const { dirs, name } = nested(o._[0]);
1298
+ if (!name) return console.error(' kreftjs make:middleware EnsureIsAdmin');
1299
+ write(path.join(ctx.base, 'app', 'Http', 'Middleware', ...dirs, name + '.js'), stubs.middleware(name));
1300
+ if (dirs.length) console.log(dim(` In a folder, give it a name in app/Http/Kernel.js: aliases: { '${snake(name).replace(/_/g, '-')}': 'Middleware/${[...dirs, name].join('/')}.js' }`));
1301
+ },
1302
+ request(args) {
1303
+ const o = options(args), ctx = context(o);
1304
+ let { dirs, name } = nested(o._[0]);
1305
+ if (!name) return console.error(' kreftjs make:request StorePostRequest [--model Post]');
1306
+ if (!/Request$/.test(name)) name += 'Request';
1307
+ const reqDir = path.join(ctx.base, 'app', 'Http', 'Requests');
1308
+ if (!fs.existsSync(reqDir)) fs.mkdirSync(reqDir, { recursive: true });
1309
+ const isTs = isTsProject(path.join(reqDir, ...dirs, name + '.js'));
1310
+ const ext = isTs ? '.ts' : '.js';
1311
+ const filePath = path.join(reqDir, ...dirs, name + ext);
1312
+ const rulesObj = introspectModelRules(ctx, o.model, name);
1313
+ if (write(filePath, stubs.request(name, rulesObj, isTs))) {
1314
+ console.log(dim(` In controller: const data = await ${name}.validate(req)`));
1315
+ }
1316
+ },
1317
+ resource(args) {
1318
+ const o = options(args), ctx = context(o);
1319
+ let { dirs, name } = nested(o._[0]);
1320
+ if (!name) return console.error(' kreftjs make:resource PostResource');
1321
+ if (!/Resource$/.test(name)) name += 'Resource';
1322
+ const resDir = path.join(ctx.base, 'app', 'Http', 'Resources');
1323
+ if (!fs.existsSync(resDir)) fs.mkdirSync(resDir, { recursive: true });
1324
+ const isTs = isTsProject(path.join(resDir, ...dirs, name + '.js'));
1325
+ const ext = isTs ? '.ts' : '.js';
1326
+ const filePath = path.join(resDir, ...dirs, name + ext);
1327
+ if (write(filePath, stubs.resource(name, isTs))) {
1328
+ console.log(dim(` In controller: return new ${name}(item)`));
1329
+ }
1330
+ },
1331
+ rule(args) {
1332
+ const o = options(args), ctx = context(o);
1333
+ let { dirs, name } = nested(o._[0]);
1334
+ if (!name) return console.error(' kreftjs make:rule Uppercase');
1335
+ const ruleDir = path.join(ctx.base, 'app', 'Rules');
1336
+ if (!fs.existsSync(ruleDir)) fs.mkdirSync(ruleDir, { recursive: true });
1337
+ const isTs = isTsProject(path.join(ruleDir, ...dirs, name + '.js'));
1338
+ const ext = isTs ? '.ts' : '.js';
1339
+ const filePath = path.join(ruleDir, ...dirs, name + ext);
1340
+ if (write(filePath, stubs.rule(name, isTs))) {
1341
+ console.log(dim(` In rules: { field: ['required', new ${name}()] }`));
1342
+ }
1343
+ },
1344
+ lang(args) {
1345
+ const o = options(args), ctx = context(o);
1346
+ const locale = (o._[0] || '').toLowerCase().trim();
1347
+ if (!locale || /[\/\\]|\.\./.test(locale)) {
1348
+ console.error(' kreftjs make:lang <locale> (e.g. kreftjs make:lang ar)');
1349
+ return;
1350
+ }
1351
+ // Place in project root lang/ (matching standard Laravel 9/10/11) or backend/lang/ if already existing
1352
+ const rootLang = path.join(ctx.project || ctx.base, 'lang');
1353
+ const backendLang = path.join(ctx.base, 'lang');
1354
+ const targetBase = fs.existsSync(backendLang) && !fs.existsSync(rootLang) ? backendLang : rootLang;
1355
+ const langDir = path.join(targetBase, locale);
1356
+ if (!fs.existsSync(langDir)) fs.mkdirSync(langDir, { recursive: true });
1357
+
1358
+ const isAr = locale === 'ar';
1359
+
1360
+ // 1. auth.json
1361
+ const authPath = path.join(langDir, 'auth.json');
1362
+ if (!fs.existsSync(authPath)) {
1363
+ const authData = isAr ? {
1364
+ failed: 'بيانات الاعتماد هذه غير متطابقة مع سجلاتنا.',
1365
+ password: 'كلمة المرور المدخلة غير صحيحة.',
1366
+ throttle: 'محاولات تسجيل دخول كثيرة جداً. يرجى المحاولة بعد :seconds ثانية.'
1367
+ } : {
1368
+ failed: 'These credentials do not match our records.',
1369
+ password: 'The provided password is incorrect.',
1370
+ throttle: 'Too many login attempts. Please try again in :seconds seconds.'
1371
+ };
1372
+ write(authPath, JSON.stringify(authData, null, 2) + '\n');
1373
+ }
1374
+
1375
+ // 2. pagination.json
1376
+ const pagPath = path.join(langDir, 'pagination.json');
1377
+ if (!fs.existsSync(pagPath)) {
1378
+ const pagData = isAr ? {
1379
+ previous: '&laquo; السابق',
1380
+ next: 'التالي &raquo;'
1381
+ } : {
1382
+ previous: '&laquo; Previous',
1383
+ next: 'Next &raquo;'
1384
+ };
1385
+ write(pagPath, JSON.stringify(pagData, null, 2) + '\n');
1386
+ }
1387
+
1388
+ // 3. passwords.json
1389
+ const passPath = path.join(langDir, 'passwords.json');
1390
+ if (!fs.existsSync(passPath)) {
1391
+ const passData = isAr ? {
1392
+ reset: 'تمت إعادة تعيين كلمة المرور بنجاح.',
1393
+ sent: 'تم إرسال رابط تعيين كلمة المرور إلى بريدك الإلكتروني.',
1394
+ throttled: 'يرجى الانتظار قبل إعادة المحاولة.',
1395
+ token: 'رمز إعادة تعيين كلمة المرور هذا غير صالح.',
1396
+ user: 'لا يمكننا العثور على مستخدم بعنوان البريد الإلكتروني هذا.'
1397
+ } : {
1398
+ reset: 'Your password has been reset.',
1399
+ sent: 'We have emailed your password reset link.',
1400
+ throttled: 'Please wait before retrying.',
1401
+ token: 'This password reset token is invalid.',
1402
+ user: "We can't find a user with that email address."
1403
+ };
1404
+ write(passPath, JSON.stringify(passData, null, 2) + '\n');
1405
+ }
1406
+
1407
+ // 4. messages.json
1408
+ const msgPath = path.join(langDir, 'messages.json');
1409
+ if (!fs.existsSync(msgPath)) {
1410
+ const msgData = isAr ? {
1411
+ welcome: 'أهلاً بك في Kreft',
1412
+ hello: 'مرحباً، :name',
1413
+ items: '{0} لا توجد عناصر|{1} عنصر واحد|{2} عنصران|[3,10] :count عناصر|[11,*] :count عنصراً'
1414
+ } : {
1415
+ welcome: 'Welcome to Kreft',
1416
+ hello: 'Hello, :name',
1417
+ items: '{0} No items|{1} One item|[2,*] :count items'
1418
+ };
1419
+ write(msgPath, JSON.stringify(msgData, null, 2) + '\n');
1420
+ }
1421
+
1422
+ // 5. validation.json
1423
+ const valPath = path.join(langDir, 'validation.json');
1424
+ if (!fs.existsSync(valPath)) {
1425
+ const valData = isAr ? {
1426
+ required: 'حقل :attribute مطلوب.',
1427
+ email: 'يجب أن يكون :attribute عنوان بريد إلكتروني صالح.',
1428
+ min: {
1429
+ string: 'يجب أن يحتوي :attribute على :min أحرف على الأقل.',
1430
+ numeric: 'يجب أن تكون قيمة :attribute على الأقل :min.',
1431
+ array: 'يجب أن يحتوي :attribute على الأقل على :min عناصر.'
1432
+ },
1433
+ max: {
1434
+ string: 'يجب ألا يتجاوز :attribute :max أحرف.',
1435
+ numeric: 'يجب ألا تتجاوز قيمة :attribute :max.',
1436
+ array: 'يجب ألا يحتوي :attribute على أكثر من :max عناصر.'
1437
+ },
1438
+ confirmed: 'تأكيد :attribute غير متطابق.',
1439
+ attributes: {
1440
+ email: 'البريد الإلكتروني',
1441
+ password: 'كلمة المرور',
1442
+ name: 'الاسم',
1443
+ title: 'العنوان'
1444
+ }
1445
+ } : {
1446
+ required: 'The :attribute field is required.',
1447
+ email: 'The :attribute field must be a valid email address.',
1448
+ min: {
1449
+ string: 'The :attribute field must be at least :min characters.',
1450
+ numeric: 'The :attribute field must be at least :min.',
1451
+ array: 'The :attribute field must have at least :min items.'
1452
+ },
1453
+ max: {
1454
+ string: 'The :attribute field must not be greater than :max characters.',
1455
+ numeric: 'The :attribute field must not be greater than :max.',
1456
+ array: 'The :attribute field must not have more than :max items.'
1457
+ },
1458
+ confirmed: 'The :attribute confirmation does not match.',
1459
+ attributes: {
1460
+ email: 'email address',
1461
+ password: 'password',
1462
+ name: 'name',
1463
+ title: 'title'
1464
+ }
1465
+ };
1466
+ write(valPath, JSON.stringify(valData, null, 2) + '\n');
1467
+ }
1468
+
1469
+ // 6. Root string translation: lang/<locale>.json
1470
+ const rootJsonPath = path.join(targetBase, `${locale}.json`);
1471
+ if (!fs.existsSync(rootJsonPath)) {
1472
+ const rootStrings = isAr ? {
1473
+ 'Sign In': 'تسجيل الدخول',
1474
+ 'Sign Up': 'إنشاء حساب جديد',
1475
+ 'Dashboard': 'لوحة التحكم',
1476
+ 'Profile': 'الملف الشخصي',
1477
+ 'Logout': 'تسجيل الخروج',
1478
+ 'Save': 'حفظ',
1479
+ 'Cancel': 'إلغاء'
1480
+ } : {
1481
+ 'Sign In': 'Sign In',
1482
+ 'Sign Up': 'Sign Up',
1483
+ 'Dashboard': 'Dashboard',
1484
+ 'Profile': 'Profile',
1485
+ 'Logout': 'Logout',
1486
+ 'Save': 'Save',
1487
+ 'Cancel': 'Cancel'
1488
+ };
1489
+ write(rootJsonPath, JSON.stringify(rootStrings, null, 2) + '\n');
1490
+ }
1491
+
1492
+ console.log(dim(` Laravel-compatible translations created in ${path.relative(process.cwd(), targetBase)}/${locale}/ & ${locale}.json`));
1493
+ console.log(dim(` Use in backend: __('auth.failed') or __('Sign In')`));
1494
+ console.log(dim(` Use in frontend: { __('auth.failed') } or { t('Sign In') }`));
1495
+ },
1496
+ };
1497
+ // kreftjs make:errors — the error pages (errors/404.kreftjs, 403, 500…)
1498
+ make.errors = function (args) {
1499
+ const o = options(args), ctx = context(o);
1500
+ const from = path.join(__dirname, '..', 'templates', 'errors');
1501
+ const to = path.resolve(o.dir || ctx.frontend, o._[0] || 'errors');
1502
+ for (const f of fs.readdirSync(from).sort()) write(path.join(to, f), fs.readFileSync(path.join(from, f), 'utf8'));
1503
+ // turn them on in the router, if index.html doesn't yet
1504
+ const index = path.join(o.dir ? path.resolve(o.dir) : ctx.frontend, 'index.html');
1505
+ if (fs.existsSync(index)) {
1506
+ const html = fs.readFileSync(index, 'utf8');
1507
+ if (!/errors\s*:/.test(html) && /(?:Kreftjs|Kreft|Som)\.router\(\{/.test(html)) {
1508
+ fs.writeFileSync(index, html.replace(/(?:Kreftjs|Kreft|Som)\.router\(\{/, (m) => `${m}\n errors: '${path.relative(path.dirname(index), to).split(path.sep).join('/')}', // error pages: errors/404.kreftjs, abort(403) → errors/403.kreftjs`));
1509
+ console.log(` ${green('UPDATED')} ${path.relative(process.cwd(), index) || 'index.html'} (errors: '${path.relative(path.dirname(index), to).split(path.sep).join('/')}' in Kreftjs.router)`);
1510
+ }
1511
+ }
1512
+ };
1513
+
1514
+ // kreftjs make:routes admin → frontend/routes/admin.kreftjs (pages), listed in Kreftjs.router({ routes: […] }) in index.html
1515
+ // kreftjs make:routes admin --api → backend/routes/admin.js (API), loaded by routes/api.js
1516
+ make.routes = function (args) {
1517
+ const o = options(args), ctx = context(o);
1518
+ const name = String(o._[0] || '').replace(/\.(kreftjs|kreft|som|js)$/, '').replace(/[^\w-]/g, '');
1519
+ if (!name) return console.error(' kreftjs make:routes admin (pages)\n kreftjs make:routes admin --api (API)');
1520
+ if (o.flags.has('api')) {
1521
+ const file = path.join(ctx.base, 'routes', name + '.js');
1522
+ if (!write(file, stubs.apiRoutes(name))) return;
1523
+ const api = require('./backend/typescript.js').findFile(path.join(ctx.base, 'routes', 'api')) || path.join(ctx.base, 'routes', 'api.js');
1524
+ if (fs.existsSync(api) && name !== 'api') {
1525
+ const text = fs.readFileSync(api, 'utf8');
1526
+ const isTs = api.endsWith('.ts');
1527
+ if (!text.includes(`require('./${name}')`) && !text.includes(`import './${name}'`)) {
1528
+ const updated = isTs
1529
+ ? text.replace(/(import \{[^}]*Route[^}]*\} from ['"](?:kreftjs|kreft|somjs|som)\/server['"][^\n]*\n)/, `$1\n// ${name} routes (routes/${name}.ts)\nimport './${name}'\n`)
1530
+ : text.replace(/(const \{ Route \} = require\('(?:kreftjs|kreft|somjs|som)\/server'\)\n)/, `$1\n// ${name} routes (routes/${name}.js)\nrequire('./${name}')\n`);
1531
+ fs.writeFileSync(api, updated !== text ? updated : text + `\n${isTs ? `import './${name}'` : `require('./${name}')`}\n`);
1532
+ console.log(` ${green('UPDATED')} ${path.relative(process.cwd(), api)} (${isTs ? `import './${name}'` : `require('./${name}')`})`);
1533
+ }
1534
+ }
1535
+ return;
1536
+ }
1537
+ const file = path.join(ctx.frontend, 'routes', name + '.kreftjs');
1538
+ if (!write(file, stubs.webRoutes(name))) return;
1539
+ const index = path.join(ctx.frontend, 'index.html');
1540
+ if (!fs.existsSync(index)) return;
1541
+ const html = fs.readFileSync(index, 'utf8');
1542
+ const entry = `'routes/${name}.kreftjs'`;
1543
+ if (html.includes(entry)) return;
1544
+ let next = html;
1545
+ if (/routes:\s*\[\s*'routes\//.test(html)) next = html.replace(/(routes:\s*\[[^\]]*?)(\s*\])/, (_, list, end) => `${list.replace(/,\s*$/, '')}, ${entry}${end}`);
1546
+ else if (/routes:\s*'routes\/[^']+'/.test(html)) next = html.replace(/routes:\s*('routes\/[^']+')/, (_, first) => `routes: [${first}, ${entry}]`);
1547
+ if (next === html) return console.log(` Add ${entry} to the routes in Kreftjs.router({ routes: [...] }) in index.html.`);
1548
+ fs.writeFileSync(index, next);
1549
+ console.log(` ${green('UPDATED')} ${path.relative(process.cwd(), index)} (${entry} in Kreftjs.router)`);
1550
+ };
1551
+
1552
+ // kreftjs make:admin you@example.com → that user may open the admin panel (/admin)
1553
+ // kreftjs make:admin you@example.com --remove
1554
+ commands['make:admin'] = async function (args) {
1555
+ const o = options(args), ctx = context(o);
1556
+ const email = String(o._[0] || '').trim().toLowerCase(); // Register keeps emails in lowercase
1557
+ if (!email) return console.error(' kreftjs make:admin you@example.com (--remove takes the rights away)');
1558
+ await withDb(o, ctx, async (db) => {
1559
+ const value = o.flags.has('remove') ? 0 : 1;
1560
+ const r = await db.run(`UPDATE ${db.q('users')} SET ${db.q('is_admin')} = ? WHERE LOWER(${db.q('email')}) = ?`, [db.kind === 'postgres' ? Boolean(value) : value, email]);
1561
+ if (r.changes) {
1562
+ console.log(` ${green('DONE')} ${email} ${value ? 'is an admin now: open /admin (reload it if it is open)' : 'is not an admin any more'}`);
1563
+ return;
1564
+ }
1565
+ const rows = await db.query(`SELECT ${db.q('email')} FROM ${db.q('users')} ORDER BY ${db.q('id')} LIMIT 20`);
1566
+ console.error(` No user with the email ${email}.${rows.length ? ' The users are:\n' + rows.map((x) => ' ' + x.email).join('\n') : ' There are no users yet: register first.'}`);
1567
+ process.exitCode = 1;
1568
+ });
1569
+ };
1570
+
1571
+ make.electron = function (args) {
1572
+ return require('./electron.js').init(args);
1573
+ };
1574
+
1575
+ make.vite = function (args) {
1576
+ return require('./vite.js').init(args);
1577
+ };
1578
+
1579
+ Object.entries(make).forEach(([k, fn]) => { commands['make:' + k] = fn; });
1580
+
1581
+ async function removeModel(args) {
1582
+ const o = options(args);
1583
+ const ctx = context(o);
1584
+
1585
+ let rawPositional = [...o._];
1586
+ if (rawPositional[0] && rawPositional[0].toLowerCase() === 'model') {
1587
+ rawPositional.shift();
1588
+ }
1589
+
1590
+ const ARTIFACT_KEYWORDS = new Set([
1591
+ 'all',
1592
+ 'controller', 'controllers', 'c',
1593
+ 'request', 'requests', 'req',
1594
+ 'route', 'routes', 'r',
1595
+ 'migration', 'migrations', 'm',
1596
+ 'seeder', 'seeders', 'seed', 's',
1597
+ 'factory', 'factories', 'f',
1598
+ 'page', 'pages', 'p',
1599
+ 'model', 'models'
1600
+ ]);
1601
+
1602
+ const positionalTokens = new Set();
1603
+ let targetModelName = o.model || null;
1604
+ let positionalFolder = null;
1605
+
1606
+ for (const token of rawPositional) {
1607
+ const lower = token.toLowerCase();
1608
+ if (ARTIFACT_KEYWORDS.has(lower)) {
1609
+ positionalTokens.add(lower);
1610
+ } else if (!targetModelName) {
1611
+ targetModelName = token;
1612
+ } else if (!positionalFolder && !o.folder) {
1613
+ positionalFolder = token;
1614
+ }
1615
+ }
1616
+
1617
+ if (!targetModelName) {
1618
+ console.error(' Usage: kreftjs remove:model <Name> [--all] [--controller] [--request] [--route] [--migration] [--seeder] [--factory] [--page] [--folder=Folder]');
1619
+ console.error(' kreftjs remove model <Name> [options]');
1620
+ console.error(' kreft:remove model <Name> [options]');
1621
+ return;
1622
+ }
1623
+
1624
+ const { dirs, name } = nested(targetModelName);
1625
+ const targetFolder = o.folder || o.subfolder || o['in'] || o.group || o.path || positionalFolder;
1626
+ const otherDirs = targetFolder ? targetFolder.split(/[\\/]/).filter(Boolean) : dirs;
1627
+ const flatModel = o.flags.has('flat-model') || o.flags.has('except-model') || o.flags.has('no-model-dir') || Boolean(targetFolder);
1628
+ const modelDirs = flatModel ? [] : dirs;
1629
+
1630
+ const wantsAll = o.flags.has('a') || o.flags.has('all') || positionalTokens.has('all');
1631
+ const wantsController = wantsAll || o.flags.has('c') || o.flags.has('controller') || o.flags.has('controllers') || positionalTokens.has('controller') || positionalTokens.has('controllers') || positionalTokens.has('c');
1632
+ const wantsRequest = wantsAll || o.flags.has('req') || o.flags.has('request') || o.flags.has('requests') || positionalTokens.has('request') || positionalTokens.has('requests') || positionalTokens.has('req');
1633
+ const wantsRoute = wantsAll || o.flags.has('r') || o.flags.has('route') || o.flags.has('routes') || positionalTokens.has('route') || positionalTokens.has('routes') || positionalTokens.has('r');
1634
+ const wantsMigration = wantsAll || o.flags.has('m') || o.flags.has('migration') || o.flags.has('migrations') || positionalTokens.has('migration') || positionalTokens.has('migrations') || positionalTokens.has('m');
1635
+ const wantsSeeder = wantsAll || o.flags.has('s') || o.flags.has('seed') || o.flags.has('seeder') || o.flags.has('seeders') || positionalTokens.has('seeder') || positionalTokens.has('seeders') || positionalTokens.has('seed') || positionalTokens.has('s');
1636
+ const wantsFactory = wantsAll || o.flags.has('f') || o.flags.has('factory') || o.flags.has('factories') || positionalTokens.has('factory') || positionalTokens.has('factories') || positionalTokens.has('f');
1637
+ const wantsPage = wantsAll || o.flags.has('p') || o.flags.has('page') || o.flags.has('pages') || positionalTokens.has('page') || positionalTokens.has('pages') || positionalTokens.has('p');
1638
+
1639
+ const anySpecific = wantsController || wantsRequest || wantsRoute || wantsMigration || wantsSeeder || wantsFactory || wantsPage;
1640
+ const wantsModel = wantsAll || o.flags.has('model') || positionalTokens.has('model') || positionalTokens.has('models') || !anySpecific;
1641
+
1642
+ const dryRun = o.flags.has('dry-run') || o.flags.has('d');
1643
+ const force = o.flags.has('force') || o.flags.has('f') || o.flags.has('yes') || o.flags.has('y');
1644
+
1645
+ // Collect candidate files to delete
1646
+ const filesToDelete = new Set();
1647
+ const checkFile = (p) => {
1648
+ if (p && fs.existsSync(p)) filesToDelete.add(p);
1649
+ };
1650
+
1651
+ // 1. Model
1652
+ if (wantsModel) {
1653
+ checkFile(path.join(ctx.base, 'app', 'Models', ...modelDirs, `${name}.js`));
1654
+ checkFile(path.join(ctx.base, 'app', 'Models', ...modelDirs, `${name}.ts`));
1655
+ if (dirs.length) {
1656
+ checkFile(path.join(ctx.base, 'app', 'Models', ...dirs, `${name}.js`));
1657
+ checkFile(path.join(ctx.base, 'app', 'Models', ...dirs, `${name}.ts`));
1658
+ checkFile(path.join(ctx.project, 'app', 'Models', ...dirs, `${name}.js`));
1659
+ checkFile(path.join(ctx.project, 'app', 'Models', ...dirs, `${name}.ts`));
1660
+ }
1661
+ checkFile(path.join(ctx.base, 'app', 'Models', `${name}.js`));
1662
+ checkFile(path.join(ctx.base, 'app', 'Models', `${name}.ts`));
1663
+ checkFile(path.join(ctx.project, 'app', 'Models', `${name}.js`));
1664
+ checkFile(path.join(ctx.project, 'app', 'Models', `${name}.ts`));
1665
+ }
1666
+
1667
+ // 2. Controller
1668
+ if (wantsController) {
1669
+ checkFile(path.join(ctx.base, 'app', 'Http', 'Controllers', ...otherDirs, `${name}Controller.js`));
1670
+ checkFile(path.join(ctx.base, 'app', 'Http', 'Controllers', ...otherDirs, `${name}Controller.ts`));
1671
+ if (dirs.length && dirs.join('/') !== otherDirs.join('/')) {
1672
+ checkFile(path.join(ctx.base, 'app', 'Http', 'Controllers', ...dirs, `${name}Controller.js`));
1673
+ checkFile(path.join(ctx.base, 'app', 'Http', 'Controllers', ...dirs, `${name}Controller.ts`));
1674
+ }
1675
+ checkFile(path.join(ctx.base, 'app', 'Http', 'Controllers', `${name}Controller.js`));
1676
+ checkFile(path.join(ctx.base, 'app', 'Http', 'Controllers', `${name}Controller.ts`));
1677
+ checkFile(path.join(ctx.project, 'app', 'Http', 'Controllers', ...otherDirs, `${name}Controller.js`));
1678
+ checkFile(path.join(ctx.project, 'app', 'Http', 'Controllers', ...dirs, `${name}Controller.js`));
1679
+ checkFile(path.join(ctx.project, 'app', 'Http', 'Controllers', `${name}Controller.js`));
1680
+ }
1681
+
1682
+ // 3. Requests
1683
+ if (wantsRequest) {
1684
+ for (const prefix of ['Store', 'Update']) {
1685
+ checkFile(path.join(ctx.base, 'app', 'Http', 'Requests', ...otherDirs, `${prefix}${name}Request.js`));
1686
+ checkFile(path.join(ctx.base, 'app', 'Http', 'Requests', ...otherDirs, `${prefix}${name}Request.ts`));
1687
+ if (dirs.length && dirs.join('/') !== otherDirs.join('/')) {
1688
+ checkFile(path.join(ctx.base, 'app', 'Http', 'Requests', ...dirs, `${prefix}${name}Request.js`));
1689
+ checkFile(path.join(ctx.base, 'app', 'Http', 'Requests', ...dirs, `${prefix}${name}Request.ts`));
1690
+ }
1691
+ checkFile(path.join(ctx.base, 'app', 'Http', 'Requests', `${prefix}${name}Request.js`));
1692
+ checkFile(path.join(ctx.base, 'app', 'Http', 'Requests', `${prefix}${name}Request.ts`));
1693
+ checkFile(path.join(ctx.project, 'app', 'Http', 'Requests', ...otherDirs, `${prefix}${name}Request.js`));
1694
+ checkFile(path.join(ctx.project, 'app', 'Http', 'Requests', ...dirs, `${prefix}${name}Request.js`));
1695
+ checkFile(path.join(ctx.project, 'app', 'Http', 'Requests', `${prefix}${name}Request.js`));
1696
+ }
1697
+ }
1698
+
1699
+ // 4. Factory
1700
+ if (wantsFactory) {
1701
+ checkFile(path.join(ctx.base, 'database', 'factories', `${name}Factory.js`));
1702
+ checkFile(path.join(ctx.base, 'database', 'factories', `${name}Factory.ts`));
1703
+ if (dirs.length) {
1704
+ checkFile(path.join(ctx.base, 'database', 'factories', ...dirs, `${name}Factory.js`));
1705
+ checkFile(path.join(ctx.base, 'database', 'factories', ...dirs, `${name}Factory.ts`));
1706
+ }
1707
+ checkFile(path.join(ctx.project, 'database', 'factories', `${name}Factory.js`));
1708
+ checkFile(path.join(ctx.project, 'database', 'factories', `${name}Factory.ts`));
1709
+ }
1710
+
1711
+ // 5. Seeder
1712
+ if (wantsSeeder) {
1713
+ checkFile(path.join(ctx.base, 'database', 'seeders', `${name}Seeder.js`));
1714
+ checkFile(path.join(ctx.base, 'database', 'seeders', `${name}Seeder.ts`));
1715
+ if (dirs.length) {
1716
+ checkFile(path.join(ctx.base, 'database', 'seeders', ...dirs, `${name}Seeder.js`));
1717
+ checkFile(path.join(ctx.base, 'database', 'seeders', ...dirs, `${name}Seeder.ts`));
1718
+ }
1719
+ checkFile(path.join(ctx.project, 'database', 'seeders', `${name}Seeder.js`));
1720
+ checkFile(path.join(ctx.project, 'database', 'seeders', `${name}Seeder.ts`));
1721
+ }
1722
+
1723
+ // 6. Page
1724
+ if (wantsPage) {
1725
+ checkFile(path.join(ctx.frontend, 'pages', ...otherDirs, `${name}.kreftjs`));
1726
+ if (dirs.length && dirs.join('/') !== otherDirs.join('/')) {
1727
+ checkFile(path.join(ctx.frontend, 'pages', ...dirs, `${name}.kreftjs`));
1728
+ }
1729
+ checkFile(path.join(ctx.frontend, 'pages', `${name}.kreftjs`));
1730
+ checkFile(path.join(ctx.project, 'frontend', 'pages', ...otherDirs, `${name}.kreftjs`));
1731
+ checkFile(path.join(ctx.project, 'frontend', 'pages', ...dirs, `${name}.kreftjs`));
1732
+ checkFile(path.join(ctx.project, 'frontend', 'pages', `${name}.kreftjs`));
1733
+ checkFile(path.join(ctx.project, 'pages', ...otherDirs, `${name}.kreftjs`));
1734
+ checkFile(path.join(ctx.project, 'pages', ...dirs, `${name}.kreftjs`));
1735
+ checkFile(path.join(ctx.project, 'pages', `${name}.kreftjs`));
1736
+ }
1737
+
1738
+ // 7. Migration
1739
+ if (wantsMigration) {
1740
+ const migrationDirs = new Set([
1741
+ migrationsOf(o, ctx),
1742
+ path.join(ctx.base, 'database', 'migrations'),
1743
+ path.join(ctx.project, 'database', 'migrations')
1744
+ ]);
1745
+ const pluralSnake = plural(snake(name));
1746
+ const singularSnake = snake(name);
1747
+ const migPattern = new RegExp(`^\\d{4}_\\d{2}_\\d{2}_\\d{6}_create_(${pluralSnake}|${singularSnake})_table\\.(js|ts|sql)$`);
1748
+ const fallbackPattern = new RegExp(`_create_(${pluralSnake}|${singularSnake})_table\\.(js|ts|sql)$`);
1749
+
1750
+ for (const mDir of migrationDirs) {
1751
+ if (!fs.existsSync(mDir)) continue;
1752
+ try {
1753
+ const files = fs.readdirSync(mDir);
1754
+ for (const file of files) {
1755
+ if (migPattern.test(file) || fallbackPattern.test(file)) {
1756
+ checkFile(path.join(mDir, file));
1757
+ }
1758
+ }
1759
+ } catch (_) {}
1760
+ }
1761
+ }
1762
+
1763
+ // 8. Routes
1764
+ let routeRemovals = [];
1765
+ if (wantsRoute || wantsController) {
1766
+ // a deleted controller must not stay required by routes/api.js (that would break every /api route, login too)
1767
+ routeRemovals.push(...removeApiRoute(ctx, name, otherDirs, { route: o.route, dryRun }));
1768
+ if (dirs.length && dirs.join('/') !== otherDirs.join('/')) {
1769
+ routeRemovals.push(...removeApiRoute(ctx, name, dirs, { route: o.route, dryRun }));
1770
+ }
1771
+ }
1772
+ if (wantsRoute) {
1773
+ routeRemovals.push(...removeWebRoute(ctx, name, otherDirs, { route: o.route, dryRun }));
1774
+ if (dirs.length && dirs.join('/') !== otherDirs.join('/')) {
1775
+ routeRemovals.push(...removeWebRoute(ctx, name, dirs, { route: o.route, dryRun }));
1776
+ }
1777
+ routeRemovals.push(...removeSidebarLink(ctx, name, otherDirs, { dryRun }));
1778
+ if (dirs.length && dirs.join('/') !== otherDirs.join('/')) {
1779
+ routeRemovals.push(...removeSidebarLink(ctx, name, dirs, { dryRun }));
1780
+ }
1781
+ }
1782
+
1783
+ const seenRouteFileDescs = new Set();
1784
+ routeRemovals = routeRemovals.filter((r) => {
1785
+ const key = `${r.file}::${r.desc}`;
1786
+ if (seenRouteFileDescs.has(key)) return false;
1787
+ seenRouteFileDescs.add(key);
1788
+ return true;
1789
+ });
1790
+
1791
+ const totalItems = filesToDelete.size + routeRemovals.length;
1792
+ if (totalItems === 0) {
1793
+ console.log(` No matching files or routes found for ${name}.`);
1794
+ return;
1795
+ }
1796
+
1797
+ if (dryRun) {
1798
+ for (const f of filesToDelete) {
1799
+ console.log(` ${yellow('WOULD DELETE')} ${path.relative(process.cwd(), f)}`);
1800
+ }
1801
+ for (const r of routeRemovals) {
1802
+ console.log(` ${yellow('WOULD REMOVE')} ${path.relative(process.cwd(), r.file)} (${r.desc})`);
1803
+ }
1804
+ console.log(`\n ${totalItems} item${totalItems === 1 ? '' : 's'} would be removed (dry-run).`);
1805
+ return;
1806
+ }
1807
+
1808
+ if (!force && process.stdin.isTTY && process.stdout.isTTY) {
1809
+ const readline = require('readline');
1810
+ const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
1811
+ const answer = await new Promise((resolve) => {
1812
+ console.log(`\n Remove ${totalItems} item${totalItems === 1 ? '' : 's'} for ${name}:`);
1813
+ for (const f of filesToDelete) console.log(` - ${path.relative(process.cwd(), f)}`);
1814
+ for (const r of routeRemovals) console.log(` - ${path.relative(process.cwd(), r.file)} (${r.desc})`);
1815
+ rl.question('\n Are you sure? [y/N]: ', (ans) => {
1816
+ rl.close();
1817
+ resolve(String(ans || '').trim().toLowerCase());
1818
+ });
1819
+ });
1820
+ if (answer !== 'y' && answer !== 'yes') {
1821
+ console.log(' Removal cancelled.');
1822
+ return;
1823
+ }
1824
+ }
1825
+
1826
+ const stopDirs = new Set(['app', 'backend', 'frontend', 'database', 'Models', 'Controllers', 'Requests', 'factories', 'seeders', 'migrations', 'pages', 'routes', 'views', '.']);
1827
+
1828
+ let count = 0;
1829
+ for (const f of filesToDelete) {
1830
+ try {
1831
+ fs.unlinkSync(f);
1832
+ cleanUpEmptyDirs(f, stopDirs);
1833
+ console.log(` ${red('DELETED')} ${path.relative(process.cwd(), f)}`);
1834
+ count++;
1835
+ } catch (e) {
1836
+ console.error(` Failed to delete ${path.relative(process.cwd(), f)}: ${e.message}`);
1837
+ }
1838
+ }
1839
+
1840
+ for (const r of routeRemovals) {
1841
+ try {
1842
+ r.apply();
1843
+ console.log(` ${red('REMOVED')} ${path.relative(process.cwd(), r.file)} (${r.desc})`);
1844
+ count++;
1845
+ } catch (e) {
1846
+ console.error(` Failed to update ${path.relative(process.cwd(), r.file)}: ${e.message}`);
1847
+ }
1848
+ }
1849
+
1850
+ console.log(`\n ${count} item${count === 1 ? '' : 's'} removed.`);
1851
+ }
1852
+
1853
+ const remove = {
1854
+ model: removeModel
1855
+ };
1856
+
1857
+ commands['remove:model'] = remove.model;
1858
+ commands['kreft:remove:model'] = remove.model;
1859
+ commands['remove'] = async function (args) {
1860
+ const o = options(args);
1861
+ const first = (o._[0] || '').toLowerCase();
1862
+ if (first === 'model') {
1863
+ return await remove.model(args);
1864
+ }
1865
+ if (first && typeof remove[first] === 'function') {
1866
+ return await remove[first](args);
1867
+ }
1868
+ return await remove.model(args);
1869
+ };
1870
+ commands['kreft:remove'] = commands['remove'];
1871
+ // the Som.js names
1872
+ commands['som:remove:model'] = remove.model;
1873
+ commands['som:remove'] = commands['remove'];
1874
+
1875
+ module.exports = { commands, stubs, make, remove, helpers: { options, context, withDb, write, nested, green, red, yellow, dim, migrationsOf, nextStamp, up, pascal, runSeed } };
1876
+ // the rest of the commands (queues, schedule, cache, tinker, make:job …) add themselves here
1877
+ require('./artisan-more.js');