@tcb-sandbox/cli 0.3.9 → 0.4.1

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/README.md CHANGED
@@ -32,8 +32,8 @@ tcb-sandbox --session-id my-session read --path hello.txt
32
32
  ## 环境变量
33
33
 
34
34
  ```bash
35
- export TRW_ENDPOINT=https://your-gateway.com
36
- export TRW_SESSION_ID=your-session-id
35
+ export TCB_SANDBOX_ENDPOINT=https://your-gateway.com
36
+ export TCB_SANDBOX_SESSION_ID=your-session-id
37
37
 
38
38
  # 然后可直接使用
39
39
  tcb-sandbox health
@@ -60,6 +60,12 @@ tcb-sandbox bash --command "ls -la"
60
60
  | `pty` | PTY 终端 |
61
61
  | `snapshot` | 快照管理 |
62
62
 
63
+ ## 测试说明
64
+
65
+ - `pnpm test`:CLI 契约回归(mock HTTP)
66
+ - 不包含:`serve` 真实内嵌 TRW 行为验证
67
+ - 跨门面(HTTP/MCP/CLI/E2B)完整矩阵请使用 sibling `../trw-example` 的 `pnpm start` 或 `pnpm test:full`
68
+
63
69
  ## 本地模式详情
64
70
 
65
71
  ```bash
@@ -486,46 +486,15 @@ export const BUNDLED_API_DOCS = {
486
486
  notes: ["Bundled fallback docs snapshot."],
487
487
  },
488
488
  {
489
- name: "workspace_snapshot",
490
- description: "Create async workspace snapshot uploaded to S3. Returns snapshot_id immediately.",
491
- endpoint: "POST /api/tools/workspace_snapshot",
489
+ name: "workspace_persist",
490
+ description: "Immediately persist workspace to COS via rsync. Returns elapsed time in ms.",
491
+ endpoint: "POST /api/tools/workspace_persist",
492
492
  input_schema: {
493
493
  type: "object",
494
- properties: {
495
- exclude: { type: "array", items: { type: "string" } },
496
- include_all: { type: "boolean" },
497
- },
498
- },
499
- limits: { max_concurrent: 2 },
500
- notes: ["Bundled fallback docs snapshot."],
501
- },
502
- {
503
- name: "workspace_snapshot_status",
504
- description: "Check snapshot task status by snapshot_id or list all session tasks.",
505
- endpoint: "POST /api/tools/workspace_snapshot_status",
506
- input_schema: {
507
- type: "object",
508
- properties: {
509
- snapshot_id: { type: "string" },
510
- },
494
+ properties: {},
511
495
  },
512
496
  limits: {},
513
- notes: ["Bundled fallback docs snapshot."],
514
- },
515
- {
516
- name: "workspace_restore",
517
- description: 'Restore workspace from snapshot. mode="merge" overlays, mode="replace" cleans first.',
518
- endpoint: "POST /api/tools/workspace_restore",
519
- input_schema: {
520
- type: "object",
521
- required: ["snapshot_id"],
522
- properties: {
523
- snapshot_id: { type: "string" },
524
- mode: { type: "string", enum: ["merge", "replace"] },
525
- },
526
- },
527
- limits: {},
528
- notes: ["Bundled fallback docs snapshot."],
497
+ notes: ["Bundled fallback docs."],
529
498
  },
530
499
  ],
531
500
  };
package/dist/cli.js CHANGED
@@ -5,9 +5,8 @@ import process from "node:process";
5
5
  import { TcbSandboxClient } from "@tcb-sandbox/sdk-js";
6
6
  import { Command } from "commander";
7
7
  import { BUNDLED_API_DOCS } from "./bundled-docs.js";
8
- import { defaultWorkspaceRoot, runServe } from "./serve.js";
8
+ import { defaultMultiSessionWorkspaceRoot, defaultWorkspaceRoot, runServe } from "./serve.js";
9
9
  const DEFAULT_TIMEOUT = 600_000;
