@lenne.tech/cli 1.7.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 (70) 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 +7 -6
  5. package/build/commands/cli/rename.js +2 -2
  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 +11 -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 +4 -4
  14. package/build/commands/doctor.js +15 -26
  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 +59 -45
  18. package/build/commands/git/clean.js +6 -6
  19. package/build/commands/git/create.js +2 -2
  20. package/build/commands/git/force-pull.js +6 -3
  21. package/build/commands/git/get.js +3 -4
  22. package/build/commands/git/install-scripts.js +2 -2
  23. package/build/commands/git/rebase.js +5 -2
  24. package/build/commands/git/rename.js +3 -4
  25. package/build/commands/git/reset.js +15 -12
  26. package/build/commands/git/squash.js +5 -2
  27. package/build/commands/git/undo.js +6 -3
  28. package/build/commands/git/update.js +5 -5
  29. package/build/commands/history.js +1 -1
  30. package/build/commands/mongodb/s3-restore.js +10 -7
  31. package/build/commands/npm/reinit.js +12 -10
  32. package/build/commands/qdrant/delete.js +2 -2
  33. package/build/commands/qdrant/stats.js +2 -11
  34. package/build/commands/server/add-property.js +47 -32
  35. package/build/commands/server/create.js +10 -6
  36. package/build/commands/server/module.js +33 -14
  37. package/build/commands/server/object.js +2 -2
  38. package/build/commands/server/permissions.js +318 -0
  39. package/build/commands/server/set-secrets.js +2 -2
  40. package/build/commands/starter/chrome-extension.js +5 -5
  41. package/build/commands/status.js +17 -21
  42. package/build/commands/templates/list.js +4 -4
  43. package/build/commands/templates/llm.js +8 -14
  44. package/build/commands/tools/install-scripts.js +105 -0
  45. package/build/commands/tools/regex.js +1 -1
  46. package/build/commands/typescript/create.js +6 -6
  47. package/build/extensions/api-mode.js +3 -2
  48. package/build/extensions/config.js +5 -5
  49. package/build/extensions/frontend-helper.js +10 -6
  50. package/build/extensions/git.js +11 -11
  51. package/build/extensions/history.js +1 -1
  52. package/build/extensions/logger.js +3 -6
  53. package/build/extensions/package-manager.js +249 -0
  54. package/build/extensions/parse-properties.js +4 -4
  55. package/build/extensions/server.js +91 -36
  56. package/build/extensions/template.js +5 -5
  57. package/build/extensions/tools.js +23 -5
  58. package/build/extensions/typescript.js +2 -2
  59. package/build/lib/claude-cli.js +2 -6
  60. package/build/lib/fallback-scanner.js +852 -0
  61. package/build/lib/marketplace.js +7 -7
  62. package/build/lib/nuxt-base-components.js +9 -8
  63. package/build/lib/plugin-utils.js +9 -13
  64. package/build/lib/shell-config.js +5 -8
  65. package/build/lib/validation.js +5 -5
  66. package/build/templates/bash-scripts/tools/pcf +9 -0
  67. package/build/templates/permissions/report.html.ejs +402 -0
  68. package/docs/commands.md +28 -2
  69. package/docs/lt.config.md +53 -3
  70. package/package.json +5 -4
@@ -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.
@@ -436,10 +460,12 @@ lt fullstack init [options]
436
460
  | `--frontend-branch <branch>` | Branch of frontend starter to use (ng-base-starter or nuxt-base-starter) |
437
461
  | `--frontend-copy <path>` | Copy frontend from local template directory |
438
462
  | `--frontend-link <path>` | Symlink frontend to local template (fastest, changes affect original) |
439
- | `--git` | Initialize git repository |
440
- | `--git-link <url>` | Git repository URL |
463
+ | `--git` | Push initial commit to remote repository (git is always initialized) |
464
+ | `--git-link <url>` | Git remote repository URL (required when `--git` is true) |
441
465
  | `--noConfirm` | Skip confirmation prompts |
442
466
 
467
+ **Note:** Git is always initialized with the `dev` branch. The `--git` flag only controls whether the initial commit is pushed to a remote repository.
468
+
443
469
  **Note:** For Nuxt frontends with `--frontend-copy` or `--frontend-link`, specify the path to the `nuxt-base-template/` subdirectory, not the repository root.
444
470
 
445
471
  **Configuration:** `commands.fullstack.*`, `defaults.noConfirm`
package/docs/lt.config.md CHANGED
@@ -101,6 +101,7 @@ The `defaults` section contains settings that apply across multiple commands. Th
101
101
  | `defaults.controller` | `'Rest'` \| `'GraphQL'` \| `'Both'` \| `'auto'` | `'Both'` | server/module, server/create |
102
102
  | `defaults.domain` | `string` | - | deployment/create (use `{name}` as placeholder) |
