@xfey/tutti 0.1.32 → 0.1.34

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.
Files changed (45) hide show
  1. package/README.md +5 -4
  2. package/dist/server-shell/cli/args.d.ts +10 -0
  3. package/dist/server-shell/cli/args.js +40 -1
  4. package/dist/server-shell/cli/cli.js +136 -5
  5. package/dist/server-shell/cli/errors.d.ts +1 -1
  6. package/dist/server-shell/cli/host-runtime-endpoint.d.ts +1 -0
  7. package/dist/server-shell/cli/host-runtime-endpoint.js +10 -5
  8. package/dist/server-shell/cli/host-server-runtime.js +2 -10
  9. package/dist/server-shell/cli/machine-project-inspector.d.ts +1 -0
  10. package/dist/server-shell/cli/machine-project-inspector.js +1 -0
  11. package/dist/server-shell/cli/managed-host.d.ts +1 -0
  12. package/dist/server-shell/cli/managed-host.js +6 -4
  13. package/dist/server-shell/cli/runtime-commands.d.ts +1 -0
  14. package/dist/server-shell/cli/runtime-commands.js +5 -1
  15. package/dist/server-shell/http/routes/project-api/project-timeline-projection.js +4 -4
  16. package/dist/server-shell/http/static-web.d.ts +1 -0
  17. package/dist/server-shell/http/static-web.js +14 -1
  18. package/dist/server-shell/local-console/browser-open.d.ts +5 -0
  19. package/dist/server-shell/local-console/browser-open.js +40 -0
  20. package/dist/server-shell/local-console/folder-picker.d.ts +20 -0
  21. package/dist/server-shell/local-console/folder-picker.js +78 -0
  22. package/dist/server-shell/local-console/index.d.ts +4 -0
  23. package/dist/server-shell/local-console/index.js +4 -0
  24. package/dist/server-shell/local-console/invocation-context.d.ts +21 -0
  25. package/dist/server-shell/local-console/invocation-context.js +56 -0
  26. package/dist/server-shell/local-console/lifecycle-lock.d.ts +13 -0
  27. package/dist/server-shell/local-console/lifecycle-lock.js +135 -0
  28. package/dist/server-shell/local-console/managed-console.d.ts +47 -0
  29. package/dist/server-shell/local-console/managed-console.js +312 -0
  30. package/dist/server-shell/local-console/project-service.d.ts +61 -0
  31. package/dist/server-shell/local-console/project-service.js +262 -0
  32. package/dist/server-shell/local-console/runtime-endpoint.d.ts +35 -0
  33. package/dist/server-shell/local-console/runtime-endpoint.js +90 -0
  34. package/dist/server-shell/local-console/server.d.ts +29 -0
  35. package/dist/server-shell/local-console/server.js +408 -0
  36. package/dist/server-shell/local-console/session.d.ts +28 -0
  37. package/dist/server-shell/local-console/session.js +168 -0
  38. package/package.json +1 -1
  39. package/web/assets/index-B84rQ4YJ.js +29 -0
  40. package/web/assets/index-C7RDpM1L.css +1 -0
  41. package/web/assets/tutti_avatar-BBhaZGi3.png +0 -0
  42. package/web/index.html +6 -3
  43. package/web/assets/index-17FuaN3j.js +0 -29
  44. package/web/assets/index-C3nAJcU3.css +0 -1
  45. package/web/assets/tutti_avatar-DAVuzlig.png +0 -0
package/README.md CHANGED
@@ -2,11 +2,12 @@
2
2
 
3
3
  ## 作用
4
4
 
5
- `apps/server` 是 Tutti 的项目 host service
5
+ `apps/server` 是 Tutti 的项目 host service,也是机器级 Local Console 的进程入口。
6
6
 
7
7
  它负责:
8
8
 
9
9
  - `tutti launch` 后的项目绑定、恢复和接管入口
10
+ - 裸 `tutti` 经 lifecycle lock 启动 / 复用的版本感知 loopback Local Console、独立 invocation context,以及既有项目 Host 编排
10
11
  - Fastify API、SSE、packaged static Web 产物服务和 server 生命周期
11
12
  - SQLite store、migrations、repository 与协作真相读写
12
13
  - `control-plane`、scheduler、`procedure-engine`、`run-engine`、`workspace-ops` 与 OpenAI provider 接入
@@ -24,10 +25,10 @@
24
25
 
25
26
  ## 当前状态
26
27
 
