@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.
@@ -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 `${marker} ${options.label.padEnd(10)} ${value}${options.active ? " _" : ""}`;
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
- `Project: ${options.projectName}`,
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(new Error("Provider setup cancelled"));
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 ?? DEFAULT_BASE_URL,
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(new Error("Provider setup cancelled"));
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(new Error("Provider setup cancelled"));
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(projectName, frame, tty) {
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
- `Project: ${projectName}`,
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 = options.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(options.projectName, frame, stdout.isTTY === true));
225
+ stdout.write(renderChecking(frame, stdout.isTTY === true));
213
226
  frame += 1;
214
227
  }, 120);
215
228
  try {
216
- stdout.write(renderChecking(options.projectName, frame, stdout.isTTY === true));
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
- if (!supportsColor(options)) {
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
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@xfey/tutti",
3
- "version": "0.1.8",
3
+ "version": "0.1.10",
4
4
  "type": "module",
5
5
  "main": "./dist/index.js",
6
6
  "types": "./dist/index.d.ts",