@codigodoleo/wp-kit 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (112) hide show
  1. package/.cspell.json +104 -0
  2. package/.editorconfig +13 -0
  3. package/.eslintignore +3 -0
  4. package/.eslintrc.json +24 -0
  5. package/.github/workflows/publish-npm.yml +32 -0
  6. package/.github/workflows/release-please.yml +25 -0
  7. package/.gitkeep +0 -0
  8. package/.husky/_/post-merge +0 -0
  9. package/.prettierignore +4 -0
  10. package/.prettierrc +8 -0
  11. package/.vscode/settings.json +19 -0
  12. package/CHANGELOG.md +47 -0
  13. package/README.md +115 -0
  14. package/VSCODE_EXTENSIONS.md +11 -0
  15. package/bin/index.js +4 -0
  16. package/docs/HOOKS-SYSTEM.md +172 -0
  17. package/git-deploy.sh +74 -0
  18. package/index.php +17 -0
  19. package/lib/cli.js +15 -0
  20. package/lib/commands/init.js +132 -0
  21. package/lib/core/generator.js +261 -0
  22. package/lib/core/hook-manager.js +172 -0
  23. package/lib/core/infer-ci-capabilities.js +119 -0
  24. package/lib/prompts/index.js +105 -0
  25. package/lib/prompts/loadModulePrompts.js +27 -0
  26. package/lib/utils/generate-from-template.js +17 -0
  27. package/lib/utils/git.js +17 -0
  28. package/lib/utils/logger.js +28 -0
  29. package/modules/deploy/index.js +39 -0
  30. package/modules/deploy/prompts.js +27 -0
  31. package/modules/deploy/templates/.github/workflows/ci.yml.hbs +103 -0
  32. package/modules/deploy/templates/.gitlab/gitlab-ci.yml.hbs +154 -0
  33. package/modules/deploy/templates/bitbucket-pipelines.yml.hbs +97 -0
  34. package/modules/docs/index.js +3 -0
  35. package/modules/docs/prompts.js +8 -0
  36. package/modules/docs/templates/README.md.hbs +160 -0
  37. package/modules/docs/templates/docs/Arquitetura.md +399 -0
  38. package/modules/docs/templates/docs/Deploy-Pipeline.md +113 -0
  39. package/modules/docs/templates/docs/Desenvolvimento.md +1116 -0
  40. package/modules/docs/templates/docs/Getting-Started.md +493 -0
  41. package/modules/docs/templates/docs/Infraestrutura.md +211 -0
  42. package/modules/docs/templates/docs/Monitoramento.md +302 -0
  43. package/modules/docs/templates/docs/Sync-the-Production-DB-with-the-Staging-DB.md +8 -0
  44. package/modules/docs/templates/docs/Troubleshooting.md +3 -0
  45. package/modules/git/.commitlintrc.json +136 -0
  46. package/modules/git/.github/PULL_REQUEST_TEMPLATE.md +42 -0
  47. package/modules/git/.gitlab/merge_request_templates/default.md +42 -0
  48. package/modules/git/.gitmessage +29 -0
  49. package/modules/git/.husky/commit-msg +4 -0
  50. package/modules/git/.husky/pre-commit +27 -0
  51. package/modules/git/.lintstagedrc.json +7 -0
  52. package/modules/git/.vscode/commit-instructions.md +59 -0
  53. package/modules/git/.vscode/conventional-commits.code-snippets +62 -0
  54. package/modules/git/.vscode/copilot.json +39 -0
  55. package/modules/git/docs/CONVENTIONAL-COMMITS.md +131 -0
  56. package/modules/git/index.js +137 -0
  57. package/modules/git/prompts.js +23 -0
  58. package/modules/git/templates/.lando.yml.hbs +13 -0
  59. package/modules/git/templates/package.json.hbs +15 -0
  60. package/modules/git/templates/workspace.json.hbs +114 -0
  61. package/modules/lint/.eslintignore +36 -0
  62. package/modules/lint/.eslintrc.json +8 -0
  63. package/modules/lint/.prettierignore +36 -0
  64. package/modules/lint/.prettierrc.json +29 -0
  65. package/modules/lint/.stylelintignore +19 -0
  66. package/modules/lint/.stylelintrc.json +9 -0
  67. package/modules/lint/index.js +15 -0
  68. package/modules/lint/pint.json +26 -0
  69. package/modules/lint/prompts.js +16 -0
  70. package/modules/lint/templates/.lando.yml.hbs +10 -0
  71. package/modules/lint/templates/package.json.hbs +16 -0
  72. package/modules/lint/templates/workspace.json.hbs +56 -0
  73. package/modules/php/index.js +3 -0
  74. package/modules/php/prompts.js +8 -0
  75. package/modules/php/scripts/php-wrapper.sh +38 -0
  76. package/modules/php/scripts/pint-wrapper.sh +44 -0
  77. package/modules/php/templates/.lando.yml.hbs +11 -0
  78. package/modules/php/templates/composer.json.hbs +6 -0
  79. package/modules/php/templates/workspace.json.hbs +74 -0
  80. package/modules/redis/prompts.js +8 -0
  81. package/modules/redis/templates/.lando.yml.hbs +8 -0
  82. package/modules/sage/index.js +20 -0
  83. package/modules/sage/prompts.js +16 -0
  84. package/modules/sage/templates/.lando.yml.hbs +64 -0
  85. package/modules/sage/templates/theme/composer.json.hbs +18 -0
  86. package/modules/sage/templates/theme/package.json.hbs +11 -0
  87. package/modules/sage/templates/theme/style.css.hbs +13 -0
  88. package/modules/sage/templates/theme/vite.config.js.hbs +53 -0
  89. package/modules/sage/templates/workspace.json.hbs +67 -0
  90. package/modules/test-directory/assets/module-file.txt +1 -0
  91. package/modules/test-directory/index.js +19 -0
  92. package/modules/test-directory/prompts.js +8 -0
  93. package/modules/test-directory/test-assets/file1.txt +1 -0
  94. package/modules/test-directory/test-assets/file2.txt +1 -0
  95. package/modules/test-directory/test-assets/subfolder/config.json +4 -0
  96. package/package.json +54 -0
  97. package/release-please-config.json +17 -0
  98. package/server/php/php.ini +48 -0
  99. package/server/www/rocket.conf +283 -0
  100. package/templates/.editorconfig.hbs +39 -0
  101. package/templates/.env.hbs +48 -0
  102. package/templates/.gitignore.hbs +86 -0
  103. package/templates/.lando.yml.hbs +44 -0
  104. package/templates/README.md.hbs +12 -0
  105. package/templates/composer.json.hbs +60 -0
  106. package/templates/package.json.hbs +47 -0
  107. package/templates/server/cmd/install-wp.sh.hbs +58 -0
  108. package/templates/server/www/vhosts.conf.hbs +71 -0
  109. package/templates/workspace.json.hbs +177 -0
  110. package/test-copy-directory.js +43 -0
  111. package/test-overwrite.js +45 -0
  112. package/wp-config.php +190 -0
