project-foundry 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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "project-foundry",
3
- "version": "1.0.1",
3
+ "version": "1.0.2",
4
4
  "description": "Project Foundry CLI & Local Node Daemon Manager",
5
5
  "bin": {
6
6
  "project-foundry": "bin/foundry.js",
package/src/daemon.js CHANGED
@@ -1,4 +1,5 @@
1
1
  import http from 'node:http';
2
+ import https from 'node:https';
2
3
  import fs from 'node:fs';
3
4
  import path from 'node:path';
4
5
  import { readConfig } from './config.js';
@@ -75,10 +76,14 @@ export function startDaemon() {
75
76
 
76
77
  fileStream.on('finish', async () => {
77
78
  try {
78
- // Encaminha requisição completa com cabeçalhos para a nuvem
79
79
  const cloudUrl = `${config.serverUrl.replace(/\/$/, '')}${url.pathname}`;
80
- const cloudReq = http.request(cloudUrl, {
80
+ const isHttps = cloudUrl.startsWith('https:');
81
+ const transport = isHttps ? https : http;
82
+
83
+ const cloudReq = transport.request(cloudUrl, {
81
84
  method: 'POST',
85
+ family: 4,
86
+ rejectUnauthorized: false,
82
87
  headers: {
83
88
  ...req.headers,
84
89
  host: new URL(config.serverUrl).host,
@@ -111,14 +116,16 @@ export function startDaemon() {
111
116
  return;
112
117
  }
113
118
 
114
- // Proxy reverso padrão para outras rotas em direção à Nuvem
119
+ // Proxy reverso para todas as outras requisições
115
120
  try {
116
121
  const targetUrl = new URL(req.url, config.serverUrl);
117
122
  const isHttps = targetUrl.protocol === 'https:';
118
- const transport = isHttps ? await import('node:https') : await import('node:http');
123
+ const transport = isHttps ? https : http;
119
124
 
120
125
  const proxyReq = transport.request(targetUrl, {
121
126
  method: req.method,
127
+ family: 4,
128
+ rejectUnauthorized: false,
122
129
  headers: {
123
130
  ...req.headers,
124
131
  host: targetUrl.host,
@@ -0,0 +1,120 @@
1
+ export async function executeLocalPrinterAction({ action, printerId, connectionConfig, payload }) {
2
+ if (!connectionConfig) {
3
+ throw new Error('Configuração de conexão da impressora não foi fornecida pelo servidor');
4
+ }
5
+
6
+ const type = connectionConfig.type;
7
+ const baseUrl = (connectionConfig.url || `http://${connectionConfig.host}:${connectionConfig.port || 80}`).replace(/\/$/, '');
8
+ const apiKey = connectionConfig.apiKey;
9
+ const headers = apiKey ? { 'X-Api-Key': apiKey } : {};
10
+
11
+ // 1. Verificação de Conexão
12
+ if (action === 'connect') {
13
+ if (type === 'klipper') {
14
+ const infoRes = await fetch(`${baseUrl}/server/info`, { headers, signal: AbortSignal.timeout(8000) });
15
+ if (!infoRes.ok) {
16
+ throw new Error(`Moonraker retornou HTTP ${infoRes.status}: ${infoRes.statusText}`);
17
+ }
18
+ const info = await infoRes.json();
19
+ const klippyState = info?.result?.klippy_state || 'ready';
20
+
21
+ let status = klippyState === 'ready' ? 'IDLE' : 'ERROR';
22
+ let message = `Klipper: ${klippyState}`;
23
+ let temperatures = {};
24
+
25
+ try {
26
+ const telRes = await fetch(
27
+ `${baseUrl}/printer/objects/query?print_stats&extruder&heater_bed`,
28
+ { headers, signal: AbortSignal.timeout(5000) }
29
+ );
30
+ if (telRes.ok) {
31
+ const tel = await telRes.json();
32
+ const stats = tel?.result?.status?.print_stats || {};
33
+ const ext = tel?.result?.status?.extruder || {};
34
+ const bed = tel?.result?.status?.heater_bed || {};
35
+ const st = String(stats.state || '').toLowerCase();
36
+ if (st === 'printing') status = 'PRINTING';
37
+ else if (st === 'paused') status = 'PAUSED';
38
+
39
+ temperatures = {
40
+ hotend: ext.temperature,
41
+ hotendTarget: ext.target,
42
+ bed: bed.temperature,
43
+ bedTarget: bed.target,
44
+ };
45
+ if (stats.message) message = stats.message;
46
+ }
47
+ } catch {}
48
+
49
+ return {
50
+ online: true,
51
+ status,
52
+ message,
53
+ temperatures,
54
+ };
55
+ }
56
+
57
+ if (type === 'octoprint') {
58
+ const verRes = await fetch(`${baseUrl}/api/version`, { headers, signal: AbortSignal.timeout(8000) });
59
+ if (!verRes.ok) {
60
+ throw new Error(`OctoPrint retornou HTTP ${verRes.status}: ${verRes.statusText}`);
61
+ }
62
+ return {
63
+ online: true,
64
+ status: 'IDLE',
65
+ message: 'OctoPrint conectado com sucesso',
66
+ };
67
+ }
68
+
69
+ throw new Error(`Protocolo de impressora "${type}" não suportado para conexão direta`);
70
+ }
71
+
72
+ // 2. Snapshot de Câmera Local
73
+ if (action === 'camera/snapshot') {
74
+ const camUrl = connectionConfig.cameraSnapshotUrl || connectionConfig.cameraUrl || `${baseUrl}/webcam/?action=snapshot`;
75
+ const camRes = await fetch(camUrl, { signal: AbortSignal.timeout(10000) });
76
+ if (!camRes.ok) {
77
+ throw new Error(`Câmera retornou HTTP ${camRes.status}: ${camRes.statusText}`);
78
+ }
79
+ const buf = await camRes.arrayBuffer();
80
+ return { base64: Buffer.from(buf).toString('base64') };
81
+ }
82
+
83
+ // 3. Controle da Impressora (Pausa, Emergência, Motores, etc.)
84
+ if (action === 'control') {
85
+ const cmd = payload?.command;
86
+ if (type === 'klipper') {
87
+ const cmdMap = {
88
+ pause: '/printer/print/pause',
89
+ resume: '/printer/print/resume',
90
+ cancel: '/printer/print/cancel',
91
+ emergency_stop: '/printer/emergency_stop',
92
+ restart: '/printer/restart',
93
+ firmware_restart: '/printer/firmware_restart',
94
+ };
95
+ const endpoint = cmdMap[cmd];
96
+ if (!endpoint) throw new Error(`Comando de controle "${cmd}" não suportado em Klipper`);
97
+ const res = await fetch(`${baseUrl}${endpoint}`, { method: 'POST', headers, signal: AbortSignal.timeout(8000) });
98
+ return { success: res.ok };
99
+ }
100
+ }
101
+
102
+ // 4. Comandos de Trabalho (Job Pause / Resume / Cancel)
103
+ if (action.startsWith('job/')) {
104
+ const jobCmd = action.replace('job/', '');
105
+ if (type === 'klipper') {
106
+ const cmdMap = {
107
+ pause: '/printer/print/pause',
108
+ resume: '/printer/print/resume',
109
+ cancel: '/printer/print/cancel',
110
+ };
111
+ const endpoint = cmdMap[jobCmd];
112
+ if (endpoint) {
113
+ const res = await fetch(`${baseUrl}${endpoint}`, { method: 'POST', headers, signal: AbortSignal.timeout(8000) });
114
+ return { success: res.ok };
115
+ }
116
+ }
117
+ }
118
+
119
+ throw new Error(`Ação "${action}" não suportada`);
120
+ }
@@ -1,5 +1,6 @@
1
1
  import WebSocket from 'ws';
2
2
  import { readConfig } from './config.js';
3
+ import { executeLocalPrinterAction } from './local-printer-executor.js';
3
4
 
4
5
  export class EdgeTunnelClient {
5
6
  constructor() {
@@ -90,52 +91,19 @@ export class EdgeTunnelClient {
90
91
  }
91
92
 
92
93
  async handleCloudCommand(msg) {
93
- const config = readConfig();
94
- const localPort = config?.localPort || 3010;
95
- const localBase = `http://127.0.0.1:${localPort}/api/v1`;
96
- const headers = {
97
- 'Content-Type': 'application/json',
98
- 'X-Api-Key': config.apiKey,
99
- };
100
-
101
94
  try {
102
95
  let resultData = null;
103
96
 
104
- if (msg.action === 'camera/snapshot') {
105
- const snapRes = await fetch(`${localBase}/printers/${msg.printerId}/camera/snapshot`, {
106
- headers: { 'X-Api-Key': config.apiKey },
107
- });
108
- if (!snapRes.ok) throw new Error(`Snapshot falhou: HTTP ${snapRes.status}`);
109
- const arrayBuf = await snapRes.arrayBuffer();
110
- resultData = { base64: Buffer.from(arrayBuf).toString('base64') };
111
- } else if (msg.action === 'control') {
112
- const cRes = await fetch(`${localBase}/printers/${msg.printerId}/control`, {
113
- method: 'POST',
114
- headers,
115
- body: JSON.stringify(msg.payload || {}),
116
- });
117
- const cJson = await cRes.json();
118
- if (!cRes.ok) throw new Error(cJson?.error?.message || 'Falha no comando de controle');
119
- resultData = cJson.data;
120
- } else if (msg.action === 'connect') {
121
- const connRes = await fetch(`${localBase}/printers/${msg.printerId}/connect`, {
122
- method: 'POST',
123
- headers,
124
- body: JSON.stringify({}),
125
- });
126
- const connJson = await connRes.json();
127
- if (!connRes.ok) throw new Error(connJson?.error?.message || 'Falha ao conectar à impressora local');
128
- resultData = connJson.data;
129
- } else if (msg.action.startsWith('job/')) {
130
- const jobAction = msg.action.replace('job/', '');
131
- const jRes = await fetch(`${localBase}/jobs/${msg.payload?.jobId}/${jobAction}`, {
132
- method: 'POST',
133
- headers,
134
- body: JSON.stringify({}),
97
+ if (msg.payload?.connectionConfig) {
98
+ // Executa diretamente na rede local (LAN) da farm
99
+ resultData = await executeLocalPrinterAction({
100
+ action: msg.action,
101
+ printerId: msg.printerId,
102
+ connectionConfig: msg.payload.connectionConfig,
103
+ payload: msg.payload,
135
104
  });
136
- const jJson = await jRes.json();
137
- if (!jRes.ok) throw new Error(jJson?.error?.message || `Falha no comando do trabalho: ${jobAction}`);
138
- resultData = jJson.data;
105
+ } else {
106
+ throw new Error('Configuração de conexão da impressora não foi fornecida pelo servidor central');
139
107
  }
140
108
 
141
109
  if (this.ws && this.ws.readyState === WebSocket.OPEN) {