@atlashub/smartstack-cli 3.36.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.
|
|
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 ||
|
|
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
|
+
});
|