@xfey/tutti 0.1.9 → 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,15 +1,20 @@
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";
9
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
+ }
10
15
  function fieldLine(options) {
11
16
  const value = options.secret && options.value.length > 0 ? "*".repeat(options.value.length) : options.value;
12
- return `> ${options.label}: ${value} _`;
17
+ return `> ${options.label}: ${value}${BLINK_ON}_${BLINK_OFF}`;
13
18
  }
14
19
  function renderProviderField(state) {
15
20
  if (state.step === "base-url") {
@@ -22,13 +27,11 @@ function renderProviderField(state) {
22
27
  value: state.baseUrl,
23
28
  }),
24
29
  "",
25
- "Press Enter to continue. Press Ctrl-C to cancel.",
30
+ "[Enter] to continue",
26
31
  ];
27
32
  }
28
33
  return [
29
34
  "Step 2 of 2 - API Key",
30
- `Base URL: ${state.baseUrl}`,
31
- "",
32
35
  "Enter the API key for this provider. It will be stored only in the machine-local Tutti credential store.",
33
36
  "",
34
37
  fieldLine({
@@ -37,7 +40,7 @@ function renderProviderField(state) {
37
40
  secret: true,
38
41
  }),
39
42
  "",
40
- "Press Enter to validate. Press Esc to edit the base URL. Press Ctrl-C to cancel.",
43
+ "[Enter] to validate [Esc] to edit Base URL",
41
44
  ];
42
45
  }
43
46
  function renderProviderForm(options) {
@@ -50,11 +53,8 @@ function renderProviderForm(options) {
50
53
  DIVIDER,
51
54
  "Tutti uses an OpenAI-compatible Responses API provider.",
52
55
  "The base URL should expose POST /responses under the entered URL.",
53
- `Default example: ${DEFAULT_BASE_URL}`,
54
56
  DIVIDER,
55
57
  "",
56
- `Project: ${options.projectName}`,
57
- "",
58
58
  ...renderProviderField(options.state),
59
59
  "",
60
60
  ];
@@ -78,7 +78,7 @@ function waitForKeypress(stdin) {
78
78
  const onKeypress = (_character, key) => {
79
79
  cleanup();
80
80
  if (key.ctrl === true && key.name === "c") {
81
- reject(new Error("Provider setup cancelled"));
81
+ reject(providerSetupCancelled());
82
82
  return;
83
83
  }
84
84
  resolve();
@@ -98,14 +98,13 @@ function waitForKeypress(stdin) {
98
98
  async function readProviderForm(options) {
99
99
  const state = {
100
100
  step: options.initialStep ?? "base-url",
101
- baseUrl: options.initialBaseUrl ?? DEFAULT_BASE_URL,
101
+ baseUrl: options.initialBaseUrl ?? "",
102
102
  apiKey: options.initialApiKey ?? "",
103
103
  error: undefined,
104
104
  };
105
105
  return await new Promise((resolve, reject) => {
106
106
  const render = () => {
107
107
  options.stdout.write(renderProviderForm({
108
- projectName: options.projectName,
109
108
  state,
110
109
  tty: options.stdout.isTTY === true,
111
110
  }));
@@ -120,7 +119,7 @@ async function readProviderForm(options) {
120
119
  const onKeypress = (character, key) => {
121
120
  if (key.ctrl === true && key.name === "c") {
122
121
  cleanup();
123
- reject(new Error("Provider setup cancelled"));
122
+ reject(providerSetupCancelled());
124
123
  return;
125
124
  }
126
125
  if (key.name === "escape") {
@@ -131,7 +130,7 @@ async function readProviderForm(options) {
131
130
  return;
132
131
  }
133
132
  cleanup();
134
- reject(new Error("Provider setup cancelled"));
133
+ reject(providerSetupCancelled());
135
134
  return;
136
135
  }
137
136
  if (key.name === "return") {
@@ -185,7 +184,7 @@ async function readProviderForm(options) {
185
184
  render();
186
185
  });
187
186
  }
188
- function renderChecking(projectName, frame, tty) {
187
+ function renderChecking(frame, tty) {
189
188
  const frames = ["|", "/", "-", "\\"];
190
189
  const indicator = frames[frame % frames.length] ?? "|";
191
190
  return [
@@ -194,8 +193,9 @@ function renderChecking(projectName, frame, tty) {
194
193
  renderTuttiTerminalLogo({ tty }),
195
194
  "",
196
195
  "Provider setup",
197
- "",
198
- `Project: ${projectName}`,
196
+ DIVIDER,
197
+ "Validating the provider with the configured Responses API endpoint.",
198
+ DIVIDER,
199
199
  "",
200
200
  `${indicator} Checking provider connection with ${DEFAULT_OPENAI_MODEL}...`,
201
201
  ].join("\n");
@@ -206,12 +206,11 @@ export async function runProviderSetupTui(options) {
206
206
  if (!stdin.isTTY || !stdout.isTTY) {
207
207
  throw new Error("Provider setup requires an interactive terminal.");
208
208
  }
209
- let initialBaseUrl = options.initialBaseUrl;
209
+ let initialBaseUrl;
210
210
  let initialApiKey = "";
211
211
  let initialStep = "base-url";
212
212
  while (true) {
213
213
  const input = await readProviderForm({
214
- projectName: options.projectName,
215
214
  ...(initialBaseUrl === undefined ? {} : { initialBaseUrl }),
216
215
  ...(initialApiKey === "" ? {} : { initialApiKey }),
217
216
  initialStep,
@@ -223,11 +222,11 @@ export async function runProviderSetupTui(options) {
223
222
  initialStep = "api-key";
224
223
  let frame = 0;
225
224
  const interval = setInterval(() => {
226
- stdout.write(renderChecking(options.projectName, frame, stdout.isTTY === true));
225
+ stdout.write(renderChecking(frame, stdout.isTTY === true));
227
226
  frame += 1;
228
227
  }, 120);
229
228
  try {
230
- stdout.write(renderChecking(options.projectName, frame, stdout.isTTY === true));
229
+ stdout.write(renderChecking(frame, stdout.isTTY === true));
231
230
  const result = await configureProjectOpenAiProvider({
232
231
  tuttiHome: options.tuttiHome,
233
232
  projectId: options.projectId,
@@ -245,7 +244,6 @@ export async function runProviderSetupTui(options) {
245
244
  };
246
245
  }
247
246
  stdout.write(renderProviderForm({
248
- projectName: options.projectName,
249
247
  state: {
250
248
  step: "api-key",
251
249
  baseUrl: input.baseUrl,
@@ -260,6 +258,9 @@ export async function runProviderSetupTui(options) {
260
258
  catch (error) {
261
259
  clearInterval(interval);
262
260
  stdout.write(SHOW_CURSOR);
261
+ if (error instanceof LaunchError) {
262
+ throw error;
263
+ }
263
264
  throw new Error(`Provider setup failed: ${String(redactError(error))}`, { cause: error });
264
265
  }
265
266
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@xfey/tutti",
3
- "version": "0.1.9",
3
+ "version": "0.1.10",
4
4
  "type": "module",
5
5
  "main": "./dist/index.js",
6
6
  "types": "./dist/index.d.ts",