@xfey/tutti 0.1.8 → 0.1.10
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/cli.js +0 -2
- package/dist/server-shell/cli/errors.d.ts +1 -1
- package/dist/server-shell/cli/errors.js +6 -0
- package/dist/server-shell/cli/launch-command.js +0 -2
- package/dist/server-shell/cli/provider-tui.d.ts +0 -2
- package/dist/server-shell/cli/provider-tui.js +54 -39
- package/dist/server-shell/cli/terminal-logo.js +1 -9
- package/package.json +1 -1
|
@@ -96,10 +96,8 @@ async function runProviderSetupCommand(workspacePath) {
|
|
|
96
96
|
...(workspacePath === undefined ? {} : { workspacePath }),
|
|
97
97
|
});
|
|
98
98
|
const result = await runProviderSetupTui({
|
|
99
|
-
projectName: project.display_name,
|
|
100
99
|
tuttiHome: project.tutti_home,
|
|
101
100
|
projectId: project.project_id,
|
|
102
|
-
initialBaseUrl: project.provider_base_url ?? "https://api.openai.com/v1",
|
|
103
101
|
});
|
|
104
102
|
process.stdout.write([
|
|
105
103
|
"",
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
export type LaunchErrorCode = "unsafe_project_root" | "unsupported_tutti_schema" | "project_identity_conflict" | "git_bootstrap_failed" | "sensitive_file_detected" | "relay_unavailable" | "relay_registration_failed" | "provider_configuration_required" | "provider_validation_failed" | "store_migration_failed" | "existing_server_unhealthy" | "takeover_rejected" | "takeover_failed" | "port_unavailable";
|
|
1
|
+
export type LaunchErrorCode = "unsafe_project_root" | "unsupported_tutti_schema" | "project_identity_conflict" | "git_bootstrap_failed" | "sensitive_file_detected" | "relay_unavailable" | "relay_registration_failed" | "provider_configuration_required" | "provider_setup_cancelled" | "provider_validation_failed" | "store_migration_failed" | "existing_server_unhealthy" | "takeover_rejected" | "takeover_failed" | "port_unavailable";
|
|
2
2
|
export declare class LaunchError extends Error {
|
|
3
3
|
readonly code: LaunchErrorCode;
|
|
4
4
|
readonly next: string;
|
|
@@ -13,6 +13,9 @@ export class LaunchError extends Error {
|
|
|
13
13
|
}
|
|
14
14
|
export function formatLaunchError(error) {
|
|
15
15
|
if (error instanceof LaunchError) {
|
|
16
|
+
if (error.code === "provider_setup_cancelled") {
|
|
17
|
+
return ["Tutti launch cancelled", "", `Reason: ${redactText(error.message)}`].join("\n");
|
|
18
|
+
}
|
|
16
19
|
const lines = [
|
|
17
20
|
"Tutti launch failed",
|
|
18
21
|
"",
|
|
@@ -40,6 +43,9 @@ export function formatLaunchError(error) {
|
|
|
40
43
|
}
|
|
41
44
|
export function formatCliError(error) {
|
|
42
45
|
if (error instanceof LaunchError) {
|
|
46
|
+
if (error.code === "provider_setup_cancelled") {
|
|
47
|
+
return ["Tutti command cancelled", "", `Reason: ${redactText(error.message)}`].join("\n");
|
|
48
|
+
}
|
|
43
49
|
const lines = ["Tutti command failed", "", `Code: ${error.code}`, `Reason: ${redactText(error.message)}`];
|
|
44
50
|
const detailLines = Object.entries(error.details)
|
|
45
51
|
.filter(([, value]) => value !== undefined)
|
|
@@ -65,10 +65,8 @@ async function ensureProviderConfigured(preparation) {
|
|
|
65
65
|
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.");
|
|
66
66
|
}
|
|
67
67
|
await runProviderSetupTui({
|
|
68
|
-
projectName: projectName(preparation),
|
|
69
68
|
tuttiHome: preparation.tutti_home,
|
|
70
69
|
projectId: preparation.project_id,
|
|
71
|
-
initialBaseUrl: "https://api.openai.com/v1",
|
|
72
70
|
});
|
|
73
71
|
}
|
|
74
72
|
export async function runForegroundLaunchCommand(options) {
|
|
@@ -6,10 +6,8 @@ export type ProviderSetupResult = {
|
|
|
6
6
|
validated_at: string;
|
|
7
7
|
};
|
|
8
8
|
export declare function runProviderSetupTui(options: {
|
|
9
|
-
projectName: string;
|
|
10
9
|
tuttiHome: string;
|
|
11
10
|
projectId: ProjectId;
|
|
12
|
-
initialBaseUrl?: string;
|
|
13
11
|
stdin?: NodeJS.ReadStream;
|
|
14
12
|
stdout?: NodeJS.WriteStream;
|
|
15
13
|
}): Promise<ProviderSetupResult>;
|
|
@@ -1,41 +1,61 @@
|
|
|
1
1
|
import { emitKeypressEvents } from "node:readline";
|
|
2
2
|
import { redactError } from "@tutti/shared/utils";
|
|
3
3
|
import { configureProjectOpenAiProvider, DEFAULT_OPENAI_MODEL, } from "../../providers/openai/index.js";
|
|
4
|
+
import { LaunchError } from "./errors.js";
|
|
4
5
|
import { renderTuttiTerminalLogo } from "./terminal-logo.js";
|
|
5
|
-
const DEFAULT_BASE_URL = "https://api.openai.com/v1";
|
|
6
6
|
const HIDE_CURSOR = "\u001B[?25l";
|
|
7
7
|
const SHOW_CURSOR = "\u001B[?25h";
|
|
8
|
+
const BLINK_ON = "\u001B[5m";
|
|
9
|
+
const BLINK_OFF = "\u001B[25m";
|
|
8
10
|
const CLEAR = "\u001B[2J\u001B[H";
|
|
11
|
+
const DIVIDER = "----------------------------------------------------------------";
|
|
12
|
+
function providerSetupCancelled() {
|
|
13
|
+
return new LaunchError("provider_setup_cancelled", "Provider setup cancelled.", "Run the command again when you are ready.");
|
|
14
|
+
}
|
|
9
15
|
function fieldLine(options) {
|
|
10
|
-
const marker = options.active ? ">" : " ";
|
|
11
16
|
const value = options.secret && options.value.length > 0 ? "*".repeat(options.value.length) : options.value;
|
|
12
|
-
return
|
|
17
|
+
return `> ${options.label}: ${value}${BLINK_ON}_${BLINK_OFF}`;
|
|
18
|
+
}
|
|
19
|
+
function renderProviderField(state) {
|
|
20
|
+
if (state.step === "base-url") {
|
|
21
|
+
return [
|
|
22
|
+
"Step 1 of 2 - Base URL",
|
|
23
|
+
"Enter the base URL for your OpenAI-compatible provider.",
|
|
24
|
+
"",
|
|
25
|
+
fieldLine({
|
|
26
|
+
label: "Base URL",
|
|
27
|
+
value: state.baseUrl,
|
|
28
|
+
}),
|
|
29
|
+
"",
|
|
30
|
+
"[Enter] to continue",
|
|
31
|
+
];
|
|
32
|
+
}
|
|
33
|
+
return [
|
|
34
|
+
"Step 2 of 2 - API Key",
|
|
35
|
+
"Enter the API key for this provider. It will be stored only in the machine-local Tutti credential store.",
|
|
36
|
+
"",
|
|
37
|
+
fieldLine({
|
|
38
|
+
label: "API Key",
|
|
39
|
+
value: state.apiKey,
|
|
40
|
+
secret: true,
|
|
41
|
+
}),
|
|
42
|
+
"",
|
|
43
|
+
"[Enter] to validate [Esc] to edit Base URL",
|
|
44
|
+
];
|
|
13
45
|
}
|
|
14
46
|
function renderProviderForm(options) {
|
|
15
|
-
const instruction = options.state.step === "base-url"
|
|
16
|
-
? "Enter the provider base URL, then press Enter."
|
|
17
|
-
: "Enter the API key, then press Enter to validate. Press Esc to edit the base URL.";
|
|
18
47
|
const lines = [
|
|
19
48
|
CLEAR,
|
|
20
49
|
HIDE_CURSOR,
|
|
21
50
|
renderTuttiTerminalLogo({ tty: options.tty === true }),
|
|
22
51
|
"",
|
|
23
52
|
"Provider setup",
|
|
53
|
+
DIVIDER,
|
|
54
|
+
"Tutti uses an OpenAI-compatible Responses API provider.",
|
|
55
|
+
"The base URL should expose POST /responses under the entered URL.",
|
|
56
|
+
DIVIDER,
|
|
24
57
|
"",
|
|
25
|
-
|
|
26
|
-
`${instruction} Press Ctrl-C to cancel.`,
|
|
27
|
-
"",
|
|
28
|
-
fieldLine({
|
|
29
|
-
active: options.state.step === "base-url",
|
|
30
|
-
label: "Base URL",
|
|
31
|
-
value: options.state.baseUrl,
|
|
32
|
-
}),
|
|
33
|
-
fieldLine({
|
|
34
|
-
active: options.state.step === "api-key",
|
|
35
|
-
label: "API Key",
|
|
36
|
-
value: options.state.apiKey,
|
|
37
|
-
secret: true,
|
|
38
|
-
}),
|
|
58
|
+
...renderProviderField(options.state),
|
|
39
59
|
"",
|
|
40
60
|
];
|
|
41
61
|
if (options.state.error !== undefined) {
|
|
@@ -58,7 +78,7 @@ function waitForKeypress(stdin) {
|
|
|
58
78
|
const onKeypress = (_character, key) => {
|
|
59
79
|
cleanup();
|
|
60
80
|
if (key.ctrl === true && key.name === "c") {
|
|
61
|
-
reject(
|
|
81
|
+
reject(providerSetupCancelled());
|
|
62
82
|
return;
|
|
63
83
|
}
|
|
64
84
|
resolve();
|
|
@@ -78,14 +98,13 @@ function waitForKeypress(stdin) {
|
|
|
78
98
|
async function readProviderForm(options) {
|
|
79
99
|
const state = {
|
|
80
100
|
step: options.initialStep ?? "base-url",
|
|
81
|
-
baseUrl: options.initialBaseUrl ??
|
|
101
|
+
baseUrl: options.initialBaseUrl ?? "",
|
|
82
102
|
apiKey: options.initialApiKey ?? "",
|
|
83
103
|
error: undefined,
|
|
84
104
|
};
|
|
85
105
|
return await new Promise((resolve, reject) => {
|
|
86
106
|
const render = () => {
|
|
87
107
|
options.stdout.write(renderProviderForm({
|
|
88
|
-
projectName: options.projectName,
|
|
89
108
|
state,
|
|
90
109
|
tty: options.stdout.isTTY === true,
|
|
91
110
|
}));
|
|
@@ -100,7 +119,7 @@ async function readProviderForm(options) {
|
|
|
100
119
|
const onKeypress = (character, key) => {
|
|
101
120
|
if (key.ctrl === true && key.name === "c") {
|
|
102
121
|
cleanup();
|
|
103
|
-
reject(
|
|
122
|
+
reject(providerSetupCancelled());
|
|
104
123
|
return;
|
|
105
124
|
}
|
|
106
125
|
if (key.name === "escape") {
|
|
@@ -111,13 +130,7 @@ async function readProviderForm(options) {
|
|
|
111
130
|
return;
|
|
112
131
|
}
|
|
113
132
|
cleanup();
|
|
114
|
-
reject(
|
|
115
|
-
return;
|
|
116
|
-
}
|
|
117
|
-
if (key.name === "tab") {
|
|
118
|
-
state.step = state.step === "base-url" ? "api-key" : "base-url";
|
|
119
|
-
state.error = undefined;
|
|
120
|
-
render();
|
|
133
|
+
reject(providerSetupCancelled());
|
|
121
134
|
return;
|
|
122
135
|
}
|
|
123
136
|
if (key.name === "return") {
|
|
@@ -171,7 +184,7 @@ async function readProviderForm(options) {
|
|
|
171
184
|
render();
|
|
172
185
|
});
|
|
173
186
|
}
|
|
174
|
-
function renderChecking(
|
|
187
|
+
function renderChecking(frame, tty) {
|
|
175
188
|
const frames = ["|", "/", "-", "\\"];
|
|
176
189
|
const indicator = frames[frame % frames.length] ?? "|";
|
|
177
190
|
return [
|
|
@@ -180,8 +193,9 @@ function renderChecking(projectName, frame, tty) {
|
|
|
180
193
|
renderTuttiTerminalLogo({ tty }),
|
|
181
194
|
"",
|
|
182
195
|
"Provider setup",
|
|
183
|
-
|
|
184
|
-
|
|
196
|
+
DIVIDER,
|
|
197
|
+
"Validating the provider with the configured Responses API endpoint.",
|
|
198
|
+
DIVIDER,
|
|
185
199
|
"",
|
|
186
200
|
`${indicator} Checking provider connection with ${DEFAULT_OPENAI_MODEL}...`,
|
|
187
201
|
].join("\n");
|
|
@@ -192,12 +206,11 @@ export async function runProviderSetupTui(options) {
|
|
|
192
206
|
if (!stdin.isTTY || !stdout.isTTY) {
|
|
193
207
|
throw new Error("Provider setup requires an interactive terminal.");
|
|
194
208
|
}
|
|
195
|
-
let initialBaseUrl
|
|
209
|
+
let initialBaseUrl;
|
|
196
210
|
let initialApiKey = "";
|
|
197
211
|
let initialStep = "base-url";
|
|
198
212
|
while (true) {
|
|
199
213
|
const input = await readProviderForm({
|
|
200
|
-
projectName: options.projectName,
|
|
201
214
|
...(initialBaseUrl === undefined ? {} : { initialBaseUrl }),
|
|
202
215
|
...(initialApiKey === "" ? {} : { initialApiKey }),
|
|
203
216
|
initialStep,
|
|
@@ -209,11 +222,11 @@ export async function runProviderSetupTui(options) {
|
|
|
209
222
|
initialStep = "api-key";
|
|
210
223
|
let frame = 0;
|
|
211
224
|
const interval = setInterval(() => {
|
|
212
|
-
stdout.write(renderChecking(
|
|
225
|
+
stdout.write(renderChecking(frame, stdout.isTTY === true));
|
|
213
226
|
frame += 1;
|
|
214
227
|
}, 120);
|
|
215
228
|
try {
|
|
216
|
-
stdout.write(renderChecking(
|
|
229
|
+
stdout.write(renderChecking(frame, stdout.isTTY === true));
|
|
217
230
|
const result = await configureProjectOpenAiProvider({
|
|
218
231
|
tuttiHome: options.tuttiHome,
|
|
219
232
|
projectId: options.projectId,
|
|
@@ -231,7 +244,6 @@ export async function runProviderSetupTui(options) {
|
|
|
231
244
|
};
|
|
232
245
|
}
|
|
233
246
|
stdout.write(renderProviderForm({
|
|
234
|
-
projectName: options.projectName,
|
|
235
247
|
state: {
|
|
236
248
|
step: "api-key",
|
|
237
249
|
baseUrl: input.baseUrl,
|
|
@@ -246,6 +258,9 @@ export async function runProviderSetupTui(options) {
|
|
|
246
258
|
catch (error) {
|
|
247
259
|
clearInterval(interval);
|
|
248
260
|
stdout.write(SHOW_CURSOR);
|
|
261
|
+
if (error instanceof LaunchError) {
|
|
262
|
+
throw error;
|
|
263
|
+
}
|
|
249
264
|
throw new Error(`Provider setup failed: ${String(redactError(error))}`, { cause: error });
|
|
250
265
|
}
|
|
251
266
|
}
|
|
@@ -1,5 +1,3 @@
|
|
|
1
|
-
const RESET = "\u001B[0m";
|
|
2
|
-
const ACTION_STRONG = "\u001B[38;2;8;121;95m";
|
|
3
1
|
// Static Braille rendering of docs/design/Tutti.png, generated from the alpha mask.
|
|
4
2
|
const TUTTI_BRAILLE_LOGO = [
|
|
5
3
|
" ⣀⣤⣶⣶⣾⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣶⡄ ⣠⣶⣶⣦ ⢀⣤⣶⣶⣄",
|
|
@@ -12,16 +10,10 @@ const TUTTI_BRAILLE_LOGO = [
|
|
|
12
10
|
" ⢰⣿⣿⣿⠃ ⣿⣿⣿⣴⣿⣿⣿⣇⣀⣴⣿⣿⣿⣿⣄⣀⣠⣾⣿⣿⣧⣀⣀⣀⣴⣿⣿⣿",
|
|
13
11
|
" ⠸⣿⣿⡿ ⠹⣿⣿⡿⠃⠻⣿⣿⣿⠟⠋⠈⠻⣿⣿⣿⡿⠛⠙⠿⣿⣿⣿⣿⠟⣿⣿⡿",
|
|
14
12
|
].join("\n");
|
|
15
|
-
function supportsColor(options) {
|
|
16
|
-
return options.color !== false && process.env.NO_COLOR === undefined;
|
|
17
|
-
}
|
|
18
13
|
export function renderTuttiTerminalLogo(options = {}) {
|
|
19
14
|
if (options.tty !== true) {
|
|
20
15
|
return "Tutti";
|
|
21
16
|
}
|
|
22
|
-
|
|
23
|
-
return TUTTI_BRAILLE_LOGO;
|
|
24
|
-
}
|
|
25
|
-
return `${ACTION_STRONG}${TUTTI_BRAILLE_LOGO}${RESET}`;
|
|
17
|
+
return TUTTI_BRAILLE_LOGO;
|
|
26
18
|
}
|
|
27
19
|
//# sourceMappingURL=terminal-logo.js.map
|