package/git-deploy.sh ADDED
@@ -0,0 +1,74 @@
1
+ #!/bin/bash
2
+
3
+ # Script para inicializar git, adicionar remote e fazer push forçado
4
+ # Uso: ./git-deploy.sh [remote-url] [branch] [commit-message]
5
+
6
+ set -e # Sai se algum comando falhar
7
+
8
+ # Configurações padrão
9
+ DEFAULT_REMOTE="git@gitlab.com:pipefy/wordpress/web-template.git"
10
+ DEFAULT_BRANCH="main"
11
+ DEFAULT_COMMIT_MESSAGE="Initial commit"
12
+
13
+ # Parâmetros
14
+ REMOTE_URL=${1:-$DEFAULT_REMOTE}
15
+ BRANCH=${2:-$DEFAULT_BRANCH}
16
+ COMMIT_MESSAGE=${3:-$DEFAULT_COMMIT_MESSAGE}
17
+
18
+ echo "🚀 Iniciando processo de deploy..."
19
+ echo "Remote: $REMOTE_URL"
20
+ echo "Branch: $BRANCH"
21
+ echo "Commit message: $COMMIT_MESSAGE"
22
+ echo ""
23
+
24
+ # Verificar se estamos em um diretório com arquivos
25
+ if [ ! "$(ls -A .)" ]; then
26
+ echo "❌ Erro: Diretório vazio. Adicione alguns arquivos antes de executar."
27
+ exit 1
28
+ fi
29
+
30
+ # Inicializar git
31
+ echo "📦 Inicializando repositório Git..."
32
+ git init
33
+
34
+ # Verificar se o remote já existe e remover se necessário
35
+ if git remote get-url origin &>/dev/null; then
36
+ echo "🔄 Removendo remote origin existente..."
37
+ git remote remove origin
38
+ fi
39
+
40
+ # Adicionar remote
41
+ echo "🔗 Adicionando remote origin..."
42
+ git remote add origin "$REMOTE_URL"
43
+
44
+ # Verificar se há arquivos para adicionar
45
+ echo "📋 Adicionando arquivos ao stage..."
46
+ git add .
47
+
48
+ # Verificar se há algo para commitar
49
+ if git diff --cached --quiet; then
50
+ echo "ℹ️ Nenhuma mudança para commitar."
51
+ else
52
+ echo "💾 Criando commit..."
53
+ git commit -m "$COMMIT_MESSAGE"
54
+ fi
55
+
56
+ # Confirmar push forçado
57
+ echo ""
58
+ echo "⚠️ ATENÇÃO: Este comando fará um push FORÇADO para a branch '$BRANCH'."
59
+ echo " Isso pode sobrescrever o histórico existente no repositório remoto."
60
+ echo ""
61
+ read -p "Deseja continuar? (y/N): " -n 1 -r
62
+ echo ""
63
+
64
+ if [[ $REPLY =~ ^[Yy]$ ]]; then
65
+ echo "🚀 Fazendo push forçado para $BRANCH..."
66
+ git push -f origin "$BRANCH"
67
+ echo ""
68
+ echo "✅ Deploy concluído com sucesso!"
69
+ echo "🔗 Repositório: $REMOTE_URL"
70
+ echo "🌿 Branch: $BRANCH"
71
+ else
72
+ echo "❌ Deploy cancelado pelo usuário."
73
+ exit 1
74
+ fi
package/index.php ADDED
@@ -0,0 +1,17 @@
1
+ <?php
2
+ /**
3
+ * Front to the WordPress application. This file doesn't do anything, but loads
4
+ * wp-blog-header.php which does and tells WordPress to load the theme.
5
+ *
6
+ * @package WordPress
7
+ */
8
+
9
+ /**
10
+ * Tells WordPress to load the WordPress theme and output it.
11
+ *
12
+ * @var bool
13
+ */
14
+ define( 'WP_USE_THEMES', true );
15
+
16
+ /** Loads the WordPress Environment and Template */
17
+ require __DIR__ . '/wp/wp-blog-header.php';
package/lib/cli.js ADDED
@@ -0,0 +1,15 @@
1
+ import { Command } from 'commander';
2
+ import { initCommand } from './commands/init.js';
3
+
4
+ const program = new Command();
5
+
6
+ export function runCli() {
7
+ program
8
+ .name('leo-wp')
9
+ .description('WP Kit — Scaffold/Kit WordPress com CI dinâmico (@codigodoleo)')
10
+ .version('0.1.0');
11
+
12
+ program.addCommand(initCommand);
13
+
14
+ program.parse();
15
+ }
@@ -0,0 +1,132 @@
1
+ import { Command } from 'commander';
2
+ import path from 'path';
3
+ import fs from 'fs-extra';
4
+ import { runPrompts } from '../prompts/index.js';
5
+ import { Generator } from '../core/generator.js';
6
+ import { log, color } from '../utils/logger.js';
7
+ import { inferCiCapabilities } from '../core/infer-ci-capabilities.js';
8
+
9
+ export const initCommand = new Command('init')
10
+ .option('-o, --output <dir>', 'Output directory', '__test-sandbox__/test-project')
11
+ .option('--defaults', 'Use default answers without prompting')
12
+ .option('--debug', 'Ativa modo debug para logs detalhados')
13
+ .action(async ({ output, defaults, debug }) => {
14
+ const target = path.resolve(output);
15
+ const { enabledModules, ...context } = await runPrompts({ defaults });
16
+
17
+ // Normalização de chaves para consistência entre módulos
18
+ if (context.enable_sage === true) {
19
+ context.useSage = true;
20
+ }
21
+ if (context.git_provider && !context.gitProvider) {
22
+ context.gitProvider = context.git_provider;
23
+ }
24
+
25
+ // 🔧 Ajustes automáticos para Sage 11
26
+ if (context.useSage && context.sageVersion === '11') {
27
+ context.phpVersion = '8.3';
28
+ context.nodeVersion = '23';
29
+ context.wpCoreVersion = '7.5';
30
+
31
+ log(color.orange, '🔧 Sage 11 selected — automatically adjusting versions:');
32
+ log(color.orange, ' → PHP: 8.3');
33
+ log(color.orange, ' → Node: 23');
34
+ log(color.orange, ' → WP Core: 7.5');
35
+ }
36
+
37
+ context.cwd = target;
38
+
39
+ // 🧠 Inferência de capacidades de CI
40
+ context.ci = inferCiCapabilities(context);
41
+
42
+ log(color.blue, '[ci] Capacidades inferidas:');
43
+ log(color.blue, ` → composer: ${context.ci.build.composer}`);
44
+ log(color.blue, ` → node: ${context.ci.build.node}`);
45
+ log(color.blue, ` → sage: ${context.ci.build.sage}`);
46
+ log(color.blue, ` → deploy.docker: ${context.ci.deploy.docker}`);
47
+ log(color.blue, ` → nodeVersion: ${context.ci.nodeVersion}`);
48
+ if (context.ci.theme.paths.themeRoot) {
49
+ log(color.blue, ` → themeRoot: ${context.ci.theme.paths.themeRoot}`);
50
+ }
51
+
52
+ const generator = new Generator(target);
53
+ await fs.ensureDir(target);
54
+
55
+ await generator.generateFile('.gitignore', context);
56
+ await generator.generateFile('README.md', context);
57
+ await generator.generateFile('.env', context);
58
+ await generator.generateFile('.editorconfig', context);
59
+ await generator.generateFile('server/cmd/install-wp.sh', context);
60
+
61
+ await generator.copyFile('.cspell.json');
62
+
63
+ await generator.copyFile('server/php/php.ini');
64
+
65
+ await generator.copyFile('.gitkeep', null, 'content/uploads/.gitkeep');
66
+ await generator.copyFile('.gitkeep', null, 'content/plugins/.gitkeep');
67
+ await generator.copyFile('.gitkeep', null, 'content/themes/.gitkeep');
68
+ await generator.copyFile('.gitkeep', null, 'content/mu-plugins/.gitkeep');
69
+ await generator.copyFile('.gitkeep', null, 'content/languages/.gitkeep');
70
+
71
+ await fs.chmod(path.join(target, 'content/uploads'), 0o777);
72
+ await fs.chmod(path.join(target, 'server/cmd/install-wp.sh'), 0o755);
73
+
74
+ await generator.generateFile('server/www/vhosts.conf', context);
75
+ if (context.useRocket) {
76
+ await generator.copyFile('server/www/rocket.conf');
77
+ }
78
+
79
+ await generator.copyFile('wp-config.php');
80
+ await generator.copyFile('index.php');
81
+
82
+ log(color.blue, `🧩 Enabled modules: ${enabledModules.join(', ')}`);
83
+
84
+ await generator.generateModularFile({
85
+ baseTemplate: 'package.json',
86
+ modules: enabledModules,
87
+ context,
88
+ format: 'json',
89
+ mergeStrategy: 'concat'
90
+ });
91
+
92
+ await generator.generateModularFile({
93
+ baseTemplate: 'composer.json',
94
+ modules: enabledModules,
95
+ context,
96
+ format: 'json',
97
+ mergeStrategy: 'concat'
98
+ });
99
+
100
+ await generator.generateModularFile({
101
+ baseTemplate: '.lando.yml',
102
+ modules: enabledModules,
103
+ context,
104
+ mergeStrategy: 'concat'
105
+ });
106
+
107
+ await generator.generateModularFile({
108
+ baseTemplate: 'workspace.json',
109
+ outputFile: `${context.projectName}.code-workspace`,
110
+ modules: enabledModules,
111
+ context,
112
+ format: 'json',
113
+ mergeStrategy: 'concat'
114
+ });
115
+
116
+ for (const mod of enabledModules) {
117
+ const modIndexPath = path.resolve('modules', mod, 'index.js');
118
+ if (await fs.pathExists(modIndexPath)) {
119
+ const { setupModule } = await import(modIndexPath);
120
+ if (typeof setupModule === 'function') {
121
+ log(color.blue, `↪ Running setup for module: ${mod}`);
122
+ await setupModule(context, generator);
123
+ }
124
+ }
125
+ }
126
+
127
+ // Executa hooks após todo o setup dos módulos
128
+ log(color.blue, '🔗 Executing post-setup hooks...');
129
+ generator.hooks.doAction('after_setup_complete', context, generator);
130
+
131
+ log(color.green, '✅ Project initialized with templates.');
132
+ });
@@ -0,0 +1,261 @@
1
+ // lib/core/generator.js
2
+ import fs from 'fs-extra';
3
+ import path from 'path';
4
+ import handlebars from 'handlebars';
5
+ import crypto from 'crypto';
6
+ import mergeWith from 'lodash.mergewith';
7
+ import yaml from 'js-yaml';
8
+ import { log, color } from '../utils/logger.js';
9
+ import { HookManager } from './hook-manager.js';
10
+
11
+ export class Generator {
12
+ constructor(baseDir = process.cwd()) {
13
+ this.cwd = baseDir;
14
+ this.hooks = new HookManager();
15
+ }
16
+
17
+ async copyFile(sourcePathRelative, moduleName = null, outputName = "") {
18
+ const baseDir = moduleName
19
+ ? path.resolve('modules', moduleName)
20
+ : path.resolve();
21
+
22
+ outputName = outputName ? outputName : sourcePathRelative;
23
+
24
+ const sourcePath = path.resolve(baseDir, sourcePathRelative);
25
+ const outputPath = path.join(this.cwd, outputName);
26
+
27
+ await fs.copy(sourcePath, outputPath);
28
+ log(color.blue, `📋 File copied: ${outputName}`);
29
+ }
30
+
31
+ /**
32
+ * Copia uma pasta inteira para o diretório de destino
33
+ * @param {string} sourcePathRelative - Caminho relativo da pasta de origem
34
+ * @param {string|null} moduleName - Nome do módulo (opcional)
35
+ * @param {string} outputName - Nome da pasta de destino (opcional)
36
+ * @param {boolean} overwrite - Se deve sobrescrever arquivos existentes (padrão: true)
37
+ */
38
+ async copyDirectory(sourcePathRelative, moduleName = null, outputName = "", overwrite = true) {
39
+ const baseDir = moduleName
40
+ ? path.resolve('modules', moduleName)
41
+ : path.resolve();
42
+
43
+ outputName = outputName ? outputName : sourcePathRelative;
44
+
45
+ const sourcePath = path.resolve(baseDir, sourcePathRelative);
46
+ const outputPath = path.join(this.cwd, outputName);
47
+
48
+ // Verifica se a pasta de origem existe
49
+ if (!await fs.pathExists(sourcePath)) {
50
+ throw new Error(`Source directory not found: ${sourcePath}`);
51
+ }
52
+
53
+ // Verifica se é realmente uma pasta
54
+ const sourceStats = await fs.stat(sourcePath);
55
+ if (!sourceStats.isDirectory()) {
56
+ throw new Error(`Source path is not a directory: ${sourcePath}`);
57
+ }
58
+
59
+ // Se a pasta de destino não existe, copia tudo
60
+ if (!await fs.pathExists(outputPath)) {
61
+ await fs.copy(sourcePath, outputPath);
62
+ log(color.blue, `📁 Directory copied: ${outputName}`);
63
+ return;
64
+ }
65
+
66
+ // Se a pasta de destino existe e overwrite é false, não faz nada
67
+ if (!overwrite) {
68
+ log(color.yellow, `📁 Directory already exists, skipping: ${outputName}`);
69
+ return;
70
+ }
71
+
72
+ // Se a pasta de destino existe e overwrite é true, copia arquivos individualmente
73
+ await this._copyDirectoryContents(sourcePath, outputPath);
74
+ log(color.blue, `📁 Directory contents merged: ${outputName}`);
75
+ }
76
+
77
+ /**
78
+ * Método auxiliar para copiar conteúdo de uma pasta para outra
79
+ * @param {string} sourcePath - Caminho absoluto da pasta de origem
80
+ * @param {string} outputPath - Caminho absoluto da pasta de destino
81
+ */
82
+ async _copyDirectoryContents(sourcePath, outputPath) {
83
+ const items = await fs.readdir(sourcePath);
84
+
85
+ for (const item of items) {
86
+ const sourceItemPath = path.join(sourcePath, item);
87
+ const outputItemPath = path.join(outputPath, item);
88
+ const stats = await fs.stat(sourceItemPath);
89
+
90
+ if (stats.isDirectory()) {
91
+ // Se é uma pasta, cria se não existir e copia recursivamente
92
+ if (!await fs.pathExists(outputItemPath)) {
93
+ await fs.mkdir(outputItemPath, { recursive: true });
94
+ }
95
+ await this._copyDirectoryContents(sourceItemPath, outputItemPath);
96
+ } else {
97
+ // Se é um arquivo, copia (sobrescreve se existir)
98
+ await fs.copy(sourceItemPath, outputItemPath);
99
+ }
100
+ }
101
+ }
102
+
103
+ async generateFile(templatePathRelative, context, moduleName = null, outputName = "") {
104
+ const baseDir = moduleName
105
+ ? path.resolve('modules', moduleName, 'templates')
106
+ : path.resolve('templates');
107
+
108
+ outputName = outputName ? outputName : templatePathRelative;
109
+
110
+ const templatePath = path.resolve(baseDir, templatePathRelative + '.hbs');
111
+ const outputPath = path.join(this.cwd, outputName);
112
+
113
+ const content = await fs.readFile(templatePath, 'utf8');
114
+ const template = handlebars.compile(content);
115
+ const result = template(context);
116
+
117
+ await fs.outputFile(outputPath, result);
118
+ log(color.blue, `📄 File generated: ${outputName}`);
119
+ }
120
+
121
+ /**
122
+ * Gera um arquivo modular combinando templates base e de módulos.
123
+ * @param {Object} options
124
+ * @param {string} options.baseTemplate - Caminho do template base
125
+ * @param {string} options.outputFile - Nome do arquivo de saída
126
+ * @param {string[]} [options.modules=[]] - Lista de módulos
127
+ * @param {Object} [options.context={}] - Contexto para handlebars
128
+ * @param {'json'|'yaml'} [options.format='yaml'] - Formato do arquivo
129
+ * @param {'concat'|'replace'} [options.mergeStrategy='concat'] - Estratégia de merge de arrays
130
+ */
131
+ async generateModularFile({ baseTemplate, outputFile = "", modules = [], context = {}, format = 'yaml', mergeStrategy = 'replace', debug = false }) {
132
+ const baseTemplatePath = path.resolve('templates', baseTemplate + '.hbs');
133
+ const baseRaw = await fs.readFile(baseTemplatePath, 'utf8');
134
+ const compiledBase = handlebars.compile(baseRaw);
135
+ outputFile = outputFile ? outputFile : baseTemplate;
136
+ let mergedOutput;
137
+ try {
138
+ mergedOutput = format === 'json'
139
+ ? JSON.parse(compiledBase(context))
140
+ : yaml.load(compiledBase(context));
141
+ } catch (err) {
142
+ if (debug) {
143
+ log(color.red, '[DEBUG] Erro ao fazer parse do template base:');
144
+ log(color.red, compiledBase(context));
145
+ log(color.red, err);
146
+ }
147
+ throw err;
148
+ }
149
+
150
+ function customizer(objValue, srcValue) {
151
+ if (Array.isArray(objValue) && Array.isArray(srcValue)) {
152
+ return mergeStrategy === 'concat'
153
+ ? objValue.concat(srcValue)
154
+ : srcValue;
155
+ }
156
+ }
157
+
158
+ for (const mod of modules) {
159
+ const modPath = path.resolve('modules', mod, 'templates', path.basename(baseTemplate) + '.hbs');
160
+
161
+ if (await fs.pathExists(modPath)) {
162
+ const modRaw = await fs.readFile(modPath, 'utf8');
163
+ const compiledMod = handlebars.compile(modRaw);
164
+ let modParsed;
165
+ try {
166
+ modParsed = format === 'json'
167
+ ? JSON.parse(compiledMod(context))
168
+ : yaml.load(compiledMod(context));
169
+ } catch (err) {
170
+ if (debug) {
171
+ log(color.red, `[DEBUG] Erro ao fazer parse do template do módulo ${mod}:`);
172
+ log(color.red, compiledMod(context));
173
+ log(color.red, err);
174
+ }
175
+ throw err;
176
+ }
177
+ mergedOutput = mergeWith({}, mergedOutput, modParsed, customizer);
178
+ }
179
+ }
180
+
181
+ const finalOutput = format === 'json'
182
+ ? JSON.stringify(mergedOutput, null, 2)
183
+ : yaml.dump(mergedOutput, { lineWidth: 120 });
184
+
185
+ const outputPath = path.join(this.cwd, outputFile);
186
+ await fs.outputFile(outputPath, finalOutput);
187
+ log(color.blue, `🧩 Modular file generated: ${outputFile}`);
188
+ }
189
+
190
+ async mergeWithExistingFile({ existingFile, moduleTemplatePath, context, format = 'json', moduleName = null, mergeStrategy = 'replace', debug = false }) {
191
+ const existingPath = path.isAbsolute(existingFile)
192
+ ? existingFile
193
+ : path.resolve(this.cwd, existingFile);
194
+
195
+ const fileExists = await fs.pathExists(existingPath);
196
+ if (!fileExists) {
197
+ throw new Error(`File to merge not found: ${existingPath}`);
198
+ }
199
+
200
+ const existingRaw = await fs.readFile(existingPath, 'utf8');
201
+ let existingParsed;
202
+ try {
203
+ existingParsed = format === 'json' ? JSON.parse(existingRaw) : yaml.load(existingRaw);
204
+ } catch (err) {
205
+ if (debug) {
206
+ log(color.red, '[DEBUG] Erro ao fazer parse do arquivo existente:');
207
+ log(color.red, existingRaw);
208
+ log(color.red, err);
209
+ }
210
+ throw err;
211
+ }
212
+
213
+ const moduleBase = moduleName
214
+ ? path.resolve('modules', moduleName, 'templates')
215
+ : path.resolve('templates');
216
+ const templatePath = path.resolve(moduleBase, moduleTemplatePath + '.hbs');
217
+
218
+ const modRaw = await fs.readFile(templatePath, 'utf8');
219
+ const compiledMod = handlebars.compile(modRaw);
220
+ let modParsed;
221
+ try {
222
+ modParsed = format === 'json'
223
+ ? JSON.parse(compiledMod(context))
224
+ : yaml.load(compiledMod(context));
225
+ } catch (err) {
226
+ if (debug) {
227
+ log(color.red, '[DEBUG] Erro ao fazer parse do template do módulo:');
228
+ log(color.red, compiledMod(context));
229
+ log(color.red, err);
230
+ }
231
+ throw err;
232
+ }
233
+
234
+ function customizer(objValue, srcValue) {
235
+ if (Array.isArray(objValue) && Array.isArray(srcValue)) {
236
+ return mergeStrategy === 'concat'
237
+ ? objValue.concat(srcValue)
238
+ : srcValue;
239
+ }
240
+ }
241
+
242
+ const merged = mergeWith({}, existingParsed, modParsed, customizer);
243
+
244
+ const output = format === 'json'
245
+ ? JSON.stringify(merged, null, 2)
246
+ : yaml.dump(merged, { lineWidth: 120 });
247
+
248
+ await fs.outputFile(existingPath, output);
249
+ log(color.blue, `🔀 Merged into: ${existingPath}`);
250
+ }
251
+
252
+ static generateSalts() {
253
+ return [
254
+ 'AUTH_KEY', 'SECURE_AUTH_KEY', 'LOGGED_IN_KEY', 'NONCE_KEY',
255
+ 'AUTH_SALT', 'SECURE_AUTH_SALT', 'LOGGED_IN_SALT', 'NONCE_SALT'
256
+ ].reduce((acc, key) => {
257
+ acc[key] = crypto.randomBytes(64).toString('base64');
258
+ return acc;
259
+ }, {});
260
+ }
261
+ }
@@ -0,0 +1,172 @@
1
+ import { log, color } from '../utils/logger.js';
2
+
3
+ /**
4
+ * Sistema de hooks similar ao WordPress
5
+ * Permite registrar callbacks que serão executados em momentos específicos
6
+ */
7
+ export class HookManager {
8
+ constructor() {
9
+ this.hooks = new Map();
10
+ this.currentHook = null;
11
+ }
12
+
13
+ /**
14
+ * Registra uma função para ser executada em um hook específico
15
+ * @param {string} hookName - Nome do hook
16
+ * @param {Function} callback - Função a ser executada
17
+ * @param {number} priority - Prioridade (padrão: 10, menor número = maior prioridade)
18
+ * @param {number} acceptedArgs - Número de argumentos aceitos pela função
19
+ */
20
+ addAction(hookName, callback, priority = 10, acceptedArgs = 1) {
21
+ if (!this.hooks.has(hookName)) {
22
+ this.hooks.set(hookName, []);
23
+ }
24
+
25
+ const hook = this.hooks.get(hookName);
26
+ hook.push({
27
+ callback,
28
+ priority,
29
+ acceptedArgs
30
+ });
31
+
32
+ // Ordena por prioridade (menor número = maior prioridade)
33
+ hook.sort((a, b) => a.priority - b.priority);
34
+ }
35
+
36
+ /**
37
+ * Registra uma função que retorna um valor (filtro)
38
+ * @param {string} hookName - Nome do hook
39
+ * @param {Function} callback - Função a ser executada
40
+ * @param {number} priority - Prioridade (padrão: 10)
41
+ * @param {number} acceptedArgs - Número de argumentos aceitos pela função
42
+ */
43
+ addFilter(hookName, callback, priority = 10, acceptedArgs = 1) {
44
+ this.addAction(hookName, callback, priority, acceptedArgs);
45
+ }
46
+
47
+ /**
48
+ * Executa todos os callbacks registrados para um hook
49
+ * @param {string} hookName - Nome do hook
50
+ * @param {...any} args - Argumentos para passar aos callbacks
51
+ */
52
+ doAction(hookName, ...args) {
53
+ if (!this.hooks.has(hookName)) {
54
+ return;
55
+ }
56
+
57
+ const hook = this.hooks.get(hookName);
58
+ const previousHook = this.currentHook;
59
+ this.currentHook = hookName;
60
+
61
+ log(color.blue, `🔗 Executing hook: ${hookName} (${hook.length} callbacks)`);
62
+
63
+ for (const { callback, acceptedArgs } of hook) {
64
+ try {
65
+ const callbackArgs = args.slice(0, acceptedArgs);
66
+ callback(...callbackArgs);
67
+ } catch (error) {
68
+ log(color.red, `❌ Error in hook ${hookName}: ${error.message}`);
69
+ }
70
+ }
71
+
72
+ this.currentHook = previousHook;
73
+ }
74
+
75
+ /**
76
+ * Executa um filtro e retorna o valor modificado
77
+ * @param {string} hookName - Nome do hook
78
+ * @param {any} value - Valor inicial
79
+ * @param {...any} args - Argumentos adicionais
80
+ * @returns {any} Valor modificado pelos filtros
81
+ */
82
+ applyFilters(hookName, value, ...args) {
83
+ if (!this.hooks.has(hookName)) {
84
+ return value;
85
+ }
86
+
87
+ const hook = this.hooks.get(hookName);
88
+ const previousHook = this.currentHook;
89
+ this.currentHook = hookName;
90
+
91
+ log(color.blue, `🔗 Applying filter: ${hookName} (${hook.length} callbacks)`);
92
+
93
+ let result = value;
94
+ for (const { callback, acceptedArgs } of hook) {
95
+ try {
96
+ const callbackArgs = [result, ...args].slice(0, acceptedArgs);
97
+ result = callback(...callbackArgs);
98
+ } catch (error) {
99
+ log(color.red, `❌ Error in filter ${hookName}: ${error.message}`);
100
+ }
101
+ }
102
+
103
+ this.currentHook = previousHook;
104
+ return result;
105
+ }
106
+
107
+ /**
108
+ * Remove um callback de um hook
109
+ * @param {string} hookName - Nome do hook
110
+ * @param {Function} callback - Função a ser removida
111
+ * @param {number} priority - Prioridade da função
112
+ */
113
+ removeAction(hookName, callback, priority = 10) {
114
+ if (!this.hooks.has(hookName)) {
115
+ return;
116
+ }
117
+
118
+ const hook = this.hooks.get(hookName);
119
+ const index = hook.findIndex(
120
+ item => item.callback === callback && item.priority === priority
121
+ );
122
+
123
+ if (index !== -1) {
124
+ hook.splice(index, 1);
125
+ }
126
+ }
127
+
128
+ /**
129
+ * Remove um filtro
130
+ * @param {string} hookName - Nome do hook
131
+ * @param {Function} callback - Função a ser removida
132
+ * @param {number} priority - Prioridade da função
133
+ */
134
+ removeFilter(hookName, callback, priority = 10) {
135
+ this.removeAction(hookName, callback, priority);
136
+ }
137
+
138
+ /**
139
+ * Verifica se um hook tem callbacks registrados
140
+ * @param {string} hookName - Nome do hook
141
+ * @returns {boolean}
142
+ */
143
+ hasAction(hookName) {
144
+ return this.hooks.has(hookName) && this.hooks.get(hookName).length > 0;
145
+ }
146
+
147
+ /**
148
+ * Verifica se um filtro tem callbacks registrados
149
+ * @param {string} hookName - Nome do hook
150
+ * @returns {boolean}
151
+ */
152
+ hasFilter(hookName) {
153
+ return this.hasAction(hookName);
154
+ }
155
+
156
+ /**
157
+ * Lista todos os hooks registrados
158
+ * @returns {string[]}
159
+ */
160
+ getHooks() {
161
+ return Array.from(this.hooks.keys());
162
+ }
163
+
164
+ /**
165
+ * Obtém informações sobre um hook específico
166
+ * @param {string} hookName - Nome do hook
167
+ * @returns {Array|null}
168
+ */
169
+ getHook(hookName) {
170
+ return this.hooks.get(hookName) || null;
171
+ }
172
+ }