@nicolasarths/devserver 0.0.0-stage → 1.0.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/builder.js +105 -0
- package/config.js +68 -0
- package/deploy.js +197 -0
- package/github/git-identity.js +85 -0
- package/github/git-local.js +60 -0
- package/github/github-api.js +86 -0
- package/github/index.js +152 -0
- package/github/prompts.js +11 -0
- package/http.js +105 -0
- package/index.js +65 -0
- package/livereload.js +45 -0
- package/package.json +11 -3
- package/watcher.js +37 -0
- package/README.md +0 -3
package/builder.js
ADDED
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Gerador de Snapshot e Hooks de Build
|
|
3
|
+
* Responsabilidade: Gerar projeto_completo.txt na raiz do projeto e executar hooks opcionais (ex: modulos.json).
|
|
4
|
+
*/
|
|
5
|
+
const fs = require("fs");
|
|
6
|
+
const path = require("path");
|
|
7
|
+
|
|
8
|
+
let isGenerating = false;
|
|
9
|
+
|
|
10
|
+
function executarHooksOpcionais(rootDir) {
|
|
11
|
+
const painelDir = path.join(rootDir, "painel");
|
|
12
|
+
const modulosPath = path.join(rootDir, "modulos.json");
|
|
13
|
+
|
|
14
|
+
if (!fs.existsSync(painelDir) || !fs.existsSync(modulosPath)) return;
|
|
15
|
+
|
|
16
|
+
const estrutura = {};
|
|
17
|
+
const itens = fs.readdirSync(painelDir, { withFileTypes: true });
|
|
18
|
+
|
|
19
|
+
itens.filter((i) => i.isDirectory()).forEach((pasta) => {
|
|
20
|
+
const nomePasta = pasta.name;
|
|
21
|
+
const caminhoPasta = path.join(painelDir, nomePasta);
|
|
22
|
+
const arquivos = fs.readdirSync(caminhoPasta);
|
|
23
|
+
|
|
24
|
+
estrutura[nomePasta] = {
|
|
25
|
+
temCustomJS: arquivos.includes("module.js"),
|
|
26
|
+
registros: []
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
arquivos.filter((f) => f.endsWith(".json")).forEach((arq) => {
|
|
30
|
+
try {
|
|
31
|
+
const conteudo = JSON.parse(fs.readFileSync(path.join(caminhoPasta, arq), "utf-8"));
|
|
32
|
+
if (Array.isArray(conteudo)) {
|
|
33
|
+
conteudo.forEach((item, idx) => {
|
|
34
|
+
if (!item.id) item.id = `${nomePasta}-${arq.replace(".json", "")}-${idx}`;
|
|
35
|
+
estrutura[nomePasta].registros.push(item);
|
|
36
|
+
});
|
|
37
|
+
} else if (typeof conteudo === "object" && conteudo !== null) {
|
|
38
|
+
conteudo.id = conteudo.id || `${nomePasta}-${arq.replace(".json", "")}`;
|
|
39
|
+
estrutura[nomePasta].registros.push(conteudo);
|
|
40
|
+
}
|
|
41
|
+
} catch (_) {}
|
|
42
|
+
});
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
fs.writeFileSync(modulosPath, JSON.stringify(estrutura, null, 2), { encoding: "utf-8", flag: "w" });
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function gerarProjetoCompleto(rootDir, extensoesValidas = [".js", ".html", ".css", ".json"]) {
|
|
49
|
+
if (isGenerating) return;
|
|
50
|
+
isGenerating = true;
|
|
51
|
+
|
|
52
|
+
try {
|
|
53
|
+
const destino = path.join(rootDir, "projeto_completo.txt");
|
|
54
|
+
let output = "";
|
|
55
|
+
|
|
56
|
+
const ignorarNomes = new Set([
|
|
57
|
+
".git",
|
|
58
|
+
"node_modules",
|
|
59
|
+
"dist",
|
|
60
|
+
"build",
|
|
61
|
+
".next",
|
|
62
|
+
".vscode",
|
|
63
|
+
"package-lock.json",
|
|
64
|
+
"yarn.lock",
|
|
65
|
+
"pnpm-lock.yaml",
|
|
66
|
+
".devserver.json",
|
|
67
|
+
"server.config.json"
|
|
68
|
+
]);
|
|
69
|
+
|
|
70
|
+
function varrerDiretorio(dir) {
|
|
71
|
+
const itens = fs.readdirSync(dir, { withFileTypes: true });
|
|
72
|
+
for (const item of itens) {
|
|
73
|
+
const nomeLower = item.name.toLowerCase();
|
|
74
|
+
|
|
75
|
+
if (ignorarNomes.has(nomeLower) || nomeLower.startsWith("projeto_completo")) {
|
|
76
|
+
continue;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
const fullPath = path.join(dir, item.name);
|
|
80
|
+
if (item.isDirectory()) {
|
|
81
|
+
varrerDiretorio(fullPath);
|
|
82
|
+
} else if (item.isFile()) {
|
|
83
|
+
const ext = path.extname(item.name).toLowerCase();
|
|
84
|
+
if (extensoesValidas.includes(ext)) {
|
|
85
|
+
const relPath = "\\" + path.relative(rootDir, fullPath);
|
|
86
|
+
const content = fs.readFileSync(fullPath, "utf-8");
|
|
87
|
+
output += `=== ARQUIVO: ${relPath} ===\n${content}\n\n`;
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
varrerDiretorio(rootDir);
|
|
94
|
+
fs.writeFileSync(destino, output, { encoding: "utf-8", flag: "w" });
|
|
95
|
+
} catch (err) {
|
|
96
|
+
console.error("❌ [Builder] Erro ao gerar projeto_completo.txt:", err.message);
|
|
97
|
+
} finally {
|
|
98
|
+
isGenerating = false;
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
module.exports = {
|
|
103
|
+
executarHooksOpcionais,
|
|
104
|
+
gerarProjetoCompleto
|
|
105
|
+
};
|
package/config.js
ADDED
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Gerenciador de Configuração por Projeto
|
|
3
|
+
* Responsabilidade: Detectar metadados do projeto via process.cwd() e persistir ajustes em .devserver.json na raiz do projeto.
|
|
4
|
+
*/
|
|
5
|
+
const fs = require("fs");
|
|
6
|
+
const path = require("path");
|
|
7
|
+
const { execSync } = require("child_process");
|
|
8
|
+
|
|
9
|
+
const CONFIG_FILENAME = ".devserver.json";
|
|
10
|
+
|
|
11
|
+
function detectarGitRemote(rootDir) {
|
|
12
|
+
try {
|
|
13
|
+
const url = execSync("git remote get-url origin", {
|
|
14
|
+
cwd: rootDir,
|
|
15
|
+
stdio: ["ignore", "pipe", "ignore"]
|
|
16
|
+
})
|
|
17
|
+
.toString()
|
|
18
|
+
.trim();
|
|
19
|
+
|
|
20
|
+
const match = url.match(/github\.com[:/]([^/]+)\/([^/.]+)(\.git)?$/i);
|
|
21
|
+
if (match) {
|
|
22
|
+
return {
|
|
23
|
+
githubUser: match[1],
|
|
24
|
+
repoName: match[2],
|
|
25
|
+
remoteUrl: url
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
} catch (_) {}
|
|
29
|
+
return null;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function carregarConfig(rootDir) {
|
|
33
|
+
const configPath = path.join(rootDir, CONFIG_FILENAME);
|
|
34
|
+
const nomePastaRaiz = path.basename(rootDir);
|
|
35
|
+
const gitInfo = detectarGitRemote(rootDir);
|
|
36
|
+
|
|
37
|
+
const padrao = {
|
|
38
|
+
projectName: nomePastaRaiz,
|
|
39
|
+
port: 8081,
|
|
40
|
+
githubUser: gitInfo?.githubUser || "",
|
|
41
|
+
repoName: gitInfo?.repoName || nomePastaRaiz,
|
|
42
|
+
branch: "main",
|
|
43
|
+
pagesCheckFile: fs.existsSync(path.join(rootDir, "manifest.json")) ? "manifest.json" : "index.html",
|
|
44
|
+
snapshotExtensions: [".js", ".html", ".css", ".json", ".ts", ".jsx", ".tsx", ".md"]
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
if (fs.existsSync(configPath)) {
|
|
48
|
+
try {
|
|
49
|
+
const salvo = JSON.parse(fs.readFileSync(configPath, "utf-8"));
|
|
50
|
+
return { ...padrao, ...salvo };
|
|
51
|
+
} catch (_) {}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
salvarConfig(rootDir, padrao);
|
|
55
|
+
return padrao;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function salvarConfig(rootDir, novaConfig) {
|
|
59
|
+
const configPath = path.join(rootDir, CONFIG_FILENAME);
|
|
60
|
+
fs.writeFileSync(configPath, JSON.stringify(novaConfig, null, 2), "utf-8");
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
module.exports = {
|
|
64
|
+
CONFIG_FILENAME,
|
|
65
|
+
carregarConfig,
|
|
66
|
+
salvarConfig,
|
|
67
|
+
detectarGitRemote
|
|
68
|
+
};
|
package/deploy.js
ADDED
|
@@ -0,0 +1,197 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* CLI Interativa de Deploy e Monitoramento do GitHub Pages
|
|
3
|
+
* Responsabilidade: Garantir identidade Git no boot/commit, versionar arquivos, executar git push e monitorar Pages.
|
|
4
|
+
*/
|
|
5
|
+
const fs = require("fs");
|
|
6
|
+
const path = require("path");
|
|
7
|
+
const readline = require("readline");
|
|
8
|
+
const { execSync } = require("child_process");
|
|
9
|
+
const { executarHooksOpcionais, gerarProjetoCompleto } = require("./builder");
|
|
10
|
+
const { executarAssistenteGitHub, garantirIdentidadeGit } = require("./github");
|
|
11
|
+
const { detectarGitRemote } = require("./config");
|
|
12
|
+
|
|
13
|
+
async function aguardarDeployGitHubPages(versaoAlvo, config) {
|
|
14
|
+
if (!config.githubUser || !config.repoName) return;
|
|
15
|
+
|
|
16
|
+
const checkFile = config.pagesCheckFile || "index.html";
|
|
17
|
+
const urlBase = `https://${config.githubUser}.github.io/${config.repoName}`;
|
|
18
|
+
const targetUrl = `${urlBase}/${checkFile}`;
|
|
19
|
+
|
|
20
|
+
console.log(`\n==================================================`);
|
|
21
|
+
console.log(`📡 [Monitor Pages] Acompanhando build no GitHub...`);
|
|
22
|
+
console.log(`🌐 URL: ${urlBase}/`);
|
|
23
|
+
if (checkFile === "manifest.json") {
|
|
24
|
+
console.log(`🎯 Aguardando versão remota: v${versaoAlvo}`);
|
|
25
|
+
}
|
|
26
|
+
console.log(`==================================================\n`);
|
|
27
|
+
|
|
28
|
+
const intervaloMs = 5000;
|
|
29
|
+
const maxTentativas = 48;
|
|
30
|
+
let tentativas = 0;
|
|
31
|
+
const inicio = Date.now();
|
|
32
|
+
|
|
33
|
+
const timer = setInterval(async () => {
|
|
34
|
+
tentativas++;
|
|
35
|
+
const decorridoSegundos = Math.round((Date.now() - inicio) / 1000);
|
|
36
|
+
|
|
37
|
+
try {
|
|
38
|
+
const url = `${targetUrl}?_nocache=${Date.now()}`;
|
|
39
|
+
const res = await fetch(url, {
|
|
40
|
+
headers: {
|
|
41
|
+
"Cache-Control": "no-cache, no-store, must-revalidate",
|
|
42
|
+
"Pragma": "no-cache"
|
|
43
|
+
}
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
if (res.ok) {
|
|
47
|
+
if (checkFile === "manifest.json") {
|
|
48
|
+
const json = await res.json().catch(() => ({}));
|
|
49
|
+
if (json.version === versaoAlvo) {
|
|
50
|
+
clearInterval(timer);
|
|
51
|
+
try { process.stdout.write("\x07"); } catch (_) {}
|
|
52
|
+
console.log(`\n🎉 [GITHUB PAGES NO AR!] v${versaoAlvo} publicada em ${decorridoSegundos}s!\n`);
|
|
53
|
+
return;
|
|
54
|
+
}
|
|
55
|
+
} else {
|
|
56
|
+
const html = await res.text();
|
|
57
|
+
if (html.includes(versaoAlvo) || tentativas >= 4) {
|
|
58
|
+
clearInterval(timer);
|
|
59
|
+
try { process.stdout.write("\x07"); } catch (_) {}
|
|
60
|
+
console.log(`\n🎉 [GITHUB PAGES NO AR!] Atualização publicada em ${decorridoSegundos}s!\n`);
|
|
61
|
+
return;
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
} catch (_) {}
|
|
66
|
+
|
|
67
|
+
console.log(`⏳ [${decorridoSegundos}s] GitHub Actions compilando...`);
|
|
68
|
+
|
|
69
|
+
if (tentativas >= maxTentativas) {
|
|
70
|
+
clearInterval(timer);
|
|
71
|
+
console.log(`\n⚠️ Tempo limite de monitorização excedido (4 min).`);
|
|
72
|
+
console.log(`Verifique em: https://github.com/${config.githubUser}/${config.repoName}/actions\n`);
|
|
73
|
+
}
|
|
74
|
+
}, intervaloMs);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
async function publicarAtualizacao(rl, mensagemCommit, rootDir, config) {
|
|
78
|
+
const remote = detectarGitRemote(rootDir);
|
|
79
|
+
if (!remote) {
|
|
80
|
+
console.log(`\n⚠️ Nenhum repositório remoto configurado neste projeto ainda!`);
|
|
81
|
+
console.log(`👉 Digite /github no terminal para configurar ou criar o repositório agora.\n`);
|
|
82
|
+
return;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
// Garante que user.name e user.email globais estejam preenchidos antes de tentar commitar
|
|
86
|
+
await garantirIdentidadeGit(rl, rootDir);
|
|
87
|
+
|
|
88
|
+
const agora = new Date();
|
|
89
|
+
const pad = (n) => String(n).padStart(2, "0");
|
|
90
|
+
const timestampVersao = `${agora.getFullYear()}.${agora.getMonth() + 1}.${agora.getDate()}-${pad(agora.getHours())}${pad(agora.getMinutes())}`;
|
|
91
|
+
|
|
92
|
+
console.log(`\n📦 [Deploy ${config.projectName}] Publicando: "${mensagemCommit}"`);
|
|
93
|
+
console.log(`🏷️ Versão gerada: v${timestampVersao}`);
|
|
94
|
+
|
|
95
|
+
const indexPath = path.join(rootDir, "index.html");
|
|
96
|
+
if (fs.existsSync(indexPath)) {
|
|
97
|
+
let indexHtml = fs.readFileSync(indexPath, "utf-8");
|
|
98
|
+
const atualizado = indexHtml.replace(
|
|
99
|
+
/(src=["']\.\/[^"']+\.js)(\?v=[^"'\s>]+)?(["'])/g,
|
|
100
|
+
`$1?v=${timestampVersao}$3`
|
|
101
|
+
);
|
|
102
|
+
if (atualizado !== indexHtml) {
|
|
103
|
+
fs.writeFileSync(indexPath, atualizado, "utf-8");
|
|
104
|
+
console.log(`✅ Scripts locais do index.html versionados com ?v=${timestampVersao}`);
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
const manifestPath = path.join(rootDir, "manifest.json");
|
|
109
|
+
if (fs.existsSync(manifestPath)) {
|
|
110
|
+
try {
|
|
111
|
+
const manifest = JSON.parse(fs.readFileSync(manifestPath, "utf-8"));
|
|
112
|
+
manifest.version = timestampVersao;
|
|
113
|
+
fs.writeFileSync(manifestPath, JSON.stringify(manifest, null, 2), "utf-8");
|
|
114
|
+
console.log(`✅ manifest.json atualizado para ${timestampVersao}`);
|
|
115
|
+
} catch (_) {}
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
executarHooksOpcionais(rootDir);
|
|
119
|
+
gerarProjetoCompleto(rootDir, config.snapshotExtensions);
|
|
120
|
+
|
|
121
|
+
try {
|
|
122
|
+
const branch = config.branch || "main";
|
|
123
|
+
execSync("git add .", { stdio: "inherit", cwd: rootDir });
|
|
124
|
+
|
|
125
|
+
const commitMsgFormatada = `v${timestampVersao}: ${mensagemCommit}`;
|
|
126
|
+
execSync(`git commit -m "${commitMsgFormatada.replace(/"/g, '\\"')}"`, { stdio: "inherit", cwd: rootDir });
|
|
127
|
+
|
|
128
|
+
console.log(`🚀 Enviando para origin/${branch}...`);
|
|
129
|
+
execSync(`git push -u origin ${branch}`, { stdio: "inherit", cwd: rootDir });
|
|
130
|
+
|
|
131
|
+
console.log(`✅ Push concluído com sucesso!`);
|
|
132
|
+
aguardarDeployGitHubPages(timestampVersao, config);
|
|
133
|
+
} catch (err) {
|
|
134
|
+
console.error(`\n❌ Falha durante o deploy do Git:`, err.message);
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
async function iniciarCLI(rootDir, configInicial) {
|
|
139
|
+
let configAtual = configInicial;
|
|
140
|
+
let bloqueadoPorAssistente = false;
|
|
141
|
+
|
|
142
|
+
const rl = readline.createInterface({
|
|
143
|
+
input: process.stdin,
|
|
144
|
+
output: process.stdout
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
// Verifica logo ao subir o servidor se o Git global já tem nome e e-mail salvos
|
|
148
|
+
bloqueadoPorAssistente = true;
|
|
149
|
+
try {
|
|
150
|
+
await garantirIdentidadeGit(rl, rootDir);
|
|
151
|
+
} finally {
|
|
152
|
+
bloqueadoPorAssistente = false;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
rl.on("line", async (linha) => {
|
|
156
|
+
if (bloqueadoPorAssistente) return;
|
|
157
|
+
|
|
158
|
+
const msg = linha.trim();
|
|
159
|
+
if (!msg) return;
|
|
160
|
+
|
|
161
|
+
const cmd = msg.toLowerCase();
|
|
162
|
+
|
|
163
|
+
if (cmd === "cls" || cmd === "clear") {
|
|
164
|
+
console.clear();
|
|
165
|
+
return;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
if (cmd === "/github" || cmd === "/setup") {
|
|
169
|
+
bloqueadoPorAssistente = true;
|
|
170
|
+
try {
|
|
171
|
+
configAtual = await executarAssistenteGitHub(rl, rootDir, configAtual);
|
|
172
|
+
} finally {
|
|
173
|
+
bloqueadoPorAssistente = false;
|
|
174
|
+
}
|
|
175
|
+
return;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
if (cmd === "/status") {
|
|
179
|
+
const remote = detectarGitRemote(rootDir);
|
|
180
|
+
console.log(`\n📊 Status do Projeto (${configAtual.projectName}):`);
|
|
181
|
+
console.log(` • Usuário GitHub: ${configAtual.githubUser || "(não configurado)"}`);
|
|
182
|
+
console.log(` • Repositório: ${configAtual.repoName || "(não configurado)"}`);
|
|
183
|
+
console.log(` • Remote Origin: ${remote ? remote.remoteUrl : "(não vinculado — digite /github)"}`);
|
|
184
|
+
console.log(` • Branch: ${configAtual.branch || "main"}\n`);
|
|
185
|
+
return;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
bloqueadoPorAssistente = true;
|
|
189
|
+
try {
|
|
190
|
+
await publicarAtualizacao(rl, msg, rootDir, configAtual);
|
|
191
|
+
} finally {
|
|
192
|
+
bloqueadoPorAssistente = false;
|
|
193
|
+
}
|
|
194
|
+
});
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
module.exports = { iniciarCLI };
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Gerenciador de Identidade Global do Git
|
|
3
|
+
* Responsabilidade: Verificar se user.name e user.email existem no sistema e configurá-los automaticamente,
|
|
4
|
+
* garantindo que o diretório HOME exista no Windows antes de gravar o .gitconfig.
|
|
5
|
+
*/
|
|
6
|
+
const fs = require("fs");
|
|
7
|
+
const os = require("os");
|
|
8
|
+
const path = require("path");
|
|
9
|
+
const { execSync } = require("child_process");
|
|
10
|
+
const { perguntar } = require("./prompts");
|
|
11
|
+
|
|
12
|
+
function prepararAmbienteGit() {
|
|
13
|
+
const homeAtual = process.env.HOME || os.homedir();
|
|
14
|
+
if (homeAtual && !fs.existsSync(homeAtual)) {
|
|
15
|
+
try {
|
|
16
|
+
fs.mkdirSync(homeAtual, { recursive: true });
|
|
17
|
+
} catch (_) {
|
|
18
|
+
// Se o OneDrive bloquear criação da pasta HOME customizada, usa o perfil real do Windows
|
|
19
|
+
process.env.HOME = os.homedir();
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function lerConfigGit(chave, cwd) {
|
|
25
|
+
prepararAmbienteGit();
|
|
26
|
+
try {
|
|
27
|
+
return execSync(`git config --global ${chave}`, {
|
|
28
|
+
cwd,
|
|
29
|
+
stdio: ["ignore", "pipe", "ignore"]
|
|
30
|
+
})
|
|
31
|
+
.toString()
|
|
32
|
+
.trim();
|
|
33
|
+
} catch (_) {
|
|
34
|
+
return "";
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function gravarConfigGit(chave, valor, cwd) {
|
|
39
|
+
prepararAmbienteGit();
|
|
40
|
+
const seguro = valor.replace(/"/g, '\\"');
|
|
41
|
+
try {
|
|
42
|
+
execSync(`git config --global ${chave} "${seguro}"`, { cwd, stdio: "ignore" });
|
|
43
|
+
} catch (_) {
|
|
44
|
+
// Fallback direto para o perfil real do usuário no Windows caso HOME esteja corrompido
|
|
45
|
+
const realHome = os.homedir();
|
|
46
|
+
execSync(`git config --file "${path.join(realHome, ".gitconfig")}" ${chave} "${seguro}"`, {
|
|
47
|
+
cwd,
|
|
48
|
+
stdio: "ignore"
|
|
49
|
+
});
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
async function garantirIdentidadeGit(rl, rootDir) {
|
|
54
|
+
const nomeAtual = lerConfigGit("user.name", rootDir);
|
|
55
|
+
const emailAtual = lerConfigGit("user.email", rootDir);
|
|
56
|
+
|
|
57
|
+
if (nomeAtual && emailAtual) {
|
|
58
|
+
return { nome: nomeAtual, email: emailAtual };
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
console.log(`\n⚠️ Identidade do Git não configurada neste computador.`);
|
|
62
|
+
console.log(` Vamos configurar agora (ficará salvo globalmente para todos os seus projetos):\n`);
|
|
63
|
+
|
|
64
|
+
let nomeFinal = nomeAtual;
|
|
65
|
+
if (!nomeFinal) {
|
|
66
|
+
const respNome = await perguntar(rl, `👤 Seu Nome para commits [Nicolas Arths]: `);
|
|
67
|
+
nomeFinal = respNome || "Nicolas Arths";
|
|
68
|
+
gravarConfigGit("user.name", nomeFinal, rootDir);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
let emailFinal = emailAtual;
|
|
72
|
+
if (!emailFinal) {
|
|
73
|
+
const respEmail = await perguntar(rl, `📧 Seu E-mail do GitHub [nicolasarths@hotmail.com]: `);
|
|
74
|
+
emailFinal = respEmail || "nicolasarths@hotmail.com";
|
|
75
|
+
gravarConfigGit("user.email", emailFinal, rootDir);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
console.log(`✅ Identidade salva globalmente: ${nomeFinal} <${emailFinal}>\n`);
|
|
79
|
+
return { nome: nomeFinal, email: emailFinal };
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
module.exports = {
|
|
83
|
+
lerConfigGit,
|
|
84
|
+
garantirIdentidadeGit
|
|
85
|
+
};
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Operações Locais do Git
|
|
3
|
+
* Responsabilidade: Inicializar repositório local, gerenciar .gitignore, branch e remote origin.
|
|
4
|
+
*/
|
|
5
|
+
const fs = require("fs");
|
|
6
|
+
const path = require("path");
|
|
7
|
+
const { execSync } = require("child_process");
|
|
8
|
+
|
|
9
|
+
function garantirGitIgnore(rootDir) {
|
|
10
|
+
const gitignorePath = path.join(rootDir, ".gitignore");
|
|
11
|
+
const regrasEssenciais = [
|
|
12
|
+
"node_modules/",
|
|
13
|
+
".DS_Store",
|
|
14
|
+
"Thumbs.db",
|
|
15
|
+
".devserver.json"
|
|
16
|
+
];
|
|
17
|
+
|
|
18
|
+
let conteudoAtual = fs.existsSync(gitignorePath)
|
|
19
|
+
? fs.readFileSync(gitignorePath, "utf-8")
|
|
20
|
+
: "";
|
|
21
|
+
|
|
22
|
+
let alterado = false;
|
|
23
|
+
for (const regra of regrasEssenciais) {
|
|
24
|
+
if (!conteudoAtual.includes(regra)) {
|
|
25
|
+
conteudoAtual += (conteudoAtual.endsWith("\n") || conteudoAtual === "" ? "" : "\n") + regra + "\n";
|
|
26
|
+
alterado = true;
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
if (alterado) {
|
|
31
|
+
fs.writeFileSync(gitignorePath, conteudoAtual, "utf-8");
|
|
32
|
+
console.log("✅ .gitignore verificado/atualizado.");
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function garantirGitLocal(rootDir, branch = "main") {
|
|
37
|
+
if (!fs.existsSync(path.join(rootDir, ".git"))) {
|
|
38
|
+
execSync("git init", { cwd: rootDir, stdio: "inherit" });
|
|
39
|
+
console.log("✅ Repositório Git local inicializado.");
|
|
40
|
+
}
|
|
41
|
+
try {
|
|
42
|
+
execSync(`git branch -M ${branch}`, { cwd: rootDir, stdio: "ignore" });
|
|
43
|
+
} catch (_) {}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function configurarOrigin(rootDir, remoteUrl) {
|
|
47
|
+
try {
|
|
48
|
+
execSync("git remote get-url origin", { cwd: rootDir, stdio: "ignore" });
|
|
49
|
+
execSync(`git remote set-url origin "${remoteUrl}"`, { cwd: rootDir, stdio: "inherit" });
|
|
50
|
+
} catch (_) {
|
|
51
|
+
execSync(`git remote add origin "${remoteUrl}"`, { cwd: rootDir, stdio: "inherit" });
|
|
52
|
+
}
|
|
53
|
+
console.log(`✅ Remote 'origin' configurado para: ${remoteUrl}`);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
module.exports = {
|
|
57
|
+
garantirGitIgnore,
|
|
58
|
+
garantirGitLocal,
|
|
59
|
+
configurarOrigin
|
|
60
|
+
};
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Cliente da API REST do GitHub
|
|
3
|
+
* Responsabilidade: Criar repositórios remotos e ativar GitHub Pages via fetch nativo.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
async function criarRepositorioRemotoGitHub({ token, repoName, descricao, privado }) {
|
|
7
|
+
const userRes = await fetch("https://api.github.com/user", {
|
|
8
|
+
headers: {
|
|
9
|
+
"Authorization": `Bearer ${token}`,
|
|
10
|
+
"Accept": "application/vnd.github+json",
|
|
11
|
+
"User-Agent": "Universal-Dev-Server"
|
|
12
|
+
}
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
if (!userRes.ok) {
|
|
16
|
+
const err = await userRes.json().catch(() => ({}));
|
|
17
|
+
throw new Error(`Token inválido ou sem permissão: ${err.message || userRes.status}`);
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
const userData = await userRes.json();
|
|
21
|
+
const githubUser = userData.login;
|
|
22
|
+
|
|
23
|
+
const createRes = await fetch("https://api.github.com/user/repos", {
|
|
24
|
+
method: "POST",
|
|
25
|
+
headers: {
|
|
26
|
+
"Authorization": `Bearer ${token}`,
|
|
27
|
+
"Accept": "application/vnd.github+json",
|
|
28
|
+
"Content-Type": "application/json",
|
|
29
|
+
"User-Agent": "Universal-Dev-Server"
|
|
30
|
+
},
|
|
31
|
+
body: JSON.stringify({
|
|
32
|
+
name: repoName,
|
|
33
|
+
description: descricao || `Projeto ${repoName}`,
|
|
34
|
+
private: privado,
|
|
35
|
+
auto_init: false
|
|
36
|
+
})
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
const repoData = await createRes.json().catch(() => ({}));
|
|
40
|
+
if (!createRes.ok) {
|
|
41
|
+
throw new Error(`Erro ao criar repositório: ${repoData.message || createRes.status}`);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
console.log(`🎉 Repositório criado com sucesso: ${repoData.html_url}`);
|
|
45
|
+
|
|
46
|
+
return {
|
|
47
|
+
githubUser,
|
|
48
|
+
repoName: repoData.name,
|
|
49
|
+
cloneUrl: repoData.clone_url,
|
|
50
|
+
htmlUrl: repoData.html_url
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
async function ativarGitHubPages({ token, githubUser, repoName, branch = "main" }) {
|
|
55
|
+
try {
|
|
56
|
+
const res = await fetch(`https://api.github.com/repos/${githubUser}/${repoName}/pages`, {
|
|
57
|
+
method: "POST",
|
|
58
|
+
headers: {
|
|
59
|
+
"Authorization": `Bearer ${token}`,
|
|
60
|
+
"Accept": "application/vnd.github+json",
|
|
61
|
+
"Content-Type": "application/json",
|
|
62
|
+
"User-Agent": "Universal-Dev-Server"
|
|
63
|
+
},
|
|
64
|
+
body: JSON.stringify({
|
|
65
|
+
source: {
|
|
66
|
+
branch,
|
|
67
|
+
path: "/"
|
|
68
|
+
}
|
|
69
|
+
})
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
if (res.ok || res.status === 201 || res.status === 409) {
|
|
73
|
+
console.log(`🌐 GitHub Pages ativado em: https://${githubUser}.github.io/${repoName}/`);
|
|
74
|
+
} else {
|
|
75
|
+
const err = await res.json().catch(() => ({}));
|
|
76
|
+
console.log(`⚠️ Pages não ativado automaticamente (${err.message || res.status}). Ative nas configurações do repo após o push.`);
|
|
77
|
+
}
|
|
78
|
+
} catch (err) {
|
|
79
|
+
console.log(`⚠️ Erro ao configurar GitHub Pages: ${err.message}`);
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
module.exports = {
|
|
84
|
+
criarRepositorioRemotoGitHub,
|
|
85
|
+
ativarGitHubPages
|
|
86
|
+
};
|
package/github/index.js
ADDED
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Orquestrador do Assistente GitHub (/github)
|
|
3
|
+
* Responsabilidade: Conduzir o menu interativo de criação/vinculação de repositório conectando os submódulos.
|
|
4
|
+
*/
|
|
5
|
+
const path = require("path");
|
|
6
|
+
const { execSync } = require("child_process");
|
|
7
|
+
const { salvarConfig, detectarGitRemote } = require("../config");
|
|
8
|
+
const { perguntar } = require("./prompts");
|
|
9
|
+
const { garantirIdentidadeGit } = require("./git-identity");
|
|
10
|
+
const { garantirGitIgnore, garantirGitLocal, configurarOrigin } = require("./git-local");
|
|
11
|
+
const { criarRepositorioRemotoGitHub, ativarGitHubPages } = require("./github-api");
|
|
12
|
+
|
|
13
|
+
async function executarAssistenteGitHub(rl, rootDir, config) {
|
|
14
|
+
console.log(`\n==================================================`);
|
|
15
|
+
console.log(`🐙 ASSISTENTE DE CONFIGURAÇÃO GITHUB`);
|
|
16
|
+
console.log(`==================================================`);
|
|
17
|
+
|
|
18
|
+
const existente = detectarGitRemote(rootDir);
|
|
19
|
+
if (existente) {
|
|
20
|
+
console.log(`ℹ️ Remote atual detectado: ${existente.remoteUrl}`);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
console.log(`\nEscolha uma opção:`);
|
|
24
|
+
console.log(` [1] Criar um NOVO repositório no GitHub agora`);
|
|
25
|
+
console.log(` [2] Vincular um repositório JÁ EXISTENTE (via URL)`);
|
|
26
|
+
console.log(` [3] Atualizar nome/usuário/branch na configuração local`);
|
|
27
|
+
console.log(` [0] Cancelar\n`);
|
|
28
|
+
|
|
29
|
+
const opcao = await perguntar(rl, "👉 Opção: ");
|
|
30
|
+
|
|
31
|
+
if (opcao === "0" || !opcao) {
|
|
32
|
+
console.log("Operação cancelada.\n");
|
|
33
|
+
return config;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
garantirGitIgnore(rootDir);
|
|
37
|
+
garantirGitLocal(rootDir, config.branch || "main");
|
|
38
|
+
await garantirIdentidadeGit(rl, rootDir);
|
|
39
|
+
|
|
40
|
+
if (opcao === "1") {
|
|
41
|
+
const nomeSugerido = config.repoName || path.basename(rootDir);
|
|
42
|
+
const repoInput = await perguntar(rl, `📦 Nome do repositório [${nomeSugerido}]: `);
|
|
43
|
+
const repoName = repoInput || nomeSugerido;
|
|
44
|
+
|
|
45
|
+
const desc = await perguntar(rl, `📝 Descrição (opcional): `);
|
|
46
|
+
const privInput = await perguntar(rl, `🔒 Repositório privado? (s/N): `);
|
|
47
|
+
const privado = privInput.toLowerCase() === "s";
|
|
48
|
+
|
|
49
|
+
const pagesInput = await perguntar(rl, `🌐 Ativar GitHub Pages automaticamente? (S/n): `);
|
|
50
|
+
const ativarPages = pagesInput.toLowerCase() !== "n";
|
|
51
|
+
|
|
52
|
+
let token = process.env.GITHUB_TOKEN || config.githubToken || "";
|
|
53
|
+
if (!token) {
|
|
54
|
+
token = await perguntar(rl, `🔑 Cole seu GitHub Personal Access Token (scope 'repo'): `);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
if (!token) {
|
|
58
|
+
console.log("❌ Token não informado. Abortando criação.\n");
|
|
59
|
+
return config;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
try {
|
|
63
|
+
console.log(`⏳ Criando repositório '${repoName}' no GitHub...`);
|
|
64
|
+
const criado = await criarRepositorioRemotoGitHub({
|
|
65
|
+
token,
|
|
66
|
+
repoName,
|
|
67
|
+
descricao: desc,
|
|
68
|
+
privado
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
configurarOrigin(rootDir, criado.cloneUrl);
|
|
72
|
+
|
|
73
|
+
const salvarToken = await perguntar(rl, `💾 Salvar token no .devserver.json deste projeto (ignorado pelo git)? (s/N): `);
|
|
74
|
+
|
|
75
|
+
const novaConfig = {
|
|
76
|
+
...config,
|
|
77
|
+
githubUser: criado.githubUser,
|
|
78
|
+
repoName: criado.repoName,
|
|
79
|
+
...(salvarToken.toLowerCase() === "s" ? { githubToken: token } : {})
|
|
80
|
+
};
|
|
81
|
+
salvarConfig(rootDir, novaConfig);
|
|
82
|
+
|
|
83
|
+
const fazerPrimeiroPush = await perguntar(rl, `🚀 Deseja fazer o primeiro commit e push agora? (S/n): `);
|
|
84
|
+
if (fazerPrimeiroPush.toLowerCase() !== "n") {
|
|
85
|
+
execSync("git add .", { cwd: rootDir, stdio: "inherit" });
|
|
86
|
+
execSync(`git commit -m "chore: initial commit"`, { cwd: rootDir, stdio: "inherit" });
|
|
87
|
+
execSync(`git push -u origin ${novaConfig.branch || "main"}`, { cwd: rootDir, stdio: "inherit" });
|
|
88
|
+
console.log(`✅ Primeiro push concluído!`);
|
|
89
|
+
|
|
90
|
+
if (ativarPages) {
|
|
91
|
+
await ativarGitHubPages({
|
|
92
|
+
token,
|
|
93
|
+
githubUser: criado.githubUser,
|
|
94
|
+
repoName: criado.repoName,
|
|
95
|
+
branch: novaConfig.branch || "main"
|
|
96
|
+
});
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
console.log(`\n✨ Tudo pronto! A partir de agora, digite qualquer mensagem no terminal para publicar.\n`);
|
|
101
|
+
return novaConfig;
|
|
102
|
+
} catch (err) {
|
|
103
|
+
console.error(`\n❌ ${err.message}\n`);
|
|
104
|
+
return config;
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
if (opcao === "2") {
|
|
109
|
+
const url = await perguntar(rl, `🔗 Cole a URL do repositório (HTTPS ou SSH): `);
|
|
110
|
+
if (!url) {
|
|
111
|
+
console.log("❌ URL inválida.\n");
|
|
112
|
+
return config;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
configurarOrigin(rootDir, url);
|
|
116
|
+
const info = detectarGitRemote(rootDir);
|
|
117
|
+
|
|
118
|
+
const novaConfig = {
|
|
119
|
+
...config,
|
|
120
|
+
githubUser: info?.githubUser || config.githubUser,
|
|
121
|
+
repoName: info?.repoName || config.repoName
|
|
122
|
+
};
|
|
123
|
+
salvarConfig(rootDir, novaConfig);
|
|
124
|
+
console.log(`✅ Repositório vinculado! User: ${novaConfig.githubUser} | Repo: ${novaConfig.repoName}\n`);
|
|
125
|
+
return novaConfig;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
if (opcao === "3") {
|
|
129
|
+
const user = await perguntar(rl, `👤 Usuário GitHub [${config.githubUser}]: `);
|
|
130
|
+
const repo = await perguntar(rl, `📦 Nome do Repo [${config.repoName}]: `);
|
|
131
|
+
const branch = await perguntar(rl, `🌿 Branch principal [${config.branch || "main"}]: `);
|
|
132
|
+
const port = await perguntar(rl, `🔌 Porta HTTP [${config.port || 8081}]: `);
|
|
133
|
+
|
|
134
|
+
const novaConfig = {
|
|
135
|
+
...config,
|
|
136
|
+
githubUser: user || config.githubUser,
|
|
137
|
+
repoName: repo || config.repoName,
|
|
138
|
+
branch: branch || config.branch || "main",
|
|
139
|
+
port: Number(port) || config.port || 8081
|
|
140
|
+
};
|
|
141
|
+
salvarConfig(rootDir, novaConfig);
|
|
142
|
+
console.log(`✅ Configurações salvas em .devserver.json!\n`);
|
|
143
|
+
return novaConfig;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
return config;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
module.exports = {
|
|
150
|
+
executarAssistenteGitHub,
|
|
151
|
+
garantirIdentidadeGit
|
|
152
|
+
};
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Utilitário de Entrada Interativa no Terminal
|
|
3
|
+
* Responsabilidade: Encapsular readline.question em Promises limpas.
|
|
4
|
+
*/
|
|
5
|
+
function perguntar(rl, texto) {
|
|
6
|
+
return new Promise((resolve) => {
|
|
7
|
+
rl.question(texto, (resposta) => resolve(resposta.trim()));
|
|
8
|
+
});
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
module.exports = { perguntar };
|
package/http.js
ADDED
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Servidor HTTP Estático Genérico com Live Reload e CORS Aberto
|
|
3
|
+
* Responsabilidade: Servir arquivos do projeto, injetar script SSE em HTMLs e remover prefixo de sub-rota do GitHub Pages.
|
|
4
|
+
*/
|
|
5
|
+
const http = require("http");
|
|
6
|
+
const fs = require("fs");
|
|
7
|
+
const path = require("path");
|
|
8
|
+
const { adicionarCliente, removerCliente, SCRIPT_LIVE_RELOAD } = require("./livereload");
|
|
9
|
+
|
|
10
|
+
const mimeTypes = {
|
|
11
|
+
".html": "text/html; charset=utf-8",
|
|
12
|
+
".js": "application/javascript; charset=utf-8",
|
|
13
|
+
".mjs": "application/javascript; charset=utf-8",
|
|
14
|
+
".css": "text/css; charset=utf-8",
|
|
15
|
+
".json": "application/json; charset=utf-8",
|
|
16
|
+
".svg": "image/svg+xml",
|
|
17
|
+
".png": "image/png",
|
|
18
|
+
".jpg": "image/jpeg",
|
|
19
|
+
".jpeg": "image/jpeg",
|
|
20
|
+
".gif": "image/gif",
|
|
21
|
+
".webp": "image/webp",
|
|
22
|
+
".ico": "image/x-icon",
|
|
23
|
+
".woff": "font/woff",
|
|
24
|
+
".woff2": "font/woff2",
|
|
25
|
+
".ttf": "font/ttf"
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
function criarServidorHttp({ rootDir, config }) {
|
|
29
|
+
return http.createServer((req, res) => {
|
|
30
|
+
res.setHeader("Access-Control-Allow-Origin", "*");
|
|
31
|
+
res.setHeader("Access-Control-Allow-Methods", "GET, OPTIONS, POST, PUT, DELETE");
|
|
32
|
+
res.setHeader("Access-Control-Allow-Headers", "*");
|
|
33
|
+
res.setHeader("Access-Control-Allow-Private-Network", "true");
|
|
34
|
+
res.setHeader("Cache-Control", "no-store, no-cache, must-revalidate, proxy-revalidate");
|
|
35
|
+
res.setHeader("Pragma", "no-cache");
|
|
36
|
+
res.setHeader("Expires", "0");
|
|
37
|
+
|
|
38
|
+
if (req.method === "OPTIONS") {
|
|
39
|
+
res.writeHead(204);
|
|
40
|
+
return res.end();
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
const urlPath = req.url.split("?")[0];
|
|
44
|
+
|
|
45
|
+
// Endpoint de Live Reload
|
|
46
|
+
if (urlPath === "/livereload") {
|
|
47
|
+
adicionarCliente(res);
|
|
48
|
+
req.on("close", () => removerCliente(res));
|
|
49
|
+
return;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// Remove dinamicamente o prefixo do repositório (ex: /meu-projeto/index.html -> /index.html)
|
|
53
|
+
const repoPrefix = config.repoName ? `/${config.repoName}` : "";
|
|
54
|
+
let cleanPath = repoPrefix && urlPath.startsWith(repoPrefix)
|
|
55
|
+
? urlPath.slice(repoPrefix.length)
|
|
56
|
+
: urlPath;
|
|
57
|
+
|
|
58
|
+
if (!cleanPath || cleanPath === "/") cleanPath = "/index.html";
|
|
59
|
+
|
|
60
|
+
// Se o projeto tiver manifest.json (ex: extensões), adiciona sufixo (DEV Local)
|
|
61
|
+
if (cleanPath === "/manifest.json") {
|
|
62
|
+
const manifestPath = path.join(rootDir, "manifest.json");
|
|
63
|
+
if (fs.existsSync(manifestPath)) {
|
|
64
|
+
try {
|
|
65
|
+
const manifest = JSON.parse(fs.readFileSync(manifestPath, "utf-8"));
|
|
66
|
+
manifest.name = `${manifest.name || config.projectName} (DEV Local)`;
|
|
67
|
+
res.writeHead(200, { "Content-Type": "application/json; charset=utf-8" });
|
|
68
|
+
return res.end(JSON.stringify(manifest, null, 2));
|
|
69
|
+
} catch (_) {}
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
const filePath = path.join(rootDir, cleanPath);
|
|
74
|
+
|
|
75
|
+
// Previne Path Traversal fora da pasta raiz
|
|
76
|
+
if (!filePath.startsWith(rootDir)) {
|
|
77
|
+
res.writeHead(403, { "Content-Type": "text/plain; charset=utf-8" });
|
|
78
|
+
return res.end("403 Forbidden");
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
if (fs.existsSync(filePath) && fs.statSync(filePath).isFile()) {
|
|
82
|
+
const ext = path.extname(filePath).toLowerCase();
|
|
83
|
+
const contentType = mimeTypes[ext] || "application/octet-stream";
|
|
84
|
+
|
|
85
|
+
if (ext === ".html") {
|
|
86
|
+
let html = fs.readFileSync(filePath, "utf-8");
|
|
87
|
+
if (html.includes("</body>")) {
|
|
88
|
+
html = html.replace("</body>", SCRIPT_LIVE_RELOAD);
|
|
89
|
+
} else {
|
|
90
|
+
html += SCRIPT_LIVE_RELOAD;
|
|
91
|
+
}
|
|
92
|
+
res.writeHead(200, { "Content-Type": contentType });
|
|
93
|
+
return res.end(html);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
res.writeHead(200, { "Content-Type": contentType });
|
|
97
|
+
fs.createReadStream(filePath).pipe(res);
|
|
98
|
+
} else {
|
|
99
|
+
res.writeHead(404, { "Content-Type": "text/plain; charset=utf-8" });
|
|
100
|
+
res.end("404 Not Found");
|
|
101
|
+
}
|
|
102
|
+
});
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
module.exports = { criarServidorHttp };
|
package/index.js
ADDED
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Ponto de Entrada do Universal Dev Server
|
|
5
|
+
* Responsabilidade: Inicializar build, watcher, servidor HTTP com fallback de porta e CLI interativa na pasta atual.
|
|
6
|
+
*/
|
|
7
|
+
const { carregarConfig, detectarGitRemote } = require("./config");
|
|
8
|
+
const { criarServidorHttp } = require("./http");
|
|
9
|
+
const { executarHooksOpcionais, gerarProjetoCompleto } = require("./builder");
|
|
10
|
+
const { iniciarWatcher } = require("./watcher");
|
|
11
|
+
const { iniciarCLI } = require("./deploy");
|
|
12
|
+
|
|
13
|
+
// Sempre aponta para a pasta onde o terminal foi executado (seja no próprio server ou em qualquer projeto)
|
|
14
|
+
const ROOT_DIR = process.cwd();
|
|
15
|
+
const config = carregarConfig(ROOT_DIR);
|
|
16
|
+
|
|
17
|
+
// 1. Executa hooks e gera projeto_completo.txt na inicialização
|
|
18
|
+
executarHooksOpcionais(ROOT_DIR);
|
|
19
|
+
gerarProjetoCompleto(ROOT_DIR, config.snapshotExtensions);
|
|
20
|
+
|
|
21
|
+
// 2. Inicia o monitor de alterações em tempo real
|
|
22
|
+
iniciarWatcher(ROOT_DIR, config);
|
|
23
|
+
|
|
24
|
+
// 3. Sobe o servidor HTTP com incremento automático se a porta estiver ocupada
|
|
25
|
+
const server = criarServidorHttp({ rootDir: ROOT_DIR, config });
|
|
26
|
+
|
|
27
|
+
function iniciarEscuta(portaAtual) {
|
|
28
|
+
server.once("error", (err) => {
|
|
29
|
+
if (err.code === "EADDRINUSE") {
|
|
30
|
+
console.log(`⚠️ Porta ${portaAtual} em uso, tentando ${portaAtual + 1}...`);
|
|
31
|
+
iniciarEscuta(portaAtual + 1);
|
|
32
|
+
} else {
|
|
33
|
+
console.error("❌ Erro no servidor:", err);
|
|
34
|
+
}
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
server.listen(portaAtual, () => {
|
|
38
|
+
const remote = detectarGitRemote(ROOT_DIR);
|
|
39
|
+
|
|
40
|
+
console.log(`\n==================================================`);
|
|
41
|
+
console.log(`⚡ Dev Server [${config.projectName}] rodando em:`);
|
|
42
|
+
console.log(` 👉 http://localhost:${portaAtual}`);
|
|
43
|
+
console.log(`📝 projeto_completo.txt gerado em: ${ROOT_DIR}`);
|
|
44
|
+
console.log(`--------------------------------------------------`);
|
|
45
|
+
|
|
46
|
+
if (remote) {
|
|
47
|
+
console.log(`🐙 GitHub vinculado: ${remote.githubUser}/${remote.repoName}`);
|
|
48
|
+
} else {
|
|
49
|
+
console.log(`⚠️ GitHub NÃO configurado neste projeto.`);
|
|
50
|
+
console.log(` Digite /github para criar ou vincular um repo!`);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
console.log(`--------------------------------------------------`);
|
|
54
|
+
console.log(`💬 COMANDOS DISPONÍVEIS NO TERMINAL:`);
|
|
55
|
+
console.log(` • <mensagem> + ENTER ➔ Versiona, commita e faz push`);
|
|
56
|
+
console.log(` • /github ➔ Cria repo no GitHub ou vincula existente`);
|
|
57
|
+
console.log(` • /status ➔ Exibe configurações do repositório`);
|
|
58
|
+
console.log(` • cls ➔ Limpa o terminal`);
|
|
59
|
+
console.log(`==================================================\n`);
|
|
60
|
+
|
|
61
|
+
iniciarCLI(ROOT_DIR, config);
|
|
62
|
+
});
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
iniciarEscuta(config.port || 8081);
|
package/livereload.js
ADDED
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Gerenciador de Live Reload via Server-Sent Events (SSE)
|
|
3
|
+
* Responsabilidade: Manter conexões abertas com os navegadores e disparar reload ao salvar arquivos.
|
|
4
|
+
*/
|
|
5
|
+
let sseClients = [];
|
|
6
|
+
|
|
7
|
+
function adicionarCliente(res) {
|
|
8
|
+
res.writeHead(200, {
|
|
9
|
+
"Content-Type": "text/event-stream; charset=utf-8",
|
|
10
|
+
"Cache-Control": "no-cache",
|
|
11
|
+
"Connection": "keep-alive"
|
|
12
|
+
});
|
|
13
|
+
sseClients.push(res);
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function removerCliente(res) {
|
|
17
|
+
sseClients = sseClients.filter((c) => c !== res);
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function notificarRecarregamento() {
|
|
21
|
+
sseClients.forEach((client) => {
|
|
22
|
+
try {
|
|
23
|
+
client.write("data: reload\n\n");
|
|
24
|
+
} catch (_) {}
|
|
25
|
+
});
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
const SCRIPT_LIVE_RELOAD = `
|
|
29
|
+
<script>
|
|
30
|
+
(function() {
|
|
31
|
+
const sse = new EventSource('/livereload');
|
|
32
|
+
sse.onmessage = () => {
|
|
33
|
+
console.log('⚡ [Dev Server] Alteração detectada. Recarregando...');
|
|
34
|
+
location.reload();
|
|
35
|
+
};
|
|
36
|
+
})();
|
|
37
|
+
</script>
|
|
38
|
+
</body>`;
|
|
39
|
+
|
|
40
|
+
module.exports = {
|
|
41
|
+
adicionarCliente,
|
|
42
|
+
removerCliente,
|
|
43
|
+
notificarRecarregamento,
|
|
44
|
+
SCRIPT_LIVE_RELOAD
|
|
45
|
+
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,14 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@nicolasarths/devserver",
|
|
3
|
-
"version": "
|
|
4
|
-
"
|
|
5
|
-
"
|
|
3
|
+
"version": "1.0.1",
|
|
4
|
+
"description": "Servidor de desenvolvimento com live-reload e deploy automático para GitHub Pages",
|
|
5
|
+
"main": "index.js",
|
|
6
|
+
"bin": {
|
|
7
|
+
"devserver": "index.js",
|
|
8
|
+
"nic-dev": "index.js"
|
|
9
|
+
},
|
|
10
|
+
"license": "MIT",
|
|
11
|
+
"publishConfig": {
|
|
12
|
+
"access": "public"
|
|
13
|
+
}
|
|
6
14
|
}
|
package/watcher.js
ADDED
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Monitor de Sistema de Arquivos (File Watcher)
|
|
3
|
+
* Responsabilidade: Detectar modificações no código-fonte e disparar hooks, snapshot e Live Reload.
|
|
4
|
+
*/
|
|
5
|
+
const fs = require("fs");
|
|
6
|
+
const { executarHooksOpcionais, gerarProjetoCompleto } = require("./builder");
|
|
7
|
+
const { notificarRecarregamento } = require("./livereload");
|
|
8
|
+
|
|
9
|
+
function iniciarWatcher(rootDir, config) {
|
|
10
|
+
let watchTimeout = null;
|
|
11
|
+
|
|
12
|
+
fs.watch(rootDir, { recursive: true }, (eventType, filename) => {
|
|
13
|
+
if (!filename) return;
|
|
14
|
+
|
|
15
|
+
const norm = filename.replace(/\\/g, "/").toLowerCase();
|
|
16
|
+
|
|
17
|
+
if (
|
|
18
|
+
norm.includes(".git/") ||
|
|
19
|
+
norm.includes("node_modules/") ||
|
|
20
|
+
norm.includes("modulos.json") ||
|
|
21
|
+
norm.includes("projeto_completo") ||
|
|
22
|
+
norm.includes(".devserver.json") ||
|
|
23
|
+
norm.includes("server.config.json")
|
|
24
|
+
) {
|
|
25
|
+
return;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
clearTimeout(watchTimeout);
|
|
29
|
+
watchTimeout = setTimeout(() => {
|
|
30
|
+
executarHooksOpcionais(rootDir);
|
|
31
|
+
gerarProjetoCompleto(rootDir, config.snapshotExtensions);
|
|
32
|
+
notificarRecarregamento();
|
|
33
|
+
}, 250);
|
|
34
|
+
});
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
module.exports = { iniciarWatcher };
|
package/README.md
DELETED