103
103
  | `defaults.noConfirm` | `boolean` | `false` | blocks/add, components/add, config/init, git/*, server/create, server/module, npm/reinit, cli/create, typescript/create, fullstack/init, deployment/create, frontend/angular |
104
+ | `defaults.packageManager` | `'npm'` \| `'pnpm'` \| `'yarn'` | `'npm'` | Fallback when no lockfile is found. Auto-detection from lockfiles takes precedence. Used by: all commands that run package manager operations |
104
105
  | `defaults.skipInstall` | `boolean` | `false` | git/update |
105
106
  | `defaults.skipLint` | `boolean` | `false` | server/module, server/object, server/addProp |
106
107
 
@@ -114,6 +115,7 @@ The `defaults` section contains settings that apply across multiple commands. Th
114
115
  "controller": "Both",
115
116
  "domain": "{name}.lenne.tech",
116
117
  "noConfirm": false,
118
+ "packageManager": "npm",
117
119
  "skipInstall": false,
118
120
  "skipLint": false
119
121
  }
@@ -129,10 +131,23 @@ defaults:
129
131
  controller: Both
130
132
  domain: "{name}.lenne.tech"
131
133
  noConfirm: false
134
+ packageManager: npm
132
135
  skipInstall: false
133
136
  skipLint: false
134
137
  ```
135
138
 
139
+ ### Package Manager Detection
140
+
141
+ The CLI automatically detects the package manager for your project. The detection order is:
142
+
143
+ 1. **Lockfile in current directory**: `pnpm-lock.yaml` -> pnpm, `yarn.lock` -> yarn, `package-lock.json` -> npm
144
+ 2. **`packageManager` field in package.json** (Corepack standard): e.g., `"packageManager": "pnpm@8.15.0"`
145
+ 3. **Lockfile in parent directories** (monorepo support)
146
+ 4. **Config fallback**: `defaults.packageManager` from lt.config
147
+ 5. **Default**: `npm`
148
+
149
+ This means all CLI commands (`lt server create`, `lt fullstack init`, `lt npm reinit`, etc.) will use the correct package manager automatically without any configuration needed.
150
+
136
151
  ### Global vs Command-Specific Settings
137
152
 
138
153
  Global defaults provide a convenient way to set organization-wide preferences. Command-specific settings override these when you need different behavior for a particular command.
@@ -333,6 +348,37 @@ Creates a new server object (embedded document).
333
348
 
334
349
  ---
335
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
+
336
382
  #### `lt server addProp`
337
383
 
338
384
  Adds a property to an existing module or object.
@@ -525,8 +571,8 @@ Creates a new fullstack workspace with API and frontend.
525
571
  | `commands.fullstack.frontendBranch` | `string` | - | Branch of frontend starter to use (ng-base-starter or nuxt-base-starter) |
526
572
  | `commands.fullstack.frontendCopy` | `string` | - | Path to local frontend template directory to copy instead of cloning |
527
573
  | `commands.fullstack.frontendLink` | `string` | - | Path to local frontend template directory to symlink (fastest, changes affect original) |
528
- | `commands.fullstack.git` | `boolean` | - | Initialize git repository |
529
- | `commands.fullstack.gitLink` | `string` | - | Git repository URL |
574
+ | `commands.fullstack.git` | `boolean` | - | Push initial commit to remote repository (git is always initialized with `dev` branch) |
575
+ | `commands.fullstack.gitLink` | `string` | - | Git remote repository URL (required when `git` is true) |
530
576
 
531
577
  **Example:**
532
578
  ```json
@@ -912,6 +958,7 @@ The `meta` section stores project information.
912
958
  "controller": "Both",
913
959
  "domain": "{name}.lenne.tech",
914
960
  "noConfirm": false,
961
+ "packageManager": "npm",
915
962
  "skipInstall": false,
916
963
  "skipLint": false
917
964
  },
@@ -924,7 +971,8 @@ The `meta` section stores project information.
924
971
  },
925
972
  "fullstack": {
926
973
  "frontend": "nuxt",
927
- "git": false
974
+ "git": false,
975
+ "gitLink": "https://github.com/myorg/myproject.git"
928
976
  },
929
977
  "git": {
930
978
  "defaultBranch": "develop",
@@ -959,6 +1007,7 @@ defaults:
959
1007
  controller: Both
960
1008
  domain: "{name}.lenne.tech"
961
1009
  noConfirm: false
1010
+ packageManager: npm
962
1011
  skipInstall: false
963
1012
  skipLint: false
964
1013
 
@@ -972,6 +1021,7 @@ commands:
972
1021
  fullstack:
973
1022
  frontend: nuxt
974
1023
  git: false
1024
+ gitLink: "https://github.com/myorg/myproject.git"
975
1025
 
976
1026
  git:
977
1027
  defaultBranch: develop
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lenne.tech/cli",
3
- "version": "1.7.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",
@@ -50,7 +51,7 @@
50
51
  "bin"
51
52
  ],
52
53
  "dependencies": {
53
- "@aws-sdk/client-s3": "3.985.0",
54
+ "@aws-sdk/client-s3": "3.986.0",
54
55
  "@lenne.tech/cli-plugin-helper": "0.0.14",
55
56
  "@types/lodash": "4.17.23",
56
57
  "bcrypt": "6.0.0",
@@ -72,8 +73,8 @@
72
73
  "@types/jest": "30.0.0",
73
74
  "@types/js-yaml": "4.0.9",
74
75
  "@types/node": "25.2.2",
75
- "@typescript-eslint/eslint-plugin": "8.54.0",
76
- "@typescript-eslint/parser": "8.54.0",
76
+ "@typescript-eslint/eslint-plugin": "8.55.0",
77
+ "@typescript-eslint/parser": "8.55.0",
77
78
  "eslint": "9.39.2",
78
79
  "eslint-config-prettier": "10.1.8",
79
80
  "husky": "9.1.7",