@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.
Files changed (111) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +308 -0
  3. package/dist/__tests__/artifact-tool.test.js +196 -0
  4. package/dist/__tests__/db-tool.test.js +152 -0
  5. package/dist/__tests__/define-tool.test.js +106 -0
  6. package/dist/__tests__/find-tool.test.js +185 -0
  7. package/dist/__tests__/get-component-api.test.js +144 -0
  8. package/dist/__tests__/hardening.test.js +73 -0
  9. package/dist/__tests__/hooks-tool.test.js +173 -0
  10. package/dist/__tests__/mcp-integration-extra.test.js +166 -0
  11. package/dist/__tests__/mcp-integration.test.js +53 -0
  12. package/dist/__tests__/pagination.test.js +114 -0
  13. package/dist/__tests__/performance-baseline.test.js +56 -0
  14. package/dist/__tests__/project-patch.test.js +144 -0
  15. package/dist/__tests__/project-source.test.js +44 -0
  16. package/dist/__tests__/project-workflows-extra.test.js +165 -0
  17. package/dist/__tests__/project-workflows.test.js +51 -0
  18. package/dist/__tests__/scaffold-addon-roundtrip.test.js +287 -0
  19. package/dist/__tests__/serialization.test.js +170 -0
  20. package/dist/__tests__/source-knowledge-parsers.test.js +42 -0
  21. package/dist/__tests__/template-development.test.js +78 -0
  22. package/dist/__tests__/template-productivity.test.js +87 -0
  23. package/dist/__tests__/tools.test.js +2143 -0
  24. package/dist/data/components.js +2818 -0
  25. package/dist/data/controllers-map.js +7571 -0
  26. package/dist/data/core-api.js +9356 -0
  27. package/dist/data/database-schema.js +4555 -0
  28. package/dist/data/events-map.js +1339 -0
  29. package/dist/data/fields-map.js +3692 -0
  30. package/dist/data/hooks.js +2257 -0
  31. package/dist/data/js-api.js +123 -0
  32. package/dist/data/libs-api.js +264 -0
  33. package/dist/data/routes-map.js +203 -0
  34. package/dist/data/schemas-validation.js +255 -0
  35. package/dist/data/schemas.js +1404 -0
  36. package/dist/data/traits-map.js +1004 -0
  37. package/dist/data/version-profiles.js +41 -0
  38. package/dist/data/widgets-map.js +131 -0
  39. package/dist/data/wysiwyg-map.js +486 -0
  40. package/dist/generated/components-source.js +14748 -0
  41. package/dist/generated/hooks-source.js +3399 -0
  42. package/dist/generated/knowledge-meta.js +101 -0
  43. package/dist/index.js +13 -0
  44. package/dist/registry/database-tools.js +112 -0
  45. package/dist/registry/extension-tools.js +473 -0
  46. package/dist/registry/generator-tools.js +506 -0
  47. package/dist/registry/knowledge-tools.js +441 -0
  48. package/dist/registry/language-tools.js +99 -0
  49. package/dist/registry/meta-tools.js +152 -0
  50. package/dist/registry/project-tools.js +48 -0
  51. package/dist/registry/resources.js +98 -0
  52. package/dist/registry/source-tools.js +211 -0
  53. package/dist/registry/template-development-tools.js +79 -0
  54. package/dist/server.js +32 -0
  55. package/dist/tools/addon-tool.js +1437 -0
  56. package/dist/tools/admin-partial-tool.js +498 -0
  57. package/dist/tools/api-tool.js +294 -0
  58. package/dist/tools/artifact-tool.js +95 -0
  59. package/dist/tools/cache-tool.js +360 -0
  60. package/dist/tools/component-tool.js +415 -0
  61. package/dist/tools/controllers-tool.js +125 -0
  62. package/dist/tools/cron-tool.js +197 -0
  63. package/dist/tools/crud-tool.js +659 -0
  64. package/dist/tools/db-tool.js +198 -0
  65. package/dist/tools/email-tool.js +222 -0
  66. package/dist/tools/external-api-tool.js +596 -0
  67. package/dist/tools/filter-tool.js +341 -0
  68. package/dist/tools/form-tool.js +275 -0
  69. package/dist/tools/grid-tool.js +189 -0
  70. package/dist/tools/hooks-tool.js +104 -0
  71. package/dist/tools/import-export-tool.js +548 -0
  72. package/dist/tools/lang-tool.js +251 -0
  73. package/dist/tools/layout-override-tool.js +125 -0
  74. package/dist/tools/layout-tool.js +548 -0
  75. package/dist/tools/maria-tool.js +127 -0
  76. package/dist/tools/mariadb.js +201 -0
  77. package/dist/tools/migration-tool.js +329 -0
  78. package/dist/tools/oauth-tool.js +520 -0
  79. package/dist/tools/parser/components-parser.js +76 -0
  80. package/dist/tools/parser/controllers-parser.js +313 -0
  81. package/dist/tools/parser/core-parser.js +294 -0
  82. package/dist/tools/parser/coverage-generator.js +424 -0
  83. package/dist/tools/parser/events-parser.js +127 -0
  84. package/dist/tools/parser/fields-parser.js +382 -0
  85. package/dist/tools/parser/hooks-parser.js +106 -0
  86. package/dist/tools/parser/sql-parser.js +197 -0
  87. package/dist/tools/parser/traits-parser.js +161 -0
  88. package/dist/tools/parser/widgets-parser.js +150 -0
  89. package/dist/tools/permission-tool.js +348 -0
  90. package/dist/tools/project-patch-tool.js +73 -0
  91. package/dist/tools/project-source-tool.js +188 -0
  92. package/dist/tools/project-workflow-tool.js +186 -0
  93. package/dist/tools/requirement-tool.js +212 -0
  94. package/dist/tools/scaffold-tool.js +815 -0
  95. package/dist/tools/seo-tool.js +412 -0
  96. package/dist/tools/source-tool.js +155 -0
  97. package/dist/tools/template-development-tool.js +271 -0
  98. package/dist/tools/template-overrides-tool.js +294 -0
  99. package/dist/tools/template-productivity-tool.js +319 -0
  100. package/dist/tools/template-tool.js +665 -0
  101. package/dist/tools/test-tool.js +183 -0
  102. package/dist/tools/webhook-tool.js +427 -0
  103. package/dist/tools/widget-tool.js +331 -0
  104. package/dist/tools/wysiwyg-tool.js +137 -0
  105. package/dist/types/scaffold.js +68 -0
  106. package/dist/utils/define-tool.js +37 -0
  107. package/dist/utils/find-tool.js +97 -0
  108. package/dist/utils/mcp-result.js +18 -0
  109. package/dist/utils/pagination.js +27 -0
  110. package/dist/utils/serialization.js +27 -0
  111. package/package.json +94 -0
