terminalos 0.4.0
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/README.md +324 -0
- package/bin/cli.js +28 -0
- package/build/assets/html2pdf-S6WA7sS3.js +240 -0
- package/build/assets/index-CfXPiaFw.css +32 -0
- package/build/assets/index-DfoqUTmD.js +141 -0
- package/build/index.html +17 -0
- package/electron/fs-watcher.js +181 -0
- package/electron/fs-watcher.ts +183 -0
- package/electron/main.js +245 -0
- package/electron/main.ts +241 -0
- package/electron/preload.js +77 -0
- package/electron/preload.ts +105 -0
- package/electron/process-detector.js +63 -0
- package/electron/process-detector.ts +70 -0
- package/electron/pty-manager.js +188 -0
- package/electron/pty-manager.ts +181 -0
- package/electron/tsconfig.json +14 -0
- package/electron/versions-manager.js +74 -0
- package/electron/versions-manager.ts +98 -0
- package/electron/window-state.js +49 -0
- package/electron/window-state.ts +52 -0
- package/package.json +104 -0
- package/runtime-dist/server.js +626 -0
|
@@ -0,0 +1,188 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
+
}) : function(o, v) {
|
|
16
|
+
o["default"] = v;
|
|
17
|
+
});
|
|
18
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
19
|
+
var ownKeys = function(o) {
|
|
20
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
21
|
+
var ar = [];
|
|
22
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
23
|
+
return ar;
|
|
24
|
+
};
|
|
25
|
+
return ownKeys(o);
|
|
26
|
+
};
|
|
27
|
+
return function (mod) {
|
|
28
|
+
if (mod && mod.__esModule) return mod;
|
|
29
|
+
var result = {};
|
|
30
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
31
|
+
__setModuleDefault(result, mod);
|
|
32
|
+
return result;
|
|
33
|
+
};
|
|
34
|
+
})();
|
|
35
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
|
+
exports.PtyManager = void 0;
|
|
37
|
+
const fs = __importStar(require("fs"));
|
|
38
|
+
const os = __importStar(require("os"));
|
|
39
|
+
const path = __importStar(require("path"));
|
|
40
|
+
const pty = __importStar(require("node-pty"));
|
|
41
|
+
const uuid_1 = require("uuid");
|
|
42
|
+
const process_detector_1 = require("./process-detector");
|
|
43
|
+
function createZdotdir() {
|
|
44
|
+
const zdotdir = fs.mkdtempSync(path.join(os.tmpdir(), "aiterm-"));
|
|
45
|
+
const zprofile = [
|
|
46
|
+
"# aiTerm: source real .zprofile (restores full PATH in packaged app)",
|
|
47
|
+
'[ -f "$HOME/.zprofile" ] && source "$HOME/.zprofile"',
|
|
48
|
+
].join("\n");
|
|
49
|
+
fs.writeFileSync(path.join(zdotdir, ".zprofile"), zprofile);
|
|
50
|
+
const zshrc = [
|
|
51
|
+
"# aiTerm: source real .zshrc first",
|
|
52
|
+
"unset ZDOTDIR",
|
|
53
|
+
'[ -f "$HOME/.zshrc" ] && source "$HOME/.zshrc"',
|
|
54
|
+
"",
|
|
55
|
+
"# Add our hook as the LAST precmd so it wins over oh-my-zsh/conda/starship",
|
|
56
|
+
"_aiterm_precmd() {",
|
|
57
|
+
' printf "\\n"',
|
|
58
|
+
' PROMPT=" "',
|
|
59
|
+
' RPROMPT=""',
|
|
60
|
+
' printf "\\033]9001;%s\\007" "${CONDA_DEFAULT_ENV:-}"',
|
|
61
|
+
"}",
|
|
62
|
+
"precmd_functions+=(_aiterm_precmd)",
|
|
63
|
+
"",
|
|
64
|
+
"# Bold the command line after Enter is pressed",
|
|
65
|
+
"_aiterm_preexec() {",
|
|
66
|
+
' printf "\\x1b[1A\\x1b[2K \\x1b[1m%s\\x1b[22m\\r\\n" "$1"',
|
|
67
|
+
"}",
|
|
68
|
+
"preexec_functions+=(_aiterm_preexec)",
|
|
69
|
+
'PROMPT=" "',
|
|
70
|
+
'RPROMPT=""',
|
|
71
|
+
].join("\n");
|
|
72
|
+
fs.writeFileSync(path.join(zdotdir, ".zshrc"), zshrc);
|
|
73
|
+
return zdotdir;
|
|
74
|
+
}
|
|
75
|
+
class PtyManager {
|
|
76
|
+
sessions = new Map();
|
|
77
|
+
win;
|
|
78
|
+
constructor(win) {
|
|
79
|
+
this.win = win;
|
|
80
|
+
}
|
|
81
|
+
create(opts) {
|
|
82
|
+
const sessionId = (0, uuid_1.v4)();
|
|
83
|
+
const shell = process.platform === "win32"
|
|
84
|
+
? "cmd.exe"
|
|
85
|
+
: (process.env.SHELL ?? "/bin/bash");
|
|
86
|
+
const cwd = opts.cwd ?? process.env.HOME ?? "/";
|
|
87
|
+
const isZsh = shell.endsWith("zsh");
|
|
88
|
+
const promptEnv = isZsh
|
|
89
|
+
? { ZDOTDIR: createZdotdir() }
|
|
90
|
+
: { PS1: " ", PROMPT: " " };
|
|
91
|
+
const ptyProcess = pty.spawn(shell, ["-l"], {
|
|
92
|
+
name: "xterm-256color",
|
|
93
|
+
cols: 80,
|
|
94
|
+
rows: 24,
|
|
95
|
+
cwd,
|
|
96
|
+
env: {
|
|
97
|
+
...process.env,
|
|
98
|
+
TERM: "xterm-256color",
|
|
99
|
+
COLORTERM: "truecolor",
|
|
100
|
+
...promptEnv,
|
|
101
|
+
...opts.env,
|
|
102
|
+
},
|
|
103
|
+
});
|
|
104
|
+
const detector = new process_detector_1.ProcessDetector();
|
|
105
|
+
const session = {
|
|
106
|
+
id: sessionId,
|
|
107
|
+
pty: ptyProcess,
|
|
108
|
+
buffer: [],
|
|
109
|
+
flushTimer: null,
|
|
110
|
+
detector,
|
|
111
|
+
cwd,
|
|
112
|
+
};
|
|
113
|
+
// Buffer data and flush every 12ms (Batching inteligente para evitar IPC slicing)
|
|
114
|
+
ptyProcess.onData((data) => {
|
|
115
|
+
session.buffer.push(data);
|
|
116
|
+
if (!session.flushTimer) {
|
|
117
|
+
session.flushTimer = setTimeout(() => {
|
|
118
|
+
if (session.buffer.length > 0) {
|
|
119
|
+
const chunk = session.buffer.join("");
|
|
120
|
+
session.buffer = [];
|
|
121
|
+
session.flushTimer = null; // Libera o timer para a próxima rodada de dados
|
|
122
|
+
if (!this.win.isDestroyed()) {
|
|
123
|
+
this.win.webContents.send("pty:data", sessionId, chunk);
|
|
124
|
+
}
|
|
125
|
+
// Check for AI process signatures
|
|
126
|
+
const result = detector.detect(chunk);
|
|
127
|
+
if (result === "detected" && !this.win.isDestroyed()) {
|
|
128
|
+
const ai = detector.getCurrentAI();
|
|
129
|
+
if (ai) {
|
|
130
|
+
this.win.webContents.send("pty:ai-detected", sessionId, ai);
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
else if (result === "exited" && !this.win.isDestroyed()) {
|
|
134
|
+
this.win.webContents.send("pty:ai-exited", sessionId);
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
}, 1);
|
|
138
|
+
}
|
|
139
|
+
});
|
|
140
|
+
ptyProcess.onExit(({ exitCode }) => {
|
|
141
|
+
if (session.flushTimer) {
|
|
142
|
+
clearTimeout(session.flushTimer); // Alterado de clearInterval
|
|
143
|
+
}
|
|
144
|
+
if (!this.win.isDestroyed()) {
|
|
145
|
+
this.win.webContents.send("pty:exit", sessionId, exitCode ?? 0);
|
|
146
|
+
}
|
|
147
|
+
this.sessions.delete(sessionId);
|
|
148
|
+
});
|
|
149
|
+
this.sessions.set(sessionId, session);
|
|
150
|
+
return sessionId;
|
|
151
|
+
}
|
|
152
|
+
write(sessionId, data) {
|
|
153
|
+
const session = this.sessions.get(sessionId);
|
|
154
|
+
if (session) {
|
|
155
|
+
session.pty.write(data);
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
resizeTimers = new Map();
|
|
159
|
+
resize(sessionId, cols, rows) {
|
|
160
|
+
const existing = this.resizeTimers.get(sessionId);
|
|
161
|
+
if (existing)
|
|
162
|
+
clearTimeout(existing);
|
|
163
|
+
const timer = setTimeout(() => {
|
|
164
|
+
const session = this.sessions.get(sessionId);
|
|
165
|
+
if (session) {
|
|
166
|
+
session.pty.resize(cols, rows);
|
|
167
|
+
}
|
|
168
|
+
this.resizeTimers.delete(sessionId);
|
|
169
|
+
}, 50);
|
|
170
|
+
this.resizeTimers.set(sessionId, timer);
|
|
171
|
+
}
|
|
172
|
+
async kill(sessionId) {
|
|
173
|
+
const session = this.sessions.get(sessionId);
|
|
174
|
+
if (session) {
|
|
175
|
+
if (session.flushTimer) {
|
|
176
|
+
clearTimeout(session.flushTimer); // Alterado de clearInterval
|
|
177
|
+
}
|
|
178
|
+
session.pty.kill();
|
|
179
|
+
this.sessions.delete(sessionId);
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
killAll() {
|
|
183
|
+
for (const [id] of this.sessions) {
|
|
184
|
+
this.kill(id);
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
exports.PtyManager = PtyManager;
|
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
import { BrowserWindow } from "electron";
|
|
2
|
+
import * as fs from "fs";
|
|
3
|
+
import * as os from "os";
|
|
4
|
+
import * as path from "path";
|
|
5
|
+
import * as pty from "node-pty";
|
|
6
|
+
import { v4 as uuidv4 } from "uuid";
|
|
7
|
+
import { ProcessDetector } from "./process-detector";
|
|
8
|
+
|
|
9
|
+
function createZdotdir(): string {
|
|
10
|
+
const zdotdir = fs.mkdtempSync(path.join(os.tmpdir(), "aiterm-"));
|
|
11
|
+
const zprofile = [
|
|
12
|
+
"# aiTerm: source real .zprofile (restores full PATH in packaged app)",
|
|
13
|
+
'[ -f "$HOME/.zprofile" ] && source "$HOME/.zprofile"',
|
|
14
|
+
].join("\n");
|
|
15
|
+
fs.writeFileSync(path.join(zdotdir, ".zprofile"), zprofile);
|
|
16
|
+
const zshrc = [
|
|
17
|
+
"# aiTerm: source real .zshrc first",
|
|
18
|
+
"unset ZDOTDIR",
|
|
19
|
+
'[ -f "$HOME/.zshrc" ] && source "$HOME/.zshrc"',
|
|
20
|
+
"",
|
|
21
|
+
"# Add our hook as the LAST precmd so it wins over oh-my-zsh/conda/starship",
|
|
22
|
+
"_aiterm_precmd() {",
|
|
23
|
+
' printf "\\n"',
|
|
24
|
+
' PROMPT=" "',
|
|
25
|
+
' RPROMPT=""',
|
|
26
|
+
' printf "\\033]9001;%s\\007" "${CONDA_DEFAULT_ENV:-}"',
|
|
27
|
+
"}",
|
|
28
|
+
"precmd_functions+=(_aiterm_precmd)",
|
|
29
|
+
"",
|
|
30
|
+
"# Bold the command line after Enter is pressed",
|
|
31
|
+
"_aiterm_preexec() {",
|
|
32
|
+
' printf "\\x1b[1A\\x1b[2K \\x1b[1m%s\\x1b[22m\\r\\n" "$1"',
|
|
33
|
+
"}",
|
|
34
|
+
"preexec_functions+=(_aiterm_preexec)",
|
|
35
|
+
'PROMPT=" "',
|
|
36
|
+
'RPROMPT=""',
|
|
37
|
+
].join("\n");
|
|
38
|
+
fs.writeFileSync(path.join(zdotdir, ".zshrc"), zshrc);
|
|
39
|
+
return zdotdir;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
interface Session {
|
|
43
|
+
id: string;
|
|
44
|
+
pty: pty.IPty;
|
|
45
|
+
buffer: string[];
|
|
46
|
+
flushTimer: ReturnType<typeof setTimeout> | null; // Alterado de setInterval para setTimeout
|
|
47
|
+
detector: ProcessDetector;
|
|
48
|
+
cwd: string;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export class PtyManager {
|
|
52
|
+
private sessions = new Map<string, Session>();
|
|
53
|
+
private win: BrowserWindow;
|
|
54
|
+
|
|
55
|
+
constructor(win: BrowserWindow) {
|
|
56
|
+
this.win = win;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
create(opts: { cwd?: string; env?: Record<string, string> }): string {
|
|
60
|
+
const sessionId = uuidv4();
|
|
61
|
+
const shell =
|
|
62
|
+
process.platform === "win32"
|
|
63
|
+
? "cmd.exe"
|
|
64
|
+
: (process.env.SHELL ?? "/bin/bash");
|
|
65
|
+
|
|
66
|
+
const cwd = opts.cwd ?? process.env.HOME ?? "/";
|
|
67
|
+
|
|
68
|
+
const isZsh = shell.endsWith("zsh");
|
|
69
|
+
const promptEnv = isZsh
|
|
70
|
+
? { ZDOTDIR: createZdotdir() }
|
|
71
|
+
: { PS1: " ", PROMPT: " " };
|
|
72
|
+
|
|
73
|
+
const ptyProcess = pty.spawn(shell, ["-l"], {
|
|
74
|
+
name: "xterm-256color",
|
|
75
|
+
cols: 80,
|
|
76
|
+
rows: 24,
|
|
77
|
+
cwd,
|
|
78
|
+
env: {
|
|
79
|
+
...process.env,
|
|
80
|
+
TERM: "xterm-256color",
|
|
81
|
+
COLORTERM: "truecolor",
|
|
82
|
+
...promptEnv,
|
|
83
|
+
...opts.env,
|
|
84
|
+
},
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
const detector = new ProcessDetector();
|
|
88
|
+
const session: Session = {
|
|
89
|
+
id: sessionId,
|
|
90
|
+
pty: ptyProcess,
|
|
91
|
+
buffer: [],
|
|
92
|
+
flushTimer: null,
|
|
93
|
+
detector,
|
|
94
|
+
cwd,
|
|
95
|
+
};
|
|
96
|
+
|
|
97
|
+
// Buffer data and flush every 12ms (Batching inteligente para evitar IPC slicing)
|
|
98
|
+
ptyProcess.onData((data) => {
|
|
99
|
+
session.buffer.push(data);
|
|
100
|
+
|
|
101
|
+
if (!session.flushTimer) {
|
|
102
|
+
session.flushTimer = setTimeout(() => {
|
|
103
|
+
if (session.buffer.length > 0) {
|
|
104
|
+
const chunk = session.buffer.join("");
|
|
105
|
+
session.buffer = [];
|
|
106
|
+
session.flushTimer = null; // Libera o timer para a próxima rodada de dados
|
|
107
|
+
|
|
108
|
+
if (!this.win.isDestroyed()) {
|
|
109
|
+
this.win.webContents.send("pty:data", sessionId, chunk);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
// Check for AI process signatures
|
|
113
|
+
const result = detector.detect(chunk);
|
|
114
|
+
if (result === "detected" && !this.win.isDestroyed()) {
|
|
115
|
+
const ai = detector.getCurrentAI();
|
|
116
|
+
if (ai) {
|
|
117
|
+
this.win.webContents.send("pty:ai-detected", sessionId, ai);
|
|
118
|
+
}
|
|
119
|
+
} else if (result === "exited" && !this.win.isDestroyed()) {
|
|
120
|
+
this.win.webContents.send("pty:ai-exited", sessionId);
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
}, 1);
|
|
124
|
+
}
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
ptyProcess.onExit(({ exitCode }) => {
|
|
128
|
+
if (session.flushTimer) {
|
|
129
|
+
clearTimeout(session.flushTimer); // Alterado de clearInterval
|
|
130
|
+
}
|
|
131
|
+
if (!this.win.isDestroyed()) {
|
|
132
|
+
this.win.webContents.send("pty:exit", sessionId, exitCode ?? 0);
|
|
133
|
+
}
|
|
134
|
+
this.sessions.delete(sessionId);
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
this.sessions.set(sessionId, session);
|
|
138
|
+
return sessionId;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
write(sessionId: string, data: string): void {
|
|
142
|
+
const session = this.sessions.get(sessionId);
|
|
143
|
+
if (session) {
|
|
144
|
+
session.pty.write(data);
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
private resizeTimers = new Map<string, ReturnType<typeof setTimeout>>();
|
|
149
|
+
|
|
150
|
+
resize(sessionId: string, cols: number, rows: number): void {
|
|
151
|
+
const existing = this.resizeTimers.get(sessionId);
|
|
152
|
+
if (existing) clearTimeout(existing);
|
|
153
|
+
|
|
154
|
+
const timer = setTimeout(() => {
|
|
155
|
+
const session = this.sessions.get(sessionId);
|
|
156
|
+
if (session) {
|
|
157
|
+
session.pty.resize(cols, rows);
|
|
158
|
+
}
|
|
159
|
+
this.resizeTimers.delete(sessionId);
|
|
160
|
+
}, 50);
|
|
161
|
+
|
|
162
|
+
this.resizeTimers.set(sessionId, timer);
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
async kill(sessionId: string): Promise<void> {
|
|
166
|
+
const session = this.sessions.get(sessionId);
|
|
167
|
+
if (session) {
|
|
168
|
+
if (session.flushTimer) {
|
|
169
|
+
clearTimeout(session.flushTimer); // Alterado de clearInterval
|
|
170
|
+
}
|
|
171
|
+
session.pty.kill();
|
|
172
|
+
this.sessions.delete(sessionId);
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
killAll(): void {
|
|
177
|
+
for (const [id] of this.sessions) {
|
|
178
|
+
this.kill(id);
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
"target": "ES2022",
|
|
4
|
+
"module": "CommonJS",
|
|
5
|
+
"moduleResolution": "node",
|
|
6
|
+
"strict": true,
|
|
7
|
+
"esModuleInterop": true,
|
|
8
|
+
"skipLibCheck": true,
|
|
9
|
+
"outDir": ".",
|
|
10
|
+
"rootDir": "."
|
|
11
|
+
},
|
|
12
|
+
"include": ["./**/*.ts"],
|
|
13
|
+
"exclude": ["node_modules"]
|
|
14
|
+
}
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.VersionsManager = void 0;
|
|
7
|
+
const electron_1 = require("electron");
|
|
8
|
+
const promises_1 = __importDefault(require("fs/promises"));
|
|
9
|
+
const path_1 = __importDefault(require("path"));
|
|
10
|
+
const crypto_1 = __importDefault(require("crypto"));
|
|
11
|
+
const MAX_VERSIONS = 50;
|
|
12
|
+
class VersionsManager {
|
|
13
|
+
getVersionsDir() {
|
|
14
|
+
return path_1.default.join(electron_1.app.getPath('userData'), 'md-versions');
|
|
15
|
+
}
|
|
16
|
+
getKey(filePath) {
|
|
17
|
+
return crypto_1.default.createHash('sha256').update(filePath).digest('hex');
|
|
18
|
+
}
|
|
19
|
+
async load(filePath) {
|
|
20
|
+
const dir = this.getVersionsDir();
|
|
21
|
+
const key = this.getKey(filePath);
|
|
22
|
+
const vFile = path_1.default.join(dir, `${key}.json`);
|
|
23
|
+
try {
|
|
24
|
+
const data = await promises_1.default.readFile(vFile, 'utf8');
|
|
25
|
+
const parsed = JSON.parse(data);
|
|
26
|
+
return parsed.versions ?? [];
|
|
27
|
+
}
|
|
28
|
+
catch {
|
|
29
|
+
return [];
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
async persist(filePath, versions) {
|
|
33
|
+
const dir = this.getVersionsDir();
|
|
34
|
+
await promises_1.default.mkdir(dir, { recursive: true });
|
|
35
|
+
const key = this.getKey(filePath);
|
|
36
|
+
const vFile = path_1.default.join(dir, `${key}.json`);
|
|
37
|
+
const data = { filePath, versions };
|
|
38
|
+
await promises_1.default.writeFile(vFile, JSON.stringify(data), 'utf8');
|
|
39
|
+
}
|
|
40
|
+
async saveVersion(filePath, content) {
|
|
41
|
+
const versions = await this.load(filePath);
|
|
42
|
+
// Skip if content is identical to last version
|
|
43
|
+
if (versions.length > 0 && versions[versions.length - 1].content === content) {
|
|
44
|
+
return null;
|
|
45
|
+
}
|
|
46
|
+
const nextVersion = versions.length > 0 ? versions[versions.length - 1].version + 1 : 1;
|
|
47
|
+
const now = Date.now();
|
|
48
|
+
const newVersion = {
|
|
49
|
+
id: new Date(now).toISOString(),
|
|
50
|
+
version: nextVersion,
|
|
51
|
+
timestamp: now,
|
|
52
|
+
content,
|
|
53
|
+
};
|
|
54
|
+
versions.push(newVersion);
|
|
55
|
+
// Prune to MAX_VERSIONS (keep most recent)
|
|
56
|
+
const pruned = versions.length > MAX_VERSIONS
|
|
57
|
+
? versions.slice(versions.length - MAX_VERSIONS)
|
|
58
|
+
: versions;
|
|
59
|
+
await this.persist(filePath, pruned);
|
|
60
|
+
return { id: newVersion.id, version: newVersion.version, timestamp: newVersion.timestamp };
|
|
61
|
+
}
|
|
62
|
+
async listVersions(filePath) {
|
|
63
|
+
const versions = await this.load(filePath);
|
|
64
|
+
return versions
|
|
65
|
+
.map(({ id, version, timestamp }) => ({ id, version, timestamp }))
|
|
66
|
+
.reverse(); // newest first
|
|
67
|
+
}
|
|
68
|
+
async getVersion(filePath, versionId) {
|
|
69
|
+
const versions = await this.load(filePath);
|
|
70
|
+
const found = versions.find(v => v.id === versionId);
|
|
71
|
+
return found?.content ?? null;
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
exports.VersionsManager = VersionsManager;
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
import { app } from 'electron'
|
|
2
|
+
import fs from 'fs/promises'
|
|
3
|
+
import path from 'path'
|
|
4
|
+
import crypto from 'crypto'
|
|
5
|
+
|
|
6
|
+
const MAX_VERSIONS = 50
|
|
7
|
+
|
|
8
|
+
export interface FileVersion {
|
|
9
|
+
id: string
|
|
10
|
+
version: number
|
|
11
|
+
timestamp: number
|
|
12
|
+
content: string
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export interface VersionMeta {
|
|
16
|
+
id: string
|
|
17
|
+
version: number
|
|
18
|
+
timestamp: number
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
interface VersionsFile {
|
|
22
|
+
filePath: string
|
|
23
|
+
versions: FileVersion[]
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export class VersionsManager {
|
|
27
|
+
private getVersionsDir(): string {
|
|
28
|
+
return path.join(app.getPath('userData'), 'md-versions')
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
private getKey(filePath: string): string {
|
|
32
|
+
return crypto.createHash('sha256').update(filePath).digest('hex')
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
private async load(filePath: string): Promise<FileVersion[]> {
|
|
36
|
+
const dir = this.getVersionsDir()
|
|
37
|
+
const key = this.getKey(filePath)
|
|
38
|
+
const vFile = path.join(dir, `${key}.json`)
|
|
39
|
+
try {
|
|
40
|
+
const data = await fs.readFile(vFile, 'utf8')
|
|
41
|
+
const parsed: VersionsFile = JSON.parse(data)
|
|
42
|
+
return parsed.versions ?? []
|
|
43
|
+
} catch {
|
|
44
|
+
return []
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
private async persist(filePath: string, versions: FileVersion[]): Promise<void> {
|
|
49
|
+
const dir = this.getVersionsDir()
|
|
50
|
+
await fs.mkdir(dir, { recursive: true })
|
|
51
|
+
const key = this.getKey(filePath)
|
|
52
|
+
const vFile = path.join(dir, `${key}.json`)
|
|
53
|
+
const data: VersionsFile = { filePath, versions }
|
|
54
|
+
await fs.writeFile(vFile, JSON.stringify(data), 'utf8')
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
async saveVersion(filePath: string, content: string): Promise<VersionMeta | null> {
|
|
58
|
+
const versions = await this.load(filePath)
|
|
59
|
+
|
|
60
|
+
// Skip if content is identical to last version
|
|
61
|
+
if (versions.length > 0 && versions[versions.length - 1].content === content) {
|
|
62
|
+
return null
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
const nextVersion = versions.length > 0 ? versions[versions.length - 1].version + 1 : 1
|
|
66
|
+
const now = Date.now()
|
|
67
|
+
const newVersion: FileVersion = {
|
|
68
|
+
id: new Date(now).toISOString(),
|
|
69
|
+
version: nextVersion,
|
|
70
|
+
timestamp: now,
|
|
71
|
+
content,
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
versions.push(newVersion)
|
|
75
|
+
|
|
76
|
+
// Prune to MAX_VERSIONS (keep most recent)
|
|
77
|
+
const pruned = versions.length > MAX_VERSIONS
|
|
78
|
+
? versions.slice(versions.length - MAX_VERSIONS)
|
|
79
|
+
: versions
|
|
80
|
+
|
|
81
|
+
await this.persist(filePath, pruned)
|
|
82
|
+
|
|
83
|
+
return { id: newVersion.id, version: newVersion.version, timestamp: newVersion.timestamp }
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
async listVersions(filePath: string): Promise<VersionMeta[]> {
|
|
87
|
+
const versions = await this.load(filePath)
|
|
88
|
+
return versions
|
|
89
|
+
.map(({ id, version, timestamp }) => ({ id, version, timestamp }))
|
|
90
|
+
.reverse() // newest first
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
async getVersion(filePath: string, versionId: string): Promise<string | null> {
|
|
94
|
+
const versions = await this.load(filePath)
|
|
95
|
+
const found = versions.find(v => v.id === versionId)
|
|
96
|
+
return found?.content ?? null
|
|
97
|
+
}
|
|
98
|
+
}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.WindowState = void 0;
|
|
7
|
+
const electron_1 = require("electron");
|
|
8
|
+
const fs_1 = __importDefault(require("fs"));
|
|
9
|
+
const path_1 = __importDefault(require("path"));
|
|
10
|
+
class WindowState {
|
|
11
|
+
filePath;
|
|
12
|
+
bounds = { width: 1280, height: 800 };
|
|
13
|
+
constructor() {
|
|
14
|
+
this.filePath = path_1.default.join(electron_1.app.getPath('userData'), 'window-state.json');
|
|
15
|
+
this.load();
|
|
16
|
+
}
|
|
17
|
+
load() {
|
|
18
|
+
try {
|
|
19
|
+
if (fs_1.default.existsSync(this.filePath)) {
|
|
20
|
+
const data = fs_1.default.readFileSync(this.filePath, 'utf8');
|
|
21
|
+
this.bounds = JSON.parse(data);
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
catch {
|
|
25
|
+
// Use defaults
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
get() {
|
|
29
|
+
return this.bounds;
|
|
30
|
+
}
|
|
31
|
+
save(win) {
|
|
32
|
+
if (!win.isMaximized() && !win.isMinimized()) {
|
|
33
|
+
const bounds = win.getBounds();
|
|
34
|
+
this.bounds = {
|
|
35
|
+
x: bounds.x,
|
|
36
|
+
y: bounds.y,
|
|
37
|
+
width: bounds.width,
|
|
38
|
+
height: bounds.height,
|
|
39
|
+
};
|
|
40
|
+
try {
|
|
41
|
+
fs_1.default.writeFileSync(this.filePath, JSON.stringify(this.bounds), 'utf8');
|
|
42
|
+
}
|
|
43
|
+
catch {
|
|
44
|
+
// Ignore write errors
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
exports.WindowState = WindowState;
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import { BrowserWindow, app } from 'electron'
|
|
2
|
+
import fs from 'fs'
|
|
3
|
+
import path from 'path'
|
|
4
|
+
|
|
5
|
+
interface WindowBounds {
|
|
6
|
+
x?: number
|
|
7
|
+
y?: number
|
|
8
|
+
width: number
|
|
9
|
+
height: number
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export class WindowState {
|
|
13
|
+
private filePath: string
|
|
14
|
+
private bounds: WindowBounds = { width: 1280, height: 800 }
|
|
15
|
+
|
|
16
|
+
constructor() {
|
|
17
|
+
this.filePath = path.join(app.getPath('userData'), 'window-state.json')
|
|
18
|
+
this.load()
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
private load(): void {
|
|
22
|
+
try {
|
|
23
|
+
if (fs.existsSync(this.filePath)) {
|
|
24
|
+
const data = fs.readFileSync(this.filePath, 'utf8')
|
|
25
|
+
this.bounds = JSON.parse(data)
|
|
26
|
+
}
|
|
27
|
+
} catch {
|
|
28
|
+
// Use defaults
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
get(): WindowBounds {
|
|
33
|
+
return this.bounds
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
save(win: BrowserWindow): void {
|
|
37
|
+
if (!win.isMaximized() && !win.isMinimized()) {
|
|
38
|
+
const bounds = win.getBounds()
|
|
39
|
+
this.bounds = {
|
|
40
|
+
x: bounds.x,
|
|
41
|
+
y: bounds.y,
|
|
42
|
+
width: bounds.width,
|
|
43
|
+
height: bounds.height,
|
|
44
|
+
}
|
|
45
|
+
try {
|
|
46
|
+
fs.writeFileSync(this.filePath, JSON.stringify(this.bounds), 'utf8')
|
|
47
|
+
} catch {
|
|
48
|
+
// Ignore write errors
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
}
|