@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,144 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
+
}) : function(o, v) {
|
|
16
|
+
o["default"] = v;
|
|
17
|
+
});
|
|
18
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
19
|
+
var ownKeys = function(o) {
|
|
20
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
21
|
+
var ar = [];
|
|
22
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
23
|
+
return ar;
|
|
24
|
+
};
|
|
25
|
+
return ownKeys(o);
|
|
26
|
+
};
|
|
27
|
+
return function (mod) {
|
|
28
|
+
if (mod && mod.__esModule) return mod;
|
|
29
|
+
var result = {};
|
|
30
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
31
|
+
__setModuleDefault(result, mod);
|
|
32
|
+
return result;
|
|
33
|
+
};
|
|
34
|
+
})();
|
|
35
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
|
+
const fc = __importStar(require("fast-check"));
|
|
37
|
+
const project_patch_tool_js_1 = require("../tools/project-patch-tool.js");
|
|
38
|
+
describe('createProjectPatch', () => {
|
|
39
|
+
test('идентичные file-maps → пустой patch', () => {
|
|
40
|
+
const result = (0, project_patch_tool_js_1.createProjectPatch)({ 'a.txt': 'hello' }, { 'a.txt': 'hello' });
|
|
41
|
+
expect(result.patch).toBe('');
|
|
42
|
+
expect(result.is_empty).toBe(true);
|
|
43
|
+
expect(result.changed_files).toBe(0);
|
|
44
|
+
});
|
|
45
|
+
test('пустые file-maps → пустой patch', () => {
|
|
46
|
+
const result = (0, project_patch_tool_js_1.createProjectPatch)({}, {});
|
|
47
|
+
expect(result.patch).toBe('');
|
|
48
|
+
expect(result.is_empty).toBe(true);
|
|
49
|
+
});
|
|
50
|
+
test('изменён один файл → patch содержит этот файл', () => {
|
|
51
|
+
const result = (0, project_patch_tool_js_1.createProjectPatch)({ 'a.txt': 'hello' }, { 'a.txt': 'world' });
|
|
52
|
+
expect(result.patch).toContain('diff --git a/a.txt b/a.txt');
|
|
53
|
+
expect(result.patch).toContain('-hello');
|
|
54
|
+
expect(result.patch).toContain('+world');
|
|
55
|
+
expect(result.changed_files).toBe(1);
|
|
56
|
+
expect(result.is_empty).toBe(false);
|
|
57
|
+
});
|
|
58
|
+
test('удалён файл → patch содержит "deleted file mode" и весь контент с -', () => {
|
|
59
|
+
const result = (0, project_patch_tool_js_1.createProjectPatch)({ 'remove.txt': 'gone' }, {});
|
|
60
|
+
expect(result.patch).toContain('deleted file mode 100644');
|
|
61
|
+
expect(result.patch).toContain('--- a/remove.txt');
|
|
62
|
+
expect(result.patch).toContain('+++ /dev/null');
|
|
63
|
+
expect(result.patch).toContain('-gone');
|
|
64
|
+
});
|
|
65
|
+
test('добавлен файл → patch содержит "new file mode" и контент с +', () => {
|
|
66
|
+
const result = (0, project_patch_tool_js_1.createProjectPatch)({}, { 'new.txt': 'fresh' });
|
|
67
|
+
expect(result.patch).toContain('new file mode 100644');
|
|
68
|
+
expect(result.patch).toContain('--- /dev/null');
|
|
69
|
+
expect(result.patch).toContain('+++ b/new.txt');
|
|
70
|
+
expect(result.patch).toContain('+fresh');
|
|
71
|
+
});
|
|
72
|
+
test('identical content, different path → registered as rename с similarity 100%', () => {
|
|
73
|
+
const result = (0, project_patch_tool_js_1.createProjectPatch)({ 'old/name.txt': 'unchanged' }, { 'new/name.txt': 'unchanged' });
|
|
74
|
+
expect(result.patch).toContain('rename from old/name.txt');
|
|
75
|
+
expect(result.patch).toContain('rename to new/name.txt');
|
|
76
|
+
expect(result.patch).toContain('similarity index 100%');
|
|
77
|
+
expect(result.renames).toEqual({ 'old/name.txt': 'new/name.txt' });
|
|
78
|
+
});
|
|
79
|
+
test('path traversal в path → throws "Unsafe project path"', () => {
|
|
80
|
+
expect(() => (0, project_patch_tool_js_1.createProjectPatch)({}, { '../escape.txt': 'pwn' })).toThrow(/Unsafe project path/);
|
|
81
|
+
expect(() => (0, project_patch_tool_js_1.createProjectPatch)({}, { '/abs/path': 'pwn' })).toThrow(/Unsafe project path/);
|
|
82
|
+
expect(() => (0, project_patch_tool_js_1.createProjectPatch)({}, { 'with\nnewline.txt': 'pwn' })).toThrow(/Unsafe project path/);
|
|
83
|
+
});
|
|
84
|
+
test('multi-file: deletes, additions, edits, renames в одном patch', () => {
|
|
85
|
+
const before = {
|
|
86
|
+
'keep.txt': 'k',
|
|
87
|
+
'edit.txt': 'old',
|
|
88
|
+
'gone.txt': 'g',
|
|
89
|
+
};
|
|
90
|
+
const after = {
|
|
91
|
+
'keep.txt': 'k',
|
|
92
|
+
'edit.txt': 'new',
|
|
93
|
+
'added.txt': 'a',
|
|
94
|
+
'moved.txt': 'g', // rename of gone.txt
|
|
95
|
+
};
|
|
96
|
+
const result = (0, project_patch_tool_js_1.createProjectPatch)(before, after);
|
|
97
|
+
expect(result.patch).toContain('rename from gone.txt');
|
|
98
|
+
expect(result.patch).toContain('rename to moved.txt');
|
|
99
|
+
expect(result.patch).toContain('-old'); // from edit.txt
|
|
100
|
+
expect(result.patch).toContain('+new');
|
|
101
|
+
expect(result.patch).toContain('+a'); // added.txt
|
|
102
|
+
// changed_files = adds - renames + edits = (1 added + 1 deleted) - 1 rename + 1 edited = 2
|
|
103
|
+
expect(result.changed_files).toBeGreaterThan(0);
|
|
104
|
+
});
|
|
105
|
+
test('content без trailing newline → "No newline at end of file"', () => {
|
|
106
|
+
const result = (0, project_patch_tool_js_1.createProjectPatch)({ 'n.txt': 'no-nl' }, { 'n.txt': 'with-nl' });
|
|
107
|
+
expect(result.patch).toContain('\');
|
|
108
|
+
});
|
|
109
|
+
test('property-based: patch никогда не throws на корректных путях', () => {
|
|
110
|
+
const safePath = fc
|
|
111
|
+
.stringMatching(/^[a-z][a-z0-9_]{0,20}(\/[a-z0-9_]{0,20})*$/)
|
|
112
|
+
.filter(s => s.length > 0);
|
|
113
|
+
fc.assert(fc.property(fc.dictionary(safePath, fc.string({ maxLength: 200 }), { maxKeys: 10 }), fc.dictionary(safePath, fc.string({ maxLength: 200 }), { maxKeys: 10 }), (before, after) => {
|
|
114
|
+
const result = (0, project_patch_tool_js_1.createProjectPatch)(before, after);
|
|
115
|
+
expect(typeof result.patch).toBe('string');
|
|
116
|
+
return true;
|
|
117
|
+
}), { numRuns: 30 });
|
|
118
|
+
});
|
|
119
|
+
test('property-based: changed_files >= 0 всегда', () => {
|
|
120
|
+
const safePath = fc
|
|
121
|
+
.stringMatching(/^[a-z][a-z0-9_]{0,15}(\/[a-z0-9_]{0,15})*$/)
|
|
122
|
+
.filter(s => s.length > 0);
|
|
123
|
+
fc.assert(fc.property(fc.dictionary(safePath, fc.string({ maxLength: 100 }), { maxKeys: 8 }), fc.dictionary(safePath, fc.string({ maxLength: 100 }), { maxKeys: 8 }), (before, after) => {
|
|
124
|
+
const result = (0, project_patch_tool_js_1.createProjectPatch)(before, after);
|
|
125
|
+
expect(result.changed_files).toBeGreaterThanOrEqual(0);
|
|
126
|
+
return true;
|
|
127
|
+
}), { numRuns: 30 });
|
|
128
|
+
});
|
|
129
|
+
test('patch валиден как string даже на пустых maps (не null/undefined)', () => {
|
|
130
|
+
const result = (0, project_patch_tool_js_1.createProjectPatch)({}, {});
|
|
131
|
+
expect(typeof result.patch).toBe('string');
|
|
132
|
+
expect(result.patch).toBe('');
|
|
133
|
+
});
|
|
134
|
+
test('rename detection: одинаковый content по новому пути + нет других совпадений → rename', () => {
|
|
135
|
+
const result = (0, project_patch_tool_js_1.createProjectPatch)({ 'a.txt': 'one' }, { 'b.txt': 'one' });
|
|
136
|
+
expect(result.renames['a.txt']).toBe('b.txt');
|
|
137
|
+
expect(result.patch).toContain('rename from a.txt');
|
|
138
|
+
});
|
|
139
|
+
test('content без неломающих символов (null-byte и \\r остаются в patch как есть — backend отвечает за экранирование)', () => {
|
|
140
|
+
const result = (0, project_patch_tool_js_1.createProjectPatch)({ 'a.php': '<?php\n/*\u0000bad*/' }, { 'a.php': '<?php\n/*fixed*/' });
|
|
141
|
+
expect(result.patch).toContain('+<?php');
|
|
142
|
+
expect(result.patch).toContain('+/*fixed*/');
|
|
143
|
+
});
|
|
144
|
+
});
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
const promises_1 = require("node:fs/promises");
|
|
4
|
+
const node_os_1 = require("node:os");
|
|
5
|
+
const node_path_1 = require("node:path");
|
|
6
|
+
const fflate_1 = require("fflate");
|
|
7
|
+
const project_source_tool_js_1 = require("../tools/project-source-tool.js");
|
|
8
|
+
describe('project sources', () => {
|
|
9
|
+
test('loads local text files while ignoring dependencies and symlinks', async () => {
|
|
10
|
+
const root = await (0, promises_1.mkdtemp)((0, node_path_1.join)((0, node_os_1.tmpdir)(), 'instantcms-mcp-'));
|
|
11
|
+
try {
|
|
12
|
+
await (0, promises_1.mkdir)((0, node_path_1.join)(root, 'system/controllers/demo'), { recursive: true });
|
|
13
|
+
await (0, promises_1.mkdir)((0, node_path_1.join)(root, 'node_modules/pkg'), { recursive: true });
|
|
14
|
+
await (0, promises_1.writeFile)((0, node_path_1.join)(root, 'system/controllers/demo/frontend.php'), '<?php');
|
|
15
|
+
await (0, promises_1.writeFile)((0, node_path_1.join)(root, 'node_modules/pkg/index.js'), 'ignored');
|
|
16
|
+
await (0, promises_1.symlink)((0, node_path_1.join)(root, 'system/controllers/demo/frontend.php'), (0, node_path_1.join)(root, 'linked.php'));
|
|
17
|
+
const result = await (0, project_source_tool_js_1.loadLocalProject)(root);
|
|
18
|
+
expect(result.files).toEqual({ 'system/controllers/demo/frontend.php': '<?php' });
|
|
19
|
+
expect(result.skipped).toContainEqual({ path: 'linked.php', reason: 'symbolic_link' });
|
|
20
|
+
}
|
|
21
|
+
finally {
|
|
22
|
+
await (0, promises_1.rm)(root, { recursive: true, force: true });
|
|
23
|
+
}
|
|
24
|
+
});
|
|
25
|
+
test('loads a selected directory from a GitHub archive', async () => {
|
|
26
|
+
const archive = (0, fflate_1.zipSync)({
|
|
27
|
+
'repo-main/addons/demo/manifest.xml': new TextEncoder().encode('<component/>'),
|
|
28
|
+
'repo-main/README.md': new TextEncoder().encode('ignored'),
|
|
29
|
+
});
|
|
30
|
+
const originalFetch = global.fetch;
|
|
31
|
+
global.fetch = jest.fn(async () => new Response(archive, { status: 200 }));
|
|
32
|
+
try {
|
|
33
|
+
const result = await (0, project_source_tool_js_1.loadGithubProject)('owner/repo', 'main', 'addons/demo');
|
|
34
|
+
expect(result.files).toEqual({ 'manifest.xml': '<component/>' });
|
|
35
|
+
expect(global.fetch).toHaveBeenCalledWith('https://codeload.github.com/owner/repo/zip/main', expect.objectContaining({ redirect: 'error' }));
|
|
36
|
+
}
|
|
37
|
+
finally {
|
|
38
|
+
global.fetch = originalFetch;
|
|
39
|
+
}
|
|
40
|
+
});
|
|
41
|
+
test('rejects invalid GitHub repository input', async () => {
|
|
42
|
+
await expect((0, project_source_tool_js_1.loadGithubProject)('https://example.com/owner/repo')).rejects.toThrow('Repository must be owner/name');
|
|
43
|
+
});
|
|
44
|
+
});
|
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
+
}) : function(o, v) {
|
|
16
|
+
o["default"] = v;
|
|
17
|
+
});
|
|
18
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
19
|
+
var ownKeys = function(o) {
|
|
20
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
21
|
+
var ar = [];
|
|
22
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
23
|
+
return ar;
|
|
24
|
+
};
|
|
25
|
+
return ownKeys(o);
|
|
26
|
+
};
|
|
27
|
+
return function (mod) {
|
|
28
|
+
if (mod && mod.__esModule) return mod;
|
|
29
|
+
var result = {};
|
|
30
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
31
|
+
__setModuleDefault(result, mod);
|
|
32
|
+
return result;
|
|
33
|
+
};
|
|
34
|
+
})();
|
|
35
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
|
+
const fc = __importStar(require("fast-check"));
|
|
37
|
+
const project_workflow_tool_js_1 = require("../tools/project-workflow-tool.js");
|
|
38
|
+
const scaffold_tool_js_1 = require("../tools/scaffold-tool.js");
|
|
39
|
+
function validAddonFiles() {
|
|
40
|
+
return (0, scaffold_tool_js_1.scaffoldAddon)({ name: 'wfextest', title: 'WF', type: 'basic' }).files;
|
|
41
|
+
}
|
|
42
|
+
describe('project workflows (extended)', () => {
|
|
43
|
+
test('audit: пустой file-map → is_valid=true, 0 errors', () => {
|
|
44
|
+
const audit = (0, project_workflow_tool_js_1.auditInstantCmsProject)({});
|
|
45
|
+
expect(audit.is_valid).toBe(true);
|
|
46
|
+
expect(audit.summary.errors).toBe(0);
|
|
47
|
+
});
|
|
48
|
+
test('audit: scaffoldAddon basic → is_valid=true', () => {
|
|
49
|
+
const audit = (0, project_workflow_tool_js_1.auditInstantCmsProject)(validAddonFiles());
|
|
50
|
+
expect(audit.kind).toBe('addon');
|
|
51
|
+
expect(audit.is_valid).toBe(true);
|
|
52
|
+
});
|
|
53
|
+
test('audit: cmsGrid (legacy class) → LEGACY_CMSGRID_CLASS error', () => {
|
|
54
|
+
const files = {
|
|
55
|
+
'package/system/controllers/wfextest/manifest.xml': '<addon/>',
|
|
56
|
+
'package/system/controllers/wfextest/grids/old.php': '<?php class custom extends cmsGrid {}',
|
|
57
|
+
};
|
|
58
|
+
const audit = (0, project_workflow_tool_js_1.auditInstantCmsProject)(files);
|
|
59
|
+
const legacy = audit.diagnostics.filter(d => d.code === 'LEGACY_CMSGRID_CLASS');
|
|
60
|
+
expect(legacy.length).toBe(1);
|
|
61
|
+
expect(legacy[0].severity).toBe('error');
|
|
62
|
+
});
|
|
63
|
+
test('audit: PHP with ->query($var) → POSSIBLE_SQL_INTERPOLATION warning', () => {
|
|
64
|
+
const sql = ['<?php ', '$db->query("SELECT * FROM x WHERE id = ', '$id");'].join('$');
|
|
65
|
+
const files = {
|
|
66
|
+
'src/danger.php': sql,
|
|
67
|
+
};
|
|
68
|
+
const audit = (0, project_workflow_tool_js_1.auditInstantCmsProject)(files);
|
|
69
|
+
const found = audit.diagnostics.filter(d => d.code === 'POSSIBLE_SQL_INTERPOLATION');
|
|
70
|
+
expect(found.length).toBeGreaterThanOrEqual(1);
|
|
71
|
+
expect(found[0].severity).toBe('warning');
|
|
72
|
+
});
|
|
73
|
+
test('audit: echo $_GET → POSSIBLE_UNESCAPED_OUTPUT warning (просто $var; форма)', () => {
|
|
74
|
+
// Текущий регексп ловит только echo $VAR; без обращения к элементам массива.
|
|
75
|
+
// Это известное ограничение детектора — фиксируем, чтобы регрессия была видна.
|
|
76
|
+
const php = '<?php echo $_GET;';
|
|
77
|
+
const files = {
|
|
78
|
+
'src/danger.php': php,
|
|
79
|
+
};
|
|
80
|
+
const audit = (0, project_workflow_tool_js_1.auditInstantCmsProject)(files);
|
|
81
|
+
const out = audit.diagnostics.filter(d => d.code === 'POSSIBLE_UNESCAPED_OUTPUT');
|
|
82
|
+
expect(out.length).toBeGreaterThanOrEqual(1);
|
|
83
|
+
expect(out[0].severity).toBe('warning');
|
|
84
|
+
});
|
|
85
|
+
test('repair: MISPLACED_LANGUAGE_FILE перемещается на правильный путь', () => {
|
|
86
|
+
const files = validAddonFiles();
|
|
87
|
+
files['package/system/controllers/wfextest/languages/ru/lang.php'] = '<?php // misplaced';
|
|
88
|
+
const result = (0, project_workflow_tool_js_1.repairInstantCmsProject)(files);
|
|
89
|
+
expect(result.applied).toHaveLength(1);
|
|
90
|
+
expect(result.applied[0].kind).toBe('move');
|
|
91
|
+
expect(result.applied[0].target).toBe('package/system/languages/ru/controllers/wfextest/lang.php');
|
|
92
|
+
expect(result.files['package/system/languages/ru/controllers/wfextest/lang.php']).toBeDefined();
|
|
93
|
+
expect(result.files['package/system/controllers/wfextest/languages/ru/lang.php']).toBeUndefined();
|
|
94
|
+
expect(result.patch.patch).toContain('rename from');
|
|
95
|
+
const remaining = result.remaining.diagnostics.filter(d => d.code === 'MISPLACED_LANGUAGE_FILE');
|
|
96
|
+
expect(remaining).toHaveLength(0);
|
|
97
|
+
});
|
|
98
|
+
test('repair идемпотентен: повторный вызов не меняет files', () => {
|
|
99
|
+
const files = validAddonFiles();
|
|
100
|
+
files['package/system/controllers/wfextest/languages/ru/lang.php'] = '<?php // misplaced';
|
|
101
|
+
const first = (0, project_workflow_tool_js_1.repairInstantCmsProject)(files);
|
|
102
|
+
const second = (0, project_workflow_tool_js_1.repairInstantCmsProject)(first.files);
|
|
103
|
+
expect(second.applied).toHaveLength(0);
|
|
104
|
+
expect(Object.keys(second.files).sort()).toEqual(Object.keys(first.files).sort());
|
|
105
|
+
});
|
|
106
|
+
test('repair не применяет UNSAFE операции (cmsGrid class, SQL interpolation)', () => {
|
|
107
|
+
const files = {
|
|
108
|
+
'package/system/controllers/demo/grids/old.php': '<?php class custom extends cmsGrid {}',
|
|
109
|
+
'src/danger.php': '<?php $db->query("...$id");',
|
|
110
|
+
};
|
|
111
|
+
const result = (0, project_workflow_tool_js_1.repairInstantCmsProject)(files);
|
|
112
|
+
expect(result.applied).toHaveLength(0);
|
|
113
|
+
});
|
|
114
|
+
test('explain: считает controllers, hooks, actions, forms', () => {
|
|
115
|
+
const files = validAddonFiles();
|
|
116
|
+
files['package/system/controllers/wfextest/hooks/sample.php'] = '<?php';
|
|
117
|
+
files['package/system/controllers/wfextest/actions/test.php'] = '<?php';
|
|
118
|
+
files['package/system/controllers/wfextest/forms/sample.php'] = '<?php';
|
|
119
|
+
const explanation = (0, project_workflow_tool_js_1.explainInstantCmsProject)(files);
|
|
120
|
+
expect(explanation.controllers).toContain('wfextest');
|
|
121
|
+
expect(explanation.hooks.length).toBeGreaterThanOrEqual(1);
|
|
122
|
+
expect(explanation.actions.length).toBeGreaterThanOrEqual(1);
|
|
123
|
+
expect(explanation.forms.length).toBeGreaterThanOrEqual(1);
|
|
124
|
+
});
|
|
125
|
+
test('plan: возвращает операции с правильными kind', () => {
|
|
126
|
+
const files = {
|
|
127
|
+
'package/system/controllers/foo/languages/ru/lang.php': '<?php',
|
|
128
|
+
};
|
|
129
|
+
const plan = (0, project_workflow_tool_js_1.planProjectChanges)(files);
|
|
130
|
+
expect(plan.operations.length).toBe(1);
|
|
131
|
+
expect(plan.operations[0].kind).toBe('move');
|
|
132
|
+
expect(plan.operations[0].safe).toBe(true);
|
|
133
|
+
expect(plan.operations[0].target).toBe('package/system/languages/ru/controllers/foo/lang.php');
|
|
134
|
+
});
|
|
135
|
+
test('upgrade: known версии (2.17 → 2.18.2) — checklist не пустой', () => {
|
|
136
|
+
const result = (0, project_workflow_tool_js_1.planInstantCmsUpgrade)({}, '2.17', '2.18.2');
|
|
137
|
+
expect(result.checklist.length).toBeGreaterThan(0);
|
|
138
|
+
expect(result.from).toBe('2.17');
|
|
139
|
+
expect(result.to).toBe('2.18.2');
|
|
140
|
+
});
|
|
141
|
+
test('upgrade: unknown версия → warnings присутствуют', () => {
|
|
142
|
+
const result = (0, project_workflow_tool_js_1.planInstantCmsUpgrade)({}, '99.99.99', '2.18.2');
|
|
143
|
+
expect(result.comparison.warnings).toBeDefined();
|
|
144
|
+
});
|
|
145
|
+
test('property-based: для произвольного набора PHP files → diagnostics всегда массив', () => {
|
|
146
|
+
const safePath = fc
|
|
147
|
+
.stringMatching(/^[a-z][a-z0-9_]{0,15}(\/[a-z0-9_]{0,15})*$/)
|
|
148
|
+
.filter(s => s.length > 0);
|
|
149
|
+
fc.assert(fc.property(fc.dictionary(safePath, fc.string({ maxLength: 500 }), { maxKeys: 5 }), files => {
|
|
150
|
+
const audit = (0, project_workflow_tool_js_1.auditInstantCmsProject)(files);
|
|
151
|
+
expect(Array.isArray(audit.diagnostics)).toBe(true);
|
|
152
|
+
return true;
|
|
153
|
+
}), { numRuns: 20 });
|
|
154
|
+
});
|
|
155
|
+
test('property-based: repair не выбрасывает на корректных путях', () => {
|
|
156
|
+
const safePath = fc
|
|
157
|
+
.stringMatching(/^[a-z][a-z0-9_]{0,15}(\/[a-z0-9_]{0,15})*$/)
|
|
158
|
+
.filter(s => s.length > 0);
|
|
159
|
+
fc.assert(fc.property(fc.dictionary(safePath, fc.string({ maxLength: 200 }), { maxKeys: 5 }), files => {
|
|
160
|
+
const result = (0, project_workflow_tool_js_1.repairInstantCmsProject)(files);
|
|
161
|
+
expect(typeof result.files).toBe('object');
|
|
162
|
+
return true;
|
|
163
|
+
}), { numRuns: 20 });
|
|
164
|
+
});
|
|
165
|
+
});
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
const project_workflow_tool_js_1 = require("../tools/project-workflow-tool.js");
|
|
4
|
+
const project_patch_tool_js_1 = require("../tools/project-patch-tool.js");
|
|
5
|
+
const node_child_process_1 = require("node:child_process");
|
|
6
|
+
const files = {
|
|
7
|
+
'system/controllers/demo/manifest.xml': '<component><name>demo</name><title>Demo</title></component>',
|
|
8
|
+
'system/controllers/demo/frontend.php': '<?php class demo extends cmsFrontend {}',
|
|
9
|
+
'system/controllers/demo/languages/ru/demo.php': '<?php define("LANG_DEMO", "Demo");',
|
|
10
|
+
'system/controllers/demo/actions/index.php': '<?php echo $title;',
|
|
11
|
+
};
|
|
12
|
+
describe('project agent workflows', () => {
|
|
13
|
+
test('audits structure, syntax and suspicious output', () => {
|
|
14
|
+
const result = (0, project_workflow_tool_js_1.auditInstantCmsProject)(files);
|
|
15
|
+
expect(result.kind).toBe('addon');
|
|
16
|
+
expect(result.diagnostics.some(item => item.code === 'MISPLACED_LANGUAGE_FILE')).toBe(true);
|
|
17
|
+
expect(result.diagnostics.some(item => item.code === 'POSSIBLE_UNESCAPED_OUTPUT')).toBe(true);
|
|
18
|
+
});
|
|
19
|
+
test('plans before applying only safe repairs', () => {
|
|
20
|
+
const plan = (0, project_workflow_tool_js_1.planProjectChanges)(files);
|
|
21
|
+
expect(plan.safe_operations).toBe(1);
|
|
22
|
+
const repaired = (0, project_workflow_tool_js_1.repairInstantCmsProject)(files);
|
|
23
|
+
expect(repaired.applied).toHaveLength(1);
|
|
24
|
+
expect(repaired.files['system/languages/ru/controllers/demo/demo.php']).toBeDefined();
|
|
25
|
+
expect(repaired.files['system/controllers/demo/languages/ru/demo.php']).toBeUndefined();
|
|
26
|
+
expect(repaired.patch.patch).toContain('rename from system/controllers/demo/languages/ru/demo.php');
|
|
27
|
+
});
|
|
28
|
+
test('creates an applicable unified patch for edits and additions', () => {
|
|
29
|
+
const result = (0, project_patch_tool_js_1.createProjectPatch)({ 'a.php': '<?php echo 1;\n' }, { 'a.php': '<?php echo 2;\n', 'b.php': '<?php\n' });
|
|
30
|
+
expect(result.changed_files).toBe(2);
|
|
31
|
+
expect(result.patch).toContain('diff --git a/a.php b/a.php');
|
|
32
|
+
expect(result.patch).toContain('-<?php echo 1;');
|
|
33
|
+
expect(result.patch).toContain('+<?php echo 2;');
|
|
34
|
+
expect(result.patch).toContain('new file mode 100644');
|
|
35
|
+
const parsed = (0, node_child_process_1.spawnSync)('git', ['apply', '--numstat', '-'], {
|
|
36
|
+
input: result.patch,
|
|
37
|
+
encoding: 'utf8',
|
|
38
|
+
});
|
|
39
|
+
expect(parsed.status).toBe(0);
|
|
40
|
+
expect(parsed.stdout).toContain('a.php');
|
|
41
|
+
});
|
|
42
|
+
test('rejects unsafe patch paths', () => {
|
|
43
|
+
expect(() => (0, project_patch_tool_js_1.createProjectPatch)({ '../secret.php': 'secret' }, {})).toThrow('Unsafe project path');
|
|
44
|
+
});
|
|
45
|
+
test('explains project and creates an upgrade checklist', () => {
|
|
46
|
+
expect((0, project_workflow_tool_js_1.explainInstantCmsProject)(files).controllers).toContain('demo');
|
|
47
|
+
const upgrade = (0, project_workflow_tool_js_1.planInstantCmsUpgrade)(files, '2.17', '2.18.2');
|
|
48
|
+
expect(upgrade.comparison.to?.version).toBe('2.18.2');
|
|
49
|
+
expect(upgrade.checklist.length).toBeGreaterThan(2);
|
|
50
|
+
});
|
|
51
|
+
});
|