rl-rockcli 0.0.10 → 0.0.11
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.js +186 -0
- package/package.json +1 -1
|
@@ -0,0 +1,186 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const logger = require('../utils/logger');
|
|
4
|
+
const { resolveSandboxId } = require('./attach/sandbox-id-resolver');
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Prompt user for sandbox ID using terminal-kit interactive UI
|
|
8
|
+
* @returns {Promise<string|null>} The sandbox ID entered by user, or null if cancelled
|
|
9
|
+
*/
|
|
10
|
+
async function promptForSandboxId() {
|
|
11
|
+
const term = require('terminal-kit').terminal;
|
|
12
|
+
|
|
13
|
+
// Clear screen and show welcome
|
|
14
|
+
term.clear();
|
|
15
|
+
term.cyan('┌─────────────────────────────────────────────────────────────┐\n');
|
|
16
|
+
term.cyan('│ │\n');
|
|
17
|
+
term.cyan('│ ');
|
|
18
|
+
term.bold.white('🪨 ROCK CLI - Sandbox Attach');
|
|
19
|
+
term.cyan(' │\n');
|
|
20
|
+
term.cyan('│ │\n');
|
|
21
|
+
term.cyan('└─────────────────────────────────────────────────────────────┘\n');
|
|
22
|
+
|
|
23
|
+
// Check for recent sandboxes from history
|
|
24
|
+
const fs = require('fs');
|
|
25
|
+
const path = require('path');
|
|
26
|
+
const os = require('os');
|
|
27
|
+
const historyDir = path.join(os.homedir(), '.rock', 'history');
|
|
28
|
+
let recentSandboxes = [];
|
|
29
|
+
|
|
30
|
+
if (fs.existsSync(historyDir)) {
|
|
31
|
+
try {
|
|
32
|
+
const dirs = fs.readdirSync(historyDir)
|
|
33
|
+
.filter(d => fs.statSync(path.join(historyDir, d)).isDirectory() && d !== 'current')
|
|
34
|
+
.map(d => ({
|
|
35
|
+
name: d,
|
|
36
|
+
mtime: fs.statSync(path.join(historyDir, d)).mtimeMs,
|
|
37
|
+
}))
|
|
38
|
+
.sort((a, b) => b.mtime - a.mtime) // Sort by most recent first
|
|
39
|
+
.slice(0, 5)
|
|
40
|
+
.map(d => d.name);
|
|
41
|
+
recentSandboxes = dirs;
|
|
42
|
+
} catch (e) {
|
|
43
|
+
// Ignore errors reading history
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// Show recent sandboxes if any
|
|
48
|
+
if (recentSandboxes.length > 0) {
|
|
49
|
+
term.gray(' 最近使用的 Sandbox:\n');
|
|
50
|
+
recentSandboxes.forEach((id, index) => {
|
|
51
|
+
term.gray(` ${index + 1}. `);
|
|
52
|
+
term.white(id);
|
|
53
|
+
term('\n');
|
|
54
|
+
});
|
|
55
|
+
term.gray(' 输入序号选择,或直接输入新的 Sandbox ID\n\n');
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// Prompt for input
|
|
59
|
+
term.bold(' Sandbox ID: ');
|
|
60
|
+
|
|
61
|
+
return new Promise((resolve) => {
|
|
62
|
+
let resolved = false;
|
|
63
|
+
function finish(val) {
|
|
64
|
+
if (resolved) return;
|
|
65
|
+
resolved = true;
|
|
66
|
+
if (typeof term.removeListener === 'function') {
|
|
67
|
+
term.removeListener('key', onKey);
|
|
68
|
+
} else if (typeof term.off === 'function') {
|
|
69
|
+
term.off('key', onKey);
|
|
70
|
+
}
|
|
71
|
+
resolve(val);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function onKey(name) {
|
|
75
|
+
if (name === 'CTRL_C') {
|
|
76
|
+
term('\n\n');
|
|
77
|
+
term.gray(' 已取消\n');
|
|
78
|
+
finish(null);
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
term.on('key', onKey);
|
|
83
|
+
|
|
84
|
+
term.inputField({
|
|
85
|
+
cancelable: true,
|
|
86
|
+
history: recentSandboxes,
|
|
87
|
+
autoComplete: recentSandboxes,
|
|
88
|
+
autoCompleteHint: true,
|
|
89
|
+
autoCompleteMenu: recentSandboxes.length > 0,
|
|
90
|
+
}, (error, input) => {
|
|
91
|
+
term('\n');
|
|
92
|
+
|
|
93
|
+
if (error || input === undefined) {
|
|
94
|
+
term.gray(' 已取消\n');
|
|
95
|
+
finish(null);
|
|
96
|
+
return;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
const trimmed = input.trim();
|
|
100
|
+
|
|
101
|
+
// Check if user entered a number to select from recent
|
|
102
|
+
if (recentSandboxes.length > 0 && /^[1-5]$/.test(trimmed)) {
|
|
103
|
+
const index = parseInt(trimmed) - 1;
|
|
104
|
+
if (index < recentSandboxes.length) {
|
|
105
|
+
finish(recentSandboxes[index]);
|
|
106
|
+
return;
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
finish(trimmed);
|
|
111
|
+
});
|
|
112
|
+
});
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
module.exports = {
|
|
116
|
+
command: 'attach [sandbox-id]',
|
|
117
|
+
describe: 'Connect to a sandbox in interactive REPL mode',
|
|
118
|
+
builder: (yargs) => {
|
|
119
|
+
return yargs
|
|
120
|
+
.positional('sandbox-id', {
|
|
121
|
+
describe: 'The sandbox ID to connect to (will prompt if not provided)',
|
|
122
|
+
type: 'string',
|
|
123
|
+
})
|
|
124
|
+
.option('verbose', {
|
|
125
|
+
alias: 'v',
|
|
126
|
+
type: 'count',
|
|
127
|
+
description: 'Verbosity level',
|
|
128
|
+
})
|
|
129
|
+
.option('api-key', {
|
|
130
|
+
describe: 'API key for authentication',
|
|
131
|
+
type: 'string',
|
|
132
|
+
})
|
|
133
|
+
.option('cluster', {
|
|
134
|
+
describe: 'Target cluster',
|
|
135
|
+
type: 'string',
|
|
136
|
+
})
|
|
137
|
+
.option('base-url', {
|
|
138
|
+
describe: 'Base URL for the API',
|
|
139
|
+
type: 'string',
|
|
140
|
+
})
|
|
141
|
+
.option('session', {
|
|
142
|
+
alias: 's',
|
|
143
|
+
describe: 'Resume a specific session by ID (preserves env vars, cwd, etc.)',
|
|
144
|
+
type: 'string',
|
|
145
|
+
})
|
|
146
|
+
.option('ui', {
|
|
147
|
+
describe: 'Attach UI mode: ink|basic|opentui (default: opentui; env: ROCK_ATTACH_UI)',
|
|
148
|
+
type: 'string',
|
|
149
|
+
choices: ['ink', 'basic', 'opentui'],
|
|
150
|
+
});
|
|
151
|
+
},
|
|
152
|
+
handler: async (argv) => {
|
|
153
|
+
const logger = require('../utils/logger');
|
|
154
|
+
logger.debug(`attach handler received argv.session=${argv.session}, argv.s=${argv.s}`);
|
|
155
|
+
logger.debug(`Full argv keys: ${Object.keys(argv).join(', ')}`);
|
|
156
|
+
|
|
157
|
+
let sandboxId = argv.sandboxId;
|
|
158
|
+
|
|
159
|
+
// If sandbox-id not provided, prompt interactively with terminal-kit UI
|
|
160
|
+
if (!sandboxId) {
|
|
161
|
+
sandboxId = await promptForSandboxId();
|
|
162
|
+
if (!sandboxId) {
|
|
163
|
+
process.exitCode = 0;
|
|
164
|
+
return;
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
// Resolve abbreviated sandbox ID to full ID
|
|
169
|
+
try {
|
|
170
|
+
sandboxId = await resolveSandboxId(sandboxId);
|
|
171
|
+
} catch (error) {
|
|
172
|
+
logger.error(error.message);
|
|
173
|
+
process.exitCode = 1;
|
|
174
|
+
return;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
const { SandboxREPL } = require('./attach/repl');
|
|
178
|
+
const repl = new SandboxREPL(sandboxId, argv);
|
|
179
|
+
try {
|
|
180
|
+
await repl.start();
|
|
181
|
+
} catch (e) {
|
|
182
|
+
logger.error(e && e.message ? e.message : String(e));
|
|
183
|
+
process.exitCode = e && typeof e.exitCode === 'number' ? e.exitCode : 1;
|
|
184
|
+
}
|
|
185
|
+
},
|
|
186
|
+
};
|