@xfey/tutti 0.1.5 → 0.1.7

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 (35) hide show
  1. package/README.md +4 -4
  2. package/dist/providers/openai/app-server/read-only-procedure.js +3 -5
  3. package/dist/providers/openai/app-server/sandbox-policy.d.ts +18 -0
  4. package/dist/providers/openai/app-server/sandbox-policy.js +16 -0
  5. package/dist/providers/openai/app-server/smoke.js +3 -3
  6. package/dist/providers/openai/app-server/workspace-write-run.js +3 -5
  7. package/dist/providers/openai/codex-app-server.d.ts +0 -9
  8. package/dist/providers/openai/codex-app-server.js +0 -5
  9. package/dist/server-shell/cli/args.d.ts +42 -0
  10. package/dist/server-shell/cli/args.js +185 -0
  11. package/dist/server-shell/cli/cli.js +179 -40
  12. package/dist/server-shell/cli/errors.d.ts +1 -0
  13. package/dist/server-shell/cli/errors.js +22 -0
  14. package/dist/server-shell/cli/host-server-runtime.js +8 -0
  15. package/dist/server-shell/cli/launch-command.d.ts +19 -0
  16. package/dist/server-shell/cli/launch-command.js +127 -0
  17. package/dist/server-shell/cli/local-control-client.d.ts +30 -0
  18. package/dist/server-shell/cli/local-control-client.js +73 -0
  19. package/dist/server-shell/cli/managed-host.d.ts +19 -0
  20. package/dist/server-shell/cli/managed-host.js +103 -0
  21. package/dist/server-shell/cli/project-resolver.d.ts +19 -0
  22. package/dist/server-shell/cli/project-resolver.js +35 -0
  23. package/dist/server-shell/cli/provider-tui.d.ts +16 -0
  24. package/dist/server-shell/cli/provider-tui.js +226 -0
  25. package/dist/server-shell/cli/runtime-commands.d.ts +32 -0
  26. package/dist/server-shell/cli/runtime-commands.js +308 -0
  27. package/dist/server-shell/cli/terminal-qr.d.ts +2 -0
  28. package/dist/server-shell/cli/terminal-qr.js +11 -0
  29. package/dist/server-shell/cli/version.d.ts +2 -0
  30. package/dist/server-shell/cli/version.js +7 -0
  31. package/dist/server-shell/http/routes/local-control.d.ts +1 -0
  32. package/dist/server-shell/http/routes/local-control.js +13 -0
  33. package/package.json +2 -1
  34. package/dist/providers/openai/app-server/permission-profile.d.ts +0 -64
  35. package/dist/providers/openai/app-server/permission-profile.js +0 -67
