@oktis-works/cms 0.2.0 → 0.3.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 (85) hide show
  1. package/dist/assets.d.ts +23 -6
  2. package/dist/assets.d.ts.map +1 -1
  3. package/dist/assets.js +262 -87
  4. package/dist/assets.js.map +1 -1
  5. package/dist/bluegreen.d.ts.map +1 -1
  6. package/dist/bluegreen.js +75 -52
  7. package/dist/bluegreen.js.map +1 -1
  8. package/dist/commands.d.ts.map +1 -1
  9. package/dist/commands.js +269 -131
  10. package/dist/commands.js.map +1 -1
  11. package/dist/config-wizard.js +34 -36
  12. package/dist/config-wizard.js.map +1 -1
  13. package/dist/deploy-target.d.ts +20 -0
  14. package/dist/deploy-target.d.ts.map +1 -0
  15. package/dist/deploy-target.js +90 -0
  16. package/dist/deploy-target.js.map +1 -0
  17. package/dist/deploy.d.ts +20 -0
  18. package/dist/deploy.d.ts.map +1 -0
  19. package/dist/deploy.js +154 -0
  20. package/dist/deploy.js.map +1 -0
  21. package/dist/docker.d.ts +16 -0
  22. package/dist/docker.d.ts.map +1 -1
  23. package/dist/docker.js +25 -0
  24. package/dist/docker.js.map +1 -1
  25. package/dist/docs-updater.d.ts +10 -0
  26. package/dist/docs-updater.d.ts.map +1 -0
  27. package/dist/docs-updater.js +45 -0
  28. package/dist/docs-updater.js.map +1 -0
  29. package/dist/doctor.js +12 -12
  30. package/dist/doctor.js.map +1 -1
  31. package/dist/env-file.js +1 -1
  32. package/dist/env-file.js.map +1 -1
  33. package/dist/env-schema.d.ts.map +1 -1
  34. package/dist/env-schema.js +64 -64
  35. package/dist/env-schema.js.map +1 -1
  36. package/dist/extension-scaffold.js +5 -5
  37. package/dist/extension-scaffold.js.map +1 -1
  38. package/dist/guards.js +8 -8
  39. package/dist/guards.js.map +1 -1
  40. package/dist/history.d.ts +48 -0
  41. package/dist/history.d.ts.map +1 -0
  42. package/dist/history.js +104 -0
  43. package/dist/history.js.map +1 -0
  44. package/dist/installer.js +9 -9
  45. package/dist/installer.js.map +1 -1
  46. package/dist/npm-registry.js +1 -1
  47. package/dist/npm-registry.js.map +1 -1
  48. package/dist/pm2.d.ts +18 -0
  49. package/dist/pm2.d.ts.map +1 -0
  50. package/dist/pm2.js +179 -0
  51. package/dist/pm2.js.map +1 -0
  52. package/dist/project-config.js +1 -1
  53. package/dist/project-config.js.map +1 -1
  54. package/dist/prompt.d.ts +27 -4
  55. package/dist/prompt.d.ts.map +1 -1
  56. package/dist/prompt.js +254 -29
  57. package/dist/prompt.js.map +1 -1
  58. package/dist/redeploy.d.ts +6 -1
  59. package/dist/redeploy.d.ts.map +1 -1
  60. package/dist/redeploy.js +137 -74
  61. package/dist/redeploy.js.map +1 -1
  62. package/dist/rollback.d.ts +15 -0
  63. package/dist/rollback.d.ts.map +1 -0
  64. package/dist/rollback.js +170 -0
  65. package/dist/rollback.js.map +1 -0
  66. package/dist/scaffold-docs.d.ts +5 -0
  67. package/dist/scaffold-docs.d.ts.map +1 -1
  68. package/dist/scaffold-docs.js +61 -19
  69. package/dist/scaffold-docs.js.map +1 -1
  70. package/dist/scaffold.d.ts +3 -1
  71. package/dist/scaffold.d.ts.map +1 -1
  72. package/dist/scaffold.js +25 -16
  73. package/dist/scaffold.js.map +1 -1
  74. package/dist/simple.d.ts +31 -0
  75. package/dist/simple.d.ts.map +1 -0
  76. package/dist/simple.js +200 -0
  77. package/dist/simple.js.map +1 -0
  78. package/dist/start.js +1 -1
  79. package/dist/theme-build.js +3 -3
  80. package/dist/theme-build.js.map +1 -1
  81. package/dist/update.d.ts +23 -15
  82. package/dist/update.d.ts.map +1 -1
  83. package/dist/update.js +211 -66
  84. package/dist/update.js.map +1 -1
  85. package/package.json +7 -7
package/dist/commands.js CHANGED
@@ -20,18 +20,18 @@ async function prepareDb(options) {
20
20
  const { ensureCoreSchema, resolveTenantId, seedCoreData } = await import('@oktis-works/database');
21
21
  const applied = await ensureCoreSchema();
22
22
  if (applied) {
23
- console.log('✓ schema core aplicado (banco inicializado)');
23
+ console.log('✓ core schema applied (database initialized)');
24
24
  }
25
25
  // Seed idempotente (roles da política + settings gerais): roda em qualquer
26
26
  // banco — sem ele, o primeiro usuário registra e cai em 403 em tudo.
27
27
  try {
28
28
  const seeded = await seedCoreData();
29
29
  if (seeded.roles > 0 || seeded.settings > 0) {
30
- console.log(`✓ seed: ${seeded.roles} roles, ${seeded.settings} settings criados`);
30
+ console.log(`✓ seed: ${seeded.roles} roles, ${seeded.settings} settings created`);
31
31
  }
32
32
  }
33
33
  catch (error) {
34
- console.warn('⚠ seed de dados padrão falhou:', error instanceof Error ? error.message : error);
34
+ console.warn('⚠ default data seed failed:', error instanceof Error ? error.message : error);
35
35
  }
36
36
  return resolveTenantId(getTenantId(options));
37
37
  }
