@atlashub/smartstack-cli 3.35.0 → 3.37.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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@atlashub/smartstack-cli",
3
- "version": "3.35.0",
3
+ "version": "3.37.0",
4
4
  "description": "SmartStack Claude Code automation toolkit - GitFlow, EF Core migrations, prompts and more",
5
5
  "author": {
6
6
  "name": "SmartStack",
@@ -21,6 +21,7 @@
21
21
  "dist",
22
22
  "templates",
23
23
  "config",
24
+ "scripts",
24
25
  ".documentation"
25
26
  ],
26
27
  "engines": {
@@ -51,7 +52,7 @@
51
52
  "ai-tools"
52
53
  ],
53
54
  "scripts": {
54
- "postinstall": "node scripts/postinstall.js || true",
55
+ "postinstall": "node scripts/postinstall.js || exit 0",
55
56
  "dev": "tsup --watch",
56
57
  "build:html": "node templates/skills/business-analyse/html/build-html.js",
57
58
  "build": "npm run build:html && npx tsup",
@@ -0,0 +1,325 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Extract API Endpoints from SmartStack Controllers
4
+ *
5
+ * Usage:
6
+ * node extract-api-endpoints.ts --module users --app-path "D:/01 - projets/SmartStack.app/02-Develop"
7
+ *
8
+ * Output: JSON array of DocApiEndpoint objects
9
+ */
10
+
11
+ import * as fs from 'fs';
12
+ import * as path from 'path';
13
+ import { glob } from 'glob';
14
+
15
+ interface DocApiEndpoint {
16
+ method: 'GET' | 'POST' | 'PUT' | 'DELETE' | 'PATCH';
17
+ path: string;
18
+ handler: string;
19
+ permission: string;
20
+ }
21
+
22
+ interface PermissionConstant {
23
+ constantPath: string; // e.g., "Permissions.Admin.Users.View"
24
+ value: string; // e.g., "platform.administration.users.read"
25
+ }
26
+
27
+ /**
28
+ * Parse command line arguments
29
+ */
30
+ function parseArgs(): { module: string; appPath: string } {
31
+ const args = process.argv.slice(2);
32
+ const moduleIndex = args.indexOf('--module');
33
+ const appPathIndex = args.indexOf('--app-path');
34
+
35
+ if (moduleIndex === -1 || appPathIndex === -1) {
36
+ console.error('Usage: node extract-api-endpoints.ts --module <module-name> --app-path <path-to-app>');
37
+ process.exit(1);
38
+ }
39
+
40
+ return {
41
+ module: args[moduleIndex + 1],
42
+ appPath: args[appPathIndex + 1],
43
+ };
44
+ }
45
+
46
+ /**
47
+ * Find controller file for given module
48
+ */
49
+ async function findControllerFile(appPath: string, module: string): Promise<string | null> {
50
+ const controllersPath = path.join(appPath, 'src/SmartStack.Api/Controllers');
51
+ const pattern = `**/*${capitalize(module)}*Controller.cs`;
52
+
53
+ try {
54
+ const files = await glob(pattern, { cwd: controllersPath, absolute: true });
55
+ return files.length > 0 ? files[0] : null;
56
+ } catch (error) {
57
+ console.error('Error finding controller:', error);
58
+ return null;
59
+ }
60
+ }
61
+
62
+ /**
63
+ * Extract NavRoute attribute value
64
+ * Example: [NavRoute("platform.administration.users")] → "platform.administration.users"
65
+ */
66
+ function extractNavRoute(content: string): string | null {
67
+ const navRouteRegex = /\[NavRoute\("([^"]+)"\)\]/;
68
+ const match = content.match(navRouteRegex);
69
+ return match ? match[1] : null;
70
+ }
71
+
72
+ /**
73
+ * Extract HTTP method and route suffix
74
+ * Examples:
75
+ * [HttpGet] → { method: 'GET', suffix: '' }
76
+ * [HttpPost("groups")] → { method: 'POST', suffix: '/groups' }
77
+ * [HttpPut("{id}")] → { method: 'PUT', suffix: '/{id}' }
78
+ */
79
+ function extractHttpMethod(line: string): { method: 'GET' | 'POST' | 'PUT' | 'DELETE' | 'PATCH'; suffix: string } | null {
80
+ const methods = ['Get', 'Post', 'Put', 'Delete', 'Patch'];
81
+
82
+ for (const method of methods) {
83
+ // Match [HttpGet] or [HttpGet("path")]
84
+ const regex = new RegExp(`\\[Http${method}(?:\\("([^"]+)"\\))?\\]`);
85
+ const match = line.match(regex);
86
+
87
+ if (match) {
88
+ const suffix = match[1] ? `/${match[1]}` : '';
89
+ return {
90
+ method: method.toUpperCase() as any,
91
+ suffix: suffix,
92
+ };
93
+ }
94
+ }
95
+
96
+ return null;
97
+ }
98
+
99
+ /**
100
+ * Extract permission from RequirePermission attribute
101
+ * Example: [RequirePermission(Permissions.Admin.Users.View)] → "Permissions.Admin.Users.View"
102
+ */
103
+ function extractPermissionConstant(line: string): string | null {
104
+ const requirePermissionRegex = /\[RequirePermission\(([^\)]+)\)\]/;
105
+ const match = line.match(requirePermissionRegex);
106
+
107
+ if (!match) return null;
108
+
109
+ // Handle both single permission and params array
110
+ const permissionArg = match[1].trim();
111
+
112
+ // If it's a params array, take the first one
113
+ if (permissionArg.includes(',')) {
114
+ return permissionArg.split(',')[0].trim();
115
+ }
116
+
117
+ return permissionArg;
118
+ }
119
+
120
+ /**
121
+ * Extract method/handler name from method signature
122
+ * Example: public async Task<ActionResult> GetUsers(...) → "GetUsers"
123
+ */
124
+ function extractHandlerName(line: string): string | null {
125
+ const handlerRegex = /public\s+(?:async\s+)?Task.*?\s+(\w+)\s*\(/;
126
+ const match = line.match(handlerRegex);
127
+ return match ? match[1] : null;
128
+ }
129
+
130
+ /**
131
+ * Load permission constants from Permissions.cs
132
+ * Maps constant paths to their string values
133
+ */
134
+ async function loadPermissionConstants(appPath: string): Promise<Map<string, string>> {
135
+ const permissionsFile = path.join(
136
+ appPath,
137
+ 'src/SmartStack.Application/Common/Authorization/Permissions.cs'
138
+ );
139
+
140
+ if (!fs.existsSync(permissionsFile)) {
141
+ console.warn('Permissions.cs not found, skipping permission resolution');
142
+ return new Map();
143
+ }
144
+
145
+ const content = fs.readFileSync(permissionsFile, 'utf-8');
146
+ const map = new Map<string, string>();
147
+
148
+ // Parse permission constant definitions
149
+ // Example: public const string View = "platform.administration.users.read";
150
+ const lines = content.split('\n');
151
+ let currentNamespace = '';
152
+
153
+ for (const line of lines) {
154
+ // Track nested class structure for namespace building
155
+ const classMatch = line.match(/public\s+(?:static\s+)?class\s+(\w+)/);
156
+ if (classMatch) {
157
+ if (!currentNamespace) {
158
+ currentNamespace = `Permissions.${classMatch[1]}`;
159
+ } else {
160
+ currentNamespace += `.${classMatch[1]}`;
161
+ }
162
+ }
163
+
164
+ // Match const string declarations
165
+ const constMatch = line.match(/public\s+const\s+string\s+(\w+)\s*=\s*"([^"]+)"/);
166
+ if (constMatch && currentNamespace) {
167
+ const constantName = constMatch[1];
168
+ const value = constMatch[2];
169
+ const fullPath = `${currentNamespace}.${constantName}`;
170
+ map.set(fullPath, value);
171
+ }
172
+
173
+ // Reset namespace on closing brace (simplified - assumes proper nesting)
174
+ if (line.trim() === '}' && currentNamespace) {
175
+ const lastDot = currentNamespace.lastIndexOf('.');
176
+ currentNamespace = lastDot > 0 ? currentNamespace.substring(0, lastDot) : '';
177
+ }
178
+ }
179
+
180
+ return map;
181
+ }
182
+
183
+ /**
184
+ * Resolve permission constant to its string value
185
+ */
186
+ function resolvePermission(
187
+ constantPath: string,
188
+ permissionMap: Map<string, string>,
189
+ navRoute: string
190
+ ): string {
191
+ // Direct lookup
192
+ if (permissionMap.has(constantPath)) {
193
+ return permissionMap.get(constantPath)!;
194
+ }
195
+
196
+ // Fallback: infer from constant name
197
+ // Permissions.Admin.Users.View → platform.administration.users.read
198
+ const parts = constantPath.split('.');
199
+ if (parts.length >= 3) {
200
+ const action = parts[parts.length - 1].toLowerCase();
201
+ const actionMap: Record<string, string> = {
202
+ 'view': 'read',
203
+ 'create': 'create',
204
+ 'update': 'update',
205
+ 'delete': 'delete',
206
+ 'assign': 'assign',
207
+ 'execute': 'execute',
208
+ 'export': 'export',
209
+ 'import': 'import',
210
+ };
211
+
212
+ const mappedAction = actionMap[action] || action;
213
+ return `${navRoute}.${mappedAction}`;
214
+ }
215
+
216
+ // Ultimate fallback
217
+ return `${navRoute}.unknown`;
218
+ }
219
+
220
+ /**
221
+ * Parse controller file and extract endpoints
222
+ */
223
+ async function extractEndpoints(
224
+ controllerPath: string,
225
+ permissionMap: Map<string, string>
226
+ ): Promise<DocApiEndpoint[]> {
227
+ const content = fs.readFileSync(controllerPath, 'utf-8');
228
+ const lines = content.split('\n');
229
+ const endpoints: DocApiEndpoint[] = [];
230
+
231
+ // Extract NavRoute for base path
232
+ const navRoute = extractNavRoute(content);
233
+ if (!navRoute) {
234
+ console.warn('NavRoute not found in controller');
235
+ return endpoints;
236
+ }
237
+
238
+ // Build base API path from navRoute
239
+ // "platform.administration.users" → "/api/platform/administration/users"
240
+ const basePath = `/api/${navRoute.replace(/\./g, '/')}`;
241
+
242
+ // Parse line by line to find endpoint methods
243
+ let currentMethod: { method: 'GET' | 'POST' | 'PUT' | 'DELETE' | 'PATCH'; suffix: string } | null = null;
244
+ let currentPermission: string | null = null;
245
+
246
+ for (let i = 0; i < lines.length; i++) {
247
+ const line = lines[i];
248
+
249
+ // Check for HTTP method attribute
250
+ const httpMethod = extractHttpMethod(line);
251
+ if (httpMethod) {
252
+ currentMethod = httpMethod;
253
+ continue;
254
+ }
255
+
256
+ // Check for RequirePermission attribute
257
+ const permissionConstant = extractPermissionConstant(line);
258
+ if (permissionConstant) {
259
+ currentPermission = resolvePermission(permissionConstant, permissionMap, navRoute);
260
+ continue;
261
+ }
262
+
263
+ // Check for method signature (handler name)
264
+ if (currentMethod) {
265
+ const handlerName = extractHandlerName(line);
266
+ if (handlerName) {
267
+ endpoints.push({
268
+ method: currentMethod.method,
269
+ path: basePath + currentMethod.suffix,
270
+ handler: handlerName,
271
+ permission: currentPermission || `${navRoute}.unknown`,
272
+ });
273
+
274
+ // Reset state
275
+ currentMethod = null;
276
+ currentPermission = null;
277
+ }
278
+ }
279
+ }
280
+
281
+ return endpoints;
282
+ }
283
+
284
+ /**
285
+ * Capitalize first letter
286
+ */
287
+ function capitalize(str: string): string {
288
+ return str.charAt(0).toUpperCase() + str.slice(1);
289
+ }
290
+
291
+ /**
292
+ * Main execution
293
+ */
294
+ async function main() {
295
+ const { module, appPath } = parseArgs();
296
+
297
+ console.error(`Extracting API endpoints for module: ${module}`);
298
+ console.error(`App path: ${appPath}`);
299
+
300
+ // Find controller file
301
+ const controllerPath = await findControllerFile(appPath, module);
302
+ if (!controllerPath) {
303
+ console.error(`Controller not found for module: ${module}`);
304
+ process.exit(1);
305
+ }
306
+
307
+ console.error(`Found controller: ${controllerPath}`);
308
+
309
+ // Load permission constants
310
+ const permissionMap = await loadPermissionConstants(appPath);
311
+ console.error(`Loaded ${permissionMap.size} permission constants`);
312
+
313
+ // Extract endpoints
314
+ const endpoints = await extractEndpoints(controllerPath, permissionMap);
315
+
316
+ console.error(`Extracted ${endpoints.length} endpoints`);
317
+
318
+ // Output JSON to stdout
319
+ console.log(JSON.stringify(endpoints, null, 2));
320
+ }
321
+
322
+ main().catch((error) => {
323
+ console.error('Fatal error:', error);
324
+ process.exit(1);
325
+ });