@xfey/tutti 0.1.28 → 0.1.30

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 (64) hide show
  1. package/README.md +1 -1
  2. package/dist/collaboration-state/index.d.ts +1 -1
  3. package/dist/collaboration-state/index.js +1 -1
  4. package/dist/collaboration-state/messages.d.ts +1 -0
  5. package/dist/collaboration-state/messages.js +12 -0
  6. package/dist/control-plane/index.d.ts +0 -1
  7. package/dist/control-plane/index.js +16 -49
  8. package/dist/control-plane/task-compile-output.js +2 -54
  9. package/dist/control-plane/worklist-terminal-feedback.d.ts +9 -1
  10. package/dist/control-plane/worklist-terminal-feedback.js +99 -1
  11. package/dist/server-shell/cli/args.d.ts +9 -0
  12. package/dist/server-shell/cli/args.js +55 -5
  13. package/dist/server-shell/cli/cli.js +46 -7
  14. package/dist/server-shell/cli/errors.d.ts +1 -1
  15. package/dist/server-shell/cli/errors.js +6 -1
  16. package/dist/server-shell/cli/host-lifecycle.d.ts +1 -0
  17. package/dist/server-shell/cli/host-lifecycle.js +155 -41
  18. package/dist/server-shell/cli/host-relay-connection-manager.d.ts +50 -0
  19. package/dist/server-shell/cli/host-relay-connection-manager.js +447 -0
  20. package/dist/server-shell/cli/host-relay-status.d.ts +4 -0
  21. package/dist/server-shell/cli/host-relay-status.js +74 -15
  22. package/dist/server-shell/cli/host-runtime-endpoint.js +26 -0
  23. package/dist/server-shell/cli/host-server-runtime.d.ts +2 -2
  24. package/dist/server-shell/cli/host-server-runtime.js +19 -15
  25. package/dist/server-shell/cli/launch-command.d.ts +1 -0
  26. package/dist/server-shell/cli/launch-command.js +8 -10
  27. package/dist/server-shell/cli/launch.d.ts +1 -0
  28. package/dist/server-shell/cli/launch.js +7 -3
  29. package/dist/server-shell/cli/local-control-client.d.ts +5 -0
  30. package/dist/server-shell/cli/local-control-client.js +9 -0
  31. package/dist/server-shell/cli/machine-local.d.ts +8 -0
  32. package/dist/server-shell/cli/machine-local.js +29 -0
  33. package/dist/server-shell/cli/machine-project-inspector.d.ts +81 -0
  34. package/dist/server-shell/cli/machine-project-inspector.js +205 -0
  35. package/dist/server-shell/cli/project-resolver.js +22 -32
  36. package/dist/server-shell/cli/relay-registration.d.ts +5 -2
  37. package/dist/server-shell/cli/relay-registration.js +7 -3
  38. package/dist/server-shell/cli/runtime-commands.d.ts +30 -4
  39. package/dist/server-shell/cli/runtime-commands.js +230 -113
  40. package/dist/server-shell/http/routes/local-control.d.ts +10 -0
  41. package/dist/server-shell/http/routes/local-control.js +29 -0
  42. package/migrations/README.md +1 -1
  43. package/node_modules/@tutti/relay-client/dist/host-control.d.ts +23 -0
  44. package/node_modules/@tutti/relay-client/dist/host-control.js +70 -3
  45. package/node_modules/@tutti/relay-client/dist/tunnel-types.d.ts +6 -2
  46. package/node_modules/@tutti/relay-client/dist/tunnel.js +13 -2
  47. package/node_modules/@tutti/shared/dist/schemas/api/clarifications.d.ts +21 -0
  48. package/node_modules/@tutti/shared/dist/schemas/api/index.d.ts +1 -1
  49. package/node_modules/@tutti/shared/dist/schemas/api/index.js +1 -1
  50. package/node_modules/@tutti/shared/dist/schemas/api/messages.d.ts +105 -0
  51. package/node_modules/@tutti/shared/dist/schemas/api/messages.js +16 -0
  52. package/node_modules/@tutti/shared/dist/schemas/api/types.d.ts +2 -1
  53. package/node_modules/@tutti/shared/dist/schemas/api/viewer-reference.d.ts +21 -0
  54. package/package.json +1 -1
  55. package/prompts/prompt-flow-map.md +1 -1
  56. package/prompts/runs/README.md +1 -1
  57. package/prompts/runs/task-continuation.md +1 -1
  58. package/prompts/runs/task-retry.md +1 -1
  59. package/prompts/runs/task-run.md +1 -1
  60. package/web/assets/index-BHRlhFwi.css +1 -0
  61. package/web/assets/index-DPU2uqZl.js +29 -0
  62. package/web/index.html +2 -2
  63. package/web/assets/index-CeGTRkQn.css +0 -1
  64. package/web/assets/index-DGnDFv9O.js +0 -29