10
- const DEFAULT_ACCEPT_HEADER = "application/problem+json, application/json;q=0.9, text/markdown;q=0.8, */*;q=0.1";
11
10
  const LOG_PRIORITIES = {
12
11
  debug: 10,
13
12
  info: 20,
@@ -92,17 +91,6 @@ function resolveLogFormat(opts) {
92
91
  function shouldLog(ctx, level) {
93
92
  return LOG_PRIORITIES[level] >= LOG_PRIORITIES[ctx.logLevel];
94
93
  }
95
- function maskHeaderValue(key, value) {
96
- const lower = key.toLowerCase();
97
- if (lower.includes("authorization") ||
98
- lower.includes("api-key") ||
99
- lower.includes("token") ||
100
- lower.includes("cookie") ||
101
- lower.includes("session")) {
102
- return "***";
103
- }
104
- return value;
105
- }
106
94
  const SENSITIVE_JSON_KEY_RE = /(authorization|api[_-]?key|token|secret|password|cookie|session|value)/i;
107
95
  function redactJsonForDebug(input) {
108
96
  if (Array.isArray(input)) {
@@ -262,7 +250,7 @@ Quick Start
262
250
  tcb-sandbox serve # alias: local — embedded TRW on loopback
263
251
  tcb-sandbox --endpoint http://127.0.0.1:9000 health
264
252
  tcb-sandbox --endpoint http://127.0.0.1:9000 docs
265
- tcb-sandbox --endpoint http://127.0.0.1:9000 --session-id demo tools call read --param path=README.md
253
+ tcb-sandbox --endpoint http://127.0.0.1:9000 --session-id demo read README.md
266
254
 
267
255
  Error Handling
268
256
  HTTP failures are parsed as RFC 9457 problem details when available.
@@ -271,17 +259,23 @@ Error Handling
271
259
  program
272
260
  .command("serve")
273
261
  .alias("local")
274
- .description("Run embedded tcb-remote-workspace locally (multi-session; X-Cloudbase-Session-Id; no auth).")
262
+ .description("Run embedded tcb-remote-workspace locally. Default: single-workspace (no session header needed). Use --multi-session for session-based subdirectories.")
275
263
  .option("--port <n>", "Listen port", "9000")
276
264
  .option("--host <addr>", "Bind address (use 0.0.0.0 for all interfaces)", "127.0.0.1")
277
- .option("--workspace-root <path>", "Parent directory; each session is a subdirectory by session id", defaultWorkspaceRoot())
265
+ .option("--workspace-root <path>", "Workspace directory (single mode) or parent directory for session subdirectories (multi-session mode)")
266
+ .option("--multi-session", "Enable multi-session mode: each X-Cloudbase-Session-Id creates a subdirectory under workspace-root", false)
278
267
  .action(async function (opts) {
279
268
  const port = asInt(opts.port);
280
- const workspaceRoot = path.resolve(opts.workspaceRoot);
269
+ const resolvedRoot = opts.workspaceRoot
270
+ ? path.resolve(opts.workspaceRoot)
271
+ : opts.multiSession
272
+ ? defaultMultiSessionWorkspaceRoot()
273
+ : defaultWorkspaceRoot();
281
274
  const code = await runServe({
282
275
  host: opts.host,
283
276
  port,
284
- workspaceRoot,
277
+ workspaceRoot: resolvedRoot,
278
+ multiSession: opts.multiSession,
285
279
  });
286
280
  process.exit(code === null ? 0 : code);
287
281
  });
@@ -310,7 +304,7 @@ Error Handling
310
304
  const response = await client.system.apiDocs();
311
305
  docs = ("data" in response ? response.data : response);
312
306
  }
313
- catch (err) {
307
+ catch (_err) {
314
308
  logMessage(ctx, "warn", "GET /api/docs unavailable, using bundled fallback docs");
315
309
  docs = BUNDLED_API_DOCS;
316
310
  }
@@ -731,61 +725,27 @@ Error Handling
731
725
  return printJson(result);
732
726
  printPretty("Capability Invoke", result);
733
727
  });
734
- const snapshot = program
735
- .command("snapshot")
736
- .description("Workspace snapshot and restore (S3-backed)");
737
- snapshot
738
- .command("create")
739
- .description("Create async workspace snapshot and upload to S3")
740
- .option("--exclude <patterns...>", "Additional exclude patterns")
741
- .option("--include-all", "Skip default excludes (only sensitive files still excluded)")
742
- .action(async function (options) {
743
- const ctx = buildContext(this);
744
- ensureSessionId(ctx);
745
- const client = createSdkClientFromContext(ctx);
746
- logMessage(ctx, "info", "POST /api/tools/workspace_snapshot");
747
- const response = await client.other.workspaceSnapshot({
748
- exclude: options.exclude,
749
- include_all: options.includeAll,
750
- });
751
- const result = "data" in response ? response.data : response;
752
- if (ctx.output === "json")
753
- return printJson(result);
754
- printPretty("Snapshot Create", result);
755
- });
756
- snapshot
757
- .command("status [snapshotId]")
758
- .description("Check snapshot status (single or all session tasks)")
759
- .action(async function (snapshotId) {
760
- const ctx = buildContext(this);
761
- ensureSessionId(ctx);
762
- const client = createSdkClientFromContext(ctx);
763
- logMessage(ctx, "info", "POST /api/tools/workspace_snapshot_status");
764
- const response = await client.other.workspaceSnapshotStatus({
765
- snapshot_id: snapshotId,
766
- });
767
- const result = "data" in response ? response.data : response;
768
- if (ctx.output === "json")
769
- return printJson(result);
770
- printPretty("Snapshot Status", result);
771
- });
772
- snapshot
773
- .command("restore <snapshotId>")
774
- .description("Restore workspace from snapshot (supports cross-session)")
775
- .option("--mode <mode>", 'Restore mode: "merge" (default) or "replace"', "merge")
776
- .action(async function (snapshotId, options) {
728
+ program
729
+ .command("persist")
730
+ .description("Persist workspace to COS storage immediately (rsync)")
731
+ .action(async function () {
777
732
  const ctx = buildContext(this);
778
733
  ensureSessionId(ctx);
779
- const client = createSdkClientFromContext(ctx);
780
- logMessage(ctx, "info", "POST /api/tools/workspace_restore");
781
- const response = await client.other.workspaceRestore({
782
- snapshot_id: snapshotId,
783
- mode: options.mode,
734
+ logMessage(ctx, "info", "POST /api/tools/workspace_persist");
735
+ const url = `${ctx.endpoint}/api/tools/workspace_persist`;
736
+ const response = await fetch(url, {
737
+ method: "POST",
738
+ headers: {
739
+ "Content-Type": "application/json",
740
+ ...ctx.headers,
741
+ },
742
+ body: JSON.stringify({}),
743
+ signal: AbortSignal.timeout(ctx.timeout),
784
744
  });
785
- const result = "data" in response ? response.data : response;
745
+ const result = await response.json();
786
746
  if (ctx.output === "json")
787
747
  return printJson(result);
788
- printPretty("Snapshot Restore", result);
748
+ printPretty("Persist", result);
789
749
  });
790
750
  const secrets = program.command("secrets").description("Secrets helpers (session-scoped)");
791
751
  secrets
package/dist/serve.js CHANGED
@@ -123,6 +123,9 @@ function pythonVersion() {
123
123
  return m ? m[1] : line.replace(/^Python\s+/i, "") || "?";
124
124
  }
125
125
  export function defaultWorkspaceRoot() {
126
+ return path.join(os.homedir(), ".tcb-sandbox", "workspace");
127
+ }
128
+ export function defaultMultiSessionWorkspaceRoot() {
126
129
  return path.join(os.homedir(), ".tcb-sandbox", "workspaces");
127
130
  }
128
131
  const MOTD_ASCII = "\n ____ _ _ ____\n / ___| | ___ _ _ __| | __ ) __ _ ___ ___\n | | | |/ _ \\| | | |/ _` | _ \\ / _` / __|/ _ \\\n | |___| | (_) | |_| | (_| | |_) | (_| \\__ \\ __/\n \\____|_|\\___/ \\__,_|\\__,_|____/ \\__,_|___/\\___|\n Remote Workspace\n";
@@ -139,8 +142,14 @@ export function printServeBanner(opts, trwMain, sandboxStatus) {
139
142
  lines.push(MOTD_ASCII.replace(/^\n/, "").replace(/\n$/, ""));
140
143
  lines.push("");
141
144
  lines.push(` ${S.dim("Listen :")} ${base}`);
142
- lines.push(` ${S.dim("Session :")} ${S.bold("X-Cloudbase-Session-Id")} ${S.dim("(不传时默认 default;示例 test-03271546 → 子目录 test-03271546)")}`);
143
- lines.push(` ${S.dim("Workspace:")} ${wsRoot}${path.sep}${S.dim("<session-id>")}`);
145
+ if (opts.multiSession) {
146
+ lines.push(` ${S.dim("Session :")} ${S.bold("X-Cloudbase-Session-Id")} ${S.dim("(multi-session mode; each session → subdirectory)")}`);
147
+ lines.push(` ${S.dim("Workspace:")} ${wsRoot}${path.sep}${S.dim("<session-id>")}`);
148
+ }
149
+ else {
150
+ lines.push(` ${S.dim("Session :")} ${S.dim("(single-workspace mode; no session header needed)")}`);
151
+ lines.push(` ${S.dim("Workspace:")} ${wsRoot}`);
152
+ }
144
153
  lines.push(` ${S.dim("Disk :")} ${disk}`);
145
154
  if (sandboxStatus) {
146
155
  const sandboxColor = sandboxStatus.startsWith("active") ? S.green : S.yellow;
@@ -193,7 +202,7 @@ async function isPortInUse(host, port) {
193
202
  const SHUTDOWN_TIMEOUT_MS = 10_000;
194
203
  const MAX_RESTART_COUNT = 3;
195
204
  const MIN_UPTIME_FOR_HEALTHY_MS = 5_000;
196
- function ensureMcporterConfig(workspaceRoot) {
205
+ function ensureMcporterConfig(_workspaceRoot) {
197
206
  const mcporterDir = path.join(os.homedir(), ".mcporter");
198
207
  const configPath = path.join(mcporterDir, "mcporter.json");
199
208
  if (fs.existsSync(configPath))
@@ -239,6 +248,8 @@ export async function runServe(opts) {
239
248
  HOST: opts.host,
240
249
  PORT: String(opts.port),
241
250
  WORKSPACE_ROOT: opts.workspaceRoot,
251
+ WORKSPACE_MODE: opts.multiSession ? "multi" : "single",
252
+ LOCAL_MODE: "true",
242
253
  };
243
254
  let shutdownRequested = false;
244
255
  let restartCount = 0;