@lenne.tech/cli 1.8.0 → 1.9.0

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 (65) hide show
  1. package/build/commands/blocks/add.js +4 -3
  2. package/build/commands/blocks/blocks.js +1 -1
  3. package/build/commands/claude/plugins.js +17 -12
  4. package/build/commands/cli/create.js +1 -1
  5. package/build/commands/cli/rename.js +1 -1
  6. package/build/commands/completion.js +1 -1
  7. package/build/commands/components/add.js +4 -3
  8. package/build/commands/components/components.js +1 -1
  9. package/build/commands/config/help.js +1 -1
  10. package/build/commands/config/validate.js +10 -1
  11. package/build/commands/directus/docker-setup.js +1 -1
  12. package/build/commands/directus/remove.js +2 -3
  13. package/build/commands/directus/typegen.js +1 -1
  14. package/build/commands/doctor.js +4 -4
  15. package/build/commands/frontend/angular.js +1 -1
  16. package/build/commands/frontend/nuxt.js +2 -2
  17. package/build/commands/fullstack/init.js +14 -10
  18. package/build/commands/git/clean.js +6 -6
  19. package/build/commands/git/force-pull.js +6 -3
  20. package/build/commands/git/get.js +2 -3
  21. package/build/commands/git/install-scripts.js +2 -2
  22. package/build/commands/git/rebase.js +5 -2
  23. package/build/commands/git/rename.js +3 -4
  24. package/build/commands/git/reset.js +14 -11
  25. package/build/commands/git/squash.js +5 -2
  26. package/build/commands/git/undo.js +6 -3
  27. package/build/commands/git/update.js +3 -3
  28. package/build/commands/history.js +1 -1
  29. package/build/commands/mongodb/s3-restore.js +10 -7
  30. package/build/commands/qdrant/delete.js +2 -2
  31. package/build/commands/qdrant/stats.js +2 -11
  32. package/build/commands/server/add-property.js +46 -31
  33. package/build/commands/server/create.js +5 -3
  34. package/build/commands/server/module.js +30 -13
  35. package/build/commands/server/object.js +1 -1
  36. package/build/commands/server/permissions.js +318 -0
  37. package/build/commands/server/set-secrets.js +2 -2
  38. package/build/commands/status.js +13 -19
  39. package/build/commands/templates/list.js +4 -4
  40. package/build/commands/templates/llm.js +8 -14
  41. package/build/commands/tools/install-scripts.js +2 -2
  42. package/build/commands/tools/regex.js +1 -1
  43. package/build/extensions/api-mode.js +3 -2
  44. package/build/extensions/config.js +5 -5
  45. package/build/extensions/frontend-helper.js +1 -1
  46. package/build/extensions/git.js +11 -11
  47. package/build/extensions/history.js +1 -1
  48. package/build/extensions/logger.js +3 -6
  49. package/build/extensions/package-manager.js +6 -6
  50. package/build/extensions/parse-properties.js +4 -4
  51. package/build/extensions/server.js +35 -23
  52. package/build/extensions/template.js +5 -5
  53. package/build/extensions/tools.js +5 -5
  54. package/build/extensions/typescript.js +2 -2
  55. package/build/lib/claude-cli.js +2 -6
  56. package/build/lib/fallback-scanner.js +852 -0
  57. package/build/lib/marketplace.js +7 -7
  58. package/build/lib/nuxt-base-components.js +3 -3
  59. package/build/lib/plugin-utils.js +9 -13
  60. package/build/lib/shell-config.js +5 -8
  61. package/build/lib/validation.js +5 -5
  62. package/build/templates/permissions/report.html.ejs +402 -0
  63. package/docs/commands.md +24 -0
  64. package/docs/lt.config.md +31 -0
  65. package/package.json +2 -1
