@oktis-works/cms 0.1.1
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/dist/build.d.ts +23 -0
- package/dist/build.d.ts.map +1 -0
- package/dist/build.js +26 -0
- package/dist/build.js.map +1 -0
- package/dist/commands.d.ts +16 -0
- package/dist/commands.d.ts.map +1 -0
- package/dist/commands.js +720 -0
- package/dist/commands.js.map +1 -0
- package/dist/db-dump.d.ts +23 -0
- package/dist/db-dump.d.ts.map +1 -0
- package/dist/db-dump.js +47 -0
- package/dist/db-dump.js.map +1 -0
- package/dist/db-init.d.ts +2 -0
- package/dist/db-init.d.ts.map +1 -0
- package/dist/db-init.js +8 -0
- package/dist/db-init.js.map +1 -0
- package/dist/doctor.d.ts +22 -0
- package/dist/doctor.d.ts.map +1 -0
- package/dist/doctor.js +88 -0
- package/dist/doctor.js.map +1 -0
- package/dist/extension-scaffold.d.ts +17 -0
- package/dist/extension-scaffold.d.ts.map +1 -0
- package/dist/extension-scaffold.js +97 -0
- package/dist/extension-scaffold.js.map +1 -0
- package/dist/extensions-state.d.ts +21 -0
- package/dist/extensions-state.d.ts.map +1 -0
- package/dist/extensions-state.js +66 -0
- package/dist/extensions-state.js.map +1 -0
- package/dist/index.d.ts +6 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +41 -0
- package/dist/index.js.map +1 -0
- package/dist/installer.d.ts +12 -0
- package/dist/installer.d.ts.map +1 -0
- package/dist/installer.js +74 -0
- package/dist/installer.js.map +1 -0
- package/dist/npm-registry.d.ts +11 -0
- package/dist/npm-registry.d.ts.map +1 -0
- package/dist/npm-registry.js +34 -0
- package/dist/npm-registry.js.map +1 -0
- package/dist/parser.d.ts +8 -0
- package/dist/parser.d.ts.map +1 -0
- package/dist/parser.js +60 -0
- package/dist/parser.js.map +1 -0
- package/dist/project-config.d.ts +27 -0
- package/dist/project-config.d.ts.map +1 -0
- package/dist/project-config.js +49 -0
- package/dist/project-config.js.map +1 -0
- package/dist/runtime-state.d.ts +25 -0
- package/dist/runtime-state.d.ts.map +1 -0
- package/dist/runtime-state.js +96 -0
- package/dist/runtime-state.js.map +1 -0
- package/dist/scaffold.d.ts +2 -0
- package/dist/scaffold.d.ts.map +1 -0
- package/dist/scaffold.js +78 -0
- package/dist/scaffold.js.map +1 -0
- package/dist/start.d.ts +2 -0
- package/dist/start.d.ts.map +1 -0
- package/dist/start.js +52 -0
- package/dist/start.js.map +1 -0
- package/package.json +44 -0
package/dist/commands.js
ADDED
|
@@ -0,0 +1,720 @@
|
|
|
1
|
+
import { basename, join, resolve } from 'node:path';
|
|
2
|
+
import { initDb } from './db-init.js';
|
|
3
|
+
function getMigrationsDir(options) {
|
|
4
|
+
return options['dir'] ?? join(process.cwd(), 'migrations');
|
|
5
|
+
}
|
|
6
|
+
function getTenantId(options) {
|
|
7
|
+
return options['tenant'] ?? 'default';
|
|
8
|
+
}
|
|
9
|
+
export const commands = [
|
|
10
|
+
{
|
|
11
|
+
name: 'init',
|
|
12
|
+
description: 'Scaffold a new OkCMS project in a directory',
|
|
13
|
+
options: [
|
|
14
|
+
{ name: 'dir', alias: 'd', description: 'Target directory (same as positional arg)', required: false },
|
|
15
|
+
],
|
|
16
|
+
handler: async (args, options) => {
|
|
17
|
+
const { scaffoldProject } = await import('./scaffold.js');
|
|
18
|
+
// O arg posicional tem prioridade sobre -d: `init cms-teste` deve criar
|
|
19
|
+
// o diretório cms-teste (o default '.' injetado pelo parser engolia args[0]).
|
|
20
|
+
const target = args[0] ?? options['dir'] ?? '.';
|
|
21
|
+
const name = args[1] ?? basename(resolve(target));
|
|
22
|
+
await scaffoldProject(target, name);
|
|
23
|
+
},
|
|
24
|
+
},
|
|
25
|
+
{
|
|
26
|
+
name: 'start',
|
|
27
|
+
description: 'Start API + Admin + Web apps for this project',
|
|
28
|
+
options: [
|
|
29
|
+
{ name: 'api', alias: 'a', description: 'Start only the API', required: false },
|
|
30
|
+
{ name: 'admin', alias: 'm', description: 'Include admin app', required: false },
|
|
31
|
+
{ name: 'web', alias: 'w', description: 'Include web app', required: false },
|
|
32
|
+
{ name: 'all', alias: 'A', description: 'Start all apps (default)', required: false },
|
|
33
|
+
],
|
|
34
|
+
handler: async (_args, options) => {
|
|
35
|
+
const { startProject } = await import('./start.js');
|
|
36
|
+
await startProject(options);
|
|
37
|
+
},
|
|
38
|
+
},
|
|
39
|
+
{
|
|
40
|
+
name: 'stop',
|
|
41
|
+
description: 'Stop apps started by `start` (via pidfile ou detecção de processos)',
|
|
42
|
+
options: [],
|
|
43
|
+
handler: async () => {
|
|
44
|
+
const { stopAll, findRunningApps, clearPids } = await import('./runtime-state.js');
|
|
45
|
+
const results = stopAll();
|
|
46
|
+
if (results.length === 0) {
|
|
47
|
+
const fallback = findRunningApps();
|
|
48
|
+
for (const proc of fallback) {
|
|
49
|
+
try {
|
|
50
|
+
process.kill(proc.pid, 'SIGTERM');
|
|
51
|
+
console.log(`[stop] ${proc.label} (pid ${proc.pid}) finalizado`);
|
|
52
|
+
}
|
|
53
|
+
catch {
|
|
54
|
+
console.error(`[stop] falha ao finalizar ${proc.label} (pid ${proc.pid})`);
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
if (fallback.length === 0) {
|
|
58
|
+
console.log('Nenhum app rodando.');
|
|
59
|
+
}
|
|
60
|
+
clearPids();
|
|
61
|
+
return;
|
|
62
|
+
}
|
|
63
|
+
for (const result of results) {
|
|
64
|
+
if (result.stopped) {
|
|
65
|
+
console.log(`[stop] ${result.label} (pid ${result.pid}) finalizado`);
|
|
66
|
+
}
|
|
67
|
+
else {
|
|
68
|
+
console.log(`[stop] ${result.label} (pid ${result.pid}) não estava mais ativo`);
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
clearPids();
|
|
72
|
+
},
|
|
73
|
+
},
|
|
74
|
+
{
|
|
75
|
+
name: 'status',
|
|
76
|
+
description: 'Show status of apps started by `start` + resumo do projeto',
|
|
77
|
+
options: [],
|
|
78
|
+
handler: async () => {
|
|
79
|
+
const { statusAll } = await import('./runtime-state.js');
|
|
80
|
+
const { loadProjectConfig } = await import('./project-config.js');
|
|
81
|
+
const config = loadProjectConfig();
|
|
82
|
+
const processes = statusAll();
|
|
83
|
+
console.log(`Projeto "${config.name}" — ports api:${config.ports.api} admin:${config.ports.admin} web:${config.ports.web}`);
|
|
84
|
+
console.log(`Tema ativo: ${config.activeTheme || '(não definido)'}`);
|
|
85
|
+
if (processes.length === 0) {
|
|
86
|
+
console.log('Nenhum pidfile encontrado (use `okcms start`).');
|
|
87
|
+
return;
|
|
88
|
+
}
|
|
89
|
+
for (const proc of processes) {
|
|
90
|
+
console.log(` ${proc.alive ? '●' : '○'} ${proc.label.padEnd(6)} pid=${proc.pid} iniciado=${proc.startedAt}`);
|
|
91
|
+
}
|
|
92
|
+
},
|
|
93
|
+
},
|
|
94
|
+
{
|
|
95
|
+
name: 'doctor',
|
|
96
|
+
description: 'Diagnóstico do ambiente: node, .env, DATABASE_URL, config (cli-devexp-003)',
|
|
97
|
+
options: [],
|
|
98
|
+
handler: async () => {
|
|
99
|
+
const { runDoctorChecks } = await import('./doctor.js');
|
|
100
|
+
const results = await runDoctorChecks();
|
|
101
|
+
let failed = 0;
|
|
102
|
+
for (const check of results) {
|
|
103
|
+
const icon = check.ok ? '\u2713' : check.required ? '\u2717' : '\u25CB';
|
|
104
|
+
console.log(` ${icon} ${check.name.padEnd(24)} ${check.detail}`);
|
|
105
|
+
if (!check.ok && check.required)
|
|
106
|
+
failed++;
|
|
107
|
+
}
|
|
108
|
+
if (failed > 0) {
|
|
109
|
+
console.error(`doctor: ${failed} problema(s) obrigatório(s) encontrado(s)`);
|
|
110
|
+
process.exitCode = 1;
|
|
111
|
+
}
|
|
112
|
+
else {
|
|
113
|
+
console.log('Ambiente OK.');
|
|
114
|
+
}
|
|
115
|
+
},
|
|
116
|
+
},
|
|
117
|
+
{
|
|
118
|
+
name: 'plugin:create',
|
|
119
|
+
description: 'Scaffold de um plugin mínimo e compatível (--name)',
|
|
120
|
+
options: [
|
|
121
|
+
{ name: 'name', alias: 'n', description: 'Nome do plugin', required: true },
|
|
122
|
+
{ name: 'dir', alias: 'd', description: 'Diretório base (default: ./plugins)', required: false },
|
|
123
|
+
],
|
|
124
|
+
handler: async (_args, options) => {
|
|
125
|
+
const { scaffoldPlugin } = await import('./extension-scaffold.js');
|
|
126
|
+
const name = options['name'];
|
|
127
|
+
if (!name) {
|
|
128
|
+
console.error('Nome do plugin é obrigatório (--name)');
|
|
129
|
+
process.exit(1);
|
|
130
|
+
}
|
|
131
|
+
try {
|
|
132
|
+
const result = scaffoldPlugin(name, options['dir']);
|
|
133
|
+
console.log(`Plugin criado em ${result.dir}`);
|
|
134
|
+
console.log(`Compatibilidade validada contra CMS ${result.manifest['compatibility'] ? '' : ''}(manifest.json)`);
|
|
135
|
+
}
|
|
136
|
+
catch (error) {
|
|
137
|
+
console.error(error instanceof Error ? error.message : String(error));
|
|
138
|
+
process.exit(1);
|
|
139
|
+
}
|
|
140
|
+
},
|
|
141
|
+
},
|
|
142
|
+
{
|
|
143
|
+
name: 'theme:create',
|
|
144
|
+
description: 'Scaffold de um tema mínimo e compatível (--name --style css|scss|tailwind)',
|
|
145
|
+
options: [
|
|
146
|
+
{ name: 'name', alias: 'n', description: 'Nome do tema', required: true },
|
|
147
|
+
{ name: 'dir', alias: 'd', description: 'Diretório base (default: ./themes)', required: false },
|
|
148
|
+
{ name: 'style', alias: 's', description: 'Engine de estilo: css|scss|tailwind', required: false },
|
|
149
|
+
],
|
|
150
|
+
handler: async (_args, options) => {
|
|
151
|
+
const { scaffoldTheme } = await import('./extension-scaffold.js');
|
|
152
|
+
const name = options['name'];
|
|
153
|
+
const style = (options['style'] ?? 'css');
|
|
154
|
+
if (!name) {
|
|
155
|
+
console.error('Nome do tema é obrigatório (--name)');
|
|
156
|
+
process.exit(1);
|
|
157
|
+
}
|
|
158
|
+
if (!['css', 'scss', 'tailwind'].includes(style)) {
|
|
159
|
+
console.error('--style deve ser css|scss|tailwind');
|
|
160
|
+
process.exit(1);
|
|
161
|
+
}
|
|
162
|
+
try {
|
|
163
|
+
const result = scaffoldTheme(name, options['dir'], style);
|
|
164
|
+
console.log(`Tema criado em ${result.dir} (engine: ${style})`);
|
|
165
|
+
}
|
|
166
|
+
catch (error) {
|
|
167
|
+
console.error(error instanceof Error ? error.message : String(error));
|
|
168
|
+
process.exit(1);
|
|
169
|
+
}
|
|
170
|
+
},
|
|
171
|
+
},
|
|
172
|
+
{
|
|
173
|
+
name: 'theme:build',
|
|
174
|
+
description: 'Compila estilos do tema (SCSS/Tailwind -> dist/theme.css com isolamento [data-theme])',
|
|
175
|
+
options: [
|
|
176
|
+
{ name: 'name', alias: 'n', description: 'Nome do tema', required: true },
|
|
177
|
+
{ name: 'themes-dir', alias: 'd', description: 'Diretório de temas (default: ./themes)', required: false },
|
|
178
|
+
],
|
|
179
|
+
handler: async (_args, options) => {
|
|
180
|
+
const { join } = await import('node:path');
|
|
181
|
+
const { mkdirSync, writeFileSync, readFileSync, existsSync } = await import('node:fs');
|
|
182
|
+
const { buildThemeStyles } = await import('@oktis-works/theme-runtime');
|
|
183
|
+
const name = options['name'];
|
|
184
|
+
if (!name) {
|
|
185
|
+
console.error('--name obrigatório');
|
|
186
|
+
process.exit(1);
|
|
187
|
+
}
|
|
188
|
+
const themesDir = options['themes-dir'] ?? join(process.cwd(), 'themes');
|
|
189
|
+
const manifestPath = join(themesDir, name, 'theme.json');
|
|
190
|
+
if (!existsSync(manifestPath)) {
|
|
191
|
+
console.error(`theme.json não encontrado em ${manifestPath}`);
|
|
192
|
+
process.exit(1);
|
|
193
|
+
}
|
|
194
|
+
const manifest = JSON.parse(readFileSync(manifestPath, 'utf-8'));
|
|
195
|
+
try {
|
|
196
|
+
const { css, report } = await buildThemeStyles({ themesRoot: themesDir, themeName: name, manifest });
|
|
197
|
+
if (!report.entry)
|
|
198
|
+
console.log(`[${name}] sem entrada de estilo (${report.engine})`);
|
|
199
|
+
for (const w of report.warnings)
|
|
200
|
+
console.warn(`warn: ${w}`);
|
|
201
|
+
mkdirSync(join(themesDir, name, 'dist'), { recursive: true });
|
|
202
|
+
writeFileSync(join(themesDir, name, report.output), css, 'utf-8');
|
|
203
|
+
console.log(`[${name}] ${report.engine} -> ${report.output} (${css.length} bytes) isolated=${report.isolated}`);
|
|
204
|
+
}
|
|
205
|
+
catch (e) {
|
|
206
|
+
console.error(e instanceof Error ? e.message : String(e));
|
|
207
|
+
process.exit(1);
|
|
208
|
+
}
|
|
209
|
+
},
|
|
210
|
+
},
|
|
211
|
+
{
|
|
212
|
+
name: 'build',
|
|
213
|
+
description: 'Build dos apps do projeto via bun filters (--apps api,admin,web)',
|
|
214
|
+
options: [
|
|
215
|
+
{ name: 'apps', alias: 'a', description: 'Apps a buildar (default: api,admin,web)', required: false },
|
|
216
|
+
],
|
|
217
|
+
handler: async (_args, options) => {
|
|
218
|
+
const { runProjectBuild } = await import('./build.js');
|
|
219
|
+
const appsArg = options['apps'];
|
|
220
|
+
const apps = appsArg
|
|
221
|
+
? appsArg.split(',').map((s) => s.trim())
|
|
222
|
+
: undefined;
|
|
223
|
+
const started = Date.now();
|
|
224
|
+
const { results, ok } = runProjectBuild({ apps });
|
|
225
|
+
for (const step of results) {
|
|
226
|
+
const icon = step.ok ? '\u2713' : '\u2717';
|
|
227
|
+
console.log(` ${icon} ${step.label.padEnd(6)} ${step.durationMs}ms`);
|
|
228
|
+
if (!step.ok && step.output) {
|
|
229
|
+
console.error(step.output.split('\n').slice(-10).join('\n'));
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
console.log(ok ? `Build concluído em ${Date.now() - started}ms.` : 'Build falhou.');
|
|
233
|
+
process.exitCode = ok ? 0 : 1;
|
|
234
|
+
},
|
|
235
|
+
},
|
|
236
|
+
{
|
|
237
|
+
name: 'prerender',
|
|
238
|
+
description: 'Prerender estático das páginas publicadas para o tema ativo (best-effort)',
|
|
239
|
+
options: [
|
|
240
|
+
{ name: 'out', alias: 'o', description: 'Diretório de saída (default: ./.prerender)', required: false },
|
|
241
|
+
],
|
|
242
|
+
handler: async (_args, options) => {
|
|
243
|
+
const { join } = await import('node:path');
|
|
244
|
+
if (!process.env['DATABASE_URL']) {
|
|
245
|
+
console.log('[prerender] DATABASE_URL não configurado — pulando (best-effort).');
|
|
246
|
+
return;
|
|
247
|
+
}
|
|
248
|
+
const outDir = options['out'] ?? join(process.cwd(), '.prerender');
|
|
249
|
+
try {
|
|
250
|
+
const { getConnection } = await import('@oktis-works/database');
|
|
251
|
+
const { prerenderStatic } = await import('@oktis-works/theme-runtime');
|
|
252
|
+
const rows = await getConnection().unsafe("SELECT type, slug FROM content WHERE status = 'PUBLISHED' ORDER BY updated_at DESC LIMIT 500");
|
|
253
|
+
const contents = rows;
|
|
254
|
+
const result = await prerenderStatic(contents.map((content) => ({
|
|
255
|
+
path: `/${content.slug}`,
|
|
256
|
+
render: async () => `<main data-type="${content.type}" data-slug="${content.slug}"></main>`,
|
|
257
|
+
})), outDir);
|
|
258
|
+
console.log(`[prerender] ${result.pages} página(s) geradas em ${outDir}`);
|
|
259
|
+
if (result.failed.length > 0) {
|
|
260
|
+
const failedPaths = result.failed.map((failed) => failed.path).join(', ');
|
|
261
|
+
console.warn(`[prerender] ${result.failed.length} falha(s): ${failedPaths}`);
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
catch (error) {
|
|
265
|
+
console.warn(`[prerender] indisponível (${error instanceof Error ? error.message : String(error)}) — pulando.`);
|
|
266
|
+
}
|
|
267
|
+
},
|
|
268
|
+
},
|
|
269
|
+
{
|
|
270
|
+
name: 'update',
|
|
271
|
+
description: 'Check @oktis-works/* packages against npm registry latest (--install to upgrade)',
|
|
272
|
+
options: [
|
|
273
|
+
{ name: 'install', alias: 'i', description: 'Run npm install for outdated packages', required: false },
|
|
274
|
+
],
|
|
275
|
+
handler: async (_args, options) => {
|
|
276
|
+
const { existsSync, readdirSync, readFileSync } = await import('node:fs');
|
|
277
|
+
const { spawnSync } = await import('node:child_process');
|
|
278
|
+
const { getLatestVersion } = await import('./npm-registry.js');
|
|
279
|
+
const nmDir = join(process.cwd(), 'node_modules', '@oktis-works');
|
|
280
|
+
if (!existsSync(nmDir)) {
|
|
281
|
+
console.error('Nenhum pacote @oktis-works/* instalado em node_modules.');
|
|
282
|
+
process.exit(1);
|
|
283
|
+
}
|
|
284
|
+
const installed = readdirSync(nmDir, { withFileTypes: true })
|
|
285
|
+
.filter((entry) => entry.isDirectory())
|
|
286
|
+
.map((entry) => entry.name);
|
|
287
|
+
const outdated = [];
|
|
288
|
+
for (const name of installed) {
|
|
289
|
+
let current = '?';
|
|
290
|
+
try {
|
|
291
|
+
const pkgJson = JSON.parse(readFileSync(join(nmDir, name, 'package.json'), 'utf-8'));
|
|
292
|
+
current = pkgJson.version ?? '?';
|
|
293
|
+
}
|
|
294
|
+
catch {
|
|
295
|
+
// sem package.json legível
|
|
296
|
+
}
|
|
297
|
+
const latest = await getLatestVersion(`@oktis-works/${name}`);
|
|
298
|
+
if (!latest || latest === current) {
|
|
299
|
+
console.log(` ✓ @oktis-works/${name} ${current} (em dia)`);
|
|
300
|
+
continue;
|
|
301
|
+
}
|
|
302
|
+
console.log(` ↑ @oktis-works/${name} ${current} → ${latest} disponível`);
|
|
303
|
+
outdated.push({ pkg: `@oktis-works/${name}`, current, latest });
|
|
304
|
+
}
|
|
305
|
+
if (outdated.length === 0)
|
|
306
|
+
return;
|
|
307
|
+
if (options['install'] !== undefined) {
|
|
308
|
+
console.log('\nAtualizando...');
|
|
309
|
+
const result = spawnSync('bun', ['add', ...outdated.map((entry) => `${entry.pkg}@latest`)], {
|
|
310
|
+
stdio: 'inherit',
|
|
311
|
+
});
|
|
312
|
+
process.exitCode = result.status ?? 1;
|
|
313
|
+
}
|
|
314
|
+
else {
|
|
315
|
+
console.log('\nUse --install para atualizar automaticamente.');
|
|
316
|
+
}
|
|
317
|
+
},
|
|
318
|
+
},
|
|
319
|
+
{
|
|
320
|
+
name: 'db:migrate',
|
|
321
|
+
description: 'Run pending database migrations',
|
|
322
|
+
options: [
|
|
323
|
+
{ name: 'dir', alias: 'd', description: 'Migrations directory', required: false },
|
|
324
|
+
{ name: 'tenant', alias: 't', description: 'Tenant ID', required: false, default: 'default' },
|
|
325
|
+
],
|
|
326
|
+
handler: async (args, options) => {
|
|
327
|
+
await initDb();
|
|
328
|
+
const { runMigrations } = await import('@oktis-works/database');
|
|
329
|
+
const result = await runMigrations(getTenantId(options), getMigrationsDir(options));
|
|
330
|
+
console.log(`Applied: ${result.applied.length}, Skipped: ${result.skipped.length}`);
|
|
331
|
+
for (const name of result.applied) {
|
|
332
|
+
console.log(` + ${name}`);
|
|
333
|
+
}
|
|
334
|
+
},
|
|
335
|
+
},
|
|
336
|
+
{
|
|
337
|
+
name: 'db:rollback',
|
|
338
|
+
description: 'Rollback the last migration',
|
|
339
|
+
options: [
|
|
340
|
+
{ name: 'dir', alias: 'd', description: 'Migrations directory', required: false },
|
|
341
|
+
{ name: 'tenant', alias: 't', description: 'Tenant ID', required: false, default: 'default' },
|
|
342
|
+
],
|
|
343
|
+
handler: async (args, options) => {
|
|
344
|
+
await initDb();
|
|
345
|
+
const { getAppliedMigrations, rollbackMigration, loadMigrationsFromDir } = await import('@oktis-works/database');
|
|
346
|
+
const tenantId = getTenantId(options);
|
|
347
|
+
const dir = getMigrationsDir(options);
|
|
348
|
+
const applied = await getAppliedMigrations(tenantId);
|
|
349
|
+
if (applied.length === 0) {
|
|
350
|
+
console.log('No migrations to rollback');
|
|
351
|
+
return;
|
|
352
|
+
}
|
|
353
|
+
const last = applied[applied.length - 1];
|
|
354
|
+
const allMigrations = loadMigrationsFromDir(dir);
|
|
355
|
+
const migration = allMigrations.find((m) => m.owner === last.owner && m.version === last.version);
|
|
356
|
+
if (!migration) {
|
|
357
|
+
console.error(`Migration file not found for: ${last.name}`);
|
|
358
|
+
process.exit(1);
|
|
359
|
+
}
|
|
360
|
+
await rollbackMigration(tenantId, migration);
|
|
361
|
+
console.log(`Rolled back: ${last.name}`);
|
|
362
|
+
},
|
|
363
|
+
},
|
|
364
|
+
{
|
|
365
|
+
name: 'db:status',
|
|
366
|
+
description: 'Show migration status',
|
|
367
|
+
options: [
|
|
368
|
+
{ name: 'tenant', alias: 't', description: 'Tenant ID', required: false, default: 'default' },
|
|
369
|
+
],
|
|
370
|
+
handler: async (args, options) => {
|
|
371
|
+
await initDb();
|
|
372
|
+
const { getAppliedMigrations } = await import('@oktis-works/database');
|
|
373
|
+
const applied = await getAppliedMigrations(getTenantId(options));
|
|
374
|
+
if (applied.length === 0) {
|
|
375
|
+
console.log('No migrations applied');
|
|
376
|
+
return;
|
|
377
|
+
}
|
|
378
|
+
console.log('Applied migrations:');
|
|
379
|
+
for (const m of applied) {
|
|
380
|
+
console.log(` ${m.version} - ${m.name} (${m.applied_at.toISOString()})`);
|
|
381
|
+
}
|
|
382
|
+
},
|
|
383
|
+
},
|
|
384
|
+
{
|
|
385
|
+
name: 'db:backup',
|
|
386
|
+
description: 'Backup the database using pg_dump (custom format)',
|
|
387
|
+
options: [
|
|
388
|
+
{ name: 'out', alias: 'o', description: 'Output file (.dump). Defaults to timestamped name', required: false },
|
|
389
|
+
],
|
|
390
|
+
handler: async () => {
|
|
391
|
+
const { backupDatabase } = await import('./db-dump.js');
|
|
392
|
+
const result = backupDatabase();
|
|
393
|
+
if (!result.ok) {
|
|
394
|
+
console.error(result.message);
|
|
395
|
+
process.exitCode = 1;
|
|
396
|
+
return;
|
|
397
|
+
}
|
|
398
|
+
console.log(result.message);
|
|
399
|
+
},
|
|
400
|
+
},
|
|
401
|
+
{
|
|
402
|
+
name: 'db:restore',
|
|
403
|
+
description: 'Restore the database from a pg_dump custom-format file',
|
|
404
|
+
options: [
|
|
405
|
+
{ name: 'file', alias: 'f', description: 'Dump file to restore from', required: true },
|
|
406
|
+
{ name: 'clean', alias: 'c', description: 'Drop objects before restore (--clean)', required: false },
|
|
407
|
+
],
|
|
408
|
+
handler: async (_args, options) => {
|
|
409
|
+
const { restoreDatabase } = await import('./db-dump.js');
|
|
410
|
+
const file = typeof options['file'] === 'string' ? options['file'] : '';
|
|
411
|
+
const result = restoreDatabase({ file, clean: Boolean(options['clean']) });
|
|
412
|
+
if (!result.ok) {
|
|
413
|
+
console.error(result.message);
|
|
414
|
+
process.exitCode = 1;
|
|
415
|
+
return;
|
|
416
|
+
}
|
|
417
|
+
console.log(result.message);
|
|
418
|
+
},
|
|
419
|
+
},
|
|
420
|
+
{
|
|
421
|
+
name: 'plugin:install',
|
|
422
|
+
description: 'Install a plugin from a local path into the project plugins dir',
|
|
423
|
+
options: [
|
|
424
|
+
{ name: 'name', alias: 'n', description: 'Plugin name or path', required: true },
|
|
425
|
+
],
|
|
426
|
+
handler: async (_args, options) => {
|
|
427
|
+
const name = options['name'];
|
|
428
|
+
if (!name) {
|
|
429
|
+
console.error('Nome ou caminho do plugin é obrigatório (--name)');
|
|
430
|
+
process.exit(1);
|
|
431
|
+
}
|
|
432
|
+
const { installExtension } = await import('./installer.js');
|
|
433
|
+
try {
|
|
434
|
+
await installExtension('plugin', name);
|
|
435
|
+
}
|
|
436
|
+
catch (error) {
|
|
437
|
+
console.error(error instanceof Error ? error.message : error);
|
|
438
|
+
process.exit(1);
|
|
439
|
+
}
|
|
440
|
+
},
|
|
441
|
+
},
|
|
442
|
+
{
|
|
443
|
+
name: 'plugin:list',
|
|
444
|
+
description: 'List installed plugins',
|
|
445
|
+
options: [],
|
|
446
|
+
handler: async () => {
|
|
447
|
+
const { listExtensions } = await import('./installer.js');
|
|
448
|
+
listExtensions('plugin');
|
|
449
|
+
},
|
|
450
|
+
},
|
|
451
|
+
{
|
|
452
|
+
name: 'plugin:search',
|
|
453
|
+
description: 'Search plugins in the npm registry (keywords:okcms-plugin)',
|
|
454
|
+
options: [
|
|
455
|
+
{ name: 'query', alias: 'q', description: 'Search terms', required: false },
|
|
456
|
+
],
|
|
457
|
+
handler: async (_args, options) => {
|
|
458
|
+
const { searchExtensions } = await import('./npm-registry.js');
|
|
459
|
+
try {
|
|
460
|
+
const results = await searchExtensions('plugin', options['query'] ?? '');
|
|
461
|
+
if (results.length === 0) {
|
|
462
|
+
console.log('Nenhum plugin encontrado.');
|
|
463
|
+
return;
|
|
464
|
+
}
|
|
465
|
+
for (const result of results) {
|
|
466
|
+
console.log(` ${result.name}@${result.version} — ${result.description}`);
|
|
467
|
+
}
|
|
468
|
+
}
|
|
469
|
+
catch (error) {
|
|
470
|
+
console.error(error instanceof Error ? error.message : error);
|
|
471
|
+
process.exit(1);
|
|
472
|
+
}
|
|
473
|
+
},
|
|
474
|
+
},
|
|
475
|
+
{
|
|
476
|
+
name: 'plugin:manage',
|
|
477
|
+
description: 'Show info or enable/disable an installed plugin (--info | --enable | --disable)',
|
|
478
|
+
options: [
|
|
479
|
+
{ name: 'name', alias: 'n', description: 'Plugin name', required: true },
|
|
480
|
+
{ name: 'info', alias: 'i', description: 'Show manifest info', required: false },
|
|
481
|
+
{ name: 'enable', alias: 'e', description: 'Enable the plugin', required: false },
|
|
482
|
+
{ name: 'disable', alias: 'd', description: 'Disable the plugin', required: false },
|
|
483
|
+
{ name: 'uninstall', alias: 'u', description: 'Uninstall (remove files + registro)', required: false },
|
|
484
|
+
],
|
|
485
|
+
handler: async (_args, options) => {
|
|
486
|
+
const { readFileSync } = await import('node:fs');
|
|
487
|
+
const { join } = await import('node:path');
|
|
488
|
+
const { loadProjectConfig } = await import('./project-config.js');
|
|
489
|
+
const { setEnabled, loadExtensionsState, uninstallExtension } = await import('./extensions-state.js');
|
|
490
|
+
const name = options['name'];
|
|
491
|
+
if (!name) {
|
|
492
|
+
console.error('Nome do plugin é obrigatório (--name)');
|
|
493
|
+
process.exit(1);
|
|
494
|
+
}
|
|
495
|
+
if (options['uninstall'] !== undefined) {
|
|
496
|
+
const result = uninstallExtension('plugin', name);
|
|
497
|
+
if (!result.removed) {
|
|
498
|
+
console.log(`Plugin "${name}" não estava instalado.`);
|
|
499
|
+
process.exitCode = 1;
|
|
500
|
+
return;
|
|
501
|
+
}
|
|
502
|
+
console.log(`Plugin "${name}" desinstalado (${result.targetDir}).`);
|
|
503
|
+
return;
|
|
504
|
+
}
|
|
505
|
+
if (options['enable'] !== undefined || options['disable'] !== undefined) {
|
|
506
|
+
const enabled = options['enable'] !== undefined;
|
|
507
|
+
const ok = setEnabled('plugin', name, enabled);
|
|
508
|
+
console.log(ok ? `Plugin "${name}" ${enabled ? 'habilitado' : 'desabilitado'}.` : `Plugin "${name}" não está registrado.`);
|
|
509
|
+
process.exitCode = ok ? 0 : 1;
|
|
510
|
+
return;
|
|
511
|
+
}
|
|
512
|
+
// default/--info: mostra manifesto + estado
|
|
513
|
+
const config = loadProjectConfig();
|
|
514
|
+
const manifestPath = join(process.cwd(), config.pluginsDir, name, 'manifest.json');
|
|
515
|
+
try {
|
|
516
|
+
const manifest = JSON.parse(readFileSync(manifestPath, 'utf-8'));
|
|
517
|
+
const state = loadExtensionsState().plugins[name];
|
|
518
|
+
console.log(`Nome: ${manifest['name'] ?? name}`);
|
|
519
|
+
console.log(`Versão: ${manifest['version'] ?? '?'}`);
|
|
520
|
+
console.log(`Descrição: ${manifest['description'] ?? '-'}`);
|
|
521
|
+
console.log(`Entry: ${manifest['main'] ?? '?'}`);
|
|
522
|
+
console.log(`Permissões: ${manifest['permissions']?.join(', ') ?? '-'}`);
|
|
523
|
+
console.log(`Estado: ${state?.enabled === false ? 'desabilitado' : 'habilitado'}`);
|
|
524
|
+
}
|
|
525
|
+
catch {
|
|
526
|
+
console.error(`Manifesto não encontrado em ${manifestPath}`);
|
|
527
|
+
process.exit(1);
|
|
528
|
+
}
|
|
529
|
+
},
|
|
530
|
+
},
|
|
531
|
+
{
|
|
532
|
+
name: 'theme:install',
|
|
533
|
+
description: 'Install a theme from a local path into the project themes dir',
|
|
534
|
+
options: [
|
|
535
|
+
{ name: 'name', alias: 'n', description: 'Theme name or path', required: true },
|
|
536
|
+
],
|
|
537
|
+
handler: async (_args, options) => {
|
|
538
|
+
const name = options['name'];
|
|
539
|
+
if (!name) {
|
|
540
|
+
console.error('Nome ou caminho do tema é obrigatório (--name)');
|
|
541
|
+
process.exit(1);
|
|
542
|
+
}
|
|
543
|
+
const { installExtension } = await import('./installer.js');
|
|
544
|
+
try {
|
|
545
|
+
await installExtension('theme', name);
|
|
546
|
+
}
|
|
547
|
+
catch (error) {
|
|
548
|
+
console.error(error instanceof Error ? error.message : error);
|
|
549
|
+
process.exit(1);
|
|
550
|
+
}
|
|
551
|
+
},
|
|
552
|
+
},
|
|
553
|
+
{
|
|
554
|
+
name: 'theme:list',
|
|
555
|
+
description: 'List installed themes',
|
|
556
|
+
options: [],
|
|
557
|
+
handler: async () => {
|
|
558
|
+
const { listExtensions } = await import('./installer.js');
|
|
559
|
+
listExtensions('theme');
|
|
560
|
+
},
|
|
561
|
+
},
|
|
562
|
+
{
|
|
563
|
+
name: 'theme:search',
|
|
564
|
+
description: 'Search themes in the npm registry (keywords:okcms-theme)',
|
|
565
|
+
options: [
|
|
566
|
+
{ name: 'query', alias: 'q', description: 'Search terms', required: false },
|
|
567
|
+
],
|
|
568
|
+
handler: async (_args, options) => {
|
|
569
|
+
const { searchExtensions } = await import('./npm-registry.js');
|
|
570
|
+
try {
|
|
571
|
+
const results = await searchExtensions('theme', options['query'] ?? '');
|
|
572
|
+
if (results.length === 0) {
|
|
573
|
+
console.log('Nenhum tema encontrado.');
|
|
574
|
+
return;
|
|
575
|
+
}
|
|
576
|
+
for (const result of results) {
|
|
577
|
+
console.log(` ${result.name}@${result.version} — ${result.description}`);
|
|
578
|
+
}
|
|
579
|
+
}
|
|
580
|
+
catch (error) {
|
|
581
|
+
console.error(error instanceof Error ? error.message : error);
|
|
582
|
+
process.exit(1);
|
|
583
|
+
}
|
|
584
|
+
},
|
|
585
|
+
},
|
|
586
|
+
{
|
|
587
|
+
name: 'theme:manage',
|
|
588
|
+
description: 'Theme info / enable / disable / --set-active / --uninstall',
|
|
589
|
+
options: [
|
|
590
|
+
{ name: 'name', alias: 'n', description: 'Theme name', required: true },
|
|
591
|
+
{ name: 'info', alias: 'i', description: 'Show theme.json info', required: false },
|
|
592
|
+
{ name: 'enable', alias: 'e', description: 'Enable the theme', required: false },
|
|
593
|
+
{ name: 'disable', alias: 'd', description: 'Disable the theme', required: false },
|
|
594
|
+
{ name: 'uninstall', alias: 'u', description: 'Uninstall (remove files + registro)', required: false },
|
|
595
|
+
{ name: 'set-active', alias: 's', description: 'Set as active theme', required: false },
|
|
596
|
+
],
|
|
597
|
+
handler: async (_args, options) => {
|
|
598
|
+
const { readFileSync, writeFileSync, existsSync } = await import('node:fs');
|
|
599
|
+
const { join } = await import('node:path');
|
|
600
|
+
const { loadProjectConfig, DEFAULT_CONFIG_FILENAME } = await import('./project-config.js');
|
|
601
|
+
const { setEnabled, loadExtensionsState } = await import('./extensions-state.js');
|
|
602
|
+
const name = options['name'];
|
|
603
|
+
if (!name) {
|
|
604
|
+
console.error('Nome do tema é obrigatório (--name)');
|
|
605
|
+
process.exit(1);
|
|
606
|
+
}
|
|
607
|
+
if (options['uninstall'] !== undefined) {
|
|
608
|
+
const { uninstallExtension } = await import('./extensions-state.js');
|
|
609
|
+
const result = uninstallExtension('theme', name);
|
|
610
|
+
if (!result.removed) {
|
|
611
|
+
console.log(`Theme "${name}" não estava instalado.`);
|
|
612
|
+
process.exitCode = 1;
|
|
613
|
+
return;
|
|
614
|
+
}
|
|
615
|
+
console.log(`Theme "${name}" desinstalado (${result.targetDir}).`);
|
|
616
|
+
return;
|
|
617
|
+
}
|
|
618
|
+
if (options['set-active'] !== undefined) {
|
|
619
|
+
const configPath = join(process.cwd(), DEFAULT_CONFIG_FILENAME);
|
|
620
|
+
if (!existsSync(configPath)) {
|
|
621
|
+
console.error(`${DEFAULT_CONFIG_FILENAME} não encontrado; rode \`okcms init\` primeiro.`);
|
|
622
|
+
process.exit(1);
|
|
623
|
+
}
|
|
624
|
+
const raw = JSON.parse(readFileSync(configPath, 'utf-8'));
|
|
625
|
+
raw['activeTheme'] = name;
|
|
626
|
+
writeFileSync(configPath, JSON.stringify(raw, null, 2));
|
|
627
|
+
console.log(`Tema ativo definido como "${name}".`);
|
|
628
|
+
return;
|
|
629
|
+
}
|
|
630
|
+
if (options['enable'] !== undefined || options['disable'] !== undefined) {
|
|
631
|
+
const enabled = options['enable'] !== undefined;
|
|
632
|
+
const ok = setEnabled('theme', name, enabled);
|
|
633
|
+
console.log(ok ? `Tema "${name}" ${enabled ? 'habilitado' : 'desabilitado'}.` : `Tema "${name}" não está registrado.`);
|
|
634
|
+
process.exitCode = ok ? 0 : 1;
|
|
635
|
+
return;
|
|
636
|
+
}
|
|
637
|
+
const config = loadProjectConfig();
|
|
638
|
+
const themeJsonPath = join(process.cwd(), config.themesDir, name, 'theme.json');
|
|
639
|
+
try {
|
|
640
|
+
const manifest = JSON.parse(readFileSync(themeJsonPath, 'utf-8'));
|
|
641
|
+
const provides = (manifest['provides'] ?? {});
|
|
642
|
+
const state = loadExtensionsState().themes[name];
|
|
643
|
+
console.log(`Nome: ${manifest['name'] ?? name}`);
|
|
644
|
+
console.log(`Versão: ${manifest['version'] ?? '?'}`);
|
|
645
|
+
console.log(`Autor: ${manifest['author'] ?? '-'}`);
|
|
646
|
+
console.log(`Parent: ${manifest['parent'] ?? '-'}`);
|
|
647
|
+
console.log(`Layouts: ${provides['layouts']?.join(', ') ?? '-'}`);
|
|
648
|
+
console.log(`Ativo: ${config.activeTheme === name ? 'sim' : 'não'} (${config.activeTheme || 'nenhum'})`);
|
|
649
|
+
console.log(`Estado: ${state?.enabled === false ? 'desabilitado' : 'habilitado'}`);
|
|
650
|
+
}
|
|
651
|
+
catch {
|
|
652
|
+
console.error(`theme.json não encontrado em ${themeJsonPath}`);
|
|
653
|
+
process.exit(1);
|
|
654
|
+
}
|
|
655
|
+
},
|
|
656
|
+
},
|
|
657
|
+
{
|
|
658
|
+
name: 'media:migrate',
|
|
659
|
+
description: 'Migrate media files between storage drivers (local <-> s3/r2/minio)',
|
|
660
|
+
options: [
|
|
661
|
+
{ name: 'from', alias: 'f', description: 'Source driver (local|s3|r2|minio)', required: true },
|
|
662
|
+
{ name: 'to', alias: 't', description: 'Target driver (local|s3|r2|minio)', required: true },
|
|
663
|
+
],
|
|
664
|
+
handler: async (_args, options) => {
|
|
665
|
+
const from = options['from'] ?? 'local';
|
|
666
|
+
const to = options['to'] ?? 's3';
|
|
667
|
+
console.log(`Migrando mídia de "${from}" para "${to}"...`);
|
|
668
|
+
const { initDb } = await import('./db-init.js');
|
|
669
|
+
await initDb();
|
|
670
|
+
const { createMigrator } = await import('@oktis-works/core');
|
|
671
|
+
const migrator = createMigrator(from, to);
|
|
672
|
+
const result = await migrator.migrateAll();
|
|
673
|
+
console.log(`Migrados: ${result.migrated}, Falhas: ${result.failed}`);
|
|
674
|
+
for (const err of result.errors) {
|
|
675
|
+
console.error(` ! ${err.filename}: ${err.error}`);
|
|
676
|
+
}
|
|
677
|
+
},
|
|
678
|
+
},
|
|
679
|
+
{
|
|
680
|
+
name: 'user:create',
|
|
681
|
+
description: 'Create a new user',
|
|
682
|
+
options: [
|
|
683
|
+
{ name: 'email', alias: 'e', description: 'User email', required: true },
|
|
684
|
+
{ name: 'password', alias: 'p', description: 'User password', required: true },
|
|
685
|
+
{ name: 'role', alias: 'r', description: 'User role', required: false, default: 'editor' },
|
|
686
|
+
],
|
|
687
|
+
handler: async (_args, _options) => {
|
|
688
|
+
console.error('user:create ainda não está implementado — use o fluxo de onboarding da API (POST /api/v1/auth/register).');
|
|
689
|
+
process.exitCode = 1;
|
|
690
|
+
},
|
|
691
|
+
},
|
|
692
|
+
{
|
|
693
|
+
name: 'seed',
|
|
694
|
+
description: 'Seed the database with initial data',
|
|
695
|
+
options: [],
|
|
696
|
+
handler: async () => {
|
|
697
|
+
console.error('seed ainda não está implementado — nenhuma alteração foi feita no banco.');
|
|
698
|
+
process.exitCode = 1;
|
|
699
|
+
},
|
|
700
|
+
},
|
|
701
|
+
{
|
|
702
|
+
name: 'system:status',
|
|
703
|
+
description: 'Show system status (environment + database health)',
|
|
704
|
+
options: [],
|
|
705
|
+
handler: async () => {
|
|
706
|
+
const { healthCheck } = await import('@oktis-works/database');
|
|
707
|
+
const { loadConfig } = await import('@oktis-works/config');
|
|
708
|
+
const config = loadConfig();
|
|
709
|
+
console.log('OkCMS Status');
|
|
710
|
+
console.log(` Environment: ${config.app.nodeEnv}`);
|
|
711
|
+
console.log(` Host: ${config.app.host}:${config.app.port}`);
|
|
712
|
+
const dbOk = await healthCheck();
|
|
713
|
+
console.log(` Database: ${dbOk ? 'connected' : 'disconnected'}`);
|
|
714
|
+
},
|
|
715
|
+
},
|
|
716
|
+
];
|
|
717
|
+
export function getCommand(name) {
|
|
718
|
+
return commands.find((c) => c.name === name);
|
|
719
|
+
}
|
|
720
|
+
//# sourceMappingURL=commands.js.map
|