project-foundry 1.0.2 → 1.0.4
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/bin/foundry.js +23 -1
- package/package.json +2 -1
- package/src/local-printer-executor.js +100 -3
- package/src/service-manager.js +67 -7
- package/src/tui.js +75 -16
- package/src/tunnel-client.js +12 -0
package/bin/foundry.js
CHANGED
|
@@ -17,7 +17,7 @@ program
|
|
|
17
17
|
.description('Project Foundry CLI & Edge Node Manager')
|
|
18
18
|
.version('1.0.0');
|
|
19
19
|
|
|
20
|
-
// Comando: daemon
|
|
20
|
+
// Comando: daemon
|
|
21
21
|
program
|
|
22
22
|
.command('daemon')
|
|
23
23
|
.description('Inicia o servidor e o túnel reverso em segundo plano')
|
|
@@ -25,6 +25,28 @@ program
|
|
|
25
25
|
startDaemon();
|
|
26
26
|
});
|
|
27
27
|
|
|
28
|
+
// Comando: start (inicia o daemon diretamente exibindo logs em tempo real)
|
|
29
|
+
program
|
|
30
|
+
.command('start')
|
|
31
|
+
.description('Inicia o servidor local e o túnel com logs visíveis no terminal')
|
|
32
|
+
.action(() => {
|
|
33
|
+
console.log(pc.bold(pc.cyan('Iniciando Project Foundry Local Server...')));
|
|
34
|
+
startDaemon();
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
// Comando: service-install
|
|
38
|
+
program
|
|
39
|
+
.command('service-install')
|
|
40
|
+
.description('Instala ou atualiza o serviço de autostart no sistema operacional')
|
|
41
|
+
.action(() => {
|
|
42
|
+
const res = installAutostartService(process.argv[1]);
|
|
43
|
+
if (res.success) {
|
|
44
|
+
console.log(pc.green(`Serviço registrado com sucesso (${res.type}): ${res.name}`));
|
|
45
|
+
} else {
|
|
46
|
+
console.error(pc.red(`Falha ao registrar serviço: ${res.error}`));
|
|
47
|
+
}
|
|
48
|
+
});
|
|
49
|
+
|
|
28
50
|
// Comando: setup
|
|
29
51
|
program
|
|
30
52
|
.command('setup')
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "project-foundry",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.4",
|
|
4
4
|
"description": "Project Foundry CLI & Local Node Daemon Manager",
|
|
5
5
|
"bin": {
|
|
6
6
|
"project-foundry": "bin/foundry.js",
|
|
@@ -18,6 +18,7 @@
|
|
|
18
18
|
"dependencies": {
|
|
19
19
|
"@clack/prompts": "^0.9.1",
|
|
20
20
|
"commander": "^12.1.0",
|
|
21
|
+
"mqtt": "^5.16.1",
|
|
21
22
|
"open": "^10.1.0",
|
|
22
23
|
"picocolors": "^1.1.1",
|
|
23
24
|
"ws": "^8.22.0"
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import mqtt from 'mqtt';
|
|
2
|
+
|
|
1
3
|
export async function executeLocalPrinterAction({ action, printerId, connectionConfig, payload }) {
|
|
2
4
|
if (!connectionConfig) {
|
|
3
5
|
throw new Error('Configuração de conexão da impressora não foi fornecida pelo servidor');
|
|
@@ -8,14 +10,27 @@ export async function executeLocalPrinterAction({ action, printerId, connectionC
|
|
|
8
10
|
const apiKey = connectionConfig.apiKey;
|
|
9
11
|
const headers = apiKey ? { 'X-Api-Key': apiKey } : {};
|
|
10
12
|
|
|
13
|
+
// =====================================================
|
|
11
14
|
// 1. Verificação de Conexão
|
|
15
|
+
// =====================================================
|
|
12
16
|
if (action === 'connect') {
|
|
17
|
+
// KLIPPER / MOONRAKER
|
|
13
18
|
if (type === 'klipper') {
|
|
14
|
-
|
|
19
|
+
let infoRes;
|
|
20
|
+
try {
|
|
21
|
+
infoRes = await fetch(`${baseUrl}/server/info`, {
|
|
22
|
+
headers,
|
|
23
|
+
signal: AbortSignal.timeout(8000),
|
|
24
|
+
});
|
|
25
|
+
} catch (err) {
|
|
26
|
+
throw new Error(`Não foi possível conectar ao Klipper em ${baseUrl} (${err.code || err.message}). Verifique se a impressora está ligada e o IP/porta estão corretos.`);
|
|
27
|
+
}
|
|
28
|
+
|
|
15
29
|
if (!infoRes.ok) {
|
|
16
|
-
throw new Error(`
|
|
30
|
+
throw new Error(`Klipper em ${baseUrl} respondeu com erro HTTP ${infoRes.status}`);
|
|
17
31
|
}
|
|
18
|
-
|
|
32
|
+
|
|
33
|
+
const info = await infoRes.json().catch(() => ({}));
|
|
19
34
|
const klippyState = info?.result?.klippy_state || 'ready';
|
|
20
35
|
|
|
21
36
|
let status = klippyState === 'ready' ? 'IDLE' : 'ERROR';
|
|
@@ -54,6 +69,82 @@ export async function executeLocalPrinterAction({ action, printerId, connectionC
|
|
|
54
69
|
};
|
|
55
70
|
}
|
|
56
71
|
|
|
72
|
+
// BAMBU LAB (LAN MQTT)
|
|
73
|
+
if (type === 'bambu') {
|
|
74
|
+
const host = connectionConfig.host;
|
|
75
|
+
const port = connectionConfig.mqttPort || 8883;
|
|
76
|
+
const username = connectionConfig.username || 'bblp';
|
|
77
|
+
const password = connectionConfig.accessCode;
|
|
78
|
+
const serialNumber = connectionConfig.serialNumber;
|
|
79
|
+
|
|
80
|
+
if (!host || !password || !serialNumber) {
|
|
81
|
+
throw new Error('Configuração da Bambu incompleta: host, accessCode e serialNumber são obrigatórios');
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
return new Promise((resolve, reject) => {
|
|
85
|
+
let isDone = false;
|
|
86
|
+
const timer = setTimeout(() => {
|
|
87
|
+
if (isDone) return;
|
|
88
|
+
isDone = true;
|
|
89
|
+
try { client.end(true); } catch {}
|
|
90
|
+
reject(new Error(`Tempo limite esgotado tentando conectar ao MQTT da Bambu em ${host}:${port}`));
|
|
91
|
+
}, 9000);
|
|
92
|
+
|
|
93
|
+
const client = mqtt.connect(`mqtts://${host}:${port}`, {
|
|
94
|
+
clientId: `foundry_edge_${Date.now()}`,
|
|
95
|
+
username,
|
|
96
|
+
password,
|
|
97
|
+
rejectUnauthorized: false,
|
|
98
|
+
connectTimeout: 8000,
|
|
99
|
+
clean: true,
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
client.on('connect', () => {
|
|
103
|
+
client.subscribe(`device/${serialNumber}/report`, () => {
|
|
104
|
+
client.publish(
|
|
105
|
+
`device/${serialNumber}/request`,
|
|
106
|
+
JSON.stringify({ pushing: { sequence_id: '1', command: 'pushall', version: 1, push_target: 1 } })
|
|
107
|
+
);
|
|
108
|
+
});
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
client.on('message', (_topic, payloadBuffer) => {
|
|
112
|
+
if (isDone) return;
|
|
113
|
+
try {
|
|
114
|
+
const data = JSON.parse(payloadBuffer.toString('utf8'));
|
|
115
|
+
if (data.print) {
|
|
116
|
+
isDone = true;
|
|
117
|
+
clearTimeout(timer);
|
|
118
|
+
client.end(true);
|
|
119
|
+
const state = String(data.print.gcode_state || 'IDLE').toUpperCase();
|
|
120
|
+
const percent = Number(data.print.mc_percent || 0);
|
|
121
|
+
resolve({
|
|
122
|
+
online: true,
|
|
123
|
+
status: state === 'RUNNING' || state === 'PREPARE' ? 'PRINTING' : (state === 'PAUSE' ? 'PAUSED' : 'IDLE'),
|
|
124
|
+
message: `Bambu LAN conectada (${state})`,
|
|
125
|
+
progress: percent,
|
|
126
|
+
temperatures: {
|
|
127
|
+
hotend: data.print.nozzle_temper,
|
|
128
|
+
hotendTarget: data.print.nozzle_target_temper,
|
|
129
|
+
bed: data.print.bed_temper,
|
|
130
|
+
bedTarget: data.print.bed_target_temper,
|
|
131
|
+
},
|
|
132
|
+
});
|
|
133
|
+
}
|
|
134
|
+
} catch {}
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
client.on('error', (err) => {
|
|
138
|
+
if (isDone) return;
|
|
139
|
+
isDone = true;
|
|
140
|
+
clearTimeout(timer);
|
|
141
|
+
try { client.end(true); } catch {}
|
|
142
|
+
reject(new Error(`Falha de conexão com a Bambu em ${host}: ${err.message}`));
|
|
143
|
+
});
|
|
144
|
+
});
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
// OCTOPRINT
|
|
57
148
|
if (type === 'octoprint') {
|
|
58
149
|
const verRes = await fetch(`${baseUrl}/api/version`, { headers, signal: AbortSignal.timeout(8000) });
|
|
59
150
|
if (!verRes.ok) {
|
|
@@ -69,7 +160,9 @@ export async function executeLocalPrinterAction({ action, printerId, connectionC
|
|
|
69
160
|
throw new Error(`Protocolo de impressora "${type}" não suportado para conexão direta`);
|
|
70
161
|
}
|
|
71
162
|
|
|
163
|
+
// =====================================================
|
|
72
164
|
// 2. Snapshot de Câmera Local
|
|
165
|
+
// =====================================================
|
|
73
166
|
if (action === 'camera/snapshot') {
|
|
74
167
|
const camUrl = connectionConfig.cameraSnapshotUrl || connectionConfig.cameraUrl || `${baseUrl}/webcam/?action=snapshot`;
|
|
75
168
|
const camRes = await fetch(camUrl, { signal: AbortSignal.timeout(10000) });
|
|
@@ -80,7 +173,9 @@ export async function executeLocalPrinterAction({ action, printerId, connectionC
|
|
|
80
173
|
return { base64: Buffer.from(buf).toString('base64') };
|
|
81
174
|
}
|
|
82
175
|
|
|
176
|
+
// =====================================================
|
|
83
177
|
// 3. Controle da Impressora (Pausa, Emergência, Motores, etc.)
|
|
178
|
+
// =====================================================
|
|
84
179
|
if (action === 'control') {
|
|
85
180
|
const cmd = payload?.command;
|
|
86
181
|
if (type === 'klipper') {
|
|
@@ -99,7 +194,9 @@ export async function executeLocalPrinterAction({ action, printerId, connectionC
|
|
|
99
194
|
}
|
|
100
195
|
}
|
|
101
196
|
|
|
197
|
+
// =====================================================
|
|
102
198
|
// 4. Comandos de Trabalho (Job Pause / Resume / Cancel)
|
|
199
|
+
// =====================================================
|
|
103
200
|
if (action.startsWith('job/')) {
|
|
104
201
|
const jobCmd = action.replace('job/', '');
|
|
105
202
|
if (type === 'klipper') {
|
package/src/service-manager.js
CHANGED
|
@@ -2,6 +2,7 @@ import { execSync, spawn } from 'node:child_process';
|
|
|
2
2
|
import fs from 'node:fs';
|
|
3
3
|
import path from 'node:path';
|
|
4
4
|
import os from 'node:os';
|
|
5
|
+
import { getConfigDir } from './config.js';
|
|
5
6
|
|
|
6
7
|
export async function isServerHealthy(port = 3010) {
|
|
7
8
|
try {
|
|
@@ -15,13 +16,71 @@ export async function isServerHealthy(port = 3010) {
|
|
|
15
16
|
}
|
|
16
17
|
}
|
|
17
18
|
|
|
19
|
+
export function stopRunningDaemon() {
|
|
20
|
+
const isWin = process.platform === 'win32';
|
|
21
|
+
if (isWin) {
|
|
22
|
+
try {
|
|
23
|
+
execSync('schtasks /end /tn "ProjectFoundry"', { stdio: 'ignore' });
|
|
24
|
+
} catch {}
|
|
25
|
+
} else {
|
|
26
|
+
try {
|
|
27
|
+
execSync('systemctl stop foundry.service', { stdio: 'ignore' });
|
|
28
|
+
} catch {}
|
|
29
|
+
try {
|
|
30
|
+
execSync('systemctl --user stop foundry.service', { stdio: 'ignore' });
|
|
31
|
+
} catch {}
|
|
32
|
+
try {
|
|
33
|
+
execSync("pkill -f 'foundry.*daemon' || true", { stdio: 'ignore' });
|
|
34
|
+
} catch {}
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export function restartAutostartService() {
|
|
39
|
+
const isWin = process.platform === 'win32';
|
|
40
|
+
if (isWin) {
|
|
41
|
+
try {
|
|
42
|
+
execSync('schtasks /run /tn "ProjectFoundry"', { stdio: 'ignore' });
|
|
43
|
+
} catch {}
|
|
44
|
+
} else {
|
|
45
|
+
try {
|
|
46
|
+
execSync('systemctl restart foundry.service', { stdio: 'ignore' });
|
|
47
|
+
} catch {
|
|
48
|
+
try {
|
|
49
|
+
execSync('systemctl --user restart foundry.service', { stdio: 'ignore' });
|
|
50
|
+
} catch {}
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export function ensurePermanentAppPath(entryPath) {
|
|
56
|
+
const rootDir = path.resolve(path.dirname(entryPath), '..');
|
|
57
|
+
|
|
58
|
+
// Se estiver rodando de cache temporário do npx ou temp
|
|
59
|
+
if (entryPath.includes('_npx') || entryPath.includes('npm-cache') || entryPath.includes('Temp') || entryPath.includes('tmp')) {
|
|
60
|
+
const permanentAppDir = path.join(getConfigDir(), 'app');
|
|
61
|
+
if (!fs.existsSync(permanentAppDir)) {
|
|
62
|
+
fs.mkdirSync(permanentAppDir, { recursive: true });
|
|
63
|
+
}
|
|
64
|
+
try {
|
|
65
|
+
// Copia arquivos do pacote para local permanente
|
|
66
|
+
fs.cpSync(rootDir, permanentAppDir, { recursive: true });
|
|
67
|
+
const permanentEntry = path.join(permanentAppDir, 'bin', 'foundry.js');
|
|
68
|
+
if (fs.existsSync(permanentEntry)) {
|
|
69
|
+
return permanentEntry;
|
|
70
|
+
}
|
|
71
|
+
} catch {}
|
|
72
|
+
}
|
|
73
|
+
return entryPath;
|
|
74
|
+
}
|
|
75
|
+
|
|
18
76
|
export function installAutostartService(serverEntryPath) {
|
|
19
77
|
const isWin = process.platform === 'win32';
|
|
78
|
+
const permanentPath = ensurePermanentAppPath(serverEntryPath);
|
|
20
79
|
|
|
21
80
|
if (isWin) {
|
|
22
81
|
try {
|
|
23
82
|
// Cria tarefa no Agendador do Windows com início automático em modo daemon
|
|
24
|
-
const cmd = `schtasks /create /tn "ProjectFoundry" /tr "node \\"${
|
|
83
|
+
const cmd = `schtasks /create /tn "ProjectFoundry" /tr "node \\"${permanentPath}\\" daemon" /sc onstart /f`;
|
|
25
84
|
execSync(cmd, { stdio: 'pipe' });
|
|
26
85
|
return { success: true, type: 'windows-task', name: 'ProjectFoundry' };
|
|
27
86
|
} catch (err) {
|
|
@@ -32,13 +91,14 @@ export function installAutostartService(serverEntryPath) {
|
|
|
32
91
|
// Linux (systemd)
|
|
33
92
|
try {
|
|
34
93
|
const isRoot = typeof process.getuid === 'function' && process.getuid() === 0;
|
|
94
|
+
const nodeBin = process.execPath || '/usr/bin/node';
|
|
35
95
|
const serviceContent = `[Unit]
|
|
36
|
-
Description=Project Foundry Local Server
|
|
96
|
+
Description=Project Foundry Local Farm Server
|
|
37
97
|
After=network.target
|
|
38
98
|
|
|
39
99
|
[Service]
|
|
40
100
|
Type=simple
|
|
41
|
-
ExecStart
|
|
101
|
+
ExecStart=${nodeBin} ${permanentPath} daemon
|
|
42
102
|
Restart=always
|
|
43
103
|
RestartSec=5
|
|
44
104
|
Environment=NODE_ENV=production
|
|
@@ -49,13 +109,13 @@ WantedBy=default.target
|
|
|
49
109
|
|
|
50
110
|
if (isRoot) {
|
|
51
111
|
fs.writeFileSync('/etc/systemd/system/foundry.service', serviceContent, 'utf8');
|
|
52
|
-
execSync('systemctl daemon-reload && systemctl enable foundry.service', { stdio: 'pipe' });
|
|
112
|
+
execSync('systemctl daemon-reload && systemctl enable foundry.service && systemctl restart foundry.service', { stdio: 'pipe' });
|
|
53
113
|
return { success: true, type: 'systemd-system', name: 'foundry.service' };
|
|
54
114
|
} else {
|
|
55
115
|
const userSystemdDir = path.join(os.homedir(), '.config', 'systemd', 'user');
|
|
56
116
|
fs.mkdirSync(userSystemdDir, { recursive: true });
|
|
57
117
|
fs.writeFileSync(path.join(userSystemdDir, 'foundry.service'), serviceContent, 'utf8');
|
|
58
|
-
execSync('systemctl --user daemon-reload && systemctl --user enable foundry.service', { stdio: 'pipe' });
|
|
118
|
+
execSync('systemctl --user daemon-reload && systemctl --user enable foundry.service && systemctl --user restart foundry.service', { stdio: 'pipe' });
|
|
59
119
|
return { success: true, type: 'systemd-user', name: 'foundry.service' };
|
|
60
120
|
}
|
|
61
121
|
} catch (err) {
|
|
@@ -64,8 +124,8 @@ WantedBy=default.target
|
|
|
64
124
|
}
|
|
65
125
|
|
|
66
126
|
export function startDaemonBackground(serverEntryPath) {
|
|
67
|
-
|
|
68
|
-
const child = spawn(process.execPath, [
|
|
127
|
+
const permanentPath = ensurePermanentAppPath(serverEntryPath);
|
|
128
|
+
const child = spawn(process.execPath, [permanentPath, 'daemon'], {
|
|
69
129
|
detached: true,
|
|
70
130
|
stdio: 'ignore',
|
|
71
131
|
});
|
package/src/tui.js
CHANGED
|
@@ -1,21 +1,43 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import { fileURLToPath } from 'node:url';
|
|
4
|
+
import { execSync } from 'node:child_process';
|
|
1
5
|
import * as p from '@clack/prompts';
|
|
2
6
|
import pc from 'picocolors';
|
|
3
7
|
import open from 'open';
|
|
4
8
|
import { readConfig, updateStoragePath } from './config.js';
|
|
5
|
-
import { isServerHealthy } from './service-manager.js';
|
|
9
|
+
import { isServerHealthy, stopRunningDaemon, restartAutostartService } from './service-manager.js';
|
|
10
|
+
import { edgeTunnelClient } from './tunnel-client.js';
|
|
11
|
+
|
|
12
|
+
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
13
|
+
|
|
14
|
+
function getPackageVersion() {
|
|
15
|
+
try {
|
|
16
|
+
const pkg = JSON.parse(fs.readFileSync(path.join(__dirname, '..', 'package.json'), 'utf8'));
|
|
17
|
+
return `v${pkg.version}`;
|
|
18
|
+
} catch {
|
|
19
|
+
return 'v1.0.3';
|
|
20
|
+
}
|
|
21
|
+
}
|
|
6
22
|
|
|
7
23
|
export async function runInteractiveTui() {
|
|
8
24
|
const config = readConfig();
|
|
9
|
-
const version =
|
|
25
|
+
const version = getPackageVersion();
|
|
10
26
|
const localPort = config?.localPort || 3010;
|
|
11
27
|
const localUrl = `http://localhost:${localPort}`;
|
|
12
28
|
|
|
29
|
+
// Garante que o túnel de comandos com a Nuvem esteja ativo
|
|
30
|
+
if (config?.apiKey && config?.serverUrl) {
|
|
31
|
+
edgeTunnelClient.start();
|
|
32
|
+
}
|
|
33
|
+
|
|
13
34
|
console.clear();
|
|
14
35
|
|
|
15
36
|
p.intro(
|
|
16
37
|
`${pc.bold(pc.cyan('======================================================'))}\n` +
|
|
17
38
|
` ${pc.bold(pc.yellow(`Project Foundry Interface (${version})`))}\n` +
|
|
18
39
|
` 🚀 Server: ${pc.green(localUrl)}\n` +
|
|
40
|
+
` 🌐 Nuvem: ${edgeTunnelClient.isConnected() ? pc.green('Conectado (Túnel Ativo)') : pc.cyan('Túnel Iniciado')}\n` +
|
|
19
41
|
` 💾 Storage: ${pc.cyan(config?.storagePath || 'Padrão')}\n` +
|
|
20
42
|
`${pc.bold(pc.cyan('======================================================'))}`
|
|
21
43
|
);
|
|
@@ -33,7 +55,7 @@ export async function runInteractiveTui() {
|
|
|
33
55
|
} else {
|
|
34
56
|
p.note(
|
|
35
57
|
`${pc.yellow('○')} O servidor local não respondeu em ${localUrl}.\n` +
|
|
36
|
-
`
|
|
58
|
+
`O serviço pode estar iniciando em segundo plano ou parado.`,
|
|
37
59
|
'Atenção'
|
|
38
60
|
);
|
|
39
61
|
}
|
|
@@ -43,16 +65,16 @@ export async function runInteractiveTui() {
|
|
|
43
65
|
message: 'Escolha uma opção:',
|
|
44
66
|
options: [
|
|
45
67
|
{ value: 'web', label: 'Web UI (Open in Browser)', hint: 'Abre o painel no navegador' },
|
|
46
|
-
{ value: 'status', label: 'Terminal UI (Status & Health)', hint: 'Verifica conexões locais' },
|
|
68
|
+
{ value: 'status', label: 'Terminal UI (Status & Health)', hint: 'Verifica saúde e conexões locais' },
|
|
47
69
|
{ value: 'storage', label: 'Storage (HD Externo)', hint: 'Visualizar ou alterar pasta de arquivos' },
|
|
48
|
-
{ value: 'update', label: `Update (current: ${version})`, hint: 'Verifica
|
|
49
|
-
{ value: 'hide', label: 'Hide to Tray (Background)', hint: '
|
|
50
|
-
{ value: '
|
|
70
|
+
{ value: 'update', label: `Update (current: ${version})`, hint: 'Verifica e atualiza para versão mais recente' },
|
|
71
|
+
{ value: 'hide', label: 'Hide to Tray (Background)', hint: 'Fecha o console e mantém o servidor rodando' },
|
|
72
|
+
{ value: 'stop', label: 'Stop Server', hint: 'Desliga o servidor local da farm' },
|
|
51
73
|
],
|
|
52
74
|
});
|
|
53
75
|
|
|
54
76
|
if (p.isCancel(choice) || choice === 'hide') {
|
|
55
|
-
p.outro(pc.green('
|
|
77
|
+
p.outro(pc.green('Painel fechado. O servidor da farm continua rodando em segundo plano.'));
|
|
56
78
|
process.exit(0);
|
|
57
79
|
}
|
|
58
80
|
|
|
@@ -100,19 +122,56 @@ export async function runInteractiveTui() {
|
|
|
100
122
|
}
|
|
101
123
|
|
|
102
124
|
if (choice === 'update') {
|
|
103
|
-
|
|
125
|
+
const s = p.spinner();
|
|
126
|
+
s.start('Verificando versão mais recente no npm...');
|
|
127
|
+
try {
|
|
128
|
+
const res = await fetch('https://registry.npmjs.org/project-foundry/latest', {
|
|
129
|
+
signal: AbortSignal.timeout(6000),
|
|
130
|
+
});
|
|
131
|
+
const data = await res.json();
|
|
132
|
+
const latestVersion = `v${data.version}`;
|
|
133
|
+
s.stop();
|
|
134
|
+
|
|
135
|
+
if (latestVersion !== version) {
|
|
136
|
+
p.log.warn(`Nova versão disponível: ${pc.green(latestVersion)} (você está na ${version})`);
|
|
137
|
+
const doUpdate = await p.confirm({
|
|
138
|
+
message: `Deseja atualizar para a versão ${latestVersion}?`,
|
|
139
|
+
initialValue: true,
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
if (doUpdate && !p.isCancel(doUpdate)) {
|
|
143
|
+
s.start('Atualizando pacote e reiniciando serviço...');
|
|
144
|
+
try {
|
|
145
|
+
execSync('npm install -g project-foundry@latest', { stdio: 'pipe' });
|
|
146
|
+
restartAutostartService();
|
|
147
|
+
s.stop(pc.green(`Atualizado para ${latestVersion} com sucesso!`));
|
|
148
|
+
p.outro(pc.cyan('Reinicie o menu com "foundry" ou "npx project-foundry" para aplicar.'));
|
|
149
|
+
process.exit(0);
|
|
150
|
+
} catch (err) {
|
|
151
|
+
s.stop(pc.yellow(`Atualização global não permitida sem sudo. Execute no terminal: npm install -g project-foundry@latest`));
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
} else {
|
|
155
|
+
p.log.success(`Você já está na versão mais recente (${version}).`);
|
|
156
|
+
}
|
|
157
|
+
} catch (err) {
|
|
158
|
+
s.stop(pc.red(`Erro ao verificar: ${err.message}`));
|
|
159
|
+
}
|
|
104
160
|
}
|
|
105
161
|
|
|
106
|
-
if (choice === '
|
|
162
|
+
if (choice === 'stop') {
|
|
107
163
|
const shouldStop = await p.confirm({
|
|
108
|
-
message: '
|
|
109
|
-
active: '
|
|
110
|
-
inactive: '
|
|
111
|
-
initialValue:
|
|
164
|
+
message: 'Atenção: deseja realmente parar o servidor local da farm?',
|
|
165
|
+
active: 'Sim, parar servidor completamente',
|
|
166
|
+
inactive: 'Não, manter ligado',
|
|
167
|
+
initialValue: false,
|
|
112
168
|
});
|
|
113
169
|
|
|
114
|
-
|
|
115
|
-
|
|
170
|
+
if (shouldStop && !p.isCancel(shouldStop)) {
|
|
171
|
+
stopRunningDaemon();
|
|
172
|
+
p.outro(pc.yellow('Servidor local encerrado. Para religar, execute "foundry" no terminal.'));
|
|
173
|
+
process.exit(0);
|
|
174
|
+
}
|
|
116
175
|
}
|
|
117
176
|
}
|
|
118
177
|
}
|
package/src/tunnel-client.js
CHANGED
|
@@ -8,6 +8,11 @@ export class EdgeTunnelClient {
|
|
|
8
8
|
this.pingTimer = null;
|
|
9
9
|
this.reconnectTimer = null;
|
|
10
10
|
this.isStopping = false;
|
|
11
|
+
this.status = 'DISCONNECTED';
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
isConnected() {
|
|
15
|
+
return Boolean(this.ws && this.ws.readyState === WebSocket.OPEN);
|
|
11
16
|
}
|
|
12
17
|
|
|
13
18
|
start() {
|
|
@@ -16,12 +21,15 @@ export class EdgeTunnelClient {
|
|
|
16
21
|
return;
|
|
17
22
|
}
|
|
18
23
|
|
|
24
|
+
if (this.isConnected()) return;
|
|
25
|
+
|
|
19
26
|
this.isStopping = false;
|
|
20
27
|
this.connect();
|
|
21
28
|
}
|
|
22
29
|
|
|
23
30
|
stop() {
|
|
24
31
|
this.isStopping = true;
|
|
32
|
+
this.status = 'DISCONNECTED';
|
|
25
33
|
if (this.pingTimer) clearInterval(this.pingTimer);
|
|
26
34
|
if (this.reconnectTimer) clearTimeout(this.reconnectTimer);
|
|
27
35
|
if (this.ws) {
|
|
@@ -50,6 +58,7 @@ export class EdgeTunnelClient {
|
|
|
50
58
|
});
|
|
51
59
|
|
|
52
60
|
this.ws.on('open', () => {
|
|
61
|
+
this.status = 'CONNECTED';
|
|
53
62
|
// Envia PING a cada 25 segundos
|
|
54
63
|
if (this.pingTimer) clearInterval(this.pingTimer);
|
|
55
64
|
this.pingTimer = setInterval(() => {
|
|
@@ -69,15 +78,18 @@ export class EdgeTunnelClient {
|
|
|
69
78
|
});
|
|
70
79
|
|
|
71
80
|
this.ws.on('close', () => {
|
|
81
|
+
this.status = 'DISCONNECTED';
|
|
72
82
|
if (this.pingTimer) clearInterval(this.pingTimer);
|
|
73
83
|
this.scheduleReconnect();
|
|
74
84
|
});
|
|
75
85
|
|
|
76
86
|
this.ws.on('error', () => {
|
|
87
|
+
this.status = 'DISCONNECTED';
|
|
77
88
|
if (this.pingTimer) clearInterval(this.pingTimer);
|
|
78
89
|
this.scheduleReconnect();
|
|
79
90
|
});
|
|
80
91
|
} catch {
|
|
92
|
+
this.status = 'DISCONNECTED';
|
|
81
93
|
this.scheduleReconnect();
|
|
82
94
|
}
|
|
83
95
|
}
|