@@ -51,9 +51,11 @@ export const commands = [
51
51
  handler: async (args, options) => {
52
52
  const { scaffoldProject, installProjectDeps } = await import('./scaffold.js');
53
53
  const { DEFAULT_DOCS_LANG, parseDocsLang } = await import('./scaffold-docs.js');
54
- // Idioma da documentação gerada (.md apenas — o código nunca muda de
55
- // língua). `--lang en|pt` decide sem TTY (CI/script); sem a flag, com
56
- // TTY, um menu pergunta; sem TTY e sem flag, o default é inglês.
54
+ const { Prompt } = await import('./prompt.js');
55
+ const prompt = new Prompt();
56
+ // Language of the generated docs (.md only — the code never changes
57
+ // language). `--lang en|pt` decides without a TTY (CI/script); with a TTY
58
+ // and no flag, an arrow menu asks; without either, English is the default.
57
59
  const rawLang = options['lang'];
58
60
  let lang;
59
61
  if (rawLang !== undefined && rawLang !== 'true') {
@@ -66,31 +68,43 @@ export const commands = [
66
68
  else if (rawLang === 'true') {
67
69
  throw new Error('--lang requires a value: en or pt');
68
70
  }
69
- else {
70
- const { select } = await import('./prompt.js');
71
- lang = await select('Idioma da documentação (README.md, PLUGIN.md, THEME.md)', [
71
+ else if (prompt.interactive) {
72
+ lang = await prompt.select('Docs language (README.md, PLUGIN.md, THEME.md)', [
72
73
  { value: 'en', label: 'English', hint: 'default' },
73
74
  { value: 'pt', label: 'Português (Brasil)' },
74
75
  ], { defaultValue: DEFAULT_DOCS_LANG });
75
76
  }
76
- // O arg posicional tem prioridade sobre -d: `init cms-teste` deve criar
77
- // o diretório cms-teste (o default '.' injetado pelo parser engolia args[0]).
78
- const target = args[0] ?? options['dir'] ?? '.';
79
- const name = args[1] ?? basename(resolve(target));
80
- const root = await scaffoldProject(target, name, lang);
81
- // Zero config: instala as dependências já no init (bun, ou npm sem bun).
82
- console.log('Instalando dependências do projeto...');
83
- const install = installProjectDeps(root);
84
- if (install.ok) {
85
- console.log(`✓ dependências instaladas via ${install.tool}`);
86
- }
87
77
  else {
88
- console.warn(`⚠ instalação automática falhou via ${install.tool}. Rode manualmente em ${root}:`);
89
- console.warn(' bun install (ou: npm install)');
90
- if (install.output) {
91
- console.warn(install.output.split('\n').slice(-5).join('\n'));
92
- }
78
+ lang = DEFAULT_DOCS_LANG;
79
+ }
80
+ // The positional arg wins over -d: `init my-site` must create my-site
81
+ // (the parser's default '.' would swallow args[0]). Without either, an
82
+ // interactive run asks where to put the project.
83
+ let target = args[0] ?? options['dir'];
84
+ if (target === undefined && prompt.interactive) {
85
+ target = await prompt.ask('Project directory', { default: '.' });
93
86
  }
87
+ target ??= '.';
88
+ const name = args[1] ?? basename(resolve(target));
89
+ const root = resolve(target);
90
+ // Zero config: dependencies are installed right in `init` (bun, or npm
91
+ // when bun is missing) — and BEFORE the summary, so the next steps are
92
+ // the last thing on screen.
93
+ await scaffoldProject(target, name, lang, async () => {
94
+ console.log('Installing project dependencies...');
95
+ const install = installProjectDeps(root);
96
+ if (install.ok) {
97
+ console.log(`✓ dependencies installed with ${install.tool}`);
98
+ }
99
+ else {
100
+ console.warn(`⚠ automatic install failed with ${install.tool}. Run it manually in ${root}:`);
101
+ console.warn(' bun install (or: npm install)');
102
+ if (install.output) {
103
+ console.warn(install.output.split('\n').slice(-5).join('\n'));
104
+ }
105
+ }
106
+ });
107
+ prompt.close();
94
108
  },
95
109
  },
96
110
  {
@@ -110,7 +124,7 @@ export const commands = [
110
124
  },
111
125
  {
112
126
  name: 'stop',
113
- description: 'Stop apps started by `start` (via pidfile ou detecção de processos)',
127
+ description: 'Stop apps started by `start` (via pidfile or process detection)',
114
128
  options: [],
115
129
  handler: async () => {
116
130
  const { stopAll, findRunningApps, clearPids } = await import('./runtime-state.js');
@@ -120,24 +134,24 @@ export const commands = [
120
134
  for (const proc of fallback) {
121
135
  try {
122
136
  process.kill(proc.pid, 'SIGTERM');
123
- console.log(`[stop] ${proc.label} (pid ${proc.pid}) finalizado`);
137
+ console.log(`[stop] ${proc.label} (pid ${proc.pid}) finished`);
124
138
  }
125
139
  catch {
126
- console.error(`[stop] falha ao finalizar ${proc.label} (pid ${proc.pid})`);
140
+ console.error(`[stop] failed to stop ${proc.label} (pid ${proc.pid})`);
127
141
  }
128
142
  }
129
143
  if (fallback.length === 0) {
130
- console.log('Nenhum app rodando.');
144
+ console.log('No apps running.');
131
145
  }
132
146
  clearPids();
133
147
  return;
134
148
  }
135
149
  for (const result of results) {
136
150
  if (result.stopped) {
137
- console.log(`[stop] ${result.label} (pid ${result.pid}) finalizado`);
151
+ console.log(`[stop] ${result.label} (pid ${result.pid}) finished`);
138
152
  }
139
153
  else {
140
- console.log(`[stop] ${result.label} (pid ${result.pid}) não estava mais ativo`);
154
+ console.log(`[stop] ${result.label} (pid ${result.pid}) was no longer running`);
141
155
  }
142
156
  }
143
157
  clearPids();
@@ -145,27 +159,27 @@ export const commands = [
145
159
  },
146
160
  {
147
161
  name: 'status',
148
- description: 'Show status of apps started by `start` + resumo do projeto',
162
+ description: 'Show status of apps started by `start` + project summary',
149
163
  options: [],
150
164
  handler: async () => {
151
165
  const { statusAll } = await import('./runtime-state.js');
152
166
  const { loadProjectConfig } = await import('./project-config.js');
153
167
  const config = loadProjectConfig();
154
168
  const processes = statusAll();
155
- console.log(`Projeto "${config.name}" — ports api:${config.ports.api} admin:${config.ports.admin} web:${config.ports.web}`);
156
- console.log(`Tema ativo: ${config.activeTheme || '(não definido)'}`);
169
+ console.log(`Project "${config.name}" — ports api:${config.ports.api} admin:${config.ports.admin} web:${config.ports.web}`);
170
+ console.log(`Active theme: ${config.activeTheme || '(not set)'}`);
157
171
  if (processes.length === 0) {
158
- console.log('Nenhum pidfile encontrado (use `okcms start`).');
172
+ console.log('No pidfile found (use `okcms start`).');
159
173
  return;
160
174
  }
161
175
  for (const proc of processes) {
162
- console.log(` ${proc.alive ? '●' : '○'} ${proc.label.padEnd(6)} pid=${proc.pid} iniciado=${proc.startedAt}`);
176
+ console.log(` ${proc.alive ? '●' : '○'} ${proc.label.padEnd(6)} pid=${proc.pid} started=${proc.startedAt}`);
163
177
  }
164
178
  },
165
179
  },
166
180
  {
167
181
  name: 'doctor',
168
- description: 'Diagnóstico do ambiente: node, .env, database, config, docker/compose/lane/proxy',
182
+ description: 'Environment diagnostics: node, .env, database, config, docker/compose/lane/proxy',
169
183
  options: [],
170
184
  handler: async () => {
171
185
  const { runDoctorChecks } = await import('./doctor.js');
@@ -178,32 +192,32 @@ export const commands = [
178
192
  failed++;
179
193
  }
180
194
  if (failed > 0) {
181
- console.error(`doctor: ${failed} problema(s) obrigatório(s) encontrado(s)`);
195
+ console.error(`doctor: ${failed} required problem(s) found`);
182
196
  process.exitCode = 1;
183
197
  }
184
198
  else {
185
- console.log('Ambiente OK.');
199
+ console.log('Environment OK.');
186
200
  }
187
201
  },
188
202
  },
189
203
  {
190
204
  name: 'plugin:create',
191
- description: 'Scaffold de um plugin mínimo e compatível (--name)',
205
+ description: 'Scaffold a minimal compatible plugin (--name)',
192
206
  options: [
193
- { name: 'name', alias: 'n', description: 'Nome do plugin', required: true },
194
- { name: 'dir', alias: 'd', description: 'Diretório base (default: ./plugins)', required: false },
207
+ { name: 'name', alias: 'n', description: 'Plugin name', required: true },
208
+ { name: 'dir', alias: 'd', description: 'Base directory (default: ./plugins)', required: false },
195
209
  ],
196
210
  handler: async (_args, options) => {
197
211
  const { scaffoldPlugin } = await import('./extension-scaffold.js');
198
212
  const name = options['name'];
199
213
  if (!name) {
200
- console.error('Nome do plugin é obrigatório (--name)');
214
+ console.error('Plugin name is required (--name)');
201
215
  process.exit(1);
202
216
  }
203
217
  try {
204
218
  const result = scaffoldPlugin(name, options['dir']);
205
- console.log(`Plugin criado em ${result.dir}`);
206
- console.log(`Compatibilidade validada contra CMS ${result.manifest['compatibility'] ? '' : ''}(manifest.json)`);
219
+ console.log(`Plugin created at ${result.dir}`);
220
+ console.log(`Compatibility validated against CMS ${result.manifest['compatibility'] ? '' : ''}(manifest.json)`);
207
221
  }
208
222
  catch (error) {
209
223
  console.error(error instanceof Error ? error.message : String(error));
@@ -213,27 +227,27 @@ export const commands = [
213
227
  },
214
228
  {
215
229
  name: 'theme:create',
216
- description: 'Scaffold de um tema mínimo e compatível (--name --style css|scss|tailwind)',
230
+ description: 'Scaffold a minimal compatible theme (--name --style css|scss|tailwind)',
217
231
  options: [
218
- { name: 'name', alias: 'n', description: 'Nome do tema', required: true },
219
- { name: 'dir', alias: 'd', description: 'Diretório base (default: ./themes)', required: false },
220
- { name: 'style', alias: 's', description: 'Engine de estilo: css|scss|tailwind', required: false },
232
+ { name: 'name', alias: 'n', description: 'Theme name', required: true },
233
+ { name: 'dir', alias: 'd', description: 'Base directory (default: ./themes)', required: false },
234
+ { name: 'style', alias: 's', description: 'Style engine: css|scss|tailwind', required: false },
221
235
  ],
222
236
  handler: async (_args, options) => {
223
237
  const { scaffoldTheme } = await import('./extension-scaffold.js');
224
238
  const name = options['name'];
225
239
  const style = (options['style'] ?? 'css');
226
240
  if (!name) {
227
- console.error('Nome do tema é obrigatório (--name)');
241
+ console.error('Theme name is required (--name)');
228
242
  process.exit(1);
229
243
  }
230
244
  if (!['css', 'scss', 'tailwind'].includes(style)) {
231
- console.error('--style deve ser css|scss|tailwind');
245
+ console.error('--style must be css|scss|tailwind');
232
246
  process.exit(1);
233
247
  }
234
248
  try {
235
249
  const result = scaffoldTheme(name, options['dir'], style);
236
- console.log(`Tema criado em ${result.dir} (engine: ${style})`);
250
+ console.log(`Theme created at ${result.dir} (engine: ${style})`);
237
251
  }
238
252
  catch (error) {
239
253
  console.error(error instanceof Error ? error.message : String(error));
@@ -243,16 +257,16 @@ export const commands = [
243
257
  },
244
258
  {
245
259
  name: 'theme:build',
246
- description: 'Compila estilos do tema (SCSS/Tailwind -> dist/theme.css com isolamento [data-theme])',
260
+ description: 'Compile theme styles (SCSS/Tailwind -> dist/theme.css with [data-theme] isolation)',
247
261
  options: [
248
- { name: 'name', alias: 'n', description: 'Nome do tema', required: true },
249
- { name: 'themes-dir', alias: 'd', description: 'Diretório de temas (default: ./themes)', required: false },
262
+ { name: 'name', alias: 'n', description: 'Theme name', required: true },
263
+ { name: 'themes-dir', alias: 'd', description: 'Themes directory (default: ./themes)', required: false },
250
264
  ],
251
265
  handler: async (_args, options) => {
252
266
  const { join } = await import('node:path');
253
267
  const name = options['name'];
254
268
  if (!name) {
255
- console.error('--name obrigatório');
269
+ console.error('--name required');
256
270
  process.exit(1);
257
271
  }
258
272
  const themesDir = options['themes-dir'] ?? join(process.cwd(), 'themes');
@@ -260,7 +274,7 @@ export const commands = [
260
274
  const { buildThemeStylesOnDisk } = await import('./theme-build.js');
261
275
  const outcome = await buildThemeStylesOnDisk(themesDir, name);
262
276
  if (!outcome.entry)
263
- console.log(`[${name}] sem entrada de estilo (${outcome.engine})`);
277
+ console.log(`[${name}] no style entry (${outcome.engine})`);
264
278
  for (const warning of outcome.warnings)
265
279
  console.warn(`warn: ${warning}`);
266
280
  console.log(`[${name}] ${outcome.engine} -> ${outcome.output} (${outcome.bytes} bytes) isolated=${outcome.isolated}`);
@@ -273,9 +287,9 @@ export const commands = [
273
287
  },
274
288
  {
275
289
  name: 'build',
276
- description: 'Build dos apps do projeto via bun filters (--apps api,admin,web)',
290
+ description: 'Build project apps via bun filters (--apps api,admin,web)',
277
291
  options: [
278
- { name: 'apps', alias: 'a', description: 'Apps a buildar (default: api,admin,web)', required: false },
292
+ { name: 'apps', alias: 'a', description: 'Apps to build (default: api,admin,web)', required: false },
279
293
  ],
280
294
  handler: async (_args, options) => {
281
295
  const { runProjectBuild, isWorkspaceProject } = await import('./build.js');
@@ -283,8 +297,8 @@ export const commands = [
283
297
  // OkCMS vêm pré-compilados do npm — o `bun run --filter` falharia com
284
298
  // "No packages matched the filter".
285
299
  if (!isWorkspaceProject(process.cwd())) {
286
- console.log('Apps do OkCMS vêm pré-compilados do npm — nada a buildar neste projeto.');
287
- console.log('Para compilar estilos de um tema use: okcms theme:build --name <tema>');
300
+ console.log('OkCMS apps come pre-built from npm — nothing to build in this project.');
301
+ console.log('To compile theme styles use: okcms theme:build --name <theme>');
288
302
  return;
289
303
  }
290
304
  const appsArg = options['apps'];
@@ -300,20 +314,20 @@ export const commands = [
300
314
  console.error(step.output.split('\n').slice(-10).join('\n'));
301
315
  }
302
316
  }
303
- console.log(ok ? `Build concluído em ${Date.now() - started}ms.` : 'Build falhou.');
317
+ console.log(ok ? `Build completed in ${Date.now() - started}ms.` : 'Build failed.');
304
318
  process.exitCode = ok ? 0 : 1;
305
319
  },
306
320
  },
307
321
  {
308
322
  name: 'prerender',
309
- description: 'Prerender estático das páginas publicadas para o tema ativo (best-effort)',
323
+ description: 'Statically prerender published pages for the active theme (best-effort)',
310
324
  options: [
311
- { name: 'out', alias: 'o', description: 'Diretório de saída (default: ./.prerender)', required: false },
325
+ { name: 'out', alias: 'o', description: 'Output directory (default: ./.prerender)', required: false },
312
326
  ],
313
327
  handler: async (_args, options) => {
314
328
  const { join } = await import('node:path');
315
329
  if (!process.env['DATABASE_URL']) {
316
- console.log('[prerender] DATABASE_URL não configurado — pulando (best-effort).');
330
+ console.log('[prerender] DATABASE_URL not set — skipping (best-effort).');
317
331
  return;
318
332
  }
319
333
  const outDir = options['out'] ?? join(process.cwd(), '.prerender');
@@ -326,27 +340,27 @@ export const commands = [
326
340
  path: `/${content.slug}`,
327
341
  render: async () => `<main data-type="${content.type}" data-slug="${content.slug}"></main>`,
328
342
  })), outDir);
329
- console.log(`[prerender] ${result.pages} página(s) geradas em ${outDir}`);
343
+ console.log(`[prerender] ${result.pages} page(s) generated at ${outDir}`);
330
344
  if (result.failed.length > 0) {
331
345
  const failedPaths = result.failed.map((failed) => failed.path).join(', ');
332
- console.warn(`[prerender] ${result.failed.length} falha(s): ${failedPaths}`);
346
+ console.warn(`[prerender] ${result.failed.length} failure(s): ${failedPaths}`);
333
347
  }
334
348
  }
335
349
  catch (error) {
336
- console.warn(`[prerender] indisponível (${error instanceof Error ? error.message : String(error)}) — pulando.`);
350
+ console.warn(`[prerender] unavailable (${error instanceof Error ? error.message : String(error)}) — skipping.`);
337
351
  }
338
352
  },
339
353
  },
340
354
  {
341
355
  name: 'config',
342
- description: 'Wizard das variáveis de ambiente (.env) — interativo, ou --list / --set CHAVE=valor',
356
+ description: 'Environment variables wizard (.env) — interactive, or --list / --set KEY=value',
343
357
  options: [
344
- { name: 'set', alias: 's', description: 'Define uma chave (repita para várias): --set PORT=3000', required: false, repeatable: true },
345
- { name: 'list', alias: 'l', description: 'Lista as chaves do .env (segredos mascarados)', required: false },
346
- { name: 'show-secrets', alias: 'S', description: 'Com --list, mostra segredos sem mascarar', required: false },
347
- { name: 'non-interactive', alias: 'n', description: 'Não espera TTY (CI) — saída de --list', required: false },
348
- { name: 'section', alias: 'x', description: 'Abre direto numa seção (app|database|redis|auth|storage|worker|cache|ports|theme|deploy)', required: false },
349
- { name: 'force', alias: 'F', description: 'Permite rodar dentro de container (não recomendado)', required: false },
358
+ { name: 'set', alias: 's', description: 'Set a key (repeat for more): --set PORT=3000', required: false, repeatable: true },
359
+ { name: 'list', alias: 'l', description: 'List .env keys (secrets masked)', required: false },
360
+ { name: 'show-secrets', alias: 'S', description: 'With --list, show secrets unmasked', required: false },
361
+ { name: 'non-interactive', alias: 'n', description: 'No TTY wait (CI) — output of --list', required: false },
362
+ { name: 'section', alias: 'x', description: 'Open directly on a section (app|database|redis|auth|storage|worker|cache|ports|theme|deploy)', required: false },
363
+ { name: 'force', alias: 'F', description: 'Allow running inside a container (not recommended)', required: false },
350
364
  ],
351
365
  handler: async (_args, options) => {
352
366
  const { runConfigWizard } = await import('./config-wizard.js');
@@ -364,21 +378,58 @@ export const commands = [
364
378
  },
365
379
  {
366
380
  name: 'update',
367
- description: 'Wizard de atualização: baixar pacotes ou deploy Docker blue/green completo',
381
+ description: 'Update packages, then optionally deploy (blue/green, simple or PM2)',
368
382
  options: [
369
- { name: 'install', alias: 'i', description: 'Modo download: aplica as atualizações no node_modules', required: false },
370
- { name: 'mode', alias: 'm', description: 'Pula o menu: download (só pacotes) ou deploy (blue/green)', required: false },
371
- { name: 'no-cache', alias: 'c', description: 'Deploy: build --no-cache (rebuild limpo, mais lento)', required: false },
372
- { name: 'remove-orphans', alias: 'r', description: 'Deploy: down --remove-orphans na lane antiga (default)', required: false },
373
- { name: 'keep-orphans', alias: 'k', description: 'Deploy: mantém contêineres órfãos da lane antiga', required: false },
374
- { name: 'yes', alias: 'y', description: 'Sem prompts: segue os defaults de cada escolha', required: false },
375
- { name: 'force', alias: 'F', description: 'Permite rodar dentro de container (não recomendado)', required: false },
383
+ {
384
+ name: 'install',
385
+ alias: 'i',
386
+ description: 'Download mode: apply the updates to node_modules',
387
+ required: false,
388
+ },
389
+ {
390
+ name: 'mode',
391
+ alias: 'm',
392
+ description: 'Skip the menu: download (packages only) or deploy',
393
+ required: false,
394
+ },
395
+ {
396
+ name: 'target',
397
+ alias: 't',
398
+ description: 'Skip the target menu: blue-green | simple | pm2',
399
+ required: false,
400
+ },
401
+ {
402
+ name: 'no-cache',
403
+ alias: 'c',
404
+ description: 'Deploy: build --no-cache (clean rebuild, slower)',
405
+ required: false,
406
+ },
407
+ {
408
+ name: 'remove-orphans',
409
+ alias: 'r',
410
+ description: 'Deploy (blue/green): down --remove-orphans on the old lane (default)',
411
+ required: false,
412
+ },
413
+ {
414
+ name: 'keep-orphans',
415
+ alias: 'k',
416
+ description: 'Deploy (blue/green): keep the orphan containers of the old lane',
417
+ required: false,
418
+ },
419
+ { name: 'yes', alias: 'y', description: 'No prompts: take every default', required: false },
420
+ {
421
+ name: 'force',
422
+ alias: 'F',
423
+ description: 'Allow running inside a container (not recommended)',
424
+ required: false,
425
+ },
376
426
  ],
377
427
  handler: async (_args, options) => {
378
428
  const { runUpdate } = await import('./update.js');
379
429
  const code = await runUpdate({
380
430
  install: options['install'] !== undefined,
381
431
  mode: options['mode'],
432
+ target: options['target'],
382
433
  noCache: options['no-cache'] !== undefined,
383
434
  removeOrphans: options['remove-orphans'] !== undefined,
384
435
  keepOrphans: options['keep-orphans'] !== undefined,
@@ -391,18 +442,54 @@ export const commands = [
391
442
  },
392
443
  {
393
444
  name: 'redeploy',
394
- description: 'Aplica plugin/tema recém-instalado: migrations de plugin + build de tema + deploy blue/green',
445
+ description: 'Apply a freshly installed plugin/theme: migrations + theme build + deploy',
395
446
  options: [
396
- { name: 'plugin', alias: 'p', description: 'Só prepara as migrations deste plugin', required: false },
397
- { name: 'theme', alias: 't', description: 'Só compila este tema', required: false },
398
- { name: 'skip-migrations', alias: 'M', description: 'Não copia SQL de plugin para migrations/', required: false },
399
- { name: 'skip-theme-build', alias: 'B', description: 'Não compila estilos dos temas', required: false },
400
- { name: 'dry-run', alias: 'n', description: 'Mostra o plano e sai sem executar nada', required: false },
401
- { name: 'no-cache', alias: 'c', description: 'Deploy: build --no-cache (rebuild limpo, mais lento)', required: false },
402
- { name: 'remove-orphans', alias: 'r', description: 'Deploy: down --remove-orphans na lane antiga (default)', required: false },
403
- { name: 'keep-orphans', alias: 'k', description: 'Deploy: mantém contêineres órfãos da lane antiga', required: false },
404
- { name: 'yes', alias: 'y', description: 'Sem prompts: segue os defaults de cada escolha', required: false },
405
- { name: 'force', alias: 'F', description: 'Permite rodar dentro de container (não recomendado)', required: false },
447
+ { name: 'plugin', alias: 'p', description: 'Only prepare the migrations of this plugin', required: false },
448
+ { name: 'theme', alias: 't', description: 'Only build this theme', required: false },
449
+ {
450
+ name: 'skip-migrations',
451
+ alias: 'M',
452
+ description: 'Do not copy plugin SQL into migrations/',
453
+ required: false,
454
+ },
455
+ {
456
+ name: 'skip-theme-build',
457
+ alias: 'B',
458
+ description: 'Do not compile theme styles',
459
+ required: false,
460
+ },
461
+ { name: 'dry-run', alias: 'n', description: 'Show the plan and exit without running', required: false },
462
+ {
463
+ name: 'target',
464
+ alias: 'T',
465
+ description: 'Skip the target menu: blue-green | simple | pm2',
466
+ required: false,
467
+ },
468
+ {
469
+ name: 'no-cache',
470
+ alias: 'c',
471
+ description: 'Deploy: build --no-cache (clean rebuild, slower)',
472
+ required: false,
473
+ },
474
+ {
475
+ name: 'remove-orphans',
476
+ alias: 'r',
477
+ description: 'Deploy (blue/green): down --remove-orphans on the old lane (default)',
478
+ required: false,
479
+ },
480
+ {
481
+ name: 'keep-orphans',
482
+ alias: 'k',
483
+ description: 'Deploy (blue/green): keep the orphan containers of the old lane',
484
+ required: false,
485
+ },
486
+ { name: 'yes', alias: 'y', description: 'No prompts: take every default', required: false },
487
+ {
488
+ name: 'force',
489
+ alias: 'F',
490
+ description: 'Allow running inside a container (not recommended)',
491
+ required: false,
492
+ },
406
493
  ],
407
494
  handler: async (_args, options) => {
408
495
  const { runRedeploy } = await import('./redeploy.js');
@@ -412,6 +499,7 @@ export const commands = [
412
499
  skipMigrations: options['skip-migrations'] !== undefined,
413
500
  skipThemeBuild: options['skip-theme-build'] !== undefined,
414
501
  dryRun: options['dry-run'] !== undefined,
502
+ target: options['target'],
415
503
  noCache: options['no-cache'] !== undefined,
416
504
  removeOrphans: options['remove-orphans'] !== undefined,
417
505
  keepOrphans: options['keep-orphans'] !== undefined,
@@ -531,7 +619,7 @@ export const commands = [
531
619
  handler: async (_args, options) => {
532
620
  const name = options['name'];
533
621
  if (!name) {
534
- console.error('Nome ou caminho do plugin é obrigatório (--name)');
622
+ console.error('Plugin name or path is required (--name)');
535
623
  process.exit(1);
536
624
  }
537
625
  const { installExtension } = await import('./installer.js');
@@ -564,7 +652,7 @@ export const commands = [
564
652
  try {
565
653
  const results = await searchExtensions('plugin', options['query'] ?? '');
566
654
  if (results.length === 0) {
567
- console.log('Nenhum plugin encontrado.');
655
+ console.log('No plugins found.');
568
656
  return;
569
657
  }
570
658
  for (const result of results) {
@@ -585,7 +673,7 @@ export const commands = [
585
673
  { name: 'info', alias: 'i', description: 'Show manifest info', required: false },
586
674
  { name: 'enable', alias: 'e', description: 'Enable the plugin', required: false },
587
675
  { name: 'disable', alias: 'd', description: 'Disable the plugin', required: false },
588
- { name: 'uninstall', alias: 'u', description: 'Uninstall (remove files + registro)', required: false },
676
+ { name: 'uninstall', alias: 'u', description: 'Uninstall (remove files + registry)', required: false },
589
677
  ],
590
678
  handler: async (_args, options) => {
591
679
  const { readFileSync } = await import('node:fs');
@@ -594,23 +682,23 @@ export const commands = [
594
682
  const { setEnabled, loadExtensionsState, uninstallExtension } = await import('./extensions-state.js');
595
683
  const name = options['name'];
596
684
  if (!name) {
597
- console.error('Nome do plugin é obrigatório (--name)');
685
+ console.error('Plugin name is required (--name)');
598
686
  process.exit(1);
599
687
  }
600
688
  if (options['uninstall'] !== undefined) {
601
689
  const result = uninstallExtension('plugin', name);
602
690
  if (!result.removed) {
603
- console.log(`Plugin "${name}" não estava instalado.`);
691
+ console.log(`Plugin "${name}" was not installed.`);
604
692
  process.exitCode = 1;
605
693
  return;
606
694
  }
607
- console.log(`Plugin "${name}" desinstalado (${result.targetDir}).`);
695
+ console.log(`Plugin "${name}" uninstalled (${result.targetDir}).`);
608
696
  return;
609
697
  }
610
698
  if (options['enable'] !== undefined || options['disable'] !== undefined) {
611
699
  const enabled = options['enable'] !== undefined;
612
700
  const ok = setEnabled('plugin', name, enabled);
613
- console.log(ok ? `Plugin "${name}" ${enabled ? 'habilitado' : 'desabilitado'}.` : `Plugin "${name}" não está registrado.`);
701
+ console.log(ok ? `Plugin "${name}" ${enabled ? 'enabled' : 'disabled'}.` : `Plugin "${name}" is not registered.`);
614
702
  process.exitCode = ok ? 0 : 1;
615
703
  return;
616
704
  }
@@ -620,15 +708,15 @@ export const commands = [
620
708
  try {
621
709
  const manifest = JSON.parse(readFileSync(manifestPath, 'utf-8'));
622
710
  const state = loadExtensionsState().plugins[name];
623
- console.log(`Nome: ${manifest['name'] ?? name}`);
624
- console.log(`Versão: ${manifest['version'] ?? '?'}`);
625
- console.log(`Descrição: ${manifest['description'] ?? '-'}`);
626
- console.log(`Entry: ${manifest['main'] ?? '?'}`);
627
- console.log(`Permissões: ${manifest['permissions']?.join(', ') ?? '-'}`);
628
- console.log(`Estado: ${state?.enabled === false ? 'desabilitado' : 'habilitado'}`);
711
+ console.log(`Name: ${manifest['name'] ?? name}`);
712
+ console.log(`Version: ${manifest['version'] ?? '?'}`);
713
+ console.log(`Description: ${manifest['description'] ?? '-'}`);
714
+ console.log(`Entry: ${manifest['main'] ?? '?'}`);
715
+ console.log(`Permissions: ${manifest['permissions']?.join(', ') ?? '-'}`);
716
+ console.log(`State: ${state?.enabled === false ? 'disabled' : 'enabled'}`);
629
717
  }
630
718
  catch {
631
- console.error(`Manifesto não encontrado em ${manifestPath}`);
719
+ console.error(`Manifest not found at ${manifestPath}`);
632
720
  process.exit(1);
633
721
  }
634
722
  },
@@ -642,7 +730,7 @@ export const commands = [
642
730
  handler: async (_args, options) => {
643
731
  const name = options['name'];
644
732
  if (!name) {
645
- console.error('Nome ou caminho do tema é obrigatório (--name)');
733
+ console.error('Theme name or path is required (--name)');
646
734
  process.exit(1);
647
735
  }
648
736
  const { installExtension } = await import('./installer.js');
@@ -675,7 +763,7 @@ export const commands = [
675
763
  try {
676
764
  const results = await searchExtensions('theme', options['query'] ?? '');
677
765
  if (results.length === 0) {
678
- console.log('Nenhum tema encontrado.');
766
+ console.log('No themes found.');
679
767
  return;
680
768
  }
681
769
  for (const result of results) {
@@ -696,7 +784,7 @@ export const commands = [
696
784
  { name: 'info', alias: 'i', description: 'Show theme.json info', required: false },
697
785
  { name: 'enable', alias: 'e', description: 'Enable the theme', required: false },
698
786
  { name: 'disable', alias: 'd', description: 'Disable the theme', required: false },
699
- { name: 'uninstall', alias: 'u', description: 'Uninstall (remove files + registro)', required: false },
787
+ { name: 'uninstall', alias: 'u', description: 'Uninstall (remove files + registry)', required: false },
700
788
  { name: 'set-active', alias: 's', description: 'Set as active theme', required: false },
701
789
  ],
702
790
  handler: async (_args, options) => {
@@ -706,36 +794,36 @@ export const commands = [
706
794
  const { setEnabled, loadExtensionsState } = await import('./extensions-state.js');
707
795
  const name = options['name'];
708
796
  if (!name) {
709
- console.error('Nome do tema é obrigatório (--name)');
797
+ console.error('Theme name is required (--name)');
710
798
  process.exit(1);
711
799
  }
712
800
  if (options['uninstall'] !== undefined) {
713
801
  const { uninstallExtension } = await import('./extensions-state.js');
714
802
  const result = uninstallExtension('theme', name);
715
803
  if (!result.removed) {
716
- console.log(`Theme "${name}" não estava instalado.`);
804
+ console.log(`Theme "${name}" was not installed.`);
717
805
  process.exitCode = 1;
718
806
  return;
719
807
  }
720
- console.log(`Theme "${name}" desinstalado (${result.targetDir}).`);
808
+ console.log(`Theme "${name}" uninstalled (${result.targetDir}).`);
721
809
  return;
722
810
  }
723
811
  if (options['set-active'] !== undefined) {
724
812
  const configPath = join(process.cwd(), DEFAULT_CONFIG_FILENAME);
725
813
  if (!existsSync(configPath)) {
726
- console.error(`${DEFAULT_CONFIG_FILENAME} não encontrado; rode \`okcms init\` primeiro.`);
814
+ console.error(`${DEFAULT_CONFIG_FILENAME} not found; run \`okcms init\` first.`);
727
815
  process.exit(1);
728
816
  }
729
817
  const raw = JSON.parse(readFileSync(configPath, 'utf-8'));
730
818
  raw['activeTheme'] = name;
731
819
  writeFileSync(configPath, JSON.stringify(raw, null, 2));
732
- console.log(`Tema ativo definido como "${name}".`);
820
+ console.log(`Active theme set to "${name}".`);
733
821
  return;
734
822
  }
735
823
  if (options['enable'] !== undefined || options['disable'] !== undefined) {
736
824
  const enabled = options['enable'] !== undefined;
737
825
  const ok = setEnabled('theme', name, enabled);
738
- console.log(ok ? `Tema "${name}" ${enabled ? 'habilitado' : 'desabilitado'}.` : `Tema "${name}" não está registrado.`);
826
+ console.log(ok ? `Theme "${name}" ${enabled ? 'enabled' : 'disabled'}.` : `Theme "${name}" is not registered.`);
739
827
  process.exitCode = ok ? 0 : 1;
740
828
  return;
741
829
  }
@@ -745,16 +833,16 @@ export const commands = [
745
833
  const manifest = JSON.parse(readFileSync(themeJsonPath, 'utf-8'));
746
834
  const provides = (manifest['provides'] ?? {});
747
835
  const state = loadExtensionsState().themes[name];
748
- console.log(`Nome: ${manifest['name'] ?? name}`);
749
- console.log(`Versão: ${manifest['version'] ?? '?'}`);
750
- console.log(`Autor: ${manifest['author'] ?? '-'}`);
836
+ console.log(`Name: ${manifest['name'] ?? name}`);
837
+ console.log(`Version: ${manifest['version'] ?? '?'}`);
838
+ console.log(`Author: ${manifest['author'] ?? '-'}`);
751
839
  console.log(`Parent: ${manifest['parent'] ?? '-'}`);
752
840
  console.log(`Layouts: ${provides['layouts']?.join(', ') ?? '-'}`);
753
- console.log(`Ativo: ${config.activeTheme === name ? 'sim' : 'não'} (${config.activeTheme || 'nenhum'})`);
754
- console.log(`Estado: ${state?.enabled === false ? 'desabilitado' : 'habilitado'}`);
841
+ console.log(`Active: ${config.activeTheme === name ? 'yes' : 'no'} (${config.activeTheme || 'none'})`);
842
+ console.log(`State: ${state?.enabled === false ? 'disabled' : 'enabled'}`);
755
843
  }
756
844
  catch {
757
- console.error(`theme.json não encontrado em ${themeJsonPath}`);
845
+ console.error(`theme.json not found at ${themeJsonPath}`);
758
846
  process.exit(1);
759
847
  }
760
848
  },
@@ -769,13 +857,13 @@ export const commands = [
769
857
  handler: async (_args, options) => {
770
858
  const from = options['from'] ?? 'local';
771
859
  const to = options['to'] ?? 's3';
772
- console.log(`Migrando mídia de "${from}" para "${to}"...`);
860
+ console.log(`Migrating media from "${from}" to "${to}"...`);
773
861
  const { initDb } = await import('./db-init.js');
774
862
  await initDb();
775
863
  const { createMigrator } = await import('@oktis-works/core');
776
864
  const migrator = createMigrator(from, to);
777
865
  const result = await migrator.migrateAll();
778
- console.log(`Migrados: ${result.migrated}, Falhas: ${result.failed}`);
866
+ console.log(`Migrated: ${result.migrated}, Failed: ${result.failed}`);
779
867
  for (const err of result.errors) {
780
868
  console.error(` ! ${err.filename}: ${err.error}`);
781
869
  }
@@ -795,7 +883,7 @@ export const commands = [
795
883
  // A política RBAC usa slugs em maiúsculas: `--role editor` → EDITOR.
796
884
  const roleSlug = String(options['role'] ?? 'EDITOR').toUpperCase();
797
885
  if (!VALID_ROLES.includes(roleSlug)) {
798
- console.error(`Role inválida: ${options['role']} — use uma de: ${VALID_ROLES.join(', ')}`);
886
+ console.error(`Invalid role: ${options['role']} — use one of: ${VALID_ROLES.join(', ')}`);
799
887
  process.exitCode = 1;
800
888
  return;
801
889
  }
@@ -809,7 +897,7 @@ export const commands = [
809
897
  const email = String(options['email']);
810
898
  const existing = await sql.unsafe('SELECT id FROM users WHERE email = $1', [email]);
811
899
  if (existing.length > 0) {
812
- console.error(`Usuário já existe: ${email}`);
900
+ console.error(`User already exists: ${email}`);
813
901
  process.exitCode = 1;
814
902
  return;
815
903
  }
@@ -823,7 +911,7 @@ export const commands = [
823
911
  ORDER BY tenant_id NULLS LAST
824
912
  LIMIT 1`, [roleSlug, tenantId]);
825
913
  if (roles.length === 0) {
826
- console.error(`Role "${roleSlug}" não encontrada após o seed.`);
914
+ console.error(`Role "${roleSlug}" not found after seeding.`);
827
915
  process.exitCode = 1;
828
916
  return;
829
917
  }
@@ -831,7 +919,7 @@ export const commands = [
831
919
  VALUES ($1, $2, $3, 'ACTIVE')
832
920
  ON CONFLICT (tenant_id, user_id)
833
921
  DO UPDATE SET role_id = EXCLUDED.role_id, status = 'ACTIVE'`, [tenantId, userId, roles[0]['id']]);
834
- console.log(`✓ usuário criado: ${email} (papel ${roleSlug})`);
922
+ console.log(`✓ user created: ${email} (role ${roleSlug})`);
835
923
  },
836
924
  },
837
925
  {
@@ -843,10 +931,10 @@ export const commands = [
843
931
  const { ensureCoreSchema, seedCoreData } = await import('@oktis-works/database');
844
932
  const applied = await ensureCoreSchema();
845
933
  if (applied) {
846
- console.log('✓ schema core aplicado (banco inicializado)');
934
+ console.log('✓ core schema applied (database initialized)');
847
935
  }
848
936
  const { roles, settings } = await seedCoreData();
849
- console.log(`✓ seed concluído: ${roles} roles, ${settings} settings criados`);
937
+ console.log(`✓ seed completed: ${roles} roles, ${settings} settings created`);
850
938
  },
851
939
  },
852
940
  {
@@ -864,6 +952,56 @@ export const commands = [
864
952
  console.log(` Database: ${dbOk ? 'connected' : 'disconnected'}`);
865
953
  },
866
954
  },
955
+ {
956
+ name: 'rollback',
957
+ description: 'Rollback to a previous state (lists history and lets you choose)',
958
+ options: [
959
+ { name: 'id', alias: 'i', description: 'History ID for direct rollback (no menu)', required: false },
960
+ { name: 'yes', alias: 'y', description: 'Confirm without prompt', required: false },
961
+ { name: 'force', alias: 'F', description: 'Allow running inside a container (not recommended)', required: false },
962
+ ],
963
+ handler: async (_args, options) => {
964
+ const { runRollback } = await import('./rollback.js');
965
+ const code = await runRollback({
966
+ id: options['id'] ? Number(options['id']) : undefined,
967
+ yes: options['yes'] !== undefined,
968
+ force: options['force'] !== undefined,
969
+ });
970
+ if (code !== 0)
971
+ process.exitCode = code;
972
+ },
973
+ },
974
+ {
975
+ name: 'deploy',
976
+ description: 'First deploy — pick a target: blue/green lanes, simple containers or PM2',
977
+ options: [
978
+ {
979
+ name: 'target',
980
+ alias: 't',
981
+ description: 'Skip the menu: blue-green | simple | pm2',
982
+ required: false,
983
+ },
984
+ { name: 'yes', alias: 'y', description: 'No prompts: take every default', required: false },
985
+ {
986
+ name: 'force',
987
+ alias: 'F',
988
+ description: 'Allow running inside a container (not recommended)',
989
+ required: false,
990
+ },
991
+ { name: 'install', alias: 'i', description: 'PM2: update packages before deploying', required: false },
992
+ ],
993
+ handler: async (_args, options) => {
994
+ const { runDeploy } = await import('./deploy.js');
995
+ const code = await runDeploy({
996
+ target: options['target'],
997
+ yes: options['yes'] !== undefined,
998
+ force: options['force'] !== undefined,
999
+ install: options['install'] !== undefined,
1000
+ });
1001
+ if (code !== 0)
1002
+ process.exitCode = code;
1003
+ },
1004
+ },
867
1005
  ];
868
1006
  export function getCommand(name) {
869
1007
  return commands.find((c) => c.name === name);