@xfey/tutti 0.1.33 → 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.
Files changed (37) hide show
  1. package/README.md +3 -3
  2. package/dist/server-shell/cli/args.d.ts +6 -0
  3. package/dist/server-shell/cli/args.js +30 -0
  4. package/dist/server-shell/cli/cli.js +118 -6
  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/launch.js +1 -0
  9. package/dist/server-shell/cli/machine-local.d.ts +3 -0
  10. package/dist/server-shell/cli/machine-local.js +24 -3
  11. package/dist/server-shell/cli/machine-project-inspector.d.ts +1 -0
  12. package/dist/server-shell/cli/machine-project-inspector.js +1 -0
  13. package/dist/server-shell/cli/managed-host.d.ts +1 -0
  14. package/dist/server-shell/cli/managed-host.js +6 -4
  15. package/dist/server-shell/cli/runtime-commands.d.ts +3 -2
  16. package/dist/server-shell/cli/runtime-commands.js +9 -1
  17. package/dist/server-shell/local-console/folder-picker.d.ts +0 -1
  18. package/dist/server-shell/local-console/folder-picker.js +2 -33
  19. package/dist/server-shell/local-console/invocation-context.d.ts +21 -0
  20. package/dist/server-shell/local-console/invocation-context.js +56 -0
  21. package/dist/server-shell/local-console/lifecycle-lock.d.ts +13 -0
  22. package/dist/server-shell/local-console/lifecycle-lock.js +135 -0
  23. package/dist/server-shell/local-console/managed-console.d.ts +38 -0
  24. package/dist/server-shell/local-console/managed-console.js +254 -36
  25. package/dist/server-shell/local-console/project-service.d.ts +16 -9
  26. package/dist/server-shell/local-console/project-service.js +152 -58
  27. package/dist/server-shell/local-console/runtime-endpoint.d.ts +15 -1
  28. package/dist/server-shell/local-console/runtime-endpoint.js +21 -5
  29. package/dist/server-shell/local-console/server.d.ts +12 -0
  30. package/dist/server-shell/local-console/server.js +152 -45
  31. package/dist/server-shell/local-console/session.d.ts +10 -15
  32. package/dist/server-shell/local-console/session.js +69 -17
  33. package/package.json +1 -1
  34. package/web/assets/index-DPuaTngs.js +29 -0
  35. package/web/assets/{index-BcZpeSaX.css → index-Dmbchapb.css} +1 -1
  36. package/web/index.html +2 -2
  37. package/web/assets/index-DKf4jz_E.js +0 -29
