rook-cli 1.3.2 → 1.3.4

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 (89) hide show
  1. package/package.json +3 -2
  2. package/rook-framework/PRD-INSTALL-COMMAND.md +379 -0
  3. package/rook-framework/PRD.md +1214 -0
  4. package/rook-framework/README.md +143 -0
  5. package/rook-framework/assets/rk-accordion.js +99 -0
  6. package/rook-framework/assets/rk-alert-dialog.js +132 -0
  7. package/rook-framework/assets/rk-bottom-app-bar.js +88 -0
  8. package/rook-framework/assets/rk-carousel.js +145 -0
  9. package/rook-framework/assets/rk-collapsible.js +151 -0
  10. package/rook-framework/assets/rk-dialog.js +161 -0
  11. package/rook-framework/assets/rk-drawer.js +214 -0
  12. package/rook-framework/assets/rk-framework-core.css +2554 -0
  13. package/rook-framework/assets/rk-framework-tokens.css +101 -0
  14. package/rook-framework/assets/rk-modal.js +91 -0
  15. package/rook-framework/assets/rk-popover.js +264 -0
  16. package/rook-framework/assets/rk-progress.js +81 -0
  17. package/rook-framework/assets/rk-quantity.js +91 -0
  18. package/rook-framework/assets/rk-scroll-area.js +286 -0
  19. package/rook-framework/assets/rk-sheet.js +157 -0
  20. package/rook-framework/assets/rk-tabs.js +179 -0
  21. package/rook-framework/assets/rk-toggle.js +153 -0
  22. package/rook-framework/blocks/rk-accordion.liquid +97 -0
  23. package/rook-framework/blocks/rk-badge.liquid +103 -0
  24. package/rook-framework/blocks/rk-button.liquid +166 -0
  25. package/rook-framework/blocks/rk-divider.liquid +100 -0
  26. package/rook-framework/blocks/rk-form-field.liquid +120 -0
  27. package/rook-framework/blocks/rk-icon.liquid +134 -0
  28. package/rook-framework/blocks/rk-image.liquid +198 -0
  29. package/rook-framework/blocks/rk-installments.liquid +99 -0
  30. package/rook-framework/blocks/rk-pix-discount.liquid +99 -0
  31. package/rook-framework/blocks/rk-price.liquid +128 -0
  32. package/rook-framework/blocks/rk-quantity.liquid +108 -0
  33. package/rook-framework/blocks/rk-quick-add.liquid +137 -0
  34. package/rook-framework/blocks/rk-skeleton.liquid +104 -0
  35. package/rook-framework/blocks/rk-typography.liquid +183 -0
  36. package/rook-framework/config/rk-settings_schema.json +259 -0
  37. package/rook-framework/snippets/rk-accordion.liquid +31 -0
  38. package/rook-framework/snippets/rk-alert-dialog.liquid +83 -0
  39. package/rook-framework/snippets/rk-aspect-ratio.liquid +23 -0
  40. package/rook-framework/snippets/rk-badge.liquid +17 -0
  41. package/rook-framework/snippets/rk-bottom-app-bar.liquid +51 -0
  42. package/rook-framework/snippets/rk-button.liquid +49 -0
  43. package/rook-framework/snippets/rk-card.liquid +64 -0
  44. package/rook-framework/snippets/rk-carousel.liquid +74 -0
  45. package/rook-framework/snippets/rk-checkbox.liquid +34 -0
  46. package/rook-framework/snippets/rk-collapsible.liquid +52 -0
  47. package/rook-framework/snippets/rk-dialog.liquid +85 -0
  48. package/rook-framework/snippets/rk-divider.liquid +25 -0
  49. package/rook-framework/snippets/rk-drawer.liquid +81 -0
  50. package/rook-framework/snippets/rk-external-assets copy.liquid +33 -0
  51. package/rook-framework/snippets/rk-external-assets.liquid +68 -0
  52. package/rook-framework/snippets/rk-form-field.liquid +83 -0
  53. package/rook-framework/snippets/rk-gap-style.liquid +32 -0
  54. package/rook-framework/snippets/rk-icon.liquid +28 -0
  55. package/rook-framework/snippets/rk-image.liquid +60 -0
  56. package/rook-framework/snippets/rk-input.liquid +35 -0
  57. package/rook-framework/snippets/rk-installments.liquid +54 -0
  58. package/rook-framework/snippets/rk-item.liquid +69 -0
  59. package/rook-framework/snippets/rk-layout-style.liquid +37 -0
  60. package/rook-framework/snippets/rk-modal.liquid +31 -0
  61. package/rook-framework/snippets/rk-pix-discount.liquid +34 -0
  62. package/rook-framework/snippets/rk-popover.liquid +77 -0
  63. package/rook-framework/snippets/rk-price.liquid +48 -0
  64. package/rook-framework/snippets/rk-progress.liquid +38 -0
  65. package/rook-framework/snippets/rk-quantity.liquid +56 -0
  66. package/rook-framework/snippets/rk-quick-add.liquid +67 -0
  67. package/rook-framework/snippets/rk-scripts.liquid +17 -0
  68. package/rook-framework/snippets/rk-scroll-area.liquid +60 -0
  69. package/rook-framework/snippets/rk-sheet.liquid +86 -0
  70. package/rook-framework/snippets/rk-size-style.liquid +48 -0
  71. package/rook-framework/snippets/rk-skeleton.liquid +25 -0
  72. package/rook-framework/snippets/rk-spacing-padding.liquid +18 -0
  73. package/rook-framework/snippets/rk-spacing-style.liquid +54 -0
  74. package/rook-framework/snippets/rk-spinner.liquid +43 -0
  75. package/rook-framework/snippets/rk-swatch.liquid +33 -0
  76. package/rook-framework/snippets/rk-table.liquid +44 -0
  77. package/rook-framework/snippets/rk-tabs.liquid +52 -0
  78. package/rook-framework/snippets/rk-textarea.liquid +42 -0
  79. package/rook-framework/snippets/rk-toggle-group.liquid +27 -0
  80. package/rook-framework/snippets/rk-toggle.liquid +58 -0
  81. package/rook-framework/snippets/rk-typography.liquid +27 -0
  82. package/rook-framework/snippets/rk-variables.liquid +74 -0
  83. package/src/app.js +24 -0
  84. package/src/commands/InstallCommand.js +133 -0
  85. package/src/mcp/server.js +111 -1
  86. package/src/services/FrameworkInstaller.js +379 -0
  87. package/src/templates/block.liquid.txt +0 -15
  88. package/src/ui/PromptUI.js +15 -1
  89. package/src/utils/logger.js +1 -1