27
- - 已具备 Fastify host shell、后台 host manager CLI 套件、Dev Runner、Host Project API、SSE、SQLite store、collaboration-state、workspace read-state、project timeline observability helper、Control Plane、Procedure / Run、workspace-ops、checks、Relay staged upload finalize、user skills 文件服务、artifacts 可视化产物查看层和 OpenAI / Codex provider 接入。
28
- - Host server 支持在显式 `webStatic.root` 或 packaged `web/` 存在时服务 Web 静态产物;`/api/*`、SSE、healthhost-local control 不被 SPA fallback 吞掉。
28
+ - 已具备 Fastify host shell、后台 host manager CLI 套件、带 `service status / stop / restart`、卸载前 `shutdown`、并发启动锁、版本替换、独立 browser context、idle / package update cleanup 与私有日志的机器级 Local Console、Dev Runner、Host Project API、SSE、SQLite store、collaboration-state、workspace read-state、project timeline observability helper、Control Plane、Procedure / Run、workspace-ops、checks、Relay staged upload finalize、user skills 文件服务、artifacts 可视化产物查看层和 OpenAI / Codex provider 接入。
29
+ - Host server 与 Local Console 支持在显式 `webStatic.root` 或 packaged `web/` 存在时服务 Web 静态产物;`/api/*`、SSE、healthhost-local control 和 `/local-console/*` API 不被 SPA fallback 吞掉。
29
30
  - Root `pack:dry-run` 会先清理 build output、重新构建,再复制 `apps/web/dist` 到 package-local `web/`,生成临时 staging `@xfey/tutti` artifact,并校验 npm pack 文件列表包含 CLI、migrations、prompts、Web index、bundled `@tutti/shared` / `@tutti/relay-client` 以及它们的外部 runtime dependencies,同时排除源码、source map、日志、SQLite、`.env`、`.tutti-dev` 与测试产物。
30
- - Root `smoke:packed-cli` 同样从干净 build output 生成 staging `@xfey/tutti` tarball、在临时目录安装,并验证 SQLite native binding、`tutti --help` / `tutti launch --help` / `tutti archive --help` / `tutti --version`、migrations、prompts、Web static index、bundled internal dependencies、bundled external runtime dependencies、CLI QR dependency、仓库锁定的 `@openai/codex` binary 可执行性和临时全局安装路径;该 smoke 不需要 Tutti source repo 存在。
31
+ - Root `smoke:packed-cli` 同样从干净 build output 生成 staging `@xfey/tutti` tarball、在临时目录安装,并验证 SQLite native binding、`tutti --help` / `tutti service --help` / `tutti service status` / `tutti shutdown --help` / `tutti launch --help` / `tutti archive --help` / `tutti --version`、migrations、prompts、Web static index、bundled internal dependencies、bundled external runtime dependencies、CLI QR dependency、仓库锁定的 `@openai/codex` binary 可执行性和临时全局安装路径;该 smoke 不需要 Tutti source repo 存在。
31
32
  - `prompts/` 是所有模型-facing prompt、指令模板和 Codex 执行模板的统一落点;长 prompt 不内联在 TypeScript 中。
32
33
  - Host Project API 只信任 Relay tunnel metadata 注入的 `relay_session_context`;direct loopback 缺少可信 context 时不作为浏览器工作区入口。
33
34
  - 详细模块落点见 `src/README.md`;logger redaction、command idempotency、schema、provider 与 Codex 边界以 `docs/system/` 对应 canonical 文档为准。
