@maxisoft/instantcms-mcp 1.2.3
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 +21 -0
- package/README.md +308 -0
- package/dist/__tests__/artifact-tool.test.js +196 -0
- package/dist/__tests__/db-tool.test.js +152 -0
- package/dist/__tests__/define-tool.test.js +106 -0
- package/dist/__tests__/find-tool.test.js +185 -0
- package/dist/__tests__/get-component-api.test.js +144 -0
- package/dist/__tests__/hardening.test.js +73 -0
- package/dist/__tests__/hooks-tool.test.js +173 -0
- package/dist/__tests__/mcp-integration-extra.test.js +166 -0
- package/dist/__tests__/mcp-integration.test.js +53 -0
- package/dist/__tests__/pagination.test.js +114 -0
- package/dist/__tests__/performance-baseline.test.js +56 -0
- package/dist/__tests__/project-patch.test.js +144 -0
- package/dist/__tests__/project-source.test.js +44 -0
- package/dist/__tests__/project-workflows-extra.test.js +165 -0
- package/dist/__tests__/project-workflows.test.js +51 -0
- package/dist/__tests__/scaffold-addon-roundtrip.test.js +287 -0
- package/dist/__tests__/serialization.test.js +170 -0
- package/dist/__tests__/source-knowledge-parsers.test.js +42 -0
- package/dist/__tests__/template-development.test.js +78 -0
- package/dist/__tests__/template-productivity.test.js +87 -0
- package/dist/__tests__/tools.test.js +2143 -0
- package/dist/data/components.js +2818 -0
- package/dist/data/controllers-map.js +7571 -0
- package/dist/data/core-api.js +9356 -0
- package/dist/data/database-schema.js +4555 -0
- package/dist/data/events-map.js +1339 -0
- package/dist/data/fields-map.js +3692 -0
- package/dist/data/hooks.js +2257 -0
- package/dist/data/js-api.js +123 -0
- package/dist/data/libs-api.js +264 -0
- package/dist/data/routes-map.js +203 -0
- package/dist/data/schemas-validation.js +255 -0
- package/dist/data/schemas.js +1404 -0
- package/dist/data/traits-map.js +1004 -0
- package/dist/data/version-profiles.js +41 -0
- package/dist/data/widgets-map.js +131 -0
- package/dist/data/wysiwyg-map.js +486 -0
- package/dist/generated/components-source.js +14748 -0
- package/dist/generated/hooks-source.js +3399 -0
- package/dist/generated/knowledge-meta.js +101 -0
- package/dist/index.js +13 -0
- package/dist/registry/database-tools.js +112 -0
- package/dist/registry/extension-tools.js +473 -0
- package/dist/registry/generator-tools.js +506 -0
- package/dist/registry/knowledge-tools.js +441 -0
- package/dist/registry/language-tools.js +99 -0
- package/dist/registry/meta-tools.js +152 -0
- package/dist/registry/project-tools.js +48 -0
- package/dist/registry/resources.js +98 -0
- package/dist/registry/source-tools.js +211 -0
- package/dist/registry/template-development-tools.js +79 -0
- package/dist/server.js +32 -0
- package/dist/tools/addon-tool.js +1437 -0
- package/dist/tools/admin-partial-tool.js +498 -0
- package/dist/tools/api-tool.js +294 -0
- package/dist/tools/artifact-tool.js +95 -0
- package/dist/tools/cache-tool.js +360 -0
- package/dist/tools/component-tool.js +415 -0
- package/dist/tools/controllers-tool.js +125 -0
- package/dist/tools/cron-tool.js +197 -0
- package/dist/tools/crud-tool.js +659 -0
- package/dist/tools/db-tool.js +198 -0
- package/dist/tools/email-tool.js +222 -0
- package/dist/tools/external-api-tool.js +596 -0
- package/dist/tools/filter-tool.js +341 -0
- package/dist/tools/form-tool.js +275 -0
- package/dist/tools/grid-tool.js +189 -0
- package/dist/tools/hooks-tool.js +104 -0
- package/dist/tools/import-export-tool.js +548 -0
- package/dist/tools/lang-tool.js +251 -0
- package/dist/tools/layout-override-tool.js +125 -0
- package/dist/tools/layout-tool.js +548 -0
- package/dist/tools/maria-tool.js +127 -0
- package/dist/tools/mariadb.js +201 -0
- package/dist/tools/migration-tool.js +329 -0
- package/dist/tools/oauth-tool.js +520 -0
- package/dist/tools/parser/components-parser.js +76 -0
- package/dist/tools/parser/controllers-parser.js +313 -0
- package/dist/tools/parser/core-parser.js +294 -0
- package/dist/tools/parser/coverage-generator.js +424 -0
- package/dist/tools/parser/events-parser.js +127 -0
- package/dist/tools/parser/fields-parser.js +382 -0
- package/dist/tools/parser/hooks-parser.js +106 -0
- package/dist/tools/parser/sql-parser.js +197 -0
- package/dist/tools/parser/traits-parser.js +161 -0
- package/dist/tools/parser/widgets-parser.js +150 -0
- package/dist/tools/permission-tool.js +348 -0
- package/dist/tools/project-patch-tool.js +73 -0
- package/dist/tools/project-source-tool.js +188 -0
- package/dist/tools/project-workflow-tool.js +186 -0
- package/dist/tools/requirement-tool.js +212 -0
- package/dist/tools/scaffold-tool.js +815 -0
- package/dist/tools/seo-tool.js +412 -0
- package/dist/tools/source-tool.js +155 -0
- package/dist/tools/template-development-tool.js +271 -0
- package/dist/tools/template-overrides-tool.js +294 -0
- package/dist/tools/template-productivity-tool.js +319 -0
- package/dist/tools/template-tool.js +665 -0
- package/dist/tools/test-tool.js +183 -0
- package/dist/tools/webhook-tool.js +427 -0
- package/dist/tools/widget-tool.js +331 -0
- package/dist/tools/wysiwyg-tool.js +137 -0
- package/dist/types/scaffold.js +68 -0
- package/dist/utils/define-tool.js +37 -0
- package/dist/utils/find-tool.js +97 -0
- package/dist/utils/mcp-result.js +18 -0
- package/dist/utils/pagination.js +27 -0
- package/dist/utils/serialization.js +27 -0
- package/package.json +94 -0
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.createProjectPatch = createProjectPatch;
|
|
4
|
+
function splitLines(content) {
|
|
5
|
+
if (!content)
|
|
6
|
+
return [];
|
|
7
|
+
const lines = content.split('\n');
|
|
8
|
+
if (lines.at(-1) === '')
|
|
9
|
+
lines.pop();
|
|
10
|
+
return lines;
|
|
11
|
+
}
|
|
12
|
+
function patchBody(oldContent, newContent) {
|
|
13
|
+
const oldLines = splitLines(oldContent);
|
|
14
|
+
const newLines = splitLines(newContent);
|
|
15
|
+
const removed = oldLines.map(line => `-${line}`);
|
|
16
|
+
const added = newLines.map(line => `+${line}`);
|
|
17
|
+
if (oldContent && !oldContent.endsWith('\n'))
|
|
18
|
+
removed.push('\');
|
|
19
|
+
if (newContent && !newContent.endsWith('\n'))
|
|
20
|
+
added.push('\');
|
|
21
|
+
return [
|
|
22
|
+
`@@ -${oldLines.length ? `1,${oldLines.length}` : '0,0'} +${newLines.length ? `1,${newLines.length}` : '0,0'} @@`,
|
|
23
|
+
...removed,
|
|
24
|
+
...added,
|
|
25
|
+
];
|
|
26
|
+
}
|
|
27
|
+
function header(path) {
|
|
28
|
+
const normalized = path.replace(/\\/g, '/').replace(/^\.\//, '');
|
|
29
|
+
if (!normalized ||
|
|
30
|
+
normalized.startsWith('/') ||
|
|
31
|
+
/[\r\n\0]/.test(normalized) ||
|
|
32
|
+
normalized.split('/').some(part => part === '..')) {
|
|
33
|
+
throw new Error(`Unsafe project path: ${path}`);
|
|
34
|
+
}
|
|
35
|
+
return normalized;
|
|
36
|
+
}
|
|
37
|
+
function createProjectPatch(beforeInput, afterInput) {
|
|
38
|
+
const before = Object.fromEntries(Object.entries(beforeInput).map(([path, value]) => [header(path), value]));
|
|
39
|
+
const after = Object.fromEntries(Object.entries(afterInput).map(([path, value]) => [header(path), value]));
|
|
40
|
+
const removed = Object.keys(before).filter(path => !(path in after));
|
|
41
|
+
const added = Object.keys(after).filter(path => !(path in before));
|
|
42
|
+
const renamed = new Map();
|
|
43
|
+
for (const oldPath of removed) {
|
|
44
|
+
const newPath = added.find(path => ![...renamed.values()].includes(path) && after[path] === before[oldPath]);
|
|
45
|
+
if (newPath)
|
|
46
|
+
renamed.set(oldPath, newPath);
|
|
47
|
+
}
|
|
48
|
+
const chunks = [];
|
|
49
|
+
for (const [oldPath, newPath] of [...renamed].sort(([a], [b]) => a.localeCompare(b))) {
|
|
50
|
+
chunks.push(`diff --git a/${oldPath} b/${newPath}`, 'similarity index 100%', `rename from ${oldPath}`, `rename to ${newPath}`);
|
|
51
|
+
}
|
|
52
|
+
for (const path of removed.filter(item => !renamed.has(item)).sort()) {
|
|
53
|
+
chunks.push(`diff --git a/${path} b/${path}`, 'deleted file mode 100644', `--- a/${path}`, '+++ /dev/null', ...patchBody(before[path], ''));
|
|
54
|
+
}
|
|
55
|
+
for (const path of added.filter(item => ![...renamed.values()].includes(item)).sort()) {
|
|
56
|
+
chunks.push(`diff --git a/${path} b/${path}`, 'new file mode 100644', '--- /dev/null', `+++ b/${path}`, ...patchBody('', after[path]));
|
|
57
|
+
}
|
|
58
|
+
for (const path of Object.keys(before)
|
|
59
|
+
.filter(path => path in after && before[path] !== after[path])
|
|
60
|
+
.sort()) {
|
|
61
|
+
chunks.push(`diff --git a/${path} b/${path}`, `--- a/${path}`, `+++ b/${path}`, ...patchBody(before[path], after[path]));
|
|
62
|
+
}
|
|
63
|
+
const patch = chunks.length ? `${chunks.join('\n')}\n` : '';
|
|
64
|
+
return {
|
|
65
|
+
patch,
|
|
66
|
+
changed_files: removed.length +
|
|
67
|
+
added.length -
|
|
68
|
+
renamed.size +
|
|
69
|
+
Object.keys(before).filter(path => path in after && before[path] !== after[path]).length,
|
|
70
|
+
renames: Object.fromEntries(renamed),
|
|
71
|
+
is_empty: !patch,
|
|
72
|
+
};
|
|
73
|
+
}
|
|
@@ -0,0 +1,188 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.loadLocalProject = loadLocalProject;
|
|
4
|
+
exports.loadGithubProject = loadGithubProject;
|
|
5
|
+
const promises_1 = require("node:fs/promises");
|
|
6
|
+
const node_path_1 = require("node:path");
|
|
7
|
+
const fflate_1 = require("fflate");
|
|
8
|
+
const DEFAULT_MAX_FILES = 2000;
|
|
9
|
+
const DEFAULT_MAX_FILE_BYTES = 1024 * 1024;
|
|
10
|
+
const DEFAULT_MAX_TOTAL_BYTES = 20 * 1024 * 1024;
|
|
11
|
+
const ignoredDirectories = new Set(['.git', 'node_modules', 'vendor', 'dist', 'build', '.idea']);
|
|
12
|
+
const textExtensions = new Set([
|
|
13
|
+
'.css',
|
|
14
|
+
'.html',
|
|
15
|
+
'.ini',
|
|
16
|
+
'.js',
|
|
17
|
+
'.json',
|
|
18
|
+
'.md',
|
|
19
|
+
'.php',
|
|
20
|
+
'.scss',
|
|
21
|
+
'.sql',
|
|
22
|
+
'.svg',
|
|
23
|
+
'.tpl',
|
|
24
|
+
'.ts',
|
|
25
|
+
'.tsx',
|
|
26
|
+
'.txt',
|
|
27
|
+
'.xml',
|
|
28
|
+
'.yaml',
|
|
29
|
+
'.yml',
|
|
30
|
+
]);
|
|
31
|
+
function limits(options) {
|
|
32
|
+
return {
|
|
33
|
+
maxFiles: options.max_files ?? DEFAULT_MAX_FILES,
|
|
34
|
+
maxFileBytes: options.max_file_bytes ?? DEFAULT_MAX_FILE_BYTES,
|
|
35
|
+
maxTotalBytes: options.max_total_bytes ?? DEFAULT_MAX_TOTAL_BYTES,
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
function isTextFile(path, bytes) {
|
|
39
|
+
const extension = path.includes('.') ? path.slice(path.lastIndexOf('.')).toLowerCase() : '';
|
|
40
|
+
if (textExtensions.has(extension) ||
|
|
41
|
+
['LICENSE', 'README', '.gitignore'].includes(path.split('/').at(-1) ?? '')) {
|
|
42
|
+
return !bytes.subarray(0, 8192).includes(0);
|
|
43
|
+
}
|
|
44
|
+
return false;
|
|
45
|
+
}
|
|
46
|
+
function assertWithinRoot(root, candidate) {
|
|
47
|
+
if (candidate !== root && !candidate.startsWith(`${root}${node_path_1.sep}`)) {
|
|
48
|
+
throw new Error(`Path escapes the requested project root: ${candidate}`);
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
async function loadLocalProject(rootInput, options = {}) {
|
|
52
|
+
const root = await (0, promises_1.realpath)((0, node_path_1.resolve)(rootInput));
|
|
53
|
+
if (!(await (0, promises_1.lstat)(root)).isDirectory())
|
|
54
|
+
throw new Error('Local project path must be a directory');
|
|
55
|
+
const configured = limits(options);
|
|
56
|
+
const files = {};
|
|
57
|
+
const skipped = [];
|
|
58
|
+
let totalBytes = 0;
|
|
59
|
+
async function walk(directory) {
|
|
60
|
+
assertWithinRoot(root, directory);
|
|
61
|
+
for (const entry of await (0, promises_1.readdir)(directory, { withFileTypes: true })) {
|
|
62
|
+
if (entry.isSymbolicLink()) {
|
|
63
|
+
skipped.push({
|
|
64
|
+
path: (0, node_path_1.relative)(root, (0, node_path_1.resolve)(directory, entry.name)),
|
|
65
|
+
reason: 'symbolic_link',
|
|
66
|
+
});
|
|
67
|
+
continue;
|
|
68
|
+
}
|
|
69
|
+
if (entry.isDirectory() && ignoredDirectories.has(entry.name))
|
|
70
|
+
continue;
|
|
71
|
+
const absolute = (0, node_path_1.resolve)(directory, entry.name);
|
|
72
|
+
assertWithinRoot(root, absolute);
|
|
73
|
+
if (entry.isDirectory()) {
|
|
74
|
+
await walk(absolute);
|
|
75
|
+
continue;
|
|
76
|
+
}
|
|
77
|
+
if (!entry.isFile())
|
|
78
|
+
continue;
|
|
79
|
+
const path = (0, node_path_1.relative)(root, absolute).split(node_path_1.sep).join('/');
|
|
80
|
+
const bytes = await (0, promises_1.readFile)(absolute);
|
|
81
|
+
if (bytes.byteLength > configured.maxFileBytes) {
|
|
82
|
+
skipped.push({ path, reason: 'file_too_large' });
|
|
83
|
+
continue;
|
|
84
|
+
}
|
|
85
|
+
if (!isTextFile(path, bytes)) {
|
|
86
|
+
skipped.push({ path, reason: 'binary_or_unsupported' });
|
|
87
|
+
continue;
|
|
88
|
+
}
|
|
89
|
+
if (Object.keys(files).length >= configured.maxFiles)
|
|
90
|
+
throw new Error('Project file limit exceeded');
|
|
91
|
+
totalBytes += bytes.byteLength;
|
|
92
|
+
if (totalBytes > configured.maxTotalBytes)
|
|
93
|
+
throw new Error('Project total size limit exceeded');
|
|
94
|
+
files[path] = bytes.toString('utf8');
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
await walk(root);
|
|
98
|
+
return {
|
|
99
|
+
source: 'local',
|
|
100
|
+
root,
|
|
101
|
+
files,
|
|
102
|
+
files_loaded: Object.keys(files).length,
|
|
103
|
+
total_bytes: totalBytes,
|
|
104
|
+
skipped,
|
|
105
|
+
};
|
|
106
|
+
}
|
|
107
|
+
function validateGithubPart(value, label) {
|
|
108
|
+
if (!/^[A-Za-z0-9._/-]+$/.test(value) || value.includes('..') || value.startsWith('/')) {
|
|
109
|
+
throw new Error(`Invalid GitHub ${label}`);
|
|
110
|
+
}
|
|
111
|
+
return value;
|
|
112
|
+
}
|
|
113
|
+
async function loadGithubProject(repository, ref = 'main', subpath = '', options = {}) {
|
|
114
|
+
const match = repository.match(/^(?:https:\/\/github\.com\/)?([A-Za-z0-9_.-]+)\/([A-Za-z0-9_.-]+?)(?:\.git)?\/?$/);
|
|
115
|
+
if (!match)
|
|
116
|
+
throw new Error('Repository must be owner/name or an https://github.com/owner/name URL');
|
|
117
|
+
const owner = validateGithubPart(match[1], 'owner');
|
|
118
|
+
const name = validateGithubPart(match[2], 'repository');
|
|
119
|
+
const safeRef = validateGithubPart(ref, 'ref');
|
|
120
|
+
const safeSubpath = subpath
|
|
121
|
+
? validateGithubPart(subpath.replace(/^\.\//, '').replace(/\/$/, ''), 'subpath')
|
|
122
|
+
: '';
|
|
123
|
+
const response = await fetch(`https://codeload.github.com/${owner}/${name}/zip/${encodeURIComponent(safeRef)}`, { headers: { 'user-agent': 'instantcms-mcp' }, redirect: 'error' });
|
|
124
|
+
if (!response.ok)
|
|
125
|
+
throw new Error(`GitHub archive request failed: HTTP ${response.status}`);
|
|
126
|
+
const contentLength = Number(response.headers.get('content-length') ?? 0);
|
|
127
|
+
if (contentLength > 50 * 1024 * 1024)
|
|
128
|
+
throw new Error('GitHub archive is too large');
|
|
129
|
+
const archive = new Uint8Array(await response.arrayBuffer());
|
|
130
|
+
if (archive.byteLength > 50 * 1024 * 1024)
|
|
131
|
+
throw new Error('GitHub archive is too large');
|
|
132
|
+
const configured = limits(options);
|
|
133
|
+
const archiveRelativePath = (archivePath) => {
|
|
134
|
+
const parts = archivePath.split('/').slice(1);
|
|
135
|
+
if (!parts.length || archivePath.endsWith('/'))
|
|
136
|
+
return null;
|
|
137
|
+
const path = parts.join('/');
|
|
138
|
+
if (safeSubpath && path !== safeSubpath && !path.startsWith(`${safeSubpath}/`))
|
|
139
|
+
return null;
|
|
140
|
+
const relativePath = safeSubpath ? path.slice(safeSubpath.length).replace(/^\//, '') : path;
|
|
141
|
+
if (!relativePath || relativePath.split('/').some(part => ignoredDirectories.has(part)))
|
|
142
|
+
return null;
|
|
143
|
+
return relativePath;
|
|
144
|
+
};
|
|
145
|
+
let projectedFiles = 0;
|
|
146
|
+
let projectedBytes = 0;
|
|
147
|
+
const entries = (0, fflate_1.unzipSync)(archive, {
|
|
148
|
+
filter: file => {
|
|
149
|
+
if (!archiveRelativePath(file.name) || file.originalSize > configured.maxFileBytes)
|
|
150
|
+
return false;
|
|
151
|
+
projectedFiles += 1;
|
|
152
|
+
projectedBytes += file.originalSize;
|
|
153
|
+
if (projectedFiles > configured.maxFiles)
|
|
154
|
+
throw new Error('Project file limit exceeded');
|
|
155
|
+
if (projectedBytes > configured.maxTotalBytes)
|
|
156
|
+
throw new Error('Project total size limit exceeded');
|
|
157
|
+
return true;
|
|
158
|
+
},
|
|
159
|
+
});
|
|
160
|
+
const files = {};
|
|
161
|
+
const skipped = [];
|
|
162
|
+
let totalBytes = 0;
|
|
163
|
+
for (const [archivePath, bytes] of Object.entries(entries)) {
|
|
164
|
+
const relativePath = archiveRelativePath(archivePath);
|
|
165
|
+
if (!relativePath)
|
|
166
|
+
continue;
|
|
167
|
+
if (!isTextFile(relativePath, bytes)) {
|
|
168
|
+
skipped.push({ path: relativePath, reason: 'binary_or_unsupported' });
|
|
169
|
+
continue;
|
|
170
|
+
}
|
|
171
|
+
if (Object.keys(files).length >= configured.maxFiles)
|
|
172
|
+
throw new Error('Project file limit exceeded');
|
|
173
|
+
totalBytes += bytes.byteLength;
|
|
174
|
+
if (totalBytes > configured.maxTotalBytes)
|
|
175
|
+
throw new Error('Project total size limit exceeded');
|
|
176
|
+
files[relativePath] = Buffer.from(bytes).toString('utf8');
|
|
177
|
+
}
|
|
178
|
+
return {
|
|
179
|
+
source: 'github',
|
|
180
|
+
repository: `${owner}/${name}`,
|
|
181
|
+
ref: safeRef,
|
|
182
|
+
subpath: safeSubpath,
|
|
183
|
+
files,
|
|
184
|
+
files_loaded: Object.keys(files).length,
|
|
185
|
+
total_bytes: totalBytes,
|
|
186
|
+
skipped,
|
|
187
|
+
};
|
|
188
|
+
}
|
|
@@ -0,0 +1,186 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.auditInstantCmsProject = auditInstantCmsProject;
|
|
4
|
+
exports.planProjectChanges = planProjectChanges;
|
|
5
|
+
exports.repairInstantCmsProject = repairInstantCmsProject;
|
|
6
|
+
exports.explainInstantCmsProject = explainInstantCmsProject;
|
|
7
|
+
exports.planInstantCmsUpgrade = planInstantCmsUpgrade;
|
|
8
|
+
const addon_tool_js_1 = require("./addon-tool.js");
|
|
9
|
+
const artifact_tool_js_1 = require("./artifact-tool.js");
|
|
10
|
+
const version_profiles_js_1 = require("../data/version-profiles.js");
|
|
11
|
+
const components_js_1 = require("../data/components.js");
|
|
12
|
+
const hooks_js_1 = require("../data/hooks.js");
|
|
13
|
+
const project_patch_tool_js_1 = require("./project-patch-tool.js");
|
|
14
|
+
function normalizeFiles(files) {
|
|
15
|
+
return Object.fromEntries(Object.entries(files).map(([filePath, content]) => [
|
|
16
|
+
filePath.replace(/\\/g, '/').replace(/^\.\//, ''),
|
|
17
|
+
content,
|
|
18
|
+
]));
|
|
19
|
+
}
|
|
20
|
+
function projectKind(files) {
|
|
21
|
+
const paths = Object.keys(files);
|
|
22
|
+
if (paths.some(file => /(?:^|\/)manifest\.xml$/.test(file)))
|
|
23
|
+
return 'addon';
|
|
24
|
+
if (paths.some(file => /(?:^|\/)widgets?\//.test(file)))
|
|
25
|
+
return 'widget';
|
|
26
|
+
if (paths.some(file => /(?:^|\/)templates?\//.test(file)))
|
|
27
|
+
return 'template';
|
|
28
|
+
return 'unknown';
|
|
29
|
+
}
|
|
30
|
+
function securityDiagnostics(files) {
|
|
31
|
+
const diagnostics = [];
|
|
32
|
+
for (const [filePath, content] of Object.entries(files)) {
|
|
33
|
+
if (!filePath.endsWith('.php'))
|
|
34
|
+
continue;
|
|
35
|
+
if (/->query\(\s*["'`][^"'`]*\$[a-z_]/i.test(content)) {
|
|
36
|
+
diagnostics.push({
|
|
37
|
+
code: 'POSSIBLE_SQL_INTERPOLATION',
|
|
38
|
+
severity: 'warning',
|
|
39
|
+
path: filePath,
|
|
40
|
+
message: 'SQL-запрос содержит интерполяцию переменной.',
|
|
41
|
+
suggestion: 'Используйте query builder или параметризованный запрос.',
|
|
42
|
+
});
|
|
43
|
+
}
|
|
44
|
+
if (/\becho\s+\$(?:_GET|_POST|_REQUEST|[a-z_]\w*)\s*;/i.test(content)) {
|
|
45
|
+
diagnostics.push({
|
|
46
|
+
code: 'POSSIBLE_UNESCAPED_OUTPUT',
|
|
47
|
+
severity: 'warning',
|
|
48
|
+
path: filePath,
|
|
49
|
+
message: 'Переменная выводится без видимого контекстного экранирования.',
|
|
50
|
+
suggestion: 'Проверьте происхождение значения и примените HTML escaping в шаблоне.',
|
|
51
|
+
});
|
|
52
|
+
}
|
|
53
|
+
if (/class\s+\w+\s+extends\s+cmsGrid\b/.test(content)) {
|
|
54
|
+
diagnostics.push({
|
|
55
|
+
code: 'LEGACY_CMSGRID_CLASS',
|
|
56
|
+
severity: 'error',
|
|
57
|
+
path: filePath,
|
|
58
|
+
message: 'Backend grid должен быть функцией grid_*, а не классом cmsGrid.',
|
|
59
|
+
suggestion: 'Перенесите конфигурацию в функцию backend/grids/grid_*.php.',
|
|
60
|
+
});
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
return diagnostics;
|
|
64
|
+
}
|
|
65
|
+
function pathDiagnostics(files) {
|
|
66
|
+
return Object.keys(files).flatMap(filePath => {
|
|
67
|
+
if (/controllers\/[^/]+\/languages\//.test(filePath)) {
|
|
68
|
+
return [
|
|
69
|
+
{
|
|
70
|
+
code: 'MISPLACED_LANGUAGE_FILE',
|
|
71
|
+
severity: 'error',
|
|
72
|
+
path: filePath,
|
|
73
|
+
message: 'Language files должны находиться вне каталога контроллера.',
|
|
74
|
+
suggestion: 'Переместите файл в system/languages/{lang}/controllers/{name}/.',
|
|
75
|
+
},
|
|
76
|
+
];
|
|
77
|
+
}
|
|
78
|
+
return [];
|
|
79
|
+
});
|
|
80
|
+
}
|
|
81
|
+
function auditInstantCmsProject(filesInput) {
|
|
82
|
+
const files = normalizeFiles(filesInput);
|
|
83
|
+
const kind = projectKind(files);
|
|
84
|
+
const artifact = (0, artifact_tool_js_1.validateGeneratedArtifacts)(files);
|
|
85
|
+
const addon = kind === 'addon' ? (0, addon_tool_js_1.validateAddon)(files) : null;
|
|
86
|
+
const addonDiagnostics = addon
|
|
87
|
+
? (addon.diagnostics ?? [])
|
|
88
|
+
.filter((item) => item.severity === 'error' || item.severity === 'warning')
|
|
89
|
+
.map(item => ({ ...item, path: item.path ?? '' }))
|
|
90
|
+
: [];
|
|
91
|
+
const diagnostics = [
|
|
92
|
+
...artifact.diagnostics,
|
|
93
|
+
...addonDiagnostics,
|
|
94
|
+
...pathDiagnostics(files),
|
|
95
|
+
...securityDiagnostics(files),
|
|
96
|
+
];
|
|
97
|
+
return {
|
|
98
|
+
kind,
|
|
99
|
+
files_checked: Object.keys(files).length,
|
|
100
|
+
is_valid: !diagnostics.some(item => item.severity === 'error'),
|
|
101
|
+
summary: {
|
|
102
|
+
errors: diagnostics.filter(item => item.severity === 'error').length,
|
|
103
|
+
warnings: diagnostics.filter(item => item.severity === 'warning').length,
|
|
104
|
+
},
|
|
105
|
+
diagnostics,
|
|
106
|
+
};
|
|
107
|
+
}
|
|
108
|
+
function planProjectChanges(files) {
|
|
109
|
+
const audit = auditInstantCmsProject(files);
|
|
110
|
+
const operations = audit.diagnostics.map(diagnostic => ({
|
|
111
|
+
kind: diagnostic.code === 'MISPLACED_LANGUAGE_FILE' ? 'move' : 'review',
|
|
112
|
+
path: diagnostic.path,
|
|
113
|
+
target: diagnostic.code === 'MISPLACED_LANGUAGE_FILE'
|
|
114
|
+
? diagnostic.path.replace(/controllers\/([^/]+)\/languages\/([^/]+)\//, 'languages/$2/controllers/$1/')
|
|
115
|
+
: undefined,
|
|
116
|
+
reason: diagnostic.suggestion ?? diagnostic.message,
|
|
117
|
+
safe: diagnostic.code === 'MISPLACED_LANGUAGE_FILE',
|
|
118
|
+
}));
|
|
119
|
+
return { audit, operations, safe_operations: operations.filter(item => item.safe).length };
|
|
120
|
+
}
|
|
121
|
+
function repairInstantCmsProject(filesInput) {
|
|
122
|
+
const before = normalizeFiles(filesInput);
|
|
123
|
+
const files = { ...before };
|
|
124
|
+
const plan = planProjectChanges(files);
|
|
125
|
+
const applied = [];
|
|
126
|
+
for (const operation of plan.operations) {
|
|
127
|
+
if (operation.kind !== 'move' || !operation.safe || !operation.target)
|
|
128
|
+
continue;
|
|
129
|
+
if (!(operation.path in files) || operation.target in files)
|
|
130
|
+
continue;
|
|
131
|
+
files[operation.target] = files[operation.path];
|
|
132
|
+
delete files[operation.path];
|
|
133
|
+
applied.push(operation);
|
|
134
|
+
}
|
|
135
|
+
return {
|
|
136
|
+
files,
|
|
137
|
+
applied,
|
|
138
|
+
patch: (0, project_patch_tool_js_1.createProjectPatch)(before, files),
|
|
139
|
+
remaining: auditInstantCmsProject(files),
|
|
140
|
+
};
|
|
141
|
+
}
|
|
142
|
+
function explainInstantCmsProject(files) {
|
|
143
|
+
const normalized = normalizeFiles(files);
|
|
144
|
+
const paths = Object.keys(normalized);
|
|
145
|
+
return {
|
|
146
|
+
kind: projectKind(normalized),
|
|
147
|
+
files: paths.length,
|
|
148
|
+
controllers: [...new Set(paths.flatMap(path => path.match(/controllers\/([^/]+)/)?.[1] ?? []))],
|
|
149
|
+
hooks: paths.filter(path => /\/hooks\/[^/]+\.php$/.test(path)),
|
|
150
|
+
actions: paths.filter(path => /\/actions\/[^/]+\.php$/.test(path)),
|
|
151
|
+
forms: paths.filter(path => /\/forms\/[^/]+\.php$/.test(path)),
|
|
152
|
+
templates: paths.filter(path => /\.tpl\.php$/.test(path)),
|
|
153
|
+
languages: paths.filter(path => /(?:^|\/)languages\//.test(path)),
|
|
154
|
+
};
|
|
155
|
+
}
|
|
156
|
+
function planInstantCmsUpgrade(files, from, to) {
|
|
157
|
+
const comparison = (0, version_profiles_js_1.compareVersionProfiles)(from, to);
|
|
158
|
+
const audit = auditInstantCmsProject(files);
|
|
159
|
+
const code = Object.values(files).join('\n');
|
|
160
|
+
const referencedHooks = [
|
|
161
|
+
...new Set([...code.matchAll(/['"]([a-z][a-z0-9_]+)['"]/g)].map(m => m[1])),
|
|
162
|
+
]
|
|
163
|
+
.filter(name => name.includes('_'))
|
|
164
|
+
.filter(name => hooks_js_1.hooks.some(hook => hook.name === name));
|
|
165
|
+
const referencedMethods = [
|
|
166
|
+
...new Set([...code.matchAll(/->([a-zA-Z_]\w*)\s*\(/g)].map(m => m[1])),
|
|
167
|
+
];
|
|
168
|
+
const knownMethods = new Set(components_js_1.components.flatMap(component => component.methods.map(method => method.name)));
|
|
169
|
+
const unknownMethods = referencedMethods.filter(method => !knownMethods.has(method));
|
|
170
|
+
return {
|
|
171
|
+
from,
|
|
172
|
+
to,
|
|
173
|
+
comparison,
|
|
174
|
+
audit,
|
|
175
|
+
compatibility: {
|
|
176
|
+
referenced_hooks: referencedHooks,
|
|
177
|
+
unknown_method_candidates: unknownMethods,
|
|
178
|
+
},
|
|
179
|
+
checklist: [
|
|
180
|
+
'Повторно проверьте manifest.xml и minimum supported version.',
|
|
181
|
+
'Запустите audit_instantcms_project после изменений.',
|
|
182
|
+
'Проверьте install/uninstall на чистой целевой InstantCMS.',
|
|
183
|
+
'Перепроверьте hooks и методы с unknown_method_candidates вручную.',
|
|
184
|
+
],
|
|
185
|
+
};
|
|
186
|
+
}
|
|
@@ -0,0 +1,212 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.analyzeRequirement = analyzeRequirement;
|
|
4
|
+
exports.suggestAddonStructure = suggestAddonStructure;
|
|
5
|
+
const schemas_js_1 = require("../data/schemas.js");
|
|
6
|
+
function analyzeRequirement(requirement) {
|
|
7
|
+
const lower = requirement.toLowerCase();
|
|
8
|
+
const features = [];
|
|
9
|
+
const requiredHooks = [];
|
|
10
|
+
const suggestedTables = [];
|
|
11
|
+
const recommendations = [];
|
|
12
|
+
const suggestedControllers = [];
|
|
13
|
+
let addonType = 'basic';
|
|
14
|
+
let complexity = 'low';
|
|
15
|
+
const nameSuggestion = generateName(requirement);
|
|
16
|
+
const titleSuggestion = generateTitle(requirement);
|
|
17
|
+
if (lower.includes('админ') ||
|
|
18
|
+
lower.includes('панель') ||
|
|
19
|
+
lower.includes('backend') ||
|
|
20
|
+
lower.includes('admin')) {
|
|
21
|
+
addonType = 'with_admin';
|
|
22
|
+
complexity = 'medium';
|
|
23
|
+
features.push('Админ-панель с CRUD');
|
|
24
|
+
suggestedControllers.push('backend');
|
|
25
|
+
}
|
|
26
|
+
if (lower.includes('событи') ||
|
|
27
|
+
lower.includes('хуки') ||
|
|
28
|
+
lower.includes('hook') ||
|
|
29
|
+
lower.includes('интеграц')) {
|
|
30
|
+
addonType = 'with_hooks';
|
|
31
|
+
if (complexity === 'low')
|
|
32
|
+
complexity = 'medium';
|
|
33
|
+
features.push('Интеграция через хуки');
|
|
34
|
+
}
|
|
35
|
+
if (lower.includes('rss') || lower.includes('лента') || lower.includes('feed')) {
|
|
36
|
+
requiredHooks.push('rss_feed_item');
|
|
37
|
+
features.push('RSS лента');
|
|
38
|
+
}
|
|
39
|
+
if (lower.includes('sitemap') || lower.includes('поисковик')) {
|
|
40
|
+
requiredHooks.push('sitemap_sources', 'sitemap_urls');
|
|
41
|
+
features.push('Интеграция с sitemap');
|
|
42
|
+
}
|
|
43
|
+
if (lower.includes('поиск') || lower.includes('search')) {
|
|
44
|
+
requiredHooks.push('search_sources', 'search_item');
|
|
45
|
+
features.push('Полнотекстовый поиск');
|
|
46
|
+
recommendations.push('Добавьте индексы для часто искомых полей');
|
|
47
|
+
}
|
|
48
|
+
if (lower.includes('тег') || lower.includes('tag')) {
|
|
49
|
+
requiredHooks.push('tagsdelete');
|
|
50
|
+
features.push('Система тегов');
|
|
51
|
+
}
|
|
52
|
+
if (lower.includes('рейтинг') || lower.includes('rating') || lower.includes('голос')) {
|
|
53
|
+
requiredHooks.push('rating_vote');
|
|
54
|
+
features.push('Рейтинг/голосование');
|
|
55
|
+
}
|
|
56
|
+
if (lower.includes('комментар') || lower.includes('comment')) {
|
|
57
|
+
requiredHooks.push('comments_after_add');
|
|
58
|
+
features.push('Комментарии');
|
|
59
|
+
}
|
|
60
|
+
if (lower.includes('уведомлен') || lower.includes('email') || lower.includes('notice')) {
|
|
61
|
+
features.push('Уведомления (email, in-site)');
|
|
62
|
+
recommendations.push("Используйте cmsCore::sendEmail() и cmsCore::getController('messages')->addNotice()");
|
|
63
|
+
}
|
|
64
|
+
if (lower.includes('категори') || lower.includes('иерархи')) {
|
|
65
|
+
suggestedTables.push({
|
|
66
|
+
name: 'categories',
|
|
67
|
+
fields: ['id', 'parent_id', 'title', 'slug', 'ordering'],
|
|
68
|
+
});
|
|
69
|
+
features.push('Иерархия категорий');
|
|
70
|
+
}
|
|
71
|
+
if (lower.includes('пользовател') || lower.includes('user') || lower.includes('профил')) {
|
|
72
|
+
suggestedControllers.push('users');
|
|
73
|
+
features.push('Профили пользователей');
|
|
74
|
+
}
|
|
75
|
+
if (lower.includes('регистрац') || lower.includes('auth')) {
|
|
76
|
+
requiredHooks.push('user_registered');
|
|
77
|
+
suggestedControllers.push('auth');
|
|
78
|
+
features.push('Авторизация/регистрация');
|
|
79
|
+
}
|
|
80
|
+
if (lower.includes('загруз') ||
|
|
81
|
+
lower.includes('upload') ||
|
|
82
|
+
lower.includes('файл') ||
|
|
83
|
+
lower.includes('image')) {
|
|
84
|
+
features.push('Загрузка файлов/изображений');
|
|
85
|
+
recommendations.push('Используйте fieldImage или fieldFile в формах');
|
|
86
|
+
}
|
|
87
|
+
if (lower.includes('кэш') || lower.includes('cache')) {
|
|
88
|
+
features.push('Кэширование данных');
|
|
89
|
+
recommendations.push('Используйте cmsCache::getInstance()->get()/set()');
|
|
90
|
+
}
|
|
91
|
+
if (lower.includes('cron') || lower.includes('планировщ') || lower.includes('schedul')) {
|
|
92
|
+
requiredHooks.push('cron_run');
|
|
93
|
+
features.push('Задачи по расписанию');
|
|
94
|
+
recommendations.push('Добавьте запись в cms_scheduler_tasks');
|
|
95
|
+
}
|
|
96
|
+
if (lower.includes('REST') ||
|
|
97
|
+
lower.includes('api') ||
|
|
98
|
+
lower.includes('ajax') ||
|
|
99
|
+
lower.includes('json')) {
|
|
100
|
+
features.push('API (AJAX/JSON endpoints)');
|
|
101
|
+
recommendations.push('Используйте $this->renderJSON() для AJAX ответов');
|
|
102
|
+
addonType = 'with_routes';
|
|
103
|
+
}
|
|
104
|
+
if (lower.includes('виджет') || lower.includes('widget')) {
|
|
105
|
+
addonType = 'with_widget';
|
|
106
|
+
features.push('Виджет для размещения на страницах');
|
|
107
|
+
}
|
|
108
|
+
if (lower.includes('каталог') ||
|
|
109
|
+
lower.includes('товар') ||
|
|
110
|
+
lower.includes('product') ||
|
|
111
|
+
lower.includes('catalog')) {
|
|
112
|
+
suggestedTables.push({
|
|
113
|
+
name: 'products',
|
|
114
|
+
fields: [
|
|
115
|
+
'id',
|
|
116
|
+
'title',
|
|
117
|
+
'description',
|
|
118
|
+
'price',
|
|
119
|
+
'category_id',
|
|
120
|
+
'user_id',
|
|
121
|
+
'date_pub',
|
|
122
|
+
'is_pub',
|
|
123
|
+
],
|
|
124
|
+
});
|
|
125
|
+
suggestedTables.push({
|
|
126
|
+
name: 'categories',
|
|
127
|
+
fields: ['id', 'parent_id', 'title', 'slug', 'ordering'],
|
|
128
|
+
});
|
|
129
|
+
features.push('Каталог товаров');
|
|
130
|
+
complexity = 'high';
|
|
131
|
+
}
|
|
132
|
+
if (lower.includes('блог') || lower.includes('статья') || lower.includes('post')) {
|
|
133
|
+
suggestedTables.push({
|
|
134
|
+
name: 'posts',
|
|
135
|
+
fields: [
|
|
136
|
+
'id',
|
|
137
|
+
'title',
|
|
138
|
+
'slug',
|
|
139
|
+
'content',
|
|
140
|
+
'user_id',
|
|
141
|
+
'date_pub',
|
|
142
|
+
'is_pub',
|
|
143
|
+
'views',
|
|
144
|
+
'rating',
|
|
145
|
+
],
|
|
146
|
+
});
|
|
147
|
+
features.push('Блог/статьи');
|
|
148
|
+
requiredHooks.push('content_after_add_approve', 'content_after_update_approve');
|
|
149
|
+
}
|
|
150
|
+
if (lower.includes('сообщен') || lower.includes('message') || lower.includes('chat')) {
|
|
151
|
+
suggestedTables.push({
|
|
152
|
+
name: 'messages',
|
|
153
|
+
fields: ['id', 'from_id', 'to_id', 'content', 'date_pub', 'is_read'],
|
|
154
|
+
});
|
|
155
|
+
suggestedControllers.push('messages');
|
|
156
|
+
features.push('Личные сообщения');
|
|
157
|
+
complexity = 'high';
|
|
158
|
+
}
|
|
159
|
+
const description = `Дополнение "${titleSuggestion}" ${features.length > 0 ? 'с функциями: ' + features.join(', ') : ''}`;
|
|
160
|
+
return {
|
|
161
|
+
addon_type: addonType,
|
|
162
|
+
suggested_name: nameSuggestion,
|
|
163
|
+
suggested_title: titleSuggestion,
|
|
164
|
+
description,
|
|
165
|
+
features,
|
|
166
|
+
required_hooks: [...new Set(requiredHooks)],
|
|
167
|
+
suggested_tables: suggestedTables,
|
|
168
|
+
suggested_controllers: [...new Set(suggestedControllers)],
|
|
169
|
+
estimated_complexity: complexity,
|
|
170
|
+
recommendations,
|
|
171
|
+
};
|
|
172
|
+
}
|
|
173
|
+
function generateName(requirement) {
|
|
174
|
+
const words = requirement
|
|
175
|
+
.toLowerCase()
|
|
176
|
+
.replace(/[^а-яёa-z0-9\s]/g, ' ')
|
|
177
|
+
.split(/\s+/)
|
|
178
|
+
.filter(w => w.length > 3)
|
|
179
|
+
.slice(0, 3);
|
|
180
|
+
let name = words.join('_').replace(/[aeiouyаеёиоуыэюя]/gi, '');
|
|
181
|
+
if (name.length < 4) {
|
|
182
|
+
name = words.join('').substring(0, 8);
|
|
183
|
+
}
|
|
184
|
+
return name || 'my_addon';
|
|
185
|
+
}
|
|
186
|
+
function generateTitle(requirement) {
|
|
187
|
+
const words = requirement
|
|
188
|
+
.toLowerCase()
|
|
189
|
+
.replace(/[^а-яёa-z\s]/g, ' ')
|
|
190
|
+
.split(/\s+/)
|
|
191
|
+
.filter(w => w.length > 2)
|
|
192
|
+
.slice(0, 4);
|
|
193
|
+
return words.map(w => w.charAt(0).toUpperCase() + w.slice(1)).join(' ') || 'Мое дополнение';
|
|
194
|
+
}
|
|
195
|
+
function suggestAddonStructure(type) {
|
|
196
|
+
const structure = schemas_js_1.addonStructures[type];
|
|
197
|
+
if (!structure) {
|
|
198
|
+
return {
|
|
199
|
+
error: `Тип "${type}" не найден`,
|
|
200
|
+
available: Object.keys(schemas_js_1.addonStructures),
|
|
201
|
+
};
|
|
202
|
+
}
|
|
203
|
+
return {
|
|
204
|
+
type: structure.type,
|
|
205
|
+
description: structure.description,
|
|
206
|
+
files: structure.files.map(f => ({
|
|
207
|
+
path: f.path,
|
|
208
|
+
required: f.required,
|
|
209
|
+
description: f.description,
|
|
210
|
+
})),
|
|
211
|
+
};
|
|
212
|
+
}
|