@@ -2,7 +2,7 @@ import { existsSync } from "node:fs";
2
2
  import { basename, join, resolve } from "node:path";
3
3
  import { fileURLToPath } from "node:url";
4
4
  import { serializeRuntimeLogLine } from "@tutti/shared/utils";
5
- import { completeRelayUpload, resolveRelayUpload, } from "@tutti/relay-client";
5
+ import { completeRelayUpload, resolveRelayUpload } from "@tutti/relay-client";
6
6
  import { ApprovalRequestManager } from "../../approvals/index.js";
7
7
  import { ReadOnlyChatAssistant } from "../../chat-assistant/index.js";
8
8
  import { resolveControlPlaneCommandRecovery } from "../../control-plane/command-recovery.js";
@@ -20,7 +20,7 @@ import { WorkspaceEventBus } from "../http/workspace-events.js";
20
20
  import { createTrustedRelaySessionMetadataStore, } from "../session/relay-session-context.js";
21
21
  import { LaunchError } from "./errors.js";
22
22
  import { relayStateFromLaunchStatus } from "./host-relay-status.js";
23
- import { appendHostLogLine, readHostRegistrationSecret, createMachineRuntimeEndpointToken, deleteMachineRuntimeEndpoint, ensureHostLogFile, getHostLogFilePath, writeMachineRuntimeEndpoint, } from "./machine-local.js";
23
+ import { appendHostLogLine, readHostRegistrationSecret, createMachineRuntimeEndpointToken, deleteMachineRuntimeEndpoint, ensureHostLogFile, getHostLogFilePath, updateMachineProjectDisplayNameSnapshot, writeMachineRuntimeEndpoint, } from "./machine-local.js";
24
24
  import { readProjectDescription, readProjectDisplayName, writeProjectDescription, writeProjectDisplayName, } from "./project-identity.js";
25
25
  export const HOST_SERVER_VERSION = "0.0.0";
26
26
  function createHostRuntimeLogger(options) {
@@ -180,6 +180,12 @@ export async function startForegroundHostServer(options) {
180
180
  };
181
181
  const updateRuntimeProjectDisplayName = (displayName) => {
182
182
  writeProjectDisplayName(options.preparation.workspace_root, displayName);
183
+ updateMachineProjectDisplayNameSnapshot({
184
+ tuttiHome: options.preparation.tutti_home,
185
+ projectId: options.preparation.project_id,
186
+ displayName,
187
+ now,
188
+ });
183
189
  projectDisplayName = displayName;
184
190
  projectContext.projectName = displayName;
185
191
  chatAssistantProject.display_name = displayName;
@@ -261,8 +267,7 @@ export async function startForegroundHostServer(options) {
261
267
  now,
262
268
  });
263
269
  let close = async () => { };
264
- let relayTunnel;
265
- let relayTunnelConnected = false;
270
+ let relayConnectionManager;
266
271
  const localControl = {
267
272
  token,
268
273
  requestShutdown: () => {
@@ -318,6 +323,10 @@ export async function startForegroundHostServer(options) {
318
323
  return options.getLaunchStatus?.() ?? {};
319
324
  },
320
325
  }),
326
+ reconnectRelay: async () => {
327
+ await relayConnectionManager?.reconnectNow();
328
+ return options.getLaunchStatus?.() ?? {};
329
+ },
321
330
  ...(options.getLaunchStatus === undefined ? {} : { getLaunchStatus: options.getLaunchStatus }),
322
331
  };
323
332
  let app;
