rl-rockcli 0.0.8 → 0.0.10
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/commands/attach/basic-repl.js +212 -0
- package/commands/attach/cleanup-history.js +189 -0
- package/commands/attach/cleanup-manager.js +163 -0
- package/commands/attach/copy-ui/copyRepl.js +195 -0
- package/commands/attach/copy-ui/index.js +7 -0
- package/commands/attach/copy-ui/render/outputBlock.js +25 -0
- package/commands/attach/copy-ui/viewport/viewport.js +23 -0
- package/commands/attach/copy-ui/viewport/wheel.js +14 -0
- package/commands/attach/history-manager.js +507 -0
- package/commands/attach/history-session.js +48 -0
- package/commands/attach/ink-repl/InkREPL.js +1507 -0
- package/commands/attach/ink-repl/builtinCommands.js +1253 -0
- package/commands/attach/ink-repl/components/ConnectingScreen.js +76 -0
- package/commands/attach/ink-repl/components/Console.js +191 -0
- package/commands/attach/ink-repl/components/DetailView.js +148 -0
- package/commands/attach/ink-repl/components/DropdownMenu.js +86 -0
- package/commands/attach/ink-repl/components/InputArea.js +125 -0
- package/commands/attach/ink-repl/components/InputLine.js +18 -0
- package/commands/attach/ink-repl/components/OutputArea.js +22 -0
- package/commands/attach/ink-repl/components/OutputItem.js +96 -0
- package/commands/attach/ink-repl/components/ShellLayout.js +61 -0
- package/commands/attach/ink-repl/components/Spinner.js +79 -0
- package/commands/attach/ink-repl/components/StatusBar.js +106 -0
- package/commands/attach/ink-repl/components/WelcomeBanner.js +48 -0
- package/commands/attach/ink-repl/contexts/LayoutContext.js +12 -0
- package/commands/attach/ink-repl/contexts/ThemeContext.js +43 -0
- package/commands/attach/ink-repl/hooks/useFunctionKeys.js +70 -0
- package/commands/attach/ink-repl/hooks/useMouse.js +162 -0
- package/commands/attach/ink-repl/hooks/useResources.js +132 -0
- package/commands/attach/ink-repl/hooks/useSpinner.js +49 -0
- package/commands/attach/ink-repl/index.js +112 -0
- package/commands/attach/ink-repl/package.json +3 -0
- package/commands/attach/ink-repl/replState.js +947 -0
- package/commands/attach/ink-repl/shortcuts/defaultKeybindings.js +138 -0
- package/commands/attach/ink-repl/shortcuts/index.js +332 -0
- package/commands/attach/ink-repl/themes/defaultDark.js +18 -0
- package/commands/attach/ink-repl/themes/defaultLight.js +18 -0
- package/commands/attach/ink-repl/themes/index.js +4 -0
- package/commands/attach/ink-repl/themes/themeManager.js +45 -0
- package/commands/attach/ink-repl/themes/themeTokens.js +15 -0
- package/commands/attach/ink-repl/utils/atCompletion.js +346 -0
- package/commands/attach/ink-repl/utils/clipboard.js +50 -0
- package/commands/attach/ink-repl/utils/consoleLogger.js +81 -0
- package/commands/attach/ink-repl/utils/exitCodeHandler.js +49 -0
- package/commands/attach/ink-repl/utils/exitCodeTips.js +56 -0
- package/commands/attach/ink-repl/utils/formatTime.js +12 -0
- package/commands/attach/ink-repl/utils/outputSelection.js +120 -0
- package/commands/attach/ink-repl/utils/outputViewport.js +77 -0
- package/commands/attach/ink-repl/utils/paginatedFileLoading.js +76 -0
- package/commands/attach/ink-repl/utils/paramHint.js +60 -0
- package/commands/attach/ink-repl/utils/parseError.js +174 -0
- package/commands/attach/ink-repl/utils/pathCompletion.js +167 -0
- package/commands/attach/ink-repl/utils/remotePathSafety.js +56 -0
- package/commands/attach/ink-repl/utils/replSelection.js +205 -0
- package/commands/attach/ink-repl/utils/responseFormatter.js +127 -0
- package/commands/attach/ink-repl/utils/textWrap.js +117 -0
- package/commands/attach/ink-repl/utils/truncate.js +115 -0
- package/commands/attach/opentui-repl/App.tsx +891 -0
- package/commands/attach/opentui-repl/builtinCommands.ts +80 -0
- package/commands/attach/opentui-repl/components/ConfirmDialog.tsx +116 -0
- package/commands/attach/opentui-repl/components/ConnectingScreen.tsx +131 -0
- package/commands/attach/opentui-repl/components/Console.tsx +73 -0
- package/commands/attach/opentui-repl/components/DetailView.tsx +45 -0
- package/commands/attach/opentui-repl/components/DropdownMenu.tsx +130 -0
- package/commands/attach/opentui-repl/components/ExecutionStatus.tsx +66 -0
- package/commands/attach/opentui-repl/components/Header.tsx +24 -0
- package/commands/attach/opentui-repl/components/OutputArea.tsx +25 -0
- package/commands/attach/opentui-repl/components/OutputBlock.tsx +108 -0
- package/commands/attach/opentui-repl/components/PromptInput.tsx +109 -0
- package/commands/attach/opentui-repl/components/StatusBar.tsx +63 -0
- package/commands/attach/opentui-repl/components/Toast.tsx +65 -0
- package/commands/attach/opentui-repl/components/WelcomeBanner.tsx +41 -0
- package/commands/attach/opentui-repl/contexts/ReplContext.tsx +137 -0
- package/commands/attach/opentui-repl/contexts/SessionContext.tsx +32 -0
- package/commands/attach/opentui-repl/contexts/ThemeContext.tsx +70 -0
- package/commands/attach/opentui-repl/contexts/ToastContext.tsx +69 -0
- package/commands/attach/opentui-repl/contexts/toast-logic.js +71 -0
- package/commands/attach/opentui-repl/hooks/useResources.ts +102 -0
- package/commands/attach/opentui-repl/hooks/useSpinner.ts +46 -0
- package/commands/attach/opentui-repl/index.js +99 -0
- package/commands/attach/opentui-repl/keybindings.ts +39 -0
- package/commands/attach/opentui-repl/package.json +3 -0
- package/commands/attach/opentui-repl/render.tsx +72 -0
- package/commands/attach/opentui-repl/tsconfig.json +12 -0
- package/commands/attach/repl.js +791 -0
- package/commands/attach/sandbox-id-resolver.js +56 -0
- package/commands/attach/session-manager.js +307 -0
- package/commands/attach/ui-mode.js +146 -0
- package/commands/log/core/constants.js +237 -0
- package/commands/log/core/display.js +370 -0
- package/commands/log/core/search.js +330 -0
- package/commands/log/core/tail.js +216 -0
- package/commands/log/core/utils.js +424 -0
- package/commands/log.js +298 -0
- package/commands/sandbox/core/log-bridge.js +119 -0
- package/commands/sandbox/core/replay/analyzer.js +311 -0
- package/commands/sandbox/core/replay/batch-orchestrator.js +536 -0
- package/commands/sandbox/core/replay/batch-task.js +369 -0
- package/commands/sandbox/core/replay/concurrent-display.js +70 -0
- package/commands/sandbox/core/replay/concurrent-orchestrator.js +170 -0
- package/commands/sandbox/core/replay/data-source.js +86 -0
- package/commands/sandbox/core/replay/display.js +231 -0
- package/commands/sandbox/core/replay/executor.js +634 -0
- package/commands/sandbox/core/replay/history-fetcher.js +124 -0
- package/commands/sandbox/core/replay/index.js +338 -0
- package/commands/sandbox/core/replay/loghouse-data-source.js +177 -0
- package/commands/sandbox/core/replay/pid-mapping.js +26 -0
- package/commands/sandbox/core/replay/request.js +109 -0
- package/commands/sandbox/core/replay/worker.js +166 -0
- package/commands/sandbox/core/session.js +346 -0
- package/commands/sandbox/log-bridge.js +2 -0
- package/commands/sandbox/ray.js +2 -0
- package/commands/sandbox/replay/analyzer.js +311 -0
- package/commands/sandbox/replay/batch-orchestrator.js +536 -0
- package/commands/sandbox/replay/batch-task.js +369 -0
- package/commands/sandbox/replay/concurrent-display.js +70 -0
- package/commands/sandbox/replay/concurrent-orchestrator.js +170 -0
- package/commands/sandbox/replay/display.js +231 -0
- package/commands/sandbox/replay/executor.js +634 -0
- package/commands/sandbox/replay/history-fetcher.js +118 -0
- package/commands/sandbox/replay/index.js +338 -0
- package/commands/sandbox/replay/pid-mapping.js +26 -0
- package/commands/sandbox/replay/request.js +109 -0
- package/commands/sandbox/replay/worker.js +166 -0
- package/commands/sandbox/replay.js +2 -0
- package/commands/sandbox/session.js +2 -0
- package/commands/sandbox-original.js +1393 -0
- package/commands/sandbox.js +499 -0
- package/help/help.json +1071 -0
- package/help/middleware.js +71 -0
- package/help/renderer.js +800 -0
- package/index.js +5 -15
- package/lib/plugin-context.js +40 -0
- package/package.json +2 -2
- package/sdks/sandbox/core/client.js +845 -0
- package/sdks/sandbox/core/config.js +70 -0
- package/sdks/sandbox/core/types.js +74 -0
- package/sdks/sandbox/httpLogger.js +251 -0
- package/sdks/sandbox/index.js +9 -0
- package/utils/asciiArt.js +138 -0
- package/utils/bun-compat.js +59 -0
- package/utils/ciPipelines.js +138 -0
- package/utils/cli.js +17 -0
- package/utils/command-router.js +79 -0
- package/utils/configManager.js +503 -0
- package/utils/dependency-resolver.js +135 -0
- package/utils/eagleeye_traceid.js +151 -0
- package/utils/envDetector.js +78 -0
- package/utils/execution_logger.js +415 -0
- package/utils/featureManager.js +68 -0
- package/utils/firstTimeTip.js +44 -0
- package/utils/hook-manager.js +125 -0
- package/utils/http-logger.js +264 -0
- package/utils/i18n.js +139 -0
- package/utils/image-progress.js +159 -0
- package/utils/logger.js +154 -0
- package/utils/plugin-loader.js +124 -0
- package/utils/plugin-manager.js +348 -0
- package/utils/ray_cli_wrapper.js +746 -0
- package/utils/sandbox-client.js +419 -0
- package/utils/terminal.js +32 -0
- package/utils/tips.js +106 -0
|
@@ -0,0 +1,507 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const fs = require('fs');
|
|
4
|
+
const path = require('path');
|
|
5
|
+
const os = require('os');
|
|
6
|
+
const crypto = require('crypto');
|
|
7
|
+
const logger = require('../../utils/logger');
|
|
8
|
+
|
|
9
|
+
function safeJsonParse(content, fallback) {
|
|
10
|
+
try {
|
|
11
|
+
return JSON.parse(content);
|
|
12
|
+
} catch {
|
|
13
|
+
return fallback;
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function writeFileAtomicSync(filePath, data) {
|
|
18
|
+
const dir = path.dirname(filePath);
|
|
19
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
20
|
+
|
|
21
|
+
const tmpPath = `${filePath}.tmp-${process.pid}-${Date.now()}`;
|
|
22
|
+
fs.writeFileSync(tmpPath, data);
|
|
23
|
+
fs.renameSync(tmpPath, filePath);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function writeJsonAtomicSync(filePath, obj) {
|
|
27
|
+
writeFileAtomicSync(filePath, JSON.stringify(obj, null, 2));
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
class HistoryManager {
|
|
31
|
+
constructor(sandboxId, options = {}) {
|
|
32
|
+
this.sandboxId = sandboxId;
|
|
33
|
+
this.baseDir = options.baseDir || path.join(os.homedir(), '.rock');
|
|
34
|
+
this.sessionId = null;
|
|
35
|
+
this.maxHistoryPerSession = options.maxHistoryPerSession || 200;
|
|
36
|
+
this._onNotice = typeof options.onNotice === 'function' ? options.onNotice : null;
|
|
37
|
+
this._noticeShown = new Set();
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
_noticeOnce(key, message) {
|
|
41
|
+
if (!this._onNotice) return;
|
|
42
|
+
if (this._noticeShown.has(key)) return;
|
|
43
|
+
this._noticeShown.add(key);
|
|
44
|
+
try {
|
|
45
|
+
this._onNotice(message);
|
|
46
|
+
} catch {
|
|
47
|
+
// ignore
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
async _withIndexLock(fn) {
|
|
52
|
+
const lockPath = path.join(this._getSandboxDir(), 'index.json.lock');
|
|
53
|
+
const staleMs = 10_000;
|
|
54
|
+
const maxWaitMs = 2_000;
|
|
55
|
+
const start = Date.now();
|
|
56
|
+
|
|
57
|
+
while (true) {
|
|
58
|
+
try {
|
|
59
|
+
fs.mkdirSync(this._getSandboxDir(), { recursive: true });
|
|
60
|
+
const fd = fs.openSync(lockPath, 'wx');
|
|
61
|
+
try {
|
|
62
|
+
fs.writeFileSync(fd, JSON.stringify({ pid: process.pid, ts: Date.now() }));
|
|
63
|
+
} finally {
|
|
64
|
+
fs.closeSync(fd);
|
|
65
|
+
}
|
|
66
|
+
break;
|
|
67
|
+
} catch (e) {
|
|
68
|
+
if (!e || e.code !== 'EEXIST') throw e;
|
|
69
|
+
|
|
70
|
+
// Stale lock recovery
|
|
71
|
+
try {
|
|
72
|
+
const stat = fs.statSync(lockPath);
|
|
73
|
+
if (Date.now() - stat.mtimeMs > staleMs) {
|
|
74
|
+
fs.rmSync(lockPath, { force: true });
|
|
75
|
+
continue;
|
|
76
|
+
}
|
|
77
|
+
} catch {
|
|
78
|
+
// ignore
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
if (Date.now() - start > maxWaitMs) {
|
|
82
|
+
throw new Error('History index is locked; please retry.');
|
|
83
|
+
}
|
|
84
|
+
// eslint-disable-next-line no-await-in-loop
|
|
85
|
+
await new Promise(r => setTimeout(r, 25));
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
try {
|
|
90
|
+
return await fn();
|
|
91
|
+
} finally {
|
|
92
|
+
try {
|
|
93
|
+
fs.rmSync(lockPath, { force: true });
|
|
94
|
+
} catch {
|
|
95
|
+
// ignore
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* Switch to an existing history session (does not create a new one).
|
|
102
|
+
* Updates index.json current_session and the current symlink when possible.
|
|
103
|
+
* @param {string} sessionId
|
|
104
|
+
* @returns {Promise<string>} sessionId
|
|
105
|
+
*/
|
|
106
|
+
async resume(sessionId) {
|
|
107
|
+
if (!sessionId) {
|
|
108
|
+
throw new Error('sessionId required');
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
const sessionDir = path.join(this._getSandboxDir(), sessionId);
|
|
112
|
+
if (!fs.existsSync(sessionDir)) {
|
|
113
|
+
throw new Error(`Session not found: ${sessionId}`);
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
this.sessionId = sessionId;
|
|
117
|
+
|
|
118
|
+
// Ensure index exists and points to this session as current
|
|
119
|
+
const sandboxDir = this._getSandboxDir();
|
|
120
|
+
const indexFile = path.join(sandboxDir, 'index.json');
|
|
121
|
+
|
|
122
|
+
await this._withIndexLock(async () => {
|
|
123
|
+
let index = { sandbox_id: this.sandboxId, sessions: [] };
|
|
124
|
+
if (fs.existsSync(indexFile)) {
|
|
125
|
+
index = safeJsonParse(fs.readFileSync(indexFile, 'utf8'), index);
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
index.sandbox_id = index.sandbox_id || this.sandboxId;
|
|
129
|
+
index.sessions = Array.isArray(index.sessions) ? index.sessions : [];
|
|
130
|
+
index.current_session = sessionId;
|
|
131
|
+
|
|
132
|
+
// Add to index if missing
|
|
133
|
+
const existsInIndex = index.sessions.some(s => s.id === sessionId || s.session_id === sessionId);
|
|
134
|
+
if (!existsInIndex) {
|
|
135
|
+
let createdAt = new Date().toISOString();
|
|
136
|
+
const metaFile = path.join(sessionDir, 'meta.json');
|
|
137
|
+
if (fs.existsSync(metaFile)) {
|
|
138
|
+
const meta = safeJsonParse(fs.readFileSync(metaFile, 'utf8'), null);
|
|
139
|
+
createdAt = (meta && meta.created_at) || createdAt;
|
|
140
|
+
}
|
|
141
|
+
index.sessions.push({
|
|
142
|
+
id: sessionId,
|
|
143
|
+
created_at: createdAt,
|
|
144
|
+
ended_at: null,
|
|
145
|
+
command_count: 0,
|
|
146
|
+
});
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
writeJsonAtomicSync(indexFile, index);
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
await this._updateCurrentSymlink();
|
|
153
|
+
return this.sessionId;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
/**
|
|
157
|
+
* Find a history session id by the underlying sandbox shell session name.
|
|
158
|
+
* @param {string} shellSessionName
|
|
159
|
+
* @returns {Promise<string|null>}
|
|
160
|
+
*/
|
|
161
|
+
async findSessionByShellSessionName(shellSessionName) {
|
|
162
|
+
if (!shellSessionName) return null;
|
|
163
|
+
const sessions = await this.listSessions();
|
|
164
|
+
for (const session of sessions) {
|
|
165
|
+
const sessionId = session.session_id || session.id;
|
|
166
|
+
if (!sessionId) continue;
|
|
167
|
+
if (session.shell_session_name === shellSessionName) {
|
|
168
|
+
return sessionId;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
// Back-compat: read meta.json if listSessions did not populate it
|
|
172
|
+
const metaFile = path.join(this._getSandboxDir(), sessionId, 'meta.json');
|
|
173
|
+
if (fs.existsSync(metaFile)) {
|
|
174
|
+
try {
|
|
175
|
+
const meta = JSON.parse(fs.readFileSync(metaFile, 'utf8'));
|
|
176
|
+
if (meta.shell_session_name === shellSessionName) {
|
|
177
|
+
return sessionId;
|
|
178
|
+
}
|
|
179
|
+
} catch {
|
|
180
|
+
// ignore
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
return null;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
/**
|
|
188
|
+
* Resume an existing history session by sandbox shell session name.
|
|
189
|
+
* @param {string} shellSessionName
|
|
190
|
+
* @returns {Promise<string|null>} history session id
|
|
191
|
+
*/
|
|
192
|
+
async resumeByShellSessionName(shellSessionName) {
|
|
193
|
+
logger.debug(`resumeByShellSessionName: searching for shellSessionName=${shellSessionName}`);
|
|
194
|
+
const sessionId = await this.findSessionByShellSessionName(shellSessionName);
|
|
195
|
+
if (!sessionId) {
|
|
196
|
+
logger.debug(`resumeByShellSessionName: no history session found for shellSessionName=${shellSessionName}`);
|
|
197
|
+
return null;
|
|
198
|
+
}
|
|
199
|
+
logger.debug(`resumeByShellSessionName: found history sessionId=${sessionId}`);
|
|
200
|
+
await this.resume(sessionId);
|
|
201
|
+
return sessionId;
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
async init(options = {}) {
|
|
205
|
+
this.sessionId = this._generateSessionId();
|
|
206
|
+
const sessionDir = this._getSessionDir();
|
|
207
|
+
|
|
208
|
+
// Create directory structure
|
|
209
|
+
fs.mkdirSync(sessionDir, { recursive: true });
|
|
210
|
+
|
|
211
|
+
// Create meta.json
|
|
212
|
+
const meta = {
|
|
213
|
+
session_id: this.sessionId,
|
|
214
|
+
sandbox_id: this.sandboxId,
|
|
215
|
+
shell_session_name: options.shellSessionName || null,
|
|
216
|
+
created_at: new Date().toISOString(),
|
|
217
|
+
ended_at: null,
|
|
218
|
+
command_count: 0,
|
|
219
|
+
};
|
|
220
|
+
writeJsonAtomicSync(path.join(sessionDir, 'meta.json'), meta);
|
|
221
|
+
|
|
222
|
+
// Create empty history file
|
|
223
|
+
fs.writeFileSync(path.join(sessionDir, 'shell_history'), '');
|
|
224
|
+
|
|
225
|
+
// Update index
|
|
226
|
+
await this._updateIndex();
|
|
227
|
+
|
|
228
|
+
// Update current symlink
|
|
229
|
+
await this._updateCurrentSymlink();
|
|
230
|
+
|
|
231
|
+
return this.sessionId;
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
getLastShellSessionName() {
|
|
235
|
+
const indexFile = path.join(this._getSandboxDir(), 'index.json');
|
|
236
|
+
if (!fs.existsSync(indexFile)) {
|
|
237
|
+
return null;
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
try {
|
|
241
|
+
const index = JSON.parse(fs.readFileSync(indexFile, 'utf8'));
|
|
242
|
+
if (!index.current_session) {
|
|
243
|
+
return null;
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
const metaFile = path.join(this._getSandboxDir(), index.current_session, 'meta.json');
|
|
247
|
+
if (!fs.existsSync(metaFile)) {
|
|
248
|
+
return null;
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
const meta = JSON.parse(fs.readFileSync(metaFile, 'utf8'));
|
|
252
|
+
return meta.shell_session_name || null;
|
|
253
|
+
} catch (error) {
|
|
254
|
+
return null;
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
async updateShellSessionName(shellSessionName) {
|
|
259
|
+
const metaFile = path.join(this._getSessionDir(), 'meta.json');
|
|
260
|
+
if (fs.existsSync(metaFile)) {
|
|
261
|
+
const meta = safeJsonParse(fs.readFileSync(metaFile, 'utf8'), {});
|
|
262
|
+
meta.shell_session_name = shellSessionName;
|
|
263
|
+
writeJsonAtomicSync(metaFile, meta);
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
async addCommand(command) {
|
|
268
|
+
// Skip if command is undefined, null, or not a string
|
|
269
|
+
if (typeof command !== 'string' || command.trim() === '') {
|
|
270
|
+
return;
|
|
271
|
+
}
|
|
272
|
+
const historyFile = path.join(this._getSessionDir(), 'shell_history');
|
|
273
|
+
fs.appendFileSync(historyFile, command + '\n');
|
|
274
|
+
|
|
275
|
+
// Update command count in meta
|
|
276
|
+
const metaFile = path.join(this._getSessionDir(), 'meta.json');
|
|
277
|
+
const meta = safeJsonParse(fs.readFileSync(metaFile, 'utf8'), {});
|
|
278
|
+
meta.command_count++;
|
|
279
|
+
writeJsonAtomicSync(metaFile, meta);
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
/**
|
|
283
|
+
* Save output history to disk
|
|
284
|
+
* @param {Array} outputs - Array of output items from REPL state
|
|
285
|
+
*/
|
|
286
|
+
async saveOutputs(outputs) {
|
|
287
|
+
const outputFile = path.join(this._getSessionDir(), 'outputs.json');
|
|
288
|
+
try {
|
|
289
|
+
// Filter out welcome messages and only keep relevant data
|
|
290
|
+
const filteredOutputs = outputs
|
|
291
|
+
.filter(item => !item.isWelcome)
|
|
292
|
+
.map(item => ({
|
|
293
|
+
command: item.command,
|
|
294
|
+
output: item.output,
|
|
295
|
+
exitCode: item.exitCode,
|
|
296
|
+
timestamp: item.timestamp,
|
|
297
|
+
prompt: item.prompt,
|
|
298
|
+
// Save metaInfo and tips for error display
|
|
299
|
+
metaInfo: item.metaInfo,
|
|
300
|
+
tips: item.tips,
|
|
301
|
+
}));
|
|
302
|
+
|
|
303
|
+
writeJsonAtomicSync(outputFile, filteredOutputs);
|
|
304
|
+
} catch (error) {
|
|
305
|
+
logger.debug(`Failed to save outputs: ${error && error.message ? error.message : String(error)}`);
|
|
306
|
+
this._noticeOnce('history-save-failed', 'Warning: failed to save attach history (outputs).');
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
/**
|
|
311
|
+
* Load output history from disk
|
|
312
|
+
* @returns {Array} Array of output items
|
|
313
|
+
*/
|
|
314
|
+
async loadOutputs() {
|
|
315
|
+
const outputFile = path.join(this._getSessionDir(), 'outputs.json');
|
|
316
|
+
if (!fs.existsSync(outputFile)) {
|
|
317
|
+
return [];
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
try {
|
|
321
|
+
const content = fs.readFileSync(outputFile, 'utf8');
|
|
322
|
+
const outputs = safeJsonParse(content, null);
|
|
323
|
+
if (!Array.isArray(outputs)) return [];
|
|
324
|
+
|
|
325
|
+
// Restore output format with IDs
|
|
326
|
+
return outputs.map((item, index) => ({
|
|
327
|
+
id: `output-${Date.now()}-${index}`,
|
|
328
|
+
command: item.command,
|
|
329
|
+
output: item.output,
|
|
330
|
+
exitCode: item.exitCode !== undefined ? item.exitCode : 0,
|
|
331
|
+
timestamp: item.timestamp ? new Date(item.timestamp) : new Date(),
|
|
332
|
+
truncated: false,
|
|
333
|
+
prompt: item.prompt || null,
|
|
334
|
+
isWelcome: false,
|
|
335
|
+
// Restore metaInfo and tips for error display
|
|
336
|
+
metaInfo: item.metaInfo || null,
|
|
337
|
+
tips: item.tips || null,
|
|
338
|
+
}));
|
|
339
|
+
} catch (error) {
|
|
340
|
+
logger.debug(`Failed to load outputs: ${error && error.message ? error.message : String(error)}`);
|
|
341
|
+
this._noticeOnce('history-load-failed', 'Warning: failed to load attach history (outputs).');
|
|
342
|
+
return [];
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
async getHistory() {
|
|
347
|
+
const historyFile = path.join(this._getSessionDir(), 'shell_history');
|
|
348
|
+
if (!fs.existsSync(historyFile)) {
|
|
349
|
+
return [];
|
|
350
|
+
}
|
|
351
|
+
const content = fs.readFileSync(historyFile, 'utf8');
|
|
352
|
+
return content.split('\n').filter(line => line.trim());
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
async listSessions() {
|
|
356
|
+
const indexFile = path.join(this._getSandboxDir(), 'index.json');
|
|
357
|
+
let index = null;
|
|
358
|
+
if (fs.existsSync(indexFile)) {
|
|
359
|
+
try {
|
|
360
|
+
index = JSON.parse(fs.readFileSync(indexFile, 'utf8'));
|
|
361
|
+
} catch (e) {
|
|
362
|
+
index = null;
|
|
363
|
+
}
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
// Fallback: scan directories when index is missing/corrupt
|
|
367
|
+
if (!index) {
|
|
368
|
+
const sandboxDir = this._getSandboxDir();
|
|
369
|
+
if (!fs.existsSync(sandboxDir)) {
|
|
370
|
+
return [];
|
|
371
|
+
}
|
|
372
|
+
const dirs = fs.readdirSync(sandboxDir, { withFileTypes: true })
|
|
373
|
+
.filter(d => d.isDirectory())
|
|
374
|
+
.map(d => d.name)
|
|
375
|
+
.filter(name => name !== 'current');
|
|
376
|
+
index = { sandbox_id: this.sandboxId, sessions: dirs.map(id => ({ id })) };
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
// Read actual data from each session's meta.json
|
|
380
|
+
const sessions = (index.sessions || []).map(session => {
|
|
381
|
+
const metaFile = path.join(this._getSandboxDir(), session.id, 'meta.json');
|
|
382
|
+
if (fs.existsSync(metaFile)) {
|
|
383
|
+
try {
|
|
384
|
+
try {
|
|
385
|
+
const stat = fs.statSync(metaFile);
|
|
386
|
+
session.updated_at = new Date(stat.mtimeMs).toISOString();
|
|
387
|
+
} catch {
|
|
388
|
+
// ignore
|
|
389
|
+
}
|
|
390
|
+
const meta = JSON.parse(fs.readFileSync(metaFile, 'utf8'));
|
|
391
|
+
session.command_count = meta.command_count || 0;
|
|
392
|
+
session.work_dir = meta.work_dir || null;
|
|
393
|
+
session.created_at = meta.created_at || null;
|
|
394
|
+
session.ended_at = meta.ended_at || null;
|
|
395
|
+
session.shell_session_name = meta.shell_session_name || null;
|
|
396
|
+
} catch (e) {
|
|
397
|
+
// Ignore parse errors
|
|
398
|
+
}
|
|
399
|
+
}
|
|
400
|
+
return session;
|
|
401
|
+
});
|
|
402
|
+
|
|
403
|
+
// Sort by created_at descending (newest first)
|
|
404
|
+
return sessions.sort((a, b) => {
|
|
405
|
+
const timeA = a.created_at ? new Date(a.created_at).getTime() : 0;
|
|
406
|
+
const timeB = b.created_at ? new Date(b.created_at).getTime() : 0;
|
|
407
|
+
return timeB - timeA;
|
|
408
|
+
});
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
/**
|
|
412
|
+
* Update the work_dir in current session's meta
|
|
413
|
+
* @param {string} workDir - Current working directory
|
|
414
|
+
*/
|
|
415
|
+
async updateWorkDir(workDir) {
|
|
416
|
+
const metaFile = path.join(this._getSessionDir(), 'meta.json');
|
|
417
|
+
if (fs.existsSync(metaFile)) {
|
|
418
|
+
const meta = safeJsonParse(fs.readFileSync(metaFile, 'utf8'), {});
|
|
419
|
+
meta.work_dir = workDir;
|
|
420
|
+
writeJsonAtomicSync(metaFile, meta);
|
|
421
|
+
}
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
/**
|
|
425
|
+
* Get history from a specific session
|
|
426
|
+
* @param {string} sessionId - The session ID to get history from
|
|
427
|
+
* @returns {string[]} Array of commands
|
|
428
|
+
*/
|
|
429
|
+
async getSessionHistory(sessionId) {
|
|
430
|
+
const historyFile = path.join(this._getSandboxDir(), sessionId, 'shell_history');
|
|
431
|
+
if (!fs.existsSync(historyFile)) {
|
|
432
|
+
return [];
|
|
433
|
+
}
|
|
434
|
+
const content = fs.readFileSync(historyFile, 'utf8');
|
|
435
|
+
return content.split('\n').filter(line => line.trim());
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
async close() {
|
|
439
|
+
const metaFile = path.join(this._getSessionDir(), 'meta.json');
|
|
440
|
+
if (fs.existsSync(metaFile)) {
|
|
441
|
+
const meta = safeJsonParse(fs.readFileSync(metaFile, 'utf8'), {});
|
|
442
|
+
meta.ended_at = new Date().toISOString();
|
|
443
|
+
writeJsonAtomicSync(metaFile, meta);
|
|
444
|
+
}
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
_generateSessionId() {
|
|
448
|
+
// Use timestamp prefix for sorting + short UUID suffix to avoid collision
|
|
449
|
+
const now = new Date();
|
|
450
|
+
const pad = (n) => String(n).padStart(2, '0');
|
|
451
|
+
const timestamp = `${now.getFullYear()}${pad(now.getMonth() + 1)}${pad(now.getDate())}_` +
|
|
452
|
+
`${pad(now.getHours())}${pad(now.getMinutes())}${pad(now.getSeconds())}`;
|
|
453
|
+
const shortId = crypto.randomUUID().split('-')[0]; // 8 chars
|
|
454
|
+
return `${timestamp}_${shortId}`;
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
_getSandboxDir() {
|
|
458
|
+
return path.join(this.baseDir, 'history', this.sandboxId);
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
_getSessionDir() {
|
|
462
|
+
return path.join(this._getSandboxDir(), this.sessionId);
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
async _updateIndex() {
|
|
466
|
+
const sandboxDir = this._getSandboxDir();
|
|
467
|
+
const indexFile = path.join(sandboxDir, 'index.json');
|
|
468
|
+
|
|
469
|
+
await this._withIndexLock(async () => {
|
|
470
|
+
let index = { sandbox_id: this.sandboxId, sessions: [] };
|
|
471
|
+
if (fs.existsSync(indexFile)) {
|
|
472
|
+
index = safeJsonParse(fs.readFileSync(indexFile, 'utf8'), index);
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
index.sandbox_id = index.sandbox_id || this.sandboxId;
|
|
476
|
+
index.sessions = Array.isArray(index.sessions) ? index.sessions : [];
|
|
477
|
+
index.current_session = this.sessionId;
|
|
478
|
+
|
|
479
|
+
// Avoid duplicates if init() is retried or called concurrently
|
|
480
|
+
const exists = index.sessions.some(s => s.id === this.sessionId);
|
|
481
|
+
if (!exists) {
|
|
482
|
+
index.sessions.push({
|
|
483
|
+
id: this.sessionId,
|
|
484
|
+
created_at: new Date().toISOString(),
|
|
485
|
+
ended_at: null,
|
|
486
|
+
command_count: 0,
|
|
487
|
+
});
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
writeJsonAtomicSync(indexFile, index);
|
|
491
|
+
});
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
async _updateCurrentSymlink() {
|
|
495
|
+
const currentLink = path.join(this._getSandboxDir(), 'current');
|
|
496
|
+
try {
|
|
497
|
+
if (fs.existsSync(currentLink)) {
|
|
498
|
+
fs.unlinkSync(currentLink);
|
|
499
|
+
}
|
|
500
|
+
fs.symlinkSync(this.sessionId, currentLink);
|
|
501
|
+
} catch (error) {
|
|
502
|
+
// Symlinks may not work on all systems, ignore
|
|
503
|
+
}
|
|
504
|
+
}
|
|
505
|
+
}
|
|
506
|
+
|
|
507
|
+
module.exports = HistoryManager;
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Ensure HistoryManager is pointing at the right history session for a given sandbox shell session.
|
|
5
|
+
*
|
|
6
|
+
* - If preferResume is true, try to resume an existing history session whose meta.shell_session_name
|
|
7
|
+
* matches shellSessionName.
|
|
8
|
+
* - Otherwise (or if resume fails), create a new history session and persist shellSessionName.
|
|
9
|
+
*
|
|
10
|
+
* @param {Object} params
|
|
11
|
+
* @param {Object} params.historyManager
|
|
12
|
+
* @param {string} params.shellSessionName
|
|
13
|
+
* @param {boolean} params.preferResume
|
|
14
|
+
* @returns {Promise<string|null>} history session id
|
|
15
|
+
*/
|
|
16
|
+
async function ensureHistorySession({ historyManager, shellSessionName, preferResume }) {
|
|
17
|
+
if (!historyManager) {
|
|
18
|
+
throw new Error('ensureHistorySession requires historyManager');
|
|
19
|
+
}
|
|
20
|
+
if (!shellSessionName) {
|
|
21
|
+
throw new Error('ensureHistorySession requires shellSessionName');
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
const logger = require('../../utils/logger');
|
|
25
|
+
logger.debug(`ensureHistorySession: shellSessionName=${shellSessionName}, preferResume=${preferResume}`);
|
|
26
|
+
|
|
27
|
+
if (preferResume && typeof historyManager.resumeByShellSessionName === 'function') {
|
|
28
|
+
const resumed = await historyManager.resumeByShellSessionName(shellSessionName);
|
|
29
|
+
if (resumed) {
|
|
30
|
+
logger.debug(`History session resumed: ${resumed}`);
|
|
31
|
+
return resumed;
|
|
32
|
+
}
|
|
33
|
+
logger.debug('No history session found to resume, will create new one');
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
if (typeof historyManager.init !== 'function') {
|
|
37
|
+
throw new Error('HistoryManager.init not available');
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
const created = await historyManager.init({ shellSessionName });
|
|
41
|
+
logger.debug(`History session created: ${created}`);
|
|
42
|
+
return created || historyManager.sessionId || null;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
module.exports = {
|
|
46
|
+
ensureHistorySession,
|
|
47
|
+
};
|
|
48
|
+
|