@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,815 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.scaffoldAddon = scaffoldAddon;
4
+ exports.scaffoldTemplate = scaffoldTemplate;
5
+ const serialization_js_1 = require("../utils/serialization.js");
6
+ function scaffoldAddon(opts) {
7
+ if (!/^[a-z][a-z0-9_]{1,63}$/.test(opts.name)) {
8
+ throw new Error('Техническое имя должно начинаться с латинской буквы и содержать 2–64 строчных букв, цифр или подчёркиваний');
9
+ }
10
+ const name = opts.name;
11
+ const Name = name.split('_').map(capitalize).join('');
12
+ const NAME = name.toUpperCase();
13
+ const version = opts.version || '1.0.0';
14
+ const author = opts.author || 'Author';
15
+ const author_url = opts.author_url || 'https://example.com';
16
+ const title = opts.title;
17
+ const description = opts.description || `Дополнение ${title}`;
18
+ const files = {};
19
+ // ── manifest.ru.ini — корень пакета ─────────────────────────────────────────
20
+ const versionMatch = /^(\d+)\.(\d+)\.(\d+)$/.exec(version);
21
+ if (!versionMatch) {
22
+ throw new Error('Версия должна быть указана в формате X.Y.Z, например 1.0.0');
23
+ }
24
+ const [, major, minor, build] = versionMatch;
25
+ files['[pkg] manifest.ru.ini'] = `[info]
26
+ title = ${(0, serialization_js_1.quoteIni)(title)}
27
+ description = ${(0, serialization_js_1.quoteIni)(description)}
28
+ image_hint =
29
+
30
+ [version]
31
+ major = ${major}
32
+ minor = ${minor}
33
+ build = ${build}
34
+
35
+ [author]
36
+ name = ${(0, serialization_js_1.quoteIni)(author)}
37
+ url = ${(0, serialization_js_1.quoteIni)(author_url)}
38
+
39
+ [install]
40
+ type = controller
41
+ name = ${name}`;
42
+ // ── install.sql ──────────────────────────────────────────────────────────────
43
+ files['[pkg] install.sql'] = `-- Замените cms_ на реальный префикс БД из system/config/config.php
44
+ CREATE TABLE IF NOT EXISTS \`cms_${name}_items\` (
45
+ \`id\` int(10) unsigned NOT NULL AUTO_INCREMENT,
46
+ \`user_id\` int(10) unsigned NOT NULL DEFAULT '0',
47
+ \`title\` varchar(255) NOT NULL DEFAULT '',
48
+ \`text\` text,
49
+ \`date_pub\` datetime NOT NULL,
50
+ \`is_pub\` tinyint(1) unsigned NOT NULL DEFAULT '1',
51
+ PRIMARY KEY (\`id\`),
52
+ KEY \`user_id\` (\`user_id\`),
53
+ KEY \`is_pub\` (\`is_pub\`),
54
+ KEY \`date_pub\` (\`date_pub\`)
55
+ ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;`;
56
+ // ── Файлы контроллера ────────────────────────────────────────────────────────
57
+ const ctrl = `package/system/controllers/${name}`;
58
+ // ── model.php ────────────────────────────────────────────────────────────────
59
+ files[`${ctrl}/model.php`] = `<?php
60
+
61
+ class model${Name} extends cmsModel {
62
+
63
+ public function getPublished(int $limit = 10): array {
64
+ return $this->filterEqual('is_pub', 1)
65
+ ->orderBy('date_pub', 'desc')
66
+ ->limit($limit)
67
+ ->get('${name}_items');
68
+ }
69
+
70
+ public function getByUser(int $user_id): array {
71
+ return $this->filterEqual('user_id', $user_id)
72
+ ->orderBy('date_pub', 'desc')
73
+ ->get('${name}_items');
74
+ }
75
+
76
+ }`;
77
+ // ── frontend.php ─────────────────────────────────────────────────────────────
78
+ files[`${ctrl}/frontend.php`] = `<?php
79
+ /**
80
+ * @property \\model${Name} $model
81
+ */
82
+ class ${name} extends cmsFrontend {
83
+
84
+ protected $useOptions = true;
85
+
86
+ // Экшены размещаются в actions/*.php
87
+ // Шаблоны — в /templates/{theme}/controllers/${name}/*.tpl.php
88
+ // Языковой файл — /system/languages/ru/controllers/${name}/${name}.php
89
+
90
+ }`;
91
+ // ── manifest.xml ─────────────────────────────────────────────────────────────
92
+ const hookEntries = (opts.hooks || [])
93
+ .map(h => ` <hook controller="${name}" name="${(0, serialization_js_1.escapeXml)(h)}" />`)
94
+ .join('\n');
95
+ files[`${ctrl}/manifest.xml`] = `<?xml version="1.0" encoding="utf-8"?>
96
+ <addon>
97
+ <name>${name}</name>
98
+ <title>${(0, serialization_js_1.escapeXml)(title)}</title>
99
+ <description>${(0, serialization_js_1.escapeXml)(description)}</description>
100
+ <version>${(0, serialization_js_1.escapeXml)(version)}</version>
101
+ <build>${build}</build>
102
+ <author>
103
+ <name>${(0, serialization_js_1.escapeXml)(author)}</name>
104
+ <url>${(0, serialization_js_1.escapeXml)(author_url)}</url>
105
+ <email></email>
106
+ </author>
107
+ <dependencies>
108
+ </dependencies>
109
+ <hooks>
110
+ ${hookEntries || ' <!-- <hook controller="${name}" name="user_registered" /> -->'}
111
+ </hooks>
112
+ </addon>`;
113
+ // ── install.php / uninstall.php ───────────────────────────────────────────────
114
+ files[`${ctrl}/install.php`] = `<?php
115
+
116
+ class install${Name} extends cmsInstaller {
117
+
118
+ public function install() {
119
+ // SQL выполняется автоматически из install.sql при установке пакета.
120
+ // Здесь можно добавить дополнительную инициализацию:
121
+ // $this->addOption('${name}', 'perpage', 10);
122
+ return true;
123
+ }
124
+
125
+ }`;
126
+ files[`${ctrl}/uninstall.php`] = `<?php
127
+
128
+ class uninstall${Name} extends cmsInstaller {
129
+
130
+ public function uninstall() {
131
+ $this->db->query("DROP TABLE IF EXISTS \`{$this->db->prefix}${name}_items\`");
132
+ return true;
133
+ }
134
+
135
+ }`;
136
+ // ── actions/index.php ────────────────────────────────────────────────────────
137
+ files[`${ctrl}/actions/index.php`] = `<?php
138
+
139
+ class action${Name}Index extends cmsAction {
140
+
141
+ public function run() {
142
+
143
+ $page = $this->request->get('page', 1);
144
+ $perpage = $this->options['perpage'] ?? 10;
145
+
146
+ $total = $this->model->filterEqual('is_pub', 1)->getCount('${name}_items');
147
+
148
+ $items = $this->model
149
+ ->filterEqual('is_pub', 1)
150
+ ->orderBy('date_pub', 'desc')
151
+ ->limitPage($page, $perpage)
152
+ ->get('${name}_items');
153
+
154
+ $this->cms_template->setTitle(LANG_${NAME}_TITLE);
155
+
156
+ // Шаблон: /templates/{theme}/controllers/${name}/index.tpl.php
157
+ return $this->cms_template->render('index', [
158
+ 'items' => $items,
159
+ 'total' => $total,
160
+ 'page' => (int) $page,
161
+ 'perpage' => (int) $perpage,
162
+ ]);
163
+ }
164
+
165
+ }`;
166
+ // ── actions/view.php ─────────────────────────────────────────────────────────
167
+ files[`${ctrl}/actions/view.php`] = `<?php
168
+
169
+ class action${Name}View extends cmsAction {
170
+
171
+ public function run() {
172
+
173
+ $id = $this->request->get('id', 0);
174
+ $item = $this->model->getItemByField('${name}_items', 'id', (int) $id);
175
+
176
+ if (!$item || !$item['is_pub']) {
177
+ return cmsCore::error404();
178
+ }
179
+
180
+ $item = cmsEventsManager::hook('${name}_before_item', $item);
181
+
182
+ $this->cms_template->setTitle($item['title']);
183
+ $this->cms_template->addBreadcrumb(LANG_${NAME}_TITLE, href_to('${name}'));
184
+ $this->cms_template->addBreadcrumb($item['title']);
185
+
186
+ // Шаблон: /templates/{theme}/controllers/${name}/view.tpl.php
187
+ return $this->cms_template->render('view', [
188
+ 'item' => $item,
189
+ ]);
190
+ }
191
+
192
+ }`;
193
+ // ── routes.php — только для with_routes ─────────────────────────────────────
194
+ if (opts.type === 'with_routes') {
195
+ files[`${ctrl}/routes.php`] = generateRoutes(name);
196
+ }
197
+ // ── Языковой файл ────────────────────────────────────────────────────────────
198
+ // ВАЖНО: файл расположен ВНЕ папки контроллера!
199
+ files[`package/system/languages/ru/controllers/${name}/${name}.php`] = `<?php
200
+ // Файл: /system/languages/ru/controllers/${name}/${name}.php
201
+ // ВАЖНО: языковой файл расположен в /system/languages/, а НЕ внутри контроллера!
202
+
203
+ define('LANG_${NAME}_TITLE', ${(0, serialization_js_1.quotePhp)(title)});
204
+ define('LANG_${NAME}_ADD', 'Добавить');
205
+ define('LANG_${NAME}_EDIT', 'Редактировать');
206
+ define('LANG_${NAME}_DELETE', 'Удалить');
207
+ define('LANG_${NAME}_NOT_FOUND', 'Ничего не найдено');
208
+
209
+ // Константы для бэкенда (рекомендуется префикс _CP_):
210
+ define('LANG_${NAME}_CP_TITLE', '${title}');
211
+ define('LANG_${NAME}_CP_ITEMS', 'Элементы');
212
+ define('LANG_${NAME}_CP_ADD', 'Добавить элемент');`;
213
+ // ── Хуки ────────────────────────────────────────────────────────────────────
214
+ if (opts.hooks && opts.hooks.length > 0) {
215
+ for (const hookName of opts.hooks) {
216
+ const hookCamel = hookName
217
+ .split('_')
218
+ .map((w) => capitalize(w))
219
+ .join('');
220
+ files[`${ctrl}/hooks/${hookName}.php`] = `<?php
221
+ // Хук: ${hookName}
222
+ // Регистрация в manifest.xml: <hook controller="${name}" name="${hookName}" />
223
+
224
+ class on${Name}${hookCamel} extends cmsAction {
225
+
226
+ /**
227
+ * @param mixed $data Данные хука (тип зависит от конкретного хука)
228
+ * @return mixed Для filter-хуков ОБЯЗАТЕЛЬНО вернуть $data
229
+ */
230
+ public function run($data) {
231
+
232
+ // Доступны стандартные свойства:
233
+ // $this->model — модель дополнения ${name}
234
+ // $this->cms_user — текущий пользователь
235
+ // $this->request — объект запроса
236
+
237
+ // Пример: загрузить другой контроллер:
238
+ // $content = cmsCore::getController('content');
239
+
240
+ // TODO: ваша логика
241
+
242
+ return $data; // ОБЯЗАТЕЛЬНО вернуть $data для filter-хуков!
243
+ }
244
+
245
+ }`;
246
+ }
247
+ }
248
+ // ── Бэкенд (if with_admin) ───────────────────────────────────────────────────
249
+ if (opts.type === 'with_admin') {
250
+ // backend.php — только getBackendMenu() и before(), БЕЗ inline экшенов
251
+ files[`${ctrl}/backend.php`] = generateBackend(name, Name, NAME);
252
+ // backend/model.php — расширение модели для бэкенда
253
+ files[`${ctrl}/backend/model.php`] = generateBackendModel(name, Name);
254
+ // backend/actions/index.php — дашборд бэкенда
255
+ files[`${ctrl}/backend/actions/index.php`] = generateBackendActionIndex(name, Name, NAME);
256
+ // backend/actions/items.php — список с трейтом listgrid
257
+ files[`${ctrl}/backend/actions/items.php`] = generateBackendActionItems(name, Name, NAME);
258
+ // backend/actions/items_add.php — добавление/редактирование с трейтом formItem
259
+ files[`${ctrl}/backend/actions/items_add.php`] = generateBackendActionItemsAdd(name, Name, NAME);
260
+ // backend/grids/grid_items.php — ФУНКЦИЯ, не класс!
261
+ files[`${ctrl}/backend/grids/grid_items.php`] = generateGridItems(name, Name, NAME);
262
+ // backend/forms/form_item.php — форма с init($do)
263
+ files[`${ctrl}/backend/forms/form_item.php`] = generateFormItem(Name, NAME);
264
+ // backend/forms/form_options.php — настройки дополнения
265
+ files[`${ctrl}/backend/forms/form_options.php`] = generateFormOptions(Name);
266
+ }
267
+ // ── Виджет (if with_widget) ──────────────────────────────────────────────────
268
+ if (opts.type === 'with_widget') {
269
+ files[`${ctrl}/widgets/list/widget.php`] = generateWidget(name, Name);
270
+ files[`${ctrl}/widgets/list/options.form.php`] = generateWidgetOptionsForm(Name);
271
+ }
272
+ return {
273
+ addon_name: name,
274
+ addon_class: Name,
275
+ type: opts.type,
276
+ files_count: Object.keys(files).length,
277
+ files,
278
+ installation_note: `Пакет для менеджера дополнений: manifest.ru.ini + install.sql + package/ → архив ${name}.zip`,
279
+ structure_notes: [
280
+ `backend.php содержит ТОЛЬКО getBackendMenu() — экшены в backend/actions/`,
281
+ `backend/grids/grid_items.php — ФУНКЦИЯ grid_items($controller), не класс`,
282
+ `Языковой файл: /system/languages/ru/controllers/${name}/${name}.php (ВНЕ контроллера!)`,
283
+ `Шаблоны фронтенда: /templates/{theme}/controllers/${name}/*.tpl.php`,
284
+ `Шаблоны бэкенда: /templates/admincoreui/controllers/${name}/*.tpl.php`,
285
+ ],
286
+ next_steps: [
287
+ `1. Упаковать в zip: manifest.ru.ini + install.sql + package/ → ${name}.zip`,
288
+ `2. Установить: Панель управления → Расширения → Загрузить пакет`,
289
+ `3. Создать шаблоны в /templates/default/controllers/${name}/index.tpl.php и view.tpl.php`,
290
+ opts.type === 'with_admin'
291
+ ? `4. Шаблоны бэкенда: /templates/admincoreui/controllers/${name}/index.tpl.php`
292
+ : `4. При необходимости добавить виджеты в widgets/`,
293
+ ],
294
+ };
295
+ }
296
+ function scaffoldTemplate(opts) {
297
+ if (!/^[a-z][a-z0-9_]{1,63}$/.test(opts.name)) {
298
+ throw new Error('Техническое имя шаблона должно начинаться с латинской буквы и содержать 2–64 допустимых символа');
299
+ }
300
+ const name = opts.name;
301
+ const files = {
302
+ 'manifest.php': `<?php
303
+ return [
304
+ 'title' => ${(0, serialization_js_1.quotePhp)(opts.title)},
305
+ 'author' => [
306
+ 'name' => ${(0, serialization_js_1.quotePhp)(opts.author || 'Author')},
307
+ 'url' => 'https://example.com',
308
+ 'help' => 'https://example.com/docs'
309
+ ],
310
+ 'properties' => [
311
+ 'has_options' => true,
312
+ 'has_profile_themes_support' => false,
313
+ 'has_profile_themes_options' => false,
314
+ 'is_dynamic_layout' => false,
315
+ 'is_backend' => false,
316
+ 'is_frontend' => true
317
+ ]
318
+ ];`,
319
+ 'main.tpl.php': `<!DOCTYPE html>
320
+ <html lang="<?= LANG_CODE ?>">
321
+ <head>
322
+ <meta charset="utf-8">
323
+ <meta name="viewport" content="width=device-width, initial-scale=1">
324
+ <title><?= $this->title() ?></title>
325
+ <?= $this->head() ?>
326
+ <?= $this->linkCSS('css/main.css') ?>
327
+ </head>
328
+ <body>
329
+
330
+ <header class="site-header">
331
+ <?= $this->widgets('header') ?>
332
+ <?= $this->widgets('top') ?>
333
+ </header>
334
+
335
+ <?= $this->breadcrumbs() ?>
336
+
337
+ <div class="site-wrap">
338
+ <?php if ($this->hasWidgetsOn('left-top') || $this->hasWidgetsOn('left-bottom')): ?>
339
+ <aside class="sidebar sidebar-left">
340
+ <?= $this->widgets('left-top') ?>
341
+ <?= $this->widgets('left-bottom') ?>
342
+ </aside>
343
+ <?php endif ?>
344
+
345
+ <main class="site-main">
346
+ <?= $this->body() ?>
347
+ </main>
348
+
349
+ <?php if ($this->hasWidgetsOn('right-top')): ?>
350
+ <aside class="sidebar sidebar-right">
351
+ <?= $this->widgets('right-top') ?>
352
+ <?= $this->widgets('right-center') ?>
353
+ <?= $this->widgets('right-bottom') ?>
354
+ </aside>
355
+ <?php endif ?>
356
+ </div>
357
+
358
+ <footer class="site-footer">
359
+ <?= $this->widgets('footer') ?>
360
+ </footer>
361
+
362
+ <?= $this->bottom() ?>
363
+ </body>
364
+ </html>`,
365
+ 'css/main.css': `/* ${opts.title} */
366
+ :root {
367
+ --color-primary: #4a90d9;
368
+ --color-text: #333;
369
+ --color-bg: #fff;
370
+ --color-border: #e0e0e0;
371
+ }
372
+
373
+ * { box-sizing: border-box; }
374
+ body { font-family: sans-serif; color: var(--color-text); background: var(--color-bg); margin: 0; }
375
+ .site-wrap { display: flex; max-width: 1200px; margin: 0 auto; padding: 0 16px; gap: 24px; }
376
+ .site-main { flex: 1; min-width: 0; }
377
+ .sidebar { width: 240px; flex-shrink: 0; }`,
378
+ };
379
+ return {
380
+ template_name: name,
381
+ files,
382
+ placement: `/templates/${name}/`,
383
+ structure_notes: [
384
+ `Шаблон размещается в /templates/${name}/ (НЕ в /system/templates/)`,
385
+ `Для переопределения шаблонов контроллеров: controllers/{controller}/{action}.tpl.php`,
386
+ `Для бэкенда используется шаблон admincoreui (не фронтенд-шаблон)`,
387
+ `Переменные в .tpl.php: $this->title(), $this->body(), $this->widgets('position'), $this->breadcrumbs()`,
388
+ `Проверка виджетов: $this->hasWidgetsOn('right-top')`,
389
+ ],
390
+ };
391
+ }
392
+ // ─────────────────────────────────────────────────────────────────────────────
393
+ // Helpers
394
+ // ─────────────────────────────────────────────────────────────────────────────
395
+ function capitalize(str) {
396
+ return str.charAt(0).toUpperCase() + str.slice(1);
397
+ }
398
+ // backend.php — ТОЛЬКО getBackendMenu() + before(), без inline экшенов
399
+ function generateBackend(name, Name, NAME) {
400
+ return `<?php
401
+ /**
402
+ * @property \\modelBackend${Name} $model
403
+ */
404
+ class backend${Name} extends cmsBackend {
405
+
406
+ // true = автоматически создать экшен 'options' для формы настроек
407
+ public $useDefaultOptionsAction = true;
408
+
409
+ // Загружать настройки дополнения
410
+ protected $useOptions = true;
411
+
412
+ public function __construct(cmsRequest $request) {
413
+ parent::__construct($request);
414
+ // Передать настройки в модель (если модели нужны опции):
415
+ // $this->model->setControllerOptions($this->options);
416
+ }
417
+
418
+ // Вызывается перед каждым экшеном бэкенда
419
+ public function before($action_name) {
420
+ if (!parent::before($action_name)) {
421
+ return false;
422
+ }
423
+ return true;
424
+ }
425
+
426
+ // Меню левой панели бэкенда
427
+ public function getBackendMenu() {
428
+ return [
429
+ [
430
+ 'title' => LANG_${NAME}_CP_ITEMS,
431
+ 'url' => href_to($this->root_url, 'items'),
432
+ 'options' => ['icon' => 'list']
433
+ ],
434
+ [
435
+ 'title' => LANG_OPTIONS,
436
+ 'url' => href_to($this->root_url, 'options'),
437
+ 'options' => ['icon' => 'cog']
438
+ ]
439
+ ];
440
+ }
441
+
442
+ }`;
443
+ }
444
+ // backend/model.php — расширение модели для бэкенда
445
+ function generateBackendModel(name, Name) {
446
+ return `<?php
447
+ /**
448
+ * Расширение модели для нужд бэкенда.
449
+ * Добавляет JOIN-ы, агрегации и методы для бэкенд-экшенов.
450
+ */
451
+ class modelBackend${Name} extends model${Name} {
452
+
453
+ // Статистика для дашборда
454
+ public function getStats(): array {
455
+ return [
456
+ 'total' => $this->getCount('${name}_items'),
457
+ 'published' => $this->filterEqual('is_pub', 1)->getCount('${name}_items'),
458
+ ];
459
+ }
460
+
461
+ }`;
462
+ }
463
+ // backend/actions/index.php — дашборд
464
+ function generateBackendActionIndex(name, Name, _NAME) {
465
+ return `<?php
466
+ /**
467
+ * Дашборд бэкенда.
468
+ * URL: /admin/${name}/ или /admin/${name}/index
469
+ *
470
+ * @property \\modelBackend${Name} $model
471
+ */
472
+ class action${Name}Index extends cmsAction {
473
+
474
+ public function run($do = false) {
475
+
476
+ // Делегация вложенным экшенам: /admin/${name}/index/something
477
+ if ($do) {
478
+ return $this->runAction('index_' . $do, array_slice($this->params, 1));
479
+ }
480
+
481
+ $stats = $this->model->getStats();
482
+
483
+ // Шаблон: /templates/admincoreui/controllers/${name}/index.tpl.php
484
+ return $this->cms_template->render([
485
+ 'stats' => $stats,
486
+ ]);
487
+ }
488
+
489
+ }`;
490
+ }
491
+ // backend/actions/items.php — список с трейтом listgrid
492
+ function generateBackendActionItems(name, Name, NAME) {
493
+ return `<?php
494
+ /**
495
+ * Список элементов в бэкенде.
496
+ * Использует trait listgrid — не нужен метод run(), настройка через __construct().
497
+ * URL: /admin/${name}/items
498
+ */
499
+ class action${Name}Items extends cmsAction {
500
+
501
+ use icms\\traits\\controllers\\actions\\listgrid;
502
+
503
+ public function __construct($controller, $params = []) {
504
+
505
+ parent::__construct($controller, $params);
506
+
507
+ // ОБЯЗАТЕЛЬНО: таблица и грид
508
+ $this->table_name = '${name}_items';
509
+ $this->grid_name = 'items'; // файл: backend/grids/grid_items.php
510
+ $this->title = LANG_${NAME}_CP_ITEMS;
511
+
512
+ // Кнопка "Добавить" в тулбаре
513
+ $this->tool_buttons = [
514
+ [
515
+ 'class' => 'add',
516
+ 'title' => LANG_${NAME}_CP_ADD,
517
+ 'href' => $this->cms_template->href_to('items', 'add')
518
+ ]
519
+ ];
520
+
521
+ // Делегировать подэкшены: items/add, items/edit, items/delete
522
+ $this->external_action_prefix = 'items_';
523
+
524
+ // Дополнительные JOIN-ы к запросу:
525
+ // $this->list_callback = function (\\cmsModel $model) {
526
+ // return $model->joinLeft('users u', 'u.id = t.user_id', ['u.nickname as user_nickname']);
527
+ // };
528
+
529
+ // Постобработка данных:
530
+ // $this->items_callback = function ($items) { return $items; };
531
+ }
532
+
533
+ }`;
534
+ }
535
+ // backend/actions/items_add.php — добавление/редактирование с трейтом formItem
536
+ function generateBackendActionItemsAdd(name, Name, NAME) {
537
+ return `<?php
538
+ /**
539
+ * Добавление / редактирование элемента в бэкенде.
540
+ * Trait formItem обрабатывает и 'add' (items/add) и 'edit' (items/edit/ID) — один файл!
541
+ * URL add: /admin/${name}/items/add
542
+ * URL edit: /admin/${name}/items/edit/{id}
543
+ * URL copy: /admin/${name}/items/add/{id}/1
544
+ */
545
+ class action${Name}ItemsAdd extends cmsAction {
546
+
547
+ use icms\\traits\\controllers\\actions\\formItem;
548
+
549
+ public function __construct($controller, $params = []) {
550
+
551
+ parent::__construct($controller, $params);
552
+
553
+ $list_url = $this->cms_template->href_to('items');
554
+
555
+ // ОБЯЗАТЕЛЬНО:
556
+ $this->table_name = '${name}_items';
557
+ $this->form_name = 'item'; // файл: backend/forms/form_item.php
558
+ $this->success_url = $list_url;
559
+
560
+ // Заголовок страницы:
561
+ $this->title = [
562
+ 'add' => LANG_${NAME}_CP_ADD,
563
+ 'edit' => '{title}' // {title} заменяется значением поля title записи
564
+ ];
565
+
566
+ // Хлебные крошки:
567
+ $this->breadcrumbs = [
568
+ [LANG_${NAME}_CP_ITEMS, $list_url],
569
+ isset($params[0]) ? '{title}' : LANG_${NAME}_CP_ADD
570
+ ];
571
+
572
+ // Кнопки Сохранить / Отменить в тулбаре:
573
+ $this->use_default_tool_buttons = true;
574
+
575
+ // Дефолтные значения для новой записи:
576
+ $this->default_item = [
577
+ 'is_pub' => 1,
578
+ 'date_pub' => date('Y-m-d H:i:s')
579
+ ];
580
+
581
+ // Коллбэки после сохранения:
582
+ // $this->add_callback = function($id, $data) {
583
+ // cmsCache::getInstance()->clean('${name}');
584
+ // };
585
+ // $this->update_callback = function($data) {
586
+ // cmsCache::getInstance()->clean('${name}');
587
+ // };
588
+ }
589
+
590
+ }`;
591
+ }
592
+ // backend/grids/grid_items.php — ФУНКЦИЯ, не класс!
593
+ function generateGridItems(name, _Name, _NAME) {
594
+ return `<?php
595
+ // ВАЖНО: грид — это ФУНКЦИЯ, а не класс!
596
+ // Имя функции = grid_ + значение $this->grid_name из экшена
597
+
598
+ function grid_items($controller) {
599
+
600
+ $options = [
601
+ 'is_sortable' => true,
602
+ 'is_filter' => true,
603
+ 'is_pagination' => true,
604
+ 'is_draggable' => false,
605
+ 'is_selectable' => false,
606
+ 'order_by' => 'id',
607
+ 'order_to' => 'desc',
608
+ 'show_id' => true
609
+ // Для drag&drop:
610
+ // 'is_draggable' => true,
611
+ // 'drag_save_url' => href_to('admin', 'reorder', '${name}_items'),
612
+ ];
613
+
614
+ $columns = [
615
+ 'title' => [
616
+ 'title' => LANG_TITLE,
617
+ 'filter' => 'like',
618
+ 'href' => href_to($controller->root_url, 'items', ['edit', '{id}'])
619
+ ],
620
+ 'date_pub' => [
621
+ 'title' => LANG_DATE_PUB,
622
+ 'width' => 150,
623
+ 'filter' => 'date',
624
+ 'handler' => function ($value) {
625
+ return html_date_time($value);
626
+ }
627
+ ],
628
+ 'is_pub' => [
629
+ 'title' => LANG_IS_PUB,
630
+ 'width' => 60,
631
+ 'flag' => true,
632
+ 'flag_toggle' => href_to($controller->root_url, 'toggle_item', ['{id}', '${name}_items', 'is_pub'])
633
+ ]
634
+ ];
635
+
636
+ $actions = [
637
+ [
638
+ 'title' => LANG_EDIT,
639
+ 'icon' => 'pen',
640
+ 'href' => href_to($controller->root_url, 'items', ['edit', '{id}'])
641
+ ],
642
+ [
643
+ 'title' => LANG_DELETE,
644
+ 'class' => 'text-danger',
645
+ 'icon' => 'times-circle',
646
+ 'confirm' => LANG_DELETE_CONFIRM,
647
+ 'href' => href_to($controller->root_url, 'items', ['delete', '{id}'])
648
+ ]
649
+ ];
650
+
651
+ return [
652
+ 'options' => $options,
653
+ 'columns' => $columns,
654
+ 'actions' => $actions
655
+ ];
656
+ }`;
657
+ }
658
+ // backend/forms/form_item.php
659
+ function generateFormItem(Name, _NAME) {
660
+ return `<?php
661
+
662
+ class form${Name}Item extends cmsForm {
663
+
664
+ // $do = 'add' или 'edit' (передаётся из trait formItem)
665
+ public function init($do) {
666
+
667
+ return [
668
+ 'basic' => [
669
+ 'title' => LANG_CP_BASIC,
670
+ 'type' => 'fieldset',
671
+ 'childs' => [
672
+ new fieldString('title', [
673
+ 'title' => LANG_TITLE,
674
+ 'rules' => [['required'], ['max_length', 255]]
675
+ ]),
676
+ new fieldHtml('text', [
677
+ 'title' => LANG_TEXT
678
+ ]),
679
+ new fieldDate('date_pub', [
680
+ 'title' => LANG_DATE_PUB,
681
+ 'default' => date('Y-m-d H:i:s')
682
+ ]),
683
+ new fieldCheckbox('is_pub', [
684
+ 'title' => LANG_IS_PUB,
685
+ 'default' => 1
686
+ ])
687
+ ]
688
+ ]
689
+ ];
690
+ }
691
+
692
+ }`;
693
+ }
694
+ // backend/forms/form_options.php
695
+ function generateFormOptions(Name) {
696
+ return `<?php
697
+
698
+ class form${Name}Options extends cmsForm {
699
+
700
+ public function init() {
701
+ return [
702
+ 'basic' => [
703
+ 'title' => LANG_CP_BASIC,
704
+ 'type' => 'fieldset',
705
+ 'childs' => [
706
+ new fieldNumber('perpage', [
707
+ 'title' => LANG_LIST_LIMIT,
708
+ 'default' => 10,
709
+ 'rules' => [['required'], ['min', 1]]
710
+ ]),
711
+ new fieldCheckbox('use_moderation', [
712
+ 'title' => LANG_USE_MODERATION
713
+ ])
714
+ ]
715
+ ]
716
+ ];
717
+ }
718
+
719
+ }`;
720
+ }
721
+ // widgets/list/widget.php
722
+ function generateWidget(name, Name) {
723
+ return `<?php
724
+ /**
725
+ * Виджет списка элементов.
726
+ * Шаблон: /templates/{theme}/controllers/${name}/list.tpl.php
727
+ */
728
+ class widget${Name}List extends cmsWidget {
729
+
730
+ // Отключить кэш для динамического контента:
731
+ // public $is_cacheable = false;
732
+
733
+ public function run() {
734
+
735
+ $limit = $this->getOption('limit', 5);
736
+
737
+ $controller = cmsCore::getController('${name}');
738
+
739
+ $items = $controller->model
740
+ ->filterEqual('is_pub', 1)
741
+ ->orderBy('date_pub', 'desc')
742
+ ->limit($limit)
743
+ ->get('${name}_items');
744
+
745
+ if (!$items) {
746
+ return false;
747
+ }
748
+
749
+ return [
750
+ 'items' => cmsEventsManager::hook('${name}_before_list', $items),
751
+ 'limit' => $limit
752
+ ];
753
+ }
754
+
755
+ }`;
756
+ }
757
+ // widgets/list/options.form.php
758
+ // Класс: formWidget{Name}ListOptions (не formWidget{Name}List!)
759
+ function generateWidgetOptionsForm(Name) {
760
+ return `<?php
761
+ // Класс опций виджета: formWidget + PascalCase(controller) + PascalCase(widget) + Options
762
+
763
+ class formWidget${Name}ListOptions extends cmsForm {
764
+
765
+ public function init($options = false) {
766
+ return [
767
+ 'woptions' => [
768
+ 'type' => 'fieldset',
769
+ 'title' => LANG_OPTIONS,
770
+ 'childs' => [
771
+ new fieldNumber('options:limit', [
772
+ 'title' => LANG_LIST_LIMIT,
773
+ 'default' => 5,
774
+ 'options' => ['is_abs' => true]
775
+ ]),
776
+ new fieldList('options:is_pub', [
777
+ 'title' => LANG_SHOW,
778
+ 'default' => 1,
779
+ 'items' => [1 => LANG_PUBLISHED_ONLY, 0 => LANG_ALL]
780
+ ])
781
+ ]
782
+ ]
783
+ ];
784
+ }
785
+
786
+ }`;
787
+ }
788
+ // routes.php
789
+ function generateRoutes(name) {
790
+ return `<?php
791
+
792
+ function routes_${name}() {
793
+
794
+ return [
795
+ // /myaddon/item-slug.html → action view
796
+ [
797
+ 'pattern' => '/^([a-z0-9\\-_]+)\\.html$/i',
798
+ 'action' => 'view',
799
+ 1 => 'slug'
800
+ ],
801
+ // /myaddon/page/2 → action index с page
802
+ [
803
+ 'pattern' => '/^page\\/(\\d+)$/i',
804
+ 'action' => 'index',
805
+ 1 => 'page'
806
+ ],
807
+ // /myaddon/ (главная)
808
+ [
809
+ 'pattern' => '/^$/',
810
+ 'action' => 'index'
811
+ ]
812
+ ];
813
+
814
+ }`;
815
+ }