@nicolasarths/devserver 1.0.1 → 1.0.2
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/auto-updater.js +149 -0
- package/builder.js +7 -1
- package/config.js +55 -17
- package/deploy.js +73 -75
- package/github/index.js +36 -7
- package/index.js +6 -24
- package/menu.js +138 -0
- package/package.json +2 -2
- package/watcher.js +24 -8
package/auto-updater.js
ADDED
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Auto-Updater do Pacote @nicolasarths/devserver
|
|
3
|
+
* Responsabilidade: Verificar no registro NPM (com trava de 60s) se há versão mais recente,
|
|
4
|
+
* baixar e atualizar a pasta server/ local antes dos processos pós-save.
|
|
5
|
+
*/
|
|
6
|
+
const fs = require("fs");
|
|
7
|
+
const os = require("os");
|
|
8
|
+
const path = require("path");
|
|
9
|
+
const { execSync } = require("child_process");
|
|
10
|
+
|
|
11
|
+
const PACKAGE_NAME = "@nicolasarths/devserver";
|
|
12
|
+
const REGISTRY_URL = "https://registry.npmjs.org/@nicolasarths%2Fdevserver/latest";
|
|
13
|
+
const COOLDOWN_MS = 60 * 1000;
|
|
14
|
+
|
|
15
|
+
let ultimaVerificacaoTimestamp = 0;
|
|
16
|
+
let atualizacaoEmAndamento = false;
|
|
17
|
+
|
|
18
|
+
function obterVersaoLocal(rootDir) {
|
|
19
|
+
const candidatos = [
|
|
20
|
+
path.join(__dirname, "package.json"),
|
|
21
|
+
path.join(rootDir, "server", "package.json"),
|
|
22
|
+
path.join(rootDir, "package.json")
|
|
23
|
+
];
|
|
24
|
+
|
|
25
|
+
for (const pkgPath of candidatos) {
|
|
26
|
+
if (!fs.existsSync(pkgPath)) continue;
|
|
27
|
+
try {
|
|
28
|
+
const pkg = JSON.parse(fs.readFileSync(pkgPath, "utf-8"));
|
|
29
|
+
if (pkg.name === PACKAGE_NAME && pkg.version) {
|
|
30
|
+
return pkg.version;
|
|
31
|
+
}
|
|
32
|
+
} catch (_) {}
|
|
33
|
+
}
|
|
34
|
+
return null;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function compararSemver(remota, local) {
|
|
38
|
+
const r = String(remota).split(".").map((n) => parseInt(n, 10) || 0);
|
|
39
|
+
const l = String(local).split(".").map((n) => parseInt(n, 10) || 0);
|
|
40
|
+
for (let i = 0; i < 3; i++) {
|
|
41
|
+
if ((r[i] || 0) > (l[i] || 0)) return 1;
|
|
42
|
+
if ((r[i] || 0) < (l[i] || 0)) return -1;
|
|
43
|
+
}
|
|
44
|
+
return 0;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function copiarArquivosAtualizados(origemDir, destinoDir) {
|
|
48
|
+
const itens = fs.readdirSync(origemDir, { withFileTypes: true });
|
|
49
|
+
for (const item of itens) {
|
|
50
|
+
const srcPath = path.join(origemDir, item.name);
|
|
51
|
+
const destPath = path.join(destinoDir, item.name);
|
|
52
|
+
|
|
53
|
+
if (item.isDirectory()) {
|
|
54
|
+
fs.mkdirSync(destPath, { recursive: true });
|
|
55
|
+
copiarArquivosAtualizados(srcPath, destPath);
|
|
56
|
+
} else if (item.isFile()) {
|
|
57
|
+
fs.copyFileSync(srcPath, destPath);
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function limparCacheModulosServer(serverDir) {
|
|
63
|
+
Object.keys(require.cache).forEach((chave) => {
|
|
64
|
+
if (chave.startsWith(serverDir) && !chave.endsWith("auto-updater.js")) {
|
|
65
|
+
delete require.cache[chave];
|
|
66
|
+
}
|
|
67
|
+
});
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
async function baixarEAplicarAtualizacao(tarballUrl, destinoServerDir, novaVersao) {
|
|
71
|
+
const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), "nic-devserver-"));
|
|
72
|
+
const tgzPath = path.join(tempDir, "package.tgz");
|
|
73
|
+
const extractDir = path.join(tempDir, "extracted");
|
|
74
|
+
|
|
75
|
+
try {
|
|
76
|
+
const res = await fetch(tarballUrl);
|
|
77
|
+
if (!res.ok) throw new Error(`HTTP ${res.status} ao baixar tarball`);
|
|
78
|
+
|
|
79
|
+
const buffer = Buffer.from(await res.arrayBuffer());
|
|
80
|
+
fs.writeFileSync(tgzPath, buffer);
|
|
81
|
+
fs.mkdirSync(extractDir, { recursive: true });
|
|
82
|
+
|
|
83
|
+
execSync(`tar -xzf "${tgzPath}" -C "${extractDir}"`, { stdio: "ignore" });
|
|
84
|
+
|
|
85
|
+
const packageRoot = path.join(extractDir, "package");
|
|
86
|
+
if (!fs.existsSync(packageRoot)) {
|
|
87
|
+
throw new Error("Estrutura do pacote extraído inválida.");
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
fs.mkdirSync(destinoServerDir, { recursive: true });
|
|
91
|
+
copiarArquivosAtualizados(packageRoot, destinoServerDir);
|
|
92
|
+
limparCacheModulosServer(destinoServerDir);
|
|
93
|
+
|
|
94
|
+
console.log(`🔄 [Auto-Update] Pasta server/ atualizada para v${novaVersao} com sucesso!`);
|
|
95
|
+
return true;
|
|
96
|
+
} finally {
|
|
97
|
+
try {
|
|
98
|
+
fs.rmSync(tempDir, { recursive: true, force: true });
|
|
99
|
+
} catch (_) {}
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
async function verificarEAtualizarServidor(rootDir, config) {
|
|
104
|
+
if (config?.isNpmPackage) return false;
|
|
105
|
+
|
|
106
|
+
const agora = Date.now();
|
|
107
|
+
if (atualizacaoEmAndamento || agora - ultimaVerificacaoTimestamp < COOLDOWN_MS) {
|
|
108
|
+
return false;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
ultimaVerificacaoTimestamp = agora;
|
|
112
|
+
atualizacaoEmAndamento = true;
|
|
113
|
+
|
|
114
|
+
try {
|
|
115
|
+
const versaoLocal = obterVersaoLocal(rootDir);
|
|
116
|
+
if (!versaoLocal) return false;
|
|
117
|
+
|
|
118
|
+
const controller = new AbortController();
|
|
119
|
+
const timeout = setTimeout(() => controller.abort(), 4000);
|
|
120
|
+
|
|
121
|
+
const res = await fetch(REGISTRY_URL, {
|
|
122
|
+
signal: controller.signal,
|
|
123
|
+
headers: { "Cache-Control": "no-cache" }
|
|
124
|
+
});
|
|
125
|
+
clearTimeout(timeout);
|
|
126
|
+
|
|
127
|
+
if (!res.ok) return false;
|
|
128
|
+
|
|
129
|
+
const meta = await res.json();
|
|
130
|
+
const versaoRemota = meta?.version;
|
|
131
|
+
const tarballUrl = meta?.dist?.tarball;
|
|
132
|
+
|
|
133
|
+
if (!versaoRemota || !tarballUrl) return false;
|
|
134
|
+
|
|
135
|
+
if (compararSemver(versaoRemota, versaoLocal) > 0) {
|
|
136
|
+
console.log(`\n⬆️ [Auto-Update] Nova versão encontrada no NPM: v${versaoRemota} (local: v${versaoLocal})`);
|
|
137
|
+
const pastaServerAlvo = path.join(rootDir, "server");
|
|
138
|
+
return await baixarEAplicarAtualizacao(tarballUrl, pastaServerAlvo, versaoRemota);
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
return false;
|
|
142
|
+
} catch (_) {
|
|
143
|
+
return false;
|
|
144
|
+
} finally {
|
|
145
|
+
atualizacaoEmAndamento = false;
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
module.exports = { verificarEAtualizarServidor };
|
package/builder.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Gerador de Snapshot e Hooks de Build
|
|
3
|
-
* Responsabilidade:
|
|
3
|
+
* Responsabilidade: Apagar o snapshot anterior, gerar projeto_completo.txt limpo na raiz do projeto e executar hooks opcionais.
|
|
4
4
|
*/
|
|
5
5
|
const fs = require("fs");
|
|
6
6
|
const path = require("path");
|
|
@@ -51,6 +51,12 @@ function gerarProjetoCompleto(rootDir, extensoesValidas = [".js", ".html", ".css
|
|
|
51
51
|
|
|
52
52
|
try {
|
|
53
53
|
const destino = path.join(rootDir, "projeto_completo.txt");
|
|
54
|
+
|
|
55
|
+
// Apaga fisicamente o snapshot anterior antes de gerar o novo
|
|
56
|
+
if (fs.existsSync(destino)) {
|
|
57
|
+
fs.unlinkSync(destino);
|
|
58
|
+
}
|
|
59
|
+
|
|
54
60
|
let output = "";
|
|
55
61
|
|
|
56
62
|
const ignorarNomes = new Set([
|
package/config.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Gerenciador de Configuração por Projeto
|
|
3
|
-
* Responsabilidade: Detectar
|
|
3
|
+
* Responsabilidade: Detectar automaticamente se o projeto atual é um pacote NPM ou um projeto Web/GitHub
|
|
4
|
+
* e persistir apenas metadados básicos em .devserver.json.
|
|
4
5
|
*/
|
|
5
6
|
const fs = require("fs");
|
|
6
7
|
const path = require("path");
|
|
@@ -29,40 +30,77 @@ function detectarGitRemote(rootDir) {
|
|
|
29
30
|
return null;
|
|
30
31
|
}
|
|
31
32
|
|
|
33
|
+
function detectarModoPacoteNpm(rootDir) {
|
|
34
|
+
const candidatos = [rootDir, path.join(rootDir, "server")];
|
|
35
|
+
for (const dir of candidatos) {
|
|
36
|
+
const pkgPath = path.join(dir, "package.json");
|
|
37
|
+
if (!fs.existsSync(pkgPath)) continue;
|
|
38
|
+
try {
|
|
39
|
+
const pkg = JSON.parse(fs.readFileSync(pkgPath, "utf-8"));
|
|
40
|
+
const isPacotePublico =
|
|
41
|
+
pkg.name === "@nicolasarths/devserver" ||
|
|
42
|
+
Boolean(pkg.bin && pkg.publishConfig?.access === "public");
|
|
43
|
+
|
|
44
|
+
if (isPacotePublico) {
|
|
45
|
+
return {
|
|
46
|
+
isNpmPackage: true,
|
|
47
|
+
npmPackageDir: dir,
|
|
48
|
+
packageName: pkg.name
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
} catch (_) {}
|
|
52
|
+
}
|
|
53
|
+
return {
|
|
54
|
+
isNpmPackage: false,
|
|
55
|
+
npmPackageDir: "",
|
|
56
|
+
packageName: ""
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
|
|
32
60
|
function carregarConfig(rootDir) {
|
|
33
61
|
const configPath = path.join(rootDir, CONFIG_FILENAME);
|
|
34
62
|
const nomePastaRaiz = path.basename(rootDir);
|
|
35
63
|
const gitInfo = detectarGitRemote(rootDir);
|
|
64
|
+
const modoNpm = detectarModoPacoteNpm(rootDir);
|
|
36
65
|
|
|
37
|
-
|
|
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
|
-
|
|
66
|
+
let salvo = {};
|
|
47
67
|
if (fs.existsSync(configPath)) {
|
|
48
68
|
try {
|
|
49
|
-
|
|
50
|
-
|
|
69
|
+
salvo = JSON.parse(fs.readFileSync(configPath, "utf-8"));
|
|
70
|
+
// Remove eventual resquício antigo de autoNpmPublish manual
|
|
71
|
+
delete salvo.autoNpmPublish;
|
|
72
|
+
delete salvo.npmPackageDir;
|
|
51
73
|
} catch (_) {}
|
|
52
74
|
}
|
|
53
75
|
|
|
54
|
-
|
|
55
|
-
|
|
76
|
+
const configFinal = {
|
|
77
|
+
projectName: salvo.projectName || nomePastaRaiz,
|
|
78
|
+
port: salvo.port || 8081,
|
|
79
|
+
githubUser: salvo.githubUser || gitInfo?.githubUser || "",
|
|
80
|
+
repoName: salvo.repoName || gitInfo?.repoName || nomePastaRaiz,
|
|
81
|
+
branch: salvo.branch || "main",
|
|
82
|
+
pagesCheckFile: salvo.pagesCheckFile || (fs.existsSync(path.join(rootDir, "manifest.json")) ? "manifest.json" : "index.html"),
|
|
83
|
+
snapshotExtensions: salvo.snapshotExtensions || [".js", ".html", ".css", ".json", ".ts", ".jsx", ".tsx", ".md"],
|
|
84
|
+
// Propriedades dinâmicas inferidas automaticamente (não dependem de configuração manual)
|
|
85
|
+
isNpmPackage: modoNpm.isNpmPackage,
|
|
86
|
+
npmPackageDir: modoNpm.npmPackageDir,
|
|
87
|
+
packageName: modoNpm.packageName
|
|
88
|
+
};
|
|
89
|
+
|
|
90
|
+
salvarConfig(rootDir, configFinal);
|
|
91
|
+
return configFinal;
|
|
56
92
|
}
|
|
57
93
|
|
|
58
94
|
function salvarConfig(rootDir, novaConfig) {
|
|
59
95
|
const configPath = path.join(rootDir, CONFIG_FILENAME);
|
|
60
|
-
|
|
96
|
+
const { isNpmPackage, npmPackageDir, packageName, ...persistir } = novaConfig;
|
|
97
|
+
fs.writeFileSync(configPath, JSON.stringify(persistir, null, 2), "utf-8");
|
|
61
98
|
}
|
|
62
99
|
|
|
63
100
|
module.exports = {
|
|
64
101
|
CONFIG_FILENAME,
|
|
65
102
|
carregarConfig,
|
|
66
103
|
salvarConfig,
|
|
67
|
-
detectarGitRemote
|
|
104
|
+
detectarGitRemote,
|
|
105
|
+
detectarModoPacoteNpm
|
|
68
106
|
};
|
package/deploy.js
CHANGED
|
@@ -1,17 +1,16 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
3
|
-
* Responsabilidade: Garantir
|
|
2
|
+
* Motor de Deploy, NPM Auto-Publish e Monitoramento do GitHub Pages
|
|
3
|
+
* Responsabilidade: Garantir configuração GitHub automaticamente se ausente, versionar, publicar no Git/NPM e monitorar Pages.
|
|
4
4
|
*/
|
|
5
5
|
const fs = require("fs");
|
|
6
6
|
const path = require("path");
|
|
7
|
-
const readline = require("readline");
|
|
8
7
|
const { execSync } = require("child_process");
|
|
9
8
|
const { executarHooksOpcionais, gerarProjetoCompleto } = require("./builder");
|
|
10
9
|
const { executarAssistenteGitHub, garantirIdentidadeGit } = require("./github");
|
|
11
|
-
const { detectarGitRemote } = require("./config");
|
|
10
|
+
const { detectarGitRemote, detectarModoPacoteNpm } = require("./config");
|
|
12
11
|
|
|
13
12
|
async function aguardarDeployGitHubPages(versaoAlvo, config) {
|
|
14
|
-
if (!config.githubUser || !config.repoName) return;
|
|
13
|
+
if (!config.githubUser || !config.repoName || config.isNpmPackage) return;
|
|
15
14
|
|
|
16
15
|
const checkFile = config.pagesCheckFile || "index.html";
|
|
17
16
|
const urlBase = `https://${config.githubUser}.github.io/${config.repoName}`;
|
|
@@ -74,24 +73,65 @@ async function aguardarDeployGitHubPages(versaoAlvo, config) {
|
|
|
74
73
|
}, intervaloMs);
|
|
75
74
|
}
|
|
76
75
|
|
|
76
|
+
function incrementarVersaoPacoteNpm(pkgDir) {
|
|
77
|
+
const pkgPath = path.join(pkgDir, "package.json");
|
|
78
|
+
if (!fs.existsSync(pkgPath)) return null;
|
|
79
|
+
|
|
80
|
+
try {
|
|
81
|
+
execSync("npm version patch --no-git-tag-version", { cwd: pkgDir, stdio: "ignore" });
|
|
82
|
+
const pkg = JSON.parse(fs.readFileSync(pkgPath, "utf-8"));
|
|
83
|
+
console.log(`📦 [NPM] Versão do pacote incrementada para: ${pkg.version}`);
|
|
84
|
+
return pkg.version;
|
|
85
|
+
} catch (err) {
|
|
86
|
+
console.error(`⚠️ Não foi possível incrementar versão no package.json:`, err.message);
|
|
87
|
+
return null;
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
function publicarNoNpm(pkgDir, versao) {
|
|
92
|
+
try {
|
|
93
|
+
console.log(`🚀 [NPM] Publicando v${versao} no registro do npm...`);
|
|
94
|
+
execSync("npm publish --access public", { cwd: pkgDir, stdio: "inherit" });
|
|
95
|
+
console.log(`🎉 [NPM PUBLICADO!] v${versao} já está disponível via npx!\n`);
|
|
96
|
+
} catch (err) {
|
|
97
|
+
console.error(`❌ Falha ao publicar no npm:`, err.message);
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
77
101
|
async function publicarAtualizacao(rl, mensagemCommit, rootDir, config) {
|
|
78
|
-
|
|
102
|
+
let configAtual = config;
|
|
103
|
+
let remote = detectarGitRemote(rootDir);
|
|
104
|
+
|
|
79
105
|
if (!remote) {
|
|
80
106
|
console.log(`\n⚠️ Nenhum repositório remoto configurado neste projeto ainda!`);
|
|
81
|
-
console.log(
|
|
82
|
-
|
|
107
|
+
console.log(`🔧 Iniciando configuração automática do GitHub antes de publicar "${mensagemCommit}"...\n`);
|
|
108
|
+
configAtual = await executarAssistenteGitHub(rl, rootDir, configAtual);
|
|
109
|
+
remote = detectarGitRemote(rootDir);
|
|
110
|
+
|
|
111
|
+
if (!remote) {
|
|
112
|
+
console.log(`❌ Publicação interrompida porque nenhum repositório foi vinculado.\n`);
|
|
113
|
+
return configAtual;
|
|
114
|
+
}
|
|
83
115
|
}
|
|
84
116
|
|
|
85
|
-
// Garante que user.name e user.email globais estejam preenchidos antes de tentar commitar
|
|
86
117
|
await garantirIdentidadeGit(rl, rootDir);
|
|
87
118
|
|
|
88
119
|
const agora = new Date();
|
|
89
120
|
const pad = (n) => String(n).padStart(2, "0");
|
|
90
121
|
const timestampVersao = `${agora.getFullYear()}.${agora.getMonth() + 1}.${agora.getDate()}-${pad(agora.getHours())}${pad(agora.getMinutes())}`;
|
|
91
122
|
|
|
92
|
-
console.log(`\n📦 [Deploy ${
|
|
123
|
+
console.log(`\n📦 [Deploy ${configAtual.projectName}] Publicando: "${mensagemCommit}"`);
|
|
93
124
|
console.log(`🏷️ Versão gerada: v${timestampVersao}`);
|
|
94
125
|
|
|
126
|
+
const modoNpm = configAtual.isNpmPackage
|
|
127
|
+
? { isNpmPackage: true, npmPackageDir: configAtual.npmPackageDir }
|
|
128
|
+
: detectarModoPacoteNpm(rootDir);
|
|
129
|
+
|
|
130
|
+
let versaoNpm = null;
|
|
131
|
+
if (modoNpm.isNpmPackage && modoNpm.npmPackageDir) {
|
|
132
|
+
versaoNpm = incrementarVersaoPacoteNpm(modoNpm.npmPackageDir);
|
|
133
|
+
}
|
|
134
|
+
|
|
95
135
|
const indexPath = path.join(rootDir, "index.html");
|
|
96
136
|
if (fs.existsSync(indexPath)) {
|
|
97
137
|
let indexHtml = fs.readFileSync(indexPath, "utf-8");
|
|
@@ -116,82 +156,40 @@ async function publicarAtualizacao(rl, mensagemCommit, rootDir, config) {
|
|
|
116
156
|
}
|
|
117
157
|
|
|
118
158
|
executarHooksOpcionais(rootDir);
|
|
119
|
-
gerarProjetoCompleto(rootDir,
|
|
159
|
+
gerarProjetoCompleto(rootDir, configAtual.snapshotExtensions);
|
|
120
160
|
|
|
121
161
|
try {
|
|
122
|
-
const branch =
|
|
162
|
+
const branch = configAtual.branch || "main";
|
|
123
163
|
execSync("git add .", { stdio: "inherit", cwd: rootDir });
|
|
124
164
|
|
|
125
|
-
const
|
|
165
|
+
const statusPorcelain = execSync("git status --porcelain", { cwd: rootDir }).toString().trim();
|
|
166
|
+
if (!statusPorcelain) {
|
|
167
|
+
console.log(`ℹ️ Nenhuma alteração nova pendente após o setup inicial.`);
|
|
168
|
+
if (modoNpm.isNpmPackage && versaoNpm) {
|
|
169
|
+
publicarNoNpm(modoNpm.npmPackageDir, versaoNpm);
|
|
170
|
+
}
|
|
171
|
+
return configAtual;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
const tagMsg = versaoNpm ? `v${versaoNpm}` : `v${timestampVersao}`;
|
|
175
|
+
const commitMsgFormatada = `${tagMsg}: ${mensagemCommit}`;
|
|
126
176
|
execSync(`git commit -m "${commitMsgFormatada.replace(/"/g, '\\"')}"`, { stdio: "inherit", cwd: rootDir });
|
|
127
177
|
|
|
128
178
|
console.log(`🚀 Enviando para origin/${branch}...`);
|
|
129
179
|
execSync(`git push -u origin ${branch}`, { stdio: "inherit", cwd: rootDir });
|
|
130
180
|
|
|
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;
|
|
181
|
+
console.log(`✅ Push no GitHub concluído com sucesso!`);
|
|
160
182
|
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
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;
|
|
183
|
+
if (modoNpm.isNpmPackage && versaoNpm) {
|
|
184
|
+
publicarNoNpm(modoNpm.npmPackageDir, versaoNpm);
|
|
185
|
+
} else {
|
|
186
|
+
aguardarDeployGitHubPages(timestampVersao, configAtual);
|
|
186
187
|
}
|
|
188
|
+
} catch (err) {
|
|
189
|
+
console.error(`\n❌ Falha durante o deploy:`, err.message);
|
|
190
|
+
}
|
|
187
191
|
|
|
188
|
-
|
|
189
|
-
try {
|
|
190
|
-
await publicarAtualizacao(rl, msg, rootDir, configAtual);
|
|
191
|
-
} finally {
|
|
192
|
-
bloqueadoPorAssistente = false;
|
|
193
|
-
}
|
|
194
|
-
});
|
|
192
|
+
return configAtual;
|
|
195
193
|
}
|
|
196
194
|
|
|
197
|
-
module.exports = {
|
|
195
|
+
module.exports = { publicarAtualizacao };
|
package/github/index.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Orquestrador do Assistente GitHub
|
|
3
|
-
* Responsabilidade: Conduzir o menu interativo de criação/vinculação de repositório
|
|
2
|
+
* Orquestrador do Assistente GitHub
|
|
3
|
+
* Responsabilidade: Conduzir o menu interativo de criação/vinculação de repositório e persistir o token globalmente no Windows.
|
|
4
4
|
*/
|
|
5
5
|
const path = require("path");
|
|
6
6
|
const { execSync } = require("child_process");
|
|
@@ -10,6 +10,31 @@ const { garantirIdentidadeGit } = require("./git-identity");
|
|
|
10
10
|
const { garantirGitIgnore, garantirGitLocal, configurarOrigin } = require("./git-local");
|
|
11
11
|
const { criarRepositorioRemotoGitHub, ativarGitHubPages } = require("./github-api");
|
|
12
12
|
|
|
13
|
+
function lerTokenGlobalWindows() {
|
|
14
|
+
if (process.env.GITHUB_TOKEN) return process.env.GITHUB_TOKEN;
|
|
15
|
+
try {
|
|
16
|
+
const out = execSync('reg query "HKCU\\Environment" /v GITHUB_TOKEN', {
|
|
17
|
+
stdio: ["ignore", "pipe", "ignore"]
|
|
18
|
+
}).toString();
|
|
19
|
+
const match = out.match(/GITHUB_TOKEN\s+REG_SZ\s+(\S+)/);
|
|
20
|
+
if (match) {
|
|
21
|
+
process.env.GITHUB_TOKEN = match[1];
|
|
22
|
+
return match[1];
|
|
23
|
+
}
|
|
24
|
+
} catch (_) {}
|
|
25
|
+
return "";
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function salvarTokenGlobalWindows(token) {
|
|
29
|
+
process.env.GITHUB_TOKEN = token;
|
|
30
|
+
try {
|
|
31
|
+
execSync(`setx GITHUB_TOKEN "${token}"`, { stdio: "ignore" });
|
|
32
|
+
console.log("✅ Token salvo globalmente no Windows (nunca mais será pedido em nenhum projeto!).");
|
|
33
|
+
} catch (err) {
|
|
34
|
+
console.log(`⚠️ Não foi possível gravar no registro do Windows: ${err.message}`);
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
13
38
|
async function executarAssistenteGitHub(rl, rootDir, config) {
|
|
14
39
|
console.log(`\n==================================================`);
|
|
15
40
|
console.log(`🐙 ASSISTENTE DE CONFIGURAÇÃO GITHUB`);
|
|
@@ -49,9 +74,12 @@ async function executarAssistenteGitHub(rl, rootDir, config) {
|
|
|
49
74
|
const pagesInput = await perguntar(rl, `🌐 Ativar GitHub Pages automaticamente? (S/n): `);
|
|
50
75
|
const ativarPages = pagesInput.toLowerCase() !== "n";
|
|
51
76
|
|
|
52
|
-
let token =
|
|
77
|
+
let token = lerTokenGlobalWindows() || config.githubToken || "";
|
|
78
|
+
let tokenNovo = false;
|
|
79
|
+
|
|
53
80
|
if (!token) {
|
|
54
81
|
token = await perguntar(rl, `🔑 Cole seu GitHub Personal Access Token (scope 'repo'): `);
|
|
82
|
+
tokenNovo = Boolean(token);
|
|
55
83
|
}
|
|
56
84
|
|
|
57
85
|
if (!token) {
|
|
@@ -68,15 +96,16 @@ async function executarAssistenteGitHub(rl, rootDir, config) {
|
|
|
68
96
|
privado
|
|
69
97
|
});
|
|
70
98
|
|
|
71
|
-
|
|
99
|
+
if (tokenNovo) {
|
|
100
|
+
salvarTokenGlobalWindows(token);
|
|
101
|
+
}
|
|
72
102
|
|
|
73
|
-
|
|
103
|
+
configurarOrigin(rootDir, criado.cloneUrl);
|
|
74
104
|
|
|
75
105
|
const novaConfig = {
|
|
76
106
|
...config,
|
|
77
107
|
githubUser: criado.githubUser,
|
|
78
|
-
repoName: criado.repoName
|
|
79
|
-
...(salvarToken.toLowerCase() === "s" ? { githubToken: token } : {})
|
|
108
|
+
repoName: criado.repoName
|
|
80
109
|
};
|
|
81
110
|
salvarConfig(rootDir, novaConfig);
|
|
82
111
|
|
package/index.js
CHANGED
|
@@ -2,15 +2,14 @@
|
|
|
2
2
|
|
|
3
3
|
/**
|
|
4
4
|
* Ponto de Entrada do Universal Dev Server
|
|
5
|
-
* Responsabilidade: Inicializar build, watcher, servidor HTTP com fallback de porta e
|
|
5
|
+
* Responsabilidade: Inicializar build, watcher, servidor HTTP com fallback de porta e menu interativo na pasta atual.
|
|
6
6
|
*/
|
|
7
|
-
const { carregarConfig
|
|
7
|
+
const { carregarConfig } = require("./config");
|
|
8
8
|
const { criarServidorHttp } = require("./http");
|
|
9
9
|
const { executarHooksOpcionais, gerarProjetoCompleto } = require("./builder");
|
|
10
10
|
const { iniciarWatcher } = require("./watcher");
|
|
11
|
-
const {
|
|
11
|
+
const { iniciarMenuInterativo } = require("./menu");
|
|
12
12
|
|
|
13
|
-
// Sempre aponta para a pasta onde o terminal foi executado (seja no próprio server ou em qualquer projeto)
|
|
14
13
|
const ROOT_DIR = process.cwd();
|
|
15
14
|
const config = carregarConfig(ROOT_DIR);
|
|
16
15
|
|
|
@@ -35,30 +34,13 @@ function iniciarEscuta(portaAtual) {
|
|
|
35
34
|
});
|
|
36
35
|
|
|
37
36
|
server.listen(portaAtual, () => {
|
|
38
|
-
const remote = detectarGitRemote(ROOT_DIR);
|
|
39
|
-
|
|
40
37
|
console.log(`\n==================================================`);
|
|
41
38
|
console.log(`⚡ Dev Server [${config.projectName}] rodando em:`);
|
|
42
39
|
console.log(` 👉 http://localhost:${portaAtual}`);
|
|
43
|
-
console.log(`📝 projeto_completo.txt
|
|
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`);
|
|
40
|
+
console.log(`📝 projeto_completo.txt sincronizado.`);
|
|
41
|
+
console.log(`==================================================`);
|
|
60
42
|
|
|
61
|
-
|
|
43
|
+
iniciarMenuInterativo(ROOT_DIR, config);
|
|
62
44
|
});
|
|
63
45
|
}
|
|
64
46
|
|
package/menu.js
ADDED
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Menu Interativo do Terminal
|
|
3
|
+
* Responsabilidade: Exibir o modo detectado automaticamente (Pacote NPM vs Projeto Web/GitHub) e rotear opções do usuário.
|
|
4
|
+
*/
|
|
5
|
+
const readline = require("readline");
|
|
6
|
+
const { salvarConfig, detectarGitRemote } = require("./config");
|
|
7
|
+
const { executarAssistenteGitHub, garantirIdentidadeGit } = require("./github");
|
|
8
|
+
const { perguntar } = require("./github/prompts");
|
|
9
|
+
const { publicarAtualizacao } = require("./deploy");
|
|
10
|
+
|
|
11
|
+
const MENSAGEM_PADRAO = "Update sem descrição";
|
|
12
|
+
|
|
13
|
+
function exibirMenu(rootDir, config) {
|
|
14
|
+
const remote = detectarGitRemote(rootDir);
|
|
15
|
+
const statusGit = remote
|
|
16
|
+
? `${remote.githubUser}/${remote.repoName} (branch: ${config.branch || "main"})`
|
|
17
|
+
: "Não vinculado (escolha [2] para configurar)";
|
|
18
|
+
|
|
19
|
+
const modoDetectado = config.isNpmPackage
|
|
20
|
+
? `📦 Pacote NPM (${config.packageName}) ➔ GitHub + Auto-Publish NPM`
|
|
21
|
+
: `🌐 Projeto Web/GitHub ➔ GitHub + Monitor GitHub Pages`;
|
|
22
|
+
|
|
23
|
+
console.log(`\n--------------------------------------------------`);
|
|
24
|
+
console.log(`🔍 Modo: ${modoDetectado}`);
|
|
25
|
+
console.log(`🐙 GitHub: ${statusGit}`);
|
|
26
|
+
console.log(`--------------------------------------------------`);
|
|
27
|
+
console.log(`📋 O QUE VOCÊ DESEJA FAZER?`);
|
|
28
|
+
console.log(` [1] 🚀 Publicar alteração (pedir mensagem de commit)`);
|
|
29
|
+
console.log(` [2] 🐙 Configurar / Criar repositório no GitHub`);
|
|
30
|
+
console.log(` [3] ⚙️ Configurar porta, branch ou projeto`);
|
|
31
|
+
console.log(` [4] 📊 Ver status detalhado`);
|
|
32
|
+
console.log(` [5] 🧹 Limpar tela`);
|
|
33
|
+
console.log(` [0] ❌ Sair e encerrar servidor`);
|
|
34
|
+
console.log(` 💡 Atalho: digite a mensagem e dê ENTER (ou só ENTER para "${MENSAGEM_PADRAO}")`);
|
|
35
|
+
console.log(`--------------------------------------------------`);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
async function configurarOpcoesProjeto(rl, rootDir, config) {
|
|
39
|
+
console.log(`\n⚙️ CONFIGURAÇÕES DO PROJETO (${config.projectName})`);
|
|
40
|
+
const porta = await perguntar(rl, `🔌 Porta HTTP [${config.port || 8081}]: `);
|
|
41
|
+
const branch = await perguntar(rl, `🌿 Branch principal [${config.branch || "main"}]: `);
|
|
42
|
+
const user = await perguntar(rl, `👤 Usuário GitHub [${config.githubUser}]: `);
|
|
43
|
+
const repo = await perguntar(rl, `📦 Nome do repositório [${config.repoName}]: `);
|
|
44
|
+
|
|
45
|
+
const novaConfig = {
|
|
46
|
+
...config,
|
|
47
|
+
port: Number(porta) || config.port || 8081,
|
|
48
|
+
branch: branch || config.branch || "main",
|
|
49
|
+
githubUser: user || config.githubUser,
|
|
50
|
+
repoName: repo || config.repoName
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
salvarConfig(rootDir, novaConfig);
|
|
54
|
+
console.log(`✅ Configurações salvas em .devserver.json! (Se mudou a porta, reinicie o servidor).`);
|
|
55
|
+
return novaConfig;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
async function iniciarMenuInterativo(rootDir, configInicial) {
|
|
59
|
+
let configAtual = configInicial;
|
|
60
|
+
let ocupado = false;
|
|
61
|
+
|
|
62
|
+
const rl = readline.createInterface({
|
|
63
|
+
input: process.stdin,
|
|
64
|
+
output: process.stdout
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
ocupado = true;
|
|
68
|
+
try {
|
|
69
|
+
await garantirIdentidadeGit(rl, rootDir);
|
|
70
|
+
} finally {
|
|
71
|
+
ocupado = false;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
exibirMenu(rootDir, configAtual);
|
|
75
|
+
|
|
76
|
+
rl.on("line", async (linha) => {
|
|
77
|
+
if (ocupado) return;
|
|
78
|
+
|
|
79
|
+
const entrada = linha.trim();
|
|
80
|
+
|
|
81
|
+
ocupado = true;
|
|
82
|
+
try {
|
|
83
|
+
if (entrada === "0") {
|
|
84
|
+
console.log("\n👋 Encerrando servidor...");
|
|
85
|
+
process.exit(0);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
if (entrada === "1") {
|
|
89
|
+
const msg = await perguntar(rl, `\n📝 Digite a mensagem do commit [${MENSAGEM_PADRAO}]: `);
|
|
90
|
+
configAtual = await publicarAtualizacao(rl, msg || MENSAGEM_PADRAO, rootDir, configAtual);
|
|
91
|
+
exibirMenu(rootDir, configAtual);
|
|
92
|
+
return;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
if (entrada === "2") {
|
|
96
|
+
configAtual = await executarAssistenteGitHub(rl, rootDir, configAtual);
|
|
97
|
+
exibirMenu(rootDir, configAtual);
|
|
98
|
+
return;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
if (entrada === "3") {
|
|
102
|
+
configAtual = await configurarOpcoesProjeto(rl, rootDir, configAtual);
|
|
103
|
+
exibirMenu(rootDir, configAtual);
|
|
104
|
+
return;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
if (entrada === "4") {
|
|
108
|
+
const remote = detectarGitRemote(rootDir);
|
|
109
|
+
console.log(`\n📊 STATUS DETALHADO:`);
|
|
110
|
+
console.log(` • Pasta Raiz: ${rootDir}`);
|
|
111
|
+
console.log(` • Projeto: ${configAtual.projectName}`);
|
|
112
|
+
console.log(` • Tipo Detectado: ${configAtual.isNpmPackage ? `Pacote NPM (${configAtual.packageName})` : "Projeto Web / GitHub"}`);
|
|
113
|
+
console.log(` • Porta Config: ${configAtual.port}`);
|
|
114
|
+
console.log(` • Usuário GitHub: ${configAtual.githubUser || "(não definido)"}`);
|
|
115
|
+
console.log(` • Repositório: ${configAtual.repoName || "(não definido)"}`);
|
|
116
|
+
console.log(` • Remote Origin: ${remote ? remote.remoteUrl : "(não vinculado)"}`);
|
|
117
|
+
console.log(` • Branch: ${configAtual.branch || "main"}`);
|
|
118
|
+
exibirMenu(rootDir, configAtual);
|
|
119
|
+
return;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
if (entrada === "5") {
|
|
123
|
+
console.clear();
|
|
124
|
+
exibirMenu(rootDir, configAtual);
|
|
125
|
+
return;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
// Se apertar apenas ENTER (vazio) ou digitar texto livre, publica direto!
|
|
129
|
+
const mensagemFinal = entrada || MENSAGEM_PADRAO;
|
|
130
|
+
configAtual = await publicarAtualizacao(rl, mensagemFinal, rootDir, configAtual);
|
|
131
|
+
exibirMenu(rootDir, configAtual);
|
|
132
|
+
} finally {
|
|
133
|
+
ocupado = false;
|
|
134
|
+
}
|
|
135
|
+
});
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
module.exports = { iniciarMenuInterativo };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@nicolasarths/devserver",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.2",
|
|
4
4
|
"description": "Servidor de desenvolvimento com live-reload e deploy automático para GitHub Pages",
|
|
5
5
|
"main": "index.js",
|
|
6
6
|
"bin": {
|
|
@@ -11,4 +11,4 @@
|
|
|
11
11
|
"publishConfig": {
|
|
12
12
|
"access": "public"
|
|
13
13
|
}
|
|
14
|
-
}
|
|
14
|
+
}
|
package/watcher.js
CHANGED
|
@@ -1,16 +1,16 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Monitor de Sistema de Arquivos (File Watcher)
|
|
3
|
-
* Responsabilidade: Detectar modificações no código-fonte e disparar
|
|
3
|
+
* Responsabilidade: Detectar modificações no código-fonte, checar auto-update do servidor e disparar build + Live Reload.
|
|
4
4
|
*/
|
|
5
5
|
const fs = require("fs");
|
|
6
|
-
const {
|
|
7
|
-
const { notificarRecarregamento } = require("./livereload");
|
|
6
|
+
const { verificarEAtualizarServidor } = require("./auto-updater");
|
|
8
7
|
|
|
9
8
|
function iniciarWatcher(rootDir, config) {
|
|
10
9
|
let watchTimeout = null;
|
|
10
|
+
let ignorarEventosInternos = false;
|
|
11
11
|
|
|
12
12
|
fs.watch(rootDir, { recursive: true }, (eventType, filename) => {
|
|
13
|
-
if (!filename) return;
|
|
13
|
+
if (!filename || ignorarEventosInternos) return;
|
|
14
14
|
|
|
15
15
|
const norm = filename.replace(/\\/g, "/").toLowerCase();
|
|
16
16
|
|
|
@@ -26,10 +26,26 @@ function iniciarWatcher(rootDir, config) {
|
|
|
26
26
|
}
|
|
27
27
|
|
|
28
28
|
clearTimeout(watchTimeout);
|
|
29
|
-
watchTimeout = setTimeout(() => {
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
29
|
+
watchTimeout = setTimeout(async () => {
|
|
30
|
+
ignorarEventosInternos = true;
|
|
31
|
+
try {
|
|
32
|
+
// 1. Verifica e atualiza a pasta server/ se houver versão nova no NPM (com trava de 60s)
|
|
33
|
+
await verificarEAtualizarServidor(rootDir, config);
|
|
34
|
+
|
|
35
|
+
// 2. Carrega dinamicamente builder e livereload (já pegando a versão nova se houve update)
|
|
36
|
+
const { executarHooksOpcionais, gerarProjetoCompleto } = require("./builder");
|
|
37
|
+
const { notificarRecarregamento } = require("./livereload");
|
|
38
|
+
|
|
39
|
+
// 3. Executa os processos pós-save normalmente
|
|
40
|
+
executarHooksOpcionais(rootDir);
|
|
41
|
+
gerarProjetoCompleto(rootDir, config.snapshotExtensions);
|
|
42
|
+
notificarRecarregamento();
|
|
43
|
+
} finally {
|
|
44
|
+
// Pequena janela para absorver eventos residualmente disparados pela cópia dos arquivos
|
|
45
|
+
setTimeout(() => {
|
|
46
|
+
ignorarEventosInternos = false;
|
|
47
|
+
}, 300);
|
|
48
|
+
}
|
|
33
49
|
}, 250);
|
|
34
50
|
});
|
|
35
51
|
}
|