@xfey/tutti 0.1.34 → 0.1.35

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.
@@ -68,6 +68,7 @@ export async function prepareLaunchProject(options) {
68
68
  tuttiHome,
69
69
  projectId,
70
70
  workspaceRoot,
71
+ relayUrl,
71
72
  displayNameSnapshot: readProjectDisplayName(workspaceRoot) ?? (basename(workspaceRoot) || projectId),
72
73
  };
73
74
  if (options.allowWorkspaceRootTakeover !== undefined) {
@@ -5,7 +5,9 @@ export type MachineProjectBindingRecord = {
5
5
  local_store_root: string;
6
6
  display_name_snapshot?: string;
7
7
  relay_project_ref?: RelayProjectRef;
8
+ relay_url?: string;
8
9
  host_registration_secret_ref?: string;
10
+ created_at?: string;
9
11
  updated_at: string;
10
12
  };
11
13
  export type HostRegistrationSecretFile = {
@@ -28,6 +30,7 @@ export type EnsureMachineProjectBindingOptions = {
28
30
  projectId: ProjectId;
29
31
  workspaceRoot: string;
30
32
  displayNameSnapshot?: string;
33
+ relayUrl?: string;
31
34
  allowWorkspaceRootTakeover?: boolean;
32
35
  now?: () => Date;
33
36
  };
@@ -78,7 +78,10 @@ export function readMachineProjectBinding(tuttiHome, projectId) {
78
78
  (typeof raw.relay_project_ref !== "string" ||
79
79
  !isPrefixedId(raw.relay_project_ref, ID_PREFIXES.relayProject))) ||
80
80
  (raw.host_registration_secret_ref !== undefined &&
81
- typeof raw.host_registration_secret_ref !== "string")) {
81
+ typeof raw.host_registration_secret_ref !== "string") ||
82
+ (raw.relay_url !== undefined &&
83
+ (typeof raw.relay_url !== "string" || raw.relay_url.trim() === "")) ||
84
+ (raw.created_at !== undefined && typeof raw.created_at !== "string")) {
82
85
  throw new LaunchError("project_identity_conflict", "Machine-local project binding is invalid or conflicts with the Git config project identity", "Inspect or remove the project binding under TUTTI_HOME after confirming it is not needed.");
83
86
  }
