@lensmcp/nx-plugin 1.0.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/LICENSE +202 -0
- package/README.md +141 -0
- package/executors/agent-build/agent-build.d.ts +21 -0
- package/executors/agent-build/agent-build.d.ts.map +1 -0
- package/executors/agent-build/agent-build.js +86 -0
- package/executors/agent-build/schema.d.ts +5 -0
- package/executors/agent-build/schema.json +25 -0
- package/executors/agent-dev/agent-dev.d.ts +24 -0
- package/executors/agent-dev/agent-dev.d.ts.map +1 -0
- package/executors/agent-dev/agent-dev.js +327 -0
- package/executors/agent-dev/schema.d.ts +9 -0
- package/executors/agent-dev/schema.json +41 -0
- package/executors/agent-verify/agent-verify.d.ts +22 -0
- package/executors/agent-verify/agent-verify.d.ts.map +1 -0
- package/executors/agent-verify/agent-verify.js +156 -0
- package/executors/agent-verify/schema.d.ts +7 -0
- package/executors/agent-verify/schema.json +23 -0
- package/executors.json +22 -0
- package/generators/init/init.d.ts +5 -0
- package/generators/init/init.d.ts.map +1 -0
- package/generators/init/init.js +167 -0
- package/generators/init/schema.d.ts +4 -0
- package/generators/init/schema.json +22 -0
- package/generators/setup-nest/schema.d.ts +4 -0
- package/generators/setup-nest/schema.json +18 -0
- package/generators/setup-nest/setup-nest.d.ts +41 -0
- package/generators/setup-nest/setup-nest.d.ts.map +1 -0
- package/generators/setup-nest/setup-nest.js +281 -0
- package/generators/setup-vite/schema.d.ts +4 -0
- package/generators/setup-vite/schema.json +18 -0
- package/generators/setup-vite/setup-vite.d.ts +31 -0
- package/generators/setup-vite/setup-vite.d.ts.map +1 -0
- package/generators/setup-vite/setup-vite.js +125 -0
- package/generators.json +22 -0
- package/index.d.ts +14 -0
- package/index.d.ts.map +1 -0
- package/index.js +21 -0
- package/package.json +47 -0
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.initGenerator = initGenerator;
|
|
4
|
+
const devkit_1 = require("@nx/devkit");
|
|
5
|
+
/** Workspace-wide config defaults, written into `nx.json#lensmcp`. */
|
|
6
|
+
const DEFAULT_LENSMCP_CONFIG = {
|
|
7
|
+
schemaVersion: 1,
|
|
8
|
+
storage: 'memory',
|
|
9
|
+
transport: 'stdio',
|
|
10
|
+
retention: {
|
|
11
|
+
sessions: 5,
|
|
12
|
+
events: 5000,
|
|
13
|
+
screenshots: 50,
|
|
14
|
+
buildReports: 20,
|
|
15
|
+
visualFrames: 200,
|
|
16
|
+
heapSnapshots: 3,
|
|
17
|
+
tracesWithErrors: 20,
|
|
18
|
+
verifications: 10,
|
|
19
|
+
},
|
|
20
|
+
redaction: {
|
|
21
|
+
headers: ['authorization', 'cookie', 'set-cookie', 'x-api-key'],
|
|
22
|
+
paths: ['password', 'token', 'secret', 'apiKey', 'api_key'],
|
|
23
|
+
},
|
|
24
|
+
channels: {
|
|
25
|
+
blockingStatusChanged: { enabled: true, minSeverity: 'error' },
|
|
26
|
+
memoryLeakSuspected: { enabled: true, minSeverity: 'warning' },
|
|
27
|
+
bundleRegression: { enabled: true, minSeverity: 'warning' },
|
|
28
|
+
visualViolationBlocking: { enabled: true, minSeverity: 'error' },
|
|
29
|
+
verificationCompleted: { enabled: true, minSeverity: 'info' },
|
|
30
|
+
},
|
|
31
|
+
// Tunable detection thresholds — when each signal fires. Edit to taste;
|
|
32
|
+
// the server reads these at boot (see @lensmcp/core resolveThresholds).
|
|
33
|
+
thresholds: {
|
|
34
|
+
dbInLoop: 5,
|
|
35
|
+
nPlusOne: 3,
|
|
36
|
+
renderStorm: 5,
|
|
37
|
+
slowRouteMs: 500,
|
|
38
|
+
slowRenderMs: 16,
|
|
39
|
+
},
|
|
40
|
+
};
|
|
41
|
+
const LENSMCP_GITIGNORE_MARKER = '# LensMCP runtime artifacts (per-session storage)';
|
|
42
|
+
const LENSMCP_GITIGNORE_ENTRY = '.lensmcp/';
|
|
43
|
+
async function initGenerator(tree, rawOptions = {}) {
|
|
44
|
+
const options = {
|
|
45
|
+
skipFormat: rawOptions.skipFormat ?? false,
|
|
46
|
+
registerHostConfig: rawOptions.registerHostConfig ?? 'auto',
|
|
47
|
+
};
|
|
48
|
+
const steps = [];
|
|
49
|
+
// 1. Register the plugin + lensmcp config in nx.json (idempotent).
|
|
50
|
+
if (tree.exists('nx.json')) {
|
|
51
|
+
(0, devkit_1.updateJson)(tree, 'nx.json', (nx) => {
|
|
52
|
+
nx.plugins = Array.isArray(nx.plugins) ? nx.plugins : [];
|
|
53
|
+
const alreadyRegistered = nx.plugins.some((p) => {
|
|
54
|
+
if (typeof p === 'string')
|
|
55
|
+
return p === '@lensmcp/nx-plugin';
|
|
56
|
+
if (p && typeof p === 'object' && 'plugin' in p) {
|
|
57
|
+
return p.plugin === '@lensmcp/nx-plugin';
|
|
58
|
+
}
|
|
59
|
+
return false;
|
|
60
|
+
});
|
|
61
|
+
if (!alreadyRegistered) {
|
|
62
|
+
nx.plugins.push('@lensmcp/nx-plugin');
|
|
63
|
+
steps.push('nx.json: registered @lensmcp/nx-plugin');
|
|
64
|
+
}
|
|
65
|
+
const existing = (nx.lensmcp ?? {});
|
|
66
|
+
nx.lensmcp = mergeDeep(DEFAULT_LENSMCP_CONFIG, existing);
|
|
67
|
+
if (!('lensmcp' in nx) || Object.keys(existing).length === 0) {
|
|
68
|
+
steps.push('nx.json: added workspace-wide `lensmcp` config block');
|
|
69
|
+
}
|
|
70
|
+
return nx;
|
|
71
|
+
});
|
|
72
|
+
}
|
|
73
|
+
else {
|
|
74
|
+
throw new Error('No nx.json found at the workspace root. Are you in a Nx workspace?');
|
|
75
|
+
}
|
|
76
|
+
// 2. .gitignore — add `.lensmcp/` line (idempotent).
|
|
77
|
+
const gitignorePath = '.gitignore';
|
|
78
|
+
const existing = tree.exists(gitignorePath) ? tree.read(gitignorePath, 'utf-8') ?? '' : '';
|
|
79
|
+
if (!existing.split('\n').some((l) => l.trim() === LENSMCP_GITIGNORE_ENTRY)) {
|
|
80
|
+
const trailingNewline = existing.endsWith('\n') ? '' : '\n';
|
|
81
|
+
tree.write(gitignorePath, `${existing}${trailingNewline}\n${LENSMCP_GITIGNORE_MARKER}\n${LENSMCP_GITIGNORE_ENTRY}\n`);
|
|
82
|
+
steps.push(`${gitignorePath}: added \`${LENSMCP_GITIGNORE_ENTRY}\``);
|
|
83
|
+
}
|
|
84
|
+
// 3. Reserve the .lensmcp/ directory with a sentinel file.
|
|
85
|
+
const keepPath = (0, devkit_1.joinPathFragments)('.lensmcp', '.keep');
|
|
86
|
+
if (!tree.exists(keepPath)) {
|
|
87
|
+
tree.write(keepPath, '');
|
|
88
|
+
steps.push(`${keepPath}: created`);
|
|
89
|
+
}
|
|
90
|
+
// 4. Write an install trail so subsequent re-runs can read what was
|
|
91
|
+
// done and (in future generators) un-install precisely.
|
|
92
|
+
const trailPath = (0, devkit_1.joinPathFragments)('.lensmcp', 'install-trail.json');
|
|
93
|
+
const existingTrail = tree.exists(trailPath)
|
|
94
|
+
? JSON.parse(tree.read(trailPath, 'utf-8') ?? '{}')
|
|
95
|
+
: undefined;
|
|
96
|
+
const trail = existingTrail ?? {
|
|
97
|
+
schemaVersion: 1,
|
|
98
|
+
installedAt: new Date().toISOString(),
|
|
99
|
+
steps: [],
|
|
100
|
+
};
|
|
101
|
+
trail.steps.push(...steps);
|
|
102
|
+
tree.write(trailPath, JSON.stringify(trail, null, 2) + '\n');
|
|
103
|
+
// 5. Register the LensMCP MCP server in the workspace `.mcp.json` so any
|
|
104
|
+
// coding agent (Claude Code, Cursor, …) opening this project gets the
|
|
105
|
+
// lens automatically. Workspace-local edits happen here, in the Nx
|
|
106
|
+
// tree (idempotent). Global agent configs (~/.claude, ~/.cursor) are
|
|
107
|
+
// only touched by `lensmcp` when `registerHostConfig: 'always'`,
|
|
108
|
+
// since those live outside the workspace.
|
|
109
|
+
if (options.registerHostConfig !== 'never') {
|
|
110
|
+
registerWorkspaceMcpConfig(tree, steps);
|
|
111
|
+
}
|
|
112
|
+
if (!options.skipFormat) {
|
|
113
|
+
await (0, devkit_1.formatFiles)(tree);
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
/** The stdio MCP server entry a host agent uses to launch LensMCP. */
|
|
117
|
+
const LENSMCP_MCP_SERVER_ENTRY = {
|
|
118
|
+
command: 'npx',
|
|
119
|
+
args: ['-y', 'lensmcp', 'mcp'],
|
|
120
|
+
};
|
|
121
|
+
/**
|
|
122
|
+
* Ensure `<workspace>/.mcp.json` has a `lensmcp` MCP server entry.
|
|
123
|
+
* Idempotent: creates the file if absent, merges into existing
|
|
124
|
+
* `mcpServers`, and never clobbers a `lensmcp` entry the user already
|
|
125
|
+
* customised.
|
|
126
|
+
*/
|
|
127
|
+
function registerWorkspaceMcpConfig(tree, steps) {
|
|
128
|
+
const path = '.mcp.json';
|
|
129
|
+
let config = {};
|
|
130
|
+
if (tree.exists(path)) {
|
|
131
|
+
try {
|
|
132
|
+
config = JSON.parse(tree.read(path, 'utf-8') ?? '{}');
|
|
133
|
+
}
|
|
134
|
+
catch {
|
|
135
|
+
config = {};
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
if (!config || typeof config !== 'object')
|
|
139
|
+
config = {};
|
|
140
|
+
if (!config.mcpServers || typeof config.mcpServers !== 'object') {
|
|
141
|
+
config.mcpServers = {};
|
|
142
|
+
}
|
|
143
|
+
if (config.mcpServers['lensmcp'])
|
|
144
|
+
return; // already registered — leave it
|
|
145
|
+
config.mcpServers['lensmcp'] = { ...LENSMCP_MCP_SERVER_ENTRY };
|
|
146
|
+
tree.write(path, JSON.stringify(config, null, 2) + '\n');
|
|
147
|
+
steps.push('.mcp.json: registered lensmcp MCP server (stdio)');
|
|
148
|
+
}
|
|
149
|
+
exports.default = initGenerator;
|
|
150
|
+
// -------- helpers --------
|
|
151
|
+
function mergeDeep(defaults, overrides) {
|
|
152
|
+
const out = { ...defaults };
|
|
153
|
+
for (const [key, value] of Object.entries(overrides)) {
|
|
154
|
+
if (value !== null &&
|
|
155
|
+
typeof value === 'object' &&
|
|
156
|
+
!Array.isArray(value) &&
|
|
157
|
+
out[key] !== null &&
|
|
158
|
+
typeof out[key] === 'object' &&
|
|
159
|
+
!Array.isArray(out[key])) {
|
|
160
|
+
out[key] = mergeDeep(out[key], value);
|
|
161
|
+
}
|
|
162
|
+
else {
|
|
163
|
+
out[key] = value;
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
return out;
|
|
167
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "http://json-schema.org/schema",
|
|
3
|
+
"$id": "LensmcpInit",
|
|
4
|
+
"title": "Install LensMCP",
|
|
5
|
+
"description": "Wires LensMCP into a host Nx workspace: registers the plugin in nx.json, adds the workspace-wide `lensmcp` config block, patches .gitignore, and prepares the .lensmcp runtime directory.",
|
|
6
|
+
"type": "object",
|
|
7
|
+
"additionalProperties": false,
|
|
8
|
+
"properties": {
|
|
9
|
+
"skipFormat": {
|
|
10
|
+
"type": "boolean",
|
|
11
|
+
"default": false,
|
|
12
|
+
"description": "Skip Prettier formatting of touched files."
|
|
13
|
+
},
|
|
14
|
+
"registerHostConfig": {
|
|
15
|
+
"type": "string",
|
|
16
|
+
"enum": ["auto", "never", "always"],
|
|
17
|
+
"default": "auto",
|
|
18
|
+
"description": "Whether to also append a `lensmcp` entry to the host agent's MCP client config (~/.claude/mcp.json, ~/.cursor/mcp.json, <workspace>/.mcp.json). `auto` patches existing files only; never modifies anything outside the workspace by default."
|
|
19
|
+
}
|
|
20
|
+
},
|
|
21
|
+
"required": []
|
|
22
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "http://json-schema.org/schema",
|
|
3
|
+
"$id": "LensmcpSetupNest",
|
|
4
|
+
"title": "setup-nest",
|
|
5
|
+
"description": "Wire LensMCP into a NestJS project: add LensmcpModule.forRoot() to AppModule + add agent-dev target.",
|
|
6
|
+
"type": "object",
|
|
7
|
+
"additionalProperties": false,
|
|
8
|
+
"properties": {
|
|
9
|
+
"project": {
|
|
10
|
+
"type": "string",
|
|
11
|
+
"description": "Target Nx project (must have an app.module.ts and main.ts).",
|
|
12
|
+
"x-prompt": "Which NestJS project?",
|
|
13
|
+
"x-priority": "important"
|
|
14
|
+
},
|
|
15
|
+
"skipFormat": { "type": "boolean", "default": false }
|
|
16
|
+
},
|
|
17
|
+
"required": ["project"]
|
|
18
|
+
}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { type Tree } from '@nx/devkit';
|
|
2
|
+
import type { SetupNestGeneratorSchema } from './schema';
|
|
3
|
+
/**
|
|
4
|
+
* Wires LensMCP into a host NestJS project with **no app-code edits**.
|
|
5
|
+
* Idempotent.
|
|
6
|
+
*
|
|
7
|
+
* Zero-config strategy (Phase 8): rewrite the bootstrap in `src/main.ts`
|
|
8
|
+
* so `NestFactory.create(AppModule, opts)` becomes
|
|
9
|
+
* `createLensmcpNestApp(AppModule, { projectName: '<project>', nestOptions: opts })`.
|
|
10
|
+
* `createLensmcpNestApp` wires `LensmcpModule` + the provider tracker +
|
|
11
|
+
* auto-instruments every provider's methods under the hood, so the app
|
|
12
|
+
* module and the providers stay untouched.
|
|
13
|
+
*
|
|
14
|
+
* 1. Find the project's `src/main.ts` (the conventional Nest entry).
|
|
15
|
+
* 2. Replace the `NestFactory.create(...)` call with
|
|
16
|
+
* `createLensmcpNestApp(...)`, threading the original 2nd arg through
|
|
17
|
+
* as `nestOptions` and adding the `@lensmcp/nest-instrumentation`
|
|
18
|
+
* import (dropping the now-unused `NestFactory` import when nothing
|
|
19
|
+
* else uses it). String/AST-lite — if the file doesn't follow the
|
|
20
|
+
* canonical shape we print a precise hint and exit non-zero.
|
|
21
|
+
* 3. Add an `agent-dev` Nx target.
|
|
22
|
+
*/
|
|
23
|
+
export declare function setupNestGenerator(tree: Tree, rawOptions: SetupNestGeneratorSchema): Promise<void>;
|
|
24
|
+
export default setupNestGenerator;
|
|
25
|
+
export declare function findAppModule(tree: Tree, root: string): string | undefined;
|
|
26
|
+
/**
|
|
27
|
+
* Rewrite a Nest `main.ts` bootstrap to the zero-config form. Returns the
|
|
28
|
+
* original string unchanged when already converted (idempotent), `null`
|
|
29
|
+
* when no `NestFactory.create(...)` call is present (caller decides), or
|
|
30
|
+
* the rewritten source otherwise.
|
|
31
|
+
*/
|
|
32
|
+
export declare function patchMainBootstrap(src: string, projectName: string): string | null;
|
|
33
|
+
/**
|
|
34
|
+
* Legacy module-style wiring. Adds `LensmcpModule.forRoot(...)` into the
|
|
35
|
+
* `@Module({ imports: [...] })` array. Superseded by the `main.ts`
|
|
36
|
+
* bootstrap rewrite ({@link patchMainBootstrap}) but kept for hosts whose
|
|
37
|
+
* entry file doesn't follow the canonical `NestFactory.create` shape.
|
|
38
|
+
* Idempotent. Returns `null` when no `@Module` imports array is found.
|
|
39
|
+
*/
|
|
40
|
+
export declare function patchAppModule(src: string, projectName: string): string | null;
|
|
41
|
+
//# sourceMappingURL=setup-nest.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"setup-nest.d.ts","sourceRoot":"","sources":["../../../src/generators/setup-nest/setup-nest.ts"],"names":[],"mappings":"AAAA,OAAO,EAIL,KAAK,IAAI,EAEV,MAAM,YAAY,CAAC;AACpB,OAAO,KAAK,EAAE,wBAAwB,EAAE,MAAM,UAAU,CAAC;AAOzD;;;;;;;;;;;;;;;;;;;GAmBG;AACH,wBAAsB,kBAAkB,CACtC,IAAI,EAAE,IAAI,EACV,UAAU,EAAE,wBAAwB,GACnC,OAAO,CAAC,IAAI,CAAC,CA+Cf;AAED,eAAe,kBAAkB,CAAC;AAYlC,wBAAgB,aAAa,CAAC,IAAI,EAAE,IAAI,EAAE,IAAI,EAAE,MAAM,GAAG,MAAM,GAAG,SAAS,CAM1E;AAID;;;;;GAKG;AACH,wBAAgB,kBAAkB,CAChC,GAAG,EAAE,MAAM,EACX,WAAW,EAAE,MAAM,GAClB,MAAM,GAAG,IAAI,CA0Bf;AAgID;;;;;;GAMG;AACH,wBAAgB,cAAc,CAAC,GAAG,EAAE,MAAM,EAAE,WAAW,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAiC9E"}
|
|
@@ -0,0 +1,281 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.setupNestGenerator = setupNestGenerator;
|
|
4
|
+
exports.findAppModule = findAppModule;
|
|
5
|
+
exports.patchMainBootstrap = patchMainBootstrap;
|
|
6
|
+
exports.patchAppModule = patchAppModule;
|
|
7
|
+
const devkit_1 = require("@nx/devkit");
|
|
8
|
+
const BOOTSTRAP_IMPORT = `import { createLensmcpNestApp } from '@lensmcp/nest-instrumentation';`;
|
|
9
|
+
// Legacy module-style wiring, kept for the manual-fallback hint.
|
|
10
|
+
const IMPORT_LINE = `import { LensmcpModule } from '@lensmcp/nest-instrumentation';`;
|
|
11
|
+
const MODULE_CALL = "LensmcpModule.forRoot({ projectName: '__PROJECT__' })";
|
|
12
|
+
/**
|
|
13
|
+
* Wires LensMCP into a host NestJS project with **no app-code edits**.
|
|
14
|
+
* Idempotent.
|
|
15
|
+
*
|
|
16
|
+
* Zero-config strategy (Phase 8): rewrite the bootstrap in `src/main.ts`
|
|
17
|
+
* so `NestFactory.create(AppModule, opts)` becomes
|
|
18
|
+
* `createLensmcpNestApp(AppModule, { projectName: '<project>', nestOptions: opts })`.
|
|
19
|
+
* `createLensmcpNestApp` wires `LensmcpModule` + the provider tracker +
|
|
20
|
+
* auto-instruments every provider's methods under the hood, so the app
|
|
21
|
+
* module and the providers stay untouched.
|
|
22
|
+
*
|
|
23
|
+
* 1. Find the project's `src/main.ts` (the conventional Nest entry).
|
|
24
|
+
* 2. Replace the `NestFactory.create(...)` call with
|
|
25
|
+
* `createLensmcpNestApp(...)`, threading the original 2nd arg through
|
|
26
|
+
* as `nestOptions` and adding the `@lensmcp/nest-instrumentation`
|
|
27
|
+
* import (dropping the now-unused `NestFactory` import when nothing
|
|
28
|
+
* else uses it). String/AST-lite — if the file doesn't follow the
|
|
29
|
+
* canonical shape we print a precise hint and exit non-zero.
|
|
30
|
+
* 3. Add an `agent-dev` Nx target.
|
|
31
|
+
*/
|
|
32
|
+
async function setupNestGenerator(tree, rawOptions) {
|
|
33
|
+
const options = {
|
|
34
|
+
project: rawOptions.project,
|
|
35
|
+
skipFormat: rawOptions.skipFormat ?? false,
|
|
36
|
+
};
|
|
37
|
+
const project = (0, devkit_1.readProjectConfiguration)(tree, options.project);
|
|
38
|
+
const mainPath = findMain(tree, project.root);
|
|
39
|
+
if (!mainPath) {
|
|
40
|
+
throw new Error(`setup-nest: no main.ts found under ${project.root}/src.`);
|
|
41
|
+
}
|
|
42
|
+
const original = tree.read(mainPath, 'utf-8') ?? '';
|
|
43
|
+
const patched = patchMainBootstrap(original, options.project);
|
|
44
|
+
if (patched === null) {
|
|
45
|
+
throw new Error(`setup-nest: could not safely patch ${mainPath}.\n` +
|
|
46
|
+
`Expected a \`NestFactory.create(AppModule)\` bootstrap call. ` +
|
|
47
|
+
`Edit manually: replace it with \`createLensmcpNestApp(AppModule, ` +
|
|
48
|
+
`{ projectName: '${options.project}' })\` and import it from ` +
|
|
49
|
+
`'@lensmcp/nest-instrumentation'.\n` +
|
|
50
|
+
`(Or use the module form: add \`${IMPORT_LINE}\` and push ` +
|
|
51
|
+
`${MODULE_CALL.replace('__PROJECT__', options.project)} into the ` +
|
|
52
|
+
`@Module imports array.)`);
|
|
53
|
+
}
|
|
54
|
+
if (patched !== original) {
|
|
55
|
+
tree.write(mainPath, patched);
|
|
56
|
+
}
|
|
57
|
+
const targets = { ...(project.targets ?? {}) };
|
|
58
|
+
if (!targets['agent-dev']) {
|
|
59
|
+
targets['agent-dev'] = {
|
|
60
|
+
executor: '@lensmcp/nx-plugin:agent-dev',
|
|
61
|
+
options: {
|
|
62
|
+
kind: 'nestjs',
|
|
63
|
+
},
|
|
64
|
+
};
|
|
65
|
+
project.targets = targets;
|
|
66
|
+
(0, devkit_1.updateProjectConfiguration)(tree, options.project, project);
|
|
67
|
+
}
|
|
68
|
+
if (!options.skipFormat) {
|
|
69
|
+
await (0, devkit_1.formatFiles)(tree);
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
exports.default = setupNestGenerator;
|
|
73
|
+
// ---------- helpers ----------
|
|
74
|
+
function findMain(tree, root) {
|
|
75
|
+
for (const name of ['src/main.ts', 'main.ts', 'src/index.ts']) {
|
|
76
|
+
const p = (0, devkit_1.joinPathFragments)(root, name);
|
|
77
|
+
if (tree.exists(p))
|
|
78
|
+
return p;
|
|
79
|
+
}
|
|
80
|
+
return undefined;
|
|
81
|
+
}
|
|
82
|
+
function findAppModule(tree, root) {
|
|
83
|
+
for (const name of ['src/app.module.ts', 'app.module.ts']) {
|
|
84
|
+
const p = (0, devkit_1.joinPathFragments)(root, name);
|
|
85
|
+
if (tree.exists(p))
|
|
86
|
+
return p;
|
|
87
|
+
}
|
|
88
|
+
return undefined;
|
|
89
|
+
}
|
|
90
|
+
const CREATE_CALL_RE = /NestFactory\s*\.\s*create\s*(?:<[\s\S]*?>)?\s*\(/;
|
|
91
|
+
/**
|
|
92
|
+
* Rewrite a Nest `main.ts` bootstrap to the zero-config form. Returns the
|
|
93
|
+
* original string unchanged when already converted (idempotent), `null`
|
|
94
|
+
* when no `NestFactory.create(...)` call is present (caller decides), or
|
|
95
|
+
* the rewritten source otherwise.
|
|
96
|
+
*/
|
|
97
|
+
function patchMainBootstrap(src, projectName) {
|
|
98
|
+
// Already zero-config — nothing to do.
|
|
99
|
+
if (/createLensmcpNestApp\s*\(/.test(src))
|
|
100
|
+
return src;
|
|
101
|
+
const m = CREATE_CALL_RE.exec(src);
|
|
102
|
+
if (!m)
|
|
103
|
+
return null;
|
|
104
|
+
const openParen = m.index + m[0].length - 1; // index of the '('
|
|
105
|
+
const closeParen = matchingParen(src, openParen);
|
|
106
|
+
if (closeParen === -1)
|
|
107
|
+
return null;
|
|
108
|
+
const argsStr = src.slice(openParen + 1, closeParen);
|
|
109
|
+
const { moduleArg, restArg } = splitTopLevelArgs(argsStr);
|
|
110
|
+
if (!moduleArg.trim())
|
|
111
|
+
return null;
|
|
112
|
+
const proj = projectName.replace(/\\/g, '\\\\').replace(/'/g, "\\'");
|
|
113
|
+
const rest = restArg.trim();
|
|
114
|
+
const optsObject = rest
|
|
115
|
+
? `{ projectName: '${proj}', nestOptions: ${rest} }`
|
|
116
|
+
: `{ projectName: '${proj}' }`;
|
|
117
|
+
const replacement = `createLensmcpNestApp(${moduleArg.trim()}, ${optsObject})`;
|
|
118
|
+
let out = src.slice(0, m.index) + replacement + src.slice(closeParen + 1);
|
|
119
|
+
out = addBootstrapImport(out);
|
|
120
|
+
out = dropUnusedNestFactoryImport(out);
|
|
121
|
+
return out;
|
|
122
|
+
}
|
|
123
|
+
/** Insert the `createLensmcpNestApp` import after the last import, unless
|
|
124
|
+
* the symbol is already imported from `@lensmcp/nest-instrumentation`. */
|
|
125
|
+
function addBootstrapImport(src) {
|
|
126
|
+
const already = /createLensmcpNestApp[\s\S]*?from\s*['"]@lensmcp\/nest-instrumentation['"]/.test(src);
|
|
127
|
+
if (already)
|
|
128
|
+
return src;
|
|
129
|
+
const importLines = [...src.matchAll(/^\s*import .+;?\s*$/gm)];
|
|
130
|
+
const last = importLines.length ? importLines[importLines.length - 1] : null;
|
|
131
|
+
if (last && last.index !== undefined) {
|
|
132
|
+
const at = last.index + last[0].length;
|
|
133
|
+
return src.slice(0, at) + `\n${BOOTSTRAP_IMPORT}` + src.slice(at);
|
|
134
|
+
}
|
|
135
|
+
return `${BOOTSTRAP_IMPORT}\n${src}`;
|
|
136
|
+
}
|
|
137
|
+
/** Strip `NestFactory` from its `@nestjs/core` named import when nothing
|
|
138
|
+
* in the file references `NestFactory` any more. */
|
|
139
|
+
function dropUnusedNestFactoryImport(src) {
|
|
140
|
+
// Reference check excludes the import statement itself.
|
|
141
|
+
if (/\bNestFactory\b/.test(stripNestFactoryImportSpan(src).rest))
|
|
142
|
+
return src;
|
|
143
|
+
const { match } = stripNestFactoryImportSpan(src);
|
|
144
|
+
if (!match)
|
|
145
|
+
return src;
|
|
146
|
+
const names = match.names.filter((n) => n !== 'NestFactory');
|
|
147
|
+
if (names.length === 0) {
|
|
148
|
+
// Remove the whole import statement (and its trailing newline).
|
|
149
|
+
return src.slice(0, match.start) + src.slice(match.end).replace(/^\n/, '');
|
|
150
|
+
}
|
|
151
|
+
const rebuilt = `import { ${names.join(', ')} } from '@nestjs/core';`;
|
|
152
|
+
return src.slice(0, match.start) + rebuilt + src.slice(match.end);
|
|
153
|
+
}
|
|
154
|
+
/** Locate the `@nestjs/core` named import; return its span + names and the
|
|
155
|
+
* source with that span removed (so the caller can test references that
|
|
156
|
+
* live *outside* the import). */
|
|
157
|
+
function stripNestFactoryImportSpan(src) {
|
|
158
|
+
const re = /import\s*\{([^}]*)\}\s*from\s*['"]@nestjs\/core['"];?/;
|
|
159
|
+
const m = re.exec(src);
|
|
160
|
+
if (!m)
|
|
161
|
+
return { match: null, rest: src };
|
|
162
|
+
const names = m[1]
|
|
163
|
+
.split(',')
|
|
164
|
+
.map((s) => s.trim())
|
|
165
|
+
.filter(Boolean);
|
|
166
|
+
const start = m.index;
|
|
167
|
+
const end = m.index + m[0].length;
|
|
168
|
+
const rest = src.slice(0, start) + src.slice(end);
|
|
169
|
+
return { match: { start, end, names }, rest };
|
|
170
|
+
}
|
|
171
|
+
/** Index of the `)` matching the `(` at `openIdx`, skipping strings,
|
|
172
|
+
* template literals and comments. Returns -1 if unbalanced. */
|
|
173
|
+
function matchingParen(src, openIdx) {
|
|
174
|
+
let depth = 0;
|
|
175
|
+
for (let i = openIdx; i < src.length; i++) {
|
|
176
|
+
const skip = skipNonCode(src, i);
|
|
177
|
+
if (skip > i) {
|
|
178
|
+
i = skip - 1;
|
|
179
|
+
continue;
|
|
180
|
+
}
|
|
181
|
+
const ch = src[i];
|
|
182
|
+
if (ch === '(')
|
|
183
|
+
depth++;
|
|
184
|
+
else if (ch === ')') {
|
|
185
|
+
depth--;
|
|
186
|
+
if (depth === 0)
|
|
187
|
+
return i;
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
return -1;
|
|
191
|
+
}
|
|
192
|
+
/** Split call arguments at the first top-level comma. */
|
|
193
|
+
function splitTopLevelArgs(argsStr) {
|
|
194
|
+
let depth = 0;
|
|
195
|
+
for (let i = 0; i < argsStr.length; i++) {
|
|
196
|
+
const skip = skipNonCode(argsStr, i);
|
|
197
|
+
if (skip > i) {
|
|
198
|
+
i = skip - 1;
|
|
199
|
+
continue;
|
|
200
|
+
}
|
|
201
|
+
const ch = argsStr[i];
|
|
202
|
+
if (ch === '(' || ch === '[' || ch === '{')
|
|
203
|
+
depth++;
|
|
204
|
+
else if (ch === ')' || ch === ']' || ch === '}')
|
|
205
|
+
depth--;
|
|
206
|
+
else if (ch === ',' && depth === 0) {
|
|
207
|
+
return {
|
|
208
|
+
moduleArg: argsStr.slice(0, i),
|
|
209
|
+
restArg: argsStr.slice(i + 1),
|
|
210
|
+
};
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
return { moduleArg: argsStr, restArg: '' };
|
|
214
|
+
}
|
|
215
|
+
/** If position `i` starts a string/template/comment, return the index just
|
|
216
|
+
* past it; otherwise return `i`. */
|
|
217
|
+
function skipNonCode(src, i) {
|
|
218
|
+
const ch = src[i];
|
|
219
|
+
if (ch === '"' || ch === "'" || ch === '`') {
|
|
220
|
+
for (let j = i + 1; j < src.length; j++) {
|
|
221
|
+
if (src[j] === '\\') {
|
|
222
|
+
j++;
|
|
223
|
+
continue;
|
|
224
|
+
}
|
|
225
|
+
if (src[j] === ch)
|
|
226
|
+
return j + 1;
|
|
227
|
+
}
|
|
228
|
+
return src.length;
|
|
229
|
+
}
|
|
230
|
+
if (ch === '/' && src[i + 1] === '/') {
|
|
231
|
+
const nl = src.indexOf('\n', i);
|
|
232
|
+
return nl === -1 ? src.length : nl;
|
|
233
|
+
}
|
|
234
|
+
if (ch === '/' && src[i + 1] === '*') {
|
|
235
|
+
const end = src.indexOf('*/', i + 2);
|
|
236
|
+
return end === -1 ? src.length : end + 2;
|
|
237
|
+
}
|
|
238
|
+
return i;
|
|
239
|
+
}
|
|
240
|
+
/**
|
|
241
|
+
* Legacy module-style wiring. Adds `LensmcpModule.forRoot(...)` into the
|
|
242
|
+
* `@Module({ imports: [...] })` array. Superseded by the `main.ts`
|
|
243
|
+
* bootstrap rewrite ({@link patchMainBootstrap}) but kept for hosts whose
|
|
244
|
+
* entry file doesn't follow the canonical `NestFactory.create` shape.
|
|
245
|
+
* Idempotent. Returns `null` when no `@Module` imports array is found.
|
|
246
|
+
*/
|
|
247
|
+
function patchAppModule(src, projectName) {
|
|
248
|
+
const hasImport = src.includes("from '@lensmcp/nest-instrumentation'") ||
|
|
249
|
+
src.includes('from "@lensmcp/nest-instrumentation"');
|
|
250
|
+
const hasModuleCall = /LensmcpModule\s*\.\s*forRoot\s*\(/.test(src);
|
|
251
|
+
if (hasImport && hasModuleCall)
|
|
252
|
+
return src;
|
|
253
|
+
const importsAnchor = src.indexOf('imports:');
|
|
254
|
+
if (importsAnchor === -1)
|
|
255
|
+
return null;
|
|
256
|
+
const bracketStart = src.indexOf('[', importsAnchor);
|
|
257
|
+
if (bracketStart === -1)
|
|
258
|
+
return null;
|
|
259
|
+
let withImport = src;
|
|
260
|
+
if (!hasImport) {
|
|
261
|
+
const importLines = [...src.matchAll(/^\s*import .+;?\s*$/gm)];
|
|
262
|
+
const lastImport = importLines.length > 0 ? importLines[importLines.length - 1] : null;
|
|
263
|
+
if (lastImport && lastImport.index !== undefined) {
|
|
264
|
+
const insertAt = lastImport.index + lastImport[0].length;
|
|
265
|
+
withImport = src.slice(0, insertAt) + `\n${IMPORT_LINE}` + src.slice(insertAt);
|
|
266
|
+
}
|
|
267
|
+
else {
|
|
268
|
+
withImport = `${IMPORT_LINE}\n` + src;
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
if (hasModuleCall)
|
|
272
|
+
return withImport;
|
|
273
|
+
const adjBracket = withImport.indexOf('[', withImport.indexOf('imports:'));
|
|
274
|
+
const before = withImport.slice(0, adjBracket + 1);
|
|
275
|
+
const after = withImport.slice(adjBracket + 1);
|
|
276
|
+
const trimmedAfter = after.replace(/^\s*/, '');
|
|
277
|
+
const startsClosed = trimmedAfter.startsWith(']');
|
|
278
|
+
const separator = startsClosed ? '' : ', ';
|
|
279
|
+
const call = MODULE_CALL.replace('__PROJECT__', projectName);
|
|
280
|
+
return `${before}${call}${separator}${after}`;
|
|
281
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "http://json-schema.org/schema",
|
|
3
|
+
"$id": "LensmcpSetupVite",
|
|
4
|
+
"title": "setup-vite",
|
|
5
|
+
"description": "Wire LensMCP into a Vite project: add @lensmcp/vite-plugin to vite.config.ts, add the agent-dev Nx target.",
|
|
6
|
+
"type": "object",
|
|
7
|
+
"additionalProperties": false,
|
|
8
|
+
"properties": {
|
|
9
|
+
"project": {
|
|
10
|
+
"type": "string",
|
|
11
|
+
"description": "Target Nx project (must already exist and have a vite.config.ts).",
|
|
12
|
+
"x-prompt": "Which Vite project?",
|
|
13
|
+
"x-priority": "important"
|
|
14
|
+
},
|
|
15
|
+
"skipFormat": { "type": "boolean", "default": false }
|
|
16
|
+
},
|
|
17
|
+
"required": ["project"]
|
|
18
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { type Tree } from '@nx/devkit';
|
|
2
|
+
import type { SetupViteGeneratorSchema } from './schema';
|
|
3
|
+
/**
|
|
4
|
+
* Wires LensMCP into a host Vite project. Idempotent on every step.
|
|
5
|
+
*
|
|
6
|
+
* 1. Locate the project's vite.config.{ts,mts,js,mjs,cts,cjs}.
|
|
7
|
+
* 2. Add `import { lensmcpVitePlugin } from '@lensmcp/vite-plugin'`
|
|
8
|
+
* near the top (after the last existing import).
|
|
9
|
+
* 3. Insert `lensmcpVitePlugin({ enabled: mode !== 'production' })`
|
|
10
|
+
* into the `plugins` array if not already present. We do *not*
|
|
11
|
+
* AST-edit — we use string heuristics on the standard
|
|
12
|
+
* `defineConfig({ plugins: [...] })` shape. If the file is too
|
|
13
|
+
* custom for the heuristics, we print a diff hint and bail.
|
|
14
|
+
* 4. Add Nx targets: agent-dev, agent-build, agent-verify.
|
|
15
|
+
*/
|
|
16
|
+
export declare function setupViteGenerator(tree: Tree, rawOptions: SetupViteGeneratorSchema): Promise<void>;
|
|
17
|
+
export default setupViteGenerator;
|
|
18
|
+
/**
|
|
19
|
+
* String-heuristic patch:
|
|
20
|
+
*
|
|
21
|
+
* 1. If the file already imports `@lensmcp/vite-plugin` and mentions
|
|
22
|
+
* `lensmcpVitePlugin(` in the plugins array, return unchanged.
|
|
23
|
+
* 2. Otherwise insert the import near the top (after the last
|
|
24
|
+
* `import …` line) and add the plugin call to the plugins array
|
|
25
|
+
* declared by `plugins: [` (first occurrence).
|
|
26
|
+
*
|
|
27
|
+
* If neither anchor is found, return `null` so the generator can ask
|
|
28
|
+
* the user to patch manually.
|
|
29
|
+
*/
|
|
30
|
+
export declare function patchViteConfig(src: string): string | null;
|
|
31
|
+
//# sourceMappingURL=setup-vite.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"setup-vite.d.ts","sourceRoot":"","sources":["../../../src/generators/setup-vite/setup-vite.ts"],"names":[],"mappings":"AAAA,OAAO,EAIL,KAAK,IAAI,EAEV,MAAM,YAAY,CAAC;AACpB,OAAO,KAAK,EAAE,wBAAwB,EAAE,MAAM,UAAU,CAAC;AAIzD;;;;;;;;;;;;GAYG;AACH,wBAAsB,kBAAkB,CACtC,IAAI,EAAE,IAAI,EACV,UAAU,EAAE,wBAAwB,GACnC,OAAO,CAAC,IAAI,CAAC,CA8Cf;AAED,eAAe,kBAAkB,CAAC;AAoBlC;;;;;;;;;;;GAWG;AACH,wBAAgB,eAAe,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAoC1D"}
|