@@ -0,0 +1,402 @@
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
+ <title>Permissions Report</title>
7
+ <style>
8
+ :root {
9
+ --bg: #ffffff; --fg: #1a1a2e; --bg-card: #f8f9fa; --border: #dee2e6;
10
+ --primary: #0d6efd; --success: #198754; --warning: #ffc107; --danger: #dc3545;
11
+ --role-everyone: #198754; --role-noone: #dc3545; --role-admin: #0d6efd;
12
+ --role-user: #e6a817; --role-self: #6c757d; --role-custom: #fd7e14;
13
+ --shadow: 0 1px 3px rgba(0,0,0,0.12);
14
+ }
15
+ @media (prefers-color-scheme: dark) {
16
+ :root {
17
+ --bg: #1a1a2e; --fg: #e0e0e0; --bg-card: #16213e; --border: #3a3a5c;
18
+ --shadow: 0 1px 3px rgba(0,0,0,0.4);
19
+ }
20
+ }
21
+ * { margin: 0; padding: 0; box-sizing: border-box; }
22
+ body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; background: var(--bg); color: var(--fg); line-height: 1.6; }
23
+ .layout { display: flex; min-height: 100vh; }
24
+ .sidebar { width: 260px; position: sticky; top: 0; height: 100vh; overflow-y: auto; background: var(--bg-card); border-right: 1px solid var(--border); padding: 1rem; flex-shrink: 0; }
25
+ .sidebar h3 { font-size: 0.85rem; text-transform: uppercase; letter-spacing: 0.05em; color: var(--primary); margin-bottom: 0.5rem; }
26
+ .sidebar a { display: block; padding: 0.25rem 0.5rem; color: var(--fg); text-decoration: none; font-size: 0.85rem; border-radius: 4px; }
27
+ .sidebar a:hover { background: var(--border); }
28
+ .sidebar a.indent { padding-left: 1.5rem; font-size: 0.8rem; opacity: 0.8; }
29
+ .main { flex: 1; padding: 2rem; max-width: 1200px; }
30
+ .dashboard { display: grid; grid-template-columns: repeat(auto-fit, minmax(150px, 1fr)); gap: 1rem; margin-bottom: 2rem; }
31
+ .stat { background: var(--bg-card); border: 1px solid var(--border); border-radius: 8px; padding: 1rem; text-align: center; box-shadow: var(--shadow); }
32
+ .stat .num { font-size: 2rem; font-weight: 700; color: var(--primary); }
33
+ .stat .label { font-size: 0.8rem; opacity: 0.7; }
34
+ .stat.warn .num { color: var(--warning); }
35
+ .stat.danger .num { color: var(--danger); }
36
+ .controls { display: flex; gap: 1rem; flex-wrap: wrap; margin-bottom: 1.5rem; align-items: center; }
37
+ .controls input, .controls select { padding: 0.4rem 0.8rem; border: 1px solid var(--border); border-radius: 6px; background: var(--bg); color: var(--fg); font-size: 0.9rem; }
38
+ .controls input { flex: 1; min-width: 200px; }
39
+ .controls label { font-size: 0.85rem; display: flex; align-items: center; gap: 0.3rem; }
40
+ .badge { display: inline-block; padding: 0.15rem 0.5rem; border-radius: 12px; font-size: 0.75rem; font-weight: 600; color: #fff; }
41
+ .badge-everyone { background: var(--role-everyone); }
42
+ .badge-noone { background: var(--role-noone); }
43
+ .badge-admin { background: var(--role-admin); }
44
+ .badge-user { background: var(--role-user); color: #000; }
45
+ .badge-self, .badge-creator { background: var(--role-self); }
46
+ .badge-custom { background: var(--role-custom); }
47
+ .badge-warn { background: var(--warning); color: #000; }
48
+ section { margin-bottom: 2rem; }
49
+ h1 { font-size: 1.8rem; margin-bottom: 0.5rem; }
50
+ h2 { font-size: 1.4rem; margin: 1.5rem 0 0.75rem; padding-bottom: 0.3rem; border-bottom: 2px solid var(--primary); }
51
+ h3 { font-size: 1.1rem; margin: 1rem 0 0.5rem; }
52
+ .meta { font-size: 0.85rem; opacity: 0.7; margin-bottom: 0.25rem; }
53
+ table { width: 100%; border-collapse: collapse; margin: 0.5rem 0 1rem; font-size: 0.85rem; }
54
+ th, td { padding: 0.4rem 0.6rem; text-align: left; border: 1px solid var(--border); }
55
+ th { background: var(--bg-card); cursor: pointer; user-select: none; white-space: nowrap; }
56
+ th:hover { background: var(--border); }
57
+ tr:nth-child(even) { background: var(--bg-card); }
58
+ .collapsible { cursor: pointer; }
59
+ .collapsible::before { content: '\25B6'; display: inline-block; margin-right: 0.5rem; transition: transform 0.2s; font-size: 0.8rem; }
60
+ .collapsible.open::before { transform: rotate(90deg); }
61
+ .collapse-content { display: none; }
62
+ .collapse-content.open { display: block; }
63
+ .warning-row { background: rgba(255, 193, 7, 0.1) !important; }
64
+ .btn { padding: 0.4rem 0.8rem; border: 1px solid var(--border); border-radius: 6px; background: var(--bg-card); color: var(--fg); cursor: pointer; font-size: 0.8rem; }
65
+ .btn:hover { background: var(--border); }
66
+ .module-header { position: sticky; top: 0; background: var(--bg); z-index: 10; padding: 0.5rem 0; border-bottom: 1px solid var(--border); }
67
+ @media (max-width: 768px) {
68
+ .sidebar { display: none; }
69
+ .main { padding: 1rem; }
70
+ .dashboard { grid-template-columns: repeat(2, 1fr); }
71
+ }
72
+ </style>
73
+ </head>
74
+ <body>
75
+ <div class="layout">
76
+ <nav class="sidebar">
77
+ <h3>Permissions Report</h3>
78
+ <a href="#dashboard">Dashboard</a>
79
+ <a href="#role-index">Role Index</a>
80
+ <a href="#warnings">Warnings (<%= props.warnings.length %>)</a>
81
+ <hr style="margin:0.5rem 0;border-color:var(--border)">
82
+ <% for (const mod of props.modules) { %>
83
+ <a href="#mod-<%= mod.name %>"><%= mod.name %></a>
84
+ <% for (const model of mod.models) { %><a href="#model-<%= mod.name %>-<%= model.className %>" class="indent">Model: <%= model.className %></a><% } %>
85
+ <% for (const ctrl of mod.controllers) { %><a href="#ctrl-<%= mod.name %>-<%= ctrl.className %>" class="indent">Ctrl: <%= ctrl.className %></a><% } %>
86
+ <% for (const res of mod.resolvers) { %><a href="#res-<%= mod.name %>-<%= res.className %>" class="indent">Resolver: <%= res.className %></a><% } %>
87
+ <% } %>
88
+ <% if (props.objects.length > 0) { %>
89
+ <hr style="margin:0.5rem 0;border-color:var(--border)">
90
+ <a href="#subobjects">SubObjects</a>
91
+ <% } %>
92
+ <hr style="margin:0.5rem 0;border-color:var(--border)">
93
+ <button class="btn" onclick="exportAs('md')">Export MD</button>
94
+ <button class="btn" onclick="exportAs('json')">Export JSON</button>
95
+ </nav>
96
+
97
+ <div class="main">
98
+ <h1>Permissions Report</h1>
99
+ <p class="meta">Generated: <%= props.generated %></p>
100
+ <p class="meta">Project: <%= props.projectPath %></p>
101
+
102
+ <section id="dashboard">
103
+ <div class="dashboard">
104
+ <div class="stat"><div class="num"><%= props.stats.totalModules %></div><div class="label">Modules</div></div>
105
+ <div class="stat"><div class="num"><%= props.stats.totalModels %></div><div class="label">Models</div></div>
106
+ <div class="stat"><div class="num"><%= props.stats.totalEndpoints %></div><div class="label">Endpoints</div></div>
107
+ <div class="stat"><div class="num"><%= props.stats.totalSubObjects %></div><div class="label">SubObjects</div></div>
108
+ <div class="stat <%= props.stats.totalWarnings > 0 ? 'danger' : '' %>"><div class="num"><%= props.stats.totalWarnings %></div><div class="label">Warnings</div></div>
109
+ <% var ecClass = props.stats.endpointCoverage >= 90 ? '' : props.stats.endpointCoverage >= 70 ? 'warn' : 'danger'; %>
110
+ <div class="stat <%= ecClass %>"><div class="num"><%= props.stats.endpointCoverage %>%</div><div class="label">Endpoint Coverage</div></div>
111
+ <% var scClass = props.stats.securityCoverage >= 90 ? '' : props.stats.securityCoverage >= 70 ? 'warn' : 'danger'; %>
112
+ <div class="stat <%= scClass %>"><div class="num"><%= props.stats.securityCoverage %>%</div><div class="label">Security Coverage</div></div>
113
+ </div>
114
+ </section>
115
+
116
+ <section>
117
+ <div class="controls">
118
+ <input type="text" id="search" placeholder="Search modules, fields, roles..." oninput="filterAll()">
119
+ <select id="roleFilter" onchange="filterAll()">
120
+ <option value="">All Roles</option>
121
+ </select>
122
+ <label><input type="checkbox" id="warnOnly" onchange="filterAll()"> Warnings only</label>
123
+ </div>
124
+ </section>
125
+
126
+ <section id="role-index">
127
+ <h2 class="collapsible open" onclick="toggle(this)">Role Index</h2>
128
+ <div class="collapse-content open">
129
+ <% if (props.roleEnums.length > 0) { %>
130
+ <table>
131
+ <thead><tr><th>Enum</th><th>Value</th><th>Type</th></tr></thead>
132
+ <tbody>
133
+ <% for (const e of props.roleEnums) { for (const v of e.values) { %>
134
+ <tr>
135
+ <td><%= e.name %>.<%= v.key %></td>
136
+ <td><%= v.key.startsWith('S_') ? '(system)' : v.value %></td>
137
+ <td><span class="badge <%= getBadgeClass(v.key) %>"><%= v.key.startsWith('S_') ? 'System' : 'Real' %></span></td>
138
+ </tr>
139
+ <% } } %>
140
+ </tbody>
141
+ </table>
142
+ <% } else { %>
143
+ <p><em>No role enums found.</em></p>
144
+ <% } %>
145
+ </div>
146
+ </section>
147
+
148
+ <section id="warnings">
149
+ <h2 class="collapsible open" onclick="toggle(this)">Warnings (<%= props.warnings.length %>)</h2>
150
+ <div class="collapse-content open">
151
+ <% if (props.warnings.length > 0) { %>
152
+ <table>
153
+ <thead><tr><th>#</th><th>Module</th><th>File</th><th>Type</th><th>Details</th></tr></thead>
154
+ <tbody>
155
+ <% props.warnings.forEach((w, i) => { %>
156
+ <tr class="warning-row">
157
+ <td><%= i + 1 %></td>
158
+ <td><%= w.module %></td>
159
+ <td><%= w.file.split('/').pop() %></td>
160
+ <td><span class="badge badge-warn"><%= w.type %></span></td>
161
+ <td><%= w.details %></td>
162
+ </tr>
163
+ <% }) %>
164
+ </tbody>
165
+ </table>
166
+ <% } else { %>
167
+ <p><em>No warnings found.</em></p>
168
+ <% } %>
169
+ </div>
170
+ </section>
171
+
172
+ <% for (const mod of props.modules) { %>
173
+ <section class="module-section" id="mod-<%= mod.name %>" data-module="<%= mod.name %>" data-has-warnings="<%= props.warnings.some(w => w.module === mod.name) %>">
174
+ <div class="module-header">
175
+ <h2 class="collapsible open" onclick="toggle(this)">Module: <%= mod.name %></h2>
176
+ </div>
177
+ <div class="collapse-content open">
178
+
179
+ <% for (const model of mod.models) { %>
180
+ <div id="model-<%= mod.name %>-<%= model.className %>">
181
+ <h3>Model: <%= model.className %></h3>
182
+ <p class="meta">File: <%= model.filePath %></p>
183
+ <% if (model.extendsClass) { %><p class="meta">Extends: <%= model.extendsClass %></p><% } %>
184
+ <p class="meta">Class Restriction: <% if (model.classRestriction.length > 0) { model.classRestriction.forEach(r => { %><span class="badge <%= getBadgeClass(r) %>"><%= r %></span> <% }) } else { %><em>(none)</em><% } %></p>
185
+ <p class="meta">securityCheck: <% if (model.securityCheck) { %><%= model.securityCheck.summary %><% } else { %><em>Not present</em><% } %></p>
186
+ <% if (model.fields.length > 0) { %>
187
+ <table>
188
+ <thead><tr><th>Field</th><th>Roles</th><th>Source</th></tr></thead>
189
+ <tbody>
190
+ <% for (const f of model.fields) { %>
191
+ <tr>
192
+ <td><%= f.name %></td>
193
+ <td><%= f.roles %></td>
194
+ <td><%= f.inherited ? 'inherited' : 'local' %></td>
195
+ </tr>
196
+ <% } %>
197
+ </tbody>
198
+ </table>
199
+ <% } %>
200
+ </div>
201
+ <% } %>
202
+
203
+ <% for (const input of mod.inputs) { %>
204
+ <div>
205
+ <h3>Input: <%= input.className %></h3>
206
+ <p class="meta">File: <%= input.filePath %></p>
207
+ <% if (input.extendsClass) { %><p class="meta">Extends: <%= input.extendsClass %></p><% } %>
208
+ <% if (input.fields.length > 0) { %>
209
+ <table>
210
+ <thead><tr><th>Field</th><th>Roles</th></tr></thead>
211
+ <tbody>
212
+ <% for (const f of input.fields) { %>
213
+ <tr><td><%= f.name %></td><td><%= f.roles %></td></tr>
214
+ <% } %>
215
+ </tbody>
216
+ </table>
217
+ <% } %>
218
+ </div>
219
+ <% } %>
220
+
221
+ <% for (const ctrl of mod.controllers) { %>
222
+ <div id="ctrl-<%= mod.name %>-<%= ctrl.className %>">
223
+ <h3>Controller: <%= ctrl.className %></h3>
224
+ <p class="meta">File: <%= ctrl.filePath %></p>
225
+ <p class="meta">Class Roles: <% if (ctrl.classRoles.length > 0) { ctrl.classRoles.forEach(r => { %><span class="badge <%= getBadgeClass(r) %>"><%= r %></span> <% }) } else { %><em>(none)</em><% } %></p>
226
+ <% if (ctrl.methods.length > 0) { %>
227
+ <table>
228
+ <thead><tr><th>Method</th><th>HTTP</th><th>Route</th><th>Roles</th><th>Effective</th></tr></thead>
229
+ <tbody>
230
+ <% for (const m of ctrl.methods) { const eff = m.roles.length > 0 ? m.roles : ctrl.classRoles; %>
231
+ <tr>
232
+ <td><%= m.name %></td>
233
+ <td><%= m.httpMethod %></td>
234
+ <td><%= m.route || '/' %></td>
235
+ <td><% if (m.roles.length > 0) { m.roles.forEach(r => { %><span class="badge <%= getBadgeClass(r) %>"><%= r %></span> <% }) } else { %><em>(none)</em><% } %></td>
236
+ <td><% eff.forEach(r => { %><span class="badge <%= getBadgeClass(r) %>"><%= r %></span> <% }) %><%= m.roles.length === 0 && ctrl.classRoles.length > 0 ? '(class)' : '' %></td>
237
+ </tr>
238
+ <% } %>
239
+ </tbody>
240
+ </table>
241
+ <% } %>
242
+ </div>
243
+ <% } %>
244
+
245
+ <% for (const res of mod.resolvers) { %>
246
+ <div id="res-<%= mod.name %>-<%= res.className %>">
247
+ <h3>Resolver: <%= res.className %></h3>
248
+ <p class="meta">File: <%= res.filePath %></p>
249
+ <p class="meta">Class Roles: <% if (res.classRoles.length > 0) { res.classRoles.forEach(r => { %><span class="badge <%= getBadgeClass(r) %>"><%= r %></span> <% }) } else { %><em>(none)</em><% } %></p>
250
+ <% if (res.methods.length > 0) { %>
251
+ <table>
252
+ <thead><tr><th>Method</th><th>Type</th><th>Roles</th><th>Effective</th></tr></thead>
253
+ <tbody>
254
+ <% for (const m of res.methods) { const eff = m.roles.length > 0 ? m.roles : res.classRoles; %>
255
+ <tr>
256
+ <td><%= m.name %></td>
257
+ <td><%= m.httpMethod %></td>
258
+ <td><% if (m.roles.length > 0) { m.roles.forEach(r => { %><span class="badge <%= getBadgeClass(r) %>"><%= r %></span> <% }) } else { %><em>(none)</em><% } %></td>
259
+ <td><% eff.forEach(r => { %><span class="badge <%= getBadgeClass(r) %>"><%= r %></span> <% }) %><%= m.roles.length === 0 && res.classRoles.length > 0 ? '(class)' : '' %></td>
260
+ </tr>
261
+ <% } %>
262
+ </tbody>
263
+ </table>
264
+ <% } %>
265
+ </div>
266
+ <% } %>
267
+
268
+ </div>
269
+ </section>
270
+ <% } %>
271
+
272
+ <% if (props.objects.length > 0) { %>
273
+ <section id="subobjects">
274
+ <h2>SubObjects</h2>
275
+ <% for (const obj of props.objects) { %>
276
+ <div>
277
+ <h3><%= obj.className %></h3>
278
+ <p class="meta">File: <%= obj.filePath %></p>
279
+ <% if (obj.extendsClass) { %><p class="meta">Extends: <%= obj.extendsClass %></p><% } %>
280
+ <% if (obj.fields.length > 0) { %>
281
+ <table>
282
+ <thead><tr><th>Field</th><th>Roles</th><th>Source</th></tr></thead>
283
+ <tbody>
284
+ <% for (const f of obj.fields) { %>
285
+ <tr><td><%= f.name %></td><td><%= f.roles %></td><td><%= f.inherited ? 'inherited' : 'local' %></td></tr>
286
+ <% } %>
287
+ </tbody>
288
+ </table>
289
+ <% } %>
290
+ </div>
291
+ <% } %>
292
+ </section>
293
+ <% } %>
294
+
295
+ </div>
296
+ </div>
297
+
298
+ <script>
299
+ var DATA = <%- JSON.stringify({ modules: props.modules, objects: props.objects, roleEnums: props.roleEnums, stats: props.stats, warnings: props.warnings }) %>;
300
+
301
+ // Populate role filter
302
+ (function() {
303
+ var sel = document.getElementById('roleFilter');
304
+ var roles = {};
305
+ DATA.roleEnums.forEach(function(e) { e.values.forEach(function(v) { roles[v.key] = true; }); });
306
+ DATA.modules.forEach(function(mod) {
307
+ mod.models.forEach(function(model) {
308
+ model.classRestriction.forEach(function(r) { roles[r] = true; });
309
+ model.fields.forEach(function(f) {
310
+ var m = f.roles.match(/`([^`]+)`/g);
311
+ if (m) m.forEach(function(r) { roles[r.replace(/`/g, '')] = true; });
312
+ });
313
+ });
314
+ mod.controllers.forEach(function(ep) {
315
+ ep.classRoles.forEach(function(r) { roles[r] = true; });
316
+ ep.methods.forEach(function(m) { m.roles.forEach(function(r) { roles[r] = true; }); });
317
+ });
318
+ mod.resolvers.forEach(function(ep) {
319
+ ep.classRoles.forEach(function(r) { roles[r] = true; });
320
+ ep.methods.forEach(function(m) { m.roles.forEach(function(r) { roles[r] = true; }); });
321
+ });
322
+ });
323
+ Object.keys(roles).sort().forEach(function(r) {
324
+ var o = document.createElement('option');
325
+ o.value = r; o.textContent = r;
326
+ sel.appendChild(o);
327
+ });
328
+ })();
329
+
330
+ function toggle(el) {
331
+ el.classList.toggle('open');
332
+ var content = el.nextElementSibling;
333
+ if (content) content.classList.toggle('open');
334
+ }
335
+
336
+ function filterAll() {
337
+ var q = document.getElementById('search').value.toLowerCase();
338
+ var role = document.getElementById('roleFilter').value;
339
+ var warnOnly = document.getElementById('warnOnly').checked;
340
+
341
+ document.querySelectorAll('.module-section').forEach(function(section) {
342
+ var hasWarnings = section.dataset.hasWarnings === 'true';
343
+ var text = section.textContent.toLowerCase();
344
+ var show = true;
345
+ if (q && !text.includes(q)) show = false;
346
+ if (warnOnly && !hasWarnings) show = false;
347
+ if (role && !text.includes(role.toLowerCase())) show = false;
348
+ section.style.display = show ? '' : 'none';
349
+ });
350
+
351
+ var warningsSection = document.getElementById('warnings');
352
+ if (warningsSection) {
353
+ warningsSection.querySelectorAll('tbody tr').forEach(function(row) {
354
+ var text = row.textContent.toLowerCase();
355
+ var show = true;
356
+ if (q && !text.includes(q)) show = false;
357
+ row.style.display = show ? '' : 'none';
358
+ });
359
+ }
360
+ }
361
+
362
+ document.querySelectorAll('th').forEach(function(th) {
363
+ th.addEventListener('click', function() {
364
+ var table = this.closest('table');
365
+ var tbody = table.querySelector('tbody');
366
+ if (!tbody) return;
367
+ var idx = Array.from(this.parentNode.children).indexOf(this);
368
+ var rows = Array.from(tbody.querySelectorAll('tr'));
369
+ var asc = this.dataset.sort !== 'asc';
370
+ rows.sort(function(a, b) {
371
+ var at = (a.children[idx] || {}).textContent || '';
372
+ var bt = (b.children[idx] || {}).textContent || '';
373
+ return asc ? at.localeCompare(bt) : bt.localeCompare(at);
374
+ });
375
+ this.dataset.sort = asc ? 'asc' : 'desc';
376
+ rows.forEach(function(r) { tbody.appendChild(r); });
377
+ });
378
+ });
379
+
380
+ function exportAs(fmt) {
381
+ var blob = new Blob([JSON.stringify(DATA, null, 2)], { type: fmt === 'json' ? 'application/json' : 'text/plain' });
382
+ var a = document.createElement('a');
383
+ a.href = URL.createObjectURL(blob);
384
+ a.download = 'permissions.' + fmt;
385
+ a.click();
386
+ }
387
+ </script>
388
+ </body>
389
+ </html>
390
+ <%
391
+ function getBadgeClass(role) {
392
+ if (!role) return 'badge-custom';
393
+ var r = role.toUpperCase();
394
+ if (r === 'S_EVERYONE') return 'badge-everyone';
395
+ if (r === 'S_NO_ONE') return 'badge-noone';
396
+ if (r === 'ADMIN') return 'badge-admin';
397
+ if (r === 'S_USER') return 'badge-user';
398
+ if (r === 'S_SELF') return 'badge-self';
399
+ if (r === 'S_CREATOR') return 'badge-creator';
400
+ return 'badge-custom';
401
+ }
402
+ %>
package/docs/commands.md CHANGED
@@ -123,6 +123,30 @@ lt server object [options]
123
123
 