@@ -1,8 +1,16 @@
1
1
  export type CliCommand = {
2
+ kind: "console";
3
+ } | {
2
4
  kind: "help";
3
5
  topic?: string;
4
6
  } | {
5
7
  kind: "version";
8
+ } | {
9
+ kind: "service";
10
+ action: "status" | "stop" | "restart";
11
+ } | {
12
+ kind: "shutdown";
13
+ yes: boolean;
6
14
  } | {
7
15
  kind: "launch";
8
16
  target?: string;
@@ -12,6 +20,8 @@ export type CliCommand = {
12
20
  kind: "internal-host-run";
13
21
  workspacePath: string;
14
22
  yes: boolean;
23
+ } | {
24
+ kind: "internal-console-run";
15
25
  } | {
16
26
  kind: "ps";
17
27
  manage: boolean;
@@ -49,12 +49,45 @@ function readOptionalTarget(args) {
49
49
  export function parseCliArgs(argv, cwd = process.cwd()) {
50
50
  const args = [...argv];
51
51
  const command = args.shift();
52
- if (command === undefined || command === "--help" || command === "-h" || command === "help") {
52
+ if (command === undefined) {
53
+ return { kind: "console" };
54
+ }
55
+ if (command === "--help" || command === "-h" || command === "help") {
53
56
  return { kind: "help", ...(args[0] === undefined ? {} : { topic: args[0] }) };
54
57
  }
55
58
  if (command === "--version" || command === "-v" || command === "version") {
56
59
  return { kind: "version" };
57
60
  }
61
+ if (command === "service") {
62
+ const action = args.shift();
63
+ if (action === undefined || isHelpArg(action)) {
64
+ return { kind: "help", topic: "service" };
65
+ }
66
+ if (args.length > 0) {
67
+ if (hasHelpArg(args)) {
68
+ return { kind: "help", topic: "service" };
69
+ }
70
+ throw new LaunchError("git_bootstrap_failed", `Unsupported service option: ${args[0] ?? ""}`, "Run `tutti service --help` to see supported service commands.");
71
+ }
72
+ if (action !== "status" && action !== "stop" && action !== "restart") {
73
+ throw new LaunchError("git_bootstrap_failed", `Unsupported service command: ${action}`, "Run `tutti service --help` to see supported service commands.");
74
+ }
75
+ return { kind: "service", action };
76
+ }
77
+ if (command === "shutdown") {
78
+ let yes = false;
79
+ for (const arg of args) {
80
+ if (arg === "--yes") {
81
+ yes = true;
82
+ continue;
83
+ }
84
+ if (isHelpArg(arg)) {
85
+ return { kind: "help", topic: "shutdown" };
86
+ }
87
+ throw new LaunchError("git_bootstrap_failed", `Unsupported shutdown option: ${arg}`, "Run `tutti shutdown [--yes]`.");
88
+ }
89
+ return { kind: "shutdown", yes };
90
+ }
58
91
  if (command === "launch") {
59
92
  let target;
60
93
  let yes = false;
@@ -83,6 +116,12 @@ export function parseCliArgs(argv, cwd = process.cwd()) {
83
116
  }
84
117
  if (command === "internal") {
85
118
  const internalCommand = args.shift();
119
+ if (internalCommand === "console-run") {
120
+ if (args.length > 0) {
121
+ throw new LaunchError("git_bootstrap_failed", "Internal console-run does not accept options", "Internal commands are managed by Tutti.");
122
+ }
123
+ return { kind: "internal-console-run" };
124
+ }
86
125
  if (internalCommand !== "host-run") {
87
126
  throw new LaunchError("git_bootstrap_failed", "Unsupported internal command", "Internal commands are managed by Tutti.");
88
127
  }
@@ -1,28 +1,36 @@
1
1
  #!/usr/bin/env node
2
- import { formatCliError } from "./errors.js";
2
+ import { formatCliError, formatCliErrorReason, LaunchError } from "./errors.js";
3
3
  import { parseCliArgs } from "./args.js";
4
4
  import { confirmTextFromTty, runBackgroundLaunchCommand, runForegroundLaunchCommand, runInternalHostRunCommand, } from "./launch-command.js";
5
5
  import { resolveExistingProjectContext } from "./project-resolver.js";
6
6
  import { runProviderSetupTui } from "./provider-tui.js";
7
- import { runArchiveCommand, runDoctorCommand, runForgetCommand, runInviteCommand, runLogsCommand, runProviderStatusCommand, runPsManageCommand, runPsCommand, runStopCommand, } from "./runtime-commands.js";
7
+ import { runArchiveCommand, runDoctorCommand, runForgetCommand, runInviteCommand, runLogsCommand, runProviderStatusCommand, runPsManageCommand, runPsCommand, runStopCommand, listRuntimeProjects, } from "./runtime-commands.js";
8
8
  import { readCliVersion } from "./version.js";
9
+ import { ensureLocalConsole, openLocalConsoleBrowser, readLocalConsoleServiceStatus, restartLocalConsoleService, runLocalConsoleProcess, stopLocalConsoleService, } from "../local-console/index.js";
9
10
  const HELP_TEXT = `Usage:
11
+ tutti
10
12
  tutti launch [target] [--yes] [--foreground]
13
+ tutti service <status|stop|restart>
11
14
  tutti ps [--manage] [--all]
12
15
  tutti invite [target]
13
16
  tutti stop [target]
14
- tutti archive <target> [--yes]
15
- tutti forget <target> [--yes]
16
- tutti logs [target] [--tail 120]
17
17
  tutti provider setup [target]
18
18
  tutti provider status [target]
19
+
20
+ Advanced and diagnostic commands:
21
+ tutti logs [target] [--tail 120]
19
22
  tutti doctor [target]
23
+ tutti shutdown [--yes]
24
+ tutti archive <target> [--yes]
25
+ tutti forget <target> [--yes]
20
26
 
21
27
  Options:
22
28
  -h, --help Show help.
23
29
  -v, --version Show version.
24
30
 
25
31
  Notes:
32
+ tutti opens Tutti in your browser.
33
+ service manages only the local browser service, not project hosts.
26
34
  launch starts the host in the background by default.
27
35
  provider setup stores credentials in the machine-local Tutti provider store.
28
36
  `;
@@ -43,6 +51,25 @@ Commands:
43
51
  setup Configure or replace the OpenAI-compatible provider credential.
44
52
  status Show the redacted provider status for a project.
45
53
  `;
54
+ const SERVICE_HELP_TEXT = `Usage:
55
+ tutti service status
56
+ tutti service stop
57
+ tutti service restart
58
+
59
+ Commands:
60
+ status Show the local browser service version, process, and log path.
61
+ stop Stop only the local browser service; project hosts keep running.
62
+ restart Replace the local browser service with the current CLI version.
63
+ `;
64
+ const SHUTDOWN_HELP_TEXT = `Usage:
65
+ tutti shutdown [--yes]
66
+
67
+ Stops all project hosts on this machine and then stops the local browser service.
68
+ Use this before uninstalling Tutti.
69
+
70
+ Options:
71
+ --yes Confirm machine-wide shutdown in a non-interactive terminal.
72
+ `;
46
73
  const PS_HELP_TEXT = `Usage:
47
74
  tutti ps [--manage] [--all]
48
75
 
@@ -95,6 +122,12 @@ function helpForTopic(topic) {
95
122
  if (topic === "provider") {
96
123
  return PROVIDER_HELP_TEXT;
97
124
  }
125
+ if (topic === "service") {
126
+ return SERVICE_HELP_TEXT;
127
+ }
128
+ if (topic === "shutdown") {
129
+ return SHUTDOWN_HELP_TEXT;
130
+ }
98
131
  if (topic === "ps") {
99
132
  return PS_HELP_TEXT;
100
133
  }
@@ -151,15 +184,110 @@ async function runProviderSetupCommand(target) {
151
184
  ].join("\n"));
152
185
  process.stdout.write("\n");
153
186
  }
187
+ async function runLocalConsoleCommand() {
188
+ const console = await ensureLocalConsole();
189
+ if (openLocalConsoleBrowser(console.url)) {
190
+ process.stdout.write(`Tutti is ready at ${console.endpoint.base_url}/console\n`);
191
+ return;
192
+ }
193
+ process.stdout.write(["Open Tutti in a browser:", console.url, ""].join("\n"));
194
+ }
195
+ async function runServiceCommand(action) {
196
+ if (action === "status") {
197
+ const status = await readLocalConsoleServiceStatus();
198
+ if (status.status === "stopped") {
199
+ return [`Tutti service: stopped`, `Log: ${status.log_path}`].join("\n");
200
+ }
201
+ if (status.status === "stale") {
202
+ return [
203
+ "Tutti service: stale",
204
+ `PID: ${status.pid}`,
205
+ `Endpoint: ${status.base_url}`,
206
+ `Started: ${status.started_at}`,
207
+ `Log: ${status.log_path}`,
208
+ "Run `tutti service restart` to recover it.",
209
+ ].join("\n");
210
+ }
211
+ return [
212
+ "Tutti service: running",
213
+ `Version: ${status.version}${status.update_required ? ` (current CLI: ${status.current_version})` : ""}`,
214
+ `PID: ${status.pid}`,
215
+ `Endpoint: ${status.base_url}`,
216
+ `Started: ${status.started_at}`,
217
+ `Log: ${status.log_path}`,
218
+ ].join("\n");
219
+ }
220
+ if (action === "stop") {
221
+ const result = await stopLocalConsoleService();
222
+ return [
223
+ result === "already_stopped" ? "Tutti service is already stopped." : "Tutti service stopped.",
224
+ "Project hosts were not affected. Run `tutti ps` to inspect them.",
225
+ ].join("\n");
226
+ }
227
+ const endpoint = await restartLocalConsoleService();
228
+ return [
229
+ "Tutti service restarted.",
230
+ `Version: ${endpoint.version}`,
231
+ `Endpoint: ${endpoint.base_url}`,
232
+ ].join("\n");
233
+ }
234
+ async function runShutdownCommand(yes) {
235
+ if (!yes) {
236
+ if (!process.stdin.isTTY || !process.stdout.isTTY) {
237
+ throw new LaunchError("shutdown_confirmation_required", "Machine-wide shutdown requires explicit confirmation", "Run `tutti shutdown --yes` after confirming that active project hosts may stop.");
238
+ }
239
+ const accepted = await confirmTextFromTty([
240
+ "Stop every Tutti project host on this machine and the local browser service?",
241
+ "Active collaborators and in-progress work may be interrupted.",
242
+ ].join("\n"));
243
+ if (!accepted) {
244
+ throw new LaunchError("shutdown_rejected", "Machine-wide shutdown was not confirmed", "No Tutti process was intentionally stopped.");
245
+ }
246
+ }
247
+ const projects = await listRuntimeProjects({ all: true });
248
+ const failures = [];
249
+ let stoppedHosts = 0;
250
+ for (const project of projects) {
251
+ try {
252
+ const message = await runStopCommand(project.project_id);
253
+ if (!message.includes("not running")) {
254
+ stoppedHosts += 1;
255
+ }
256
+ }
257
+ catch (error) {
258
+ failures.push(`${project.project_id}: ${formatCliErrorReason(error)}`);
259
+ }
260
+ }
261
+ const service = await stopLocalConsoleService();
262
+ if (failures.length > 0) {
263
+ throw new LaunchError("existing_server_unhealthy", `Tutti stopped ${stoppedHosts} project host(s), but ${failures.length} host(s) could not be stopped safely`, "Run `tutti ps --all` and inspect the remaining host processes before uninstalling.", { failures });
264
+ }
265
+ return [
266
+ `Stopped ${stoppedHosts} project host(s).`,
267
+ service === "already_stopped"
268
+ ? "The Tutti browser service was already stopped."
269
+ : "Stopped the Tutti browser service.",
270
+ "Tutti is ready to uninstall.",
271
+ ].join("\n");
272
+ }
154
273
  try {
155
274
  const command = parseCliArgs(process.argv.slice(2));
156
275
  switch (command.kind) {
276
+ case "console":
277
+ await runLocalConsoleCommand();
278
+ break;
157
279
  case "help":
158
280
  process.stdout.write(helpForTopic(command.topic));
159
281
  break;
160
282
  case "version":
161
283
  process.stdout.write(`${readCliVersion()}\n`);
162
284
  break;
285
+ case "service":
286
+ process.stdout.write(`${await runServiceCommand(command.action)}\n`);
287
+ break;
288
+ case "shutdown":
289
+ process.stdout.write(`${await runShutdownCommand(command.yes)}\n`);
290
+ break;
163
291
  case "launch":
164
292
  if (command.foreground) {
165
293
  await runForegroundLaunchCommand(command);
@@ -172,6 +300,9 @@ try {
172
300
  case "internal-host-run":
173
301
  await runInternalHostRunCommand(command);
174
302
  break;
303
+ case "internal-console-run":
304
+ await runLocalConsoleProcess();
305
+ break;
175
306
  case "ps":
176
307
  if (command.manage) {
177
308
  await runPsManageCommand({ all: command.all });
@@ -1,4 +1,4 @@
1
- export type LaunchErrorCode = "unsafe_project_root" | "unsupported_tutti_schema" | "project_identity_conflict" | "git_bootstrap_failed" | "sensitive_file_detected" | "relay_unavailable" | "relay_registration_failed" | "host_not_running" | "archive_confirmation_required" | "archive_rejected" | "forget_confirmation_required" | "forget_rejected" | "project_target_ambiguous" | "project_target_not_found" | "provider_configuration_required" | "provider_setup_cancelled" | "provider_validation_failed" | "store_migration_failed" | "existing_server_unhealthy" | "takeover_rejected" | "takeover_failed" | "port_unavailable";
1
+ export type LaunchErrorCode = "unsafe_project_root" | "unsupported_tutti_schema" | "project_identity_conflict" | "git_bootstrap_failed" | "sensitive_file_detected" | "relay_unavailable" | "relay_registration_failed" | "host_not_running" | "shutdown_confirmation_required" | "shutdown_rejected" | "archive_confirmation_required" | "archive_rejected" | "forget_confirmation_required" | "forget_rejected" | "project_target_ambiguous" | "project_target_not_found" | "provider_configuration_required" | "provider_setup_cancelled" | "provider_validation_failed" | "store_migration_failed" | "existing_server_unhealthy" | "takeover_rejected" | "takeover_failed" | "port_unavailable";
2
2
  export declare class LaunchError extends Error {
3
3
  readonly code: LaunchErrorCode;
4
4
  readonly next: string;
@@ -3,6 +3,7 @@ import type { MachineRuntimeEndpointRecord } from "./machine-local.js";
3
3
  export type FetchLike = (input: string | URL, init?: RequestInit) => Promise<Response>;
4
4
  export type HostRuntimeEndpointProbeResult = {
5
5
  kind: "alive";
6
+ version?: string;
6
7
  readiness?: {
7
8
  relay?: "not_connected" | "connected";
8
9
  };
@@ -13,15 +13,19 @@ function parseHostHealthPayload(value) {
13
13
  if (record.service !== "tutti-host" || record.status !== "ok") {
14
14
  return null;
15
15
  }
16
+ const version = typeof record.version === "string" ? record.version : undefined;
16
17
  if (typeof record.readiness !== "object" ||
17
18
  record.readiness === null ||
18
19
  Array.isArray(record.readiness)) {
19
- return {};
20
+ return version === undefined ? {} : { version };
20
21
  }
21
22
  const readiness = record.readiness;
22
- return readiness.relay === "connected" || readiness.relay === "not_connected"
23
- ? { relay: readiness.relay }
24
- : {};
23
+ return {
24
+ ...(version === undefined ? {} : { version }),
25
+ ...(readiness.relay === "connected" || readiness.relay === "not_connected"
26
+ ? { relay: readiness.relay }
27
+ : {}),
28
+ };
25
29
  }
26
30
  async function fetchWithTimeout(fetchImpl, url, init = {}, timeoutMs = DEFAULT_PROBE_TIMEOUT_MS) {
27
31
  const controller = new AbortController();
@@ -55,7 +59,8 @@ export function createRuntimeEndpointProbe(fetchImpl = fetch) {
55
59
  }
56
60
  return {
57
61
  kind: "alive",
58
- ...(readiness.relay === undefined ? {} : { readiness }),
62
+ ...(readiness.version === undefined ? {} : { version: readiness.version }),
63
+ ...(readiness.relay === undefined ? {} : { readiness: { relay: readiness.relay } }),
59
64
  };
60
65
  }
61
66
  catch (error) {
@@ -1,6 +1,4 @@
1
- import { existsSync } from "node:fs";
2
- import { basename, join, resolve } from "node:path";
3
- import { fileURLToPath } from "node:url";
1
+ import { basename, join } from "node:path";
4
2
  import { serializeRuntimeLogLine } from "@tutti/shared/utils";
5
3
  import { completeRelayUpload, resolveRelayUpload } from "@tutti/relay-client";
6
4
  import { ApprovalRequestManager } from "../../approvals/index.js";
@@ -14,6 +12,7 @@ import { readProviderUsageProjection, recordProviderUsageEvent, } from "../../pr
14
12
  import { resolveProjectOpenAiRunPipelineRunner } from "../../run-pipeline/openai.js";
15
13
  import { openHostProjectStore } from "../../store/index.js";
16
14
  import { cleanupStaleRunWorkspaces } from "../../workspace-ops/index.js";
15
+ import { resolvePackagedWebStaticRoot } from "../http/static-web.js";
17
16
  import { AGENT_CONTEXT_BASE_PATH, AgentContextTokenRegistry, } from "../http/routes/agent-context.js";
18
17
  import { readHostRepoProjection } from "../http/routes/project-api.js";
19
18
  import { WorkspaceEventBus } from "../http/workspace-events.js";
@@ -38,13 +37,6 @@ function createHostRuntimeLogger(options) {
38
37
  },
39
38
  };
40
39
  }
41
- function resolvePackagedWebStaticRoot(env = process.env) {
42
- if (env.TUTTI_WEB_STATIC_ROOT !== undefined && env.TUTTI_WEB_STATIC_ROOT.trim() !== "") {
43
- return resolve(env.TUTTI_WEB_STATIC_ROOT);
44
- }
45
- const packagedRoot = fileURLToPath(new URL("../../../web", import.meta.url));
46
- return existsSync(join(packagedRoot, "index.html")) ? packagedRoot : undefined;
47
- }
48
40
  function createAgentContextRuntime(options) {
49
41
  return {
50
42
  baseUrl: new URL(AGENT_CONTEXT_BASE_PATH, options.baseUrl).toString(),
@@ -41,6 +41,7 @@ export type MachineProjectHostInspection = {
41
41
  } | {
42
42
  kind: "running";
43
43
  identity_verified: boolean;
44
+ version?: string;
44
45
  relay_readiness?: "connected" | "not_connected";
45
46
  relay_connection_status?: "connecting" | "connected" | "reconnecting" | "unavailable" | "stopped";
46
47
  } | {
@@ -145,6 +145,7 @@ async function inspectHost(options) {
145
145
  return {
146
146
  kind: "running",
147
147
  identity_verified: status.project_id === options.projectId,
148
+ ...(probe.version === undefined ? {} : { version: probe.version }),
148
149
  ...(probe.readiness?.relay === undefined ? {} : { relay_readiness: probe.readiness.relay }),
149
150
  ...(status.relay_connection?.status === undefined
150
151
  ? {}
@@ -10,6 +10,7 @@ export type ManagedHostReadyResult = {
10
10
  export declare function spawnDetachedHost(options: {
11
11
  workspaceRoot: string;
12
12
  env?: NodeJS.ProcessEnv;
13
+ inheritProcessEnv?: boolean;
13
14
  }): void;
14
15
  export declare function waitForManagedHostReady(options: {
15
16
  tuttiHome: string;
@@ -30,10 +30,12 @@ export function spawnDetachedHost(options) {
30
30
  ], {
31
31
  cwd: options.workspaceRoot,
32
32
  detached: true,
33
- env: {
34
- ...process.env,
35
- ...(options.env ?? {}),
36
- },
33
+ env: options.inheritProcessEnv === false
34
+ ? options.env
35
+ : {
36
+ ...process.env,
37
+ ...(options.env ?? {}),
38
+ },
37
39
  stdio: "ignore",
38
40
  });
39
41
  child.unref();
@@ -15,6 +15,7 @@ export type RuntimeProjectRow = {
15
15
  last_connected_at?: string;
16
16
  next_retry_at?: string;
17
17
  relay_reason_code?: string;
18
+ host_version?: string;
18
19
  };
19
20
  export declare function formatProjectIdForDisplay(projectId: ProjectId, projectIds: readonly ProjectId[]): string;
20
21
  export declare function listRuntimeProjects(options?: {
@@ -66,7 +66,8 @@ export function formatProjectIdForDisplay(projectId, projectIds) {
66
66
  }
67
67
  export async function listRuntimeProjects(options = {}) {
68
68
  const cwd = options.cwd ?? process.cwd();
69
- const tuttiHome = resolveTuttiHome(options.env?.TUTTI_HOME, cwd);
69
+ const env = options.env ?? process.env;
70
+ const tuttiHome = resolveTuttiHome(env.TUTTI_HOME, cwd);
70
71
  const rows = [];
71
72
  const inspections = await inspectMachineProjects({
72
73
  tuttiHome,
@@ -90,6 +91,9 @@ export async function listRuntimeProjects(options = {}) {
90
91
  display_name: inspection.display_name,
91
92
  workspace_root: inspection.workspace_root ?? "-",
92
93
  ...(endpoint === undefined ? {} : { endpoint: endpoint.base_url }),
94
+ ...(inspection.host.kind !== "running" || inspection.host.version === undefined
95
+ ? {}
96
+ : { host_version: inspection.host.version }),
93
97
  };
94
98
  if (provider !== undefined) {
95
99
  row.provider_status = provider.status;
@@ -12,10 +12,10 @@ const RUN_USAGE_SOURCES = [
12
12
  const TASK_ACCEPTED_USAGE_SOURCES = ["task_compile"];
13
13
  const TRACE_CONSENSUS_LIMIT = 3;
14
14
  const TRACE_TASK_CHANGE_LIMIT = 2;
15
- const TRACE_CONTRACT_SCOPE_LIMIT = 5;
16
- const TRACE_CHANGED_PATH_LIMIT = 6;
17
- const TRACE_CHECKPOINT_ITEM_LIMIT = 5;
18
- const TRACE_GENERATED_FILE_LIMIT = 5;
15
+ const TRACE_CONTRACT_SCOPE_LIMIT = 40;
16
+ const TRACE_CHANGED_PATH_LIMIT = 100;
17
+ const TRACE_CHECKPOINT_ITEM_LIMIT = 50;
18
+ const TRACE_GENERATED_FILE_LIMIT = 50;
19
19
  const TRACE_SOURCE_SNIPPET_LIMIT = 3;
20
20
  const TRACE_SOURCE_SNIPPET_BODY_LIMIT = 280;
21
21
  const TRACE_CHANGE_LINK_LIMIT = 3;
@@ -3,4 +3,5 @@ export type HostWebStaticOptions = {
3
3
  root: string;
4
4
  };
5
5
  export declare function registerHostWebStaticRoutes(app: FastifyInstance, options: HostWebStaticOptions): void;
6
+ export declare function resolvePackagedWebStaticRoot(env?: NodeJS.ProcessEnv): string | undefined;
6
7
  //# sourceMappingURL=static-web.d.ts.map
@@ -1,10 +1,12 @@
1
1
  import { existsSync, readFileSync } from "node:fs";
2
- import { join } from "node:path";
2
+ import { join, resolve } from "node:path";
3
+ import { fileURLToPath } from "node:url";
3
4
  import fastifyStatic from "@fastify/static";
4
5
  const BLOCKED_SPA_FALLBACK_PREFIXES = [
5
6
  "/api/",
6
7
  "/health",
7
8
  "/host-local/",
9
+ "/local-console/",
8
10
  "/assets/",
9
11
  ];
10
12
  function requestPath(request) {
@@ -49,4 +51,15 @@ export function registerHostWebStaticRoutes(app, options) {
49
51
  sendIndex(reply, indexHtml);
50
52
  });
51
53
  }
54
+ export function resolvePackagedWebStaticRoot(env = process.env) {
55
+ if (env.TUTTI_WEB_STATIC_ROOT !== undefined && env.TUTTI_WEB_STATIC_ROOT.trim() !== "") {
56
+ return resolve(env.TUTTI_WEB_STATIC_ROOT);
57
+ }
58
+ const packagedRoot = fileURLToPath(new URL("../../../web", import.meta.url));
59
+ if (existsSync(join(packagedRoot, "index.html"))) {
60
+ return packagedRoot;
61
+ }
62
+ const workspaceWebDist = fileURLToPath(new URL("../../../../web/dist", import.meta.url));
63
+ return existsSync(join(workspaceWebDist, "index.html")) ? workspaceWebDist : undefined;
64
+ }
52
65
  //# sourceMappingURL=static-web.js.map
@@ -0,0 +1,5 @@
1
+ export declare function openLocalConsoleBrowser(url: string, options?: {
2
+ platform?: NodeJS.Platform;
3
+ env?: NodeJS.ProcessEnv;
4
+ }): boolean;
5
+ //# sourceMappingURL=browser-open.d.ts.map
@@ -0,0 +1,40 @@
1
+ import { spawn } from "node:child_process";
2
+ import { constants, accessSync } from "node:fs";
3
+ import { delimiter, join } from "node:path";
4
+ function executableInPath(command, env) {
5
+ for (const directory of (env.PATH ?? "").split(delimiter)) {
6
+ if (directory.trim() === "") {
7
+ continue;
8
+ }
9
+ const candidate = join(directory, command);
10
+ try {
11
+ accessSync(candidate, constants.X_OK);
12
+ return candidate;
13
+ }
14
+ catch {
15
+ // Keep searching PATH.
16
+ }
17
+ }
18
+ return null;
19
+ }
20
+ export function openLocalConsoleBrowser(url, options = {}) {
21
+ const platform = options.platform ?? process.platform;
22
+ const env = options.env ?? process.env;
23
+ const command = platform === "darwin"
24
+ ? "/usr/bin/open"
25
+ : platform === "linux" && (env.DISPLAY !== undefined || env.WAYLAND_DISPLAY !== undefined)
26
+ ? executableInPath("xdg-open", env)
27
+ : null;
28
+ if (command === null) {
29
+ return false;
30
+ }
31
+ const child = spawn(command, [url], {
32
+ detached: true,
33
+ env,
34
+ stdio: "ignore",
35
+ });
36
+ child.on("error", () => undefined);
37
+ child.unref();
38
+ return true;
39
+ }
40
+ //# sourceMappingURL=browser-open.js.map
@@ -0,0 +1,20 @@
1
+ export type LocalFolderPickerKind = "macos" | "zenity" | "kdialog" | "unavailable";
2
+ export type LocalFolderPickResult = {
3
+ kind: "selected";
4
+ path: string;
5
+ } | {
6
+ kind: "cancelled";
7
+ } | {
8
+ kind: "unavailable";
9
+ message: string;
10
+ };
11
+ export declare function detectLocalFolderPicker(platform?: NodeJS.Platform, env?: NodeJS.ProcessEnv): {
12
+ kind: LocalFolderPickerKind;
13
+ executable?: string;
14
+ };
15
+ export declare function pickLocalFolder(options?: {
16
+ prompt?: string;
17
+ platform?: NodeJS.Platform;
18
+ env?: NodeJS.ProcessEnv;
19
+ }): Promise<LocalFolderPickResult>;
20
+ //# sourceMappingURL=folder-picker.d.ts.map