@atlashub/smartstack-cli 2.9.0 → 3.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/.documentation/agents.html +1 -371
- package/.documentation/business-analyse.html +81 -17
- package/.documentation/cli-commands.html +1 -1
- package/.documentation/commands.html +1 -1
- package/.documentation/efcore.html +1 -1
- package/.documentation/gitflow.html +1 -1
- package/.documentation/hooks.html +27 -66
- package/.documentation/index.html +166 -166
- package/.documentation/init.html +6 -7
- package/.documentation/installation.html +1 -1
- package/.documentation/ralph-loop.html +1 -9
- package/.documentation/test-web.html +15 -39
- package/dist/index.js +23 -16
- package/dist/index.js.map +1 -1
- package/dist/mcp-entry.mjs +1302 -223
- package/dist/mcp-entry.mjs.map +1 -1
- package/package.json +1 -1
- package/templates/agents/efcore/db-deploy.md +1 -1
- package/templates/agents/efcore/migration.md +26 -10
- package/templates/agents/efcore/rebase-snapshot.md +24 -7
- package/templates/agents/efcore/squash.md +73 -57
- package/templates/agents/gitflow/commit.md +138 -18
- package/templates/agents/gitflow/exec.md +1 -1
- package/templates/agents/gitflow/finish.md +79 -62
- package/templates/agents/gitflow/init-clone.md +186 -0
- package/templates/agents/gitflow/init-detect.md +137 -0
- package/templates/agents/gitflow/init-validate.md +210 -0
- package/templates/agents/gitflow/init.md +231 -74
- package/templates/agents/gitflow/merge.md +115 -33
- package/templates/agents/gitflow/pr.md +151 -46
- package/templates/agents/gitflow/start.md +76 -33
- package/templates/agents/gitflow/status.md +41 -71
- package/templates/hooks/appsettings-guard.sh +76 -0
- package/templates/hooks/ef-migration-check.md +1 -1
- package/templates/hooks/hooks.json +9 -0
- package/templates/project/appsettings.json.template +8 -2
- package/templates/project/test-frontend/msw/handlers.ts +58 -0
- package/templates/project/test-frontend/msw/server.ts +25 -0
- package/templates/project/test-frontend/setup.ts +16 -0
- package/templates/project/test-frontend/test-utils.tsx +59 -0
- package/templates/project/test-frontend/vitest.config.ts +31 -0
- package/templates/skills/_resources/config-safety.md +61 -0
- package/templates/skills/_resources/formatting-guide.md +2 -2
- package/templates/skills/application/SKILL.md +12 -3
- package/templates/skills/application/steps/step-04-backend.md +21 -0
- package/templates/skills/application/steps/step-07-tests.md +259 -120
- package/templates/skills/business-analyse/SKILL.md +57 -28
- package/templates/skills/business-analyse/_shared.md +70 -39
- package/templates/skills/business-analyse/html/ba-interactive.html +2596 -0
- package/templates/skills/business-analyse/questionnaire/00-application.md +123 -131
- package/templates/skills/business-analyse/questionnaire/01-context.md +173 -24
- package/templates/skills/business-analyse/questionnaire/02-stakeholders.md +170 -50
- package/templates/skills/business-analyse/questionnaire/03-scope.md +154 -48
- package/templates/skills/business-analyse/questionnaire/10-documentation.md +1 -1
- package/templates/skills/business-analyse/questionnaire/14-risk-assumptions.md +135 -0
- package/templates/skills/business-analyse/questionnaire/15-success-metrics.md +136 -0
- package/templates/skills/business-analyse/questionnaire.md +55 -46
- package/templates/skills/business-analyse/steps/step-00-init.md +24 -2
- package/templates/skills/business-analyse/steps/step-01-cadrage.md +31 -20
- package/templates/skills/business-analyse/steps/step-03-specify.md +58 -0
- package/templates/skills/business-analyse/steps/step-05-handoff.md +301 -1
- package/templates/skills/business-analyse/steps/step-06-extract.md +518 -0
- package/templates/skills/check-version/SKILL.md +1 -1
- package/templates/skills/efcore/steps/db/step-deploy.md +22 -3
- package/templates/skills/efcore/steps/db/step-reset.md +27 -4
- package/templates/skills/efcore/steps/db/step-seed.md +46 -2
- package/templates/skills/efcore/steps/db/step-status.md +14 -0
- package/templates/skills/efcore/steps/migration/step-01-check.md +31 -5
- package/templates/skills/efcore/steps/migration/step-02-create.md +20 -4
- package/templates/skills/efcore/steps/rebase-snapshot/step-03-create.md +60 -0
- package/templates/skills/efcore/steps/shared/step-00-init.md +47 -8
- package/templates/skills/efcore/steps/squash/step-03-create.md +27 -5
- package/templates/skills/gitflow/SKILL.md +91 -29
- package/templates/skills/gitflow/_shared.md +144 -2
- package/templates/skills/gitflow/phases/status.md +11 -1
- package/templates/skills/gitflow/steps/step-commit.md +1 -1
- package/templates/skills/gitflow/steps/step-init.md +202 -39
- package/templates/skills/gitflow/steps/step-pr.md +17 -5
- package/templates/skills/gitflow/templates/config.json +10 -1
- package/templates/skills/ralph-loop/SKILL.md +22 -15
- package/templates/skills/ralph-loop/steps/step-01-task.md +89 -4
- package/templates/skills/ralph-loop/steps/step-02-execute.md +408 -23
- package/templates/skills/ralph-loop/steps/step-03-commit.md +84 -2
- package/templates/skills/ralph-loop/steps/step-04-check.md +235 -6
- package/templates/skills/ralph-loop/steps/step-05-report.md +115 -0
- package/templates/skills/validate-feature/SKILL.md +83 -0
- package/templates/skills/validate-feature/steps/step-01-compile.md +38 -0
- package/templates/skills/validate-feature/steps/step-02-unit-tests.md +45 -0
- package/templates/skills/validate-feature/steps/step-03-integration-tests.md +53 -0
- package/templates/skills/validate-feature/steps/step-04-api-smoke.md +157 -0
package/dist/mcp-entry.mjs
CHANGED
|
@@ -471,8 +471,8 @@ var init_parseUtil = __esm({
|
|
|
471
471
|
init_errors();
|
|
472
472
|
init_en();
|
|
473
473
|
makeIssue = (params) => {
|
|
474
|
-
const { data, path:
|
|
475
|
-
const fullPath = [...
|
|
474
|
+
const { data, path: path30, errorMaps, issueData } = params;
|
|
475
|
+
const fullPath = [...path30, ...issueData.path || []];
|
|
476
476
|
const fullIssue = {
|
|
477
477
|
...issueData,
|
|
478
478
|
path: fullPath
|
|
@@ -786,11 +786,11 @@ var init_types = __esm({
|
|
|
786
786
|
init_parseUtil();
|
|
787
787
|
init_util();
|
|
788
788
|
ParseInputLazyPath = class {
|
|
789
|
-
constructor(parent, value,
|
|
789
|
+
constructor(parent, value, path30, key) {
|
|
790
790
|
this._cachedPath = [];
|
|
791
791
|
this.parent = parent;
|
|
792
792
|
this.data = value;
|
|
793
|
-
this._path =
|
|
793
|
+
this._path = path30;
|
|
794
794
|
this._key = key;
|
|
795
795
|
}
|
|
796
796
|
get path() {
|
|
@@ -4367,10 +4367,10 @@ function assignProp(target, prop, value) {
|
|
|
4367
4367
|
configurable: true
|
|
4368
4368
|
});
|
|
4369
4369
|
}
|
|
4370
|
-
function getElementAtPath(obj,
|
|
4371
|
-
if (!
|
|
4370
|
+
function getElementAtPath(obj, path30) {
|
|
4371
|
+
if (!path30)
|
|
4372
4372
|
return obj;
|
|
4373
|
-
return
|
|
4373
|
+
return path30.reduce((acc, key) => acc?.[key], obj);
|
|
4374
4374
|
}
|
|
4375
4375
|
function promiseAllObject(promisesObj) {
|
|
4376
4376
|
const keys = Object.keys(promisesObj);
|
|
@@ -4619,11 +4619,11 @@ function aborted(x, startIndex = 0) {
|
|
|
4619
4619
|
}
|
|
4620
4620
|
return false;
|
|
4621
4621
|
}
|
|
4622
|
-
function prefixIssues(
|
|
4622
|
+
function prefixIssues(path30, issues) {
|
|
4623
4623
|
return issues.map((iss) => {
|
|
4624
4624
|
var _a;
|
|
4625
4625
|
(_a = iss).path ?? (_a.path = []);
|
|
4626
|
-
iss.path.unshift(
|
|
4626
|
+
iss.path.unshift(path30);
|
|
4627
4627
|
return iss;
|
|
4628
4628
|
});
|
|
4629
4629
|
}
|
|
@@ -14435,8 +14435,8 @@ var require_utils = __commonJS({
|
|
|
14435
14435
|
}
|
|
14436
14436
|
return ind;
|
|
14437
14437
|
}
|
|
14438
|
-
function removeDotSegments(
|
|
14439
|
-
let input =
|
|
14438
|
+
function removeDotSegments(path30) {
|
|
14439
|
+
let input = path30;
|
|
14440
14440
|
const output = [];
|
|
14441
14441
|
let nextSlash = -1;
|
|
14442
14442
|
let len = 0;
|
|
@@ -14636,8 +14636,8 @@ var require_schemes = __commonJS({
|
|
|
14636
14636
|
wsComponent.secure = void 0;
|
|
14637
14637
|
}
|
|
14638
14638
|
if (wsComponent.resourceName) {
|
|
14639
|
-
const [
|
|
14640
|
-
wsComponent.path =
|
|
14639
|
+
const [path30, query] = wsComponent.resourceName.split("?");
|
|
14640
|
+
wsComponent.path = path30 && path30 !== "/" ? path30 : void 0;
|
|
14641
14641
|
wsComponent.query = query;
|
|
14642
14642
|
wsComponent.resourceName = void 0;
|
|
14643
14643
|
}
|
|
@@ -28777,12 +28777,12 @@ var init_esm7 = __esm({
|
|
|
28777
28777
|
/**
|
|
28778
28778
|
* Get the Path object referenced by the string path, resolved from this Path
|
|
28779
28779
|
*/
|
|
28780
|
-
resolve(
|
|
28781
|
-
if (!
|
|
28780
|
+
resolve(path30) {
|
|
28781
|
+
if (!path30) {
|
|
28782
28782
|
return this;
|
|
28783
28783
|
}
|
|
28784
|
-
const rootPath = this.getRootString(
|
|
28785
|
-
const dir =
|
|
28784
|
+
const rootPath = this.getRootString(path30);
|
|
28785
|
+
const dir = path30.substring(rootPath.length);
|
|
28786
28786
|
const dirParts = dir.split(this.splitSep);
|
|
28787
28787
|
const result = rootPath ? this.getRoot(rootPath).#resolveParts(dirParts) : this.#resolveParts(dirParts);
|
|
28788
28788
|
return result;
|
|
@@ -29534,8 +29534,8 @@ var init_esm7 = __esm({
|
|
|
29534
29534
|
/**
|
|
29535
29535
|
* @internal
|
|
29536
29536
|
*/
|
|
29537
|
-
getRootString(
|
|
29538
|
-
return win32.parse(
|
|
29537
|
+
getRootString(path30) {
|
|
29538
|
+
return win32.parse(path30).root;
|
|
29539
29539
|
}
|
|
29540
29540
|
/**
|
|
29541
29541
|
* @internal
|
|
@@ -29581,8 +29581,8 @@ var init_esm7 = __esm({
|
|
|
29581
29581
|
/**
|
|
29582
29582
|
* @internal
|
|
29583
29583
|
*/
|
|
29584
|
-
getRootString(
|
|
29585
|
-
return
|
|
29584
|
+
getRootString(path30) {
|
|
29585
|
+
return path30.startsWith("/") ? "/" : "";
|
|
29586
29586
|
}
|
|
29587
29587
|
/**
|
|
29588
29588
|
* @internal
|
|
@@ -29671,11 +29671,11 @@ var init_esm7 = __esm({
|
|
|
29671
29671
|
/**
|
|
29672
29672
|
* Get the depth of a provided path, string, or the cwd
|
|
29673
29673
|
*/
|
|
29674
|
-
depth(
|
|
29675
|
-
if (typeof
|
|
29676
|
-
|
|
29674
|
+
depth(path30 = this.cwd) {
|
|
29675
|
+
if (typeof path30 === "string") {
|
|
29676
|
+
path30 = this.cwd.resolve(path30);
|
|
29677
29677
|
}
|
|
29678
|
-
return
|
|
29678
|
+
return path30.depth();
|
|
29679
29679
|
}
|
|
29680
29680
|
/**
|
|
29681
29681
|
* Return the cache of child entries. Exposed so subclasses can create
|
|
@@ -30162,9 +30162,9 @@ var init_esm7 = __esm({
|
|
|
30162
30162
|
process3();
|
|
30163
30163
|
return results;
|
|
30164
30164
|
}
|
|
30165
|
-
chdir(
|
|
30165
|
+
chdir(path30 = this.cwd) {
|
|
30166
30166
|
const oldCwd = this.cwd;
|
|
30167
|
-
this.cwd = typeof
|
|
30167
|
+
this.cwd = typeof path30 === "string" ? this.cwd.resolve(path30) : path30;
|
|
30168
30168
|
this.cwd[setAsCwd](oldCwd);
|
|
30169
30169
|
}
|
|
30170
30170
|
};
|
|
@@ -30545,8 +30545,8 @@ var init_processor = __esm({
|
|
|
30545
30545
|
}
|
|
30546
30546
|
// match, absolute, ifdir
|
|
30547
30547
|
entries() {
|
|
30548
|
-
return [...this.store.entries()].map(([
|
|
30549
|
-
|
|
30548
|
+
return [...this.store.entries()].map(([path30, n]) => [
|
|
30549
|
+
path30,
|
|
30550
30550
|
!!(n & 2),
|
|
30551
30551
|
!!(n & 1)
|
|
30552
30552
|
]);
|
|
@@ -30761,9 +30761,9 @@ var init_walker = __esm({
|
|
|
30761
30761
|
signal;
|
|
30762
30762
|
maxDepth;
|
|
30763
30763
|
includeChildMatches;
|
|
30764
|
-
constructor(patterns,
|
|
30764
|
+
constructor(patterns, path30, opts) {
|
|
30765
30765
|
this.patterns = patterns;
|
|
30766
|
-
this.path =
|
|
30766
|
+
this.path = path30;
|
|
30767
30767
|
this.opts = opts;
|
|
30768
30768
|
this.#sep = !opts.posix && opts.platform === "win32" ? "\\" : "/";
|
|
30769
30769
|
this.includeChildMatches = opts.includeChildMatches !== false;
|
|
@@ -30782,11 +30782,11 @@ var init_walker = __esm({
|
|
|
30782
30782
|
});
|
|
30783
30783
|
}
|
|
30784
30784
|
}
|
|
30785
|
-
#ignored(
|
|
30786
|
-
return this.seen.has(
|
|
30785
|
+
#ignored(path30) {
|
|
30786
|
+
return this.seen.has(path30) || !!this.#ignore?.ignored?.(path30);
|
|
30787
30787
|
}
|
|
30788
|
-
#childrenIgnored(
|
|
30789
|
-
return !!this.#ignore?.childrenIgnored?.(
|
|
30788
|
+
#childrenIgnored(path30) {
|
|
30789
|
+
return !!this.#ignore?.childrenIgnored?.(path30);
|
|
30790
30790
|
}
|
|
30791
30791
|
// backpressure mechanism
|
|
30792
30792
|
pause() {
|
|
@@ -31001,8 +31001,8 @@ var init_walker = __esm({
|
|
|
31001
31001
|
};
|
|
31002
31002
|
GlobWalker = class extends GlobUtil {
|
|
31003
31003
|
matches = /* @__PURE__ */ new Set();
|
|
31004
|
-
constructor(patterns,
|
|
31005
|
-
super(patterns,
|
|
31004
|
+
constructor(patterns, path30, opts) {
|
|
31005
|
+
super(patterns, path30, opts);
|
|
31006
31006
|
}
|
|
31007
31007
|
matchEmit(e) {
|
|
31008
31008
|
this.matches.add(e);
|
|
@@ -31039,8 +31039,8 @@ var init_walker = __esm({
|
|
|
31039
31039
|
};
|
|
31040
31040
|
GlobStream = class extends GlobUtil {
|
|
31041
31041
|
results;
|
|
31042
|
-
constructor(patterns,
|
|
31043
|
-
super(patterns,
|
|
31042
|
+
constructor(patterns, path30, opts) {
|
|
31043
|
+
super(patterns, path30, opts);
|
|
31044
31044
|
this.results = new Minipass({
|
|
31045
31045
|
signal: this.signal,
|
|
31046
31046
|
objectMode: true
|
|
@@ -31477,10 +31477,10 @@ var init_fs = __esm({
|
|
|
31477
31477
|
init_esm_shims();
|
|
31478
31478
|
init_esm8();
|
|
31479
31479
|
FileSystemError = class extends Error {
|
|
31480
|
-
constructor(message, operation,
|
|
31480
|
+
constructor(message, operation, path30, cause) {
|
|
31481
31481
|
super(message);
|
|
31482
31482
|
this.operation = operation;
|
|
31483
|
-
this.path =
|
|
31483
|
+
this.path = path30;
|
|
31484
31484
|
this.cause = cause;
|
|
31485
31485
|
this.name = "FileSystemError";
|
|
31486
31486
|
}
|
|
@@ -31641,7 +31641,7 @@ var init_types3 = __esm({
|
|
|
31641
31641
|
dryRun: external_exports.boolean().default(false).describe("Preview without writing files or creating migration")
|
|
31642
31642
|
});
|
|
31643
31643
|
TestTypeSchema = external_exports.enum(["unit", "integration", "security", "e2e"]);
|
|
31644
|
-
TestTargetSchema = external_exports.enum(["entity", "service", "controller", "validator", "repository", "all"]);
|
|
31644
|
+
TestTargetSchema = external_exports.enum(["entity", "service", "controller", "validator", "repository", "infrastructure", "all"]);
|
|
31645
31645
|
ScaffoldTestsInputSchema = external_exports.object({
|
|
31646
31646
|
target: TestTargetSchema.describe("Type of component to test"),
|
|
31647
31647
|
name: external_exports.string().min(1).describe('Component name (PascalCase, e.g., "User", "Order")'),
|
|
@@ -33365,6 +33365,37 @@ var init_validate_conventions = __esm({
|
|
|
33365
33365
|
}
|
|
33366
33366
|
});
|
|
33367
33367
|
|
|
33368
|
+
// src/mcp/utils/semver.ts
|
|
33369
|
+
function parseSemver(version2) {
|
|
33370
|
+
const match2 = version2.match(/^(\d+)\.(\d+)\.(\d+)$/);
|
|
33371
|
+
if (!match2) {
|
|
33372
|
+
return null;
|
|
33373
|
+
}
|
|
33374
|
+
return [
|
|
33375
|
+
parseInt(match2[1], 10),
|
|
33376
|
+
parseInt(match2[2], 10),
|
|
33377
|
+
parseInt(match2[3], 10)
|
|
33378
|
+
];
|
|
33379
|
+
}
|
|
33380
|
+
function compareVersions(a, b) {
|
|
33381
|
+
const partsA = parseSemver(a);
|
|
33382
|
+
const partsB = parseSemver(b);
|
|
33383
|
+
if (!partsA && !partsB) return 0;
|
|
33384
|
+
if (!partsA) return 1;
|
|
33385
|
+
if (!partsB) return -1;
|
|
33386
|
+
for (let i = 0; i < 3; i++) {
|
|
33387
|
+
if (partsA[i] > partsB[i]) return 1;
|
|
33388
|
+
if (partsA[i] < partsB[i]) return -1;
|
|
33389
|
+
}
|
|
33390
|
+
return 0;
|
|
33391
|
+
}
|
|
33392
|
+
var init_semver = __esm({
|
|
33393
|
+
"src/mcp/utils/semver.ts"() {
|
|
33394
|
+
"use strict";
|
|
33395
|
+
init_esm_shims();
|
|
33396
|
+
}
|
|
33397
|
+
});
|
|
33398
|
+
|
|
33368
33399
|
// src/mcp/tools/check-migrations.ts
|
|
33369
33400
|
import path9 from "path";
|
|
33370
33401
|
async function handleCheckMigrations(args, config2) {
|
|
@@ -33441,29 +33472,6 @@ async function parseMigrations(migrationsPath, rootPath) {
|
|
|
33441
33472
|
return a.sequence.localeCompare(b.sequence);
|
|
33442
33473
|
});
|
|
33443
33474
|
}
|
|
33444
|
-
function parseSemver(version2) {
|
|
33445
|
-
const match2 = version2.match(/^(\d+)\.(\d+)\.(\d+)$/);
|
|
33446
|
-
if (!match2) {
|
|
33447
|
-
return null;
|
|
33448
|
-
}
|
|
33449
|
-
return [
|
|
33450
|
-
parseInt(match2[1], 10),
|
|
33451
|
-
parseInt(match2[2], 10),
|
|
33452
|
-
parseInt(match2[3], 10)
|
|
33453
|
-
];
|
|
33454
|
-
}
|
|
33455
|
-
function compareVersions(a, b) {
|
|
33456
|
-
const partsA = parseSemver(a);
|
|
33457
|
-
const partsB = parseSemver(b);
|
|
33458
|
-
if (!partsA && !partsB) return 0;
|
|
33459
|
-
if (!partsA) return 1;
|
|
33460
|
-
if (!partsB) return -1;
|
|
33461
|
-
for (let i = 0; i < 3; i++) {
|
|
33462
|
-
if (partsA[i] > partsB[i]) return 1;
|
|
33463
|
-
if (partsA[i] < partsB[i]) return -1;
|
|
33464
|
-
}
|
|
33465
|
-
return 0;
|
|
33466
|
-
}
|
|
33467
33475
|
function checkNamingConventions(result, _config) {
|
|
33468
33476
|
for (const migration of result.migrations) {
|
|
33469
33477
|
if (migration.context === "Unknown") {
|
|
@@ -33656,6 +33664,7 @@ var init_check_migrations = __esm({
|
|
|
33656
33664
|
init_fs();
|
|
33657
33665
|
init_git();
|
|
33658
33666
|
init_detector();
|
|
33667
|
+
init_semver();
|
|
33659
33668
|
init_logger();
|
|
33660
33669
|
checkMigrationsTool = {
|
|
33661
33670
|
name: "check_migrations",
|
|
@@ -34774,7 +34783,7 @@ var require_no_conflict = __commonJS({
|
|
|
34774
34783
|
"use strict";
|
|
34775
34784
|
init_esm_shims();
|
|
34776
34785
|
exports.__esModule = true;
|
|
34777
|
-
exports["default"] = function(
|
|
34786
|
+
exports["default"] = function(Handlebars4) {
|
|
34778
34787
|
(function() {
|
|
34779
34788
|
if (typeof globalThis === "object") return;
|
|
34780
34789
|
Object.prototype.__defineGetter__("__magic__", function() {
|
|
@@ -34784,11 +34793,11 @@ var require_no_conflict = __commonJS({
|
|
|
34784
34793
|
delete Object.prototype.__magic__;
|
|
34785
34794
|
})();
|
|
34786
34795
|
var $Handlebars = globalThis.Handlebars;
|
|
34787
|
-
|
|
34788
|
-
if (globalThis.Handlebars ===
|
|
34796
|
+
Handlebars4.noConflict = function() {
|
|
34797
|
+
if (globalThis.Handlebars === Handlebars4) {
|
|
34789
34798
|
globalThis.Handlebars = $Handlebars;
|
|
34790
34799
|
}
|
|
34791
|
-
return
|
|
34800
|
+
return Handlebars4;
|
|
34792
34801
|
};
|
|
34793
34802
|
};
|
|
34794
34803
|
module.exports = exports["default"];
|
|
@@ -34867,13 +34876,13 @@ var require_ast = __commonJS({
|
|
|
34867
34876
|
helperExpression: function helperExpression(node) {
|
|
34868
34877
|
return node.type === "SubExpression" || (node.type === "MustacheStatement" || node.type === "BlockStatement") && !!(node.params && node.params.length || node.hash);
|
|
34869
34878
|
},
|
|
34870
|
-
scopedId: function scopedId(
|
|
34871
|
-
return /^\.|this\b/.test(
|
|
34879
|
+
scopedId: function scopedId(path30) {
|
|
34880
|
+
return /^\.|this\b/.test(path30.original);
|
|
34872
34881
|
},
|
|
34873
34882
|
// an ID is simple if it only has one part, and that part is not
|
|
34874
34883
|
// `..` or `this`.
|
|
34875
|
-
simpleId: function simpleId(
|
|
34876
|
-
return
|
|
34884
|
+
simpleId: function simpleId(path30) {
|
|
34885
|
+
return path30.parts.length === 1 && !AST2.helpers.scopedId(path30) && !path30.depth;
|
|
34877
34886
|
}
|
|
34878
34887
|
}
|
|
34879
34888
|
};
|
|
@@ -35947,12 +35956,12 @@ var require_helpers2 = __commonJS({
|
|
|
35947
35956
|
loc
|
|
35948
35957
|
};
|
|
35949
35958
|
}
|
|
35950
|
-
function prepareMustache(
|
|
35959
|
+
function prepareMustache(path30, params, hash, open, strip, locInfo) {
|
|
35951
35960
|
var escapeFlag = open.charAt(3) || open.charAt(2), escaped = escapeFlag !== "{" && escapeFlag !== "&";
|
|
35952
35961
|
var decorator = /\*/.test(open);
|
|
35953
35962
|
return {
|
|
35954
35963
|
type: decorator ? "Decorator" : "MustacheStatement",
|
|
35955
|
-
path:
|
|
35964
|
+
path: path30,
|
|
35956
35965
|
params,
|
|
35957
35966
|
hash,
|
|
35958
35967
|
escaped,
|
|
@@ -36224,9 +36233,9 @@ var require_compiler = __commonJS({
|
|
|
36224
36233
|
},
|
|
36225
36234
|
DecoratorBlock: function DecoratorBlock(decorator) {
|
|
36226
36235
|
var program = decorator.program && this.compileProgram(decorator.program);
|
|
36227
|
-
var params = this.setupFullMustacheParams(decorator, program, void 0),
|
|
36236
|
+
var params = this.setupFullMustacheParams(decorator, program, void 0), path30 = decorator.path;
|
|
36228
36237
|
this.useDecorators = true;
|
|
36229
|
-
this.opcode("registerDecorator", params.length,
|
|
36238
|
+
this.opcode("registerDecorator", params.length, path30.original);
|
|
36230
36239
|
},
|
|
36231
36240
|
PartialStatement: function PartialStatement(partial2) {
|
|
36232
36241
|
this.usePartial = true;
|
|
@@ -36290,46 +36299,46 @@ var require_compiler = __commonJS({
|
|
|
36290
36299
|
}
|
|
36291
36300
|
},
|
|
36292
36301
|
ambiguousSexpr: function ambiguousSexpr(sexpr, program, inverse) {
|
|
36293
|
-
var
|
|
36294
|
-
this.opcode("getContext",
|
|
36302
|
+
var path30 = sexpr.path, name = path30.parts[0], isBlock = program != null || inverse != null;
|
|
36303
|
+
this.opcode("getContext", path30.depth);
|
|
36295
36304
|
this.opcode("pushProgram", program);
|
|
36296
36305
|
this.opcode("pushProgram", inverse);
|
|
36297
|
-
|
|
36298
|
-
this.accept(
|
|
36306
|
+
path30.strict = true;
|
|
36307
|
+
this.accept(path30);
|
|
36299
36308
|
this.opcode("invokeAmbiguous", name, isBlock);
|
|
36300
36309
|
},
|
|
36301
36310
|
simpleSexpr: function simpleSexpr(sexpr) {
|
|
36302
|
-
var
|
|
36303
|
-
|
|
36304
|
-
this.accept(
|
|
36311
|
+
var path30 = sexpr.path;
|
|
36312
|
+
path30.strict = true;
|
|
36313
|
+
this.accept(path30);
|
|
36305
36314
|
this.opcode("resolvePossibleLambda");
|
|
36306
36315
|
},
|
|
36307
36316
|
helperSexpr: function helperSexpr(sexpr, program, inverse) {
|
|
36308
|
-
var params = this.setupFullMustacheParams(sexpr, program, inverse),
|
|
36317
|
+
var params = this.setupFullMustacheParams(sexpr, program, inverse), path30 = sexpr.path, name = path30.parts[0];
|
|
36309
36318
|
if (this.options.knownHelpers[name]) {
|
|
36310
36319
|
this.opcode("invokeKnownHelper", params.length, name);
|
|
36311
36320
|
} else if (this.options.knownHelpersOnly) {
|
|
36312
36321
|
throw new _exception2["default"]("You specified knownHelpersOnly, but used the unknown helper " + name, sexpr);
|
|
36313
36322
|
} else {
|
|
36314
|
-
|
|
36315
|
-
|
|
36316
|
-
this.accept(
|
|
36317
|
-
this.opcode("invokeHelper", params.length,
|
|
36323
|
+
path30.strict = true;
|
|
36324
|
+
path30.falsy = true;
|
|
36325
|
+
this.accept(path30);
|
|
36326
|
+
this.opcode("invokeHelper", params.length, path30.original, _ast2["default"].helpers.simpleId(path30));
|
|
36318
36327
|
}
|
|
36319
36328
|
},
|
|
36320
|
-
PathExpression: function PathExpression(
|
|
36321
|
-
this.addDepth(
|
|
36322
|
-
this.opcode("getContext",
|
|
36323
|
-
var name =
|
|
36329
|
+
PathExpression: function PathExpression(path30) {
|
|
36330
|
+
this.addDepth(path30.depth);
|
|
36331
|
+
this.opcode("getContext", path30.depth);
|
|
36332
|
+
var name = path30.parts[0], scoped = _ast2["default"].helpers.scopedId(path30), blockParamId = !path30.depth && !scoped && this.blockParamIndex(name);
|
|
36324
36333
|
if (blockParamId) {
|
|
36325
|
-
this.opcode("lookupBlockParam", blockParamId,
|
|
36334
|
+
this.opcode("lookupBlockParam", blockParamId, path30.parts);
|
|
36326
36335
|
} else if (!name) {
|
|
36327
36336
|
this.opcode("pushContext");
|
|
36328
|
-
} else if (
|
|
36337
|
+
} else if (path30.data) {
|
|
36329
36338
|
this.options.data = true;
|
|
36330
|
-
this.opcode("lookupData",
|
|
36339
|
+
this.opcode("lookupData", path30.depth, path30.parts, path30.strict);
|
|
36331
36340
|
} else {
|
|
36332
|
-
this.opcode("lookupOnContext",
|
|
36341
|
+
this.opcode("lookupOnContext", path30.parts, path30.falsy, path30.strict, scoped);
|
|
36333
36342
|
}
|
|
36334
36343
|
},
|
|
36335
36344
|
StringLiteral: function StringLiteral(string3) {
|
|
@@ -36685,16 +36694,16 @@ var require_util3 = __commonJS({
|
|
|
36685
36694
|
}
|
|
36686
36695
|
exports.urlGenerate = urlGenerate;
|
|
36687
36696
|
function normalize2(aPath) {
|
|
36688
|
-
var
|
|
36697
|
+
var path30 = aPath;
|
|
36689
36698
|
var url2 = urlParse(aPath);
|
|
36690
36699
|
if (url2) {
|
|
36691
36700
|
if (!url2.path) {
|
|
36692
36701
|
return aPath;
|
|
36693
36702
|
}
|
|
36694
|
-
|
|
36703
|
+
path30 = url2.path;
|
|
36695
36704
|
}
|
|
36696
|
-
var isAbsolute = exports.isAbsolute(
|
|
36697
|
-
var parts =
|
|
36705
|
+
var isAbsolute = exports.isAbsolute(path30);
|
|
36706
|
+
var parts = path30.split(/\/+/);
|
|
36698
36707
|
for (var part, up = 0, i = parts.length - 1; i >= 0; i--) {
|
|
36699
36708
|
part = parts[i];
|
|
36700
36709
|
if (part === ".") {
|
|
@@ -36711,15 +36720,15 @@ var require_util3 = __commonJS({
|
|
|
36711
36720
|
}
|
|
36712
36721
|
}
|
|
36713
36722
|
}
|
|
36714
|
-
|
|
36715
|
-
if (
|
|
36716
|
-
|
|
36723
|
+
path30 = parts.join("/");
|
|
36724
|
+
if (path30 === "") {
|
|
36725
|
+
path30 = isAbsolute ? "/" : ".";
|
|
36717
36726
|
}
|
|
36718
36727
|
if (url2) {
|
|
36719
|
-
url2.path =
|
|
36728
|
+
url2.path = path30;
|
|
36720
36729
|
return urlGenerate(url2);
|
|
36721
36730
|
}
|
|
36722
|
-
return
|
|
36731
|
+
return path30;
|
|
36723
36732
|
}
|
|
36724
36733
|
exports.normalize = normalize2;
|
|
36725
36734
|
function join2(aRoot, aPath) {
|
|
@@ -39520,8 +39529,8 @@ var require_printer = __commonJS({
|
|
|
39520
39529
|
return this.accept(sexpr.path) + " " + params + hash;
|
|
39521
39530
|
};
|
|
39522
39531
|
PrintVisitor.prototype.PathExpression = function(id) {
|
|
39523
|
-
var
|
|
39524
|
-
return (id.data ? "@" : "") + "PATH:" +
|
|
39532
|
+
var path30 = id.parts.join("/");
|
|
39533
|
+
return (id.data ? "@" : "") + "PATH:" + path30;
|
|
39525
39534
|
};
|
|
39526
39535
|
PrintVisitor.prototype.StringLiteral = function(string3) {
|
|
39527
39536
|
return '"' + string3.value + '"';
|
|
@@ -50738,11 +50747,11 @@ var require_mime_types = __commonJS({
|
|
|
50738
50747
|
}
|
|
50739
50748
|
return exts[0];
|
|
50740
50749
|
}
|
|
50741
|
-
function lookup(
|
|
50742
|
-
if (!
|
|
50750
|
+
function lookup(path30) {
|
|
50751
|
+
if (!path30 || typeof path30 !== "string") {
|
|
50743
50752
|
return false;
|
|
50744
50753
|
}
|
|
50745
|
-
var extension2 = extname("x." +
|
|
50754
|
+
var extension2 = extname("x." + path30).toLowerCase().substr(1);
|
|
50746
50755
|
if (!extension2) {
|
|
50747
50756
|
return false;
|
|
50748
50757
|
}
|
|
@@ -51905,7 +51914,7 @@ var require_form_data = __commonJS({
|
|
|
51905
51914
|
init_esm_shims();
|
|
51906
51915
|
var CombinedStream = require_combined_stream();
|
|
51907
51916
|
var util4 = __require("util");
|
|
51908
|
-
var
|
|
51917
|
+
var path30 = __require("path");
|
|
51909
51918
|
var http3 = __require("http");
|
|
51910
51919
|
var https2 = __require("https");
|
|
51911
51920
|
var parseUrl = __require("url").parse;
|
|
@@ -52033,11 +52042,11 @@ var require_form_data = __commonJS({
|
|
|
52033
52042
|
FormData3.prototype._getContentDisposition = function(value, options) {
|
|
52034
52043
|
var filename;
|
|
52035
52044
|
if (typeof options.filepath === "string") {
|
|
52036
|
-
filename =
|
|
52045
|
+
filename = path30.normalize(options.filepath).replace(/\\/g, "/");
|
|
52037
52046
|
} else if (options.filename || value && (value.name || value.path)) {
|
|
52038
|
-
filename =
|
|
52047
|
+
filename = path30.basename(options.filename || value && (value.name || value.path));
|
|
52039
52048
|
} else if (value && value.readable && hasOwn(value, "httpVersion")) {
|
|
52040
|
-
filename =
|
|
52049
|
+
filename = path30.basename(value.client._httpMessage.path || "");
|
|
52041
52050
|
}
|
|
52042
52051
|
if (filename) {
|
|
52043
52052
|
return 'filename="' + filename + '"';
|
|
@@ -52236,9 +52245,9 @@ function isVisitable(thing) {
|
|
|
52236
52245
|
function removeBrackets(key) {
|
|
52237
52246
|
return utils_default.endsWith(key, "[]") ? key.slice(0, -2) : key;
|
|
52238
52247
|
}
|
|
52239
|
-
function renderKey(
|
|
52240
|
-
if (!
|
|
52241
|
-
return
|
|
52248
|
+
function renderKey(path30, key, dots) {
|
|
52249
|
+
if (!path30) return key;
|
|
52250
|
+
return path30.concat(key).map(function each(token, i) {
|
|
52242
52251
|
token = removeBrackets(token);
|
|
52243
52252
|
return !dots && i ? "[" + token + "]" : token;
|
|
52244
52253
|
}).join(dots ? "." : "");
|
|
@@ -52283,9 +52292,9 @@ function toFormData(obj, formData, options) {
|
|
|
52283
52292
|
}
|
|
52284
52293
|
return value;
|
|
52285
52294
|
}
|
|
52286
|
-
function defaultVisitor(value, key,
|
|
52295
|
+
function defaultVisitor(value, key, path30) {
|
|
52287
52296
|
let arr = value;
|
|
52288
|
-
if (value && !
|
|
52297
|
+
if (value && !path30 && typeof value === "object") {
|
|
52289
52298
|
if (utils_default.endsWith(key, "{}")) {
|
|
52290
52299
|
key = metaTokens ? key : key.slice(0, -2);
|
|
52291
52300
|
value = JSON.stringify(value);
|
|
@@ -52304,7 +52313,7 @@ function toFormData(obj, formData, options) {
|
|
|
52304
52313
|
if (isVisitable(value)) {
|
|
52305
52314
|
return true;
|
|
52306
52315
|
}
|
|
52307
|
-
formData.append(renderKey(
|
|
52316
|
+
formData.append(renderKey(path30, key, dots), convertValue(value));
|
|
52308
52317
|
return false;
|
|
52309
52318
|
}
|
|
52310
52319
|
const stack = [];
|
|
@@ -52313,10 +52322,10 @@ function toFormData(obj, formData, options) {
|
|
|
52313
52322
|
convertValue,
|
|
52314
52323
|
isVisitable
|
|
52315
52324
|
});
|
|
52316
|
-
function build(value,
|
|
52325
|
+
function build(value, path30) {
|
|
52317
52326
|
if (utils_default.isUndefined(value)) return;
|
|
52318
52327
|
if (stack.indexOf(value) !== -1) {
|
|
52319
|
-
throw Error("Circular reference detected in " +
|
|
52328
|
+
throw Error("Circular reference detected in " + path30.join("."));
|
|
52320
52329
|
}
|
|
52321
52330
|
stack.push(value);
|
|
52322
52331
|
utils_default.forEach(value, function each(el, key) {
|
|
@@ -52324,11 +52333,11 @@ function toFormData(obj, formData, options) {
|
|
|
52324
52333
|
formData,
|
|
52325
52334
|
el,
|
|
52326
52335
|
utils_default.isString(key) ? key.trim() : key,
|
|
52327
|
-
|
|
52336
|
+
path30,
|
|
52328
52337
|
exposedHelpers
|
|
52329
52338
|
);
|
|
52330
52339
|
if (result === true) {
|
|
52331
|
-
build(el,
|
|
52340
|
+
build(el, path30 ? path30.concat(key) : [key]);
|
|
52332
52341
|
}
|
|
52333
52342
|
});
|
|
52334
52343
|
stack.pop();
|
|
@@ -52613,7 +52622,7 @@ var init_platform = __esm({
|
|
|
52613
52622
|
// node_modules/axios/lib/helpers/toURLEncodedForm.js
|
|
52614
52623
|
function toURLEncodedForm(data, options) {
|
|
52615
52624
|
return toFormData_default(data, new platform_default.classes.URLSearchParams(), {
|
|
52616
|
-
visitor: function(value, key,
|
|
52625
|
+
visitor: function(value, key, path30, helpers) {
|
|
52617
52626
|
if (platform_default.isNode && utils_default.isBuffer(value)) {
|
|
52618
52627
|
this.append(key, value.toString("base64"));
|
|
52619
52628
|
return false;
|
|
@@ -52652,11 +52661,11 @@ function arrayToObject(arr) {
|
|
|
52652
52661
|
return obj;
|
|
52653
52662
|
}
|
|
52654
52663
|
function formDataToJSON(formData) {
|
|
52655
|
-
function buildPath(
|
|
52656
|
-
let name =
|
|
52664
|
+
function buildPath(path30, value, target, index) {
|
|
52665
|
+
let name = path30[index++];
|
|
52657
52666
|
if (name === "__proto__") return true;
|
|
52658
52667
|
const isNumericKey = Number.isFinite(+name);
|
|
52659
|
-
const isLast = index >=
|
|
52668
|
+
const isLast = index >= path30.length;
|
|
52660
52669
|
name = !name && utils_default.isArray(target) ? target.length : name;
|
|
52661
52670
|
if (isLast) {
|
|
52662
52671
|
if (utils_default.hasOwnProp(target, name)) {
|
|
@@ -52669,7 +52678,7 @@ function formDataToJSON(formData) {
|
|
|
52669
52678
|
if (!target[name] || !utils_default.isObject(target[name])) {
|
|
52670
52679
|
target[name] = [];
|
|
52671
52680
|
}
|
|
52672
|
-
const result = buildPath(
|
|
52681
|
+
const result = buildPath(path30, value, target[name], index);
|
|
52673
52682
|
if (result && utils_default.isArray(target[name])) {
|
|
52674
52683
|
target[name] = arrayToObject(target[name]);
|
|
52675
52684
|
}
|
|
@@ -55566,9 +55575,9 @@ var init_http = __esm({
|
|
|
55566
55575
|
auth = urlUsername + ":" + urlPassword;
|
|
55567
55576
|
}
|
|
55568
55577
|
auth && headers.delete("authorization");
|
|
55569
|
-
let
|
|
55578
|
+
let path30;
|
|
55570
55579
|
try {
|
|
55571
|
-
|
|
55580
|
+
path30 = buildURL(
|
|
55572
55581
|
parsed.pathname + parsed.search,
|
|
55573
55582
|
config2.params,
|
|
55574
55583
|
config2.paramsSerializer
|
|
@@ -55586,7 +55595,7 @@ var init_http = __esm({
|
|
|
55586
55595
|
false
|
|
55587
55596
|
);
|
|
55588
55597
|
const options = {
|
|
55589
|
-
path:
|
|
55598
|
+
path: path30,
|
|
55590
55599
|
method,
|
|
55591
55600
|
headers: headers.toJSON(),
|
|
55592
55601
|
agents: { http: config2.httpAgent, https: config2.httpsAgent },
|
|
@@ -55839,14 +55848,14 @@ var init_cookies = __esm({
|
|
|
55839
55848
|
cookies_default = platform_default.hasStandardBrowserEnv ? (
|
|
55840
55849
|
// Standard browser envs support document.cookie
|
|
55841
55850
|
{
|
|
55842
|
-
write(name, value, expires,
|
|
55851
|
+
write(name, value, expires, path30, domain, secure, sameSite) {
|
|
55843
55852
|
if (typeof document === "undefined") return;
|
|
55844
55853
|
const cookie = [`${name}=${encodeURIComponent(value)}`];
|
|
55845
55854
|
if (utils_default.isNumber(expires)) {
|
|
55846
55855
|
cookie.push(`expires=${new Date(expires).toUTCString()}`);
|
|
55847
55856
|
}
|
|
55848
|
-
if (utils_default.isString(
|
|
55849
|
-
cookie.push(`path=${
|
|
55857
|
+
if (utils_default.isString(path30)) {
|
|
55858
|
+
cookie.push(`path=${path30}`);
|
|
55850
55859
|
}
|
|
55851
55860
|
if (utils_default.isString(domain)) {
|
|
55852
55861
|
cookie.push(`domain=${domain}`);
|
|
@@ -57600,8 +57609,12 @@ var init_api_docs = __esm({
|
|
|
57600
57609
|
import path12 from "path";
|
|
57601
57610
|
async function handleSuggestMigration(args, config2) {
|
|
57602
57611
|
const input = SuggestMigrationInputSchema2.parse(args);
|
|
57612
|
+
const sanitizedDescription = input.description.replace(/[^a-zA-Z0-9\s\-_]/g, "").trim();
|
|
57613
|
+
if (sanitizedDescription.length < 3) {
|
|
57614
|
+
throw new Error("Migration description must contain at least 3 alphanumeric characters after sanitization");
|
|
57615
|
+
}
|
|
57603
57616
|
const context = input.context || config2.defaultDbContext || "core";
|
|
57604
|
-
logger.info("Suggesting migration name", { description:
|
|
57617
|
+
logger.info("Suggesting migration name", { description: sanitizedDescription, context });
|
|
57605
57618
|
const structure = await findSmartStackStructure(config2.smartstack.projectPath);
|
|
57606
57619
|
const existingMigrations = await findExistingMigrations(structure, config2, context);
|
|
57607
57620
|
let version2 = input.version;
|
|
@@ -57619,7 +57632,10 @@ async function handleSuggestMigration(args, config2) {
|
|
|
57619
57632
|
} else {
|
|
57620
57633
|
version2 = version2 || "1.0.0";
|
|
57621
57634
|
}
|
|
57622
|
-
const pascalDescription = toPascalCase(
|
|
57635
|
+
const pascalDescription = toPascalCase(sanitizedDescription);
|
|
57636
|
+
if (!pascalDescription || !/^[A-Z][a-zA-Z0-9]*$/.test(pascalDescription)) {
|
|
57637
|
+
throw new Error(`Invalid migration description after PascalCase conversion: "${pascalDescription}"`);
|
|
57638
|
+
}
|
|
57623
57639
|
const sequenceStr = sequence.toString().padStart(3, "0");
|
|
57624
57640
|
const migrationName = `${context}_v${version2}_${sequenceStr}_${pascalDescription}`;
|
|
57625
57641
|
const dbContextName = context === "core" ? "CoreDbContext" : "ExtensionsDbContext";
|
|
@@ -57690,27 +57706,23 @@ async function findExistingMigrations(structure, config2, context) {
|
|
|
57690
57706
|
}
|
|
57691
57707
|
}
|
|
57692
57708
|
migrations.sort((a, b) => {
|
|
57693
|
-
const verCompare =
|
|
57709
|
+
const verCompare = compareVersions(a.version, b.version);
|
|
57694
57710
|
if (verCompare !== 0) return verCompare;
|
|
57695
57711
|
return a.sequence - b.sequence;
|
|
57696
57712
|
});
|
|
57697
|
-
} catch {
|
|
57698
|
-
|
|
57713
|
+
} catch (error2) {
|
|
57714
|
+
const err = error2 instanceof Error ? error2 : new Error(String(error2));
|
|
57715
|
+
if (err.message.includes("ENOENT") || err.message.includes("no such file")) {
|
|
57716
|
+
logger.debug("Migrations folder not found (expected for new projects)", { path: migrationsPath });
|
|
57717
|
+
} else {
|
|
57718
|
+
logger.warn("Failed to read migrations folder", { path: migrationsPath, error: err.message });
|
|
57719
|
+
}
|
|
57699
57720
|
}
|
|
57700
57721
|
return migrations;
|
|
57701
57722
|
}
|
|
57702
57723
|
function toPascalCase(str) {
|
|
57703
57724
|
return str.replace(/[^a-zA-Z0-9\s]/g, "").split(/\s+/).map((word) => word.charAt(0).toUpperCase() + word.slice(1).toLowerCase()).join("");
|
|
57704
57725
|
}
|
|
57705
|
-
function compareVersions2(a, b) {
|
|
57706
|
-
const aParts = a.split(".").map(Number);
|
|
57707
|
-
const bParts = b.split(".").map(Number);
|
|
57708
|
-
for (let i = 0; i < 3; i++) {
|
|
57709
|
-
if (aParts[i] > bParts[i]) return 1;
|
|
57710
|
-
if (aParts[i] < bParts[i]) return -1;
|
|
57711
|
-
}
|
|
57712
|
-
return 0;
|
|
57713
|
-
}
|
|
57714
57726
|
var suggestMigrationTool, SuggestMigrationInputSchema2;
|
|
57715
57727
|
var init_suggest_migration = __esm({
|
|
57716
57728
|
"src/mcp/tools/suggest-migration.ts"() {
|
|
@@ -57719,6 +57731,7 @@ var init_suggest_migration = __esm({
|
|
|
57719
57731
|
init_zod();
|
|
57720
57732
|
init_detector();
|
|
57721
57733
|
init_fs();
|
|
57734
|
+
init_semver();
|
|
57722
57735
|
init_logger();
|
|
57723
57736
|
suggestMigrationTool = {
|
|
57724
57737
|
name: "suggest_migration",
|
|
@@ -57744,9 +57757,9 @@ var init_suggest_migration = __esm({
|
|
|
57744
57757
|
}
|
|
57745
57758
|
};
|
|
57746
57759
|
SuggestMigrationInputSchema2 = external_exports.object({
|
|
57747
|
-
description: external_exports.string().describe("Description of what the migration does"),
|
|
57760
|
+
description: external_exports.string().min(3, "Migration description must be at least 3 characters").max(100, "Migration description must be at most 100 characters").describe("Description of what the migration does"),
|
|
57748
57761
|
context: external_exports.enum(["core", "extensions"]).optional().describe("DbContext name (default: auto-detected from project config)"),
|
|
57749
|
-
version: external_exports.string().optional().describe('Semver version (e.g., "1.0.0")')
|
|
57762
|
+
version: external_exports.string().regex(/^\d+\.\d+\.\d+$/, 'Version must be semver format (e.g., "1.0.0")').optional().describe('Semver version (e.g., "1.0.0")')
|
|
57750
57763
|
});
|
|
57751
57764
|
}
|
|
57752
57765
|
});
|
|
@@ -58146,6 +58159,9 @@ async function handleScaffoldTests(args, config2) {
|
|
|
58146
58159
|
case "repository":
|
|
58147
58160
|
await scaffoldRepositoryTests(input.name, options, testTypes, structure, config2, result, dryRun);
|
|
58148
58161
|
break;
|
|
58162
|
+
case "infrastructure":
|
|
58163
|
+
await scaffoldInfrastructureTests(input.name, options, structure, config2, result, dryRun);
|
|
58164
|
+
break;
|
|
58149
58165
|
case "all":
|
|
58150
58166
|
await scaffoldEntityTests(input.name, options, testTypes, structure, config2, result, dryRun);
|
|
58151
58167
|
await scaffoldServiceTests(input.name, options, testTypes, structure, config2, result, dryRun);
|
|
@@ -58183,25 +58199,7 @@ async function scaffoldEntityTests(name, options, testTypes, structure, config2,
|
|
|
58183
58199
|
type: "created"
|
|
58184
58200
|
});
|
|
58185
58201
|
}
|
|
58186
|
-
|
|
58187
|
-
const securityContent = import_handlebars2.default.compile(securityTestTemplate)({
|
|
58188
|
-
...context,
|
|
58189
|
-
nameLower: name.charAt(0).toLowerCase() + name.slice(1),
|
|
58190
|
-
apiNamespace: config2.conventions.namespaces.api
|
|
58191
|
-
});
|
|
58192
|
-
const securityPath = path14.join(structure.root, "Tests", "Security", `${name}SecurityTests.cs`);
|
|
58193
|
-
validatePathSecurity(securityPath, structure.root);
|
|
58194
|
-
if (!dryRun) {
|
|
58195
|
-
await ensureDirectory(path14.dirname(securityPath));
|
|
58196
|
-
await writeText(securityPath, securityContent);
|
|
58197
|
-
}
|
|
58198
|
-
result.files.push({
|
|
58199
|
-
path: path14.relative(structure.root, securityPath),
|
|
58200
|
-
content: securityContent,
|
|
58201
|
-
type: "created"
|
|
58202
|
-
});
|
|
58203
|
-
}
|
|
58204
|
-
result.instructions.push(`Add package reference: <PackageReference Include="FluentAssertions" Version="6.*" />`);
|
|
58202
|
+
result.instructions.push(`Add package reference: <PackageReference Include="FluentAssertions" Version="8.*" />`);
|
|
58205
58203
|
result.instructions.push(`Add package reference: <PackageReference Include="xunit" Version="2.*" />`);
|
|
58206
58204
|
}
|
|
58207
58205
|
async function scaffoldServiceTests(name, options, testTypes, structure, config2, result, dryRun) {
|
|
@@ -58258,6 +58256,18 @@ async function scaffoldControllerTests(name, options, testTypes, structure, conf
|
|
|
58258
58256
|
content,
|
|
58259
58257
|
type: "created"
|
|
58260
58258
|
});
|
|
58259
|
+
const realContent = import_handlebars2.default.compile(controllerIntegrationTestTemplate)(context);
|
|
58260
|
+
const realTestPath = path14.join(structure.root, "Tests", "Integration", "Controllers", `${name}ControllerIntegrationTests.cs`);
|
|
58261
|
+
validatePathSecurity(realTestPath, structure.root);
|
|
58262
|
+
if (!dryRun) {
|
|
58263
|
+
await ensureDirectory(path14.dirname(realTestPath));
|
|
58264
|
+
await writeText(realTestPath, realContent);
|
|
58265
|
+
}
|
|
58266
|
+
result.files.push({
|
|
58267
|
+
path: path14.relative(structure.root, realTestPath),
|
|
58268
|
+
content: realContent,
|
|
58269
|
+
type: "created"
|
|
58270
|
+
});
|
|
58261
58271
|
}
|
|
58262
58272
|
if (testTypes.includes("security")) {
|
|
58263
58273
|
const securityContent = import_handlebars2.default.compile(securityTestTemplate)(context);
|
|
@@ -58273,7 +58283,8 @@ async function scaffoldControllerTests(name, options, testTypes, structure, conf
|
|
|
58273
58283
|
type: "created"
|
|
58274
58284
|
});
|
|
58275
58285
|
}
|
|
58276
|
-
result.instructions.push(`Add package reference: <PackageReference Include="Microsoft.AspNetCore.Mvc.Testing" Version="
|
|
58286
|
+
result.instructions.push(`Add package reference: <PackageReference Include="Microsoft.AspNetCore.Mvc.Testing" Version="9.*" />`);
|
|
58287
|
+
result.instructions.push(`Add package reference: <PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="9.*" />`);
|
|
58277
58288
|
}
|
|
58278
58289
|
async function scaffoldValidatorTests(name, options, testTypes, structure, config2, result, dryRun) {
|
|
58279
58290
|
const testNamespace = `${config2.conventions.namespaces.application}.Tests`;
|
|
@@ -58327,6 +58338,41 @@ async function scaffoldRepositoryTests(name, options, testTypes, structure, conf
|
|
|
58327
58338
|
}
|
|
58328
58339
|
result.instructions.push(`Add package reference: <PackageReference Include="Microsoft.EntityFrameworkCore.InMemory" Version="8.*" />`);
|
|
58329
58340
|
}
|
|
58341
|
+
async function scaffoldInfrastructureTests(name, options, structure, config2, result, dryRun) {
|
|
58342
|
+
const testNamespace = `${config2.conventions.namespaces.application}.Tests`;
|
|
58343
|
+
const infrastructureNamespace = config2.conventions.namespaces.infrastructure;
|
|
58344
|
+
const context = {
|
|
58345
|
+
name,
|
|
58346
|
+
testNamespace,
|
|
58347
|
+
infrastructureNamespace,
|
|
58348
|
+
...options
|
|
58349
|
+
};
|
|
58350
|
+
const templates = [
|
|
58351
|
+
{ template: testFactoryTemplate, filename: "SmartStackTestFactory.cs" },
|
|
58352
|
+
{ template: testAuthHandlerTemplate, filename: "TestAuthHandler.cs" },
|
|
58353
|
+
{ template: integrationTestBaseTemplate, filename: "IntegrationTestBase.cs" },
|
|
58354
|
+
{ template: testDataSeederTemplate, filename: "TestDataSeeder.cs" }
|
|
58355
|
+
];
|
|
58356
|
+
for (const { template, filename } of templates) {
|
|
58357
|
+
const content = import_handlebars2.default.compile(template)(context);
|
|
58358
|
+
const testPath = path14.join(structure.root, "Tests", "Integration", filename);
|
|
58359
|
+
validatePathSecurity(testPath, structure.root);
|
|
58360
|
+
if (!dryRun) {
|
|
58361
|
+
await ensureDirectory(path14.dirname(testPath));
|
|
58362
|
+
await writeText(testPath, content);
|
|
58363
|
+
}
|
|
58364
|
+
result.files.push({
|
|
58365
|
+
path: path14.relative(structure.root, testPath),
|
|
58366
|
+
content,
|
|
58367
|
+
type: "created"
|
|
58368
|
+
});
|
|
58369
|
+
}
|
|
58370
|
+
result.instructions.push(`Add package reference: <PackageReference Include="Microsoft.AspNetCore.Mvc.Testing" Version="9.*" />`);
|
|
58371
|
+
result.instructions.push(`Add package reference: <PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="9.*" />`);
|
|
58372
|
+
result.instructions.push(`Add package reference: <PackageReference Include="Microsoft.Data.Sqlite" Version="9.*" />`);
|
|
58373
|
+
result.instructions.push(`Add package reference: <PackageReference Include="FluentAssertions" Version="8.*" />`);
|
|
58374
|
+
result.instructions.push(`IMPORTANT: Add to your API .csproj: <InternalsVisibleTo Include="$(AssemblyName).Tests" /> or add 'public partial class Program { }' to Program.cs`);
|
|
58375
|
+
}
|
|
58330
58376
|
function formatTestResult(result, _target, name, dryRun) {
|
|
58331
58377
|
const lines = [];
|
|
58332
58378
|
lines.push(`# Scaffold Tests: ${name}`);
|
|
@@ -58368,7 +58414,7 @@ function formatTestResult(result, _target, name, dryRun) {
|
|
|
58368
58414
|
lines.push("");
|
|
58369
58415
|
return lines.join("\n");
|
|
58370
58416
|
}
|
|
58371
|
-
var import_handlebars2, scaffoldTestsTool, entityTestTemplate, serviceTestTemplate, controllerTestTemplate, validatorTestTemplate, repositoryTestTemplate, securityTestTemplate;
|
|
58417
|
+
var import_handlebars2, scaffoldTestsTool, entityTestTemplate, serviceTestTemplate, controllerTestTemplate, validatorTestTemplate, repositoryTestTemplate, securityTestTemplate, testFactoryTemplate, testAuthHandlerTemplate, integrationTestBaseTemplate, testDataSeederTemplate, controllerIntegrationTestTemplate;
|
|
58372
58418
|
var init_scaffold_tests = __esm({
|
|
58373
58419
|
"src/mcp/tools/scaffold-tests.ts"() {
|
|
58374
58420
|
"use strict";
|
|
@@ -58386,8 +58432,8 @@ var init_scaffold_tests = __esm({
|
|
|
58386
58432
|
properties: {
|
|
58387
58433
|
target: {
|
|
58388
58434
|
type: "string",
|
|
58389
|
-
enum: ["entity", "service", "controller", "validator", "repository", "all"],
|
|
58390
|
-
description:
|
|
58435
|
+
enum: ["entity", "service", "controller", "validator", "repository", "infrastructure", "all"],
|
|
58436
|
+
description: 'Type of component to test. Use "infrastructure" to generate shared test base classes (WebApplicationFactory, IntegrationTestBase, TestAuthHandler).'
|
|
58391
58437
|
},
|
|
58392
58438
|
name: {
|
|
58393
58439
|
type: "string",
|
|
@@ -59744,6 +59790,547 @@ public class {{name}}SecurityTests : IClassFixture<WebApplicationFactory<Program
|
|
|
59744
59790
|
|
|
59745
59791
|
#endregion
|
|
59746
59792
|
}
|
|
59793
|
+
`;
|
|
59794
|
+
testFactoryTemplate = `using System;
|
|
59795
|
+
using System.Data.Common;
|
|
59796
|
+
using System.Linq;
|
|
59797
|
+
using Microsoft.AspNetCore.Authentication;
|
|
59798
|
+
using Microsoft.AspNetCore.Hosting;
|
|
59799
|
+
using Microsoft.AspNetCore.Mvc.Testing;
|
|
59800
|
+
using Microsoft.Data.Sqlite;
|
|
59801
|
+
using Microsoft.EntityFrameworkCore;
|
|
59802
|
+
using Microsoft.Extensions.DependencyInjection;
|
|
59803
|
+
using {{infrastructureNamespace}}.Persistence;
|
|
59804
|
+
|
|
59805
|
+
namespace {{testNamespace}}.Integration;
|
|
59806
|
+
|
|
59807
|
+
/// <summary>
|
|
59808
|
+
/// Shared WebApplicationFactory for integration tests.
|
|
59809
|
+
/// Uses SQLite in-memory for real SQL behavior.
|
|
59810
|
+
/// Replaces JWT auth with TestAuthHandler.
|
|
59811
|
+
/// </summary>
|
|
59812
|
+
public class SmartStackTestFactory : WebApplicationFactory<Program>, IAsyncLifetime
|
|
59813
|
+
{
|
|
59814
|
+
private DbConnection _connection = null!;
|
|
59815
|
+
|
|
59816
|
+
protected override void ConfigureWebHost(IWebHostBuilder builder)
|
|
59817
|
+
{
|
|
59818
|
+
builder.ConfigureServices(services =>
|
|
59819
|
+
{
|
|
59820
|
+
// Remove existing DbContext registration
|
|
59821
|
+
var dbDescriptor = services.SingleOrDefault(
|
|
59822
|
+
d => d.ServiceType == typeof(DbContextOptions<ApplicationDbContext>));
|
|
59823
|
+
if (dbDescriptor != null)
|
|
59824
|
+
services.Remove(dbDescriptor);
|
|
59825
|
+
|
|
59826
|
+
// Remove existing DbConnection if registered
|
|
59827
|
+
var connDescriptor = services.SingleOrDefault(
|
|
59828
|
+
d => d.ServiceType == typeof(DbConnection));
|
|
59829
|
+
if (connDescriptor != null)
|
|
59830
|
+
services.Remove(connDescriptor);
|
|
59831
|
+
|
|
59832
|
+
// Create and open a shared SQLite in-memory connection
|
|
59833
|
+
_connection = new SqliteConnection("DataSource=:memory:");
|
|
59834
|
+
_connection.Open();
|
|
59835
|
+
|
|
59836
|
+
services.AddSingleton(_connection);
|
|
59837
|
+
services.AddDbContext<ApplicationDbContext>((sp, options) =>
|
|
59838
|
+
{
|
|
59839
|
+
options.UseSqlite(sp.GetRequiredService<DbConnection>());
|
|
59840
|
+
});
|
|
59841
|
+
|
|
59842
|
+
// Replace authentication with test handler
|
|
59843
|
+
services.AddAuthentication("Test")
|
|
59844
|
+
.AddScheme<AuthenticationSchemeOptions, TestAuthHandler>("Test", null);
|
|
59845
|
+
});
|
|
59846
|
+
|
|
59847
|
+
builder.UseEnvironment("Testing");
|
|
59848
|
+
}
|
|
59849
|
+
|
|
59850
|
+
public async Task InitializeAsync()
|
|
59851
|
+
{
|
|
59852
|
+
// Ensure database is created with current schema
|
|
59853
|
+
using var scope = Services.CreateScope();
|
|
59854
|
+
var db = scope.ServiceProvider.GetRequiredService<ApplicationDbContext>();
|
|
59855
|
+
await db.Database.EnsureCreatedAsync();
|
|
59856
|
+
|
|
59857
|
+
// Seed minimal test data (tenant, admin user, base permissions)
|
|
59858
|
+
var seeder = new TestDataSeeder(db);
|
|
59859
|
+
await seeder.SeedAsync();
|
|
59860
|
+
}
|
|
59861
|
+
|
|
59862
|
+
async Task IAsyncLifetime.DisposeAsync()
|
|
59863
|
+
{
|
|
59864
|
+
if (_connection is not null)
|
|
59865
|
+
{
|
|
59866
|
+
await _connection.CloseAsync();
|
|
59867
|
+
await _connection.DisposeAsync();
|
|
59868
|
+
}
|
|
59869
|
+
}
|
|
59870
|
+
|
|
59871
|
+
/// <summary>
|
|
59872
|
+
/// Resets the database between test classes for isolation.
|
|
59873
|
+
/// </summary>
|
|
59874
|
+
public async Task ResetDatabaseAsync()
|
|
59875
|
+
{
|
|
59876
|
+
using var scope = Services.CreateScope();
|
|
59877
|
+
var db = scope.ServiceProvider.GetRequiredService<ApplicationDbContext>();
|
|
59878
|
+
await db.Database.EnsureDeletedAsync();
|
|
59879
|
+
await db.Database.EnsureCreatedAsync();
|
|
59880
|
+
|
|
59881
|
+
var seeder = new TestDataSeeder(db);
|
|
59882
|
+
await seeder.SeedAsync();
|
|
59883
|
+
}
|
|
59884
|
+
}
|
|
59885
|
+
`;
|
|
59886
|
+
testAuthHandlerTemplate = `using System.Security.Claims;
|
|
59887
|
+
using System.Text.Encodings.Web;
|
|
59888
|
+
using Microsoft.AspNetCore.Authentication;
|
|
59889
|
+
using Microsoft.Extensions.Logging;
|
|
59890
|
+
using Microsoft.Extensions.Options;
|
|
59891
|
+
|
|
59892
|
+
namespace {{testNamespace}}.Integration;
|
|
59893
|
+
|
|
59894
|
+
/// <summary>
|
|
59895
|
+
/// Test authentication handler that creates a fake authenticated user.
|
|
59896
|
+
/// Bypasses JWT validation for integration tests.
|
|
59897
|
+
/// </summary>
|
|
59898
|
+
public class TestAuthHandler : AuthenticationHandler<AuthenticationSchemeOptions>
|
|
59899
|
+
{
|
|
59900
|
+
public static Guid DefaultTenantId { get; set; } = Guid.Parse("11111111-1111-1111-1111-111111111111");
|
|
59901
|
+
public static string DefaultUserId { get; set; } = "test-user-id";
|
|
59902
|
+
public static string DefaultUserName { get; set; } = "test-admin@smartstack.io";
|
|
59903
|
+
public static string DefaultRole { get; set; } = "Administrator";
|
|
59904
|
+
|
|
59905
|
+
public TestAuthHandler(
|
|
59906
|
+
IOptionsMonitor<AuthenticationSchemeOptions> options,
|
|
59907
|
+
ILoggerFactory logger,
|
|
59908
|
+
UrlEncoder encoder)
|
|
59909
|
+
: base(options, logger, encoder)
|
|
59910
|
+
{
|
|
59911
|
+
}
|
|
59912
|
+
|
|
59913
|
+
protected override Task<AuthenticateResult> HandleAuthenticateAsync()
|
|
59914
|
+
{
|
|
59915
|
+
// Check if the request explicitly opts out of auth (for 401 tests)
|
|
59916
|
+
if (Request.Headers.ContainsKey("X-Test-Anonymous"))
|
|
59917
|
+
{
|
|
59918
|
+
return Task.FromResult(AuthenticateResult.Fail("Anonymous request"));
|
|
59919
|
+
}
|
|
59920
|
+
|
|
59921
|
+
var tenantId = DefaultTenantId.ToString();
|
|
59922
|
+
if (Request.Headers.TryGetValue("X-Tenant-Id", out var tenantHeader))
|
|
59923
|
+
{
|
|
59924
|
+
tenantId = tenantHeader.ToString();
|
|
59925
|
+
}
|
|
59926
|
+
|
|
59927
|
+
var claims = new[]
|
|
59928
|
+
{
|
|
59929
|
+
new Claim(ClaimTypes.NameIdentifier, DefaultUserId),
|
|
59930
|
+
new Claim(ClaimTypes.Name, DefaultUserName),
|
|
59931
|
+
new Claim(ClaimTypes.Email, DefaultUserName),
|
|
59932
|
+
new Claim(ClaimTypes.Role, DefaultRole),
|
|
59933
|
+
new Claim("tenant_id", tenantId),
|
|
59934
|
+
// Add all permissions for test user (admin has full access)
|
|
59935
|
+
new Claim("permissions", "*"),
|
|
59936
|
+
};
|
|
59937
|
+
|
|
59938
|
+
var identity = new ClaimsIdentity(claims, "Test");
|
|
59939
|
+
var principal = new ClaimsPrincipal(identity);
|
|
59940
|
+
var ticket = new AuthenticationTicket(principal, "Test");
|
|
59941
|
+
|
|
59942
|
+
return Task.FromResult(AuthenticateResult.Success(ticket));
|
|
59943
|
+
}
|
|
59944
|
+
}
|
|
59945
|
+
`;
|
|
59946
|
+
integrationTestBaseTemplate = `using System;
|
|
59947
|
+
using System.Net.Http;
|
|
59948
|
+
using System.Net.Http.Headers;
|
|
59949
|
+
using System.Net.Http.Json;
|
|
59950
|
+
using System.Threading.Tasks;
|
|
59951
|
+
using FluentAssertions;
|
|
59952
|
+
using Microsoft.Extensions.DependencyInjection;
|
|
59953
|
+
using {{infrastructureNamespace}}.Persistence;
|
|
59954
|
+
using Xunit;
|
|
59955
|
+
|
|
59956
|
+
namespace {{testNamespace}}.Integration;
|
|
59957
|
+
|
|
59958
|
+
/// <summary>
|
|
59959
|
+
/// Base class for integration tests.
|
|
59960
|
+
/// Provides configured HttpClient with auth and tenant headers.
|
|
59961
|
+
/// </summary>
|
|
59962
|
+
public abstract class IntegrationTestBase : IClassFixture<SmartStackTestFactory>, IAsyncLifetime
|
|
59963
|
+
{
|
|
59964
|
+
protected readonly SmartStackTestFactory Factory;
|
|
59965
|
+
protected readonly HttpClient Client;
|
|
59966
|
+
protected readonly Guid TestTenantId = TestAuthHandler.DefaultTenantId;
|
|
59967
|
+
|
|
59968
|
+
protected IntegrationTestBase(SmartStackTestFactory factory)
|
|
59969
|
+
{
|
|
59970
|
+
Factory = factory;
|
|
59971
|
+
Client = factory.CreateClient();
|
|
59972
|
+
Client.DefaultRequestHeaders.Add("X-Tenant-Id", TestTenantId.ToString());
|
|
59973
|
+
}
|
|
59974
|
+
|
|
59975
|
+
public virtual async Task InitializeAsync()
|
|
59976
|
+
{
|
|
59977
|
+
// Reset database for each test class to ensure isolation
|
|
59978
|
+
await Factory.ResetDatabaseAsync();
|
|
59979
|
+
}
|
|
59980
|
+
|
|
59981
|
+
public virtual Task DisposeAsync() => Task.CompletedTask;
|
|
59982
|
+
|
|
59983
|
+
/// <summary>
|
|
59984
|
+
/// Creates an HttpClient without authentication (for 401 tests).
|
|
59985
|
+
/// </summary>
|
|
59986
|
+
protected HttpClient CreateAnonymousClient()
|
|
59987
|
+
{
|
|
59988
|
+
var client = Factory.CreateClient();
|
|
59989
|
+
client.DefaultRequestHeaders.Add("X-Test-Anonymous", "true");
|
|
59990
|
+
client.DefaultRequestHeaders.Add("X-Tenant-Id", TestTenantId.ToString());
|
|
59991
|
+
return client;
|
|
59992
|
+
}
|
|
59993
|
+
|
|
59994
|
+
/// <summary>
|
|
59995
|
+
/// Creates an HttpClient for a different tenant (for isolation tests).
|
|
59996
|
+
/// </summary>
|
|
59997
|
+
protected HttpClient CreateClientForTenant(Guid tenantId)
|
|
59998
|
+
{
|
|
59999
|
+
var client = Factory.CreateClient();
|
|
60000
|
+
client.DefaultRequestHeaders.Add("X-Tenant-Id", tenantId.ToString());
|
|
60001
|
+
return client;
|
|
60002
|
+
}
|
|
60003
|
+
|
|
60004
|
+
/// <summary>
|
|
60005
|
+
/// Gets direct access to the DbContext for verification queries.
|
|
60006
|
+
/// </summary>
|
|
60007
|
+
protected async Task<T> WithDbContextAsync<T>(Func<ApplicationDbContext, Task<T>> action)
|
|
60008
|
+
{
|
|
60009
|
+
using var scope = Factory.Services.CreateScope();
|
|
60010
|
+
var db = scope.ServiceProvider.GetRequiredService<ApplicationDbContext>();
|
|
60011
|
+
return await action(db);
|
|
60012
|
+
}
|
|
60013
|
+
}
|
|
60014
|
+
`;
|
|
60015
|
+
testDataSeederTemplate = `using System;
|
|
60016
|
+
using System.Threading.Tasks;
|
|
60017
|
+
using {{infrastructureNamespace}}.Persistence;
|
|
60018
|
+
|
|
60019
|
+
namespace {{testNamespace}}.Integration;
|
|
60020
|
+
|
|
60021
|
+
/// <summary>
|
|
60022
|
+
/// Seeds minimal test data required for integration tests.
|
|
60023
|
+
/// Creates a test tenant, admin user, and base navigation/permissions.
|
|
60024
|
+
/// </summary>
|
|
60025
|
+
public class TestDataSeeder
|
|
60026
|
+
{
|
|
60027
|
+
private readonly ApplicationDbContext _db;
|
|
60028
|
+
|
|
60029
|
+
public static readonly Guid TestTenantId = Guid.Parse("11111111-1111-1111-1111-111111111111");
|
|
60030
|
+
public static readonly Guid TestUserId = Guid.Parse("22222222-2222-2222-2222-222222222222");
|
|
60031
|
+
|
|
60032
|
+
public TestDataSeeder(ApplicationDbContext db)
|
|
60033
|
+
{
|
|
60034
|
+
_db = db;
|
|
60035
|
+
}
|
|
60036
|
+
|
|
60037
|
+
public async Task SeedAsync()
|
|
60038
|
+
{
|
|
60039
|
+
// NOTE: Add your tenant and user seeding logic here.
|
|
60040
|
+
// This depends on your specific entity model.
|
|
60041
|
+
// Example:
|
|
60042
|
+
// var tenant = Tenant.Create("test-tenant", "Test Tenant");
|
|
60043
|
+
// _db.Set<Tenant>().Add(tenant);
|
|
60044
|
+
// await _db.SaveChangesAsync();
|
|
60045
|
+
|
|
60046
|
+
await Task.CompletedTask;
|
|
60047
|
+
}
|
|
60048
|
+
}
|
|
60049
|
+
`;
|
|
60050
|
+
controllerIntegrationTestTemplate = `using System;
|
|
60051
|
+
using System.Collections.Generic;
|
|
60052
|
+
using System.Net;
|
|
60053
|
+
using System.Net.Http;
|
|
60054
|
+
using System.Net.Http.Json;
|
|
60055
|
+
using System.Threading.Tasks;
|
|
60056
|
+
using FluentAssertions;
|
|
60057
|
+
using Xunit;
|
|
60058
|
+
using {{testNamespace}}.Integration;
|
|
60059
|
+
|
|
60060
|
+
namespace {{testNamespace}}.Integration.Controllers;
|
|
60061
|
+
|
|
60062
|
+
/// <summary>
|
|
60063
|
+
/// REAL integration tests for {{name}}Controller.
|
|
60064
|
+
/// Uses actual DI pipeline, real services, SQLite database.
|
|
60065
|
+
/// No mocks - verifies the complete request/response cycle.
|
|
60066
|
+
/// </summary>
|
|
60067
|
+
public class {{name}}ControllerIntegrationTests : IntegrationTestBase
|
|
60068
|
+
{
|
|
60069
|
+
{{#unless isSystemEntity}}
|
|
60070
|
+
private readonly Guid _tenantId;
|
|
60071
|
+
{{/unless}}
|
|
60072
|
+
|
|
60073
|
+
public {{name}}ControllerIntegrationTests(SmartStackTestFactory factory)
|
|
60074
|
+
: base(factory)
|
|
60075
|
+
{
|
|
60076
|
+
{{#unless isSystemEntity}}
|
|
60077
|
+
_tenantId = TestTenantId;
|
|
60078
|
+
{{/unless}}
|
|
60079
|
+
}
|
|
60080
|
+
|
|
60081
|
+
#region GET Tests
|
|
60082
|
+
|
|
60083
|
+
[Fact]
|
|
60084
|
+
public async Task GetAll_WhenAuthenticated_ShouldReturn200()
|
|
60085
|
+
{
|
|
60086
|
+
// Act
|
|
60087
|
+
var response = await Client.GetAsync("/api/{{nameLower}}");
|
|
60088
|
+
|
|
60089
|
+
// Assert
|
|
60090
|
+
response.StatusCode.Should().Be(HttpStatusCode.OK);
|
|
60091
|
+
}
|
|
60092
|
+
|
|
60093
|
+
[Fact]
|
|
60094
|
+
public async Task GetById_WhenEntityExists_ShouldReturn200WithData()
|
|
60095
|
+
{
|
|
60096
|
+
// Arrange - Create an entity first
|
|
60097
|
+
var createRequest = new { Code = "get-test-01", Name = "Get Test Entity" };
|
|
60098
|
+
var createResponse = await Client.PostAsJsonAsync("/api/{{nameLower}}", createRequest);
|
|
60099
|
+
createResponse.StatusCode.Should().BeOneOf(HttpStatusCode.Created, HttpStatusCode.OK);
|
|
60100
|
+
|
|
60101
|
+
var created = await createResponse.Content.ReadFromJsonAsync<Dictionary<string, object>>();
|
|
60102
|
+
created.Should().NotBeNull();
|
|
60103
|
+
var id = created!["id"]?.ToString();
|
|
60104
|
+
id.Should().NotBeNullOrEmpty();
|
|
60105
|
+
|
|
60106
|
+
// Act
|
|
60107
|
+
var response = await Client.GetAsync($"/api/{{nameLower}}/{id}");
|
|
60108
|
+
|
|
60109
|
+
// Assert
|
|
60110
|
+
response.StatusCode.Should().Be(HttpStatusCode.OK);
|
|
60111
|
+
var entity = await response.Content.ReadFromJsonAsync<Dictionary<string, object>>();
|
|
60112
|
+
entity.Should().NotBeNull();
|
|
60113
|
+
}
|
|
60114
|
+
|
|
60115
|
+
[Fact]
|
|
60116
|
+
public async Task GetById_WhenNotExists_ShouldReturn404()
|
|
60117
|
+
{
|
|
60118
|
+
// Arrange
|
|
60119
|
+
var nonExistentId = Guid.NewGuid();
|
|
60120
|
+
|
|
60121
|
+
// Act
|
|
60122
|
+
var response = await Client.GetAsync($"/api/{{nameLower}}/{nonExistentId}");
|
|
60123
|
+
|
|
60124
|
+
// Assert
|
|
60125
|
+
response.StatusCode.Should().Be(HttpStatusCode.NotFound);
|
|
60126
|
+
}
|
|
60127
|
+
|
|
60128
|
+
#endregion
|
|
60129
|
+
|
|
60130
|
+
#region POST Tests
|
|
60131
|
+
|
|
60132
|
+
[Fact]
|
|
60133
|
+
public async Task Create_WhenValidData_ShouldPersistAndReturn201()
|
|
60134
|
+
{
|
|
60135
|
+
// Arrange
|
|
60136
|
+
var request = new { Code = "create-test-01", Name = "Created Entity" };
|
|
60137
|
+
|
|
60138
|
+
// Act
|
|
60139
|
+
var response = await Client.PostAsJsonAsync("/api/{{nameLower}}", request);
|
|
60140
|
+
|
|
60141
|
+
// Assert
|
|
60142
|
+
response.StatusCode.Should().BeOneOf(HttpStatusCode.Created, HttpStatusCode.OK);
|
|
60143
|
+
var created = await response.Content.ReadFromJsonAsync<Dictionary<string, object>>();
|
|
60144
|
+
created.Should().NotBeNull();
|
|
60145
|
+
var id = created!["id"]?.ToString();
|
|
60146
|
+
id.Should().NotBeNullOrEmpty();
|
|
60147
|
+
|
|
60148
|
+
// Verify REAL persistence - read back from DB
|
|
60149
|
+
var getResponse = await Client.GetAsync($"/api/{{nameLower}}/{id}");
|
|
60150
|
+
getResponse.StatusCode.Should().Be(HttpStatusCode.OK);
|
|
60151
|
+
var persisted = await getResponse.Content.ReadFromJsonAsync<Dictionary<string, object>>();
|
|
60152
|
+
persisted.Should().NotBeNull();
|
|
60153
|
+
}
|
|
60154
|
+
|
|
60155
|
+
{{#if includeValidation}}
|
|
60156
|
+
[Fact]
|
|
60157
|
+
public async Task Create_WhenInvalidData_ShouldReturn400()
|
|
60158
|
+
{
|
|
60159
|
+
// Arrange
|
|
60160
|
+
var request = new { Code = (string?)null, Name = (string?)null };
|
|
60161
|
+
|
|
60162
|
+
// Act
|
|
60163
|
+
var response = await Client.PostAsJsonAsync("/api/{{nameLower}}", request);
|
|
60164
|
+
|
|
60165
|
+
// Assert
|
|
60166
|
+
response.StatusCode.Should().BeOneOf(HttpStatusCode.BadRequest, HttpStatusCode.UnprocessableEntity);
|
|
60167
|
+
}
|
|
60168
|
+
{{/if}}
|
|
60169
|
+
|
|
60170
|
+
[Fact]
|
|
60171
|
+
public async Task Create_WhenDuplicateCode_ShouldReturn409()
|
|
60172
|
+
{
|
|
60173
|
+
// Arrange
|
|
60174
|
+
var request = new { Code = "duplicate-test", Name = "First Entity" };
|
|
60175
|
+
var firstResponse = await Client.PostAsJsonAsync("/api/{{nameLower}}", request);
|
|
60176
|
+
firstResponse.StatusCode.Should().BeOneOf(HttpStatusCode.Created, HttpStatusCode.OK);
|
|
60177
|
+
|
|
60178
|
+
// Act - Try to create with same code
|
|
60179
|
+
var duplicateRequest = new { Code = "duplicate-test", Name = "Duplicate Entity" };
|
|
60180
|
+
var response = await Client.PostAsJsonAsync("/api/{{nameLower}}", duplicateRequest);
|
|
60181
|
+
|
|
60182
|
+
// Assert
|
|
60183
|
+
response.StatusCode.Should().BeOneOf(HttpStatusCode.Conflict, HttpStatusCode.BadRequest);
|
|
60184
|
+
}
|
|
60185
|
+
|
|
60186
|
+
#endregion
|
|
60187
|
+
|
|
60188
|
+
#region PUT Tests
|
|
60189
|
+
|
|
60190
|
+
[Fact]
|
|
60191
|
+
public async Task Update_WhenEntityExists_ShouldPersistChanges()
|
|
60192
|
+
{
|
|
60193
|
+
// Arrange - Create an entity first
|
|
60194
|
+
var createRequest = new { Code = "update-test-01", Name = "Original Name" };
|
|
60195
|
+
var createResponse = await Client.PostAsJsonAsync("/api/{{nameLower}}", createRequest);
|
|
60196
|
+
var created = await createResponse.Content.ReadFromJsonAsync<Dictionary<string, object>>();
|
|
60197
|
+
var id = created!["id"]?.ToString();
|
|
60198
|
+
|
|
60199
|
+
// Act
|
|
60200
|
+
var updateRequest = new { Code = "update-test-01", Name = "Updated Name" };
|
|
60201
|
+
var response = await Client.PutAsJsonAsync($"/api/{{nameLower}}/{id}", updateRequest);
|
|
60202
|
+
|
|
60203
|
+
// Assert
|
|
60204
|
+
response.StatusCode.Should().Be(HttpStatusCode.OK);
|
|
60205
|
+
|
|
60206
|
+
// Verify persistence
|
|
60207
|
+
var getResponse = await Client.GetAsync($"/api/{{nameLower}}/{id}");
|
|
60208
|
+
var updated = await getResponse.Content.ReadFromJsonAsync<Dictionary<string, object>>();
|
|
60209
|
+
updated.Should().NotBeNull();
|
|
60210
|
+
}
|
|
60211
|
+
|
|
60212
|
+
[Fact]
|
|
60213
|
+
public async Task Update_WhenNotExists_ShouldReturn404()
|
|
60214
|
+
{
|
|
60215
|
+
// Arrange
|
|
60216
|
+
var nonExistentId = Guid.NewGuid();
|
|
60217
|
+
var request = new { Code = "ghost", Name = "Ghost Entity" };
|
|
60218
|
+
|
|
60219
|
+
// Act
|
|
60220
|
+
var response = await Client.PutAsJsonAsync($"/api/{{nameLower}}/{nonExistentId}", request);
|
|
60221
|
+
|
|
60222
|
+
// Assert
|
|
60223
|
+
response.StatusCode.Should().Be(HttpStatusCode.NotFound);
|
|
60224
|
+
}
|
|
60225
|
+
|
|
60226
|
+
#endregion
|
|
60227
|
+
|
|
60228
|
+
#region DELETE Tests
|
|
60229
|
+
|
|
60230
|
+
[Fact]
|
|
60231
|
+
public async Task Delete_WhenEntityExists_ShouldReturn204()
|
|
60232
|
+
{
|
|
60233
|
+
// Arrange - Create an entity first
|
|
60234
|
+
var createRequest = new { Code = "delete-test-01", Name = "To Delete" };
|
|
60235
|
+
var createResponse = await Client.PostAsJsonAsync("/api/{{nameLower}}", createRequest);
|
|
60236
|
+
var created = await createResponse.Content.ReadFromJsonAsync<Dictionary<string, object>>();
|
|
60237
|
+
var id = created!["id"]?.ToString();
|
|
60238
|
+
|
|
60239
|
+
// Act
|
|
60240
|
+
var response = await Client.DeleteAsync($"/api/{{nameLower}}/{id}");
|
|
60241
|
+
|
|
60242
|
+
// Assert
|
|
60243
|
+
response.StatusCode.Should().BeOneOf(HttpStatusCode.NoContent, HttpStatusCode.OK);
|
|
60244
|
+
}
|
|
60245
|
+
|
|
60246
|
+
[Fact]
|
|
60247
|
+
public async Task Delete_WhenNotExists_ShouldReturn404()
|
|
60248
|
+
{
|
|
60249
|
+
// Arrange
|
|
60250
|
+
var nonExistentId = Guid.NewGuid();
|
|
60251
|
+
|
|
60252
|
+
// Act
|
|
60253
|
+
var response = await Client.DeleteAsync($"/api/{{nameLower}}/{nonExistentId}");
|
|
60254
|
+
|
|
60255
|
+
// Assert
|
|
60256
|
+
response.StatusCode.Should().Be(HttpStatusCode.NotFound);
|
|
60257
|
+
}
|
|
60258
|
+
|
|
60259
|
+
#endregion
|
|
60260
|
+
|
|
60261
|
+
{{#unless isSystemEntity}}
|
|
60262
|
+
#region Tenant Isolation Tests (REAL)
|
|
60263
|
+
|
|
60264
|
+
[Fact]
|
|
60265
|
+
public async Task Create_ThenGetFromOtherTenant_ShouldReturn404()
|
|
60266
|
+
{
|
|
60267
|
+
// Arrange - Create entity in default tenant
|
|
60268
|
+
var request = new { Code = "tenant-iso-01", Name = "Tenant A Entity" };
|
|
60269
|
+
var createResponse = await Client.PostAsJsonAsync("/api/{{nameLower}}", request);
|
|
60270
|
+
var created = await createResponse.Content.ReadFromJsonAsync<Dictionary<string, object>>();
|
|
60271
|
+
var id = created!["id"]?.ToString();
|
|
60272
|
+
|
|
60273
|
+
// Act - Try to access from a different tenant
|
|
60274
|
+
var otherTenantClient = CreateClientForTenant(Guid.NewGuid());
|
|
60275
|
+
var response = await otherTenantClient.GetAsync($"/api/{{nameLower}}/{id}");
|
|
60276
|
+
|
|
60277
|
+
// Assert - Should not see other tenant's data
|
|
60278
|
+
response.StatusCode.Should().BeOneOf(HttpStatusCode.NotFound, HttpStatusCode.Forbidden);
|
|
60279
|
+
}
|
|
60280
|
+
|
|
60281
|
+
[Fact]
|
|
60282
|
+
public async Task GetAll_ShouldOnlyReturnCurrentTenantData()
|
|
60283
|
+
{
|
|
60284
|
+
// Arrange - Create entity in default tenant
|
|
60285
|
+
var request = new { Code = "tenant-list-01", Name = "My Tenant Entity" };
|
|
60286
|
+
await Client.PostAsJsonAsync("/api/{{nameLower}}", request);
|
|
60287
|
+
|
|
60288
|
+
// Act - Get all from a different tenant
|
|
60289
|
+
var otherTenantClient = CreateClientForTenant(Guid.NewGuid());
|
|
60290
|
+
var response = await otherTenantClient.GetAsync("/api/{{nameLower}}");
|
|
60291
|
+
|
|
60292
|
+
// Assert
|
|
60293
|
+
response.StatusCode.Should().Be(HttpStatusCode.OK);
|
|
60294
|
+
var content = await response.Content.ReadAsStringAsync();
|
|
60295
|
+
content.Should().NotContain("tenant-list-01");
|
|
60296
|
+
}
|
|
60297
|
+
|
|
60298
|
+
#endregion
|
|
60299
|
+
{{/unless}}
|
|
60300
|
+
|
|
60301
|
+
{{#if includeAuthorization}}
|
|
60302
|
+
#region Authorization Tests (REAL)
|
|
60303
|
+
|
|
60304
|
+
[Fact]
|
|
60305
|
+
public async Task GetAll_WhenNotAuthenticated_ShouldReturn401()
|
|
60306
|
+
{
|
|
60307
|
+
// Arrange
|
|
60308
|
+
var anonClient = CreateAnonymousClient();
|
|
60309
|
+
|
|
60310
|
+
// Act
|
|
60311
|
+
var response = await anonClient.GetAsync("/api/{{nameLower}}");
|
|
60312
|
+
|
|
60313
|
+
// Assert
|
|
60314
|
+
response.StatusCode.Should().Be(HttpStatusCode.Unauthorized);
|
|
60315
|
+
}
|
|
60316
|
+
|
|
60317
|
+
[Fact]
|
|
60318
|
+
public async Task Create_WhenNotAuthenticated_ShouldReturn401()
|
|
60319
|
+
{
|
|
60320
|
+
// Arrange
|
|
60321
|
+
var anonClient = CreateAnonymousClient();
|
|
60322
|
+
var request = new { Code = "unauth-test", Name = "Should Fail" };
|
|
60323
|
+
|
|
60324
|
+
// Act
|
|
60325
|
+
var response = await anonClient.PostAsJsonAsync("/api/{{nameLower}}", request);
|
|
60326
|
+
|
|
60327
|
+
// Assert
|
|
60328
|
+
response.StatusCode.Should().Be(HttpStatusCode.Unauthorized);
|
|
60329
|
+
}
|
|
60330
|
+
|
|
60331
|
+
#endregion
|
|
60332
|
+
{{/if}}
|
|
60333
|
+
}
|
|
59747
60334
|
`;
|
|
59748
60335
|
}
|
|
59749
60336
|
});
|
|
@@ -63685,8 +64272,493 @@ Use this before scaffold_frontend_extension to understand what slots to add.`,
|
|
|
63685
64272
|
}
|
|
63686
64273
|
});
|
|
63687
64274
|
|
|
63688
|
-
// src/mcp/tools/
|
|
64275
|
+
// src/mcp/tools/scaffold-frontend-tests.ts
|
|
63689
64276
|
import path22 from "path";
|
|
64277
|
+
async function handleScaffoldFrontendTests(args, config2) {
|
|
64278
|
+
const input = ScaffoldFrontendTestsInputSchema.parse(args);
|
|
64279
|
+
const dryRun = input.options?.dryRun || false;
|
|
64280
|
+
const name = input.name;
|
|
64281
|
+
const nameLower = name.charAt(0).toLowerCase() + name.slice(1);
|
|
64282
|
+
const apiRoute = input.apiRoute || `/api/${nameLower}`;
|
|
64283
|
+
logger.info("Scaffolding frontend tests", { name, components: input.components, dryRun });
|
|
64284
|
+
const structure = await findSmartStackStructure(config2.smartstack.projectPath);
|
|
64285
|
+
const result = {
|
|
64286
|
+
success: true,
|
|
64287
|
+
files: [],
|
|
64288
|
+
instructions: []
|
|
64289
|
+
};
|
|
64290
|
+
const context = { name, nameLower, apiRoute };
|
|
64291
|
+
const components = input.components.includes("all") ? ["page", "list", "detail", "hooks", "api"] : input.components;
|
|
64292
|
+
const webRoot = input.options?.outputPath || path22.join(structure.root, "web", "smartstack-web");
|
|
64293
|
+
const templateMap = {
|
|
64294
|
+
page: { template: pageTestTemplate, filename: `${name}Page.test.tsx` },
|
|
64295
|
+
list: { template: listTestTemplate, filename: `${name}ListView.test.tsx` },
|
|
64296
|
+
detail: { template: detailTestTemplate, filename: `${name}DetailPage.test.tsx` },
|
|
64297
|
+
hooks: { template: hooksTestTemplate, filename: `use${name}Preferences.test.ts` },
|
|
64298
|
+
api: { template: apiTestTemplate, filename: `${nameLower}Api.test.ts` }
|
|
64299
|
+
};
|
|
64300
|
+
try {
|
|
64301
|
+
for (const component of components) {
|
|
64302
|
+
const entry = templateMap[component];
|
|
64303
|
+
if (!entry) continue;
|
|
64304
|
+
const content = import_handlebars3.default.compile(entry.template)(context);
|
|
64305
|
+
let testDir;
|
|
64306
|
+
if (component === "hooks") {
|
|
64307
|
+
testDir = path22.join(webRoot, "src", "hooks", "__tests__");
|
|
64308
|
+
} else if (component === "api") {
|
|
64309
|
+
testDir = path22.join(webRoot, "src", "services", "api", "__tests__");
|
|
64310
|
+
} else {
|
|
64311
|
+
testDir = path22.join(webRoot, "src", "pages", nameLower, "__tests__");
|
|
64312
|
+
}
|
|
64313
|
+
const testPath = path22.join(testDir, entry.filename);
|
|
64314
|
+
validatePathSecurity(testPath, structure.root);
|
|
64315
|
+
if (!dryRun) {
|
|
64316
|
+
await ensureDirectory(testDir);
|
|
64317
|
+
await writeText(testPath, content);
|
|
64318
|
+
}
|
|
64319
|
+
result.files.push({
|
|
64320
|
+
path: path22.relative(structure.root, testPath),
|
|
64321
|
+
content,
|
|
64322
|
+
type: "created"
|
|
64323
|
+
});
|
|
64324
|
+
}
|
|
64325
|
+
} catch (error2) {
|
|
64326
|
+
result.success = false;
|
|
64327
|
+
result.instructions.push(`Error: ${error2 instanceof Error ? error2.message : String(error2)}`);
|
|
64328
|
+
}
|
|
64329
|
+
result.instructions.push("Install test dependencies: npm install -D vitest @testing-library/react @testing-library/jest-dom @testing-library/user-event jsdom msw");
|
|
64330
|
+
result.instructions.push("Copy test infrastructure from templates/project/test-frontend/ to your web project src/test/");
|
|
64331
|
+
result.instructions.push('Add "test": "vitest run" and "test:watch": "vitest" to package.json scripts');
|
|
64332
|
+
return formatResult8(result, name, dryRun);
|
|
64333
|
+
}
|
|
64334
|
+
function formatResult8(result, name, dryRun) {
|
|
64335
|
+
const lines = [];
|
|
64336
|
+
lines.push(`# Scaffold Frontend Tests: ${name}`);
|
|
64337
|
+
lines.push("");
|
|
64338
|
+
if (dryRun) {
|
|
64339
|
+
lines.push("> **DRY RUN MODE** - No files were written");
|
|
64340
|
+
lines.push("");
|
|
64341
|
+
}
|
|
64342
|
+
if (!result.success) {
|
|
64343
|
+
lines.push("## Error");
|
|
64344
|
+
lines.push("");
|
|
64345
|
+
lines.push(result.instructions.join("\n"));
|
|
64346
|
+
return lines.join("\n");
|
|
64347
|
+
}
|
|
64348
|
+
lines.push(`## Generated Test Files (${result.files.length})`);
|
|
64349
|
+
lines.push("");
|
|
64350
|
+
for (const file of result.files) {
|
|
64351
|
+
lines.push(`### \`${file.path}\``);
|
|
64352
|
+
lines.push("");
|
|
64353
|
+
lines.push("```tsx");
|
|
64354
|
+
lines.push(file.content);
|
|
64355
|
+
lines.push("```");
|
|
64356
|
+
lines.push("");
|
|
64357
|
+
}
|
|
64358
|
+
if (result.instructions.length > 0) {
|
|
64359
|
+
lines.push("## Next Steps");
|
|
64360
|
+
lines.push("");
|
|
64361
|
+
for (const instruction of result.instructions) {
|
|
64362
|
+
lines.push(`- ${instruction}`);
|
|
64363
|
+
}
|
|
64364
|
+
lines.push("");
|
|
64365
|
+
}
|
|
64366
|
+
lines.push("## Test Conventions Applied");
|
|
64367
|
+
lines.push("");
|
|
64368
|
+
lines.push("- **Framework**: Vitest + @testing-library/react");
|
|
64369
|
+
lines.push("- **API Mocking**: MSW (Mock Service Worker)");
|
|
64370
|
+
lines.push("- **Pattern**: Arrange (setup MSW handlers) -> Act (render component) -> Assert (check DOM)");
|
|
64371
|
+
lines.push("- **Providers**: BrowserRouter, I18nextProvider, AuthContext");
|
|
64372
|
+
lines.push("");
|
|
64373
|
+
return lines.join("\n");
|
|
64374
|
+
}
|
|
64375
|
+
var import_handlebars3, ScaffoldFrontendTestsInputSchema, scaffoldFrontendTestsTool, pageTestTemplate, listTestTemplate, detailTestTemplate, hooksTestTemplate, apiTestTemplate;
|
|
64376
|
+
var init_scaffold_frontend_tests = __esm({
|
|
64377
|
+
"src/mcp/tools/scaffold-frontend-tests.ts"() {
|
|
64378
|
+
"use strict";
|
|
64379
|
+
init_esm_shims();
|
|
64380
|
+
import_handlebars3 = __toESM(require_lib());
|
|
64381
|
+
init_zod();
|
|
64382
|
+
init_fs();
|
|
64383
|
+
init_detector();
|
|
64384
|
+
init_logger();
|
|
64385
|
+
ScaffoldFrontendTestsInputSchema = external_exports.object({
|
|
64386
|
+
name: external_exports.string().min(1).describe('Entity name in PascalCase (e.g., "Product", "Order")'),
|
|
64387
|
+
components: external_exports.array(external_exports.enum(["page", "list", "form", "detail", "hooks", "api", "all"])).default(["all"]).describe("Components to generate tests for"),
|
|
64388
|
+
apiRoute: external_exports.string().optional().describe('API route path (e.g., "/api/products")'),
|
|
64389
|
+
options: external_exports.object({
|
|
64390
|
+
outputPath: external_exports.string().optional().describe("Custom output path for generated test files"),
|
|
64391
|
+
dryRun: external_exports.boolean().default(false).describe("Preview without writing files")
|
|
64392
|
+
}).optional()
|
|
64393
|
+
});
|
|
64394
|
+
scaffoldFrontendTestsTool = {
|
|
64395
|
+
name: "scaffold_frontend_tests",
|
|
64396
|
+
description: "Generate React component tests (Vitest + Testing Library + MSW) for SmartStack frontend pages, lists, forms, and hooks.",
|
|
64397
|
+
inputSchema: {
|
|
64398
|
+
type: "object",
|
|
64399
|
+
properties: {
|
|
64400
|
+
name: {
|
|
64401
|
+
type: "string",
|
|
64402
|
+
description: 'Entity name in PascalCase (e.g., "Product", "Order")'
|
|
64403
|
+
},
|
|
64404
|
+
components: {
|
|
64405
|
+
type: "array",
|
|
64406
|
+
items: {
|
|
64407
|
+
type: "string",
|
|
64408
|
+
enum: ["page", "list", "form", "detail", "hooks", "api", "all"]
|
|
64409
|
+
},
|
|
64410
|
+
default: ["all"],
|
|
64411
|
+
description: "Components to generate tests for"
|
|
64412
|
+
},
|
|
64413
|
+
apiRoute: {
|
|
64414
|
+
type: "string",
|
|
64415
|
+
description: 'API route path (e.g., "/api/products")'
|
|
64416
|
+
},
|
|
64417
|
+
options: {
|
|
64418
|
+
type: "object",
|
|
64419
|
+
properties: {
|
|
64420
|
+
outputPath: { type: "string" },
|
|
64421
|
+
dryRun: { type: "boolean", default: false }
|
|
64422
|
+
}
|
|
64423
|
+
}
|
|
64424
|
+
},
|
|
64425
|
+
required: ["name"]
|
|
64426
|
+
}
|
|
64427
|
+
};
|
|
64428
|
+
pageTestTemplate = `import { describe, it, expect, vi } from 'vitest';
|
|
64429
|
+
import { renderWithProviders, screen, waitFor } from '@/test/test-utils';
|
|
64430
|
+
import { server } from '@/test/msw/server';
|
|
64431
|
+
import { http, HttpResponse } from 'msw';
|
|
64432
|
+
|
|
64433
|
+
// Adjust import path to your actual page component
|
|
64434
|
+
// import { {{name}}Page } from '@/pages/{{nameLower}}/{{name}}Page';
|
|
64435
|
+
|
|
64436
|
+
describe('{{name}}Page', () => {
|
|
64437
|
+
it('should render the page title', async () => {
|
|
64438
|
+
// TODO: Uncomment and adjust import path
|
|
64439
|
+
// renderWithProviders(<{{name}}Page />);
|
|
64440
|
+
// await waitFor(() => {
|
|
64441
|
+
// expect(screen.getByRole('heading')).toBeInTheDocument();
|
|
64442
|
+
// });
|
|
64443
|
+
expect(true).toBe(true); // Placeholder - replace with actual test
|
|
64444
|
+
});
|
|
64445
|
+
|
|
64446
|
+
it('should show loading state initially', () => {
|
|
64447
|
+
// renderWithProviders(<{{name}}Page />);
|
|
64448
|
+
// expect(screen.getByTestId('page-loader')).toBeInTheDocument();
|
|
64449
|
+
expect(true).toBe(true);
|
|
64450
|
+
});
|
|
64451
|
+
|
|
64452
|
+
it('should display data after loading', async () => {
|
|
64453
|
+
server.use(
|
|
64454
|
+
http.get('{{apiRoute}}', () =>
|
|
64455
|
+
HttpResponse.json({
|
|
64456
|
+
items: [
|
|
64457
|
+
{ id: '1', code: 'test-01', name: 'Test {{name}}' },
|
|
64458
|
+
],
|
|
64459
|
+
totalCount: 1,
|
|
64460
|
+
pageNumber: 1,
|
|
64461
|
+
pageSize: 10,
|
|
64462
|
+
})
|
|
64463
|
+
)
|
|
64464
|
+
);
|
|
64465
|
+
|
|
64466
|
+
// renderWithProviders(<{{name}}Page />);
|
|
64467
|
+
// await waitFor(() => {
|
|
64468
|
+
// expect(screen.getByText('Test {{name}}')).toBeInTheDocument();
|
|
64469
|
+
// });
|
|
64470
|
+
expect(true).toBe(true);
|
|
64471
|
+
});
|
|
64472
|
+
|
|
64473
|
+
it('should show empty state when no data', async () => {
|
|
64474
|
+
server.use(
|
|
64475
|
+
http.get('{{apiRoute}}', () =>
|
|
64476
|
+
HttpResponse.json({
|
|
64477
|
+
items: [],
|
|
64478
|
+
totalCount: 0,
|
|
64479
|
+
pageNumber: 1,
|
|
64480
|
+
pageSize: 10,
|
|
64481
|
+
})
|
|
64482
|
+
)
|
|
64483
|
+
);
|
|
64484
|
+
|
|
64485
|
+
// renderWithProviders(<{{name}}Page />);
|
|
64486
|
+
// await waitFor(() => {
|
|
64487
|
+
// expect(screen.getByText(/empty|no data|aucun/i)).toBeInTheDocument();
|
|
64488
|
+
// });
|
|
64489
|
+
expect(true).toBe(true);
|
|
64490
|
+
});
|
|
64491
|
+
|
|
64492
|
+
it('should show error state on API failure', async () => {
|
|
64493
|
+
server.use(
|
|
64494
|
+
http.get('{{apiRoute}}', () =>
|
|
64495
|
+
HttpResponse.error()
|
|
64496
|
+
)
|
|
64497
|
+
);
|
|
64498
|
+
|
|
64499
|
+
// renderWithProviders(<{{name}}Page />);
|
|
64500
|
+
// await waitFor(() => {
|
|
64501
|
+
// expect(screen.getByText(/error|erreur/i)).toBeInTheDocument();
|
|
64502
|
+
// });
|
|
64503
|
+
expect(true).toBe(true);
|
|
64504
|
+
});
|
|
64505
|
+
});
|
|
64506
|
+
`;
|
|
64507
|
+
listTestTemplate = `import { describe, it, expect } from 'vitest';
|
|
64508
|
+
import { renderWithProviders, screen, within } from '@/test/test-utils';
|
|
64509
|
+
import { server } from '@/test/msw/server';
|
|
64510
|
+
import { http, HttpResponse } from 'msw';
|
|
64511
|
+
|
|
64512
|
+
// Adjust import path
|
|
64513
|
+
// import { {{name}}ListView } from '@/components/{{nameLower}}/{{name}}ListView';
|
|
64514
|
+
|
|
64515
|
+
const mockItems = [
|
|
64516
|
+
{ id: '1', code: 'item-01', name: 'First {{name}}' },
|
|
64517
|
+
{ id: '2', code: 'item-02', name: 'Second {{name}}' },
|
|
64518
|
+
{ id: '3', code: 'item-03', name: 'Third {{name}}' },
|
|
64519
|
+
];
|
|
64520
|
+
|
|
64521
|
+
describe('{{name}}ListView', () => {
|
|
64522
|
+
it('should render a list of items', () => {
|
|
64523
|
+
// renderWithProviders(<{{name}}ListView items={mockItems} />);
|
|
64524
|
+
// expect(screen.getByText('First {{name}}')).toBeInTheDocument();
|
|
64525
|
+
// expect(screen.getByText('Second {{name}}')).toBeInTheDocument();
|
|
64526
|
+
expect(true).toBe(true);
|
|
64527
|
+
});
|
|
64528
|
+
|
|
64529
|
+
it('should render EntityCard for each item', () => {
|
|
64530
|
+
// renderWithProviders(<{{name}}ListView items={mockItems} />);
|
|
64531
|
+
// const cards = screen.getAllByTestId('entity-card');
|
|
64532
|
+
// expect(cards).toHaveLength(3);
|
|
64533
|
+
expect(true).toBe(true);
|
|
64534
|
+
});
|
|
64535
|
+
|
|
64536
|
+
it('should show empty state when no items', () => {
|
|
64537
|
+
// renderWithProviders(<{{name}}ListView items={[]} />);
|
|
64538
|
+
// expect(screen.getByText(/empty|no data|aucun/i)).toBeInTheDocument();
|
|
64539
|
+
expect(true).toBe(true);
|
|
64540
|
+
});
|
|
64541
|
+
|
|
64542
|
+
it('should support view toggle (table/cards)', () => {
|
|
64543
|
+
// renderWithProviders(<{{name}}ListView items={mockItems} />);
|
|
64544
|
+
// const viewToggle = screen.getByTestId('view-toggle');
|
|
64545
|
+
// expect(viewToggle).toBeInTheDocument();
|
|
64546
|
+
expect(true).toBe(true);
|
|
64547
|
+
});
|
|
64548
|
+
|
|
64549
|
+
it('should handle pagination', () => {
|
|
64550
|
+
// renderWithProviders(
|
|
64551
|
+
// <{{name}}ListView
|
|
64552
|
+
// items={mockItems}
|
|
64553
|
+
// totalCount={30}
|
|
64554
|
+
// pageSize={10}
|
|
64555
|
+
// currentPage={1}
|
|
64556
|
+
// />
|
|
64557
|
+
// );
|
|
64558
|
+
// expect(screen.getByTestId('pagination')).toBeInTheDocument();
|
|
64559
|
+
expect(true).toBe(true);
|
|
64560
|
+
});
|
|
64561
|
+
});
|
|
64562
|
+
`;
|
|
64563
|
+
detailTestTemplate = `import { describe, it, expect } from 'vitest';
|
|
64564
|
+
import { renderWithProviders, screen, waitFor } from '@/test/test-utils';
|
|
64565
|
+
import { server } from '@/test/msw/server';
|
|
64566
|
+
import { http, HttpResponse } from 'msw';
|
|
64567
|
+
|
|
64568
|
+
// Adjust import path
|
|
64569
|
+
// import { {{name}}DetailPage } from '@/pages/{{nameLower}}/{{name}}DetailPage';
|
|
64570
|
+
|
|
64571
|
+
const mockEntity = {
|
|
64572
|
+
id: '1',
|
|
64573
|
+
code: 'test-01',
|
|
64574
|
+
name: 'Test {{name}}',
|
|
64575
|
+
createdAt: '2024-01-01T00:00:00Z',
|
|
64576
|
+
createdBy: 'admin',
|
|
64577
|
+
};
|
|
64578
|
+
|
|
64579
|
+
describe('{{name}}DetailPage', () => {
|
|
64580
|
+
it('should render entity details', async () => {
|
|
64581
|
+
server.use(
|
|
64582
|
+
http.get('{{apiRoute}}/:id', () =>
|
|
64583
|
+
HttpResponse.json(mockEntity)
|
|
64584
|
+
)
|
|
64585
|
+
);
|
|
64586
|
+
|
|
64587
|
+
// renderWithProviders(<{{name}}DetailPage />);
|
|
64588
|
+
// await waitFor(() => {
|
|
64589
|
+
// expect(screen.getByText('Test {{name}}')).toBeInTheDocument();
|
|
64590
|
+
// });
|
|
64591
|
+
expect(true).toBe(true);
|
|
64592
|
+
});
|
|
64593
|
+
|
|
64594
|
+
it('should show loading state', () => {
|
|
64595
|
+
// renderWithProviders(<{{name}}DetailPage />);
|
|
64596
|
+
// expect(screen.getByTestId('page-loader')).toBeInTheDocument();
|
|
64597
|
+
expect(true).toBe(true);
|
|
64598
|
+
});
|
|
64599
|
+
|
|
64600
|
+
it('should show 404 when entity not found', async () => {
|
|
64601
|
+
server.use(
|
|
64602
|
+
http.get('{{apiRoute}}/:id', () =>
|
|
64603
|
+
new HttpResponse(null, { status: 404 })
|
|
64604
|
+
)
|
|
64605
|
+
);
|
|
64606
|
+
|
|
64607
|
+
// renderWithProviders(<{{name}}DetailPage />);
|
|
64608
|
+
// await waitFor(() => {
|
|
64609
|
+
// expect(screen.getByText(/not found|introuvable/i)).toBeInTheDocument();
|
|
64610
|
+
// });
|
|
64611
|
+
expect(true).toBe(true);
|
|
64612
|
+
});
|
|
64613
|
+
|
|
64614
|
+
it('should have a back button', () => {
|
|
64615
|
+
// renderWithProviders(<{{name}}DetailPage />);
|
|
64616
|
+
// expect(screen.getByRole('link', { name: /back|retour/i })).toBeInTheDocument();
|
|
64617
|
+
expect(true).toBe(true);
|
|
64618
|
+
});
|
|
64619
|
+
});
|
|
64620
|
+
`;
|
|
64621
|
+
hooksTestTemplate = `import { describe, it, expect } from 'vitest';
|
|
64622
|
+
import { renderHook, act } from '@testing-library/react';
|
|
64623
|
+
|
|
64624
|
+
// Adjust import path
|
|
64625
|
+
// import { use{{name}}Preferences } from '@/hooks/use{{name}}Preferences';
|
|
64626
|
+
|
|
64627
|
+
describe('use{{name}}Preferences', () => {
|
|
64628
|
+
it('should return default pageSize', () => {
|
|
64629
|
+
// const { result } = renderHook(() => use{{name}}Preferences());
|
|
64630
|
+
// expect(result.current.pageSize).toBe(10);
|
|
64631
|
+
expect(true).toBe(true);
|
|
64632
|
+
});
|
|
64633
|
+
|
|
64634
|
+
it('should return default viewMode', () => {
|
|
64635
|
+
// const { result } = renderHook(() => use{{name}}Preferences());
|
|
64636
|
+
// expect(result.current.viewMode).toBe('table');
|
|
64637
|
+
expect(true).toBe(true);
|
|
64638
|
+
});
|
|
64639
|
+
|
|
64640
|
+
it('should update pageSize', () => {
|
|
64641
|
+
// const { result } = renderHook(() => use{{name}}Preferences());
|
|
64642
|
+
// act(() => {
|
|
64643
|
+
// result.current.setPageSize(25);
|
|
64644
|
+
// });
|
|
64645
|
+
// expect(result.current.pageSize).toBe(25);
|
|
64646
|
+
expect(true).toBe(true);
|
|
64647
|
+
});
|
|
64648
|
+
|
|
64649
|
+
it('should update viewMode', () => {
|
|
64650
|
+
// const { result } = renderHook(() => use{{name}}Preferences());
|
|
64651
|
+
// act(() => {
|
|
64652
|
+
// result.current.setViewMode('cards');
|
|
64653
|
+
// });
|
|
64654
|
+
// expect(result.current.viewMode).toBe('cards');
|
|
64655
|
+
expect(true).toBe(true);
|
|
64656
|
+
});
|
|
64657
|
+
});
|
|
64658
|
+
`;
|
|
64659
|
+
apiTestTemplate = `import { describe, it, expect, beforeEach } from 'vitest';
|
|
64660
|
+
import { server } from '@/test/msw/server';
|
|
64661
|
+
import { http, HttpResponse } from 'msw';
|
|
64662
|
+
|
|
64663
|
+
// Adjust import path
|
|
64664
|
+
// import { {{nameLower}}Api } from '@/services/api/{{nameLower}}Api';
|
|
64665
|
+
|
|
64666
|
+
describe('{{nameLower}}Api', () => {
|
|
64667
|
+
describe('getAll', () => {
|
|
64668
|
+
it('should fetch paginated list', async () => {
|
|
64669
|
+
server.use(
|
|
64670
|
+
http.get('{{apiRoute}}', () =>
|
|
64671
|
+
HttpResponse.json({
|
|
64672
|
+
items: [{ id: '1', code: 'test', name: 'Test' }],
|
|
64673
|
+
totalCount: 1,
|
|
64674
|
+
pageNumber: 1,
|
|
64675
|
+
pageSize: 10,
|
|
64676
|
+
})
|
|
64677
|
+
)
|
|
64678
|
+
);
|
|
64679
|
+
|
|
64680
|
+
// const result = await {{nameLower}}Api.getAll();
|
|
64681
|
+
// expect(result.items).toHaveLength(1);
|
|
64682
|
+
// expect(result.totalCount).toBe(1);
|
|
64683
|
+
expect(true).toBe(true);
|
|
64684
|
+
});
|
|
64685
|
+
});
|
|
64686
|
+
|
|
64687
|
+
describe('getById', () => {
|
|
64688
|
+
it('should fetch entity by ID', async () => {
|
|
64689
|
+
server.use(
|
|
64690
|
+
http.get('{{apiRoute}}/:id', () =>
|
|
64691
|
+
HttpResponse.json({ id: '1', code: 'test', name: 'Test' })
|
|
64692
|
+
)
|
|
64693
|
+
);
|
|
64694
|
+
|
|
64695
|
+
// const result = await {{nameLower}}Api.getById('1');
|
|
64696
|
+
// expect(result.id).toBe('1');
|
|
64697
|
+
expect(true).toBe(true);
|
|
64698
|
+
});
|
|
64699
|
+
|
|
64700
|
+
it('should throw on 404', async () => {
|
|
64701
|
+
server.use(
|
|
64702
|
+
http.get('{{apiRoute}}/:id', () =>
|
|
64703
|
+
new HttpResponse(null, { status: 404 })
|
|
64704
|
+
)
|
|
64705
|
+
);
|
|
64706
|
+
|
|
64707
|
+
// await expect({{nameLower}}Api.getById('999')).rejects.toThrow();
|
|
64708
|
+
expect(true).toBe(true);
|
|
64709
|
+
});
|
|
64710
|
+
});
|
|
64711
|
+
|
|
64712
|
+
describe('create', () => {
|
|
64713
|
+
it('should create entity and return it', async () => {
|
|
64714
|
+
server.use(
|
|
64715
|
+
http.post('{{apiRoute}}', () =>
|
|
64716
|
+
HttpResponse.json(
|
|
64717
|
+
{ id: '1', code: 'new-test', name: 'New Test' },
|
|
64718
|
+
{ status: 201 }
|
|
64719
|
+
)
|
|
64720
|
+
)
|
|
64721
|
+
);
|
|
64722
|
+
|
|
64723
|
+
// const result = await {{nameLower}}Api.create({ code: 'new-test', name: 'New Test' });
|
|
64724
|
+
// expect(result.id).toBe('1');
|
|
64725
|
+
expect(true).toBe(true);
|
|
64726
|
+
});
|
|
64727
|
+
});
|
|
64728
|
+
|
|
64729
|
+
describe('update', () => {
|
|
64730
|
+
it('should update entity', async () => {
|
|
64731
|
+
server.use(
|
|
64732
|
+
http.put('{{apiRoute}}/:id', () =>
|
|
64733
|
+
HttpResponse.json({ id: '1', code: 'updated', name: 'Updated' })
|
|
64734
|
+
)
|
|
64735
|
+
);
|
|
64736
|
+
|
|
64737
|
+
// const result = await {{nameLower}}Api.update('1', { code: 'updated', name: 'Updated' });
|
|
64738
|
+
// expect(result.code).toBe('updated');
|
|
64739
|
+
expect(true).toBe(true);
|
|
64740
|
+
});
|
|
64741
|
+
});
|
|
64742
|
+
|
|
64743
|
+
describe('delete', () => {
|
|
64744
|
+
it('should delete entity', async () => {
|
|
64745
|
+
server.use(
|
|
64746
|
+
http.delete('{{apiRoute}}/:id', () =>
|
|
64747
|
+
new HttpResponse(null, { status: 204 })
|
|
64748
|
+
)
|
|
64749
|
+
);
|
|
64750
|
+
|
|
64751
|
+
// await expect({{nameLower}}Api.delete('1')).resolves.not.toThrow();
|
|
64752
|
+
expect(true).toBe(true);
|
|
64753
|
+
});
|
|
64754
|
+
});
|
|
64755
|
+
});
|
|
64756
|
+
`;
|
|
64757
|
+
}
|
|
64758
|
+
});
|
|
64759
|
+
|
|
64760
|
+
// src/mcp/tools/validate-security.ts
|
|
64761
|
+
import path23 from "path";
|
|
63690
64762
|
async function handleValidateSecurity(args, config2) {
|
|
63691
64763
|
const input = ValidateSecurityInputSchema.parse(args);
|
|
63692
64764
|
const projectPath = input.path || config2.smartstack.projectPath;
|
|
@@ -63768,7 +64840,7 @@ async function checkHardcodedSecrets(structure, result) {
|
|
|
63768
64840
|
severity: "blocking",
|
|
63769
64841
|
category: "hardcoded-secrets",
|
|
63770
64842
|
message: `Hardcoded ${name} detected`,
|
|
63771
|
-
file:
|
|
64843
|
+
file: path23.relative(structure.root, file),
|
|
63772
64844
|
line: lineNumber,
|
|
63773
64845
|
code: truncateCode(lineContent),
|
|
63774
64846
|
suggestion: `Move ${name} to configuration or environment variables`,
|
|
@@ -63803,7 +64875,7 @@ async function checkSqlInjection(structure, result) {
|
|
|
63803
64875
|
severity: "blocking",
|
|
63804
64876
|
category: "sql-injection",
|
|
63805
64877
|
message: `Potential SQL injection: ${name}`,
|
|
63806
|
-
file:
|
|
64878
|
+
file: path23.relative(structure.root, file),
|
|
63807
64879
|
line: lineNumber,
|
|
63808
64880
|
code: truncateCode(lineContent),
|
|
63809
64881
|
suggestion: 'Use parameterized queries: FromSqlRaw("SELECT * FROM x WHERE id = {0}", id) or FromSqlInterpolated',
|
|
@@ -63843,7 +64915,7 @@ async function checkTenantIsolation(structure, result) {
|
|
|
63843
64915
|
severity: "blocking",
|
|
63844
64916
|
category: "tenant-isolation",
|
|
63845
64917
|
message: "Create method missing tenantId parameter for tenant entity",
|
|
63846
|
-
file:
|
|
64918
|
+
file: path23.relative(structure.root, file),
|
|
63847
64919
|
line: lineNumber,
|
|
63848
64920
|
code: truncateCode(match2[0]),
|
|
63849
64921
|
suggestion: "Add Guid tenantId as first parameter: Create(Guid tenantId, ...)",
|
|
@@ -63866,7 +64938,7 @@ async function checkTenantIsolation(structure, result) {
|
|
|
63866
64938
|
severity: "critical",
|
|
63867
64939
|
category: "tenant-isolation",
|
|
63868
64940
|
message: "Direct DbContext access without tenant filtering",
|
|
63869
|
-
file:
|
|
64941
|
+
file: path23.relative(structure.root, file),
|
|
63870
64942
|
line: lineNumber,
|
|
63871
64943
|
code: truncateCode(match2[0]),
|
|
63872
64944
|
suggestion: "Use repository with global tenant filter or add explicit TenantId filter",
|
|
@@ -63903,7 +64975,7 @@ async function checkAuthorization(structure, result) {
|
|
|
63903
64975
|
severity: "blocking",
|
|
63904
64976
|
category: "authorization",
|
|
63905
64977
|
message: `Controller ${controllerName} missing authorization attribute`,
|
|
63906
|
-
file:
|
|
64978
|
+
file: path23.relative(structure.root, file),
|
|
63907
64979
|
line: lineNumber,
|
|
63908
64980
|
suggestion: 'Add [NavRoute("context.application.module")] or [Authorize] attribute to the controller class',
|
|
63909
64981
|
cweId: "CWE-862"
|
|
@@ -63951,7 +65023,7 @@ async function checkPatterns(file, content, patterns, structure, result) {
|
|
|
63951
65023
|
severity: "critical",
|
|
63952
65024
|
category: "dangerous-functions",
|
|
63953
65025
|
message: `Dangerous function: ${name}`,
|
|
63954
|
-
file:
|
|
65026
|
+
file: path23.relative(structure.root, file),
|
|
63955
65027
|
line: lineNumber,
|
|
63956
65028
|
code: truncateCode(lineContent),
|
|
63957
65029
|
suggestion: "Avoid using dangerous functions with user-controlled input. Use safe alternatives.",
|
|
@@ -64160,7 +65232,7 @@ var init_validate_security = __esm({
|
|
|
64160
65232
|
});
|
|
64161
65233
|
|
|
64162
65234
|
// src/mcp/tools/analyze-code-quality.ts
|
|
64163
|
-
import
|
|
65235
|
+
import path24 from "path";
|
|
64164
65236
|
async function handleAnalyzeCodeQuality(args, config2) {
|
|
64165
65237
|
const input = AnalyzeCodeQualityInputSchema.parse(args);
|
|
64166
65238
|
const projectPath = input.path || config2.smartstack.projectPath;
|
|
@@ -64179,7 +65251,7 @@ async function handleAnalyzeCodeQuality(args, config2) {
|
|
|
64179
65251
|
const filteredCsFiles = csFiles.filter((f) => !isExcludedPath(f));
|
|
64180
65252
|
for (const file of filteredCsFiles) {
|
|
64181
65253
|
const content = await readText(file);
|
|
64182
|
-
const relPath =
|
|
65254
|
+
const relPath = path24.relative(structure.root, file);
|
|
64183
65255
|
const lineCount = content.split("\n").length;
|
|
64184
65256
|
const functions = extractCSharpFunctions(content, relPath);
|
|
64185
65257
|
allFunctionMetrics.push(...functions);
|
|
@@ -64196,7 +65268,7 @@ async function handleAnalyzeCodeQuality(args, config2) {
|
|
|
64196
65268
|
const filteredTsFiles = tsFiles.filter((f) => !isExcludedPath(f));
|
|
64197
65269
|
for (const file of filteredTsFiles) {
|
|
64198
65270
|
const content = await readText(file);
|
|
64199
|
-
const relPath =
|
|
65271
|
+
const relPath = path24.relative(structure.root, file);
|
|
64200
65272
|
const lineCount = content.split("\n").length;
|
|
64201
65273
|
const functions = extractTypeScriptFunctions(content, relPath);
|
|
64202
65274
|
allFunctionMetrics.push(...functions);
|
|
@@ -64812,7 +65884,7 @@ var init_analyze_code_quality = __esm({
|
|
|
64812
65884
|
});
|
|
64813
65885
|
|
|
64814
65886
|
// src/mcp/tools/analyze-hierarchy-patterns.ts
|
|
64815
|
-
import
|
|
65887
|
+
import path25 from "path";
|
|
64816
65888
|
async function handleAnalyzeHierarchyPatterns(args, config2) {
|
|
64817
65889
|
const input = AnalyzeHierarchyPatternsInputSchema.parse(args);
|
|
64818
65890
|
const projectPath = input.path || config2.smartstack.projectPath;
|
|
@@ -64835,7 +65907,7 @@ async function handleAnalyzeHierarchyPatterns(args, config2) {
|
|
|
64835
65907
|
recommendations,
|
|
64836
65908
|
circularDependencies: detectCircularDependencies(entityGraph)
|
|
64837
65909
|
};
|
|
64838
|
-
return
|
|
65910
|
+
return formatResult9(result, outputFormat, structure.root);
|
|
64839
65911
|
}
|
|
64840
65912
|
async function buildEntityGraph(domainPath, filter3) {
|
|
64841
65913
|
const entityFiles = await findFiles("**/*.cs", { cwd: domainPath });
|
|
@@ -64891,7 +65963,7 @@ async function buildEntityGraph(domainPath, filter3) {
|
|
|
64891
65963
|
return entityMap;
|
|
64892
65964
|
}
|
|
64893
65965
|
function parseEntity(content, file, domainPath) {
|
|
64894
|
-
const fileName =
|
|
65966
|
+
const fileName = path25.basename(file, ".cs");
|
|
64895
65967
|
if (fileName.endsWith("Dto") || fileName.endsWith("Command") || fileName.endsWith("Query") || fileName.endsWith("Handler") || fileName.endsWith("Validator") || fileName.endsWith("Exception") || fileName.startsWith("I")) {
|
|
64896
65968
|
return null;
|
|
64897
65969
|
}
|
|
@@ -64935,7 +66007,7 @@ function parseEntity(content, file, domainPath) {
|
|
|
64935
66007
|
const parentEntity = foreignKeys.length > 0 ? foreignKeys[0].referencedEntity : void 0;
|
|
64936
66008
|
return {
|
|
64937
66009
|
name: entityName,
|
|
64938
|
-
file:
|
|
66010
|
+
file: path25.relative(domainPath, file),
|
|
64939
66011
|
baseClass: hasSystemEntity ? "SystemEntity" : "BaseEntity",
|
|
64940
66012
|
isTenantAware: hasITenantEntity,
|
|
64941
66013
|
isSystemEntity: hasSystemEntity,
|
|
@@ -64966,24 +66038,24 @@ function detectCircularDependencies(entityMap) {
|
|
|
64966
66038
|
const cycles = [];
|
|
64967
66039
|
for (const [name] of entityMap) {
|
|
64968
66040
|
let dfs2 = function(current) {
|
|
64969
|
-
if (
|
|
64970
|
-
const cycleStart =
|
|
64971
|
-
cycles.push([...
|
|
66041
|
+
if (path30.includes(current)) {
|
|
66042
|
+
const cycleStart = path30.indexOf(current);
|
|
66043
|
+
cycles.push([...path30.slice(cycleStart), current]);
|
|
64972
66044
|
return true;
|
|
64973
66045
|
}
|
|
64974
66046
|
if (visited.has(current)) return false;
|
|
64975
66047
|
visited.add(current);
|
|
64976
|
-
|
|
66048
|
+
path30.push(current);
|
|
64977
66049
|
const node = entityMap.get(current);
|
|
64978
66050
|
if (node?.parent) {
|
|
64979
66051
|
dfs2(node.parent);
|
|
64980
66052
|
}
|
|
64981
|
-
|
|
66053
|
+
path30.pop();
|
|
64982
66054
|
return false;
|
|
64983
66055
|
};
|
|
64984
66056
|
var dfs = dfs2;
|
|
64985
66057
|
const visited = /* @__PURE__ */ new Set();
|
|
64986
|
-
const
|
|
66058
|
+
const path30 = [];
|
|
64987
66059
|
dfs2(name);
|
|
64988
66060
|
}
|
|
64989
66061
|
const unique = /* @__PURE__ */ new Map();
|
|
@@ -65190,7 +66262,7 @@ function getImplementationSteps(pattern, node) {
|
|
|
65190
66262
|
return [];
|
|
65191
66263
|
}
|
|
65192
66264
|
}
|
|
65193
|
-
function
|
|
66265
|
+
function formatResult9(result, format, _rootPath) {
|
|
65194
66266
|
if (format === "json") {
|
|
65195
66267
|
return JSON.stringify(result, null, 2);
|
|
65196
66268
|
}
|
|
@@ -68928,7 +70000,7 @@ var init_conventions = __esm({
|
|
|
68928
70000
|
});
|
|
68929
70001
|
|
|
68930
70002
|
// src/mcp/resources/project-info.ts
|
|
68931
|
-
import
|
|
70003
|
+
import path26 from "path";
|
|
68932
70004
|
async function getProjectInfoResource(config2) {
|
|
68933
70005
|
const projectPath = config2.smartstack.projectPath;
|
|
68934
70006
|
const projectInfo = await detectProject(projectPath);
|
|
@@ -68959,16 +70031,16 @@ async function getProjectInfoResource(config2) {
|
|
|
68959
70031
|
lines.push("```");
|
|
68960
70032
|
lines.push(`${projectInfo.name}/`);
|
|
68961
70033
|
if (structure.domain) {
|
|
68962
|
-
lines.push(`\u251C\u2500\u2500 ${
|
|
70034
|
+
lines.push(`\u251C\u2500\u2500 ${path26.basename(structure.domain)}/ # Domain layer (entities)`);
|
|
68963
70035
|
}
|
|
68964
70036
|
if (structure.application) {
|
|
68965
|
-
lines.push(`\u251C\u2500\u2500 ${
|
|
70037
|
+
lines.push(`\u251C\u2500\u2500 ${path26.basename(structure.application)}/ # Application layer (services)`);
|
|
68966
70038
|
}
|
|
68967
70039
|
if (structure.infrastructure) {
|
|
68968
|
-
lines.push(`\u251C\u2500\u2500 ${
|
|
70040
|
+
lines.push(`\u251C\u2500\u2500 ${path26.basename(structure.infrastructure)}/ # Infrastructure (EF Core)`);
|
|
68969
70041
|
}
|
|
68970
70042
|
if (structure.api) {
|
|
68971
|
-
lines.push(`\u251C\u2500\u2500 ${
|
|
70043
|
+
lines.push(`\u251C\u2500\u2500 ${path26.basename(structure.api)}/ # API layer (controllers)`);
|
|
68972
70044
|
}
|
|
68973
70045
|
if (structure.web) {
|
|
68974
70046
|
lines.push(`\u2514\u2500\u2500 web/smartstack-web/ # React frontend`);
|
|
@@ -68981,8 +70053,8 @@ async function getProjectInfoResource(config2) {
|
|
|
68981
70053
|
lines.push("| Project | Path |");
|
|
68982
70054
|
lines.push("|---------|------|");
|
|
68983
70055
|
for (const csproj of projectInfo.csprojFiles) {
|
|
68984
|
-
const name =
|
|
68985
|
-
const relativePath =
|
|
70056
|
+
const name = path26.basename(csproj, ".csproj");
|
|
70057
|
+
const relativePath = path26.relative(projectPath, csproj);
|
|
68986
70058
|
lines.push(`| ${name} | \`${relativePath}\` |`);
|
|
68987
70059
|
}
|
|
68988
70060
|
lines.push("");
|
|
@@ -68992,10 +70064,10 @@ async function getProjectInfoResource(config2) {
|
|
|
68992
70064
|
cwd: structure.migrations,
|
|
68993
70065
|
ignore: ["*.Designer.cs"]
|
|
68994
70066
|
});
|
|
68995
|
-
const migrations = migrationFiles.map((f) =>
|
|
70067
|
+
const migrations = migrationFiles.map((f) => path26.basename(f)).filter((f) => !f.includes("ModelSnapshot") && !f.includes(".Designer.")).sort();
|
|
68996
70068
|
lines.push("## EF Core Migrations");
|
|
68997
70069
|
lines.push("");
|
|
68998
|
-
lines.push(`**Location**: \`${
|
|
70070
|
+
lines.push(`**Location**: \`${path26.relative(projectPath, structure.migrations)}\``);
|
|
68999
70071
|
lines.push(`**Total Migrations**: ${migrations.length}`);
|
|
69000
70072
|
lines.push("");
|
|
69001
70073
|
if (migrations.length > 0) {
|
|
@@ -69030,11 +70102,11 @@ async function getProjectInfoResource(config2) {
|
|
|
69030
70102
|
lines.push("dotnet build");
|
|
69031
70103
|
lines.push("");
|
|
69032
70104
|
lines.push("# Run API");
|
|
69033
|
-
lines.push(`cd ${structure.api ?
|
|
70105
|
+
lines.push(`cd ${structure.api ? path26.relative(projectPath, structure.api) : "src/Api"}`);
|
|
69034
70106
|
lines.push("dotnet run");
|
|
69035
70107
|
lines.push("");
|
|
69036
70108
|
lines.push("# Run frontend");
|
|
69037
|
-
lines.push(`cd ${structure.web ?
|
|
70109
|
+
lines.push(`cd ${structure.web ? path26.relative(projectPath, structure.web) : "web"}`);
|
|
69038
70110
|
lines.push("npm run dev");
|
|
69039
70111
|
lines.push("");
|
|
69040
70112
|
lines.push("# Create migration");
|
|
@@ -69072,7 +70144,7 @@ var init_project_info = __esm({
|
|
|
69072
70144
|
});
|
|
69073
70145
|
|
|
69074
70146
|
// src/mcp/resources/api-endpoints.ts
|
|
69075
|
-
import
|
|
70147
|
+
import path27 from "path";
|
|
69076
70148
|
async function getApiEndpointsResource(config2, endpointFilter) {
|
|
69077
70149
|
const structure = await findSmartStackStructure(config2.smartstack.projectPath);
|
|
69078
70150
|
if (!structure.api) {
|
|
@@ -69091,7 +70163,7 @@ async function getApiEndpointsResource(config2, endpointFilter) {
|
|
|
69091
70163
|
}
|
|
69092
70164
|
async function parseController(filePath, _rootPath) {
|
|
69093
70165
|
const content = await readText(filePath);
|
|
69094
|
-
const fileName =
|
|
70166
|
+
const fileName = path27.basename(filePath, ".cs");
|
|
69095
70167
|
const controllerName = fileName.replace("Controller", "");
|
|
69096
70168
|
const endpoints = [];
|
|
69097
70169
|
const routeMatch = content.match(/\[Route\s*\(\s*"([^"]+)"\s*\)\]/);
|
|
@@ -69253,7 +70325,7 @@ var init_api_endpoints = __esm({
|
|
|
69253
70325
|
});
|
|
69254
70326
|
|
|
69255
70327
|
// src/mcp/resources/db-schema.ts
|
|
69256
|
-
import
|
|
70328
|
+
import path28 from "path";
|
|
69257
70329
|
async function getDbSchemaResource(config2, tableFilter) {
|
|
69258
70330
|
const structure = await findSmartStackStructure(config2.smartstack.projectPath);
|
|
69259
70331
|
if (!structure.domain && !structure.infrastructure) {
|
|
@@ -69337,7 +70409,7 @@ async function parseEntity2(filePath, rootPath, _config) {
|
|
|
69337
70409
|
tableName,
|
|
69338
70410
|
properties,
|
|
69339
70411
|
relationships,
|
|
69340
|
-
file:
|
|
70412
|
+
file: path28.relative(rootPath, filePath)
|
|
69341
70413
|
};
|
|
69342
70414
|
}
|
|
69343
70415
|
async function enrichFromConfigurations(entities, infrastructurePath, _config) {
|
|
@@ -69498,7 +70570,7 @@ var init_db_schema = __esm({
|
|
|
69498
70570
|
});
|
|
69499
70571
|
|
|
69500
70572
|
// src/mcp/resources/entities.ts
|
|
69501
|
-
import
|
|
70573
|
+
import path29 from "path";
|
|
69502
70574
|
async function getEntitiesResource(config2, entityFilter) {
|
|
69503
70575
|
const structure = await findSmartStackStructure(config2.smartstack.projectPath);
|
|
69504
70576
|
if (!structure.domain) {
|
|
@@ -69552,7 +70624,7 @@ async function parseEntitySummary(filePath, rootPath, config2) {
|
|
|
69552
70624
|
hasSoftDelete,
|
|
69553
70625
|
hasRowVersion,
|
|
69554
70626
|
file: filePath,
|
|
69555
|
-
relativePath:
|
|
70627
|
+
relativePath: path29.relative(rootPath, filePath)
|
|
69556
70628
|
};
|
|
69557
70629
|
}
|
|
69558
70630
|
function inferTableInfo(entityName, config2) {
|
|
@@ -69761,6 +70833,8 @@ async function createServer() {
|
|
|
69761
70833
|
analyzeTestCoverageTool,
|
|
69762
70834
|
validateTestConventionsTool,
|
|
69763
70835
|
suggestTestScenariosTool,
|
|
70836
|
+
// Frontend Test Tools
|
|
70837
|
+
scaffoldFrontendTestsTool,
|
|
69764
70838
|
// Frontend Route Tools
|
|
69765
70839
|
scaffoldApiClientTool,
|
|
69766
70840
|
scaffoldRoutesTool,
|
|
@@ -69786,35 +70860,39 @@ async function createServer() {
|
|
|
69786
70860
|
let result;
|
|
69787
70861
|
switch (name) {
|
|
69788
70862
|
case "validate_conventions":
|
|
69789
|
-
result = await handleValidateConventions(args, config2);
|
|
70863
|
+
result = await handleValidateConventions(args ?? {}, config2);
|
|
69790
70864
|
break;
|
|
69791
70865
|
case "check_migrations":
|
|
69792
|
-
result = await handleCheckMigrations(args, config2);
|
|
70866
|
+
result = await handleCheckMigrations(args ?? {}, config2);
|
|
69793
70867
|
break;
|
|
69794
70868
|
case "scaffold_extension":
|
|
69795
|
-
result = await handleScaffoldExtension(args, config2);
|
|
70869
|
+
result = await handleScaffoldExtension(args ?? {}, config2);
|
|
69796
70870
|
break;
|
|
69797
70871
|
case "api_docs":
|
|
69798
|
-
result = await handleApiDocs(args, config2);
|
|
70872
|
+
result = await handleApiDocs(args ?? {}, config2);
|
|
69799
70873
|
break;
|
|
69800
70874
|
case "suggest_migration":
|
|
69801
|
-
result = await handleSuggestMigration(args, config2);
|
|
70875
|
+
result = await handleSuggestMigration(args ?? {}, config2);
|
|
69802
70876
|
break;
|
|
69803
70877
|
case "generate_permissions":
|
|
69804
|
-
result = await handleGeneratePermissions(args, config2);
|
|
70878
|
+
result = await handleGeneratePermissions(args ?? {}, config2);
|
|
69805
70879
|
break;
|
|
69806
70880
|
// Test Tools
|
|
69807
70881
|
case "scaffold_tests":
|
|
69808
|
-
result = await handleScaffoldTests(args, config2);
|
|
70882
|
+
result = await handleScaffoldTests(args ?? {}, config2);
|
|
69809
70883
|
break;
|
|
69810
70884
|
case "analyze_test_coverage":
|
|
69811
|
-
result = await handleAnalyzeTestCoverage(args, config2);
|
|
70885
|
+
result = await handleAnalyzeTestCoverage(args ?? {}, config2);
|
|
69812
70886
|
break;
|
|
69813
70887
|
case "validate_test_conventions":
|
|
69814
|
-
result = await handleValidateTestConventions(args, config2);
|
|
70888
|
+
result = await handleValidateTestConventions(args ?? {}, config2);
|
|
69815
70889
|
break;
|
|
69816
70890
|
case "suggest_test_scenarios":
|
|
69817
|
-
result = await handleSuggestTestScenarios(args, config2);
|
|
70891
|
+
result = await handleSuggestTestScenarios(args ?? {}, config2);
|
|
70892
|
+
break;
|
|
70893
|
+
// Frontend Test Tools
|
|
70894
|
+
case "scaffold_frontend_tests":
|
|
70895
|
+
result = await handleScaffoldFrontendTests(args ?? {}, config2);
|
|
69818
70896
|
break;
|
|
69819
70897
|
// Frontend Route Tools
|
|
69820
70898
|
case "scaffold_api_client":
|
|
@@ -69958,6 +71036,7 @@ var init_server3 = __esm({
|
|
|
69958
71036
|
init_validate_frontend_routes();
|
|
69959
71037
|
init_scaffold_frontend_extension();
|
|
69960
71038
|
init_analyze_extension_points();
|
|
71039
|
+
init_scaffold_frontend_tests();
|
|
69961
71040
|
init_validate_security();
|
|
69962
71041
|
init_analyze_code_quality();
|
|
69963
71042
|
init_analyze_hierarchy_patterns();
|