@@ -0,0 +1,135 @@
1
+ import { randomBytes } from "node:crypto";
2
+ import { chmodSync, closeSync, constants, mkdirSync, openSync, readFileSync, statSync, unlinkSync, writeFileSync, } from "node:fs";
3
+ import { dirname, join } from "node:path";
4
+ const LOCK_STALE_MS = 30_000;
5
+ const LOCK_WAIT_MS = 35_000;
6
+ const LOCK_POLL_MS = 100;
7
+ function delay(milliseconds) {
8
+ return new Promise((resolveDelay) => setTimeout(resolveDelay, milliseconds));
9
+ }
10
+ function lockPath(tuttiHome) {
11
+ return join(tuttiHome, "runtime", "local-console.lock");
12
+ }
13
+ function ensurePrivateRuntimeDirectory(tuttiHome) {
14
+ const directory = dirname(lockPath(tuttiHome));
15
+ mkdirSync(directory, { recursive: true, mode: 0o700 });
16
+ chmodSync(directory, 0o700);
17
+ }
18
+ function lockOwnerAlive(path) {
19
+ try {
20
+ const value = JSON.parse(readFileSync(path, "utf8"));
21
+ if (!Number.isInteger(value.pid)) {
22
+ return false;
23
+ }
24
+ process.kill(value.pid, 0);
25
+ return true;
26
+ }
27
+ catch (error) {
28
+ return (typeof error === "object" &&
29
+ error !== null &&
30
+ "code" in error &&
31
+ error.code === "EPERM");
32
+ }
33
+ }
34
+ function removeStaleLock(path, now, staleMs) {
35
+ try {
36
+ const age = now() - statSync(path).mtimeMs;
37
+ if (age < staleMs || lockOwnerAlive(path)) {
38
+ return false;
39
+ }
40
+ unlinkSync(path);
41
+ return true;
42
+ }
43
+ catch (error) {
44
+ if (typeof error === "object" &&
45
+ error !== null &&
46
+ "code" in error &&
47
+ error.code === "ENOENT") {
48
+ return true;
49
+ }
50
+ return false;
51
+ }
52
+ }
53
+ async function acquireLock(options) {
54
+ const now = options.now ?? Date.now;
55
+ const timing = options.timing ?? {
56
+ staleMs: LOCK_STALE_MS,
57
+ waitMs: LOCK_WAIT_MS,
58
+ pollMs: LOCK_POLL_MS,
59
+ };
60
+ const path = lockPath(options.tuttiHome);
61
+ const deadline = now() + timing.waitMs;
62
+ const ownerId = randomBytes(16).toString("base64url");
63
+ ensurePrivateRuntimeDirectory(options.tuttiHome);
64
+ while (now() < deadline) {
65
+ try {
66
+ const descriptor = openSync(path, constants.O_CREAT | constants.O_EXCL | constants.O_WRONLY, 0o600);
67
+ try {
68
+ writeFileSync(descriptor, `${JSON.stringify({
69
+ pid: process.pid,
70
+ owner_id: ownerId,
71
+ acquired_at: new Date(now()).toISOString(),
72
+ })}\n`, "utf8");
73
+ }
74
+ finally {
75
+ closeSync(descriptor);
76
+ }
77
+ chmodSync(path, 0o600);
78
+ return () => {
79
+ try {
80
+ let record;
81
+ try {
82
+ record = JSON.parse(readFileSync(path, "utf8"));
83
+ }
84
+ catch {
85
+ return;
86
+ }
87
+ if (record.owner_id !== ownerId) {
88
+ return;
89
+ }
90
+ unlinkSync(path);
91
+ }
92
+ catch (error) {
93
+ if (typeof error !== "object" ||
94
+ error === null ||
95
+ !("code" in error) ||
96
+ error.code !== "ENOENT") {
97
+ throw error;
98
+ }
99
+ }
100
+ };
101
+ }
102
+ catch (error) {
103
+ if (typeof error !== "object" ||
104
+ error === null ||
105
+ !("code" in error) ||
106
+ error.code !== "EEXIST") {
107
+ throw error;
108
+ }
109
+ removeStaleLock(path, now, timing.staleMs);
110
+ await delay(timing.pollMs);
111
+ }
112
+ }
113
+ throw new Error("Timed out waiting for the Tutti service lifecycle lock.");
114
+ }
115
+ export async function withLocalConsoleLifecycleLock(options) {
116
+ const timing = options.timing === undefined
117
+ ? undefined
118
+ : {
119
+ staleMs: options.timing.staleMs ?? LOCK_STALE_MS,
120
+ waitMs: options.timing.waitMs ?? LOCK_WAIT_MS,
121
+ pollMs: options.timing.pollMs ?? LOCK_POLL_MS,
122
+ };
123
+ const release = await acquireLock({
124
+ tuttiHome: options.tuttiHome,
125
+ ...(options.now === undefined ? {} : { now: options.now }),
126
+ ...(timing === undefined ? {} : { timing }),
127
+ });
128
+ try {
129
+ return await options.run();
130
+ }
131
+ finally {
132
+ release();
133
+ }
134
+ }
135
+ //# sourceMappingURL=lifecycle-lock.js.map
@@ -1,9 +1,47 @@
1
1
  import { type LocalConsoleRuntimeEndpoint } from "./runtime-endpoint.js";
2
+ type FetchLike = (input: string | URL, init?: RequestInit) => Promise<Response>;
3
+ export type LocalConsoleServiceStatus = {
4
+ status: "stopped";
5
+ log_path: string;
6
+ } | {
7
+ status: "stale";
8
+ pid: number;
9
+ base_url: string;
10
+ started_at: string;
11
+ log_path: string;
12
+ } | {
13
+ status: "running";
14
+ pid: number;
15
+ base_url: string;
16
+ started_at: string;
17
+ version: string;
18
+ current_version: string;
19
+ update_required: boolean;
20
+ log_path: string;
21
+ };
22
+ export declare function getLocalConsoleLogPath(tuttiHome: string): string;
2
23
  export declare function ensureLocalConsole(options?: {
3
24
  cwd?: string;
4
25
  env?: NodeJS.ProcessEnv;
26
+ fetchImpl?: FetchLike;
5
27
  }): Promise<{
6
28
  url: string;
7
29
  endpoint: LocalConsoleRuntimeEndpoint;
8
30
  }>;
