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
This commit is contained in:
1 parent
7977c264be
commit
7467433a61
12 files changed
+1593
-3
No files matched your search
@@ -0,0 +1,269 @@
|
||||
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
|
||||
});
|
||||
Reference in new issue
Block a user