@@ -0,0 +1,133 @@
1
+ /**
2
+ * InstallCommand — Comando "install" do CLI.
3
+ *
4
+ * Orquestra o fluxo de instalação do Rook UI Core Framework:
5
+ * 1. Valida que o diretório é um tema Shopify
6
+ * 2. Detecta instalação prévia
7
+ * 3. Confirma com o usuário (modo interativo)
8
+ * 4. Delega ao FrameworkInstaller
9
+ *
10
+ * Princípio: Inversão de Dependência (DIP) — dependências injetadas
11
+ * Princípio: Responsabilidade Única (SRP) — orquestra apenas o fluxo do comando "install"
12
+ */
13
+
14
+ import path from 'path';
15
+
16
+ export class InstallCommand {
17
+
18
+ /**
19
+ * @param {import('../utils/logger.js').Logger} logger
20
+ * @param {import('../ui/PromptUI.js').PromptUI} promptUI
21
+ * @param {import('../services/FrameworkInstaller.js').FrameworkInstaller} frameworkInstaller
22
+ */
23
+ constructor(logger, promptUI, frameworkInstaller) {
24
+ this.logger = logger;
25
+ this.promptUI = promptUI;
26
+ this.frameworkInstaller = frameworkInstaller;
27
+ }
28
+
29
+ /**
30
+ * Executa o fluxo principal do comando "install".
31
+ *
32
+ * @param {Object} [opcoes={}]
33
+ * @param {string} [opcoes.path] - Diretório do tema Shopify (padrão: cwd)
34
+ * @param {boolean} [opcoes.force] - Sobrescreve sem perguntar
35
+ * @param {boolean} [opcoes.skipLayout] - Não modifica theme.liquid
36
+ * @param {boolean} [opcoes.skipSettings] - Não modifica settings_schema.json
37
+ * @returns {Promise<void>}
38
+ */
39
+ async executar(opcoes = {}) {
40
+ try {
41
+ const themePath = path.resolve(opcoes.path || process.cwd());
42
+ const isHeadless = opcoes.force || opcoes.path;
43
+
44
+ // 1. Validação: é um tema Shopify?
45
+ const isValid = await this.frameworkInstaller.validate(themePath);
46
+
47
+ if (!isValid) {
48
+ if (isHeadless) {
49
+ this.logger.erro(`O diretório "${themePath}" não parece ser um tema Shopify.`);
50
+ process.exitCode = 1;
51
+ return;
52
+ }
53
+
54
+ const continuar = await this.promptUI.confirmar(
55
+ 'Este diretório não parece ser um tema Shopify. Deseja continuar mesmo assim?',
56
+ false
57
+ );
58
+
59
+ if (!continuar) {
60
+ this.logger.aviso('Instalação cancelada.');
61
+ return;
62
+ }
63
+ }
64
+
65
+ // Detectar informações do tema
66
+ const themeInfo = await this.frameworkInstaller.detectThemeInfo(themePath);
67
+ if (themeInfo) {
68
+ this.logger.info(`Tema detectado: ${themeInfo.name} v${themeInfo.version}`);
69
+ }
70
+
71
+ // 2. Detectar instalação prévia
72
+ const jaInstalado = await this.frameworkInstaller.isAlreadyInstalled(themePath);
73
+
74
+ if (jaInstalado) {
75
+ if (isHeadless) {
76
+ this.logger.aviso('Rook UI já instalado. Reinstalando (--force)...');
77
+ } else {
78
+ const reinstalar = await this.promptUI.confirmar(
79
+ 'Rook UI já está instalado neste tema. Deseja reinstalar/atualizar?',
80
+ false
81
+ );
82
+
83
+ if (!reinstalar) {
84
+ this.logger.aviso('Instalação cancelada.');
85
+ return;
86
+ }
87
+ }
88
+ }
89
+
90
+ // 3. Confirmação (modo interativo)
91
+ if (!isHeadless) {
92
+ const confirmar = await this.promptUI.confirmar(
93
+ 'Instalar Rook UI Core Framework neste tema?',
94
+ true
95
+ );
96
+
97
+ if (!confirmar) {
98
+ this.logger.aviso('Instalação cancelada.');
99
+ return;
100
+ }
101
+ }
102
+
103
+ // 4. Execução
104
+ this.logger.destaque('\n ⚡ Instalando Rook UI Core Framework...\n');
105
+
106
+ const resultado = await this.frameworkInstaller.install(themePath, {
107
+ force: opcoes.force || false,
108
+ skipLayout: opcoes.skipLayout || false,
109
+ skipSettings: opcoes.skipSettings || false,
110
+ });
111
+
112
+ // 5. Resumo final
113
+ console.log('');
114
+ this.logger.destaque('═══════════════════════════════════════════');
115
+ this.logger.sucesso('🎉 Rook UI Core Framework instalado com sucesso!');
116
+ this.logger.sutil(` Assets: ${resultado.assets} arquivo(s)`);
117
+ this.logger.sutil(` Snippets: ${resultado.snippets} arquivo(s)`);
118
+ this.logger.sutil(` Blocks: ${resultado.blocks} arquivo(s)`);
119
+ this.logger.sutil(` Settings: ${resultado.settingsSections} seção(ões)`);
120
+ this.logger.sutil(` Layout: ${resultado.layoutPatched ? 'atualizado' : 'sem alteração'}`);
121
+ this.logger.destaque('═══════════════════════════════════════════');
122
+ console.log('');
123
+
124
+ } catch (erro) {
125
+ if (erro.name === 'ExitPromptError') {
126
+ this.logger.aviso('Operação cancelada pelo usuário.');
127
+ return;
128
+ }
129
+ this.logger.erro(`Erro durante a instalação: ${erro.message}`);
130
+ process.exitCode = 1;
131
+ }
132
+ }
133
+ }
package/src/mcp/server.js CHANGED
@@ -43,6 +43,7 @@ import { DownloadService } from '../services/DownloadService.js';
43
43
  import { ScaffoldService } from '../services/ScaffoldService.js';
