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.
- package/package.json +3 -2
- package/rook-framework/PRD-INSTALL-COMMAND.md +379 -0
- package/rook-framework/PRD.md +1214 -0
- package/rook-framework/README.md +143 -0
- package/rook-framework/assets/rk-accordion.js +99 -0
- package/rook-framework/assets/rk-alert-dialog.js +132 -0
- package/rook-framework/assets/rk-bottom-app-bar.js +88 -0
- package/rook-framework/assets/rk-carousel.js +145 -0
- package/rook-framework/assets/rk-collapsible.js +151 -0
- package/rook-framework/assets/rk-dialog.js +161 -0
- package/rook-framework/assets/rk-drawer.js +214 -0
- package/rook-framework/assets/rk-framework-core.css +2554 -0
- package/rook-framework/assets/rk-framework-tokens.css +101 -0
- package/rook-framework/assets/rk-modal.js +91 -0
- package/rook-framework/assets/rk-popover.js +264 -0
- package/rook-framework/assets/rk-progress.js +81 -0
- package/rook-framework/assets/rk-quantity.js +91 -0
- package/rook-framework/assets/rk-scroll-area.js +286 -0
- package/rook-framework/assets/rk-sheet.js +157 -0
- package/rook-framework/assets/rk-tabs.js +179 -0
- package/rook-framework/assets/rk-toggle.js +153 -0
- package/rook-framework/blocks/rk-accordion.liquid +97 -0
- package/rook-framework/blocks/rk-badge.liquid +103 -0
- package/rook-framework/blocks/rk-button.liquid +166 -0
- package/rook-framework/blocks/rk-divider.liquid +100 -0
- package/rook-framework/blocks/rk-form-field.liquid +120 -0
- package/rook-framework/blocks/rk-icon.liquid +134 -0
- package/rook-framework/blocks/rk-image.liquid +198 -0
- package/rook-framework/blocks/rk-installments.liquid +99 -0
- package/rook-framework/blocks/rk-pix-discount.liquid +99 -0
- package/rook-framework/blocks/rk-price.liquid +128 -0
- package/rook-framework/blocks/rk-quantity.liquid +108 -0
- package/rook-framework/blocks/rk-quick-add.liquid +137 -0
- package/rook-framework/blocks/rk-skeleton.liquid +104 -0
- package/rook-framework/blocks/rk-typography.liquid +183 -0
- package/rook-framework/config/rk-settings_schema.json +259 -0
- package/rook-framework/snippets/rk-accordion.liquid +31 -0
- package/rook-framework/snippets/rk-alert-dialog.liquid +83 -0
- package/rook-framework/snippets/rk-aspect-ratio.liquid +23 -0
- package/rook-framework/snippets/rk-badge.liquid +17 -0
- package/rook-framework/snippets/rk-bottom-app-bar.liquid +51 -0
- package/rook-framework/snippets/rk-button.liquid +49 -0
- package/rook-framework/snippets/rk-card.liquid +64 -0
- package/rook-framework/snippets/rk-carousel.liquid +74 -0
- package/rook-framework/snippets/rk-checkbox.liquid +34 -0
- package/rook-framework/snippets/rk-collapsible.liquid +52 -0
- package/rook-framework/snippets/rk-dialog.liquid +85 -0
- package/rook-framework/snippets/rk-divider.liquid +25 -0
- package/rook-framework/snippets/rk-drawer.liquid +81 -0
- package/rook-framework/snippets/rk-external-assets copy.liquid +33 -0
- package/rook-framework/snippets/rk-external-assets.liquid +68 -0
- package/rook-framework/snippets/rk-form-field.liquid +83 -0
- package/rook-framework/snippets/rk-gap-style.liquid +32 -0
- package/rook-framework/snippets/rk-icon.liquid +28 -0
- package/rook-framework/snippets/rk-image.liquid +60 -0
- package/rook-framework/snippets/rk-input.liquid +35 -0
- package/rook-framework/snippets/rk-installments.liquid +54 -0
- package/rook-framework/snippets/rk-item.liquid +69 -0
- package/rook-framework/snippets/rk-layout-style.liquid +37 -0
- package/rook-framework/snippets/rk-modal.liquid +31 -0
- package/rook-framework/snippets/rk-pix-discount.liquid +34 -0
- package/rook-framework/snippets/rk-popover.liquid +77 -0
- package/rook-framework/snippets/rk-price.liquid +48 -0
- package/rook-framework/snippets/rk-progress.liquid +38 -0
- package/rook-framework/snippets/rk-quantity.liquid +56 -0
- package/rook-framework/snippets/rk-quick-add.liquid +67 -0
- package/rook-framework/snippets/rk-scripts.liquid +17 -0
- package/rook-framework/snippets/rk-scroll-area.liquid +60 -0
- package/rook-framework/snippets/rk-sheet.liquid +86 -0
- package/rook-framework/snippets/rk-size-style.liquid +48 -0
- package/rook-framework/snippets/rk-skeleton.liquid +25 -0
- package/rook-framework/snippets/rk-spacing-padding.liquid +18 -0
- package/rook-framework/snippets/rk-spacing-style.liquid +54 -0
- package/rook-framework/snippets/rk-spinner.liquid +43 -0
- package/rook-framework/snippets/rk-swatch.liquid +33 -0
- package/rook-framework/snippets/rk-table.liquid +44 -0
- package/rook-framework/snippets/rk-tabs.liquid +52 -0
- package/rook-framework/snippets/rk-textarea.liquid +42 -0
- package/rook-framework/snippets/rk-toggle-group.liquid +27 -0
- package/rook-framework/snippets/rk-toggle.liquid +58 -0
- package/rook-framework/snippets/rk-typography.liquid +27 -0
- package/rook-framework/snippets/rk-variables.liquid +74 -0
- package/src/app.js +24 -0
- package/src/commands/InstallCommand.js +133 -0
- package/src/mcp/server.js +111 -1
- package/src/services/FrameworkInstaller.js +379 -0
- package/src/templates/block.liquid.txt +0 -15
- package/src/ui/PromptUI.js +15 -1
- 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 %}
|