31
+ export declare function readLocalConsoleServiceStatus(options?: {
32
+ cwd?: string;
33
+ env?: NodeJS.ProcessEnv;
34
+ fetchImpl?: FetchLike;
35
+ }): Promise<LocalConsoleServiceStatus>;
36
+ export declare function stopLocalConsoleService(options?: {
37
+ cwd?: string;
38
+ env?: NodeJS.ProcessEnv;
39
+ fetchImpl?: FetchLike;
40
+ }): Promise<"stopped" | "already_stopped">;
41
+ export declare function restartLocalConsoleService(options?: {
42
+ cwd?: string;
43
+ env?: NodeJS.ProcessEnv;
44
+ fetchImpl?: FetchLike;
45
+ }): Promise<LocalConsoleRuntimeEndpoint>;
46
+ export {};
9
47
  //# sourceMappingURL=managed-console.d.ts.map
@@ -1,10 +1,16 @@
1
1
  import { spawn } from "node:child_process";
2
- import { resolve } from "node:path";
2
+ import { chmodSync, closeSync, existsSync, mkdirSync, openSync, renameSync, statSync, unlinkSync, } from "node:fs";
3
+ import { dirname, join, resolve } from "node:path";
3
4
  import { resolveTuttiHome } from "../../providers/openai/index.js";
5
+ import { readCliVersion } from "../cli/version.js";
6
+ import { createLocalConsoleInvocationContext, createLocalConsoleServiceEnvironment, } from "./invocation-context.js";
7
+ import { withLocalConsoleLifecycleLock } from "./lifecycle-lock.js";
4
8
  import { deleteLocalConsoleRuntimeEndpoint, readLocalConsoleRuntimeEndpoint, } from "./runtime-endpoint.js";
5
9
  import { LOCAL_CONSOLE_API_BASE } from "./server.js";
6
10
  const READY_TIMEOUT_MS = 15_000;
7
11
  const READY_POLL_MS = 150;
12
+ const STOP_TIMEOUT_MS = 5_000;
13
+ const CONSOLE_LOG_MAX_BYTES = 2 * 1024 * 1024;
8
14
  function delay(milliseconds) {
9
15
  return new Promise((resolveDelay) => setTimeout(resolveDelay, milliseconds));
10
16
  }
@@ -13,82 +19,294 @@ function cliEntrypoint() {
13
19
  if (entrypoint === undefined || entrypoint.trim() === "") {
14
20
  throw new Error("Cannot locate the Tutti CLI entrypoint.");
15
21
  }
16
- return entrypoint;
22
+ return resolve(entrypoint);
17
23
  }