44
44
  import { FileMapper } from '../filesystem/FileMapper.js';
45
45
  import { ConflictResolver } from '../filesystem/ConflictResolver.js';
46
+ import { FrameworkInstaller } from '../services/FrameworkInstaller.js';
46
47
  import { generateNames } from '../utils/stringUtils.js';
47
48
 
48
49
  import path from 'path';
@@ -69,6 +70,7 @@ const downloadService = new DownloadService(logger, tokenManager);
69
70
  const conflictResolver = new ConflictResolver(logger);
70
71
  const fileMapper = new FileMapper(logger, conflictResolver);
71
72
  const scaffoldService = new ScaffoldService(logger);
73
+ const frameworkInstaller = new FrameworkInstaller(logger);
72
74
 
73
75
  // No modo MCP, o ConflictResolver opera em modo headless (sobrescreve tudo)
74
76
  conflictResolver.definirDecisaoGlobal(true);
@@ -99,6 +101,28 @@ const InstallComponentSchema = z.object({
99
101
  .describe('Obrigatório no Cursor/Claude Desktop: Caminho absoluto para a raiz do projeto (ex: /Users/nome/projeto).'),
100
102
  });
101
103
 
104
+ const InstallFrameworkSchema = z.object({
105
+ projectPath: z
106
+ .string()
107
+ .min(1)
108
+ .describe('Obrigatório: Caminho absoluto para a raiz do tema Shopify (ex: /Users/nome/meu-tema).'),
109
+ force: z
110
+ .boolean()
111
+ .optional()
112
+ .default(true)
113
+ .describe('Sobrescreve arquivos existentes sem perguntar (padrão: true no MCP).'),
114
+ skipLayout: z
115
+ .boolean()
116
+ .optional()
117
+ .default(false)
118
+ .describe('Se true, não modifica o layout/theme.liquid.'),
119
+ skipSettings: z
120
+ .boolean()
121
+ .optional()
122
+ .default(false)
123
+ .describe('Se true, não modifica o config/settings_schema.json.'),
124
+ });
125
+
102
126
  const GenerateScaffoldSchema = z.object({
103
127
  name: z
104
128
  .string()
@@ -267,6 +291,58 @@ async function handleGenerateScaffold(args) {
267
291
  };
268
292
  }
269
293
 
294
+ /**
295
+ * Handler: install_framework
296
+ * Instala o Rook UI Core Framework em um tema Shopify.
297
+ */
298
+ async function handleInstallFramework(args) {
299
+ const { projectPath, force, skipLayout, skipSettings } = InstallFrameworkSchema.parse(args);
300
+
301
+ // Validar tema
302
+ const isValid = await frameworkInstaller.validate(projectPath);
303
+ if (!isValid) {
304
+ return {
305
+ content: [{
306
+ type: 'text',
307
+ text: JSON.stringify({
308
+ success: false,
309
+ message: `O diretório "${projectPath}" não parece ser um tema Shopify válido.`,
310
+ }, null, 2),
311
+ }],
312
+ };
313
+ }
314
+
315
+ // Detectar tema
316
+ const themeInfo = await frameworkInstaller.detectThemeInfo(projectPath);
317
+ const jaInstalado = await frameworkInstaller.isAlreadyInstalled(projectPath);
318
+
319
+ // Executar instalação
320
+ const resultado = await frameworkInstaller.install(projectPath, {
321
+ force,
322
+ skipLayout,
323
+ skipSettings,
324
+ });
325
+
326
+ return {
327
+ content: [{
328
+ type: 'text',
329
+ text: JSON.stringify({
330
+ success: true,
331
+ message: 'Rook UI Core Framework instalado com sucesso.',
332
+ theme: themeInfo || undefined,
333
+ wasReinstall: jaInstalado,
334
+ result: {
335
+ assets: resultado.assets,
336
+ snippets: resultado.snippets,
337
+ blocks: resultado.blocks,
338
+ settingsSections: resultado.settingsSections,
339
+ layoutPatched: resultado.layoutPatched,
340
+ },
341
+ }, null, 2),
342
+ }],
343
+ };
344
+ }
345
+
270
346
  // ═══════════════════════════════════════════════════════════════
271
347
  // Função auxiliar: Download + Distribuição
272
348
  // ═══════════════════════════════════════════════════════════════
@@ -354,6 +430,37 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
354
430
  required: ['name', 'type', 'projectPath'],
355
431
  },
