Files
OpenTerminal/tests/.commands-store.cjs
T
954801926@qq.com 7467433a61 feat(M5): command history/library + completion + session logs
- 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
2026-09-06 17:28:15 +08:00

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
});