Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 1 addition & 2 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -4,5 +4,4 @@ json/
dist/
build/
*.log
.env
package-lock.json
.env
32 changes: 25 additions & 7 deletions app/auth.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
const { ipcMain, app } = require('electron');
const { ipcMain, app, safeStorage } = require('electron');
const fs = require('fs');
const path = require('path');
const { LOCAL_FILE_PATH } = require("./main/helpers/paths.js")
Expand Down Expand Up @@ -139,7 +139,14 @@ async function verifyToken(token) {

function saveToken(tokenData) {
try {
fs.writeFileSync(tokenFile, JSON.stringify(tokenData, null, 2));
const raw = JSON.stringify(tokenData);
if (safeStorage.isEncryptionAvailable()) {
const encrypted = safeStorage.encryptString(raw);
fs.writeFileSync(tokenFile, encrypted);
} else {
console.warn('[Auth] safeStorage is not available, falling back to plaintext token storage');
fs.writeFileSync(tokenFile, raw);
}
return true;
} catch (error) {
console.error('Error saving token:', error);
Expand All @@ -149,11 +156,20 @@ function saveToken(tokenData) {

function loadToken() {
try {
if (fs.existsSync(tokenFile)) {
const data = fs.readFileSync(tokenFile, 'utf-8');
return JSON.parse(data);
if (!fs.existsSync(tokenFile)) {
return null;
}
return null;
const buffer = fs.readFileSync(tokenFile);
const text = buffer.toString('utf-8');
// Legacy plaintext fallback: files starting with '{' were written unencrypted
if (text.trimStart().startsWith('{')) {
return JSON.parse(text);
}
if (safeStorage.isEncryptionAvailable()) {
const decrypted = safeStorage.decryptString(buffer);
return JSON.parse(decrypted);
}
return JSON.parse(text);
} catch (error) {
console.error('Error loading token:', error);
return null;
Expand Down Expand Up @@ -372,7 +388,9 @@ ipcMain.handle("set-non-account-mode", async (_, value = true) => {
data.nonAccountMode = value

try {
fs.writeFileSync(LOCAL_FILE_PATH, JSON.stringify(data, null, 4), "utf-8")
const tempPath = LOCAL_FILE_PATH + ".tmp"
fs.writeFileSync(tempPath, JSON.stringify(data, null, 4), "utf-8")
fs.renameSync(tempPath, LOCAL_FILE_PATH)
return { ok: true }
} catch (e) {
return { ok: false }
Expand Down
94 changes: 69 additions & 25 deletions app/electron/live-server.js
Original file line number Diff line number Diff line change
Expand Up @@ -19,22 +19,47 @@ ipcMain.handle("start-live-server", async (event, htmlPath) => {
}

const root = path.dirname(htmlPath)
const port = 3000
const wsPort = 3001

function getMimeType(filePath) {
const ext = path.extname(filePath).toLowerCase()
const map = {
'.html': 'text/html',
'.js': 'application/javascript',
'.css': 'text/css',
'.json': 'application/json',
'.png': 'image/png',
'.jpg': 'image/jpeg',
'.jpeg': 'image/jpeg',
'.gif': 'image/gif',
'.svg': 'image/svg+xml',
'.ico': 'image/x-icon',
'.woff': 'font/woff',
'.woff2': 'font/woff2',
'.ttf': 'font/ttf',
'.otf': 'font/otf'
}
return map[ext] || 'application/octet-stream'
}

function inject(html) {
const script = `
<script>
const ws = new WebSocket("ws://localhost:${wsPort}")
const ws = new WebSocket("ws://localhost:${3001}")
ws.onmessage = () => location.reload()
</script>
`
return html.replace("</body>", script + "</body>")
return html.replace(/<\/body>/i, script + "</body>")
}

liveServer = http.createServer((req, res) => {

let filePath = path.join(root, req.url === "/" ? path.basename(htmlPath) : req.url)
const resolvedFile = path.resolve(filePath)
const resolvedRoot = path.resolve(root)
if (!resolvedFile.startsWith(resolvedRoot + path.sep)) {
res.writeHead(403)
return res.end("Forbidden")
}

fs.readFile(filePath, (err, data) => {

Expand All @@ -47,33 +72,52 @@ ipcMain.handle("start-live-server", async (event, htmlPath) => {
data = inject(data.toString())
}

res.writeHead(200)
res.writeHead(200, { 'Content-Type': getMimeType(filePath) })
res.end(data)

})
})

liveServer.listen(port)

wss = new WebSocket.Server({ port: wsPort })

watcher = chokidar.watch(root).on("change", () => {
wss.clients.forEach(client => {
if (client.readyState === WebSocket.OPEN) {
client.send("reload")
}
})

let port = 3000
let wsPort = 3001
return new Promise((resolve) => {
function tryListen(attempt = 0) {
liveServer.listen(port, () => {
const addr = liveServer.address()
if (addr && typeof addr === 'object') {
port = addr.port
wsPort = port + 1
}

const wsServer = http.createServer()
wss = new WebSocket.Server({ server: wsServer })
wsServer.listen(wsPort)

watcher = chokidar.watch(root).on("change", () => {
wss.clients.forEach(client => {
if (client.readyState === WebSocket.OPEN) {
client.send("reload")
}
})
})

const url = `http://localhost:${port}`
shell.openExternal(url)
resolve({ success: true, url })
})
liveServer.on("error", (err) => {
if (err.code === "EADDRINUSE" && attempt < 10) {
port++
wsPort++
tryListen(attempt + 1)
} else {
console.error("[Live Server] Failed to start:", err)
resolve({ error: err.message })
}
})
}
tryListen()
})

const url = `http://localhost:${port}`

shell.openExternal(url)

return {
success: true,
url
}
})

ipcMain.handle("stop-live-server", async () => {
Expand Down
2 changes: 1 addition & 1 deletion app/main/helpers/os.js
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,7 @@ async function readDirTree(rootPath, options = {}) {

entries.sort((a, b) => {
if (a.type === b.type) {
return a.name.localeCompare(b.name);
return a.name.localeCompare(b.name) || 0;
}
if (a.type === 'dir') return -1;
if (b.type === 'dir') return 1;
Expand Down
9 changes: 4 additions & 5 deletions app/main/helpers/requests.js
Original file line number Diff line number Diff line change
Expand Up @@ -143,8 +143,8 @@ function getPackageData() {
return {};
}
}
async function getAppIcon() {
const settings = await readSettings()
function getAppIcon() {
const settings = readSettings()

if ("app" in settings) {
if ("icon" in settings.app) {
Expand Down Expand Up @@ -183,12 +183,11 @@ async function readFileContent(filePath, encoding = 'utf8') {
? filePath
: path.join(app.getAppPath(), filePath);

const abs = path.resolve(base, filePath);
const data = await fsPromise.readFile(abs, { encoding: encoding === null ? undefined : encoding });
const data = await fsPromise.readFile(base, { encoding: encoding === null ? undefined : encoding });
return data;
}
function updateLocalAppData(newData) {
const filePath = path.join(__dirname, "local.json");
const filePath = LOCAL_FILE_PATH;

let currentData = {};
if (fs.existsSync(filePath)) {
Expand Down
48 changes: 38 additions & 10 deletions app/main/helpers/terminal.js
Original file line number Diff line number Diff line change
Expand Up @@ -40,14 +40,16 @@ class TerminalManager {
handleOutput(data, type, event) {
const output = data.toString();
const prefix = type === 'stderr' ? '[ERR] ' : '';

console.log(`[Terminal ${type}] ${output}`);

event.sender.send("terminal-result", {
type: type === 'stderr' ? 'error' : 'output',
data: prefix + output,
timestamp: Date.now()
});

if (!event.sender.isDestroyed()) {
event.sender.send("terminal-result", {
type: type === 'stderr' ? 'error' : 'output',
data: prefix + output,
timestamp: Date.now()
});
}
}

cleanupInputHandler() {
Expand Down Expand Up @@ -86,7 +88,32 @@ class TerminalManager {
}

executeCommand(event, data) {
const { cmd, cwd } = data;
let { cmd, cwd } = data;

if (typeof cmd !== 'string') {
event.sender.send("terminal-result", {
type: 'error',
data: 'Invalid command: must be a string\r\n'
});
return;
}

cmd = cmd.trim();
if (!cmd) {
event.sender.send("terminal-result", {
type: 'error',
data: 'Empty command\r\n'
});
return;
}

if (cmd.length > 5000) {
event.sender.send("terminal-result", {
type: 'error',
data: 'Command too long (max 5000 chars)\r\n'
});
return;
}

if (this.activeProcess) {
event.sender.send("terminal-result", {
Expand Down Expand Up @@ -163,9 +190,10 @@ class TerminalManager {
this.inputHandler = (e, input) => {
if (this.activeProcess && !this.activeProcess.killed) {
try {
const inputWithNewline = input.endsWith('\n') ? input : input + '\n';
const str = String(input ?? '');
const inputWithNewline = str.endsWith('\n') ? str : str + '\n';
this.activeProcess.stdin.write(inputWithNewline);
console.log(`[Terminal] Sent input: ${input}`);
console.log(`[Terminal] Sent input: ${str}`);
} catch (err) {
console.error("Error writing to stdin:", err.message);
event.sender.send("terminal-result", {
Expand Down
Loading