@@ -0,0 +1,19 @@
1
+ import { type ProjectId } from "@tutti/shared/ids";
2
+ import { type OpenAiProviderConfigProjection } from "../../providers/openai/index.js";
3
+ import { type MachineProjectBindingRecord, type MachineRuntimeEndpointRecord } from "./machine-local.js";
4
+ export type ExistingProjectContext = {
5
+ workspace_root: string;
6
+ tutti_home: string;
7
+ project_id: ProjectId;
8
+ display_name: string;
9
+ binding: MachineProjectBindingRecord | null;
10
+ runtime_endpoint: MachineRuntimeEndpointRecord | null;
11
+ provider_config: OpenAiProviderConfigProjection;
12
+ provider_base_url?: string;
13
+ };
14
+ export declare function resolveExistingProjectContext(options: {
15
+ workspacePath?: string;
16
+ cwd?: string;
17
+ env?: NodeJS.ProcessEnv;
18
+ }): ExistingProjectContext;
19
+ //# sourceMappingURL=project-resolver.d.ts.map
@@ -0,0 +1,35 @@
1
+ import { resolve } from "node:path";
2
+ import { readOpenAiProviderConfigProjection, readProjectOpenAiProviderConfig, resolveTuttiHome, } from "../../providers/openai/index.js";
3
+ import { LaunchError } from "./errors.js";
4
+ import { assertSafeProjectRoot } from "./git-bootstrap.js";
5
+ import { readMachineProjectBinding, readMachineRuntimeEndpoint, } from "./machine-local.js";
6
+ import { readProjectDisplayName, readProjectIdentity } from "./project-identity.js";
7
+ export function resolveExistingProjectContext(options) {
8
+ const cwd = options.cwd ?? process.cwd();
9
+ const env = options.env ?? process.env;
10
+ const workspaceRoot = resolve(cwd, options.workspacePath ?? ".");
11
+ assertSafeProjectRoot(workspaceRoot);
12
+ const identity = readProjectIdentity(workspaceRoot);
13
+ if (identity.kind !== "present") {
14
+ throw new LaunchError("project_identity_conflict", "Current directory is not a Tutti project", "Run `tutti launch` from a project root first.");
15
+ }
16
+ const tuttiHome = resolveTuttiHome(env.TUTTI_HOME, cwd);
17
+ const binding = readMachineProjectBinding(tuttiHome, identity.project_id);
18
+ const projectConfig = readProjectOpenAiProviderConfig(tuttiHome, identity.project_id);
19
+ return {
20
+ workspace_root: workspaceRoot,
21
+ tutti_home: tuttiHome,
22
+ project_id: identity.project_id,
23
+ display_name: readProjectDisplayName(workspaceRoot) ?? identity.project_id,
24
+ binding,
25
+ runtime_endpoint: readMachineRuntimeEndpoint(tuttiHome, identity.project_id),
26
+ provider_config: readOpenAiProviderConfigProjection({
27
+ tuttiHome,
28
+ projectId: identity.project_id,
29
+ }),
30
+ ...(projectConfig.kind === "ok" && projectConfig.config.api_base_url !== undefined
31
+ ? { provider_base_url: projectConfig.config.api_base_url }
32
+ : {}),
33
+ };
34
+ }
35
+ //# sourceMappingURL=project-resolver.js.map
@@ -0,0 +1,16 @@
1
+ import type { ProjectId } from "@tutti/shared/ids";
2
+ export type ProviderSetupResult = {
3
+ base_url: string;
4
+ redacted_key: string;
5
+ default_model: string;
6
+ validated_at: string;
7
+ };
8
+ export declare function runProviderSetupTui(options: {
9
+ projectName: string;
10
+ tuttiHome: string;
11
+ projectId: ProjectId;
12
+ initialBaseUrl?: string;
13
+ stdin?: NodeJS.ReadStream;
14
+ stdout?: NodeJS.WriteStream;
15
+ }): Promise<ProviderSetupResult>;
16
+ //# sourceMappingURL=provider-tui.d.ts.map
@@ -0,0 +1,226 @@
1
+ import { emitKeypressEvents } from "node:readline";
2
+ import { redactError } from "@tutti/shared/utils";
3
+ import { configureProjectOpenAiProvider, DEFAULT_OPENAI_MODEL, } from "../../providers/openai/index.js";
4
+ const DEFAULT_BASE_URL = "https://api.openai.com/v1";
5
+ const HIDE_CURSOR = "\u001B[?25l";
6
+ const SHOW_CURSOR = "\u001B[?25h";
7
+ const CLEAR = "\u001B[2J\u001B[H";
8
+ function fieldLine(options) {
9
+ const active = options.index === options.selected;
10
+ const marker = active ? ">" : " ";
11
+ const value = options.secret && options.value.length > 0 ? "*".repeat(options.value.length) : options.value;
12
+ return `${marker} ${options.label.padEnd(10)} ${value}${active ? " _" : ""}`;
13
+ }
14
+ function renderProviderForm(options) {
15
+ const lines = [
16
+ CLEAR,
17
+ HIDE_CURSOR,
18
+ "Tutti Provider Setup",
19
+ "",
20
+ `Project: ${options.projectName}`,
21
+ "Use Up/Down to move, type to edit, Enter to validate, Ctrl-C to cancel.",
22
+ "",
23
+ fieldLine({
24
+ index: 0,
25
+ selected: options.state.selected,
26
+ label: "Base URL",
27
+ value: options.state.baseUrl,
28
+ }),
29
+ fieldLine({
30
+ index: 1,
31
+ selected: options.state.selected,
32
+ label: "API Key",
33
+ value: options.state.apiKey,
34
+ secret: true,
35
+ }),
36
+ "",
37
+ ];
38
+ if (options.state.error !== undefined) {
39
+ lines.push(`Error: ${options.state.error}`, "");
40
+ }
41
+ if (options.footer !== undefined) {
42
+ lines.push(options.footer);
43
+ }
44
+ return lines.join("\n");
45
+ }
46
+ function validationFailureMessage(result) {
47
+ if (result.kind === "configured") {
48
+ return "";
49
+ }
50
+ const retry = result.retryable ? " Retry after the provider recovers." : "";
51
+ return `${result.reason}.${retry}`;
52
+ }
53
+ function waitForKeypress(stdin) {
54
+ return new Promise((resolve, reject) => {
55
+ const onKeypress = (_character, key) => {
56
+ cleanup();
57
+ if (key.ctrl === true && key.name === "c") {
58
+ reject(new Error("Provider setup cancelled"));
59
+ return;
60
+ }
61
+ resolve();
62
+ };
63
+ const cleanup = () => {
64
+ stdin.off("keypress", onKeypress);
65
+ if (stdin.isTTY) {
66
+ stdin.setRawMode(false);
67
+ }
68
+ };
69
+ if (stdin.isTTY) {
70
+ stdin.setRawMode(true);
71
+ }
72
+ stdin.once("keypress", onKeypress);
73
+ });
74
+ }
75
+ async function readProviderForm(options) {
76
+ const state = {
77
+ selected: 0,
78
+ baseUrl: options.initialBaseUrl ?? DEFAULT_BASE_URL,
79
+ apiKey: "",
80
+ error: undefined,
81
+ };
82
+ return await new Promise((resolve, reject) => {
83
+ const render = () => {
84
+ options.stdout.write(renderProviderForm({ projectName: options.projectName, state }));
85
+ };
86
+ const cleanup = () => {
87
+ options.stdin.off("keypress", onKeypress);
88
+ if (options.stdin.isTTY) {
89
+ options.stdin.setRawMode(false);
90
+ }
91
+ options.stdout.write(SHOW_CURSOR);
92
+ };
93
+ const onKeypress = (character, key) => {
94
+ if (key.ctrl === true && key.name === "c") {
95
+ cleanup();
96
+ reject(new Error("Provider setup cancelled"));
97
+ return;
98
+ }
99
+ if (key.name === "escape") {
100
+ cleanup();
101
+ reject(new Error("Provider setup cancelled"));
102
+ return;
103
+ }
104
+ if (key.name === "up") {
105
+ state.selected = state.selected === 0 ? 1 : 0;
106
+ state.error = undefined;
107
+ render();
108
+ return;
109
+ }
110
+ if (key.name === "down" || key.name === "tab") {
111
+ state.selected = state.selected === 0 ? 1 : 0;
112
+ state.error = undefined;
113
+ render();
114
+ return;
115
+ }
116
+ if (key.name === "return") {
117
+ if (state.baseUrl.trim() === "" || state.apiKey.trim() === "") {
118
+ state.error = "Base URL and API key are required.";
119
+ render();
120
+ return;
121
+ }
122
+ cleanup();
123
+ resolve({ baseUrl: state.baseUrl.trim(), apiKey: state.apiKey.trim() });
124
+ return;
125
+ }
126
+ if (key.name === "backspace") {
127
+ if (state.selected === 0) {
128
+ state.baseUrl = state.baseUrl.slice(0, -1);
129
+ }
130
+ else {
131
+ state.apiKey = state.apiKey.slice(0, -1);
132
+ }
133
+ state.error = undefined;
134
+ render();
135
+ return;
136
+ }
137
+ if (character !== undefined && character >= " " && character !== "\u007F") {
138
+ if (state.selected === 0) {
139
+ state.baseUrl += character;
140
+ }
141
+ else {
142
+ state.apiKey += character;
143
+ }
144
+ state.error = undefined;
145
+ render();
146
+ }
147
+ };
148
+ emitKeypressEvents(options.stdin);
149
+ if (options.stdin.isTTY) {
150
+ options.stdin.setRawMode(true);
151
+ }
152
+ options.stdin.on("keypress", onKeypress);
153
+ render();
154
+ });
155
+ }
156
+ function renderChecking(projectName, frame) {
157
+ const frames = ["|", "/", "-", "\\"];
158
+ const indicator = frames[frame % frames.length] ?? "|";
159
+ return [
160
+ CLEAR,
161
+ HIDE_CURSOR,
162
+ "Tutti Provider Setup",
163
+ "",
164
+ `Project: ${projectName}`,
165
+ "",
166
+ `${indicator} Checking provider connection with ${DEFAULT_OPENAI_MODEL}...`,
167
+ ].join("\n");
168
+ }
169
+ export async function runProviderSetupTui(options) {
170
+ const stdin = options.stdin ?? process.stdin;
171
+ const stdout = options.stdout ?? process.stdout;
172
+ if (!stdin.isTTY || !stdout.isTTY) {
173
+ throw new Error("Provider setup requires an interactive terminal.");
174
+ }
175
+ let initialBaseUrl = options.initialBaseUrl;
176
+ while (true) {
177
+ const input = await readProviderForm({
178
+ projectName: options.projectName,
179
+ ...(initialBaseUrl === undefined ? {} : { initialBaseUrl }),
180
+ stdin,
181
+ stdout,
182
+ });
183
+ initialBaseUrl = input.baseUrl;
184
+ let frame = 0;
185
+ const interval = setInterval(() => {
186
+ stdout.write(renderChecking(options.projectName, frame));
187
+ frame += 1;
188
+ }, 120);
189
+ try {
190
+ stdout.write(renderChecking(options.projectName, frame));
191
+ const result = await configureProjectOpenAiProvider({
192
+ tuttiHome: options.tuttiHome,
193
+ projectId: options.projectId,
194
+ apiBaseUrl: input.baseUrl,
195
+ apiKey: input.apiKey,
196
+ });
197
+ clearInterval(interval);
198
+ stdout.write(SHOW_CURSOR);
199
+ if (result.kind === "configured") {
200
+ return {
201
+ base_url: input.baseUrl,
202
+ redacted_key: result.projection.redacted_key,
203
+ default_model: result.projection.default_model,
204
+ validated_at: result.projection.validated_at,
205
+ };
206
+ }
207
+ stdout.write(renderProviderForm({
208
+ projectName: options.projectName,
209
+ state: {
210
+ selected: 1,
211
+ baseUrl: input.baseUrl,
212
+ apiKey: input.apiKey,
213
+ error: validationFailureMessage(result),
214
+ },
215
+ footer: "Press any key to edit and try again.",
216
+ }));
217
+ await waitForKeypress(stdin);
218
+ }
219
+ catch (error) {
220
+ clearInterval(interval);
221
+ stdout.write(SHOW_CURSOR);
222
+ throw new Error(`Provider setup failed: ${String(redactError(error))}`, { cause: error });
223
+ }
224
+ }
225
+ }
226
+ //# sourceMappingURL=provider-tui.js.map
@@ -0,0 +1,32 @@
1
+ import { type ProjectId } from "@tutti/shared/ids";
2
+ import { type FetchLike } from "./host-runtime-endpoint.js";
3
+ import { type MachineRuntimeEndpointRecord } from "./machine-local.js";
4
+ export type RuntimeProjectRow = {
5
+ project_id: ProjectId;
6
+ status: "online" | "stale";
7
+ display_name: string;
8
+ workspace_root: string;
9
+ endpoint?: string;
10
+ provider_status?: string;
11
+ relay_project_ref?: string;
12
+ join_url?: string;
13
+ };
14
+ export declare function listRuntimeProjects(options?: {
15
+ env?: NodeJS.ProcessEnv;
16
+ cwd?: string;
17
+ fetchImpl?: FetchLike;
18
+ }): Promise<RuntimeProjectRow[]>;
19
+ export declare function runPsCommand(): Promise<string>;
20
+ export declare function runPsManageCommand(options?: {
21
+ stdin?: NodeJS.ReadStream;
22
+ stdout?: NodeJS.WriteStream;
23
+ }): Promise<void>;
24
+ export declare function runStopCommand(workspacePath?: string): Promise<string>;
25
+ export declare function runInviteCommand(workspacePath?: string): Promise<string>;
26
+ export declare function runProviderStatusCommand(workspacePath?: string): string;
27
+ export declare function runLogsCommand(workspacePath?: string, tailLines?: number): string;
28
+ export declare function readRuntimeEndpointForProject(tuttiHome: string, projectId: ProjectId): MachineRuntimeEndpointRecord | null;
29
+ export declare function runtimeEndpointPathForProject(tuttiHome: string, projectId: ProjectId): string;
30
+ export declare function projectLocalStoreRoot(tuttiHome: string, projectId: ProjectId): string;
31
+ export declare function resolveWorkspacePath(value: string | undefined): string;
32
+ //# sourceMappingURL=runtime-commands.d.ts.map
@@ -0,0 +1,308 @@
1
+ import { existsSync, readdirSync, readFileSync } from "node:fs";
2
+ import { join, resolve } from "node:path";
3
+ import { emitKeypressEvents } from "node:readline";
4
+ import { ID_PREFIXES, isPrefixedId } from "@tutti/shared/ids";
5
+ import { redactText } from "@tutti/shared/utils";
6
+ import { createRuntimeEndpointProbe } from "./host-runtime-endpoint.js";
7
+ import { getHostLogFilePath, getMachineRuntimeEndpointPath, getProjectLocalStoreRoot, readMachineProjectBinding, readMachineRuntimeEndpoint, } from "./machine-local.js";
8
+ import { readHostLocalLaunchStatus, readHostLocalProject, readHostLocalProviderConfig, requestHostLocalShutdown, rotateHostLocalInvite, } from "./local-control-client.js";
9
+ import { resolveExistingProjectContext } from "./project-resolver.js";
10
+ import { renderTerminalQr } from "./terminal-qr.js";
11
+ import { resolveTuttiHome } from "../../providers/openai/index.js";
12
+ function resolveProject(workspacePath) {
13
+ return resolveExistingProjectContext({
14
+ ...(workspacePath === undefined ? {} : { workspacePath }),
15
+ });
16
+ }
17
+ function projectIdsFromTuttiHome(tuttiHome) {
18
+ const projectsRoot = join(tuttiHome, "projects");
19
+ if (!existsSync(projectsRoot)) {
20
+ return [];
21
+ }
22
+ return readdirSync(projectsRoot, { withFileTypes: true })
23
+ .filter((entry) => entry.isDirectory() && isPrefixedId(entry.name, ID_PREFIXES.project))
24
+ .map((entry) => entry.name)
25
+ .sort();
26
+ }
27
+ function readEndpointFile(tuttiHome, projectId) {
28
+ try {
29
+ return readMachineRuntimeEndpoint(tuttiHome, projectId);
30
+ }
31
+ catch {
32
+ return null;
33
+ }
34
+ }
35
+ export async function listRuntimeProjects(options = {}) {
36
+ const cwd = options.cwd ?? process.cwd();
37
+ const tuttiHome = resolveTuttiHome(options.env?.TUTTI_HOME, cwd);
38
+ const probe = createRuntimeEndpointProbe(options.fetchImpl ?? fetch);
39
+ const rows = [];
40
+ for (const projectId of projectIdsFromTuttiHome(tuttiHome)) {
41
+ const endpoint = readEndpointFile(tuttiHome, projectId);
42
+ if (endpoint === null) {
43
+ continue;
44
+ }
45
+ const binding = readMachineProjectBinding(tuttiHome, projectId);
46
+ const health = await probe(endpoint);
47
+ if (health.kind === "stale") {
48
+ rows.push({
49
+ project_id: projectId,
50
+ status: "stale",
51
+ display_name: binding?.workspace_root === undefined ? projectId : binding.workspace_root.split(/[\\/]/u).pop() ?? projectId,
52
+ workspace_root: binding?.workspace_root ?? endpoint.workspace_root,
53
+ endpoint: endpoint.base_url,
54
+ });
55
+ continue;
56
+ }
57
+ const fetchOption = options.fetchImpl === undefined ? {} : { fetchImpl: options.fetchImpl };
58
+ const [project, provider, launchStatus] = await Promise.allSettled([
59
+ readHostLocalProject({ endpoint, ...fetchOption }),
60
+ readHostLocalProviderConfig({ endpoint, ...fetchOption }),
61
+ readHostLocalLaunchStatus({ endpoint, ...fetchOption }),
62
+ ]);
63
+ const row = {
64
+ project_id: projectId,
65
+ status: "online",
66
+ display_name: project.status === "fulfilled" ? project.value.display_name : binding?.workspace_root.split(/[\\/]/u).pop() ?? projectId,
67
+ workspace_root: binding?.workspace_root ?? endpoint.workspace_root,
68
+ endpoint: endpoint.base_url,
69
+ };
70
+ if (provider.status === "fulfilled") {
71
+ row.provider_status = provider.value.status;
72
+ }
73
+ if (launchStatus.status === "fulfilled" && launchStatus.value.relay?.relay_project_ref !== undefined) {
74
+ row.relay_project_ref = launchStatus.value.relay.relay_project_ref;
75
+ }
76
+ if (launchStatus.status === "fulfilled" && launchStatus.value.relay?.join_url !== undefined) {
77
+ row.join_url = launchStatus.value.relay.join_url;
78
+ }
79
+ rows.push(row);
80
+ }
81
+ return rows;
82
+ }
83
+ function formatTable(rows) {
84
+ if (rows.length === 0) {
85
+ return "No Tutti host processes found.";
86
+ }
87
+ const header = ["STATUS", "PROJECT", "PROVIDER", "WORKSPACE"];
88
+ const data = rows.map((row) => [
89
+ row.status,
90
+ row.display_name,
91
+ row.provider_status ?? "-",
92
+ redactText(row.workspace_root),
93
+ ]);
94
+ const widths = header.map((label, index) => Math.max(label.length, ...data.map((row) => row[index]?.length ?? 0)));
95
+ const line = (columns) => columns.map((column, index) => column.padEnd(widths[index] ?? column.length)).join(" ");
96
+ return [line(header), line(widths.map((width) => "-".repeat(width))), ...data.map(line)].join("\n");
97
+ }
98
+ const CLEAR = "\u001B[2J\u001B[H";
99
+ const HIDE_CURSOR = "\u001B[?25l";
100
+ const SHOW_CURSOR = "\u001B[?25h";
101
+ function keypress(stdin) {
102
+ return new Promise((resolveKeypress) => {
103
+ stdin.once("keypress", (character, key) => {
104
+ resolveKeypress({
105
+ ...(character === undefined ? {} : { character }),
106
+ key,
107
+ });
108
+ });
109
+ });
110
+ }
111
+ function selectedProject(rows, selectedIndex) {
112
+ return rows[selectedIndex];
113
+ }
114
+ function renderManager(options) {
115
+ const lines = [
116
+ CLEAR,
117
+ HIDE_CURSOR,
118
+ "Tutti Projects",
119
+ "",
120
+ options.rows.length === 0
121
+ ? "No Tutti host processes found."
122
+ : "Up/Down select, Enter/i invite, l logs, s stop, r refresh, q quit.",
123
+ "",
124
+ ];
125
+ if (options.rows.length > 0) {
126
+ const header = ["", "STATUS", "PROJECT", "PROVIDER", "WORKSPACE"];
127
+ const data = options.rows.map((row, index) => [
128
+ index === options.selectedIndex ? ">" : " ",
129
+ row.status,
130
+ row.display_name,
131
+ row.provider_status ?? "-",
132
+ redactText(row.workspace_root),
133
+ ]);
134
+ const widths = header.map((label, index) => Math.max(label.length, ...data.map((row) => row[index]?.length ?? 0)));
135
+ const line = (columns) => columns.map((column, index) => column.padEnd(widths[index] ?? column.length)).join(" ");
136
+ lines.push(line(header), line(widths.map((width) => "-".repeat(width))), ...data.map(line), "");
137
+ }
138
+ if (options.message !== undefined) {
139
+ lines.push(options.message, "");
140
+ }
141
+ if (options.detail !== undefined) {
142
+ lines.push(options.detail, "");
143
+ }
144
+ return lines.join("\n");
145
+ }
146
+ export async function runPsCommand() {
147
+ return formatTable(await listRuntimeProjects());
148
+ }
149
+ export async function runPsManageCommand(options = {}) {
150
+ const stdin = options.stdin ?? process.stdin;
151
+ const stdout = options.stdout ?? process.stdout;
152
+ if (!stdin.isTTY || !stdout.isTTY) {
153
+ stdout.write(`${await runPsCommand()}\n`);
154
+ return;
155
+ }
156
+ emitKeypressEvents(stdin);
157
+ stdin.setRawMode(true);
158
+ let rows = await listRuntimeProjects();
159
+ let selectedIndex = 0;
160
+ let message;
161
+ let detail;
162
+ const render = () => {
163
+ stdout.write(renderManager({
164
+ rows,
165
+ selectedIndex,
166
+ ...(message === undefined ? {} : { message }),
167
+ ...(detail === undefined ? {} : { detail }),
168
+ }));
169
+ };
170
+ try {
171
+ render();
172
+ while (true) {
173
+ const input = await keypress(stdin);
174
+ if (input.key.ctrl === true && input.key.name === "c") {
175
+ return;
176
+ }
177
+ if (input.key.name === "escape" || input.character === "q") {
178
+ return;
179
+ }
180
+ if (input.key.name === "up" && rows.length > 0) {
181
+ selectedIndex = selectedIndex === 0 ? rows.length - 1 : selectedIndex - 1;
182
+ message = undefined;
183
+ detail = undefined;
184
+ render();
185
+ continue;
186
+ }
187
+ if (input.key.name === "down" && rows.length > 0) {
188
+ selectedIndex = selectedIndex === rows.length - 1 ? 0 : selectedIndex + 1;
189
+ message = undefined;
190
+ detail = undefined;
191
+ render();
192
+ continue;
193
+ }
194
+ if (input.character === "r") {
195
+ rows = await listRuntimeProjects();
196
+ selectedIndex = Math.min(selectedIndex, Math.max(rows.length - 1, 0));
197
+ message = "Refreshed.";
198
+ detail = undefined;
199
+ render();
200
+ continue;
201
+ }
202
+ const project = selectedProject(rows, selectedIndex);
203
+ if (project === undefined) {
204
+ message = "No project selected.";
205
+ detail = undefined;
206
+ render();
207
+ continue;
208
+ }
209
+ if (input.key.name === "return" || input.character === "i") {
210
+ try {
211
+ detail = await runInviteCommand(project.workspace_root);
212
+ message = `Invite refreshed for ${project.display_name}.`;
213
+ }
214
+ catch (error) {
215
+ message = error instanceof Error ? error.message : "Invite refresh failed.";
216
+ detail = undefined;
217
+ }
218
+ render();
219
+ continue;
220
+ }
221
+ if (input.character === "l") {
222
+ detail = runLogsCommand(project.workspace_root, 40);
223
+ message = `Showing last 40 host log lines for ${project.display_name}.`;
224
+ render();
225
+ continue;
226
+ }
227
+ if (input.character === "s") {
228
+ try {
229
+ message = await runStopCommand(project.workspace_root);
230
+ rows = await listRuntimeProjects();
231
+ selectedIndex = Math.min(selectedIndex, Math.max(rows.length - 1, 0));
232
+ detail = undefined;
233
+ }
234
+ catch (error) {
235
+ message = error instanceof Error ? error.message : "Stop failed.";
236
+ detail = undefined;
237
+ }
238
+ render();
239
+ continue;
240
+ }
241
+ message = "Unsupported key.";
242
+ detail = undefined;
243
+ render();
244
+ }
245
+ }
246
+ finally {
247
+ stdin.setRawMode(false);
248
+ stdout.write(SHOW_CURSOR);
249
+ }
250
+ }
251
+ export async function runStopCommand(workspacePath) {
252
+ const project = resolveProject(workspacePath);
253
+ const endpoint = project.runtime_endpoint;
254
+ if (endpoint === null) {
255
+ return "Tutti host is not running for this project.";
256
+ }
257
+ await requestHostLocalShutdown({ endpoint });
258
+ return `Stopped ${project.display_name}.`;
259
+ }
260
+ export async function runInviteCommand(workspacePath) {
261
+ const project = resolveProject(workspacePath);
262
+ const endpoint = project.runtime_endpoint;
263
+ if (endpoint === null) {
264
+ throw new Error("Tutti host is not running for this project.");
265
+ }
266
+ const status = await rotateHostLocalInvite({ endpoint });
267
+ const joinUrl = status.relay?.join_url;
268
+ if (joinUrl === undefined) {
269
+ throw new Error("Relay did not return a visible join URL.");
270
+ }
271
+ return [`Join URL: ${joinUrl}`, "", renderTerminalQr(joinUrl)].join("\n");
272
+ }
273
+ export function runProviderStatusCommand(workspacePath) {
274
+ const project = resolveProject(workspacePath);
275
+ return [
276
+ `Project: ${project.display_name}`,
277
+ `Provider: ${project.provider_config.status}`,
278
+ ...(project.provider_config.status === "configured"
279
+ ? [
280
+ `Model: ${project.provider_config.default_model}`,
281
+ `Key: ${project.provider_config.redacted_key}`,
282
+ `Validated: ${project.provider_config.validated_at}`,
283
+ ]
284
+ : []),
285
+ ].join("\n");
286
+ }
287
+ export function runLogsCommand(workspacePath, tailLines = 120) {
288
+ const project = resolveProject(workspacePath);
289
+ const logPath = getHostLogFilePath(project.tutti_home);
290
+ if (!existsSync(logPath)) {
291
+ return `No host log exists yet at ${redactText(logPath)}.`;
292
+ }
293
+ const lines = readFileSync(logPath, "utf8").split(/\r?\n/u).filter(Boolean).slice(-tailLines);
294
+ return lines.length === 0 ? "Host log is empty." : lines.join("\n");
295
+ }
296
+ export function readRuntimeEndpointForProject(tuttiHome, projectId) {
297
+ return readEndpointFile(tuttiHome, projectId);
298
+ }
299
+ export function runtimeEndpointPathForProject(tuttiHome, projectId) {
300
+ return getMachineRuntimeEndpointPath(tuttiHome, projectId);
301
+ }
302
+ export function projectLocalStoreRoot(tuttiHome, projectId) {
303
+ return getProjectLocalStoreRoot(tuttiHome, projectId);
304
+ }
305
+ export function resolveWorkspacePath(value) {
306
+ return resolve(process.cwd(), value ?? ".");
307
+ }
308
+ //# sourceMappingURL=runtime-commands.js.map
@@ -0,0 +1,2 @@
1
+ export declare function renderTerminalQr(input: string): string;
2
+ //# sourceMappingURL=terminal-qr.d.ts.map
@@ -0,0 +1,11 @@
1
+ import { createRequire } from "node:module";
2
+ const require = createRequire(import.meta.url);
3
+ export function renderTerminalQr(input) {
4
+ const qrcode = require("qrcode-terminal");
5
+ let output = "";
6
+ qrcode.generate(input, { small: true }, (rendered) => {
7
+ output = rendered;
8
+ });
9
+ return output.trimEnd();
10
+ }
11
+ //# sourceMappingURL=terminal-qr.js.map
@@ -0,0 +1,2 @@
1
+ export declare function readCliVersion(): string;
2
+ //# sourceMappingURL=version.d.ts.map
@@ -0,0 +1,7 @@
1
+ import { readFileSync } from "node:fs";
2
+ export function readCliVersion() {
3
+ const packageUrl = new URL("../../../package.json", import.meta.url);
4
+ const parsed = JSON.parse(readFileSync(packageUrl, "utf8"));
5
+ return typeof parsed.version === "string" ? parsed.version : "0.0.0";
6
+ }
7
+ //# sourceMappingURL=version.js.map
@@ -3,6 +3,7 @@ import type { ProviderConfigProjection } from "@tutti/shared/schemas/api";
3
3
  export type HostLocalControlOptions = {
4
4
  token: string;
5
5
  getLaunchStatus?: () => HostLocalLaunchStatus;
6
+ refreshInvite?: () => Promise<HostLocalLaunchStatus>;
6
7
  project?: {
7
8
  get: () => HostLocalProjectProjection;
8
9
  update: (input: HostLocalProjectBody) => HostLocalProjectProjection;