@@ -0,0 +1,161 @@
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
+ exports.generateTraitsMap = generateTraitsMap;
37
+ const fs = __importStar(require("fs"));
38
+ const path = __importStar(require("path"));
39
+ function parseTraitFile(filePath, namespace) {
40
+ const content = fs.readFileSync(filePath, 'utf-8');
41
+ const nameMatch = filePath.match(/([^\/]+)\.php$/);
42
+ if (!nameMatch)
43
+ return null;
44
+ const classMatch = content.match(/trait\s+(\w+)/);
45
+ if (!classMatch)
46
+ return null;
47
+ const traitName = classMatch[1];
48
+ let description;
49
+ const docMatch = content.match(/\*\*[\s\S]*?@/);
50
+ if (docMatch) {
51
+ const descMatch = docMatch[0].match(/@([\s\S]+?)@/);
52
+ if (descMatch) {
53
+ description = descMatch[1]
54
+ .trim()
55
+ .replace(/\n\s*\*/g, '\n')
56
+ .trim();
57
+ }
58
+ }
59
+ const methods = [];
60
+ const methodRegex = /(public|protected|private)\s+function\s+(\w+)\s*\(([^)]*)\)/g;
61
+ let methodMatch;
62
+ while ((methodMatch = methodRegex.exec(content)) !== null) {
63
+ const paramsStr = methodMatch[3].trim();
64
+ const params = paramsStr ? paramsStr.split(',').map(p => p.trim()) : [];
65
+ methods.push({
66
+ name: methodMatch[2],
67
+ visibility: methodMatch[1],
68
+ params,
69
+ });
70
+ }
71
+ return {
72
+ name: traitName,
73
+ namespace,
74
+ filePath: filePath.replace(process.cwd(), ''),
75
+ methods,
76
+ description,
77
+ };
78
+ }
79
+ function scanTraitsDir(dirPath, namespace, traits) {
80
+ if (!fs.existsSync(dirPath))
81
+ return;
82
+ const entries = fs.readdirSync(dirPath, { withFileTypes: true });
83
+ for (const entry of entries) {
84
+ const entryPath = path.join(dirPath, entry.name);
85
+ if (entry.isDirectory()) {
86
+ const newNamespace = namespace ? `${namespace}\\${entry.name}` : entry.name;
87
+ scanTraitsDir(entryPath, newNamespace, traits);
88
+ }
89
+ else if (entry.name.endsWith('.php')) {
90
+ const trait = parseTraitFile(entryPath, namespace);
91
+ if (trait) {
92
+ traits.push(trait);
93
+ }
94
+ }
95
+ }
96
+ }
97
+ function generateTraitsMap(sourceDir, outputPath) {
98
+ const traitsDir = path.join(sourceDir, 'system', 'traits');
99
+ const traits = [];
100
+ scanTraitsDir(traitsDir, 'icms', traits);
101
+ const byNamespace = {};
102
+ for (const trait of traits) {
103
+ if (!byNamespace[trait.namespace]) {
104
+ byNamespace[trait.namespace] = [];
105
+ }
106
+ byNamespace[trait.namespace].push(trait);
107
+ }
108
+ const data = {
109
+ traits,
110
+ byNamespace,
111
+ methodCount: traits.reduce((sum, t) => sum + t.methods.length, 0),
112
+ generatedAt: process.env.KNOWLEDGE_GENERATED_AT || new Date().toISOString(),
113
+ };
114
+ const typescriptContent = `// AUTO-GENERATED from source/system/traits
115
+ // Do not edit manually - run 'npm run parse:traits' to regenerate
116
+
117
+ export interface TraitMethod {
118
+ name: string;
119
+ visibility: 'public' | 'protected' | 'private';
120
+ params: string[];
121
+ description?: string;
122
+ }
123
+
124
+ export interface TraitInfo {
125
+ name: string;
126
+ namespace: string;
127
+ filePath: string;
128
+ methods: TraitMethod[];
129
+ description?: string;
130
+ }
131
+
132
+ export interface TraitsMap {
133
+ traits: TraitInfo[];
134
+ byNamespace: Record<string, TraitInfo[]>;
135
+ methodCount: number;
136
+ generatedAt: string;
137
+ }
138
+
139
+ export const traitsMap: TraitsMap = ${JSON.stringify(data, null, 2)};
140
+
141
+ export function getTrait(name: string): TraitInfo | undefined {
142
+ return traitsMap.traits.find(t => t.name === name);
143
+ }
144
+
145
+ export function getTraitMethods(name: string): TraitMethod[] {
146
+ const trait = getTrait(name);
147
+ return trait?.methods || [];
148
+ }
149
+
150
+ export function getTraitsByNamespace(namespace: string): TraitInfo[] {
151
+ return traitsMap.byNamespace[namespace] || [];
152
+ }
153
+ `;
154
+ fs.writeFileSync(outputPath, typescriptContent);
155
+ console.log(`Generated ${data.traits.length} traits with ${data.methodCount} methods to ${outputPath}`);
156
+ }
157
+ if (require.main === module) {
158
+ const sourcePath = path.resolve(process.env.INSTANTCMS_SOURCE || 'source');
159
+ const outputPath = path.resolve('src/data/traits-map.ts');
160
+ generateTraitsMap(sourcePath, outputPath);
161
+ }
@@ -0,0 +1,150 @@
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
+ exports.generateWidgetsMap = generateWidgetsMap;
37
+ const fs = __importStar(require("fs"));
38
+ const path = __importStar(require("path"));
39
+ function scanWidgetsDir(dirPath, widgets) {
40
+ if (!fs.existsSync(dirPath))
41
+ return;
42
+ const entries = fs.readdirSync(dirPath, { withFileTypes: true });
43
+ for (const entry of entries) {
44
+ if (!entry.isDirectory())
45
+ continue;
46
+ if (entry.name.startsWith('.'))
47
+ continue;
48
+ const widgetPath = path.join(dirPath, entry.name);
49
+ const widgetFile = path.join(widgetPath, 'widget.php');
50
+ const optionsFormFile = path.join(widgetPath, 'options.form.php');
51
+ if (fs.existsSync(widgetFile)) {
52
+ const content = fs.readFileSync(widgetFile, 'utf-8');
53
+ const classMatch = content.match(/class\s+(widget\w+)\s+extends\s+(\w+)/);
54
+ if (classMatch) {
55
+ const className = classMatch[1];
56
+ let controller;
57
+ const controllerMatch = className.match(/^widget([A-Z]\w+)$/);
58
+ if (controllerMatch) {
59
+ controller = controllerMatch[1].toLowerCase();
60
+ }
61
+ let description;
62
+ const docMatch = content.match(/\*\s*([^\n]+)/);
63
+ if (docMatch) {
64
+ description = docMatch[1].trim();
65
+ }
66
+ widgets.push({
67
+ name: entry.name,
68
+ className,
69
+ controller,
70
+ filePath: widgetFile.replace(process.cwd(), ''),
71
+ hasOptionsForm: fs.existsSync(optionsFormFile),
72
+ optionsFormPath: fs.existsSync(optionsFormFile)
73
+ ? optionsFormFile.replace(process.cwd(), '')
74
+ : undefined,
75
+ description,
76
+ });
77
+ }
78
+ }
79
+ }
80
+ }
81
+ function generateWidgetsMap(sourceDir, outputPath) {
82
+ const widgetsDir = path.join(sourceDir, 'system', 'widgets');
83
+ const widgets = [];
84
+ scanWidgetsDir(widgetsDir, widgets);
85
+ const byName = {};
86
+ const byController = {};
87
+ for (const widget of widgets) {
88
+ byName[widget.name] = widget;
89
+ const ctrl = widget.controller || '_system';
90
+ if (!byController[ctrl]) {
91
+ byController[ctrl] = [];
92
+ }
93
+ byController[ctrl].push(widget);
94
+ }
95
+ const data = {
96
+ widgets,
97
+ byName,
98
+ byController,
99
+ widgetCount: widgets.length,
100
+ generatedAt: process.env.KNOWLEDGE_GENERATED_AT || new Date().toISOString(),
101
+ };
102
+ const typescriptContent = `// AUTO-GENERATED from source/system/widgets
103
+ // Do not edit manually - run 'npm run parse:widgets' to regenerate
104
+
105
+ export interface WidgetOption {
106
+ name: string;
107
+ type: string;
108
+ description?: string;
109
+ }
110
+
111
+ export interface WidgetInfo {
112
+ name: string;
113
+ className: string;
114
+ controller?: string;
115
+ filePath: string;
116
+ hasOptionsForm: boolean;
117
+ optionsFormPath?: string;
118
+ description?: string;
119
+ }
120
+
121
+ export interface WidgetsMap {
122
+ widgets: WidgetInfo[];
123
+ byName: Record<string, WidgetInfo>;
124
+ byController: Record<string, WidgetInfo[]>;
125
+ widgetCount: number;
126
+ generatedAt: string;
127
+ }
128
+
129
+ export const widgetsMap: WidgetsMap = ${JSON.stringify(data, null, 2)};
130
+
131
+ export function getWidget(name: string): WidgetInfo | undefined {
132
+ return widgetsMap.byName[name];
133
+ }
134
+
135
+ export function getWidgetsByController(controller: string): WidgetInfo[] {
136
+ return widgetsMap.byController[controller] || [];
137
+ }
138
+
139
+ export function getSystemWidgets(): WidgetInfo[] {
140
+ return widgetsMap.byController['_system'] || [];
141
+ }
142
+ `;
143
+ fs.writeFileSync(outputPath, typescriptContent);
144
+ console.log(`Generated ${data.widgetCount} widgets to ${outputPath}`);
145
+ }
146
+ if (require.main === module) {
147
+ const sourcePath = path.resolve(process.env.INSTANTCMS_SOURCE || 'source');
148
+ const outputPath = path.resolve('src/data/widgets-map.ts');
149
+ generateWidgetsMap(sourcePath, outputPath);
150
+ }
@@ -0,0 +1,348 @@
1
+ "use strict";
2
+ /**
3
+ * @fileoverview Permission system scaffolding tool for InstantCMS
4
+ * Generates permission checker, hooks, and admin backend
5
+ */
6
+ Object.defineProperty(exports, "__esModule", { value: true });
7
+ exports.permissionSchema = exports.PermissionTypeEnum = void 0;
8
+ exports.scaffoldPermission = scaffoldPermission;
9
+ const zod_1 = require("zod");
10
+ const scaffold_1 = require("../types/scaffold");
11
+ /**
12
+ * Available permission types in InstantCMS
13
+ */
14
+ exports.PermissionTypeEnum = zod_1.z.enum([
15
+ 'view',
16
+ 'add',
17
+ 'edit',
18
+ 'delete',
19
+ 'publish',
20
+ 'moderate',
21
+ 'admin',
22
+ ]);
23
+ /**
24
+ * Built-in permission rules for InstantCMS content types
25
+ */
26
+ const PERMISSION_RULES = [
27
+ { type: 'view', description: 'Просмотр контента', defaultRoles: ['all'] },
28
+ { type: 'add', description: 'Добавление контента', defaultRoles: ['auth'] },
29
+ { type: 'edit', description: 'Редактирование своего', defaultRoles: ['auth'] },
30
+ { type: 'delete', description: 'Удаление своего', defaultRoles: ['auth'] },
31
+ { type: 'publish', description: 'Публикация', defaultRoles: ['moderator', 'admin'] },
32
+ { type: 'moderate', description: 'Модерация', defaultRoles: ['moderator', 'admin'] },
33
+ { type: 'admin', description: 'Администрирование', defaultRoles: ['admin'] },
34
+ ];
35
+ /**
36
+ * Zod schema for permission tool input validation
37
+ */
38
+ exports.permissionSchema = zod_1.z.object({
39
+ /** System name (lowercase, snake_case) */
40
+ name: zod_1.z
41
+ .string()
42
+ .min(1)
43
+ .max(50)
44
+ .regex(/^[a-z][a-z0-9_]*$/, 'Только lowercase буквы, цифры и подчёркивание'),
45
+ /** Display title */
46
+ title: zod_1.z.string().min(1).max(100),
47
+ /** Optional description */
48
+ description: zod_1.z.string().optional(),
49
+ /** Controller name */
50
+ controller: zod_1.z.string().optional(),
51
+ /** List of permissions to generate */
52
+ permissions: zod_1.z
53
+ .array(zod_1.z.enum(['view', 'add', 'edit', 'delete', 'publish', 'moderate', 'admin']))
54
+ .optional(),
55
+ /** Permission category */
56
+ category: zod_1.z.string().optional(),
57
+ /** Additional options */
58
+ options: zod_1.z
59
+ .object({
60
+ /** Enable category-based permissions */
61
+ withCategories: zod_1.z.boolean().optional().default(false),
62
+ /** Check item ownership for edit/delete */
63
+ withOwnership: zod_1.z.boolean().optional().default(true),
64
+ /** Generate role-based permissions */
65
+ withRoles: zod_1.z.boolean().optional().default(true),
66
+ })
67
+ .optional(),
68
+ });
69
+ /**
70
+ * Generates the manifest.json file
71
+ */
72
+ function generateManifest(name, title, description, controller, permissions, category, options) {
73
+ return `<?php
74
+ // InstantCMS 2. manifest.json
75
+ return [
76
+ 'type' => 'content_type',
77
+ 'name' => '${name}',
78
+ 'title' => '${title}',
79
+ 'description' => '${description}',
80
+ 'controller' => '${controller}',
81
+ 'permissions' => ['${permissions.join("', '")}'],
82
+ 'category' => '${category}',
83
+ 'version' => '1.0.0',
84
+ 'dependencies' => [],
85
+ 'options' => [
86
+ 'categories' => ${options?.withCategories ?? false},
87
+ 'ownership' => ${options?.withOwnership ?? true},
88
+ 'roles' => ${options?.withRoles ?? true},
89
+ ],
90
+ ];`;
91
+ }
92
+ /**
93
+ * Generates the permission configuration file
94
+ */
95
+ function generatePermissionConfig(name, title, description, controller, permissions, category) {
96
+ const permConfigs = PERMISSION_RULES.filter(r => permissions.includes(r.type))
97
+ .map(r => ` '${r.type}' => [
98
+ 'title' => '${r.description}',
99
+ 'default' => ['${r.defaultRoles.join("', '")}'],
100
+ ]`)
101
+ .join(',\n');
102
+ return `<?php
103
+ // InstantCMS 2. system/config/permissions/${name}.php
104
+
105
+ return [
106
+ 'name' => '${name}',
107
+ 'title' => '${title}',
108
+ 'description' => '${description}',
109
+ 'controller' => '${controller}',
110
+ 'category' => '${category}',
111
+ 'rules' => [
112
+ ${permConfigs}
113
+ ],
114
+ ];`;
115
+ }
116
+ /**
117
+ * Generates the permissions.php checker class
118
+ */
119
+ function generatePermissionChecker(name, nameCapital, permissions, options) {
120
+ const permStrings = permissions.map(p => `'${p}'`).join(', ');
121
+ return `<?php
122
+ // InstantCMS 2. ${name}/permissions.php
123
+
124
+ class ${nameCapital}Permissions {
125
+ private static $instance = null;
126
+ private $config = [];
127
+
128
+ private function __construct() {
129
+ $config_path = cmsConfig::get('root_path') . '/system/config/permissions/${name}.php';
130
+ if (file_exists($config_path)) {
131
+ $this->config = include $config_path;
132
+ }
133
+ }
134
+
135
+ public static function getInstance() {
136
+ if (self::$instance === null) {
137
+ self::$instance = new self();
138
+ }
139
+ return self::$instance;
140
+ }
141
+
142
+ public function can($permission, $user_id = null, $item = null) {
143
+ $user_id = $user_id ?: cmsUser::getInstance()->id;
144
+
145
+ if (!$user_id) {
146
+ return in_array($permission, ['view']);
147
+ }
148
+
149
+ $user = cmsModel::getInstance('users')->getUser($user_id);
150
+ if (!$user) {
151
+ return false;
152
+ }
153
+
154
+ if (${options?.withOwnership ?? true} && $item) {
155
+ if ($item['user_id'] == $user_id && in_array($permission, ['edit', 'delete'])) {
156
+ return true;
157
+ }
158
+ }
159
+
160
+ $rules = $this->config['rules'][$permission]['default'] ?? [];
161
+
162
+ if ($user['is_admin'] || in_array('admin', $rules)) {
163
+ return true;
164
+ }
165
+
166
+ return in_array($this->getRoleById($user['role_id']), $rules);
167
+ }
168
+
169
+ private function getRoleById($role_id) {
170
+ $roles = [
171
+ 1 => 'admin',
172
+ 2 => 'moderator',
173
+ 3 => 'auth',
174
+ 4 => 'all',
175
+ ];
176
+ return $roles[$role_id] ?? 'all';
177
+ }
178
+
179
+ public function getAvailablePermissions() {
180
+ return [${permStrings}];
181
+ }
182
+ }`;
183
+ }
184
+ /**
185
+ * Generates hook handlers for permission events
186
+ */
187
+ function generateHookHandlers(name, nameCapital) {
188
+ return `<?php
189
+ // InstantCMS 2. hooks/${name}/permissions.hooks.php
190
+
191
+ class on${nameCapital}PermissionsHook {
192
+ public function onUserAfterLogin($user) {
193
+ $permissions = ${nameCapital}Permissions::getInstance();
194
+ }
195
+
196
+ public function onContentBeforeSave($item) {
197
+ $permissions = ${nameCapital}Permissions::getInstance();
198
+
199
+ if (!$permissions->can('edit', null, $item)) {
200
+ cmsUser::addSessionMessage('У вас нет прав на редактирование', 'error');
201
+ return false;
202
+ }
203
+
204
+ return $item;
205
+ }
206
+
207
+ public function onContentBeforeDelete($item) {
208
+ $permissions = ${nameCapital}Permissions::getInstance();
209
+
210
+ if (!$permissions->can('delete', null, $item)) {
211
+ cmsUser::addSessionMessage('У вас нет прав на удаление', 'error');
212
+ return false;
213
+ }
214
+
215
+ return $item;
216
+ }
217
+ }`;
218
+ }
219
+ /**
220
+ * Generates admin backend for permissions management
221
+ */
222
+ function generateAdminBackend(name, nameCapital, permissions) {
223
+ const permStrings = permissions.map(p => `'${p}'`).join(', ');
224
+ return `<?php
225
+ // InstantCMS 2. ${name}/backend/permissions.php
226
+
227
+ class backend${nameCapital}Permissions extends cmsBackend {
228
+
229
+ protected $useOptions = false;
230
+
231
+ public function __construct($request) {
232
+ parent::__construct($request);
233
+ $this->controlName = '${nameCapital}Permissions';
234
+ }
235
+
236
+ public function actionIndex() {
237
+ $this->renderTemplate('permissions', [
238
+ 'permissions' => ${nameCapital}Permissions::getInstance()->getAvailablePermissions(),
239
+ 'roles' => $this->model->getRoles(),
240
+ ]);
241
+ }
242
+
243
+ public function actionSave() {
244
+ $data = $this->request->getAll();
245
+
246
+ $config_path = cmsConfig::get('root_path') . '/system/config/permissions/${name}.php';
247
+
248
+ $config = [
249
+ 'name' => '${name}',
250
+ 'title' => '${nameCapital}',
251
+ 'rules' => [],
252
+ ];
253
+
254
+ foreach ([${permStrings}] as $perm) {
255
+ $config['rules'][$perm] = [
256
+ 'title' => $data['titles'][$perm] ?? '',
257
+ 'default' => $data['defaults'][$perm] ?? [],
258
+ ];
259
+ }
260
+
261
+ file_put_contents($config_path, '<?php\\nreturn ' . var_export($config, true) . ';');
262
+
263
+ cmsUser::addSessionMessage('Права сохранены', 'success');
264
+ $this->redirectToAction('index');
265
+ }
266
+
267
+ public function actionCheck() {
268
+ $permission = $this->request->get('permission', '');
269
+ $user_id = $this->request->get('user_id', 0);
270
+
271
+ $permissions = ${nameCapital}Permissions::getInstance();
272
+ $result = $permissions->can($permission, $user_id);
273
+
274
+ $this->halt(200, json_encode(['allowed' => $result]));
275
+ }
276
+ }`;
277
+ }
278
+ /**
279
+ * Generates roles configuration file
280
+ */
281
+ function generateRoles(name, permissions) {
282
+ return `<?php
283
+ // InstantCMS 2. ${name}/roles.php
284
+
285
+ return [
286
+ 'admin' => [
287
+ 'title' => 'Администратор',
288
+ 'permissions' => ['${permissions.join("', '")}'],
289
+ ],
290
+ 'moderator' => [
291
+ 'title' => 'Модератор',
292
+ 'permissions' => ['${permissions.filter((p) => p !== 'admin').join("', '")}'],
293
+ ],
294
+ 'auth' => [
295
+ 'title' => 'Авторизованный',
296
+ 'permissions' => ['view', 'add'],
297
+ ],
298
+ 'all' => [
299
+ 'title' => 'Гость',
300
+ 'permissions' => ['view'],
301
+ ],
302
+ ];`;
303
+ }
304
+ /**
305
+ * Generates a complete permission system for an InstantCMS addon
306
+ *
307
+ * @param input - Configuration for the permission system
308
+ * @returns Object containing generated files and metadata
309
+ *
310
+ * @example
311
+ * ```typescript
312
+ * const result = scaffoldPermission({
313
+ * name: 'blog_post',
314
+ * title: 'Записи блога',
315
+ * permissions: ['view', 'add', 'edit', 'delete'],
316
+ * options: { withOwnership: true, withRoles: true }
317
+ * });
318
+ * ```
319
+ */
320
+ function scaffoldPermission(input) {
321
+ const { lowercase, UpperCamelCase } = (0, scaffold_1.normalizeAddonName)(input.name);
322
+ const title = input.title;
323
+ const description = input.description || '';
324
+ const controller = input.controller || lowercase;
325
+ const permissions = input.permissions || ['view', 'add', 'edit', 'delete'];
326
+ const category = input.category || 'content';
327
+ const options = {
328
+ withCategories: input.options?.withCategories ?? false,
329
+ withOwnership: input.options?.withOwnership ?? true,
330
+ withRoles: input.options?.withRoles ?? true,
331
+ };
332
+ const files = {};
333
+ files[`${lowercase}/manifest.json`] = generateManifest(lowercase, title, description, controller, permissions, category, options);
334
+ files[`system/config/permissions/${lowercase}.php`] = generatePermissionConfig(lowercase, title, description, controller, permissions, category);
335
+ files[`${lowercase}/permissions.php`] = generatePermissionChecker(lowercase, UpperCamelCase, permissions, options);
336
+ files[`system/hooks/${lowercase}/permissions.hooks.php`] = generateHookHandlers(lowercase, UpperCamelCase);
337
+ files[`${lowercase}/backend/permissions.php`] = generateAdminBackend(lowercase, UpperCamelCase, permissions);
338
+ if (options.withRoles) {
339
+ files[`${lowercase}/roles.php`] = generateRoles(lowercase, permissions);
340
+ }
341
+ return {
342
+ addon_name: lowercase,
343
+ title,
344
+ permissions_count: permissions.length,
345
+ options,
346
+ files,
347
+ };
348
+ }