@@ -333,7 +342,9 @@ export async function startForegroundHostServer(options) {
333
342
  agentContext: { tokenRegistry: agentContextTokenRegistry },
334
343
  health: {
335
344
  readiness: {
336
- relay: () => (relayTunnelConnected ? "connected" : "not_connected"),
345
+ relay: () => relayConnectionManager?.getState().status === "connected"
346
+ ? "connected"
347
+ : "not_connected",
337
348
  store: () => "ready",
338
349
  },
339
350
  },
@@ -432,8 +443,7 @@ export async function startForegroundHostServer(options) {
432
443
  close = () => {
433
444
  closePromise ??= (async () => {
434
445
  try {
435
- relayTunnelConnected = false;
436
- relayTunnel?.close(1000, "host closing");
446
+ await relayConnectionManager?.stop();
437
447
  await app.close();
438
448
  }
439
449
  finally {
@@ -459,14 +469,8 @@ export async function startForegroundHostServer(options) {
459
469
  log_file_path: logFilePath,
460
470
  runtime_endpoint: runtimeEndpoint,
461
471
  trusted_relay_metadata_store: trustedMetadataStore,
462
- attachRelayTunnel: (tunnel) => {
463
- relayTunnel = tunnel;
464
- relayTunnelConnected = true;
465
- void tunnel.closed.finally(() => {
466
- if (relayTunnel === tunnel) {
467
- relayTunnelConnected = false;
468
- }
469
- });
472
+ attachRelayConnectionManager: (manager) => {
473
+ relayConnectionManager = manager;
470
474
  },
471
475
  closed,
472
476
  close,
@@ -1,5 +1,6 @@
1
1
  import type { LaunchConfirmationRequest } from "./git-bootstrap.js";
2
2
  export declare function confirmFromTty(request: LaunchConfirmationRequest): Promise<boolean>;
3
+ export declare function confirmTextFromTty(message: string): Promise<boolean>;
3
4
  export declare function renderCompletionPage(options: {
4
5
  joinUrl: string;
5
6
  joinTokenExpiresAt?: string;
@@ -1,6 +1,6 @@
1
1
  import { createInterface } from "node:readline/promises";
2
2
  import { basename } from "node:path";
3
- import { formatLaunchLifecycleResult, startLaunchProject, waitForForegroundHostShutdown } from "./host-lifecycle.js";
3
+ import { formatLaunchLifecycleResult, startLaunchProject, waitForForegroundHostShutdown, } from "./host-lifecycle.js";
4
4
  import { prepareLaunchProject } from "./launch.js";
5
5
  import { spawnDetachedHost, waitForManagedHostReady } from "./managed-host.js";
6
6
  import { resolveLaunchWorkspacePath } from "./project-resolver.js";
@@ -10,6 +10,9 @@ import { runProviderSetupTui } from "./provider-tui.js";
10
10
  import { LaunchError } from "./errors.js";
11
11
  const CLEAR = "\u001B[2J\u001B[H";
12
12
  export async function confirmFromTty(request) {
13
+ return await confirmTextFromTty(request.message);
14
+ }
15
+ export async function confirmTextFromTty(message) {
13
16
  if (!process.stdin.isTTY || !process.stdout.isTTY) {
14
17
  return false;
15
18
  }
@@ -18,7 +21,7 @@ export async function confirmFromTty(request) {
18
21
  output: process.stdout,
19
22
  });
20
23
  try {
21
- const answer = await readline.question(`${request.message}\n\nContinue? [y/N] `);
24
+ const answer = await readline.question(`${message}\n\nContinue? [y/N] `);
22
25
  return /^(y|yes)$/iu.test(answer.trim());
23
26
  }
24
27
  finally {
@@ -49,8 +52,7 @@ function visibleTerminalLength(line) {
49
52
  const end = Array.from(stripped.slice(start + 2)).findIndex((character) => {
50
53
  const codePoint = character.codePointAt(0);
51
54
  return (codePoint !== undefined &&
52
- ((codePoint >= 0x41 && codePoint <= 0x5a) ||
53
- (codePoint >= 0x61 && codePoint <= 0x7a)));
55
+ ((codePoint >= 0x41 && codePoint <= 0x5a) || (codePoint >= 0x61 && codePoint <= 0x7a)));
54
56
  });
55
57
  if (end < 0) {
56
58
  break;
@@ -71,12 +73,8 @@ export function renderCompletionPage(options) {
71
73
  const projectLabel = formatProjectLabel(options.workspaceRoot, options.projectId);
72
74
  const joinUrl = formatTerminalLink(options.joinUrl, { hyperlinks: options.hyperlinks === true });
73
75
  const joinValidity = formatJoinLinkValidity({
74
- ...(options.joinTokenExpiresAt === undefined
75
- ? {}
76
- : { expiresAt: options.joinTokenExpiresAt }),
77
- ...(options.joinTokenReusable === undefined
78
- ? {}
79
- : { reusable: options.joinTokenReusable }),
76
+ ...(options.joinTokenExpiresAt === undefined ? {} : { expiresAt: options.joinTokenExpiresAt }),
77
+ ...(options.joinTokenReusable === undefined ? {} : { reusable: options.joinTokenReusable }),
80
78
  includeRemaining: false,
81
79
  });
82
80
  const summaryLines = [
@@ -39,6 +39,7 @@ export type LaunchLocalContext = {
39
39
  local_diagnostic_endpoint: LaunchLocalDiagnosticEndpoint;
40
40
  existing_identity: ProjectIdentityReadResult;
41
41
  };
42
+ export declare function resolveRelayUrl(env?: NodeJS.ProcessEnv): string;
42
43
  export declare function resolveLaunchLocalContext(options: Pick<LaunchPreparationOptions, "workspacePath" | "env" | "cwd">): LaunchLocalContext;
43
44
  export declare function prepareLaunchProject(options: LaunchPreparationOptions): Promise<LaunchPreparationResult>;
44
45
  export declare function formatLaunchPreparation(result: LaunchPreparationResult): string;
@@ -1,13 +1,16 @@
1
- import { resolve } from "node:path";
1
+ import { basename, resolve } from "node:path";
2
2
  import { createProjectId } from "@tutti/shared/ids";
3
3
  import { readOpenAiProviderConfigProjection, resolveTuttiHome, } from "../../providers/openai/index.js";
4
4
  import { LaunchError } from "./errors.js";
5
5
  import { assertSafeProjectRoot, ensureGitBootstrap, } from "./git-bootstrap.js";
6
6
  import { ensureMachineProjectBinding } from "./machine-local.js";
7
- import { readProjectIdentity, writeProjectIdentity, } from "./project-identity.js";
7
+ import { readProjectIdentity, readProjectDisplayName, writeProjectIdentity, } from "./project-identity.js";
8
8
  const DEFAULT_RELAY_URL = "https://tutti.now";
9
9
  const DEFAULT_HOST_ENDPOINT_HOST = "127.0.0.1";
10
10
  const AUTO_HOST_ENDPOINT_PORT = 0;
11
+ export function resolveRelayUrl(env = process.env) {
12
+ return env.TUTTI_RELAY_URL ?? DEFAULT_RELAY_URL;
13
+ }
11
14
  function readOptionalPort(value) {
12
15
  if (value === undefined || value.trim() === "") {
13
16
  return AUTO_HOST_ENDPOINT_PORT;
@@ -35,7 +38,7 @@ export function resolveLaunchLocalContext(options) {
35
38
  return {
36
39
  workspace_root: workspaceRoot,
37
40
  tutti_home: resolveTuttiHome(env.TUTTI_HOME, cwd),
38
- relay_url: env.TUTTI_RELAY_URL ?? DEFAULT_RELAY_URL,
41
+ relay_url: resolveRelayUrl(env),
39
42
  local_diagnostic_endpoint: resolveLocalDiagnosticEndpoint(env),
40
43
  existing_identity: readProjectIdentity(workspaceRoot),
41
44
  };
@@ -65,6 +68,7 @@ export async function prepareLaunchProject(options) {
65
68
  tuttiHome,
66
69
  projectId,
67
70
  workspaceRoot,
71
+ displayNameSnapshot: readProjectDisplayName(workspaceRoot) ?? (basename(workspaceRoot) || projectId),
68
72
  };
69
73
  if (options.allowWorkspaceRootTakeover !== undefined) {
70
74
  bindingOptions.allowWorkspaceRootTakeover = options.allowWorkspaceRootTakeover;
@@ -10,6 +10,11 @@ export declare function rotateHostLocalInvite(options: {
10
10
  endpoint: MachineRuntimeEndpointRecord;
11
11
  fetchImpl?: LocalControlFetch;
12
12
  }): Promise<HostLocalLaunchStatus>;
13
+ export declare function reconnectHostLocalRelay(options: {
14
+ endpoint: MachineRuntimeEndpointRecord;
15
+ fetchImpl?: LocalControlFetch;
16
+ timeoutMs?: number;
17
+ }): Promise<HostLocalLaunchStatus>;
13
18
  export declare function readHostLocalProject(options: {
14
19
  endpoint: MachineRuntimeEndpointRecord;
15
20
  fetchImpl?: LocalControlFetch;
@@ -42,6 +42,15 @@ export async function rotateHostLocalInvite(options) {
42
42
  path: "/host-local/v1/invite/rotate",
43
43
  });
44
44
  }
45
+ export async function reconnectHostLocalRelay(options) {
46
+ return await requestLocalJson({
47
+ endpoint: options.endpoint,
48
+ ...(options.fetchImpl === undefined ? {} : { fetchImpl: options.fetchImpl }),
49
+ ...(options.timeoutMs === undefined ? {} : { timeoutMs: options.timeoutMs }),
50
+ method: "POST",
51
+ path: "/host-local/v1/relay/reconnect",
52
+ });
53
+ }
45
54
  export async function readHostLocalProject(options) {
46
55
  return await requestLocalJson({
47
56
  ...options,
@@ -3,6 +3,7 @@ export type MachineProjectBindingRecord = {
3
3
  project_id: ProjectId;
4
4
  workspace_root: string;
5
5
  local_store_root: string;
6
+ display_name_snapshot?: string;
6
7
  relay_project_ref?: RelayProjectRef;
7
8
  host_registration_secret_ref?: string;
8
9
  updated_at: string;
@@ -26,6 +27,7 @@ export type EnsureMachineProjectBindingOptions = {
26
27
  tuttiHome: string;
27
28
  projectId: ProjectId;
28
29
  workspaceRoot: string;
30
+ displayNameSnapshot?: string;
29
31
  allowWorkspaceRootTakeover?: boolean;
30
32
  now?: () => Date;
31
33
  };
@@ -72,5 +74,11 @@ export declare function updateMachineProjectRelayBinding(options: {
72
74
  relayProjectRef: RelayProjectRef;
73
75
  now?: () => Date;
74
76
  }): MachineProjectBindingRecord;
77
+ export declare function updateMachineProjectDisplayNameSnapshot(options: {
78
+ tuttiHome: string;
79
+ projectId: ProjectId;
80
+ displayName: string;
81
+ now?: () => Date;
82
+ }): MachineProjectBindingRecord;
75
83
  export declare function ensureMachineProjectBinding(options: EnsureMachineProjectBindingOptions): MachineProjectBindingResult;
76
84
  //# sourceMappingURL=machine-local.d.ts.map
@@ -72,6 +72,8 @@ export function readMachineProjectBinding(tuttiHome, projectId) {
72
72
  typeof raw.workspace_root !== "string" ||
73
73
  typeof raw.local_store_root !== "string" ||
74
74
  typeof raw.updated_at !== "string" ||
75
+ (raw.display_name_snapshot !== undefined &&
76
+ (typeof raw.display_name_snapshot !== "string" || raw.display_name_snapshot.trim() === "")) ||
75
77
  (raw.relay_project_ref !== undefined &&
76
78
  (typeof raw.relay_project_ref !== "string" ||
77
79
  !isPrefixedId(raw.relay_project_ref, ID_PREFIXES.relayProject))) ||
@@ -85,6 +87,9 @@ export function readMachineProjectBinding(tuttiHome, projectId) {
85
87
  local_store_root: raw.local_store_root,
86
88
  updated_at: raw.updated_at,
87
89
  };
90
+ if (raw.display_name_snapshot !== undefined) {
91
+ binding.display_name_snapshot = raw.display_name_snapshot;
92
+ }
88
93
  if (raw.relay_project_ref !== undefined) {
89
94
  binding.relay_project_ref = raw.relay_project_ref;
90
95
  }
@@ -200,6 +205,23 @@ export function updateMachineProjectRelayBinding(options) {
200
205
  writePrivateJsonFile(getMachineProjectBindingPath(options.tuttiHome, options.projectId), binding);
201
206
  return binding;
202
207
  }
208
+ export function updateMachineProjectDisplayNameSnapshot(options) {
209
+ const existing = readMachineProjectBinding(options.tuttiHome, options.projectId);
210
+ if (existing === null) {
211
+ throw new LaunchError("project_identity_conflict", "Machine-local project binding is missing while updating its display name", "Run `tutti launch` again from the project workspace.");
212
+ }
213
+ const displayName = options.displayName.trim();
214
+ if (displayName === "") {
215
+ throw new LaunchError("project_identity_conflict", "Machine-local project display name snapshot must be non-empty", "Use a non-empty project display name.");
216
+ }
217
+ const binding = {
218
+ ...existing,
219
+ display_name_snapshot: displayName,
220
+ updated_at: (options.now ?? (() => new Date()))().toISOString(),
221
+ };
222
+ writePrivateJsonFile(getMachineProjectBindingPath(options.tuttiHome, options.projectId), binding);
223
+ return binding;
224
+ }
203
225
  function ensureHostRegistrationSecret(tuttiHome, projectId, now) {
204
226
  const secretPath = getHostRegistrationSecretPath(tuttiHome, projectId);
205
227
  const ref = createHostRegistrationSecretRef(projectId);
@@ -236,6 +258,13 @@ export function ensureMachineProjectBinding(options) {
236
258
  host_registration_secret_ref: existing?.host_registration_secret_ref ?? secret.ref,
237
259
  updated_at: now().toISOString(),
238
260
  };
261
+ const displayNameSnapshot = options.displayNameSnapshot?.trim();
262
+ if (displayNameSnapshot !== undefined && displayNameSnapshot !== "") {
263
+ binding.display_name_snapshot = displayNameSnapshot;
264
+ }
265
+ else if (existing?.display_name_snapshot !== undefined) {
266
+ binding.display_name_snapshot = existing.display_name_snapshot;
267
+ }
239
268
  if (existing?.relay_project_ref !== undefined) {
240
269
  binding.relay_project_ref = existing.relay_project_ref;
241
270
  }
@@ -0,0 +1,81 @@
1
+ import { type ProjectId } from "@tutti/shared/ids";
2
+ import { type FetchLike, type HostRuntimeEndpointProbe } from "./host-runtime-endpoint.js";
3
+ import { type MachineProjectBindingRecord, type MachineRuntimeEndpointRecord } from "./machine-local.js";
4
+ export type MachineProjectBindingInspection = {
5
+ kind: "valid";
6
+ record: MachineProjectBindingRecord;
7
+ } | {
8
+ kind: "missing";
9
+ } | {
10
+ kind: "invalid";
11
+ reason_code: "invalid_binding";
12
+ };
13
+ export type MachineProjectEndpointInspection = {
14
+ kind: "valid";
15
+ record: MachineRuntimeEndpointRecord;
16
+ } | {
17
+ kind: "missing";
18
+ } | {
19
+ kind: "invalid";
20
+ reason_code: "invalid_endpoint";
21
+ };
22
+ export type MachineProjectWorkspaceInspection = {
23
+ kind: "matching";
24
+ display_name?: string;
25
+ } | {
26
+ kind: "missing";
27
+ } | {
28
+ kind: "replaced";
29
+ actual_project_id: ProjectId;
30
+ } | {
31
+ kind: "not_a_tutti_project";
32
+ } | {
33
+ kind: "unreadable";
34
+ reason_code: "workspace_root_unavailable" | "identity_read_failed";
35
+ };
36
+ export type MachineProjectHostInspection = {
37
+ kind: "stopped";
38
+ } | {
39
+ kind: "stale";
40
+ reason_code: "invalid_endpoint" | "unreachable" | "local_control_unavailable";
41
+ } | {
42
+ kind: "running";
43
+ identity_verified: boolean;
44
+ relay_readiness?: "connected" | "not_connected";
45
+ relay_connection_status?: "connecting" | "connected" | "reconnecting" | "unavailable" | "stopped";
46
+ } | {
47
+ kind: "identity_mismatch";
48
+ actual_project_id: ProjectId;
49
+ };
50
+ export type MachineProjectRelayInspection = "connected" | "connecting" | "reconnecting" | "unavailable" | "unknown";
51
+ export type MachineProjectInventoryInspection = {
52
+ project_id: ProjectId;
53
+ binding: MachineProjectBindingInspection;
54
+ endpoint: MachineProjectEndpointInspection;
55
+ workspace: MachineProjectWorkspaceInspection;
56
+ workspace_root?: string;
57
+ normalized_workspace_root?: string;
58
+ display_name: string;
59
+ };
60
+ export type MachineProjectInspection = MachineProjectInventoryInspection & {
61
+ host: MachineProjectHostInspection;
62
+ relay: MachineProjectRelayInspection;
63
+ };
64
+ export type MachineProjectInspectorOptions = {
65
+ tuttiHome: string;
66
+ fetchImpl?: FetchLike;
67
+ probeRuntimeEndpoint?: HostRuntimeEndpointProbe;
68
+ };
69
+ export type MachineProjectInspectionIndex = {
70
+ by_project_id: ReadonlyMap<ProjectId, MachineProjectInventoryInspection>;
71
+ by_normalized_workspace_root: ReadonlyMap<string, readonly MachineProjectInventoryInspection[]>;
72
+ by_alias: ReadonlyMap<string, readonly MachineProjectInventoryInspection[]>;
73
+ };
74
+ export declare function listMachineProjectIds(tuttiHome: string): ProjectId[];
75
+ export declare function normalizeMachineWorkspacePath(workspaceRoot: string): string;
76
+ export declare function createMachineProjectInspectionIndex(inspections: readonly MachineProjectInventoryInspection[]): MachineProjectInspectionIndex;
77
+ export declare function inspectMachineProject(projectId: ProjectId, options: MachineProjectInspectorOptions): Promise<MachineProjectInspection>;
78
+ export declare function inspectMachineProjectInventory(projectId: ProjectId, tuttiHome: string): MachineProjectInventoryInspection;
79
+ export declare function inspectMachineProjectInventories(tuttiHome: string): MachineProjectInventoryInspection[];
80
+ export declare function inspectMachineProjects(options: MachineProjectInspectorOptions): Promise<MachineProjectInspection[]>;
81
+ //# sourceMappingURL=machine-project-inspector.d.ts.map
@@ -0,0 +1,205 @@
1
+ import { existsSync, readdirSync, realpathSync } from "node:fs";
2
+ import { basename, join, resolve } from "node:path";
3
+ import { ID_PREFIXES, isPrefixedId } from "@tutti/shared/ids";
4
+ import { createRuntimeEndpointProbe, } from "./host-runtime-endpoint.js";
5
+ import { readHostLocalLaunchStatus } from "./local-control-client.js";
6
+ import { readMachineProjectBinding, readMachineRuntimeEndpoint, } from "./machine-local.js";
7
+ import { readProjectDisplayName, readProjectIdentity } from "./project-identity.js";
8
+ export function listMachineProjectIds(tuttiHome) {
9
+ const projectsRoot = join(tuttiHome, "projects");
10
+ if (!existsSync(projectsRoot)) {
11
+ return [];
12
+ }
13
+ return readdirSync(projectsRoot, { withFileTypes: true })
14
+ .filter((entry) => entry.isDirectory() && isPrefixedId(entry.name, ID_PREFIXES.project))
15
+ .map((entry) => entry.name)
16
+ .sort();
17
+ }
18
+ export function normalizeMachineWorkspacePath(workspaceRoot) {
19
+ try {
20
+ return realpathSync.native(workspaceRoot);
21
+ }
22
+ catch {
23
+ return resolve(workspaceRoot);
24
+ }
25
+ }
26
+ function normalizeMachineProjectAlias(value) {
27
+ return value.trim().toLowerCase();
28
+ }
29
+ export function createMachineProjectInspectionIndex(inspections) {
30
+ const byProjectId = new Map();
31
+ const byWorkspace = new Map();
32
+ const byAlias = new Map();
33
+ for (const inspection of inspections) {
34
+ byProjectId.set(inspection.project_id, inspection);
35
+ if (inspection.normalized_workspace_root !== undefined) {
36
+ const matches = byWorkspace.get(inspection.normalized_workspace_root) ?? [];
37
+ matches.push(inspection);
38
+ byWorkspace.set(inspection.normalized_workspace_root, matches);
39
+ }
40
+ if (inspection.workspace.kind !== "matching") {
41
+ continue;
42
+ }
43
+ const workspaceName = inspection.workspace_root === undefined ? undefined : basename(inspection.workspace_root);
44
+ const aliases = new Set([inspection.display_name, workspaceName]
45
+ .filter((alias) => alias !== undefined && alias.trim() !== "")
46
+ .map(normalizeMachineProjectAlias));
47
+ for (const alias of aliases) {
48
+ const matches = byAlias.get(alias) ?? [];
49
+ matches.push(inspection);
50
+ byAlias.set(alias, matches);
51
+ }
52
+ }
53
+ return {
54
+ by_project_id: byProjectId,
55
+ by_normalized_workspace_root: byWorkspace,
56
+ by_alias: byAlias,
57
+ };
58
+ }
59
+ function inspectBinding(tuttiHome, projectId) {
60
+ try {
61
+ const record = readMachineProjectBinding(tuttiHome, projectId);
62
+ return record === null ? { kind: "missing" } : { kind: "valid", record };
63
+ }
64
+ catch {
65
+ return { kind: "invalid", reason_code: "invalid_binding" };
66
+ }
67
+ }
68
+ function inspectEndpoint(tuttiHome, projectId) {
69
+ try {
70
+ const record = readMachineRuntimeEndpoint(tuttiHome, projectId);
71
+ return record === null ? { kind: "missing" } : { kind: "valid", record };
72
+ }
73
+ catch {
74
+ return { kind: "invalid", reason_code: "invalid_endpoint" };
75
+ }
76
+ }
77
+ function inspectWorkspace(workspaceRoot, projectId) {
78
+ if (workspaceRoot === undefined) {
79
+ return { kind: "unreadable", reason_code: "workspace_root_unavailable" };
80
+ }
81
+ if (!existsSync(workspaceRoot)) {
82
+ return { kind: "missing" };
83
+ }
84
+ try {
85
+ const identity = readProjectIdentity(workspaceRoot);
86
+ if (identity.kind === "missing") {
87
+ return { kind: "not_a_tutti_project" };
88
+ }
89
+ if (identity.project_id !== projectId) {
90
+ return { kind: "replaced", actual_project_id: identity.project_id };
91
+ }
92
+ const displayName = readProjectDisplayName(workspaceRoot);
93
+ return displayName === null
94
+ ? { kind: "matching" }
95
+ : { kind: "matching", display_name: displayName };
96
+ }
97
+ catch {
98
+ return { kind: "unreadable", reason_code: "identity_read_failed" };
99
+ }
100
+ }
101
+ function relayInspectionFromHost(host) {
102
+ if (host.kind !== "running") {
103
+ return "unknown";
104
+ }
105
+ if (host.relay_connection_status === "connected") {
106
+ return "connected";
107
+ }
108
+ if (host.relay_connection_status === "connecting") {
109
+ return "connecting";
110
+ }
111
+ if (host.relay_connection_status === "reconnecting") {
112
+ return "reconnecting";
113
+ }
114
+ if (host.relay_connection_status === "unavailable" ||
115
+ host.relay_connection_status === "stopped") {
116
+ return "unavailable";
117
+ }
118
+ if (host.relay_readiness === "connected") {
119
+ return "connected";
120
+ }
121
+ if (host.relay_readiness === "not_connected") {
122
+ return "unavailable";
123
+ }
124
+ return "unknown";
125
+ }
126
+ async function inspectHost(options) {
127
+ if (options.endpoint.kind === "missing") {
128
+ return { kind: "stopped" };
129
+ }
130
+ if (options.endpoint.kind === "invalid") {
131
+ return { kind: "stale", reason_code: "invalid_endpoint" };
132
+ }
133
+ const probe = await options.probeRuntimeEndpoint(options.endpoint.record);
134
+ if (probe.kind === "stale") {
135
+ return { kind: "stale", reason_code: "unreachable" };
136
+ }
137
+ try {
138
+ const status = await readHostLocalLaunchStatus({
139
+ endpoint: options.endpoint.record,
140
+ ...(options.fetchImpl === undefined ? {} : { fetchImpl: options.fetchImpl }),
141
+ });
142
+ if (status.project_id !== undefined && status.project_id !== options.projectId) {
143
+ return { kind: "identity_mismatch", actual_project_id: status.project_id };
144
+ }
145
+ return {
146
+ kind: "running",
147
+ identity_verified: status.project_id === options.projectId,
148
+ ...(probe.readiness?.relay === undefined ? {} : { relay_readiness: probe.readiness.relay }),
149
+ ...(status.relay_connection?.status === undefined
150
+ ? {}
151
+ : { relay_connection_status: status.relay_connection.status }),
152
+ };
153
+ }
154
+ catch {
155
+ return { kind: "stale", reason_code: "local_control_unavailable" };
156
+ }
157
+ }
158
+ export async function inspectMachineProject(projectId, options) {
159
+ const inventory = inspectMachineProjectInventory(projectId, options.tuttiHome);
160
+ const probeRuntimeEndpoint = options.probeRuntimeEndpoint ?? createRuntimeEndpointProbe(options.fetchImpl ?? fetch);
161
+ const host = await inspectHost({
162
+ projectId,
163
+ endpoint: inventory.endpoint,
164
+ probeRuntimeEndpoint,
165
+ ...(options.fetchImpl === undefined ? {} : { fetchImpl: options.fetchImpl }),
166
+ });
167
+ return {
168
+ ...inventory,
169
+ host,
170
+ relay: relayInspectionFromHost(host),
171
+ };
172
+ }
173
+ export function inspectMachineProjectInventory(projectId, tuttiHome) {
174
+ const binding = inspectBinding(tuttiHome, projectId);
175
+ const endpoint = inspectEndpoint(tuttiHome, projectId);
176
+ const workspaceRoot = binding.kind === "valid"
177
+ ? binding.record.workspace_root
178
+ : endpoint.kind === "valid"
179
+ ? endpoint.record.workspace_root
180
+ : undefined;
181
+ const workspace = inspectWorkspace(workspaceRoot, projectId);
182
+ const snapshotName = binding.kind === "valid" ? binding.record.display_name_snapshot : undefined;
183
+ const workspaceName = workspaceRoot === undefined ? undefined : basename(workspaceRoot);
184
+ const matchingDisplayName = workspace.kind === "matching" ? workspace.display_name : undefined;
185
+ return {
186
+ project_id: projectId,
187
+ binding,
188
+ endpoint,
189
+ workspace,
190
+ ...(workspaceRoot === undefined
191
+ ? {}
192
+ : {
193
+ workspace_root: workspaceRoot,
194
+ normalized_workspace_root: normalizeMachineWorkspacePath(workspaceRoot),
195
+ }),
196
+ display_name: matchingDisplayName ?? snapshotName ?? workspaceName ?? projectId,
197
+ };
198
+ }
199
+ export function inspectMachineProjectInventories(tuttiHome) {
200
+ return listMachineProjectIds(tuttiHome).map((projectId) => inspectMachineProjectInventory(projectId, tuttiHome));
201
+ }
202
+ export async function inspectMachineProjects(options) {
203
+ return await Promise.all(listMachineProjectIds(options.tuttiHome).map((projectId) => inspectMachineProject(projectId, options)));
204
+ }
205
+ //# sourceMappingURL=machine-project-inspector.js.map