@nicolasarths/devserver 1.0.0 → 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.
@@ -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: Gerar projeto_completo.txt na raiz do projeto e executar hooks opcionais (ex: modulos.json).
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 metadados do projeto via process.cwd() e persistir ajustes em .devserver.json na raiz do projeto.
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
- 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
-
66
+ let salvo = {};
47
67
  if (fs.existsSync(configPath)) {
48
68
  try {
49
- const salvo = JSON.parse(fs.readFileSync(configPath, "utf-8"));
50
- return { ...padrao, ...salvo };
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
- salvarConfig(rootDir, padrao);
55
- return padrao;
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
- fs.writeFileSync(configPath, JSON.stringify(novaConfig, null, 2), "utf-8");
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
- * 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.
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
- const remote = detectarGitRemote(rootDir);
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(`👉 Digite /github no terminal para configurar ou criar o repositório agora.\n`);
82
- return;
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 ${config.projectName}] Publicando: "${mensagemCommit}"`);
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, config.snapshotExtensions);
159
+ gerarProjetoCompleto(rootDir, configAtual.snapshotExtensions);
120
160
 
121
161
  try {
122
- const branch = config.branch || "main";
162
+ const branch = configAtual.branch || "main";
123
163
  execSync("git add .", { stdio: "inherit", cwd: rootDir });
124
164
 
125
- const commitMsgFormatada = `v${timestampVersao}: ${mensagemCommit}`;
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
- 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;
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
- bloqueadoPorAssistente = true;
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 = { iniciarCLI };
195
+ module.exports = { publicarAtualizacao };
package/github/index.js CHANGED
@@ -1,6 +1,6 @@
1
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.
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 = process.env.GITHUB_TOKEN || config.githubToken || "";
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
- configurarOrigin(rootDir, criado.cloneUrl);
99
+ if (tokenNovo) {
100
+ salvarTokenGlobalWindows(token);
101
+ }
72
102
 
73
- const salvarToken = await perguntar(rl, `💾 Salvar token no .devserver.json deste projeto (ignorado pelo git)? (s/N): `);
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 CLI interativa na pasta atual.
5
+ * Responsabilidade: Inicializar build, watcher, servidor HTTP com fallback de porta e menu interativo na pasta atual.
6
6
  */
7
- const { carregarConfig, detectarGitRemote } = require("./config");
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 { iniciarCLI } = require("./deploy");
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 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`);
40
+ console.log(`📝 projeto_completo.txt sincronizado.`);
41
+ console.log(`==================================================`);
60
42
 
61
- iniciarCLI(ROOT_DIR, config);
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,13 +1,14 @@
1
1
  {
2
2
  "name": "@nicolasarths/devserver",
3
- "version": "1.0.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": {
7
- "nic-dev": "./index.js"
7
+ "devserver": "index.js",
8
+ "nic-dev": "index.js"
8
9
  },
9
10
  "license": "MIT",
10
11
  "publishConfig": {
11
12
  "access": "public"
12
13
  }
13
- }
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 hooks, snapshot e Live Reload.
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 { executarHooksOpcionais, gerarProjetoCompleto } = require("./builder");
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
- executarHooksOpcionais(rootDir);
31
- gerarProjetoCompleto(rootDir, config.snapshotExtensions);
32
- notificarRecarregamento();
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
  }