124
124
  ---
125
125
 
126
+ ### `lt server permissions`
127
+
128
+ Scans all server modules and generates a permissions report showing roles, restrictions, and security gaps.
129
+
130
+ **Usage:**
131
+ ```bash
132
+ lt server permissions [options]
133
+ ```
134
+
135
+ **Options:**
136
+ | Option | Description |
137
+ |--------|-------------|
138
+ | `--path <dir>` | Path to NestJS project (default: auto-detect) |
139
+ | `--output <file>` | Output file (default: `permissions.<format>`) |
140
+ | `--format <md\|json\|html>` | Output format (default: `html` for TTY, `json` for CI) |
141
+ | `--open` / `--no-open` | Open report in browser (default: `true` for TTY) |
142
+ | `--console` | Print summary to console |
143
+ | `--fail-on-warnings` | Exit code 1 on warnings (for CI/CD) |
144
+ | `--noConfirm` | Skip confirmation prompts |
145
+
146
+ **Configuration:** `commands.server.permissions.*`, `defaults.noConfirm`
147
+
148
+ ---
149
+
126
150
  ### `lt server addProp`
127
151
 
128
152
  Adds a property to an existing module or object.
package/docs/lt.config.md CHANGED
@@ -348,6 +348,37 @@ Creates a new server object (embedded document).
348
348
 