18
- async function endpointReady(endpoint) {
24
+ export function getLocalConsoleLogPath(tuttiHome) {
25
+ return join(tuttiHome, "logs", "console.log");
26
+ }
27
+ function prepareConsoleLog(tuttiHome) {
28
+ const path = getLocalConsoleLogPath(tuttiHome);
29
+ mkdirSync(dirname(path), { recursive: true, mode: 0o700 });
30
+ chmodSync(dirname(path), 0o700);
31
+ if (existsSync(path) && statSync(path).size >= CONSOLE_LOG_MAX_BYTES) {
32
+ const previousPath = `${path}.1`;
33
+ if (existsSync(previousPath)) {
34
+ unlinkSync(previousPath);
35
+ }
36
+ renameSync(path, previousPath);
37
+ }
38
+ const descriptor = openSync(path, "a", 0o600);
39
+ chmodSync(path, 0o600);
40
+ return descriptor;
41
+ }
42
+ async function endpointHealth(endpoint, fetchImpl) {
19
43
  try {
20
- const response = await fetch(`${endpoint.base_url}${LOCAL_CONSOLE_API_BASE}/health`, {
44
+ const response = await fetchImpl(`${endpoint.base_url}${LOCAL_CONSOLE_API_BASE}/health`, {
21
45
  signal: AbortSignal.timeout(1_000),
22
46
  });
23
47
  if (!response.ok) {
24
- return false;
48
+ return { kind: "stale" };
25
49
  }
26
50
  const value = (await response.json());
27
- return value.status === "ready";
51
+ if (value.status !== "ready") {
52
+ return { kind: "stale" };
53
+ }
54
+ if (endpoint.schema_version === 1) {
55
+ return { kind: "ready", identityVerified: false };
56
+ }
57
+ return {
58
+ kind: "ready",
59
+ identityVerified: value.instance_id === endpoint.instance_id && value.pid === endpoint.pid,
60
+ };
28
61
  }
29
62
  catch {
30
- return false;
63
+ return { kind: "stale" };
31
64
  }
32
65
  }
66
+ function endpointMatchesCurrentRuntime(endpoint) {
67
+ return (endpoint.schema_version === 2 &&
68
+ endpoint.version === readCliVersion() &&
69
+ resolve(endpoint.entrypoint) === cliEntrypoint());
70
+ }
33
71
  function spawnDetachedConsole(options) {
34
- const child = spawn(process.execPath, [...process.execArgv, cliEntrypoint(), "internal", "console-run"], {
35
- cwd: options.cwd,
36
- detached: true,
37
- env: options.env,
38
- stdio: "ignore",
39
- });
40
- child.unref();
72
+ const logDescriptor = prepareConsoleLog(options.tuttiHome);
73
+ try {
74
+ const child = spawn(process.execPath, [...process.execArgv, cliEntrypoint(), "internal", "console-run"], {
75
+ cwd: options.cwd,
76
+ detached: true,
77
+ env: createLocalConsoleServiceEnvironment({
78
+ env: options.env,
79
+ tuttiHome: options.tuttiHome,
80
+ }),
81
+ stdio: ["ignore", logDescriptor, logDescriptor],
82
+ });
83
+ child.on("error", () => undefined);
84
+ child.unref();
85
+ return child;
86
+ }
87
+ finally {
88
+ closeSync(logDescriptor);
89
+ }
41
90
  }
42
- async function waitForConsoleEndpoint(tuttiHome) {
91
+ async function waitForConsoleEndpoint(options) {
43
92
  const deadline = Date.now() + READY_TIMEOUT_MS;
44
93
  while (Date.now() < deadline) {
45
- const endpoint = readLocalConsoleRuntimeEndpoint(tuttiHome);
46
- if (endpoint !== null && (await endpointReady(endpoint))) {
47
- return endpoint;
94
+ const endpoint = readLocalConsoleRuntimeEndpoint(options.tuttiHome);
95
+ if (endpoint?.schema_version === 2 && endpointMatchesCurrentRuntime(endpoint)) {
96
+ const health = await endpointHealth(endpoint, options.fetchImpl);
97
+ if (health.kind === "ready" && health.identityVerified) {
98
+ return endpoint;
99
+ }
48
100
  }
49
101
  await delay(READY_POLL_MS);
50
102
  }
51
- throw new Error("Tutti Local Console did not become ready. Run `tutti doctor` for diagnostics.");
103
+ throw new Error(`Tutti service did not become ready. Inspect ${getLocalConsoleLogPath(options.tuttiHome)}.`);
52
104
  }
53
- async function ensureConsoleEndpoint(options) {
54
- const existing = readLocalConsoleRuntimeEndpoint(options.tuttiHome);
55
- if (existing !== null && (await endpointReady(existing))) {
56
- return existing;
105
+ async function waitForEndpointToStop(options) {
106
+ const deadline = Date.now() + STOP_TIMEOUT_MS;
107
+ while (Date.now() < deadline) {
108
+ const current = readLocalConsoleRuntimeEndpoint(options.tuttiHome);
109
+ if (current === null || current.control_token !== options.endpoint.control_token) {
110
+ return true;
111
+ }
112
+ if ((await endpointHealth(options.endpoint, options.fetchImpl)).kind === "stale") {
113
+ deleteLocalConsoleRuntimeEndpoint({
114
+ tuttiHome: options.tuttiHome,
115
+ expectedControlToken: options.endpoint.control_token,
116
+ });
117
+ return true;
118
+ }
119
+ await delay(100);
57
120
  }
58
- if (existing !== null) {
121
+ return false;
122
+ }
123
+ async function authenticateLegacyEndpoint(options) {
124
+ try {
125
+ const response = await options.fetchImpl(`${options.endpoint.base_url}${LOCAL_CONSOLE_API_BASE}/access-tokens`, {
126
+ method: "POST",
127
+ headers: {
128
+ authorization: `Bearer ${options.endpoint.control_token}`,
129
+ "content-type": "application/json",
130
+ },
131
+ body: JSON.stringify({ current_directory: options.cwd }),
132
+ signal: AbortSignal.timeout(2_000),
133
+ });
134
+ return response.ok;
135
+ }
136
+ catch {
137
+ return false;
138
+ }
139
+ }
140
+ async function stopEndpoint(options) {
141
+ const health = await endpointHealth(options.endpoint, options.fetchImpl);
142
+ if (health.kind === "stale") {
59
143
  deleteLocalConsoleRuntimeEndpoint({
60
144
  tuttiHome: options.tuttiHome,
61
- expectedControlToken: existing.control_token,
145
+ expectedControlToken: options.endpoint.control_token,
146
+ });
147
+ return;
148
+ }
149
+ if (options.endpoint.schema_version === 2 && !health.identityVerified) {
150
+ throw new Error("The running Tutti service identity does not match its endpoint record.");
151
+ }
152
+ let shutdownAccepted = false;
153
+ try {
154
+ const response = await options.fetchImpl(`${options.endpoint.base_url}${LOCAL_CONSOLE_API_BASE}/control/shutdown`, {
155
+ method: "POST",
156
+ headers: { authorization: `Bearer ${options.endpoint.control_token}` },
157
+ signal: AbortSignal.timeout(2_000),
62
158
  });
159
+ shutdownAccepted = response.ok;
160
+ }
161
+ catch {
162
+ // A legacy service is handled by the authenticated SIGTERM fallback below.
163
+ }
164
+ if (!shutdownAccepted) {
165
+ const authenticated = await authenticateLegacyEndpoint(options);
166
+ if (!authenticated) {
167
+ throw new Error("The running Tutti service identity could not be verified.");
168
+ }
169
+ process.kill(options.endpoint.pid, "SIGTERM");
170
+ }
171
+ if (!(await waitForEndpointToStop(options))) {
172
+ throw new Error("The Tutti service did not stop before the shutdown timeout.");
173
+ }
174
+ }
175
+ async function ensureConsoleEndpointUnlocked(options) {
176
+ const existing = readLocalConsoleRuntimeEndpoint(options.tuttiHome);
177
+ if (existing !== null) {
178
+ const health = await endpointHealth(existing, options.fetchImpl);
179
+ if (health.kind === "ready" &&
180
+ (existing.schema_version === 1 || health.identityVerified) &&
181
+ endpointMatchesCurrentRuntime(existing)) {
182
+ return existing;
183
+ }
184
+ if (health.kind === "ready") {
185
+ await stopEndpoint({ ...options, endpoint: existing });
186
+ }
187
+ else {
188
+ deleteLocalConsoleRuntimeEndpoint({
189
+ tuttiHome: options.tuttiHome,
190
+ expectedControlToken: existing.control_token,
191
+ });
192
+ }
193
+ }
194
+ const child = spawnDetachedConsole(options);
195
+ try {
196
+ return await waitForConsoleEndpoint(options);
197
+ }
198
+ catch (error) {
199
+ if (child.pid !== undefined) {
200
+ try {
201
+ process.kill(child.pid, "SIGTERM");
202
+ }
203
+ catch {
204
+ // The process already exited.
205
+ }
206
+ }
207
+ throw error;
63
208
  }
64
- spawnDetachedConsole({ cwd: options.cwd, env: options.env });
65
- return await waitForConsoleEndpoint(options.tuttiHome);
66
209
  }
67
- async function issueBrowserAccess(endpoint, currentDirectory) {
68
- const response = await fetch(`${endpoint.base_url}${LOCAL_CONSOLE_API_BASE}/access-tokens`, {
210
+ async function issueBrowserAccess(options) {
211
+ const context = createLocalConsoleInvocationContext({ cwd: options.cwd, env: options.env });
212
+ const response = await options.fetchImpl(`${options.endpoint.base_url}${LOCAL_CONSOLE_API_BASE}/access-tokens`, {
69
213
  method: "POST",
70
214
  headers: {
71
- authorization: `Bearer ${endpoint.control_token}`,
215
+ authorization: `Bearer ${options.endpoint.control_token}`,
72
216
  accept: "application/json",
73
217
  "content-type": "application/json",
74
218
  },
75
- body: JSON.stringify({ current_directory: currentDirectory }),
219
+ body: JSON.stringify({
220
+ current_directory: context.currentDirectory,
221
+ environment: context.environment,
222
+ }),
76
223
  signal: AbortSignal.timeout(3_000),
77
224
  });
78
225
  if (!response.ok) {
79
- throw new Error("Tutti Local Console could not create a browser access link.");
226
+ throw new Error("Tutti could not create a browser access link.");
80
227
  }
81
228
  const value = (await response.json());
82
229
  if (typeof value.access_token !== "string" || value.access_token.trim() === "") {
83
- throw new Error("Tutti Local Console returned an invalid browser access link.");
230
+ throw new Error("Tutti returned an invalid browser access link.");
84
231
  }
85
- return `${endpoint.base_url}/console#access_token=${encodeURIComponent(value.access_token)}`;
232
+ return `${options.endpoint.base_url}/console#access_token=${encodeURIComponent(value.access_token)}`;
86
233
  }
87
234
  export async function ensureLocalConsole(options = {}) {
88
235
  const cwd = resolve(options.cwd ?? process.cwd());
89
236
  const env = options.env ?? process.env;
237
+ const fetchImpl = options.fetchImpl ?? fetch;
238
+ const tuttiHome = resolveTuttiHome(env.TUTTI_HOME, cwd);
239
+ const endpoint = await withLocalConsoleLifecycleLock({
240
+ tuttiHome,
241
+ run: async () => await ensureConsoleEndpointUnlocked({ cwd, env, tuttiHome, fetchImpl }),
242
+ });
243
+ return {
244
+ endpoint,
245
+ url: await issueBrowserAccess({ endpoint, cwd, env, fetchImpl }),
246
+ };
247
+ }
248
+ export async function readLocalConsoleServiceStatus(options = {}) {
249
+ const cwd = resolve(options.cwd ?? process.cwd());
250
+ const env = options.env ?? process.env;
251
+ const tuttiHome = resolveTuttiHome(env.TUTTI_HOME, cwd);
252
+ const logPath = getLocalConsoleLogPath(tuttiHome);
253
+ const endpoint = readLocalConsoleRuntimeEndpoint(tuttiHome);
254
+ if (endpoint === null) {
255
+ return { status: "stopped", log_path: logPath };
256
+ }
257
+ const health = await endpointHealth(endpoint, options.fetchImpl ?? fetch);
258
+ if (health.kind === "stale" || (endpoint.schema_version === 2 && !health.identityVerified)) {
259
+ return {
260
+ status: "stale",
261
+ pid: endpoint.pid,
262
+ base_url: endpoint.base_url,
263
+ started_at: endpoint.started_at,
264
+ log_path: logPath,
265
+ };
266
+ }
267
+ const currentVersion = readCliVersion();
268
+ return {
269
+ status: "running",
270
+ pid: endpoint.pid,
271
+ base_url: endpoint.base_url,
272
+ started_at: endpoint.started_at,
273
+ version: endpoint.schema_version === 2 ? endpoint.version : "unknown",
274
+ current_version: currentVersion,
275
+ update_required: !endpointMatchesCurrentRuntime(endpoint),
276
+ log_path: logPath,
277
+ };
278
+ }
279
+ export async function stopLocalConsoleService(options = {}) {
280
+ const cwd = resolve(options.cwd ?? process.cwd());
281
+ const env = options.env ?? process.env;
282
+ const fetchImpl = options.fetchImpl ?? fetch;
90
283
  const tuttiHome = resolveTuttiHome(env.TUTTI_HOME, cwd);
91
- const endpoint = await ensureConsoleEndpoint({ cwd, env, tuttiHome });
92
- return { endpoint, url: await issueBrowserAccess(endpoint, cwd) };
284
+ return await withLocalConsoleLifecycleLock({
285
+ tuttiHome,
286
+ run: async () => {
287
+ const endpoint = readLocalConsoleRuntimeEndpoint(tuttiHome);
288
+ if (endpoint === null) {
289
+ return "already_stopped";
290
+ }
291
+ await stopEndpoint({ endpoint, tuttiHome, cwd, fetchImpl });
292
+ return "stopped";
293
+ },
294
+ });
295
+ }
296
+ export async function restartLocalConsoleService(options = {}) {
297
+ const cwd = resolve(options.cwd ?? process.cwd());
298
+ const env = options.env ?? process.env;
299
+ const fetchImpl = options.fetchImpl ?? fetch;
300
+ const tuttiHome = resolveTuttiHome(env.TUTTI_HOME, cwd);
301
+ return await withLocalConsoleLifecycleLock({
302
+ tuttiHome,
303
+ run: async () => {
304
+ const endpoint = readLocalConsoleRuntimeEndpoint(tuttiHome);
305
+ if (endpoint !== null) {
306
+ await stopEndpoint({ endpoint, tuttiHome, cwd, fetchImpl });
307
+ }
308
+ return await ensureConsoleEndpointUnlocked({ cwd, env, tuttiHome, fetchImpl });
309
+ },
310
+ });
93
311
  }
94
312
  //# sourceMappingURL=managed-console.js.map
@@ -1,14 +1,20 @@
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
+ import { type LocalConsoleInvocationContext } from "./invocation-context.js";
3
4
  export type LocalConsoleProject = {
4
5
  project_id: ProjectId;
5
6
  display_name: string;
6
7
  workspace_path: string;
7
8
  status: RuntimeProjectRow["status"];
8
9
  provider_status: "configured" | "not_configured" | "invalid";
9
- relay_project_ref?: string;
10
+ relay_project_ref?: RelayProjectRef;
10
11
  join_url?: string;
12
+ open_url?: string;
11
13
  relay_connection_status?: string;
14
+ created_at?: string;
15
+ activity_at?: string;
16
+ host_version?: string;
17
+ update_required?: boolean;
12
18
  };
13
19
  export type LocalConsoleProviderInput = {
14
20
  base_url: string;
@@ -17,6 +23,7 @@ export type LocalConsoleProviderInput = {
17
23
  };
18
24
  export type LocalConsoleLaunchResult = {
19
25
  project: LocalConsoleProject;
26
+ project_created: boolean;
20
27
  join_url: string;
21
28
  join_token_expires_at?: string;
22
29
  };
@@ -27,11 +34,11 @@ export declare class LocalConsoleProjectError extends Error {
27
34
  }
28
35
  export declare class LocalConsoleProjectService {
29
36
  #private;
30
- constructor(options?: {
31
- cwd?: string;
32
- env?: NodeJS.ProcessEnv;
37
+ constructor(options: {
38
+ tuttiHome: string;
39
+ serviceEnvironment?: NodeJS.ProcessEnv;
33
40
  });
34
- listProjects(): Promise<LocalConsoleProject[]>;
41
+ listProjects(context: LocalConsoleInvocationContext): Promise<LocalConsoleProject[]>;
35
42
  discoverModels(input: {
36
43
  baseUrl: string;
37
44
  apiKey: string;
@@ -45,12 +52,12 @@ export declare class LocalConsoleProjectService {
45
52
  launchProject(input: {
46
53
  workspacePath: string;
47
54
  provider?: LocalConsoleProviderInput;
48
- }): Promise<LocalConsoleLaunchResult>;
49
- refreshInvite(projectId: string): Promise<{
55
+ }, context: LocalConsoleInvocationContext): Promise<LocalConsoleLaunchResult>;
56
+ refreshInvite(projectId: string, context: LocalConsoleInvocationContext): Promise<{
50
57
  join_url: string;
51
58
  expires_at?: string;
52
59
  }>;
53
- stopProject(projectId: string): Promise<{
60
+ stopProject(projectId: string, context: LocalConsoleInvocationContext): Promise<{
54
61
  message: string;
55
62
  }>;
56
63
  }