356
432
  },
433
+ {
434
+ name: 'install_framework',
435
+ description:
436
+ 'Instala o Rook UI Core Framework em um tema Shopify. ' +
437
+ 'Copia assets, snippets e blocks, injeta settings no settings_schema.json e configura o layout/theme.liquid.',
438
+ inputSchema: {
439
+ type: 'object',
440
+ properties: {
441
+ projectPath: {
442
+ type: 'string',
443
+ description: 'Obrigatório: Caminho absoluto para a raiz do tema Shopify.',
444
+ },
445
+ force: {
446
+ type: 'boolean',
447
+ description: 'Sobrescreve arquivos existentes sem perguntar (padrão: true).',
448
+ default: true,
449
+ },
450
+ skipLayout: {
451
+ type: 'boolean',
452
+ description: 'Se true, não modifica o layout/theme.liquid.',
453
+ default: false,
454
+ },
455
+ skipSettings: {
456
+ type: 'boolean',
457
+ description: 'Se true, não modifica o config/settings_schema.json.',
458
+ default: false,
459
+ },
460
+ },
461
+ required: ['projectPath'],
462
+ },
463
+ },
357
464
  {
358
465
  name: 'generate_scaffold',
359
466
  description:
@@ -395,11 +502,14 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
395
502
  case 'install_component':
396
503
  return await handleInstallComponent(args);
397
504
 
505
+ case 'install_framework':
506
+ return await handleInstallFramework(args);
507
+
398
508
  case 'generate_scaffold':
399
509
  return await handleGenerateScaffold(args);
400
510
 
401
511
  default:
402
- throw new Error(`Tool desconhecida: "${name}". Tools disponíveis: list_components, install_component, generate_scaffold`);
512
+ throw new Error(`Tool desconhecida: "${name}". Tools disponíveis: list_components, install_component, install_framework, generate_scaffold`);
403
513
  }
