fabroku 0.1.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.
- package/README.md +100 -0
- package/bin/fabroku.js +68 -0
- package/lib/api.js +70 -0
- package/lib/commands/apps.js +87 -0
- package/lib/commands/login.js +152 -0
- package/lib/commands/verify.js +175 -0
- package/lib/commands/whoami.js +36 -0
- package/lib/config.js +64 -0
- package/package.json +31 -0
package/README.md
ADDED
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
# 🚀 Fabroku CLI
|
|
2
|
+
|
|
3
|
+
Ferramenta de linha de comando para o [Fabroku PaaS](https://github.com/fabricadesoftware-ifc/Fabroku) — verifica arquivos de deploy, autentica via GitHub e gerencia apps.
|
|
4
|
+
|
|
5
|
+
## Instalação
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
npm i -g fabroku
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
> Requer Node.js 18+
|
|
12
|
+
|
|
13
|
+
## Comandos
|
|
14
|
+
|
|
15
|
+
### `fabroku verify`
|
|
16
|
+
|
|
17
|
+
Verifica se o projeto tem os arquivos necessários para deploy no Dokku.
|
|
18
|
+
|
|
19
|
+
```bash
|
|
20
|
+
# No diretório do projeto
|
|
21
|
+
fabroku verify
|
|
22
|
+
|
|
23
|
+
# Especificando diretório
|
|
24
|
+
fabroku verify --dir ./meu-projeto
|
|
25
|
+
|
|
26
|
+
# Forçar tipo (frontend ou backend)
|
|
27
|
+
fabroku verify --type backend
|
|
28
|
+
|
|
29
|
+
# Gerar arquivos faltantes
|
|
30
|
+
fabroku verify --fix
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
**Frontend** (Vue, React, etc.) precisa de:
|
|
34
|
+
- `.buildpacks`
|
|
35
|
+
- `.static`
|
|
36
|
+
- `static.json`
|
|
37
|
+
|
|
38
|
+
**Backend** (Django, Flask, etc.) precisa de:
|
|
39
|
+
- `Procfile`
|
|
40
|
+
- `requirements.txt`
|
|
41
|
+
- `runtime.txt`
|
|
42
|
+
|
|
43
|
+
### `fabroku login`
|
|
44
|
+
|
|
45
|
+
Autenticação via GitHub OAuth — abre o navegador automaticamente.
|
|
46
|
+
|
|
47
|
+
```bash
|
|
48
|
+
fabroku login
|
|
49
|
+
|
|
50
|
+
# Apontar para API de produção
|
|
51
|
+
fabroku login --api-url https://api.fabroku.ifc.edu.br
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
### `fabroku logout`
|
|
55
|
+
|
|
56
|
+
Encerrar sessão.
|
|
57
|
+
|
|
58
|
+
```bash
|
|
59
|
+
fabroku logout
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
### `fabroku whoami`
|
|
63
|
+
|
|
64
|
+
Verificar usuário autenticado e status do token.
|
|
65
|
+
|
|
66
|
+
```bash
|
|
67
|
+
fabroku whoami
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
### `fabroku apps`
|
|
71
|
+
|
|
72
|
+
Listar seus apps.
|
|
73
|
+
|
|
74
|
+
```bash
|
|
75
|
+
fabroku apps
|
|
76
|
+
|
|
77
|
+
# Filtrar por projeto
|
|
78
|
+
fabroku apps --project 42
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
## Configuração
|
|
82
|
+
|
|
83
|
+
A CLI salva as credenciais em `~/.fabroku/config.json`:
|
|
84
|
+
|
|
85
|
+
```json
|
|
86
|
+
{
|
|
87
|
+
"api_url": "http://localhost:8000",
|
|
88
|
+
"token": "...",
|
|
89
|
+
"user": "seu-usuario"
|
|
90
|
+
}
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
## Desenvolvimento
|
|
94
|
+
|
|
95
|
+
```bash
|
|
96
|
+
cd Fabroku_CLI
|
|
97
|
+
npm install
|
|
98
|
+
npm link # Instala globalmente em modo dev
|
|
99
|
+
fabroku --help # Testa
|
|
100
|
+
```
|
package/bin/fabroku.js
ADDED
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* 🚀 Fabroku CLI — Ferramenta de deploy para o Fabroku
|
|
5
|
+
*
|
|
6
|
+
* Instalação: npm i -g fabroku
|
|
7
|
+
* Uso: fabroku <comando> [opções]
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import { Command } from "commander";
|
|
11
|
+
|
|
12
|
+
import { login, logout } from "../lib/commands/login.js";
|
|
13
|
+
import { verify } from "../lib/commands/verify.js";
|
|
14
|
+
import { apps } from "../lib/commands/apps.js";
|
|
15
|
+
import { whoami } from "../lib/commands/whoami.js";
|
|
16
|
+
|
|
17
|
+
const program = new Command();
|
|
18
|
+
|
|
19
|
+
program
|
|
20
|
+
.name("fabroku")
|
|
21
|
+
.description("🚀 Fabroku CLI — Ferramenta de deploy para o Fabroku PaaS")
|
|
22
|
+
.version("0.1.0");
|
|
23
|
+
|
|
24
|
+
// ---- login ----
|
|
25
|
+
program
|
|
26
|
+
.command("login")
|
|
27
|
+
.description("Autenticar na plataforma Fabroku via GitHub")
|
|
28
|
+
.option("--api-url <url>", "URL base da API Fabroku")
|
|
29
|
+
.action(async (options) => {
|
|
30
|
+
await login({ apiUrl: options.apiUrl });
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
// ---- logout ----
|
|
34
|
+
program
|
|
35
|
+
.command("logout")
|
|
36
|
+
.description("Encerrar a sessão da CLI")
|
|
37
|
+
.action(() => logout());
|
|
38
|
+
|
|
39
|
+
// ---- verify ----
|
|
40
|
+
program
|
|
41
|
+
.command("verify")
|
|
42
|
+
.description("Verificar se o projeto tem os arquivos necessários para deploy")
|
|
43
|
+
.option("-d, --dir <path>", "Diretório do projeto", ".")
|
|
44
|
+
.option("-t, --type <type>", "Tipo da aplicação (frontend ou backend)")
|
|
45
|
+
.option("--fix", "Gerar arquivos faltantes automaticamente")
|
|
46
|
+
.action((options) => {
|
|
47
|
+
const code = verify(options);
|
|
48
|
+
if (code) process.exit(code);
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
// ---- apps ----
|
|
52
|
+
program
|
|
53
|
+
.command("apps")
|
|
54
|
+
.description("Listar seus apps na plataforma Fabroku")
|
|
55
|
+
.option("-p, --project <id>", "Filtrar por ID do projeto")
|
|
56
|
+
.action(async (options) => {
|
|
57
|
+
await apps(options);
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
// ---- whoami ----
|
|
61
|
+
program
|
|
62
|
+
.command("whoami")
|
|
63
|
+
.description("Verificar o usuário autenticado")
|
|
64
|
+
.action(async () => {
|
|
65
|
+
await whoami();
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
program.parse();
|
package/lib/api.js
ADDED
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Cliente HTTP para a API Fabroku.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { getApiUrl, getToken } from "./config.js";
|
|
6
|
+
|
|
7
|
+
export class APIError extends Error {
|
|
8
|
+
constructor(statusCode, detail) {
|
|
9
|
+
super(`[${statusCode}] ${detail}`);
|
|
10
|
+
this.statusCode = statusCode;
|
|
11
|
+
this.detail = detail;
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export class FabrokuAPI {
|
|
16
|
+
constructor() {
|
|
17
|
+
this.baseUrl = getApiUrl();
|
|
18
|
+
this.token = getToken();
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
get headers() {
|
|
22
|
+
const h = { Accept: "application/json" };
|
|
23
|
+
if (this.token) h.Authorization = `CLI ${this.token}`;
|
|
24
|
+
return h;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
async request(method, path, options = {}) {
|
|
28
|
+
const url = `${this.baseUrl}${path}`;
|
|
29
|
+
const resp = await fetch(url, {
|
|
30
|
+
method,
|
|
31
|
+
headers: { ...this.headers, ...options.headers },
|
|
32
|
+
body: options.body ? JSON.stringify(options.body) : undefined,
|
|
33
|
+
signal: AbortSignal.timeout(15000),
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
if (!resp.ok) {
|
|
37
|
+
let detail;
|
|
38
|
+
try {
|
|
39
|
+
const data = await resp.json();
|
|
40
|
+
detail = data.detail || JSON.stringify(data);
|
|
41
|
+
} catch {
|
|
42
|
+
detail = await resp.text();
|
|
43
|
+
}
|
|
44
|
+
throw new APIError(resp.status, detail);
|
|
45
|
+
}
|
|
46
|
+
return resp.json();
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
async get(path) {
|
|
50
|
+
return this.request("GET", path);
|
|
51
|
+
}
|
|
52
|
+
async post(path, body) {
|
|
53
|
+
return this.request("POST", path, { body });
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
// --- Endpoints ---
|
|
57
|
+
|
|
58
|
+
async checkAuth() {
|
|
59
|
+
return this.get("/api/auth/check/");
|
|
60
|
+
}
|
|
61
|
+
async listApps() {
|
|
62
|
+
return this.get("/api/apps/apps/");
|
|
63
|
+
}
|
|
64
|
+
async listProjects() {
|
|
65
|
+
return this.get("/api/projects/projects/");
|
|
66
|
+
}
|
|
67
|
+
async getUserMe() {
|
|
68
|
+
return this.get("/api/auth/users/me/");
|
|
69
|
+
}
|
|
70
|
+
}
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Comando `fabroku apps` — Listar apps do usuário.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import chalk from "chalk";
|
|
6
|
+
|
|
7
|
+
import { FabrokuAPI, APIError } from "../api.js";
|
|
8
|
+
import { isAuthenticated } from "../config.js";
|
|
9
|
+
|
|
10
|
+
const STATUS_COLORS = {
|
|
11
|
+
RUNNING: "green",
|
|
12
|
+
STOPPED: "red",
|
|
13
|
+
ERROR: "red",
|
|
14
|
+
STARTING: "yellow",
|
|
15
|
+
DEPLOYING: "cyan",
|
|
16
|
+
DELETING: "magenta",
|
|
17
|
+
STOPPING: "yellow",
|
|
18
|
+
RESTARTING: "blue",
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
export async function apps(options) {
|
|
22
|
+
if (!isAuthenticated()) {
|
|
23
|
+
console.log(chalk.red("❌ Você precisa fazer login primeiro."));
|
|
24
|
+
console.log(` Use: ${chalk.bold("fabroku login")}`);
|
|
25
|
+
process.exit(1);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
const api = new FabrokuAPI();
|
|
29
|
+
|
|
30
|
+
let appList;
|
|
31
|
+
try {
|
|
32
|
+
const data = await api.listApps();
|
|
33
|
+
appList = data.results || [];
|
|
34
|
+
} catch (e) {
|
|
35
|
+
if (e instanceof APIError && e.statusCode === 401) {
|
|
36
|
+
console.log(
|
|
37
|
+
chalk.red("❌ Token expirado ou inválido. Faça login novamente."),
|
|
38
|
+
);
|
|
39
|
+
console.log(` Use: ${chalk.bold("fabroku login")}`);
|
|
40
|
+
} else {
|
|
41
|
+
console.log(chalk.red(`❌ Erro na API: ${e.message}`));
|
|
42
|
+
}
|
|
43
|
+
process.exit(1);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// Filtra por projeto
|
|
47
|
+
if (options.project) {
|
|
48
|
+
appList = appList.filter(
|
|
49
|
+
(a) => String(a.project) === String(options.project),
|
|
50
|
+
);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
if (appList.length === 0) {
|
|
54
|
+
console.log("\nNenhum app encontrado.");
|
|
55
|
+
if (options.project)
|
|
56
|
+
console.log(` (filtrado por projeto: ${options.project})`);
|
|
57
|
+
return;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// Header
|
|
61
|
+
console.log();
|
|
62
|
+
console.log(
|
|
63
|
+
chalk.dim("ID".padEnd(6)) +
|
|
64
|
+
chalk.dim("Nome".padEnd(25)) +
|
|
65
|
+
chalk.dim("Status".padEnd(14)) +
|
|
66
|
+
chalk.dim("Domínio".padEnd(30)) +
|
|
67
|
+
chalk.dim("Projeto"),
|
|
68
|
+
);
|
|
69
|
+
console.log(chalk.dim("─".repeat(85)));
|
|
70
|
+
|
|
71
|
+
// Rows
|
|
72
|
+
for (const app of appList) {
|
|
73
|
+
const status = app.status || "STOPPED";
|
|
74
|
+
const color = STATUS_COLORS[status] || "white";
|
|
75
|
+
const statusText = status.charAt(0) + status.slice(1).toLowerCase();
|
|
76
|
+
|
|
77
|
+
console.log(
|
|
78
|
+
String(app.id || "").padEnd(6) +
|
|
79
|
+
(app.name || "").padEnd(25) +
|
|
80
|
+
chalk[color](statusText.padEnd(14)) +
|
|
81
|
+
(app.domain || "-").padEnd(30) +
|
|
82
|
+
String(app.project || ""),
|
|
83
|
+
);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
console.log(`\n📦 Total: ${appList.length} app(s)\n`);
|
|
87
|
+
}
|
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Comando `fabroku login` — Autenticação via GitHub OAuth.
|
|
3
|
+
*
|
|
4
|
+
* Abre o browser, recebe o token via servidor HTTP local.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { createServer } from "node:http";
|
|
8
|
+
import { URL } from "node:url";
|
|
9
|
+
import chalk from "chalk";
|
|
10
|
+
import open from "open";
|
|
11
|
+
|
|
12
|
+
import {
|
|
13
|
+
clearCredentials,
|
|
14
|
+
getApiUrl,
|
|
15
|
+
isAuthenticated,
|
|
16
|
+
setCredentials,
|
|
17
|
+
} from "../config.js";
|
|
18
|
+
|
|
19
|
+
function findFreePort() {
|
|
20
|
+
return new Promise((resolve, reject) => {
|
|
21
|
+
const server = createServer();
|
|
22
|
+
server.listen(0, () => {
|
|
23
|
+
const { port } = server.address();
|
|
24
|
+
server.close(() => resolve(port));
|
|
25
|
+
});
|
|
26
|
+
server.on("error", reject);
|
|
27
|
+
});
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function htmlPage(title, body) {
|
|
31
|
+
return `<!DOCTYPE html>
|
|
32
|
+
<html><head><meta charset="utf-8"><title>${title}</title>
|
|
33
|
+
<style>
|
|
34
|
+
body{font-family:system-ui,sans-serif;display:flex;justify-content:center;
|
|
35
|
+
align-items:center;min-height:100vh;margin:0;background:#1a1a2e;color:#eee}
|
|
36
|
+
div{text-align:center;padding:2rem}
|
|
37
|
+
h1{margin-bottom:1rem}
|
|
38
|
+
</style></head>
|
|
39
|
+
<body><div>${body}</div></body></html>`;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export async function login(options) {
|
|
43
|
+
if (isAuthenticated()) {
|
|
44
|
+
const readline = await import("node:readline");
|
|
45
|
+
const rl = readline.createInterface({
|
|
46
|
+
input: process.stdin,
|
|
47
|
+
output: process.stdout,
|
|
48
|
+
});
|
|
49
|
+
const answer = await new Promise((resolve) => {
|
|
50
|
+
rl.question(
|
|
51
|
+
"Você já está autenticado. Deseja fazer login novamente? (s/N) ",
|
|
52
|
+
resolve,
|
|
53
|
+
);
|
|
54
|
+
});
|
|
55
|
+
rl.close();
|
|
56
|
+
if (answer.toLowerCase() !== "s") return;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
const baseUrl = options.apiUrl || getApiUrl();
|
|
60
|
+
const port = await findFreePort();
|
|
61
|
+
const loginUrl = `${baseUrl}/api/auth/cli/login/?port=${port}`;
|
|
62
|
+
|
|
63
|
+
console.log(`\n🔐 Abrindo browser para autenticação...`);
|
|
64
|
+
console.log(` URL: ${chalk.dim(loginUrl)}`);
|
|
65
|
+
console.log(` Aguardando callback na porta ${port}...\n`);
|
|
66
|
+
|
|
67
|
+
// Abre o browser
|
|
68
|
+
await open(loginUrl);
|
|
69
|
+
|
|
70
|
+
// Servidor local que espera o callback
|
|
71
|
+
return new Promise((resolve) => {
|
|
72
|
+
const connections = new Set();
|
|
73
|
+
|
|
74
|
+
const server = createServer((req, res) => {
|
|
75
|
+
const url = new URL(req.url, `http://localhost:${port}`);
|
|
76
|
+
|
|
77
|
+
if (url.pathname === "/callback") {
|
|
78
|
+
const token = url.searchParams.get("token");
|
|
79
|
+
const user = url.searchParams.get("user");
|
|
80
|
+
const error = url.searchParams.get("error");
|
|
81
|
+
const message = url.searchParams.get("message");
|
|
82
|
+
|
|
83
|
+
if (token) {
|
|
84
|
+
res.writeHead(200, { "Content-Type": "text/html; charset=utf-8" });
|
|
85
|
+
res.end(
|
|
86
|
+
htmlPage(
|
|
87
|
+
"Fabroku CLI — Autenticado",
|
|
88
|
+
"<h1>✅ Login realizado com sucesso!</h1><p>Pode fechar esta janela e voltar para o terminal.</p>",
|
|
89
|
+
),
|
|
90
|
+
);
|
|
91
|
+
|
|
92
|
+
setCredentials(token, user || "unknown", baseUrl);
|
|
93
|
+
console.log(`✅ Autenticado como ${chalk.green.bold(user)}`);
|
|
94
|
+
console.log(` Token salvo em ~/.fabroku/config.json\n`);
|
|
95
|
+
|
|
96
|
+
shutdown();
|
|
97
|
+
resolve();
|
|
98
|
+
} else {
|
|
99
|
+
res.writeHead(200, { "Content-Type": "text/html; charset=utf-8" });
|
|
100
|
+
res.end(
|
|
101
|
+
htmlPage(
|
|
102
|
+
"Fabroku CLI — Erro",
|
|
103
|
+
`<h1>❌ Erro na autenticação</h1><p>${message || error || "Erro desconhecido"}</p>`,
|
|
104
|
+
),
|
|
105
|
+
);
|
|
106
|
+
|
|
107
|
+
console.log(
|
|
108
|
+
chalk.red(`❌ Erro: ${error}: ${message || "Erro desconhecido"}`),
|
|
109
|
+
);
|
|
110
|
+
|
|
111
|
+
shutdown();
|
|
112
|
+
resolve();
|
|
113
|
+
}
|
|
114
|
+
} else {
|
|
115
|
+
res.writeHead(200, { "Content-Type": "text/html" });
|
|
116
|
+
res.end(htmlPage("Fabroku CLI", "<p>Aguardando callback...</p>"));
|
|
117
|
+
}
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
// Rastreia conexões para poder destruí-las ao encerrar
|
|
121
|
+
server.on("connection", (socket) => {
|
|
122
|
+
connections.add(socket);
|
|
123
|
+
socket.on("close", () => connections.delete(socket));
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
function shutdown() {
|
|
127
|
+
clearTimeout(timer);
|
|
128
|
+
server.close();
|
|
129
|
+
for (const socket of connections) socket.destroy();
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
server.listen(port);
|
|
133
|
+
|
|
134
|
+
// Timeout de 2 minutos
|
|
135
|
+
const timer = setTimeout(() => {
|
|
136
|
+
console.log(
|
|
137
|
+
chalk.red("❌ Timeout: autenticação não foi concluída em 2 minutos."),
|
|
138
|
+
);
|
|
139
|
+
shutdown();
|
|
140
|
+
resolve();
|
|
141
|
+
}, 120_000);
|
|
142
|
+
});
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
export function logout() {
|
|
146
|
+
if (!isAuthenticated()) {
|
|
147
|
+
console.log("Você não está autenticado.");
|
|
148
|
+
return;
|
|
149
|
+
}
|
|
150
|
+
clearCredentials();
|
|
151
|
+
console.log("👋 Sessão encerrada com sucesso.");
|
|
152
|
+
}
|
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Comando `fabroku verify` — Verifica arquivos necessários para deploy.
|
|
3
|
+
*
|
|
4
|
+
* Lógica:
|
|
5
|
+
* - Não-fábrica → detecta tipo (frontend/backend) → verifica arquivos
|
|
6
|
+
* - Fábrica → pode usar config personalizada
|
|
7
|
+
*
|
|
8
|
+
* Frontend: .buildpacks, .static, static.json
|
|
9
|
+
* Backend: Procfile, requirements.txt, runtime.txt
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import { existsSync, readFileSync, writeFileSync } from "node:fs";
|
|
13
|
+
import { join, resolve as pathResolve } from "node:path";
|
|
14
|
+
import chalk from "chalk";
|
|
15
|
+
|
|
16
|
+
const FRONTEND_FILES = {
|
|
17
|
+
".buildpacks": {
|
|
18
|
+
description: "Lista de buildpacks para deploy estático",
|
|
19
|
+
content:
|
|
20
|
+
"https://github.com/heroku/heroku-buildpack-nodejs\nhttps://github.com/dokku/buildpack-nginx\n",
|
|
21
|
+
},
|
|
22
|
+
".static": {
|
|
23
|
+
description: "Marcador para build estática",
|
|
24
|
+
content: "",
|
|
25
|
+
},
|
|
26
|
+
"static.json": {
|
|
27
|
+
description: "Configuração do servidor estático (rotas SPA)",
|
|
28
|
+
content:
|
|
29
|
+
JSON.stringify(
|
|
30
|
+
{
|
|
31
|
+
root: "dist/",
|
|
32
|
+
clean_urls: true,
|
|
33
|
+
routes: { "/**": "index.html" },
|
|
34
|
+
headers: {
|
|
35
|
+
"/**": { "Cache-Control": "public, max-age=0, must-revalidate" },
|
|
36
|
+
"/assets/**": {
|
|
37
|
+
"Cache-Control": "public, max-age=31536000, immutable",
|
|
38
|
+
},
|
|
39
|
+
},
|
|
40
|
+
},
|
|
41
|
+
null,
|
|
42
|
+
2,
|
|
43
|
+
) + "\n",
|
|
44
|
+
},
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
const BACKEND_FILES = {
|
|
48
|
+
Procfile: {
|
|
49
|
+
description: "Define o comando de execução do servidor",
|
|
50
|
+
content: "web: gunicorn config.wsgi --bind 0.0.0.0:$PORT\n",
|
|
51
|
+
},
|
|
52
|
+
"requirements.txt": {
|
|
53
|
+
description: "Dependências Python do projeto",
|
|
54
|
+
content: null, // Não gera automaticamente
|
|
55
|
+
},
|
|
56
|
+
"runtime.txt": {
|
|
57
|
+
description: "Versão do Python para deploy",
|
|
58
|
+
content: "python-3.13.2\n",
|
|
59
|
+
},
|
|
60
|
+
};
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Detecta o tipo de aplicação pelo diretório.
|
|
64
|
+
*/
|
|
65
|
+
function detectType(dir) {
|
|
66
|
+
// Frontend: tem package.json
|
|
67
|
+
if (existsSync(join(dir, "package.json"))) {
|
|
68
|
+
// Verifica se não é um backend Node (tem Procfile com "node")
|
|
69
|
+
const procfilePath = join(dir, "Procfile");
|
|
70
|
+
if (existsSync(procfilePath)) {
|
|
71
|
+
const content = readFileSync(procfilePath, "utf-8");
|
|
72
|
+
if (content.includes("node") || content.includes("npm")) {
|
|
73
|
+
return "backend";
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
return "frontend";
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// Backend Python: manage.py ou requirements.txt ou setup.py ou pyproject.toml
|
|
80
|
+
const backendMarkers = [
|
|
81
|
+
"manage.py",
|
|
82
|
+
"requirements.txt",
|
|
83
|
+
"setup.py",
|
|
84
|
+
"pyproject.toml",
|
|
85
|
+
"Pipfile",
|
|
86
|
+
];
|
|
87
|
+
if (backendMarkers.some((f) => existsSync(join(dir, f)))) {
|
|
88
|
+
return "backend";
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
return null;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
export function verify(options) {
|
|
95
|
+
const dir = pathResolve(options.dir || ".");
|
|
96
|
+
const forceType = options.type || null;
|
|
97
|
+
const fix = options.fix || false;
|
|
98
|
+
|
|
99
|
+
console.log(`\n📂 Verificando: ${chalk.bold(dir)}\n`);
|
|
100
|
+
|
|
101
|
+
// Detecta ou usa tipo forçado
|
|
102
|
+
let type = forceType;
|
|
103
|
+
if (!type) {
|
|
104
|
+
type = detectType(dir);
|
|
105
|
+
if (!type) {
|
|
106
|
+
console.log(
|
|
107
|
+
chalk.yellow("⚠️ Não foi possível detectar o tipo da aplicação."),
|
|
108
|
+
);
|
|
109
|
+
console.log(
|
|
110
|
+
` Use ${chalk.bold("--type frontend")} ou ${chalk.bold("--type backend")}\n`,
|
|
111
|
+
);
|
|
112
|
+
process.exit(1);
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
const typeName = type === "frontend" ? "FrontEnd" : "BackEnd";
|
|
117
|
+
const typeDesc =
|
|
118
|
+
type === "frontend"
|
|
119
|
+
? "Aplicação SPA/estática (Vue, React, etc.)"
|
|
120
|
+
: "Aplicação Python (Django, Flask, etc.)";
|
|
121
|
+
|
|
122
|
+
console.log(`🔍 Tipo detectado: ${chalk.cyan.bold(typeName)}`);
|
|
123
|
+
console.log(` ${typeDesc}\n`);
|
|
124
|
+
|
|
125
|
+
const requiredFiles = type === "frontend" ? FRONTEND_FILES : BACKEND_FILES;
|
|
126
|
+
let missing = 0;
|
|
127
|
+
let fixed = 0;
|
|
128
|
+
|
|
129
|
+
for (const [filename, info] of Object.entries(requiredFiles)) {
|
|
130
|
+
const filePath = join(dir, filename);
|
|
131
|
+
const exists = existsSync(filePath);
|
|
132
|
+
|
|
133
|
+
if (exists) {
|
|
134
|
+
console.log(` ${chalk.green("✅")} ${filename}`);
|
|
135
|
+
} else {
|
|
136
|
+
console.log(
|
|
137
|
+
` ${chalk.red("❌")} ${filename} — ${chalk.dim("faltando")}`,
|
|
138
|
+
);
|
|
139
|
+
missing++;
|
|
140
|
+
|
|
141
|
+
if (fix && info.content !== null) {
|
|
142
|
+
writeFileSync(filePath, info.content);
|
|
143
|
+
console.log(` ${chalk.yellow("→")} Gerado com conteúdo padrão`);
|
|
144
|
+
fixed++;
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
console.log();
|
|
150
|
+
|
|
151
|
+
if (missing === 0) {
|
|
152
|
+
console.log(chalk.green("🚀 Projeto pronto para deploy!\n"));
|
|
153
|
+
} else if (fix && fixed > 0) {
|
|
154
|
+
const remaining = missing - fixed;
|
|
155
|
+
console.log(chalk.yellow(`🔧 ${fixed} arquivo(s) gerado(s).`));
|
|
156
|
+
if (remaining > 0) {
|
|
157
|
+
console.log(
|
|
158
|
+
chalk.red(
|
|
159
|
+
` ${remaining} arquivo(s) precisam ser criados manualmente.`,
|
|
160
|
+
),
|
|
161
|
+
);
|
|
162
|
+
} else {
|
|
163
|
+
console.log(chalk.green("🚀 Projeto pronto para deploy!\n"));
|
|
164
|
+
}
|
|
165
|
+
} else {
|
|
166
|
+
console.log(
|
|
167
|
+
chalk.yellow(`⚠️ ${missing} arquivo(s) faltando para deploy.`),
|
|
168
|
+
);
|
|
169
|
+
console.log(
|
|
170
|
+
` Use ${chalk.bold("fabroku verify --fix")} para gerar automaticamente.\n`,
|
|
171
|
+
);
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
return missing === 0 || (fix && missing - fixed === 0) ? 0 : 1;
|
|
175
|
+
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Comando `fabroku whoami` — Verificar usuário autenticado.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import chalk from "chalk";
|
|
6
|
+
|
|
7
|
+
import { FabrokuAPI, APIError } from "../api.js";
|
|
8
|
+
import { isAuthenticated, loadConfig } from "../config.js";
|
|
9
|
+
|
|
10
|
+
export async function whoami() {
|
|
11
|
+
if (!isAuthenticated()) {
|
|
12
|
+
console.log(chalk.red("❌ Não autenticado."));
|
|
13
|
+
console.log(` Use: ${chalk.bold("fabroku login")}`);
|
|
14
|
+
process.exit(1);
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
const config = loadConfig();
|
|
18
|
+
console.log(`\n👤 Logado como: ${chalk.green.bold(config.user || "?")}`);
|
|
19
|
+
console.log(` API: ${chalk.dim(config.api_url)}`);
|
|
20
|
+
|
|
21
|
+
try {
|
|
22
|
+
const api = new FabrokuAPI();
|
|
23
|
+
const user = await api.getUserMe();
|
|
24
|
+
|
|
25
|
+
console.log(` Email: ${user.email}`);
|
|
26
|
+
if (user.is_fabric) console.log(" 🏭 Membro da Fábrica");
|
|
27
|
+
if (user.is_superuser) console.log(" 🔑 Administrador");
|
|
28
|
+
console.log(chalk.green(" ✅ Token válido\n"));
|
|
29
|
+
} catch (e) {
|
|
30
|
+
if (e instanceof APIError && e.statusCode === 401) {
|
|
31
|
+
console.log(chalk.red(" ❌ Token expirado ou inválido\n"));
|
|
32
|
+
} else {
|
|
33
|
+
console.log(chalk.yellow(` ⚠️ Erro ao verificar: ${e.message}\n`));
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
}
|
package/lib/config.js
ADDED
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Gerenciamento de configuração da CLI (~/.fabroku/config.json).
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
6
|
+
import { homedir } from "node:os";
|
|
7
|
+
import { join } from "node:path";
|
|
8
|
+
|
|
9
|
+
const CONFIG_DIR = join(homedir(), ".fabroku");
|
|
10
|
+
const CONFIG_FILE = join(CONFIG_DIR, "config.json");
|
|
11
|
+
|
|
12
|
+
const DEFAULT_CONFIG = {
|
|
13
|
+
api_url: "https://fabroku-api.fabricadesoftware.ifc.edu.br",
|
|
14
|
+
token: null,
|
|
15
|
+
user: null,
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
function ensureDir() {
|
|
19
|
+
if (!existsSync(CONFIG_DIR)) {
|
|
20
|
+
mkdirSync(CONFIG_DIR, { recursive: true });
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export function loadConfig() {
|
|
25
|
+
ensureDir();
|
|
26
|
+
if (!existsSync(CONFIG_FILE)) {
|
|
27
|
+
saveConfig(DEFAULT_CONFIG);
|
|
28
|
+
return { ...DEFAULT_CONFIG };
|
|
29
|
+
}
|
|
30
|
+
return JSON.parse(readFileSync(CONFIG_FILE, "utf-8"));
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export function saveConfig(config) {
|
|
34
|
+
ensureDir();
|
|
35
|
+
writeFileSync(CONFIG_FILE, JSON.stringify(config, null, 2));
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export function getToken() {
|
|
39
|
+
return loadConfig().token;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export function getApiUrl() {
|
|
43
|
+
return loadConfig().api_url || DEFAULT_CONFIG.api_url;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export function setCredentials(token, user, apiUrl) {
|
|
47
|
+
const config = loadConfig();
|
|
48
|
+
config.token = token;
|
|
49
|
+
config.user = user;
|
|
50
|
+
if (apiUrl) config.api_url = apiUrl;
|
|
51
|
+
saveConfig(config);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export function clearCredentials() {
|
|
55
|
+
const config = loadConfig();
|
|
56
|
+
config.token = null;
|
|
57
|
+
config.user = null;
|
|
58
|
+
config.api_url = DEFAULT_CONFIG.api_url;
|
|
59
|
+
saveConfig(config);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export function isAuthenticated() {
|
|
63
|
+
return getToken() !== null;
|
|
64
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "fabroku",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "CLI para o Fabroku PaaS — Verificação de deploy, autenticação e gerenciamento de apps.",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"bin": {
|
|
7
|
+
"fabroku": "./bin/fabroku.js"
|
|
8
|
+
},
|
|
9
|
+
"engines": {
|
|
10
|
+
"node": ">=18.0.0"
|
|
11
|
+
},
|
|
12
|
+
"files": [
|
|
13
|
+
"bin/",
|
|
14
|
+
"lib/",
|
|
15
|
+
"README.md"
|
|
16
|
+
],
|
|
17
|
+
"keywords": [
|
|
18
|
+
"fabroku",
|
|
19
|
+
"deploy",
|
|
20
|
+
"cli",
|
|
21
|
+
"paas",
|
|
22
|
+
"dokku"
|
|
23
|
+
],
|
|
24
|
+
"author": "Fábrica de Software IFC",
|
|
25
|
+
"license": "MIT",
|
|
26
|
+
"dependencies": {
|
|
27
|
+
"chalk": "^5.3.0",
|
|
28
|
+
"commander": "^12.1.0",
|
|
29
|
+
"open": "^10.1.0"
|
|
30
|
+
}
|
|
31
|
+
}
|