349
349
  ---
350
350
 
351
+ #### `lt server permissions`
352
+
353
+ Scans server modules and generates a permissions report.
354
+
355
+ | Field | Type | Default | Description |
356
+ |-------|------|---------|-------------|
357
+ | `commands.server.permissions.format` | `'md'` \| `'json'` \| `'html'` | `'html'` (TTY) / `'json'` (CI) | Output format |
358
+ | `commands.server.permissions.output` | `string` | `permissions.<format>` | Output file path |
359
+ | `commands.server.permissions.path` | `string` | auto-detect | Path to NestJS project |
360
+ | `commands.server.permissions.open` | `boolean` | `true` (TTY) / `false` (CI) | Open report in browser |
361
+ | `commands.server.permissions.console` | `boolean` | `false` | Print summary to console |
362
+ | `commands.server.permissions.failOnWarnings` | `boolean` | `false` | Exit code 1 on warnings |
363
+ | `commands.server.permissions.noConfirm` | `boolean` | `false` | Skip confirmation prompts |
364
+
365
+ **Example:**
366
+ ```json
367
+ {
368
+ "commands": {
369
+ "server": {
370
+ "permissions": {
371
+ "format": "html",
372
+ "open": true,
373
+ "failOnWarnings": true
374
+ }
375
+ }
376
+ }
377
+ }
378
+ ```
379
+
380
+ ---
381
+
351
382
  #### `lt server addProp`
352
383
 
353
384
  Adds a property to an existing module or object.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lenne.tech/cli",
3
- "version": "1.8.0",
3
+ "version": "1.9.0",
4
4
  "description": "lenne.Tech CLI: lt",
5
5
  "keywords": [
6
6
  "lenne.Tech",
@@ -18,6 +18,7 @@
18
18
  "lt": "bin/lt"
19
19
  },
20
20
  "scripts": {
21
+ "check": "npm install && npm run format && npm run build && npm run start",
21
22
  "postinstall": "node bin/postinstall.js 2>/dev/null || true",
22
23
  "build": "npm run lint && npm run test && npm run clean-build && npm run compile && npm run copy-templates",
23
24
  "clean-build": "npx rimraf ./build",