@tcb-sandbox/cli 0.3.7 → 0.3.9
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/CHANGELOG.md +4 -0
- package/README.md +59 -39
- package/dist/bundled-docs.js +9 -6
- package/dist/cli.js +9 -176
- package/dist/sandbox.js +86 -0
- package/dist/serve.js +131 -32
- package/dist/trw-embedded.js +883 -854
- package/docs/README.md +2 -2
- package/docs/local-mode.md +21 -1
- package/docs/quick-start.md +19 -2
- package/package.json +3 -3
package/dist/serve.js
CHANGED
|
@@ -3,6 +3,7 @@ import fs from "node:fs";
|
|
|
3
3
|
import os from "node:os";
|
|
4
4
|
import path from "node:path";
|
|
5
5
|
import { fileURLToPath } from "node:url";
|
|
6
|
+
import { spawnSandboxed } from "./sandbox.js";
|
|
6
7
|
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
7
8
|
function readJsonVersion(pkgPath) {
|
|
8
9
|
try {
|
|
@@ -125,7 +126,7 @@ export function defaultWorkspaceRoot() {
|
|
|
125
126
|
return path.join(os.homedir(), ".tcb-sandbox", "workspaces");
|
|
126
127
|
}
|
|
127
128
|
const MOTD_ASCII = "\n ____ _ _ ____\n / ___| | ___ _ _ __| | __ ) __ _ ___ ___\n | | | |/ _ \\| | | |/ _` | _ \\ / _` / __|/ _ \\\n | |___| | (_) | |_| | (_| | |_) | (_| \\__ \\ __/\n \\____|_|\\___/ \\__,_|\\__,_|____/ \\__,_|___/\\___|\n Remote Workspace\n";
|
|
128
|
-
export function printServeBanner(opts, trwMain) {
|
|
129
|
+
export function printServeBanner(opts, trwMain, sandboxStatus) {
|
|
129
130
|
const trwVer = trwPackageVersion(trwMain);
|
|
130
131
|
const cliVer = cliPackageVersion();
|
|
131
132
|
const displayHost = opts.host === "0.0.0.0" ? "127.0.0.1" : opts.host;
|
|
@@ -138,9 +139,13 @@ export function printServeBanner(opts, trwMain) {
|
|
|
138
139
|
lines.push(MOTD_ASCII.replace(/^\n/, "").replace(/\n$/, ""));
|
|
139
140
|
lines.push("");
|
|
140
141
|
lines.push(` ${S.dim("Listen :")} ${base}`);
|
|
141
|
-
lines.push(` ${S.dim("Session :")} ${S.bold("X-Cloudbase-Session-Id")} ${S.dim("
|
|
142
|
+
lines.push(` ${S.dim("Session :")} ${S.bold("X-Cloudbase-Session-Id")} ${S.dim("(不传时默认 default;示例 test-03271546 → 子目录 test-03271546)")}`);
|
|
142
143
|
lines.push(` ${S.dim("Workspace:")} ${wsRoot}${path.sep}${S.dim("<session-id>")}`);
|
|
143
144
|
lines.push(` ${S.dim("Disk :")} ${disk}`);
|
|
145
|
+
if (sandboxStatus) {
|
|
146
|
+
const sandboxColor = sandboxStatus.startsWith("active") ? S.green : S.yellow;
|
|
147
|
+
lines.push(` ${S.dim("Sandbox :")} ${sandboxColor(sandboxStatus)}`);
|
|
148
|
+
}
|
|
144
149
|
lines.push(` ${S.dim("Runtime :")} Node ${nodeVer} · Bun ${bunVersion()} · Python ${pythonVersion()}`);
|
|
145
150
|
lines.push(` ${S.dim("Versions :")} TCB ${trwVer} · CLI ${cliVer}`);
|
|
146
151
|
lines.push("");
|
|
@@ -154,44 +159,138 @@ export function printServeBanner(opts, trwMain) {
|
|
|
154
159
|
lines.push(` ${S.cyan("skills")} AI 技能管理${S.dim("(add / list / find)")}`);
|
|
155
160
|
lines.push(` ${S.cyan("tmux")} 多窗口${S.dim("(Ctrl-b c 新建 · Ctrl-b % 分屏)")}`);
|
|
156
161
|
lines.push("");
|
|
157
|
-
lines.push(` ${S.yellow("▸")} ${S.bold("须知")} ${S.dim("(本机 serve,非 SCF 容器)")}`);
|
|
158
|
-
lines.push(` ${S.dim("·")} 默认无鉴权;默认仅监听回环,${S.bold("--host 0.0.0.0")} 会暴露到网络,请自行评估风险`);
|
|
159
|
-
lines.push(` ${S.dim("·")} ${S.bold("Ctrl+C")} 停止本进程;无容器冻结 / TTL / 唤醒策略`);
|
|
160
|
-
lines.push(` ${S.dim("·")} 若系统 Python 受 PEP 668 限制,用 ${S.bold("uv")} ${S.dim("(推荐)")} 或 ${S.bold("python3 -m venv")}`);
|
|
161
|
-
lines.push(` ${S.dim("·")} 各 session 工作区在 ${S.bold(wsRoot + path.sep + "<session-id>")} 下;工具侧路径策略与 tcb-remote-workspace 一致`);
|
|
162
|
-
lines.push(` ${S.dim("·")} 长时间任务优先 ${S.bold("tmux")} 或 ${S.bold("tcb-sandbox pty")};子进程分离 ${S.bold("(cmd > log 2>&1 &)")} 仍可能随会话结束被清理`);
|
|
163
|
-
lines.push("");
|
|
164
162
|
process.stdout.write(lines.join("\n"));
|
|
165
163
|
}
|
|
166
|
-
|
|
167
|
-
const
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
stdio: "inherit",
|
|
172
|
-
env: {
|
|
173
|
-
...process.env,
|
|
174
|
-
HOST: opts.host,
|
|
175
|
-
PORT: String(opts.port),
|
|
176
|
-
WORKSPACE_ROOT: opts.workspaceRoot,
|
|
177
|
-
},
|
|
178
|
-
});
|
|
179
|
-
const forward = (sig) => {
|
|
164
|
+
async function waitForReady(host, port, timeoutMs) {
|
|
165
|
+
const base = `http://${host === "0.0.0.0" ? "127.0.0.1" : host}:${port}`;
|
|
166
|
+
const deadline = Date.now() + timeoutMs;
|
|
167
|
+
const interval = 300;
|
|
168
|
+
while (Date.now() < deadline) {
|
|
180
169
|
try {
|
|
181
|
-
|
|
170
|
+
const res = await fetch(`${base}/health`, { signal: AbortSignal.timeout(2000) });
|
|
171
|
+
if (res.ok)
|
|
172
|
+
return true;
|
|
182
173
|
}
|
|
183
174
|
catch {
|
|
184
175
|
}
|
|
176
|
+
await new Promise((r) => setTimeout(r, interval));
|
|
177
|
+
}
|
|
178
|
+
return false;
|
|
179
|
+
}
|
|
180
|
+
async function isPortInUse(host, port) {
|
|
181
|
+
const net = await import("node:net");
|
|
182
|
+
return new Promise((resolve) => {
|
|
183
|
+
const server = net.createServer();
|
|
184
|
+
server.once("error", (err) => {
|
|
185
|
+
resolve(err.code === "EADDRINUSE");
|
|
186
|
+
});
|
|
187
|
+
server.once("listening", () => {
|
|
188
|
+
server.close(() => resolve(false));
|
|
189
|
+
});
|
|
190
|
+
server.listen(port, host);
|
|
191
|
+
});
|
|
192
|
+
}
|
|
193
|
+
const SHUTDOWN_TIMEOUT_MS = 10_000;
|
|
194
|
+
const MAX_RESTART_COUNT = 3;
|
|
195
|
+
const MIN_UPTIME_FOR_HEALTHY_MS = 5_000;
|
|
196
|
+
function ensureMcporterConfig(workspaceRoot) {
|
|
197
|
+
const mcporterDir = path.join(os.homedir(), ".mcporter");
|
|
198
|
+
const configPath = path.join(mcporterDir, "mcporter.json");
|
|
199
|
+
if (fs.existsSync(configPath))
|
|
200
|
+
return;
|
|
201
|
+
fs.mkdirSync(mcporterDir, { recursive: true });
|
|
202
|
+
const envId = process.env.CLOUDBASE_ENV_ID ?? "";
|
|
203
|
+
const mcpBin = process.env.CLOUDBASE_MCP_BIN ?? "";
|
|
204
|
+
const servers = {};
|
|
205
|
+
if (envId && mcpBin) {
|
|
206
|
+
servers.cloudbase = {
|
|
207
|
+
command: "node",
|
|
208
|
+
args: [mcpBin],
|
|
209
|
+
env: {
|
|
210
|
+
CLOUDBASE_ENV_ID: envId,
|
|
211
|
+
TENCENTCLOUD_SECRETID: process.env.TENCENTCLOUD_SECRETID ?? "",
|
|
212
|
+
TENCENTCLOUD_SECRETKEY: process.env.TENCENTCLOUD_SECRETKEY ?? "",
|
|
213
|
+
TENCENTCLOUD_SESSIONTOKEN: process.env.TENCENTCLOUD_SESSIONTOKEN ?? "",
|
|
214
|
+
INTEGRATION_IDE: process.env.INTEGRATION_IDE ?? "codebuddy",
|
|
215
|
+
WORKSPACE_FOLDER_PATHS: process.env.WORKSPACE_FOLDER_PATHS ?? "",
|
|
216
|
+
},
|
|
217
|
+
};
|
|
218
|
+
}
|
|
219
|
+
fs.writeFileSync(configPath, JSON.stringify({ mcpServers: servers }, null, 2), "utf8");
|
|
220
|
+
}
|
|
221
|
+
function spawnTrw(trwMain, env, workspaceRoot) {
|
|
222
|
+
if (process.platform === "darwin") {
|
|
223
|
+
const result = spawnSandboxed(trwMain, env, workspaceRoot);
|
|
224
|
+
return { child: result.child, sandboxStatus: result.status };
|
|
225
|
+
}
|
|
226
|
+
const child = spawn(process.execPath, [trwMain], { stdio: "inherit", env });
|
|
227
|
+
return { child, sandboxStatus: `skipped (${process.platform})` };
|
|
228
|
+
}
|
|
229
|
+
export async function runServe(opts) {
|
|
230
|
+
const trwMain = resolveTcbEmbeddedMain();
|
|
231
|
+
fs.mkdirSync(opts.workspaceRoot, { recursive: true });
|
|
232
|
+
ensureMcporterConfig(opts.workspaceRoot);
|
|
233
|
+
if (await isPortInUse(opts.host, opts.port)) {
|
|
234
|
+
process.stderr.write(`[serve] Port ${opts.port} is already in use on ${opts.host}. Try --port ${opts.port + 1}\n`);
|
|
235
|
+
return Promise.reject(new Error(`Port ${opts.port} is already in use`));
|
|
236
|
+
}
|
|
237
|
+
const env = {
|
|
238
|
+
...process.env,
|
|
239
|
+
HOST: opts.host,
|
|
240
|
+
PORT: String(opts.port),
|
|
241
|
+
WORKSPACE_ROOT: opts.workspaceRoot,
|
|
185
242
|
};
|
|
186
|
-
|
|
187
|
-
|
|
243
|
+
let shutdownRequested = false;
|
|
244
|
+
let restartCount = 0;
|
|
245
|
+
let bannerPrinted = false;
|
|
188
246
|
return new Promise((resolve, reject) => {
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
247
|
+
function startChild() {
|
|
248
|
+
const { child, sandboxStatus } = spawnTrw(trwMain, env, opts.workspaceRoot);
|
|
249
|
+
const startedAt = Date.now();
|
|
250
|
+
if (!bannerPrinted) {
|
|
251
|
+
waitForReady(opts.host, opts.port, 30_000).then((ready) => {
|
|
252
|
+
bannerPrinted = true;
|
|
253
|
+
printServeBanner(opts, trwMain, sandboxStatus);
|
|
254
|
+
if (!ready) {
|
|
255
|
+
process.stderr.write("[serve] WARNING: TRW did not become ready within 30s. Service may not be healthy.\n");
|
|
256
|
+
}
|
|
257
|
+
});
|
|
258
|
+
}
|
|
259
|
+
const forward = (sig) => {
|
|
260
|
+
shutdownRequested = true;
|
|
261
|
+
try {
|
|
262
|
+
child.kill(sig);
|
|
263
|
+
}
|
|
264
|
+
catch {
|
|
265
|
+
}
|
|
266
|
+
setTimeout(() => {
|
|
267
|
+
process.stderr.write(`[serve] TRW did not exit within ${SHUTDOWN_TIMEOUT_MS / 1000}s after ${sig}, forcing exit.\n`);
|
|
268
|
+
process.exit(1);
|
|
269
|
+
}, SHUTDOWN_TIMEOUT_MS).unref();
|
|
270
|
+
};
|
|
271
|
+
process.removeAllListeners("SIGINT");
|
|
272
|
+
process.removeAllListeners("SIGTERM");
|
|
273
|
+
process.on("SIGINT", () => forward("SIGINT"));
|
|
274
|
+
process.on("SIGTERM", () => forward("SIGTERM"));
|
|
275
|
+
child.on("error", reject);
|
|
276
|
+
child.on("close", (code, _signal) => {
|
|
277
|
+
if (shutdownRequested) {
|
|
278
|
+
resolve(code);
|
|
279
|
+
return;
|
|
280
|
+
}
|
|
281
|
+
const uptime = Date.now() - startedAt;
|
|
282
|
+
if (code !== 0 && restartCount < MAX_RESTART_COUNT && uptime >= MIN_UPTIME_FOR_HEALTHY_MS) {
|
|
283
|
+
restartCount++;
|
|
284
|
+
process.stderr.write(`[serve] TRW exited with code ${code} after ${Math.round(uptime / 1000)}s. Restarting (${restartCount}/${MAX_RESTART_COUNT})...\n`);
|
|
285
|
+
startChild();
|
|
286
|
+
return;
|
|
287
|
+
}
|
|
288
|
+
if (code !== 0 && restartCount >= MAX_RESTART_COUNT) {
|
|
289
|
+
process.stderr.write(`[serve] TRW crashed ${MAX_RESTART_COUNT} times. Giving up.\n`);
|
|
290
|
+
}
|
|
194
291
|
resolve(code);
|
|
195
|
-
|
|
292
|
+
});
|
|
293
|
+
}
|
|
294
|
+
startChild();
|
|
196
295
|
});
|
|
197
296
|
}
|