404
514
  } catch (erro) {
405
515
  // Retorna erro estruturado para a IA
@@ -0,0 +1,379 @@
1
+ /**
2
+ * FrameworkInstaller — Serviço de instalação do Rook UI Core Framework.
3
+ *
4
+ * Encapsula toda a lógica de instalação do framework em um tema Shopify:
5
+ * 1. Copiar assets (CSS + JS)
6
+ * 2. Copiar snippets (Liquid)
7
+ * 3. Copiar blocks (Liquid)
8
+ * 4. Injetar settings no settings_schema.json
9
+ * 5. Patch do layout/theme.liquid
10
+ *
11
+ * Princípio: Responsabilidade Única (SRP) — só instala o framework
12
+ * Princípio: Inversão de Dependência (DIP) — dependências injetadas
13
+ */
14
+
15
+ import path from 'path';
16
+ import fs from 'fs-extra';
17
+ import { fileURLToPath } from 'url';
18
+
19
+ const __dirname = path.dirname(fileURLToPath(import.meta.url));
20
+ const FRAMEWORK_DIR = path.resolve(__dirname, '../../rook-framework');
21
+
22
+ /** Bloco Liquid injetado no <head> do theme.liquid */
23
+ const HEAD_INJECTION = `
24
+ {%- comment -%} Rook UI Core Framework {%- endcomment -%}
25
+ {%- render 'rk-variables' -%}
26
+ {{ 'rk-framework-tokens.css' | asset_url | stylesheet_tag }}
27
+ {{ 'rk-framework-core.css' | asset_url | stylesheet_tag }}
28
+ {%- render 'rk-external-assets', location: 'head' -%}`;
29
+
30
+ /** Bloco Liquid injetado antes do </body> do theme.liquid */
31
+ const BODY_INJECTION = `
32
+ {%- comment -%} Rook UI Core Framework — Scripts {%- endcomment -%}
33
+ {%- render 'rk-external-assets', location: 'body' -%}
34
+ {%- render 'rk-scripts' -%}`;
35
+
36
+ /** Marcador para detectar instalação prévia */
37
+ const RK_MARKER = 'rk-variables';
38
+
39
+ /** Prefixo das seções no settings_schema */
40
+ const RK_SECTION_PREFIX = 'Rook UI';
41
+
42
+ export class FrameworkInstaller {
43
+
44
+ /**
45
+ * @param {import('../utils/logger.js').Logger} logger
46
+ */
47
+ constructor(logger) {
48
+ this.logger = logger;
49
+ }
50
+
51
+ /**
52
+ * Executa a instalação completa do framework.
53
+ *
54
+ * @param {string} themePath - Caminho raiz do tema Shopify
55
+ * @param {Object} [options={}]
56
+ * @param {boolean} [options.force=false] - Sobrescrever sem perguntar
57
+ * @param {boolean} [options.skipLayout=false] - Não modificar theme.liquid
58
+ * @param {boolean} [options.skipSettings=false] - Não modificar settings_schema.json
59
+ * @returns {Promise<{assets: number, snippets: number, blocks: number, settingsSections: number, layoutPatched: boolean}>}
60
+ */
61
+ async install(themePath, options = {}) {
62
+ const resultado = {
63
+ assets: 0,
64
+ snippets: 0,
65
+ blocks: 0,
66
+ settingsSections: 0,
67
+ layoutPatched: false,
68
+ };
69
+
70
+ // Etapa 1: Assets
71
+ this.logger.destaque('\n [1/5] Copiando assets...');
72
+ resultado.assets = await this._copyDir(
73
+ path.join(FRAMEWORK_DIR, 'assets'),
74
+ path.join(themePath, 'assets'),
75
+ options.force
76
+ );
77
+ this.logger.sucesso(` ${resultado.assets} arquivo(s) copiados para assets/`);
78
+
79
+ // Etapa 2: Snippets
80
+ this.logger.destaque('\n [2/5] Copiando snippets...');
81
+ resultado.snippets = await this._copyDir(
82
+ path.join(FRAMEWORK_DIR, 'snippets'),
83
+ path.join(themePath, 'snippets'),
84
+ options.force,
85
+ (filename) => !filename.includes(' copy')
86
+ );
87
+ this.logger.sucesso(` ${resultado.snippets} arquivo(s) copiados para snippets/`);
88
+
89
+ // Etapa 3: Blocks
90
+ this.logger.destaque('\n [3/5] Copiando blocks...');
91
+ resultado.blocks = await this._copyDir(
92
+ path.join(FRAMEWORK_DIR, 'blocks'),
93
+ path.join(themePath, 'blocks'),
94
+ options.force
95
+ );
96
+ this.logger.sucesso(` ${resultado.blocks} arquivo(s) copiados para blocks/`);
97
+
98
+ // Etapa 4: Settings
99
+ if (!options.skipSettings) {
100
+ this.logger.destaque('\n [4/5] Injetando settings no settings_schema.json...');
101
+ resultado.settingsSections = await this._injectSettings(themePath);
102
+ this.logger.sucesso(` ${resultado.settingsSections} seção(ões) Rook UI injetadas`);
103
+ } else {
104
+ this.logger.sutil('[4/5] Pulando settings (--skip-settings)');
105
+ }
106
+
107
+ // Etapa 5: Layout
108
+ if (!options.skipLayout) {
109
+ this.logger.destaque('\n [5/5] Configurando layout/theme.liquid...');
110
+ resultado.layoutPatched = await this._patchThemeLayout(themePath);
111
+ if (resultado.layoutPatched) {
112
+ this.logger.sucesso(' theme.liquid atualizado com sucesso');
113
+ } else {
114
+ this.logger.sutil('theme.liquid já contém o Rook UI, pulando');
115
+ }
116
+ } else {
117
+ this.logger.sutil('[5/5] Pulando layout (--skip-layout)');
118
+ }
119
+
120
+ return resultado;
121
+ }
122
+
123
+ /**
124
+ * Valida que o diretório é um tema Shopify.
125
+ *
126
+ * @param {string} themePath
127
+ * @returns {Promise<boolean>}
128
+ */
129
+ async validate(themePath) {
130
+ const indicators = [
131
+ 'config/settings_schema.json',
132
+ 'layout/theme.liquid',
133
+ 'snippets',
134
+ 'sections',
135
+ 'templates',
136
+ ];
137
+
138
+ let found = 0;
139
+ for (const indicator of indicators) {
140
+ if (await fs.pathExists(path.join(themePath, indicator))) {
141
+ found++;
142
+ }
143
+ }
144
+
145
+ return found >= 3;
146
+ }
147
+
148
+ /**
149
+ * Detecta se o Rook UI já está instalado.
150
+ *
151
+ * @param {string} themePath
152
+ * @returns {Promise<boolean>}
153
+ */
154
+ async isAlreadyInstalled(themePath) {
155
+ const checks = [
156
+ path.join(themePath, 'snippets', 'rk-variables.liquid'),
157
+ path.join(themePath, 'assets', 'rk-framework-core.css'),
158
+ ];
159
+
160
+ for (const check of checks) {
161
+ if (await fs.pathExists(check)) {
162
+ return true;
163
+ }
164
+ }
165
+
166
+ // Verificar settings_schema.json
167
+ try {
168
+ const settingsPath = path.join(themePath, 'config', 'settings_schema.json');
169
+ if (await fs.pathExists(settingsPath)) {
170
+ const schema = JSON.parse(await fs.readFile(settingsPath, 'utf8'));
171
+ return schema.some(s => s.name && s.name.startsWith(RK_SECTION_PREFIX));
172
+ }
173
+ } catch {
174
+ // Ignora erro de leitura
175
+ }
176
+
177
+ return false;
178
+ }
179
+
180
+ /**
181
+ * Detecta informações do tema (nome e versão).
182
+ *
183
+ * @param {string} themePath
184
+ * @returns {Promise<{name: string, version: string}|null>}
185
+ */
186
+ async detectThemeInfo(themePath) {
187
+ try {
188
+ const settingsPath = path.join(themePath, 'config', 'settings_schema.json');
189
+ const schema = JSON.parse(await fs.readFile(settingsPath, 'utf8'));
190
+ const themeInfo = schema.find(s => s.name === 'theme_info');
191
+
192
+ if (themeInfo) {
193
+ return {
194
+ name: themeInfo.theme_name || 'Desconhecido',
195
+ version: themeInfo.theme_version || '?',
196
+ };
197
+ }
198
+ } catch {
199
+ // Ignora
200
+ }
201
+ return null;
202
+ }
203
+
204
+ /**
205
+ * Copia todos os arquivos de um diretório para outro.
206
+ *
207
+ * @param {string} srcDir - Diretório de origem
208
+ * @param {string} destDir - Diretório de destino
209
+ * @param {boolean} force - Sobrescrever sem perguntar
210
+ * @param {Function} [filter] - Função de filtro (recebe filename, retorna boolean)
211
+ * @returns {Promise<number>} Quantidade de arquivos copiados
212
+ * @private
213
+ */
214
+ async _copyDir(srcDir, destDir, force = false, filter = null) {
215
+ if (!await fs.pathExists(srcDir)) {
216
+ return 0;
217
+ }
218
+
219
+ await fs.ensureDir(destDir);
220
+
221
+ const files = await fs.readdir(srcDir);
222
+ let count = 0;
223
+
224
+ for (const file of files) {
225
+ if (filter && !filter(file)) {
226
+ continue;
227
+ }
228
+
229
+ const srcFile = path.join(srcDir, file);
230
+ const destFile = path.join(destDir, file);
231
+ const stat = await fs.stat(srcFile);
232
+
233
+ if (!stat.isFile()) {
234
+ continue;
235
+ }
236
+
237
+ // Se o arquivo já existe e não é force, pular
238
+ if (!force && await fs.pathExists(destFile)) {
239
+ // Em modo não-force, sobrescreve mesmo assim para garantir atualização
240
+ // O ConflictResolver é usado no nível do comando, não aqui
241
+ }
242
+
243
+ await fs.copy(srcFile, destFile, { overwrite: true });
244
+ this.logger.sutil(`→ ${path.basename(destDir)}/${file}`);
245
+ count++;
246
+ }
247
+
248
+ return count;
249
+ }
250
+
251
+ /**
252
+ * Injeta as seções do Rook UI no settings_schema.json do tema.
253
+ *
254
+ * @param {string} themePath
255
+ * @returns {Promise<number>} Quantidade de seções injetadas
256
+ * @private
257
+ */
258
+ async _injectSettings(themePath) {
259
+ const settingsPath = path.join(themePath, 'config', 'settings_schema.json');
260
+ const rkSettingsPath = path.join(FRAMEWORK_DIR, 'config', 'rk-settings_schema.json');
261
+
262
+ if (!await fs.pathExists(settingsPath)) {
263
+ this.logger.aviso('settings_schema.json não encontrado, pulando injeção de settings');
264
+ return 0;
265
+ }
266
+
267
+ // Backup
268
+ const backupPath = settingsPath + '.rk-backup';
269
+ await fs.copy(settingsPath, backupPath, { overwrite: true });
270
+ this.logger.sutil(`Backup criado: settings_schema.json.rk-backup`);
271
+
272
+ try {
273
+ // Ler ambos os arquivos
274
+ let schema = JSON.parse(await fs.readFile(settingsPath, 'utf8'));
275
+ const rkSections = JSON.parse(await fs.readFile(rkSettingsPath, 'utf8'));
276
+
277
+ // Remover seções Rook UI existentes (para atualização limpa)
278
+ schema = schema.filter(s => !s.name || !s.name.startsWith(RK_SECTION_PREFIX));
279
+
280
+ // Inserir novas seções no final
281
+ schema.push(...rkSections);
282
+
283
+ // Salvar
284
+ await fs.writeFile(settingsPath, JSON.stringify(schema, null, 2), 'utf8');
285
+
286
+ // Validar JSON resultante
287
+ JSON.parse(await fs.readFile(settingsPath, 'utf8'));
288
+
289
+ // Log das seções inseridas
290
+ for (const section of rkSections) {
291
+ const settingsCount = section.settings?.filter(s => s.id).length || 0;
292
+ this.logger.sutil(`→ ${section.name} (${settingsCount} settings)`);
293
+ }
294
+
295
+ return rkSections.length;
296
+ } catch (erro) {
297
+ // Restaurar backup em caso de erro
298
+ this.logger.erro(`Erro ao injetar settings: ${erro.message}`);
299
+ this.logger.info('Restaurando backup...');
300
+ await fs.copy(backupPath, settingsPath, { overwrite: true });
301
+ throw erro;
302
+ }
303
+ }
304
+
305
+ /**
306
+ * Modifica o layout/theme.liquid para carregar o framework.
307
+ *
308
+ * @param {string} themePath
309
+ * @returns {Promise<boolean>} true se modificou, false se já existia
310
+ * @private
311
+ */
312
+ async _patchThemeLayout(themePath) {
313
+ const layoutPath = path.join(themePath, 'layout', 'theme.liquid');
314
+
315
+ if (!await fs.pathExists(layoutPath)) {
316
+ this.logger.aviso('layout/theme.liquid não encontrado, pulando patch');
317
+ return false;
318
+ }
319
+
320
+ // Backup
321
+ const backupPath = layoutPath + '.rk-backup';
322
+ await fs.copy(layoutPath, backupPath, { overwrite: true });
323
+ this.logger.sutil(`Backup criado: theme.liquid.rk-backup`);
324
+
325
+ let content = await fs.readFile(layoutPath, 'utf8');
326
+
327
+ // Detectar instalação prévia
328
+ if (content.includes(RK_MARKER)) {
329
+ // Remover blocos antigos para reinstalar limpo
330
+ content = this._removeExistingRkBlocks(content);
331
+ }
332
+
333
+ // Injetar no <head> — antes de {{ content_for_header }}
334
+ const headAnchor = '{{ content_for_header }}';
335
+ const headFallback = '</head>';
336
+
337
+ if (content.includes(headAnchor)) {
338
+ content = content.replace(headAnchor, HEAD_INJECTION + '\n\n ' + headAnchor);
339
+ this.logger.sutil('→ Injetado no <head>: rk-variables, tokens, core CSS');
340
+ } else if (content.includes(headFallback)) {
341
+ content = content.replace(headFallback, HEAD_INJECTION + '\n ' + headFallback);
342
+ this.logger.sutil('→ Injetado antes de </head> (fallback)');
343
+ } else {
344
+ this.logger.aviso('Não foi possível encontrar ponto de injeção no <head>');
345
+ }
346
+
347
+ // Injetar antes do </body>
348
+ const bodyAnchor = '</body>';
349
+
350
+ if (content.includes(bodyAnchor)) {
351
+ content = content.replace(bodyAnchor, BODY_INJECTION + '\n ' + bodyAnchor);
352
+ this.logger.sutil('→ Injetado antes do </body>: rk-scripts');
353
+ } else {
354
+ this.logger.aviso('Não foi possível encontrar </body> no theme.liquid');
355
+ }
356
+
357
+ await fs.writeFile(layoutPath, content, 'utf8');
358
+ return true;
359
+ }
360
+
361
+ /**
362
+ * Remove blocos Rook UI existentes do theme.liquid para reinstalação limpa.
363
+ *
364
+ * @param {string} content - Conteúdo do theme.liquid
365
+ * @returns {string} Conteúdo limpo
366
+ * @private
367
+ */
368
+ _removeExistingRkBlocks(content) {
369
+ // Remover bloco do <head>
370
+ const headBlockRegex = /\n?\s*\{%-?\s*comment\s*-?%\}\s*Rook UI Core Framework\s*\{%-?\s*endcomment\s*-?%\}[\s\S]*?\{%-?\s*render\s+'rk-external-assets'[^%]*%\}\s*\n?/g;
371
+ content = content.replace(headBlockRegex, '\n');
372
+
373
+ // Remover bloco do </body>
374
+ const bodyBlockRegex = /\n?\s*\{%-?\s*comment\s*-?%\}\s*Rook UI Core Framework — Scripts\s*\{%-?\s*endcomment\s*-?%\}[\s\S]*?\{%-?\s*render\s+'rk-scripts'\s*-?%\}\s*\n?/g;
375
+ content = content.replace(bodyBlockRegex, '\n');
376
+
377
+ return content;
378
+ }
379
+ }
@@ -58,18 +58,3 @@
58
58
  }
59
59
  {% endschema %}
60
60
 
61
- {% javascript %}
62
- class {{PascalName}}Block extends HTMLElement {
63
- connectedCallback() {
64
- this.addEventListener('click', this.handleClick.bind(this));
65
- }
66
-
67
- handleClick() {
68
- console.log('Block clicked:', this.dataset.blockId);
69
- }
70
- }
71
-
72
- if (!customElements.get('{{kebabName}}-block')) {
73
- customElements.define('{{kebabName}}-block', {{PascalName}}Block);
74
- }
75
- {% endjavascript %}