@atlashub/smartstack-mcp 1.1.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/README.md +199 -0
- package/config/default-config.json +62 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +2849 -0
- package/dist/index.js.map +1 -0
- package/package.json +65 -0
- package/templates/component.tsx.hbs +298 -0
- package/templates/controller.cs.hbs +166 -0
- package/templates/entity-extension.cs.hbs +87 -0
- package/templates/service-extension.cs.hbs +53 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,2849 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
#!/usr/bin/env node
|
|
3
|
+
|
|
4
|
+
// src/server.ts
|
|
5
|
+
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
|
|
6
|
+
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
7
|
+
import {
|
|
8
|
+
CallToolRequestSchema,
|
|
9
|
+
ListToolsRequestSchema,
|
|
10
|
+
ListResourcesRequestSchema,
|
|
11
|
+
ReadResourceRequestSchema
|
|
12
|
+
} from "@modelcontextprotocol/sdk/types.js";
|
|
13
|
+
|
|
14
|
+
// src/lib/logger.ts
|
|
15
|
+
var Logger = class {
|
|
16
|
+
level = "info";
|
|
17
|
+
levels = {
|
|
18
|
+
debug: 0,
|
|
19
|
+
info: 1,
|
|
20
|
+
warn: 2,
|
|
21
|
+
error: 3
|
|
22
|
+
};
|
|
23
|
+
setLevel(level) {
|
|
24
|
+
this.level = level;
|
|
25
|
+
}
|
|
26
|
+
shouldLog(level) {
|
|
27
|
+
return this.levels[level] >= this.levels[this.level];
|
|
28
|
+
}
|
|
29
|
+
formatEntry(level, message, data) {
|
|
30
|
+
const entry = {
|
|
31
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
32
|
+
level,
|
|
33
|
+
message,
|
|
34
|
+
...data !== void 0 && { data }
|
|
35
|
+
};
|
|
36
|
+
return JSON.stringify(entry);
|
|
37
|
+
}
|
|
38
|
+
debug(message, data) {
|
|
39
|
+
if (this.shouldLog("debug")) {
|
|
40
|
+
console.error(this.formatEntry("debug", message, data));
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
info(message, data) {
|
|
44
|
+
if (this.shouldLog("info")) {
|
|
45
|
+
console.error(this.formatEntry("info", message, data));
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
warn(message, data) {
|
|
49
|
+
if (this.shouldLog("warn")) {
|
|
50
|
+
console.error(this.formatEntry("warn", message, data));
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
error(message, data) {
|
|
54
|
+
if (this.shouldLog("error")) {
|
|
55
|
+
console.error(this.formatEntry("error", message, data));
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
// Tool execution logging
|
|
59
|
+
toolStart(toolName, args) {
|
|
60
|
+
this.info(`Tool started: ${toolName}`, { args });
|
|
61
|
+
}
|
|
62
|
+
toolEnd(toolName, success, duration) {
|
|
63
|
+
this.info(`Tool completed: ${toolName}`, { success, duration });
|
|
64
|
+
}
|
|
65
|
+
toolError(toolName, error) {
|
|
66
|
+
this.error(`Tool failed: ${toolName}`, {
|
|
67
|
+
error: error.message,
|
|
68
|
+
stack: error.stack
|
|
69
|
+
});
|
|
70
|
+
}
|
|
71
|
+
};
|
|
72
|
+
var logger = new Logger();
|
|
73
|
+
var envLevel = process.env.LOG_LEVEL;
|
|
74
|
+
if (envLevel && ["debug", "info", "warn", "error"].includes(envLevel)) {
|
|
75
|
+
logger.setLevel(envLevel);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// src/config.ts
|
|
79
|
+
import path2 from "path";
|
|
80
|
+
|
|
81
|
+
// src/utils/fs.ts
|
|
82
|
+
import fs from "fs-extra";
|
|
83
|
+
import path from "path";
|
|
84
|
+
import { glob } from "glob";
|
|
85
|
+
async function fileExists(filePath) {
|
|
86
|
+
try {
|
|
87
|
+
const stat = await fs.stat(filePath);
|
|
88
|
+
return stat.isFile();
|
|
89
|
+
} catch {
|
|
90
|
+
return false;
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
async function directoryExists(dirPath) {
|
|
94
|
+
try {
|
|
95
|
+
const stat = await fs.stat(dirPath);
|
|
96
|
+
return stat.isDirectory();
|
|
97
|
+
} catch {
|
|
98
|
+
return false;
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
async function ensureDirectory(dirPath) {
|
|
102
|
+
await fs.ensureDir(dirPath);
|
|
103
|
+
}
|
|
104
|
+
async function readJson(filePath) {
|
|
105
|
+
const content = await fs.readFile(filePath, "utf-8");
|
|
106
|
+
return JSON.parse(content);
|
|
107
|
+
}
|
|
108
|
+
async function readText(filePath) {
|
|
109
|
+
return fs.readFile(filePath, "utf-8");
|
|
110
|
+
}
|
|
111
|
+
async function writeText(filePath, content) {
|
|
112
|
+
await fs.ensureDir(path.dirname(filePath));
|
|
113
|
+
await fs.writeFile(filePath, content, "utf-8");
|
|
114
|
+
}
|
|
115
|
+
async function findFiles(pattern, options = {}) {
|
|
116
|
+
const { cwd = process.cwd(), ignore = [] } = options;
|
|
117
|
+
const files = await glob(pattern, {
|
|
118
|
+
cwd,
|
|
119
|
+
ignore: ["**/node_modules/**", "**/bin/**", "**/obj/**", ...ignore],
|
|
120
|
+
absolute: true,
|
|
121
|
+
nodir: true
|
|
122
|
+
});
|
|
123
|
+
return files;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
// src/config.ts
|
|
127
|
+
var defaultConfig = {
|
|
128
|
+
version: "1.0.0",
|
|
129
|
+
smartstack: {
|
|
130
|
+
projectPath: process.env.SMARTSTACK_PROJECT_PATH || "D:/SmartStack.app/02-Develop",
|
|
131
|
+
apiUrl: process.env.SMARTSTACK_API_URL || "https://localhost:5001",
|
|
132
|
+
apiEnabled: process.env.SMARTSTACK_API_ENABLED !== "false"
|
|
133
|
+
},
|
|
134
|
+
conventions: {
|
|
135
|
+
tablePrefix: {
|
|
136
|
+
core: "Core_",
|
|
137
|
+
client: "Client_"
|
|
138
|
+
},
|
|
139
|
+
migrationFormat: "YYYYMMDD_{Prefix}_NNN_{Description}",
|
|
140
|
+
namespaces: {
|
|
141
|
+
domain: "SmartStack.Domain",
|
|
142
|
+
application: "SmartStack.Application",
|
|
143
|
+
infrastructure: "SmartStack.Infrastructure",
|
|
144
|
+
api: "SmartStack.Api"
|
|
145
|
+
},
|
|
146
|
+
servicePattern: {
|
|
147
|
+
interface: "I{Name}Service",
|
|
148
|
+
implementation: "{Name}Service"
|
|
149
|
+
}
|
|
150
|
+
},
|
|
151
|
+
efcore: {
|
|
152
|
+
contexts: [
|
|
153
|
+
{
|
|
154
|
+
name: "ApplicationDbContext",
|
|
155
|
+
projectPath: "auto-detect",
|
|
156
|
+
migrationsFolder: "Migrations"
|
|
157
|
+
}
|
|
158
|
+
],
|
|
159
|
+
validation: {
|
|
160
|
+
checkModelSnapshot: true,
|
|
161
|
+
checkMigrationOrder: true,
|
|
162
|
+
requireBuildSuccess: true
|
|
163
|
+
}
|
|
164
|
+
},
|
|
165
|
+
scaffolding: {
|
|
166
|
+
outputPath: "auto-detect",
|
|
167
|
+
templates: {
|
|
168
|
+
service: "templates/service-extension.cs.hbs",
|
|
169
|
+
entity: "templates/entity-extension.cs.hbs",
|
|
170
|
+
controller: "templates/controller.cs.hbs",
|
|
171
|
+
component: "templates/component.tsx.hbs"
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
};
|
|
175
|
+
var cachedConfig = null;
|
|
176
|
+
async function getConfig() {
|
|
177
|
+
if (cachedConfig) {
|
|
178
|
+
return cachedConfig;
|
|
179
|
+
}
|
|
180
|
+
const configPath = path2.join(process.cwd(), "config", "default-config.json");
|
|
181
|
+
if (await fileExists(configPath)) {
|
|
182
|
+
try {
|
|
183
|
+
const fileConfig = await readJson(configPath);
|
|
184
|
+
cachedConfig = mergeConfig(defaultConfig, fileConfig);
|
|
185
|
+
logger.info("Configuration loaded from file", { path: configPath });
|
|
186
|
+
} catch (error) {
|
|
187
|
+
logger.warn("Failed to load config file, using defaults", { error });
|
|
188
|
+
cachedConfig = defaultConfig;
|
|
189
|
+
}
|
|
190
|
+
} else {
|
|
191
|
+
logger.debug("No config file found, using defaults");
|
|
192
|
+
cachedConfig = defaultConfig;
|
|
193
|
+
}
|
|
194
|
+
if (process.env.SMARTSTACK_PROJECT_PATH) {
|
|
195
|
+
cachedConfig.smartstack.projectPath = process.env.SMARTSTACK_PROJECT_PATH;
|
|
196
|
+
}
|
|
197
|
+
if (process.env.SMARTSTACK_API_URL) {
|
|
198
|
+
cachedConfig.smartstack.apiUrl = process.env.SMARTSTACK_API_URL;
|
|
199
|
+
}
|
|
200
|
+
return cachedConfig;
|
|
201
|
+
}
|
|
202
|
+
function mergeConfig(base, override) {
|
|
203
|
+
return {
|
|
204
|
+
...base,
|
|
205
|
+
...override,
|
|
206
|
+
smartstack: {
|
|
207
|
+
...base.smartstack,
|
|
208
|
+
...override.smartstack
|
|
209
|
+
},
|
|
210
|
+
conventions: {
|
|
211
|
+
...base.conventions,
|
|
212
|
+
...override.conventions,
|
|
213
|
+
tablePrefix: {
|
|
214
|
+
...base.conventions.tablePrefix,
|
|
215
|
+
...override.conventions?.tablePrefix
|
|
216
|
+
},
|
|
217
|
+
namespaces: {
|
|
218
|
+
...base.conventions.namespaces,
|
|
219
|
+
...override.conventions?.namespaces
|
|
220
|
+
},
|
|
221
|
+
servicePattern: {
|
|
222
|
+
...base.conventions.servicePattern,
|
|
223
|
+
...override.conventions?.servicePattern
|
|
224
|
+
}
|
|
225
|
+
},
|
|
226
|
+
efcore: {
|
|
227
|
+
...base.efcore,
|
|
228
|
+
...override.efcore,
|
|
229
|
+
contexts: override.efcore?.contexts || base.efcore.contexts,
|
|
230
|
+
validation: {
|
|
231
|
+
...base.efcore.validation,
|
|
232
|
+
...override.efcore?.validation
|
|
233
|
+
}
|
|
234
|
+
},
|
|
235
|
+
scaffolding: {
|
|
236
|
+
...base.scaffolding,
|
|
237
|
+
...override.scaffolding,
|
|
238
|
+
templates: {
|
|
239
|
+
...base.scaffolding.templates,
|
|
240
|
+
...override.scaffolding?.templates
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
};
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
// src/types/index.ts
|
|
247
|
+
import { z } from "zod";
|
|
248
|
+
var SmartStackConfigSchema = z.object({
|
|
249
|
+
projectPath: z.string(),
|
|
250
|
+
apiUrl: z.string().url().optional(),
|
|
251
|
+
apiEnabled: z.boolean().default(true)
|
|
252
|
+
});
|
|
253
|
+
var ConventionsConfigSchema = z.object({
|
|
254
|
+
schemas: z.object({
|
|
255
|
+
platform: z.string().default("core"),
|
|
256
|
+
extensions: z.string().default("extensions")
|
|
257
|
+
}),
|
|
258
|
+
tablePrefixes: z.array(z.string()).default([
|
|
259
|
+
"auth_",
|
|
260
|
+
"nav_",
|
|
261
|
+
"usr_",
|
|
262
|
+
"ai_",
|
|
263
|
+
"cfg_",
|
|
264
|
+
"wkf_",
|
|
265
|
+
"support_",
|
|
266
|
+
"entra_",
|
|
267
|
+
"ref_",
|
|
268
|
+
"loc_",
|
|
269
|
+
"lic_"
|
|
270
|
+
]),
|
|
271
|
+
migrationFormat: z.string().default("YYYYMMDD_NNN_{Description}"),
|
|
272
|
+
namespaces: z.object({
|
|
273
|
+
domain: z.string(),
|
|
274
|
+
application: z.string(),
|
|
275
|
+
infrastructure: z.string(),
|
|
276
|
+
api: z.string()
|
|
277
|
+
}),
|
|
278
|
+
servicePattern: z.object({
|
|
279
|
+
interface: z.string().default("I{Name}Service"),
|
|
280
|
+
implementation: z.string().default("{Name}Service")
|
|
281
|
+
})
|
|
282
|
+
});
|
|
283
|
+
var EfCoreContextSchema = z.object({
|
|
284
|
+
name: z.string(),
|
|
285
|
+
projectPath: z.string(),
|
|
286
|
+
migrationsFolder: z.string().default("Migrations")
|
|
287
|
+
});
|
|
288
|
+
var EfCoreConfigSchema = z.object({
|
|
289
|
+
contexts: z.array(EfCoreContextSchema),
|
|
290
|
+
validation: z.object({
|
|
291
|
+
checkModelSnapshot: z.boolean().default(true),
|
|
292
|
+
checkMigrationOrder: z.boolean().default(true),
|
|
293
|
+
requireBuildSuccess: z.boolean().default(true)
|
|
294
|
+
})
|
|
295
|
+
});
|
|
296
|
+
var ScaffoldingConfigSchema = z.object({
|
|
297
|
+
outputPath: z.string(),
|
|
298
|
+
templates: z.object({
|
|
299
|
+
service: z.string(),
|
|
300
|
+
entity: z.string(),
|
|
301
|
+
controller: z.string(),
|
|
302
|
+
component: z.string()
|
|
303
|
+
})
|
|
304
|
+
});
|
|
305
|
+
var ConfigSchema = z.object({
|
|
306
|
+
version: z.string(),
|
|
307
|
+
smartstack: SmartStackConfigSchema,
|
|
308
|
+
conventions: ConventionsConfigSchema,
|
|
309
|
+
efcore: EfCoreConfigSchema,
|
|
310
|
+
scaffolding: ScaffoldingConfigSchema
|
|
311
|
+
});
|
|
312
|
+
var ValidateConventionsInputSchema = z.object({
|
|
313
|
+
path: z.string().optional().describe("Project path to validate (default: SmartStack.app path)"),
|
|
314
|
+
checks: z.array(z.enum(["tables", "migrations", "services", "namespaces", "all"])).default(["all"]).describe("Types of checks to perform")
|
|
315
|
+
});
|
|
316
|
+
var CheckMigrationsInputSchema = z.object({
|
|
317
|
+
projectPath: z.string().optional().describe("EF Core project path"),
|
|
318
|
+
branch: z.string().optional().describe("Git branch to check (default: current)"),
|
|
319
|
+
compareBranch: z.string().optional().describe("Branch to compare against")
|
|
320
|
+
});
|
|
321
|
+
var ScaffoldExtensionInputSchema = z.object({
|
|
322
|
+
type: z.enum(["service", "entity", "controller", "component"]).describe("Type of extension to scaffold"),
|
|
323
|
+
name: z.string().describe('Name of the extension (e.g., "UserProfile", "Order")'),
|
|
324
|
+
options: z.object({
|
|
325
|
+
namespace: z.string().optional().describe("Custom namespace"),
|
|
326
|
+
baseEntity: z.string().optional().describe("Base entity to extend (for entity type)"),
|
|
327
|
+
methods: z.array(z.string()).optional().describe("Methods to generate (for service type)"),
|
|
328
|
+
outputPath: z.string().optional().describe("Custom output path")
|
|
329
|
+
}).optional()
|
|
330
|
+
});
|
|
331
|
+
var ApiDocsInputSchema = z.object({
|
|
332
|
+
endpoint: z.string().optional().describe('Filter by endpoint path (e.g., "/api/users")'),
|
|
333
|
+
format: z.enum(["markdown", "json", "openapi"]).default("markdown").describe("Output format"),
|
|
334
|
+
controller: z.string().optional().describe("Filter by controller name")
|
|
335
|
+
});
|
|
336
|
+
|
|
337
|
+
// src/lib/detector.ts
|
|
338
|
+
import path4 from "path";
|
|
339
|
+
|
|
340
|
+
// src/utils/git.ts
|
|
341
|
+
import { exec } from "child_process";
|
|
342
|
+
import { promisify } from "util";
|
|
343
|
+
import path3 from "path";
|
|
344
|
+
var execAsync = promisify(exec);
|
|
345
|
+
async function git(command, cwd) {
|
|
346
|
+
const options = cwd ? { cwd } : {};
|
|
347
|
+
const { stdout } = await execAsync(`git ${command}`, options);
|
|
348
|
+
return stdout.trim();
|
|
349
|
+
}
|
|
350
|
+
async function isGitRepo(cwd) {
|
|
351
|
+
const gitDir = path3.join(cwd || process.cwd(), ".git");
|
|
352
|
+
return directoryExists(gitDir);
|
|
353
|
+
}
|
|
354
|
+
async function getCurrentBranch(cwd) {
|
|
355
|
+
try {
|
|
356
|
+
return await git("branch --show-current", cwd);
|
|
357
|
+
} catch {
|
|
358
|
+
return null;
|
|
359
|
+
}
|
|
360
|
+
}
|
|
361
|
+
async function branchExists(branch, cwd) {
|
|
362
|
+
try {
|
|
363
|
+
await git(`rev-parse --verify ${branch}`, cwd);
|
|
364
|
+
return true;
|
|
365
|
+
} catch {
|
|
366
|
+
return false;
|
|
367
|
+
}
|
|
368
|
+
}
|
|
369
|
+
async function getFileFromBranch(branch, filePath, cwd) {
|
|
370
|
+
try {
|
|
371
|
+
return await git(`show ${branch}:${filePath}`, cwd);
|
|
372
|
+
} catch {
|
|
373
|
+
return null;
|
|
374
|
+
}
|
|
375
|
+
}
|
|
376
|
+
async function getDiff(fromBranch, toBranch, filePath, cwd) {
|
|
377
|
+
try {
|
|
378
|
+
const pathArg = filePath ? ` -- ${filePath}` : "";
|
|
379
|
+
return await git(`diff ${fromBranch}...${toBranch}${pathArg}`, cwd);
|
|
380
|
+
} catch {
|
|
381
|
+
return "";
|
|
382
|
+
}
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
// src/utils/dotnet.ts
|
|
386
|
+
import { exec as exec2 } from "child_process";
|
|
387
|
+
import { promisify as promisify2 } from "util";
|
|
388
|
+
var execAsync2 = promisify2(exec2);
|
|
389
|
+
async function findCsprojFiles(cwd) {
|
|
390
|
+
return findFiles("**/*.csproj", { cwd: cwd || process.cwd() });
|
|
391
|
+
}
|
|
392
|
+
async function hasEfCore(csprojPath) {
|
|
393
|
+
try {
|
|
394
|
+
const content = await readText(csprojPath);
|
|
395
|
+
return content.includes("Microsoft.EntityFrameworkCore");
|
|
396
|
+
} catch {
|
|
397
|
+
return false;
|
|
398
|
+
}
|
|
399
|
+
}
|
|
400
|
+
async function findDbContextName(projectPath) {
|
|
401
|
+
try {
|
|
402
|
+
const csFiles = await findFiles("**/*.cs", { cwd: projectPath });
|
|
403
|
+
for (const file of csFiles) {
|
|
404
|
+
const content = await readText(file);
|
|
405
|
+
const match = content.match(/class\s+(\w+)\s*:\s*(?:\w+,\s*)*DbContext/);
|
|
406
|
+
if (match) {
|
|
407
|
+
return match[1];
|
|
408
|
+
}
|
|
409
|
+
}
|
|
410
|
+
return null;
|
|
411
|
+
} catch {
|
|
412
|
+
return null;
|
|
413
|
+
}
|
|
414
|
+
}
|
|
415
|
+
async function getTargetFramework(csprojPath) {
|
|
416
|
+
try {
|
|
417
|
+
const content = await readText(csprojPath);
|
|
418
|
+
const match = content.match(/<TargetFramework>([^<]+)<\/TargetFramework>/);
|
|
419
|
+
return match ? match[1] : null;
|
|
420
|
+
} catch {
|
|
421
|
+
return null;
|
|
422
|
+
}
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
// src/lib/detector.ts
|
|
426
|
+
async function detectProject(projectPath) {
|
|
427
|
+
logger.debug("Detecting project info", { path: projectPath });
|
|
428
|
+
const info = {
|
|
429
|
+
name: path4.basename(projectPath),
|
|
430
|
+
version: "0.0.0",
|
|
431
|
+
isGitRepo: false,
|
|
432
|
+
hasDotNet: false,
|
|
433
|
+
hasEfCore: false,
|
|
434
|
+
hasReact: false,
|
|
435
|
+
csprojFiles: []
|
|
436
|
+
};
|
|
437
|
+
if (!await directoryExists(projectPath)) {
|
|
438
|
+
logger.warn("Project path does not exist", { path: projectPath });
|
|
439
|
+
return info;
|
|
440
|
+
}
|
|
441
|
+
info.isGitRepo = await isGitRepo(projectPath);
|
|
442
|
+
if (info.isGitRepo) {
|
|
443
|
+
info.currentBranch = await getCurrentBranch(projectPath) || void 0;
|
|
444
|
+
}
|
|
445
|
+
info.csprojFiles = await findCsprojFiles(projectPath);
|
|
446
|
+
info.hasDotNet = info.csprojFiles.length > 0;
|
|
447
|
+
if (info.hasDotNet) {
|
|
448
|
+
for (const csproj of info.csprojFiles) {
|
|
449
|
+
if (await hasEfCore(csproj)) {
|
|
450
|
+
info.hasEfCore = true;
|
|
451
|
+
info.dbContextName = await findDbContextName(path4.dirname(csproj)) || void 0;
|
|
452
|
+
break;
|
|
453
|
+
}
|
|
454
|
+
}
|
|
455
|
+
}
|
|
456
|
+
const packageJsonPath = path4.join(projectPath, "web", "smartstack-web", "package.json");
|
|
457
|
+
if (await fileExists(packageJsonPath)) {
|
|
458
|
+
try {
|
|
459
|
+
const packageJson = JSON.parse(await readText(packageJsonPath));
|
|
460
|
+
info.hasReact = !!packageJson.dependencies?.react;
|
|
461
|
+
info.version = packageJson.version || info.version;
|
|
462
|
+
} catch {
|
|
463
|
+
}
|
|
464
|
+
}
|
|
465
|
+
if (info.csprojFiles.length > 0) {
|
|
466
|
+
const mainCsproj = info.csprojFiles.find((f) => f.includes("SmartStack.Api")) || info.csprojFiles[0];
|
|
467
|
+
const targetFramework = await getTargetFramework(mainCsproj);
|
|
468
|
+
if (targetFramework) {
|
|
469
|
+
logger.debug("Target framework detected", { framework: targetFramework });
|
|
470
|
+
}
|
|
471
|
+
}
|
|
472
|
+
logger.info("Project detected", info);
|
|
473
|
+
return info;
|
|
474
|
+
}
|
|
475
|
+
async function findSmartStackStructure(projectPath) {
|
|
476
|
+
const structure = { root: projectPath };
|
|
477
|
+
const csprojFiles = await findCsprojFiles(projectPath);
|
|
478
|
+
for (const csproj of csprojFiles) {
|
|
479
|
+
const projectName = path4.basename(csproj, ".csproj").toLowerCase();
|
|
480
|
+
const projectDir = path4.dirname(csproj);
|
|
481
|
+
if (projectName.includes("domain")) {
|
|
482
|
+
structure.domain = projectDir;
|
|
483
|
+
} else if (projectName.includes("application")) {
|
|
484
|
+
structure.application = projectDir;
|
|
485
|
+
} else if (projectName.includes("infrastructure")) {
|
|
486
|
+
structure.infrastructure = projectDir;
|
|
487
|
+
} else if (projectName.includes("api")) {
|
|
488
|
+
structure.api = projectDir;
|
|
489
|
+
}
|
|
490
|
+
}
|
|
491
|
+
if (structure.infrastructure) {
|
|
492
|
+
const migrationsPath = path4.join(structure.infrastructure, "Persistence", "Migrations");
|
|
493
|
+
if (await directoryExists(migrationsPath)) {
|
|
494
|
+
structure.migrations = migrationsPath;
|
|
495
|
+
}
|
|
496
|
+
}
|
|
497
|
+
const webPath = path4.join(projectPath, "web", "smartstack-web");
|
|
498
|
+
if (await directoryExists(webPath)) {
|
|
499
|
+
structure.web = webPath;
|
|
500
|
+
}
|
|
501
|
+
return structure;
|
|
502
|
+
}
|
|
503
|
+
async function findEntityFiles(domainPath) {
|
|
504
|
+
const entityFiles = await findFiles("**/*.cs", { cwd: domainPath });
|
|
505
|
+
const entities = [];
|
|
506
|
+
for (const file of entityFiles) {
|
|
507
|
+
const content = await readText(file);
|
|
508
|
+
if (content.match(/public\s+(?:class|record)\s+\w+/) && content.match(/public\s+(?:int|long|Guid|string)\s+Id\s*\{/)) {
|
|
509
|
+
entities.push(file);
|
|
510
|
+
}
|
|
511
|
+
}
|
|
512
|
+
return entities;
|
|
513
|
+
}
|
|
514
|
+
async function findControllerFiles(apiPath) {
|
|
515
|
+
const files = await findFiles("**/*Controller.cs", { cwd: apiPath });
|
|
516
|
+
return files;
|
|
517
|
+
}
|
|
518
|
+
|
|
519
|
+
// src/tools/validate-conventions.ts
|
|
520
|
+
import path5 from "path";
|
|
521
|
+
var validateConventionsTool = {
|
|
522
|
+
name: "validate_conventions",
|
|
523
|
+
description: "Validate AtlasHub/SmartStack conventions: SQL schemas (core/extensions), domain table prefixes (auth_, nav_, ai_, etc.), migration naming (YYYYMMDD_NNN_*), service interfaces (I*Service), namespace structure",
|
|
524
|
+
inputSchema: {
|
|
525
|
+
type: "object",
|
|
526
|
+
properties: {
|
|
527
|
+
path: {
|
|
528
|
+
type: "string",
|
|
529
|
+
description: "Project path to validate (default: SmartStack.app path from config)"
|
|
530
|
+
},
|
|
531
|
+
checks: {
|
|
532
|
+
type: "array",
|
|
533
|
+
items: {
|
|
534
|
+
type: "string",
|
|
535
|
+
enum: ["tables", "migrations", "services", "namespaces", "all"]
|
|
536
|
+
},
|
|
537
|
+
description: "Types of checks to perform",
|
|
538
|
+
default: ["all"]
|
|
539
|
+
}
|
|
540
|
+
}
|
|
541
|
+
}
|
|
542
|
+
};
|
|
543
|
+
async function handleValidateConventions(args, config) {
|
|
544
|
+
const input = ValidateConventionsInputSchema.parse(args);
|
|
545
|
+
const projectPath = input.path || config.smartstack.projectPath;
|
|
546
|
+
const checks = input.checks.includes("all") ? ["tables", "migrations", "services", "namespaces"] : input.checks;
|
|
547
|
+
logger.info("Validating conventions", { projectPath, checks });
|
|
548
|
+
const result = {
|
|
549
|
+
valid: true,
|
|
550
|
+
errors: [],
|
|
551
|
+
warnings: [],
|
|
552
|
+
summary: ""
|
|
553
|
+
};
|
|
554
|
+
const structure = await findSmartStackStructure(projectPath);
|
|
555
|
+
if (checks.includes("tables")) {
|
|
556
|
+
await validateTablePrefixes(structure, config, result);
|
|
557
|
+
}
|
|
558
|
+
if (checks.includes("migrations")) {
|
|
559
|
+
await validateMigrationNaming(structure, config, result);
|
|
560
|
+
}
|
|
561
|
+
if (checks.includes("services")) {
|
|
562
|
+
await validateServiceInterfaces(structure, config, result);
|
|
563
|
+
}
|
|
564
|
+
if (checks.includes("namespaces")) {
|
|
565
|
+
await validateNamespaces(structure, config, result);
|
|
566
|
+
}
|
|
567
|
+
result.valid = result.errors.length === 0;
|
|
568
|
+
result.summary = generateSummary(result, checks);
|
|
569
|
+
return formatResult(result);
|
|
570
|
+
}
|
|
571
|
+
async function validateTablePrefixes(structure, config, result) {
|
|
572
|
+
if (!structure.infrastructure) {
|
|
573
|
+
result.warnings.push({
|
|
574
|
+
type: "warning",
|
|
575
|
+
category: "tables",
|
|
576
|
+
message: "Infrastructure project not found, skipping table validation"
|
|
577
|
+
});
|
|
578
|
+
return;
|
|
579
|
+
}
|
|
580
|
+
const configFiles = await findFiles("**/Configurations/**/*.cs", {
|
|
581
|
+
cwd: structure.infrastructure
|
|
582
|
+
});
|
|
583
|
+
const validSchemas = [config.conventions.schemas.platform, config.conventions.schemas.extensions];
|
|
584
|
+
const validPrefixes = config.conventions.tablePrefixes;
|
|
585
|
+
const schemaConstantsMap = {
|
|
586
|
+
"SchemaConstants.Core": config.conventions.schemas.platform,
|
|
587
|
+
"SchemaConstants.Extensions": config.conventions.schemas.extensions
|
|
588
|
+
};
|
|
589
|
+
for (const file of configFiles) {
|
|
590
|
+
const content = await readText(file);
|
|
591
|
+
const tableWithStringSchemaMatches = content.matchAll(/\.ToTable\s*\(\s*"([^"]+)"\s*,\s*"([^"]+)"\s*\)/g);
|
|
592
|
+
for (const match of tableWithStringSchemaMatches) {
|
|
593
|
+
const tableName = match[1];
|
|
594
|
+
const schemaName = match[2];
|
|
595
|
+
if (!validSchemas.includes(schemaName)) {
|
|
596
|
+
result.errors.push({
|
|
597
|
+
type: "error",
|
|
598
|
+
category: "tables",
|
|
599
|
+
message: `Table "${tableName}" uses invalid schema "${schemaName}"`,
|
|
600
|
+
file: path5.relative(structure.root, file),
|
|
601
|
+
suggestion: `Use schema "${config.conventions.schemas.platform}" for SmartStack tables or "${config.conventions.schemas.extensions}" for client extensions`
|
|
602
|
+
});
|
|
603
|
+
}
|
|
604
|
+
const hasValidPrefix = validPrefixes.some((prefix) => tableName.startsWith(prefix));
|
|
605
|
+
if (!hasValidPrefix && !tableName.startsWith("__")) {
|
|
606
|
+
result.warnings.push({
|
|
607
|
+
type: "warning",
|
|
608
|
+
category: "tables",
|
|
609
|
+
message: `Table "${tableName}" does not use a standard domain prefix`,
|
|
610
|
+
file: path5.relative(structure.root, file),
|
|
611
|
+
suggestion: `Consider using a domain prefix: ${validPrefixes.slice(0, 5).join(", ")}, etc.`
|
|
612
|
+
});
|
|
613
|
+
}
|
|
614
|
+
}
|
|
615
|
+
const tableWithConstantSchemaMatches = content.matchAll(/\.ToTable\s*\(\s*"([^"]+)"\s*,\s*(SchemaConstants\.\w+)\s*\)/g);
|
|
616
|
+
for (const match of tableWithConstantSchemaMatches) {
|
|
617
|
+
const tableName = match[1];
|
|
618
|
+
const schemaConstant = match[2];
|
|
619
|
+
const resolvedSchema = schemaConstantsMap[schemaConstant];
|
|
620
|
+
if (!resolvedSchema) {
|
|
621
|
+
result.errors.push({
|
|
622
|
+
type: "error",
|
|
623
|
+
category: "tables",
|
|
624
|
+
message: `Table "${tableName}" uses unknown schema constant "${schemaConstant}"`,
|
|
625
|
+
file: path5.relative(structure.root, file),
|
|
626
|
+
suggestion: `Use SchemaConstants.Core for SmartStack tables or SchemaConstants.Extensions for client extensions`
|
|
627
|
+
});
|
|
628
|
+
}
|
|
629
|
+
const hasValidPrefix = validPrefixes.some((prefix) => tableName.startsWith(prefix));
|
|
630
|
+
if (!hasValidPrefix && !tableName.startsWith("__")) {
|
|
631
|
+
result.warnings.push({
|
|
632
|
+
type: "warning",
|
|
633
|
+
category: "tables",
|
|
634
|
+
message: `Table "${tableName}" does not use a standard domain prefix`,
|
|
635
|
+
file: path5.relative(structure.root, file),
|
|
636
|
+
suggestion: `Consider using a domain prefix: ${validPrefixes.slice(0, 5).join(", ")}, etc.`
|
|
637
|
+
});
|
|
638
|
+
}
|
|
639
|
+
}
|
|
640
|
+
const tableWithoutSchemaMatches = content.matchAll(/\.ToTable\s*\(\s*"([^"]+)"\s*\)(?!\s*,)/g);
|
|
641
|
+
for (const match of tableWithoutSchemaMatches) {
|
|
642
|
+
const tableName = match[1];
|
|
643
|
+
if (!tableName.startsWith("__")) {
|
|
644
|
+
result.errors.push({
|
|
645
|
+
type: "error",
|
|
646
|
+
category: "tables",
|
|
647
|
+
message: `Table "${tableName}" is missing schema specification`,
|
|
648
|
+
file: path5.relative(structure.root, file),
|
|
649
|
+
suggestion: `Add schema: .ToTable("${tableName}", SchemaConstants.Core)`
|
|
650
|
+
});
|
|
651
|
+
}
|
|
652
|
+
}
|
|
653
|
+
}
|
|
654
|
+
}
|
|
655
|
+
async function validateMigrationNaming(structure, _config, result) {
|
|
656
|
+
if (!structure.migrations) {
|
|
657
|
+
result.warnings.push({
|
|
658
|
+
type: "warning",
|
|
659
|
+
category: "migrations",
|
|
660
|
+
message: "Migrations folder not found, skipping migration validation"
|
|
661
|
+
});
|
|
662
|
+
return;
|
|
663
|
+
}
|
|
664
|
+
const migrationFiles = await findFiles("*.cs", { cwd: structure.migrations });
|
|
665
|
+
const migrationPattern = /^(\d{8})_(\d{3})_(.+)\.cs$/;
|
|
666
|
+
const designerPattern = /\.Designer\.cs$/;
|
|
667
|
+
for (const file of migrationFiles) {
|
|
668
|
+
const fileName = path5.basename(file);
|
|
669
|
+
if (designerPattern.test(fileName) || fileName.includes("ModelSnapshot")) {
|
|
670
|
+
continue;
|
|
671
|
+
}
|
|
672
|
+
if (!migrationPattern.test(fileName)) {
|
|
673
|
+
result.errors.push({
|
|
674
|
+
type: "error",
|
|
675
|
+
category: "migrations",
|
|
676
|
+
message: `Migration "${fileName}" does not follow naming convention`,
|
|
677
|
+
file: path5.relative(structure.root, file),
|
|
678
|
+
suggestion: `Expected format: YYYYMMDD_NNN_Description.cs (e.g., 20260115_001_InitialCreate.cs)`
|
|
679
|
+
});
|
|
680
|
+
}
|
|
681
|
+
}
|
|
682
|
+
const orderedMigrations = migrationFiles.map((f) => path5.basename(f)).filter((f) => migrationPattern.test(f) && !f.includes("Designer")).sort();
|
|
683
|
+
for (let i = 1; i < orderedMigrations.length; i++) {
|
|
684
|
+
const prev = orderedMigrations[i - 1];
|
|
685
|
+
const curr = orderedMigrations[i];
|
|
686
|
+
const prevDate = prev.substring(0, 8);
|
|
687
|
+
const currDate = curr.substring(0, 8);
|
|
688
|
+
if (currDate < prevDate) {
|
|
689
|
+
result.warnings.push({
|
|
690
|
+
type: "warning",
|
|
691
|
+
category: "migrations",
|
|
692
|
+
message: `Migration order issue: "${curr}" dated before "${prev}"`
|
|
693
|
+
});
|
|
694
|
+
}
|
|
695
|
+
}
|
|
696
|
+
}
|
|
697
|
+
async function validateServiceInterfaces(structure, config, result) {
|
|
698
|
+
if (!structure.application) {
|
|
699
|
+
result.warnings.push({
|
|
700
|
+
type: "warning",
|
|
701
|
+
category: "services",
|
|
702
|
+
message: "Application project not found, skipping service validation"
|
|
703
|
+
});
|
|
704
|
+
return;
|
|
705
|
+
}
|
|
706
|
+
const serviceFiles = await findFiles("**/*Service.cs", {
|
|
707
|
+
cwd: structure.application
|
|
708
|
+
});
|
|
709
|
+
for (const file of serviceFiles) {
|
|
710
|
+
const content = await readText(file);
|
|
711
|
+
const fileName = path5.basename(file, ".cs");
|
|
712
|
+
if (fileName.startsWith("I")) continue;
|
|
713
|
+
const expectedInterface = `I${fileName}`;
|
|
714
|
+
const interfacePattern = new RegExp(`:\\s*${expectedInterface}\\b`);
|
|
715
|
+
if (!interfacePattern.test(content)) {
|
|
716
|
+
const declaresInterface = content.includes(`interface ${expectedInterface}`);
|
|
717
|
+
if (!declaresInterface) {
|
|
718
|
+
result.warnings.push({
|
|
719
|
+
type: "warning",
|
|
720
|
+
category: "services",
|
|
721
|
+
message: `Service "${fileName}" should implement "${expectedInterface}"`,
|
|
722
|
+
file: path5.relative(structure.root, file),
|
|
723
|
+
suggestion: `Create interface ${expectedInterface} and implement it`
|
|
724
|
+
});
|
|
725
|
+
}
|
|
726
|
+
}
|
|
727
|
+
}
|
|
728
|
+
}
|
|
729
|
+
async function validateNamespaces(structure, config, result) {
|
|
730
|
+
const layers = [
|
|
731
|
+
{ path: structure.domain, expected: config.conventions.namespaces.domain, name: "Domain" },
|
|
732
|
+
{ path: structure.application, expected: config.conventions.namespaces.application, name: "Application" },
|
|
733
|
+
{ path: structure.infrastructure, expected: config.conventions.namespaces.infrastructure, name: "Infrastructure" },
|
|
734
|
+
{ path: structure.api, expected: config.conventions.namespaces.api, name: "Api" }
|
|
735
|
+
];
|
|
736
|
+
for (const layer of layers) {
|
|
737
|
+
if (!layer.path) continue;
|
|
738
|
+
const csFiles = await findFiles("**/*.cs", { cwd: layer.path });
|
|
739
|
+
for (const file of csFiles.slice(0, 10)) {
|
|
740
|
+
const content = await readText(file);
|
|
741
|
+
const namespaceMatch = content.match(/namespace\s+([\w.]+)/);
|
|
742
|
+
if (namespaceMatch) {
|
|
743
|
+
const namespace = namespaceMatch[1];
|
|
744
|
+
if (!namespace.startsWith(layer.expected)) {
|
|
745
|
+
result.errors.push({
|
|
746
|
+
type: "error",
|
|
747
|
+
category: "namespaces",
|
|
748
|
+
message: `${layer.name} file has incorrect namespace "${namespace}"`,
|
|
749
|
+
file: path5.relative(structure.root, file),
|
|
750
|
+
suggestion: `Should start with "${layer.expected}"`
|
|
751
|
+
});
|
|
752
|
+
}
|
|
753
|
+
}
|
|
754
|
+
}
|
|
755
|
+
}
|
|
756
|
+
}
|
|
757
|
+
function generateSummary(result, checks) {
|
|
758
|
+
const parts = [];
|
|
759
|
+
parts.push(`Checks performed: ${checks.join(", ")}`);
|
|
760
|
+
parts.push(`Errors: ${result.errors.length}`);
|
|
761
|
+
parts.push(`Warnings: ${result.warnings.length}`);
|
|
762
|
+
parts.push(`Status: ${result.valid ? "PASSED" : "FAILED"}`);
|
|
763
|
+
return parts.join(" | ");
|
|
764
|
+
}
|
|
765
|
+
function formatResult(result) {
|
|
766
|
+
const lines = [];
|
|
767
|
+
lines.push("# Conventions Validation Report");
|
|
768
|
+
lines.push("");
|
|
769
|
+
lines.push(`## Summary`);
|
|
770
|
+
lines.push(`- **Status**: ${result.valid ? "\u2705 PASSED" : "\u274C FAILED"}`);
|
|
771
|
+
lines.push(`- **Errors**: ${result.errors.length}`);
|
|
772
|
+
lines.push(`- **Warnings**: ${result.warnings.length}`);
|
|
773
|
+
lines.push("");
|
|
774
|
+
if (result.errors.length > 0) {
|
|
775
|
+
lines.push("## Errors");
|
|
776
|
+
lines.push("");
|
|
777
|
+
for (const error of result.errors) {
|
|
778
|
+
lines.push(`### \u274C ${error.category}: ${error.message}`);
|
|
779
|
+
if (error.file) lines.push(`- **File**: \`${error.file}\``);
|
|
780
|
+
if (error.line) lines.push(`- **Line**: ${error.line}`);
|
|
781
|
+
if (error.suggestion) lines.push(`- **Suggestion**: ${error.suggestion}`);
|
|
782
|
+
lines.push("");
|
|
783
|
+
}
|
|
784
|
+
}
|
|
785
|
+
if (result.warnings.length > 0) {
|
|
786
|
+
lines.push("## Warnings");
|
|
787
|
+
lines.push("");
|
|
788
|
+
for (const warning of result.warnings) {
|
|
789
|
+
lines.push(`### \u26A0\uFE0F ${warning.category}: ${warning.message}`);
|
|
790
|
+
if (warning.file) lines.push(`- **File**: \`${warning.file}\``);
|
|
791
|
+
if (warning.suggestion) lines.push(`- **Suggestion**: ${warning.suggestion}`);
|
|
792
|
+
lines.push("");
|
|
793
|
+
}
|
|
794
|
+
}
|
|
795
|
+
if (result.errors.length === 0 && result.warnings.length === 0) {
|
|
796
|
+
lines.push("## Result");
|
|
797
|
+
lines.push("");
|
|
798
|
+
lines.push("All conventions validated successfully! \u2728");
|
|
799
|
+
}
|
|
800
|
+
return lines.join("\n");
|
|
801
|
+
}
|
|
802
|
+
|
|
803
|
+
// src/tools/check-migrations.ts
|
|
804
|
+
import path6 from "path";
|
|
805
|
+
var checkMigrationsTool = {
|
|
806
|
+
name: "check_migrations",
|
|
807
|
+
description: "Analyze EF Core migrations for conflicts, ordering issues, and ModelSnapshot discrepancies between branches",
|
|
808
|
+
inputSchema: {
|
|
809
|
+
type: "object",
|
|
810
|
+
properties: {
|
|
811
|
+
projectPath: {
|
|
812
|
+
type: "string",
|
|
813
|
+
description: "EF Core project path (default: auto-detect from config)"
|
|
814
|
+
},
|
|
815
|
+
branch: {
|
|
816
|
+
type: "string",
|
|
817
|
+
description: "Git branch to check (default: current branch)"
|
|
818
|
+
},
|
|
819
|
+
compareBranch: {
|
|
820
|
+
type: "string",
|
|
821
|
+
description: 'Branch to compare against (e.g., "develop")'
|
|
822
|
+
}
|
|
823
|
+
}
|
|
824
|
+
}
|
|
825
|
+
};
|
|
826
|
+
async function handleCheckMigrations(args, config) {
|
|
827
|
+
const input = CheckMigrationsInputSchema.parse(args);
|
|
828
|
+
const projectPath = input.projectPath || config.smartstack.projectPath;
|
|
829
|
+
logger.info("Checking migrations", { projectPath, branch: input.branch });
|
|
830
|
+
const structure = await findSmartStackStructure(projectPath);
|
|
831
|
+
if (!structure.migrations) {
|
|
832
|
+
return "# Migration Check\n\n\u274C No migrations folder found in the project.";
|
|
833
|
+
}
|
|
834
|
+
const result = {
|
|
835
|
+
hasConflicts: false,
|
|
836
|
+
migrations: [],
|
|
837
|
+
conflicts: [],
|
|
838
|
+
suggestions: []
|
|
839
|
+
};
|
|
840
|
+
const currentBranch = input.branch || await getCurrentBranch(projectPath) || "unknown";
|
|
841
|
+
result.migrations = await parseMigrations(structure.migrations, structure.root);
|
|
842
|
+
checkNamingConventions(result, config);
|
|
843
|
+
checkChronologicalOrder(result);
|
|
844
|
+
if (input.compareBranch && await branchExists(input.compareBranch, projectPath)) {
|
|
845
|
+
await checkBranchConflicts(
|
|
846
|
+
result,
|
|
847
|
+
structure,
|
|
848
|
+
currentBranch,
|
|
849
|
+
input.compareBranch,
|
|
850
|
+
projectPath
|
|
851
|
+
);
|
|
852
|
+
}
|
|
853
|
+
await checkModelSnapshot(result, structure);
|
|
854
|
+
result.hasConflicts = result.conflicts.length > 0;
|
|
855
|
+
generateSuggestions(result);
|
|
856
|
+
return formatResult2(result, currentBranch, input.compareBranch);
|
|
857
|
+
}
|
|
858
|
+
async function parseMigrations(migrationsPath, rootPath) {
|
|
859
|
+
const files = await findFiles("*.cs", { cwd: migrationsPath });
|
|
860
|
+
const migrations = [];
|
|
861
|
+
const pattern = /^(\d{8})_(\d{3})_(.+)\.cs$/;
|
|
862
|
+
for (const file of files) {
|
|
863
|
+
const fileName = path6.basename(file);
|
|
864
|
+
if (fileName.includes(".Designer.") || fileName.includes("ModelSnapshot")) {
|
|
865
|
+
continue;
|
|
866
|
+
}
|
|
867
|
+
const match = pattern.exec(fileName);
|
|
868
|
+
if (match) {
|
|
869
|
+
migrations.push({
|
|
870
|
+
name: fileName.replace(".cs", ""),
|
|
871
|
+
timestamp: match[1],
|
|
872
|
+
prefix: match[2],
|
|
873
|
+
// Now this is the sequence number (NNN)
|
|
874
|
+
description: match[3],
|
|
875
|
+
file: path6.relative(rootPath, file),
|
|
876
|
+
applied: true
|
|
877
|
+
// We'd need DB connection to check this
|
|
878
|
+
});
|
|
879
|
+
} else {
|
|
880
|
+
migrations.push({
|
|
881
|
+
name: fileName.replace(".cs", ""),
|
|
882
|
+
timestamp: "",
|
|
883
|
+
prefix: "Unknown",
|
|
884
|
+
description: fileName.replace(".cs", ""),
|
|
885
|
+
file: path6.relative(rootPath, file),
|
|
886
|
+
applied: true
|
|
887
|
+
});
|
|
888
|
+
}
|
|
889
|
+
}
|
|
890
|
+
return migrations.sort((a, b) => {
|
|
891
|
+
const dateCompare = a.timestamp.localeCompare(b.timestamp);
|
|
892
|
+
if (dateCompare !== 0) return dateCompare;
|
|
893
|
+
return a.prefix.localeCompare(b.prefix);
|
|
894
|
+
});
|
|
895
|
+
}
|
|
896
|
+
function checkNamingConventions(result, _config) {
|
|
897
|
+
for (const migration of result.migrations) {
|
|
898
|
+
if (!migration.timestamp) {
|
|
899
|
+
result.conflicts.push({
|
|
900
|
+
type: "naming",
|
|
901
|
+
description: `Migration "${migration.name}" does not follow naming convention`,
|
|
902
|
+
files: [migration.file],
|
|
903
|
+
resolution: `Rename to format: YYYYMMDD_NNN_Description (e.g., 20260115_001_InitialCreate)`
|
|
904
|
+
});
|
|
905
|
+
}
|
|
906
|
+
if (migration.prefix === "Unknown") {
|
|
907
|
+
result.conflicts.push({
|
|
908
|
+
type: "naming",
|
|
909
|
+
description: `Migration "${migration.name}" missing sequence number`,
|
|
910
|
+
files: [migration.file],
|
|
911
|
+
resolution: `Use format: YYYYMMDD_NNN_Description where NNN is a 3-digit sequence (001, 002, etc.)`
|
|
912
|
+
});
|
|
913
|
+
}
|
|
914
|
+
}
|
|
915
|
+
}
|
|
916
|
+
function checkChronologicalOrder(result) {
|
|
917
|
+
const migrations = result.migrations.filter((m) => m.timestamp);
|
|
918
|
+
for (let i = 1; i < migrations.length; i++) {
|
|
919
|
+
const prev = migrations[i - 1];
|
|
920
|
+
const curr = migrations[i];
|
|
921
|
+
if (curr.timestamp < prev.timestamp) {
|
|
922
|
+
result.conflicts.push({
|
|
923
|
+
type: "order",
|
|
924
|
+
description: `Migration "${curr.name}" (${curr.timestamp}) is dated before "${prev.name}" (${prev.timestamp})`,
|
|
925
|
+
files: [curr.file, prev.file],
|
|
926
|
+
resolution: "Reorder migrations or update timestamps"
|
|
927
|
+
});
|
|
928
|
+
}
|
|
929
|
+
if (curr.timestamp === prev.timestamp && curr.prefix === prev.prefix) {
|
|
930
|
+
result.conflicts.push({
|
|
931
|
+
type: "order",
|
|
932
|
+
description: `Migrations "${curr.name}" and "${prev.name}" have same timestamp`,
|
|
933
|
+
files: [curr.file, prev.file],
|
|
934
|
+
resolution: "Use different sequence numbers (NNN) or different dates"
|
|
935
|
+
});
|
|
936
|
+
}
|
|
937
|
+
}
|
|
938
|
+
}
|
|
939
|
+
async function checkBranchConflicts(result, structure, currentBranch, compareBranch, projectPath) {
|
|
940
|
+
if (!structure.migrations) return;
|
|
941
|
+
const migrationsRelPath = path6.relative(projectPath, structure.migrations).replace(/\\/g, "/");
|
|
942
|
+
const snapshotFiles = await findFiles("*ModelSnapshot.cs", { cwd: structure.migrations });
|
|
943
|
+
if (snapshotFiles.length > 0) {
|
|
944
|
+
const snapshotRelPath = path6.relative(projectPath, snapshotFiles[0]).replace(/\\/g, "/");
|
|
945
|
+
const currentSnapshot = await readText(snapshotFiles[0]);
|
|
946
|
+
const compareSnapshot = await getFileFromBranch(compareBranch, snapshotRelPath, projectPath);
|
|
947
|
+
if (compareSnapshot && currentSnapshot !== compareSnapshot) {
|
|
948
|
+
const diff = await getDiff(compareBranch, currentBranch, snapshotRelPath, projectPath);
|
|
949
|
+
if (diff) {
|
|
950
|
+
result.conflicts.push({
|
|
951
|
+
type: "snapshot",
|
|
952
|
+
description: `ModelSnapshot differs between "${currentBranch}" and "${compareBranch}"`,
|
|
953
|
+
files: [snapshotRelPath],
|
|
954
|
+
resolution: "Rebase on target branch and regenerate migrations with: dotnet ef migrations add <Name>"
|
|
955
|
+
});
|
|
956
|
+
}
|
|
957
|
+
}
|
|
958
|
+
}
|
|
959
|
+
const compareMigrations = await getFileFromBranch(
|
|
960
|
+
compareBranch,
|
|
961
|
+
migrationsRelPath,
|
|
962
|
+
projectPath
|
|
963
|
+
);
|
|
964
|
+
if (compareMigrations) {
|
|
965
|
+
logger.debug("Branch comparison completed", { compareBranch });
|
|
966
|
+
}
|
|
967
|
+
}
|
|
968
|
+
async function checkModelSnapshot(result, structure) {
|
|
969
|
+
if (!structure.migrations) return;
|
|
970
|
+
const snapshotFiles = await findFiles("*ModelSnapshot.cs", { cwd: structure.migrations });
|
|
971
|
+
if (snapshotFiles.length === 0) {
|
|
972
|
+
result.conflicts.push({
|
|
973
|
+
type: "snapshot",
|
|
974
|
+
description: "No ModelSnapshot file found",
|
|
975
|
+
files: [],
|
|
976
|
+
resolution: "Run: dotnet ef migrations add InitialCreate"
|
|
977
|
+
});
|
|
978
|
+
return;
|
|
979
|
+
}
|
|
980
|
+
if (snapshotFiles.length > 1) {
|
|
981
|
+
result.conflicts.push({
|
|
982
|
+
type: "snapshot",
|
|
983
|
+
description: "Multiple ModelSnapshot files found",
|
|
984
|
+
files: snapshotFiles.map((f) => path6.relative(structure.root, f)),
|
|
985
|
+
resolution: "Remove duplicate snapshots, keep only one per DbContext"
|
|
986
|
+
});
|
|
987
|
+
}
|
|
988
|
+
const snapshotContent = await readText(snapshotFiles[0]);
|
|
989
|
+
for (const migration of result.migrations) {
|
|
990
|
+
if (migration.timestamp && !snapshotContent.includes(migration.name)) {
|
|
991
|
+
result.conflicts.push({
|
|
992
|
+
type: "dependency",
|
|
993
|
+
description: `Migration "${migration.name}" not referenced in ModelSnapshot`,
|
|
994
|
+
files: [migration.file],
|
|
995
|
+
resolution: "Migration may not be applied. Run: dotnet ef database update"
|
|
996
|
+
});
|
|
997
|
+
}
|
|
998
|
+
}
|
|
999
|
+
}
|
|
1000
|
+
function generateSuggestions(result) {
|
|
1001
|
+
if (result.conflicts.some((c) => c.type === "snapshot")) {
|
|
1002
|
+
result.suggestions.push(
|
|
1003
|
+
"Consider rebasing on the target branch before merging to avoid snapshot conflicts"
|
|
1004
|
+
);
|
|
1005
|
+
}
|
|
1006
|
+
if (result.conflicts.some((c) => c.type === "naming")) {
|
|
1007
|
+
result.suggestions.push(
|
|
1008
|
+
"Use convention: YYYYMMDD_NNN_Description for migration naming (e.g., 20260115_001_InitialCreate)"
|
|
1009
|
+
);
|
|
1010
|
+
}
|
|
1011
|
+
if (result.conflicts.some((c) => c.type === "order")) {
|
|
1012
|
+
result.suggestions.push(
|
|
1013
|
+
"Ensure migrations are created in chronological order to avoid conflicts"
|
|
1014
|
+
);
|
|
1015
|
+
}
|
|
1016
|
+
if (result.migrations.length > 20) {
|
|
1017
|
+
result.suggestions.push(
|
|
1018
|
+
"Consider squashing old migrations to reduce complexity. Use: /efcore squash"
|
|
1019
|
+
);
|
|
1020
|
+
}
|
|
1021
|
+
}
|
|
1022
|
+
function formatResult2(result, currentBranch, compareBranch) {
|
|
1023
|
+
const lines = [];
|
|
1024
|
+
lines.push("# EF Core Migration Check Report");
|
|
1025
|
+
lines.push("");
|
|
1026
|
+
lines.push("## Overview");
|
|
1027
|
+
lines.push(`- **Current Branch**: ${currentBranch}`);
|
|
1028
|
+
if (compareBranch) {
|
|
1029
|
+
lines.push(`- **Compare Branch**: ${compareBranch}`);
|
|
1030
|
+
}
|
|
1031
|
+
lines.push(`- **Total Migrations**: ${result.migrations.length}`);
|
|
1032
|
+
lines.push(`- **Conflicts Found**: ${result.conflicts.length}`);
|
|
1033
|
+
lines.push(`- **Status**: ${result.hasConflicts ? "\u274C CONFLICTS DETECTED" : "\u2705 OK"}`);
|
|
1034
|
+
lines.push("");
|
|
1035
|
+
lines.push("## Migrations");
|
|
1036
|
+
lines.push("");
|
|
1037
|
+
lines.push("| Name | Timestamp | Prefix | Description |");
|
|
1038
|
+
lines.push("|------|-----------|--------|-------------|");
|
|
1039
|
+
for (const migration of result.migrations) {
|
|
1040
|
+
lines.push(
|
|
1041
|
+
`| ${migration.name} | ${migration.timestamp || "N/A"} | ${migration.prefix} | ${migration.description} |`
|
|
1042
|
+
);
|
|
1043
|
+
}
|
|
1044
|
+
lines.push("");
|
|
1045
|
+
if (result.conflicts.length > 0) {
|
|
1046
|
+
lines.push("## Conflicts");
|
|
1047
|
+
lines.push("");
|
|
1048
|
+
for (const conflict of result.conflicts) {
|
|
1049
|
+
const icon = conflict.type === "snapshot" ? "\u{1F504}" : conflict.type === "order" ? "\u{1F4C5}" : conflict.type === "naming" ? "\u{1F4DD}" : "\u26A0\uFE0F";
|
|
1050
|
+
lines.push(`### ${icon} ${conflict.type.toUpperCase()}: ${conflict.description}`);
|
|
1051
|
+
if (conflict.files.length > 0) {
|
|
1052
|
+
lines.push(`- **Files**: ${conflict.files.map((f) => `\`${f}\``).join(", ")}`);
|
|
1053
|
+
}
|
|
1054
|
+
lines.push(`- **Resolution**: ${conflict.resolution}`);
|
|
1055
|
+
lines.push("");
|
|
1056
|
+
}
|
|
1057
|
+
}
|
|
1058
|
+
if (result.suggestions.length > 0) {
|
|
1059
|
+
lines.push("## Suggestions");
|
|
1060
|
+
lines.push("");
|
|
1061
|
+
for (const suggestion of result.suggestions) {
|
|
1062
|
+
lines.push(`- \u{1F4A1} ${suggestion}`);
|
|
1063
|
+
}
|
|
1064
|
+
lines.push("");
|
|
1065
|
+
}
|
|
1066
|
+
return lines.join("\n");
|
|
1067
|
+
}
|
|
1068
|
+
|
|
1069
|
+
// src/tools/scaffold-extension.ts
|
|
1070
|
+
import Handlebars from "handlebars";
|
|
1071
|
+
import path7 from "path";
|
|
1072
|
+
var scaffoldExtensionTool = {
|
|
1073
|
+
name: "scaffold_extension",
|
|
1074
|
+
description: "Generate code to extend SmartStack: service (interface + implementation), entity (class + EF config), controller (REST endpoints), or React component",
|
|
1075
|
+
inputSchema: {
|
|
1076
|
+
type: "object",
|
|
1077
|
+
properties: {
|
|
1078
|
+
type: {
|
|
1079
|
+
type: "string",
|
|
1080
|
+
enum: ["service", "entity", "controller", "component"],
|
|
1081
|
+
description: "Type of extension to scaffold"
|
|
1082
|
+
},
|
|
1083
|
+
name: {
|
|
1084
|
+
type: "string",
|
|
1085
|
+
description: 'Name of the extension (e.g., "UserProfile", "Order")'
|
|
1086
|
+
},
|
|
1087
|
+
options: {
|
|
1088
|
+
type: "object",
|
|
1089
|
+
properties: {
|
|
1090
|
+
namespace: {
|
|
1091
|
+
type: "string",
|
|
1092
|
+
description: "Custom namespace (optional)"
|
|
1093
|
+
},
|
|
1094
|
+
baseEntity: {
|
|
1095
|
+
type: "string",
|
|
1096
|
+
description: "Base entity to extend (for entity type)"
|
|
1097
|
+
},
|
|
1098
|
+
methods: {
|
|
1099
|
+
type: "array",
|
|
1100
|
+
items: { type: "string" },
|
|
1101
|
+
description: "Methods to generate (for service type)"
|
|
1102
|
+
},
|
|
1103
|
+
outputPath: {
|
|
1104
|
+
type: "string",
|
|
1105
|
+
description: "Custom output path"
|
|
1106
|
+
}
|
|
1107
|
+
}
|
|
1108
|
+
}
|
|
1109
|
+
},
|
|
1110
|
+
required: ["type", "name"]
|
|
1111
|
+
}
|
|
1112
|
+
};
|
|
1113
|
+
Handlebars.registerHelper("pascalCase", (str) => {
|
|
1114
|
+
return str.charAt(0).toUpperCase() + str.slice(1);
|
|
1115
|
+
});
|
|
1116
|
+
Handlebars.registerHelper("camelCase", (str) => {
|
|
1117
|
+
return str.charAt(0).toLowerCase() + str.slice(1);
|
|
1118
|
+
});
|
|
1119
|
+
Handlebars.registerHelper("kebabCase", (str) => {
|
|
1120
|
+
return str.replace(/([a-z])([A-Z])/g, "$1-$2").toLowerCase();
|
|
1121
|
+
});
|
|
1122
|
+
async function handleScaffoldExtension(args, config) {
|
|
1123
|
+
const input = ScaffoldExtensionInputSchema.parse(args);
|
|
1124
|
+
logger.info("Scaffolding extension", { type: input.type, name: input.name });
|
|
1125
|
+
const structure = await findSmartStackStructure(config.smartstack.projectPath);
|
|
1126
|
+
const result = {
|
|
1127
|
+
success: true,
|
|
1128
|
+
files: [],
|
|
1129
|
+
instructions: []
|
|
1130
|
+
};
|
|
1131
|
+
try {
|
|
1132
|
+
switch (input.type) {
|
|
1133
|
+
case "service":
|
|
1134
|
+
await scaffoldService(input.name, input.options, structure, config, result);
|
|
1135
|
+
break;
|
|
1136
|
+
case "entity":
|
|
1137
|
+
await scaffoldEntity(input.name, input.options, structure, config, result);
|
|
1138
|
+
break;
|
|
1139
|
+
case "controller":
|
|
1140
|
+
await scaffoldController(input.name, input.options, structure, config, result);
|
|
1141
|
+
break;
|
|
1142
|
+
case "component":
|
|
1143
|
+
await scaffoldComponent(input.name, input.options, structure, config, result);
|
|
1144
|
+
break;
|
|
1145
|
+
}
|
|
1146
|
+
} catch (error) {
|
|
1147
|
+
result.success = false;
|
|
1148
|
+
result.instructions.push(`Error: ${error instanceof Error ? error.message : "Unknown error"}`);
|
|
1149
|
+
}
|
|
1150
|
+
return formatResult3(result, input.type, input.name);
|
|
1151
|
+
}
|
|
1152
|
+
async function scaffoldService(name, options, structure, config, result) {
|
|
1153
|
+
const namespace = options?.namespace || `${config.conventions.namespaces.application}.Services`;
|
|
1154
|
+
const methods = options?.methods || ["GetByIdAsync", "GetAllAsync", "CreateAsync", "UpdateAsync", "DeleteAsync"];
|
|
1155
|
+
const interfaceTemplate = `using System.Threading;
|
|
1156
|
+
using System.Threading.Tasks;
|
|
1157
|
+
using System.Collections.Generic;
|
|
1158
|
+
|
|
1159
|
+
namespace {{namespace}};
|
|
1160
|
+
|
|
1161
|
+
/// <summary>
|
|
1162
|
+
/// Service interface for {{name}} operations
|
|
1163
|
+
/// </summary>
|
|
1164
|
+
public interface I{{name}}Service
|
|
1165
|
+
{
|
|
1166
|
+
{{#each methods}}
|
|
1167
|
+
/// <summary>
|
|
1168
|
+
/// {{this}} operation
|
|
1169
|
+
/// </summary>
|
|
1170
|
+
Task<object> {{this}}(CancellationToken cancellationToken = default);
|
|
1171
|
+
|
|
1172
|
+
{{/each}}
|
|
1173
|
+
}
|
|
1174
|
+
`;
|
|
1175
|
+
const implementationTemplate = `using System.Threading;
|
|
1176
|
+
using System.Threading.Tasks;
|
|
1177
|
+
using System.Collections.Generic;
|
|
1178
|
+
using Microsoft.Extensions.Logging;
|
|
1179
|
+
|
|
1180
|
+
namespace {{namespace}};
|
|
1181
|
+
|
|
1182
|
+
/// <summary>
|
|
1183
|
+
/// Service implementation for {{name}} operations
|
|
1184
|
+
/// </summary>
|
|
1185
|
+
public class {{name}}Service : I{{name}}Service
|
|
1186
|
+
{
|
|
1187
|
+
private readonly ILogger<{{name}}Service> _logger;
|
|
1188
|
+
|
|
1189
|
+
public {{name}}Service(ILogger<{{name}}Service> logger)
|
|
1190
|
+
{
|
|
1191
|
+
_logger = logger;
|
|
1192
|
+
}
|
|
1193
|
+
|
|
1194
|
+
{{#each methods}}
|
|
1195
|
+
/// <inheritdoc />
|
|
1196
|
+
public async Task<object> {{this}}(CancellationToken cancellationToken = default)
|
|
1197
|
+
{
|
|
1198
|
+
_logger.LogInformation("Executing {{this}}");
|
|
1199
|
+
// TODO: Implement {{this}}
|
|
1200
|
+
await Task.CompletedTask;
|
|
1201
|
+
throw new NotImplementedException();
|
|
1202
|
+
}
|
|
1203
|
+
|
|
1204
|
+
{{/each}}
|
|
1205
|
+
}
|
|
1206
|
+
`;
|
|
1207
|
+
const diTemplate = `// Add to DependencyInjection.cs or ServiceCollectionExtensions.cs:
|
|
1208
|
+
services.AddScoped<I{{name}}Service, {{name}}Service>();
|
|
1209
|
+
`;
|
|
1210
|
+
const context = { namespace, name, methods };
|
|
1211
|
+
const interfaceContent = Handlebars.compile(interfaceTemplate)(context);
|
|
1212
|
+
const implementationContent = Handlebars.compile(implementationTemplate)(context);
|
|
1213
|
+
const diContent = Handlebars.compile(diTemplate)(context);
|
|
1214
|
+
const basePath = structure.application || config.smartstack.projectPath;
|
|
1215
|
+
const servicesPath = path7.join(basePath, "Services");
|
|
1216
|
+
await ensureDirectory(servicesPath);
|
|
1217
|
+
const interfacePath = path7.join(servicesPath, `I${name}Service.cs`);
|
|
1218
|
+
const implementationPath = path7.join(servicesPath, `${name}Service.cs`);
|
|
1219
|
+
await writeText(interfacePath, interfaceContent);
|
|
1220
|
+
result.files.push({ path: interfacePath, content: interfaceContent, type: "created" });
|
|
1221
|
+
await writeText(implementationPath, implementationContent);
|
|
1222
|
+
result.files.push({ path: implementationPath, content: implementationContent, type: "created" });
|
|
1223
|
+
result.instructions.push("Register service in DI container:");
|
|
1224
|
+
result.instructions.push(diContent);
|
|
1225
|
+
}
|
|
1226
|
+
async function scaffoldEntity(name, options, structure, config, result) {
|
|
1227
|
+
const namespace = options?.namespace || config.conventions.namespaces.domain;
|
|
1228
|
+
const baseEntity = options?.baseEntity;
|
|
1229
|
+
const entityTemplate = `using System;
|
|
1230
|
+
|
|
1231
|
+
namespace {{namespace}};
|
|
1232
|
+
|
|
1233
|
+
/// <summary>
|
|
1234
|
+
/// {{name}} entity{{#if baseEntity}} extending {{baseEntity}}{{/if}}
|
|
1235
|
+
/// </summary>
|
|
1236
|
+
public class {{name}}{{#if baseEntity}} : {{baseEntity}}{{/if}}
|
|
1237
|
+
{
|
|
1238
|
+
{{#unless baseEntity}}
|
|
1239
|
+
public Guid Id { get; set; }
|
|
1240
|
+
|
|
1241
|
+
public DateTime CreatedAt { get; set; } = DateTime.UtcNow;
|
|
1242
|
+
|
|
1243
|
+
public DateTime? UpdatedAt { get; set; }
|
|
1244
|
+
|
|
1245
|
+
{{/unless}}
|
|
1246
|
+
// TODO: Add {{name}} specific properties
|
|
1247
|
+
}
|
|
1248
|
+
`;
|
|
1249
|
+
const configTemplate = `using Microsoft.EntityFrameworkCore;
|
|
1250
|
+
using Microsoft.EntityFrameworkCore.Metadata.Builders;
|
|
1251
|
+
|
|
1252
|
+
namespace {{infrastructureNamespace}}.Persistence.Configurations;
|
|
1253
|
+
|
|
1254
|
+
public class {{name}}Configuration : IEntityTypeConfiguration<{{domainNamespace}}.{{name}}>
|
|
1255
|
+
{
|
|
1256
|
+
public void Configure(EntityTypeBuilder<{{domainNamespace}}.{{name}}> builder)
|
|
1257
|
+
{
|
|
1258
|
+
builder.ToTable("{{tablePrefix}}{{name}}s");
|
|
1259
|
+
|
|
1260
|
+
builder.HasKey(e => e.Id);
|
|
1261
|
+
|
|
1262
|
+
// TODO: Add {{name}} specific configuration
|
|
1263
|
+
}
|
|
1264
|
+
}
|
|
1265
|
+
`;
|
|
1266
|
+
const context = {
|
|
1267
|
+
namespace,
|
|
1268
|
+
name,
|
|
1269
|
+
baseEntity,
|
|
1270
|
+
infrastructureNamespace: config.conventions.namespaces.infrastructure,
|
|
1271
|
+
domainNamespace: config.conventions.namespaces.domain,
|
|
1272
|
+
tablePrefix: config.conventions.tablePrefix.core
|
|
1273
|
+
};
|
|
1274
|
+
const entityContent = Handlebars.compile(entityTemplate)(context);
|
|
1275
|
+
const configContent = Handlebars.compile(configTemplate)(context);
|
|
1276
|
+
const domainPath = structure.domain || path7.join(config.smartstack.projectPath, "Domain");
|
|
1277
|
+
const infraPath = structure.infrastructure || path7.join(config.smartstack.projectPath, "Infrastructure");
|
|
1278
|
+
await ensureDirectory(domainPath);
|
|
1279
|
+
await ensureDirectory(path7.join(infraPath, "Persistence", "Configurations"));
|
|
1280
|
+
const entityFilePath = path7.join(domainPath, `${name}.cs`);
|
|
1281
|
+
const configFilePath = path7.join(infraPath, "Persistence", "Configurations", `${name}Configuration.cs`);
|
|
1282
|
+
await writeText(entityFilePath, entityContent);
|
|
1283
|
+
result.files.push({ path: entityFilePath, content: entityContent, type: "created" });
|
|
1284
|
+
await writeText(configFilePath, configContent);
|
|
1285
|
+
result.files.push({ path: configFilePath, content: configContent, type: "created" });
|
|
1286
|
+
result.instructions.push(`Add DbSet to ApplicationDbContext:`);
|
|
1287
|
+
result.instructions.push(`public DbSet<${name}> ${name}s => Set<${name}>();`);
|
|
1288
|
+
result.instructions.push("");
|
|
1289
|
+
result.instructions.push("Create migration:");
|
|
1290
|
+
result.instructions.push(`dotnet ef migrations add Add${name}`);
|
|
1291
|
+
}
|
|
1292
|
+
async function scaffoldController(name, options, structure, config, result) {
|
|
1293
|
+
const namespace = options?.namespace || `${config.conventions.namespaces.api}.Controllers`;
|
|
1294
|
+
const controllerTemplate = `using Microsoft.AspNetCore.Authorization;
|
|
1295
|
+
using Microsoft.AspNetCore.Mvc;
|
|
1296
|
+
using Microsoft.Extensions.Logging;
|
|
1297
|
+
|
|
1298
|
+
namespace {{namespace}};
|
|
1299
|
+
|
|
1300
|
+
/// <summary>
|
|
1301
|
+
/// API controller for {{name}} operations
|
|
1302
|
+
/// </summary>
|
|
1303
|
+
[ApiController]
|
|
1304
|
+
[Route("api/[controller]")]
|
|
1305
|
+
[Authorize]
|
|
1306
|
+
public class {{name}}Controller : ControllerBase
|
|
1307
|
+
{
|
|
1308
|
+
private readonly ILogger<{{name}}Controller> _logger;
|
|
1309
|
+
|
|
1310
|
+
public {{name}}Controller(ILogger<{{name}}Controller> logger)
|
|
1311
|
+
{
|
|
1312
|
+
_logger = logger;
|
|
1313
|
+
}
|
|
1314
|
+
|
|
1315
|
+
/// <summary>
|
|
1316
|
+
/// Get all {{nameLower}}s
|
|
1317
|
+
/// </summary>
|
|
1318
|
+
[HttpGet]
|
|
1319
|
+
public async Task<ActionResult<IEnumerable<{{name}}Dto>>> GetAll()
|
|
1320
|
+
{
|
|
1321
|
+
_logger.LogInformation("Getting all {{nameLower}}s");
|
|
1322
|
+
// TODO: Implement
|
|
1323
|
+
return Ok(Array.Empty<{{name}}Dto>());
|
|
1324
|
+
}
|
|
1325
|
+
|
|
1326
|
+
/// <summary>
|
|
1327
|
+
/// Get {{nameLower}} by ID
|
|
1328
|
+
/// </summary>
|
|
1329
|
+
[HttpGet("{id:guid}")]
|
|
1330
|
+
public async Task<ActionResult<{{name}}Dto>> GetById(Guid id)
|
|
1331
|
+
{
|
|
1332
|
+
_logger.LogInformation("Getting {{nameLower}} {Id}", id);
|
|
1333
|
+
// TODO: Implement
|
|
1334
|
+
return NotFound();
|
|
1335
|
+
}
|
|
1336
|
+
|
|
1337
|
+
/// <summary>
|
|
1338
|
+
/// Create new {{nameLower}}
|
|
1339
|
+
/// </summary>
|
|
1340
|
+
[HttpPost]
|
|
1341
|
+
public async Task<ActionResult<{{name}}Dto>> Create([FromBody] Create{{name}}Request request)
|
|
1342
|
+
{
|
|
1343
|
+
_logger.LogInformation("Creating {{nameLower}}");
|
|
1344
|
+
// TODO: Implement
|
|
1345
|
+
return CreatedAtAction(nameof(GetById), new { id = Guid.NewGuid() }, null);
|
|
1346
|
+
}
|
|
1347
|
+
|
|
1348
|
+
/// <summary>
|
|
1349
|
+
/// Update {{nameLower}}
|
|
1350
|
+
/// </summary>
|
|
1351
|
+
[HttpPut("{id:guid}")]
|
|
1352
|
+
public async Task<ActionResult> Update(Guid id, [FromBody] Update{{name}}Request request)
|
|
1353
|
+
{
|
|
1354
|
+
_logger.LogInformation("Updating {{nameLower}} {Id}", id);
|
|
1355
|
+
// TODO: Implement
|
|
1356
|
+
return NoContent();
|
|
1357
|
+
}
|
|
1358
|
+
|
|
1359
|
+
/// <summary>
|
|
1360
|
+
/// Delete {{nameLower}}
|
|
1361
|
+
/// </summary>
|
|
1362
|
+
[HttpDelete("{id:guid}")]
|
|
1363
|
+
public async Task<ActionResult> Delete(Guid id)
|
|
1364
|
+
{
|
|
1365
|
+
_logger.LogInformation("Deleting {{nameLower}} {Id}", id);
|
|
1366
|
+
// TODO: Implement
|
|
1367
|
+
return NoContent();
|
|
1368
|
+
}
|
|
1369
|
+
}
|
|
1370
|
+
|
|
1371
|
+
// DTOs
|
|
1372
|
+
public record {{name}}Dto(Guid Id, DateTime CreatedAt);
|
|
1373
|
+
public record Create{{name}}Request();
|
|
1374
|
+
public record Update{{name}}Request();
|
|
1375
|
+
`;
|
|
1376
|
+
const context = {
|
|
1377
|
+
namespace,
|
|
1378
|
+
name,
|
|
1379
|
+
nameLower: name.charAt(0).toLowerCase() + name.slice(1)
|
|
1380
|
+
};
|
|
1381
|
+
const controllerContent = Handlebars.compile(controllerTemplate)(context);
|
|
1382
|
+
const apiPath = structure.api || path7.join(config.smartstack.projectPath, "Api");
|
|
1383
|
+
const controllersPath = path7.join(apiPath, "Controllers");
|
|
1384
|
+
await ensureDirectory(controllersPath);
|
|
1385
|
+
const controllerFilePath = path7.join(controllersPath, `${name}Controller.cs`);
|
|
1386
|
+
await writeText(controllerFilePath, controllerContent);
|
|
1387
|
+
result.files.push({ path: controllerFilePath, content: controllerContent, type: "created" });
|
|
1388
|
+
result.instructions.push("Controller created. API endpoints:");
|
|
1389
|
+
result.instructions.push(` GET /api/${name.toLowerCase()}`);
|
|
1390
|
+
result.instructions.push(` GET /api/${name.toLowerCase()}/{id}`);
|
|
1391
|
+
result.instructions.push(` POST /api/${name.toLowerCase()}`);
|
|
1392
|
+
result.instructions.push(` PUT /api/${name.toLowerCase()}/{id}`);
|
|
1393
|
+
result.instructions.push(` DELETE /api/${name.toLowerCase()}/{id}`);
|
|
1394
|
+
}
|
|
1395
|
+
async function scaffoldComponent(name, options, structure, config, result) {
|
|
1396
|
+
const componentTemplate = `import React, { useState, useEffect } from 'react';
|
|
1397
|
+
|
|
1398
|
+
interface {{name}}Props {
|
|
1399
|
+
id?: string;
|
|
1400
|
+
onSave?: (data: {{name}}Data) => void;
|
|
1401
|
+
onCancel?: () => void;
|
|
1402
|
+
}
|
|
1403
|
+
|
|
1404
|
+
interface {{name}}Data {
|
|
1405
|
+
id?: string;
|
|
1406
|
+
// TODO: Add {{name}} data properties
|
|
1407
|
+
}
|
|
1408
|
+
|
|
1409
|
+
/**
|
|
1410
|
+
* {{name}} component
|
|
1411
|
+
*/
|
|
1412
|
+
export const {{name}}: React.FC<{{name}}Props> = ({ id, onSave, onCancel }) => {
|
|
1413
|
+
const [data, setData] = useState<{{name}}Data | null>(null);
|
|
1414
|
+
const [loading, setLoading] = useState(false);
|
|
1415
|
+
const [error, setError] = useState<string | null>(null);
|
|
1416
|
+
|
|
1417
|
+
useEffect(() => {
|
|
1418
|
+
if (id) {
|
|
1419
|
+
// TODO: Fetch {{nameLower}} data
|
|
1420
|
+
setLoading(true);
|
|
1421
|
+
// fetch...
|
|
1422
|
+
setLoading(false);
|
|
1423
|
+
}
|
|
1424
|
+
}, [id]);
|
|
1425
|
+
|
|
1426
|
+
const handleSubmit = async (e: React.FormEvent) => {
|
|
1427
|
+
e.preventDefault();
|
|
1428
|
+
if (data && onSave) {
|
|
1429
|
+
onSave(data);
|
|
1430
|
+
}
|
|
1431
|
+
};
|
|
1432
|
+
|
|
1433
|
+
if (loading) {
|
|
1434
|
+
return <div className="animate-pulse">Loading...</div>;
|
|
1435
|
+
}
|
|
1436
|
+
|
|
1437
|
+
if (error) {
|
|
1438
|
+
return <div className="text-red-500">{error}</div>;
|
|
1439
|
+
}
|
|
1440
|
+
|
|
1441
|
+
return (
|
|
1442
|
+
<div className="p-4">
|
|
1443
|
+
<h2 className="text-xl font-semibold mb-4">{{name}}</h2>
|
|
1444
|
+
<form onSubmit={handleSubmit} className="space-y-4">
|
|
1445
|
+
{/* TODO: Add form fields */}
|
|
1446
|
+
<div className="flex gap-2">
|
|
1447
|
+
<button
|
|
1448
|
+
type="submit"
|
|
1449
|
+
className="px-4 py-2 bg-blue-500 text-white rounded hover:bg-blue-600"
|
|
1450
|
+
>
|
|
1451
|
+
Save
|
|
1452
|
+
</button>
|
|
1453
|
+
{onCancel && (
|
|
1454
|
+
<button
|
|
1455
|
+
type="button"
|
|
1456
|
+
onClick={onCancel}
|
|
1457
|
+
className="px-4 py-2 bg-gray-200 rounded hover:bg-gray-300"
|
|
1458
|
+
>
|
|
1459
|
+
Cancel
|
|
1460
|
+
</button>
|
|
1461
|
+
)}
|
|
1462
|
+
</div>
|
|
1463
|
+
</form>
|
|
1464
|
+
</div>
|
|
1465
|
+
);
|
|
1466
|
+
};
|
|
1467
|
+
|
|
1468
|
+
export default {{name}};
|
|
1469
|
+
`;
|
|
1470
|
+
const hookTemplate = `import { useState, useEffect, useCallback } from 'react';
|
|
1471
|
+
import { {{nameLower}}Api } from '../services/api/{{nameLower}}';
|
|
1472
|
+
|
|
1473
|
+
interface {{name}}Data {
|
|
1474
|
+
id?: string;
|
|
1475
|
+
// TODO: Add properties
|
|
1476
|
+
}
|
|
1477
|
+
|
|
1478
|
+
interface Use{{name}}Options {
|
|
1479
|
+
id?: string;
|
|
1480
|
+
autoFetch?: boolean;
|
|
1481
|
+
}
|
|
1482
|
+
|
|
1483
|
+
export function use{{name}}(options: Use{{name}}Options = {}) {
|
|
1484
|
+
const { id, autoFetch = true } = options;
|
|
1485
|
+
const [data, setData] = useState<{{name}}Data | null>(null);
|
|
1486
|
+
const [loading, setLoading] = useState(false);
|
|
1487
|
+
const [error, setError] = useState<Error | null>(null);
|
|
1488
|
+
|
|
1489
|
+
const fetch{{name}} = useCallback(async (fetchId?: string) => {
|
|
1490
|
+
const targetId = fetchId || id;
|
|
1491
|
+
if (!targetId) return;
|
|
1492
|
+
|
|
1493
|
+
setLoading(true);
|
|
1494
|
+
setError(null);
|
|
1495
|
+
try {
|
|
1496
|
+
// TODO: Implement API call
|
|
1497
|
+
// const result = await {{nameLower}}Api.getById(targetId);
|
|
1498
|
+
// setData(result);
|
|
1499
|
+
} catch (e) {
|
|
1500
|
+
setError(e instanceof Error ? e : new Error('Unknown error'));
|
|
1501
|
+
} finally {
|
|
1502
|
+
setLoading(false);
|
|
1503
|
+
}
|
|
1504
|
+
}, [id]);
|
|
1505
|
+
|
|
1506
|
+
const save{{name}} = useCallback(async (saveData: {{name}}Data) => {
|
|
1507
|
+
setLoading(true);
|
|
1508
|
+
setError(null);
|
|
1509
|
+
try {
|
|
1510
|
+
// TODO: Implement API call
|
|
1511
|
+
// const result = saveData.id
|
|
1512
|
+
// ? await {{nameLower}}Api.update(saveData.id, saveData)
|
|
1513
|
+
// : await {{nameLower}}Api.create(saveData);
|
|
1514
|
+
// setData(result);
|
|
1515
|
+
// return result;
|
|
1516
|
+
} catch (e) {
|
|
1517
|
+
setError(e instanceof Error ? e : new Error('Unknown error'));
|
|
1518
|
+
throw e;
|
|
1519
|
+
} finally {
|
|
1520
|
+
setLoading(false);
|
|
1521
|
+
}
|
|
1522
|
+
}, []);
|
|
1523
|
+
|
|
1524
|
+
useEffect(() => {
|
|
1525
|
+
if (autoFetch && id) {
|
|
1526
|
+
fetch{{name}}();
|
|
1527
|
+
}
|
|
1528
|
+
}, [autoFetch, id, fetch{{name}}]);
|
|
1529
|
+
|
|
1530
|
+
return {
|
|
1531
|
+
data,
|
|
1532
|
+
loading,
|
|
1533
|
+
error,
|
|
1534
|
+
fetch: fetch{{name}},
|
|
1535
|
+
save: save{{name}},
|
|
1536
|
+
setData,
|
|
1537
|
+
};
|
|
1538
|
+
}
|
|
1539
|
+
`;
|
|
1540
|
+
const context = {
|
|
1541
|
+
name,
|
|
1542
|
+
nameLower: name.charAt(0).toLowerCase() + name.slice(1)
|
|
1543
|
+
};
|
|
1544
|
+
const componentContent = Handlebars.compile(componentTemplate)(context);
|
|
1545
|
+
const hookContent = Handlebars.compile(hookTemplate)(context);
|
|
1546
|
+
const webPath = structure.web || path7.join(config.smartstack.projectPath, "web", "smartstack-web");
|
|
1547
|
+
const componentsPath = options?.outputPath || path7.join(webPath, "src", "components");
|
|
1548
|
+
const hooksPath = path7.join(webPath, "src", "hooks");
|
|
1549
|
+
await ensureDirectory(componentsPath);
|
|
1550
|
+
await ensureDirectory(hooksPath);
|
|
1551
|
+
const componentFilePath = path7.join(componentsPath, `${name}.tsx`);
|
|
1552
|
+
const hookFilePath = path7.join(hooksPath, `use${name}.ts`);
|
|
1553
|
+
await writeText(componentFilePath, componentContent);
|
|
1554
|
+
result.files.push({ path: componentFilePath, content: componentContent, type: "created" });
|
|
1555
|
+
await writeText(hookFilePath, hookContent);
|
|
1556
|
+
result.files.push({ path: hookFilePath, content: hookContent, type: "created" });
|
|
1557
|
+
result.instructions.push("Import and use the component:");
|
|
1558
|
+
result.instructions.push(`import { ${name} } from './components/${name}';`);
|
|
1559
|
+
result.instructions.push(`import { use${name} } from './hooks/use${name}';`);
|
|
1560
|
+
}
|
|
1561
|
+
function formatResult3(result, type, name) {
|
|
1562
|
+
const lines = [];
|
|
1563
|
+
lines.push(`# Scaffold ${type}: ${name}`);
|
|
1564
|
+
lines.push("");
|
|
1565
|
+
if (result.success) {
|
|
1566
|
+
lines.push("## \u2705 Files Generated");
|
|
1567
|
+
lines.push("");
|
|
1568
|
+
for (const file of result.files) {
|
|
1569
|
+
lines.push(`### ${file.type === "created" ? "\u{1F4C4}" : "\u270F\uFE0F"} ${path7.basename(file.path)}`);
|
|
1570
|
+
lines.push(`**Path**: \`${file.path}\``);
|
|
1571
|
+
lines.push("");
|
|
1572
|
+
lines.push("```" + (file.path.endsWith(".cs") ? "csharp" : "typescript"));
|
|
1573
|
+
const contentLines = file.content.split("\n").slice(0, 50);
|
|
1574
|
+
lines.push(contentLines.join("\n"));
|
|
1575
|
+
if (file.content.split("\n").length > 50) {
|
|
1576
|
+
lines.push("// ... (truncated)");
|
|
1577
|
+
}
|
|
1578
|
+
lines.push("```");
|
|
1579
|
+
lines.push("");
|
|
1580
|
+
}
|
|
1581
|
+
if (result.instructions.length > 0) {
|
|
1582
|
+
lines.push("## \u{1F4CB} Next Steps");
|
|
1583
|
+
lines.push("");
|
|
1584
|
+
for (const instruction of result.instructions) {
|
|
1585
|
+
if (instruction.startsWith("services.") || instruction.startsWith("public DbSet")) {
|
|
1586
|
+
lines.push("```csharp");
|
|
1587
|
+
lines.push(instruction);
|
|
1588
|
+
lines.push("```");
|
|
1589
|
+
} else if (instruction.startsWith("dotnet ")) {
|
|
1590
|
+
lines.push("```bash");
|
|
1591
|
+
lines.push(instruction);
|
|
1592
|
+
lines.push("```");
|
|
1593
|
+
} else if (instruction.startsWith("import ")) {
|
|
1594
|
+
lines.push("```typescript");
|
|
1595
|
+
lines.push(instruction);
|
|
1596
|
+
lines.push("```");
|
|
1597
|
+
} else {
|
|
1598
|
+
lines.push(instruction);
|
|
1599
|
+
}
|
|
1600
|
+
}
|
|
1601
|
+
}
|
|
1602
|
+
} else {
|
|
1603
|
+
lines.push("## \u274C Generation Failed");
|
|
1604
|
+
lines.push("");
|
|
1605
|
+
for (const instruction of result.instructions) {
|
|
1606
|
+
lines.push(`- ${instruction}`);
|
|
1607
|
+
}
|
|
1608
|
+
}
|
|
1609
|
+
return lines.join("\n");
|
|
1610
|
+
}
|
|
1611
|
+
|
|
1612
|
+
// src/tools/api-docs.ts
|
|
1613
|
+
import axios from "axios";
|
|
1614
|
+
import path8 from "path";
|
|
1615
|
+
var apiDocsTool = {
|
|
1616
|
+
name: "api_docs",
|
|
1617
|
+
description: "Get API documentation for SmartStack endpoints. Can fetch from Swagger/OpenAPI or parse controller files directly.",
|
|
1618
|
+
inputSchema: {
|
|
1619
|
+
type: "object",
|
|
1620
|
+
properties: {
|
|
1621
|
+
endpoint: {
|
|
1622
|
+
type: "string",
|
|
1623
|
+
description: 'Filter by endpoint path (e.g., "/api/users"). Leave empty for all endpoints.'
|
|
1624
|
+
},
|
|
1625
|
+
format: {
|
|
1626
|
+
type: "string",
|
|
1627
|
+
enum: ["markdown", "json", "openapi"],
|
|
1628
|
+
description: "Output format",
|
|
1629
|
+
default: "markdown"
|
|
1630
|
+
},
|
|
1631
|
+
controller: {
|
|
1632
|
+
type: "string",
|
|
1633
|
+
description: 'Filter by controller name (e.g., "Users")'
|
|
1634
|
+
}
|
|
1635
|
+
}
|
|
1636
|
+
}
|
|
1637
|
+
};
|
|
1638
|
+
async function handleApiDocs(args, config) {
|
|
1639
|
+
const input = ApiDocsInputSchema.parse(args);
|
|
1640
|
+
logger.info("Fetching API documentation", { endpoint: input.endpoint, format: input.format });
|
|
1641
|
+
let endpoints = [];
|
|
1642
|
+
if (config.smartstack.apiEnabled && config.smartstack.apiUrl) {
|
|
1643
|
+
try {
|
|
1644
|
+
endpoints = await fetchFromSwagger(config.smartstack.apiUrl);
|
|
1645
|
+
logger.debug("Fetched endpoints from Swagger", { count: endpoints.length });
|
|
1646
|
+
} catch (error) {
|
|
1647
|
+
logger.warn("Failed to fetch from Swagger, falling back to code parsing", { error });
|
|
1648
|
+
}
|
|
1649
|
+
}
|
|
1650
|
+
if (endpoints.length === 0) {
|
|
1651
|
+
const structure = await findSmartStackStructure(config.smartstack.projectPath);
|
|
1652
|
+
endpoints = await parseControllers(structure);
|
|
1653
|
+
}
|
|
1654
|
+
if (input.endpoint) {
|
|
1655
|
+
const filter = input.endpoint.toLowerCase();
|
|
1656
|
+
endpoints = endpoints.filter((e) => e.path.toLowerCase().includes(filter));
|
|
1657
|
+
}
|
|
1658
|
+
if (input.controller) {
|
|
1659
|
+
const filter = input.controller.toLowerCase();
|
|
1660
|
+
endpoints = endpoints.filter((e) => e.controller.toLowerCase().includes(filter));
|
|
1661
|
+
}
|
|
1662
|
+
switch (input.format) {
|
|
1663
|
+
case "json":
|
|
1664
|
+
return JSON.stringify(endpoints, null, 2);
|
|
1665
|
+
case "openapi":
|
|
1666
|
+
return formatAsOpenApi(endpoints);
|
|
1667
|
+
case "markdown":
|
|
1668
|
+
default:
|
|
1669
|
+
return formatAsMarkdown(endpoints);
|
|
1670
|
+
}
|
|
1671
|
+
}
|
|
1672
|
+
async function fetchFromSwagger(apiUrl) {
|
|
1673
|
+
const swaggerUrl = `${apiUrl}/swagger/v1/swagger.json`;
|
|
1674
|
+
const response = await axios.get(swaggerUrl, {
|
|
1675
|
+
timeout: 5e3,
|
|
1676
|
+
httpsAgent: new (await import("https")).Agent({ rejectUnauthorized: false })
|
|
1677
|
+
});
|
|
1678
|
+
const spec = response.data;
|
|
1679
|
+
const endpoints = [];
|
|
1680
|
+
for (const [pathKey, pathItem] of Object.entries(spec.paths || {})) {
|
|
1681
|
+
const pathObj = pathItem;
|
|
1682
|
+
for (const method of ["get", "post", "put", "patch", "delete"]) {
|
|
1683
|
+
const operation = pathObj[method];
|
|
1684
|
+
if (!operation) continue;
|
|
1685
|
+
const tags = operation.tags || ["Unknown"];
|
|
1686
|
+
const parameters = operation.parameters || [];
|
|
1687
|
+
endpoints.push({
|
|
1688
|
+
method: method.toUpperCase(),
|
|
1689
|
+
path: pathKey,
|
|
1690
|
+
controller: tags[0],
|
|
1691
|
+
action: operation.operationId || method,
|
|
1692
|
+
summary: operation.summary,
|
|
1693
|
+
parameters: parameters.map((p) => ({
|
|
1694
|
+
name: p.name,
|
|
1695
|
+
in: p.in,
|
|
1696
|
+
type: p.schema?.type || "string",
|
|
1697
|
+
required: p.required || false,
|
|
1698
|
+
description: p.description
|
|
1699
|
+
})),
|
|
1700
|
+
requestBody: operation.requestBody ? "See schema" : void 0,
|
|
1701
|
+
responses: Object.entries(operation.responses || {}).map(([status, resp]) => ({
|
|
1702
|
+
status: parseInt(status, 10),
|
|
1703
|
+
description: resp.description
|
|
1704
|
+
})),
|
|
1705
|
+
authorize: !!(operation.security && operation.security.length > 0)
|
|
1706
|
+
});
|
|
1707
|
+
}
|
|
1708
|
+
}
|
|
1709
|
+
return endpoints;
|
|
1710
|
+
}
|
|
1711
|
+
async function parseControllers(structure) {
|
|
1712
|
+
if (!structure.api) {
|
|
1713
|
+
return [];
|
|
1714
|
+
}
|
|
1715
|
+
const controllerFiles = await findControllerFiles(structure.api);
|
|
1716
|
+
const endpoints = [];
|
|
1717
|
+
for (const file of controllerFiles) {
|
|
1718
|
+
const content = await readText(file);
|
|
1719
|
+
const fileName = path8.basename(file, ".cs");
|
|
1720
|
+
const controllerName = fileName.replace("Controller", "");
|
|
1721
|
+
const routeMatch = content.match(/\[Route\s*\(\s*"([^"]+)"\s*\)\]/);
|
|
1722
|
+
const baseRoute = routeMatch ? routeMatch[1].replace("[controller]", controllerName.toLowerCase()) : `/api/${controllerName.toLowerCase()}`;
|
|
1723
|
+
const hasAuthorize = content.includes("[Authorize]");
|
|
1724
|
+
const httpMethods = [
|
|
1725
|
+
{ pattern: /\[HttpGet(?:\s*\(\s*"([^"]*)"\s*\))?\]/g, method: "GET" },
|
|
1726
|
+
{ pattern: /\[HttpPost(?:\s*\(\s*"([^"]*)"\s*\))?\]/g, method: "POST" },
|
|
1727
|
+
{ pattern: /\[HttpPut(?:\s*\(\s*"([^"]*)"\s*\))?\]/g, method: "PUT" },
|
|
1728
|
+
{ pattern: /\[HttpPatch(?:\s*\(\s*"([^"]*)"\s*\))?\]/g, method: "PATCH" },
|
|
1729
|
+
{ pattern: /\[HttpDelete(?:\s*\(\s*"([^"]*)"\s*\))?\]/g, method: "DELETE" }
|
|
1730
|
+
];
|
|
1731
|
+
for (const { pattern, method } of httpMethods) {
|
|
1732
|
+
let match;
|
|
1733
|
+
while ((match = pattern.exec(content)) !== null) {
|
|
1734
|
+
const routeSuffix = match[1] || "";
|
|
1735
|
+
const fullPath = routeSuffix ? `${baseRoute}/${routeSuffix}` : baseRoute;
|
|
1736
|
+
const afterAttribute = content.substring(match.index);
|
|
1737
|
+
const methodMatch = afterAttribute.match(/public\s+(?:async\s+)?(?:Task<)?(?:ActionResult<)?(\w+)(?:>)?\s+(\w+)\s*\(/);
|
|
1738
|
+
const parameters = [];
|
|
1739
|
+
const pathParams = fullPath.match(/\{(\w+)(?::\w+)?\}/g);
|
|
1740
|
+
if (pathParams) {
|
|
1741
|
+
for (const param of pathParams) {
|
|
1742
|
+
const paramName = param.replace(/[{}:]/g, "").replace(/\w+$/, "");
|
|
1743
|
+
parameters.push({
|
|
1744
|
+
name: paramName || param.replace(/[{}]/g, "").split(":")[0],
|
|
1745
|
+
in: "path",
|
|
1746
|
+
type: "string",
|
|
1747
|
+
required: true
|
|
1748
|
+
});
|
|
1749
|
+
}
|
|
1750
|
+
}
|
|
1751
|
+
if (afterAttribute.includes("[FromBody]")) {
|
|
1752
|
+
const bodyMatch = afterAttribute.match(/\[FromBody\]\s*(\w+)\s+(\w+)/);
|
|
1753
|
+
if (bodyMatch) {
|
|
1754
|
+
parameters.push({
|
|
1755
|
+
name: bodyMatch[2],
|
|
1756
|
+
in: "body",
|
|
1757
|
+
type: bodyMatch[1],
|
|
1758
|
+
required: true
|
|
1759
|
+
});
|
|
1760
|
+
}
|
|
1761
|
+
}
|
|
1762
|
+
const queryMatches = afterAttribute.matchAll(/\[FromQuery\]\s*(\w+)\s+(\w+)/g);
|
|
1763
|
+
for (const qm of queryMatches) {
|
|
1764
|
+
parameters.push({
|
|
1765
|
+
name: qm[2],
|
|
1766
|
+
in: "query",
|
|
1767
|
+
type: qm[1],
|
|
1768
|
+
required: false
|
|
1769
|
+
});
|
|
1770
|
+
}
|
|
1771
|
+
endpoints.push({
|
|
1772
|
+
method,
|
|
1773
|
+
path: fullPath.replace(/\/+/g, "/"),
|
|
1774
|
+
controller: controllerName,
|
|
1775
|
+
action: methodMatch ? methodMatch[2] : "Unknown",
|
|
1776
|
+
parameters,
|
|
1777
|
+
responses: [{ status: 200, description: "Success" }],
|
|
1778
|
+
authorize: hasAuthorize
|
|
1779
|
+
});
|
|
1780
|
+
}
|
|
1781
|
+
}
|
|
1782
|
+
}
|
|
1783
|
+
return endpoints;
|
|
1784
|
+
}
|
|
1785
|
+
function formatAsMarkdown(endpoints) {
|
|
1786
|
+
const lines = [];
|
|
1787
|
+
lines.push("# SmartStack API Documentation");
|
|
1788
|
+
lines.push("");
|
|
1789
|
+
lines.push(`Generated: ${(/* @__PURE__ */ new Date()).toISOString()}`);
|
|
1790
|
+
lines.push("");
|
|
1791
|
+
const byController = /* @__PURE__ */ new Map();
|
|
1792
|
+
for (const endpoint of endpoints) {
|
|
1793
|
+
const existing = byController.get(endpoint.controller) || [];
|
|
1794
|
+
existing.push(endpoint);
|
|
1795
|
+
byController.set(endpoint.controller, existing);
|
|
1796
|
+
}
|
|
1797
|
+
for (const [controller, controllerEndpoints] of byController) {
|
|
1798
|
+
lines.push(`## ${controller}`);
|
|
1799
|
+
lines.push("");
|
|
1800
|
+
for (const endpoint of controllerEndpoints) {
|
|
1801
|
+
const authBadge = endpoint.authorize ? " \u{1F512}" : "";
|
|
1802
|
+
lines.push(`### \`${endpoint.method}\` ${endpoint.path}${authBadge}`);
|
|
1803
|
+
lines.push("");
|
|
1804
|
+
if (endpoint.summary) {
|
|
1805
|
+
lines.push(endpoint.summary);
|
|
1806
|
+
lines.push("");
|
|
1807
|
+
}
|
|
1808
|
+
if (endpoint.parameters.length > 0) {
|
|
1809
|
+
lines.push("**Parameters:**");
|
|
1810
|
+
lines.push("");
|
|
1811
|
+
lines.push("| Name | In | Type | Required |");
|
|
1812
|
+
lines.push("|------|-----|------|----------|");
|
|
1813
|
+
for (const param of endpoint.parameters) {
|
|
1814
|
+
lines.push(`| ${param.name} | ${param.in} | ${param.type} | ${param.required ? "Yes" : "No"} |`);
|
|
1815
|
+
}
|
|
1816
|
+
lines.push("");
|
|
1817
|
+
}
|
|
1818
|
+
if (endpoint.responses.length > 0) {
|
|
1819
|
+
lines.push("**Responses:**");
|
|
1820
|
+
lines.push("");
|
|
1821
|
+
for (const resp of endpoint.responses) {
|
|
1822
|
+
lines.push(`- \`${resp.status}\`: ${resp.description || "No description"}`);
|
|
1823
|
+
}
|
|
1824
|
+
lines.push("");
|
|
1825
|
+
}
|
|
1826
|
+
}
|
|
1827
|
+
}
|
|
1828
|
+
if (endpoints.length === 0) {
|
|
1829
|
+
lines.push("No endpoints found matching the filter criteria.");
|
|
1830
|
+
}
|
|
1831
|
+
return lines.join("\n");
|
|
1832
|
+
}
|
|
1833
|
+
function formatAsOpenApi(endpoints) {
|
|
1834
|
+
const spec = {
|
|
1835
|
+
openapi: "3.0.0",
|
|
1836
|
+
info: {
|
|
1837
|
+
title: "SmartStack API",
|
|
1838
|
+
version: "1.0.0"
|
|
1839
|
+
},
|
|
1840
|
+
paths: {}
|
|
1841
|
+
};
|
|
1842
|
+
for (const endpoint of endpoints) {
|
|
1843
|
+
if (!spec.paths[endpoint.path]) {
|
|
1844
|
+
spec.paths[endpoint.path] = {};
|
|
1845
|
+
}
|
|
1846
|
+
spec.paths[endpoint.path][endpoint.method.toLowerCase()] = {
|
|
1847
|
+
tags: [endpoint.controller],
|
|
1848
|
+
operationId: endpoint.action,
|
|
1849
|
+
summary: endpoint.summary,
|
|
1850
|
+
parameters: endpoint.parameters.filter((p) => p.in !== "body").map((p) => ({
|
|
1851
|
+
name: p.name,
|
|
1852
|
+
in: p.in,
|
|
1853
|
+
required: p.required,
|
|
1854
|
+
schema: { type: p.type }
|
|
1855
|
+
})),
|
|
1856
|
+
responses: Object.fromEntries(
|
|
1857
|
+
endpoint.responses.map((r) => [
|
|
1858
|
+
r.status.toString(),
|
|
1859
|
+
{ description: r.description || "Response" }
|
|
1860
|
+
])
|
|
1861
|
+
),
|
|
1862
|
+
security: endpoint.authorize ? [{ bearerAuth: [] }] : void 0
|
|
1863
|
+
};
|
|
1864
|
+
}
|
|
1865
|
+
return JSON.stringify(spec, null, 2);
|
|
1866
|
+
}
|
|
1867
|
+
|
|
1868
|
+
// src/resources/conventions.ts
|
|
1869
|
+
var conventionsResourceTemplate = {
|
|
1870
|
+
uri: "smartstack://conventions",
|
|
1871
|
+
name: "AtlasHub Conventions",
|
|
1872
|
+
description: "Documentation of AtlasHub/SmartStack naming conventions, patterns, and best practices",
|
|
1873
|
+
mimeType: "text/markdown"
|
|
1874
|
+
};
|
|
1875
|
+
async function getConventionsResource(config) {
|
|
1876
|
+
const { schemas, tablePrefixes, migrationFormat, namespaces, servicePattern } = config.conventions;
|
|
1877
|
+
return `# AtlasHub SmartStack Conventions
|
|
1878
|
+
|
|
1879
|
+
## Overview
|
|
1880
|
+
|
|
1881
|
+
This document describes the mandatory conventions for extending the SmartStack/AtlasHub platform.
|
|
1882
|
+
Following these conventions ensures compatibility and prevents conflicts.
|
|
1883
|
+
|
|
1884
|
+
---
|
|
1885
|
+
|
|
1886
|
+
## 1. Database Conventions
|
|
1887
|
+
|
|
1888
|
+
### SQL Schemas
|
|
1889
|
+
|
|
1890
|
+
SmartStack uses SQL Server schemas to separate platform tables from client extensions:
|
|
1891
|
+
|
|
1892
|
+
| Schema | Usage | Description |
|
|
1893
|
+
|--------|-------|-------------|
|
|
1894
|
+
| \`${schemas.platform}\` | SmartStack platform | All native SmartStack tables |
|
|
1895
|
+
| \`${schemas.extensions}\` | Client extensions | Custom tables added by clients |
|
|
1896
|
+
|
|
1897
|
+
### Domain Table Prefixes
|
|
1898
|
+
|
|
1899
|
+
Tables are organized by domain using prefixes:
|
|
1900
|
+
|
|
1901
|
+
| Prefix | Domain | Example Tables |
|
|
1902
|
+
|--------|--------|----------------|
|
|
1903
|
+
| \`auth_\` | Authorization | auth_Users, auth_Roles, auth_Permissions |
|
|
1904
|
+
| \`nav_\` | Navigation | nav_Contexts, nav_Applications, nav_Modules |
|
|
1905
|
+
| \`usr_\` | User profiles | usr_Profiles, usr_Preferences |
|
|
1906
|
+
| \`ai_\` | AI features | ai_Providers, ai_Models, ai_Prompts |
|
|
1907
|
+
| \`cfg_\` | Configuration | cfg_Settings |
|
|
1908
|
+
| \`wkf_\` | Workflows | wkf_EmailTemplates, wkf_Workflows |
|
|
1909
|
+
| \`support_\` | Support | support_Tickets, support_Comments |
|
|
1910
|
+
| \`entra_\` | Entra sync | entra_Groups, entra_SyncState |
|
|
1911
|
+
| \`ref_\` | References | ref_Companies, ref_Departments |
|
|
1912
|
+
| \`loc_\` | Localization | loc_Languages, loc_Translations |
|
|
1913
|
+
| \`lic_\` | Licensing | lic_Licenses |
|
|
1914
|
+
|
|
1915
|
+
### Entity Configuration
|
|
1916
|
+
|
|
1917
|
+
\`\`\`csharp
|
|
1918
|
+
public class MyEntityConfiguration : IEntityTypeConfiguration<MyEntity>
|
|
1919
|
+
{
|
|
1920
|
+
public void Configure(EntityTypeBuilder<MyEntity> builder)
|
|
1921
|
+
{
|
|
1922
|
+
// CORRECT: Use schema + domain prefix
|
|
1923
|
+
builder.ToTable("auth_Users", "${schemas.platform}");
|
|
1924
|
+
|
|
1925
|
+
// WRONG: No schema specified
|
|
1926
|
+
// builder.ToTable("auth_Users");
|
|
1927
|
+
|
|
1928
|
+
// WRONG: No domain prefix
|
|
1929
|
+
// builder.ToTable("Users", "${schemas.platform}");
|
|
1930
|
+
}
|
|
1931
|
+
}
|
|
1932
|
+
\`\`\`
|
|
1933
|
+
|
|
1934
|
+
### Extending Core Entities
|
|
1935
|
+
|
|
1936
|
+
When extending a core entity, use the \`${schemas.extensions}\` schema:
|
|
1937
|
+
|
|
1938
|
+
\`\`\`csharp
|
|
1939
|
+
// Client extension for auth_Users
|
|
1940
|
+
public class UserExtension
|
|
1941
|
+
{
|
|
1942
|
+
public Guid UserId { get; set; } // FK to auth_Users
|
|
1943
|
+
public User User { get; set; }
|
|
1944
|
+
|
|
1945
|
+
// Custom properties
|
|
1946
|
+
public string CustomField { get; set; }
|
|
1947
|
+
}
|
|
1948
|
+
|
|
1949
|
+
// Configuration - use extensions schema
|
|
1950
|
+
builder.ToTable("client_UserExtensions", "${schemas.extensions}");
|
|
1951
|
+
builder.HasOne(e => e.User)
|
|
1952
|
+
.WithOne()
|
|
1953
|
+
.HasForeignKey<UserExtension>(e => e.UserId);
|
|
1954
|
+
\`\`\`
|
|
1955
|
+
|
|
1956
|
+
---
|
|
1957
|
+
|
|
1958
|
+
## 2. Migration Conventions
|
|
1959
|
+
|
|
1960
|
+
### Naming Format
|
|
1961
|
+
|
|
1962
|
+
Migrations MUST follow this naming pattern:
|
|
1963
|
+
|
|
1964
|
+
\`\`\`
|
|
1965
|
+
${migrationFormat}
|
|
1966
|
+
\`\`\`
|
|
1967
|
+
|
|
1968
|
+
**Examples:**
|
|
1969
|
+
- \`20260115_001_InitialSchema.cs\`
|
|
1970
|
+
- \`20260120_002_AddUserProfiles.cs\`
|
|
1971
|
+
- \`20260125_003_AddSupportTickets.cs\`
|
|
1972
|
+
|
|
1973
|
+
### Creating Migrations
|
|
1974
|
+
|
|
1975
|
+
\`\`\`bash
|
|
1976
|
+
# Create a new migration
|
|
1977
|
+
dotnet ef migrations add 20260115_001_InitialSchema
|
|
1978
|
+
|
|
1979
|
+
# With context specified
|
|
1980
|
+
dotnet ef migrations add 20260115_001_InitialSchema --context ApplicationDbContext
|
|
1981
|
+
\`\`\`
|
|
1982
|
+
|
|
1983
|
+
### Migration Rules
|
|
1984
|
+
|
|
1985
|
+
1. **One migration per feature** - Group related changes in a single migration
|
|
1986
|
+
2. **Timestamps ensure order** - Use YYYYMMDD format for chronological sorting
|
|
1987
|
+
3. **Sequence numbers** - Use NNN (001, 002, etc.) for migrations on the same day
|
|
1988
|
+
4. **Descriptive names** - Use clear descriptions (InitialSchema, AddUserProfiles, etc.)
|
|
1989
|
+
5. **Schema must be specified** - All tables must specify their schema in ToTable()
|
|
1990
|
+
|
|
1991
|
+
---
|
|
1992
|
+
|
|
1993
|
+
## 3. Namespace Conventions
|
|
1994
|
+
|
|
1995
|
+
### Layer Structure
|
|
1996
|
+
|
|
1997
|
+
| Layer | Namespace | Purpose |
|
|
1998
|
+
|-------|-----------|---------|
|
|
1999
|
+
| Domain | \`${namespaces.domain}\` | Entities, value objects, domain events |
|
|
2000
|
+
| Application | \`${namespaces.application}\` | Use cases, services, interfaces |
|
|
2001
|
+
| Infrastructure | \`${namespaces.infrastructure}\` | EF Core, external services |
|
|
2002
|
+
| API | \`${namespaces.api}\` | Controllers, DTOs, middleware |
|
|
2003
|
+
|
|
2004
|
+
### Example Namespaces
|
|
2005
|
+
|
|
2006
|
+
\`\`\`csharp
|
|
2007
|
+
// Entity in Domain layer
|
|
2008
|
+
namespace ${namespaces.domain}.Entities;
|
|
2009
|
+
public class User { }
|
|
2010
|
+
|
|
2011
|
+
// Service interface in Application layer
|
|
2012
|
+
namespace ${namespaces.application}.Services;
|
|
2013
|
+
public interface IUserService { }
|
|
2014
|
+
|
|
2015
|
+
// EF Configuration in Infrastructure layer
|
|
2016
|
+
namespace ${namespaces.infrastructure}.Persistence.Configurations;
|
|
2017
|
+
public class UserConfiguration { }
|
|
2018
|
+
|
|
2019
|
+
// Controller in API layer
|
|
2020
|
+
namespace ${namespaces.api}.Controllers;
|
|
2021
|
+
public class UsersController { }
|
|
2022
|
+
\`\`\`
|
|
2023
|
+
|
|
2024
|
+
---
|
|
2025
|
+
|
|
2026
|
+
## 4. Service Conventions
|
|
2027
|
+
|
|
2028
|
+
### Naming Pattern
|
|
2029
|
+
|
|
2030
|
+
| Type | Pattern | Example |
|
|
2031
|
+
|------|---------|---------|
|
|
2032
|
+
| Interface | \`${servicePattern.interface.replace("{Name}", "<Name>")}\` | \`IUserService\` |
|
|
2033
|
+
| Implementation | \`${servicePattern.implementation.replace("{Name}", "<Name>")}\` | \`UserService\` |
|
|
2034
|
+
|
|
2035
|
+
### Service Structure
|
|
2036
|
+
|
|
2037
|
+
\`\`\`csharp
|
|
2038
|
+
// Interface (in Application layer)
|
|
2039
|
+
namespace ${namespaces.application}.Services;
|
|
2040
|
+
|
|
2041
|
+
public interface IUserService
|
|
2042
|
+
{
|
|
2043
|
+
Task<UserDto> GetByIdAsync(Guid id, CancellationToken ct = default);
|
|
2044
|
+
Task<IEnumerable<UserDto>> GetAllAsync(CancellationToken ct = default);
|
|
2045
|
+
Task<UserDto> CreateAsync(CreateUserCommand cmd, CancellationToken ct = default);
|
|
2046
|
+
Task UpdateAsync(Guid id, UpdateUserCommand cmd, CancellationToken ct = default);
|
|
2047
|
+
Task DeleteAsync(Guid id, CancellationToken ct = default);
|
|
2048
|
+
}
|
|
2049
|
+
|
|
2050
|
+
// Implementation (in Infrastructure or Application layer)
|
|
2051
|
+
public class UserService : IUserService
|
|
2052
|
+
{
|
|
2053
|
+
private readonly IRepository<User> _repository;
|
|
2054
|
+
private readonly ILogger<UserService> _logger;
|
|
2055
|
+
|
|
2056
|
+
public UserService(IRepository<User> repository, ILogger<UserService> logger)
|
|
2057
|
+
{
|
|
2058
|
+
_repository = repository;
|
|
2059
|
+
_logger = logger;
|
|
2060
|
+
}
|
|
2061
|
+
|
|
2062
|
+
// Implementation...
|
|
2063
|
+
}
|
|
2064
|
+
\`\`\`
|
|
2065
|
+
|
|
2066
|
+
### Dependency Injection
|
|
2067
|
+
|
|
2068
|
+
\`\`\`csharp
|
|
2069
|
+
// In DependencyInjection.cs or ServiceCollectionExtensions.cs
|
|
2070
|
+
public static IServiceCollection AddApplicationServices(this IServiceCollection services)
|
|
2071
|
+
{
|
|
2072
|
+
services.AddScoped<IUserService, UserService>();
|
|
2073
|
+
services.AddScoped<IRoleService, RoleService>();
|
|
2074
|
+
// ...
|
|
2075
|
+
return services;
|
|
2076
|
+
}
|
|
2077
|
+
\`\`\`
|
|
2078
|
+
|
|
2079
|
+
---
|
|
2080
|
+
|
|
2081
|
+
## 5. Extension Patterns
|
|
2082
|
+
|
|
2083
|
+
### Extending Services
|
|
2084
|
+
|
|
2085
|
+
Use DI replacement to override core services:
|
|
2086
|
+
|
|
2087
|
+
\`\`\`csharp
|
|
2088
|
+
// Your custom implementation
|
|
2089
|
+
public class CustomUserService : UserService, IUserService
|
|
2090
|
+
{
|
|
2091
|
+
public CustomUserService(IRepository<User> repo, ILogger<CustomUserService> logger)
|
|
2092
|
+
: base(repo, logger) { }
|
|
2093
|
+
|
|
2094
|
+
public override async Task<UserDto> GetByIdAsync(Guid id, CancellationToken ct = default)
|
|
2095
|
+
{
|
|
2096
|
+
// Custom logic before
|
|
2097
|
+
var result = await base.GetByIdAsync(id, ct);
|
|
2098
|
+
// Custom logic after
|
|
2099
|
+
return result;
|
|
2100
|
+
}
|
|
2101
|
+
}
|
|
2102
|
+
|
|
2103
|
+
// Replace in DI
|
|
2104
|
+
services.Replace(ServiceDescriptor.Scoped<IUserService, CustomUserService>());
|
|
2105
|
+
\`\`\`
|
|
2106
|
+
|
|
2107
|
+
### Extension Hooks
|
|
2108
|
+
|
|
2109
|
+
Core services expose hooks for common extension points:
|
|
2110
|
+
|
|
2111
|
+
\`\`\`csharp
|
|
2112
|
+
public interface IUserServiceHooks
|
|
2113
|
+
{
|
|
2114
|
+
Task OnUserCreatedAsync(User user, CancellationToken ct);
|
|
2115
|
+
Task OnUserUpdatedAsync(User user, CancellationToken ct);
|
|
2116
|
+
Task OnUserDeletedAsync(Guid userId, CancellationToken ct);
|
|
2117
|
+
}
|
|
2118
|
+
\`\`\`
|
|
2119
|
+
|
|
2120
|
+
---
|
|
2121
|
+
|
|
2122
|
+
## 6. Configuration
|
|
2123
|
+
|
|
2124
|
+
### appsettings.json Structure
|
|
2125
|
+
|
|
2126
|
+
\`\`\`json
|
|
2127
|
+
{
|
|
2128
|
+
"AtlasHub": {
|
|
2129
|
+
"Licensing": {
|
|
2130
|
+
"Key": "XXXX-XXXX-XXXX-XXXX",
|
|
2131
|
+
"Validate": true
|
|
2132
|
+
},
|
|
2133
|
+
"Features": {
|
|
2134
|
+
"AI": true,
|
|
2135
|
+
"Support": true,
|
|
2136
|
+
"Workflows": true
|
|
2137
|
+
}
|
|
2138
|
+
},
|
|
2139
|
+
"Client": {
|
|
2140
|
+
"Custom": {
|
|
2141
|
+
// Client-specific configuration
|
|
2142
|
+
}
|
|
2143
|
+
}
|
|
2144
|
+
}
|
|
2145
|
+
\`\`\`
|
|
2146
|
+
|
|
2147
|
+
---
|
|
2148
|
+
|
|
2149
|
+
## Quick Reference
|
|
2150
|
+
|
|
2151
|
+
| Category | Convention | Example |
|
|
2152
|
+
|----------|------------|---------|
|
|
2153
|
+
| Platform schema | \`${schemas.platform}\` | \`.ToTable("auth_Users", "${schemas.platform}")\` |
|
|
2154
|
+
| Extensions schema | \`${schemas.extensions}\` | \`.ToTable("client_Custom", "${schemas.extensions}")\` |
|
|
2155
|
+
| Table prefixes | \`${tablePrefixes.slice(0, 5).join(", ")}\`, etc. | \`auth_Users\`, \`nav_Modules\` |
|
|
2156
|
+
| Migration | \`YYYYMMDD_NNN_Description\` | \`20260115_001_InitialCreate\` |
|
|
2157
|
+
| Interface | \`I<Name>Service\` | \`IUserService\` |
|
|
2158
|
+
| Implementation | \`<Name>Service\` | \`UserService\` |
|
|
2159
|
+
| Domain namespace | \`${namespaces.domain}\` | - |
|
|
2160
|
+
| API namespace | \`${namespaces.api}\` | - |
|
|
2161
|
+
`;
|
|
2162
|
+
}
|
|
2163
|
+
|
|
2164
|
+
// src/resources/project-info.ts
|
|
2165
|
+
import path9 from "path";
|
|
2166
|
+
var projectInfoResourceTemplate = {
|
|
2167
|
+
uri: "smartstack://project",
|
|
2168
|
+
name: "SmartStack Project Info",
|
|
2169
|
+
description: "Current SmartStack project information, structure, and configuration",
|
|
2170
|
+
mimeType: "text/markdown"
|
|
2171
|
+
};
|
|
2172
|
+
async function getProjectInfoResource(config) {
|
|
2173
|
+
const projectPath = config.smartstack.projectPath;
|
|
2174
|
+
const projectInfo = await detectProject(projectPath);
|
|
2175
|
+
const structure = await findSmartStackStructure(projectPath);
|
|
2176
|
+
const lines = [];
|
|
2177
|
+
lines.push("# SmartStack Project Information");
|
|
2178
|
+
lines.push("");
|
|
2179
|
+
lines.push("## Overview");
|
|
2180
|
+
lines.push("");
|
|
2181
|
+
lines.push(`| Property | Value |`);
|
|
2182
|
+
lines.push(`|----------|-------|`);
|
|
2183
|
+
lines.push(`| **Name** | ${projectInfo.name} |`);
|
|
2184
|
+
lines.push(`| **Version** | ${projectInfo.version} |`);
|
|
2185
|
+
lines.push(`| **Path** | \`${projectPath}\` |`);
|
|
2186
|
+
lines.push(`| **Git Repository** | ${projectInfo.isGitRepo ? "Yes" : "No"} |`);
|
|
2187
|
+
if (projectInfo.currentBranch) {
|
|
2188
|
+
lines.push(`| **Current Branch** | \`${projectInfo.currentBranch}\` |`);
|
|
2189
|
+
}
|
|
2190
|
+
lines.push(`| **.NET Project** | ${projectInfo.hasDotNet ? "Yes" : "No"} |`);
|
|
2191
|
+
lines.push(`| **EF Core** | ${projectInfo.hasEfCore ? "Yes" : "No"} |`);
|
|
2192
|
+
if (projectInfo.dbContextName) {
|
|
2193
|
+
lines.push(`| **DbContext** | \`${projectInfo.dbContextName}\` |`);
|
|
2194
|
+
}
|
|
2195
|
+
lines.push(`| **React Frontend** | ${projectInfo.hasReact ? "Yes" : "No"} |`);
|
|
2196
|
+
lines.push("");
|
|
2197
|
+
lines.push("## Project Structure");
|
|
2198
|
+
lines.push("");
|
|
2199
|
+
lines.push("```");
|
|
2200
|
+
lines.push(`${projectInfo.name}/`);
|
|
2201
|
+
if (structure.domain) {
|
|
2202
|
+
lines.push(`\u251C\u2500\u2500 ${path9.basename(structure.domain)}/ # Domain layer (entities)`);
|
|
2203
|
+
}
|
|
2204
|
+
if (structure.application) {
|
|
2205
|
+
lines.push(`\u251C\u2500\u2500 ${path9.basename(structure.application)}/ # Application layer (services)`);
|
|
2206
|
+
}
|
|
2207
|
+
if (structure.infrastructure) {
|
|
2208
|
+
lines.push(`\u251C\u2500\u2500 ${path9.basename(structure.infrastructure)}/ # Infrastructure (EF Core)`);
|
|
2209
|
+
}
|
|
2210
|
+
if (structure.api) {
|
|
2211
|
+
lines.push(`\u251C\u2500\u2500 ${path9.basename(structure.api)}/ # API layer (controllers)`);
|
|
2212
|
+
}
|
|
2213
|
+
if (structure.web) {
|
|
2214
|
+
lines.push(`\u2514\u2500\u2500 web/smartstack-web/ # React frontend`);
|
|
2215
|
+
}
|
|
2216
|
+
lines.push("```");
|
|
2217
|
+
lines.push("");
|
|
2218
|
+
if (projectInfo.csprojFiles.length > 0) {
|
|
2219
|
+
lines.push("## .NET Projects");
|
|
2220
|
+
lines.push("");
|
|
2221
|
+
lines.push("| Project | Path |");
|
|
2222
|
+
lines.push("|---------|------|");
|
|
2223
|
+
for (const csproj of projectInfo.csprojFiles) {
|
|
2224
|
+
const name = path9.basename(csproj, ".csproj");
|
|
2225
|
+
const relativePath = path9.relative(projectPath, csproj);
|
|
2226
|
+
lines.push(`| ${name} | \`${relativePath}\` |`);
|
|
2227
|
+
}
|
|
2228
|
+
lines.push("");
|
|
2229
|
+
}
|
|
2230
|
+
if (structure.migrations) {
|
|
2231
|
+
const migrationFiles = await findFiles("*.cs", {
|
|
2232
|
+
cwd: structure.migrations,
|
|
2233
|
+
ignore: ["*.Designer.cs"]
|
|
2234
|
+
});
|
|
2235
|
+
const migrations = migrationFiles.map((f) => path9.basename(f)).filter((f) => !f.includes("ModelSnapshot") && !f.includes(".Designer.")).sort();
|
|
2236
|
+
lines.push("## EF Core Migrations");
|
|
2237
|
+
lines.push("");
|
|
2238
|
+
lines.push(`**Location**: \`${path9.relative(projectPath, structure.migrations)}\``);
|
|
2239
|
+
lines.push(`**Total Migrations**: ${migrations.length}`);
|
|
2240
|
+
lines.push("");
|
|
2241
|
+
if (migrations.length > 0) {
|
|
2242
|
+
lines.push("### Recent Migrations");
|
|
2243
|
+
lines.push("");
|
|
2244
|
+
const recent = migrations.slice(-5);
|
|
2245
|
+
for (const migration of recent) {
|
|
2246
|
+
lines.push(`- \`${migration.replace(".cs", "")}\``);
|
|
2247
|
+
}
|
|
2248
|
+
lines.push("");
|
|
2249
|
+
}
|
|
2250
|
+
}
|
|
2251
|
+
lines.push("## Configuration");
|
|
2252
|
+
lines.push("");
|
|
2253
|
+
lines.push("### MCP Server Config");
|
|
2254
|
+
lines.push("");
|
|
2255
|
+
lines.push("```json");
|
|
2256
|
+
lines.push(JSON.stringify({
|
|
2257
|
+
smartstack: config.smartstack,
|
|
2258
|
+
conventions: {
|
|
2259
|
+
tablePrefix: config.conventions.tablePrefix,
|
|
2260
|
+
migrationFormat: config.conventions.migrationFormat
|
|
2261
|
+
}
|
|
2262
|
+
}, null, 2));
|
|
2263
|
+
lines.push("```");
|
|
2264
|
+
lines.push("");
|
|
2265
|
+
lines.push("## Quick Commands");
|
|
2266
|
+
lines.push("");
|
|
2267
|
+
lines.push("```bash");
|
|
2268
|
+
lines.push("# Build the solution");
|
|
2269
|
+
lines.push("dotnet build");
|
|
2270
|
+
lines.push("");
|
|
2271
|
+
lines.push("# Run API");
|
|
2272
|
+
lines.push(`cd ${structure.api ? path9.relative(projectPath, structure.api) : "SmartStack.Api"}`);
|
|
2273
|
+
lines.push("dotnet run");
|
|
2274
|
+
lines.push("");
|
|
2275
|
+
lines.push("# Run frontend");
|
|
2276
|
+
lines.push(`cd ${structure.web ? path9.relative(projectPath, structure.web) : "web/smartstack-web"}`);
|
|
2277
|
+
lines.push("npm run dev");
|
|
2278
|
+
lines.push("");
|
|
2279
|
+
lines.push("# Create migration");
|
|
2280
|
+
lines.push("dotnet ef migrations add YYYYMMDD_Core_NNN_Description");
|
|
2281
|
+
lines.push("");
|
|
2282
|
+
lines.push("# Apply migrations");
|
|
2283
|
+
lines.push("dotnet ef database update");
|
|
2284
|
+
lines.push("```");
|
|
2285
|
+
lines.push("");
|
|
2286
|
+
lines.push("## Available MCP Tools");
|
|
2287
|
+
lines.push("");
|
|
2288
|
+
lines.push("| Tool | Description |");
|
|
2289
|
+
lines.push("|------|-------------|");
|
|
2290
|
+
lines.push("| `validate_conventions` | Check code against AtlasHub conventions |");
|
|
2291
|
+
lines.push("| `check_migrations` | Analyze EF Core migrations for conflicts |");
|
|
2292
|
+
lines.push("| `scaffold_extension` | Generate service, entity, controller, or component |");
|
|
2293
|
+
lines.push("| `api_docs` | Get API endpoint documentation |");
|
|
2294
|
+
lines.push("");
|
|
2295
|
+
return lines.join("\n");
|
|
2296
|
+
}
|
|
2297
|
+
|
|
2298
|
+
// src/resources/api-endpoints.ts
|
|
2299
|
+
import path10 from "path";
|
|
2300
|
+
var apiEndpointsResourceTemplate = {
|
|
2301
|
+
uri: "smartstack://api/",
|
|
2302
|
+
name: "SmartStack API Endpoints",
|
|
2303
|
+
description: "API endpoint documentation. Use smartstack://api/{endpoint} to filter.",
|
|
2304
|
+
mimeType: "text/markdown"
|
|
2305
|
+
};
|
|
2306
|
+
async function getApiEndpointsResource(config, endpointFilter) {
|
|
2307
|
+
const structure = await findSmartStackStructure(config.smartstack.projectPath);
|
|
2308
|
+
if (!structure.api) {
|
|
2309
|
+
return "# API Endpoints\n\nNo API project found.";
|
|
2310
|
+
}
|
|
2311
|
+
const controllerFiles = await findControllerFiles(structure.api);
|
|
2312
|
+
const allEndpoints = [];
|
|
2313
|
+
for (const file of controllerFiles) {
|
|
2314
|
+
const endpoints2 = await parseController(file, structure.root);
|
|
2315
|
+
allEndpoints.push(...endpoints2);
|
|
2316
|
+
}
|
|
2317
|
+
const endpoints = endpointFilter ? allEndpoints.filter(
|
|
2318
|
+
(e) => e.path.toLowerCase().includes(endpointFilter.toLowerCase()) || e.controller.toLowerCase().includes(endpointFilter.toLowerCase())
|
|
2319
|
+
) : allEndpoints;
|
|
2320
|
+
return formatEndpoints(endpoints, endpointFilter);
|
|
2321
|
+
}
|
|
2322
|
+
async function parseController(filePath, rootPath) {
|
|
2323
|
+
const content = await readText(filePath);
|
|
2324
|
+
const fileName = path10.basename(filePath, ".cs");
|
|
2325
|
+
const controllerName = fileName.replace("Controller", "");
|
|
2326
|
+
const endpoints = [];
|
|
2327
|
+
const routeMatch = content.match(/\[Route\s*\(\s*"([^"]+)"\s*\)\]/);
|
|
2328
|
+
const baseRoute = routeMatch ? routeMatch[1].replace("[controller]", controllerName.toLowerCase()) : `/api/${controllerName.toLowerCase()}`;
|
|
2329
|
+
const classAuthorize = /\[Authorize\s*(?:\([^)]*\))?\s*\]\s*(?:\[.*\]\s*)*public\s+class/.test(content);
|
|
2330
|
+
const methodPattern = /\/\/\/\s*<summary>\s*\n\s*\/\/\/\s*([^\n]+)\s*\n\s*\/\/\/\s*<\/summary>[\s\S]*?(?=\[Http)|(\[Http(Get|Post|Put|Patch|Delete)(?:\s*\(\s*"([^"]*)"\s*\))?\][\s\S]*?public\s+(?:async\s+)?(?:Task<)?(?:ActionResult<)?(\w+)(?:[<>[\],\s\w]*)?\s+(\w+)\s*\(([^)]*)\))/g;
|
|
2331
|
+
let lastSummary = "";
|
|
2332
|
+
let match;
|
|
2333
|
+
const httpMethods = ["HttpGet", "HttpPost", "HttpPut", "HttpPatch", "HttpDelete"];
|
|
2334
|
+
for (const httpMethod of httpMethods) {
|
|
2335
|
+
const regex = new RegExp(
|
|
2336
|
+
`\\[${httpMethod}(?:\\s*\\(\\s*"([^"]*)"\\s*\\))?\\]\\s*(?:\\[.*?\\]\\s*)*public\\s+(?:async\\s+)?(?:Task<)?(?:ActionResult<)?(\\w+)(?:[<>\\[\\],\\s\\w]*)?\\s+(\\w+)\\s*\\(([^)]*)\\)`,
|
|
2337
|
+
"g"
|
|
2338
|
+
);
|
|
2339
|
+
while ((match = regex.exec(content)) !== null) {
|
|
2340
|
+
const routeSuffix = match[1] || "";
|
|
2341
|
+
const returnType = match[2];
|
|
2342
|
+
const actionName = match[3];
|
|
2343
|
+
const params = match[4];
|
|
2344
|
+
let fullPath = baseRoute;
|
|
2345
|
+
if (routeSuffix) {
|
|
2346
|
+
fullPath = `${baseRoute}/${routeSuffix}`;
|
|
2347
|
+
}
|
|
2348
|
+
const parameters = parseParameters(params);
|
|
2349
|
+
const methodAuthorize = content.substring(Math.max(0, match.index - 200), match.index).includes("[Authorize");
|
|
2350
|
+
endpoints.push({
|
|
2351
|
+
method: httpMethod.replace("Http", "").toUpperCase(),
|
|
2352
|
+
path: fullPath.replace(/\/+/g, "/"),
|
|
2353
|
+
controller: controllerName,
|
|
2354
|
+
action: actionName,
|
|
2355
|
+
parameters,
|
|
2356
|
+
returnType,
|
|
2357
|
+
authorize: classAuthorize || methodAuthorize
|
|
2358
|
+
});
|
|
2359
|
+
}
|
|
2360
|
+
}
|
|
2361
|
+
return endpoints;
|
|
2362
|
+
}
|
|
2363
|
+
function parseParameters(paramsString) {
|
|
2364
|
+
if (!paramsString.trim()) return [];
|
|
2365
|
+
const params = [];
|
|
2366
|
+
const parts = paramsString.split(",");
|
|
2367
|
+
for (const part of parts) {
|
|
2368
|
+
const trimmed = part.trim();
|
|
2369
|
+
if (!trimmed) continue;
|
|
2370
|
+
const match = trimmed.match(/(\w+)\s*(?:=.*)?$/);
|
|
2371
|
+
if (match) {
|
|
2372
|
+
if (trimmed.includes("[FromBody]")) {
|
|
2373
|
+
params.push(`body: ${match[1]}`);
|
|
2374
|
+
} else if (trimmed.includes("[FromQuery]")) {
|
|
2375
|
+
params.push(`query: ${match[1]}`);
|
|
2376
|
+
} else if (trimmed.includes("[FromRoute]")) {
|
|
2377
|
+
params.push(`route: ${match[1]}`);
|
|
2378
|
+
} else {
|
|
2379
|
+
params.push(match[1]);
|
|
2380
|
+
}
|
|
2381
|
+
}
|
|
2382
|
+
}
|
|
2383
|
+
return params;
|
|
2384
|
+
}
|
|
2385
|
+
function formatEndpoints(endpoints, filter) {
|
|
2386
|
+
const lines = [];
|
|
2387
|
+
lines.push("# SmartStack API Endpoints");
|
|
2388
|
+
lines.push("");
|
|
2389
|
+
if (filter) {
|
|
2390
|
+
lines.push(`> Filtered by: \`${filter}\``);
|
|
2391
|
+
lines.push("");
|
|
2392
|
+
}
|
|
2393
|
+
if (endpoints.length === 0) {
|
|
2394
|
+
lines.push("No endpoints found matching the criteria.");
|
|
2395
|
+
return lines.join("\n");
|
|
2396
|
+
}
|
|
2397
|
+
const byController = /* @__PURE__ */ new Map();
|
|
2398
|
+
for (const endpoint of endpoints) {
|
|
2399
|
+
const existing = byController.get(endpoint.controller) || [];
|
|
2400
|
+
existing.push(endpoint);
|
|
2401
|
+
byController.set(endpoint.controller, existing);
|
|
2402
|
+
}
|
|
2403
|
+
lines.push("## Summary");
|
|
2404
|
+
lines.push("");
|
|
2405
|
+
lines.push(`**Total Endpoints**: ${endpoints.length}`);
|
|
2406
|
+
lines.push(`**Controllers**: ${byController.size}`);
|
|
2407
|
+
lines.push("");
|
|
2408
|
+
const methodCounts = /* @__PURE__ */ new Map();
|
|
2409
|
+
for (const endpoint of endpoints) {
|
|
2410
|
+
methodCounts.set(endpoint.method, (methodCounts.get(endpoint.method) || 0) + 1);
|
|
2411
|
+
}
|
|
2412
|
+
lines.push("| Method | Count |");
|
|
2413
|
+
lines.push("|--------|-------|");
|
|
2414
|
+
for (const [method, count] of methodCounts) {
|
|
2415
|
+
lines.push(`| ${method} | ${count} |`);
|
|
2416
|
+
}
|
|
2417
|
+
lines.push("");
|
|
2418
|
+
for (const [controller, controllerEndpoints] of byController) {
|
|
2419
|
+
lines.push(`## ${controller}Controller`);
|
|
2420
|
+
lines.push("");
|
|
2421
|
+
controllerEndpoints.sort((a, b) => {
|
|
2422
|
+
const pathCompare = a.path.localeCompare(b.path);
|
|
2423
|
+
if (pathCompare !== 0) return pathCompare;
|
|
2424
|
+
return a.method.localeCompare(b.method);
|
|
2425
|
+
});
|
|
2426
|
+
for (const endpoint of controllerEndpoints) {
|
|
2427
|
+
const authBadge = endpoint.authorize ? " \u{1F512}" : "";
|
|
2428
|
+
const methodColor = getMethodEmoji(endpoint.method);
|
|
2429
|
+
lines.push(`### ${methodColor} \`${endpoint.method}\` ${endpoint.path}${authBadge}`);
|
|
2430
|
+
lines.push("");
|
|
2431
|
+
lines.push(`**Action**: \`${endpoint.action}\``);
|
|
2432
|
+
lines.push(`**Returns**: \`${endpoint.returnType}\``);
|
|
2433
|
+
if (endpoint.parameters.length > 0) {
|
|
2434
|
+
lines.push("");
|
|
2435
|
+
lines.push("**Parameters**:");
|
|
2436
|
+
for (const param of endpoint.parameters) {
|
|
2437
|
+
lines.push(`- \`${param}\``);
|
|
2438
|
+
}
|
|
2439
|
+
}
|
|
2440
|
+
lines.push("");
|
|
2441
|
+
}
|
|
2442
|
+
}
|
|
2443
|
+
lines.push("## Quick Reference");
|
|
2444
|
+
lines.push("");
|
|
2445
|
+
lines.push("| Method | Path | Action | Auth |");
|
|
2446
|
+
lines.push("|--------|------|--------|------|");
|
|
2447
|
+
for (const endpoint of endpoints) {
|
|
2448
|
+
lines.push(
|
|
2449
|
+
`| ${endpoint.method} | \`${endpoint.path}\` | ${endpoint.action} | ${endpoint.authorize ? "\u{1F512}" : "\u2713"} |`
|
|
2450
|
+
);
|
|
2451
|
+
}
|
|
2452
|
+
lines.push("");
|
|
2453
|
+
return lines.join("\n");
|
|
2454
|
+
}
|
|
2455
|
+
function getMethodEmoji(method) {
|
|
2456
|
+
switch (method) {
|
|
2457
|
+
case "GET":
|
|
2458
|
+
return "\u{1F535}";
|
|
2459
|
+
case "POST":
|
|
2460
|
+
return "\u{1F7E2}";
|
|
2461
|
+
case "PUT":
|
|
2462
|
+
return "\u{1F7E1}";
|
|
2463
|
+
case "PATCH":
|
|
2464
|
+
return "\u{1F7E0}";
|
|
2465
|
+
case "DELETE":
|
|
2466
|
+
return "\u{1F534}";
|
|
2467
|
+
default:
|
|
2468
|
+
return "\u26AA";
|
|
2469
|
+
}
|
|
2470
|
+
}
|
|
2471
|
+
|
|
2472
|
+
// src/resources/db-schema.ts
|
|
2473
|
+
import path11 from "path";
|
|
2474
|
+
var dbSchemaResourceTemplate = {
|
|
2475
|
+
uri: "smartstack://schema/",
|
|
2476
|
+
name: "SmartStack Database Schema",
|
|
2477
|
+
description: "Database schema information. Use smartstack://schema/{table} to get specific table.",
|
|
2478
|
+
mimeType: "text/markdown"
|
|
2479
|
+
};
|
|
2480
|
+
async function getDbSchemaResource(config, tableFilter) {
|
|
2481
|
+
const structure = await findSmartStackStructure(config.smartstack.projectPath);
|
|
2482
|
+
if (!structure.domain && !structure.infrastructure) {
|
|
2483
|
+
return "# Database Schema\n\nNo domain or infrastructure project found.";
|
|
2484
|
+
}
|
|
2485
|
+
const entities = [];
|
|
2486
|
+
if (structure.domain) {
|
|
2487
|
+
const entityFiles = await findEntityFiles(structure.domain);
|
|
2488
|
+
for (const file of entityFiles) {
|
|
2489
|
+
const entity = await parseEntity(file, structure.root, config);
|
|
2490
|
+
if (entity) {
|
|
2491
|
+
entities.push(entity);
|
|
2492
|
+
}
|
|
2493
|
+
}
|
|
2494
|
+
}
|
|
2495
|
+
if (structure.infrastructure) {
|
|
2496
|
+
await enrichFromConfigurations(entities, structure.infrastructure, config);
|
|
2497
|
+
}
|
|
2498
|
+
const filteredEntities = tableFilter ? entities.filter(
|
|
2499
|
+
(e) => e.name.toLowerCase().includes(tableFilter.toLowerCase()) || e.tableName.toLowerCase().includes(tableFilter.toLowerCase())
|
|
2500
|
+
) : entities;
|
|
2501
|
+
return formatSchema(filteredEntities, tableFilter, config);
|
|
2502
|
+
}
|
|
2503
|
+
async function parseEntity(filePath, rootPath, config) {
|
|
2504
|
+
const content = await readText(filePath);
|
|
2505
|
+
const fileName = path11.basename(filePath, ".cs");
|
|
2506
|
+
const classMatch = content.match(/public\s+(?:class|record)\s+(\w+)(?:\s*:\s*(\w+))?/);
|
|
2507
|
+
if (!classMatch) return null;
|
|
2508
|
+
const entityName = classMatch[1];
|
|
2509
|
+
const baseClass = classMatch[2];
|
|
2510
|
+
if (entityName.endsWith("Dto") || entityName.endsWith("Command") || entityName.endsWith("Query") || entityName.endsWith("Handler")) {
|
|
2511
|
+
return null;
|
|
2512
|
+
}
|
|
2513
|
+
const properties = [];
|
|
2514
|
+
const relationships = [];
|
|
2515
|
+
const propertyPattern = /public\s+(?:required\s+)?(\w+(?:<[\w,\s]+>)?)\??\s+(\w+)\s*\{/g;
|
|
2516
|
+
let match;
|
|
2517
|
+
while ((match = propertyPattern.exec(content)) !== null) {
|
|
2518
|
+
const propertyType = match[1];
|
|
2519
|
+
const propertyName = match[2];
|
|
2520
|
+
if (propertyType.startsWith("ICollection") || propertyType.startsWith("List")) {
|
|
2521
|
+
const targetMatch = propertyType.match(/<(\w+)>/);
|
|
2522
|
+
if (targetMatch) {
|
|
2523
|
+
relationships.push({
|
|
2524
|
+
type: "one-to-many",
|
|
2525
|
+
targetEntity: targetMatch[1],
|
|
2526
|
+
propertyName
|
|
2527
|
+
});
|
|
2528
|
+
}
|
|
2529
|
+
continue;
|
|
2530
|
+
}
|
|
2531
|
+
const isNavigationProperty = /^[A-Z]/.test(propertyType) && ![
|
|
2532
|
+
"Guid",
|
|
2533
|
+
"String",
|
|
2534
|
+
"Int32",
|
|
2535
|
+
"Int64",
|
|
2536
|
+
"DateTime",
|
|
2537
|
+
"DateTimeOffset",
|
|
2538
|
+
"Boolean",
|
|
2539
|
+
"Decimal",
|
|
2540
|
+
"Double",
|
|
2541
|
+
"Float",
|
|
2542
|
+
"Byte"
|
|
2543
|
+
].includes(propertyType) && !propertyType.includes("?");
|
|
2544
|
+
if (isNavigationProperty && !propertyType.includes("<")) {
|
|
2545
|
+
relationships.push({
|
|
2546
|
+
type: "one-to-one",
|
|
2547
|
+
targetEntity: propertyType,
|
|
2548
|
+
propertyName
|
|
2549
|
+
});
|
|
2550
|
+
continue;
|
|
2551
|
+
}
|
|
2552
|
+
properties.push({
|
|
2553
|
+
name: propertyName,
|
|
2554
|
+
type: mapCSharpType(propertyType),
|
|
2555
|
+
nullable: content.includes(`${propertyType}? ${propertyName}`) || content.includes(`${propertyType}? ${propertyName}`),
|
|
2556
|
+
isPrimaryKey: propertyName === "Id" || propertyName === `${entityName}Id`
|
|
2557
|
+
});
|
|
2558
|
+
}
|
|
2559
|
+
const tableName = `${config.conventions.tablePrefix.core}${entityName}s`;
|
|
2560
|
+
return {
|
|
2561
|
+
name: entityName,
|
|
2562
|
+
tableName,
|
|
2563
|
+
properties,
|
|
2564
|
+
relationships,
|
|
2565
|
+
file: path11.relative(rootPath, filePath)
|
|
2566
|
+
};
|
|
2567
|
+
}
|
|
2568
|
+
async function enrichFromConfigurations(entities, infrastructurePath, config) {
|
|
2569
|
+
const configFiles = await findFiles("**/Configurations/**/*.cs", {
|
|
2570
|
+
cwd: infrastructurePath
|
|
2571
|
+
});
|
|
2572
|
+
for (const file of configFiles) {
|
|
2573
|
+
const content = await readText(file);
|
|
2574
|
+
const tableMatch = content.match(/\.ToTable\s*\(\s*"([^"]+)"/);
|
|
2575
|
+
if (tableMatch) {
|
|
2576
|
+
const entityMatch = content.match(/IEntityTypeConfiguration<(\w+)>/);
|
|
2577
|
+
if (entityMatch) {
|
|
2578
|
+
const entityName = entityMatch[1];
|
|
2579
|
+
const entity = entities.find((e) => e.name === entityName);
|
|
2580
|
+
if (entity) {
|
|
2581
|
+
entity.tableName = tableMatch[1];
|
|
2582
|
+
}
|
|
2583
|
+
}
|
|
2584
|
+
}
|
|
2585
|
+
const maxLengthMatches = content.matchAll(/\.Property\s*\(\s*\w+\s*=>\s*\w+\.(\w+)\s*\)[\s\S]*?\.HasMaxLength\s*\(\s*(\d+)\s*\)/g);
|
|
2586
|
+
for (const match of maxLengthMatches) {
|
|
2587
|
+
const propertyName = match[1];
|
|
2588
|
+
const maxLength = parseInt(match[2], 10);
|
|
2589
|
+
for (const entity of entities) {
|
|
2590
|
+
const property = entity.properties.find((p) => p.name === propertyName);
|
|
2591
|
+
if (property) {
|
|
2592
|
+
property.maxLength = maxLength;
|
|
2593
|
+
}
|
|
2594
|
+
}
|
|
2595
|
+
}
|
|
2596
|
+
}
|
|
2597
|
+
}
|
|
2598
|
+
function mapCSharpType(csharpType) {
|
|
2599
|
+
const typeMap = {
|
|
2600
|
+
"Guid": "uniqueidentifier",
|
|
2601
|
+
"string": "nvarchar",
|
|
2602
|
+
"String": "nvarchar",
|
|
2603
|
+
"int": "int",
|
|
2604
|
+
"Int32": "int",
|
|
2605
|
+
"long": "bigint",
|
|
2606
|
+
"Int64": "bigint",
|
|
2607
|
+
"DateTime": "datetime2",
|
|
2608
|
+
"DateTimeOffset": "datetimeoffset",
|
|
2609
|
+
"bool": "bit",
|
|
2610
|
+
"Boolean": "bit",
|
|
2611
|
+
"decimal": "decimal",
|
|
2612
|
+
"Decimal": "decimal",
|
|
2613
|
+
"double": "float",
|
|
2614
|
+
"Double": "float",
|
|
2615
|
+
"float": "real",
|
|
2616
|
+
"Float": "real",
|
|
2617
|
+
"byte[]": "varbinary"
|
|
2618
|
+
};
|
|
2619
|
+
const baseType = csharpType.replace("?", "");
|
|
2620
|
+
return typeMap[baseType] || "nvarchar";
|
|
2621
|
+
}
|
|
2622
|
+
function formatSchema(entities, filter, config) {
|
|
2623
|
+
const lines = [];
|
|
2624
|
+
lines.push("# SmartStack Database Schema");
|
|
2625
|
+
lines.push("");
|
|
2626
|
+
if (filter) {
|
|
2627
|
+
lines.push(`> Filtered by: \`${filter}\``);
|
|
2628
|
+
lines.push("");
|
|
2629
|
+
}
|
|
2630
|
+
if (entities.length === 0) {
|
|
2631
|
+
lines.push("No entities found matching the criteria.");
|
|
2632
|
+
return lines.join("\n");
|
|
2633
|
+
}
|
|
2634
|
+
lines.push("## Summary");
|
|
2635
|
+
lines.push("");
|
|
2636
|
+
lines.push(`**Total Entities**: ${entities.length}`);
|
|
2637
|
+
lines.push("");
|
|
2638
|
+
lines.push("| Entity | Table | Properties | Relationships |");
|
|
2639
|
+
lines.push("|--------|-------|------------|---------------|");
|
|
2640
|
+
for (const entity of entities) {
|
|
2641
|
+
lines.push(
|
|
2642
|
+
`| ${entity.name} | \`${entity.tableName}\` | ${entity.properties.length} | ${entity.relationships.length} |`
|
|
2643
|
+
);
|
|
2644
|
+
}
|
|
2645
|
+
lines.push("");
|
|
2646
|
+
for (const entity of entities) {
|
|
2647
|
+
lines.push(`## ${entity.name}`);
|
|
2648
|
+
lines.push("");
|
|
2649
|
+
lines.push(`**Table**: \`${entity.tableName}\``);
|
|
2650
|
+
lines.push(`**File**: \`${entity.file}\``);
|
|
2651
|
+
lines.push("");
|
|
2652
|
+
lines.push("### Columns");
|
|
2653
|
+
lines.push("");
|
|
2654
|
+
lines.push("| Column | SQL Type | Nullable | Notes |");
|
|
2655
|
+
lines.push("|--------|----------|----------|-------|");
|
|
2656
|
+
for (const prop of entity.properties) {
|
|
2657
|
+
const notes = [];
|
|
2658
|
+
if (prop.isPrimaryKey) notes.push("PK");
|
|
2659
|
+
if (prop.maxLength) notes.push(`MaxLength(${prop.maxLength})`);
|
|
2660
|
+
lines.push(
|
|
2661
|
+
`| ${prop.name} | ${prop.type} | ${prop.nullable ? "Yes" : "No"} | ${notes.join(", ")} |`
|
|
2662
|
+
);
|
|
2663
|
+
}
|
|
2664
|
+
lines.push("");
|
|
2665
|
+
if (entity.relationships.length > 0) {
|
|
2666
|
+
lines.push("### Relationships");
|
|
2667
|
+
lines.push("");
|
|
2668
|
+
lines.push("| Type | Target | Property |");
|
|
2669
|
+
lines.push("|------|--------|----------|");
|
|
2670
|
+
for (const rel of entity.relationships) {
|
|
2671
|
+
const typeIcon = rel.type === "one-to-one" ? "1:1" : rel.type === "one-to-many" ? "1:N" : "N:N";
|
|
2672
|
+
lines.push(`| ${typeIcon} | ${rel.targetEntity} | ${rel.propertyName} |`);
|
|
2673
|
+
}
|
|
2674
|
+
lines.push("");
|
|
2675
|
+
}
|
|
2676
|
+
lines.push("### EF Core Configuration");
|
|
2677
|
+
lines.push("");
|
|
2678
|
+
lines.push("```csharp");
|
|
2679
|
+
lines.push(`public class ${entity.name}Configuration : IEntityTypeConfiguration<${entity.name}>`);
|
|
2680
|
+
lines.push("{");
|
|
2681
|
+
lines.push(` public void Configure(EntityTypeBuilder<${entity.name}> builder)`);
|
|
2682
|
+
lines.push(" {");
|
|
2683
|
+
lines.push(` builder.ToTable("${entity.tableName}");`);
|
|
2684
|
+
lines.push("");
|
|
2685
|
+
const pk = entity.properties.find((p) => p.isPrimaryKey);
|
|
2686
|
+
if (pk) {
|
|
2687
|
+
lines.push(` builder.HasKey(e => e.${pk.name});`);
|
|
2688
|
+
}
|
|
2689
|
+
lines.push(" }");
|
|
2690
|
+
lines.push("}");
|
|
2691
|
+
lines.push("```");
|
|
2692
|
+
lines.push("");
|
|
2693
|
+
}
|
|
2694
|
+
if (entities.length > 1) {
|
|
2695
|
+
lines.push("## Entity Relationships");
|
|
2696
|
+
lines.push("");
|
|
2697
|
+
lines.push("```");
|
|
2698
|
+
for (const entity of entities) {
|
|
2699
|
+
for (const rel of entity.relationships) {
|
|
2700
|
+
const arrow = rel.type === "one-to-one" ? "\u2500\u2500" : rel.type === "one-to-many" ? "\u2500<" : ">\u2500<";
|
|
2701
|
+
lines.push(`${entity.name} ${arrow} ${rel.targetEntity}`);
|
|
2702
|
+
}
|
|
2703
|
+
}
|
|
2704
|
+
lines.push("```");
|
|
2705
|
+
lines.push("");
|
|
2706
|
+
}
|
|
2707
|
+
return lines.join("\n");
|
|
2708
|
+
}
|
|
2709
|
+
|
|
2710
|
+
// src/server.ts
|
|
2711
|
+
async function createServer() {
|
|
2712
|
+
const config = await getConfig();
|
|
2713
|
+
const server = new Server(
|
|
2714
|
+
{
|
|
2715
|
+
name: "smartstack-mcp",
|
|
2716
|
+
version: "1.0.0"
|
|
2717
|
+
},
|
|
2718
|
+
{
|
|
2719
|
+
capabilities: {
|
|
2720
|
+
tools: {},
|
|
2721
|
+
resources: {}
|
|
2722
|
+
}
|
|
2723
|
+
}
|
|
2724
|
+
);
|
|
2725
|
+
server.setRequestHandler(ListToolsRequestSchema, async () => {
|
|
2726
|
+
logger.debug("Listing tools");
|
|
2727
|
+
return {
|
|
2728
|
+
tools: [
|
|
2729
|
+
validateConventionsTool,
|
|
2730
|
+
checkMigrationsTool,
|
|
2731
|
+
scaffoldExtensionTool,
|
|
2732
|
+
apiDocsTool
|
|
2733
|
+
]
|
|
2734
|
+
};
|
|
2735
|
+
});
|
|
2736
|
+
server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
2737
|
+
const { name, arguments: args } = request.params;
|
|
2738
|
+
const startTime = Date.now();
|
|
2739
|
+
logger.toolStart(name, args);
|
|
2740
|
+
try {
|
|
2741
|
+
let result;
|
|
2742
|
+
switch (name) {
|
|
2743
|
+
case "validate_conventions":
|
|
2744
|
+
result = await handleValidateConventions(args, config);
|
|
2745
|
+
break;
|
|
2746
|
+
case "check_migrations":
|
|
2747
|
+
result = await handleCheckMigrations(args, config);
|
|
2748
|
+
break;
|
|
2749
|
+
case "scaffold_extension":
|
|
2750
|
+
result = await handleScaffoldExtension(args, config);
|
|
2751
|
+
break;
|
|
2752
|
+
case "api_docs":
|
|
2753
|
+
result = await handleApiDocs(args, config);
|
|
2754
|
+
break;
|
|
2755
|
+
default:
|
|
2756
|
+
throw new Error(`Unknown tool: ${name}`);
|
|
2757
|
+
}
|
|
2758
|
+
logger.toolEnd(name, true, Date.now() - startTime);
|
|
2759
|
+
return {
|
|
2760
|
+
content: [
|
|
2761
|
+
{
|
|
2762
|
+
type: "text",
|
|
2763
|
+
text: result
|
|
2764
|
+
}
|
|
2765
|
+
]
|
|
2766
|
+
};
|
|
2767
|
+
} catch (error) {
|
|
2768
|
+
const err = error instanceof Error ? error : new Error(String(error));
|
|
2769
|
+
logger.toolError(name, err);
|
|
2770
|
+
return {
|
|
2771
|
+
content: [
|
|
2772
|
+
{
|
|
2773
|
+
type: "text",
|
|
2774
|
+
text: `Error: ${err.message}`
|
|
2775
|
+
}
|
|
2776
|
+
],
|
|
2777
|
+
isError: true
|
|
2778
|
+
};
|
|
2779
|
+
}
|
|
2780
|
+
});
|
|
2781
|
+
server.setRequestHandler(ListResourcesRequestSchema, async () => {
|
|
2782
|
+
logger.debug("Listing resources");
|
|
2783
|
+
return {
|
|
2784
|
+
resources: [
|
|
2785
|
+
conventionsResourceTemplate,
|
|
2786
|
+
projectInfoResourceTemplate,
|
|
2787
|
+
apiEndpointsResourceTemplate,
|
|
2788
|
+
dbSchemaResourceTemplate
|
|
2789
|
+
]
|
|
2790
|
+
};
|
|
2791
|
+
});
|
|
2792
|
+
server.setRequestHandler(ReadResourceRequestSchema, async (request) => {
|
|
2793
|
+
const { uri } = request.params;
|
|
2794
|
+
logger.debug("Reading resource", { uri });
|
|
2795
|
+
try {
|
|
2796
|
+
let content;
|
|
2797
|
+
let mimeType = "text/markdown";
|
|
2798
|
+
if (uri === "smartstack://conventions") {
|
|
2799
|
+
content = await getConventionsResource(config);
|
|
2800
|
+
} else if (uri === "smartstack://project") {
|
|
2801
|
+
content = await getProjectInfoResource(config);
|
|
2802
|
+
} else if (uri.startsWith("smartstack://api/")) {
|
|
2803
|
+
const endpoint = uri.replace("smartstack://api/", "");
|
|
2804
|
+
content = await getApiEndpointsResource(config, endpoint);
|
|
2805
|
+
} else if (uri.startsWith("smartstack://schema/")) {
|
|
2806
|
+
const table = uri.replace("smartstack://schema/", "");
|
|
2807
|
+
content = await getDbSchemaResource(config, table);
|
|
2808
|
+
} else {
|
|
2809
|
+
throw new Error(`Unknown resource: ${uri}`);
|
|
2810
|
+
}
|
|
2811
|
+
return {
|
|
2812
|
+
contents: [
|
|
2813
|
+
{
|
|
2814
|
+
uri,
|
|
2815
|
+
mimeType,
|
|
2816
|
+
text: content
|
|
2817
|
+
}
|
|
2818
|
+
]
|
|
2819
|
+
};
|
|
2820
|
+
} catch (error) {
|
|
2821
|
+
const err = error instanceof Error ? error : new Error(String(error));
|
|
2822
|
+
logger.error("Resource read failed", { uri, error: err.message });
|
|
2823
|
+
throw err;
|
|
2824
|
+
}
|
|
2825
|
+
});
|
|
2826
|
+
return server;
|
|
2827
|
+
}
|
|
2828
|
+
async function runServer() {
|
|
2829
|
+
logger.info("Starting SmartStack MCP Server...");
|
|
2830
|
+
const server = await createServer();
|
|
2831
|
+
const transport = new StdioServerTransport();
|
|
2832
|
+
await server.connect(transport);
|
|
2833
|
+
logger.info("SmartStack MCP Server running on stdio");
|
|
2834
|
+
}
|
|
2835
|
+
|
|
2836
|
+
// src/index.ts
|
|
2837
|
+
process.on("uncaughtException", (error) => {
|
|
2838
|
+
logger.error("Uncaught exception", { error: error.message, stack: error.stack });
|
|
2839
|
+
process.exit(1);
|
|
2840
|
+
});
|
|
2841
|
+
process.on("unhandledRejection", (reason) => {
|
|
2842
|
+
logger.error("Unhandled rejection", { reason });
|
|
2843
|
+
process.exit(1);
|
|
2844
|
+
});
|
|
2845
|
+
runServer().catch((error) => {
|
|
2846
|
+
logger.error("Failed to start server", { error: error.message });
|
|
2847
|
+
process.exit(1);
|
|
2848
|
+
});
|
|
2849
|
+
//# sourceMappingURL=index.js.map
|