84
87
  const binding = {
@@ -93,9 +96,15 @@ export function readMachineProjectBinding(tuttiHome, projectId) {
93
96
  if (raw.relay_project_ref !== undefined) {
94
97
  binding.relay_project_ref = raw.relay_project_ref;
95
98
  }
99
+ if (raw.relay_url !== undefined) {
100
+ binding.relay_url = raw.relay_url;
101
+ }
96
102
  if (raw.host_registration_secret_ref !== undefined) {
97
103
  binding.host_registration_secret_ref = raw.host_registration_secret_ref;
98
104
  }
105
+ if (raw.created_at !== undefined) {
106
+ binding.created_at = raw.created_at;
107
+ }
99
108
  return binding;
100
109
  }
101
110
  export function readMachineRuntimeEndpoint(tuttiHome, projectId) {
@@ -226,7 +235,11 @@ function ensureHostRegistrationSecret(tuttiHome, projectId, now) {
226
235
  const secretPath = getHostRegistrationSecretPath(tuttiHome, projectId);
227
236
  const ref = createHostRegistrationSecretRef(projectId);
228
237
  if (existsSync(secretPath)) {
229
- return { ref, created: false };
238
+ return {
239
+ ref,
240
+ created: false,
241
+ createdAt: readHostRegistrationSecret(tuttiHome, projectId).created_at,
242
+ };
230
243
  }
231
244
  const timestamp = now().toISOString();
232
245
  const secretFile = {
@@ -237,7 +250,7 @@ function ensureHostRegistrationSecret(tuttiHome, projectId, now) {
237
250
  updated_at: timestamp,
238
251
  };
239
252
  writePrivateJsonFile(secretPath, secretFile);
240
- return { ref, created: true };
253
+ return { ref, created: true, createdAt: timestamp };
241
254
  }
242
255
  export function ensureMachineProjectBinding(options) {
243
256
  const now = options.now ?? (() => new Date());
@@ -256,6 +269,7 @@ export function ensureMachineProjectBinding(options) {
256
269
  workspace_root: workspaceRoot,
257
270
  local_store_root: localStoreRoot,
258
271
  host_registration_secret_ref: existing?.host_registration_secret_ref ?? secret.ref,
272
+ created_at: existing?.created_at ?? secret.createdAt,
259
273
  updated_at: now().toISOString(),
260
274
  };
261
275
  const displayNameSnapshot = options.displayNameSnapshot?.trim();
@@ -268,6 +282,13 @@ export function ensureMachineProjectBinding(options) {
268
282
  if (existing?.relay_project_ref !== undefined) {
269
283
  binding.relay_project_ref = existing.relay_project_ref;
270
284
  }
285
+ const relayUrl = options.relayUrl?.trim();
286
+ if (relayUrl !== undefined && relayUrl !== "") {
287
+ binding.relay_url = relayUrl;
288
+ }
289
+ else if (existing?.relay_url !== undefined) {
290
+ binding.relay_url = existing.relay_url;
291
+ }
271
292
  writePrivateJsonFile(bindingPath, binding);
272
293
  return {
273
294
  binding,
@@ -1,4 +1,4 @@
1
- import type { ProjectId } from "@tutti/shared/ids";
1
+ import type { ProjectId, RelayProjectRef } from "@tutti/shared/ids";
2
2
  import { type FetchLike } from "./host-runtime-endpoint.js";
3
3
  import { type MachineRuntimeEndpointRecord } from "./machine-local.js";
4
4
  import type { LocalControlFetch } from "./local-control-client.js";
@@ -9,7 +9,7 @@ export type RuntimeProjectRow = {
9
9
  workspace_root: string;
10
10
  endpoint?: string;
11
11
  provider_status?: string;
12
- relay_project_ref?: string;
12
+ relay_project_ref?: RelayProjectRef;
13
13
  join_url?: string;
14
14
  relay_connection_status?: string;
15
15
  last_connected_at?: string;
@@ -101,6 +101,10 @@ export async function listRuntimeProjects(options = {}) {
101
101
  if (launchStatus?.relay?.relay_project_ref !== undefined) {
102
102
  row.relay_project_ref = launchStatus.relay.relay_project_ref;
103
103
  }
104
+ else if (inspection.binding.kind === "valid" &&
105
+ inspection.binding.record.relay_project_ref !== undefined) {
106
+ row.relay_project_ref = inspection.binding.record.relay_project_ref;
107
+ }
104
108
  if (launchStatus?.relay?.join_url !== undefined) {
105
109
  row.join_url = launchStatus.relay.join_url;
106
110
  }
@@ -1,4 +1,4 @@
1
- import type { ProjectId } from "@tutti/shared/ids";
1
+ import { type ProjectId, type RelayProjectRef } from "@tutti/shared/ids";
2
2
  import { type RuntimeProjectRow } from "../cli/runtime-commands.js";
3
3
  import { type LocalConsoleInvocationContext } from "./invocation-context.js";
4
4
  export type LocalConsoleProject = {
@@ -7,9 +7,11 @@ export type LocalConsoleProject = {
7
7
  workspace_path: string;
8
8
  status: RuntimeProjectRow["status"];
9
9
  provider_status: "configured" | "not_configured" | "invalid";
10
- relay_project_ref?: string;
10
+ relay_project_ref?: RelayProjectRef;
11
11
  join_url?: string;
12
+ open_url?: string;
12
13
  relay_connection_status?: string;
14
+ created_at?: string;
13
15
  activity_at?: string;
14
16
  host_version?: string;
15
17
  update_required?: boolean;
@@ -21,6 +23,7 @@ export type LocalConsoleProviderInput = {
21
23
  };
22
24
  export type LocalConsoleLaunchResult = {
23
25
  project: LocalConsoleProject;
26
+ project_created: boolean;
24
27
  join_url: string;
25
28
  join_token_expires_at?: string;
26
29
  };
@@ -1,10 +1,11 @@
1
1
  import { existsSync, statSync } from "node:fs";
2
2
  import { resolve } from "node:path";
3
+ import { relayProjectRouteSegment } from "@tutti/shared/ids";
3
4
  import { configureProjectOpenAiProvider, discoverOpenAiModels, readOpenAiProviderConfigProjection, } from "../../providers/openai/index.js";
4
5
  import { formatCliErrorReason, LaunchError } from "../cli/errors.js";
5
- import { prepareLaunchProject } from "../cli/launch.js";
6
+ import { prepareLaunchProject, resolveRelayUrl } from "../cli/launch.js";
6
7
  import { rotateHostLocalInvite } from "../cli/local-control-client.js";
7
- import { readMachineProjectBinding, readMachineRuntimeEndpoint } from "../cli/machine-local.js";
8
+ import { readHostRegistrationSecret, readMachineProjectBinding, readMachineRuntimeEndpoint, } from "../cli/machine-local.js";
8
9
  import { spawnDetachedHost, waitForManagedHostReady } from "../cli/managed-host.js";
9
10
  import { resolveManagedProjectContext } from "../cli/project-resolver.js";
10
11
  import { listRuntimeProjects, runStopCommand, } from "../cli/runtime-commands.js";
@@ -47,8 +48,9 @@ function validateProvider(input) {
47
48
  }
48
49
  return provider;
49
50
  }
50
- function projectFromRuntimeRow(row, providerStatus, activityAt) {
51
+ function projectFromRuntimeRow(row, providerStatus, metadata) {
51
52
  const currentVersion = readCliVersion();
53
+ const openUrl = projectOpenUrl(metadata.relayUrl, row.relay_project_ref);
52
54
  return {
53
55
  project_id: row.project_id,
54
56
  display_name: row.display_name,
@@ -57,17 +59,31 @@ function projectFromRuntimeRow(row, providerStatus, activityAt) {
57
59
  provider_status: providerStatus,
58
60
  ...(row.relay_project_ref === undefined ? {} : { relay_project_ref: row.relay_project_ref }),
59
61
  ...(row.join_url === undefined ? {} : { join_url: row.join_url }),
62
+ ...(openUrl === undefined ? {} : { open_url: openUrl }),
60
63
  ...(row.relay_connection_status === undefined
61
64
  ? {}
62
65
  : { relay_connection_status: row.relay_connection_status }),
63
- ...(activityAt === undefined ? {} : { activity_at: activityAt }),
66
+ ...(metadata.activityAt === undefined ? {} : { activity_at: metadata.activityAt }),
67
+ ...(metadata.createdAt === undefined ? {} : { created_at: metadata.createdAt }),
64
68
  ...(row.host_version === undefined ? {} : { host_version: row.host_version }),
65
69
  ...(row.host_version === undefined || row.host_version === currentVersion
66
70
  ? {}
67
71
  : { update_required: true }),
68
72
  };
69
73
  }
70
- function readProjectActivityAt(tuttiHome, row) {
74
+ function projectOpenUrl(relayUrl, relayProjectRef) {
75
+ if (relayUrl === undefined || relayProjectRef === undefined) {
76
+ return undefined;
77
+ }
78
+ try {
79
+ const url = new URL(`/projects?open=${encodeURIComponent(relayProjectRouteSegment(relayProjectRef))}`, relayUrl);
80
+ return url.protocol === "http:" || url.protocol === "https:" ? url.toString() : undefined;
81
+ }
82
+ catch {
83
+ return undefined;
84
+ }
85
+ }
86
+ function readProjectActivityAt(tuttiHome, row, binding) {
71
87
  if (row.last_connected_at !== undefined) {
72
88
  return row.last_connected_at;
73
89
  }
@@ -81,7 +97,18 @@ function readProjectActivityAt(tuttiHome, row) {
81
97
  // Invalid endpoint state is already represented by the project status.
82
98
  }
83
99
  try {
84
- return readMachineProjectBinding(tuttiHome, row.project_id)?.updated_at;
100
+ return binding?.updated_at;
101
+ }
102
+ catch {
103
+ return undefined;
104
+ }
105
+ }
106
+ function relayUrlFromJoinUrl(joinUrl) {
107
+ if (joinUrl === undefined) {
108
+ return undefined;
109
+ }
110
+ try {
111
+ return new URL(joinUrl).origin;
85
112
  }
86
113
  catch {
87
114
  return undefined;
@@ -118,11 +145,32 @@ export class LocalConsoleProjectService {
118
145
  ...operation,
119
146
  });
120
147
  return rows.map((row) => {
148
+ let binding = null;
149
+ try {
150
+ binding = readMachineProjectBinding(this.#tuttiHome, row.project_id);
151
+ }
152
+ catch {
153
+ // Invalid binding state is already represented by the project status.
154
+ }
155
+ let createdAt = binding?.created_at;
156
+ if (createdAt === undefined) {
157
+ try {
158
+ createdAt = readHostRegistrationSecret(this.#tuttiHome, row.project_id).created_at;
159
+ }
160
+ catch {
161
+ // Damaged legacy metadata remains visible without an invented creation date.
162
+ }
163
+ }
121
164
  const provider = readOpenAiProviderConfigProjection({
122
165
  tuttiHome: this.#tuttiHome,
123
166
  projectId: row.project_id,
124
167
  });
125
- return projectFromRuntimeRow(row, provider.status, readProjectActivityAt(this.#tuttiHome, row));
168
+ const activityAt = readProjectActivityAt(this.#tuttiHome, row, binding);
169
+ return projectFromRuntimeRow(row, provider.status, {
170
+ ...(activityAt === undefined ? {} : { activityAt }),
171
+ ...(createdAt === undefined ? {} : { createdAt }),
172
+ relayUrl: binding?.relay_url ?? relayUrlFromJoinUrl(row.join_url) ?? resolveRelayUrl(operation.env),
173
+ });
126
174
  });
127
175
  }
128
176
  async discoverModels(input) {
@@ -200,6 +248,7 @@ export class LocalConsoleProjectService {
200
248
  }
201
249
  return {
202
250
  project,
251
+ project_created: preparation.project_identity_created,
203
252
  join_url: ready.join_url,
204
253
  ...(ready.join_token_expires_at === undefined
205
254
  ? {}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@xfey/tutti",
3
- "version": "0.1.34",
3
+ "version": "0.1.35",
4
4
  "type": "module",
5
5
  "main": "./dist/index.js",
6
6
  "types": "./dist/index.d.ts",