- main/commands.ts: JSON-backed history (500 cap, dedupe), command library CRUD, per-session plain-text output logs (start/stop/index/open dir) - pty data path feeds the session logger via injected hooks - TerminalView: input line capture -> history recording, prefix completion popup (history + library, Tab/Arrows/Esc), session log toggle buttons - Sidebar: 命令 section — history (click to run, clear) and library (add/delete, click to run into active session) - tests: commands-store self-test ALL PASSED; full suite green
270 lines
9.2 KiB
JavaScript
270 lines
9.2 KiB
JavaScript
var __create = Object.create;
|
|
var __defProp = Object.defineProperty;
|
|
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
var __getProtoOf = Object.getPrototypeOf;
|
|
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
var __commonJS = (cb, mod) => function __require() {
|
|
return mod || (0, cb[__getOwnPropNames(cb)[0]])((mod = { exports: {} }).exports, mod), mod.exports;
|
|
};
|
|
var __export = (target, all) => {
|
|
for (var name in all)
|
|
__defProp(target, name, { get: all[name], enumerable: true });
|
|
};
|
|
var __copyProps = (to, from, except, desc) => {
|
|
if (from && typeof from === "object" || typeof from === "function") {
|
|
for (let key of __getOwnPropNames(from))
|
|
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
}
|
|
return to;
|
|
};
|
|
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
|
|
// If the importer is in node compatibility mode or this is not an ESM
|
|
// file that has been converted to a CommonJS file using a Babel-
|
|
// compatible transform (i.e. "__esModule" has not been set), then set
|
|
// "default" to the CommonJS "module.exports" for node compatibility.
|
|
isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
|
|
mod
|
|
));
|
|
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
|
|
// tests/electron-stub.cjs
|
|
var require_electron_stub = __commonJS({
|
|
"tests/electron-stub.cjs"(exports2, module2) {
|
|
module2.exports = {
|
|
BrowserWindow: {
|
|
getAllWindows: () => []
|
|
},
|
|
app: {},
|
|
ipcMain: {},
|
|
safeStorage: {
|
|
isEncryptionAvailable: () => false
|
|
}
|
|
};
|
|
}
|
|
});
|
|
|
|
// src/main/commands.ts
|
|
var commands_exports = {};
|
|
__export(commands_exports, {
|
|
CommandsStore: () => CommandsStore,
|
|
defaultCommandsPath: () => defaultCommandsPath
|
|
});
|
|
module.exports = __toCommonJS(commands_exports);
|
|
var import_electron = __toESM(require_electron_stub());
|
|
var import_crypto = require("crypto");
|
|
var import_fs = require("fs");
|
|
var import_promises = require("fs/promises");
|
|
var import_path = require("path");
|
|
var HISTORY_CAP = 500;
|
|
function emptyFile() {
|
|
return { history: [], library: [] };
|
|
}
|
|
function defaultCommandsPath(userDataPath) {
|
|
return userDataPath;
|
|
}
|
|
function two(n) {
|
|
return String(n).padStart(2, "0");
|
|
}
|
|
function stamp(d = /* @__PURE__ */ new Date()) {
|
|
return `${d.getFullYear()}${two(d.getMonth() + 1)}${two(d.getDate())}-${two(d.getHours())}${two(d.getMinutes())}${two(d.getSeconds())}`;
|
|
}
|
|
var CommandsStore = class {
|
|
/** <userData>/commands.json */
|
|
file;
|
|
/** <userData>/logs */
|
|
logsDir;
|
|
/** <userData>/logs/index.json */
|
|
indexFile;
|
|
openPathImpl;
|
|
/** All known logs keyed by absolute file path (hydrated from index on boot). */
|
|
metasByFile = /* @__PURE__ */ new Map();
|
|
/** Logs currently accumulating output, keyed by sessionId. */
|
|
activeBySession = /* @__PURE__ */ new Map();
|
|
constructor(userData, openPathImpl) {
|
|
const ud = userData ?? import_electron.app.getPath("userData");
|
|
this.file = (0, import_path.join)(ud, "commands.json");
|
|
this.logsDir = (0, import_path.join)(ud, "logs");
|
|
this.indexFile = (0, import_path.join)(this.logsDir, "index.json");
|
|
this.openPathImpl = openPathImpl;
|
|
this.hydrateIndex();
|
|
}
|
|
// ---- commands file (history + library) -----------------------------------
|
|
loadCommands() {
|
|
try {
|
|
const raw = JSON.parse((0, import_fs.readFileSync)(this.file, "utf8"));
|
|
if (raw && typeof raw === "object" && Array.isArray(raw.history)) {
|
|
const data = raw;
|
|
return {
|
|
history: Array.isArray(data.history) ? data.history : [],
|
|
library: Array.isArray(data.library) ? data.library : []
|
|
};
|
|
}
|
|
} catch {
|
|
}
|
|
return emptyFile();
|
|
}
|
|
saveCommands(data) {
|
|
(0, import_fs.mkdirSync)((0, import_path.join)(this.file, ".."), { recursive: true });
|
|
(0, import_fs.writeFileSync)(this.file, JSON.stringify(data, null, 2), "utf8");
|
|
}
|
|
/** Record one executed command: trim; dedupe vs newest; move to front; cap. */
|
|
recordCommand(cmd) {
|
|
const trimmed = cmd.trim();
|
|
if (!trimmed) return;
|
|
const data = this.loadCommands();
|
|
const top = data.history[0];
|
|
if (top && top.command === trimmed) {
|
|
top.lastUsedAt = Date.now();
|
|
} else {
|
|
data.history.unshift({
|
|
id: (0, import_crypto.randomUUID)(),
|
|
command: trimmed,
|
|
createdAt: Date.now(),
|
|
lastUsedAt: Date.now()
|
|
});
|
|
if (data.history.length > HISTORY_CAP) data.history.length = HISTORY_CAP;
|
|
}
|
|
this.saveCommands(data);
|
|
}
|
|
/** History sorted by most recently used. */
|
|
listHistory() {
|
|
return this.loadCommands().history.slice().sort((a, b) => (b.lastUsedAt ?? 0) - (a.lastUsedAt ?? 0));
|
|
}
|
|
clearHistory() {
|
|
const data = this.loadCommands();
|
|
data.history = [];
|
|
this.saveCommands(data);
|
|
}
|
|
/** Library sorted newest first. */
|
|
listLibrary() {
|
|
return this.loadCommands().library.slice().sort((a, b) => b.createdAt - a.createdAt);
|
|
}
|
|
/** Insert (no id) or update (has id); returns the stored entry. */
|
|
saveLibraryItem(item) {
|
|
const data = this.loadCommands();
|
|
let stored;
|
|
if (typeof item.id === "string" && item.id.length > 0) {
|
|
stored = data.library.find((x) => x.id === item.id);
|
|
}
|
|
if (stored) {
|
|
stored.name = item.name;
|
|
stored.command = item.command;
|
|
stored.note = item.note;
|
|
stored.group = item.group;
|
|
stored.params = item.params;
|
|
} else {
|
|
stored = {
|
|
id: item.id && item.id.length > 0 ? item.id : (0, import_crypto.randomUUID)(),
|
|
name: item.name,
|
|
command: item.command,
|
|
note: item.note,
|
|
group: item.group,
|
|
params: item.params,
|
|
createdAt: Date.now()
|
|
};
|
|
data.library.push(stored);
|
|
}
|
|
this.saveCommands(data);
|
|
return stored;
|
|
}
|
|
deleteLibraryItem(id) {
|
|
const data = this.loadCommands();
|
|
const next = data.library.filter((x) => x.id !== id);
|
|
if (next.length < data.library.length) {
|
|
data.library = next;
|
|
this.saveCommands(data);
|
|
}
|
|
}
|
|
// ---- session logs -----------------------------------------------------------
|
|
hydrateIndex() {
|
|
try {
|
|
if (!(0, import_fs.existsSync)(this.indexFile)) return;
|
|
const raw = JSON.parse((0, import_fs.readFileSync)(this.indexFile, "utf8"));
|
|
if (!Array.isArray(raw)) return;
|
|
for (const x of raw) {
|
|
if (x && typeof x.sessionId === "string" && typeof x.file === "string") {
|
|
const meta = x;
|
|
this.metasByFile.set(meta.file, meta);
|
|
if (meta.endedAt === void 0) this.activeBySession.set(meta.sessionId, meta);
|
|
}
|
|
}
|
|
} catch {
|
|
}
|
|
}
|
|
/** Write the full log index so listSessionLogs survives restart. */
|
|
persistIndex() {
|
|
try {
|
|
(0, import_fs.mkdirSync)(this.logsDir, { recursive: true });
|
|
const all = Array.from(this.metasByFile.values()).sort(
|
|
(a, b) => b.startedAt - a.startedAt
|
|
);
|
|
(0, import_fs.writeFileSync)(this.indexFile, JSON.stringify(all, null, 2), "utf8");
|
|
} catch {
|
|
}
|
|
}
|
|
/** Finalize a log (set endedAt, drop from active, persist index). */
|
|
finishLog(meta) {
|
|
meta.endedAt = Date.now();
|
|
this.activeBySession.delete(meta.sessionId);
|
|
this.persistIndex();
|
|
}
|
|
/** Start logging a session; any prior active log for the id is stopped first. */
|
|
logStart(sessionId) {
|
|
const prev = this.activeBySession.get(sessionId);
|
|
if (prev) this.finishLog(prev);
|
|
(0, import_fs.mkdirSync)(this.logsDir, { recursive: true });
|
|
const fileName = `${stamp()}-${sessionId.slice(0, 8)}.log`;
|
|
const file = (0, import_path.join)(this.logsDir, fileName);
|
|
describeFile(file);
|
|
const meta = { sessionId, file, fileName, startedAt: Date.now() };
|
|
this.metasByFile.set(file, meta);
|
|
this.activeBySession.set(sessionId, meta);
|
|
this.persistIndex();
|
|
return meta;
|
|
}
|
|
/** Append output for an active session. No-op when not logging (post-stop safe). */
|
|
logWrite(sessionId, data) {
|
|
const meta = this.activeBySession.get(sessionId);
|
|
if (!meta) {
|
|
return;
|
|
}
|
|
(0, import_promises.appendFile)(meta.file, data, "utf8").catch(() => {
|
|
});
|
|
}
|
|
/** Stop logging a session, stamping endedAt into the registry + index. */
|
|
logStop(sessionId) {
|
|
const meta = this.activeBySession.get(sessionId);
|
|
if (!meta) return;
|
|
this.finishLog(meta);
|
|
}
|
|
/** All known session logs (including persisted ones), newest first. */
|
|
listSessionLogs() {
|
|
return Array.from(this.metasByFile.values()).sort((a, b) => b.startedAt - a.startedAt);
|
|
}
|
|
/** Reveal the logs directory in the OS file manager. */
|
|
openLogsDir() {
|
|
try {
|
|
(0, import_fs.mkdirSync)(this.logsDir, { recursive: true });
|
|
if (this.openPathImpl) {
|
|
void this.openPathImpl(this.logsDir);
|
|
} else {
|
|
void import_electron.shell.openPath(this.logsDir);
|
|
}
|
|
} catch {
|
|
}
|
|
}
|
|
};
|
|
function describeFile(p) {
|
|
try {
|
|
(0, import_fs.writeFileSync)(p, "", "utf8");
|
|
} catch {
|
|
}
|
|
}
|
|
// Annotate the CommonJS export names for ESM import in node:
|
|
0 && (module.exports = {
|
|
CommandsStore,
|
|
defaultCommandsPath
|
|
});
|