@xfey/tutti 0.1.6 → 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.
@@ -0,0 +1,127 @@
1
+ import { createInterface } from "node:readline/promises";
2
+ import { basename } from "node:path";
3
+ import { redactText } from "@tutti/shared/utils";
4
+ import { formatLaunchLifecycleResult, startLaunchProject, waitForForegroundHostShutdown } from "./host-lifecycle.js";
5
+ import { prepareLaunchProject } from "./launch.js";
6
+ import { spawnDetachedHost, waitForManagedHostReady } from "./managed-host.js";
7
+ import { renderTerminalQr } from "./terminal-qr.js";
8
+ import { runProviderSetupTui } from "./provider-tui.js";
9
+ import { LaunchError } from "./errors.js";
10
+ import { runStopCommand } from "./runtime-commands.js";
11
+ const CLEAR = "\u001B[2J\u001B[H";
12
+ export async function confirmFromTty(request) {
13
+ if (!process.stdin.isTTY || !process.stdout.isTTY) {
14
+ return false;
15
+ }
16
+ const readline = createInterface({
17
+ input: process.stdin,
18
+ output: process.stdout,
19
+ });
20
+ try {
21
+ const answer = await readline.question(`${request.message}\n\nContinue? [y/N] `);
22
+ return /^(y|yes)$/iu.test(answer.trim());
23
+ }
24
+ finally {
25
+ readline.close();
26
+ }
27
+ }
28
+ function projectName(preparation) {
29
+ return basename(preparation.workspace_root) || preparation.project_id;
30
+ }
31
+ async function waitForEnter() {
32
+ if (!process.stdin.isTTY || !process.stdout.isTTY) {
33
+ return;
34
+ }
35
+ const readline = createInterface({
36
+ input: process.stdin,
37
+ output: process.stdout,
38
+ });
39
+ try {
40
+ await readline.question("");
41
+ }
42
+ finally {
43
+ readline.close();
44
+ }
45
+ }
46
+ function renderCompletionPage(options) {
47
+ return [
48
+ ...(process.stdout.isTTY ? [CLEAR] : []),
49
+ "Tutti is running in the background.",
50
+ "",
51
+ `Project: ${options.projectName}`,
52
+ `Workspace: ${redactText(options.workspaceRoot)}`,
53
+ "",
54
+ `Join URL: ${options.joinUrl}`,
55
+ "",
56
+ renderTerminalQr(options.joinUrl),
57
+ "",
58
+ "按下回车关闭页面,Host 会继续在后台运行。",
59
+ ].join("\n");
60
+ }
61
+ async function ensureProviderConfigured(preparation) {
62
+ if (preparation.provider_status === "configured") {
63
+ return;
64
+ }
65
+ if (!process.stdin.isTTY || !process.stdout.isTTY) {
66
+ throw new LaunchError("provider_configuration_required", "Provider credentials are not configured for this project", "Run `tutti provider setup` from an interactive terminal, then run `tutti launch` again.");
67
+ }
68
+ await runProviderSetupTui({
69
+ projectName: projectName(preparation),
70
+ tuttiHome: preparation.tutti_home,
71
+ projectId: preparation.project_id,
72
+ initialBaseUrl: "https://api.openai.com/v1",
73
+ });
74
+ }
75
+ export async function runForegroundLaunchCommand(options) {
76
+ const result = await startLaunchProject({
77
+ workspacePath: options.workspacePath,
78
+ yes: options.yes,
79
+ confirm: confirmFromTty,
80
+ });
81
+ process.stdout.write(`${formatLaunchLifecycleResult(result, { hyperlinks: process.stdout.isTTY })}\n`);
82
+ if (result.kind === "hosting") {
83
+ await waitForForegroundHostShutdown(result.host);
84
+ }
85
+ }
86
+ export async function runInternalHostRunCommand(options) {
87
+ const result = await startLaunchProject({
88
+ workspacePath: options.workspacePath,
89
+ yes: options.yes,
90
+ });
91
+ if (result.kind === "hosting") {
92
+ await waitForForegroundHostShutdown(result.host);
93
+ }
94
+ }
95
+ export async function runBackgroundLaunchCommand(options) {
96
+ const preparation = await prepareLaunchProject({
97
+ workspacePath: options.workspacePath,
98
+ yes: options.yes,
99
+ confirm: confirmFromTty,
100
+ });
101
+ await ensureProviderConfigured(preparation);
102
+ spawnDetachedHost({
103
+ workspaceRoot: preparation.workspace_root,
104
+ });
105
+ const ready = await waitForManagedHostReady({
106
+ tuttiHome: preparation.tutti_home,
107
+ projectId: preparation.project_id,
108
+ workspaceRoot: preparation.workspace_root,
109
+ });
110
+ if (ready.join_url === undefined) {
111
+ throw new LaunchError("relay_registration_failed", "Host started but Relay did not return a visible join URL", "Run `tutti invite` to rotate a fresh invite link.");
112
+ }
113
+ process.stdout.write(`${renderCompletionPage({
114
+ projectName: projectName(preparation),
115
+ workspaceRoot: preparation.workspace_root,
116
+ joinUrl: ready.join_url,
117
+ })}\n`);
118
+ await waitForEnter();
119
+ }
120
+ export async function runRestartCommand(options) {
121
+ await runStopCommand(options.workspacePath).catch(() => undefined);
122
+ await runBackgroundLaunchCommand({
123
+ workspacePath: options.workspacePath ?? process.cwd(),
124
+ yes: options.yes,
125
+ });
126
+ }
127
+ //# sourceMappingURL=launch-command.js.map
@@ -0,0 +1,30 @@
1
+ import type { HostLocalLaunchStatus, HostLocalProjectProjection, HostLocalProviderConfigBody } from "../http/routes/local-control.js";
2
+ import type { HostProviderConfigProjection } from "../http/routes/project-api/types.js";
3
+ import type { MachineRuntimeEndpointRecord } from "./machine-local.js";
4
+ export type LocalControlFetch = (input: string | URL, init?: RequestInit) => Promise<Response>;
5
+ export declare function readHostLocalLaunchStatus(options: {
6
+ endpoint: MachineRuntimeEndpointRecord;
7
+ fetchImpl?: LocalControlFetch;
8
+ }): Promise<HostLocalLaunchStatus>;
9
+ export declare function rotateHostLocalInvite(options: {
10
+ endpoint: MachineRuntimeEndpointRecord;
11
+ fetchImpl?: LocalControlFetch;
12
+ }): Promise<HostLocalLaunchStatus>;
13
+ export declare function readHostLocalProject(options: {
14
+ endpoint: MachineRuntimeEndpointRecord;
15
+ fetchImpl?: LocalControlFetch;
16
+ }): Promise<HostLocalProjectProjection>;
17
+ export declare function readHostLocalProviderConfig(options: {
18
+ endpoint: MachineRuntimeEndpointRecord;
19
+ fetchImpl?: LocalControlFetch;
20
+ }): Promise<HostProviderConfigProjection>;
21
+ export declare function writeHostLocalProviderConfig(options: {
22
+ endpoint: MachineRuntimeEndpointRecord;
23
+ body: HostLocalProviderConfigBody;
24
+ fetchImpl?: LocalControlFetch;
25
+ }): Promise<HostProviderConfigProjection>;
26
+ export declare function requestHostLocalShutdown(options: {
27
+ endpoint: MachineRuntimeEndpointRecord;
28
+ fetchImpl?: LocalControlFetch;
29
+ }): Promise<void>;
30
+ //# sourceMappingURL=local-control-client.d.ts.map
@@ -0,0 +1,73 @@
1
+ const DEFAULT_TIMEOUT_MS = 2_000;
2
+ async function fetchWithTimeout(fetchImpl, url, init = {}, timeoutMs = DEFAULT_TIMEOUT_MS) {
3
+ const controller = new AbortController();
4
+ const timeout = setTimeout(() => {
5
+ controller.abort();
6
+ }, timeoutMs);
7
+ try {
8
+ return await fetchImpl(url, {
9
+ ...init,
10
+ signal: controller.signal,
11
+ });
12
+ }
13
+ finally {
14
+ clearTimeout(timeout);
15
+ }
16
+ }
17
+ async function requestLocalJson(options) {
18
+ const response = await fetchWithTimeout(options.fetchImpl ?? fetch, new URL(options.path, options.endpoint.base_url), {
19
+ method: options.method ?? "GET",
20
+ headers: {
21
+ accept: "application/json",
22
+ authorization: `Bearer ${options.endpoint.token}`,
23
+ ...(options.body === undefined ? {} : { "content-type": "application/json" }),
24
+ },
25
+ ...(options.body === undefined ? {} : { body: JSON.stringify(options.body) }),
26
+ }, options.timeoutMs);
27
+ if (!response.ok) {
28
+ throw new Error(`host-local ${options.path} returned HTTP ${response.status}`);
29
+ }
30
+ return (await response.json());
31
+ }
32
+ export async function readHostLocalLaunchStatus(options) {
33
+ return await requestLocalJson({
34
+ ...options,
35
+ path: "/host-local/v1/launch-status",
36
+ });
37
+ }
38
+ export async function rotateHostLocalInvite(options) {
39
+ return await requestLocalJson({
40
+ ...options,
41
+ method: "POST",
42
+ path: "/host-local/v1/invite/rotate",
43
+ });
44
+ }
45
+ export async function readHostLocalProject(options) {
46
+ return await requestLocalJson({
47
+ ...options,
48
+ path: "/host-local/v1/project",
49
+ });
50
+ }
51
+ export async function readHostLocalProviderConfig(options) {
52
+ return await requestLocalJson({
53
+ ...options,
54
+ path: "/host-local/v1/provider/config",
55
+ });
56
+ }
57
+ export async function writeHostLocalProviderConfig(options) {
58
+ return await requestLocalJson({
59
+ endpoint: options.endpoint,
60
+ ...(options.fetchImpl === undefined ? {} : { fetchImpl: options.fetchImpl }),
61
+ method: "POST",
62
+ path: "/host-local/v1/provider/config",
63
+ body: options.body,
64
+ });
65
+ }
66
+ export async function requestHostLocalShutdown(options) {
67
+ await requestLocalJson({
68
+ ...options,
69
+ method: "POST",
70
+ path: "/host-local/v1/shutdown",
71
+ });
72
+ }
73
+ //# sourceMappingURL=local-control-client.js.map
@@ -0,0 +1,19 @@
1
+ import { type FetchLike } from "./host-runtime-endpoint.js";
2
+ import { type MachineRuntimeEndpointRecord } from "./machine-local.js";
3
+ import type { ProjectId } from "@tutti/shared/ids";
4
+ export type ManagedHostReadyResult = {
5
+ endpoint: MachineRuntimeEndpointRecord;
6
+ join_url?: string;
7
+ };
8
+ export declare function spawnDetachedHost(options: {
9
+ workspaceRoot: string;
10
+ env?: NodeJS.ProcessEnv;
11
+ }): void;
12
+ export declare function waitForManagedHostReady(options: {
13
+ tuttiHome: string;
14
+ projectId: ProjectId;
15
+ workspaceRoot: string;
16
+ fetchImpl?: FetchLike;
17
+ timeoutMs?: number;
18
+ }): Promise<ManagedHostReadyResult>;
19
+ //# sourceMappingURL=managed-host.d.ts.map
@@ -0,0 +1,103 @@
1
+ import { spawn } from "node:child_process";
2
+ import { readFileSync } from "node:fs";
3
+ import { resolve } from "node:path";
4
+ import { createRuntimeEndpointProbe } from "./host-runtime-endpoint.js";
5
+ import { getHostLogFilePath, readMachineRuntimeEndpoint, } from "./machine-local.js";
6
+ import { readHostLocalLaunchStatus, rotateHostLocalInvite } from "./local-control-client.js";
7
+ const DEFAULT_READY_TIMEOUT_MS = 60_000;
8
+ const READY_POLL_INTERVAL_MS = 250;
9
+ function delay(ms) {
10
+ return new Promise((resolveDelay) => {
11
+ setTimeout(resolveDelay, ms);
12
+ });
13
+ }
14
+ function currentCliEntrypoint() {
15
+ const entrypoint = process.argv[1];
16
+ if (entrypoint === undefined || entrypoint.trim() === "") {
17
+ throw new Error("Cannot locate the current Tutti CLI entrypoint.");
18
+ }
19
+ return entrypoint;
20
+ }
21
+ export function spawnDetachedHost(options) {
22
+ const child = spawn(process.execPath, [
23
+ ...process.execArgv,
24
+ currentCliEntrypoint(),
25
+ "internal",
26
+ "host-run",
27
+ "--workspace",
28
+ resolve(options.workspaceRoot),
29
+ "--yes",
30
+ ], {
31
+ cwd: options.workspaceRoot,
32
+ detached: true,
33
+ env: {
34
+ ...process.env,
35
+ ...(options.env ?? {}),
36
+ },
37
+ stdio: "ignore",
38
+ });
39
+ child.unref();
40
+ }
41
+ function readHostLogTail(tuttiHome, maxLines = 80) {
42
+ try {
43
+ const text = readFileSync(getHostLogFilePath(tuttiHome), "utf8");
44
+ return text.split(/\r?\n/u).filter(Boolean).slice(-maxLines).join("\n");
45
+ }
46
+ catch {
47
+ return "";
48
+ }
49
+ }
50
+ export async function waitForManagedHostReady(options) {
51
+ const deadline = Date.now() + (options.timeoutMs ?? DEFAULT_READY_TIMEOUT_MS);
52
+ const probe = createRuntimeEndpointProbe(options.fetchImpl ?? fetch);
53
+ let lastReason = "runtime endpoint was not written";
54
+ while (Date.now() < deadline) {
55
+ const endpoint = readMachineRuntimeEndpoint(options.tuttiHome, options.projectId);
56
+ if (endpoint === null) {
57
+ await delay(READY_POLL_INTERVAL_MS);
58
+ continue;
59
+ }
60
+ if (resolve(endpoint.workspace_root) !== resolve(options.workspaceRoot)) {
61
+ lastReason = "runtime endpoint points at a different workspace";
62
+ await delay(READY_POLL_INTERVAL_MS);
63
+ continue;
64
+ }
65
+ const health = await probe(endpoint);
66
+ if (health.kind === "stale") {
67
+ lastReason = health.reason;
68
+ await delay(READY_POLL_INTERVAL_MS);
69
+ continue;
70
+ }
71
+ try {
72
+ let status = await readHostLocalLaunchStatus({
73
+ endpoint,
74
+ ...(options.fetchImpl === undefined ? {} : { fetchImpl: options.fetchImpl }),
75
+ });
76
+ if (status.relay?.join_url === undefined) {
77
+ status = await rotateHostLocalInvite({
78
+ endpoint,
79
+ ...(options.fetchImpl === undefined ? {} : { fetchImpl: options.fetchImpl }),
80
+ });
81
+ }
82
+ if (status.relay?.join_url !== undefined) {
83
+ return {
84
+ endpoint,
85
+ join_url: status.relay.join_url,
86
+ };
87
+ }
88
+ lastReason = "host is running but Relay invite is not visible yet";
89
+ }
90
+ catch (error) {
91
+ lastReason = error instanceof Error ? error.message : "host-local launch status failed";
92
+ }
93
+ await delay(READY_POLL_INTERVAL_MS);
94
+ }
95
+ const tail = readHostLogTail(options.tuttiHome);
96
+ throw new Error([
97
+ `Tutti host did not become ready before timeout: ${lastReason}`,
98
+ tail.length === 0 ? "" : `Recent host log:\n${tail}`,
99
+ ]
100
+ .filter(Boolean)
101
+ .join("\n"));
102
+ }
103
+ //# sourceMappingURL=managed-host.js.map
@@ -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