@xfey/tutti 0.1.34 → 0.1.36
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/dist/server-shell/cli/host-runtime-endpoint.d.ts +5 -0
- package/dist/server-shell/cli/host-runtime-endpoint.js +3 -0
- package/dist/server-shell/cli/host-server-runtime.d.ts +1 -1
- package/dist/server-shell/cli/host-server-runtime.js +7 -1
- package/dist/server-shell/cli/launch.js +1 -0
- package/dist/server-shell/cli/local-control-client.d.ts +5 -1
- package/dist/server-shell/cli/local-control-client.js +22 -0
- package/dist/server-shell/cli/machine-local.d.ts +3 -0
- package/dist/server-shell/cli/machine-local.js +24 -3
- package/dist/server-shell/cli/managed-host.d.ts +1 -0
- package/dist/server-shell/cli/managed-host.js +4 -1
- package/dist/server-shell/cli/runtime-commands.d.ts +2 -2
- package/dist/server-shell/cli/runtime-commands.js +4 -0
- package/dist/server-shell/http/routes/local-control.d.ts +2 -0
- package/dist/server-shell/http/routes/local-control.js +78 -1
- package/dist/server-shell/local-console/project-service.d.ts +14 -2
- package/dist/server-shell/local-console/project-service.js +172 -15
- package/dist/server-shell/local-console/server.js +4 -0
- package/package.json +1 -1
- package/web/assets/index-D9jQgROm.js +29 -0
- package/web/assets/{index-C7RDpM1L.css → index-DnBL91c3.css} +1 -1
- package/web/index.html +2 -2
- package/web/assets/index-B84rQ4YJ.js +0 -29
|
@@ -1,15 +1,18 @@
|
|
|
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 {
|
|
6
|
-
import {
|
|
7
|
-
import {
|
|
6
|
+
import { createRuntimeEndpointProbe, waitForHostShutdown, } from "../cli/host-runtime-endpoint.js";
|
|
7
|
+
import { prepareLaunchProject, resolveRelayUrl } from "../cli/launch.js";
|
|
8
|
+
import { requestHostLocalIdleShutdown, rotateHostLocalInvite, } from "../cli/local-control-client.js";
|
|
9
|
+
import { readHostRegistrationSecret, readMachineProjectBinding, readMachineRuntimeEndpoint, } from "../cli/machine-local.js";
|
|
8
10
|
import { spawnDetachedHost, waitForManagedHostReady } from "../cli/managed-host.js";
|
|
9
11
|
import { resolveManagedProjectContext } from "../cli/project-resolver.js";
|
|
10
12
|
import { listRuntimeProjects, runStopCommand, } from "../cli/runtime-commands.js";
|
|
11
13
|
import { readCliVersion } from "../cli/version.js";
|
|
12
14
|
import { createLocalConsoleOperationEnvironment, } from "./invocation-context.js";
|
|
15
|
+
const OPEN_UPDATE_SHUTDOWN_WAIT_MS = 5_000;
|
|
13
16
|
export class LocalConsoleProjectError extends Error {
|
|
14
17
|
code;
|
|
15
18
|
statusCode;
|
|
@@ -47,27 +50,43 @@ function validateProvider(input) {
|
|
|
47
50
|
}
|
|
48
51
|
return provider;
|
|
49
52
|
}
|
|
50
|
-
function projectFromRuntimeRow(row, providerStatus,
|
|
53
|
+
function projectFromRuntimeRow(row, providerStatus, providerModel, metadata) {
|
|
51
54
|
const currentVersion = readCliVersion();
|
|
55
|
+
const openUrl = projectOpenUrl(metadata.relayUrl, row.relay_project_ref);
|
|
52
56
|
return {
|
|
53
57
|
project_id: row.project_id,
|
|
54
58
|
display_name: row.display_name,
|
|
55
59
|
workspace_path: row.workspace_root,
|
|
56
60
|
status: row.status,
|
|
57
61
|
provider_status: providerStatus,
|
|
62
|
+
...(providerModel === undefined ? {} : { provider_model: providerModel }),
|
|
58
63
|
...(row.relay_project_ref === undefined ? {} : { relay_project_ref: row.relay_project_ref }),
|
|
59
64
|
...(row.join_url === undefined ? {} : { join_url: row.join_url }),
|
|
65
|
+
...(openUrl === undefined ? {} : { open_url: openUrl }),
|
|
60
66
|
...(row.relay_connection_status === undefined
|
|
61
67
|
? {}
|
|
62
68
|
: { relay_connection_status: row.relay_connection_status }),
|
|
63
|
-
...(activityAt === undefined ? {} : { activity_at: activityAt }),
|
|
69
|
+
...(metadata.activityAt === undefined ? {} : { activity_at: metadata.activityAt }),
|
|
70
|
+
...(metadata.createdAt === undefined ? {} : { created_at: metadata.createdAt }),
|
|
64
71
|
...(row.host_version === undefined ? {} : { host_version: row.host_version }),
|
|
65
72
|
...(row.host_version === undefined || row.host_version === currentVersion
|
|
66
73
|
? {}
|
|
67
74
|
: { update_required: true }),
|
|
68
75
|
};
|
|
69
76
|
}
|
|
70
|
-
function
|
|
77
|
+
function projectOpenUrl(relayUrl, relayProjectRef) {
|
|
78
|
+
if (relayUrl === undefined || relayProjectRef === undefined) {
|
|
79
|
+
return undefined;
|
|
80
|
+
}
|
|
81
|
+
try {
|
|
82
|
+
const url = new URL(`/projects?open=${encodeURIComponent(relayProjectRouteSegment(relayProjectRef))}`, relayUrl);
|
|
83
|
+
return url.protocol === "http:" || url.protocol === "https:" ? url.toString() : undefined;
|
|
84
|
+
}
|
|
85
|
+
catch {
|
|
86
|
+
return undefined;
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
function readProjectActivityAt(tuttiHome, row, binding) {
|
|
71
90
|
if (row.last_connected_at !== undefined) {
|
|
72
91
|
return row.last_connected_at;
|
|
73
92
|
}
|
|
@@ -81,7 +100,18 @@ function readProjectActivityAt(tuttiHome, row) {
|
|
|
81
100
|
// Invalid endpoint state is already represented by the project status.
|
|
82
101
|
}
|
|
83
102
|
try {
|
|
84
|
-
return
|
|
103
|
+
return binding?.updated_at;
|
|
104
|
+
}
|
|
105
|
+
catch {
|
|
106
|
+
return undefined;
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
function relayUrlFromJoinUrl(joinUrl) {
|
|
110
|
+
if (joinUrl === undefined) {
|
|
111
|
+
return undefined;
|
|
112
|
+
}
|
|
113
|
+
try {
|
|
114
|
+
return new URL(joinUrl).origin;
|
|
85
115
|
}
|
|
86
116
|
catch {
|
|
87
117
|
return undefined;
|
|
@@ -90,11 +120,13 @@ function readProjectActivityAt(tuttiHome, row) {
|
|
|
90
120
|
export class LocalConsoleProjectService {
|
|
91
121
|
#tuttiHome;
|
|
92
122
|
#serviceEnvironment;
|
|
123
|
+
#fetchImpl;
|
|
93
124
|
#launches = new Map();
|
|
94
125
|
#mutationTail = Promise.resolve();
|
|
95
126
|
constructor(options) {
|
|
96
127
|
this.#tuttiHome = resolve(options.tuttiHome);
|
|
97
128
|
this.#serviceEnvironment = options.serviceEnvironment ?? process.env;
|
|
129
|
+
this.#fetchImpl = options.fetchImpl ?? fetch;
|
|
98
130
|
}
|
|
99
131
|
#operationOptions(context) {
|
|
100
132
|
return {
|
|
@@ -115,14 +147,58 @@ export class LocalConsoleProjectService {
|
|
|
115
147
|
const operation = this.#operationOptions(context);
|
|
116
148
|
const rows = await listRuntimeProjects({
|
|
117
149
|
all: true,
|
|
150
|
+
fetchImpl: this.#fetchImpl,
|
|
118
151
|
...operation,
|
|
119
152
|
});
|
|
120
153
|
return rows.map((row) => {
|
|
154
|
+
let binding = null;
|
|
155
|
+
try {
|
|
156
|
+
binding = readMachineProjectBinding(this.#tuttiHome, row.project_id);
|
|
157
|
+
}
|
|
158
|
+
catch {
|
|
159
|
+
// Invalid binding state is already represented by the project status.
|
|
160
|
+
}
|
|
161
|
+
let createdAt = binding?.created_at;
|
|
162
|
+
if (createdAt === undefined) {
|
|
163
|
+
try {
|
|
164
|
+
createdAt = readHostRegistrationSecret(this.#tuttiHome, row.project_id).created_at;
|
|
165
|
+
}
|
|
166
|
+
catch {
|
|
167
|
+
// Damaged legacy metadata remains visible without an invented creation date.
|
|
168
|
+
}
|
|
169
|
+
}
|
|
121
170
|
const provider = readOpenAiProviderConfigProjection({
|
|
122
171
|
tuttiHome: this.#tuttiHome,
|
|
123
172
|
projectId: row.project_id,
|
|
124
173
|
});
|
|
125
|
-
|
|
174
|
+
const providerModel = provider.status === "not_configured" ? undefined : provider.default_model;
|
|
175
|
+
const activityAt = readProjectActivityAt(this.#tuttiHome, row, binding);
|
|
176
|
+
return projectFromRuntimeRow(row, provider.status, providerModel, {
|
|
177
|
+
...(activityAt === undefined ? {} : { activityAt }),
|
|
178
|
+
...(createdAt === undefined ? {} : { createdAt }),
|
|
179
|
+
relayUrl: binding?.relay_url ?? relayUrlFromJoinUrl(row.join_url) ?? resolveRelayUrl(operation.env),
|
|
180
|
+
});
|
|
181
|
+
});
|
|
182
|
+
}
|
|
183
|
+
async #readProject(projectId, context) {
|
|
184
|
+
const project = (await this.listProjects(context)).find((candidate) => candidate.project_id === projectId);
|
|
185
|
+
if (project === undefined) {
|
|
186
|
+
throw new LocalConsoleProjectError("project_not_found", "The project could not be read from the machine project list.", 404);
|
|
187
|
+
}
|
|
188
|
+
return project;
|
|
189
|
+
}
|
|
190
|
+
async #startManagedHost(options) {
|
|
191
|
+
spawnDetachedHost({
|
|
192
|
+
workspaceRoot: options.workspaceRoot,
|
|
193
|
+
env: options.env,
|
|
194
|
+
inheritProcessEnv: false,
|
|
195
|
+
});
|
|
196
|
+
return await waitForManagedHostReady({
|
|
197
|
+
tuttiHome: options.tuttiHome,
|
|
198
|
+
projectId: options.projectId,
|
|
199
|
+
workspaceRoot: options.workspaceRoot,
|
|
200
|
+
fetchImpl: this.#fetchImpl,
|
|
201
|
+
...(options.inviteMode === undefined ? {} : { inviteMode: options.inviteMode }),
|
|
126
202
|
});
|
|
127
203
|
}
|
|
128
204
|
async discoverModels(input) {
|
|
@@ -180,15 +256,11 @@ export class LocalConsoleProjectService {
|
|
|
180
256
|
throw new LocalConsoleProjectError("provider_configuration_required", "This project does not have a valid saved Provider. Configure it in the launch form.");
|
|
181
257
|
}
|
|
182
258
|
try {
|
|
183
|
-
|
|
259
|
+
const ready = await this.#startManagedHost({
|
|
184
260
|
workspaceRoot: preparation.workspace_root,
|
|
185
|
-
env: operation.env,
|
|
186
|
-
inheritProcessEnv: false,
|
|
187
|
-
});
|
|
188
|
-
const ready = await waitForManagedHostReady({
|
|
189
261
|
tuttiHome: preparation.tutti_home,
|
|
190
262
|
projectId: preparation.project_id,
|
|
191
|
-
|
|
263
|
+
env: operation.env,
|
|
192
264
|
});
|
|
193
265
|
if (ready.join_url === undefined) {
|
|
194
266
|
throw new LocalConsoleProjectError("relay_registration_failed", "The host started, but Relay did not return a visible join link.");
|
|
@@ -200,6 +272,7 @@ export class LocalConsoleProjectService {
|
|
|
200
272
|
}
|
|
201
273
|
return {
|
|
202
274
|
project,
|
|
275
|
+
project_created: preparation.project_identity_created,
|
|
203
276
|
join_url: ready.join_url,
|
|
204
277
|
...(ready.join_token_expires_at === undefined
|
|
205
278
|
? {}
|
|
@@ -210,6 +283,87 @@ export class LocalConsoleProjectService {
|
|
|
210
283
|
throw this.#normalizeError(error);
|
|
211
284
|
}
|
|
212
285
|
}
|
|
286
|
+
async openProject(projectId, context) {
|
|
287
|
+
return await this.#runMutation(async () => {
|
|
288
|
+
const current = await this.#readProject(projectId, context);
|
|
289
|
+
if (current.open_url === undefined) {
|
|
290
|
+
throw new LocalConsoleProjectError("project_open_unavailable", "This project does not have a Relay destination yet.");
|
|
291
|
+
}
|
|
292
|
+
if (current.update_required !== true) {
|
|
293
|
+
return {
|
|
294
|
+
project: current,
|
|
295
|
+
open_url: current.open_url,
|
|
296
|
+
disposition: "opened_current",
|
|
297
|
+
};
|
|
298
|
+
}
|
|
299
|
+
let managed;
|
|
300
|
+
try {
|
|
301
|
+
managed = resolveManagedProjectContext({
|
|
302
|
+
target: projectId,
|
|
303
|
+
...this.#operationOptions(context),
|
|
304
|
+
});
|
|
305
|
+
}
|
|
306
|
+
catch (error) {
|
|
307
|
+
throw this.#normalizeError(error);
|
|
308
|
+
}
|
|
309
|
+
if (!managed.workspace_exists ||
|
|
310
|
+
managed.runtime_endpoint === null ||
|
|
311
|
+
managed.provider_config.status !== "configured") {
|
|
312
|
+
return {
|
|
313
|
+
project: current,
|
|
314
|
+
open_url: current.open_url,
|
|
315
|
+
disposition: "deferred_unavailable",
|
|
316
|
+
};
|
|
317
|
+
}
|
|
318
|
+
let shutdownDisposition;
|
|
319
|
+
try {
|
|
320
|
+
shutdownDisposition = await requestHostLocalIdleShutdown({
|
|
321
|
+
endpoint: managed.runtime_endpoint,
|
|
322
|
+
fetchImpl: this.#fetchImpl,
|
|
323
|
+
});
|
|
324
|
+
}
|
|
325
|
+
catch {
|
|
326
|
+
return {
|
|
327
|
+
project: current,
|
|
328
|
+
open_url: current.open_url,
|
|
329
|
+
disposition: "deferred_unavailable",
|
|
330
|
+
};
|
|
331
|
+
}
|
|
332
|
+
if (shutdownDisposition !== "accepted") {
|
|
333
|
+
return {
|
|
334
|
+
project: current,
|
|
335
|
+
open_url: current.open_url,
|
|
336
|
+
disposition: shutdownDisposition === "busy" ? "deferred_busy" : "deferred_unsupported",
|
|
337
|
+
};
|
|
338
|
+
}
|
|
339
|
+
try {
|
|
340
|
+
await waitForHostShutdown({
|
|
341
|
+
endpoint: managed.runtime_endpoint,
|
|
342
|
+
probeRuntimeEndpoint: createRuntimeEndpointProbe(this.#fetchImpl),
|
|
343
|
+
shutdownWaitMs: OPEN_UPDATE_SHUTDOWN_WAIT_MS,
|
|
344
|
+
});
|
|
345
|
+
await this.#startManagedHost({
|
|
346
|
+
workspaceRoot: managed.workspace_root,
|
|
347
|
+
tuttiHome: managed.tutti_home,
|
|
348
|
+
projectId: managed.project_id,
|
|
349
|
+
env: this.#operationOptions(context).env,
|
|
350
|
+
inviteMode: "preserve",
|
|
351
|
+
});
|
|
352
|
+
const updated = await this.#readProject(projectId, context);
|
|
353
|
+
if (updated.open_url === undefined) {
|
|
354
|
+
throw new LocalConsoleProjectError("project_open_unavailable", "The updated project does not have a Relay destination.");
|
|
355
|
+
}
|
|
356
|
+
return {
|
|
357
|
+
project: updated,
|
|
358
|
+
open_url: updated.open_url,
|
|
359
|
+
disposition: "updated",
|
|
360
|
+
};
|
|
361
|
+
}
|
|
362
|
+
catch (error) {
|
|
363
|
+
throw this.#normalizeError(error);
|
|
364
|
+
}
|
|
365
|
+
});
|
|
366
|
+
}
|
|
213
367
|
async refreshInvite(projectId, context) {
|
|
214
368
|
return await this.#runMutation(async () => {
|
|
215
369
|
const project = resolveManagedProjectContext({
|
|
@@ -220,7 +374,10 @@ export class LocalConsoleProjectService {
|
|
|
220
374
|
throw new LocalConsoleProjectError("host_not_running", "Launch this project before creating an invite link.");
|
|
221
375
|
}
|
|
222
376
|
try {
|
|
223
|
-
const status = await rotateHostLocalInvite({
|
|
377
|
+
const status = await rotateHostLocalInvite({
|
|
378
|
+
endpoint: project.runtime_endpoint,
|
|
379
|
+
fetchImpl: this.#fetchImpl,
|
|
380
|
+
});
|
|
224
381
|
const joinUrl = status.relay?.join_url;
|
|
225
382
|
if (joinUrl === undefined) {
|
|
226
383
|
throw new LocalConsoleProjectError("relay_registration_failed", "Relay did not return a visible invite link.");
|
|
@@ -276,6 +276,10 @@ export function createLocalConsoleServer(options) {
|
|
|
276
276
|
...(request.body.provider === undefined ? {} : { provider: request.body.provider }),
|
|
277
277
|
}, session.context);
|
|
278
278
|
});
|
|
279
|
+
app.post(`${LOCAL_CONSOLE_API_BASE}/projects/:projectId/open`, async (request) => {
|
|
280
|
+
const session = requireSession(request, true);
|
|
281
|
+
return await projects.openProject(request.params.projectId, session.context);
|
|
282
|
+
});
|
|
279
283
|
app.post(`${LOCAL_CONSOLE_API_BASE}/projects/:projectId/invite`, async (request) => {
|
|
280
284
|
const session = requireSession(request, true);
|
|
281
285
|
return await projects.refreshInvite(request.params.projectId, session.context);
|